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