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#
  10import optparse, sys, os, marshal, popen2, subprocess, shelve
  12import tempfile, getopt, sha, os.path, time, platform
  13import re
  14from sets import Set;
  16verbose = False
  18def die(msg):
  20    if verbose:
  21        raise Exception(msg)
  22    else:
  23        sys.stderr.write(msg + "\n")
  24        sys.exit(1)
  25def write_pipe(c, str):
  27    if verbose:
  28        sys.stderr.write('Writing pipe: %s\n' % c)
  29    pipe = os.popen(c, 'w')
  31    val = pipe.write(str)
  32    if pipe.close():
  33        die('Command failed: %s' % c)
  34    return val
  36def read_pipe(c, ignore_error=False):
  38    if verbose:
  39        sys.stderr.write('Reading pipe: %s\n' % c)
  40    pipe = os.popen(c, 'rb')
  42    val = pipe.read()
  43    if pipe.close() and not ignore_error:
  44        die('Command failed: %s' % c)
  45    return val
  47def read_pipe_lines(c):
  50    if verbose:
  51        sys.stderr.write('Reading pipe: %s\n' % c)
  52    ## todo: check return status
  53    pipe = os.popen(c, 'rb')
  54    val = pipe.readlines()
  55    if pipe.close():
  56        die('Command failed: %s' % c)
  57    return val
  59def system(cmd):
  61    if verbose:
  62        sys.stderr.write("executing %s\n" % cmd)
  63    if os.system(cmd) != 0:
  64        die("command failed: %s" % cmd)
  65def isP4Exec(kind):
  67    """Determine if a Perforce 'kind' should have execute permission
  68    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
  70    or x follows one of a few letters.  Otherwise, if there is an 'x' after
  71    a plus sign, it is also executable"""
  72    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
  73def setP4ExecBit(file, mode):
  75    # Reopens an already open file and changes the execute bit to match
  76    # the execute bit setting in the passed in mode.
  77    p4Type = "+x"
  79    if not isModeExec(mode):
  81        p4Type = getP4OpenedType(file)
  82        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
  83        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
  84        if p4Type[-1] == "+":
  85            p4Type = p4Type[0:-1]
  86    system("p4 reopen -t %s %s" % (p4Type, file))
  88def getP4OpenedType(file):
  90    # Returns the perforce file type for the given file.
  91    result = read_pipe("p4 opened %s" % file)
  93    match = re.match(".*\((.+)\)$", result)
  94    if match:
  95        return match.group(1)
  96    else:
  97        die("Could not determine file type for %s" % file)
  98def diffTreePattern():
 100    # This is a simple generator for the diff tree regex pattern. This could be
 101    # a class variable if this and parseDiffTreeEntry were a part of a class.
 102    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 103    while True:
 104        yield pattern
 105def parseDiffTreeEntry(entry):
 107    """Parses a single diff tree entry into its component elements.
 108    See git-diff-tree(1) manpage for details about the format of the diff
 110    output. This method returns a dictionary with the following elements:
 111    src_mode - The mode of the source file
 113    dst_mode - The mode of the destination file
 114    src_sha1 - The sha1 for the source file
 115    dst_sha1 - The sha1 fr the destination file
 116    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 117    status_score - The score for the status (applicable for 'C' and 'R'
 118                   statuses). This is None if there is no score.
 119    src - The path for the source file.
 120    dst - The path for the destination file. This is only present for
 121          copy or renames. If it is not present, this is None.
 122    If the pattern is not matched, None is returned."""
 124    match = diffTreePattern().next().match(entry)
 126    if match:
 127        return {
 128            'src_mode': match.group(1),
 129            'dst_mode': match.group(2),
 130            'src_sha1': match.group(3),
 131            'dst_sha1': match.group(4),
 132            'status': match.group(5),
 133            'status_score': match.group(6),
 134            'src': match.group(7),
 135            'dst': match.group(10)
 136        }
 137    return None
 138def isModeExec(mode):
 140    # Returns True if the given git mode represents an executable file,
 141    # otherwise False.
 142    return mode[-3:] == "755"
 143def isModeExecChanged(src_mode, dst_mode):
 145    return isModeExec(src_mode) != isModeExec(dst_mode)
 146def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
 148    cmd = "p4 -G %s" % cmd
 149    if verbose:
 150        sys.stderr.write("Opening pipe: %s\n" % cmd)
 151    # Use a temporary file to avoid deadlocks without
 153    # subprocess.communicate(), which would put another copy
 154    # of stdout into memory.
 155    stdin_file = None
 156    if stdin is not None:
 157        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 158        stdin_file.write(stdin)
 159        stdin_file.flush()
 160        stdin_file.seek(0)
 161    p4 = subprocess.Popen(cmd, shell=True,
 163                          stdin=stdin_file,
 164                          stdout=subprocess.PIPE)
 165    result = []
 167    try:
 168        while True:
 169            entry = marshal.load(p4.stdout)
 170            result.append(entry)
 171    except EOFError:
 172        pass
 173    exitCode = p4.wait()
 174    if exitCode != 0:
 175        entry = {}
 176        entry["p4ExitCode"] = exitCode
 177        result.append(entry)
 178    return result
 180def p4Cmd(cmd):
 182    list = p4CmdList(cmd)
 183    result = {}
 184    for entry in list:
 185        result.update(entry)
 186    return result;
 187def p4Where(depotPath):
 189    if not depotPath.endswith("/"):
 190        depotPath += "/"
 191    output = p4Cmd("where %s..." % depotPath)
 192    if output["code"] == "error":
 193        return ""
 194    clientPath = ""
 195    if "path" in output:
 196        clientPath = output.get("path")
 197    elif "data" in output:
 198        data = output.get("data")
 199        lastSpace = data.rfind(" ")
 200        clientPath = data[lastSpace + 1:]
 201    if clientPath.endswith("..."):
 203        clientPath = clientPath[:-3]
 204    return clientPath
 205def currentGitBranch():
 207    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 208def isValidGitDir(path):
 210    if (os.path.exists(path + "/HEAD")
 211        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 212        return True;
 213    return False
 214def parseRevision(ref):
 216    return read_pipe("git rev-parse %s" % ref).strip()
 217def extractLogMessageFromGitCommit(commit):
 219    logMessage = ""
 220    ## fixme: title is first line of commit, not 1st paragraph.
 222    foundTitle = False
 223    for log in read_pipe_lines("git cat-file commit %s" % commit):
 224       if not foundTitle:
 225           if len(log) == 1:
 226               foundTitle = True
 227           continue
 228       logMessage += log
 230    return logMessage
 231def extractSettingsGitLog(log):
 233    values = {}
 234    for line in log.split("\n"):
 235        line = line.strip()
 236        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 237        if not m:
 238            continue
 239        assignments = m.group(1).split (':')
 241        for a in assignments:
 242            vals = a.split ('=')
 243            key = vals[0].strip()
 244            val = ('='.join (vals[1:])).strip()
 245            if val.endswith ('\"') and val.startswith('"'):
 246                val = val[1:-1]
 247            values[key] = val
 249    paths = values.get("depot-paths")
 251    if not paths:
 252        paths = values.get("depot-path")
 253    if paths:
 254        values['depot-paths'] = paths.split(',')
 255    return values
 256def gitBranchExists(branch):
 258    proc = subprocess.Popen(["git", "rev-parse", branch],
 259                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 260    return proc.wait() == 0;
 261def gitConfig(key):
 263    return read_pipe("git config %s" % key, ignore_error=True).strip()
 264def p4BranchesInGit(branchesAreInRemotes = True):
 266    branches = {}
 267    cmdline = "git rev-parse --symbolic "
 269    if branchesAreInRemotes:
 270        cmdline += " --remotes"
 271    else:
 272        cmdline += " --branches"
 273    for line in read_pipe_lines(cmdline):
 275        line = line.strip()
 276        ## only import to p4/
 278        if not line.startswith('p4/') or line == "p4/HEAD":
 279            continue
 280        branch = line
 281        # strip off p4
 283        branch = re.sub ("^p4/", "", line)
 284        branches[branch] = parseRevision(line)
 286    return branches
 287def findUpstreamBranchPoint(head = "HEAD"):
 289    branches = p4BranchesInGit()
 290    # map from depot-path to branch name
 291    branchByDepotPath = {}
 292    for branch in branches.keys():
 293        tip = branches[branch]
 294        log = extractLogMessageFromGitCommit(tip)
 295        settings = extractSettingsGitLog(log)
 296        if settings.has_key("depot-paths"):
 297            paths = ",".join(settings["depot-paths"])
 298            branchByDepotPath[paths] = "remotes/p4/" + branch
 299    settings = None
 301    parent = 0
 302    while parent < 65535:
 303        commit = head + "~%s" % parent
 304        log = extractLogMessageFromGitCommit(commit)
 305        settings = extractSettingsGitLog(log)
 306        if settings.has_key("depot-paths"):
 307            paths = ",".join(settings["depot-paths"])
 308            if branchByDepotPath.has_key(paths):
 309                return [branchByDepotPath[paths], settings]
 310        parent = parent + 1
 312    return ["", settings]
 314def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 316    if not silent:
 317        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 318               % localRefPrefix)
 319    originPrefix = "origin/p4/"
 321    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 323        line = line.strip()
 324        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 325            continue
 326        headName = line[len(originPrefix):]
 328        remoteHead = localRefPrefix + headName
 329        originHead = line
 330        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 332        if (not original.has_key('depot-paths')
 333            or not original.has_key('change')):
 334            continue
 335        update = False
 337        if not gitBranchExists(remoteHead):
 338            if verbose:
 339                print "creating %s" % remoteHead
 340            update = True
 341        else:
 342            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 343            if settings.has_key('change') > 0:
 344                if settings['depot-paths'] == original['depot-paths']:
 345                    originP4Change = int(original['change'])
 346                    p4Change = int(settings['change'])
 347                    if originP4Change > p4Change:
 348                        print ("%s (%s) is newer than %s (%s). "
 349                               "Updating p4 branch from origin."
 350                               % (originHead, originP4Change,
 351                                  remoteHead, p4Change))
 352                        update = True
 353                else:
 354                    print ("Ignoring: %s was imported from %s while "
 355                           "%s was imported from %s"
 356                           % (originHead, ','.join(original['depot-paths']),
 357                              remoteHead, ','.join(settings['depot-paths'])))
 358        if update:
 360            system("git update-ref %s %s" % (remoteHead, originHead))
 361def originP4BranchesExist():
 363        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 364def p4ChangesForPaths(depotPaths, changeRange):
 366    assert depotPaths
 367    output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange)
 368                                                        for p in depotPaths]))
 369    changes = []
 371    for line in output:
 372        changeNum = line.split(" ")[1]
 373        changes.append(int(changeNum))
 374    changes.sort()
 376    return changes
 377class Command:
 379    def __init__(self):
 380        self.usage = "usage: %prog [options]"
 381        self.needsGit = True
 382class P4Debug(Command):
 384    def __init__(self):
 385        Command.__init__(self)
 386        self.options = [
 387            optparse.make_option("--verbose", dest="verbose", action="store_true",
 388                                 default=False),
 389            ]
 390        self.description = "A tool to debug the output of p4 -G."
 391        self.needsGit = False
 392        self.verbose = False
 393    def run(self, args):
 395        j = 0
 396        for output in p4CmdList(" ".join(args)):
 397            print 'Element: %d' % j
 398            j += 1
 399            print output
 400        return True
 401class P4RollBack(Command):
 403    def __init__(self):
 404        Command.__init__(self)
 405        self.options = [
 406            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 407            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 408        ]
 409        self.description = "A tool to debug the multi-branch import. Don't use :)"
 410        self.verbose = False
 411        self.rollbackLocalBranches = False
 412    def run(self, args):
 414        if len(args) != 1:
 415            return False
 416        maxChange = int(args[0])
 417        if "p4ExitCode" in p4Cmd("changes -m 1"):
 419            die("Problems executing p4");
 420        if self.rollbackLocalBranches:
 422            refPrefix = "refs/heads/"
 423            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 424        else:
 425            refPrefix = "refs/remotes/"
 426            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 427        for line in lines:
 429            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 430                line = line.strip()
 431                ref = refPrefix + line
 432                log = extractLogMessageFromGitCommit(ref)
 433                settings = extractSettingsGitLog(log)
 434                depotPaths = settings['depot-paths']
 436                change = settings['change']
 437                changed = False
 439                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 441                                                           for p in depotPaths]))) == 0:
 442                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 443                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 444                    continue
 445                while change and int(change) > maxChange:
 447                    changed = True
 448                    if self.verbose:
 449                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 450                    system("git update-ref %s \"%s^\"" % (ref, ref))
 451                    log = extractLogMessageFromGitCommit(ref)
 452                    settings =  extractSettingsGitLog(log)
 453                    depotPaths = settings['depot-paths']
 456                    change = settings['change']
 457                if changed:
 459                    print "%s rewound to %s" % (ref, change)
 460        return True
 462class P4Submit(Command):
 464    def __init__(self):
 465        Command.__init__(self)
 466        self.options = [
 467                optparse.make_option("--continue", action="store_false", dest="firstTime"),
 468                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 469                optparse.make_option("--origin", dest="origin"),
 470                optparse.make_option("--reset", action="store_true", dest="reset"),
 471                optparse.make_option("--log-substitutions", dest="substFile"),
 472                optparse.make_option("--dry-run", action="store_true"),
 473                optparse.make_option("--direct", dest="directSubmit", action="store_true"),
 474                optparse.make_option("--trust-me-like-a-fool", dest="trustMeLikeAFool", action="store_true"),
 475                optparse.make_option("-M", dest="detectRename", action="store_true"),
 476        ]
 477        self.description = "Submit changes from git to the perforce depot."
 478        self.usage += " [name of git branch to submit into perforce depot]"
 479        self.firstTime = True
 480        self.reset = False
 481        self.interactive = True
 482        self.dryRun = False
 483        self.substFile = ""
 484        self.firstTime = True
 485        self.origin = ""
 486        self.directSubmit = False
 487        self.trustMeLikeAFool = False
 488        self.detectRename = False
 489        self.verbose = False
 490        self.isWindows = (platform.system() == "Windows")
 491        self.logSubstitutions = {}
 493        self.logSubstitutions["<enter description here>"] = "%log%"
 494        self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
 495    def check(self):
 497        if len(p4CmdList("opened ...")) > 0:
 498            die("You have files opened with perforce! Close them before starting the sync.")
 499    def start(self):
 501        if len(self.config) > 0 and not self.reset:
 502            die("Cannot start sync. Previous sync config found at %s\n"
 503                "If you want to start submitting again from scratch "
 504                "maybe you want to call git-p4 submit --reset" % self.configFile)
 505        commits = []
 507        if self.directSubmit:
 508            commits.append("0")
 509        else:
 510            for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
 511                commits.append(line.strip())
 512            commits.reverse()
 513        self.config["commits"] = commits
 515    def prepareLogMessage(self, template, message):
 517        result = ""
 518        for line in template.split("\n"):
 520            if line.startswith("#"):
 521                result += line + "\n"
 522                continue
 523            substituted = False
 525            for key in self.logSubstitutions.keys():
 526                if line.find(key) != -1:
 527                    value = self.logSubstitutions[key]
 528                    value = value.replace("%log%", message)
 529                    if value != "@remove@":
 530                        result += line.replace(key, value) + "\n"
 531                    substituted = True
 532                    break
 533            if not substituted:
 535                result += line + "\n"
 536        return result
 538    def prepareSubmitTemplate(self):
 540        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 541        template = ""
 542        inFilesSection = False
 543        for line in read_pipe_lines("p4 change -o"):
 544            if inFilesSection:
 545                if line.startswith("\t"):
 546                    # path starts and ends with a tab
 547                    path = line[1:]
 548                    lastTab = path.rfind("\t")
 549                    if lastTab != -1:
 550                        path = path[:lastTab]
 551                        if not path.startswith(self.depotPath):
 552                            continue
 553                else:
 554                    inFilesSection = False
 555            else:
 556                if line.startswith("Files:"):
 557                    inFilesSection = True
 558            template += line
 560        return template
 562    def applyCommit(self, id):
 564        if self.directSubmit:
 565            print "Applying local change in working directory/index"
 566            diff = self.diffStatus
 567        else:
 568            print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 569            diffOpts = ("", "-M")[self.detectRename]
 570            diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 571        filesToAdd = set()
 572        filesToDelete = set()
 573        editedFiles = set()
 574        filesToChangeExecBit = {}
 575        for line in diff:
 576            diff = parseDiffTreeEntry(line)
 577            modifier = diff['status']
 578            path = diff['src']
 579            if modifier == "M":
 580                system("p4 edit \"%s\"" % path)
 581                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 582                    filesToChangeExecBit[path] = diff['dst_mode']
 583                editedFiles.add(path)
 584            elif modifier == "A":
 585                filesToAdd.add(path)
 586                filesToChangeExecBit[path] = diff['dst_mode']
 587                if path in filesToDelete:
 588                    filesToDelete.remove(path)
 589            elif modifier == "D":
 590                filesToDelete.add(path)
 591                if path in filesToAdd:
 592                    filesToAdd.remove(path)
 593            elif modifier == "R":
 594                src, dest = diff['src'], diff['dst']
 595                system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
 596                system("p4 edit \"%s\"" % (dest))
 597                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 598                    filesToChangeExecBit[dest] = diff['dst_mode']
 599                os.unlink(dest)
 600                editedFiles.add(dest)
 601                filesToDelete.add(src)
 602            else:
 603                die("unknown modifier %s for %s" % (modifier, path))
 604        if self.directSubmit:
 606            diffcmd = "cat \"%s\"" % self.diffFile
 607        else:
 608            diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 609        patchcmd = diffcmd + " | git apply "
 610        tryPatchCmd = patchcmd + "--check -"
 611        applyPatchCmd = patchcmd + "--check --apply -"
 612        if os.system(tryPatchCmd) != 0:
 614            print "Unfortunately applying the change failed!"
 615            print "What do you want to do?"
 616            response = "x"
 617            while response != "s" and response != "a" and response != "w":
 618                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 619                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 620            if response == "s":
 621                print "Skipping! Good luck with the next patches..."
 622                for f in editedFiles:
 623                    system("p4 revert \"%s\"" % f);
 624                for f in filesToAdd:
 625                    system("rm %s" %f)
 626                return
 627            elif response == "a":
 628                os.system(applyPatchCmd)
 629                if len(filesToAdd) > 0:
 630                    print "You may also want to call p4 add on the following files:"
 631                    print " ".join(filesToAdd)
 632                if len(filesToDelete):
 633                    print "The following files should be scheduled for deletion with p4 delete:"
 634                    print " ".join(filesToDelete)
 635                die("Please resolve and submit the conflict manually and "
 636                    + "continue afterwards with git-p4 submit --continue")
 637            elif response == "w":
 638                system(diffcmd + " > patch.txt")
 639                print "Patch saved to patch.txt in %s !" % self.clientPath
 640                die("Please resolve and submit the conflict manually and "
 641                    "continue afterwards with git-p4 submit --continue")
 642        system(applyPatchCmd)
 644        for f in filesToAdd:
 646            system("p4 add \"%s\"" % f)
 647        for f in filesToDelete:
 648            system("p4 revert \"%s\"" % f)
 649            system("p4 delete \"%s\"" % f)
 650        # Set/clear executable bits
 652        for f in filesToChangeExecBit.keys():
 653            mode = filesToChangeExecBit[f]
 654            setP4ExecBit(f, mode)
 655        logMessage = ""
 657        if not self.directSubmit:
 658            logMessage = extractLogMessageFromGitCommit(id)
 659            logMessage = logMessage.replace("\n", "\n\t")
 660            if self.isWindows:
 661                logMessage = logMessage.replace("\n", "\r\n")
 662            logMessage = logMessage.strip()
 663        template = self.prepareSubmitTemplate()
 665        if self.interactive:
 667            submitTemplate = self.prepareLogMessage(template, logMessage)
 668            diff = read_pipe("p4 diff -du ...")
 669            for newFile in filesToAdd:
 671                diff += "==== new file ====\n"
 672                diff += "--- /dev/null\n"
 673                diff += "+++ %s\n" % newFile
 674                f = open(newFile, "r")
 675                for line in f.readlines():
 676                    diff += "+" + line
 677                f.close()
 678            separatorLine = "######## everything below this line is just the diff #######"
 680            if platform.system() == "Windows":
 681                separatorLine += "\r"
 682            separatorLine += "\n"
 683            response = "e"
 685            if self.trustMeLikeAFool:
 686                response = "y"
 687            firstIteration = True
 689            while response == "e":
 690                if not firstIteration:
 691                    response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ")
 692                firstIteration = False
 693                if response == "e":
 694                    [handle, fileName] = tempfile.mkstemp()
 695                    tmpFile = os.fdopen(handle, "w+")
 696                    tmpFile.write(submitTemplate + separatorLine + diff)
 697                    tmpFile.close()
 698                    defaultEditor = "vi"
 699                    if platform.system() == "Windows":
 700                        defaultEditor = "notepad"
 701                    editor = os.environ.get("EDITOR", defaultEditor);
 702                    system(editor + " " + fileName)
 703                    tmpFile = open(fileName, "rb")
 704                    message = tmpFile.read()
 705                    tmpFile.close()
 706                    os.remove(fileName)
 707                    submitTemplate = message[:message.index(separatorLine)]
 708                    if self.isWindows:
 709                        submitTemplate = submitTemplate.replace("\r\n", "\n")
 710            if response == "y" or response == "yes":
 712               if self.dryRun:
 713                   print submitTemplate
 714                   raw_input("Press return to continue...")
 715               else:
 716                   if self.directSubmit:
 717                       print "Submitting to git first"
 718                       os.chdir(self.oldWorkingDirectory)
 719                       write_pipe("git commit -a -F -", submitTemplate)
 720                       os.chdir(self.clientPath)
 721                   write_pipe("p4 submit -i", submitTemplate)
 723            elif response == "s":
 724                for f in editedFiles:
 725                    system("p4 revert \"%s\"" % f);
 726                for f in filesToAdd:
 727                    system("p4 revert \"%s\"" % f);
 728                    system("rm %s" %f)
 729                for f in filesToDelete:
 730                    system("p4 delete \"%s\"" % f);
 731                return
 732            else:
 733                print "Not submitting!"
 734                self.interactive = False
 735        else:
 736            fileName = "submit.txt"
 737            file = open(fileName, "w+")
 738            file.write(self.prepareLogMessage(template, logMessage))
 739            file.close()
 740            print ("Perforce submit template written as %s. "
 741                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 742                   % (fileName, fileName))
 743    def run(self, args):
 745        if len(args) == 0:
 746            self.master = currentGitBranch()
 747            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 748                die("Detecting current git branch failed!")
 749        elif len(args) == 1:
 750            self.master = args[0]
 751        else:
 752            return False
 753        [upstream, settings] = findUpstreamBranchPoint()
 755        self.depotPath = settings['depot-paths'][0]
 756        if len(self.origin) == 0:
 757            self.origin = upstream
 758        if self.verbose:
 760            print "Origin branch is " + self.origin
 761        if len(self.depotPath) == 0:
 763            print "Internal error: cannot locate perforce depot path from existing branches"
 764            sys.exit(128)
 765        self.clientPath = p4Where(self.depotPath)
 767        if len(self.clientPath) == 0:
 769            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
 770            sys.exit(128)
 771        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
 773        self.oldWorkingDirectory = os.getcwd()
 774        if self.directSubmit:
 776            self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD")
 777            if len(self.diffStatus) == 0:
 778                print "No changes in working directory to submit."
 779                return True
 780            patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD")
 781            self.diffFile = self.gitdir + "/p4-git-diff"
 782            f = open(self.diffFile, "wb")
 783            f.write(patch)
 784            f.close();
 785        os.chdir(self.clientPath)
 787        print "Syncronizing p4 checkout..."
 788        system("p4 sync ...")
 789        if self.reset:
 791            self.firstTime = True
 792        if len(self.substFile) > 0:
 794            for line in open(self.substFile, "r").readlines():
 795                tokens = line.strip().split("=")
 796                self.logSubstitutions[tokens[0]] = tokens[1]
 797        self.check()
 799        self.configFile = self.gitdir + "/p4-git-sync.cfg"
 800        self.config = shelve.open(self.configFile, writeback=True)
 801        if self.firstTime:
 803            self.start()
 804        commits = self.config.get("commits", [])
 806        while len(commits) > 0:
 808            self.firstTime = False
 809            commit = commits[0]
 810            commits = commits[1:]
 811            self.config["commits"] = commits
 812            self.applyCommit(commit)
 813            if not self.interactive:
 814                break
 815        self.config.close()
 817        if self.directSubmit:
 819            os.remove(self.diffFile)
 820        if len(commits) == 0:
 822            if self.firstTime:
 823                print "No changes found to apply between %s and current HEAD" % self.origin
 824            else:
 825                print "All changes applied!"
 826                os.chdir(self.oldWorkingDirectory)
 827                sync = P4Sync()
 829                sync.run([])
 830                response = raw_input("Do you want to rebase current HEAD from Perforce now using git-p4 rebase? [y]es/[n]o ")
 832                if response == "y" or response == "yes":
 833                    rebase = P4Rebase()
 834                    rebase.rebase()
 835            os.remove(self.configFile)
 836        return True
 838class P4Sync(Command):
 840    def __init__(self):
 841        Command.__init__(self)
 842        self.options = [
 843                optparse.make_option("--branch", dest="branch"),
 844                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 845                optparse.make_option("--changesfile", dest="changesFile"),
 846                optparse.make_option("--silent", dest="silent", action="store_true"),
 847                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
 848                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 849                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
 850                                     help="Import into refs/heads/ , not refs/remotes"),
 851                optparse.make_option("--max-changes", dest="maxChanges"),
 852                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
 853                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
 854        ]
 855        self.description = """Imports from Perforce into a git repository.\n
 856    example:
 857    //depot/my/project/ -- to import the current head
 858    //depot/my/project/@all -- to import everything
 859    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 860    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 862        self.usage += " //depot/path[@revRange]"
 864        self.silent = False
 865        self.createdBranches = Set()
 866        self.committedChanges = Set()
 867        self.branch = ""
 868        self.detectBranches = False
 869        self.detectLabels = False
 870        self.changesFile = ""
 871        self.syncWithOrigin = True
 872        self.verbose = False
 873        self.importIntoRemotes = True
 874        self.maxChanges = ""
 875        self.isWindows = (platform.system() == "Windows")
 876        self.keepRepoPath = False
 877        self.depotPaths = None
 878        self.p4BranchesInGit = []
 879        if gitConfig("git-p4.syncFromOrigin") == "false":
 881            self.syncWithOrigin = False
 882    def extractFilesFromCommit(self, commit):
 884        files = []
 885        fnum = 0
 886        while commit.has_key("depotFile%s" % fnum):
 887            path =  commit["depotFile%s" % fnum]
 888            found = [p for p in self.depotPaths
 890                     if path.startswith (p)]
 891            if not found:
 892                fnum = fnum + 1
 893                continue
 894            file = {}
 896            file["path"] = path
 897            file["rev"] = commit["rev%s" % fnum]
 898            file["action"] = commit["action%s" % fnum]
 899            file["type"] = commit["type%s" % fnum]
 900            files.append(file)
 901            fnum = fnum + 1
 902        return files
 903    def stripRepoPath(self, path, prefixes):
 905        if self.keepRepoPath:
 906            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
 907        for p in prefixes:
 909            if path.startswith(p):
 910                path = path[len(p):]
 911        return path
 913    def splitFilesIntoBranches(self, commit):
 915        branches = {}
 916        fnum = 0
 917        while commit.has_key("depotFile%s" % fnum):
 918            path =  commit["depotFile%s" % fnum]
 919            found = [p for p in self.depotPaths
 920                     if path.startswith (p)]
 921            if not found:
 922                fnum = fnum + 1
 923                continue
 924            file = {}
 926            file["path"] = path
 927            file["rev"] = commit["rev%s" % fnum]
 928            file["action"] = commit["action%s" % fnum]
 929            file["type"] = commit["type%s" % fnum]
 930            fnum = fnum + 1
 931            relPath = self.stripRepoPath(path, self.depotPaths)
 933            for branch in self.knownBranches.keys():
 935                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
 937                if relPath.startswith(branch + "/"):
 938                    if branch not in branches:
 939                        branches[branch] = []
 940                    branches[branch].append(file)
 941                    break
 942        return branches
 944    ## Should move this out, doesn't use SELF.
 946    def readP4Files(self, files):
 947        files = [f for f in files
 948                 if f['action'] != 'delete']
 949        if not files:
 951            return
 952        filedata = p4CmdList('-x - print',
 954                             stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
 955                                              for f in files]),
 956                             stdin_mode='w+')
 957        if "p4ExitCode" in filedata[0]:
 958            die("Problems executing p4. Error: [%d]."
 959                % (filedata[0]['p4ExitCode']));
 960        j = 0;
 962        contents = {}
 963        while j < len(filedata):
 964            stat = filedata[j]
 965            j += 1
 966            text = ''
 967            while j < len(filedata) and filedata[j]['code'] in ('text',
 968                                                                'binary'):
 969                text += filedata[j]['data']
 970                j += 1
 971            if not stat.has_key('depotFile'):
 974                sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
 975                continue
 976            contents[stat['depotFile']] = text
 978        for f in files:
 980            assert not f.has_key('data')
 981            f['data'] = contents[f['path']]
 982    def commit(self, details, files, branch, branchPrefixes, parent = ""):
 984        epoch = details["time"]
 985        author = details["user"]
 986        if self.verbose:
 988            print "commit into %s" % branch
 989        # start with reading files; if that fails, we should not
 991        # create a commit.
 992        new_files = []
 993        for f in files:
 994            if [p for p in branchPrefixes if f['path'].startswith(p)]:
 995                new_files.append (f)
 996            else:
 997                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
 998        files = new_files
 999        self.readP4Files(files)
1000        self.gitStream.write("commit %s\n" % branch)
1005#        gitStream.write("mark :%s\n" % details["change"])
1006        self.committedChanges.add(int(details["change"]))
1007        committer = ""
1008        if author not in self.users:
1009            self.getUserMapFromPerforceServer()
1010        if author in self.users:
1011            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1012        else:
1013            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1014        self.gitStream.write("committer %s\n" % committer)
1016        self.gitStream.write("data <<EOT\n")
1018        self.gitStream.write(details["desc"])
1019        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1020                             % (','.join (branchPrefixes), details["change"]))
1021        if len(details['options']) > 0:
1022            self.gitStream.write(": options = %s" % details['options'])
1023        self.gitStream.write("]\nEOT\n\n")
1024        if len(parent) > 0:
1026            if self.verbose:
1027                print "parent %s" % parent
1028            self.gitStream.write("from %s\n" % parent)
1029        for file in files:
1031            if file["type"] == "apple":
1032                print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1033                continue
1034            relPath = self.stripRepoPath(file['path'], branchPrefixes)
1036            if file["action"] == "delete":
1037                self.gitStream.write("D %s\n" % relPath)
1038            else:
1039                data = file['data']
1040                mode = "644"
1042                if isP4Exec(file["type"]):
1043                    mode = "755"
1044                elif file["type"] == "symlink":
1045                    mode = "120000"
1046                    # p4 print on a symlink contains "target\n", so strip it off
1047                    data = data[:-1]
1048                if self.isWindows and file["type"].endswith("text"):
1050                    data = data.replace("\r\n", "\n")
1051                self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1053                self.gitStream.write("data %s\n" % len(data))
1054                self.gitStream.write(data)
1055                self.gitStream.write("\n")
1056        self.gitStream.write("\n")
1058        change = int(details["change"])
1060        if self.labels.has_key(change):
1062            label = self.labels[change]
1063            labelDetails = label[0]
1064            labelRevisions = label[1]
1065            if self.verbose:
1066                print "Change %s is labelled %s" % (change, labelDetails)
1067            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1069                                                    for p in branchPrefixes]))
1070            if len(files) == len(labelRevisions):
1072                cleanedFiles = {}
1074                for info in files:
1075                    if info["action"] == "delete":
1076                        continue
1077                    cleanedFiles[info["depotFile"]] = info["rev"]
1078                if cleanedFiles == labelRevisions:
1080                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1081                    self.gitStream.write("from %s\n" % branch)
1082                    owner = labelDetails["Owner"]
1084                    tagger = ""
1085                    if author in self.users:
1086                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1087                    else:
1088                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1089                    self.gitStream.write("tagger %s\n" % tagger)
1090                    self.gitStream.write("data <<EOT\n")
1091                    self.gitStream.write(labelDetails["Description"])
1092                    self.gitStream.write("EOT\n\n")
1093                else:
1095                    if not self.silent:
1096                        print ("Tag %s does not match with change %s: files do not match."
1097                               % (labelDetails["label"], change))
1098            else:
1100                if not self.silent:
1101                    print ("Tag %s does not match with change %s: file count is different."
1102                           % (labelDetails["label"], change))
1103    def getUserCacheFilename(self):
1105        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1106        return home + "/.gitp4-usercache.txt"
1107    def getUserMapFromPerforceServer(self):
1109        if self.userMapFromPerforceServer:
1110            return
1111        self.users = {}
1112        for output in p4CmdList("users"):
1114            if not output.has_key("User"):
1115                continue
1116            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1117        s = ''
1120        for (key, val) in self.users.items():
1121            s += "%s\t%s\n" % (key, val)
1122        open(self.getUserCacheFilename(), "wb").write(s)
1124        self.userMapFromPerforceServer = True
1125    def loadUserMapFromCache(self):
1127        self.users = {}
1128        self.userMapFromPerforceServer = False
1129        try:
1130            cache = open(self.getUserCacheFilename(), "rb")
1131            lines = cache.readlines()
1132            cache.close()
1133            for line in lines:
1134                entry = line.strip().split("\t")
1135                self.users[entry[0]] = entry[1]
1136        except IOError:
1137            self.getUserMapFromPerforceServer()
1138    def getLabels(self):
1140        self.labels = {}
1141        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1143        if len(l) > 0 and not self.silent:
1144            print "Finding files belonging to labels in %s" % `self.depotPaths`
1145        for output in l:
1147            label = output["label"]
1148            revisions = {}
1149            newestChange = 0
1150            if self.verbose:
1151                print "Querying files for label %s" % label
1152            for file in p4CmdList("files "
1153                                  +  ' '.join (["%s...@%s" % (p, label)
1154                                                for p in self.depotPaths])):
1155                revisions[file["depotFile"]] = file["rev"]
1156                change = int(file["change"])
1157                if change > newestChange:
1158                    newestChange = change
1159            self.labels[newestChange] = [output, revisions]
1161        if self.verbose:
1163            print "Label changes: %s" % self.labels.keys()
1164    def guessProjectName(self):
1166        for p in self.depotPaths:
1167            if p.endswith("/"):
1168                p = p[:-1]
1169            p = p[p.strip().rfind("/") + 1:]
1170            if not p.endswith("/"):
1171               p += "/"
1172            return p
1173    def getBranchMapping(self):
1175        lostAndFoundBranches = set()
1176        for info in p4CmdList("branches"):
1178            details = p4Cmd("branch -o %s" % info["branch"])
1179            viewIdx = 0
1180            while details.has_key("View%s" % viewIdx):
1181                paths = details["View%s" % viewIdx].split(" ")
1182                viewIdx = viewIdx + 1
1183                # require standard //depot/foo/... //depot/bar/... mapping
1184                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1185                    continue
1186                source = paths[0]
1187                destination = paths[1]
1188                ## HACK
1189                if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1190                    source = source[len(self.depotPaths[0]):-4]
1191                    destination = destination[len(self.depotPaths[0]):-4]
1192                    if destination in self.knownBranches:
1194                        if not self.silent:
1195                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1196                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1197                        continue
1198                    self.knownBranches[destination] = source
1200                    lostAndFoundBranches.discard(destination)
1202                    if source not in self.knownBranches:
1204                        lostAndFoundBranches.add(source)
1205        for branch in lostAndFoundBranches:
1208            self.knownBranches[branch] = branch
1209    def getBranchMappingFromGitBranches(self):
1211        branches = p4BranchesInGit(self.importIntoRemotes)
1212        for branch in branches.keys():
1213            if branch == "master":
1214                branch = "main"
1215            else:
1216                branch = branch[len(self.projectName):]
1217            self.knownBranches[branch] = branch
1218    def listExistingP4GitBranches(self):
1220        # branches holds mapping from name to commit
1221        branches = p4BranchesInGit(self.importIntoRemotes)
1222        self.p4BranchesInGit = branches.keys()
1223        for branch in branches.keys():
1224            self.initialParents[self.refPrefix + branch] = branches[branch]
1225    def updateOptionDict(self, d):
1227        option_keys = {}
1228        if self.keepRepoPath:
1229            option_keys['keepRepoPath'] = 1
1230        d["options"] = ' '.join(sorted(option_keys.keys()))
1232    def readOptions(self, d):
1234        self.keepRepoPath = (d.has_key('options')
1235                             and ('keepRepoPath' in d['options']))
1236    def gitRefForBranch(self, branch):
1238        if branch == "main":
1239            return self.refPrefix + "master"
1240        if len(branch) <= 0:
1242            return branch
1243        return self.refPrefix + self.projectName + branch
1245    def gitCommitByP4Change(self, ref, change):
1247        if self.verbose:
1248            print "looking in ref " + ref + " for change %s using bisect..." % change
1249        earliestCommit = ""
1251        latestCommit = parseRevision(ref)
1252        while True:
1254            if self.verbose:
1255                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1256            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1257            if len(next) == 0:
1258                if self.verbose:
1259                    print "argh"
1260                return ""
1261            log = extractLogMessageFromGitCommit(next)
1262            settings = extractSettingsGitLog(log)
1263            currentChange = int(settings['change'])
1264            if self.verbose:
1265                print "current change %s" % currentChange
1266            if currentChange == change:
1268                if self.verbose:
1269                    print "found %s" % next
1270                return next
1271            if currentChange < change:
1273                earliestCommit = "^%s" % next
1274            else:
1275                latestCommit = "%s" % next
1276        return ""
1278    def importNewBranch(self, branch, maxChange):
1280        # make fast-import flush all changes to disk and update the refs using the checkpoint
1281        # command so that we can try to find the branch parent in the git history
1282        self.gitStream.write("checkpoint\n\n");
1283        self.gitStream.flush();
1284        branchPrefix = self.depotPaths[0] + branch + "/"
1285        range = "@1,%s" % maxChange
1286        #print "prefix" + branchPrefix
1287        changes = p4ChangesForPaths([branchPrefix], range)
1288        if len(changes) <= 0:
1289            return False
1290        firstChange = changes[0]
1291        #print "first change in branch: %s" % firstChange
1292        sourceBranch = self.knownBranches[branch]
1293        sourceDepotPath = self.depotPaths[0] + sourceBranch
1294        sourceRef = self.gitRefForBranch(sourceBranch)
1295        #print "source " + sourceBranch
1296        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1298        #print "branch parent: %s" % branchParentChange
1299        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1300        if len(gitParent) > 0:
1301            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1302            #print "parent git commit: %s" % gitParent
1303        self.importChanges(changes)
1305        return True
1306    def importChanges(self, changes):
1308        cnt = 1
1309        for change in changes:
1310            description = p4Cmd("describe %s" % change)
1311            self.updateOptionDict(description)
1312            if not self.silent:
1314                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1315                sys.stdout.flush()
1316            cnt = cnt + 1
1317            try:
1319                if self.detectBranches:
1320                    branches = self.splitFilesIntoBranches(description)
1321                    for branch in branches.keys():
1322                        ## HACK  --hwn
1323                        branchPrefix = self.depotPaths[0] + branch + "/"
1324                        parent = ""
1326                        filesForCommit = branches[branch]
1328                        if self.verbose:
1330                            print "branch is %s" % branch
1331                        self.updatedBranches.add(branch)
1333                        if branch not in self.createdBranches:
1335                            self.createdBranches.add(branch)
1336                            parent = self.knownBranches[branch]
1337                            if parent == branch:
1338                                parent = ""
1339                            else:
1340                                fullBranch = self.projectName + branch
1341                                if fullBranch not in self.p4BranchesInGit:
1342                                    if not self.silent:
1343                                        print("\n    Importing new branch %s" % fullBranch);
1344                                    if self.importNewBranch(branch, change - 1):
1345                                        parent = ""
1346                                        self.p4BranchesInGit.append(fullBranch)
1347                                    if not self.silent:
1348                                        print("\n    Resuming with change %s" % change);
1349                                if self.verbose:
1351                                    print "parent determined through known branches: %s" % parent
1352                        branch = self.gitRefForBranch(branch)
1354                        parent = self.gitRefForBranch(parent)
1355                        if self.verbose:
1357                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1358                        if len(parent) == 0 and branch in self.initialParents:
1360                            parent = self.initialParents[branch]
1361                            del self.initialParents[branch]
1362                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1364                else:
1365                    files = self.extractFilesFromCommit(description)
1366                    self.commit(description, files, self.branch, self.depotPaths,
1367                                self.initialParent)
1368                    self.initialParent = ""
1369            except IOError:
1370                print self.gitError.read()
1371                sys.exit(1)
1372    def importHeadRevision(self, revision):
1374        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1375        details = { "user" : "git perforce import user", "time" : int(time.time()) }
1377        details["desc"] = ("Initial import of %s from the state at revision %s"
1378                           % (' '.join(self.depotPaths), revision))
1379        details["change"] = revision
1380        newestRevision = 0
1381        fileCnt = 0
1383        for info in p4CmdList("files "
1384                              +  ' '.join(["%s...%s"
1385                                           % (p, revision)
1386                                           for p in self.depotPaths])):
1387            if info['code'] == 'error':
1389                sys.stderr.write("p4 returned an error: %s\n"
1390                                 % info['data'])
1391                sys.exit(1)
1392            change = int(info["change"])
1395            if change > newestRevision:
1396                newestRevision = change
1397            if info["action"] == "delete":
1399                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1400                #fileCnt = fileCnt + 1
1401                continue
1402            for prop in ["depotFile", "rev", "action", "type" ]:
1404                details["%s%s" % (prop, fileCnt)] = info[prop]
1405            fileCnt = fileCnt + 1
1407        details["change"] = newestRevision
1409        self.updateOptionDict(details)
1410        try:
1411            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1412        except IOError:
1413            print "IO error with git fast-import. Is your git version recent enough?"
1414            print self.gitError.read()
1415    def run(self, args):
1418        self.depotPaths = []
1419        self.changeRange = ""
1420        self.initialParent = ""
1421        self.previousDepotPaths = []
1422        # map from branch depot path to parent branch
1424        self.knownBranches = {}
1425        self.initialParents = {}
1426        self.hasOrigin = originP4BranchesExist()
1427        if not self.syncWithOrigin:
1428            self.hasOrigin = False
1429        if self.importIntoRemotes:
1431            self.refPrefix = "refs/remotes/p4/"
1432        else:
1433            self.refPrefix = "refs/heads/p4/"
1434        if self.syncWithOrigin and self.hasOrigin:
1436            if not self.silent:
1437                print "Syncing with origin first by calling git fetch origin"
1438            system("git fetch origin")
1439        if len(self.branch) == 0:
1441            self.branch = self.refPrefix + "master"
1442            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1443                system("git update-ref %s refs/heads/p4" % self.branch)
1444                system("git branch -D p4");
1445            # create it /after/ importing, when master exists
1446            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1447                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1448        # TODO: should always look at previous commits,
1450        # merge with previous imports, if possible.
1451        if args == []:
1452            if self.hasOrigin:
1453                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1454            self.listExistingP4GitBranches()
1455            if len(self.p4BranchesInGit) > 1:
1457                if not self.silent:
1458                    print "Importing from/into multiple branches"
1459                self.detectBranches = True
1460            if self.verbose:
1462                print "branches: %s" % self.p4BranchesInGit
1463            p4Change = 0
1465            for branch in self.p4BranchesInGit:
1466                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1467                settings = extractSettingsGitLog(logMsg)
1469                self.readOptions(settings)
1471                if (settings.has_key('depot-paths')
1472                    and settings.has_key ('change')):
1473                    change = int(settings['change']) + 1
1474                    p4Change = max(p4Change, change)
1475                    depotPaths = sorted(settings['depot-paths'])
1477                    if self.previousDepotPaths == []:
1478                        self.previousDepotPaths = depotPaths
1479                    else:
1480                        paths = []
1481                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1482                            for i in range(0, min(len(cur), len(prev))):
1483                                if cur[i] <> prev[i]:
1484                                    i = i - 1
1485                                    break
1486                            paths.append (cur[:i + 1])
1488                        self.previousDepotPaths = paths
1490            if p4Change > 0:
1492                self.depotPaths = sorted(self.previousDepotPaths)
1493                self.changeRange = "@%s,#head" % p4Change
1494                if not self.detectBranches:
1495                    self.initialParent = parseRevision(self.branch)
1496                if not self.silent and not self.detectBranches:
1497                    print "Performing incremental import into %s git branch" % self.branch
1498        if not self.branch.startswith("refs/"):
1500            self.branch = "refs/heads/" + self.branch
1501        if len(args) == 0 and self.depotPaths:
1503            if not self.silent:
1504                print "Depot paths: %s" % ' '.join(self.depotPaths)
1505        else:
1506            if self.depotPaths and self.depotPaths != args:
1507                print ("previous import used depot path %s and now %s was specified. "
1508                       "This doesn't work!" % (' '.join (self.depotPaths),
1509                                               ' '.join (args)))
1510                sys.exit(1)
1511            self.depotPaths = sorted(args)
1513        revision = ""
1515        self.users = {}
1516        newPaths = []
1518        for p in self.depotPaths:
1519            if p.find("@") != -1:
1520                atIdx = p.index("@")
1521                self.changeRange = p[atIdx:]
1522                if self.changeRange == "@all":
1523                    self.changeRange = ""
1524                elif ',' not in self.changeRange:
1525                    revision = self.changeRange
1526                    self.changeRange = ""
1527                p = p[:atIdx]
1528            elif p.find("#") != -1:
1529                hashIdx = p.index("#")
1530                revision = p[hashIdx:]
1531                p = p[:hashIdx]
1532            elif self.previousDepotPaths == []:
1533                revision = "#head"
1534            p = re.sub ("\.\.\.$", "", p)
1536            if not p.endswith("/"):
1537                p += "/"
1538            newPaths.append(p)
1540        self.depotPaths = newPaths
1542        self.loadUserMapFromCache()
1545        self.labels = {}
1546        if self.detectLabels:
1547            self.getLabels();
1548        if self.detectBranches:
1550            ## FIXME - what's a P4 projectName ?
1551            self.projectName = self.guessProjectName()
1552            if self.hasOrigin:
1554                self.getBranchMappingFromGitBranches()
1555            else:
1556                self.getBranchMapping()
1557            if self.verbose:
1558                print "p4-git branches: %s" % self.p4BranchesInGit
1559                print "initial parents: %s" % self.initialParents
1560            for b in self.p4BranchesInGit:
1561                if b != "master":
1562                    ## FIXME
1564                    b = b[len(self.projectName):]
1565                self.createdBranches.add(b)
1566        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1568        importProcess = subprocess.Popen(["git", "fast-import"],
1570                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1571                                         stderr=subprocess.PIPE);
1572        self.gitOutput = importProcess.stdout
1573        self.gitStream = importProcess.stdin
1574        self.gitError = importProcess.stderr
1575        if revision:
1577            self.importHeadRevision(revision)
1578        else:
1579            changes = []
1580            if len(self.changesFile) > 0:
1582                output = open(self.changesFile).readlines()
1583                changeSet = Set()
1584                for line in output:
1585                    changeSet.add(int(line))
1586                for change in changeSet:
1588                    changes.append(change)
1589                changes.sort()
1591            else:
1592                if self.verbose:
1593                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1594                                                              self.changeRange)
1595                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1596                if len(self.maxChanges) > 0:
1598                    changes = changes[:min(int(self.maxChanges), len(changes))]
1599            if len(changes) == 0:
1601                if not self.silent:
1602                    print "No changes to import!"
1603                return True
1604            if not self.silent and not self.detectBranches:
1606                print "Import destination: %s" % self.branch
1607            self.updatedBranches = set()
1609            self.importChanges(changes)
1611            if not self.silent:
1613                print ""
1614                if len(self.updatedBranches) > 0:
1615                    sys.stdout.write("Updated branches: ")
1616                    for b in self.updatedBranches:
1617                        sys.stdout.write("%s " % b)
1618                    sys.stdout.write("\n")
1619        self.gitStream.close()
1621        if importProcess.wait() != 0:
1622            die("fast-import failed: %s" % self.gitError.read())
1623        self.gitOutput.close()
1624        self.gitError.close()
1625        return True
1627class P4Rebase(Command):
1629    def __init__(self):
1630        Command.__init__(self)
1631        self.options = [ ]
1632        self.description = ("Fetches the latest revision from perforce and "
1633                            + "rebases the current work (branch) against it")
1634        self.verbose = False
1635    def run(self, args):
1637        sync = P4Sync()
1638        sync.run([])
1639        return self.rebase()
1641    def rebase(self):
1643        [upstream, settings] = findUpstreamBranchPoint()
1644        if len(upstream) == 0:
1645            die("Cannot find upstream branchpoint for rebase")
1646        # the branchpoint may be p4/foo~3, so strip off the parent
1648        upstream = re.sub("~[0-9]+$", "", upstream)
1649        print "Rebasing the current branch onto %s" % upstream
1651        oldHead = read_pipe("git rev-parse HEAD").strip()
1652        system("git rebase %s" % upstream)
1653        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1654        return True
1655class P4Clone(P4Sync):
1657    def __init__(self):
1658        P4Sync.__init__(self)
1659        self.description = "Creates a new git repository and imports from Perforce into it"
1660        self.usage = "usage: %prog [options] //depot/path[@revRange]"
1661        self.options.append(
1662            optparse.make_option("--destination", dest="cloneDestination",
1663                                 action='store', default=None,
1664                                 help="where to leave result of the clone"))
1665        self.cloneDestination = None
1666        self.needsGit = False
1667    def defaultDestination(self, args):
1669        ## TODO: use common prefix of args?
1670        depotPath = args[0]
1671        depotDir = re.sub("(@[^@]*)$", "", depotPath)
1672        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1673        depotDir = re.sub(r"\.\.\.$,", "", depotDir)
1674        depotDir = re.sub(r"/$", "", depotDir)
1675        return os.path.split(depotDir)[1]
1676    def run(self, args):
1678        if len(args) < 1:
1679            return False
1680        if self.keepRepoPath and not self.cloneDestination:
1682            sys.stderr.write("Must specify destination for --keep-path\n")
1683            sys.exit(1)
1684        depotPaths = args
1686        if not self.cloneDestination and len(depotPaths) > 1:
1688            self.cloneDestination = depotPaths[-1]
1689            depotPaths = depotPaths[:-1]
1690        for p in depotPaths:
1692            if not p.startswith("//"):
1693                return False
1694        if not self.cloneDestination:
1696            self.cloneDestination = self.defaultDestination(args)
1697        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1699        if not os.path.exists(self.cloneDestination):
1700            os.makedirs(self.cloneDestination)
1701        os.chdir(self.cloneDestination)
1702        system("git init")
1703        self.gitdir = os.getcwd() + "/.git"
1704        if not P4Sync.run(self, depotPaths):
1705            return False
1706        if self.branch != "master":
1707            if gitBranchExists("refs/remotes/p4/master"):
1708                system("git branch master refs/remotes/p4/master")
1709                system("git checkout -f")
1710            else:
1711                print "Could not detect main branch. No checkout/master branch created."
1712        return True
1714class P4Branches(Command):
1716    def __init__(self):
1717        Command.__init__(self)
1718        self.options = [ ]
1719        self.description = ("Shows the git branches that hold imports and their "
1720                            + "corresponding perforce depot paths")
1721        self.verbose = False
1722    def run(self, args):
1724        if originP4BranchesExist():
1725            createOrUpdateBranchesFromOrigin()
1726        cmdline = "git rev-parse --symbolic "
1728        cmdline += " --remotes"
1729        for line in read_pipe_lines(cmdline):
1731            line = line.strip()
1732            if not line.startswith('p4/') or line == "p4/HEAD":
1734                continue
1735            branch = line
1736            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1738            settings = extractSettingsGitLog(log)
1739            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1741        return True
1742class HelpFormatter(optparse.IndentedHelpFormatter):
1744    def __init__(self):
1745        optparse.IndentedHelpFormatter.__init__(self)
1746    def format_description(self, description):
1748        if description:
1749            return description + "\n"
1750        else:
1751            return ""
1752def printUsage(commands):
1754    print "usage: %s <command> [options]" % sys.argv[0]
1755    print ""
1756    print "valid commands: %s" % ", ".join(commands)
1757    print ""
1758    print "Try %s <command> --help for command specific help." % sys.argv[0]
1759    print ""
1760commands = {
1762    "debug" : P4Debug,
1763    "submit" : P4Submit,
1764    "commit" : P4Submit,
1765    "sync" : P4Sync,
1766    "rebase" : P4Rebase,
1767    "clone" : P4Clone,
1768    "rollback" : P4RollBack,
1769    "branches" : P4Branches
1770}
1771def main():
1774    if len(sys.argv[1:]) == 0:
1775        printUsage(commands.keys())
1776        sys.exit(2)
1777    cmd = ""
1779    cmdName = sys.argv[1]
1780    try:
1781        klass = commands[cmdName]
1782        cmd = klass()
1783    except KeyError:
1784        print "unknown command %s" % cmdName
1785        print ""
1786        printUsage(commands.keys())
1787        sys.exit(2)
1788    options = cmd.options
1790    cmd.gitdir = os.environ.get("GIT_DIR", None)
1791    args = sys.argv[2:]
1793    if len(options) > 0:
1795        options.append(optparse.make_option("--git-dir", dest="gitdir"))
1796        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1798                                       options,
1799                                       description = cmd.description,
1800                                       formatter = HelpFormatter())
1801        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1803    global verbose
1804    verbose = cmd.verbose
1805    if cmd.needsGit:
1806        if cmd.gitdir == None:
1807            cmd.gitdir = os.path.abspath(".git")
1808            if not isValidGitDir(cmd.gitdir):
1809                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1810                if os.path.exists(cmd.gitdir):
1811                    cdup = read_pipe("git rev-parse --show-cdup").strip()
1812                    if len(cdup) > 0:
1813                        os.chdir(cdup);
1814        if not isValidGitDir(cmd.gitdir):
1816            if isValidGitDir(cmd.gitdir + "/.git"):
1817                cmd.gitdir += "/.git"
1818            else:
1819                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1820        os.environ["GIT_DIR"] = cmd.gitdir
1822    if not cmd.run(args):
1824        parser.print_help()
1825if __name__ == '__main__':
1828    main()