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