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