contrib / fast-import / git-p4on commit git-p4: warn if git authorship won't be retained (848de9c)
   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").lower() == "true"
 778
 779        if self.detectRenames:
 780            diffOpts = "-M"
 781        else:
 782            diffOpts = ""
 783
 784        if gitConfig("git-p4.detectCopies").lower() == "true":
 785            diffOpts += " -C"
 786
 787        if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
 788            diffOpts += " --find-copies-harder"
 789
 790        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 791        filesToAdd = set()
 792        filesToDelete = set()
 793        editedFiles = set()
 794        filesToChangeExecBit = {}
 795        for line in diff:
 796            diff = parseDiffTreeEntry(line)
 797            modifier = diff['status']
 798            path = diff['src']
 799            if modifier == "M":
 800                p4_system("edit \"%s\"" % path)
 801                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 802                    filesToChangeExecBit[path] = diff['dst_mode']
 803                editedFiles.add(path)
 804            elif modifier == "A":
 805                filesToAdd.add(path)
 806                filesToChangeExecBit[path] = diff['dst_mode']
 807                if path in filesToDelete:
 808                    filesToDelete.remove(path)
 809            elif modifier == "D":
 810                filesToDelete.add(path)
 811                if path in filesToAdd:
 812                    filesToAdd.remove(path)
 813            elif modifier == "C":
 814                src, dest = diff['src'], diff['dst']
 815                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 816                if diff['src_sha1'] != diff['dst_sha1']:
 817                    p4_system("edit \"%s\"" % (dest))
 818                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 819                    p4_system("edit \"%s\"" % (dest))
 820                    filesToChangeExecBit[dest] = diff['dst_mode']
 821                os.unlink(dest)
 822                editedFiles.add(dest)
 823            elif modifier == "R":
 824                src, dest = diff['src'], diff['dst']
 825                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 826                if diff['src_sha1'] != diff['dst_sha1']:
 827                    p4_system("edit \"%s\"" % (dest))
 828                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 829                    p4_system("edit \"%s\"" % (dest))
 830                    filesToChangeExecBit[dest] = diff['dst_mode']
 831                os.unlink(dest)
 832                editedFiles.add(dest)
 833                filesToDelete.add(src)
 834            else:
 835                die("unknown modifier %s for %s" % (modifier, path))
 836
 837        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 838        patchcmd = diffcmd + " | git apply "
 839        tryPatchCmd = patchcmd + "--check -"
 840        applyPatchCmd = patchcmd + "--check --apply -"
 841
 842        if os.system(tryPatchCmd) != 0:
 843            print "Unfortunately applying the change failed!"
 844            print "What do you want to do?"
 845            response = "x"
 846            while response != "s" and response != "a" and response != "w":
 847                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 848                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 849            if response == "s":
 850                print "Skipping! Good luck with the next patches..."
 851                for f in editedFiles:
 852                    p4_system("revert \"%s\"" % f);
 853                for f in filesToAdd:
 854                    system("rm %s" %f)
 855                return
 856            elif response == "a":
 857                os.system(applyPatchCmd)
 858                if len(filesToAdd) > 0:
 859                    print "You may also want to call p4 add on the following files:"
 860                    print " ".join(filesToAdd)
 861                if len(filesToDelete):
 862                    print "The following files should be scheduled for deletion with p4 delete:"
 863                    print " ".join(filesToDelete)
 864                die("Please resolve and submit the conflict manually and "
 865                    + "continue afterwards with git-p4 submit --continue")
 866            elif response == "w":
 867                system(diffcmd + " > patch.txt")
 868                print "Patch saved to patch.txt in %s !" % self.clientPath
 869                die("Please resolve and submit the conflict manually and "
 870                    "continue afterwards with git-p4 submit --continue")
 871
 872        system(applyPatchCmd)
 873
 874        for f in filesToAdd:
 875            p4_system("add \"%s\"" % f)
 876        for f in filesToDelete:
 877            p4_system("revert \"%s\"" % f)
 878            p4_system("delete \"%s\"" % f)
 879
 880        # Set/clear executable bits
 881        for f in filesToChangeExecBit.keys():
 882            mode = filesToChangeExecBit[f]
 883            setP4ExecBit(f, mode)
 884
 885        logMessage = extractLogMessageFromGitCommit(id)
 886        logMessage = logMessage.strip()
 887
 888        template = self.prepareSubmitTemplate()
 889
 890        if self.interactive:
 891            submitTemplate = self.prepareLogMessage(template, logMessage)
 892
 893            if self.preserveUser:
 894               submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
 895
 896            if os.environ.has_key("P4DIFF"):
 897                del(os.environ["P4DIFF"])
 898            diff = ""
 899            for editedFile in editedFiles:
 900                diff += p4_read_pipe("diff -du %r" % editedFile)
 901
 902            newdiff = ""
 903            for newFile in filesToAdd:
 904                newdiff += "==== new file ====\n"
 905                newdiff += "--- /dev/null\n"
 906                newdiff += "+++ %s\n" % newFile
 907                f = open(newFile, "r")
 908                for line in f.readlines():
 909                    newdiff += "+" + line
 910                f.close()
 911
 912            if self.checkAuthorship and not self.p4UserIsMe(p4User):
 913                submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
 914                submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n"
 915                submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"
 916
 917            separatorLine = "######## everything below this line is just the diff #######\n"
 918
 919            [handle, fileName] = tempfile.mkstemp()
 920            tmpFile = os.fdopen(handle, "w+")
 921            if self.isWindows:
 922                submitTemplate = submitTemplate.replace("\n", "\r\n")
 923                separatorLine = separatorLine.replace("\n", "\r\n")
 924                newdiff = newdiff.replace("\n", "\r\n")
 925            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
 926            tmpFile.close()
 927            mtime = os.stat(fileName).st_mtime
 928            if os.environ.has_key("P4EDITOR"):
 929                editor = os.environ.get("P4EDITOR")
 930            else:
 931                editor = read_pipe("git var GIT_EDITOR").strip()
 932            system(editor + " " + fileName)
 933
 934            if gitConfig("git-p4.skipSubmitEditCheck") == "true":
 935                checkModTime = False
 936            else:
 937                checkModTime = True
 938
 939            response = "y"
 940            if checkModTime and (os.stat(fileName).st_mtime <= mtime):
 941                response = "x"
 942                while response != "y" and response != "n":
 943                    response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
 944
 945            if response == "y":
 946                tmpFile = open(fileName, "rb")
 947                message = tmpFile.read()
 948                tmpFile.close()
 949                submitTemplate = message[:message.index(separatorLine)]
 950                if self.isWindows:
 951                    submitTemplate = submitTemplate.replace("\r\n", "\n")
 952                p4_write_pipe("submit -i", submitTemplate)
 953
 954                if self.preserveUser:
 955                    if p4User:
 956                        # Get last changelist number. Cannot easily get it from
 957                        # the submit command output as the output is unmarshalled.
 958                        changelist = self.lastP4Changelist()
 959                        self.modifyChangelistUser(changelist, p4User)
 960
 961            else:
 962                for f in editedFiles:
 963                    p4_system("revert \"%s\"" % f);
 964                for f in filesToAdd:
 965                    p4_system("revert \"%s\"" % f);
 966                    system("rm %s" %f)
 967
 968            os.remove(fileName)
 969        else:
 970            fileName = "submit.txt"
 971            file = open(fileName, "w+")
 972            file.write(self.prepareLogMessage(template, logMessage))
 973            file.close()
 974            print ("Perforce submit template written as %s. "
 975                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 976                   % (fileName, fileName))
 977
 978    def run(self, args):
 979        if len(args) == 0:
 980            self.master = currentGitBranch()
 981            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 982                die("Detecting current git branch failed!")
 983        elif len(args) == 1:
 984            self.master = args[0]
 985        else:
 986            return False
 987
 988        allowSubmit = gitConfig("git-p4.allowSubmit")
 989        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
 990            die("%s is not in git-p4.allowSubmit" % self.master)
 991
 992        [upstream, settings] = findUpstreamBranchPoint()
 993        self.depotPath = settings['depot-paths'][0]
 994        if len(self.origin) == 0:
 995            self.origin = upstream
 996
 997        if self.preserveUser:
 998            if not self.canChangeChangelists():
 999                die("Cannot preserve user names without p4 super-user or admin permissions")
1000
1001        if self.verbose:
1002            print "Origin branch is " + self.origin
1003
1004        if len(self.depotPath) == 0:
1005            print "Internal error: cannot locate perforce depot path from existing branches"
1006            sys.exit(128)
1007
1008        self.clientPath = p4Where(self.depotPath)
1009
1010        if len(self.clientPath) == 0:
1011            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
1012            sys.exit(128)
1013
1014        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1015        self.oldWorkingDirectory = os.getcwd()
1016
1017        chdir(self.clientPath)
1018        print "Synchronizing p4 checkout..."
1019        p4_system("sync ...")
1020
1021        self.check()
1022
1023        commits = []
1024        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1025            commits.append(line.strip())
1026        commits.reverse()
1027
1028        if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1029            self.checkAuthorship = False
1030        else:
1031            self.checkAuthorship = True
1032
1033        if self.preserveUser:
1034            self.checkValidP4Users(commits)
1035
1036        while len(commits) > 0:
1037            commit = commits[0]
1038            commits = commits[1:]
1039            self.applyCommit(commit)
1040            if not self.interactive:
1041                break
1042
1043        if len(commits) == 0:
1044            print "All changes applied!"
1045            chdir(self.oldWorkingDirectory)
1046
1047            sync = P4Sync()
1048            sync.run([])
1049
1050            rebase = P4Rebase()
1051            rebase.rebase()
1052
1053        return True
1054
1055class P4Sync(Command, P4UserMap):
1056    delete_actions = ( "delete", "move/delete", "purge" )
1057
1058    def __init__(self):
1059        Command.__init__(self)
1060        P4UserMap.__init__(self)
1061        self.options = [
1062                optparse.make_option("--branch", dest="branch"),
1063                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1064                optparse.make_option("--changesfile", dest="changesFile"),
1065                optparse.make_option("--silent", dest="silent", action="store_true"),
1066                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1067                optparse.make_option("--verbose", dest="verbose", action="store_true"),
1068                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1069                                     help="Import into refs/heads/ , not refs/remotes"),
1070                optparse.make_option("--max-changes", dest="maxChanges"),
1071                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1072                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1073                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1074                                     help="Only sync files that are included in the Perforce Client Spec")
1075        ]
1076        self.description = """Imports from Perforce into a git repository.\n
1077    example:
1078    //depot/my/project/ -- to import the current head
1079    //depot/my/project/@all -- to import everything
1080    //depot/my/project/@1,6 -- to import only from revision 1 to 6
1081
1082    (a ... is not needed in the path p4 specification, it's added implicitly)"""
1083
1084        self.usage += " //depot/path[@revRange]"
1085        self.silent = False
1086        self.createdBranches = set()
1087        self.committedChanges = set()
1088        self.branch = ""
1089        self.detectBranches = False
1090        self.detectLabels = False
1091        self.changesFile = ""
1092        self.syncWithOrigin = True
1093        self.verbose = False
1094        self.importIntoRemotes = True
1095        self.maxChanges = ""
1096        self.isWindows = (platform.system() == "Windows")
1097        self.keepRepoPath = False
1098        self.depotPaths = None
1099        self.p4BranchesInGit = []
1100        self.cloneExclude = []
1101        self.useClientSpec = False
1102        self.clientSpecDirs = []
1103
1104        if gitConfig("git-p4.syncFromOrigin") == "false":
1105            self.syncWithOrigin = False
1106
1107    #
1108    # P4 wildcards are not allowed in filenames.  P4 complains
1109    # if you simply add them, but you can force it with "-f", in
1110    # which case it translates them into %xx encoding internally.
1111    # Search for and fix just these four characters.  Do % last so
1112    # that fixing it does not inadvertently create new %-escapes.
1113    #
1114    def wildcard_decode(self, path):
1115        # Cannot have * in a filename in windows; untested as to
1116        # what p4 would do in such a case.
1117        if not self.isWindows:
1118            path = path.replace("%2A", "*")
1119        path = path.replace("%23", "#") \
1120                   .replace("%40", "@") \
1121                   .replace("%25", "%")
1122        return path
1123
1124    def extractFilesFromCommit(self, commit):
1125        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1126                             for path in self.cloneExclude]
1127        files = []
1128        fnum = 0
1129        while commit.has_key("depotFile%s" % fnum):
1130            path =  commit["depotFile%s" % fnum]
1131
1132            if [p for p in self.cloneExclude
1133                if p4PathStartsWith(path, p)]:
1134                found = False
1135            else:
1136                found = [p for p in self.depotPaths
1137                         if p4PathStartsWith(path, p)]
1138            if not found:
1139                fnum = fnum + 1
1140                continue
1141
1142            file = {}
1143            file["path"] = path
1144            file["rev"] = commit["rev%s" % fnum]
1145            file["action"] = commit["action%s" % fnum]
1146            file["type"] = commit["type%s" % fnum]
1147            files.append(file)
1148            fnum = fnum + 1
1149        return files
1150
1151    def stripRepoPath(self, path, prefixes):
1152        if self.useClientSpec:
1153
1154            # if using the client spec, we use the output directory
1155            # specified in the client.  For example, a view
1156            #   //depot/foo/branch/... //client/branch/foo/...
1157            # will end up putting all foo/branch files into
1158            #  branch/foo/
1159            for val in self.clientSpecDirs:
1160                if path.startswith(val[0]):
1161                    # replace the depot path with the client path
1162                    path = path.replace(val[0], val[1][1])
1163                    # now strip out the client (//client/...)
1164                    path = re.sub("^(//[^/]+/)", '', path)
1165                    # the rest is all path
1166                    return path
1167
1168        if self.keepRepoPath:
1169            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1170
1171        for p in prefixes:
1172            if p4PathStartsWith(path, p):
1173                path = path[len(p):]
1174
1175        return path
1176
1177    def splitFilesIntoBranches(self, commit):
1178        branches = {}
1179        fnum = 0
1180        while commit.has_key("depotFile%s" % fnum):
1181            path =  commit["depotFile%s" % fnum]
1182            found = [p for p in self.depotPaths
1183                     if p4PathStartsWith(path, p)]
1184            if not found:
1185                fnum = fnum + 1
1186                continue
1187
1188            file = {}
1189            file["path"] = path
1190            file["rev"] = commit["rev%s" % fnum]
1191            file["action"] = commit["action%s" % fnum]
1192            file["type"] = commit["type%s" % fnum]
1193            fnum = fnum + 1
1194
1195            relPath = self.stripRepoPath(path, self.depotPaths)
1196
1197            for branch in self.knownBranches.keys():
1198
1199                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1200                if relPath.startswith(branch + "/"):
1201                    if branch not in branches:
1202                        branches[branch] = []
1203                    branches[branch].append(file)
1204                    break
1205
1206        return branches
1207
1208    # output one file from the P4 stream
1209    # - helper for streamP4Files
1210
1211    def streamOneP4File(self, file, contents):
1212        if file["type"] == "apple":
1213            print "\nfile %s is a strange apple file that forks. Ignoring" % \
1214                file['depotFile']
1215            return
1216
1217        relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1218        relPath = self.wildcard_decode(relPath)
1219        if verbose:
1220            sys.stderr.write("%s\n" % relPath)
1221
1222        mode = "644"
1223        if isP4Exec(file["type"]):
1224            mode = "755"
1225        elif file["type"] == "symlink":
1226            mode = "120000"
1227            # p4 print on a symlink contains "target\n", so strip it off
1228            data = ''.join(contents)
1229            contents = [data[:-1]]
1230
1231        if self.isWindows and file["type"].endswith("text"):
1232            mangled = []
1233            for data in contents:
1234                data = data.replace("\r\n", "\n")
1235                mangled.append(data)
1236            contents = mangled
1237
1238        if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1239            contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1240        elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1241            contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1242
1243        self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1244
1245        # total length...
1246        length = 0
1247        for d in contents:
1248            length = length + len(d)
1249
1250        self.gitStream.write("data %d\n" % length)
1251        for d in contents:
1252            self.gitStream.write(d)
1253        self.gitStream.write("\n")
1254
1255    def streamOneP4Deletion(self, file):
1256        relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1257        if verbose:
1258            sys.stderr.write("delete %s\n" % relPath)
1259        self.gitStream.write("D %s\n" % relPath)
1260
1261    # handle another chunk of streaming data
1262    def streamP4FilesCb(self, marshalled):
1263
1264        if marshalled.has_key('depotFile') and self.stream_have_file_info:
1265            # start of a new file - output the old one first
1266            self.streamOneP4File(self.stream_file, self.stream_contents)
1267            self.stream_file = {}
1268            self.stream_contents = []
1269            self.stream_have_file_info = False
1270
1271        # pick up the new file information... for the
1272        # 'data' field we need to append to our array
1273        for k in marshalled.keys():
1274            if k == 'data':
1275                self.stream_contents.append(marshalled['data'])
1276            else:
1277                self.stream_file[k] = marshalled[k]
1278
1279        self.stream_have_file_info = True
1280
1281    # Stream directly from "p4 files" into "git fast-import"
1282    def streamP4Files(self, files):
1283        filesForCommit = []
1284        filesToRead = []
1285        filesToDelete = []
1286
1287        for f in files:
1288            includeFile = True
1289            for val in self.clientSpecDirs:
1290                if f['path'].startswith(val[0]):
1291                    if val[1][0] <= 0:
1292                        includeFile = False
1293                    break
1294
1295            if includeFile:
1296                filesForCommit.append(f)
1297                if f['action'] in self.delete_actions:
1298                    filesToDelete.append(f)
1299                else:
1300                    filesToRead.append(f)
1301
1302        # deleted files...
1303        for f in filesToDelete:
1304            self.streamOneP4Deletion(f)
1305
1306        if len(filesToRead) > 0:
1307            self.stream_file = {}
1308            self.stream_contents = []
1309            self.stream_have_file_info = False
1310
1311            # curry self argument
1312            def streamP4FilesCbSelf(entry):
1313                self.streamP4FilesCb(entry)
1314
1315            p4CmdList("-x - print",
1316                '\n'.join(['%s#%s' % (f['path'], f['rev'])
1317                                                  for f in filesToRead]),
1318                cb=streamP4FilesCbSelf)
1319
1320            # do the last chunk
1321            if self.stream_file.has_key('depotFile'):
1322                self.streamOneP4File(self.stream_file, self.stream_contents)
1323
1324    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1325        epoch = details["time"]
1326        author = details["user"]
1327        self.branchPrefixes = branchPrefixes
1328
1329        if self.verbose:
1330            print "commit into %s" % branch
1331
1332        # start with reading files; if that fails, we should not
1333        # create a commit.
1334        new_files = []
1335        for f in files:
1336            if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1337                new_files.append (f)
1338            else:
1339                sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1340
1341        self.gitStream.write("commit %s\n" % branch)
1342#        gitStream.write("mark :%s\n" % details["change"])
1343        self.committedChanges.add(int(details["change"]))
1344        committer = ""
1345        if author not in self.users:
1346            self.getUserMapFromPerforceServer()
1347        if author in self.users:
1348            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1349        else:
1350            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1351
1352        self.gitStream.write("committer %s\n" % committer)
1353
1354        self.gitStream.write("data <<EOT\n")
1355        self.gitStream.write(details["desc"])
1356        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1357                             % (','.join (branchPrefixes), details["change"]))
1358        if len(details['options']) > 0:
1359            self.gitStream.write(": options = %s" % details['options'])
1360        self.gitStream.write("]\nEOT\n\n")
1361
1362        if len(parent) > 0:
1363            if self.verbose:
1364                print "parent %s" % parent
1365            self.gitStream.write("from %s\n" % parent)
1366
1367        self.streamP4Files(new_files)
1368        self.gitStream.write("\n")
1369
1370        change = int(details["change"])
1371
1372        if self.labels.has_key(change):
1373            label = self.labels[change]
1374            labelDetails = label[0]
1375            labelRevisions = label[1]
1376            if self.verbose:
1377                print "Change %s is labelled %s" % (change, labelDetails)
1378
1379            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1380                                                    for p in branchPrefixes]))
1381
1382            if len(files) == len(labelRevisions):
1383
1384                cleanedFiles = {}
1385                for info in files:
1386                    if info["action"] in self.delete_actions:
1387                        continue
1388                    cleanedFiles[info["depotFile"]] = info["rev"]
1389
1390                if cleanedFiles == labelRevisions:
1391                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1392                    self.gitStream.write("from %s\n" % branch)
1393
1394                    owner = labelDetails["Owner"]
1395                    tagger = ""
1396                    if author in self.users:
1397                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1398                    else:
1399                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1400                    self.gitStream.write("tagger %s\n" % tagger)
1401                    self.gitStream.write("data <<EOT\n")
1402                    self.gitStream.write(labelDetails["Description"])
1403                    self.gitStream.write("EOT\n\n")
1404
1405                else:
1406                    if not self.silent:
1407                        print ("Tag %s does not match with change %s: files do not match."
1408                               % (labelDetails["label"], change))
1409
1410            else:
1411                if not self.silent:
1412                    print ("Tag %s does not match with change %s: file count is different."
1413                           % (labelDetails["label"], change))
1414
1415    def getLabels(self):
1416        self.labels = {}
1417
1418        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1419        if len(l) > 0 and not self.silent:
1420            print "Finding files belonging to labels in %s" % `self.depotPaths`
1421
1422        for output in l:
1423            label = output["label"]
1424            revisions = {}
1425            newestChange = 0
1426            if self.verbose:
1427                print "Querying files for label %s" % label
1428            for file in p4CmdList("files "
1429                                  +  ' '.join (["%s...@%s" % (p, label)
1430                                                for p in self.depotPaths])):
1431                revisions[file["depotFile"]] = file["rev"]
1432                change = int(file["change"])
1433                if change > newestChange:
1434                    newestChange = change
1435
1436            self.labels[newestChange] = [output, revisions]
1437
1438        if self.verbose:
1439            print "Label changes: %s" % self.labels.keys()
1440
1441    def guessProjectName(self):
1442        for p in self.depotPaths:
1443            if p.endswith("/"):
1444                p = p[:-1]
1445            p = p[p.strip().rfind("/") + 1:]
1446            if not p.endswith("/"):
1447               p += "/"
1448            return p
1449
1450    def getBranchMapping(self):
1451        lostAndFoundBranches = set()
1452
1453        for info in p4CmdList("branches"):
1454            details = p4Cmd("branch -o %s" % info["branch"])
1455            viewIdx = 0
1456            while details.has_key("View%s" % viewIdx):
1457                paths = details["View%s" % viewIdx].split(" ")
1458                viewIdx = viewIdx + 1
1459                # require standard //depot/foo/... //depot/bar/... mapping
1460                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1461                    continue
1462                source = paths[0]
1463                destination = paths[1]
1464                ## HACK
1465                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1466                    source = source[len(self.depotPaths[0]):-4]
1467                    destination = destination[len(self.depotPaths[0]):-4]
1468
1469                    if destination in self.knownBranches:
1470                        if not self.silent:
1471                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1472                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1473                        continue
1474
1475                    self.knownBranches[destination] = source
1476
1477                    lostAndFoundBranches.discard(destination)
1478
1479                    if source not in self.knownBranches:
1480                        lostAndFoundBranches.add(source)
1481
1482
1483        for branch in lostAndFoundBranches:
1484            self.knownBranches[branch] = branch
1485
1486    def getBranchMappingFromGitBranches(self):
1487        branches = p4BranchesInGit(self.importIntoRemotes)
1488        for branch in branches.keys():
1489            if branch == "master":
1490                branch = "main"
1491            else:
1492                branch = branch[len(self.projectName):]
1493            self.knownBranches[branch] = branch
1494
1495    def listExistingP4GitBranches(self):
1496        # branches holds mapping from name to commit
1497        branches = p4BranchesInGit(self.importIntoRemotes)
1498        self.p4BranchesInGit = branches.keys()
1499        for branch in branches.keys():
1500            self.initialParents[self.refPrefix + branch] = branches[branch]
1501
1502    def updateOptionDict(self, d):
1503        option_keys = {}
1504        if self.keepRepoPath:
1505            option_keys['keepRepoPath'] = 1
1506
1507        d["options"] = ' '.join(sorted(option_keys.keys()))
1508
1509    def readOptions(self, d):
1510        self.keepRepoPath = (d.has_key('options')
1511                             and ('keepRepoPath' in d['options']))
1512
1513    def gitRefForBranch(self, branch):
1514        if branch == "main":
1515            return self.refPrefix + "master"
1516
1517        if len(branch) <= 0:
1518            return branch
1519
1520        return self.refPrefix + self.projectName + branch
1521
1522    def gitCommitByP4Change(self, ref, change):
1523        if self.verbose:
1524            print "looking in ref " + ref + " for change %s using bisect..." % change
1525
1526        earliestCommit = ""
1527        latestCommit = parseRevision(ref)
1528
1529        while True:
1530            if self.verbose:
1531                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1532            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1533            if len(next) == 0:
1534                if self.verbose:
1535                    print "argh"
1536                return ""
1537            log = extractLogMessageFromGitCommit(next)
1538            settings = extractSettingsGitLog(log)
1539            currentChange = int(settings['change'])
1540            if self.verbose:
1541                print "current change %s" % currentChange
1542
1543            if currentChange == change:
1544                if self.verbose:
1545                    print "found %s" % next
1546                return next
1547
1548            if currentChange < change:
1549                earliestCommit = "^%s" % next
1550            else:
1551                latestCommit = "%s" % next
1552
1553        return ""
1554
1555    def importNewBranch(self, branch, maxChange):
1556        # make fast-import flush all changes to disk and update the refs using the checkpoint
1557        # command so that we can try to find the branch parent in the git history
1558        self.gitStream.write("checkpoint\n\n");
1559        self.gitStream.flush();
1560        branchPrefix = self.depotPaths[0] + branch + "/"
1561        range = "@1,%s" % maxChange
1562        #print "prefix" + branchPrefix
1563        changes = p4ChangesForPaths([branchPrefix], range)
1564        if len(changes) <= 0:
1565            return False
1566        firstChange = changes[0]
1567        #print "first change in branch: %s" % firstChange
1568        sourceBranch = self.knownBranches[branch]
1569        sourceDepotPath = self.depotPaths[0] + sourceBranch
1570        sourceRef = self.gitRefForBranch(sourceBranch)
1571        #print "source " + sourceBranch
1572
1573        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1574        #print "branch parent: %s" % branchParentChange
1575        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1576        if len(gitParent) > 0:
1577            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1578            #print "parent git commit: %s" % gitParent
1579
1580        self.importChanges(changes)
1581        return True
1582
1583    def importChanges(self, changes):
1584        cnt = 1
1585        for change in changes:
1586            description = p4Cmd("describe %s" % change)
1587            self.updateOptionDict(description)
1588
1589            if not self.silent:
1590                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1591                sys.stdout.flush()
1592            cnt = cnt + 1
1593
1594            try:
1595                if self.detectBranches:
1596                    branches = self.splitFilesIntoBranches(description)
1597                    for branch in branches.keys():
1598                        ## HACK  --hwn
1599                        branchPrefix = self.depotPaths[0] + branch + "/"
1600
1601                        parent = ""
1602
1603                        filesForCommit = branches[branch]
1604
1605                        if self.verbose:
1606                            print "branch is %s" % branch
1607
1608                        self.updatedBranches.add(branch)
1609
1610                        if branch not in self.createdBranches:
1611                            self.createdBranches.add(branch)
1612                            parent = self.knownBranches[branch]
1613                            if parent == branch:
1614                                parent = ""
1615                            else:
1616                                fullBranch = self.projectName + branch
1617                                if fullBranch not in self.p4BranchesInGit:
1618                                    if not self.silent:
1619                                        print("\n    Importing new branch %s" % fullBranch);
1620                                    if self.importNewBranch(branch, change - 1):
1621                                        parent = ""
1622                                        self.p4BranchesInGit.append(fullBranch)
1623                                    if not self.silent:
1624                                        print("\n    Resuming with change %s" % change);
1625
1626                                if self.verbose:
1627                                    print "parent determined through known branches: %s" % parent
1628
1629                        branch = self.gitRefForBranch(branch)
1630                        parent = self.gitRefForBranch(parent)
1631
1632                        if self.verbose:
1633                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1634
1635                        if len(parent) == 0 and branch in self.initialParents:
1636                            parent = self.initialParents[branch]
1637                            del self.initialParents[branch]
1638
1639                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1640                else:
1641                    files = self.extractFilesFromCommit(description)
1642                    self.commit(description, files, self.branch, self.depotPaths,
1643                                self.initialParent)
1644                    self.initialParent = ""
1645            except IOError:
1646                print self.gitError.read()
1647                sys.exit(1)
1648
1649    def importHeadRevision(self, revision):
1650        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1651
1652        details = { "user" : "git perforce import user", "time" : int(time.time()) }
1653        details["desc"] = ("Initial import of %s from the state at revision %s\n"
1654                           % (' '.join(self.depotPaths), revision))
1655        details["change"] = revision
1656        newestRevision = 0
1657
1658        fileCnt = 0
1659        for info in p4CmdList("files "
1660                              +  ' '.join(["%s...%s"
1661                                           % (p, revision)
1662                                           for p in self.depotPaths])):
1663
1664            if 'code' in info and info['code'] == 'error':
1665                sys.stderr.write("p4 returned an error: %s\n"
1666                                 % info['data'])
1667                if info['data'].find("must refer to client") >= 0:
1668                    sys.stderr.write("This particular p4 error is misleading.\n")
1669                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
1670                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
1671                sys.exit(1)
1672            if 'p4ExitCode' in info:
1673                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1674                sys.exit(1)
1675
1676
1677            change = int(info["change"])
1678            if change > newestRevision:
1679                newestRevision = change
1680
1681            if info["action"] in self.delete_actions:
1682                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1683                #fileCnt = fileCnt + 1
1684                continue
1685
1686            for prop in ["depotFile", "rev", "action", "type" ]:
1687                details["%s%s" % (prop, fileCnt)] = info[prop]
1688
1689            fileCnt = fileCnt + 1
1690
1691        details["change"] = newestRevision
1692        self.updateOptionDict(details)
1693        try:
1694            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1695        except IOError:
1696            print "IO error with git fast-import. Is your git version recent enough?"
1697            print self.gitError.read()
1698
1699
1700    def getClientSpec(self):
1701        specList = p4CmdList( "client -o" )
1702        temp = {}
1703        for entry in specList:
1704            for k,v in entry.iteritems():
1705                if k.startswith("View"):
1706
1707                    # p4 has these %%1 to %%9 arguments in specs to
1708                    # reorder paths; which we can't handle (yet :)
1709                    if re.match('%%\d', v) != None:
1710                        print "Sorry, can't handle %%n arguments in client specs"
1711                        sys.exit(1)
1712
1713                    if v.startswith('"'):
1714                        start = 1
1715                    else:
1716                        start = 0
1717                    index = v.find("...")
1718
1719                    # save the "client view"; i.e the RHS of the view
1720                    # line that tells the client where to put the
1721                    # files for this view.
1722                    cv = v[index+3:].strip() # +3 to remove previous '...'
1723
1724                    # if the client view doesn't end with a
1725                    # ... wildcard, then we're going to mess up the
1726                    # output directory, so fail gracefully.
1727                    if not cv.endswith('...'):
1728                        print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1729                        sys.exit(1)
1730                    cv=cv[:-3]
1731
1732                    # now save the view; +index means included, -index
1733                    # means it should be filtered out.
1734                    v = v[start:index]
1735                    if v.startswith("-"):
1736                        v = v[1:]
1737                        include = -len(v)
1738                    else:
1739                        include = len(v)
1740
1741                    temp[v] = (include, cv)
1742
1743        self.clientSpecDirs = temp.items()
1744        self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1745
1746    def run(self, args):
1747        self.depotPaths = []
1748        self.changeRange = ""
1749        self.initialParent = ""
1750        self.previousDepotPaths = []
1751
1752        # map from branch depot path to parent branch
1753        self.knownBranches = {}
1754        self.initialParents = {}
1755        self.hasOrigin = originP4BranchesExist()
1756        if not self.syncWithOrigin:
1757            self.hasOrigin = False
1758
1759        if self.importIntoRemotes:
1760            self.refPrefix = "refs/remotes/p4/"
1761        else:
1762            self.refPrefix = "refs/heads/p4/"
1763
1764        if self.syncWithOrigin and self.hasOrigin:
1765            if not self.silent:
1766                print "Syncing with origin first by calling git fetch origin"
1767            system("git fetch origin")
1768
1769        if len(self.branch) == 0:
1770            self.branch = self.refPrefix + "master"
1771            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1772                system("git update-ref %s refs/heads/p4" % self.branch)
1773                system("git branch -D p4");
1774            # create it /after/ importing, when master exists
1775            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1776                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1777
1778        if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1779            self.getClientSpec()
1780
1781        # TODO: should always look at previous commits,
1782        # merge with previous imports, if possible.
1783        if args == []:
1784            if self.hasOrigin:
1785                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1786            self.listExistingP4GitBranches()
1787
1788            if len(self.p4BranchesInGit) > 1:
1789                if not self.silent:
1790                    print "Importing from/into multiple branches"
1791                self.detectBranches = True
1792
1793            if self.verbose:
1794                print "branches: %s" % self.p4BranchesInGit
1795
1796            p4Change = 0
1797            for branch in self.p4BranchesInGit:
1798                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1799
1800                settings = extractSettingsGitLog(logMsg)
1801
1802                self.readOptions(settings)
1803                if (settings.has_key('depot-paths')
1804                    and settings.has_key ('change')):
1805                    change = int(settings['change']) + 1
1806                    p4Change = max(p4Change, change)
1807
1808                    depotPaths = sorted(settings['depot-paths'])
1809                    if self.previousDepotPaths == []:
1810                        self.previousDepotPaths = depotPaths
1811                    else:
1812                        paths = []
1813                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1814                            for i in range(0, min(len(cur), len(prev))):
1815                                if cur[i] <> prev[i]:
1816                                    i = i - 1
1817                                    break
1818
1819                            paths.append (cur[:i + 1])
1820
1821                        self.previousDepotPaths = paths
1822
1823            if p4Change > 0:
1824                self.depotPaths = sorted(self.previousDepotPaths)
1825                self.changeRange = "@%s,#head" % p4Change
1826                if not self.detectBranches:
1827                    self.initialParent = parseRevision(self.branch)
1828                if not self.silent and not self.detectBranches:
1829                    print "Performing incremental import into %s git branch" % self.branch
1830
1831        if not self.branch.startswith("refs/"):
1832            self.branch = "refs/heads/" + self.branch
1833
1834        if len(args) == 0 and self.depotPaths:
1835            if not self.silent:
1836                print "Depot paths: %s" % ' '.join(self.depotPaths)
1837        else:
1838            if self.depotPaths and self.depotPaths != args:
1839                print ("previous import used depot path %s and now %s was specified. "
1840                       "This doesn't work!" % (' '.join (self.depotPaths),
1841                                               ' '.join (args)))
1842                sys.exit(1)
1843
1844            self.depotPaths = sorted(args)
1845
1846        revision = ""
1847        self.users = {}
1848
1849        newPaths = []
1850        for p in self.depotPaths:
1851            if p.find("@") != -1:
1852                atIdx = p.index("@")
1853                self.changeRange = p[atIdx:]
1854                if self.changeRange == "@all":
1855                    self.changeRange = ""
1856                elif ',' not in self.changeRange:
1857                    revision = self.changeRange
1858                    self.changeRange = ""
1859                p = p[:atIdx]
1860            elif p.find("#") != -1:
1861                hashIdx = p.index("#")
1862                revision = p[hashIdx:]
1863                p = p[:hashIdx]
1864            elif self.previousDepotPaths == []:
1865                revision = "#head"
1866
1867            p = re.sub ("\.\.\.$", "", p)
1868            if not p.endswith("/"):
1869                p += "/"
1870
1871            newPaths.append(p)
1872
1873        self.depotPaths = newPaths
1874
1875
1876        self.loadUserMapFromCache()
1877        self.labels = {}
1878        if self.detectLabels:
1879            self.getLabels();
1880
1881        if self.detectBranches:
1882            ## FIXME - what's a P4 projectName ?
1883            self.projectName = self.guessProjectName()
1884
1885            if self.hasOrigin:
1886                self.getBranchMappingFromGitBranches()
1887            else:
1888                self.getBranchMapping()
1889            if self.verbose:
1890                print "p4-git branches: %s" % self.p4BranchesInGit
1891                print "initial parents: %s" % self.initialParents
1892            for b in self.p4BranchesInGit:
1893                if b != "master":
1894
1895                    ## FIXME
1896                    b = b[len(self.projectName):]
1897                self.createdBranches.add(b)
1898
1899        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1900
1901        importProcess = subprocess.Popen(["git", "fast-import"],
1902                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1903                                         stderr=subprocess.PIPE);
1904        self.gitOutput = importProcess.stdout
1905        self.gitStream = importProcess.stdin
1906        self.gitError = importProcess.stderr
1907
1908        if revision:
1909            self.importHeadRevision(revision)
1910        else:
1911            changes = []
1912
1913            if len(self.changesFile) > 0:
1914                output = open(self.changesFile).readlines()
1915                changeSet = set()
1916                for line in output:
1917                    changeSet.add(int(line))
1918
1919                for change in changeSet:
1920                    changes.append(change)
1921
1922                changes.sort()
1923            else:
1924                # catch "git-p4 sync" with no new branches, in a repo that
1925                # does not have any existing git-p4 branches
1926                if len(args) == 0 and not self.p4BranchesInGit:
1927                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
1928                if self.verbose:
1929                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1930                                                              self.changeRange)
1931                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1932
1933                if len(self.maxChanges) > 0:
1934                    changes = changes[:min(int(self.maxChanges), len(changes))]
1935
1936            if len(changes) == 0:
1937                if not self.silent:
1938                    print "No changes to import!"
1939                return True
1940
1941            if not self.silent and not self.detectBranches:
1942                print "Import destination: %s" % self.branch
1943
1944            self.updatedBranches = set()
1945
1946            self.importChanges(changes)
1947
1948            if not self.silent:
1949                print ""
1950                if len(self.updatedBranches) > 0:
1951                    sys.stdout.write("Updated branches: ")
1952                    for b in self.updatedBranches:
1953                        sys.stdout.write("%s " % b)
1954                    sys.stdout.write("\n")
1955
1956        self.gitStream.close()
1957        if importProcess.wait() != 0:
1958            die("fast-import failed: %s" % self.gitError.read())
1959        self.gitOutput.close()
1960        self.gitError.close()
1961
1962        return True
1963
1964class P4Rebase(Command):
1965    def __init__(self):
1966        Command.__init__(self)
1967        self.options = [ ]
1968        self.description = ("Fetches the latest revision from perforce and "
1969                            + "rebases the current work (branch) against it")
1970        self.verbose = False
1971
1972    def run(self, args):
1973        sync = P4Sync()
1974        sync.run([])
1975
1976        return self.rebase()
1977
1978    def rebase(self):
1979        if os.system("git update-index --refresh") != 0:
1980            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.");
1981        if len(read_pipe("git diff-index HEAD --")) > 0:
1982            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1983
1984        [upstream, settings] = findUpstreamBranchPoint()
1985        if len(upstream) == 0:
1986            die("Cannot find upstream branchpoint for rebase")
1987
1988        # the branchpoint may be p4/foo~3, so strip off the parent
1989        upstream = re.sub("~[0-9]+$", "", upstream)
1990
1991        print "Rebasing the current branch onto %s" % upstream
1992        oldHead = read_pipe("git rev-parse HEAD").strip()
1993        system("git rebase %s" % upstream)
1994        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1995        return True
1996
1997class P4Clone(P4Sync):
1998    def __init__(self):
1999        P4Sync.__init__(self)
2000        self.description = "Creates a new git repository and imports from Perforce into it"
2001        self.usage = "usage: %prog [options] //depot/path[@revRange]"
2002        self.options += [
2003            optparse.make_option("--destination", dest="cloneDestination",
2004                                 action='store', default=None,
2005                                 help="where to leave result of the clone"),
2006            optparse.make_option("-/", dest="cloneExclude",
2007                                 action="append", type="string",
2008                                 help="exclude depot path"),
2009            optparse.make_option("--bare", dest="cloneBare",
2010                                 action="store_true", default=False),
2011        ]
2012        self.cloneDestination = None
2013        self.needsGit = False
2014        self.cloneBare = False
2015
2016    # This is required for the "append" cloneExclude action
2017    def ensure_value(self, attr, value):
2018        if not hasattr(self, attr) or getattr(self, attr) is None:
2019            setattr(self, attr, value)
2020        return getattr(self, attr)
2021
2022    def defaultDestination(self, args):
2023        ## TODO: use common prefix of args?
2024        depotPath = args[0]
2025        depotDir = re.sub("(@[^@]*)$", "", depotPath)
2026        depotDir = re.sub("(#[^#]*)$", "", depotDir)
2027        depotDir = re.sub(r"\.\.\.$", "", depotDir)
2028        depotDir = re.sub(r"/$", "", depotDir)
2029        return os.path.split(depotDir)[1]
2030
2031    def run(self, args):
2032        if len(args) < 1:
2033            return False
2034
2035        if self.keepRepoPath and not self.cloneDestination:
2036            sys.stderr.write("Must specify destination for --keep-path\n")
2037            sys.exit(1)
2038
2039        depotPaths = args
2040
2041        if not self.cloneDestination and len(depotPaths) > 1:
2042            self.cloneDestination = depotPaths[-1]
2043            depotPaths = depotPaths[:-1]
2044
2045        self.cloneExclude = ["/"+p for p in self.cloneExclude]
2046        for p in depotPaths:
2047            if not p.startswith("//"):
2048                return False
2049
2050        if not self.cloneDestination:
2051            self.cloneDestination = self.defaultDestination(args)
2052
2053        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2054
2055        if not os.path.exists(self.cloneDestination):
2056            os.makedirs(self.cloneDestination)
2057        chdir(self.cloneDestination)
2058
2059        init_cmd = [ "git", "init" ]
2060        if self.cloneBare:
2061            init_cmd.append("--bare")
2062        subprocess.check_call(init_cmd)
2063
2064        if not P4Sync.run(self, depotPaths):
2065            return False
2066        if self.branch != "master":
2067            if self.importIntoRemotes:
2068                masterbranch = "refs/remotes/p4/master"
2069            else:
2070                masterbranch = "refs/heads/p4/master"
2071            if gitBranchExists(masterbranch):
2072                system("git branch master %s" % masterbranch)
2073                if not self.cloneBare:
2074                    system("git checkout -f")
2075            else:
2076                print "Could not detect main branch. No checkout/master branch created."
2077
2078        return True
2079
2080class P4Branches(Command):
2081    def __init__(self):
2082        Command.__init__(self)
2083        self.options = [ ]
2084        self.description = ("Shows the git branches that hold imports and their "
2085                            + "corresponding perforce depot paths")
2086        self.verbose = False
2087
2088    def run(self, args):
2089        if originP4BranchesExist():
2090            createOrUpdateBranchesFromOrigin()
2091
2092        cmdline = "git rev-parse --symbolic "
2093        cmdline += " --remotes"
2094
2095        for line in read_pipe_lines(cmdline):
2096            line = line.strip()
2097
2098            if not line.startswith('p4/') or line == "p4/HEAD":
2099                continue
2100            branch = line
2101
2102            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2103            settings = extractSettingsGitLog(log)
2104
2105            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2106        return True
2107
2108class HelpFormatter(optparse.IndentedHelpFormatter):
2109    def __init__(self):
2110        optparse.IndentedHelpFormatter.__init__(self)
2111
2112    def format_description(self, description):
2113        if description:
2114            return description + "\n"
2115        else:
2116            return ""
2117
2118def printUsage(commands):
2119    print "usage: %s <command> [options]" % sys.argv[0]
2120    print ""
2121    print "valid commands: %s" % ", ".join(commands)
2122    print ""
2123    print "Try %s <command> --help for command specific help." % sys.argv[0]
2124    print ""
2125
2126commands = {
2127    "debug" : P4Debug,
2128    "submit" : P4Submit,
2129    "commit" : P4Submit,
2130    "sync" : P4Sync,
2131    "rebase" : P4Rebase,
2132    "clone" : P4Clone,
2133    "rollback" : P4RollBack,
2134    "branches" : P4Branches
2135}
2136
2137
2138def main():
2139    if len(sys.argv[1:]) == 0:
2140        printUsage(commands.keys())
2141        sys.exit(2)
2142
2143    cmd = ""
2144    cmdName = sys.argv[1]
2145    try:
2146        klass = commands[cmdName]
2147        cmd = klass()
2148    except KeyError:
2149        print "unknown command %s" % cmdName
2150        print ""
2151        printUsage(commands.keys())
2152        sys.exit(2)
2153
2154    options = cmd.options
2155    cmd.gitdir = os.environ.get("GIT_DIR", None)
2156
2157    args = sys.argv[2:]
2158
2159    if len(options) > 0:
2160        options.append(optparse.make_option("--git-dir", dest="gitdir"))
2161
2162        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2163                                       options,
2164                                       description = cmd.description,
2165                                       formatter = HelpFormatter())
2166
2167        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2168    global verbose
2169    verbose = cmd.verbose
2170    if cmd.needsGit:
2171        if cmd.gitdir == None:
2172            cmd.gitdir = os.path.abspath(".git")
2173            if not isValidGitDir(cmd.gitdir):
2174                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2175                if os.path.exists(cmd.gitdir):
2176                    cdup = read_pipe("git rev-parse --show-cdup").strip()
2177                    if len(cdup) > 0:
2178                        chdir(cdup);
2179
2180        if not isValidGitDir(cmd.gitdir):
2181            if isValidGitDir(cmd.gitdir + "/.git"):
2182                cmd.gitdir += "/.git"
2183            else:
2184                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2185
2186        os.environ["GIT_DIR"] = cmd.gitdir
2187
2188    if not cmd.run(args):
2189        parser.print_help()
2190
2191
2192if __name__ == '__main__':
2193    main()