git-p4.pyon commit git-p4: add option to system() to return subshell status (cbff4b2)
   1#!/usr/bin/env python
   2#
   3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
   4#
   5# Author: Simon Hausmann <simon@lst.de>
   6# Copyright: 2007 Simon Hausmann <simon@lst.de>
   7#            2007 Trolltech ASA
   8# License: MIT <http://www.opensource.org/licenses/mit-license.php>
   9#
  10import sys
  11if sys.hexversion < 0x02040000:
  12    # The limiter is the subprocess module
  13    sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
  14    sys.exit(1)
  15import os
  16import optparse
  17import marshal
  18import subprocess
  19import tempfile
  20import time
  21import platform
  22import re
  23import shutil
  24import stat
  25
  26try:
  27    from subprocess import CalledProcessError
  28except ImportError:
  29    # from python2.7:subprocess.py
  30    # Exception classes used by this module.
  31    class CalledProcessError(Exception):
  32        """This exception is raised when a process run by check_call() returns
  33        a non-zero exit status.  The exit status will be stored in the
  34        returncode attribute."""
  35        def __init__(self, returncode, cmd):
  36            self.returncode = returncode
  37            self.cmd = cmd
  38        def __str__(self):
  39            return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
  40
  41verbose = False
  42
  43# Only labels/tags matching this will be imported/exported
  44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
  45
  46# Grab changes in blocks of this many revisions, unless otherwise requested
  47defaultBlockSize = 512
  48
  49def p4_build_cmd(cmd):
  50    """Build a suitable p4 command line.
  51
  52    This consolidates building and returning a p4 command line into one
  53    location. It means that hooking into the environment, or other configuration
  54    can be done more easily.
  55    """
  56    real_cmd = ["p4"]
  57
  58    user = gitConfig("git-p4.user")
  59    if len(user) > 0:
  60        real_cmd += ["-u",user]
  61
  62    password = gitConfig("git-p4.password")
  63    if len(password) > 0:
  64        real_cmd += ["-P", password]
  65
  66    port = gitConfig("git-p4.port")
  67    if len(port) > 0:
  68        real_cmd += ["-p", port]
  69
  70    host = gitConfig("git-p4.host")
  71    if len(host) > 0:
  72        real_cmd += ["-H", host]
  73
  74    client = gitConfig("git-p4.client")
  75    if len(client) > 0:
  76        real_cmd += ["-c", client]
  77
  78
  79    if isinstance(cmd,basestring):
  80        real_cmd = ' '.join(real_cmd) + ' ' + cmd
  81    else:
  82        real_cmd += cmd
  83    return real_cmd
  84
  85def chdir(path, is_client_path=False):
  86    """Do chdir to the given path, and set the PWD environment
  87       variable for use by P4.  It does not look at getcwd() output.
  88       Since we're not using the shell, it is necessary to set the
  89       PWD environment variable explicitly.
  90
  91       Normally, expand the path to force it to be absolute.  This
  92       addresses the use of relative path names inside P4 settings,
  93       e.g. P4CONFIG=.p4config.  P4 does not simply open the filename
  94       as given; it looks for .p4config using PWD.
  95
  96       If is_client_path, the path was handed to us directly by p4,
  97       and may be a symbolic link.  Do not call os.getcwd() in this
  98       case, because it will cause p4 to think that PWD is not inside
  99       the client path.
 100       """
 101
 102    os.chdir(path)
 103    if not is_client_path:
 104        path = os.getcwd()
 105    os.environ['PWD'] = path
 106
 107def die(msg):
 108    if verbose:
 109        raise Exception(msg)
 110    else:
 111        sys.stderr.write(msg + "\n")
 112        sys.exit(1)
 113
 114def write_pipe(c, stdin):
 115    if verbose:
 116        sys.stderr.write('Writing pipe: %s\n' % str(c))
 117
 118    expand = isinstance(c,basestring)
 119    p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
 120    pipe = p.stdin
 121    val = pipe.write(stdin)
 122    pipe.close()
 123    if p.wait():
 124        die('Command failed: %s' % str(c))
 125
 126    return val
 127
 128def p4_write_pipe(c, stdin):
 129    real_cmd = p4_build_cmd(c)
 130    return write_pipe(real_cmd, stdin)
 131
 132def read_pipe(c, ignore_error=False):
 133    if verbose:
 134        sys.stderr.write('Reading pipe: %s\n' % str(c))
 135
 136    expand = isinstance(c,basestring)
 137    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 138    pipe = p.stdout
 139    val = pipe.read()
 140    if p.wait() and not ignore_error:
 141        die('Command failed: %s' % str(c))
 142
 143    return val
 144
 145def p4_read_pipe(c, ignore_error=False):
 146    real_cmd = p4_build_cmd(c)
 147    return read_pipe(real_cmd, ignore_error)
 148
 149def read_pipe_lines(c):
 150    if verbose:
 151        sys.stderr.write('Reading pipe: %s\n' % str(c))
 152
 153    expand = isinstance(c, basestring)
 154    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 155    pipe = p.stdout
 156    val = pipe.readlines()
 157    if pipe.close() or p.wait():
 158        die('Command failed: %s' % str(c))
 159
 160    return val
 161
 162def p4_read_pipe_lines(c):
 163    """Specifically invoke p4 on the command supplied. """
 164    real_cmd = p4_build_cmd(c)
 165    return read_pipe_lines(real_cmd)
 166
 167def p4_has_command(cmd):
 168    """Ask p4 for help on this command.  If it returns an error, the
 169       command does not exist in this version of p4."""
 170    real_cmd = p4_build_cmd(["help", cmd])
 171    p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
 172                                   stderr=subprocess.PIPE)
 173    p.communicate()
 174    return p.returncode == 0
 175
 176def p4_has_move_command():
 177    """See if the move command exists, that it supports -k, and that
 178       it has not been administratively disabled.  The arguments
 179       must be correct, but the filenames do not have to exist.  Use
 180       ones with wildcards so even if they exist, it will fail."""
 181
 182    if not p4_has_command("move"):
 183        return False
 184    cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
 185    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 186    (out, err) = p.communicate()
 187    # return code will be 1 in either case
 188    if err.find("Invalid option") >= 0:
 189        return False
 190    if err.find("disabled") >= 0:
 191        return False
 192    # assume it failed because @... was invalid changelist
 193    return True
 194
 195def system(cmd, ignore_error=False):
 196    expand = isinstance(cmd,basestring)
 197    if verbose:
 198        sys.stderr.write("executing %s\n" % str(cmd))
 199    retcode = subprocess.call(cmd, shell=expand)
 200    if retcode and not ignore_error:
 201        raise CalledProcessError(retcode, cmd)
 202
 203    return retcode
 204
 205def p4_system(cmd):
 206    """Specifically invoke p4 as the system command. """
 207    real_cmd = p4_build_cmd(cmd)
 208    expand = isinstance(real_cmd, basestring)
 209    retcode = subprocess.call(real_cmd, shell=expand)
 210    if retcode:
 211        raise CalledProcessError(retcode, real_cmd)
 212
 213_p4_version_string = None
 214def p4_version_string():
 215    """Read the version string, showing just the last line, which
 216       hopefully is the interesting version bit.
 217
 218       $ p4 -V
 219       Perforce - The Fast Software Configuration Management System.
 220       Copyright 1995-2011 Perforce Software.  All rights reserved.
 221       Rev. P4/NTX86/2011.1/393975 (2011/12/16).
 222    """
 223    global _p4_version_string
 224    if not _p4_version_string:
 225        a = p4_read_pipe_lines(["-V"])
 226        _p4_version_string = a[-1].rstrip()
 227    return _p4_version_string
 228
 229def p4_integrate(src, dest):
 230    p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
 231
 232def p4_sync(f, *options):
 233    p4_system(["sync"] + list(options) + [wildcard_encode(f)])
 234
 235def p4_add(f):
 236    # forcibly add file names with wildcards
 237    if wildcard_present(f):
 238        p4_system(["add", "-f", f])
 239    else:
 240        p4_system(["add", f])
 241
 242def p4_delete(f):
 243    p4_system(["delete", wildcard_encode(f)])
 244
 245def p4_edit(f):
 246    p4_system(["edit", wildcard_encode(f)])
 247
 248def p4_revert(f):
 249    p4_system(["revert", wildcard_encode(f)])
 250
 251def p4_reopen(type, f):
 252    p4_system(["reopen", "-t", type, wildcard_encode(f)])
 253
 254def p4_move(src, dest):
 255    p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
 256
 257def p4_last_change():
 258    results = p4CmdList(["changes", "-m", "1"])
 259    return int(results[0]['change'])
 260
 261def p4_describe(change):
 262    """Make sure it returns a valid result by checking for
 263       the presence of field "time".  Return a dict of the
 264       results."""
 265
 266    ds = p4CmdList(["describe", "-s", str(change)])
 267    if len(ds) != 1:
 268        die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
 269
 270    d = ds[0]
 271
 272    if "p4ExitCode" in d:
 273        die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
 274                                                      str(d)))
 275    if "code" in d:
 276        if d["code"] == "error":
 277            die("p4 describe -s %d returned error code: %s" % (change, str(d)))
 278
 279    if "time" not in d:
 280        die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
 281
 282    return d
 283
 284#
 285# Canonicalize the p4 type and return a tuple of the
 286# base type, plus any modifiers.  See "p4 help filetypes"
 287# for a list and explanation.
 288#
 289def split_p4_type(p4type):
 290
 291    p4_filetypes_historical = {
 292        "ctempobj": "binary+Sw",
 293        "ctext": "text+C",
 294        "cxtext": "text+Cx",
 295        "ktext": "text+k",
 296        "kxtext": "text+kx",
 297        "ltext": "text+F",
 298        "tempobj": "binary+FSw",
 299        "ubinary": "binary+F",
 300        "uresource": "resource+F",
 301        "uxbinary": "binary+Fx",
 302        "xbinary": "binary+x",
 303        "xltext": "text+Fx",
 304        "xtempobj": "binary+Swx",
 305        "xtext": "text+x",
 306        "xunicode": "unicode+x",
 307        "xutf16": "utf16+x",
 308    }
 309    if p4type in p4_filetypes_historical:
 310        p4type = p4_filetypes_historical[p4type]
 311    mods = ""
 312    s = p4type.split("+")
 313    base = s[0]
 314    mods = ""
 315    if len(s) > 1:
 316        mods = s[1]
 317    return (base, mods)
 318
 319#
 320# return the raw p4 type of a file (text, text+ko, etc)
 321#
 322def p4_type(f):
 323    results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
 324    return results[0]['headType']
 325
 326#
 327# Given a type base and modifier, return a regexp matching
 328# the keywords that can be expanded in the file
 329#
 330def p4_keywords_regexp_for_type(base, type_mods):
 331    if base in ("text", "unicode", "binary"):
 332        kwords = None
 333        if "ko" in type_mods:
 334            kwords = 'Id|Header'
 335        elif "k" in type_mods:
 336            kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
 337        else:
 338            return None
 339        pattern = r"""
 340            \$              # Starts with a dollar, followed by...
 341            (%s)            # one of the keywords, followed by...
 342            (:[^$\n]+)?     # possibly an old expansion, followed by...
 343            \$              # another dollar
 344            """ % kwords
 345        return pattern
 346    else:
 347        return None
 348
 349#
 350# Given a file, return a regexp matching the possible
 351# RCS keywords that will be expanded, or None for files
 352# with kw expansion turned off.
 353#
 354def p4_keywords_regexp_for_file(file):
 355    if not os.path.exists(file):
 356        return None
 357    else:
 358        (type_base, type_mods) = split_p4_type(p4_type(file))
 359        return p4_keywords_regexp_for_type(type_base, type_mods)
 360
 361def setP4ExecBit(file, mode):
 362    # Reopens an already open file and changes the execute bit to match
 363    # the execute bit setting in the passed in mode.
 364
 365    p4Type = "+x"
 366
 367    if not isModeExec(mode):
 368        p4Type = getP4OpenedType(file)
 369        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 370        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 371        if p4Type[-1] == "+":
 372            p4Type = p4Type[0:-1]
 373
 374    p4_reopen(p4Type, file)
 375
 376def getP4OpenedType(file):
 377    # Returns the perforce file type for the given file.
 378
 379    result = p4_read_pipe(["opened", wildcard_encode(file)])
 380    match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
 381    if match:
 382        return match.group(1)
 383    else:
 384        die("Could not determine file type for %s (result: '%s')" % (file, result))
 385
 386# Return the set of all p4 labels
 387def getP4Labels(depotPaths):
 388    labels = set()
 389    if isinstance(depotPaths,basestring):
 390        depotPaths = [depotPaths]
 391
 392    for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
 393        label = l['label']
 394        labels.add(label)
 395
 396    return labels
 397
 398# Return the set of all git tags
 399def getGitTags():
 400    gitTags = set()
 401    for line in read_pipe_lines(["git", "tag"]):
 402        tag = line.strip()
 403        gitTags.add(tag)
 404    return gitTags
 405
 406def diffTreePattern():
 407    # This is a simple generator for the diff tree regex pattern. This could be
 408    # a class variable if this and parseDiffTreeEntry were a part of a class.
 409    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 410    while True:
 411        yield pattern
 412
 413def parseDiffTreeEntry(entry):
 414    """Parses a single diff tree entry into its component elements.
 415
 416    See git-diff-tree(1) manpage for details about the format of the diff
 417    output. This method returns a dictionary with the following elements:
 418
 419    src_mode - The mode of the source file
 420    dst_mode - The mode of the destination file
 421    src_sha1 - The sha1 for the source file
 422    dst_sha1 - The sha1 fr the destination file
 423    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 424    status_score - The score for the status (applicable for 'C' and 'R'
 425                   statuses). This is None if there is no score.
 426    src - The path for the source file.
 427    dst - The path for the destination file. This is only present for
 428          copy or renames. If it is not present, this is None.
 429
 430    If the pattern is not matched, None is returned."""
 431
 432    match = diffTreePattern().next().match(entry)
 433    if match:
 434        return {
 435            'src_mode': match.group(1),
 436            'dst_mode': match.group(2),
 437            'src_sha1': match.group(3),
 438            'dst_sha1': match.group(4),
 439            'status': match.group(5),
 440            'status_score': match.group(6),
 441            'src': match.group(7),
 442            'dst': match.group(10)
 443        }
 444    return None
 445
 446def isModeExec(mode):
 447    # Returns True if the given git mode represents an executable file,
 448    # otherwise False.
 449    return mode[-3:] == "755"
 450
 451def isModeExecChanged(src_mode, dst_mode):
 452    return isModeExec(src_mode) != isModeExec(dst_mode)
 453
 454def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
 455
 456    if isinstance(cmd,basestring):
 457        cmd = "-G " + cmd
 458        expand = True
 459    else:
 460        cmd = ["-G"] + cmd
 461        expand = False
 462
 463    cmd = p4_build_cmd(cmd)
 464    if verbose:
 465        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
 466
 467    # Use a temporary file to avoid deadlocks without
 468    # subprocess.communicate(), which would put another copy
 469    # of stdout into memory.
 470    stdin_file = None
 471    if stdin is not None:
 472        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 473        if isinstance(stdin,basestring):
 474            stdin_file.write(stdin)
 475        else:
 476            for i in stdin:
 477                stdin_file.write(i + '\n')
 478        stdin_file.flush()
 479        stdin_file.seek(0)
 480
 481    p4 = subprocess.Popen(cmd,
 482                          shell=expand,
 483                          stdin=stdin_file,
 484                          stdout=subprocess.PIPE)
 485
 486    result = []
 487    try:
 488        while True:
 489            entry = marshal.load(p4.stdout)
 490            if cb is not None:
 491                cb(entry)
 492            else:
 493                result.append(entry)
 494    except EOFError:
 495        pass
 496    exitCode = p4.wait()
 497    if exitCode != 0:
 498        entry = {}
 499        entry["p4ExitCode"] = exitCode
 500        result.append(entry)
 501
 502    return result
 503
 504def p4Cmd(cmd):
 505    list = p4CmdList(cmd)
 506    result = {}
 507    for entry in list:
 508        result.update(entry)
 509    return result;
 510
 511def p4Where(depotPath):
 512    if not depotPath.endswith("/"):
 513        depotPath += "/"
 514    depotPathLong = depotPath + "..."
 515    outputList = p4CmdList(["where", depotPathLong])
 516    output = None
 517    for entry in outputList:
 518        if "depotFile" in entry:
 519            # Search for the base client side depot path, as long as it starts with the branch's P4 path.
 520            # The base path always ends with "/...".
 521            if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
 522                output = entry
 523                break
 524        elif "data" in entry:
 525            data = entry.get("data")
 526            space = data.find(" ")
 527            if data[:space] == depotPath:
 528                output = entry
 529                break
 530    if output == None:
 531        return ""
 532    if output["code"] == "error":
 533        return ""
 534    clientPath = ""
 535    if "path" in output:
 536        clientPath = output.get("path")
 537    elif "data" in output:
 538        data = output.get("data")
 539        lastSpace = data.rfind(" ")
 540        clientPath = data[lastSpace + 1:]
 541
 542    if clientPath.endswith("..."):
 543        clientPath = clientPath[:-3]
 544    return clientPath
 545
 546def currentGitBranch():
 547    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 548
 549def isValidGitDir(path):
 550    if (os.path.exists(path + "/HEAD")
 551        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 552        return True;
 553    return False
 554
 555def parseRevision(ref):
 556    return read_pipe("git rev-parse %s" % ref).strip()
 557
 558def branchExists(ref):
 559    rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
 560                     ignore_error=True)
 561    return len(rev) > 0
 562
 563def extractLogMessageFromGitCommit(commit):
 564    logMessage = ""
 565
 566    ## fixme: title is first line of commit, not 1st paragraph.
 567    foundTitle = False
 568    for log in read_pipe_lines("git cat-file commit %s" % commit):
 569       if not foundTitle:
 570           if len(log) == 1:
 571               foundTitle = True
 572           continue
 573
 574       logMessage += log
 575    return logMessage
 576
 577def extractSettingsGitLog(log):
 578    values = {}
 579    for line in log.split("\n"):
 580        line = line.strip()
 581        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 582        if not m:
 583            continue
 584
 585        assignments = m.group(1).split (':')
 586        for a in assignments:
 587            vals = a.split ('=')
 588            key = vals[0].strip()
 589            val = ('='.join (vals[1:])).strip()
 590            if val.endswith ('\"') and val.startswith('"'):
 591                val = val[1:-1]
 592
 593            values[key] = val
 594
 595    paths = values.get("depot-paths")
 596    if not paths:
 597        paths = values.get("depot-path")
 598    if paths:
 599        values['depot-paths'] = paths.split(',')
 600    return values
 601
 602def gitBranchExists(branch):
 603    proc = subprocess.Popen(["git", "rev-parse", branch],
 604                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 605    return proc.wait() == 0;
 606
 607_gitConfig = {}
 608
 609def gitConfig(key):
 610    if not _gitConfig.has_key(key):
 611        cmd = [ "git", "config", key ]
 612        s = read_pipe(cmd, ignore_error=True)
 613        _gitConfig[key] = s.strip()
 614    return _gitConfig[key]
 615
 616def gitConfigBool(key):
 617    """Return a bool, using git config --bool.  It is True only if the
 618       variable is set to true, and False if set to false or not present
 619       in the config."""
 620
 621    if not _gitConfig.has_key(key):
 622        cmd = [ "git", "config", "--bool", key ]
 623        s = read_pipe(cmd, ignore_error=True)
 624        v = s.strip()
 625        _gitConfig[key] = v == "true"
 626    return _gitConfig[key]
 627
 628def gitConfigList(key):
 629    if not _gitConfig.has_key(key):
 630        s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
 631        _gitConfig[key] = s.strip().split(os.linesep)
 632    return _gitConfig[key]
 633
 634def p4BranchesInGit(branchesAreInRemotes=True):
 635    """Find all the branches whose names start with "p4/", looking
 636       in remotes or heads as specified by the argument.  Return
 637       a dictionary of { branch: revision } for each one found.
 638       The branch names are the short names, without any
 639       "p4/" prefix."""
 640
 641    branches = {}
 642
 643    cmdline = "git rev-parse --symbolic "
 644    if branchesAreInRemotes:
 645        cmdline += "--remotes"
 646    else:
 647        cmdline += "--branches"
 648
 649    for line in read_pipe_lines(cmdline):
 650        line = line.strip()
 651
 652        # only import to p4/
 653        if not line.startswith('p4/'):
 654            continue
 655        # special symbolic ref to p4/master
 656        if line == "p4/HEAD":
 657            continue
 658
 659        # strip off p4/ prefix
 660        branch = line[len("p4/"):]
 661
 662        branches[branch] = parseRevision(line)
 663
 664    return branches
 665
 666def branch_exists(branch):
 667    """Make sure that the given ref name really exists."""
 668
 669    cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
 670    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 671    out, _ = p.communicate()
 672    if p.returncode:
 673        return False
 674    # expect exactly one line of output: the branch name
 675    return out.rstrip() == branch
 676
 677def findUpstreamBranchPoint(head = "HEAD"):
 678    branches = p4BranchesInGit()
 679    # map from depot-path to branch name
 680    branchByDepotPath = {}
 681    for branch in branches.keys():
 682        tip = branches[branch]
 683        log = extractLogMessageFromGitCommit(tip)
 684        settings = extractSettingsGitLog(log)
 685        if settings.has_key("depot-paths"):
 686            paths = ",".join(settings["depot-paths"])
 687            branchByDepotPath[paths] = "remotes/p4/" + branch
 688
 689    settings = None
 690    parent = 0
 691    while parent < 65535:
 692        commit = head + "~%s" % parent
 693        log = extractLogMessageFromGitCommit(commit)
 694        settings = extractSettingsGitLog(log)
 695        if settings.has_key("depot-paths"):
 696            paths = ",".join(settings["depot-paths"])
 697            if branchByDepotPath.has_key(paths):
 698                return [branchByDepotPath[paths], settings]
 699
 700        parent = parent + 1
 701
 702    return ["", settings]
 703
 704def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 705    if not silent:
 706        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 707               % localRefPrefix)
 708
 709    originPrefix = "origin/p4/"
 710
 711    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 712        line = line.strip()
 713        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 714            continue
 715
 716        headName = line[len(originPrefix):]
 717        remoteHead = localRefPrefix + headName
 718        originHead = line
 719
 720        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 721        if (not original.has_key('depot-paths')
 722            or not original.has_key('change')):
 723            continue
 724
 725        update = False
 726        if not gitBranchExists(remoteHead):
 727            if verbose:
 728                print "creating %s" % remoteHead
 729            update = True
 730        else:
 731            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 732            if settings.has_key('change') > 0:
 733                if settings['depot-paths'] == original['depot-paths']:
 734                    originP4Change = int(original['change'])
 735                    p4Change = int(settings['change'])
 736                    if originP4Change > p4Change:
 737                        print ("%s (%s) is newer than %s (%s). "
 738                               "Updating p4 branch from origin."
 739                               % (originHead, originP4Change,
 740                                  remoteHead, p4Change))
 741                        update = True
 742                else:
 743                    print ("Ignoring: %s was imported from %s while "
 744                           "%s was imported from %s"
 745                           % (originHead, ','.join(original['depot-paths']),
 746                              remoteHead, ','.join(settings['depot-paths'])))
 747
 748        if update:
 749            system("git update-ref %s %s" % (remoteHead, originHead))
 750
 751def originP4BranchesExist():
 752        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 753
 754
 755def p4ParseNumericChangeRange(parts):
 756    changeStart = int(parts[0][1:])
 757    if parts[1] == '#head':
 758        changeEnd = p4_last_change()
 759    else:
 760        changeEnd = int(parts[1])
 761
 762    return (changeStart, changeEnd)
 763
 764def chooseBlockSize(blockSize):
 765    if blockSize:
 766        return blockSize
 767    else:
 768        return defaultBlockSize
 769
 770def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
 771    assert depotPaths
 772
 773    # Parse the change range into start and end. Try to find integer
 774    # revision ranges as these can be broken up into blocks to avoid
 775    # hitting server-side limits (maxrows, maxscanresults). But if
 776    # that doesn't work, fall back to using the raw revision specifier
 777    # strings, without using block mode.
 778
 779    if changeRange is None or changeRange == '':
 780        changeStart = 1
 781        changeEnd = p4_last_change()
 782        block_size = chooseBlockSize(requestedBlockSize)
 783    else:
 784        parts = changeRange.split(',')
 785        assert len(parts) == 2
 786        try:
 787            (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
 788            block_size = chooseBlockSize(requestedBlockSize)
 789        except:
 790            changeStart = parts[0][1:]
 791            changeEnd = parts[1]
 792            if requestedBlockSize:
 793                die("cannot use --changes-block-size with non-numeric revisions")
 794            block_size = None
 795
 796    # Accumulate change numbers in a dictionary to avoid duplicates
 797    changes = {}
 798
 799    for p in depotPaths:
 800        # Retrieve changes a block at a time, to prevent running
 801        # into a MaxResults/MaxScanRows error from the server.
 802
 803        while True:
 804            cmd = ['changes']
 805
 806            if block_size:
 807                end = min(changeEnd, changeStart + block_size)
 808                revisionRange = "%d,%d" % (changeStart, end)
 809            else:
 810                revisionRange = "%s,%s" % (changeStart, changeEnd)
 811
 812            cmd += ["%s...@%s" % (p, revisionRange)]
 813
 814            for line in p4_read_pipe_lines(cmd):
 815                changeNum = int(line.split(" ")[1])
 816                changes[changeNum] = True
 817
 818            if not block_size:
 819                break
 820
 821            if end >= changeEnd:
 822                break
 823
 824            changeStart = end + 1
 825
 826    changelist = changes.keys()
 827    changelist.sort()
 828    return changelist
 829
 830def p4PathStartsWith(path, prefix):
 831    # This method tries to remedy a potential mixed-case issue:
 832    #
 833    # If UserA adds  //depot/DirA/file1
 834    # and UserB adds //depot/dira/file2
 835    #
 836    # we may or may not have a problem. If you have core.ignorecase=true,
 837    # we treat DirA and dira as the same directory
 838    if gitConfigBool("core.ignorecase"):
 839        return path.lower().startswith(prefix.lower())
 840    return path.startswith(prefix)
 841
 842def getClientSpec():
 843    """Look at the p4 client spec, create a View() object that contains
 844       all the mappings, and return it."""
 845
 846    specList = p4CmdList("client -o")
 847    if len(specList) != 1:
 848        die('Output from "client -o" is %d lines, expecting 1' %
 849            len(specList))
 850
 851    # dictionary of all client parameters
 852    entry = specList[0]
 853
 854    # the //client/ name
 855    client_name = entry["Client"]
 856
 857    # just the keys that start with "View"
 858    view_keys = [ k for k in entry.keys() if k.startswith("View") ]
 859
 860    # hold this new View
 861    view = View(client_name)
 862
 863    # append the lines, in order, to the view
 864    for view_num in range(len(view_keys)):
 865        k = "View%d" % view_num
 866        if k not in view_keys:
 867            die("Expected view key %s missing" % k)
 868        view.append(entry[k])
 869
 870    return view
 871
 872def getClientRoot():
 873    """Grab the client directory."""
 874
 875    output = p4CmdList("client -o")
 876    if len(output) != 1:
 877        die('Output from "client -o" is %d lines, expecting 1' % len(output))
 878
 879    entry = output[0]
 880    if "Root" not in entry:
 881        die('Client has no "Root"')
 882
 883    return entry["Root"]
 884
 885#
 886# P4 wildcards are not allowed in filenames.  P4 complains
 887# if you simply add them, but you can force it with "-f", in
 888# which case it translates them into %xx encoding internally.
 889#
 890def wildcard_decode(path):
 891    # Search for and fix just these four characters.  Do % last so
 892    # that fixing it does not inadvertently create new %-escapes.
 893    # Cannot have * in a filename in windows; untested as to
 894    # what p4 would do in such a case.
 895    if not platform.system() == "Windows":
 896        path = path.replace("%2A", "*")
 897    path = path.replace("%23", "#") \
 898               .replace("%40", "@") \
 899               .replace("%25", "%")
 900    return path
 901
 902def wildcard_encode(path):
 903    # do % first to avoid double-encoding the %s introduced here
 904    path = path.replace("%", "%25") \
 905               .replace("*", "%2A") \
 906               .replace("#", "%23") \
 907               .replace("@", "%40")
 908    return path
 909
 910def wildcard_present(path):
 911    m = re.search("[*#@%]", path)
 912    return m is not None
 913
 914class Command:
 915    def __init__(self):
 916        self.usage = "usage: %prog [options]"
 917        self.needsGit = True
 918        self.verbose = False
 919
 920class P4UserMap:
 921    def __init__(self):
 922        self.userMapFromPerforceServer = False
 923        self.myP4UserId = None
 924
 925    def p4UserId(self):
 926        if self.myP4UserId:
 927            return self.myP4UserId
 928
 929        results = p4CmdList("user -o")
 930        for r in results:
 931            if r.has_key('User'):
 932                self.myP4UserId = r['User']
 933                return r['User']
 934        die("Could not find your p4 user id")
 935
 936    def p4UserIsMe(self, p4User):
 937        # return True if the given p4 user is actually me
 938        me = self.p4UserId()
 939        if not p4User or p4User != me:
 940            return False
 941        else:
 942            return True
 943
 944    def getUserCacheFilename(self):
 945        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
 946        return home + "/.gitp4-usercache.txt"
 947
 948    def getUserMapFromPerforceServer(self):
 949        if self.userMapFromPerforceServer:
 950            return
 951        self.users = {}
 952        self.emails = {}
 953
 954        for output in p4CmdList("users"):
 955            if not output.has_key("User"):
 956                continue
 957            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
 958            self.emails[output["Email"]] = output["User"]
 959
 960
 961        s = ''
 962        for (key, val) in self.users.items():
 963            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
 964
 965        open(self.getUserCacheFilename(), "wb").write(s)
 966        self.userMapFromPerforceServer = True
 967
 968    def loadUserMapFromCache(self):
 969        self.users = {}
 970        self.userMapFromPerforceServer = False
 971        try:
 972            cache = open(self.getUserCacheFilename(), "rb")
 973            lines = cache.readlines()
 974            cache.close()
 975            for line in lines:
 976                entry = line.strip().split("\t")
 977                self.users[entry[0]] = entry[1]
 978        except IOError:
 979            self.getUserMapFromPerforceServer()
 980
 981class P4Debug(Command):
 982    def __init__(self):
 983        Command.__init__(self)
 984        self.options = []
 985        self.description = "A tool to debug the output of p4 -G."
 986        self.needsGit = False
 987
 988    def run(self, args):
 989        j = 0
 990        for output in p4CmdList(args):
 991            print 'Element: %d' % j
 992            j += 1
 993            print output
 994        return True
 995
 996class P4RollBack(Command):
 997    def __init__(self):
 998        Command.__init__(self)
 999        self.options = [
1000            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1001        ]
1002        self.description = "A tool to debug the multi-branch import. Don't use :)"
1003        self.rollbackLocalBranches = False
1004
1005    def run(self, args):
1006        if len(args) != 1:
1007            return False
1008        maxChange = int(args[0])
1009
1010        if "p4ExitCode" in p4Cmd("changes -m 1"):
1011            die("Problems executing p4");
1012
1013        if self.rollbackLocalBranches:
1014            refPrefix = "refs/heads/"
1015            lines = read_pipe_lines("git rev-parse --symbolic --branches")
1016        else:
1017            refPrefix = "refs/remotes/"
1018            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1019
1020        for line in lines:
1021            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1022                line = line.strip()
1023                ref = refPrefix + line
1024                log = extractLogMessageFromGitCommit(ref)
1025                settings = extractSettingsGitLog(log)
1026
1027                depotPaths = settings['depot-paths']
1028                change = settings['change']
1029
1030                changed = False
1031
1032                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
1033                                                           for p in depotPaths]))) == 0:
1034                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1035                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1036                    continue
1037
1038                while change and int(change) > maxChange:
1039                    changed = True
1040                    if self.verbose:
1041                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1042                    system("git update-ref %s \"%s^\"" % (ref, ref))
1043                    log = extractLogMessageFromGitCommit(ref)
1044                    settings =  extractSettingsGitLog(log)
1045
1046
1047                    depotPaths = settings['depot-paths']
1048                    change = settings['change']
1049
1050                if changed:
1051                    print "%s rewound to %s" % (ref, change)
1052
1053        return True
1054
1055class P4Submit(Command, P4UserMap):
1056
1057    conflict_behavior_choices = ("ask", "skip", "quit")
1058
1059    def __init__(self):
1060        Command.__init__(self)
1061        P4UserMap.__init__(self)
1062        self.options = [
1063                optparse.make_option("--origin", dest="origin"),
1064                optparse.make_option("-M", dest="detectRenames", action="store_true"),
1065                # preserve the user, requires relevant p4 permissions
1066                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1067                optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1068                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1069                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1070                optparse.make_option("--conflict", dest="conflict_behavior",
1071                                     choices=self.conflict_behavior_choices),
1072                optparse.make_option("--branch", dest="branch"),
1073        ]
1074        self.description = "Submit changes from git to the perforce depot."
1075        self.usage += " [name of git branch to submit into perforce depot]"
1076        self.origin = ""
1077        self.detectRenames = False
1078        self.preserveUser = gitConfigBool("git-p4.preserveUser")
1079        self.dry_run = False
1080        self.prepare_p4_only = False
1081        self.conflict_behavior = None
1082        self.isWindows = (platform.system() == "Windows")
1083        self.exportLabels = False
1084        self.p4HasMoveCommand = p4_has_move_command()
1085        self.branch = None
1086
1087    def check(self):
1088        if len(p4CmdList("opened ...")) > 0:
1089            die("You have files opened with perforce! Close them before starting the sync.")
1090
1091    def separate_jobs_from_description(self, message):
1092        """Extract and return a possible Jobs field in the commit
1093           message.  It goes into a separate section in the p4 change
1094           specification.
1095
1096           A jobs line starts with "Jobs:" and looks like a new field
1097           in a form.  Values are white-space separated on the same
1098           line or on following lines that start with a tab.
1099
1100           This does not parse and extract the full git commit message
1101           like a p4 form.  It just sees the Jobs: line as a marker
1102           to pass everything from then on directly into the p4 form,
1103           but outside the description section.
1104
1105           Return a tuple (stripped log message, jobs string)."""
1106
1107        m = re.search(r'^Jobs:', message, re.MULTILINE)
1108        if m is None:
1109            return (message, None)
1110
1111        jobtext = message[m.start():]
1112        stripped_message = message[:m.start()].rstrip()
1113        return (stripped_message, jobtext)
1114
1115    def prepareLogMessage(self, template, message, jobs):
1116        """Edits the template returned from "p4 change -o" to insert
1117           the message in the Description field, and the jobs text in
1118           the Jobs field."""
1119        result = ""
1120
1121        inDescriptionSection = False
1122
1123        for line in template.split("\n"):
1124            if line.startswith("#"):
1125                result += line + "\n"
1126                continue
1127
1128            if inDescriptionSection:
1129                if line.startswith("Files:") or line.startswith("Jobs:"):
1130                    inDescriptionSection = False
1131                    # insert Jobs section
1132                    if jobs:
1133                        result += jobs + "\n"
1134                else:
1135                    continue
1136            else:
1137                if line.startswith("Description:"):
1138                    inDescriptionSection = True
1139                    line += "\n"
1140                    for messageLine in message.split("\n"):
1141                        line += "\t" + messageLine + "\n"
1142
1143            result += line + "\n"
1144
1145        return result
1146
1147    def patchRCSKeywords(self, file, pattern):
1148        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1149        (handle, outFileName) = tempfile.mkstemp(dir='.')
1150        try:
1151            outFile = os.fdopen(handle, "w+")
1152            inFile = open(file, "r")
1153            regexp = re.compile(pattern, re.VERBOSE)
1154            for line in inFile.readlines():
1155                line = regexp.sub(r'$\1$', line)
1156                outFile.write(line)
1157            inFile.close()
1158            outFile.close()
1159            # Forcibly overwrite the original file
1160            os.unlink(file)
1161            shutil.move(outFileName, file)
1162        except:
1163            # cleanup our temporary file
1164            os.unlink(outFileName)
1165            print "Failed to strip RCS keywords in %s" % file
1166            raise
1167
1168        print "Patched up RCS keywords in %s" % file
1169
1170    def p4UserForCommit(self,id):
1171        # Return the tuple (perforce user,git email) for a given git commit id
1172        self.getUserMapFromPerforceServer()
1173        gitEmail = read_pipe(["git", "log", "--max-count=1",
1174                              "--format=%ae", id])
1175        gitEmail = gitEmail.strip()
1176        if not self.emails.has_key(gitEmail):
1177            return (None,gitEmail)
1178        else:
1179            return (self.emails[gitEmail],gitEmail)
1180
1181    def checkValidP4Users(self,commits):
1182        # check if any git authors cannot be mapped to p4 users
1183        for id in commits:
1184            (user,email) = self.p4UserForCommit(id)
1185            if not user:
1186                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1187                if gitConfigBool("git-p4.allowMissingP4Users"):
1188                    print "%s" % msg
1189                else:
1190                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1191
1192    def lastP4Changelist(self):
1193        # Get back the last changelist number submitted in this client spec. This
1194        # then gets used to patch up the username in the change. If the same
1195        # client spec is being used by multiple processes then this might go
1196        # wrong.
1197        results = p4CmdList("client -o")        # find the current client
1198        client = None
1199        for r in results:
1200            if r.has_key('Client'):
1201                client = r['Client']
1202                break
1203        if not client:
1204            die("could not get client spec")
1205        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1206        for r in results:
1207            if r.has_key('change'):
1208                return r['change']
1209        die("Could not get changelist number for last submit - cannot patch up user details")
1210
1211    def modifyChangelistUser(self, changelist, newUser):
1212        # fixup the user field of a changelist after it has been submitted.
1213        changes = p4CmdList("change -o %s" % changelist)
1214        if len(changes) != 1:
1215            die("Bad output from p4 change modifying %s to user %s" %
1216                (changelist, newUser))
1217
1218        c = changes[0]
1219        if c['User'] == newUser: return   # nothing to do
1220        c['User'] = newUser
1221        input = marshal.dumps(c)
1222
1223        result = p4CmdList("change -f -i", stdin=input)
1224        for r in result:
1225            if r.has_key('code'):
1226                if r['code'] == 'error':
1227                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1228            if r.has_key('data'):
1229                print("Updated user field for changelist %s to %s" % (changelist, newUser))
1230                return
1231        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1232
1233    def canChangeChangelists(self):
1234        # check to see if we have p4 admin or super-user permissions, either of
1235        # which are required to modify changelists.
1236        results = p4CmdList(["protects", self.depotPath])
1237        for r in results:
1238            if r.has_key('perm'):
1239                if r['perm'] == 'admin':
1240                    return 1
1241                if r['perm'] == 'super':
1242                    return 1
1243        return 0
1244
1245    def prepareSubmitTemplate(self):
1246        """Run "p4 change -o" to grab a change specification template.
1247           This does not use "p4 -G", as it is nice to keep the submission
1248           template in original order, since a human might edit it.
1249
1250           Remove lines in the Files section that show changes to files
1251           outside the depot path we're committing into."""
1252
1253        template = ""
1254        inFilesSection = False
1255        for line in p4_read_pipe_lines(['change', '-o']):
1256            if line.endswith("\r\n"):
1257                line = line[:-2] + "\n"
1258            if inFilesSection:
1259                if line.startswith("\t"):
1260                    # path starts and ends with a tab
1261                    path = line[1:]
1262                    lastTab = path.rfind("\t")
1263                    if lastTab != -1:
1264                        path = path[:lastTab]
1265                        if not p4PathStartsWith(path, self.depotPath):
1266                            continue
1267                else:
1268                    inFilesSection = False
1269            else:
1270                if line.startswith("Files:"):
1271                    inFilesSection = True
1272
1273            template += line
1274
1275        return template
1276
1277    def edit_template(self, template_file):
1278        """Invoke the editor to let the user change the submission
1279           message.  Return true if okay to continue with the submit."""
1280
1281        # if configured to skip the editing part, just submit
1282        if gitConfigBool("git-p4.skipSubmitEdit"):
1283            return True
1284
1285        # look at the modification time, to check later if the user saved
1286        # the file
1287        mtime = os.stat(template_file).st_mtime
1288
1289        # invoke the editor
1290        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1291            editor = os.environ.get("P4EDITOR")
1292        else:
1293            editor = read_pipe("git var GIT_EDITOR").strip()
1294        system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1295
1296        # If the file was not saved, prompt to see if this patch should
1297        # be skipped.  But skip this verification step if configured so.
1298        if gitConfigBool("git-p4.skipSubmitEditCheck"):
1299            return True
1300
1301        # modification time updated means user saved the file
1302        if os.stat(template_file).st_mtime > mtime:
1303            return True
1304
1305        while True:
1306            response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1307            if response == 'y':
1308                return True
1309            if response == 'n':
1310                return False
1311
1312    def get_diff_description(self, editedFiles, filesToAdd):
1313        # diff
1314        if os.environ.has_key("P4DIFF"):
1315            del(os.environ["P4DIFF"])
1316        diff = ""
1317        for editedFile in editedFiles:
1318            diff += p4_read_pipe(['diff', '-du',
1319                                  wildcard_encode(editedFile)])
1320
1321        # new file diff
1322        newdiff = ""
1323        for newFile in filesToAdd:
1324            newdiff += "==== new file ====\n"
1325            newdiff += "--- /dev/null\n"
1326            newdiff += "+++ %s\n" % newFile
1327            f = open(newFile, "r")
1328            for line in f.readlines():
1329                newdiff += "+" + line
1330            f.close()
1331
1332        return (diff + newdiff).replace('\r\n', '\n')
1333
1334    def applyCommit(self, id):
1335        """Apply one commit, return True if it succeeded."""
1336
1337        print "Applying", read_pipe(["git", "show", "-s",
1338                                     "--format=format:%h %s", id])
1339
1340        (p4User, gitEmail) = self.p4UserForCommit(id)
1341
1342        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1343        filesToAdd = set()
1344        filesToDelete = set()
1345        editedFiles = set()
1346        pureRenameCopy = set()
1347        filesToChangeExecBit = {}
1348
1349        for line in diff:
1350            diff = parseDiffTreeEntry(line)
1351            modifier = diff['status']
1352            path = diff['src']
1353            if modifier == "M":
1354                p4_edit(path)
1355                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1356                    filesToChangeExecBit[path] = diff['dst_mode']
1357                editedFiles.add(path)
1358            elif modifier == "A":
1359                filesToAdd.add(path)
1360                filesToChangeExecBit[path] = diff['dst_mode']
1361                if path in filesToDelete:
1362                    filesToDelete.remove(path)
1363            elif modifier == "D":
1364                filesToDelete.add(path)
1365                if path in filesToAdd:
1366                    filesToAdd.remove(path)
1367            elif modifier == "C":
1368                src, dest = diff['src'], diff['dst']
1369                p4_integrate(src, dest)
1370                pureRenameCopy.add(dest)
1371                if diff['src_sha1'] != diff['dst_sha1']:
1372                    p4_edit(dest)
1373                    pureRenameCopy.discard(dest)
1374                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1375                    p4_edit(dest)
1376                    pureRenameCopy.discard(dest)
1377                    filesToChangeExecBit[dest] = diff['dst_mode']
1378                if self.isWindows:
1379                    # turn off read-only attribute
1380                    os.chmod(dest, stat.S_IWRITE)
1381                os.unlink(dest)
1382                editedFiles.add(dest)
1383            elif modifier == "R":
1384                src, dest = diff['src'], diff['dst']
1385                if self.p4HasMoveCommand:
1386                    p4_edit(src)        # src must be open before move
1387                    p4_move(src, dest)  # opens for (move/delete, move/add)
1388                else:
1389                    p4_integrate(src, dest)
1390                    if diff['src_sha1'] != diff['dst_sha1']:
1391                        p4_edit(dest)
1392                    else:
1393                        pureRenameCopy.add(dest)
1394                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1395                    if not self.p4HasMoveCommand:
1396                        p4_edit(dest)   # with move: already open, writable
1397                    filesToChangeExecBit[dest] = diff['dst_mode']
1398                if not self.p4HasMoveCommand:
1399                    if self.isWindows:
1400                        os.chmod(dest, stat.S_IWRITE)
1401                    os.unlink(dest)
1402                    filesToDelete.add(src)
1403                editedFiles.add(dest)
1404            else:
1405                die("unknown modifier %s for %s" % (modifier, path))
1406
1407        diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1408        patchcmd = diffcmd + " | git apply "
1409        tryPatchCmd = patchcmd + "--check -"
1410        applyPatchCmd = patchcmd + "--check --apply -"
1411        patch_succeeded = True
1412
1413        if os.system(tryPatchCmd) != 0:
1414            fixed_rcs_keywords = False
1415            patch_succeeded = False
1416            print "Unfortunately applying the change failed!"
1417
1418            # Patch failed, maybe it's just RCS keyword woes. Look through
1419            # the patch to see if that's possible.
1420            if gitConfigBool("git-p4.attemptRCSCleanup"):
1421                file = None
1422                pattern = None
1423                kwfiles = {}
1424                for file in editedFiles | filesToDelete:
1425                    # did this file's delta contain RCS keywords?
1426                    pattern = p4_keywords_regexp_for_file(file)
1427
1428                    if pattern:
1429                        # this file is a possibility...look for RCS keywords.
1430                        regexp = re.compile(pattern, re.VERBOSE)
1431                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1432                            if regexp.search(line):
1433                                if verbose:
1434                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1435                                kwfiles[file] = pattern
1436                                break
1437
1438                for file in kwfiles:
1439                    if verbose:
1440                        print "zapping %s with %s" % (line,pattern)
1441                    # File is being deleted, so not open in p4.  Must
1442                    # disable the read-only bit on windows.
1443                    if self.isWindows and file not in editedFiles:
1444                        os.chmod(file, stat.S_IWRITE)
1445                    self.patchRCSKeywords(file, kwfiles[file])
1446                    fixed_rcs_keywords = True
1447
1448            if fixed_rcs_keywords:
1449                print "Retrying the patch with RCS keywords cleaned up"
1450                if os.system(tryPatchCmd) == 0:
1451                    patch_succeeded = True
1452
1453        if not patch_succeeded:
1454            for f in editedFiles:
1455                p4_revert(f)
1456            return False
1457
1458        #
1459        # Apply the patch for real, and do add/delete/+x handling.
1460        #
1461        system(applyPatchCmd)
1462
1463        for f in filesToAdd:
1464            p4_add(f)
1465        for f in filesToDelete:
1466            p4_revert(f)
1467            p4_delete(f)
1468
1469        # Set/clear executable bits
1470        for f in filesToChangeExecBit.keys():
1471            mode = filesToChangeExecBit[f]
1472            setP4ExecBit(f, mode)
1473
1474        #
1475        # Build p4 change description, starting with the contents
1476        # of the git commit message.
1477        #
1478        logMessage = extractLogMessageFromGitCommit(id)
1479        logMessage = logMessage.strip()
1480        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1481
1482        template = self.prepareSubmitTemplate()
1483        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1484
1485        if self.preserveUser:
1486           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1487
1488        if self.checkAuthorship and not self.p4UserIsMe(p4User):
1489            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1490            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1491            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1492
1493        separatorLine = "######## everything below this line is just the diff #######\n"
1494        if not self.prepare_p4_only:
1495            submitTemplate += separatorLine
1496            submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1497
1498        (handle, fileName) = tempfile.mkstemp()
1499        tmpFile = os.fdopen(handle, "w+b")
1500        if self.isWindows:
1501            submitTemplate = submitTemplate.replace("\n", "\r\n")
1502        tmpFile.write(submitTemplate)
1503        tmpFile.close()
1504
1505        if self.prepare_p4_only:
1506            #
1507            # Leave the p4 tree prepared, and the submit template around
1508            # and let the user decide what to do next
1509            #
1510            print
1511            print "P4 workspace prepared for submission."
1512            print "To submit or revert, go to client workspace"
1513            print "  " + self.clientPath
1514            print
1515            print "To submit, use \"p4 submit\" to write a new description,"
1516            print "or \"p4 submit -i <%s\" to use the one prepared by" \
1517                  " \"git p4\"." % fileName
1518            print "You can delete the file \"%s\" when finished." % fileName
1519
1520            if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1521                print "To preserve change ownership by user %s, you must\n" \
1522                      "do \"p4 change -f <change>\" after submitting and\n" \
1523                      "edit the User field."
1524            if pureRenameCopy:
1525                print "After submitting, renamed files must be re-synced."
1526                print "Invoke \"p4 sync -f\" on each of these files:"
1527                for f in pureRenameCopy:
1528                    print "  " + f
1529
1530            print
1531            print "To revert the changes, use \"p4 revert ...\", and delete"
1532            print "the submit template file \"%s\"" % fileName
1533            if filesToAdd:
1534                print "Since the commit adds new files, they must be deleted:"
1535                for f in filesToAdd:
1536                    print "  " + f
1537            print
1538            return True
1539
1540        #
1541        # Let the user edit the change description, then submit it.
1542        #
1543        if self.edit_template(fileName):
1544            # read the edited message and submit
1545            ret = True
1546            tmpFile = open(fileName, "rb")
1547            message = tmpFile.read()
1548            tmpFile.close()
1549            if self.isWindows:
1550                message = message.replace("\r\n", "\n")
1551            submitTemplate = message[:message.index(separatorLine)]
1552            p4_write_pipe(['submit', '-i'], submitTemplate)
1553
1554            if self.preserveUser:
1555                if p4User:
1556                    # Get last changelist number. Cannot easily get it from
1557                    # the submit command output as the output is
1558                    # unmarshalled.
1559                    changelist = self.lastP4Changelist()
1560                    self.modifyChangelistUser(changelist, p4User)
1561
1562            # The rename/copy happened by applying a patch that created a
1563            # new file.  This leaves it writable, which confuses p4.
1564            for f in pureRenameCopy:
1565                p4_sync(f, "-f")
1566
1567        else:
1568            # skip this patch
1569            ret = False
1570            print "Submission cancelled, undoing p4 changes."
1571            for f in editedFiles:
1572                p4_revert(f)
1573            for f in filesToAdd:
1574                p4_revert(f)
1575                os.remove(f)
1576            for f in filesToDelete:
1577                p4_revert(f)
1578
1579        os.remove(fileName)
1580        return ret
1581
1582    # Export git tags as p4 labels. Create a p4 label and then tag
1583    # with that.
1584    def exportGitTags(self, gitTags):
1585        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1586        if len(validLabelRegexp) == 0:
1587            validLabelRegexp = defaultLabelRegexp
1588        m = re.compile(validLabelRegexp)
1589
1590        for name in gitTags:
1591
1592            if not m.match(name):
1593                if verbose:
1594                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1595                continue
1596
1597            # Get the p4 commit this corresponds to
1598            logMessage = extractLogMessageFromGitCommit(name)
1599            values = extractSettingsGitLog(logMessage)
1600
1601            if not values.has_key('change'):
1602                # a tag pointing to something not sent to p4; ignore
1603                if verbose:
1604                    print "git tag %s does not give a p4 commit" % name
1605                continue
1606            else:
1607                changelist = values['change']
1608
1609            # Get the tag details.
1610            inHeader = True
1611            isAnnotated = False
1612            body = []
1613            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1614                l = l.strip()
1615                if inHeader:
1616                    if re.match(r'tag\s+', l):
1617                        isAnnotated = True
1618                    elif re.match(r'\s*$', l):
1619                        inHeader = False
1620                        continue
1621                else:
1622                    body.append(l)
1623
1624            if not isAnnotated:
1625                body = ["lightweight tag imported by git p4\n"]
1626
1627            # Create the label - use the same view as the client spec we are using
1628            clientSpec = getClientSpec()
1629
1630            labelTemplate  = "Label: %s\n" % name
1631            labelTemplate += "Description:\n"
1632            for b in body:
1633                labelTemplate += "\t" + b + "\n"
1634            labelTemplate += "View:\n"
1635            for depot_side in clientSpec.mappings:
1636                labelTemplate += "\t%s\n" % depot_side
1637
1638            if self.dry_run:
1639                print "Would create p4 label %s for tag" % name
1640            elif self.prepare_p4_only:
1641                print "Not creating p4 label %s for tag due to option" \
1642                      " --prepare-p4-only" % name
1643            else:
1644                p4_write_pipe(["label", "-i"], labelTemplate)
1645
1646                # Use the label
1647                p4_system(["tag", "-l", name] +
1648                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1649
1650                if verbose:
1651                    print "created p4 label for tag %s" % name
1652
1653    def run(self, args):
1654        if len(args) == 0:
1655            self.master = currentGitBranch()
1656            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1657                die("Detecting current git branch failed!")
1658        elif len(args) == 1:
1659            self.master = args[0]
1660            if not branchExists(self.master):
1661                die("Branch %s does not exist" % self.master)
1662        else:
1663            return False
1664
1665        allowSubmit = gitConfig("git-p4.allowSubmit")
1666        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1667            die("%s is not in git-p4.allowSubmit" % self.master)
1668
1669        [upstream, settings] = findUpstreamBranchPoint()
1670        self.depotPath = settings['depot-paths'][0]
1671        if len(self.origin) == 0:
1672            self.origin = upstream
1673
1674        if self.preserveUser:
1675            if not self.canChangeChangelists():
1676                die("Cannot preserve user names without p4 super-user or admin permissions")
1677
1678        # if not set from the command line, try the config file
1679        if self.conflict_behavior is None:
1680            val = gitConfig("git-p4.conflict")
1681            if val:
1682                if val not in self.conflict_behavior_choices:
1683                    die("Invalid value '%s' for config git-p4.conflict" % val)
1684            else:
1685                val = "ask"
1686            self.conflict_behavior = val
1687
1688        if self.verbose:
1689            print "Origin branch is " + self.origin
1690
1691        if len(self.depotPath) == 0:
1692            print "Internal error: cannot locate perforce depot path from existing branches"
1693            sys.exit(128)
1694
1695        self.useClientSpec = False
1696        if gitConfigBool("git-p4.useclientspec"):
1697            self.useClientSpec = True
1698        if self.useClientSpec:
1699            self.clientSpecDirs = getClientSpec()
1700
1701        # Check for the existance of P4 branches
1702        branchesDetected = (len(p4BranchesInGit().keys()) > 1)
1703
1704        if self.useClientSpec and not branchesDetected:
1705            # all files are relative to the client spec
1706            self.clientPath = getClientRoot()
1707        else:
1708            self.clientPath = p4Where(self.depotPath)
1709
1710        if self.clientPath == "":
1711            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1712
1713        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1714        self.oldWorkingDirectory = os.getcwd()
1715
1716        # ensure the clientPath exists
1717        new_client_dir = False
1718        if not os.path.exists(self.clientPath):
1719            new_client_dir = True
1720            os.makedirs(self.clientPath)
1721
1722        chdir(self.clientPath, is_client_path=True)
1723        if self.dry_run:
1724            print "Would synchronize p4 checkout in %s" % self.clientPath
1725        else:
1726            print "Synchronizing p4 checkout..."
1727            if new_client_dir:
1728                # old one was destroyed, and maybe nobody told p4
1729                p4_sync("...", "-f")
1730            else:
1731                p4_sync("...")
1732        self.check()
1733
1734        commits = []
1735        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
1736            commits.append(line.strip())
1737        commits.reverse()
1738
1739        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1740            self.checkAuthorship = False
1741        else:
1742            self.checkAuthorship = True
1743
1744        if self.preserveUser:
1745            self.checkValidP4Users(commits)
1746
1747        #
1748        # Build up a set of options to be passed to diff when
1749        # submitting each commit to p4.
1750        #
1751        if self.detectRenames:
1752            # command-line -M arg
1753            self.diffOpts = "-M"
1754        else:
1755            # If not explicitly set check the config variable
1756            detectRenames = gitConfig("git-p4.detectRenames")
1757
1758            if detectRenames.lower() == "false" or detectRenames == "":
1759                self.diffOpts = ""
1760            elif detectRenames.lower() == "true":
1761                self.diffOpts = "-M"
1762            else:
1763                self.diffOpts = "-M%s" % detectRenames
1764
1765        # no command-line arg for -C or --find-copies-harder, just
1766        # config variables
1767        detectCopies = gitConfig("git-p4.detectCopies")
1768        if detectCopies.lower() == "false" or detectCopies == "":
1769            pass
1770        elif detectCopies.lower() == "true":
1771            self.diffOpts += " -C"
1772        else:
1773            self.diffOpts += " -C%s" % detectCopies
1774
1775        if gitConfigBool("git-p4.detectCopiesHarder"):
1776            self.diffOpts += " --find-copies-harder"
1777
1778        #
1779        # Apply the commits, one at a time.  On failure, ask if should
1780        # continue to try the rest of the patches, or quit.
1781        #
1782        if self.dry_run:
1783            print "Would apply"
1784        applied = []
1785        last = len(commits) - 1
1786        for i, commit in enumerate(commits):
1787            if self.dry_run:
1788                print " ", read_pipe(["git", "show", "-s",
1789                                      "--format=format:%h %s", commit])
1790                ok = True
1791            else:
1792                ok = self.applyCommit(commit)
1793            if ok:
1794                applied.append(commit)
1795            else:
1796                if self.prepare_p4_only and i < last:
1797                    print "Processing only the first commit due to option" \
1798                          " --prepare-p4-only"
1799                    break
1800                if i < last:
1801                    quit = False
1802                    while True:
1803                        # prompt for what to do, or use the option/variable
1804                        if self.conflict_behavior == "ask":
1805                            print "What do you want to do?"
1806                            response = raw_input("[s]kip this commit but apply"
1807                                                 " the rest, or [q]uit? ")
1808                            if not response:
1809                                continue
1810                        elif self.conflict_behavior == "skip":
1811                            response = "s"
1812                        elif self.conflict_behavior == "quit":
1813                            response = "q"
1814                        else:
1815                            die("Unknown conflict_behavior '%s'" %
1816                                self.conflict_behavior)
1817
1818                        if response[0] == "s":
1819                            print "Skipping this commit, but applying the rest"
1820                            break
1821                        if response[0] == "q":
1822                            print "Quitting"
1823                            quit = True
1824                            break
1825                    if quit:
1826                        break
1827
1828        chdir(self.oldWorkingDirectory)
1829
1830        if self.dry_run:
1831            pass
1832        elif self.prepare_p4_only:
1833            pass
1834        elif len(commits) == len(applied):
1835            print "All commits applied!"
1836
1837            sync = P4Sync()
1838            if self.branch:
1839                sync.branch = self.branch
1840            sync.run([])
1841
1842            rebase = P4Rebase()
1843            rebase.rebase()
1844
1845        else:
1846            if len(applied) == 0:
1847                print "No commits applied."
1848            else:
1849                print "Applied only the commits marked with '*':"
1850                for c in commits:
1851                    if c in applied:
1852                        star = "*"
1853                    else:
1854                        star = " "
1855                    print star, read_pipe(["git", "show", "-s",
1856                                           "--format=format:%h %s",  c])
1857                print "You will have to do 'git p4 sync' and rebase."
1858
1859        if gitConfigBool("git-p4.exportLabels"):
1860            self.exportLabels = True
1861
1862        if self.exportLabels:
1863            p4Labels = getP4Labels(self.depotPath)
1864            gitTags = getGitTags()
1865
1866            missingGitTags = gitTags - p4Labels
1867            self.exportGitTags(missingGitTags)
1868
1869        # exit with error unless everything applied perfectly
1870        if len(commits) != len(applied):
1871                sys.exit(1)
1872
1873        return True
1874
1875class View(object):
1876    """Represent a p4 view ("p4 help views"), and map files in a
1877       repo according to the view."""
1878
1879    def __init__(self, client_name):
1880        self.mappings = []
1881        self.client_prefix = "//%s/" % client_name
1882        # cache results of "p4 where" to lookup client file locations
1883        self.client_spec_path_cache = {}
1884
1885    def append(self, view_line):
1886        """Parse a view line, splitting it into depot and client
1887           sides.  Append to self.mappings, preserving order.  This
1888           is only needed for tag creation."""
1889
1890        # Split the view line into exactly two words.  P4 enforces
1891        # structure on these lines that simplifies this quite a bit.
1892        #
1893        # Either or both words may be double-quoted.
1894        # Single quotes do not matter.
1895        # Double-quote marks cannot occur inside the words.
1896        # A + or - prefix is also inside the quotes.
1897        # There are no quotes unless they contain a space.
1898        # The line is already white-space stripped.
1899        # The two words are separated by a single space.
1900        #
1901        if view_line[0] == '"':
1902            # First word is double quoted.  Find its end.
1903            close_quote_index = view_line.find('"', 1)
1904            if close_quote_index <= 0:
1905                die("No first-word closing quote found: %s" % view_line)
1906            depot_side = view_line[1:close_quote_index]
1907            # skip closing quote and space
1908            rhs_index = close_quote_index + 1 + 1
1909        else:
1910            space_index = view_line.find(" ")
1911            if space_index <= 0:
1912                die("No word-splitting space found: %s" % view_line)
1913            depot_side = view_line[0:space_index]
1914            rhs_index = space_index + 1
1915
1916        # prefix + means overlay on previous mapping
1917        if depot_side.startswith("+"):
1918            depot_side = depot_side[1:]
1919
1920        # prefix - means exclude this path, leave out of mappings
1921        exclude = False
1922        if depot_side.startswith("-"):
1923            exclude = True
1924            depot_side = depot_side[1:]
1925
1926        if not exclude:
1927            self.mappings.append(depot_side)
1928
1929    def convert_client_path(self, clientFile):
1930        # chop off //client/ part to make it relative
1931        if not clientFile.startswith(self.client_prefix):
1932            die("No prefix '%s' on clientFile '%s'" %
1933                (self.client_prefix, clientFile))
1934        return clientFile[len(self.client_prefix):]
1935
1936    def update_client_spec_path_cache(self, files):
1937        """ Caching file paths by "p4 where" batch query """
1938
1939        # List depot file paths exclude that already cached
1940        fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1941
1942        if len(fileArgs) == 0:
1943            return  # All files in cache
1944
1945        where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1946        for res in where_result:
1947            if "code" in res and res["code"] == "error":
1948                # assume error is "... file(s) not in client view"
1949                continue
1950            if "clientFile" not in res:
1951                die("No clientFile in 'p4 where' output")
1952            if "unmap" in res:
1953                # it will list all of them, but only one not unmap-ped
1954                continue
1955            if gitConfigBool("core.ignorecase"):
1956                res['depotFile'] = res['depotFile'].lower()
1957            self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1958
1959        # not found files or unmap files set to ""
1960        for depotFile in fileArgs:
1961            if gitConfigBool("core.ignorecase"):
1962                depotFile = depotFile.lower()
1963            if depotFile not in self.client_spec_path_cache:
1964                self.client_spec_path_cache[depotFile] = ""
1965
1966    def map_in_client(self, depot_path):
1967        """Return the relative location in the client where this
1968           depot file should live.  Returns "" if the file should
1969           not be mapped in the client."""
1970
1971        if gitConfigBool("core.ignorecase"):
1972            depot_path = depot_path.lower()
1973
1974        if depot_path in self.client_spec_path_cache:
1975            return self.client_spec_path_cache[depot_path]
1976
1977        die( "Error: %s is not found in client spec path" % depot_path )
1978        return ""
1979
1980class P4Sync(Command, P4UserMap):
1981    delete_actions = ( "delete", "move/delete", "purge" )
1982
1983    def __init__(self):
1984        Command.__init__(self)
1985        P4UserMap.__init__(self)
1986        self.options = [
1987                optparse.make_option("--branch", dest="branch"),
1988                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1989                optparse.make_option("--changesfile", dest="changesFile"),
1990                optparse.make_option("--silent", dest="silent", action="store_true"),
1991                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1992                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1993                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1994                                     help="Import into refs/heads/ , not refs/remotes"),
1995                optparse.make_option("--max-changes", dest="maxChanges",
1996                                     help="Maximum number of changes to import"),
1997                optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
1998                                     help="Internal block size to use when iteratively calling p4 changes"),
1999                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2000                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2001                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2002                                     help="Only sync files that are included in the Perforce Client Spec"),
2003                optparse.make_option("-/", dest="cloneExclude",
2004                                     action="append", type="string",
2005                                     help="exclude depot path"),
2006        ]
2007        self.description = """Imports from Perforce into a git repository.\n
2008    example:
2009    //depot/my/project/ -- to import the current head
2010    //depot/my/project/@all -- to import everything
2011    //depot/my/project/@1,6 -- to import only from revision 1 to 6
2012
2013    (a ... is not needed in the path p4 specification, it's added implicitly)"""
2014
2015        self.usage += " //depot/path[@revRange]"
2016        self.silent = False
2017        self.createdBranches = set()
2018        self.committedChanges = set()
2019        self.branch = ""
2020        self.detectBranches = False
2021        self.detectLabels = False
2022        self.importLabels = False
2023        self.changesFile = ""
2024        self.syncWithOrigin = True
2025        self.importIntoRemotes = True
2026        self.maxChanges = ""
2027        self.changes_block_size = None
2028        self.keepRepoPath = False
2029        self.depotPaths = None
2030        self.p4BranchesInGit = []
2031        self.cloneExclude = []
2032        self.useClientSpec = False
2033        self.useClientSpec_from_options = False
2034        self.clientSpecDirs = None
2035        self.tempBranches = []
2036        self.tempBranchLocation = "git-p4-tmp"
2037
2038        if gitConfig("git-p4.syncFromOrigin") == "false":
2039            self.syncWithOrigin = False
2040
2041    # This is required for the "append" cloneExclude action
2042    def ensure_value(self, attr, value):
2043        if not hasattr(self, attr) or getattr(self, attr) is None:
2044            setattr(self, attr, value)
2045        return getattr(self, attr)
2046
2047    # Force a checkpoint in fast-import and wait for it to finish
2048    def checkpoint(self):
2049        self.gitStream.write("checkpoint\n\n")
2050        self.gitStream.write("progress checkpoint\n\n")
2051        out = self.gitOutput.readline()
2052        if self.verbose:
2053            print "checkpoint finished: " + out
2054
2055    def extractFilesFromCommit(self, commit):
2056        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2057                             for path in self.cloneExclude]
2058        files = []
2059        fnum = 0
2060        while commit.has_key("depotFile%s" % fnum):
2061            path =  commit["depotFile%s" % fnum]
2062
2063            if [p for p in self.cloneExclude
2064                if p4PathStartsWith(path, p)]:
2065                found = False
2066            else:
2067                found = [p for p in self.depotPaths
2068                         if p4PathStartsWith(path, p)]
2069            if not found:
2070                fnum = fnum + 1
2071                continue
2072
2073            file = {}
2074            file["path"] = path
2075            file["rev"] = commit["rev%s" % fnum]
2076            file["action"] = commit["action%s" % fnum]
2077            file["type"] = commit["type%s" % fnum]
2078            files.append(file)
2079            fnum = fnum + 1
2080        return files
2081
2082    def stripRepoPath(self, path, prefixes):
2083        """When streaming files, this is called to map a p4 depot path
2084           to where it should go in git.  The prefixes are either
2085           self.depotPaths, or self.branchPrefixes in the case of
2086           branch detection."""
2087
2088        if self.useClientSpec:
2089            # branch detection moves files up a level (the branch name)
2090            # from what client spec interpretation gives
2091            path = self.clientSpecDirs.map_in_client(path)
2092            if self.detectBranches:
2093                for b in self.knownBranches:
2094                    if path.startswith(b + "/"):
2095                        path = path[len(b)+1:]
2096
2097        elif self.keepRepoPath:
2098            # Preserve everything in relative path name except leading
2099            # //depot/; just look at first prefix as they all should
2100            # be in the same depot.
2101            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2102            if p4PathStartsWith(path, depot):
2103                path = path[len(depot):]
2104
2105        else:
2106            for p in prefixes:
2107                if p4PathStartsWith(path, p):
2108                    path = path[len(p):]
2109                    break
2110
2111        path = wildcard_decode(path)
2112        return path
2113
2114    def splitFilesIntoBranches(self, commit):
2115        """Look at each depotFile in the commit to figure out to what
2116           branch it belongs."""
2117
2118        if self.clientSpecDirs:
2119            files = self.extractFilesFromCommit(commit)
2120            self.clientSpecDirs.update_client_spec_path_cache(files)
2121
2122        branches = {}
2123        fnum = 0
2124        while commit.has_key("depotFile%s" % fnum):
2125            path =  commit["depotFile%s" % fnum]
2126            found = [p for p in self.depotPaths
2127                     if p4PathStartsWith(path, p)]
2128            if not found:
2129                fnum = fnum + 1
2130                continue
2131
2132            file = {}
2133            file["path"] = path
2134            file["rev"] = commit["rev%s" % fnum]
2135            file["action"] = commit["action%s" % fnum]
2136            file["type"] = commit["type%s" % fnum]
2137            fnum = fnum + 1
2138
2139            # start with the full relative path where this file would
2140            # go in a p4 client
2141            if self.useClientSpec:
2142                relPath = self.clientSpecDirs.map_in_client(path)
2143            else:
2144                relPath = self.stripRepoPath(path, self.depotPaths)
2145
2146            for branch in self.knownBranches.keys():
2147                # add a trailing slash so that a commit into qt/4.2foo
2148                # doesn't end up in qt/4.2, e.g.
2149                if relPath.startswith(branch + "/"):
2150                    if branch not in branches:
2151                        branches[branch] = []
2152                    branches[branch].append(file)
2153                    break
2154
2155        return branches
2156
2157    # output one file from the P4 stream
2158    # - helper for streamP4Files
2159
2160    def streamOneP4File(self, file, contents):
2161        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2162        if verbose:
2163            sys.stderr.write("%s\n" % relPath)
2164
2165        (type_base, type_mods) = split_p4_type(file["type"])
2166
2167        git_mode = "100644"
2168        if "x" in type_mods:
2169            git_mode = "100755"
2170        if type_base == "symlink":
2171            git_mode = "120000"
2172            # p4 print on a symlink sometimes contains "target\n";
2173            # if it does, remove the newline
2174            data = ''.join(contents)
2175            if not data:
2176                # Some version of p4 allowed creating a symlink that pointed
2177                # to nothing.  This causes p4 errors when checking out such
2178                # a change, and errors here too.  Work around it by ignoring
2179                # the bad symlink; hopefully a future change fixes it.
2180                print "\nIgnoring empty symlink in %s" % file['depotFile']
2181                return
2182            elif data[-1] == '\n':
2183                contents = [data[:-1]]
2184            else:
2185                contents = [data]
2186
2187        if type_base == "utf16":
2188            # p4 delivers different text in the python output to -G
2189            # than it does when using "print -o", or normal p4 client
2190            # operations.  utf16 is converted to ascii or utf8, perhaps.
2191            # But ascii text saved as -t utf16 is completely mangled.
2192            # Invoke print -o to get the real contents.
2193            #
2194            # On windows, the newlines will always be mangled by print, so put
2195            # them back too.  This is not needed to the cygwin windows version,
2196            # just the native "NT" type.
2197            #
2198            text = p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ])
2199            if p4_version_string().find("/NT") >= 0:
2200                text = text.replace("\r\n", "\n")
2201            contents = [ text ]
2202
2203        if type_base == "apple":
2204            # Apple filetype files will be streamed as a concatenation of
2205            # its appledouble header and the contents.  This is useless
2206            # on both macs and non-macs.  If using "print -q -o xx", it
2207            # will create "xx" with the data, and "%xx" with the header.
2208            # This is also not very useful.
2209            #
2210            # Ideally, someday, this script can learn how to generate
2211            # appledouble files directly and import those to git, but
2212            # non-mac machines can never find a use for apple filetype.
2213            print "\nIgnoring apple filetype file %s" % file['depotFile']
2214            return
2215
2216        # Note that we do not try to de-mangle keywords on utf16 files,
2217        # even though in theory somebody may want that.
2218        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2219        if pattern:
2220            regexp = re.compile(pattern, re.VERBOSE)
2221            text = ''.join(contents)
2222            text = regexp.sub(r'$\1$', text)
2223            contents = [ text ]
2224
2225        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2226
2227        # total length...
2228        length = 0
2229        for d in contents:
2230            length = length + len(d)
2231
2232        self.gitStream.write("data %d\n" % length)
2233        for d in contents:
2234            self.gitStream.write(d)
2235        self.gitStream.write("\n")
2236
2237    def streamOneP4Deletion(self, file):
2238        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2239        if verbose:
2240            sys.stderr.write("delete %s\n" % relPath)
2241        self.gitStream.write("D %s\n" % relPath)
2242
2243    # handle another chunk of streaming data
2244    def streamP4FilesCb(self, marshalled):
2245
2246        # catch p4 errors and complain
2247        err = None
2248        if "code" in marshalled:
2249            if marshalled["code"] == "error":
2250                if "data" in marshalled:
2251                    err = marshalled["data"].rstrip()
2252        if err:
2253            f = None
2254            if self.stream_have_file_info:
2255                if "depotFile" in self.stream_file:
2256                    f = self.stream_file["depotFile"]
2257            # force a failure in fast-import, else an empty
2258            # commit will be made
2259            self.gitStream.write("\n")
2260            self.gitStream.write("die-now\n")
2261            self.gitStream.close()
2262            # ignore errors, but make sure it exits first
2263            self.importProcess.wait()
2264            if f:
2265                die("Error from p4 print for %s: %s" % (f, err))
2266            else:
2267                die("Error from p4 print: %s" % err)
2268
2269        if marshalled.has_key('depotFile') and self.stream_have_file_info:
2270            # start of a new file - output the old one first
2271            self.streamOneP4File(self.stream_file, self.stream_contents)
2272            self.stream_file = {}
2273            self.stream_contents = []
2274            self.stream_have_file_info = False
2275
2276        # pick up the new file information... for the
2277        # 'data' field we need to append to our array
2278        for k in marshalled.keys():
2279            if k == 'data':
2280                self.stream_contents.append(marshalled['data'])
2281            else:
2282                self.stream_file[k] = marshalled[k]
2283
2284        self.stream_have_file_info = True
2285
2286    # Stream directly from "p4 files" into "git fast-import"
2287    def streamP4Files(self, files):
2288        filesForCommit = []
2289        filesToRead = []
2290        filesToDelete = []
2291
2292        for f in files:
2293            # if using a client spec, only add the files that have
2294            # a path in the client
2295            if self.clientSpecDirs:
2296                if self.clientSpecDirs.map_in_client(f['path']) == "":
2297                    continue
2298
2299            filesForCommit.append(f)
2300            if f['action'] in self.delete_actions:
2301                filesToDelete.append(f)
2302            else:
2303                filesToRead.append(f)
2304
2305        # deleted files...
2306        for f in filesToDelete:
2307            self.streamOneP4Deletion(f)
2308
2309        if len(filesToRead) > 0:
2310            self.stream_file = {}
2311            self.stream_contents = []
2312            self.stream_have_file_info = False
2313
2314            # curry self argument
2315            def streamP4FilesCbSelf(entry):
2316                self.streamP4FilesCb(entry)
2317
2318            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2319
2320            p4CmdList(["-x", "-", "print"],
2321                      stdin=fileArgs,
2322                      cb=streamP4FilesCbSelf)
2323
2324            # do the last chunk
2325            if self.stream_file.has_key('depotFile'):
2326                self.streamOneP4File(self.stream_file, self.stream_contents)
2327
2328    def make_email(self, userid):
2329        if userid in self.users:
2330            return self.users[userid]
2331        else:
2332            return "%s <a@b>" % userid
2333
2334    # Stream a p4 tag
2335    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2336        if verbose:
2337            print "writing tag %s for commit %s" % (labelName, commit)
2338        gitStream.write("tag %s\n" % labelName)
2339        gitStream.write("from %s\n" % commit)
2340
2341        if labelDetails.has_key('Owner'):
2342            owner = labelDetails["Owner"]
2343        else:
2344            owner = None
2345
2346        # Try to use the owner of the p4 label, or failing that,
2347        # the current p4 user id.
2348        if owner:
2349            email = self.make_email(owner)
2350        else:
2351            email = self.make_email(self.p4UserId())
2352        tagger = "%s %s %s" % (email, epoch, self.tz)
2353
2354        gitStream.write("tagger %s\n" % tagger)
2355
2356        print "labelDetails=",labelDetails
2357        if labelDetails.has_key('Description'):
2358            description = labelDetails['Description']
2359        else:
2360            description = 'Label from git p4'
2361
2362        gitStream.write("data %d\n" % len(description))
2363        gitStream.write(description)
2364        gitStream.write("\n")
2365
2366    def commit(self, details, files, branch, parent = ""):
2367        epoch = details["time"]
2368        author = details["user"]
2369
2370        if self.verbose:
2371            print "commit into %s" % branch
2372
2373        # start with reading files; if that fails, we should not
2374        # create a commit.
2375        new_files = []
2376        for f in files:
2377            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2378                new_files.append (f)
2379            else:
2380                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2381
2382        if self.clientSpecDirs:
2383            self.clientSpecDirs.update_client_spec_path_cache(files)
2384
2385        self.gitStream.write("commit %s\n" % branch)
2386#        gitStream.write("mark :%s\n" % details["change"])
2387        self.committedChanges.add(int(details["change"]))
2388        committer = ""
2389        if author not in self.users:
2390            self.getUserMapFromPerforceServer()
2391        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2392
2393        self.gitStream.write("committer %s\n" % committer)
2394
2395        self.gitStream.write("data <<EOT\n")
2396        self.gitStream.write(details["desc"])
2397        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2398                             (','.join(self.branchPrefixes), details["change"]))
2399        if len(details['options']) > 0:
2400            self.gitStream.write(": options = %s" % details['options'])
2401        self.gitStream.write("]\nEOT\n\n")
2402
2403        if len(parent) > 0:
2404            if self.verbose:
2405                print "parent %s" % parent
2406            self.gitStream.write("from %s\n" % parent)
2407
2408        self.streamP4Files(new_files)
2409        self.gitStream.write("\n")
2410
2411        change = int(details["change"])
2412
2413        if self.labels.has_key(change):
2414            label = self.labels[change]
2415            labelDetails = label[0]
2416            labelRevisions = label[1]
2417            if self.verbose:
2418                print "Change %s is labelled %s" % (change, labelDetails)
2419
2420            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2421                                                for p in self.branchPrefixes])
2422
2423            if len(files) == len(labelRevisions):
2424
2425                cleanedFiles = {}
2426                for info in files:
2427                    if info["action"] in self.delete_actions:
2428                        continue
2429                    cleanedFiles[info["depotFile"]] = info["rev"]
2430
2431                if cleanedFiles == labelRevisions:
2432                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2433
2434                else:
2435                    if not self.silent:
2436                        print ("Tag %s does not match with change %s: files do not match."
2437                               % (labelDetails["label"], change))
2438
2439            else:
2440                if not self.silent:
2441                    print ("Tag %s does not match with change %s: file count is different."
2442                           % (labelDetails["label"], change))
2443
2444    # Build a dictionary of changelists and labels, for "detect-labels" option.
2445    def getLabels(self):
2446        self.labels = {}
2447
2448        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2449        if len(l) > 0 and not self.silent:
2450            print "Finding files belonging to labels in %s" % `self.depotPaths`
2451
2452        for output in l:
2453            label = output["label"]
2454            revisions = {}
2455            newestChange = 0
2456            if self.verbose:
2457                print "Querying files for label %s" % label
2458            for file in p4CmdList(["files"] +
2459                                      ["%s...@%s" % (p, label)
2460                                          for p in self.depotPaths]):
2461                revisions[file["depotFile"]] = file["rev"]
2462                change = int(file["change"])
2463                if change > newestChange:
2464                    newestChange = change
2465
2466            self.labels[newestChange] = [output, revisions]
2467
2468        if self.verbose:
2469            print "Label changes: %s" % self.labels.keys()
2470
2471    # Import p4 labels as git tags. A direct mapping does not
2472    # exist, so assume that if all the files are at the same revision
2473    # then we can use that, or it's something more complicated we should
2474    # just ignore.
2475    def importP4Labels(self, stream, p4Labels):
2476        if verbose:
2477            print "import p4 labels: " + ' '.join(p4Labels)
2478
2479        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2480        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2481        if len(validLabelRegexp) == 0:
2482            validLabelRegexp = defaultLabelRegexp
2483        m = re.compile(validLabelRegexp)
2484
2485        for name in p4Labels:
2486            commitFound = False
2487
2488            if not m.match(name):
2489                if verbose:
2490                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2491                continue
2492
2493            if name in ignoredP4Labels:
2494                continue
2495
2496            labelDetails = p4CmdList(['label', "-o", name])[0]
2497
2498            # get the most recent changelist for each file in this label
2499            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2500                                for p in self.depotPaths])
2501
2502            if change.has_key('change'):
2503                # find the corresponding git commit; take the oldest commit
2504                changelist = int(change['change'])
2505                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2506                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2507                if len(gitCommit) == 0:
2508                    print "could not find git commit for changelist %d" % changelist
2509                else:
2510                    gitCommit = gitCommit.strip()
2511                    commitFound = True
2512                    # Convert from p4 time format
2513                    try:
2514                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2515                    except ValueError:
2516                        print "Could not convert label time %s" % labelDetails['Update']
2517                        tmwhen = 1
2518
2519                    when = int(time.mktime(tmwhen))
2520                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2521                    if verbose:
2522                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2523            else:
2524                if verbose:
2525                    print "Label %s has no changelists - possibly deleted?" % name
2526
2527            if not commitFound:
2528                # We can't import this label; don't try again as it will get very
2529                # expensive repeatedly fetching all the files for labels that will
2530                # never be imported. If the label is moved in the future, the
2531                # ignore will need to be removed manually.
2532                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2533
2534    def guessProjectName(self):
2535        for p in self.depotPaths:
2536            if p.endswith("/"):
2537                p = p[:-1]
2538            p = p[p.strip().rfind("/") + 1:]
2539            if not p.endswith("/"):
2540               p += "/"
2541            return p
2542
2543    def getBranchMapping(self):
2544        lostAndFoundBranches = set()
2545
2546        user = gitConfig("git-p4.branchUser")
2547        if len(user) > 0:
2548            command = "branches -u %s" % user
2549        else:
2550            command = "branches"
2551
2552        for info in p4CmdList(command):
2553            details = p4Cmd(["branch", "-o", info["branch"]])
2554            viewIdx = 0
2555            while details.has_key("View%s" % viewIdx):
2556                paths = details["View%s" % viewIdx].split(" ")
2557                viewIdx = viewIdx + 1
2558                # require standard //depot/foo/... //depot/bar/... mapping
2559                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2560                    continue
2561                source = paths[0]
2562                destination = paths[1]
2563                ## HACK
2564                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2565                    source = source[len(self.depotPaths[0]):-4]
2566                    destination = destination[len(self.depotPaths[0]):-4]
2567
2568                    if destination in self.knownBranches:
2569                        if not self.silent:
2570                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2571                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2572                        continue
2573
2574                    self.knownBranches[destination] = source
2575
2576                    lostAndFoundBranches.discard(destination)
2577
2578                    if source not in self.knownBranches:
2579                        lostAndFoundBranches.add(source)
2580
2581        # Perforce does not strictly require branches to be defined, so we also
2582        # check git config for a branch list.
2583        #
2584        # Example of branch definition in git config file:
2585        # [git-p4]
2586        #   branchList=main:branchA
2587        #   branchList=main:branchB
2588        #   branchList=branchA:branchC
2589        configBranches = gitConfigList("git-p4.branchList")
2590        for branch in configBranches:
2591            if branch:
2592                (source, destination) = branch.split(":")
2593                self.knownBranches[destination] = source
2594
2595                lostAndFoundBranches.discard(destination)
2596
2597                if source not in self.knownBranches:
2598                    lostAndFoundBranches.add(source)
2599
2600
2601        for branch in lostAndFoundBranches:
2602            self.knownBranches[branch] = branch
2603
2604    def getBranchMappingFromGitBranches(self):
2605        branches = p4BranchesInGit(self.importIntoRemotes)
2606        for branch in branches.keys():
2607            if branch == "master":
2608                branch = "main"
2609            else:
2610                branch = branch[len(self.projectName):]
2611            self.knownBranches[branch] = branch
2612
2613    def updateOptionDict(self, d):
2614        option_keys = {}
2615        if self.keepRepoPath:
2616            option_keys['keepRepoPath'] = 1
2617
2618        d["options"] = ' '.join(sorted(option_keys.keys()))
2619
2620    def readOptions(self, d):
2621        self.keepRepoPath = (d.has_key('options')
2622                             and ('keepRepoPath' in d['options']))
2623
2624    def gitRefForBranch(self, branch):
2625        if branch == "main":
2626            return self.refPrefix + "master"
2627
2628        if len(branch) <= 0:
2629            return branch
2630
2631        return self.refPrefix + self.projectName + branch
2632
2633    def gitCommitByP4Change(self, ref, change):
2634        if self.verbose:
2635            print "looking in ref " + ref + " for change %s using bisect..." % change
2636
2637        earliestCommit = ""
2638        latestCommit = parseRevision(ref)
2639
2640        while True:
2641            if self.verbose:
2642                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2643            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2644            if len(next) == 0:
2645                if self.verbose:
2646                    print "argh"
2647                return ""
2648            log = extractLogMessageFromGitCommit(next)
2649            settings = extractSettingsGitLog(log)
2650            currentChange = int(settings['change'])
2651            if self.verbose:
2652                print "current change %s" % currentChange
2653
2654            if currentChange == change:
2655                if self.verbose:
2656                    print "found %s" % next
2657                return next
2658
2659            if currentChange < change:
2660                earliestCommit = "^%s" % next
2661            else:
2662                latestCommit = "%s" % next
2663
2664        return ""
2665
2666    def importNewBranch(self, branch, maxChange):
2667        # make fast-import flush all changes to disk and update the refs using the checkpoint
2668        # command so that we can try to find the branch parent in the git history
2669        self.gitStream.write("checkpoint\n\n");
2670        self.gitStream.flush();
2671        branchPrefix = self.depotPaths[0] + branch + "/"
2672        range = "@1,%s" % maxChange
2673        #print "prefix" + branchPrefix
2674        changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2675        if len(changes) <= 0:
2676            return False
2677        firstChange = changes[0]
2678        #print "first change in branch: %s" % firstChange
2679        sourceBranch = self.knownBranches[branch]
2680        sourceDepotPath = self.depotPaths[0] + sourceBranch
2681        sourceRef = self.gitRefForBranch(sourceBranch)
2682        #print "source " + sourceBranch
2683
2684        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2685        #print "branch parent: %s" % branchParentChange
2686        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2687        if len(gitParent) > 0:
2688            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2689            #print "parent git commit: %s" % gitParent
2690
2691        self.importChanges(changes)
2692        return True
2693
2694    def searchParent(self, parent, branch, target):
2695        parentFound = False
2696        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2697                                     "--no-merges", parent]):
2698            blob = blob.strip()
2699            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2700                parentFound = True
2701                if self.verbose:
2702                    print "Found parent of %s in commit %s" % (branch, blob)
2703                break
2704        if parentFound:
2705            return blob
2706        else:
2707            return None
2708
2709    def importChanges(self, changes):
2710        cnt = 1
2711        for change in changes:
2712            description = p4_describe(change)
2713            self.updateOptionDict(description)
2714
2715            if not self.silent:
2716                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2717                sys.stdout.flush()
2718            cnt = cnt + 1
2719
2720            try:
2721                if self.detectBranches:
2722                    branches = self.splitFilesIntoBranches(description)
2723                    for branch in branches.keys():
2724                        ## HACK  --hwn
2725                        branchPrefix = self.depotPaths[0] + branch + "/"
2726                        self.branchPrefixes = [ branchPrefix ]
2727
2728                        parent = ""
2729
2730                        filesForCommit = branches[branch]
2731
2732                        if self.verbose:
2733                            print "branch is %s" % branch
2734
2735                        self.updatedBranches.add(branch)
2736
2737                        if branch not in self.createdBranches:
2738                            self.createdBranches.add(branch)
2739                            parent = self.knownBranches[branch]
2740                            if parent == branch:
2741                                parent = ""
2742                            else:
2743                                fullBranch = self.projectName + branch
2744                                if fullBranch not in self.p4BranchesInGit:
2745                                    if not self.silent:
2746                                        print("\n    Importing new branch %s" % fullBranch);
2747                                    if self.importNewBranch(branch, change - 1):
2748                                        parent = ""
2749                                        self.p4BranchesInGit.append(fullBranch)
2750                                    if not self.silent:
2751                                        print("\n    Resuming with change %s" % change);
2752
2753                                if self.verbose:
2754                                    print "parent determined through known branches: %s" % parent
2755
2756                        branch = self.gitRefForBranch(branch)
2757                        parent = self.gitRefForBranch(parent)
2758
2759                        if self.verbose:
2760                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2761
2762                        if len(parent) == 0 and branch in self.initialParents:
2763                            parent = self.initialParents[branch]
2764                            del self.initialParents[branch]
2765
2766                        blob = None
2767                        if len(parent) > 0:
2768                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2769                            if self.verbose:
2770                                print "Creating temporary branch: " + tempBranch
2771                            self.commit(description, filesForCommit, tempBranch)
2772                            self.tempBranches.append(tempBranch)
2773                            self.checkpoint()
2774                            blob = self.searchParent(parent, branch, tempBranch)
2775                        if blob:
2776                            self.commit(description, filesForCommit, branch, blob)
2777                        else:
2778                            if self.verbose:
2779                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2780                            self.commit(description, filesForCommit, branch, parent)
2781                else:
2782                    files = self.extractFilesFromCommit(description)
2783                    self.commit(description, files, self.branch,
2784                                self.initialParent)
2785                    # only needed once, to connect to the previous commit
2786                    self.initialParent = ""
2787            except IOError:
2788                print self.gitError.read()
2789                sys.exit(1)
2790
2791    def importHeadRevision(self, revision):
2792        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2793
2794        details = {}
2795        details["user"] = "git perforce import user"
2796        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2797                           % (' '.join(self.depotPaths), revision))
2798        details["change"] = revision
2799        newestRevision = 0
2800
2801        fileCnt = 0
2802        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2803
2804        for info in p4CmdList(["files"] + fileArgs):
2805
2806            if 'code' in info and info['code'] == 'error':
2807                sys.stderr.write("p4 returned an error: %s\n"
2808                                 % info['data'])
2809                if info['data'].find("must refer to client") >= 0:
2810                    sys.stderr.write("This particular p4 error is misleading.\n")
2811                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2812                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2813                sys.exit(1)
2814            if 'p4ExitCode' in info:
2815                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2816                sys.exit(1)
2817
2818
2819            change = int(info["change"])
2820            if change > newestRevision:
2821                newestRevision = change
2822
2823            if info["action"] in self.delete_actions:
2824                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2825                #fileCnt = fileCnt + 1
2826                continue
2827
2828            for prop in ["depotFile", "rev", "action", "type" ]:
2829                details["%s%s" % (prop, fileCnt)] = info[prop]
2830
2831            fileCnt = fileCnt + 1
2832
2833        details["change"] = newestRevision
2834
2835        # Use time from top-most change so that all git p4 clones of
2836        # the same p4 repo have the same commit SHA1s.
2837        res = p4_describe(newestRevision)
2838        details["time"] = res["time"]
2839
2840        self.updateOptionDict(details)
2841        try:
2842            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2843        except IOError:
2844            print "IO error with git fast-import. Is your git version recent enough?"
2845            print self.gitError.read()
2846
2847
2848    def run(self, args):
2849        self.depotPaths = []
2850        self.changeRange = ""
2851        self.previousDepotPaths = []
2852        self.hasOrigin = False
2853
2854        # map from branch depot path to parent branch
2855        self.knownBranches = {}
2856        self.initialParents = {}
2857
2858        if self.importIntoRemotes:
2859            self.refPrefix = "refs/remotes/p4/"
2860        else:
2861            self.refPrefix = "refs/heads/p4/"
2862
2863        if self.syncWithOrigin:
2864            self.hasOrigin = originP4BranchesExist()
2865            if self.hasOrigin:
2866                if not self.silent:
2867                    print 'Syncing with origin first, using "git fetch origin"'
2868                system("git fetch origin")
2869
2870        branch_arg_given = bool(self.branch)
2871        if len(self.branch) == 0:
2872            self.branch = self.refPrefix + "master"
2873            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2874                system("git update-ref %s refs/heads/p4" % self.branch)
2875                system("git branch -D p4")
2876
2877        # accept either the command-line option, or the configuration variable
2878        if self.useClientSpec:
2879            # will use this after clone to set the variable
2880            self.useClientSpec_from_options = True
2881        else:
2882            if gitConfigBool("git-p4.useclientspec"):
2883                self.useClientSpec = True
2884        if self.useClientSpec:
2885            self.clientSpecDirs = getClientSpec()
2886
2887        # TODO: should always look at previous commits,
2888        # merge with previous imports, if possible.
2889        if args == []:
2890            if self.hasOrigin:
2891                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2892
2893            # branches holds mapping from branch name to sha1
2894            branches = p4BranchesInGit(self.importIntoRemotes)
2895
2896            # restrict to just this one, disabling detect-branches
2897            if branch_arg_given:
2898                short = self.branch.split("/")[-1]
2899                if short in branches:
2900                    self.p4BranchesInGit = [ short ]
2901            else:
2902                self.p4BranchesInGit = branches.keys()
2903
2904            if len(self.p4BranchesInGit) > 1:
2905                if not self.silent:
2906                    print "Importing from/into multiple branches"
2907                self.detectBranches = True
2908                for branch in branches.keys():
2909                    self.initialParents[self.refPrefix + branch] = \
2910                        branches[branch]
2911
2912            if self.verbose:
2913                print "branches: %s" % self.p4BranchesInGit
2914
2915            p4Change = 0
2916            for branch in self.p4BranchesInGit:
2917                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2918
2919                settings = extractSettingsGitLog(logMsg)
2920
2921                self.readOptions(settings)
2922                if (settings.has_key('depot-paths')
2923                    and settings.has_key ('change')):
2924                    change = int(settings['change']) + 1
2925                    p4Change = max(p4Change, change)
2926
2927                    depotPaths = sorted(settings['depot-paths'])
2928                    if self.previousDepotPaths == []:
2929                        self.previousDepotPaths = depotPaths
2930                    else:
2931                        paths = []
2932                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2933                            prev_list = prev.split("/")
2934                            cur_list = cur.split("/")
2935                            for i in range(0, min(len(cur_list), len(prev_list))):
2936                                if cur_list[i] <> prev_list[i]:
2937                                    i = i - 1
2938                                    break
2939
2940                            paths.append ("/".join(cur_list[:i + 1]))
2941
2942                        self.previousDepotPaths = paths
2943
2944            if p4Change > 0:
2945                self.depotPaths = sorted(self.previousDepotPaths)
2946                self.changeRange = "@%s,#head" % p4Change
2947                if not self.silent and not self.detectBranches:
2948                    print "Performing incremental import into %s git branch" % self.branch
2949
2950        # accept multiple ref name abbreviations:
2951        #    refs/foo/bar/branch -> use it exactly
2952        #    p4/branch -> prepend refs/remotes/ or refs/heads/
2953        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2954        if not self.branch.startswith("refs/"):
2955            if self.importIntoRemotes:
2956                prepend = "refs/remotes/"
2957            else:
2958                prepend = "refs/heads/"
2959            if not self.branch.startswith("p4/"):
2960                prepend += "p4/"
2961            self.branch = prepend + self.branch
2962
2963        if len(args) == 0 and self.depotPaths:
2964            if not self.silent:
2965                print "Depot paths: %s" % ' '.join(self.depotPaths)
2966        else:
2967            if self.depotPaths and self.depotPaths != args:
2968                print ("previous import used depot path %s and now %s was specified. "
2969                       "This doesn't work!" % (' '.join (self.depotPaths),
2970                                               ' '.join (args)))
2971                sys.exit(1)
2972
2973            self.depotPaths = sorted(args)
2974
2975        revision = ""
2976        self.users = {}
2977
2978        # Make sure no revision specifiers are used when --changesfile
2979        # is specified.
2980        bad_changesfile = False
2981        if len(self.changesFile) > 0:
2982            for p in self.depotPaths:
2983                if p.find("@") >= 0 or p.find("#") >= 0:
2984                    bad_changesfile = True
2985                    break
2986        if bad_changesfile:
2987            die("Option --changesfile is incompatible with revision specifiers")
2988
2989        newPaths = []
2990        for p in self.depotPaths:
2991            if p.find("@") != -1:
2992                atIdx = p.index("@")
2993                self.changeRange = p[atIdx:]
2994                if self.changeRange == "@all":
2995                    self.changeRange = ""
2996                elif ',' not in self.changeRange:
2997                    revision = self.changeRange
2998                    self.changeRange = ""
2999                p = p[:atIdx]
3000            elif p.find("#") != -1:
3001                hashIdx = p.index("#")
3002                revision = p[hashIdx:]
3003                p = p[:hashIdx]
3004            elif self.previousDepotPaths == []:
3005                # pay attention to changesfile, if given, else import
3006                # the entire p4 tree at the head revision
3007                if len(self.changesFile) == 0:
3008                    revision = "#head"
3009
3010            p = re.sub ("\.\.\.$", "", p)
3011            if not p.endswith("/"):
3012                p += "/"
3013
3014            newPaths.append(p)
3015
3016        self.depotPaths = newPaths
3017
3018        # --detect-branches may change this for each branch
3019        self.branchPrefixes = self.depotPaths
3020
3021        self.loadUserMapFromCache()
3022        self.labels = {}
3023        if self.detectLabels:
3024            self.getLabels();
3025
3026        if self.detectBranches:
3027            ## FIXME - what's a P4 projectName ?
3028            self.projectName = self.guessProjectName()
3029
3030            if self.hasOrigin:
3031                self.getBranchMappingFromGitBranches()
3032            else:
3033                self.getBranchMapping()
3034            if self.verbose:
3035                print "p4-git branches: %s" % self.p4BranchesInGit
3036                print "initial parents: %s" % self.initialParents
3037            for b in self.p4BranchesInGit:
3038                if b != "master":
3039
3040                    ## FIXME
3041                    b = b[len(self.projectName):]
3042                self.createdBranches.add(b)
3043
3044        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3045
3046        self.importProcess = subprocess.Popen(["git", "fast-import"],
3047                                              stdin=subprocess.PIPE,
3048                                              stdout=subprocess.PIPE,
3049                                              stderr=subprocess.PIPE);
3050        self.gitOutput = self.importProcess.stdout
3051        self.gitStream = self.importProcess.stdin
3052        self.gitError = self.importProcess.stderr
3053
3054        if revision:
3055            self.importHeadRevision(revision)
3056        else:
3057            changes = []
3058
3059            if len(self.changesFile) > 0:
3060                output = open(self.changesFile).readlines()
3061                changeSet = set()
3062                for line in output:
3063                    changeSet.add(int(line))
3064
3065                for change in changeSet:
3066                    changes.append(change)
3067
3068                changes.sort()
3069            else:
3070                # catch "git p4 sync" with no new branches, in a repo that
3071                # does not have any existing p4 branches
3072                if len(args) == 0:
3073                    if not self.p4BranchesInGit:
3074                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3075
3076                    # The default branch is master, unless --branch is used to
3077                    # specify something else.  Make sure it exists, or complain
3078                    # nicely about how to use --branch.
3079                    if not self.detectBranches:
3080                        if not branch_exists(self.branch):
3081                            if branch_arg_given:
3082                                die("Error: branch %s does not exist." % self.branch)
3083                            else:
3084                                die("Error: no branch %s; perhaps specify one with --branch." %
3085                                    self.branch)
3086
3087                if self.verbose:
3088                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3089                                                              self.changeRange)
3090                changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3091
3092                if len(self.maxChanges) > 0:
3093                    changes = changes[:min(int(self.maxChanges), len(changes))]
3094
3095            if len(changes) == 0:
3096                if not self.silent:
3097                    print "No changes to import!"
3098            else:
3099                if not self.silent and not self.detectBranches:
3100                    print "Import destination: %s" % self.branch
3101
3102                self.updatedBranches = set()
3103
3104                if not self.detectBranches:
3105                    if args:
3106                        # start a new branch
3107                        self.initialParent = ""
3108                    else:
3109                        # build on a previous revision
3110                        self.initialParent = parseRevision(self.branch)
3111
3112                self.importChanges(changes)
3113
3114                if not self.silent:
3115                    print ""
3116                    if len(self.updatedBranches) > 0:
3117                        sys.stdout.write("Updated branches: ")
3118                        for b in self.updatedBranches:
3119                            sys.stdout.write("%s " % b)
3120                        sys.stdout.write("\n")
3121
3122        if gitConfigBool("git-p4.importLabels"):
3123            self.importLabels = True
3124
3125        if self.importLabels:
3126            p4Labels = getP4Labels(self.depotPaths)
3127            gitTags = getGitTags()
3128
3129            missingP4Labels = p4Labels - gitTags
3130            self.importP4Labels(self.gitStream, missingP4Labels)
3131
3132        self.gitStream.close()
3133        if self.importProcess.wait() != 0:
3134            die("fast-import failed: %s" % self.gitError.read())
3135        self.gitOutput.close()
3136        self.gitError.close()
3137
3138        # Cleanup temporary branches created during import
3139        if self.tempBranches != []:
3140            for branch in self.tempBranches:
3141                read_pipe("git update-ref -d %s" % branch)
3142            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3143
3144        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3145        # a convenient shortcut refname "p4".
3146        if self.importIntoRemotes:
3147            head_ref = self.refPrefix + "HEAD"
3148            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3149                system(["git", "symbolic-ref", head_ref, self.branch])
3150
3151        return True
3152
3153class P4Rebase(Command):
3154    def __init__(self):
3155        Command.__init__(self)
3156        self.options = [
3157                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3158        ]
3159        self.importLabels = False
3160        self.description = ("Fetches the latest revision from perforce and "
3161                            + "rebases the current work (branch) against it")
3162
3163    def run(self, args):
3164        sync = P4Sync()
3165        sync.importLabels = self.importLabels
3166        sync.run([])
3167
3168        return self.rebase()
3169
3170    def rebase(self):
3171        if os.system("git update-index --refresh") != 0:
3172            die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up-to-date or stash away all your changes with git stash.");
3173        if len(read_pipe("git diff-index HEAD --")) > 0:
3174            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3175
3176        [upstream, settings] = findUpstreamBranchPoint()
3177        if len(upstream) == 0:
3178            die("Cannot find upstream branchpoint for rebase")
3179
3180        # the branchpoint may be p4/foo~3, so strip off the parent
3181        upstream = re.sub("~[0-9]+$", "", upstream)
3182
3183        print "Rebasing the current branch onto %s" % upstream
3184        oldHead = read_pipe("git rev-parse HEAD").strip()
3185        system("git rebase %s" % upstream)
3186        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3187        return True
3188
3189class P4Clone(P4Sync):
3190    def __init__(self):
3191        P4Sync.__init__(self)
3192        self.description = "Creates a new git repository and imports from Perforce into it"
3193        self.usage = "usage: %prog [options] //depot/path[@revRange]"
3194        self.options += [
3195            optparse.make_option("--destination", dest="cloneDestination",
3196                                 action='store', default=None,
3197                                 help="where to leave result of the clone"),
3198            optparse.make_option("--bare", dest="cloneBare",
3199                                 action="store_true", default=False),
3200        ]
3201        self.cloneDestination = None
3202        self.needsGit = False
3203        self.cloneBare = False
3204
3205    def defaultDestination(self, args):
3206        ## TODO: use common prefix of args?
3207        depotPath = args[0]
3208        depotDir = re.sub("(@[^@]*)$", "", depotPath)
3209        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3210        depotDir = re.sub(r"\.\.\.$", "", depotDir)
3211        depotDir = re.sub(r"/$", "", depotDir)
3212        return os.path.split(depotDir)[1]
3213
3214    def run(self, args):
3215        if len(args) < 1:
3216            return False
3217
3218        if self.keepRepoPath and not self.cloneDestination:
3219            sys.stderr.write("Must specify destination for --keep-path\n")
3220            sys.exit(1)
3221
3222        depotPaths = args
3223
3224        if not self.cloneDestination and len(depotPaths) > 1:
3225            self.cloneDestination = depotPaths[-1]
3226            depotPaths = depotPaths[:-1]
3227
3228        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3229        for p in depotPaths:
3230            if not p.startswith("//"):
3231                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3232                return False
3233
3234        if not self.cloneDestination:
3235            self.cloneDestination = self.defaultDestination(args)
3236
3237        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3238
3239        if not os.path.exists(self.cloneDestination):
3240            os.makedirs(self.cloneDestination)
3241        chdir(self.cloneDestination)
3242
3243        init_cmd = [ "git", "init" ]
3244        if self.cloneBare:
3245            init_cmd.append("--bare")
3246        retcode = subprocess.call(init_cmd)
3247        if retcode:
3248            raise CalledProcessError(retcode, init_cmd)
3249
3250        if not P4Sync.run(self, depotPaths):
3251            return False
3252
3253        # create a master branch and check out a work tree
3254        if gitBranchExists(self.branch):
3255            system([ "git", "branch", "master", self.branch ])
3256            if not self.cloneBare:
3257                system([ "git", "checkout", "-f" ])
3258        else:
3259            print 'Not checking out any branch, use ' \
3260                  '"git checkout -q -b master <branch>"'
3261
3262        # auto-set this variable if invoked with --use-client-spec
3263        if self.useClientSpec_from_options:
3264            system("git config --bool git-p4.useclientspec true")
3265
3266        return True
3267
3268class P4Branches(Command):
3269    def __init__(self):
3270        Command.__init__(self)
3271        self.options = [ ]
3272        self.description = ("Shows the git branches that hold imports and their "
3273                            + "corresponding perforce depot paths")
3274        self.verbose = False
3275
3276    def run(self, args):
3277        if originP4BranchesExist():
3278            createOrUpdateBranchesFromOrigin()
3279
3280        cmdline = "git rev-parse --symbolic "
3281        cmdline += " --remotes"
3282
3283        for line in read_pipe_lines(cmdline):
3284            line = line.strip()
3285
3286            if not line.startswith('p4/') or line == "p4/HEAD":
3287                continue
3288            branch = line
3289
3290            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3291            settings = extractSettingsGitLog(log)
3292
3293            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3294        return True
3295
3296class HelpFormatter(optparse.IndentedHelpFormatter):
3297    def __init__(self):
3298        optparse.IndentedHelpFormatter.__init__(self)
3299
3300    def format_description(self, description):
3301        if description:
3302            return description + "\n"
3303        else:
3304            return ""
3305
3306def printUsage(commands):
3307    print "usage: %s <command> [options]" % sys.argv[0]
3308    print ""
3309    print "valid commands: %s" % ", ".join(commands)
3310    print ""
3311    print "Try %s <command> --help for command specific help." % sys.argv[0]
3312    print ""
3313
3314commands = {
3315    "debug" : P4Debug,
3316    "submit" : P4Submit,
3317    "commit" : P4Submit,
3318    "sync" : P4Sync,
3319    "rebase" : P4Rebase,
3320    "clone" : P4Clone,
3321    "rollback" : P4RollBack,
3322    "branches" : P4Branches
3323}
3324
3325
3326def main():
3327    if len(sys.argv[1:]) == 0:
3328        printUsage(commands.keys())
3329        sys.exit(2)
3330
3331    cmdName = sys.argv[1]
3332    try:
3333        klass = commands[cmdName]
3334        cmd = klass()
3335    except KeyError:
3336        print "unknown command %s" % cmdName
3337        print ""
3338        printUsage(commands.keys())
3339        sys.exit(2)
3340
3341    options = cmd.options
3342    cmd.gitdir = os.environ.get("GIT_DIR", None)
3343
3344    args = sys.argv[2:]
3345
3346    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3347    if cmd.needsGit:
3348        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3349
3350    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3351                                   options,
3352                                   description = cmd.description,
3353                                   formatter = HelpFormatter())
3354
3355    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3356    global verbose
3357    verbose = cmd.verbose
3358    if cmd.needsGit:
3359        if cmd.gitdir == None:
3360            cmd.gitdir = os.path.abspath(".git")
3361            if not isValidGitDir(cmd.gitdir):
3362                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3363                if os.path.exists(cmd.gitdir):
3364                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3365                    if len(cdup) > 0:
3366                        chdir(cdup);
3367
3368        if not isValidGitDir(cmd.gitdir):
3369            if isValidGitDir(cmd.gitdir + "/.git"):
3370                cmd.gitdir += "/.git"
3371            else:
3372                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3373
3374        os.environ["GIT_DIR"] = cmd.gitdir
3375
3376    if not cmd.run(args):
3377        parser.print_help()
3378        sys.exit(2)
3379
3380
3381if __name__ == '__main__':
3382    main()