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