contrib / fast-import / git-p4on commit Merge branch 'maint' (f574cb3)
   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
  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 = "%s " % "p4"
  26
  27    user = gitConfig("git-p4.user")
  28    if len(user) > 0:
  29        real_cmd += "-u %s " % user
  30
  31    password = gitConfig("git-p4.password")
  32    if len(password) > 0:
  33        real_cmd += "-P %s " % password
  34
  35    port = gitConfig("git-p4.port")
  36    if len(port) > 0:
  37        real_cmd += "-p %s " % port
  38
  39    host = gitConfig("git-p4.host")
  40    if len(host) > 0:
  41        real_cmd += "-h %s " % host
  42
  43    client = gitConfig("git-p4.client")
  44    if len(client) > 0:
  45        real_cmd += "-c %s " % client
  46
  47    real_cmd += "%s" % (cmd)
  48    if verbose:
  49        print real_cmd
  50    return real_cmd
  51
  52def chdir(dir):
  53    if os.name == 'nt':
  54        os.environ['PWD']=dir
  55    os.chdir(dir)
  56
  57def die(msg):
  58    if verbose:
  59        raise Exception(msg)
  60    else:
  61        sys.stderr.write(msg + "\n")
  62        sys.exit(1)
  63
  64def write_pipe(c, str):
  65    if verbose:
  66        sys.stderr.write('Writing pipe: %s\n' % c)
  67
  68    pipe = os.popen(c, 'w')
  69    val = pipe.write(str)
  70    if pipe.close():
  71        die('Command failed: %s' % c)
  72
  73    return val
  74
  75def p4_write_pipe(c, str):
  76    real_cmd = p4_build_cmd(c)
  77    return write_pipe(real_cmd, str)
  78
  79def read_pipe(c, ignore_error=False):
  80    if verbose:
  81        sys.stderr.write('Reading pipe: %s\n' % c)
  82
  83    pipe = os.popen(c, 'rb')
  84    val = pipe.read()
  85    if pipe.close() and not ignore_error:
  86        die('Command failed: %s' % c)
  87
  88    return val
  89
  90def p4_read_pipe(c, ignore_error=False):
  91    real_cmd = p4_build_cmd(c)
  92    return read_pipe(real_cmd, ignore_error)
  93
  94def read_pipe_lines(c):
  95    if verbose:
  96        sys.stderr.write('Reading pipe: %s\n' % c)
  97    ## todo: check return status
  98    pipe = os.popen(c, 'rb')
  99    val = pipe.readlines()
 100    if pipe.close():
 101        die('Command failed: %s' % c)
 102
 103    return val
 104
 105def p4_read_pipe_lines(c):
 106    """Specifically invoke p4 on the command supplied. """
 107    real_cmd = p4_build_cmd(c)
 108    return read_pipe_lines(real_cmd)
 109
 110def system(cmd):
 111    if verbose:
 112        sys.stderr.write("executing %s\n" % cmd)
 113    if os.system(cmd) != 0:
 114        die("command failed: %s" % cmd)
 115
 116def p4_system(cmd):
 117    """Specifically invoke p4 as the system command. """
 118    real_cmd = p4_build_cmd(cmd)
 119    return system(real_cmd)
 120
 121def isP4Exec(kind):
 122    """Determine if a Perforce 'kind' should have execute permission
 123
 124    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
 125    or x follows one of a few letters.  Otherwise, if there is an 'x' after
 126    a plus sign, it is also executable"""
 127    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
 128
 129def setP4ExecBit(file, mode):
 130    # Reopens an already open file and changes the execute bit to match
 131    # the execute bit setting in the passed in mode.
 132
 133    p4Type = "+x"
 134
 135    if not isModeExec(mode):
 136        p4Type = getP4OpenedType(file)
 137        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 138        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 139        if p4Type[-1] == "+":
 140            p4Type = p4Type[0:-1]
 141
 142    p4_system("reopen -t %s %s" % (p4Type, file))
 143
 144def getP4OpenedType(file):
 145    # Returns the perforce file type for the given file.
 146
 147    result = p4_read_pipe("opened %s" % file)
 148    match = re.match(".*\((.+)\)\r?$", result)
 149    if match:
 150        return match.group(1)
 151    else:
 152        die("Could not determine file type for %s (result: '%s')" % (file, result))
 153
 154def diffTreePattern():
 155    # This is a simple generator for the diff tree regex pattern. This could be
 156    # a class variable if this and parseDiffTreeEntry were a part of a class.
 157    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 158    while True:
 159        yield pattern
 160
 161def parseDiffTreeEntry(entry):
 162    """Parses a single diff tree entry into its component elements.
 163
 164    See git-diff-tree(1) manpage for details about the format of the diff
 165    output. This method returns a dictionary with the following elements:
 166
 167    src_mode - The mode of the source file
 168    dst_mode - The mode of the destination file
 169    src_sha1 - The sha1 for the source file
 170    dst_sha1 - The sha1 fr the destination file
 171    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 172    status_score - The score for the status (applicable for 'C' and 'R'
 173                   statuses). This is None if there is no score.
 174    src - The path for the source file.
 175    dst - The path for the destination file. This is only present for
 176          copy or renames. If it is not present, this is None.
 177
 178    If the pattern is not matched, None is returned."""
 179
 180    match = diffTreePattern().next().match(entry)
 181    if match:
 182        return {
 183            'src_mode': match.group(1),
 184            'dst_mode': match.group(2),
 185            'src_sha1': match.group(3),
 186            'dst_sha1': match.group(4),
 187            'status': match.group(5),
 188            'status_score': match.group(6),
 189            'src': match.group(7),
 190            'dst': match.group(10)
 191        }
 192    return None
 193
 194def isModeExec(mode):
 195    # Returns True if the given git mode represents an executable file,
 196    # otherwise False.
 197    return mode[-3:] == "755"
 198
 199def isModeExecChanged(src_mode, dst_mode):
 200    return isModeExec(src_mode) != isModeExec(dst_mode)
 201
 202def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
 203    cmd = p4_build_cmd("-G %s" % (cmd))
 204    if verbose:
 205        sys.stderr.write("Opening pipe: %s\n" % cmd)
 206
 207    # Use a temporary file to avoid deadlocks without
 208    # subprocess.communicate(), which would put another copy
 209    # of stdout into memory.
 210    stdin_file = None
 211    if stdin is not None:
 212        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 213        stdin_file.write(stdin)
 214        stdin_file.flush()
 215        stdin_file.seek(0)
 216
 217    p4 = subprocess.Popen(cmd, shell=True,
 218                          stdin=stdin_file,
 219                          stdout=subprocess.PIPE)
 220
 221    result = []
 222    try:
 223        while True:
 224            entry = marshal.load(p4.stdout)
 225            if cb is not None:
 226                cb(entry)
 227            else:
 228                result.append(entry)
 229    except EOFError:
 230        pass
 231    exitCode = p4.wait()
 232    if exitCode != 0:
 233        entry = {}
 234        entry["p4ExitCode"] = exitCode
 235        result.append(entry)
 236
 237    return result
 238
 239def p4Cmd(cmd):
 240    list = p4CmdList(cmd)
 241    result = {}
 242    for entry in list:
 243        result.update(entry)
 244    return result;
 245
 246def p4Where(depotPath):
 247    if not depotPath.endswith("/"):
 248        depotPath += "/"
 249    depotPath = depotPath + "..."
 250    outputList = p4CmdList("where %s" % depotPath)
 251    output = None
 252    for entry in outputList:
 253        if "depotFile" in entry:
 254            if entry["depotFile"] == depotPath:
 255                output = entry
 256                break
 257        elif "data" in entry:
 258            data = entry.get("data")
 259            space = data.find(" ")
 260            if data[:space] == depotPath:
 261                output = entry
 262                break
 263    if output == None:
 264        return ""
 265    if output["code"] == "error":
 266        return ""
 267    clientPath = ""
 268    if "path" in output:
 269        clientPath = output.get("path")
 270    elif "data" in output:
 271        data = output.get("data")
 272        lastSpace = data.rfind(" ")
 273        clientPath = data[lastSpace + 1:]
 274
 275    if clientPath.endswith("..."):
 276        clientPath = clientPath[:-3]
 277    return clientPath
 278
 279def currentGitBranch():
 280    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 281
 282def isValidGitDir(path):
 283    if (os.path.exists(path + "/HEAD")
 284        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 285        return True;
 286    return False
 287
 288def parseRevision(ref):
 289    return read_pipe("git rev-parse %s" % ref).strip()
 290
 291def extractLogMessageFromGitCommit(commit):
 292    logMessage = ""
 293
 294    ## fixme: title is first line of commit, not 1st paragraph.
 295    foundTitle = False
 296    for log in read_pipe_lines("git cat-file commit %s" % commit):
 297       if not foundTitle:
 298           if len(log) == 1:
 299               foundTitle = True
 300           continue
 301
 302       logMessage += log
 303    return logMessage
 304
 305def extractSettingsGitLog(log):
 306    values = {}
 307    for line in log.split("\n"):
 308        line = line.strip()
 309        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 310        if not m:
 311            continue
 312
 313        assignments = m.group(1).split (':')
 314        for a in assignments:
 315            vals = a.split ('=')
 316            key = vals[0].strip()
 317            val = ('='.join (vals[1:])).strip()
 318            if val.endswith ('\"') and val.startswith('"'):
 319                val = val[1:-1]
 320
 321            values[key] = val
 322
 323    paths = values.get("depot-paths")
 324    if not paths:
 325        paths = values.get("depot-path")
 326    if paths:
 327        values['depot-paths'] = paths.split(',')
 328    return values
 329
 330def gitBranchExists(branch):
 331    proc = subprocess.Popen(["git", "rev-parse", branch],
 332                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 333    return proc.wait() == 0;
 334
 335_gitConfig = {}
 336def gitConfig(key, args = None): # set args to "--bool", for instance
 337    if not _gitConfig.has_key(key):
 338        argsFilter = ""
 339        if args != None:
 340            argsFilter = "%s " % args
 341        cmd = "git config %s%s" % (argsFilter, key)
 342        _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
 343    return _gitConfig[key]
 344
 345def p4BranchesInGit(branchesAreInRemotes = True):
 346    branches = {}
 347
 348    cmdline = "git rev-parse --symbolic "
 349    if branchesAreInRemotes:
 350        cmdline += " --remotes"
 351    else:
 352        cmdline += " --branches"
 353
 354    for line in read_pipe_lines(cmdline):
 355        line = line.strip()
 356
 357        ## only import to p4/
 358        if not line.startswith('p4/') or line == "p4/HEAD":
 359            continue
 360        branch = line
 361
 362        # strip off p4
 363        branch = re.sub ("^p4/", "", line)
 364
 365        branches[branch] = parseRevision(line)
 366    return branches
 367
 368def findUpstreamBranchPoint(head = "HEAD"):
 369    branches = p4BranchesInGit()
 370    # map from depot-path to branch name
 371    branchByDepotPath = {}
 372    for branch in branches.keys():
 373        tip = branches[branch]
 374        log = extractLogMessageFromGitCommit(tip)
 375        settings = extractSettingsGitLog(log)
 376        if settings.has_key("depot-paths"):
 377            paths = ",".join(settings["depot-paths"])
 378            branchByDepotPath[paths] = "remotes/p4/" + branch
 379
 380    settings = None
 381    parent = 0
 382    while parent < 65535:
 383        commit = head + "~%s" % parent
 384        log = extractLogMessageFromGitCommit(commit)
 385        settings = extractSettingsGitLog(log)
 386        if settings.has_key("depot-paths"):
 387            paths = ",".join(settings["depot-paths"])
 388            if branchByDepotPath.has_key(paths):
 389                return [branchByDepotPath[paths], settings]
 390
 391        parent = parent + 1
 392
 393    return ["", settings]
 394
 395def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 396    if not silent:
 397        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 398               % localRefPrefix)
 399
 400    originPrefix = "origin/p4/"
 401
 402    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 403        line = line.strip()
 404        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 405            continue
 406
 407        headName = line[len(originPrefix):]
 408        remoteHead = localRefPrefix + headName
 409        originHead = line
 410
 411        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 412        if (not original.has_key('depot-paths')
 413            or not original.has_key('change')):
 414            continue
 415
 416        update = False
 417        if not gitBranchExists(remoteHead):
 418            if verbose:
 419                print "creating %s" % remoteHead
 420            update = True
 421        else:
 422            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 423            if settings.has_key('change') > 0:
 424                if settings['depot-paths'] == original['depot-paths']:
 425                    originP4Change = int(original['change'])
 426                    p4Change = int(settings['change'])
 427                    if originP4Change > p4Change:
 428                        print ("%s (%s) is newer than %s (%s). "
 429                               "Updating p4 branch from origin."
 430                               % (originHead, originP4Change,
 431                                  remoteHead, p4Change))
 432                        update = True
 433                else:
 434                    print ("Ignoring: %s was imported from %s while "
 435                           "%s was imported from %s"
 436                           % (originHead, ','.join(original['depot-paths']),
 437                              remoteHead, ','.join(settings['depot-paths'])))
 438
 439        if update:
 440            system("git update-ref %s %s" % (remoteHead, originHead))
 441
 442def originP4BranchesExist():
 443        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 444
 445def p4ChangesForPaths(depotPaths, changeRange):
 446    assert depotPaths
 447    output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
 448                                                        for p in depotPaths]))
 449
 450    changes = {}
 451    for line in output:
 452        changeNum = int(line.split(" ")[1])
 453        changes[changeNum] = True
 454
 455    changelist = changes.keys()
 456    changelist.sort()
 457    return changelist
 458
 459def p4PathStartsWith(path, prefix):
 460    # This method tries to remedy a potential mixed-case issue:
 461    #
 462    # If UserA adds  //depot/DirA/file1
 463    # and UserB adds //depot/dira/file2
 464    #
 465    # we may or may not have a problem. If you have core.ignorecase=true,
 466    # we treat DirA and dira as the same directory
 467    ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
 468    if ignorecase:
 469        return path.lower().startswith(prefix.lower())
 470    return path.startswith(prefix)
 471
 472class Command:
 473    def __init__(self):
 474        self.usage = "usage: %prog [options]"
 475        self.needsGit = True
 476
 477class P4Debug(Command):
 478    def __init__(self):
 479        Command.__init__(self)
 480        self.options = [
 481            optparse.make_option("--verbose", dest="verbose", action="store_true",
 482                                 default=False),
 483            ]
 484        self.description = "A tool to debug the output of p4 -G."
 485        self.needsGit = False
 486        self.verbose = False
 487
 488    def run(self, args):
 489        j = 0
 490        for output in p4CmdList(" ".join(args)):
 491            print 'Element: %d' % j
 492            j += 1
 493            print output
 494        return True
 495
 496class P4RollBack(Command):
 497    def __init__(self):
 498        Command.__init__(self)
 499        self.options = [
 500            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 501            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 502        ]
 503        self.description = "A tool to debug the multi-branch import. Don't use :)"
 504        self.verbose = False
 505        self.rollbackLocalBranches = False
 506
 507    def run(self, args):
 508        if len(args) != 1:
 509            return False
 510        maxChange = int(args[0])
 511
 512        if "p4ExitCode" in p4Cmd("changes -m 1"):
 513            die("Problems executing p4");
 514
 515        if self.rollbackLocalBranches:
 516            refPrefix = "refs/heads/"
 517            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 518        else:
 519            refPrefix = "refs/remotes/"
 520            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 521
 522        for line in lines:
 523            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 524                line = line.strip()
 525                ref = refPrefix + line
 526                log = extractLogMessageFromGitCommit(ref)
 527                settings = extractSettingsGitLog(log)
 528
 529                depotPaths = settings['depot-paths']
 530                change = settings['change']
 531
 532                changed = False
 533
 534                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 535                                                           for p in depotPaths]))) == 0:
 536                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 537                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 538                    continue
 539
 540                while change and int(change) > maxChange:
 541                    changed = True
 542                    if self.verbose:
 543                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 544                    system("git update-ref %s \"%s^\"" % (ref, ref))
 545                    log = extractLogMessageFromGitCommit(ref)
 546                    settings =  extractSettingsGitLog(log)
 547
 548
 549                    depotPaths = settings['depot-paths']
 550                    change = settings['change']
 551
 552                if changed:
 553                    print "%s rewound to %s" % (ref, change)
 554
 555        return True
 556
 557class P4Submit(Command):
 558    def __init__(self):
 559        Command.__init__(self)
 560        self.options = [
 561                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 562                optparse.make_option("--origin", dest="origin"),
 563                optparse.make_option("-M", dest="detectRenames", action="store_true"),
 564        ]
 565        self.description = "Submit changes from git to the perforce depot."
 566        self.usage += " [name of git branch to submit into perforce depot]"
 567        self.interactive = True
 568        self.origin = ""
 569        self.detectRenames = False
 570        self.verbose = False
 571        self.isWindows = (platform.system() == "Windows")
 572
 573    def check(self):
 574        if len(p4CmdList("opened ...")) > 0:
 575            die("You have files opened with perforce! Close them before starting the sync.")
 576
 577    # replaces everything between 'Description:' and the next P4 submit template field with the
 578    # commit message
 579    def prepareLogMessage(self, template, message):
 580        result = ""
 581
 582        inDescriptionSection = False
 583
 584        for line in template.split("\n"):
 585            if line.startswith("#"):
 586                result += line + "\n"
 587                continue
 588
 589            if inDescriptionSection:
 590                if line.startswith("Files:") or line.startswith("Jobs:"):
 591                    inDescriptionSection = False
 592                else:
 593                    continue
 594            else:
 595                if line.startswith("Description:"):
 596                    inDescriptionSection = True
 597                    line += "\n"
 598                    for messageLine in message.split("\n"):
 599                        line += "\t" + messageLine + "\n"
 600
 601            result += line + "\n"
 602
 603        return result
 604
 605    def prepareSubmitTemplate(self):
 606        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 607        template = ""
 608        inFilesSection = False
 609        for line in p4_read_pipe_lines("change -o"):
 610            if line.endswith("\r\n"):
 611                line = line[:-2] + "\n"
 612            if inFilesSection:
 613                if line.startswith("\t"):
 614                    # path starts and ends with a tab
 615                    path = line[1:]
 616                    lastTab = path.rfind("\t")
 617                    if lastTab != -1:
 618                        path = path[:lastTab]
 619                        if not p4PathStartsWith(path, self.depotPath):
 620                            continue
 621                else:
 622                    inFilesSection = False
 623            else:
 624                if line.startswith("Files:"):
 625                    inFilesSection = True
 626
 627            template += line
 628
 629        return template
 630
 631    def applyCommit(self, id):
 632        print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 633
 634        if not self.detectRenames:
 635            # If not explicitly set check the config variable
 636            self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
 637
 638        if self.detectRenames:
 639            diffOpts = "-M"
 640        else:
 641            diffOpts = ""
 642
 643        if gitConfig("git-p4.detectCopies").lower() == "true":
 644            diffOpts += " -C"
 645
 646        if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
 647            diffOpts += " --find-copies-harder"
 648
 649        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 650        filesToAdd = set()
 651        filesToDelete = set()
 652        editedFiles = set()
 653        filesToChangeExecBit = {}
 654        for line in diff:
 655            diff = parseDiffTreeEntry(line)
 656            modifier = diff['status']
 657            path = diff['src']
 658            if modifier == "M":
 659                p4_system("edit \"%s\"" % path)
 660                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 661                    filesToChangeExecBit[path] = diff['dst_mode']
 662                editedFiles.add(path)
 663            elif modifier == "A":
 664                filesToAdd.add(path)
 665                filesToChangeExecBit[path] = diff['dst_mode']
 666                if path in filesToDelete:
 667                    filesToDelete.remove(path)
 668            elif modifier == "D":
 669                filesToDelete.add(path)
 670                if path in filesToAdd:
 671                    filesToAdd.remove(path)
 672            elif modifier == "C":
 673                src, dest = diff['src'], diff['dst']
 674                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 675                if diff['src_sha1'] != diff['dst_sha1']:
 676                    p4_system("edit \"%s\"" % (dest))
 677                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 678                    p4_system("edit \"%s\"" % (dest))
 679                    filesToChangeExecBit[dest] = diff['dst_mode']
 680                os.unlink(dest)
 681                editedFiles.add(dest)
 682            elif modifier == "R":
 683                src, dest = diff['src'], diff['dst']
 684                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 685                if diff['src_sha1'] != diff['dst_sha1']:
 686                    p4_system("edit \"%s\"" % (dest))
 687                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 688                    p4_system("edit \"%s\"" % (dest))
 689                    filesToChangeExecBit[dest] = diff['dst_mode']
 690                os.unlink(dest)
 691                editedFiles.add(dest)
 692                filesToDelete.add(src)
 693            else:
 694                die("unknown modifier %s for %s" % (modifier, path))
 695
 696        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 697        patchcmd = diffcmd + " | git apply "
 698        tryPatchCmd = patchcmd + "--check -"
 699        applyPatchCmd = patchcmd + "--check --apply -"
 700
 701        if os.system(tryPatchCmd) != 0:
 702            print "Unfortunately applying the change failed!"
 703            print "What do you want to do?"
 704            response = "x"
 705            while response != "s" and response != "a" and response != "w":
 706                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 707                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 708            if response == "s":
 709                print "Skipping! Good luck with the next patches..."
 710                for f in editedFiles:
 711                    p4_system("revert \"%s\"" % f);
 712                for f in filesToAdd:
 713                    system("rm %s" %f)
 714                return
 715            elif response == "a":
 716                os.system(applyPatchCmd)
 717                if len(filesToAdd) > 0:
 718                    print "You may also want to call p4 add on the following files:"
 719                    print " ".join(filesToAdd)
 720                if len(filesToDelete):
 721                    print "The following files should be scheduled for deletion with p4 delete:"
 722                    print " ".join(filesToDelete)
 723                die("Please resolve and submit the conflict manually and "
 724                    + "continue afterwards with git-p4 submit --continue")
 725            elif response == "w":
 726                system(diffcmd + " > patch.txt")
 727                print "Patch saved to patch.txt in %s !" % self.clientPath
 728                die("Please resolve and submit the conflict manually and "
 729                    "continue afterwards with git-p4 submit --continue")
 730
 731        system(applyPatchCmd)
 732
 733        for f in filesToAdd:
 734            p4_system("add \"%s\"" % f)
 735        for f in filesToDelete:
 736            p4_system("revert \"%s\"" % f)
 737            p4_system("delete \"%s\"" % f)
 738
 739        # Set/clear executable bits
 740        for f in filesToChangeExecBit.keys():
 741            mode = filesToChangeExecBit[f]
 742            setP4ExecBit(f, mode)
 743
 744        logMessage = extractLogMessageFromGitCommit(id)
 745        logMessage = logMessage.strip()
 746
 747        template = self.prepareSubmitTemplate()
 748
 749        if self.interactive:
 750            submitTemplate = self.prepareLogMessage(template, logMessage)
 751            if os.environ.has_key("P4DIFF"):
 752                del(os.environ["P4DIFF"])
 753            diff = ""
 754            for editedFile in editedFiles:
 755                diff += p4_read_pipe("diff -du %r" % editedFile)
 756
 757            newdiff = ""
 758            for newFile in filesToAdd:
 759                newdiff += "==== new file ====\n"
 760                newdiff += "--- /dev/null\n"
 761                newdiff += "+++ %s\n" % newFile
 762                f = open(newFile, "r")
 763                for line in f.readlines():
 764                    newdiff += "+" + line
 765                f.close()
 766
 767            separatorLine = "######## everything below this line is just the diff #######\n"
 768
 769            [handle, fileName] = tempfile.mkstemp()
 770            tmpFile = os.fdopen(handle, "w+")
 771            if self.isWindows:
 772                submitTemplate = submitTemplate.replace("\n", "\r\n")
 773                separatorLine = separatorLine.replace("\n", "\r\n")
 774                newdiff = newdiff.replace("\n", "\r\n")
 775            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
 776            tmpFile.close()
 777            mtime = os.stat(fileName).st_mtime
 778            if os.environ.has_key("P4EDITOR"):
 779                editor = os.environ.get("P4EDITOR")
 780            else:
 781                editor = read_pipe("git var GIT_EDITOR").strip()
 782            system(editor + " " + fileName)
 783
 784            response = "y"
 785            if os.stat(fileName).st_mtime <= mtime:
 786                response = "x"
 787                while response != "y" and response != "n":
 788                    response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
 789
 790            if response == "y":
 791                tmpFile = open(fileName, "rb")
 792                message = tmpFile.read()
 793                tmpFile.close()
 794                submitTemplate = message[:message.index(separatorLine)]
 795                if self.isWindows:
 796                    submitTemplate = submitTemplate.replace("\r\n", "\n")
 797                p4_write_pipe("submit -i", submitTemplate)
 798            else:
 799                for f in editedFiles:
 800                    p4_system("revert \"%s\"" % f);
 801                for f in filesToAdd:
 802                    p4_system("revert \"%s\"" % f);
 803                    system("rm %s" %f)
 804
 805            os.remove(fileName)
 806        else:
 807            fileName = "submit.txt"
 808            file = open(fileName, "w+")
 809            file.write(self.prepareLogMessage(template, logMessage))
 810            file.close()
 811            print ("Perforce submit template written as %s. "
 812                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 813                   % (fileName, fileName))
 814
 815    def run(self, args):
 816        if len(args) == 0:
 817            self.master = currentGitBranch()
 818            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 819                die("Detecting current git branch failed!")
 820        elif len(args) == 1:
 821            self.master = args[0]
 822        else:
 823            return False
 824
 825        allowSubmit = gitConfig("git-p4.allowSubmit")
 826        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
 827            die("%s is not in git-p4.allowSubmit" % self.master)
 828
 829        [upstream, settings] = findUpstreamBranchPoint()
 830        self.depotPath = settings['depot-paths'][0]
 831        if len(self.origin) == 0:
 832            self.origin = upstream
 833
 834        if self.verbose:
 835            print "Origin branch is " + self.origin
 836
 837        if len(self.depotPath) == 0:
 838            print "Internal error: cannot locate perforce depot path from existing branches"
 839            sys.exit(128)
 840
 841        self.clientPath = p4Where(self.depotPath)
 842
 843        if len(self.clientPath) == 0:
 844            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
 845            sys.exit(128)
 846
 847        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
 848        self.oldWorkingDirectory = os.getcwd()
 849
 850        chdir(self.clientPath)
 851        print "Synchronizing p4 checkout..."
 852        p4_system("sync ...")
 853
 854        self.check()
 855
 856        commits = []
 857        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
 858            commits.append(line.strip())
 859        commits.reverse()
 860
 861        while len(commits) > 0:
 862            commit = commits[0]
 863            commits = commits[1:]
 864            self.applyCommit(commit)
 865            if not self.interactive:
 866                break
 867
 868        if len(commits) == 0:
 869            print "All changes applied!"
 870            chdir(self.oldWorkingDirectory)
 871
 872            sync = P4Sync()
 873            sync.run([])
 874
 875            rebase = P4Rebase()
 876            rebase.rebase()
 877
 878        return True
 879
 880class P4Sync(Command):
 881    delete_actions = ( "delete", "move/delete", "purge" )
 882
 883    def __init__(self):
 884        Command.__init__(self)
 885        self.options = [
 886                optparse.make_option("--branch", dest="branch"),
 887                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 888                optparse.make_option("--changesfile", dest="changesFile"),
 889                optparse.make_option("--silent", dest="silent", action="store_true"),
 890                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
 891                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 892                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
 893                                     help="Import into refs/heads/ , not refs/remotes"),
 894                optparse.make_option("--max-changes", dest="maxChanges"),
 895                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
 896                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
 897                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
 898                                     help="Only sync files that are included in the Perforce Client Spec")
 899        ]
 900        self.description = """Imports from Perforce into a git repository.\n
 901    example:
 902    //depot/my/project/ -- to import the current head
 903    //depot/my/project/@all -- to import everything
 904    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 905
 906    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 907
 908        self.usage += " //depot/path[@revRange]"
 909        self.silent = False
 910        self.createdBranches = set()
 911        self.committedChanges = set()
 912        self.branch = ""
 913        self.detectBranches = False
 914        self.detectLabels = False
 915        self.changesFile = ""
 916        self.syncWithOrigin = True
 917        self.verbose = False
 918        self.importIntoRemotes = True
 919        self.maxChanges = ""
 920        self.isWindows = (platform.system() == "Windows")
 921        self.keepRepoPath = False
 922        self.depotPaths = None
 923        self.p4BranchesInGit = []
 924        self.cloneExclude = []
 925        self.useClientSpec = False
 926        self.clientSpecDirs = []
 927
 928        if gitConfig("git-p4.syncFromOrigin") == "false":
 929            self.syncWithOrigin = False
 930
 931    #
 932    # P4 wildcards are not allowed in filenames.  P4 complains
 933    # if you simply add them, but you can force it with "-f", in
 934    # which case it translates them into %xx encoding internally.
 935    # Search for and fix just these four characters.  Do % last so
 936    # that fixing it does not inadvertently create new %-escapes.
 937    #
 938    def wildcard_decode(self, path):
 939        # Cannot have * in a filename in windows; untested as to
 940        # what p4 would do in such a case.
 941        if not self.isWindows:
 942            path = path.replace("%2A", "*")
 943        path = path.replace("%23", "#") \
 944                   .replace("%40", "@") \
 945                   .replace("%25", "%")
 946        return path
 947
 948    def extractFilesFromCommit(self, commit):
 949        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
 950                             for path in self.cloneExclude]
 951        files = []
 952        fnum = 0
 953        while commit.has_key("depotFile%s" % fnum):
 954            path =  commit["depotFile%s" % fnum]
 955
 956            if [p for p in self.cloneExclude
 957                if p4PathStartsWith(path, p)]:
 958                found = False
 959            else:
 960                found = [p for p in self.depotPaths
 961                         if p4PathStartsWith(path, p)]
 962            if not found:
 963                fnum = fnum + 1
 964                continue
 965
 966            file = {}
 967            file["path"] = path
 968            file["rev"] = commit["rev%s" % fnum]
 969            file["action"] = commit["action%s" % fnum]
 970            file["type"] = commit["type%s" % fnum]
 971            files.append(file)
 972            fnum = fnum + 1
 973        return files
 974
 975    def stripRepoPath(self, path, prefixes):
 976        if self.useClientSpec:
 977
 978            # if using the client spec, we use the output directory
 979            # specified in the client.  For example, a view
 980            #   //depot/foo/branch/... //client/branch/foo/...
 981            # will end up putting all foo/branch files into
 982            #  branch/foo/
 983            for val in self.clientSpecDirs:
 984                if path.startswith(val[0]):
 985                    # replace the depot path with the client path
 986                    path = path.replace(val[0], val[1][1])
 987                    # now strip out the client (//client/...)
 988                    path = re.sub("^(//[^/]+/)", '', path)
 989                    # the rest is all path
 990                    return path
 991
 992        if self.keepRepoPath:
 993            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
 994
 995        for p in prefixes:
 996            if p4PathStartsWith(path, p):
 997                path = path[len(p):]
 998
 999        return path
1000
1001    def splitFilesIntoBranches(self, commit):
1002        branches = {}
1003        fnum = 0
1004        while commit.has_key("depotFile%s" % fnum):
1005            path =  commit["depotFile%s" % fnum]
1006            found = [p for p in self.depotPaths
1007                     if p4PathStartsWith(path, p)]
1008            if not found:
1009                fnum = fnum + 1
1010                continue
1011
1012            file = {}
1013            file["path"] = path
1014            file["rev"] = commit["rev%s" % fnum]
1015            file["action"] = commit["action%s" % fnum]
1016            file["type"] = commit["type%s" % fnum]
1017            fnum = fnum + 1
1018
1019            relPath = self.stripRepoPath(path, self.depotPaths)
1020
1021            for branch in self.knownBranches.keys():
1022
1023                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1024                if relPath.startswith(branch + "/"):
1025                    if branch not in branches:
1026                        branches[branch] = []
1027                    branches[branch].append(file)
1028                    break
1029
1030        return branches
1031
1032    # output one file from the P4 stream
1033    # - helper for streamP4Files
1034
1035    def streamOneP4File(self, file, contents):
1036        if file["type"] == "apple":
1037            print "\nfile %s is a strange apple file that forks. Ignoring" % \
1038                file['depotFile']
1039            return
1040
1041        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1042        relPath = self.wildcard_decode(relPath)
1043        if verbose:
1044            sys.stderr.write("%s\n" % relPath)
1045
1046        mode = "644"
1047        if isP4Exec(file["type"]):
1048            mode = "755"
1049        elif file["type"] == "symlink":
1050            mode = "120000"
1051            # p4 print on a symlink contains "target\n", so strip it off
1052            data = ''.join(contents)
1053            contents = [data[:-1]]
1054
1055        if self.isWindows and file["type"].endswith("text"):
1056            mangled = []
1057            for data in contents:
1058                data = data.replace("\r\n", "\n")
1059                mangled.append(data)
1060            contents = mangled
1061
1062        if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1063            contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1064        elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1065            contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1066
1067        self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1068
1069        # total length...
1070        length = 0
1071        for d in contents:
1072            length = length + len(d)
1073
1074        self.gitStream.write("data %d\n" % length)
1075        for d in contents:
1076            self.gitStream.write(d)
1077        self.gitStream.write("\n")
1078
1079    def streamOneP4Deletion(self, file):
1080        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1081        if verbose:
1082            sys.stderr.write("delete %s\n" % relPath)
1083        self.gitStream.write("D %s\n" % relPath)
1084
1085    # handle another chunk of streaming data
1086    def streamP4FilesCb(self, marshalled):
1087
1088        if marshalled.has_key('depotFile') and self.stream_have_file_info:
1089            # start of a new file - output the old one first
1090            self.streamOneP4File(self.stream_file, self.stream_contents)
1091            self.stream_file = {}
1092            self.stream_contents = []
1093            self.stream_have_file_info = False
1094
1095        # pick up the new file information... for the
1096        # 'data' field we need to append to our array
1097        for k in marshalled.keys():
1098            if k == 'data':
1099                self.stream_contents.append(marshalled['data'])
1100            else:
1101                self.stream_file[k] = marshalled[k]
1102
1103        self.stream_have_file_info = True
1104
1105    # Stream directly from "p4 files" into "git fast-import"
1106    def streamP4Files(self, files):
1107        filesForCommit = []
1108        filesToRead = []
1109        filesToDelete = []
1110
1111        for f in files:
1112            includeFile = True
1113            for val in self.clientSpecDirs:
1114                if f['path'].startswith(val[0]):
1115                    if val[1][0] <= 0:
1116                        includeFile = False
1117                    break
1118
1119            if includeFile:
1120                filesForCommit.append(f)
1121                if f['action'] in self.delete_actions:
1122                    filesToDelete.append(f)
1123                else:
1124                    filesToRead.append(f)
1125
1126        # deleted files...
1127        for f in filesToDelete:
1128            self.streamOneP4Deletion(f)
1129
1130        if len(filesToRead) > 0:
1131            self.stream_file = {}
1132            self.stream_contents = []
1133            self.stream_have_file_info = False
1134
1135            # curry self argument
1136            def streamP4FilesCbSelf(entry):
1137                self.streamP4FilesCb(entry)
1138
1139            p4CmdList("-x - print",
1140                '\n'.join(['%s#%s' % (f['path'], f['rev'])
1141                                                  for f in filesToRead]),
1142                cb=streamP4FilesCbSelf)
1143
1144            # do the last chunk
1145            if self.stream_file.has_key('depotFile'):
1146                self.streamOneP4File(self.stream_file, self.stream_contents)
1147
1148    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1149        epoch = details["time"]
1150        author = details["user"]
1151        self.branchPrefixes = branchPrefixes
1152
1153        if self.verbose:
1154            print "commit into %s" % branch
1155
1156        # start with reading files; if that fails, we should not
1157        # create a commit.
1158        new_files = []
1159        for f in files:
1160            if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1161                new_files.append (f)
1162            else:
1163                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1164
1165        self.gitStream.write("commit %s\n" % branch)
1166#        gitStream.write("mark :%s\n" % details["change"])
1167        self.committedChanges.add(int(details["change"]))
1168        committer = ""
1169        if author not in self.users:
1170            self.getUserMapFromPerforceServer()
1171        if author in self.users:
1172            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1173        else:
1174            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1175
1176        self.gitStream.write("committer %s\n" % committer)
1177
1178        self.gitStream.write("data <<EOT\n")
1179        self.gitStream.write(details["desc"])
1180        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1181                             % (','.join (branchPrefixes), details["change"]))
1182        if len(details['options']) > 0:
1183            self.gitStream.write(": options = %s" % details['options'])
1184        self.gitStream.write("]\nEOT\n\n")
1185
1186        if len(parent) > 0:
1187            if self.verbose:
1188                print "parent %s" % parent
1189            self.gitStream.write("from %s\n" % parent)
1190
1191        self.streamP4Files(new_files)
1192        self.gitStream.write("\n")
1193
1194        change = int(details["change"])
1195
1196        if self.labels.has_key(change):
1197            label = self.labels[change]
1198            labelDetails = label[0]
1199            labelRevisions = label[1]
1200            if self.verbose:
1201                print "Change %s is labelled %s" % (change, labelDetails)
1202
1203            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1204                                                    for p in branchPrefixes]))
1205
1206            if len(files) == len(labelRevisions):
1207
1208                cleanedFiles = {}
1209                for info in files:
1210                    if info["action"] in self.delete_actions:
1211                        continue
1212                    cleanedFiles[info["depotFile"]] = info["rev"]
1213
1214                if cleanedFiles == labelRevisions:
1215                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1216                    self.gitStream.write("from %s\n" % branch)
1217
1218                    owner = labelDetails["Owner"]
1219                    tagger = ""
1220                    if author in self.users:
1221                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1222                    else:
1223                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1224                    self.gitStream.write("tagger %s\n" % tagger)
1225                    self.gitStream.write("data <<EOT\n")
1226                    self.gitStream.write(labelDetails["Description"])
1227                    self.gitStream.write("EOT\n\n")
1228
1229                else:
1230                    if not self.silent:
1231                        print ("Tag %s does not match with change %s: files do not match."
1232                               % (labelDetails["label"], change))
1233
1234            else:
1235                if not self.silent:
1236                    print ("Tag %s does not match with change %s: file count is different."
1237                           % (labelDetails["label"], change))
1238
1239    def getUserCacheFilename(self):
1240        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1241        return home + "/.gitp4-usercache.txt"
1242
1243    def getUserMapFromPerforceServer(self):
1244        if self.userMapFromPerforceServer:
1245            return
1246        self.users = {}
1247
1248        for output in p4CmdList("users"):
1249            if not output.has_key("User"):
1250                continue
1251            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1252
1253
1254        s = ''
1255        for (key, val) in self.users.items():
1256            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1257
1258        open(self.getUserCacheFilename(), "wb").write(s)
1259        self.userMapFromPerforceServer = True
1260
1261    def loadUserMapFromCache(self):
1262        self.users = {}
1263        self.userMapFromPerforceServer = False
1264        try:
1265            cache = open(self.getUserCacheFilename(), "rb")
1266            lines = cache.readlines()
1267            cache.close()
1268            for line in lines:
1269                entry = line.strip().split("\t")
1270                self.users[entry[0]] = entry[1]
1271        except IOError:
1272            self.getUserMapFromPerforceServer()
1273
1274    def getLabels(self):
1275        self.labels = {}
1276
1277        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1278        if len(l) > 0 and not self.silent:
1279            print "Finding files belonging to labels in %s" % `self.depotPaths`
1280
1281        for output in l:
1282            label = output["label"]
1283            revisions = {}
1284            newestChange = 0
1285            if self.verbose:
1286                print "Querying files for label %s" % label
1287            for file in p4CmdList("files "
1288                                  +  ' '.join (["%s...@%s" % (p, label)
1289                                                for p in self.depotPaths])):
1290                revisions[file["depotFile"]] = file["rev"]
1291                change = int(file["change"])
1292                if change > newestChange:
1293                    newestChange = change
1294
1295            self.labels[newestChange] = [output, revisions]
1296
1297        if self.verbose:
1298            print "Label changes: %s" % self.labels.keys()
1299
1300    def guessProjectName(self):
1301        for p in self.depotPaths:
1302            if p.endswith("/"):
1303                p = p[:-1]
1304            p = p[p.strip().rfind("/") + 1:]
1305            if not p.endswith("/"):
1306               p += "/"
1307            return p
1308
1309    def getBranchMapping(self):
1310        lostAndFoundBranches = set()
1311
1312        for info in p4CmdList("branches"):
1313            details = p4Cmd("branch -o %s" % info["branch"])
1314            viewIdx = 0
1315            while details.has_key("View%s" % viewIdx):
1316                paths = details["View%s" % viewIdx].split(" ")
1317                viewIdx = viewIdx + 1
1318                # require standard //depot/foo/... //depot/bar/... mapping
1319                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1320                    continue
1321                source = paths[0]
1322                destination = paths[1]
1323                ## HACK
1324                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1325                    source = source[len(self.depotPaths[0]):-4]
1326                    destination = destination[len(self.depotPaths[0]):-4]
1327
1328                    if destination in self.knownBranches:
1329                        if not self.silent:
1330                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1331                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1332                        continue
1333
1334                    self.knownBranches[destination] = source
1335
1336                    lostAndFoundBranches.discard(destination)
1337
1338                    if source not in self.knownBranches:
1339                        lostAndFoundBranches.add(source)
1340
1341
1342        for branch in lostAndFoundBranches:
1343            self.knownBranches[branch] = branch
1344
1345    def getBranchMappingFromGitBranches(self):
1346        branches = p4BranchesInGit(self.importIntoRemotes)
1347        for branch in branches.keys():
1348            if branch == "master":
1349                branch = "main"
1350            else:
1351                branch = branch[len(self.projectName):]
1352            self.knownBranches[branch] = branch
1353
1354    def listExistingP4GitBranches(self):
1355        # branches holds mapping from name to commit
1356        branches = p4BranchesInGit(self.importIntoRemotes)
1357        self.p4BranchesInGit = branches.keys()
1358        for branch in branches.keys():
1359            self.initialParents[self.refPrefix + branch] = branches[branch]
1360
1361    def updateOptionDict(self, d):
1362        option_keys = {}
1363        if self.keepRepoPath:
1364            option_keys['keepRepoPath'] = 1
1365
1366        d["options"] = ' '.join(sorted(option_keys.keys()))
1367
1368    def readOptions(self, d):
1369        self.keepRepoPath = (d.has_key('options')
1370                             and ('keepRepoPath' in d['options']))
1371
1372    def gitRefForBranch(self, branch):
1373        if branch == "main":
1374            return self.refPrefix + "master"
1375
1376        if len(branch) <= 0:
1377            return branch
1378
1379        return self.refPrefix + self.projectName + branch
1380
1381    def gitCommitByP4Change(self, ref, change):
1382        if self.verbose:
1383            print "looking in ref " + ref + " for change %s using bisect..." % change
1384
1385        earliestCommit = ""
1386        latestCommit = parseRevision(ref)
1387
1388        while True:
1389            if self.verbose:
1390                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1391            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1392            if len(next) == 0:
1393                if self.verbose:
1394                    print "argh"
1395                return ""
1396            log = extractLogMessageFromGitCommit(next)
1397            settings = extractSettingsGitLog(log)
1398            currentChange = int(settings['change'])
1399            if self.verbose:
1400                print "current change %s" % currentChange
1401
1402            if currentChange == change:
1403                if self.verbose:
1404                    print "found %s" % next
1405                return next
1406
1407            if currentChange < change:
1408                earliestCommit = "^%s" % next
1409            else:
1410                latestCommit = "%s" % next
1411
1412        return ""
1413
1414    def importNewBranch(self, branch, maxChange):
1415        # make fast-import flush all changes to disk and update the refs using the checkpoint
1416        # command so that we can try to find the branch parent in the git history
1417        self.gitStream.write("checkpoint\n\n");
1418        self.gitStream.flush();
1419        branchPrefix = self.depotPaths[0] + branch + "/"
1420        range = "@1,%s" % maxChange
1421        #print "prefix" + branchPrefix
1422        changes = p4ChangesForPaths([branchPrefix], range)
1423        if len(changes) <= 0:
1424            return False
1425        firstChange = changes[0]
1426        #print "first change in branch: %s" % firstChange
1427        sourceBranch = self.knownBranches[branch]
1428        sourceDepotPath = self.depotPaths[0] + sourceBranch
1429        sourceRef = self.gitRefForBranch(sourceBranch)
1430        #print "source " + sourceBranch
1431
1432        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1433        #print "branch parent: %s" % branchParentChange
1434        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1435        if len(gitParent) > 0:
1436            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1437            #print "parent git commit: %s" % gitParent
1438
1439        self.importChanges(changes)
1440        return True
1441
1442    def importChanges(self, changes):
1443        cnt = 1
1444        for change in changes:
1445            description = p4Cmd("describe %s" % change)
1446            self.updateOptionDict(description)
1447
1448            if not self.silent:
1449                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1450                sys.stdout.flush()
1451            cnt = cnt + 1
1452
1453            try:
1454                if self.detectBranches:
1455                    branches = self.splitFilesIntoBranches(description)
1456                    for branch in branches.keys():
1457                        ## HACK  --hwn
1458                        branchPrefix = self.depotPaths[0] + branch + "/"
1459
1460                        parent = ""
1461
1462                        filesForCommit = branches[branch]
1463
1464                        if self.verbose:
1465                            print "branch is %s" % branch
1466
1467                        self.updatedBranches.add(branch)
1468
1469                        if branch not in self.createdBranches:
1470                            self.createdBranches.add(branch)
1471                            parent = self.knownBranches[branch]
1472                            if parent == branch:
1473                                parent = ""
1474                            else:
1475                                fullBranch = self.projectName + branch
1476                                if fullBranch not in self.p4BranchesInGit:
1477                                    if not self.silent:
1478                                        print("\n    Importing new branch %s" % fullBranch);
1479                                    if self.importNewBranch(branch, change - 1):
1480                                        parent = ""
1481                                        self.p4BranchesInGit.append(fullBranch)
1482                                    if not self.silent:
1483                                        print("\n    Resuming with change %s" % change);
1484
1485                                if self.verbose:
1486                                    print "parent determined through known branches: %s" % parent
1487
1488                        branch = self.gitRefForBranch(branch)
1489                        parent = self.gitRefForBranch(parent)
1490
1491                        if self.verbose:
1492                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1493
1494                        if len(parent) == 0 and branch in self.initialParents:
1495                            parent = self.initialParents[branch]
1496                            del self.initialParents[branch]
1497
1498                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1499                else:
1500                    files = self.extractFilesFromCommit(description)
1501                    self.commit(description, files, self.branch, self.depotPaths,
1502                                self.initialParent)
1503                    self.initialParent = ""
1504            except IOError:
1505                print self.gitError.read()
1506                sys.exit(1)
1507
1508    def importHeadRevision(self, revision):
1509        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1510
1511        details = { "user" : "git perforce import user", "time" : int(time.time()) }
1512        details["desc"] = ("Initial import of %s from the state at revision %s\n"
1513                           % (' '.join(self.depotPaths), revision))
1514        details["change"] = revision
1515        newestRevision = 0
1516
1517        fileCnt = 0
1518        for info in p4CmdList("files "
1519                              +  ' '.join(["%s...%s"
1520                                           % (p, revision)
1521                                           for p in self.depotPaths])):
1522
1523            if 'code' in info and info['code'] == 'error':
1524                sys.stderr.write("p4 returned an error: %s\n"
1525                                 % info['data'])
1526                if info['data'].find("must refer to client") >= 0:
1527                    sys.stderr.write("This particular p4 error is misleading.\n")
1528                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
1529                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
1530                sys.exit(1)
1531            if 'p4ExitCode' in info:
1532                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1533                sys.exit(1)
1534
1535
1536            change = int(info["change"])
1537            if change > newestRevision:
1538                newestRevision = change
1539
1540            if info["action"] in self.delete_actions:
1541                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1542                #fileCnt = fileCnt + 1
1543                continue
1544
1545            for prop in ["depotFile", "rev", "action", "type" ]:
1546                details["%s%s" % (prop, fileCnt)] = info[prop]
1547
1548            fileCnt = fileCnt + 1
1549
1550        details["change"] = newestRevision
1551        self.updateOptionDict(details)
1552        try:
1553            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1554        except IOError:
1555            print "IO error with git fast-import. Is your git version recent enough?"
1556            print self.gitError.read()
1557
1558
1559    def getClientSpec(self):
1560        specList = p4CmdList( "client -o" )
1561        temp = {}
1562        for entry in specList:
1563            for k,v in entry.iteritems():
1564                if k.startswith("View"):
1565
1566                    # p4 has these %%1 to %%9 arguments in specs to
1567                    # reorder paths; which we can't handle (yet :)
1568                    if re.match('%%\d', v) != None:
1569                        print "Sorry, can't handle %%n arguments in client specs"
1570                        sys.exit(1)
1571
1572                    if v.startswith('"'):
1573                        start = 1
1574                    else:
1575                        start = 0
1576                    index = v.find("...")
1577
1578                    # save the "client view"; i.e the RHS of the view
1579                    # line that tells the client where to put the
1580                    # files for this view.
1581                    cv = v[index+3:].strip() # +3 to remove previous '...'
1582
1583                    # if the client view doesn't end with a
1584                    # ... wildcard, then we're going to mess up the
1585                    # output directory, so fail gracefully.
1586                    if not cv.endswith('...'):
1587                        print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1588                        sys.exit(1)
1589                    cv=cv[:-3]
1590
1591                    # now save the view; +index means included, -index
1592                    # means it should be filtered out.
1593                    v = v[start:index]
1594                    if v.startswith("-"):
1595                        v = v[1:]
1596                        include = -len(v)
1597                    else:
1598                        include = len(v)
1599
1600                    temp[v] = (include, cv)
1601
1602        self.clientSpecDirs = temp.items()
1603        self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1604
1605    def run(self, args):
1606        self.depotPaths = []
1607        self.changeRange = ""
1608        self.initialParent = ""
1609        self.previousDepotPaths = []
1610
1611        # map from branch depot path to parent branch
1612        self.knownBranches = {}
1613        self.initialParents = {}
1614        self.hasOrigin = originP4BranchesExist()
1615        if not self.syncWithOrigin:
1616            self.hasOrigin = False
1617
1618        if self.importIntoRemotes:
1619            self.refPrefix = "refs/remotes/p4/"
1620        else:
1621            self.refPrefix = "refs/heads/p4/"
1622
1623        if self.syncWithOrigin and self.hasOrigin:
1624            if not self.silent:
1625                print "Syncing with origin first by calling git fetch origin"
1626            system("git fetch origin")
1627
1628        if len(self.branch) == 0:
1629            self.branch = self.refPrefix + "master"
1630            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1631                system("git update-ref %s refs/heads/p4" % self.branch)
1632                system("git branch -D p4");
1633            # create it /after/ importing, when master exists
1634            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1635                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1636
1637        if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1638            self.getClientSpec()
1639
1640        # TODO: should always look at previous commits,
1641        # merge with previous imports, if possible.
1642        if args == []:
1643            if self.hasOrigin:
1644                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1645            self.listExistingP4GitBranches()
1646
1647            if len(self.p4BranchesInGit) > 1:
1648                if not self.silent:
1649                    print "Importing from/into multiple branches"
1650                self.detectBranches = True
1651
1652            if self.verbose:
1653                print "branches: %s" % self.p4BranchesInGit
1654
1655            p4Change = 0
1656            for branch in self.p4BranchesInGit:
1657                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1658
1659                settings = extractSettingsGitLog(logMsg)
1660
1661                self.readOptions(settings)
1662                if (settings.has_key('depot-paths')
1663                    and settings.has_key ('change')):
1664                    change = int(settings['change']) + 1
1665                    p4Change = max(p4Change, change)
1666
1667                    depotPaths = sorted(settings['depot-paths'])
1668                    if self.previousDepotPaths == []:
1669                        self.previousDepotPaths = depotPaths
1670                    else:
1671                        paths = []
1672                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1673                            for i in range(0, min(len(cur), len(prev))):
1674                                if cur[i] <> prev[i]:
1675                                    i = i - 1
1676                                    break
1677
1678                            paths.append (cur[:i + 1])
1679
1680                        self.previousDepotPaths = paths
1681
1682            if p4Change > 0:
1683                self.depotPaths = sorted(self.previousDepotPaths)
1684                self.changeRange = "@%s,#head" % p4Change
1685                if not self.detectBranches:
1686                    self.initialParent = parseRevision(self.branch)
1687                if not self.silent and not self.detectBranches:
1688                    print "Performing incremental import into %s git branch" % self.branch
1689
1690        if not self.branch.startswith("refs/"):
1691            self.branch = "refs/heads/" + self.branch
1692
1693        if len(args) == 0 and self.depotPaths:
1694            if not self.silent:
1695                print "Depot paths: %s" % ' '.join(self.depotPaths)
1696        else:
1697            if self.depotPaths and self.depotPaths != args:
1698                print ("previous import used depot path %s and now %s was specified. "
1699                       "This doesn't work!" % (' '.join (self.depotPaths),
1700                                               ' '.join (args)))
1701                sys.exit(1)
1702
1703            self.depotPaths = sorted(args)
1704
1705        revision = ""
1706        self.users = {}
1707
1708        newPaths = []
1709        for p in self.depotPaths:
1710            if p.find("@") != -1:
1711                atIdx = p.index("@")
1712                self.changeRange = p[atIdx:]
1713                if self.changeRange == "@all":
1714                    self.changeRange = ""
1715                elif ',' not in self.changeRange:
1716                    revision = self.changeRange
1717                    self.changeRange = ""
1718                p = p[:atIdx]
1719            elif p.find("#") != -1:
1720                hashIdx = p.index("#")
1721                revision = p[hashIdx:]
1722                p = p[:hashIdx]
1723            elif self.previousDepotPaths == []:
1724                revision = "#head"
1725
1726            p = re.sub ("\.\.\.$", "", p)
1727            if not p.endswith("/"):
1728                p += "/"
1729
1730            newPaths.append(p)
1731
1732        self.depotPaths = newPaths
1733
1734
1735        self.loadUserMapFromCache()
1736        self.labels = {}
1737        if self.detectLabels:
1738            self.getLabels();
1739
1740        if self.detectBranches:
1741            ## FIXME - what's a P4 projectName ?
1742            self.projectName = self.guessProjectName()
1743
1744            if self.hasOrigin:
1745                self.getBranchMappingFromGitBranches()
1746            else:
1747                self.getBranchMapping()
1748            if self.verbose:
1749                print "p4-git branches: %s" % self.p4BranchesInGit
1750                print "initial parents: %s" % self.initialParents
1751            for b in self.p4BranchesInGit:
1752                if b != "master":
1753
1754                    ## FIXME
1755                    b = b[len(self.projectName):]
1756                self.createdBranches.add(b)
1757
1758        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1759
1760        importProcess = subprocess.Popen(["git", "fast-import"],
1761                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1762                                         stderr=subprocess.PIPE);
1763        self.gitOutput = importProcess.stdout
1764        self.gitStream = importProcess.stdin
1765        self.gitError = importProcess.stderr
1766
1767        if revision:
1768            self.importHeadRevision(revision)
1769        else:
1770            changes = []
1771
1772            if len(self.changesFile) > 0:
1773                output = open(self.changesFile).readlines()
1774                changeSet = set()
1775                for line in output:
1776                    changeSet.add(int(line))
1777
1778                for change in changeSet:
1779                    changes.append(change)
1780
1781                changes.sort()
1782            else:
1783                # catch "git-p4 sync" with no new branches, in a repo that
1784                # does not have any existing git-p4 branches
1785                if len(args) == 0 and not self.p4BranchesInGit:
1786                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
1787                if self.verbose:
1788                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1789                                                              self.changeRange)
1790                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1791
1792                if len(self.maxChanges) > 0:
1793                    changes = changes[:min(int(self.maxChanges), len(changes))]
1794
1795            if len(changes) == 0:
1796                if not self.silent:
1797                    print "No changes to import!"
1798                return True
1799
1800            if not self.silent and not self.detectBranches:
1801                print "Import destination: %s" % self.branch
1802
1803            self.updatedBranches = set()
1804
1805            self.importChanges(changes)
1806
1807            if not self.silent:
1808                print ""
1809                if len(self.updatedBranches) > 0:
1810                    sys.stdout.write("Updated branches: ")
1811                    for b in self.updatedBranches:
1812                        sys.stdout.write("%s " % b)
1813                    sys.stdout.write("\n")
1814
1815        self.gitStream.close()
1816        if importProcess.wait() != 0:
1817            die("fast-import failed: %s" % self.gitError.read())
1818        self.gitOutput.close()
1819        self.gitError.close()
1820
1821        return True
1822
1823class P4Rebase(Command):
1824    def __init__(self):
1825        Command.__init__(self)
1826        self.options = [ ]
1827        self.description = ("Fetches the latest revision from perforce and "
1828                            + "rebases the current work (branch) against it")
1829        self.verbose = False
1830
1831    def run(self, args):
1832        sync = P4Sync()
1833        sync.run([])
1834
1835        return self.rebase()
1836
1837    def rebase(self):
1838        if os.system("git update-index --refresh") != 0:
1839            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.");
1840        if len(read_pipe("git diff-index HEAD --")) > 0:
1841            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1842
1843        [upstream, settings] = findUpstreamBranchPoint()
1844        if len(upstream) == 0:
1845            die("Cannot find upstream branchpoint for rebase")
1846
1847        # the branchpoint may be p4/foo~3, so strip off the parent
1848        upstream = re.sub("~[0-9]+$", "", upstream)
1849
1850        print "Rebasing the current branch onto %s" % upstream
1851        oldHead = read_pipe("git rev-parse HEAD").strip()
1852        system("git rebase %s" % upstream)
1853        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1854        return True
1855
1856class P4Clone(P4Sync):
1857    def __init__(self):
1858        P4Sync.__init__(self)
1859        self.description = "Creates a new git repository and imports from Perforce into it"
1860        self.usage = "usage: %prog [options] //depot/path[@revRange]"
1861        self.options += [
1862            optparse.make_option("--destination", dest="cloneDestination",
1863                                 action='store', default=None,
1864                                 help="where to leave result of the clone"),
1865            optparse.make_option("-/", dest="cloneExclude",
1866                                 action="append", type="string",
1867                                 help="exclude depot path"),
1868            optparse.make_option("--bare", dest="cloneBare",
1869                                 action="store_true", default=False),
1870        ]
1871        self.cloneDestination = None
1872        self.needsGit = False
1873        self.cloneBare = False
1874
1875    # This is required for the "append" cloneExclude action
1876    def ensure_value(self, attr, value):
1877        if not hasattr(self, attr) or getattr(self, attr) is None:
1878            setattr(self, attr, value)
1879        return getattr(self, attr)
1880
1881    def defaultDestination(self, args):
1882        ## TODO: use common prefix of args?
1883        depotPath = args[0]
1884        depotDir = re.sub("(@[^@]*)$", "", depotPath)
1885        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1886        depotDir = re.sub(r"\.\.\.$", "", depotDir)
1887        depotDir = re.sub(r"/$", "", depotDir)
1888        return os.path.split(depotDir)[1]
1889
1890    def run(self, args):
1891        if len(args) < 1:
1892            return False
1893
1894        if self.keepRepoPath and not self.cloneDestination:
1895            sys.stderr.write("Must specify destination for --keep-path\n")
1896            sys.exit(1)
1897
1898        depotPaths = args
1899
1900        if not self.cloneDestination and len(depotPaths) > 1:
1901            self.cloneDestination = depotPaths[-1]
1902            depotPaths = depotPaths[:-1]
1903
1904        self.cloneExclude = ["/"+p for p in self.cloneExclude]
1905        for p in depotPaths:
1906            if not p.startswith("//"):
1907                return False
1908
1909        if not self.cloneDestination:
1910            self.cloneDestination = self.defaultDestination(args)
1911
1912        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1913
1914        if not os.path.exists(self.cloneDestination):
1915            os.makedirs(self.cloneDestination)
1916        chdir(self.cloneDestination)
1917
1918        init_cmd = [ "git", "init" ]
1919        if self.cloneBare:
1920            init_cmd.append("--bare")
1921        subprocess.check_call(init_cmd)
1922
1923        if not P4Sync.run(self, depotPaths):
1924            return False
1925        if self.branch != "master":
1926            if self.importIntoRemotes:
1927                masterbranch = "refs/remotes/p4/master"
1928            else:
1929                masterbranch = "refs/heads/p4/master"
1930            if gitBranchExists(masterbranch):
1931                system("git branch master %s" % masterbranch)
1932                if not self.cloneBare:
1933                    system("git checkout -f")
1934            else:
1935                print "Could not detect main branch. No checkout/master branch created."
1936
1937        return True
1938
1939class P4Branches(Command):
1940    def __init__(self):
1941        Command.__init__(self)
1942        self.options = [ ]
1943        self.description = ("Shows the git branches that hold imports and their "
1944                            + "corresponding perforce depot paths")
1945        self.verbose = False
1946
1947    def run(self, args):
1948        if originP4BranchesExist():
1949            createOrUpdateBranchesFromOrigin()
1950
1951        cmdline = "git rev-parse --symbolic "
1952        cmdline += " --remotes"
1953
1954        for line in read_pipe_lines(cmdline):
1955            line = line.strip()
1956
1957            if not line.startswith('p4/') or line == "p4/HEAD":
1958                continue
1959            branch = line
1960
1961            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1962            settings = extractSettingsGitLog(log)
1963
1964            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1965        return True
1966
1967class HelpFormatter(optparse.IndentedHelpFormatter):
1968    def __init__(self):
1969        optparse.IndentedHelpFormatter.__init__(self)
1970
1971    def format_description(self, description):
1972        if description:
1973            return description + "\n"
1974        else:
1975            return ""
1976
1977def printUsage(commands):
1978    print "usage: %s <command> [options]" % sys.argv[0]
1979    print ""
1980    print "valid commands: %s" % ", ".join(commands)
1981    print ""
1982    print "Try %s <command> --help for command specific help." % sys.argv[0]
1983    print ""
1984
1985commands = {
1986    "debug" : P4Debug,
1987    "submit" : P4Submit,
1988    "commit" : P4Submit,
1989    "sync" : P4Sync,
1990    "rebase" : P4Rebase,
1991    "clone" : P4Clone,
1992    "rollback" : P4RollBack,
1993    "branches" : P4Branches
1994}
1995
1996
1997def main():
1998    if len(sys.argv[1:]) == 0:
1999        printUsage(commands.keys())
2000        sys.exit(2)
2001
2002    cmd = ""
2003    cmdName = sys.argv[1]
2004    try:
2005        klass = commands[cmdName]
2006        cmd = klass()
2007    except KeyError:
2008        print "unknown command %s" % cmdName
2009        print ""
2010        printUsage(commands.keys())
2011        sys.exit(2)
2012
2013    options = cmd.options
2014    cmd.gitdir = os.environ.get("GIT_DIR", None)
2015
2016    args = sys.argv[2:]
2017
2018    if len(options) > 0:
2019        options.append(optparse.make_option("--git-dir", dest="gitdir"))
2020
2021        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2022                                       options,
2023                                       description = cmd.description,
2024                                       formatter = HelpFormatter())
2025
2026        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2027    global verbose
2028    verbose = cmd.verbose
2029    if cmd.needsGit:
2030        if cmd.gitdir == None:
2031            cmd.gitdir = os.path.abspath(".git")
2032            if not isValidGitDir(cmd.gitdir):
2033                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2034                if os.path.exists(cmd.gitdir):
2035                    cdup = read_pipe("git rev-parse --show-cdup").strip()
2036                    if len(cdup) > 0:
2037                        chdir(cdup);
2038
2039        if not isValidGitDir(cmd.gitdir):
2040            if isValidGitDir(cmd.gitdir + "/.git"):
2041                cmd.gitdir += "/.git"
2042            else:
2043                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2044
2045        os.environ["GIT_DIR"] = cmd.gitdir
2046
2047    if not cmd.run(args):
2048        parser.print_help()
2049
2050
2051if __name__ == '__main__':
2052    main()