git-p4.pyon commit git p4: fix submit when no master branch (44e8d26)
   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                optparse.make_option("--branch", dest="branch"),
 926        ]
 927        self.description = "Submit changes from git to the perforce depot."
 928        self.usage += " [name of git branch to submit into perforce depot]"
 929        self.origin = ""
 930        self.detectRenames = False
 931        self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
 932        self.dry_run = False
 933        self.prepare_p4_only = False
 934        self.conflict_behavior = None
 935        self.isWindows = (platform.system() == "Windows")
 936        self.exportLabels = False
 937        self.p4HasMoveCommand = p4_has_move_command()
 938        self.branch = None
 939
 940    def check(self):
 941        if len(p4CmdList("opened ...")) > 0:
 942            die("You have files opened with perforce! Close them before starting the sync.")
 943
 944    def separate_jobs_from_description(self, message):
 945        """Extract and return a possible Jobs field in the commit
 946           message.  It goes into a separate section in the p4 change
 947           specification.
 948
 949           A jobs line starts with "Jobs:" and looks like a new field
 950           in a form.  Values are white-space separated on the same
 951           line or on following lines that start with a tab.
 952
 953           This does not parse and extract the full git commit message
 954           like a p4 form.  It just sees the Jobs: line as a marker
 955           to pass everything from then on directly into the p4 form,
 956           but outside the description section.
 957
 958           Return a tuple (stripped log message, jobs string)."""
 959
 960        m = re.search(r'^Jobs:', message, re.MULTILINE)
 961        if m is None:
 962            return (message, None)
 963
 964        jobtext = message[m.start():]
 965        stripped_message = message[:m.start()].rstrip()
 966        return (stripped_message, jobtext)
 967
 968    def prepareLogMessage(self, template, message, jobs):
 969        """Edits the template returned from "p4 change -o" to insert
 970           the message in the Description field, and the jobs text in
 971           the Jobs field."""
 972        result = ""
 973
 974        inDescriptionSection = False
 975
 976        for line in template.split("\n"):
 977            if line.startswith("#"):
 978                result += line + "\n"
 979                continue
 980
 981            if inDescriptionSection:
 982                if line.startswith("Files:") or line.startswith("Jobs:"):
 983                    inDescriptionSection = False
 984                    # insert Jobs section
 985                    if jobs:
 986                        result += jobs + "\n"
 987                else:
 988                    continue
 989            else:
 990                if line.startswith("Description:"):
 991                    inDescriptionSection = True
 992                    line += "\n"
 993                    for messageLine in message.split("\n"):
 994                        line += "\t" + messageLine + "\n"
 995
 996            result += line + "\n"
 997
 998        return result
 999
1000    def patchRCSKeywords(self, file, pattern):
1001        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1002        (handle, outFileName) = tempfile.mkstemp(dir='.')
1003        try:
1004            outFile = os.fdopen(handle, "w+")
1005            inFile = open(file, "r")
1006            regexp = re.compile(pattern, re.VERBOSE)
1007            for line in inFile.readlines():
1008                line = regexp.sub(r'$\1$', line)
1009                outFile.write(line)
1010            inFile.close()
1011            outFile.close()
1012            # Forcibly overwrite the original file
1013            os.unlink(file)
1014            shutil.move(outFileName, file)
1015        except:
1016            # cleanup our temporary file
1017            os.unlink(outFileName)
1018            print "Failed to strip RCS keywords in %s" % file
1019            raise
1020
1021        print "Patched up RCS keywords in %s" % file
1022
1023    def p4UserForCommit(self,id):
1024        # Return the tuple (perforce user,git email) for a given git commit id
1025        self.getUserMapFromPerforceServer()
1026        gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
1027        gitEmail = gitEmail.strip()
1028        if not self.emails.has_key(gitEmail):
1029            return (None,gitEmail)
1030        else:
1031            return (self.emails[gitEmail],gitEmail)
1032
1033    def checkValidP4Users(self,commits):
1034        # check if any git authors cannot be mapped to p4 users
1035        for id in commits:
1036            (user,email) = self.p4UserForCommit(id)
1037            if not user:
1038                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1039                if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
1040                    print "%s" % msg
1041                else:
1042                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1043
1044    def lastP4Changelist(self):
1045        # Get back the last changelist number submitted in this client spec. This
1046        # then gets used to patch up the username in the change. If the same
1047        # client spec is being used by multiple processes then this might go
1048        # wrong.
1049        results = p4CmdList("client -o")        # find the current client
1050        client = None
1051        for r in results:
1052            if r.has_key('Client'):
1053                client = r['Client']
1054                break
1055        if not client:
1056            die("could not get client spec")
1057        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1058        for r in results:
1059            if r.has_key('change'):
1060                return r['change']
1061        die("Could not get changelist number for last submit - cannot patch up user details")
1062
1063    def modifyChangelistUser(self, changelist, newUser):
1064        # fixup the user field of a changelist after it has been submitted.
1065        changes = p4CmdList("change -o %s" % changelist)
1066        if len(changes) != 1:
1067            die("Bad output from p4 change modifying %s to user %s" %
1068                (changelist, newUser))
1069
1070        c = changes[0]
1071        if c['User'] == newUser: return   # nothing to do
1072        c['User'] = newUser
1073        input = marshal.dumps(c)
1074
1075        result = p4CmdList("change -f -i", stdin=input)
1076        for r in result:
1077            if r.has_key('code'):
1078                if r['code'] == 'error':
1079                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1080            if r.has_key('data'):
1081                print("Updated user field for changelist %s to %s" % (changelist, newUser))
1082                return
1083        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1084
1085    def canChangeChangelists(self):
1086        # check to see if we have p4 admin or super-user permissions, either of
1087        # which are required to modify changelists.
1088        results = p4CmdList(["protects", self.depotPath])
1089        for r in results:
1090            if r.has_key('perm'):
1091                if r['perm'] == 'admin':
1092                    return 1
1093                if r['perm'] == 'super':
1094                    return 1
1095        return 0
1096
1097    def prepareSubmitTemplate(self):
1098        """Run "p4 change -o" to grab a change specification template.
1099           This does not use "p4 -G", as it is nice to keep the submission
1100           template in original order, since a human might edit it.
1101
1102           Remove lines in the Files section that show changes to files
1103           outside the depot path we're committing into."""
1104
1105        template = ""
1106        inFilesSection = False
1107        for line in p4_read_pipe_lines(['change', '-o']):
1108            if line.endswith("\r\n"):
1109                line = line[:-2] + "\n"
1110            if inFilesSection:
1111                if line.startswith("\t"):
1112                    # path starts and ends with a tab
1113                    path = line[1:]
1114                    lastTab = path.rfind("\t")
1115                    if lastTab != -1:
1116                        path = path[:lastTab]
1117                        if not p4PathStartsWith(path, self.depotPath):
1118                            continue
1119                else:
1120                    inFilesSection = False
1121            else:
1122                if line.startswith("Files:"):
1123                    inFilesSection = True
1124
1125            template += line
1126
1127        return template
1128
1129    def edit_template(self, template_file):
1130        """Invoke the editor to let the user change the submission
1131           message.  Return true if okay to continue with the submit."""
1132
1133        # if configured to skip the editing part, just submit
1134        if gitConfig("git-p4.skipSubmitEdit") == "true":
1135            return True
1136
1137        # look at the modification time, to check later if the user saved
1138        # the file
1139        mtime = os.stat(template_file).st_mtime
1140
1141        # invoke the editor
1142        if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1143            editor = os.environ.get("P4EDITOR")
1144        else:
1145            editor = read_pipe("git var GIT_EDITOR").strip()
1146        system(editor + " " + template_file)
1147
1148        # If the file was not saved, prompt to see if this patch should
1149        # be skipped.  But skip this verification step if configured so.
1150        if gitConfig("git-p4.skipSubmitEditCheck") == "true":
1151            return True
1152
1153        # modification time updated means user saved the file
1154        if os.stat(template_file).st_mtime > mtime:
1155            return True
1156
1157        while True:
1158            response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1159            if response == 'y':
1160                return True
1161            if response == 'n':
1162                return False
1163
1164    def applyCommit(self, id):
1165        """Apply one commit, return True if it succeeded."""
1166
1167        print "Applying", read_pipe(["git", "show", "-s",
1168                                     "--format=format:%h %s", id])
1169
1170        (p4User, gitEmail) = self.p4UserForCommit(id)
1171
1172        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1173        filesToAdd = set()
1174        filesToDelete = set()
1175        editedFiles = set()
1176        pureRenameCopy = set()
1177        filesToChangeExecBit = {}
1178
1179        for line in diff:
1180            diff = parseDiffTreeEntry(line)
1181            modifier = diff['status']
1182            path = diff['src']
1183            if modifier == "M":
1184                p4_edit(path)
1185                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1186                    filesToChangeExecBit[path] = diff['dst_mode']
1187                editedFiles.add(path)
1188            elif modifier == "A":
1189                filesToAdd.add(path)
1190                filesToChangeExecBit[path] = diff['dst_mode']
1191                if path in filesToDelete:
1192                    filesToDelete.remove(path)
1193            elif modifier == "D":
1194                filesToDelete.add(path)
1195                if path in filesToAdd:
1196                    filesToAdd.remove(path)
1197            elif modifier == "C":
1198                src, dest = diff['src'], diff['dst']
1199                p4_integrate(src, dest)
1200                pureRenameCopy.add(dest)
1201                if diff['src_sha1'] != diff['dst_sha1']:
1202                    p4_edit(dest)
1203                    pureRenameCopy.discard(dest)
1204                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1205                    p4_edit(dest)
1206                    pureRenameCopy.discard(dest)
1207                    filesToChangeExecBit[dest] = diff['dst_mode']
1208                os.unlink(dest)
1209                editedFiles.add(dest)
1210            elif modifier == "R":
1211                src, dest = diff['src'], diff['dst']
1212                if self.p4HasMoveCommand:
1213                    p4_edit(src)        # src must be open before move
1214                    p4_move(src, dest)  # opens for (move/delete, move/add)
1215                else:
1216                    p4_integrate(src, dest)
1217                    if diff['src_sha1'] != diff['dst_sha1']:
1218                        p4_edit(dest)
1219                    else:
1220                        pureRenameCopy.add(dest)
1221                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1222                    if not self.p4HasMoveCommand:
1223                        p4_edit(dest)   # with move: already open, writable
1224                    filesToChangeExecBit[dest] = diff['dst_mode']
1225                if not self.p4HasMoveCommand:
1226                    os.unlink(dest)
1227                    filesToDelete.add(src)
1228                editedFiles.add(dest)
1229            else:
1230                die("unknown modifier %s for %s" % (modifier, path))
1231
1232        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1233        patchcmd = diffcmd + " | git apply "
1234        tryPatchCmd = patchcmd + "--check -"
1235        applyPatchCmd = patchcmd + "--check --apply -"
1236        patch_succeeded = True
1237
1238        if os.system(tryPatchCmd) != 0:
1239            fixed_rcs_keywords = False
1240            patch_succeeded = False
1241            print "Unfortunately applying the change failed!"
1242
1243            # Patch failed, maybe it's just RCS keyword woes. Look through
1244            # the patch to see if that's possible.
1245            if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1246                file = None
1247                pattern = None
1248                kwfiles = {}
1249                for file in editedFiles | filesToDelete:
1250                    # did this file's delta contain RCS keywords?
1251                    pattern = p4_keywords_regexp_for_file(file)
1252
1253                    if pattern:
1254                        # this file is a possibility...look for RCS keywords.
1255                        regexp = re.compile(pattern, re.VERBOSE)
1256                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1257                            if regexp.search(line):
1258                                if verbose:
1259                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1260                                kwfiles[file] = pattern
1261                                break
1262
1263                for file in kwfiles:
1264                    if verbose:
1265                        print "zapping %s with %s" % (line,pattern)
1266                    self.patchRCSKeywords(file, kwfiles[file])
1267                    fixed_rcs_keywords = True
1268
1269            if fixed_rcs_keywords:
1270                print "Retrying the patch with RCS keywords cleaned up"
1271                if os.system(tryPatchCmd) == 0:
1272                    patch_succeeded = True
1273
1274        if not patch_succeeded:
1275            for f in editedFiles:
1276                p4_revert(f)
1277            return False
1278
1279        #
1280        # Apply the patch for real, and do add/delete/+x handling.
1281        #
1282        system(applyPatchCmd)
1283
1284        for f in filesToAdd:
1285            p4_add(f)
1286        for f in filesToDelete:
1287            p4_revert(f)
1288            p4_delete(f)
1289
1290        # Set/clear executable bits
1291        for f in filesToChangeExecBit.keys():
1292            mode = filesToChangeExecBit[f]
1293            setP4ExecBit(f, mode)
1294
1295        #
1296        # Build p4 change description, starting with the contents
1297        # of the git commit message.
1298        #
1299        logMessage = extractLogMessageFromGitCommit(id)
1300        logMessage = logMessage.strip()
1301        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1302
1303        template = self.prepareSubmitTemplate()
1304        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1305
1306        if self.preserveUser:
1307           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1308
1309        if self.checkAuthorship and not self.p4UserIsMe(p4User):
1310            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1311            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1312            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1313
1314        separatorLine = "######## everything below this line is just the diff #######\n"
1315
1316        # diff
1317        if os.environ.has_key("P4DIFF"):
1318            del(os.environ["P4DIFF"])
1319        diff = ""
1320        for editedFile in editedFiles:
1321            diff += p4_read_pipe(['diff', '-du',
1322                                  wildcard_encode(editedFile)])
1323
1324        # new file diff
1325        newdiff = ""
1326        for newFile in filesToAdd:
1327            newdiff += "==== new file ====\n"
1328            newdiff += "--- /dev/null\n"
1329            newdiff += "+++ %s\n" % newFile
1330            f = open(newFile, "r")
1331            for line in f.readlines():
1332                newdiff += "+" + line
1333            f.close()
1334
1335        # change description file: submitTemplate, separatorLine, diff, newdiff
1336        (handle, fileName) = tempfile.mkstemp()
1337        tmpFile = os.fdopen(handle, "w+")
1338        if self.isWindows:
1339            submitTemplate = submitTemplate.replace("\n", "\r\n")
1340            separatorLine = separatorLine.replace("\n", "\r\n")
1341            newdiff = newdiff.replace("\n", "\r\n")
1342        tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1343        tmpFile.close()
1344
1345        if self.prepare_p4_only:
1346            #
1347            # Leave the p4 tree prepared, and the submit template around
1348            # and let the user decide what to do next
1349            #
1350            print
1351            print "P4 workspace prepared for submission."
1352            print "To submit or revert, go to client workspace"
1353            print "  " + self.clientPath
1354            print
1355            print "To submit, use \"p4 submit\" to write a new description,"
1356            print "or \"p4 submit -i %s\" to use the one prepared by" \
1357                  " \"git p4\"." % fileName
1358            print "You can delete the file \"%s\" when finished." % fileName
1359
1360            if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1361                print "To preserve change ownership by user %s, you must\n" \
1362                      "do \"p4 change -f <change>\" after submitting and\n" \
1363                      "edit the User field."
1364            if pureRenameCopy:
1365                print "After submitting, renamed files must be re-synced."
1366                print "Invoke \"p4 sync -f\" on each of these files:"
1367                for f in pureRenameCopy:
1368                    print "  " + f
1369
1370            print
1371            print "To revert the changes, use \"p4 revert ...\", and delete"
1372            print "the submit template file \"%s\"" % fileName
1373            if filesToAdd:
1374                print "Since the commit adds new files, they must be deleted:"
1375                for f in filesToAdd:
1376                    print "  " + f
1377            print
1378            return True
1379
1380        #
1381        # Let the user edit the change description, then submit it.
1382        #
1383        if self.edit_template(fileName):
1384            # read the edited message and submit
1385            ret = True
1386            tmpFile = open(fileName, "rb")
1387            message = tmpFile.read()
1388            tmpFile.close()
1389            submitTemplate = message[:message.index(separatorLine)]
1390            if self.isWindows:
1391                submitTemplate = submitTemplate.replace("\r\n", "\n")
1392            p4_write_pipe(['submit', '-i'], submitTemplate)
1393
1394            if self.preserveUser:
1395                if p4User:
1396                    # Get last changelist number. Cannot easily get it from
1397                    # the submit command output as the output is
1398                    # unmarshalled.
1399                    changelist = self.lastP4Changelist()
1400                    self.modifyChangelistUser(changelist, p4User)
1401
1402            # The rename/copy happened by applying a patch that created a
1403            # new file.  This leaves it writable, which confuses p4.
1404            for f in pureRenameCopy:
1405                p4_sync(f, "-f")
1406
1407        else:
1408            # skip this patch
1409            ret = False
1410            print "Submission cancelled, undoing p4 changes."
1411            for f in editedFiles:
1412                p4_revert(f)
1413            for f in filesToAdd:
1414                p4_revert(f)
1415                os.remove(f)
1416            for f in filesToDelete:
1417                p4_revert(f)
1418
1419        os.remove(fileName)
1420        return ret
1421
1422    # Export git tags as p4 labels. Create a p4 label and then tag
1423    # with that.
1424    def exportGitTags(self, gitTags):
1425        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1426        if len(validLabelRegexp) == 0:
1427            validLabelRegexp = defaultLabelRegexp
1428        m = re.compile(validLabelRegexp)
1429
1430        for name in gitTags:
1431
1432            if not m.match(name):
1433                if verbose:
1434                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1435                continue
1436
1437            # Get the p4 commit this corresponds to
1438            logMessage = extractLogMessageFromGitCommit(name)
1439            values = extractSettingsGitLog(logMessage)
1440
1441            if not values.has_key('change'):
1442                # a tag pointing to something not sent to p4; ignore
1443                if verbose:
1444                    print "git tag %s does not give a p4 commit" % name
1445                continue
1446            else:
1447                changelist = values['change']
1448
1449            # Get the tag details.
1450            inHeader = True
1451            isAnnotated = False
1452            body = []
1453            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1454                l = l.strip()
1455                if inHeader:
1456                    if re.match(r'tag\s+', l):
1457                        isAnnotated = True
1458                    elif re.match(r'\s*$', l):
1459                        inHeader = False
1460                        continue
1461                else:
1462                    body.append(l)
1463
1464            if not isAnnotated:
1465                body = ["lightweight tag imported by git p4\n"]
1466
1467            # Create the label - use the same view as the client spec we are using
1468            clientSpec = getClientSpec()
1469
1470            labelTemplate  = "Label: %s\n" % name
1471            labelTemplate += "Description:\n"
1472            for b in body:
1473                labelTemplate += "\t" + b + "\n"
1474            labelTemplate += "View:\n"
1475            for mapping in clientSpec.mappings:
1476                labelTemplate += "\t%s\n" % mapping.depot_side.path
1477
1478            if self.dry_run:
1479                print "Would create p4 label %s for tag" % name
1480            elif self.prepare_p4_only:
1481                print "Not creating p4 label %s for tag due to option" \
1482                      " --prepare-p4-only" % name
1483            else:
1484                p4_write_pipe(["label", "-i"], labelTemplate)
1485
1486                # Use the label
1487                p4_system(["tag", "-l", name] +
1488                          ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1489
1490                if verbose:
1491                    print "created p4 label for tag %s" % name
1492
1493    def run(self, args):
1494        if len(args) == 0:
1495            self.master = currentGitBranch()
1496            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1497                die("Detecting current git branch failed!")
1498        elif len(args) == 1:
1499            self.master = args[0]
1500            if not branchExists(self.master):
1501                die("Branch %s does not exist" % self.master)
1502        else:
1503            return False
1504
1505        allowSubmit = gitConfig("git-p4.allowSubmit")
1506        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1507            die("%s is not in git-p4.allowSubmit" % self.master)
1508
1509        [upstream, settings] = findUpstreamBranchPoint()
1510        self.depotPath = settings['depot-paths'][0]
1511        if len(self.origin) == 0:
1512            self.origin = upstream
1513
1514        if self.preserveUser:
1515            if not self.canChangeChangelists():
1516                die("Cannot preserve user names without p4 super-user or admin permissions")
1517
1518        # if not set from the command line, try the config file
1519        if self.conflict_behavior is None:
1520            val = gitConfig("git-p4.conflict")
1521            if val:
1522                if val not in self.conflict_behavior_choices:
1523                    die("Invalid value '%s' for config git-p4.conflict" % val)
1524            else:
1525                val = "ask"
1526            self.conflict_behavior = val
1527
1528        if self.verbose:
1529            print "Origin branch is " + self.origin
1530
1531        if len(self.depotPath) == 0:
1532            print "Internal error: cannot locate perforce depot path from existing branches"
1533            sys.exit(128)
1534
1535        self.useClientSpec = False
1536        if gitConfig("git-p4.useclientspec", "--bool") == "true":
1537            self.useClientSpec = True
1538        if self.useClientSpec:
1539            self.clientSpecDirs = getClientSpec()
1540
1541        if self.useClientSpec:
1542            # all files are relative to the client spec
1543            self.clientPath = getClientRoot()
1544        else:
1545            self.clientPath = p4Where(self.depotPath)
1546
1547        if self.clientPath == "":
1548            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1549
1550        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1551        self.oldWorkingDirectory = os.getcwd()
1552
1553        # ensure the clientPath exists
1554        new_client_dir = False
1555        if not os.path.exists(self.clientPath):
1556            new_client_dir = True
1557            os.makedirs(self.clientPath)
1558
1559        chdir(self.clientPath)
1560        if self.dry_run:
1561            print "Would synchronize p4 checkout in %s" % self.clientPath
1562        else:
1563            print "Synchronizing p4 checkout..."
1564            if new_client_dir:
1565                # old one was destroyed, and maybe nobody told p4
1566                p4_sync("...", "-f")
1567            else:
1568                p4_sync("...")
1569        self.check()
1570
1571        commits = []
1572        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1573            commits.append(line.strip())
1574        commits.reverse()
1575
1576        if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1577            self.checkAuthorship = False
1578        else:
1579            self.checkAuthorship = True
1580
1581        if self.preserveUser:
1582            self.checkValidP4Users(commits)
1583
1584        #
1585        # Build up a set of options to be passed to diff when
1586        # submitting each commit to p4.
1587        #
1588        if self.detectRenames:
1589            # command-line -M arg
1590            self.diffOpts = "-M"
1591        else:
1592            # If not explicitly set check the config variable
1593            detectRenames = gitConfig("git-p4.detectRenames")
1594
1595            if detectRenames.lower() == "false" or detectRenames == "":
1596                self.diffOpts = ""
1597            elif detectRenames.lower() == "true":
1598                self.diffOpts = "-M"
1599            else:
1600                self.diffOpts = "-M%s" % detectRenames
1601
1602        # no command-line arg for -C or --find-copies-harder, just
1603        # config variables
1604        detectCopies = gitConfig("git-p4.detectCopies")
1605        if detectCopies.lower() == "false" or detectCopies == "":
1606            pass
1607        elif detectCopies.lower() == "true":
1608            self.diffOpts += " -C"
1609        else:
1610            self.diffOpts += " -C%s" % detectCopies
1611
1612        if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1613            self.diffOpts += " --find-copies-harder"
1614
1615        #
1616        # Apply the commits, one at a time.  On failure, ask if should
1617        # continue to try the rest of the patches, or quit.
1618        #
1619        if self.dry_run:
1620            print "Would apply"
1621        applied = []
1622        last = len(commits) - 1
1623        for i, commit in enumerate(commits):
1624            if self.dry_run:
1625                print " ", read_pipe(["git", "show", "-s",
1626                                      "--format=format:%h %s", commit])
1627                ok = True
1628            else:
1629                ok = self.applyCommit(commit)
1630            if ok:
1631                applied.append(commit)
1632            else:
1633                if self.prepare_p4_only and i < last:
1634                    print "Processing only the first commit due to option" \
1635                          " --prepare-p4-only"
1636                    break
1637                if i < last:
1638                    quit = False
1639                    while True:
1640                        # prompt for what to do, or use the option/variable
1641                        if self.conflict_behavior == "ask":
1642                            print "What do you want to do?"
1643                            response = raw_input("[s]kip this commit but apply"
1644                                                 " the rest, or [q]uit? ")
1645                            if not response:
1646                                continue
1647                        elif self.conflict_behavior == "skip":
1648                            response = "s"
1649                        elif self.conflict_behavior == "quit":
1650                            response = "q"
1651                        else:
1652                            die("Unknown conflict_behavior '%s'" %
1653                                self.conflict_behavior)
1654
1655                        if response[0] == "s":
1656                            print "Skipping this commit, but applying the rest"
1657                            break
1658                        if response[0] == "q":
1659                            print "Quitting"
1660                            quit = True
1661                            break
1662                    if quit:
1663                        break
1664
1665        chdir(self.oldWorkingDirectory)
1666
1667        if self.dry_run:
1668            pass
1669        elif self.prepare_p4_only:
1670            pass
1671        elif len(commits) == len(applied):
1672            print "All commits applied!"
1673
1674            sync = P4Sync()
1675            if self.branch:
1676                sync.branch = self.branch
1677            sync.run([])
1678
1679            rebase = P4Rebase()
1680            rebase.rebase()
1681
1682        else:
1683            if len(applied) == 0:
1684                print "No commits applied."
1685            else:
1686                print "Applied only the commits marked with '*':"
1687                for c in commits:
1688                    if c in applied:
1689                        star = "*"
1690                    else:
1691                        star = " "
1692                    print star, read_pipe(["git", "show", "-s",
1693                                           "--format=format:%h %s",  c])
1694                print "You will have to do 'git p4 sync' and rebase."
1695
1696        if gitConfig("git-p4.exportLabels", "--bool") == "true":
1697            self.exportLabels = True
1698
1699        if self.exportLabels:
1700            p4Labels = getP4Labels(self.depotPath)
1701            gitTags = getGitTags()
1702
1703            missingGitTags = gitTags - p4Labels
1704            self.exportGitTags(missingGitTags)
1705
1706        # exit with error unless everything applied perfecly
1707        if len(commits) != len(applied):
1708                sys.exit(1)
1709
1710        return True
1711
1712class View(object):
1713    """Represent a p4 view ("p4 help views"), and map files in a
1714       repo according to the view."""
1715
1716    class Path(object):
1717        """A depot or client path, possibly containing wildcards.
1718           The only one supported is ... at the end, currently.
1719           Initialize with the full path, with //depot or //client."""
1720
1721        def __init__(self, path, is_depot):
1722            self.path = path
1723            self.is_depot = is_depot
1724            self.find_wildcards()
1725            # remember the prefix bit, useful for relative mappings
1726            m = re.match("(//[^/]+/)", self.path)
1727            if not m:
1728                die("Path %s does not start with //prefix/" % self.path)
1729            prefix = m.group(1)
1730            if not self.is_depot:
1731                # strip //client/ on client paths
1732                self.path = self.path[len(prefix):]
1733
1734        def find_wildcards(self):
1735            """Make sure wildcards are valid, and set up internal
1736               variables."""
1737
1738            self.ends_triple_dot = False
1739            # There are three wildcards allowed in p4 views
1740            # (see "p4 help views").  This code knows how to
1741            # handle "..." (only at the end), but cannot deal with
1742            # "%%n" or "*".  Only check the depot_side, as p4 should
1743            # validate that the client_side matches too.
1744            if re.search(r'%%[1-9]', self.path):
1745                die("Can't handle %%n wildcards in view: %s" % self.path)
1746            if self.path.find("*") >= 0:
1747                die("Can't handle * wildcards in view: %s" % self.path)
1748            triple_dot_index = self.path.find("...")
1749            if triple_dot_index >= 0:
1750                if triple_dot_index != len(self.path) - 3:
1751                    die("Can handle only single ... wildcard, at end: %s" %
1752                        self.path)
1753                self.ends_triple_dot = True
1754
1755        def ensure_compatible(self, other_path):
1756            """Make sure the wildcards agree."""
1757            if self.ends_triple_dot != other_path.ends_triple_dot:
1758                 die("Both paths must end with ... if either does;\n" +
1759                     "paths: %s %s" % (self.path, other_path.path))
1760
1761        def match_wildcards(self, test_path):
1762            """See if this test_path matches us, and fill in the value
1763               of the wildcards if so.  Returns a tuple of
1764               (True|False, wildcards[]).  For now, only the ... at end
1765               is supported, so at most one wildcard."""
1766            if self.ends_triple_dot:
1767                dotless = self.path[:-3]
1768                if test_path.startswith(dotless):
1769                    wildcard = test_path[len(dotless):]
1770                    return (True, [ wildcard ])
1771            else:
1772                if test_path == self.path:
1773                    return (True, [])
1774            return (False, [])
1775
1776        def match(self, test_path):
1777            """Just return if it matches; don't bother with the wildcards."""
1778            b, _ = self.match_wildcards(test_path)
1779            return b
1780
1781        def fill_in_wildcards(self, wildcards):
1782            """Return the relative path, with the wildcards filled in
1783               if there are any."""
1784            if self.ends_triple_dot:
1785                return self.path[:-3] + wildcards[0]
1786            else:
1787                return self.path
1788
1789    class Mapping(object):
1790        def __init__(self, depot_side, client_side, overlay, exclude):
1791            # depot_side is without the trailing /... if it had one
1792            self.depot_side = View.Path(depot_side, is_depot=True)
1793            self.client_side = View.Path(client_side, is_depot=False)
1794            self.overlay = overlay  # started with "+"
1795            self.exclude = exclude  # started with "-"
1796            assert not (self.overlay and self.exclude)
1797            self.depot_side.ensure_compatible(self.client_side)
1798
1799        def __str__(self):
1800            c = " "
1801            if self.overlay:
1802                c = "+"
1803            if self.exclude:
1804                c = "-"
1805            return "View.Mapping: %s%s -> %s" % \
1806                   (c, self.depot_side.path, self.client_side.path)
1807
1808        def map_depot_to_client(self, depot_path):
1809            """Calculate the client path if using this mapping on the
1810               given depot path; does not consider the effect of other
1811               mappings in a view.  Even excluded mappings are returned."""
1812            matches, wildcards = self.depot_side.match_wildcards(depot_path)
1813            if not matches:
1814                return ""
1815            client_path = self.client_side.fill_in_wildcards(wildcards)
1816            return client_path
1817
1818    #
1819    # View methods
1820    #
1821    def __init__(self):
1822        self.mappings = []
1823
1824    def append(self, view_line):
1825        """Parse a view line, splitting it into depot and client
1826           sides.  Append to self.mappings, preserving order."""
1827
1828        # Split the view line into exactly two words.  P4 enforces
1829        # structure on these lines that simplifies this quite a bit.
1830        #
1831        # Either or both words may be double-quoted.
1832        # Single quotes do not matter.
1833        # Double-quote marks cannot occur inside the words.
1834        # A + or - prefix is also inside the quotes.
1835        # There are no quotes unless they contain a space.
1836        # The line is already white-space stripped.
1837        # The two words are separated by a single space.
1838        #
1839        if view_line[0] == '"':
1840            # First word is double quoted.  Find its end.
1841            close_quote_index = view_line.find('"', 1)
1842            if close_quote_index <= 0:
1843                die("No first-word closing quote found: %s" % view_line)
1844            depot_side = view_line[1:close_quote_index]
1845            # skip closing quote and space
1846            rhs_index = close_quote_index + 1 + 1
1847        else:
1848            space_index = view_line.find(" ")
1849            if space_index <= 0:
1850                die("No word-splitting space found: %s" % view_line)
1851            depot_side = view_line[0:space_index]
1852            rhs_index = space_index + 1
1853
1854        if view_line[rhs_index] == '"':
1855            # Second word is double quoted.  Make sure there is a
1856            # double quote at the end too.
1857            if not view_line.endswith('"'):
1858                die("View line with rhs quote should end with one: %s" %
1859                    view_line)
1860            # skip the quotes
1861            client_side = view_line[rhs_index+1:-1]
1862        else:
1863            client_side = view_line[rhs_index:]
1864
1865        # prefix + means overlay on previous mapping
1866        overlay = False
1867        if depot_side.startswith("+"):
1868            overlay = True
1869            depot_side = depot_side[1:]
1870
1871        # prefix - means exclude this path
1872        exclude = False
1873        if depot_side.startswith("-"):
1874            exclude = True
1875            depot_side = depot_side[1:]
1876
1877        m = View.Mapping(depot_side, client_side, overlay, exclude)
1878        self.mappings.append(m)
1879
1880    def map_in_client(self, depot_path):
1881        """Return the relative location in the client where this
1882           depot file should live.  Returns "" if the file should
1883           not be mapped in the client."""
1884
1885        paths_filled = []
1886        client_path = ""
1887
1888        # look at later entries first
1889        for m in self.mappings[::-1]:
1890
1891            # see where will this path end up in the client
1892            p = m.map_depot_to_client(depot_path)
1893
1894            if p == "":
1895                # Depot path does not belong in client.  Must remember
1896                # this, as previous items should not cause files to
1897                # exist in this path either.  Remember that the list is
1898                # being walked from the end, which has higher precedence.
1899                # Overlap mappings do not exclude previous mappings.
1900                if not m.overlay:
1901                    paths_filled.append(m.client_side)
1902
1903            else:
1904                # This mapping matched; no need to search any further.
1905                # But, the mapping could be rejected if the client path
1906                # has already been claimed by an earlier mapping (i.e.
1907                # one later in the list, which we are walking backwards).
1908                already_mapped_in_client = False
1909                for f in paths_filled:
1910                    # this is View.Path.match
1911                    if f.match(p):
1912                        already_mapped_in_client = True
1913                        break
1914                if not already_mapped_in_client:
1915                    # Include this file, unless it is from a line that
1916                    # explicitly said to exclude it.
1917                    if not m.exclude:
1918                        client_path = p
1919
1920                # a match, even if rejected, always stops the search
1921                break
1922
1923        return client_path
1924
1925class P4Sync(Command, P4UserMap):
1926    delete_actions = ( "delete", "move/delete", "purge" )
1927
1928    def __init__(self):
1929        Command.__init__(self)
1930        P4UserMap.__init__(self)
1931        self.options = [
1932                optparse.make_option("--branch", dest="branch"),
1933                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1934                optparse.make_option("--changesfile", dest="changesFile"),
1935                optparse.make_option("--silent", dest="silent", action="store_true"),
1936                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1937                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1938                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1939                                     help="Import into refs/heads/ , not refs/remotes"),
1940                optparse.make_option("--max-changes", dest="maxChanges"),
1941                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1942                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1943                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1944                                     help="Only sync files that are included in the Perforce Client Spec")
1945        ]
1946        self.description = """Imports from Perforce into a git repository.\n
1947    example:
1948    //depot/my/project/ -- to import the current head
1949    //depot/my/project/@all -- to import everything
1950    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1951
1952    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1953
1954        self.usage += " //depot/path[@revRange]"
1955        self.silent = False
1956        self.createdBranches = set()
1957        self.committedChanges = set()
1958        self.branch = ""
1959        self.detectBranches = False
1960        self.detectLabels = False
1961        self.importLabels = False
1962        self.changesFile = ""
1963        self.syncWithOrigin = True
1964        self.importIntoRemotes = True
1965        self.maxChanges = ""
1966        self.isWindows = (platform.system() == "Windows")
1967        self.keepRepoPath = False
1968        self.depotPaths = None
1969        self.p4BranchesInGit = []
1970        self.cloneExclude = []
1971        self.useClientSpec = False
1972        self.useClientSpec_from_options = False
1973        self.clientSpecDirs = None
1974        self.tempBranches = []
1975        self.tempBranchLocation = "git-p4-tmp"
1976
1977        if gitConfig("git-p4.syncFromOrigin") == "false":
1978            self.syncWithOrigin = False
1979
1980    # Force a checkpoint in fast-import and wait for it to finish
1981    def checkpoint(self):
1982        self.gitStream.write("checkpoint\n\n")
1983        self.gitStream.write("progress checkpoint\n\n")
1984        out = self.gitOutput.readline()
1985        if self.verbose:
1986            print "checkpoint finished: " + out
1987
1988    def extractFilesFromCommit(self, commit):
1989        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1990                             for path in self.cloneExclude]
1991        files = []
1992        fnum = 0
1993        while commit.has_key("depotFile%s" % fnum):
1994            path =  commit["depotFile%s" % fnum]
1995
1996            if [p for p in self.cloneExclude
1997                if p4PathStartsWith(path, p)]:
1998                found = False
1999            else:
2000                found = [p for p in self.depotPaths
2001                         if p4PathStartsWith(path, p)]
2002            if not found:
2003                fnum = fnum + 1
2004                continue
2005
2006            file = {}
2007            file["path"] = path
2008            file["rev"] = commit["rev%s" % fnum]
2009            file["action"] = commit["action%s" % fnum]
2010            file["type"] = commit["type%s" % fnum]
2011            files.append(file)
2012            fnum = fnum + 1
2013        return files
2014
2015    def stripRepoPath(self, path, prefixes):
2016        """When streaming files, this is called to map a p4 depot path
2017           to where it should go in git.  The prefixes are either
2018           self.depotPaths, or self.branchPrefixes in the case of
2019           branch detection."""
2020
2021        if self.useClientSpec:
2022            # branch detection moves files up a level (the branch name)
2023            # from what client spec interpretation gives
2024            path = self.clientSpecDirs.map_in_client(path)
2025            if self.detectBranches:
2026                for b in self.knownBranches:
2027                    if path.startswith(b + "/"):
2028                        path = path[len(b)+1:]
2029
2030        elif self.keepRepoPath:
2031            # Preserve everything in relative path name except leading
2032            # //depot/; just look at first prefix as they all should
2033            # be in the same depot.
2034            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2035            if p4PathStartsWith(path, depot):
2036                path = path[len(depot):]
2037
2038        else:
2039            for p in prefixes:
2040                if p4PathStartsWith(path, p):
2041                    path = path[len(p):]
2042                    break
2043
2044        path = wildcard_decode(path)
2045        return path
2046
2047    def splitFilesIntoBranches(self, commit):
2048        """Look at each depotFile in the commit to figure out to what
2049           branch it belongs."""
2050
2051        branches = {}
2052        fnum = 0
2053        while commit.has_key("depotFile%s" % fnum):
2054            path =  commit["depotFile%s" % fnum]
2055            found = [p for p in self.depotPaths
2056                     if p4PathStartsWith(path, p)]
2057            if not found:
2058                fnum = fnum + 1
2059                continue
2060
2061            file = {}
2062            file["path"] = path
2063            file["rev"] = commit["rev%s" % fnum]
2064            file["action"] = commit["action%s" % fnum]
2065            file["type"] = commit["type%s" % fnum]
2066            fnum = fnum + 1
2067
2068            # start with the full relative path where this file would
2069            # go in a p4 client
2070            if self.useClientSpec:
2071                relPath = self.clientSpecDirs.map_in_client(path)
2072            else:
2073                relPath = self.stripRepoPath(path, self.depotPaths)
2074
2075            for branch in self.knownBranches.keys():
2076                # add a trailing slash so that a commit into qt/4.2foo
2077                # doesn't end up in qt/4.2, e.g.
2078                if relPath.startswith(branch + "/"):
2079                    if branch not in branches:
2080                        branches[branch] = []
2081                    branches[branch].append(file)
2082                    break
2083
2084        return branches
2085
2086    # output one file from the P4 stream
2087    # - helper for streamP4Files
2088
2089    def streamOneP4File(self, file, contents):
2090        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2091        if verbose:
2092            sys.stderr.write("%s\n" % relPath)
2093
2094        (type_base, type_mods) = split_p4_type(file["type"])
2095
2096        git_mode = "100644"
2097        if "x" in type_mods:
2098            git_mode = "100755"
2099        if type_base == "symlink":
2100            git_mode = "120000"
2101            # p4 print on a symlink contains "target\n"; remove the newline
2102            data = ''.join(contents)
2103            contents = [data[:-1]]
2104
2105        if type_base == "utf16":
2106            # p4 delivers different text in the python output to -G
2107            # than it does when using "print -o", or normal p4 client
2108            # operations.  utf16 is converted to ascii or utf8, perhaps.
2109            # But ascii text saved as -t utf16 is completely mangled.
2110            # Invoke print -o to get the real contents.
2111            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
2112            contents = [ text ]
2113
2114        if type_base == "apple":
2115            # Apple filetype files will be streamed as a concatenation of
2116            # its appledouble header and the contents.  This is useless
2117            # on both macs and non-macs.  If using "print -q -o xx", it
2118            # will create "xx" with the data, and "%xx" with the header.
2119            # This is also not very useful.
2120            #
2121            # Ideally, someday, this script can learn how to generate
2122            # appledouble files directly and import those to git, but
2123            # non-mac machines can never find a use for apple filetype.
2124            print "\nIgnoring apple filetype file %s" % file['depotFile']
2125            return
2126
2127        # Perhaps windows wants unicode, utf16 newlines translated too;
2128        # but this is not doing it.
2129        if self.isWindows and type_base == "text":
2130            mangled = []
2131            for data in contents:
2132                data = data.replace("\r\n", "\n")
2133                mangled.append(data)
2134            contents = mangled
2135
2136        # Note that we do not try to de-mangle keywords on utf16 files,
2137        # even though in theory somebody may want that.
2138        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2139        if pattern:
2140            regexp = re.compile(pattern, re.VERBOSE)
2141            text = ''.join(contents)
2142            text = regexp.sub(r'$\1$', text)
2143            contents = [ text ]
2144
2145        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2146
2147        # total length...
2148        length = 0
2149        for d in contents:
2150            length = length + len(d)
2151
2152        self.gitStream.write("data %d\n" % length)
2153        for d in contents:
2154            self.gitStream.write(d)
2155        self.gitStream.write("\n")
2156
2157    def streamOneP4Deletion(self, file):
2158        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2159        if verbose:
2160            sys.stderr.write("delete %s\n" % relPath)
2161        self.gitStream.write("D %s\n" % relPath)
2162
2163    # handle another chunk of streaming data
2164    def streamP4FilesCb(self, marshalled):
2165
2166        # catch p4 errors and complain
2167        err = None
2168        if "code" in marshalled:
2169            if marshalled["code"] == "error":
2170                if "data" in marshalled:
2171                    err = marshalled["data"].rstrip()
2172        if err:
2173            f = None
2174            if self.stream_have_file_info:
2175                if "depotFile" in self.stream_file:
2176                    f = self.stream_file["depotFile"]
2177            # force a failure in fast-import, else an empty
2178            # commit will be made
2179            self.gitStream.write("\n")
2180            self.gitStream.write("die-now\n")
2181            self.gitStream.close()
2182            # ignore errors, but make sure it exits first
2183            self.importProcess.wait()
2184            if f:
2185                die("Error from p4 print for %s: %s" % (f, err))
2186            else:
2187                die("Error from p4 print: %s" % err)
2188
2189        if marshalled.has_key('depotFile') and self.stream_have_file_info:
2190            # start of a new file - output the old one first
2191            self.streamOneP4File(self.stream_file, self.stream_contents)
2192            self.stream_file = {}
2193            self.stream_contents = []
2194            self.stream_have_file_info = False
2195
2196        # pick up the new file information... for the
2197        # 'data' field we need to append to our array
2198        for k in marshalled.keys():
2199            if k == 'data':
2200                self.stream_contents.append(marshalled['data'])
2201            else:
2202                self.stream_file[k] = marshalled[k]
2203
2204        self.stream_have_file_info = True
2205
2206    # Stream directly from "p4 files" into "git fast-import"
2207    def streamP4Files(self, files):
2208        filesForCommit = []
2209        filesToRead = []
2210        filesToDelete = []
2211
2212        for f in files:
2213            # if using a client spec, only add the files that have
2214            # a path in the client
2215            if self.clientSpecDirs:
2216                if self.clientSpecDirs.map_in_client(f['path']) == "":
2217                    continue
2218
2219            filesForCommit.append(f)
2220            if f['action'] in self.delete_actions:
2221                filesToDelete.append(f)
2222            else:
2223                filesToRead.append(f)
2224
2225        # deleted files...
2226        for f in filesToDelete:
2227            self.streamOneP4Deletion(f)
2228
2229        if len(filesToRead) > 0:
2230            self.stream_file = {}
2231            self.stream_contents = []
2232            self.stream_have_file_info = False
2233
2234            # curry self argument
2235            def streamP4FilesCbSelf(entry):
2236                self.streamP4FilesCb(entry)
2237
2238            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2239
2240            p4CmdList(["-x", "-", "print"],
2241                      stdin=fileArgs,
2242                      cb=streamP4FilesCbSelf)
2243
2244            # do the last chunk
2245            if self.stream_file.has_key('depotFile'):
2246                self.streamOneP4File(self.stream_file, self.stream_contents)
2247
2248    def make_email(self, userid):
2249        if userid in self.users:
2250            return self.users[userid]
2251        else:
2252            return "%s <a@b>" % userid
2253
2254    # Stream a p4 tag
2255    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2256        if verbose:
2257            print "writing tag %s for commit %s" % (labelName, commit)
2258        gitStream.write("tag %s\n" % labelName)
2259        gitStream.write("from %s\n" % commit)
2260
2261        if labelDetails.has_key('Owner'):
2262            owner = labelDetails["Owner"]
2263        else:
2264            owner = None
2265
2266        # Try to use the owner of the p4 label, or failing that,
2267        # the current p4 user id.
2268        if owner:
2269            email = self.make_email(owner)
2270        else:
2271            email = self.make_email(self.p4UserId())
2272        tagger = "%s %s %s" % (email, epoch, self.tz)
2273
2274        gitStream.write("tagger %s\n" % tagger)
2275
2276        print "labelDetails=",labelDetails
2277        if labelDetails.has_key('Description'):
2278            description = labelDetails['Description']
2279        else:
2280            description = 'Label from git p4'
2281
2282        gitStream.write("data %d\n" % len(description))
2283        gitStream.write(description)
2284        gitStream.write("\n")
2285
2286    def commit(self, details, files, branch, parent = ""):
2287        epoch = details["time"]
2288        author = details["user"]
2289
2290        if self.verbose:
2291            print "commit into %s" % branch
2292
2293        # start with reading files; if that fails, we should not
2294        # create a commit.
2295        new_files = []
2296        for f in files:
2297            if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2298                new_files.append (f)
2299            else:
2300                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2301
2302        self.gitStream.write("commit %s\n" % branch)
2303#        gitStream.write("mark :%s\n" % details["change"])
2304        self.committedChanges.add(int(details["change"]))
2305        committer = ""
2306        if author not in self.users:
2307            self.getUserMapFromPerforceServer()
2308        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2309
2310        self.gitStream.write("committer %s\n" % committer)
2311
2312        self.gitStream.write("data <<EOT\n")
2313        self.gitStream.write(details["desc"])
2314        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2315                             (','.join(self.branchPrefixes), details["change"]))
2316        if len(details['options']) > 0:
2317            self.gitStream.write(": options = %s" % details['options'])
2318        self.gitStream.write("]\nEOT\n\n")
2319
2320        if len(parent) > 0:
2321            if self.verbose:
2322                print "parent %s" % parent
2323            self.gitStream.write("from %s\n" % parent)
2324
2325        self.streamP4Files(new_files)
2326        self.gitStream.write("\n")
2327
2328        change = int(details["change"])
2329
2330        if self.labels.has_key(change):
2331            label = self.labels[change]
2332            labelDetails = label[0]
2333            labelRevisions = label[1]
2334            if self.verbose:
2335                print "Change %s is labelled %s" % (change, labelDetails)
2336
2337            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2338                                                for p in self.branchPrefixes])
2339
2340            if len(files) == len(labelRevisions):
2341
2342                cleanedFiles = {}
2343                for info in files:
2344                    if info["action"] in self.delete_actions:
2345                        continue
2346                    cleanedFiles[info["depotFile"]] = info["rev"]
2347
2348                if cleanedFiles == labelRevisions:
2349                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2350
2351                else:
2352                    if not self.silent:
2353                        print ("Tag %s does not match with change %s: files do not match."
2354                               % (labelDetails["label"], change))
2355
2356            else:
2357                if not self.silent:
2358                    print ("Tag %s does not match with change %s: file count is different."
2359                           % (labelDetails["label"], change))
2360
2361    # Build a dictionary of changelists and labels, for "detect-labels" option.
2362    def getLabels(self):
2363        self.labels = {}
2364
2365        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2366        if len(l) > 0 and not self.silent:
2367            print "Finding files belonging to labels in %s" % `self.depotPaths`
2368
2369        for output in l:
2370            label = output["label"]
2371            revisions = {}
2372            newestChange = 0
2373            if self.verbose:
2374                print "Querying files for label %s" % label
2375            for file in p4CmdList(["files"] +
2376                                      ["%s...@%s" % (p, label)
2377                                          for p in self.depotPaths]):
2378                revisions[file["depotFile"]] = file["rev"]
2379                change = int(file["change"])
2380                if change > newestChange:
2381                    newestChange = change
2382
2383            self.labels[newestChange] = [output, revisions]
2384
2385        if self.verbose:
2386            print "Label changes: %s" % self.labels.keys()
2387
2388    # Import p4 labels as git tags. A direct mapping does not
2389    # exist, so assume that if all the files are at the same revision
2390    # then we can use that, or it's something more complicated we should
2391    # just ignore.
2392    def importP4Labels(self, stream, p4Labels):
2393        if verbose:
2394            print "import p4 labels: " + ' '.join(p4Labels)
2395
2396        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2397        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2398        if len(validLabelRegexp) == 0:
2399            validLabelRegexp = defaultLabelRegexp
2400        m = re.compile(validLabelRegexp)
2401
2402        for name in p4Labels:
2403            commitFound = False
2404
2405            if not m.match(name):
2406                if verbose:
2407                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2408                continue
2409
2410            if name in ignoredP4Labels:
2411                continue
2412
2413            labelDetails = p4CmdList(['label', "-o", name])[0]
2414
2415            # get the most recent changelist for each file in this label
2416            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2417                                for p in self.depotPaths])
2418
2419            if change.has_key('change'):
2420                # find the corresponding git commit; take the oldest commit
2421                changelist = int(change['change'])
2422                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2423                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2424                if len(gitCommit) == 0:
2425                    print "could not find git commit for changelist %d" % changelist
2426                else:
2427                    gitCommit = gitCommit.strip()
2428                    commitFound = True
2429                    # Convert from p4 time format
2430                    try:
2431                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2432                    except ValueError:
2433                        print "Could not convert label time %s" % labelDetails['Update']
2434                        tmwhen = 1
2435
2436                    when = int(time.mktime(tmwhen))
2437                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2438                    if verbose:
2439                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2440            else:
2441                if verbose:
2442                    print "Label %s has no changelists - possibly deleted?" % name
2443
2444            if not commitFound:
2445                # We can't import this label; don't try again as it will get very
2446                # expensive repeatedly fetching all the files for labels that will
2447                # never be imported. If the label is moved in the future, the
2448                # ignore will need to be removed manually.
2449                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2450
2451    def guessProjectName(self):
2452        for p in self.depotPaths:
2453            if p.endswith("/"):
2454                p = p[:-1]
2455            p = p[p.strip().rfind("/") + 1:]
2456            if not p.endswith("/"):
2457               p += "/"
2458            return p
2459
2460    def getBranchMapping(self):
2461        lostAndFoundBranches = set()
2462
2463        user = gitConfig("git-p4.branchUser")
2464        if len(user) > 0:
2465            command = "branches -u %s" % user
2466        else:
2467            command = "branches"
2468
2469        for info in p4CmdList(command):
2470            details = p4Cmd(["branch", "-o", info["branch"]])
2471            viewIdx = 0
2472            while details.has_key("View%s" % viewIdx):
2473                paths = details["View%s" % viewIdx].split(" ")
2474                viewIdx = viewIdx + 1
2475                # require standard //depot/foo/... //depot/bar/... mapping
2476                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2477                    continue
2478                source = paths[0]
2479                destination = paths[1]
2480                ## HACK
2481                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2482                    source = source[len(self.depotPaths[0]):-4]
2483                    destination = destination[len(self.depotPaths[0]):-4]
2484
2485                    if destination in self.knownBranches:
2486                        if not self.silent:
2487                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2488                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2489                        continue
2490
2491                    self.knownBranches[destination] = source
2492
2493                    lostAndFoundBranches.discard(destination)
2494
2495                    if source not in self.knownBranches:
2496                        lostAndFoundBranches.add(source)
2497
2498        # Perforce does not strictly require branches to be defined, so we also
2499        # check git config for a branch list.
2500        #
2501        # Example of branch definition in git config file:
2502        # [git-p4]
2503        #   branchList=main:branchA
2504        #   branchList=main:branchB
2505        #   branchList=branchA:branchC
2506        configBranches = gitConfigList("git-p4.branchList")
2507        for branch in configBranches:
2508            if branch:
2509                (source, destination) = branch.split(":")
2510                self.knownBranches[destination] = source
2511
2512                lostAndFoundBranches.discard(destination)
2513
2514                if source not in self.knownBranches:
2515                    lostAndFoundBranches.add(source)
2516
2517
2518        for branch in lostAndFoundBranches:
2519            self.knownBranches[branch] = branch
2520
2521    def getBranchMappingFromGitBranches(self):
2522        branches = p4BranchesInGit(self.importIntoRemotes)
2523        for branch in branches.keys():
2524            if branch == "master":
2525                branch = "main"
2526            else:
2527                branch = branch[len(self.projectName):]
2528            self.knownBranches[branch] = branch
2529
2530    def updateOptionDict(self, d):
2531        option_keys = {}
2532        if self.keepRepoPath:
2533            option_keys['keepRepoPath'] = 1
2534
2535        d["options"] = ' '.join(sorted(option_keys.keys()))
2536
2537    def readOptions(self, d):
2538        self.keepRepoPath = (d.has_key('options')
2539                             and ('keepRepoPath' in d['options']))
2540
2541    def gitRefForBranch(self, branch):
2542        if branch == "main":
2543            return self.refPrefix + "master"
2544
2545        if len(branch) <= 0:
2546            return branch
2547
2548        return self.refPrefix + self.projectName + branch
2549
2550    def gitCommitByP4Change(self, ref, change):
2551        if self.verbose:
2552            print "looking in ref " + ref + " for change %s using bisect..." % change
2553
2554        earliestCommit = ""
2555        latestCommit = parseRevision(ref)
2556
2557        while True:
2558            if self.verbose:
2559                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2560            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2561            if len(next) == 0:
2562                if self.verbose:
2563                    print "argh"
2564                return ""
2565            log = extractLogMessageFromGitCommit(next)
2566            settings = extractSettingsGitLog(log)
2567            currentChange = int(settings['change'])
2568            if self.verbose:
2569                print "current change %s" % currentChange
2570
2571            if currentChange == change:
2572                if self.verbose:
2573                    print "found %s" % next
2574                return next
2575
2576            if currentChange < change:
2577                earliestCommit = "^%s" % next
2578            else:
2579                latestCommit = "%s" % next
2580
2581        return ""
2582
2583    def importNewBranch(self, branch, maxChange):
2584        # make fast-import flush all changes to disk and update the refs using the checkpoint
2585        # command so that we can try to find the branch parent in the git history
2586        self.gitStream.write("checkpoint\n\n");
2587        self.gitStream.flush();
2588        branchPrefix = self.depotPaths[0] + branch + "/"
2589        range = "@1,%s" % maxChange
2590        #print "prefix" + branchPrefix
2591        changes = p4ChangesForPaths([branchPrefix], range)
2592        if len(changes) <= 0:
2593            return False
2594        firstChange = changes[0]
2595        #print "first change in branch: %s" % firstChange
2596        sourceBranch = self.knownBranches[branch]
2597        sourceDepotPath = self.depotPaths[0] + sourceBranch
2598        sourceRef = self.gitRefForBranch(sourceBranch)
2599        #print "source " + sourceBranch
2600
2601        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2602        #print "branch parent: %s" % branchParentChange
2603        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2604        if len(gitParent) > 0:
2605            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2606            #print "parent git commit: %s" % gitParent
2607
2608        self.importChanges(changes)
2609        return True
2610
2611    def searchParent(self, parent, branch, target):
2612        parentFound = False
2613        for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2614            blob = blob.strip()
2615            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2616                parentFound = True
2617                if self.verbose:
2618                    print "Found parent of %s in commit %s" % (branch, blob)
2619                break
2620        if parentFound:
2621            return blob
2622        else:
2623            return None
2624
2625    def importChanges(self, changes):
2626        cnt = 1
2627        for change in changes:
2628            description = p4_describe(change)
2629            self.updateOptionDict(description)
2630
2631            if not self.silent:
2632                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2633                sys.stdout.flush()
2634            cnt = cnt + 1
2635
2636            try:
2637                if self.detectBranches:
2638                    branches = self.splitFilesIntoBranches(description)
2639                    for branch in branches.keys():
2640                        ## HACK  --hwn
2641                        branchPrefix = self.depotPaths[0] + branch + "/"
2642                        self.branchPrefixes = [ branchPrefix ]
2643
2644                        parent = ""
2645
2646                        filesForCommit = branches[branch]
2647
2648                        if self.verbose:
2649                            print "branch is %s" % branch
2650
2651                        self.updatedBranches.add(branch)
2652
2653                        if branch not in self.createdBranches:
2654                            self.createdBranches.add(branch)
2655                            parent = self.knownBranches[branch]
2656                            if parent == branch:
2657                                parent = ""
2658                            else:
2659                                fullBranch = self.projectName + branch
2660                                if fullBranch not in self.p4BranchesInGit:
2661                                    if not self.silent:
2662                                        print("\n    Importing new branch %s" % fullBranch);
2663                                    if self.importNewBranch(branch, change - 1):
2664                                        parent = ""
2665                                        self.p4BranchesInGit.append(fullBranch)
2666                                    if not self.silent:
2667                                        print("\n    Resuming with change %s" % change);
2668
2669                                if self.verbose:
2670                                    print "parent determined through known branches: %s" % parent
2671
2672                        branch = self.gitRefForBranch(branch)
2673                        parent = self.gitRefForBranch(parent)
2674
2675                        if self.verbose:
2676                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2677
2678                        if len(parent) == 0 and branch in self.initialParents:
2679                            parent = self.initialParents[branch]
2680                            del self.initialParents[branch]
2681
2682                        blob = None
2683                        if len(parent) > 0:
2684                            tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2685                            if self.verbose:
2686                                print "Creating temporary branch: " + tempBranch
2687                            self.commit(description, filesForCommit, tempBranch)
2688                            self.tempBranches.append(tempBranch)
2689                            self.checkpoint()
2690                            blob = self.searchParent(parent, branch, tempBranch)
2691                        if blob:
2692                            self.commit(description, filesForCommit, branch, blob)
2693                        else:
2694                            if self.verbose:
2695                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2696                            self.commit(description, filesForCommit, branch, parent)
2697                else:
2698                    files = self.extractFilesFromCommit(description)
2699                    self.commit(description, files, self.branch,
2700                                self.initialParent)
2701                    # only needed once, to connect to the previous commit
2702                    self.initialParent = ""
2703            except IOError:
2704                print self.gitError.read()
2705                sys.exit(1)
2706
2707    def importHeadRevision(self, revision):
2708        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2709
2710        details = {}
2711        details["user"] = "git perforce import user"
2712        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2713                           % (' '.join(self.depotPaths), revision))
2714        details["change"] = revision
2715        newestRevision = 0
2716
2717        fileCnt = 0
2718        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2719
2720        for info in p4CmdList(["files"] + fileArgs):
2721
2722            if 'code' in info and info['code'] == 'error':
2723                sys.stderr.write("p4 returned an error: %s\n"
2724                                 % info['data'])
2725                if info['data'].find("must refer to client") >= 0:
2726                    sys.stderr.write("This particular p4 error is misleading.\n")
2727                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2728                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2729                sys.exit(1)
2730            if 'p4ExitCode' in info:
2731                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2732                sys.exit(1)
2733
2734
2735            change = int(info["change"])
2736            if change > newestRevision:
2737                newestRevision = change
2738
2739            if info["action"] in self.delete_actions:
2740                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2741                #fileCnt = fileCnt + 1
2742                continue
2743
2744            for prop in ["depotFile", "rev", "action", "type" ]:
2745                details["%s%s" % (prop, fileCnt)] = info[prop]
2746
2747            fileCnt = fileCnt + 1
2748
2749        details["change"] = newestRevision
2750
2751        # Use time from top-most change so that all git p4 clones of
2752        # the same p4 repo have the same commit SHA1s.
2753        res = p4_describe(newestRevision)
2754        details["time"] = res["time"]
2755
2756        self.updateOptionDict(details)
2757        try:
2758            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2759        except IOError:
2760            print "IO error with git fast-import. Is your git version recent enough?"
2761            print self.gitError.read()
2762
2763
2764    def run(self, args):
2765        self.depotPaths = []
2766        self.changeRange = ""
2767        self.previousDepotPaths = []
2768        self.hasOrigin = False
2769
2770        # map from branch depot path to parent branch
2771        self.knownBranches = {}
2772        self.initialParents = {}
2773
2774        if self.importIntoRemotes:
2775            self.refPrefix = "refs/remotes/p4/"
2776        else:
2777            self.refPrefix = "refs/heads/p4/"
2778
2779        if self.syncWithOrigin:
2780            self.hasOrigin = originP4BranchesExist()
2781            if self.hasOrigin:
2782                if not self.silent:
2783                    print 'Syncing with origin first, using "git fetch origin"'
2784                system("git fetch origin")
2785
2786        branch_arg_given = bool(self.branch)
2787        if len(self.branch) == 0:
2788            self.branch = self.refPrefix + "master"
2789            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2790                system("git update-ref %s refs/heads/p4" % self.branch)
2791                system("git branch -D p4")
2792
2793        # accept either the command-line option, or the configuration variable
2794        if self.useClientSpec:
2795            # will use this after clone to set the variable
2796            self.useClientSpec_from_options = True
2797        else:
2798            if gitConfig("git-p4.useclientspec", "--bool") == "true":
2799                self.useClientSpec = True
2800        if self.useClientSpec:
2801            self.clientSpecDirs = getClientSpec()
2802
2803        # TODO: should always look at previous commits,
2804        # merge with previous imports, if possible.
2805        if args == []:
2806            if self.hasOrigin:
2807                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2808
2809            # branches holds mapping from branch name to sha1
2810            branches = p4BranchesInGit(self.importIntoRemotes)
2811
2812            # restrict to just this one, disabling detect-branches
2813            if branch_arg_given:
2814                short = self.branch.split("/")[-1]
2815                if short in branches:
2816                    self.p4BranchesInGit = [ short ]
2817            else:
2818                self.p4BranchesInGit = branches.keys()
2819
2820            if len(self.p4BranchesInGit) > 1:
2821                if not self.silent:
2822                    print "Importing from/into multiple branches"
2823                self.detectBranches = True
2824                for branch in branches.keys():
2825                    self.initialParents[self.refPrefix + branch] = \
2826                        branches[branch]
2827
2828            if self.verbose:
2829                print "branches: %s" % self.p4BranchesInGit
2830
2831            p4Change = 0
2832            for branch in self.p4BranchesInGit:
2833                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2834
2835                settings = extractSettingsGitLog(logMsg)
2836
2837                self.readOptions(settings)
2838                if (settings.has_key('depot-paths')
2839                    and settings.has_key ('change')):
2840                    change = int(settings['change']) + 1
2841                    p4Change = max(p4Change, change)
2842
2843                    depotPaths = sorted(settings['depot-paths'])
2844                    if self.previousDepotPaths == []:
2845                        self.previousDepotPaths = depotPaths
2846                    else:
2847                        paths = []
2848                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2849                            prev_list = prev.split("/")
2850                            cur_list = cur.split("/")
2851                            for i in range(0, min(len(cur_list), len(prev_list))):
2852                                if cur_list[i] <> prev_list[i]:
2853                                    i = i - 1
2854                                    break
2855
2856                            paths.append ("/".join(cur_list[:i + 1]))
2857
2858                        self.previousDepotPaths = paths
2859
2860            if p4Change > 0:
2861                self.depotPaths = sorted(self.previousDepotPaths)
2862                self.changeRange = "@%s,#head" % p4Change
2863                if not self.silent and not self.detectBranches:
2864                    print "Performing incremental import into %s git branch" % self.branch
2865
2866        # accept multiple ref name abbreviations:
2867        #    refs/foo/bar/branch -> use it exactly
2868        #    p4/branch -> prepend refs/remotes/ or refs/heads/
2869        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2870        if not self.branch.startswith("refs/"):
2871            if self.importIntoRemotes:
2872                prepend = "refs/remotes/"
2873            else:
2874                prepend = "refs/heads/"
2875            if not self.branch.startswith("p4/"):
2876                prepend += "p4/"
2877            self.branch = prepend + self.branch
2878
2879        if len(args) == 0 and self.depotPaths:
2880            if not self.silent:
2881                print "Depot paths: %s" % ' '.join(self.depotPaths)
2882        else:
2883            if self.depotPaths and self.depotPaths != args:
2884                print ("previous import used depot path %s and now %s was specified. "
2885                       "This doesn't work!" % (' '.join (self.depotPaths),
2886                                               ' '.join (args)))
2887                sys.exit(1)
2888
2889            self.depotPaths = sorted(args)
2890
2891        revision = ""
2892        self.users = {}
2893
2894        # Make sure no revision specifiers are used when --changesfile
2895        # is specified.
2896        bad_changesfile = False
2897        if len(self.changesFile) > 0:
2898            for p in self.depotPaths:
2899                if p.find("@") >= 0 or p.find("#") >= 0:
2900                    bad_changesfile = True
2901                    break
2902        if bad_changesfile:
2903            die("Option --changesfile is incompatible with revision specifiers")
2904
2905        newPaths = []
2906        for p in self.depotPaths:
2907            if p.find("@") != -1:
2908                atIdx = p.index("@")
2909                self.changeRange = p[atIdx:]
2910                if self.changeRange == "@all":
2911                    self.changeRange = ""
2912                elif ',' not in self.changeRange:
2913                    revision = self.changeRange
2914                    self.changeRange = ""
2915                p = p[:atIdx]
2916            elif p.find("#") != -1:
2917                hashIdx = p.index("#")
2918                revision = p[hashIdx:]
2919                p = p[:hashIdx]
2920            elif self.previousDepotPaths == []:
2921                # pay attention to changesfile, if given, else import
2922                # the entire p4 tree at the head revision
2923                if len(self.changesFile) == 0:
2924                    revision = "#head"
2925
2926            p = re.sub ("\.\.\.$", "", p)
2927            if not p.endswith("/"):
2928                p += "/"
2929
2930            newPaths.append(p)
2931
2932        self.depotPaths = newPaths
2933
2934        # --detect-branches may change this for each branch
2935        self.branchPrefixes = self.depotPaths
2936
2937        self.loadUserMapFromCache()
2938        self.labels = {}
2939        if self.detectLabels:
2940            self.getLabels();
2941
2942        if self.detectBranches:
2943            ## FIXME - what's a P4 projectName ?
2944            self.projectName = self.guessProjectName()
2945
2946            if self.hasOrigin:
2947                self.getBranchMappingFromGitBranches()
2948            else:
2949                self.getBranchMapping()
2950            if self.verbose:
2951                print "p4-git branches: %s" % self.p4BranchesInGit
2952                print "initial parents: %s" % self.initialParents
2953            for b in self.p4BranchesInGit:
2954                if b != "master":
2955
2956                    ## FIXME
2957                    b = b[len(self.projectName):]
2958                self.createdBranches.add(b)
2959
2960        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2961
2962        self.importProcess = subprocess.Popen(["git", "fast-import"],
2963                                              stdin=subprocess.PIPE,
2964                                              stdout=subprocess.PIPE,
2965                                              stderr=subprocess.PIPE);
2966        self.gitOutput = self.importProcess.stdout
2967        self.gitStream = self.importProcess.stdin
2968        self.gitError = self.importProcess.stderr
2969
2970        if revision:
2971            self.importHeadRevision(revision)
2972        else:
2973            changes = []
2974
2975            if len(self.changesFile) > 0:
2976                output = open(self.changesFile).readlines()
2977                changeSet = set()
2978                for line in output:
2979                    changeSet.add(int(line))
2980
2981                for change in changeSet:
2982                    changes.append(change)
2983
2984                changes.sort()
2985            else:
2986                # catch "git p4 sync" with no new branches, in a repo that
2987                # does not have any existing p4 branches
2988                if len(args) == 0:
2989                    if not self.p4BranchesInGit:
2990                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
2991
2992                    # The default branch is master, unless --branch is used to
2993                    # specify something else.  Make sure it exists, or complain
2994                    # nicely about how to use --branch.
2995                    if not self.detectBranches:
2996                        if not branch_exists(self.branch):
2997                            if branch_arg_given:
2998                                die("Error: branch %s does not exist." % self.branch)
2999                            else:
3000                                die("Error: no branch %s; perhaps specify one with --branch." %
3001                                    self.branch)
3002
3003                if self.verbose:
3004                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3005                                                              self.changeRange)
3006                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
3007
3008                if len(self.maxChanges) > 0:
3009                    changes = changes[:min(int(self.maxChanges), len(changes))]
3010
3011            if len(changes) == 0:
3012                if not self.silent:
3013                    print "No changes to import!"
3014            else:
3015                if not self.silent and not self.detectBranches:
3016                    print "Import destination: %s" % self.branch
3017
3018                self.updatedBranches = set()
3019
3020                if not self.detectBranches:
3021                    if args:
3022                        # start a new branch
3023                        self.initialParent = ""
3024                    else:
3025                        # build on a previous revision
3026                        self.initialParent = parseRevision(self.branch)
3027
3028                self.importChanges(changes)
3029
3030                if not self.silent:
3031                    print ""
3032                    if len(self.updatedBranches) > 0:
3033                        sys.stdout.write("Updated branches: ")
3034                        for b in self.updatedBranches:
3035                            sys.stdout.write("%s " % b)
3036                        sys.stdout.write("\n")
3037
3038        if gitConfig("git-p4.importLabels", "--bool") == "true":
3039            self.importLabels = True
3040
3041        if self.importLabels:
3042            p4Labels = getP4Labels(self.depotPaths)
3043            gitTags = getGitTags()
3044
3045            missingP4Labels = p4Labels - gitTags
3046            self.importP4Labels(self.gitStream, missingP4Labels)
3047
3048        self.gitStream.close()
3049        if self.importProcess.wait() != 0:
3050            die("fast-import failed: %s" % self.gitError.read())
3051        self.gitOutput.close()
3052        self.gitError.close()
3053
3054        # Cleanup temporary branches created during import
3055        if self.tempBranches != []:
3056            for branch in self.tempBranches:
3057                read_pipe("git update-ref -d %s" % branch)
3058            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3059
3060        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3061        # a convenient shortcut refname "p4".
3062        if self.importIntoRemotes:
3063            head_ref = self.refPrefix + "HEAD"
3064            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3065                system(["git", "symbolic-ref", head_ref, self.branch])
3066
3067        return True
3068
3069class P4Rebase(Command):
3070    def __init__(self):
3071        Command.__init__(self)
3072        self.options = [
3073                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3074        ]
3075        self.importLabels = False
3076        self.description = ("Fetches the latest revision from perforce and "
3077                            + "rebases the current work (branch) against it")
3078
3079    def run(self, args):
3080        sync = P4Sync()
3081        sync.importLabels = self.importLabels
3082        sync.run([])
3083
3084        return self.rebase()
3085
3086    def rebase(self):
3087        if os.system("git update-index --refresh") != 0:
3088            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.");
3089        if len(read_pipe("git diff-index HEAD --")) > 0:
3090            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
3091
3092        [upstream, settings] = findUpstreamBranchPoint()
3093        if len(upstream) == 0:
3094            die("Cannot find upstream branchpoint for rebase")
3095
3096        # the branchpoint may be p4/foo~3, so strip off the parent
3097        upstream = re.sub("~[0-9]+$", "", upstream)
3098
3099        print "Rebasing the current branch onto %s" % upstream
3100        oldHead = read_pipe("git rev-parse HEAD").strip()
3101        system("git rebase %s" % upstream)
3102        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
3103        return True
3104
3105class P4Clone(P4Sync):
3106    def __init__(self):
3107        P4Sync.__init__(self)
3108        self.description = "Creates a new git repository and imports from Perforce into it"
3109        self.usage = "usage: %prog [options] //depot/path[@revRange]"
3110        self.options += [
3111            optparse.make_option("--destination", dest="cloneDestination",
3112                                 action='store', default=None,
3113                                 help="where to leave result of the clone"),
3114            optparse.make_option("-/", dest="cloneExclude",
3115                                 action="append", type="string",
3116                                 help="exclude depot path"),
3117            optparse.make_option("--bare", dest="cloneBare",
3118                                 action="store_true", default=False),
3119        ]
3120        self.cloneDestination = None
3121        self.needsGit = False
3122        self.cloneBare = False
3123
3124    # This is required for the "append" cloneExclude action
3125    def ensure_value(self, attr, value):
3126        if not hasattr(self, attr) or getattr(self, attr) is None:
3127            setattr(self, attr, value)
3128        return getattr(self, attr)
3129
3130    def defaultDestination(self, args):
3131        ## TODO: use common prefix of args?
3132        depotPath = args[0]
3133        depotDir = re.sub("(@[^@]*)$", "", depotPath)
3134        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3135        depotDir = re.sub(r"\.\.\.$", "", depotDir)
3136        depotDir = re.sub(r"/$", "", depotDir)
3137        return os.path.split(depotDir)[1]
3138
3139    def run(self, args):
3140        if len(args) < 1:
3141            return False
3142
3143        if self.keepRepoPath and not self.cloneDestination:
3144            sys.stderr.write("Must specify destination for --keep-path\n")
3145            sys.exit(1)
3146
3147        depotPaths = args
3148
3149        if not self.cloneDestination and len(depotPaths) > 1:
3150            self.cloneDestination = depotPaths[-1]
3151            depotPaths = depotPaths[:-1]
3152
3153        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3154        for p in depotPaths:
3155            if not p.startswith("//"):
3156                return False
3157
3158        if not self.cloneDestination:
3159            self.cloneDestination = self.defaultDestination(args)
3160
3161        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3162
3163        if not os.path.exists(self.cloneDestination):
3164            os.makedirs(self.cloneDestination)
3165        chdir(self.cloneDestination)
3166
3167        init_cmd = [ "git", "init" ]
3168        if self.cloneBare:
3169            init_cmd.append("--bare")
3170        subprocess.check_call(init_cmd)
3171
3172        if not P4Sync.run(self, depotPaths):
3173            return False
3174
3175        # create a master branch and check out a work tree
3176        if gitBranchExists(self.branch):
3177            system([ "git", "branch", "master", self.branch ])
3178            if not self.cloneBare:
3179                system([ "git", "checkout", "-f" ])
3180        else:
3181            print 'Not checking out any branch, use ' \
3182                  '"git checkout -q -b master <branch>"'
3183
3184        # auto-set this variable if invoked with --use-client-spec
3185        if self.useClientSpec_from_options:
3186            system("git config --bool git-p4.useclientspec true")
3187
3188        return True
3189
3190class P4Branches(Command):
3191    def __init__(self):
3192        Command.__init__(self)
3193        self.options = [ ]
3194        self.description = ("Shows the git branches that hold imports and their "
3195                            + "corresponding perforce depot paths")
3196        self.verbose = False
3197
3198    def run(self, args):
3199        if originP4BranchesExist():
3200            createOrUpdateBranchesFromOrigin()
3201
3202        cmdline = "git rev-parse --symbolic "
3203        cmdline += " --remotes"
3204
3205        for line in read_pipe_lines(cmdline):
3206            line = line.strip()
3207
3208            if not line.startswith('p4/') or line == "p4/HEAD":
3209                continue
3210            branch = line
3211
3212            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3213            settings = extractSettingsGitLog(log)
3214
3215            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3216        return True
3217
3218class HelpFormatter(optparse.IndentedHelpFormatter):
3219    def __init__(self):
3220        optparse.IndentedHelpFormatter.__init__(self)
3221
3222    def format_description(self, description):
3223        if description:
3224            return description + "\n"
3225        else:
3226            return ""
3227
3228def printUsage(commands):
3229    print "usage: %s <command> [options]" % sys.argv[0]
3230    print ""
3231    print "valid commands: %s" % ", ".join(commands)
3232    print ""
3233    print "Try %s <command> --help for command specific help." % sys.argv[0]
3234    print ""
3235
3236commands = {
3237    "debug" : P4Debug,
3238    "submit" : P4Submit,
3239    "commit" : P4Submit,
3240    "sync" : P4Sync,
3241    "rebase" : P4Rebase,
3242    "clone" : P4Clone,
3243    "rollback" : P4RollBack,
3244    "branches" : P4Branches
3245}
3246
3247
3248def main():
3249    if len(sys.argv[1:]) == 0:
3250        printUsage(commands.keys())
3251        sys.exit(2)
3252
3253    cmdName = sys.argv[1]
3254    try:
3255        klass = commands[cmdName]
3256        cmd = klass()
3257    except KeyError:
3258        print "unknown command %s" % cmdName
3259        print ""
3260        printUsage(commands.keys())
3261        sys.exit(2)
3262
3263    options = cmd.options
3264    cmd.gitdir = os.environ.get("GIT_DIR", None)
3265
3266    args = sys.argv[2:]
3267
3268    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3269    if cmd.needsGit:
3270        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3271
3272    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3273                                   options,
3274                                   description = cmd.description,
3275                                   formatter = HelpFormatter())
3276
3277    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3278    global verbose
3279    verbose = cmd.verbose
3280    if cmd.needsGit:
3281        if cmd.gitdir == None:
3282            cmd.gitdir = os.path.abspath(".git")
3283            if not isValidGitDir(cmd.gitdir):
3284                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3285                if os.path.exists(cmd.gitdir):
3286                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3287                    if len(cdup) > 0:
3288                        chdir(cdup);
3289
3290        if not isValidGitDir(cmd.gitdir):
3291            if isValidGitDir(cmd.gitdir + "/.git"):
3292                cmd.gitdir += "/.git"
3293            else:
3294                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3295
3296        os.environ["GIT_DIR"] = cmd.gitdir
3297
3298    if not cmd.run(args):
3299        parser.print_help()
3300        sys.exit(2)
3301
3302
3303if __name__ == '__main__':
3304    main()