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