git-p4.pyon commit Merge branch 'it/fetch-pack-many-refs' (77cab8a)
   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
 599def getClientSpec():
 600    """Look at the p4 client spec, create a View() object that contains
 601       all the mappings, and return it."""
 602
 603    specList = p4CmdList("client -o")
 604    if len(specList) != 1:
 605        die('Output from "client -o" is %d lines, expecting 1' %
 606            len(specList))
 607
 608    # dictionary of all client parameters
 609    entry = specList[0]
 610
 611    # just the keys that start with "View"
 612    view_keys = [ k for k in entry.keys() if k.startswith("View") ]
 613
 614    # hold this new View
 615    view = View()
 616
 617    # append the lines, in order, to the view
 618    for view_num in range(len(view_keys)):
 619        k = "View%d" % view_num
 620        if k not in view_keys:
 621            die("Expected view key %s missing" % k)
 622        view.append(entry[k])
 623
 624    return view
 625
 626def getClientRoot():
 627    """Grab the client directory."""
 628
 629    output = p4CmdList("client -o")
 630    if len(output) != 1:
 631        die('Output from "client -o" is %d lines, expecting 1' % len(output))
 632
 633    entry = output[0]
 634    if "Root" not in entry:
 635        die('Client has no "Root"')
 636
 637    return entry["Root"]
 638
 639class Command:
 640    def __init__(self):
 641        self.usage = "usage: %prog [options]"
 642        self.needsGit = True
 643
 644class P4UserMap:
 645    def __init__(self):
 646        self.userMapFromPerforceServer = False
 647        self.myP4UserId = None
 648
 649    def p4UserId(self):
 650        if self.myP4UserId:
 651            return self.myP4UserId
 652
 653        results = p4CmdList("user -o")
 654        for r in results:
 655            if r.has_key('User'):
 656                self.myP4UserId = r['User']
 657                return r['User']
 658        die("Could not find your p4 user id")
 659
 660    def p4UserIsMe(self, p4User):
 661        # return True if the given p4 user is actually me
 662        me = self.p4UserId()
 663        if not p4User or p4User != me:
 664            return False
 665        else:
 666            return True
 667
 668    def getUserCacheFilename(self):
 669        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
 670        return home + "/.gitp4-usercache.txt"
 671
 672    def getUserMapFromPerforceServer(self):
 673        if self.userMapFromPerforceServer:
 674            return
 675        self.users = {}
 676        self.emails = {}
 677
 678        for output in p4CmdList("users"):
 679            if not output.has_key("User"):
 680                continue
 681            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
 682            self.emails[output["Email"]] = output["User"]
 683
 684
 685        s = ''
 686        for (key, val) in self.users.items():
 687            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
 688
 689        open(self.getUserCacheFilename(), "wb").write(s)
 690        self.userMapFromPerforceServer = True
 691
 692    def loadUserMapFromCache(self):
 693        self.users = {}
 694        self.userMapFromPerforceServer = False
 695        try:
 696            cache = open(self.getUserCacheFilename(), "rb")
 697            lines = cache.readlines()
 698            cache.close()
 699            for line in lines:
 700                entry = line.strip().split("\t")
 701                self.users[entry[0]] = entry[1]
 702        except IOError:
 703            self.getUserMapFromPerforceServer()
 704
 705class P4Debug(Command):
 706    def __init__(self):
 707        Command.__init__(self)
 708        self.options = [
 709            optparse.make_option("--verbose", dest="verbose", action="store_true",
 710                                 default=False),
 711            ]
 712        self.description = "A tool to debug the output of p4 -G."
 713        self.needsGit = False
 714        self.verbose = False
 715
 716    def run(self, args):
 717        j = 0
 718        for output in p4CmdList(args):
 719            print 'Element: %d' % j
 720            j += 1
 721            print output
 722        return True
 723
 724class P4RollBack(Command):
 725    def __init__(self):
 726        Command.__init__(self)
 727        self.options = [
 728            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 729            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 730        ]
 731        self.description = "A tool to debug the multi-branch import. Don't use :)"
 732        self.verbose = False
 733        self.rollbackLocalBranches = False
 734
 735    def run(self, args):
 736        if len(args) != 1:
 737            return False
 738        maxChange = int(args[0])
 739
 740        if "p4ExitCode" in p4Cmd("changes -m 1"):
 741            die("Problems executing p4");
 742
 743        if self.rollbackLocalBranches:
 744            refPrefix = "refs/heads/"
 745            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 746        else:
 747            refPrefix = "refs/remotes/"
 748            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 749
 750        for line in lines:
 751            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 752                line = line.strip()
 753                ref = refPrefix + line
 754                log = extractLogMessageFromGitCommit(ref)
 755                settings = extractSettingsGitLog(log)
 756
 757                depotPaths = settings['depot-paths']
 758                change = settings['change']
 759
 760                changed = False
 761
 762                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 763                                                           for p in depotPaths]))) == 0:
 764                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 765                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 766                    continue
 767
 768                while change and int(change) > maxChange:
 769                    changed = True
 770                    if self.verbose:
 771                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 772                    system("git update-ref %s \"%s^\"" % (ref, ref))
 773                    log = extractLogMessageFromGitCommit(ref)
 774                    settings =  extractSettingsGitLog(log)
 775
 776
 777                    depotPaths = settings['depot-paths']
 778                    change = settings['change']
 779
 780                if changed:
 781                    print "%s rewound to %s" % (ref, change)
 782
 783        return True
 784
 785class P4Submit(Command, P4UserMap):
 786    def __init__(self):
 787        Command.__init__(self)
 788        P4UserMap.__init__(self)
 789        self.options = [
 790                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 791                optparse.make_option("--origin", dest="origin"),
 792                optparse.make_option("-M", dest="detectRenames", action="store_true"),
 793                # preserve the user, requires relevant p4 permissions
 794                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
 795        ]
 796        self.description = "Submit changes from git to the perforce depot."
 797        self.usage += " [name of git branch to submit into perforce depot]"
 798        self.interactive = True
 799        self.origin = ""
 800        self.detectRenames = False
 801        self.verbose = False
 802        self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
 803        self.isWindows = (platform.system() == "Windows")
 804
 805    def check(self):
 806        if len(p4CmdList("opened ...")) > 0:
 807            die("You have files opened with perforce! Close them before starting the sync.")
 808
 809    # replaces everything between 'Description:' and the next P4 submit template field with the
 810    # commit message
 811    def prepareLogMessage(self, template, message):
 812        result = ""
 813
 814        inDescriptionSection = False
 815
 816        for line in template.split("\n"):
 817            if line.startswith("#"):
 818                result += line + "\n"
 819                continue
 820
 821            if inDescriptionSection:
 822                if line.startswith("Files:") or line.startswith("Jobs:"):
 823                    inDescriptionSection = False
 824                else:
 825                    continue
 826            else:
 827                if line.startswith("Description:"):
 828                    inDescriptionSection = True
 829                    line += "\n"
 830                    for messageLine in message.split("\n"):
 831                        line += "\t" + messageLine + "\n"
 832
 833            result += line + "\n"
 834
 835        return result
 836
 837    def patchRCSKeywords(self, file, pattern):
 838        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
 839        (handle, outFileName) = tempfile.mkstemp(dir='.')
 840        try:
 841            outFile = os.fdopen(handle, "w+")
 842            inFile = open(file, "r")
 843            regexp = re.compile(pattern, re.VERBOSE)
 844            for line in inFile.readlines():
 845                line = regexp.sub(r'$\1$', line)
 846                outFile.write(line)
 847            inFile.close()
 848            outFile.close()
 849            # Forcibly overwrite the original file
 850            os.unlink(file)
 851            shutil.move(outFileName, file)
 852        except:
 853            # cleanup our temporary file
 854            os.unlink(outFileName)
 855            print "Failed to strip RCS keywords in %s" % file
 856            raise
 857
 858        print "Patched up RCS keywords in %s" % file
 859
 860    def p4UserForCommit(self,id):
 861        # Return the tuple (perforce user,git email) for a given git commit id
 862        self.getUserMapFromPerforceServer()
 863        gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
 864        gitEmail = gitEmail.strip()
 865        if not self.emails.has_key(gitEmail):
 866            return (None,gitEmail)
 867        else:
 868            return (self.emails[gitEmail],gitEmail)
 869
 870    def checkValidP4Users(self,commits):
 871        # check if any git authors cannot be mapped to p4 users
 872        for id in commits:
 873            (user,email) = self.p4UserForCommit(id)
 874            if not user:
 875                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
 876                if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
 877                    print "%s" % msg
 878                else:
 879                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
 880
 881    def lastP4Changelist(self):
 882        # Get back the last changelist number submitted in this client spec. This
 883        # then gets used to patch up the username in the change. If the same
 884        # client spec is being used by multiple processes then this might go
 885        # wrong.
 886        results = p4CmdList("client -o")        # find the current client
 887        client = None
 888        for r in results:
 889            if r.has_key('Client'):
 890                client = r['Client']
 891                break
 892        if not client:
 893            die("could not get client spec")
 894        results = p4CmdList(["changes", "-c", client, "-m", "1"])
 895        for r in results:
 896            if r.has_key('change'):
 897                return r['change']
 898        die("Could not get changelist number for last submit - cannot patch up user details")
 899
 900    def modifyChangelistUser(self, changelist, newUser):
 901        # fixup the user field of a changelist after it has been submitted.
 902        changes = p4CmdList("change -o %s" % changelist)
 903        if len(changes) != 1:
 904            die("Bad output from p4 change modifying %s to user %s" %
 905                (changelist, newUser))
 906
 907        c = changes[0]
 908        if c['User'] == newUser: return   # nothing to do
 909        c['User'] = newUser
 910        input = marshal.dumps(c)
 911
 912        result = p4CmdList("change -f -i", stdin=input)
 913        for r in result:
 914            if r.has_key('code'):
 915                if r['code'] == 'error':
 916                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
 917            if r.has_key('data'):
 918                print("Updated user field for changelist %s to %s" % (changelist, newUser))
 919                return
 920        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
 921
 922    def canChangeChangelists(self):
 923        # check to see if we have p4 admin or super-user permissions, either of
 924        # which are required to modify changelists.
 925        results = p4CmdList(["protects", self.depotPath])
 926        for r in results:
 927            if r.has_key('perm'):
 928                if r['perm'] == 'admin':
 929                    return 1
 930                if r['perm'] == 'super':
 931                    return 1
 932        return 0
 933
 934    def prepareSubmitTemplate(self):
 935        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 936        template = ""
 937        inFilesSection = False
 938        for line in p4_read_pipe_lines(['change', '-o']):
 939            if line.endswith("\r\n"):
 940                line = line[:-2] + "\n"
 941            if inFilesSection:
 942                if line.startswith("\t"):
 943                    # path starts and ends with a tab
 944                    path = line[1:]
 945                    lastTab = path.rfind("\t")
 946                    if lastTab != -1:
 947                        path = path[:lastTab]
 948                        if not p4PathStartsWith(path, self.depotPath):
 949                            continue
 950                else:
 951                    inFilesSection = False
 952            else:
 953                if line.startswith("Files:"):
 954                    inFilesSection = True
 955
 956            template += line
 957
 958        return template
 959
 960    def edit_template(self, template_file):
 961        """Invoke the editor to let the user change the submission
 962           message.  Return true if okay to continue with the submit."""
 963
 964        # if configured to skip the editing part, just submit
 965        if gitConfig("git-p4.skipSubmitEdit") == "true":
 966            return True
 967
 968        # look at the modification time, to check later if the user saved
 969        # the file
 970        mtime = os.stat(template_file).st_mtime
 971
 972        # invoke the editor
 973        if os.environ.has_key("P4EDITOR"):
 974            editor = os.environ.get("P4EDITOR")
 975        else:
 976            editor = read_pipe("git var GIT_EDITOR").strip()
 977        system(editor + " " + template_file)
 978
 979        # If the file was not saved, prompt to see if this patch should
 980        # be skipped.  But skip this verification step if configured so.
 981        if gitConfig("git-p4.skipSubmitEditCheck") == "true":
 982            return True
 983
 984        # modification time updated means user saved the file
 985        if os.stat(template_file).st_mtime > mtime:
 986            return True
 987
 988        while True:
 989            response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
 990            if response == 'y':
 991                return True
 992            if response == 'n':
 993                return False
 994
 995    def applyCommit(self, id):
 996        print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 997
 998        (p4User, gitEmail) = self.p4UserForCommit(id)
 999
1000        if not self.detectRenames:
1001            # If not explicitly set check the config variable
1002            self.detectRenames = gitConfig("git-p4.detectRenames")
1003
1004        if self.detectRenames.lower() == "false" or self.detectRenames == "":
1005            diffOpts = ""
1006        elif self.detectRenames.lower() == "true":
1007            diffOpts = "-M"
1008        else:
1009            diffOpts = "-M%s" % self.detectRenames
1010
1011        detectCopies = gitConfig("git-p4.detectCopies")
1012        if detectCopies.lower() == "true":
1013            diffOpts += " -C"
1014        elif detectCopies != "" and detectCopies.lower() != "false":
1015            diffOpts += " -C%s" % detectCopies
1016
1017        if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1018            diffOpts += " --find-copies-harder"
1019
1020        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
1021        filesToAdd = set()
1022        filesToDelete = set()
1023        editedFiles = set()
1024        filesToChangeExecBit = {}
1025
1026        for line in diff:
1027            diff = parseDiffTreeEntry(line)
1028            modifier = diff['status']
1029            path = diff['src']
1030            if modifier == "M":
1031                p4_edit(path)
1032                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1033                    filesToChangeExecBit[path] = diff['dst_mode']
1034                editedFiles.add(path)
1035            elif modifier == "A":
1036                filesToAdd.add(path)
1037                filesToChangeExecBit[path] = diff['dst_mode']
1038                if path in filesToDelete:
1039                    filesToDelete.remove(path)
1040            elif modifier == "D":
1041                filesToDelete.add(path)
1042                if path in filesToAdd:
1043                    filesToAdd.remove(path)
1044            elif modifier == "C":
1045                src, dest = diff['src'], diff['dst']
1046                p4_integrate(src, dest)
1047                if diff['src_sha1'] != diff['dst_sha1']:
1048                    p4_edit(dest)
1049                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1050                    p4_edit(dest)
1051                    filesToChangeExecBit[dest] = diff['dst_mode']
1052                os.unlink(dest)
1053                editedFiles.add(dest)
1054            elif modifier == "R":
1055                src, dest = diff['src'], diff['dst']
1056                p4_integrate(src, dest)
1057                if diff['src_sha1'] != diff['dst_sha1']:
1058                    p4_edit(dest)
1059                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1060                    p4_edit(dest)
1061                    filesToChangeExecBit[dest] = diff['dst_mode']
1062                os.unlink(dest)
1063                editedFiles.add(dest)
1064                filesToDelete.add(src)
1065            else:
1066                die("unknown modifier %s for %s" % (modifier, path))
1067
1068        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
1069        patchcmd = diffcmd + " | git apply "
1070        tryPatchCmd = patchcmd + "--check -"
1071        applyPatchCmd = patchcmd + "--check --apply -"
1072        patch_succeeded = True
1073
1074        if os.system(tryPatchCmd) != 0:
1075            fixed_rcs_keywords = False
1076            patch_succeeded = False
1077            print "Unfortunately applying the change failed!"
1078
1079            # Patch failed, maybe it's just RCS keyword woes. Look through
1080            # the patch to see if that's possible.
1081            if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1082                file = None
1083                pattern = None
1084                kwfiles = {}
1085                for file in editedFiles | filesToDelete:
1086                    # did this file's delta contain RCS keywords?
1087                    pattern = p4_keywords_regexp_for_file(file)
1088
1089                    if pattern:
1090                        # this file is a possibility...look for RCS keywords.
1091                        regexp = re.compile(pattern, re.VERBOSE)
1092                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1093                            if regexp.search(line):
1094                                if verbose:
1095                                    print "got keyword match on %s in %s in %s" % (pattern, line, file)
1096                                kwfiles[file] = pattern
1097                                break
1098
1099                for file in kwfiles:
1100                    if verbose:
1101                        print "zapping %s with %s" % (line,pattern)
1102                    self.patchRCSKeywords(file, kwfiles[file])
1103                    fixed_rcs_keywords = True
1104
1105            if fixed_rcs_keywords:
1106                print "Retrying the patch with RCS keywords cleaned up"
1107                if os.system(tryPatchCmd) == 0:
1108                    patch_succeeded = True
1109
1110        if not patch_succeeded:
1111            print "What do you want to do?"
1112            response = "x"
1113            while response != "s" and response != "a" and response != "w":
1114                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
1115                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
1116            if response == "s":
1117                print "Skipping! Good luck with the next patches..."
1118                for f in editedFiles:
1119                    p4_revert(f)
1120                for f in filesToAdd:
1121                    os.remove(f)
1122                return
1123            elif response == "a":
1124                os.system(applyPatchCmd)
1125                if len(filesToAdd) > 0:
1126                    print "You may also want to call p4 add on the following files:"
1127                    print " ".join(filesToAdd)
1128                if len(filesToDelete):
1129                    print "The following files should be scheduled for deletion with p4 delete:"
1130                    print " ".join(filesToDelete)
1131                die("Please resolve and submit the conflict manually and "
1132                    + "continue afterwards with git p4 submit --continue")
1133            elif response == "w":
1134                system(diffcmd + " > patch.txt")
1135                print "Patch saved to patch.txt in %s !" % self.clientPath
1136                die("Please resolve and submit the conflict manually and "
1137                    "continue afterwards with git p4 submit --continue")
1138
1139        system(applyPatchCmd)
1140
1141        for f in filesToAdd:
1142            p4_add(f)
1143        for f in filesToDelete:
1144            p4_revert(f)
1145            p4_delete(f)
1146
1147        # Set/clear executable bits
1148        for f in filesToChangeExecBit.keys():
1149            mode = filesToChangeExecBit[f]
1150            setP4ExecBit(f, mode)
1151
1152        logMessage = extractLogMessageFromGitCommit(id)
1153        logMessage = logMessage.strip()
1154
1155        template = self.prepareSubmitTemplate()
1156
1157        if self.interactive:
1158            submitTemplate = self.prepareLogMessage(template, logMessage)
1159
1160            if self.preserveUser:
1161               submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
1162
1163            if os.environ.has_key("P4DIFF"):
1164                del(os.environ["P4DIFF"])
1165            diff = ""
1166            for editedFile in editedFiles:
1167                diff += p4_read_pipe(['diff', '-du', editedFile])
1168
1169            newdiff = ""
1170            for newFile in filesToAdd:
1171                newdiff += "==== new file ====\n"
1172                newdiff += "--- /dev/null\n"
1173                newdiff += "+++ %s\n" % newFile
1174                f = open(newFile, "r")
1175                for line in f.readlines():
1176                    newdiff += "+" + line
1177                f.close()
1178
1179            if self.checkAuthorship and not self.p4UserIsMe(p4User):
1180                submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1181                submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1182                submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1183
1184            separatorLine = "######## everything below this line is just the diff #######\n"
1185
1186            (handle, fileName) = tempfile.mkstemp()
1187            tmpFile = os.fdopen(handle, "w+")
1188            if self.isWindows:
1189                submitTemplate = submitTemplate.replace("\n", "\r\n")
1190                separatorLine = separatorLine.replace("\n", "\r\n")
1191                newdiff = newdiff.replace("\n", "\r\n")
1192            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1193            tmpFile.close()
1194
1195            if self.edit_template(fileName):
1196                # read the edited message and submit
1197                tmpFile = open(fileName, "rb")
1198                message = tmpFile.read()
1199                tmpFile.close()
1200                submitTemplate = message[:message.index(separatorLine)]
1201                if self.isWindows:
1202                    submitTemplate = submitTemplate.replace("\r\n", "\n")
1203                p4_write_pipe(['submit', '-i'], submitTemplate)
1204
1205                if self.preserveUser:
1206                    if p4User:
1207                        # Get last changelist number. Cannot easily get it from
1208                        # the submit command output as the output is
1209                        # unmarshalled.
1210                        changelist = self.lastP4Changelist()
1211                        self.modifyChangelistUser(changelist, p4User)
1212            else:
1213                # skip this patch
1214                print "Submission cancelled, undoing p4 changes."
1215                for f in editedFiles:
1216                    p4_revert(f)
1217                for f in filesToAdd:
1218                    p4_revert(f)
1219                    os.remove(f)
1220
1221            os.remove(fileName)
1222        else:
1223            fileName = "submit.txt"
1224            file = open(fileName, "w+")
1225            file.write(self.prepareLogMessage(template, logMessage))
1226            file.close()
1227            print ("Perforce submit template written as %s. "
1228                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
1229                   % (fileName, fileName))
1230
1231    def run(self, args):
1232        if len(args) == 0:
1233            self.master = currentGitBranch()
1234            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1235                die("Detecting current git branch failed!")
1236        elif len(args) == 1:
1237            self.master = args[0]
1238            if not branchExists(self.master):
1239                die("Branch %s does not exist" % self.master)
1240        else:
1241            return False
1242
1243        allowSubmit = gitConfig("git-p4.allowSubmit")
1244        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1245            die("%s is not in git-p4.allowSubmit" % self.master)
1246
1247        [upstream, settings] = findUpstreamBranchPoint()
1248        self.depotPath = settings['depot-paths'][0]
1249        if len(self.origin) == 0:
1250            self.origin = upstream
1251
1252        if self.preserveUser:
1253            if not self.canChangeChangelists():
1254                die("Cannot preserve user names without p4 super-user or admin permissions")
1255
1256        if self.verbose:
1257            print "Origin branch is " + self.origin
1258
1259        if len(self.depotPath) == 0:
1260            print "Internal error: cannot locate perforce depot path from existing branches"
1261            sys.exit(128)
1262
1263        self.useClientSpec = False
1264        if gitConfig("git-p4.useclientspec", "--bool") == "true":
1265            self.useClientSpec = True
1266        if self.useClientSpec:
1267            self.clientSpecDirs = getClientSpec()
1268
1269        if self.useClientSpec:
1270            # all files are relative to the client spec
1271            self.clientPath = getClientRoot()
1272        else:
1273            self.clientPath = p4Where(self.depotPath)
1274
1275        if self.clientPath == "":
1276            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1277
1278        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1279        self.oldWorkingDirectory = os.getcwd()
1280
1281        # ensure the clientPath exists
1282        if not os.path.exists(self.clientPath):
1283            os.makedirs(self.clientPath)
1284
1285        chdir(self.clientPath)
1286        print "Synchronizing p4 checkout..."
1287        p4_sync("...")
1288        self.check()
1289
1290        commits = []
1291        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1292            commits.append(line.strip())
1293        commits.reverse()
1294
1295        if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1296            self.checkAuthorship = False
1297        else:
1298            self.checkAuthorship = True
1299
1300        if self.preserveUser:
1301            self.checkValidP4Users(commits)
1302
1303        while len(commits) > 0:
1304            commit = commits[0]
1305            commits = commits[1:]
1306            self.applyCommit(commit)
1307            if not self.interactive:
1308                break
1309
1310        if len(commits) == 0:
1311            print "All changes applied!"
1312            chdir(self.oldWorkingDirectory)
1313
1314            sync = P4Sync()
1315            sync.run([])
1316
1317            rebase = P4Rebase()
1318            rebase.rebase()
1319
1320        return True
1321
1322class View(object):
1323    """Represent a p4 view ("p4 help views"), and map files in a
1324       repo according to the view."""
1325
1326    class Path(object):
1327        """A depot or client path, possibly containing wildcards.
1328           The only one supported is ... at the end, currently.
1329           Initialize with the full path, with //depot or //client."""
1330
1331        def __init__(self, path, is_depot):
1332            self.path = path
1333            self.is_depot = is_depot
1334            self.find_wildcards()
1335            # remember the prefix bit, useful for relative mappings
1336            m = re.match("(//[^/]+/)", self.path)
1337            if not m:
1338                die("Path %s does not start with //prefix/" % self.path)
1339            prefix = m.group(1)
1340            if not self.is_depot:
1341                # strip //client/ on client paths
1342                self.path = self.path[len(prefix):]
1343
1344        def find_wildcards(self):
1345            """Make sure wildcards are valid, and set up internal
1346               variables."""
1347
1348            self.ends_triple_dot = False
1349            # There are three wildcards allowed in p4 views
1350            # (see "p4 help views").  This code knows how to
1351            # handle "..." (only at the end), but cannot deal with
1352            # "%%n" or "*".  Only check the depot_side, as p4 should
1353            # validate that the client_side matches too.
1354            if re.search(r'%%[1-9]', self.path):
1355                die("Can't handle %%n wildcards in view: %s" % self.path)
1356            if self.path.find("*") >= 0:
1357                die("Can't handle * wildcards in view: %s" % self.path)
1358            triple_dot_index = self.path.find("...")
1359            if triple_dot_index >= 0:
1360                if triple_dot_index != len(self.path) - 3:
1361                    die("Can handle only single ... wildcard, at end: %s" %
1362                        self.path)
1363                self.ends_triple_dot = True
1364
1365        def ensure_compatible(self, other_path):
1366            """Make sure the wildcards agree."""
1367            if self.ends_triple_dot != other_path.ends_triple_dot:
1368                 die("Both paths must end with ... if either does;\n" +
1369                     "paths: %s %s" % (self.path, other_path.path))
1370
1371        def match_wildcards(self, test_path):
1372            """See if this test_path matches us, and fill in the value
1373               of the wildcards if so.  Returns a tuple of
1374               (True|False, wildcards[]).  For now, only the ... at end
1375               is supported, so at most one wildcard."""
1376            if self.ends_triple_dot:
1377                dotless = self.path[:-3]
1378                if test_path.startswith(dotless):
1379                    wildcard = test_path[len(dotless):]
1380                    return (True, [ wildcard ])
1381            else:
1382                if test_path == self.path:
1383                    return (True, [])
1384            return (False, [])
1385
1386        def match(self, test_path):
1387            """Just return if it matches; don't bother with the wildcards."""
1388            b, _ = self.match_wildcards(test_path)
1389            return b
1390
1391        def fill_in_wildcards(self, wildcards):
1392            """Return the relative path, with the wildcards filled in
1393               if there are any."""
1394            if self.ends_triple_dot:
1395                return self.path[:-3] + wildcards[0]
1396            else:
1397                return self.path
1398
1399    class Mapping(object):
1400        def __init__(self, depot_side, client_side, overlay, exclude):
1401            # depot_side is without the trailing /... if it had one
1402            self.depot_side = View.Path(depot_side, is_depot=True)
1403            self.client_side = View.Path(client_side, is_depot=False)
1404            self.overlay = overlay  # started with "+"
1405            self.exclude = exclude  # started with "-"
1406            assert not (self.overlay and self.exclude)
1407            self.depot_side.ensure_compatible(self.client_side)
1408
1409        def __str__(self):
1410            c = " "
1411            if self.overlay:
1412                c = "+"
1413            if self.exclude:
1414                c = "-"
1415            return "View.Mapping: %s%s -> %s" % \
1416                   (c, self.depot_side.path, self.client_side.path)
1417
1418        def map_depot_to_client(self, depot_path):
1419            """Calculate the client path if using this mapping on the
1420               given depot path; does not consider the effect of other
1421               mappings in a view.  Even excluded mappings are returned."""
1422            matches, wildcards = self.depot_side.match_wildcards(depot_path)
1423            if not matches:
1424                return ""
1425            client_path = self.client_side.fill_in_wildcards(wildcards)
1426            return client_path
1427
1428    #
1429    # View methods
1430    #
1431    def __init__(self):
1432        self.mappings = []
1433
1434    def append(self, view_line):
1435        """Parse a view line, splitting it into depot and client
1436           sides.  Append to self.mappings, preserving order."""
1437
1438        # Split the view line into exactly two words.  P4 enforces
1439        # structure on these lines that simplifies this quite a bit.
1440        #
1441        # Either or both words may be double-quoted.
1442        # Single quotes do not matter.
1443        # Double-quote marks cannot occur inside the words.
1444        # A + or - prefix is also inside the quotes.
1445        # There are no quotes unless they contain a space.
1446        # The line is already white-space stripped.
1447        # The two words are separated by a single space.
1448        #
1449        if view_line[0] == '"':
1450            # First word is double quoted.  Find its end.
1451            close_quote_index = view_line.find('"', 1)
1452            if close_quote_index <= 0:
1453                die("No first-word closing quote found: %s" % view_line)
1454            depot_side = view_line[1:close_quote_index]
1455            # skip closing quote and space
1456            rhs_index = close_quote_index + 1 + 1
1457        else:
1458            space_index = view_line.find(" ")
1459            if space_index <= 0:
1460                die("No word-splitting space found: %s" % view_line)
1461            depot_side = view_line[0:space_index]
1462            rhs_index = space_index + 1
1463
1464        if view_line[rhs_index] == '"':
1465            # Second word is double quoted.  Make sure there is a
1466            # double quote at the end too.
1467            if not view_line.endswith('"'):
1468                die("View line with rhs quote should end with one: %s" %
1469                    view_line)
1470            # skip the quotes
1471            client_side = view_line[rhs_index+1:-1]
1472        else:
1473            client_side = view_line[rhs_index:]
1474
1475        # prefix + means overlay on previous mapping
1476        overlay = False
1477        if depot_side.startswith("+"):
1478            overlay = True
1479            depot_side = depot_side[1:]
1480
1481        # prefix - means exclude this path
1482        exclude = False
1483        if depot_side.startswith("-"):
1484            exclude = True
1485            depot_side = depot_side[1:]
1486
1487        m = View.Mapping(depot_side, client_side, overlay, exclude)
1488        self.mappings.append(m)
1489
1490    def map_in_client(self, depot_path):
1491        """Return the relative location in the client where this
1492           depot file should live.  Returns "" if the file should
1493           not be mapped in the client."""
1494
1495        paths_filled = []
1496        client_path = ""
1497
1498        # look at later entries first
1499        for m in self.mappings[::-1]:
1500
1501            # see where will this path end up in the client
1502            p = m.map_depot_to_client(depot_path)
1503
1504            if p == "":
1505                # Depot path does not belong in client.  Must remember
1506                # this, as previous items should not cause files to
1507                # exist in this path either.  Remember that the list is
1508                # being walked from the end, which has higher precedence.
1509                # Overlap mappings do not exclude previous mappings.
1510                if not m.overlay:
1511                    paths_filled.append(m.client_side)
1512
1513            else:
1514                # This mapping matched; no need to search any further.
1515                # But, the mapping could be rejected if the client path
1516                # has already been claimed by an earlier mapping (i.e.
1517                # one later in the list, which we are walking backwards).
1518                already_mapped_in_client = False
1519                for f in paths_filled:
1520                    # this is View.Path.match
1521                    if f.match(p):
1522                        already_mapped_in_client = True
1523                        break
1524                if not already_mapped_in_client:
1525                    # Include this file, unless it is from a line that
1526                    # explicitly said to exclude it.
1527                    if not m.exclude:
1528                        client_path = p
1529
1530                # a match, even if rejected, always stops the search
1531                break
1532
1533        return client_path
1534
1535class P4Sync(Command, P4UserMap):
1536    delete_actions = ( "delete", "move/delete", "purge" )
1537
1538    def __init__(self):
1539        Command.__init__(self)
1540        P4UserMap.__init__(self)
1541        self.options = [
1542                optparse.make_option("--branch", dest="branch"),
1543                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1544                optparse.make_option("--changesfile", dest="changesFile"),
1545                optparse.make_option("--silent", dest="silent", action="store_true"),
1546                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1547                optparse.make_option("--verbose", dest="verbose", action="store_true"),
1548                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1549                                     help="Import into refs/heads/ , not refs/remotes"),
1550                optparse.make_option("--max-changes", dest="maxChanges"),
1551                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1552                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1553                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1554                                     help="Only sync files that are included in the Perforce Client Spec")
1555        ]
1556        self.description = """Imports from Perforce into a git repository.\n
1557    example:
1558    //depot/my/project/ -- to import the current head
1559    //depot/my/project/@all -- to import everything
1560    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1561
1562    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1563
1564        self.usage += " //depot/path[@revRange]"
1565        self.silent = False
1566        self.createdBranches = set()
1567        self.committedChanges = set()
1568        self.branch = ""
1569        self.detectBranches = False
1570        self.detectLabels = False
1571        self.changesFile = ""
1572        self.syncWithOrigin = True
1573        self.verbose = False
1574        self.importIntoRemotes = True
1575        self.maxChanges = ""
1576        self.isWindows = (platform.system() == "Windows")
1577        self.keepRepoPath = False
1578        self.depotPaths = None
1579        self.p4BranchesInGit = []
1580        self.cloneExclude = []
1581        self.useClientSpec = False
1582        self.useClientSpec_from_options = False
1583        self.clientSpecDirs = None
1584        self.tempBranches = []
1585        self.tempBranchLocation = "git-p4-tmp"
1586
1587        if gitConfig("git-p4.syncFromOrigin") == "false":
1588            self.syncWithOrigin = False
1589
1590    #
1591    # P4 wildcards are not allowed in filenames.  P4 complains
1592    # if you simply add them, but you can force it with "-f", in
1593    # which case it translates them into %xx encoding internally.
1594    # Search for and fix just these four characters.  Do % last so
1595    # that fixing it does not inadvertently create new %-escapes.
1596    #
1597    def wildcard_decode(self, path):
1598        # Cannot have * in a filename in windows; untested as to
1599        # what p4 would do in such a case.
1600        if not self.isWindows:
1601            path = path.replace("%2A", "*")
1602        path = path.replace("%23", "#") \
1603                   .replace("%40", "@") \
1604                   .replace("%25", "%")
1605        return path
1606
1607    # Force a checkpoint in fast-import and wait for it to finish
1608    def checkpoint(self):
1609        self.gitStream.write("checkpoint\n\n")
1610        self.gitStream.write("progress checkpoint\n\n")
1611        out = self.gitOutput.readline()
1612        if self.verbose:
1613            print "checkpoint finished: " + out
1614
1615    def extractFilesFromCommit(self, commit):
1616        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1617                             for path in self.cloneExclude]
1618        files = []
1619        fnum = 0
1620        while commit.has_key("depotFile%s" % fnum):
1621            path =  commit["depotFile%s" % fnum]
1622
1623            if [p for p in self.cloneExclude
1624                if p4PathStartsWith(path, p)]:
1625                found = False
1626            else:
1627                found = [p for p in self.depotPaths
1628                         if p4PathStartsWith(path, p)]
1629            if not found:
1630                fnum = fnum + 1
1631                continue
1632
1633            file = {}
1634            file["path"] = path
1635            file["rev"] = commit["rev%s" % fnum]
1636            file["action"] = commit["action%s" % fnum]
1637            file["type"] = commit["type%s" % fnum]
1638            files.append(file)
1639            fnum = fnum + 1
1640        return files
1641
1642    def stripRepoPath(self, path, prefixes):
1643        if self.useClientSpec:
1644            return self.clientSpecDirs.map_in_client(path)
1645
1646        if self.keepRepoPath:
1647            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1648
1649        for p in prefixes:
1650            if p4PathStartsWith(path, p):
1651                path = path[len(p):]
1652
1653        return path
1654
1655    def splitFilesIntoBranches(self, commit):
1656        branches = {}
1657        fnum = 0
1658        while commit.has_key("depotFile%s" % fnum):
1659            path =  commit["depotFile%s" % fnum]
1660            found = [p for p in self.depotPaths
1661                     if p4PathStartsWith(path, p)]
1662            if not found:
1663                fnum = fnum + 1
1664                continue
1665
1666            file = {}
1667            file["path"] = path
1668            file["rev"] = commit["rev%s" % fnum]
1669            file["action"] = commit["action%s" % fnum]
1670            file["type"] = commit["type%s" % fnum]
1671            fnum = fnum + 1
1672
1673            relPath = self.stripRepoPath(path, self.depotPaths)
1674
1675            for branch in self.knownBranches.keys():
1676
1677                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1678                if relPath.startswith(branch + "/"):
1679                    if branch not in branches:
1680                        branches[branch] = []
1681                    branches[branch].append(file)
1682                    break
1683
1684        return branches
1685
1686    # output one file from the P4 stream
1687    # - helper for streamP4Files
1688
1689    def streamOneP4File(self, file, contents):
1690        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1691        relPath = self.wildcard_decode(relPath)
1692        if verbose:
1693            sys.stderr.write("%s\n" % relPath)
1694
1695        (type_base, type_mods) = split_p4_type(file["type"])
1696
1697        git_mode = "100644"
1698        if "x" in type_mods:
1699            git_mode = "100755"
1700        if type_base == "symlink":
1701            git_mode = "120000"
1702            # p4 print on a symlink contains "target\n"; remove the newline
1703            data = ''.join(contents)
1704            contents = [data[:-1]]
1705
1706        if type_base == "utf16":
1707            # p4 delivers different text in the python output to -G
1708            # than it does when using "print -o", or normal p4 client
1709            # operations.  utf16 is converted to ascii or utf8, perhaps.
1710            # But ascii text saved as -t utf16 is completely mangled.
1711            # Invoke print -o to get the real contents.
1712            text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
1713            contents = [ text ]
1714
1715        if type_base == "apple":
1716            # Apple filetype files will be streamed as a concatenation of
1717            # its appledouble header and the contents.  This is useless
1718            # on both macs and non-macs.  If using "print -q -o xx", it
1719            # will create "xx" with the data, and "%xx" with the header.
1720            # This is also not very useful.
1721            #
1722            # Ideally, someday, this script can learn how to generate
1723            # appledouble files directly and import those to git, but
1724            # non-mac machines can never find a use for apple filetype.
1725            print "\nIgnoring apple filetype file %s" % file['depotFile']
1726            return
1727
1728        # Perhaps windows wants unicode, utf16 newlines translated too;
1729        # but this is not doing it.
1730        if self.isWindows and type_base == "text":
1731            mangled = []
1732            for data in contents:
1733                data = data.replace("\r\n", "\n")
1734                mangled.append(data)
1735            contents = mangled
1736
1737        # Note that we do not try to de-mangle keywords on utf16 files,
1738        # even though in theory somebody may want that.
1739        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
1740        if pattern:
1741            regexp = re.compile(pattern, re.VERBOSE)
1742            text = ''.join(contents)
1743            text = regexp.sub(r'$\1$', text)
1744            contents = [ text ]
1745
1746        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
1747
1748        # total length...
1749        length = 0
1750        for d in contents:
1751            length = length + len(d)
1752
1753        self.gitStream.write("data %d\n" % length)
1754        for d in contents:
1755            self.gitStream.write(d)
1756        self.gitStream.write("\n")
1757
1758    def streamOneP4Deletion(self, file):
1759        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1760        if verbose:
1761            sys.stderr.write("delete %s\n" % relPath)
1762        self.gitStream.write("D %s\n" % relPath)
1763
1764    # handle another chunk of streaming data
1765    def streamP4FilesCb(self, marshalled):
1766
1767        if marshalled.has_key('depotFile') and self.stream_have_file_info:
1768            # start of a new file - output the old one first
1769            self.streamOneP4File(self.stream_file, self.stream_contents)
1770            self.stream_file = {}
1771            self.stream_contents = []
1772            self.stream_have_file_info = False
1773
1774        # pick up the new file information... for the
1775        # 'data' field we need to append to our array
1776        for k in marshalled.keys():
1777            if k == 'data':
1778                self.stream_contents.append(marshalled['data'])
1779            else:
1780                self.stream_file[k] = marshalled[k]
1781
1782        self.stream_have_file_info = True
1783
1784    # Stream directly from "p4 files" into "git fast-import"
1785    def streamP4Files(self, files):
1786        filesForCommit = []
1787        filesToRead = []
1788        filesToDelete = []
1789
1790        for f in files:
1791            # if using a client spec, only add the files that have
1792            # a path in the client
1793            if self.clientSpecDirs:
1794                if self.clientSpecDirs.map_in_client(f['path']) == "":
1795                    continue
1796
1797            filesForCommit.append(f)
1798            if f['action'] in self.delete_actions:
1799                filesToDelete.append(f)
1800            else:
1801                filesToRead.append(f)
1802
1803        # deleted files...
1804        for f in filesToDelete:
1805            self.streamOneP4Deletion(f)
1806
1807        if len(filesToRead) > 0:
1808            self.stream_file = {}
1809            self.stream_contents = []
1810            self.stream_have_file_info = False
1811
1812            # curry self argument
1813            def streamP4FilesCbSelf(entry):
1814                self.streamP4FilesCb(entry)
1815
1816            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
1817
1818            p4CmdList(["-x", "-", "print"],
1819                      stdin=fileArgs,
1820                      cb=streamP4FilesCbSelf)
1821
1822            # do the last chunk
1823            if self.stream_file.has_key('depotFile'):
1824                self.streamOneP4File(self.stream_file, self.stream_contents)
1825
1826    def make_email(self, userid):
1827        if userid in self.users:
1828            return self.users[userid]
1829        else:
1830            return "%s <a@b>" % userid
1831
1832    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1833        epoch = details["time"]
1834        author = details["user"]
1835        self.branchPrefixes = branchPrefixes
1836
1837        if self.verbose:
1838            print "commit into %s" % branch
1839
1840        # start with reading files; if that fails, we should not
1841        # create a commit.
1842        new_files = []
1843        for f in files:
1844            if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1845                new_files.append (f)
1846            else:
1847                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1848
1849        self.gitStream.write("commit %s\n" % branch)
1850#        gitStream.write("mark :%s\n" % details["change"])
1851        self.committedChanges.add(int(details["change"]))
1852        committer = ""
1853        if author not in self.users:
1854            self.getUserMapFromPerforceServer()
1855        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
1856
1857        self.gitStream.write("committer %s\n" % committer)
1858
1859        self.gitStream.write("data <<EOT\n")
1860        self.gitStream.write(details["desc"])
1861        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1862                             % (','.join (branchPrefixes), details["change"]))
1863        if len(details['options']) > 0:
1864            self.gitStream.write(": options = %s" % details['options'])
1865        self.gitStream.write("]\nEOT\n\n")
1866
1867        if len(parent) > 0:
1868            if self.verbose:
1869                print "parent %s" % parent
1870            self.gitStream.write("from %s\n" % parent)
1871
1872        self.streamP4Files(new_files)
1873        self.gitStream.write("\n")
1874
1875        change = int(details["change"])
1876
1877        if self.labels.has_key(change):
1878            label = self.labels[change]
1879            labelDetails = label[0]
1880            labelRevisions = label[1]
1881            if self.verbose:
1882                print "Change %s is labelled %s" % (change, labelDetails)
1883
1884            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
1885                                                    for p in branchPrefixes])
1886
1887            if len(files) == len(labelRevisions):
1888
1889                cleanedFiles = {}
1890                for info in files:
1891                    if info["action"] in self.delete_actions:
1892                        continue
1893                    cleanedFiles[info["depotFile"]] = info["rev"]
1894
1895                if cleanedFiles == labelRevisions:
1896                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1897                    self.gitStream.write("from %s\n" % branch)
1898
1899                    owner = labelDetails["Owner"]
1900
1901                    # Try to use the owner of the p4 label, or failing that,
1902                    # the current p4 user id.
1903                    if owner:
1904                        email = self.make_email(owner)
1905                    else:
1906                        email = self.make_email(self.p4UserId())
1907                    tagger = "%s %s %s" % (email, epoch, self.tz)
1908
1909                    self.gitStream.write("tagger %s\n" % tagger)
1910
1911                    description = labelDetails["Description"]
1912                    self.gitStream.write("data %d\n" % len(description))
1913                    self.gitStream.write(description)
1914                    self.gitStream.write("\n")
1915
1916                else:
1917                    if not self.silent:
1918                        print ("Tag %s does not match with change %s: files do not match."
1919                               % (labelDetails["label"], change))
1920
1921            else:
1922                if not self.silent:
1923                    print ("Tag %s does not match with change %s: file count is different."
1924                           % (labelDetails["label"], change))
1925
1926    def getLabels(self):
1927        self.labels = {}
1928
1929        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
1930        if len(l) > 0 and not self.silent:
1931            print "Finding files belonging to labels in %s" % `self.depotPaths`
1932
1933        for output in l:
1934            label = output["label"]
1935            revisions = {}
1936            newestChange = 0
1937            if self.verbose:
1938                print "Querying files for label %s" % label
1939            for file in p4CmdList(["files"] +
1940                                      ["%s...@%s" % (p, label)
1941                                          for p in self.depotPaths]):
1942                revisions[file["depotFile"]] = file["rev"]
1943                change = int(file["change"])
1944                if change > newestChange:
1945                    newestChange = change
1946
1947            self.labels[newestChange] = [output, revisions]
1948
1949        if self.verbose:
1950            print "Label changes: %s" % self.labels.keys()
1951
1952    def guessProjectName(self):
1953        for p in self.depotPaths:
1954            if p.endswith("/"):
1955                p = p[:-1]
1956            p = p[p.strip().rfind("/") + 1:]
1957            if not p.endswith("/"):
1958               p += "/"
1959            return p
1960
1961    def getBranchMapping(self):
1962        lostAndFoundBranches = set()
1963
1964        user = gitConfig("git-p4.branchUser")
1965        if len(user) > 0:
1966            command = "branches -u %s" % user
1967        else:
1968            command = "branches"
1969
1970        for info in p4CmdList(command):
1971            details = p4Cmd(["branch", "-o", info["branch"]])
1972            viewIdx = 0
1973            while details.has_key("View%s" % viewIdx):
1974                paths = details["View%s" % viewIdx].split(" ")
1975                viewIdx = viewIdx + 1
1976                # require standard //depot/foo/... //depot/bar/... mapping
1977                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1978                    continue
1979                source = paths[0]
1980                destination = paths[1]
1981                ## HACK
1982                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1983                    source = source[len(self.depotPaths[0]):-4]
1984                    destination = destination[len(self.depotPaths[0]):-4]
1985
1986                    if destination in self.knownBranches:
1987                        if not self.silent:
1988                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1989                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1990                        continue
1991
1992                    self.knownBranches[destination] = source
1993
1994                    lostAndFoundBranches.discard(destination)
1995
1996                    if source not in self.knownBranches:
1997                        lostAndFoundBranches.add(source)
1998
1999        # Perforce does not strictly require branches to be defined, so we also
2000        # check git config for a branch list.
2001        #
2002        # Example of branch definition in git config file:
2003        # [git-p4]
2004        #   branchList=main:branchA
2005        #   branchList=main:branchB
2006        #   branchList=branchA:branchC
2007        configBranches = gitConfigList("git-p4.branchList")
2008        for branch in configBranches:
2009            if branch:
2010                (source, destination) = branch.split(":")
2011                self.knownBranches[destination] = source
2012
2013                lostAndFoundBranches.discard(destination)
2014
2015                if source not in self.knownBranches:
2016                    lostAndFoundBranches.add(source)
2017
2018
2019        for branch in lostAndFoundBranches:
2020            self.knownBranches[branch] = branch
2021
2022    def getBranchMappingFromGitBranches(self):
2023        branches = p4BranchesInGit(self.importIntoRemotes)
2024        for branch in branches.keys():
2025            if branch == "master":
2026                branch = "main"
2027            else:
2028                branch = branch[len(self.projectName):]
2029            self.knownBranches[branch] = branch
2030
2031    def listExistingP4GitBranches(self):
2032        # branches holds mapping from name to commit
2033        branches = p4BranchesInGit(self.importIntoRemotes)
2034        self.p4BranchesInGit = branches.keys()
2035        for branch in branches.keys():
2036            self.initialParents[self.refPrefix + branch] = branches[branch]
2037
2038    def updateOptionDict(self, d):
2039        option_keys = {}
2040        if self.keepRepoPath:
2041            option_keys['keepRepoPath'] = 1
2042
2043        d["options"] = ' '.join(sorted(option_keys.keys()))
2044
2045    def readOptions(self, d):
2046        self.keepRepoPath = (d.has_key('options')
2047                             and ('keepRepoPath' in d['options']))
2048
2049    def gitRefForBranch(self, branch):
2050        if branch == "main":
2051            return self.refPrefix + "master"
2052
2053        if len(branch) <= 0:
2054            return branch
2055
2056        return self.refPrefix + self.projectName + branch
2057
2058    def gitCommitByP4Change(self, ref, change):
2059        if self.verbose:
2060            print "looking in ref " + ref + " for change %s using bisect..." % change
2061
2062        earliestCommit = ""
2063        latestCommit = parseRevision(ref)
2064
2065        while True:
2066            if self.verbose:
2067                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2068            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2069            if len(next) == 0:
2070                if self.verbose:
2071                    print "argh"
2072                return ""
2073            log = extractLogMessageFromGitCommit(next)
2074            settings = extractSettingsGitLog(log)
2075            currentChange = int(settings['change'])
2076            if self.verbose:
2077                print "current change %s" % currentChange
2078
2079            if currentChange == change:
2080                if self.verbose:
2081                    print "found %s" % next
2082                return next
2083
2084            if currentChange < change:
2085                earliestCommit = "^%s" % next
2086            else:
2087                latestCommit = "%s" % next
2088
2089        return ""
2090
2091    def importNewBranch(self, branch, maxChange):
2092        # make fast-import flush all changes to disk and update the refs using the checkpoint
2093        # command so that we can try to find the branch parent in the git history
2094        self.gitStream.write("checkpoint\n\n");
2095        self.gitStream.flush();
2096        branchPrefix = self.depotPaths[0] + branch + "/"
2097        range = "@1,%s" % maxChange
2098        #print "prefix" + branchPrefix
2099        changes = p4ChangesForPaths([branchPrefix], range)
2100        if len(changes) <= 0:
2101            return False
2102        firstChange = changes[0]
2103        #print "first change in branch: %s" % firstChange
2104        sourceBranch = self.knownBranches[branch]
2105        sourceDepotPath = self.depotPaths[0] + sourceBranch
2106        sourceRef = self.gitRefForBranch(sourceBranch)
2107        #print "source " + sourceBranch
2108
2109        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2110        #print "branch parent: %s" % branchParentChange
2111        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2112        if len(gitParent) > 0:
2113            self.initialParents[self.gitRefForBranch(branch)] = gitParent
2114            #print "parent git commit: %s" % gitParent
2115
2116        self.importChanges(changes)
2117        return True
2118
2119    def searchParent(self, parent, branch, target):
2120        parentFound = False
2121        for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2122            blob = blob.strip()
2123            if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2124                parentFound = True
2125                if self.verbose:
2126                    print "Found parent of %s in commit %s" % (branch, blob)
2127                break
2128        if parentFound:
2129            return blob
2130        else:
2131            return None
2132
2133    def importChanges(self, changes):
2134        cnt = 1
2135        for change in changes:
2136            description = p4Cmd(["describe", str(change)])
2137            self.updateOptionDict(description)
2138
2139            if not self.silent:
2140                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2141                sys.stdout.flush()
2142            cnt = cnt + 1
2143
2144            try:
2145                if self.detectBranches:
2146                    branches = self.splitFilesIntoBranches(description)
2147                    for branch in branches.keys():
2148                        ## HACK  --hwn
2149                        branchPrefix = self.depotPaths[0] + branch + "/"
2150
2151                        parent = ""
2152
2153                        filesForCommit = branches[branch]
2154
2155                        if self.verbose:
2156                            print "branch is %s" % branch
2157
2158                        self.updatedBranches.add(branch)
2159
2160                        if branch not in self.createdBranches:
2161                            self.createdBranches.add(branch)
2162                            parent = self.knownBranches[branch]
2163                            if parent == branch:
2164                                parent = ""
2165                            else:
2166                                fullBranch = self.projectName + branch
2167                                if fullBranch not in self.p4BranchesInGit:
2168                                    if not self.silent:
2169                                        print("\n    Importing new branch %s" % fullBranch);
2170                                    if self.importNewBranch(branch, change - 1):
2171                                        parent = ""
2172                                        self.p4BranchesInGit.append(fullBranch)
2173                                    if not self.silent:
2174                                        print("\n    Resuming with change %s" % change);
2175
2176                                if self.verbose:
2177                                    print "parent determined through known branches: %s" % parent
2178
2179                        branch = self.gitRefForBranch(branch)
2180                        parent = self.gitRefForBranch(parent)
2181
2182                        if self.verbose:
2183                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2184
2185                        if len(parent) == 0 and branch in self.initialParents:
2186                            parent = self.initialParents[branch]
2187                            del self.initialParents[branch]
2188
2189                        blob = None
2190                        if len(parent) > 0:
2191                            tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2192                            if self.verbose:
2193                                print "Creating temporary branch: " + tempBranch
2194                            self.commit(description, filesForCommit, tempBranch, [branchPrefix])
2195                            self.tempBranches.append(tempBranch)
2196                            self.checkpoint()
2197                            blob = self.searchParent(parent, branch, tempBranch)
2198                        if blob:
2199                            self.commit(description, filesForCommit, branch, [branchPrefix], blob)
2200                        else:
2201                            if self.verbose:
2202                                print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2203                            self.commit(description, filesForCommit, branch, [branchPrefix], parent)
2204                else:
2205                    files = self.extractFilesFromCommit(description)
2206                    self.commit(description, files, self.branch, self.depotPaths,
2207                                self.initialParent)
2208                    self.initialParent = ""
2209            except IOError:
2210                print self.gitError.read()
2211                sys.exit(1)
2212
2213    def importHeadRevision(self, revision):
2214        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2215
2216        details = {}
2217        details["user"] = "git perforce import user"
2218        details["desc"] = ("Initial import of %s from the state at revision %s\n"
2219                           % (' '.join(self.depotPaths), revision))
2220        details["change"] = revision
2221        newestRevision = 0
2222
2223        fileCnt = 0
2224        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2225
2226        for info in p4CmdList(["files"] + fileArgs):
2227
2228            if 'code' in info and info['code'] == 'error':
2229                sys.stderr.write("p4 returned an error: %s\n"
2230                                 % info['data'])
2231                if info['data'].find("must refer to client") >= 0:
2232                    sys.stderr.write("This particular p4 error is misleading.\n")
2233                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
2234                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
2235                sys.exit(1)
2236            if 'p4ExitCode' in info:
2237                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2238                sys.exit(1)
2239
2240
2241            change = int(info["change"])
2242            if change > newestRevision:
2243                newestRevision = change
2244
2245            if info["action"] in self.delete_actions:
2246                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2247                #fileCnt = fileCnt + 1
2248                continue
2249
2250            for prop in ["depotFile", "rev", "action", "type" ]:
2251                details["%s%s" % (prop, fileCnt)] = info[prop]
2252
2253            fileCnt = fileCnt + 1
2254
2255        details["change"] = newestRevision
2256
2257        # Use time from top-most change so that all git p4 clones of
2258        # the same p4 repo have the same commit SHA1s.
2259        res = p4CmdList("describe -s %d" % newestRevision)
2260        newestTime = None
2261        for r in res:
2262            if r.has_key('time'):
2263                newestTime = int(r['time'])
2264        if newestTime is None:
2265            die("\"describe -s\" on newest change %d did not give a time")
2266        details["time"] = newestTime
2267
2268        self.updateOptionDict(details)
2269        try:
2270            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
2271        except IOError:
2272            print "IO error with git fast-import. Is your git version recent enough?"
2273            print self.gitError.read()
2274
2275
2276    def run(self, args):
2277        self.depotPaths = []
2278        self.changeRange = ""
2279        self.initialParent = ""
2280        self.previousDepotPaths = []
2281
2282        # map from branch depot path to parent branch
2283        self.knownBranches = {}
2284        self.initialParents = {}
2285        self.hasOrigin = originP4BranchesExist()
2286        if not self.syncWithOrigin:
2287            self.hasOrigin = False
2288
2289        if self.importIntoRemotes:
2290            self.refPrefix = "refs/remotes/p4/"
2291        else:
2292            self.refPrefix = "refs/heads/p4/"
2293
2294        if self.syncWithOrigin and self.hasOrigin:
2295            if not self.silent:
2296                print "Syncing with origin first by calling git fetch origin"
2297            system("git fetch origin")
2298
2299        if len(self.branch) == 0:
2300            self.branch = self.refPrefix + "master"
2301            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2302                system("git update-ref %s refs/heads/p4" % self.branch)
2303                system("git branch -D p4");
2304            # create it /after/ importing, when master exists
2305            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
2306                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
2307
2308        # accept either the command-line option, or the configuration variable
2309        if self.useClientSpec:
2310            # will use this after clone to set the variable
2311            self.useClientSpec_from_options = True
2312        else:
2313            if gitConfig("git-p4.useclientspec", "--bool") == "true":
2314                self.useClientSpec = True
2315        if self.useClientSpec:
2316            self.clientSpecDirs = getClientSpec()
2317
2318        # TODO: should always look at previous commits,
2319        # merge with previous imports, if possible.
2320        if args == []:
2321            if self.hasOrigin:
2322                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2323            self.listExistingP4GitBranches()
2324
2325            if len(self.p4BranchesInGit) > 1:
2326                if not self.silent:
2327                    print "Importing from/into multiple branches"
2328                self.detectBranches = True
2329
2330            if self.verbose:
2331                print "branches: %s" % self.p4BranchesInGit
2332
2333            p4Change = 0
2334            for branch in self.p4BranchesInGit:
2335                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
2336
2337                settings = extractSettingsGitLog(logMsg)
2338
2339                self.readOptions(settings)
2340                if (settings.has_key('depot-paths')
2341                    and settings.has_key ('change')):
2342                    change = int(settings['change']) + 1
2343                    p4Change = max(p4Change, change)
2344
2345                    depotPaths = sorted(settings['depot-paths'])
2346                    if self.previousDepotPaths == []:
2347                        self.previousDepotPaths = depotPaths
2348                    else:
2349                        paths = []
2350                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2351                            prev_list = prev.split("/")
2352                            cur_list = cur.split("/")
2353                            for i in range(0, min(len(cur_list), len(prev_list))):
2354                                if cur_list[i] <> prev_list[i]:
2355                                    i = i - 1
2356                                    break
2357
2358                            paths.append ("/".join(cur_list[:i + 1]))
2359
2360                        self.previousDepotPaths = paths
2361
2362            if p4Change > 0:
2363                self.depotPaths = sorted(self.previousDepotPaths)
2364                self.changeRange = "@%s,#head" % p4Change
2365                if not self.detectBranches:
2366                    self.initialParent = parseRevision(self.branch)
2367                if not self.silent and not self.detectBranches:
2368                    print "Performing incremental import into %s git branch" % self.branch
2369
2370        if not self.branch.startswith("refs/"):
2371            self.branch = "refs/heads/" + self.branch
2372
2373        if len(args) == 0 and self.depotPaths:
2374            if not self.silent:
2375                print "Depot paths: %s" % ' '.join(self.depotPaths)
2376        else:
2377            if self.depotPaths and self.depotPaths != args:
2378                print ("previous import used depot path %s and now %s was specified. "
2379                       "This doesn't work!" % (' '.join (self.depotPaths),
2380                                               ' '.join (args)))
2381                sys.exit(1)
2382
2383            self.depotPaths = sorted(args)
2384
2385        revision = ""
2386        self.users = {}
2387
2388        # Make sure no revision specifiers are used when --changesfile
2389        # is specified.
2390        bad_changesfile = False
2391        if len(self.changesFile) > 0:
2392            for p in self.depotPaths:
2393                if p.find("@") >= 0 or p.find("#") >= 0:
2394                    bad_changesfile = True
2395                    break
2396        if bad_changesfile:
2397            die("Option --changesfile is incompatible with revision specifiers")
2398
2399        newPaths = []
2400        for p in self.depotPaths:
2401            if p.find("@") != -1:
2402                atIdx = p.index("@")
2403                self.changeRange = p[atIdx:]
2404                if self.changeRange == "@all":
2405                    self.changeRange = ""
2406                elif ',' not in self.changeRange:
2407                    revision = self.changeRange
2408                    self.changeRange = ""
2409                p = p[:atIdx]
2410            elif p.find("#") != -1:
2411                hashIdx = p.index("#")
2412                revision = p[hashIdx:]
2413                p = p[:hashIdx]
2414            elif self.previousDepotPaths == []:
2415                # pay attention to changesfile, if given, else import
2416                # the entire p4 tree at the head revision
2417                if len(self.changesFile) == 0:
2418                    revision = "#head"
2419
2420            p = re.sub ("\.\.\.$", "", p)
2421            if not p.endswith("/"):
2422                p += "/"
2423
2424            newPaths.append(p)
2425
2426        self.depotPaths = newPaths
2427
2428
2429        self.loadUserMapFromCache()
2430        self.labels = {}
2431        if self.detectLabels:
2432            self.getLabels();
2433
2434        if self.detectBranches:
2435            ## FIXME - what's a P4 projectName ?
2436            self.projectName = self.guessProjectName()
2437
2438            if self.hasOrigin:
2439                self.getBranchMappingFromGitBranches()
2440            else:
2441                self.getBranchMapping()
2442            if self.verbose:
2443                print "p4-git branches: %s" % self.p4BranchesInGit
2444                print "initial parents: %s" % self.initialParents
2445            for b in self.p4BranchesInGit:
2446                if b != "master":
2447
2448                    ## FIXME
2449                    b = b[len(self.projectName):]
2450                self.createdBranches.add(b)
2451
2452        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2453
2454        importProcess = subprocess.Popen(["git", "fast-import"],
2455                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2456                                         stderr=subprocess.PIPE);
2457        self.gitOutput = importProcess.stdout
2458        self.gitStream = importProcess.stdin
2459        self.gitError = importProcess.stderr
2460
2461        if revision:
2462            self.importHeadRevision(revision)
2463        else:
2464            changes = []
2465
2466            if len(self.changesFile) > 0:
2467                output = open(self.changesFile).readlines()
2468                changeSet = set()
2469                for line in output:
2470                    changeSet.add(int(line))
2471
2472                for change in changeSet:
2473                    changes.append(change)
2474
2475                changes.sort()
2476            else:
2477                # catch "git p4 sync" with no new branches, in a repo that
2478                # does not have any existing p4 branches
2479                if len(args) == 0 and not self.p4BranchesInGit:
2480                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
2481                if self.verbose:
2482                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
2483                                                              self.changeRange)
2484                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
2485
2486                if len(self.maxChanges) > 0:
2487                    changes = changes[:min(int(self.maxChanges), len(changes))]
2488
2489            if len(changes) == 0:
2490                if not self.silent:
2491                    print "No changes to import!"
2492                return True
2493
2494            if not self.silent and not self.detectBranches:
2495                print "Import destination: %s" % self.branch
2496
2497            self.updatedBranches = set()
2498
2499            self.importChanges(changes)
2500
2501            if not self.silent:
2502                print ""
2503                if len(self.updatedBranches) > 0:
2504                    sys.stdout.write("Updated branches: ")
2505                    for b in self.updatedBranches:
2506                        sys.stdout.write("%s " % b)
2507                    sys.stdout.write("\n")
2508
2509        self.gitStream.close()
2510        if importProcess.wait() != 0:
2511            die("fast-import failed: %s" % self.gitError.read())
2512        self.gitOutput.close()
2513        self.gitError.close()
2514
2515        # Cleanup temporary branches created during import
2516        if self.tempBranches != []:
2517            for branch in self.tempBranches:
2518                read_pipe("git update-ref -d %s" % branch)
2519            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
2520
2521        return True
2522
2523class P4Rebase(Command):
2524    def __init__(self):
2525        Command.__init__(self)
2526        self.options = [ ]
2527        self.description = ("Fetches the latest revision from perforce and "
2528                            + "rebases the current work (branch) against it")
2529        self.verbose = False
2530
2531    def run(self, args):
2532        sync = P4Sync()
2533        sync.run([])
2534
2535        return self.rebase()
2536
2537    def rebase(self):
2538        if os.system("git update-index --refresh") != 0:
2539            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.");
2540        if len(read_pipe("git diff-index HEAD --")) > 0:
2541            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2542
2543        [upstream, settings] = findUpstreamBranchPoint()
2544        if len(upstream) == 0:
2545            die("Cannot find upstream branchpoint for rebase")
2546
2547        # the branchpoint may be p4/foo~3, so strip off the parent
2548        upstream = re.sub("~[0-9]+$", "", upstream)
2549
2550        print "Rebasing the current branch onto %s" % upstream
2551        oldHead = read_pipe("git rev-parse HEAD").strip()
2552        system("git rebase %s" % upstream)
2553        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2554        return True
2555
2556class P4Clone(P4Sync):
2557    def __init__(self):
2558        P4Sync.__init__(self)
2559        self.description = "Creates a new git repository and imports from Perforce into it"
2560        self.usage = "usage: %prog [options] //depot/path[@revRange]"
2561        self.options += [
2562            optparse.make_option("--destination", dest="cloneDestination",
2563                                 action='store', default=None,
2564                                 help="where to leave result of the clone"),
2565            optparse.make_option("-/", dest="cloneExclude",
2566                                 action="append", type="string",
2567                                 help="exclude depot path"),
2568            optparse.make_option("--bare", dest="cloneBare",
2569                                 action="store_true", default=False),
2570        ]
2571        self.cloneDestination = None
2572        self.needsGit = False
2573        self.cloneBare = False
2574
2575    # This is required for the "append" cloneExclude action
2576    def ensure_value(self, attr, value):
2577        if not hasattr(self, attr) or getattr(self, attr) is None:
2578            setattr(self, attr, value)
2579        return getattr(self, attr)
2580
2581    def defaultDestination(self, args):
2582        ## TODO: use common prefix of args?
2583        depotPath = args[0]
2584        depotDir = re.sub("(@[^@]*)$", "", depotPath)
2585        depotDir = re.sub("(#[^#]*)$", "", depotDir)
2586        depotDir = re.sub(r"\.\.\.$", "", depotDir)
2587        depotDir = re.sub(r"/$", "", depotDir)
2588        return os.path.split(depotDir)[1]
2589
2590    def run(self, args):
2591        if len(args) < 1:
2592            return False
2593
2594        if self.keepRepoPath and not self.cloneDestination:
2595            sys.stderr.write("Must specify destination for --keep-path\n")
2596            sys.exit(1)
2597
2598        depotPaths = args
2599
2600        if not self.cloneDestination and len(depotPaths) > 1:
2601            self.cloneDestination = depotPaths[-1]
2602            depotPaths = depotPaths[:-1]
2603
2604        self.cloneExclude = ["/"+p for p in self.cloneExclude]
2605        for p in depotPaths:
2606            if not p.startswith("//"):
2607                return False
2608
2609        if not self.cloneDestination:
2610            self.cloneDestination = self.defaultDestination(args)
2611
2612        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2613
2614        if not os.path.exists(self.cloneDestination):
2615            os.makedirs(self.cloneDestination)
2616        chdir(self.cloneDestination)
2617
2618        init_cmd = [ "git", "init" ]
2619        if self.cloneBare:
2620            init_cmd.append("--bare")
2621        subprocess.check_call(init_cmd)
2622
2623        if not P4Sync.run(self, depotPaths):
2624            return False
2625        if self.branch != "master":
2626            if self.importIntoRemotes:
2627                masterbranch = "refs/remotes/p4/master"
2628            else:
2629                masterbranch = "refs/heads/p4/master"
2630            if gitBranchExists(masterbranch):
2631                system("git branch master %s" % masterbranch)
2632                if not self.cloneBare:
2633                    system("git checkout -f")
2634            else:
2635                print "Could not detect main branch. No checkout/master branch created."
2636
2637        # auto-set this variable if invoked with --use-client-spec
2638        if self.useClientSpec_from_options:
2639            system("git config --bool git-p4.useclientspec true")
2640
2641        return True
2642
2643class P4Branches(Command):
2644    def __init__(self):
2645        Command.__init__(self)
2646        self.options = [ ]
2647        self.description = ("Shows the git branches that hold imports and their "
2648                            + "corresponding perforce depot paths")
2649        self.verbose = False
2650
2651    def run(self, args):
2652        if originP4BranchesExist():
2653            createOrUpdateBranchesFromOrigin()
2654
2655        cmdline = "git rev-parse --symbolic "
2656        cmdline += " --remotes"
2657
2658        for line in read_pipe_lines(cmdline):
2659            line = line.strip()
2660
2661            if not line.startswith('p4/') or line == "p4/HEAD":
2662                continue
2663            branch = line
2664
2665            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2666            settings = extractSettingsGitLog(log)
2667
2668            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2669        return True
2670
2671class HelpFormatter(optparse.IndentedHelpFormatter):
2672    def __init__(self):
2673        optparse.IndentedHelpFormatter.__init__(self)
2674
2675    def format_description(self, description):
2676        if description:
2677            return description + "\n"
2678        else:
2679            return ""
2680
2681def printUsage(commands):
2682    print "usage: %s <command> [options]" % sys.argv[0]
2683    print ""
2684    print "valid commands: %s" % ", ".join(commands)
2685    print ""
2686    print "Try %s <command> --help for command specific help." % sys.argv[0]
2687    print ""
2688
2689commands = {
2690    "debug" : P4Debug,
2691    "submit" : P4Submit,
2692    "commit" : P4Submit,
2693    "sync" : P4Sync,
2694    "rebase" : P4Rebase,
2695    "clone" : P4Clone,
2696    "rollback" : P4RollBack,
2697    "branches" : P4Branches
2698}
2699
2700
2701def main():
2702    if len(sys.argv[1:]) == 0:
2703        printUsage(commands.keys())
2704        sys.exit(2)
2705
2706    cmd = ""
2707    cmdName = sys.argv[1]
2708    try:
2709        klass = commands[cmdName]
2710        cmd = klass()
2711    except KeyError:
2712        print "unknown command %s" % cmdName
2713        print ""
2714        printUsage(commands.keys())
2715        sys.exit(2)
2716
2717    options = cmd.options
2718    cmd.gitdir = os.environ.get("GIT_DIR", None)
2719
2720    args = sys.argv[2:]
2721
2722    if len(options) > 0:
2723        if cmd.needsGit:
2724            options.append(optparse.make_option("--git-dir", dest="gitdir"))
2725
2726        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2727                                       options,
2728                                       description = cmd.description,
2729                                       formatter = HelpFormatter())
2730
2731        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2732    global verbose
2733    verbose = cmd.verbose
2734    if cmd.needsGit:
2735        if cmd.gitdir == None:
2736            cmd.gitdir = os.path.abspath(".git")
2737            if not isValidGitDir(cmd.gitdir):
2738                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2739                if os.path.exists(cmd.gitdir):
2740                    cdup = read_pipe("git rev-parse --show-cdup").strip()
2741                    if len(cdup) > 0:
2742                        chdir(cdup);
2743
2744        if not isValidGitDir(cmd.gitdir):
2745            if isValidGitDir(cmd.gitdir + "/.git"):
2746                cmd.gitdir += "/.git"
2747            else:
2748                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2749
2750        os.environ["GIT_DIR"] = cmd.gitdir
2751
2752    if not cmd.run(args):
2753        parser.print_help()
2754        sys.exit(2)
2755
2756
2757if __name__ == '__main__':
2758    main()