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