git-p4.pyon commit merge-file: let conflict markers match end-of-line style of the context (86efa21)
   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        submitted = False
1547
1548        try:
1549            if self.edit_template(fileName):
1550                # read the edited message and submit
1551                tmpFile = open(fileName, "rb")
1552                message = tmpFile.read()
1553                tmpFile.close()
1554                if self.isWindows:
1555                    message = message.replace("\r\n", "\n")
1556                submitTemplate = message[:message.index(separatorLine)]
1557                p4_write_pipe(['submit', '-i'], submitTemplate)
1558
1559                if self.preserveUser:
1560                    if p4User:
1561                        # Get last changelist number. Cannot easily get it from
1562                        # the submit command output as the output is
1563                        # unmarshalled.
1564                        changelist = self.lastP4Changelist()
1565                        self.modifyChangelistUser(changelist, p4User)
1566
1567                # The rename/copy happened by applying a patch that created a
1568                # new file.  This leaves it writable, which confuses p4.
1569                for f in pureRenameCopy:
1570                    p4_sync(f, "-f")
1571                submitted = True
1572
1573        finally:
1574            # skip this patch
1575            if not submitted:
1576                print "Submission cancelled, undoing p4 changes."
1577                for f in editedFiles:
1578                    p4_revert(f)
1579                for f in filesToAdd:
1580                    p4_revert(f)
1581                    os.remove(f)
1582                for f in filesToDelete:
1583                    p4_revert(f)
1584
1585        os.remove(fileName)
1586        return submitted
1587
1588    # Export git tags as p4 labels. Create a p4 label and then tag
1589    # with that.
1590    def exportGitTags(self, gitTags):
1591        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1592        if len(validLabelRegexp) == 0:
1593            validLabelRegexp = defaultLabelRegexp
1594        m = re.compile(validLabelRegexp)
1595
1596        for name in gitTags:
1597
1598            if not m.match(name):
1599                if verbose:
1600                    print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1601                continue
1602
1603            # Get the p4 commit this corresponds to
1604            logMessage = extractLogMessageFromGitCommit(name)
1605            values = extractSettingsGitLog(logMessage)
1606
1607            if not values.has_key('change'):
1608                # a tag pointing to something not sent to p4; ignore
1609                if verbose:
1610                    print "git tag %s does not give a p4 commit" % name
1611                continue
1612            else:
1613                changelist = values['change']
1614
1615            # Get the tag details.
1616            inHeader = True
1617            isAnnotated = False
1618            body = []
1619            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1620                l = l.strip()
1621                if inHeader:
1622                    if re.match(r'tag\s+', l):
1623                        isAnnotated = True
1624                    elif re.match(r'\s*$', l):
1625                        inHeader = False
1626                        continue
1627                else:
1628                    body.append(l)
1629
1630            if not isAnnotated:
1631                body = ["lightweight tag imported by git p4\n"]
1632
1633            # Create the label - use the same view as the client spec we are using
1634            clientSpec = getClientSpec()
1635
1636            labelTemplate  = "Label: %s\n" % name
1637            labelTemplate += "Description:\n"
1638            for b in body:
1639                labelTemplate += "\t" + b + "\n"
1640            labelTemplate += "View:\n"
1641            for depot_side in clientSpec.mappings:
1642                labelTemplate += "\t%s\n" % depot_side
1643
1644            if self.dry_run:
1645                print "Would create p4 label %s for tag" % name
1646            elif self.prepare_p4_only:
1647                print "Not creating p4 label %s for tag due to option" \
1648                      " --prepare-p4-only" % name
1649            else:
1650                p4_write_pipe(["label", "-i"], labelTemplate)
1651
1652                # Use the label
1653                p4_system(["tag", "-l", name] +
1654                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1655
1656                if verbose:
1657                    print "created p4 label for tag %s" % name
1658
1659    def run(self, args):
1660        if len(args) == 0:
1661            self.master = currentGitBranch()
1662        elif len(args) == 1:
1663            self.master = args[0]
1664            if not branchExists(self.master):
1665                die("Branch %s does not exist" % self.master)
1666        else:
1667            return False
1668
1669        if self.master:
1670            allowSubmit = gitConfig("git-p4.allowSubmit")
1671            if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1672                die("%s is not in git-p4.allowSubmit" % self.master)
1673
1674        [upstream, settings] = findUpstreamBranchPoint()
1675        self.depotPath = settings['depot-paths'][0]
1676        if len(self.origin) == 0:
1677            self.origin = upstream
1678
1679        if self.preserveUser:
1680            if not self.canChangeChangelists():
1681                die("Cannot preserve user names without p4 super-user or admin permissions")
1682
1683        # if not set from the command line, try the config file
1684        if self.conflict_behavior is None:
1685            val = gitConfig("git-p4.conflict")
1686            if val:
1687                if val not in self.conflict_behavior_choices:
1688                    die("Invalid value '%s' for config git-p4.conflict" % val)
1689            else:
1690                val = "ask"
1691            self.conflict_behavior = val
1692
1693        if self.verbose:
1694            print "Origin branch is " + self.origin
1695
1696        if len(self.depotPath) == 0:
1697            print "Internal error: cannot locate perforce depot path from existing branches"
1698            sys.exit(128)
1699
1700        self.useClientSpec = False
1701        if gitConfigBool("git-p4.useclientspec"):
1702            self.useClientSpec = True
1703        if self.useClientSpec:
1704            self.clientSpecDirs = getClientSpec()
1705
1706        # Check for the existance of P4 branches
1707        branchesDetected = (len(p4BranchesInGit().keys()) > 1)
1708
1709        if self.useClientSpec and not branchesDetected:
1710            # all files are relative to the client spec
1711            self.clientPath = getClientRoot()
1712        else:
1713            self.clientPath = p4Where(self.depotPath)
1714
1715        if self.clientPath == "":
1716            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1717
1718        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1719        self.oldWorkingDirectory = os.getcwd()
1720
1721        # ensure the clientPath exists
1722        new_client_dir = False
1723        if not os.path.exists(self.clientPath):
1724            new_client_dir = True
1725            os.makedirs(self.clientPath)
1726
1727        chdir(self.clientPath, is_client_path=True)
1728        if self.dry_run:
1729            print "Would synchronize p4 checkout in %s" % self.clientPath
1730        else:
1731            print "Synchronizing p4 checkout..."
1732            if new_client_dir:
1733                # old one was destroyed, and maybe nobody told p4
1734                p4_sync("...", "-f")
1735            else:
1736                p4_sync("...")
1737        self.check()
1738
1739        commits = []
1740        if self.master:
1741            commitish = self.master
1742        else:
1743            commitish = 'HEAD'
1744
1745        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
1746            commits.append(line.strip())
1747        commits.reverse()
1748
1749        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1750            self.checkAuthorship = False
1751        else:
1752            self.checkAuthorship = True
1753
1754        if self.preserveUser:
1755            self.checkValidP4Users(commits)
1756
1757        #
1758        # Build up a set of options to be passed to diff when
1759        # submitting each commit to p4.
1760        #
1761        if self.detectRenames:
1762            # command-line -M arg
1763            self.diffOpts = "-M"
1764        else:
1765            # If not explicitly set check the config variable
1766            detectRenames = gitConfig("git-p4.detectRenames")
1767
1768            if detectRenames.lower() == "false" or detectRenames == "":
1769                self.diffOpts = ""
1770            elif detectRenames.lower() == "true":
1771                self.diffOpts = "-M"
1772            else:
1773                self.diffOpts = "-M%s" % detectRenames
1774
1775        # no command-line arg for -C or --find-copies-harder, just
1776        # config variables
1777        detectCopies = gitConfig("git-p4.detectCopies")
1778        if detectCopies.lower() == "false" or detectCopies == "":
1779            pass
1780        elif detectCopies.lower() == "true":
1781            self.diffOpts += " -C"
1782        else:
1783            self.diffOpts += " -C%s" % detectCopies
1784
1785        if gitConfigBool("git-p4.detectCopiesHarder"):
1786            self.diffOpts += " --find-copies-harder"
1787
1788        #
1789        # Apply the commits, one at a time.  On failure, ask if should
1790        # continue to try the rest of the patches, or quit.
1791        #
1792        if self.dry_run:
1793            print "Would apply"
1794        applied = []
1795        last = len(commits) - 1
1796        for i, commit in enumerate(commits):
1797            if self.dry_run:
1798                print " ", read_pipe(["git", "show", "-s",
1799                                      "--format=format:%h %s", commit])
1800                ok = True
1801            else:
1802                ok = self.applyCommit(commit)
1803            if ok:
1804                applied.append(commit)
1805            else:
1806                if self.prepare_p4_only and i < last:
1807                    print "Processing only the first commit due to option" \
1808                          " --prepare-p4-only"
1809                    break
1810                if i < last:
1811                    quit = False
1812                    while True:
1813                        # prompt for what to do, or use the option/variable
1814                        if self.conflict_behavior == "ask":
1815                            print "What do you want to do?"
1816                            response = raw_input("[s]kip this commit but apply"
1817                                                 " the rest, or [q]uit? ")
1818                            if not response:
1819                                continue
1820                        elif self.conflict_behavior == "skip":
1821                            response = "s"
1822                        elif self.conflict_behavior == "quit":
1823                            response = "q"
1824                        else:
1825                            die("Unknown conflict_behavior '%s'" %
1826                                self.conflict_behavior)
1827
1828                        if response[0] == "s":
1829                            print "Skipping this commit, but applying the rest"
1830                            break
1831                        if response[0] == "q":
1832                            print "Quitting"
1833                            quit = True
1834                            break
1835                    if quit:
1836                        break
1837
1838        chdir(self.oldWorkingDirectory)
1839
1840        if self.dry_run:
1841            pass
1842        elif self.prepare_p4_only:
1843            pass
1844        elif len(commits) == len(applied):
1845            print "All commits applied!"
1846
1847            sync = P4Sync()
1848            if self.branch:
1849                sync.branch = self.branch
1850            sync.run([])
1851
1852            rebase = P4Rebase()
1853            rebase.rebase()
1854
1855        else:
1856            if len(applied) == 0:
1857                print "No commits applied."
1858            else:
1859                print "Applied only the commits marked with '*':"
1860                for c in commits:
1861                    if c in applied:
1862                        star = "*"
1863                    else:
1864                        star = " "
1865                    print star, read_pipe(["git", "show", "-s",
1866                                           "--format=format:%h %s",  c])
1867                print "You will have to do 'git p4 sync' and rebase."
1868
1869        if gitConfigBool("git-p4.exportLabels"):
1870            self.exportLabels = True
1871
1872        if self.exportLabels:
1873            p4Labels = getP4Labels(self.depotPath)
1874            gitTags = getGitTags()
1875
1876            missingGitTags = gitTags - p4Labels
1877            self.exportGitTags(missingGitTags)
1878
1879        # exit with error unless everything applied perfectly
1880        if len(commits) != len(applied):
1881                sys.exit(1)
1882
1883        return True
1884
1885class View(object):
1886    """Represent a p4 view ("p4 help views"), and map files in a
1887       repo according to the view."""
1888
1889    def __init__(self, client_name):
1890        self.mappings = []
1891        self.client_prefix = "//%s/" % client_name
1892        # cache results of "p4 where" to lookup client file locations
1893        self.client_spec_path_cache = {}
1894
1895    def append(self, view_line):
1896        """Parse a view line, splitting it into depot and client
1897           sides.  Append to self.mappings, preserving order.  This
1898           is only needed for tag creation."""
1899
1900        # Split the view line into exactly two words.  P4 enforces
1901        # structure on these lines that simplifies this quite a bit.
1902        #
1903        # Either or both words may be double-quoted.
1904        # Single quotes do not matter.
1905        # Double-quote marks cannot occur inside the words.
1906        # A + or - prefix is also inside the quotes.
1907        # There are no quotes unless they contain a space.
1908        # The line is already white-space stripped.
1909        # The two words are separated by a single space.
1910        #
1911        if view_line[0] == '"':
1912            # First word is double quoted.  Find its end.
1913            close_quote_index = view_line.find('"', 1)
1914            if close_quote_index <= 0:
1915                die("No first-word closing quote found: %s" % view_line)
1916            depot_side = view_line[1:close_quote_index]
1917            # skip closing quote and space
1918            rhs_index = close_quote_index + 1 + 1
1919        else:
1920            space_index = view_line.find(" ")
1921            if space_index <= 0:
1922                die("No word-splitting space found: %s" % view_line)
1923            depot_side = view_line[0:space_index]
1924            rhs_index = space_index + 1
1925
1926        # prefix + means overlay on previous mapping
1927        if depot_side.startswith("+"):
1928            depot_side = depot_side[1:]
1929
1930        # prefix - means exclude this path, leave out of mappings
1931        exclude = False
1932        if depot_side.startswith("-"):
1933            exclude = True
1934            depot_side = depot_side[1:]
1935
1936        if not exclude:
1937            self.mappings.append(depot_side)
1938
1939    def convert_client_path(self, clientFile):
1940        # chop off //client/ part to make it relative
1941        if not clientFile.startswith(self.client_prefix):
1942            die("No prefix '%s' on clientFile '%s'" %
1943                (self.client_prefix, clientFile))
1944        return clientFile[len(self.client_prefix):]
1945
1946    def update_client_spec_path_cache(self, files):
1947        """ Caching file paths by "p4 where" batch query """
1948
1949        # List depot file paths exclude that already cached
1950        fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1951
1952        if len(fileArgs) == 0:
1953            return  # All files in cache
1954
1955        where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1956        for res in where_result:
1957            if "code" in res and res["code"] == "error":
1958                # assume error is "... file(s) not in client view"
1959                continue
1960            if "clientFile" not in res:
1961                die("No clientFile in 'p4 where' output")
1962            if "unmap" in res:
1963                # it will list all of them, but only one not unmap-ped
1964                continue
1965            if gitConfigBool("core.ignorecase"):
1966                res['depotFile'] = res['depotFile'].lower()
1967            self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1968
1969        # not found files or unmap files set to ""
1970        for depotFile in fileArgs:
1971            if gitConfigBool("core.ignorecase"):
1972                depotFile = depotFile.lower()
1973            if depotFile not in self.client_spec_path_cache:
1974                self.client_spec_path_cache[depotFile] = ""
1975
1976    def map_in_client(self, depot_path):
1977        """Return the relative location in the client where this
1978           depot file should live.  Returns "" if the file should
1979           not be mapped in the client."""
1980
1981        if gitConfigBool("core.ignorecase"):
1982            depot_path = depot_path.lower()
1983
1984        if depot_path in self.client_spec_path_cache:
1985            return self.client_spec_path_cache[depot_path]
1986
1987        die( "Error: %s is not found in client spec path" % depot_path )
1988        return ""
1989
1990class P4Sync(Command, P4UserMap):
1991    delete_actions = ( "delete", "move/delete", "purge" )
1992
1993    def __init__(self):
1994        Command.__init__(self)
1995        P4UserMap.__init__(self)
1996        self.options = [
1997                optparse.make_option("--branch", dest="branch"),
1998                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1999                optparse.make_option("--changesfile", dest="changesFile"),
2000                optparse.make_option("--silent", dest="silent", action="store_true"),
2001                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2002                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2003                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2004                                     help="Import into refs/heads/ , not refs/remotes"),
2005                optparse.make_option("--max-changes", dest="maxChanges",
2006                                     help="Maximum number of changes to import"),
2007                optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2008                                     help="Internal block size to use when iteratively calling p4 changes"),
2009                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2010                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2011                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2012                                     help="Only sync files that are included in the Perforce Client Spec"),
2013                optparse.make_option("-/", dest="cloneExclude",
2014                                     action="append", type="string",
2015                                     help="exclude depot path"),
2016        ]
2017        self.description = """Imports from Perforce into a git repository.\n
2018    example:
2019    //depot/my/project/ -- to import the current head
2020    //depot/my/project/@all -- to import everything
2021    //depot/my/project/@1,6 -- to import only from revision 1 to 6
2022
2023    (a ... is not needed in the path p4 specification, it's added implicitly)"""
2024
2025        self.usage += " //depot/path[@revRange]"
2026        self.silent = False
2027        self.createdBranches = set()
2028        self.committedChanges = set()
2029        self.branch = ""
2030        self.detectBranches = False
2031        self.detectLabels = False
2032        self.importLabels = False
2033        self.changesFile = ""
2034        self.syncWithOrigin = True
2035        self.importIntoRemotes = True
2036        self.maxChanges = ""
2037        self.changes_block_size = None
2038        self.keepRepoPath = False
2039        self.depotPaths = None
2040        self.p4BranchesInGit = []
2041        self.cloneExclude = []
2042        self.useClientSpec = False
2043        self.useClientSpec_from_options = False
2044        self.clientSpecDirs = None
2045        self.tempBranches = []
2046        self.tempBranchLocation = "git-p4-tmp"
2047
2048        if gitConfig("git-p4.syncFromOrigin") == "false":
2049            self.syncWithOrigin = False
2050
2051    # This is required for the "append" cloneExclude action
2052    def ensure_value(self, attr, value):
2053        if not hasattr(self, attr) or getattr(self, attr) is None:
2054            setattr(self, attr, value)
2055        return getattr(self, attr)
2056
2057    # Force a checkpoint in fast-import and wait for it to finish
2058    def checkpoint(self):
2059        self.gitStream.write("checkpoint\n\n")
2060        self.gitStream.write("progress checkpoint\n\n")
2061        out = self.gitOutput.readline()
2062        if self.verbose:
2063            print "checkpoint finished: " + out
2064
2065    def extractFilesFromCommit(self, commit):
2066        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2067                             for path in self.cloneExclude]
2068        files = []
2069        fnum = 0
2070        while commit.has_key("depotFile%s" % fnum):
2071            path =  commit["depotFile%s" % fnum]
2072
2073            if [p for p in self.cloneExclude
2074                if p4PathStartsWith(path, p)]:
2075                found = False
2076            else:
2077                found = [p for p in self.depotPaths
2078                         if p4PathStartsWith(path, p)]
2079            if not found:
2080                fnum = fnum + 1
2081                continue
2082
2083            file = {}
2084            file["path"] = path
2085            file["rev"] = commit["rev%s" % fnum]
2086            file["action"] = commit["action%s" % fnum]
2087            file["type"] = commit["type%s" % fnum]
2088            files.append(file)
2089            fnum = fnum + 1
2090        return files
2091
2092    def stripRepoPath(self, path, prefixes):
2093        """When streaming files, this is called to map a p4 depot path
2094           to where it should go in git.  The prefixes are either
2095           self.depotPaths, or self.branchPrefixes in the case of
2096           branch detection."""
2097
2098        if self.useClientSpec:
2099            # branch detection moves files up a level (the branch name)
2100            # from what client spec interpretation gives
2101            path = self.clientSpecDirs.map_in_client(path)
2102            if self.detectBranches:
2103                for b in self.knownBranches:
2104                    if path.startswith(b + "/"):
2105                        path = path[len(b)+1:]
2106
2107        elif self.keepRepoPath:
2108            # Preserve everything in relative path name except leading
2109            # //depot/; just look at first prefix as they all should
2110            # be in the same depot.
2111            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2112            if p4PathStartsWith(path, depot):
2113                path = path[len(depot):]
2114
2115        else:
2116            for p in prefixes:
2117                if p4PathStartsWith(path, p):
2118                    path = path[len(p):]
2119                    break
2120
2121        path = wildcard_decode(path)
2122        return path
2123
2124    def splitFilesIntoBranches(self, commit):
2125        """Look at each depotFile in the commit to figure out to what
2126           branch it belongs."""
2127
2128        if self.clientSpecDirs:
2129            files = self.extractFilesFromCommit(commit)
2130            self.clientSpecDirs.update_client_spec_path_cache(files)
2131
2132        branches = {}
2133        fnum = 0
2134        while commit.has_key("depotFile%s" % fnum):
2135            path =  commit["depotFile%s" % fnum]
2136            found = [p for p in self.depotPaths
2137                     if p4PathStartsWith(path, p)]
2138            if not found:
2139                fnum = fnum + 1
2140                continue
2141
2142            file = {}
2143            file["path"] = path
2144            file["rev"] = commit["rev%s" % fnum]
2145            file["action"] = commit["action%s" % fnum]
2146            file["type"] = commit["type%s" % fnum]
2147            fnum = fnum + 1
2148
2149            # start with the full relative path where this file would
2150            # go in a p4 client
2151            if self.useClientSpec:
2152                relPath = self.clientSpecDirs.map_in_client(path)
2153            else:
2154                relPath = self.stripRepoPath(path, self.depotPaths)
2155
2156            for branch in self.knownBranches.keys():
2157                # add a trailing slash so that a commit into qt/4.2foo
2158                # doesn't end up in qt/4.2, e.g.
2159                if relPath.startswith(branch + "/"):
2160                    if branch not in branches:
2161                        branches[branch] = []
2162                    branches[branch].append(file)
2163                    break
2164
2165        return branches
2166
2167    # output one file from the P4 stream
2168    # - helper for streamP4Files
2169
2170    def streamOneP4File(self, file, contents):
2171        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2172        if verbose:
2173            sys.stderr.write("%s\n" % relPath)
2174
2175        (type_base, type_mods) = split_p4_type(file["type"])
2176
2177        git_mode = "100644"
2178        if "x" in type_mods:
2179            git_mode = "100755"
2180        if type_base == "symlink":
2181            git_mode = "120000"
2182            # p4 print on a symlink sometimes contains "target\n";
2183            # if it does, remove the newline
2184            data = ''.join(contents)
2185            if not data:
2186                # Some version of p4 allowed creating a symlink that pointed
2187                # to nothing.  This causes p4 errors when checking out such
2188                # a change, and errors here too.  Work around it by ignoring
2189                # the bad symlink; hopefully a future change fixes it.
2190                print "\nIgnoring empty symlink in %s" % file['depotFile']
2191                return
2192            elif data[-1] == '\n':
2193                contents = [data[:-1]]
2194            else:
2195                contents = [data]
2196
2197        if type_base == "utf16":
2198            # p4 delivers different text in the python output to -G
2199            # than it does when using "print -o", or normal p4 client
2200            # operations.  utf16 is converted to ascii or utf8, perhaps.
2201            # But ascii text saved as -t utf16 is completely mangled.
2202            # Invoke print -o to get the real contents.
2203            #
2204            # On windows, the newlines will always be mangled by print, so put
2205            # them back too.  This is not needed to the cygwin windows version,
2206            # just the native "NT" type.
2207            #
2208            try:
2209                text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2210            except Exception as e:
2211                if 'Translation of file content failed' in str(e):
2212                    type_base = 'binary'
2213                else:
2214                    raise e
2215            else:
2216                if p4_version_string().find('/NT') >= 0:
2217                    text = text.replace('\r\n', '\n')
2218                contents = [ text ]
2219
2220        if type_base == "apple":
2221            # Apple filetype files will be streamed as a concatenation of
2222            # its appledouble header and the contents.  This is useless
2223            # on both macs and non-macs.  If using "print -q -o xx", it
2224            # will create "xx" with the data, and "%xx" with the header.
2225            # This is also not very useful.
2226            #
2227            # Ideally, someday, this script can learn how to generate
2228            # appledouble files directly and import those to git, but
2229            # non-mac machines can never find a use for apple filetype.
2230            print "\nIgnoring apple filetype file %s" % file['depotFile']
2231            return
2232
2233        # Note that we do not try to de-mangle keywords on utf16 files,
2234        # even though in theory somebody may want that.
2235        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2236        if pattern:
2237            regexp = re.compile(pattern, re.VERBOSE)
2238            text = ''.join(contents)
2239            text = regexp.sub(r'$\1$', text)
2240            contents = [ text ]
2241
2242        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2243
2244        # total length...
2245        length = 0
2246        for d in contents:
2247            length = length + len(d)
2248
2249        self.gitStream.write("data %d\n" % length)
2250        for d in contents:
2251            self.gitStream.write(d)
2252        self.gitStream.write("\n")
2253
2254    def streamOneP4Deletion(self, file):
2255        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2256        if verbose:
2257            sys.stderr.write("delete %s\n" % relPath)
2258        self.gitStream.write("D %s\n" % relPath)
2259
2260    # handle another chunk of streaming data
2261    def streamP4FilesCb(self, marshalled):
2262
2263        # catch p4 errors and complain
2264        err = None
2265        if "code" in marshalled:
2266            if marshalled["code"] == "error":
2267                if "data" in marshalled:
2268                    err = marshalled["data"].rstrip()
2269        if err:
2270            f = None
2271            if self.stream_have_file_info:
2272                if "depotFile" in self.stream_file:
2273                    f = self.stream_file["depotFile"]
2274            # force a failure in fast-import, else an empty
2275            # commit will be made
2276            self.gitStream.write("\n")
2277            self.gitStream.write("die-now\n")
2278            self.gitStream.close()
2279            # ignore errors, but make sure it exits first
2280            self.importProcess.wait()
2281            if f:
2282                die("Error from p4 print for %s: %s" % (f, err))
2283            else:
2284                die("Error from p4 print: %s" % err)
2285
2286        if marshalled.has_key('depotFile') and self.stream_have_file_info:
2287            # start of a new file - output the old one first
2288            self.streamOneP4File(self.stream_file, self.stream_contents)
2289            self.stream_file = {}
2290            self.stream_contents = []
2291            self.stream_have_file_info = False
2292
2293        # pick up the new file information... for the
2294        # 'data' field we need to append to our array
2295        for k in marshalled.keys():
2296            if k == 'data':
2297                self.stream_contents.append(marshalled['data'])
2298            else:
2299                self.stream_file[k] = marshalled[k]
2300
2301        self.stream_have_file_info = True
2302
2303    # Stream directly from "p4 files" into "git fast-import"
2304    def streamP4Files(self, files):
2305        filesForCommit = []
2306        filesToRead = []
2307        filesToDelete = []
2308
2309        for f in files:
2310            filesForCommit.append(f)
2311            if f['action'] in self.delete_actions:
2312                filesToDelete.append(f)
2313            else:
2314                filesToRead.append(f)
2315
2316        # deleted files...
2317        for f in filesToDelete:
2318            self.streamOneP4Deletion(f)
2319
2320        if len(filesToRead) > 0:
2321            self.stream_file = {}
2322            self.stream_contents = []
2323            self.stream_have_file_info = False
2324
2325            # curry self argument
2326            def streamP4FilesCbSelf(entry):
2327                self.streamP4FilesCb(entry)
2328
2329            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2330
2331            p4CmdList(["-x", "-", "print"],
2332                      stdin=fileArgs,
2333                      cb=streamP4FilesCbSelf)
2334
2335            # do the last chunk
2336            if self.stream_file.has_key('depotFile'):
2337                self.streamOneP4File(self.stream_file, self.stream_contents)
2338
2339    def make_email(self, userid):
2340        if userid in self.users:
2341            return self.users[userid]
2342        else:
2343            return "%s <a@b>" % userid
2344
2345    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2346        """ Stream a p4 tag.
2347        commit is either a git commit, or a fast-import mark, ":<p4commit>"
2348        """
2349
2350        if verbose:
2351            print "writing tag %s for commit %s" % (labelName, commit)
2352        gitStream.write("tag %s\n" % labelName)
2353        gitStream.write("from %s\n" % commit)
2354
2355        if labelDetails.has_key('Owner'):
2356            owner = labelDetails["Owner"]
2357        else:
2358            owner = None
2359
2360        # Try to use the owner of the p4 label, or failing that,
2361        # the current p4 user id.
2362        if owner:
2363            email = self.make_email(owner)
2364        else:
2365            email = self.make_email(self.p4UserId())
2366        tagger = "%s %s %s" % (email, epoch, self.tz)
2367
2368        gitStream.write("tagger %s\n" % tagger)
2369
2370        print "labelDetails=",labelDetails
2371        if labelDetails.has_key('Description'):
2372            description = labelDetails['Description']
2373        else:
2374            description = 'Label from git p4'
2375
2376        gitStream.write("data %d\n" % len(description))
2377        gitStream.write(description)
2378        gitStream.write("\n")
2379
2380    def inClientSpec(self, path):
2381        if not self.clientSpecDirs:
2382            return True
2383        inClientSpec = self.clientSpecDirs.map_in_client(path)
2384        if not inClientSpec and self.verbose:
2385            print('Ignoring file outside of client spec: {0}'.format(path))
2386        return inClientSpec
2387
2388    def hasBranchPrefix(self, path):
2389        if not self.branchPrefixes:
2390            return True
2391        hasPrefix = [p for p in self.branchPrefixes
2392                        if p4PathStartsWith(path, p)]
2393        if hasPrefix and self.verbose:
2394            print('Ignoring file outside of prefix: {0}'.format(path))
2395        return hasPrefix
2396
2397    def commit(self, details, files, branch, parent = ""):
2398        epoch = details["time"]
2399        author = details["user"]
2400
2401        if self.verbose:
2402            print('commit into {0}'.format(branch))
2403
2404        if self.clientSpecDirs:
2405            self.clientSpecDirs.update_client_spec_path_cache(files)
2406
2407        files = [f for f in files
2408            if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
2409
2410        if not files and not gitConfigBool('git-p4.keepEmptyCommits'):
2411            print('Ignoring revision {0} as it would produce an empty commit.'
2412                .format(details['change']))
2413            return
2414
2415        self.gitStream.write("commit %s\n" % branch)
2416        self.gitStream.write("mark :%s\n" % details["change"])
2417        self.committedChanges.add(int(details["change"]))
2418        committer = ""
2419        if author not in self.users:
2420            self.getUserMapFromPerforceServer()
2421        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2422
2423        self.gitStream.write("committer %s\n" % committer)
2424
2425        self.gitStream.write("data <<EOT\n")
2426        self.gitStream.write(details["desc"])
2427        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2428                             (','.join(self.branchPrefixes), details["change"]))
2429        if len(details['options']) > 0:
2430            self.gitStream.write(": options = %s" % details['options'])
2431        self.gitStream.write("]\nEOT\n\n")
2432
2433        if len(parent) > 0:
2434            if self.verbose:
2435                print "parent %s" % parent
2436            self.gitStream.write("from %s\n" % parent)
2437
2438        self.streamP4Files(files)
2439        self.gitStream.write("\n")
2440
2441        change = int(details["change"])
2442
2443        if self.labels.has_key(change):
2444            label = self.labels[change]
2445            labelDetails = label[0]
2446            labelRevisions = label[1]
2447            if self.verbose:
2448                print "Change %s is labelled %s" % (change, labelDetails)
2449
2450            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2451                                                for p in self.branchPrefixes])
2452
2453            if len(files) == len(labelRevisions):
2454
2455                cleanedFiles = {}
2456                for info in files:
2457                    if info["action"] in self.delete_actions:
2458                        continue
2459                    cleanedFiles[info["depotFile"]] = info["rev"]
2460
2461                if cleanedFiles == labelRevisions:
2462                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2463
2464                else:
2465                    if not self.silent:
2466                        print ("Tag %s does not match with change %s: files do not match."
2467                               % (labelDetails["label"], change))
2468
2469            else:
2470                if not self.silent:
2471                    print ("Tag %s does not match with change %s: file count is different."
2472                           % (labelDetails["label"], change))
2473
2474    # Build a dictionary of changelists and labels, for "detect-labels" option.
2475    def getLabels(self):
2476        self.labels = {}
2477
2478        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2479        if len(l) > 0 and not self.silent:
2480            print "Finding files belonging to labels in %s" % `self.depotPaths`
2481
2482        for output in l:
2483            label = output["label"]
2484            revisions = {}
2485            newestChange = 0
2486            if self.verbose:
2487                print "Querying files for label %s" % label
2488            for file in p4CmdList(["files"] +
2489                                      ["%s...@%s" % (p, label)
2490                                          for p in self.depotPaths]):
2491                revisions[file["depotFile"]] = file["rev"]
2492                change = int(file["change"])
2493                if change > newestChange:
2494                    newestChange = change
2495
2496            self.labels[newestChange] = [output, revisions]
2497
2498        if self.verbose:
2499            print "Label changes: %s" % self.labels.keys()
2500
2501    # Import p4 labels as git tags. A direct mapping does not
2502    # exist, so assume that if all the files are at the same revision
2503    # then we can use that, or it's something more complicated we should
2504    # just ignore.
2505    def importP4Labels(self, stream, p4Labels):
2506        if verbose:
2507            print "import p4 labels: " + ' '.join(p4Labels)
2508
2509        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2510        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2511        if len(validLabelRegexp) == 0:
2512            validLabelRegexp = defaultLabelRegexp
2513        m = re.compile(validLabelRegexp)
2514
2515        for name in p4Labels:
2516            commitFound = False
2517
2518            if not m.match(name):
2519                if verbose:
2520                    print "label %s does not match regexp %s" % (name,validLabelRegexp)
2521                continue
2522
2523            if name in ignoredP4Labels:
2524                continue
2525
2526            labelDetails = p4CmdList(['label', "-o", name])[0]
2527
2528            # get the most recent changelist for each file in this label
2529            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2530                                for p in self.depotPaths])
2531
2532            if change.has_key('change'):
2533                # find the corresponding git commit; take the oldest commit
2534                changelist = int(change['change'])
2535                if changelist in self.committedChanges:
2536                    gitCommit = ":%d" % changelist       # use a fast-import mark
2537                    commitFound = True
2538                else:
2539                    gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2540                        "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
2541                    if len(gitCommit) == 0:
2542                        print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
2543                    else:
2544                        commitFound = True
2545                        gitCommit = gitCommit.strip()
2546
2547                if commitFound:
2548                    # Convert from p4 time format
2549                    try:
2550                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2551                    except ValueError:
2552                        print "Could not convert label time %s" % labelDetails['Update']
2553                        tmwhen = 1
2554
2555                    when = int(time.mktime(tmwhen))
2556                    self.streamTag(stream, name, labelDetails, gitCommit, when)
2557                    if verbose:
2558                        print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2559            else:
2560                if verbose:
2561                    print "Label %s has no changelists - possibly deleted?" % name
2562
2563            if not commitFound:
2564                # We can't import this label; don't try again as it will get very
2565                # expensive repeatedly fetching all the files for labels that will
2566                # never be imported. If the label is moved in the future, the
2567                # ignore will need to be removed manually.
2568                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2569
2570    def guessProjectName(self):
2571        for p in self.depotPaths:
2572            if p.endswith("/"):
2573                p = p[:-1]
2574            p = p[p.strip().rfind("/") + 1:]
2575            if not p.endswith("/"):
2576               p += "/"
2577            return p
2578
2579    def getBranchMapping(self):
2580        lostAndFoundBranches = set()
2581
2582        user = gitConfig("git-p4.branchUser")
2583        if len(user) > 0:
2584            command = "branches -u %s" % user
2585        else:
2586            command = "branches"
2587
2588        for info in p4CmdList(command):
2589            details = p4Cmd(["branch", "-o", info["branch"]])
2590            viewIdx = 0
2591            while details.has_key("View%s" % viewIdx):
2592                paths = details["View%s" % viewIdx].split(" ")
2593                viewIdx = viewIdx + 1
2594                # require standard //depot/foo/... //depot/bar/... mapping
2595                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2596                    continue
2597                source = paths[0]
2598                destination = paths[1]
2599                ## HACK
2600                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2601                    source = source[len(self.depotPaths[0]):-4]
2602                    destination = destination[len(self.depotPaths[0]):-4]
2603
2604                    if destination in self.knownBranches:
2605                        if not self.silent:
2606                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2607                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2608                        continue
2609
2610                    self.knownBranches[destination] = source
2611
2612                    lostAndFoundBranches.discard(destination)
2613
2614                    if source not in self.knownBranches:
2615                        lostAndFoundBranches.add(source)
2616
2617        # Perforce does not strictly require branches to be defined, so we also
2618        # check git config for a branch list.
2619        #
2620        # Example of branch definition in git config file:
2621        # [git-p4]
2622        #   branchList=main:branchA
2623        #   branchList=main:branchB
2624        #   branchList=branchA:branchC
2625        configBranches = gitConfigList("git-p4.branchList")
2626        for branch in configBranches:
2627            if branch:
2628                (source, destination) = branch.split(":")
2629                self.knownBranches[destination] = source
2630
2631                lostAndFoundBranches.discard(destination)
2632
2633                if source not in self.knownBranches:
2634                    lostAndFoundBranches.add(source)
2635
2636
2637        for branch in lostAndFoundBranches:
2638            self.knownBranches[branch] = branch
2639
2640    def getBranchMappingFromGitBranches(self):
2641        branches = p4BranchesInGit(self.importIntoRemotes)
2642        for branch in branches.keys():
2643            if branch == "master":
2644                branch = "main"
2645            else:
2646                branch = branch[len(self.projectName):]
2647            self.knownBranches[branch] = branch
2648
2649    def updateOptionDict(self, d):
2650        option_keys = {}
2651        if self.keepRepoPath:
2652            option_keys['keepRepoPath'] = 1
2653
2654        d["options"] = ' '.join(sorted(option_keys.keys()))
2655
2656    def readOptions(self, d):
2657        self.keepRepoPath = (d.has_key('options')
2658                             and ('keepRepoPath' in d['options']))
2659
2660    def gitRefForBranch(self, branch):
2661        if branch == "main":
2662            return self.refPrefix + "master"
2663
2664        if len(branch) <= 0:
2665            return branch
2666
2667        return self.refPrefix + self.projectName + branch
2668
2669    def gitCommitByP4Change(self, ref, change):
2670        if self.verbose:
2671            print "looking in ref " + ref + " for change %s using bisect..." % change
2672
2673        earliestCommit = ""
2674        latestCommit = parseRevision(ref)
2675
2676        while True:
2677            if self.verbose:
2678                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2679            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2680            if len(next) == 0:
2681                if self.verbose:
2682                    print "argh"
2683                return ""
2684            log = extractLogMessageFromGitCommit(next)
2685            settings = extractSettingsGitLog(log)
2686            currentChange = int(settings['change'])
2687            if self.verbose:
2688                print "current change %s" % currentChange
2689
2690            if currentChange == change:
2691                if self.verbose:
2692                    print "found %s" % next
2693                return next
2694
2695            if currentChange < change:
2696                earliestCommit = "^%s" % next
2697            else:
2698                latestCommit = "%s" % next
2699
2700        return ""
2701
2702    def importNewBranch(self, branch, maxChange):
2703        # make fast-import flush all changes to disk and update the refs using the checkpoint
2704        # command so that we can try to find the branch parent in the git history
2705        self.gitStream.write("checkpoint\n\n");
2706        self.gitStream.flush();
2707        branchPrefix = self.depotPaths[0] + branch + "/"
2708        range = "@1,%s" % maxChange
2709        #print "prefix" + branchPrefix
2710        changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2711        if len(changes) <= 0:
2712            return False
2713        firstChange = changes[0]
2714        #print "first change in branch: %s" % firstChange
2715        sourceBranch = self.knownBranches[branch]
2716        sourceDepotPath = self.depotPaths[0] + sourceBranch
2717        sourceRef = self.gitRefForBranch(sourceBranch)
2718        #print "source " + sourceBranch
2719
2720        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2721        #print "branch parent: %s" % branchParentChange
2722        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2723        if len(gitParent) > 0:
2724            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2725            #print "parent git commit: %s" % gitParent
2726
2727        self.importChanges(changes)
2728        return True
2729
2730    def searchParent(self, parent, branch, target):
2731        parentFound = False
2732        for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2733                                     "--no-merges", parent]):
2734            blob = blob.strip()
2735            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2736                parentFound = True
2737                if self.verbose:
2738                    print "Found parent of %s in commit %s" % (branch, blob)
2739                break
2740        if parentFound:
2741            return blob
2742        else:
2743            return None
2744
2745    def importChanges(self, changes):
2746        cnt = 1
2747        for change in changes:
2748            description = p4_describe(change)
2749            self.updateOptionDict(description)
2750
2751            if not self.silent:
2752                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2753                sys.stdout.flush()
2754            cnt = cnt + 1
2755
2756            try:
2757                if self.detectBranches:
2758                    branches = self.splitFilesIntoBranches(description)
2759                    for branch in branches.keys():
2760                        ## HACK  --hwn
2761                        branchPrefix = self.depotPaths[0] + branch + "/"
2762                        self.branchPrefixes = [ branchPrefix ]
2763
2764                        parent = ""
2765
2766                        filesForCommit = branches[branch]
2767
2768                        if self.verbose:
2769                            print "branch is %s" % branch
2770
2771                        self.updatedBranches.add(branch)
2772
2773                        if branch not in self.createdBranches:
2774                            self.createdBranches.add(branch)
2775                            parent = self.knownBranches[branch]
2776                            if parent == branch:
2777                                parent = ""
2778                            else:
2779                                fullBranch = self.projectName + branch
2780                                if fullBranch not in self.p4BranchesInGit:
2781                                    if not self.silent:
2782                                        print("\n    Importing new branch %s" % fullBranch);
2783                                    if self.importNewBranch(branch, change - 1):
2784                                        parent = ""
2785                                        self.p4BranchesInGit.append(fullBranch)
2786                                    if not self.silent:
2787                                        print("\n    Resuming with change %s" % change);
2788
2789                                if self.verbose:
2790                                    print "parent determined through known branches: %s" % parent
2791
2792                        branch = self.gitRefForBranch(branch)
2793                        parent = self.gitRefForBranch(parent)
2794
2795                        if self.verbose:
2796                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2797
2798                        if len(parent) == 0 and branch in self.initialParents:
2799                            parent = self.initialParents[branch]
2800                            del self.initialParents[branch]
2801
2802                        blob = None
2803                        if len(parent) > 0:
2804                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2805                            if self.verbose:
2806                                print "Creating temporary branch: " + tempBranch
2807                            self.commit(description, filesForCommit, tempBranch)
2808                            self.tempBranches.append(tempBranch)
2809                            self.checkpoint()
2810                            blob = self.searchParent(parent, branch, tempBranch)
2811                        if blob:
2812                            self.commit(description, filesForCommit, branch, blob)
2813                        else:
2814                            if self.verbose:
2815                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2816                            self.commit(description, filesForCommit, branch, parent)
2817                else:
2818                    files = self.extractFilesFromCommit(description)
2819                    self.commit(description, files, self.branch,
2820                                self.initialParent)
2821                    # only needed once, to connect to the previous commit
2822                    self.initialParent = ""
2823            except IOError:
2824                print self.gitError.read()
2825                sys.exit(1)
2826
2827    def importHeadRevision(self, revision):
2828        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2829
2830        details = {}
2831        details["user"] = "git perforce import user"
2832        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2833                           % (' '.join(self.depotPaths), revision))
2834        details["change"] = revision
2835        newestRevision = 0
2836
2837        fileCnt = 0
2838        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2839
2840        for info in p4CmdList(["files"] + fileArgs):
2841
2842            if 'code' in info and info['code'] == 'error':
2843                sys.stderr.write("p4 returned an error: %s\n"
2844                                 % info['data'])
2845                if info['data'].find("must refer to client") >= 0:
2846                    sys.stderr.write("This particular p4 error is misleading.\n")
2847                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2848                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2849                sys.exit(1)
2850            if 'p4ExitCode' in info:
2851                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2852                sys.exit(1)
2853
2854
2855            change = int(info["change"])
2856            if change > newestRevision:
2857                newestRevision = change
2858
2859            if info["action"] in self.delete_actions:
2860                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2861                #fileCnt = fileCnt + 1
2862                continue
2863
2864            for prop in ["depotFile", "rev", "action", "type" ]:
2865                details["%s%s" % (prop, fileCnt)] = info[prop]
2866
2867            fileCnt = fileCnt + 1
2868
2869        details["change"] = newestRevision
2870
2871        # Use time from top-most change so that all git p4 clones of
2872        # the same p4 repo have the same commit SHA1s.
2873        res = p4_describe(newestRevision)
2874        details["time"] = res["time"]
2875
2876        self.updateOptionDict(details)
2877        try:
2878            self.commit(details, self.extractFilesFromCommit(details), self.branch)
2879        except IOError:
2880            print "IO error with git fast-import. Is your git version recent enough?"
2881            print self.gitError.read()
2882
2883
2884    def run(self, args):
2885        self.depotPaths = []
2886        self.changeRange = ""
2887        self.previousDepotPaths = []
2888        self.hasOrigin = False
2889
2890        # map from branch depot path to parent branch
2891        self.knownBranches = {}
2892        self.initialParents = {}
2893
2894        if self.importIntoRemotes:
2895            self.refPrefix = "refs/remotes/p4/"
2896        else:
2897            self.refPrefix = "refs/heads/p4/"
2898
2899        if self.syncWithOrigin:
2900            self.hasOrigin = originP4BranchesExist()
2901            if self.hasOrigin:
2902                if not self.silent:
2903                    print 'Syncing with origin first, using "git fetch origin"'
2904                system("git fetch origin")
2905
2906        branch_arg_given = bool(self.branch)
2907        if len(self.branch) == 0:
2908            self.branch = self.refPrefix + "master"
2909            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2910                system("git update-ref %s refs/heads/p4" % self.branch)
2911                system("git branch -D p4")
2912
2913        # accept either the command-line option, or the configuration variable
2914        if self.useClientSpec:
2915            # will use this after clone to set the variable
2916            self.useClientSpec_from_options = True
2917        else:
2918            if gitConfigBool("git-p4.useclientspec"):
2919                self.useClientSpec = True
2920        if self.useClientSpec:
2921            self.clientSpecDirs = getClientSpec()
2922
2923        # TODO: should always look at previous commits,
2924        # merge with previous imports, if possible.
2925        if args == []:
2926            if self.hasOrigin:
2927                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2928
2929            # branches holds mapping from branch name to sha1
2930            branches = p4BranchesInGit(self.importIntoRemotes)
2931
2932            # restrict to just this one, disabling detect-branches
2933            if branch_arg_given:
2934                short = self.branch.split("/")[-1]
2935                if short in branches:
2936                    self.p4BranchesInGit = [ short ]
2937            else:
2938                self.p4BranchesInGit = branches.keys()
2939
2940            if len(self.p4BranchesInGit) > 1:
2941                if not self.silent:
2942                    print "Importing from/into multiple branches"
2943                self.detectBranches = True
2944                for branch in branches.keys():
2945                    self.initialParents[self.refPrefix + branch] = \
2946                        branches[branch]
2947
2948            if self.verbose:
2949                print "branches: %s" % self.p4BranchesInGit
2950
2951            p4Change = 0
2952            for branch in self.p4BranchesInGit:
2953                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2954
2955                settings = extractSettingsGitLog(logMsg)
2956
2957                self.readOptions(settings)
2958                if (settings.has_key('depot-paths')
2959                    and settings.has_key ('change')):
2960                    change = int(settings['change']) + 1
2961                    p4Change = max(p4Change, change)
2962
2963                    depotPaths = sorted(settings['depot-paths'])
2964                    if self.previousDepotPaths == []:
2965                        self.previousDepotPaths = depotPaths
2966                    else:
2967                        paths = []
2968                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2969                            prev_list = prev.split("/")
2970                            cur_list = cur.split("/")
2971                            for i in range(0, min(len(cur_list), len(prev_list))):
2972                                if cur_list[i] <> prev_list[i]:
2973                                    i = i - 1
2974                                    break
2975
2976                            paths.append ("/".join(cur_list[:i + 1]))
2977
2978                        self.previousDepotPaths = paths
2979
2980            if p4Change > 0:
2981                self.depotPaths = sorted(self.previousDepotPaths)
2982                self.changeRange = "@%s,#head" % p4Change
2983                if not self.silent and not self.detectBranches:
2984                    print "Performing incremental import into %s git branch" % self.branch
2985
2986        # accept multiple ref name abbreviations:
2987        #    refs/foo/bar/branch -> use it exactly
2988        #    p4/branch -> prepend refs/remotes/ or refs/heads/
2989        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2990        if not self.branch.startswith("refs/"):
2991            if self.importIntoRemotes:
2992                prepend = "refs/remotes/"
2993            else:
2994                prepend = "refs/heads/"
2995            if not self.branch.startswith("p4/"):
2996                prepend += "p4/"
2997            self.branch = prepend + self.branch
2998
2999        if len(args) == 0 and self.depotPaths:
3000            if not self.silent:
3001                print "Depot paths: %s" % ' '.join(self.depotPaths)
3002        else:
3003            if self.depotPaths and self.depotPaths != args:
3004                print ("previous import used depot path %s and now %s was specified. "
3005                       "This doesn't work!" % (' '.join (self.depotPaths),
3006                                               ' '.join (args)))
3007                sys.exit(1)
3008
3009            self.depotPaths = sorted(args)
3010
3011        revision = ""
3012        self.users = {}
3013
3014        # Make sure no revision specifiers are used when --changesfile
3015        # is specified.
3016        bad_changesfile = False
3017        if len(self.changesFile) > 0:
3018            for p in self.depotPaths:
3019                if p.find("@") >= 0 or p.find("#") >= 0:
3020                    bad_changesfile = True
3021                    break
3022        if bad_changesfile:
3023            die("Option --changesfile is incompatible with revision specifiers")
3024
3025        newPaths = []
3026        for p in self.depotPaths:
3027            if p.find("@") != -1:
3028                atIdx = p.index("@")
3029                self.changeRange = p[atIdx:]
3030                if self.changeRange == "@all":
3031                    self.changeRange = ""
3032                elif ',' not in self.changeRange:
3033                    revision = self.changeRange
3034                    self.changeRange = ""
3035                p = p[:atIdx]
3036            elif p.find("#") != -1:
3037                hashIdx = p.index("#")
3038                revision = p[hashIdx:]
3039                p = p[:hashIdx]
3040            elif self.previousDepotPaths == []:
3041                # pay attention to changesfile, if given, else import
3042                # the entire p4 tree at the head revision
3043                if len(self.changesFile) == 0:
3044                    revision = "#head"
3045
3046            p = re.sub ("\.\.\.$", "", p)
3047            if not p.endswith("/"):
3048                p += "/"
3049
3050            newPaths.append(p)
3051
3052        self.depotPaths = newPaths
3053
3054        # --detect-branches may change this for each branch
3055        self.branchPrefixes = self.depotPaths
3056
3057        self.loadUserMapFromCache()
3058        self.labels = {}
3059        if self.detectLabels:
3060            self.getLabels();
3061
3062        if self.detectBranches:
3063            ## FIXME - what's a P4 projectName ?
3064            self.projectName = self.guessProjectName()
3065
3066            if self.hasOrigin:
3067                self.getBranchMappingFromGitBranches()
3068            else:
3069                self.getBranchMapping()
3070            if self.verbose:
3071                print "p4-git branches: %s" % self.p4BranchesInGit
3072                print "initial parents: %s" % self.initialParents
3073            for b in self.p4BranchesInGit:
3074                if b != "master":
3075
3076                    ## FIXME
3077                    b = b[len(self.projectName):]
3078                self.createdBranches.add(b)
3079
3080        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3081
3082        self.importProcess = subprocess.Popen(["git", "fast-import"],
3083                                              stdin=subprocess.PIPE,
3084                                              stdout=subprocess.PIPE,
3085                                              stderr=subprocess.PIPE);
3086        self.gitOutput = self.importProcess.stdout
3087        self.gitStream = self.importProcess.stdin
3088        self.gitError = self.importProcess.stderr
3089
3090        if revision:
3091            self.importHeadRevision(revision)
3092        else:
3093            changes = []
3094
3095            if len(self.changesFile) > 0:
3096                output = open(self.changesFile).readlines()
3097                changeSet = set()
3098                for line in output:
3099                    changeSet.add(int(line))
3100
3101                for change in changeSet:
3102                    changes.append(change)
3103
3104                changes.sort()
3105            else:
3106                # catch "git p4 sync" with no new branches, in a repo that
3107                # does not have any existing p4 branches
3108                if len(args) == 0:
3109                    if not self.p4BranchesInGit:
3110                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3111
3112                    # The default branch is master, unless --branch is used to
3113                    # specify something else.  Make sure it exists, or complain
3114                    # nicely about how to use --branch.
3115                    if not self.detectBranches:
3116                        if not branch_exists(self.branch):
3117                            if branch_arg_given:
3118                                die("Error: branch %s does not exist." % self.branch)
3119                            else:
3120                                die("Error: no branch %s; perhaps specify one with --branch." %
3121                                    self.branch)
3122
3123                if self.verbose:
3124                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3125                                                              self.changeRange)
3126                changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3127
3128                if len(self.maxChanges) > 0:
3129                    changes = changes[:min(int(self.maxChanges), len(changes))]
3130
3131            if len(changes) == 0:
3132                if not self.silent:
3133                    print "No changes to import!"
3134            else:
3135                if not self.silent and not self.detectBranches:
3136                    print "Import destination: %s" % self.branch
3137
3138                self.updatedBranches = set()
3139
3140                if not self.detectBranches:
3141                    if args:
3142                        # start a new branch
3143                        self.initialParent = ""
3144                    else:
3145                        # build on a previous revision
3146                        self.initialParent = parseRevision(self.branch)
3147
3148                self.importChanges(changes)
3149
3150                if not self.silent:
3151                    print ""
3152                    if len(self.updatedBranches) > 0:
3153                        sys.stdout.write("Updated branches: ")
3154                        for b in self.updatedBranches:
3155                            sys.stdout.write("%s " % b)
3156                        sys.stdout.write("\n")
3157
3158        if gitConfigBool("git-p4.importLabels"):
3159            self.importLabels = True
3160
3161        if self.importLabels:
3162            p4Labels = getP4Labels(self.depotPaths)
3163            gitTags = getGitTags()
3164
3165            missingP4Labels = p4Labels - gitTags
3166            self.importP4Labels(self.gitStream, missingP4Labels)
3167
3168        self.gitStream.close()
3169        if self.importProcess.wait() != 0:
3170            die("fast-import failed: %s" % self.gitError.read())
3171        self.gitOutput.close()
3172        self.gitError.close()
3173
3174        # Cleanup temporary branches created during import
3175        if self.tempBranches != []:
3176            for branch in self.tempBranches:
3177                read_pipe("git update-ref -d %s" % branch)
3178            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3179
3180        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3181        # a convenient shortcut refname "p4".
3182        if self.importIntoRemotes:
3183            head_ref = self.refPrefix + "HEAD"
3184            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3185                system(["git", "symbolic-ref", head_ref, self.branch])
3186
3187        return True
3188
3189class P4Rebase(Command):
3190    def __init__(self):
3191        Command.__init__(self)
3192        self.options = [
3193                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3194        ]
3195        self.importLabels = False
3196        self.description = ("Fetches the latest revision from perforce and "
3197                            + "rebases the current work (branch) against it")
3198
3199    def run(self, args):
3200        sync = P4Sync()
3201        sync.importLabels = self.importLabels
3202        sync.run([])
3203
3204        return self.rebase()
3205
3206    def rebase(self):
3207        if os.system("git update-index --refresh") != 0:
3208            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.");
3209        if len(read_pipe("git diff-index HEAD --")) > 0:
3210            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3211
3212        [upstream, settings] = findUpstreamBranchPoint()
3213        if len(upstream) == 0:
3214            die("Cannot find upstream branchpoint for rebase")
3215
3216        # the branchpoint may be p4/foo~3, so strip off the parent
3217        upstream = re.sub("~[0-9]+$", "", upstream)
3218
3219        print "Rebasing the current branch onto %s" % upstream
3220        oldHead = read_pipe("git rev-parse HEAD").strip()
3221        system("git rebase %s" % upstream)
3222        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3223        return True
3224
3225class P4Clone(P4Sync):
3226    def __init__(self):
3227        P4Sync.__init__(self)
3228        self.description = "Creates a new git repository and imports from Perforce into it"
3229        self.usage = "usage: %prog [options] //depot/path[@revRange]"
3230        self.options += [
3231            optparse.make_option("--destination", dest="cloneDestination",
3232                                 action='store', default=None,
3233                                 help="where to leave result of the clone"),
3234            optparse.make_option("--bare", dest="cloneBare",
3235                                 action="store_true", default=False),
3236        ]
3237        self.cloneDestination = None
3238        self.needsGit = False
3239        self.cloneBare = False
3240
3241    def defaultDestination(self, args):
3242        ## TODO: use common prefix of args?
3243        depotPath = args[0]
3244        depotDir = re.sub("(@[^@]*)$", "", depotPath)
3245        depotDir = re.sub("(#[^#]*)$", "", depotDir)
3246        depotDir = re.sub(r"\.\.\.$", "", depotDir)
3247        depotDir = re.sub(r"/$", "", depotDir)
3248        return os.path.split(depotDir)[1]
3249
3250    def run(self, args):
3251        if len(args) < 1:
3252            return False
3253
3254        if self.keepRepoPath and not self.cloneDestination:
3255            sys.stderr.write("Must specify destination for --keep-path\n")
3256            sys.exit(1)
3257
3258        depotPaths = args
3259
3260        if not self.cloneDestination and len(depotPaths) > 1:
3261            self.cloneDestination = depotPaths[-1]
3262            depotPaths = depotPaths[:-1]
3263
3264        self.cloneExclude = ["/"+p for p in self.cloneExclude]
3265        for p in depotPaths:
3266            if not p.startswith("//"):
3267                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3268                return False
3269
3270        if not self.cloneDestination:
3271            self.cloneDestination = self.defaultDestination(args)
3272
3273        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3274
3275        if not os.path.exists(self.cloneDestination):
3276            os.makedirs(self.cloneDestination)
3277        chdir(self.cloneDestination)
3278
3279        init_cmd = [ "git", "init" ]
3280        if self.cloneBare:
3281            init_cmd.append("--bare")
3282        retcode = subprocess.call(init_cmd)
3283        if retcode:
3284            raise CalledProcessError(retcode, init_cmd)
3285
3286        if not P4Sync.run(self, depotPaths):
3287            return False
3288
3289        # create a master branch and check out a work tree
3290        if gitBranchExists(self.branch):
3291            system([ "git", "branch", "master", self.branch ])
3292            if not self.cloneBare:
3293                system([ "git", "checkout", "-f" ])
3294        else:
3295            print 'Not checking out any branch, use ' \
3296                  '"git checkout -q -b master <branch>"'
3297
3298        # auto-set this variable if invoked with --use-client-spec
3299        if self.useClientSpec_from_options:
3300            system("git config --bool git-p4.useclientspec true")
3301
3302        return True
3303
3304class P4Branches(Command):
3305    def __init__(self):
3306        Command.__init__(self)
3307        self.options = [ ]
3308        self.description = ("Shows the git branches that hold imports and their "
3309                            + "corresponding perforce depot paths")
3310        self.verbose = False
3311
3312    def run(self, args):
3313        if originP4BranchesExist():
3314            createOrUpdateBranchesFromOrigin()
3315
3316        cmdline = "git rev-parse --symbolic "
3317        cmdline += " --remotes"
3318
3319        for line in read_pipe_lines(cmdline):
3320            line = line.strip()
3321
3322            if not line.startswith('p4/') or line == "p4/HEAD":
3323                continue
3324            branch = line
3325
3326            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3327            settings = extractSettingsGitLog(log)
3328
3329            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3330        return True
3331
3332class HelpFormatter(optparse.IndentedHelpFormatter):
3333    def __init__(self):
3334        optparse.IndentedHelpFormatter.__init__(self)
3335
3336    def format_description(self, description):
3337        if description:
3338            return description + "\n"
3339        else:
3340            return ""
3341
3342def printUsage(commands):
3343    print "usage: %s <command> [options]" % sys.argv[0]
3344    print ""
3345    print "valid commands: %s" % ", ".join(commands)
3346    print ""
3347    print "Try %s <command> --help for command specific help." % sys.argv[0]
3348    print ""
3349
3350commands = {
3351    "debug" : P4Debug,
3352    "submit" : P4Submit,
3353    "commit" : P4Submit,
3354    "sync" : P4Sync,
3355    "rebase" : P4Rebase,
3356    "clone" : P4Clone,
3357    "rollback" : P4RollBack,
3358    "branches" : P4Branches
3359}
3360
3361
3362def main():
3363    if len(sys.argv[1:]) == 0:
3364        printUsage(commands.keys())
3365        sys.exit(2)
3366
3367    cmdName = sys.argv[1]
3368    try:
3369        klass = commands[cmdName]
3370        cmd = klass()
3371    except KeyError:
3372        print "unknown command %s" % cmdName
3373        print ""
3374        printUsage(commands.keys())
3375        sys.exit(2)
3376
3377    options = cmd.options
3378    cmd.gitdir = os.environ.get("GIT_DIR", None)
3379
3380    args = sys.argv[2:]
3381
3382    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3383    if cmd.needsGit:
3384        options.append(optparse.make_option("--git-dir", dest="gitdir"))
3385
3386    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3387                                   options,
3388                                   description = cmd.description,
3389                                   formatter = HelpFormatter())
3390
3391    (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3392    global verbose
3393    verbose = cmd.verbose
3394    if cmd.needsGit:
3395        if cmd.gitdir == None:
3396            cmd.gitdir = os.path.abspath(".git")
3397            if not isValidGitDir(cmd.gitdir):
3398                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3399                if os.path.exists(cmd.gitdir):
3400                    cdup = read_pipe("git rev-parse --show-cdup").strip()
3401                    if len(cdup) > 0:
3402                        chdir(cdup);
3403
3404        if not isValidGitDir(cmd.gitdir):
3405            if isValidGitDir(cmd.gitdir + "/.git"):
3406                cmd.gitdir += "/.git"
3407            else:
3408                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3409
3410        os.environ["GIT_DIR"] = cmd.gitdir
3411
3412    if not cmd.run(args):
3413        parser.print_help()
3414        sys.exit(2)
3415
3416
3417if __name__ == '__main__':
3418    main()