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