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