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("--direct", dest="directSubmit", action="store_true"),
 473                optparse.make_option("-M", dest="detectRename", action="store_true"),
 474        ]
 475        self.description = "Submit changes from git to the perforce depot."
 476        self.usage += " [name of git branch to submit into perforce depot]"
 477        self.firstTime = True
 478        self.reset = False
 479        self.interactive = True
 480        self.substFile = ""
 481        self.firstTime = True
 482        self.origin = ""
 483        self.directSubmit = False
 484        self.detectRename = False
 485        self.verbose = False
 486        self.isWindows = (platform.system() == "Windows")
 487        self.logSubstitutions = {}
 489        self.logSubstitutions["<enter description here>"] = "%log%"
 490        self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
 491    def check(self):
 493        if len(p4CmdList("opened ...")) > 0:
 494            die("You have files opened with perforce! Close them before starting the sync.")
 495    def start(self):
 497        if len(self.config) > 0 and not self.reset:
 498            die("Cannot start sync. Previous sync config found at %s\n"
 499                "If you want to start submitting again from scratch "
 500                "maybe you want to call git-p4 submit --reset" % self.configFile)
 501        commits = []
 503        if self.directSubmit:
 504            commits.append("0")
 505        else:
 506            for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
 507                commits.append(line.strip())
 508            commits.reverse()
 509        self.config["commits"] = commits
 511    def prepareLogMessage(self, template, message):
 513        result = ""
 514        for line in template.split("\n"):
 516            if line.startswith("#"):
 517                result += line + "\n"
 518                continue
 519            substituted = False
 521            for key in self.logSubstitutions.keys():
 522                if line.find(key) != -1:
 523                    value = self.logSubstitutions[key]
 524                    value = value.replace("%log%", message)
 525                    if value != "@remove@":
 526                        result += line.replace(key, value) + "\n"
 527                    substituted = True
 528                    break
 529            if not substituted:
 531                result += line + "\n"
 532        return result
 534    def prepareSubmitTemplate(self):
 536        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 537        template = ""
 538        inFilesSection = False
 539        for line in read_pipe_lines("p4 change -o"):
 540            if inFilesSection:
 541                if line.startswith("\t"):
 542                    # path starts and ends with a tab
 543                    path = line[1:]
 544                    lastTab = path.rfind("\t")
 545                    if lastTab != -1:
 546                        path = path[:lastTab]
 547                        if not path.startswith(self.depotPath):
 548                            continue
 549                else:
 550                    inFilesSection = False
 551            else:
 552                if line.startswith("Files:"):
 553                    inFilesSection = True
 554            template += line
 556        return template
 558    def applyCommit(self, id):
 560        if self.directSubmit:
 561            print "Applying local change in working directory/index"
 562            diff = self.diffStatus
 563        else:
 564            print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 565            diffOpts = ("", "-M")[self.detectRename]
 566            diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 567        filesToAdd = set()
 568        filesToDelete = set()
 569        editedFiles = set()
 570        filesToChangeExecBit = {}
 571        for line in diff:
 572            diff = parseDiffTreeEntry(line)
 573            modifier = diff['status']
 574            path = diff['src']
 575            if modifier == "M":
 576                system("p4 edit \"%s\"" % path)
 577                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 578                    filesToChangeExecBit[path] = diff['dst_mode']
 579                editedFiles.add(path)
 580            elif modifier == "A":
 581                filesToAdd.add(path)
 582                filesToChangeExecBit[path] = diff['dst_mode']
 583                if path in filesToDelete:
 584                    filesToDelete.remove(path)
 585            elif modifier == "D":
 586                filesToDelete.add(path)
 587                if path in filesToAdd:
 588                    filesToAdd.remove(path)
 589            elif modifier == "R":
 590                src, dest = diff['src'], diff['dst']
 591                system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
 592                system("p4 edit \"%s\"" % (dest))
 593                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 594                    filesToChangeExecBit[dest] = diff['dst_mode']
 595                os.unlink(dest)
 596                editedFiles.add(dest)
 597                filesToDelete.add(src)
 598            else:
 599                die("unknown modifier %s for %s" % (modifier, path))
 600        if self.directSubmit:
 602            diffcmd = "cat \"%s\"" % self.diffFile
 603        else:
 604            diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 605        patchcmd = diffcmd + " | git apply "
 606        tryPatchCmd = patchcmd + "--check -"
 607        applyPatchCmd = patchcmd + "--check --apply -"
 608        if os.system(tryPatchCmd) != 0:
 610            print "Unfortunately applying the change failed!"
 611            print "What do you want to do?"
 612            response = "x"
 613            while response != "s" and response != "a" and response != "w":
 614                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 615                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 616            if response == "s":
 617                print "Skipping! Good luck with the next patches..."
 618                for f in editedFiles:
 619                    system("p4 revert \"%s\"" % f);
 620                for f in filesToAdd:
 621                    system("rm %s" %f)
 622                return
 623            elif response == "a":
 624                os.system(applyPatchCmd)
 625                if len(filesToAdd) > 0:
 626                    print "You may also want to call p4 add on the following files:"
 627                    print " ".join(filesToAdd)
 628                if len(filesToDelete):
 629                    print "The following files should be scheduled for deletion with p4 delete:"
 630                    print " ".join(filesToDelete)
 631                die("Please resolve and submit the conflict manually and "
 632                    + "continue afterwards with git-p4 submit --continue")
 633            elif response == "w":
 634                system(diffcmd + " > patch.txt")
 635                print "Patch saved to patch.txt in %s !" % self.clientPath
 636                die("Please resolve and submit the conflict manually and "
 637                    "continue afterwards with git-p4 submit --continue")
 638        system(applyPatchCmd)
 640        for f in filesToAdd:
 642            system("p4 add \"%s\"" % f)
 643        for f in filesToDelete:
 644            system("p4 revert \"%s\"" % f)
 645            system("p4 delete \"%s\"" % f)
 646        # Set/clear executable bits
 648        for f in filesToChangeExecBit.keys():
 649            mode = filesToChangeExecBit[f]
 650            setP4ExecBit(f, mode)
 651        logMessage = ""
 653        if not self.directSubmit:
 654            logMessage = extractLogMessageFromGitCommit(id)
 655            logMessage = logMessage.replace("\n", "\n\t")
 656            if self.isWindows:
 657                logMessage = logMessage.replace("\n", "\r\n")
 658            logMessage = logMessage.strip()
 659        template = self.prepareSubmitTemplate()
 661        if self.interactive:
 663            submitTemplate = self.prepareLogMessage(template, logMessage)
 664            diff = read_pipe("p4 diff -du ...")
 665            for newFile in filesToAdd:
 667                diff += "==== new file ====\n"
 668                diff += "--- /dev/null\n"
 669                diff += "+++ %s\n" % newFile
 670                f = open(newFile, "r")
 671                for line in f.readlines():
 672                    diff += "+" + line
 673                f.close()
 674            separatorLine = "######## everything below this line is just the diff #######"
 676            if platform.system() == "Windows":
 677                separatorLine += "\r"
 678            separatorLine += "\n"
 679            [handle, fileName] = tempfile.mkstemp()
 681            tmpFile = os.fdopen(handle, "w+")
 682            tmpFile.write(submitTemplate + separatorLine + diff)
 683            tmpFile.close()
 684            defaultEditor = "vi"
 685            if platform.system() == "Windows":
 686                defaultEditor = "notepad"
 687            editor = os.environ.get("EDITOR", defaultEditor);
 688            system(editor + " " + fileName)
 689            tmpFile = open(fileName, "rb")
 690            message = tmpFile.read()
 691            tmpFile.close()
 692            os.remove(fileName)
 693            submitTemplate = message[:message.index(separatorLine)]
 694            if self.isWindows:
 695                submitTemplate = submitTemplate.replace("\r\n", "\n")
 696            if self.directSubmit:
 698                print "Submitting to git first"
 699                os.chdir(self.oldWorkingDirectory)
 700                write_pipe("git commit -a -F -", submitTemplate)
 701                os.chdir(self.clientPath)
 702            write_pipe("p4 submit -i", submitTemplate)
 704        else:
 705            fileName = "submit.txt"
 706            file = open(fileName, "w+")
 707            file.write(self.prepareLogMessage(template, logMessage))
 708            file.close()
 709            print ("Perforce submit template written as %s. "
 710                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 711                   % (fileName, fileName))
 712    def run(self, args):
 714        if len(args) == 0:
 715            self.master = currentGitBranch()
 716            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 717                die("Detecting current git branch failed!")
 718        elif len(args) == 1:
 719            self.master = args[0]
 720        else:
 721            return False
 722        [upstream, settings] = findUpstreamBranchPoint()
 724        self.depotPath = settings['depot-paths'][0]
 725        if len(self.origin) == 0:
 726            self.origin = upstream
 727        if self.verbose:
 729            print "Origin branch is " + self.origin
 730        if len(self.depotPath) == 0:
 732            print "Internal error: cannot locate perforce depot path from existing branches"
 733            sys.exit(128)
 734        self.clientPath = p4Where(self.depotPath)
 736        if len(self.clientPath) == 0:
 738            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
 739            sys.exit(128)
 740        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
 742        self.oldWorkingDirectory = os.getcwd()
 743        if self.directSubmit:
 745            self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD")
 746            if len(self.diffStatus) == 0:
 747                print "No changes in working directory to submit."
 748                return True
 749            patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD")
 750            self.diffFile = self.gitdir + "/p4-git-diff"
 751            f = open(self.diffFile, "wb")
 752            f.write(patch)
 753            f.close();
 754        os.chdir(self.clientPath)
 756        print "Syncronizing p4 checkout..."
 757        system("p4 sync ...")
 758        if self.reset:
 760            self.firstTime = True
 761        if len(self.substFile) > 0:
 763            for line in open(self.substFile, "r").readlines():
 764                tokens = line.strip().split("=")
 765                self.logSubstitutions[tokens[0]] = tokens[1]
 766        self.check()
 768        self.configFile = self.gitdir + "/p4-git-sync.cfg"
 769        self.config = shelve.open(self.configFile, writeback=True)
 770        if self.firstTime:
 772            self.start()
 773        commits = self.config.get("commits", [])
 775        while len(commits) > 0:
 777            self.firstTime = False
 778            commit = commits[0]
 779            commits = commits[1:]
 780            self.config["commits"] = commits
 781            self.applyCommit(commit)
 782            if not self.interactive:
 783                break
 784        self.config.close()
 786        if self.directSubmit:
 788            os.remove(self.diffFile)
 789        if len(commits) == 0:
 791            if self.firstTime:
 792                print "No changes found to apply between %s and current HEAD" % self.origin
 793            else:
 794                print "All changes applied!"
 795                os.chdir(self.oldWorkingDirectory)
 796                sync = P4Sync()
 798                sync.run([])
 799                rebase = P4Rebase()
 801                rebase.rebase()
 802            os.remove(self.configFile)
 803        return True
 805class P4Sync(Command):
 807    def __init__(self):
 808        Command.__init__(self)
 809        self.options = [
 810                optparse.make_option("--branch", dest="branch"),
 811                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 812                optparse.make_option("--changesfile", dest="changesFile"),
 813                optparse.make_option("--silent", dest="silent", action="store_true"),
 814                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
 815                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 816                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
 817                                     help="Import into refs/heads/ , not refs/remotes"),
 818                optparse.make_option("--max-changes", dest="maxChanges"),
 819                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
 820                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
 821        ]
 822        self.description = """Imports from Perforce into a git repository.\n
 823    example:
 824    //depot/my/project/ -- to import the current head
 825    //depot/my/project/@all -- to import everything
 826    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 827    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 829        self.usage += " //depot/path[@revRange]"
 831        self.silent = False
 832        self.createdBranches = Set()
 833        self.committedChanges = Set()
 834        self.branch = ""
 835        self.detectBranches = False
 836        self.detectLabels = False
 837        self.changesFile = ""
 838        self.syncWithOrigin = True
 839        self.verbose = False
 840        self.importIntoRemotes = True
 841        self.maxChanges = ""
 842        self.isWindows = (platform.system() == "Windows")
 843        self.keepRepoPath = False
 844        self.depotPaths = None
 845        self.p4BranchesInGit = []
 846        if gitConfig("git-p4.syncFromOrigin") == "false":
 848            self.syncWithOrigin = False
 849    def extractFilesFromCommit(self, commit):
 851        files = []
 852        fnum = 0
 853        while commit.has_key("depotFile%s" % fnum):
 854            path =  commit["depotFile%s" % fnum]
 855            found = [p for p in self.depotPaths
 857                     if path.startswith (p)]
 858            if not found:
 859                fnum = fnum + 1
 860                continue
 861            file = {}
 863            file["path"] = path
 864            file["rev"] = commit["rev%s" % fnum]
 865            file["action"] = commit["action%s" % fnum]
 866            file["type"] = commit["type%s" % fnum]
 867            files.append(file)
 868            fnum = fnum + 1
 869        return files
 870    def stripRepoPath(self, path, prefixes):
 872        if self.keepRepoPath:
 873            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
 874        for p in prefixes:
 876            if path.startswith(p):
 877                path = path[len(p):]
 878        return path
 880    def splitFilesIntoBranches(self, commit):
 882        branches = {}
 883        fnum = 0
 884        while commit.has_key("depotFile%s" % fnum):
 885            path =  commit["depotFile%s" % fnum]
 886            found = [p for p in self.depotPaths
 887                     if path.startswith (p)]
 888            if not found:
 889                fnum = fnum + 1
 890                continue
 891            file = {}
 893            file["path"] = path
 894            file["rev"] = commit["rev%s" % fnum]
 895            file["action"] = commit["action%s" % fnum]
 896            file["type"] = commit["type%s" % fnum]
 897            fnum = fnum + 1
 898            relPath = self.stripRepoPath(path, self.depotPaths)
 900            for branch in self.knownBranches.keys():
 902                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
 904                if relPath.startswith(branch + "/"):
 905                    if branch not in branches:
 906                        branches[branch] = []
 907                    branches[branch].append(file)
 908                    break
 909        return branches
 911    ## Should move this out, doesn't use SELF.
 913    def readP4Files(self, files):
 914        files = [f for f in files
 915                 if f['action'] != 'delete']
 916        if not files:
 918            return
 919        filedata = p4CmdList('-x - print',
 921                             stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
 922                                              for f in files]),
 923                             stdin_mode='w+')
 924        if "p4ExitCode" in filedata[0]:
 925            die("Problems executing p4. Error: [%d]."
 926                % (filedata[0]['p4ExitCode']));
 927        j = 0;
 929        contents = {}
 930        while j < len(filedata):
 931            stat = filedata[j]
 932            j += 1
 933            text = ''
 934            while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
 935                tmp = filedata[j]['data']
 936                if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
 937                    tmp = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', tmp)
 938                elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
 939                    tmp = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', tmp)
 940                text += tmp
 941                j += 1
 942            if not stat.has_key('depotFile'):
 945                sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
 946                continue
 947            contents[stat['depotFile']] = text
 949        for f in files:
 951            assert not f.has_key('data')
 952            f['data'] = contents[f['path']]
 953    def commit(self, details, files, branch, branchPrefixes, parent = ""):
 955        epoch = details["time"]
 956        author = details["user"]
 957        if self.verbose:
 959            print "commit into %s" % branch
 960        # start with reading files; if that fails, we should not
 962        # create a commit.
 963        new_files = []
 964        for f in files:
 965            if [p for p in branchPrefixes if f['path'].startswith(p)]:
 966                new_files.append (f)
 967            else:
 968                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
 969        files = new_files
 970        self.readP4Files(files)
 971        self.gitStream.write("commit %s\n" % branch)
 976#        gitStream.write("mark :%s\n" % details["change"])
 977        self.committedChanges.add(int(details["change"]))
 978        committer = ""
 979        if author not in self.users:
 980            self.getUserMapFromPerforceServer()
 981        if author in self.users:
 982            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
 983        else:
 984            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
 985        self.gitStream.write("committer %s\n" % committer)
 987        self.gitStream.write("data <<EOT\n")
 989        self.gitStream.write(details["desc"])
 990        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
 991                             % (','.join (branchPrefixes), details["change"]))
 992        if len(details['options']) > 0:
 993            self.gitStream.write(": options = %s" % details['options'])
 994        self.gitStream.write("]\nEOT\n\n")
 995        if len(parent) > 0:
 997            if self.verbose:
 998                print "parent %s" % parent
 999            self.gitStream.write("from %s\n" % parent)
1000        for file in files:
1002            if file["type"] == "apple":
1003                print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1004                continue
1005            relPath = self.stripRepoPath(file['path'], branchPrefixes)
1007            if file["action"] == "delete":
1008                self.gitStream.write("D %s\n" % relPath)
1009            else:
1010                data = file['data']
1011                mode = "644"
1013                if isP4Exec(file["type"]):
1014                    mode = "755"
1015                elif file["type"] == "symlink":
1016                    mode = "120000"
1017                    # p4 print on a symlink contains "target\n", so strip it off
1018                    data = data[:-1]
1019                if self.isWindows and file["type"].endswith("text"):
1021                    data = data.replace("\r\n", "\n")
1022                self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1024                self.gitStream.write("data %s\n" % len(data))
1025                self.gitStream.write(data)
1026                self.gitStream.write("\n")
1027        self.gitStream.write("\n")
1029        change = int(details["change"])
1031        if self.labels.has_key(change):
1033            label = self.labels[change]
1034            labelDetails = label[0]
1035            labelRevisions = label[1]
1036            if self.verbose:
1037                print "Change %s is labelled %s" % (change, labelDetails)
1038            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1040                                                    for p in branchPrefixes]))
1041            if len(files) == len(labelRevisions):
1043                cleanedFiles = {}
1045                for info in files:
1046                    if info["action"] == "delete":
1047                        continue
1048                    cleanedFiles[info["depotFile"]] = info["rev"]
1049                if cleanedFiles == labelRevisions:
1051                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1052                    self.gitStream.write("from %s\n" % branch)
1053                    owner = labelDetails["Owner"]
1055                    tagger = ""
1056                    if author in self.users:
1057                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1058                    else:
1059                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1060                    self.gitStream.write("tagger %s\n" % tagger)
1061                    self.gitStream.write("data <<EOT\n")
1062                    self.gitStream.write(labelDetails["Description"])
1063                    self.gitStream.write("EOT\n\n")
1064                else:
1066                    if not self.silent:
1067                        print ("Tag %s does not match with change %s: files do not match."
1068                               % (labelDetails["label"], change))
1069            else:
1071                if not self.silent:
1072                    print ("Tag %s does not match with change %s: file count is different."
1073                           % (labelDetails["label"], change))
1074    def getUserCacheFilename(self):
1076        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1077        return home + "/.gitp4-usercache.txt"
1078    def getUserMapFromPerforceServer(self):
1080        if self.userMapFromPerforceServer:
1081            return
1082        self.users = {}
1083        for output in p4CmdList("users"):
1085            if not output.has_key("User"):
1086                continue
1087            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1088        s = ''
1091        for (key, val) in self.users.items():
1092            s += "%s\t%s\n" % (key, val)
1093        open(self.getUserCacheFilename(), "wb").write(s)
1095        self.userMapFromPerforceServer = True
1096    def loadUserMapFromCache(self):
1098        self.users = {}
1099        self.userMapFromPerforceServer = False
1100        try:
1101            cache = open(self.getUserCacheFilename(), "rb")
1102            lines = cache.readlines()
1103            cache.close()
1104            for line in lines:
1105                entry = line.strip().split("\t")
1106                self.users[entry[0]] = entry[1]
1107        except IOError:
1108            self.getUserMapFromPerforceServer()
1109    def getLabels(self):
1111        self.labels = {}
1112        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1114        if len(l) > 0 and not self.silent:
1115            print "Finding files belonging to labels in %s" % `self.depotPaths`
1116        for output in l:
1118            label = output["label"]
1119            revisions = {}
1120            newestChange = 0
1121            if self.verbose:
1122                print "Querying files for label %s" % label
1123            for file in p4CmdList("files "
1124                                  +  ' '.join (["%s...@%s" % (p, label)
1125                                                for p in self.depotPaths])):
1126                revisions[file["depotFile"]] = file["rev"]
1127                change = int(file["change"])
1128                if change > newestChange:
1129                    newestChange = change
1130            self.labels[newestChange] = [output, revisions]
1132        if self.verbose:
1134            print "Label changes: %s" % self.labels.keys()
1135    def guessProjectName(self):
1137        for p in self.depotPaths:
1138            if p.endswith("/"):
1139                p = p[:-1]
1140            p = p[p.strip().rfind("/") + 1:]
1141            if not p.endswith("/"):
1142               p += "/"
1143            return p
1144    def getBranchMapping(self):
1146        lostAndFoundBranches = set()
1147        for info in p4CmdList("branches"):
1149            details = p4Cmd("branch -o %s" % info["branch"])
1150            viewIdx = 0
1151            while details.has_key("View%s" % viewIdx):
1152                paths = details["View%s" % viewIdx].split(" ")
1153                viewIdx = viewIdx + 1
1154                # require standard //depot/foo/... //depot/bar/... mapping
1155                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1156                    continue
1157                source = paths[0]
1158                destination = paths[1]
1159                ## HACK
1160                if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1161                    source = source[len(self.depotPaths[0]):-4]
1162                    destination = destination[len(self.depotPaths[0]):-4]
1163                    if destination in self.knownBranches:
1165                        if not self.silent:
1166                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1167                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1168                        continue
1169                    self.knownBranches[destination] = source
1171                    lostAndFoundBranches.discard(destination)
1173                    if source not in self.knownBranches:
1175                        lostAndFoundBranches.add(source)
1176        for branch in lostAndFoundBranches:
1179            self.knownBranches[branch] = branch
1180    def getBranchMappingFromGitBranches(self):
1182        branches = p4BranchesInGit(self.importIntoRemotes)
1183        for branch in branches.keys():
1184            if branch == "master":
1185                branch = "main"
1186            else:
1187                branch = branch[len(self.projectName):]
1188            self.knownBranches[branch] = branch
1189    def listExistingP4GitBranches(self):
1191        # branches holds mapping from name to commit
1192        branches = p4BranchesInGit(self.importIntoRemotes)
1193        self.p4BranchesInGit = branches.keys()
1194        for branch in branches.keys():
1195            self.initialParents[self.refPrefix + branch] = branches[branch]
1196    def updateOptionDict(self, d):
1198        option_keys = {}
1199        if self.keepRepoPath:
1200            option_keys['keepRepoPath'] = 1
1201        d["options"] = ' '.join(sorted(option_keys.keys()))
1203    def readOptions(self, d):
1205        self.keepRepoPath = (d.has_key('options')
1206                             and ('keepRepoPath' in d['options']))
1207    def gitRefForBranch(self, branch):
1209        if branch == "main":
1210            return self.refPrefix + "master"
1211        if len(branch) <= 0:
1213            return branch
1214        return self.refPrefix + self.projectName + branch
1216    def gitCommitByP4Change(self, ref, change):
1218        if self.verbose:
1219            print "looking in ref " + ref + " for change %s using bisect..." % change
1220        earliestCommit = ""
1222        latestCommit = parseRevision(ref)
1223        while True:
1225            if self.verbose:
1226                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1227            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1228            if len(next) == 0:
1229                if self.verbose:
1230                    print "argh"
1231                return ""
1232            log = extractLogMessageFromGitCommit(next)
1233            settings = extractSettingsGitLog(log)
1234            currentChange = int(settings['change'])
1235            if self.verbose:
1236                print "current change %s" % currentChange
1237            if currentChange == change:
1239                if self.verbose:
1240                    print "found %s" % next
1241                return next
1242            if currentChange < change:
1244                earliestCommit = "^%s" % next
1245            else:
1246                latestCommit = "%s" % next
1247        return ""
1249    def importNewBranch(self, branch, maxChange):
1251        # make fast-import flush all changes to disk and update the refs using the checkpoint
1252        # command so that we can try to find the branch parent in the git history
1253        self.gitStream.write("checkpoint\n\n");
1254        self.gitStream.flush();
1255        branchPrefix = self.depotPaths[0] + branch + "/"
1256        range = "@1,%s" % maxChange
1257        #print "prefix" + branchPrefix
1258        changes = p4ChangesForPaths([branchPrefix], range)
1259        if len(changes) <= 0:
1260            return False
1261        firstChange = changes[0]
1262        #print "first change in branch: %s" % firstChange
1263        sourceBranch = self.knownBranches[branch]
1264        sourceDepotPath = self.depotPaths[0] + sourceBranch
1265        sourceRef = self.gitRefForBranch(sourceBranch)
1266        #print "source " + sourceBranch
1267        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1269        #print "branch parent: %s" % branchParentChange
1270        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1271        if len(gitParent) > 0:
1272            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1273            #print "parent git commit: %s" % gitParent
1274        self.importChanges(changes)
1276        return True
1277    def importChanges(self, changes):
1279        cnt = 1
1280        for change in changes:
1281            description = p4Cmd("describe %s" % change)
1282            self.updateOptionDict(description)
1283            if not self.silent:
1285                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1286                sys.stdout.flush()
1287            cnt = cnt + 1
1288            try:
1290                if self.detectBranches:
1291                    branches = self.splitFilesIntoBranches(description)
1292                    for branch in branches.keys():
1293                        ## HACK  --hwn
1294                        branchPrefix = self.depotPaths[0] + branch + "/"
1295                        parent = ""
1297                        filesForCommit = branches[branch]
1299                        if self.verbose:
1301                            print "branch is %s" % branch
1302                        self.updatedBranches.add(branch)
1304                        if branch not in self.createdBranches:
1306                            self.createdBranches.add(branch)
1307                            parent = self.knownBranches[branch]
1308                            if parent == branch:
1309                                parent = ""
1310                            else:
1311                                fullBranch = self.projectName + branch
1312                                if fullBranch not in self.p4BranchesInGit:
1313                                    if not self.silent:
1314                                        print("\n    Importing new branch %s" % fullBranch);
1315                                    if self.importNewBranch(branch, change - 1):
1316                                        parent = ""
1317                                        self.p4BranchesInGit.append(fullBranch)
1318                                    if not self.silent:
1319                                        print("\n    Resuming with change %s" % change);
1320                                if self.verbose:
1322                                    print "parent determined through known branches: %s" % parent
1323                        branch = self.gitRefForBranch(branch)
1325                        parent = self.gitRefForBranch(parent)
1326                        if self.verbose:
1328                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1329                        if len(parent) == 0 and branch in self.initialParents:
1331                            parent = self.initialParents[branch]
1332                            del self.initialParents[branch]
1333                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1335                else:
1336                    files = self.extractFilesFromCommit(description)
1337                    self.commit(description, files, self.branch, self.depotPaths,
1338                                self.initialParent)
1339                    self.initialParent = ""
1340            except IOError:
1341                print self.gitError.read()
1342                sys.exit(1)
1343    def importHeadRevision(self, revision):
1345        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1346        details = { "user" : "git perforce import user", "time" : int(time.time()) }
1348        details["desc"] = ("Initial import of %s from the state at revision %s"
1349                           % (' '.join(self.depotPaths), revision))
1350        details["change"] = revision
1351        newestRevision = 0
1352        fileCnt = 0
1354        for info in p4CmdList("files "
1355                              +  ' '.join(["%s...%s"
1356                                           % (p, revision)
1357                                           for p in self.depotPaths])):
1358            if info['code'] == 'error':
1360                sys.stderr.write("p4 returned an error: %s\n"
1361                                 % info['data'])
1362                sys.exit(1)
1363            change = int(info["change"])
1366            if change > newestRevision:
1367                newestRevision = change
1368            if info["action"] == "delete":
1370                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1371                #fileCnt = fileCnt + 1
1372                continue
1373            for prop in ["depotFile", "rev", "action", "type" ]:
1375                details["%s%s" % (prop, fileCnt)] = info[prop]
1376            fileCnt = fileCnt + 1
1378        details["change"] = newestRevision
1380        self.updateOptionDict(details)
1381        try:
1382            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1383        except IOError:
1384            print "IO error with git fast-import. Is your git version recent enough?"
1385            print self.gitError.read()
1386    def run(self, args):
1389        self.depotPaths = []
1390        self.changeRange = ""
1391        self.initialParent = ""
1392        self.previousDepotPaths = []
1393        # map from branch depot path to parent branch
1395        self.knownBranches = {}
1396        self.initialParents = {}
1397        self.hasOrigin = originP4BranchesExist()
1398        if not self.syncWithOrigin:
1399            self.hasOrigin = False
1400        if self.importIntoRemotes:
1402            self.refPrefix = "refs/remotes/p4/"
1403        else:
1404            self.refPrefix = "refs/heads/p4/"
1405        if self.syncWithOrigin and self.hasOrigin:
1407            if not self.silent:
1408                print "Syncing with origin first by calling git fetch origin"
1409            system("git fetch origin")
1410        if len(self.branch) == 0:
1412            self.branch = self.refPrefix + "master"
1413            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1414                system("git update-ref %s refs/heads/p4" % self.branch)
1415                system("git branch -D p4");
1416            # create it /after/ importing, when master exists
1417            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1418                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1419        # TODO: should always look at previous commits,
1421        # merge with previous imports, if possible.
1422        if args == []:
1423            if self.hasOrigin:
1424                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1425            self.listExistingP4GitBranches()
1426            if len(self.p4BranchesInGit) > 1:
1428                if not self.silent:
1429                    print "Importing from/into multiple branches"
1430                self.detectBranches = True
1431            if self.verbose:
1433                print "branches: %s" % self.p4BranchesInGit
1434            p4Change = 0
1436            for branch in self.p4BranchesInGit:
1437                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1438                settings = extractSettingsGitLog(logMsg)
1440                self.readOptions(settings)
1442                if (settings.has_key('depot-paths')
1443                    and settings.has_key ('change')):
1444                    change = int(settings['change']) + 1
1445                    p4Change = max(p4Change, change)
1446                    depotPaths = sorted(settings['depot-paths'])
1448                    if self.previousDepotPaths == []:
1449                        self.previousDepotPaths = depotPaths
1450                    else:
1451                        paths = []
1452                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1453                            for i in range(0, min(len(cur), len(prev))):
1454                                if cur[i] <> prev[i]:
1455                                    i = i - 1
1456                                    break
1457                            paths.append (cur[:i + 1])
1459                        self.previousDepotPaths = paths
1461            if p4Change > 0:
1463                self.depotPaths = sorted(self.previousDepotPaths)
1464                self.changeRange = "@%s,#head" % p4Change
1465                if not self.detectBranches:
1466                    self.initialParent = parseRevision(self.branch)
1467                if not self.silent and not self.detectBranches:
1468                    print "Performing incremental import into %s git branch" % self.branch
1469        if not self.branch.startswith("refs/"):
1471            self.branch = "refs/heads/" + self.branch
1472        if len(args) == 0 and self.depotPaths:
1474            if not self.silent:
1475                print "Depot paths: %s" % ' '.join(self.depotPaths)
1476        else:
1477            if self.depotPaths and self.depotPaths != args:
1478                print ("previous import used depot path %s and now %s was specified. "
1479                       "This doesn't work!" % (' '.join (self.depotPaths),
1480                                               ' '.join (args)))
1481                sys.exit(1)
1482            self.depotPaths = sorted(args)
1484        revision = ""
1486        self.users = {}
1487        newPaths = []
1489        for p in self.depotPaths:
1490            if p.find("@") != -1:
1491                atIdx = p.index("@")
1492                self.changeRange = p[atIdx:]
1493                if self.changeRange == "@all":
1494                    self.changeRange = ""
1495                elif ',' not in self.changeRange:
1496                    revision = self.changeRange
1497                    self.changeRange = ""
1498                p = p[:atIdx]
1499            elif p.find("#") != -1:
1500                hashIdx = p.index("#")
1501                revision = p[hashIdx:]
1502                p = p[:hashIdx]
1503            elif self.previousDepotPaths == []:
1504                revision = "#head"
1505            p = re.sub ("\.\.\.$", "", p)
1507            if not p.endswith("/"):
1508                p += "/"
1509            newPaths.append(p)
1511        self.depotPaths = newPaths
1513        self.loadUserMapFromCache()
1516        self.labels = {}
1517        if self.detectLabels:
1518            self.getLabels();
1519        if self.detectBranches:
1521            ## FIXME - what's a P4 projectName ?
1522            self.projectName = self.guessProjectName()
1523            if self.hasOrigin:
1525                self.getBranchMappingFromGitBranches()
1526            else:
1527                self.getBranchMapping()
1528            if self.verbose:
1529                print "p4-git branches: %s" % self.p4BranchesInGit
1530                print "initial parents: %s" % self.initialParents
1531            for b in self.p4BranchesInGit:
1532                if b != "master":
1533                    ## FIXME
1535                    b = b[len(self.projectName):]
1536                self.createdBranches.add(b)
1537        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1539        importProcess = subprocess.Popen(["git", "fast-import"],
1541                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1542                                         stderr=subprocess.PIPE);
1543        self.gitOutput = importProcess.stdout
1544        self.gitStream = importProcess.stdin
1545        self.gitError = importProcess.stderr
1546        if revision:
1548            self.importHeadRevision(revision)
1549        else:
1550            changes = []
1551            if len(self.changesFile) > 0:
1553                output = open(self.changesFile).readlines()
1554                changeSet = Set()
1555                for line in output:
1556                    changeSet.add(int(line))
1557                for change in changeSet:
1559                    changes.append(change)
1560                changes.sort()
1562            else:
1563                if self.verbose:
1564                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1565                                                              self.changeRange)
1566                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1567                if len(self.maxChanges) > 0:
1569                    changes = changes[:min(int(self.maxChanges), len(changes))]
1570            if len(changes) == 0:
1572                if not self.silent:
1573                    print "No changes to import!"
1574                return True
1575            if not self.silent and not self.detectBranches:
1577                print "Import destination: %s" % self.branch
1578            self.updatedBranches = set()
1580            self.importChanges(changes)
1582            if not self.silent:
1584                print ""
1585                if len(self.updatedBranches) > 0:
1586                    sys.stdout.write("Updated branches: ")
1587                    for b in self.updatedBranches:
1588                        sys.stdout.write("%s " % b)
1589                    sys.stdout.write("\n")
1590        self.gitStream.close()
1592        if importProcess.wait() != 0:
1593            die("fast-import failed: %s" % self.gitError.read())
1594        self.gitOutput.close()
1595        self.gitError.close()
1596        return True
1598class P4Rebase(Command):
1600    def __init__(self):
1601        Command.__init__(self)
1602        self.options = [ ]
1603        self.description = ("Fetches the latest revision from perforce and "
1604                            + "rebases the current work (branch) against it")
1605        self.verbose = False
1606    def run(self, args):
1608        sync = P4Sync()
1609        sync.run([])
1610        return self.rebase()
1612    def rebase(self):
1614        if os.system("git update-index --refresh") != 0:
1615            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.");
1616        if len(read_pipe("git diff-index HEAD --")) > 0:
1617            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1618        [upstream, settings] = findUpstreamBranchPoint()
1620        if len(upstream) == 0:
1621            die("Cannot find upstream branchpoint for rebase")
1622        # the branchpoint may be p4/foo~3, so strip off the parent
1624        upstream = re.sub("~[0-9]+$", "", upstream)
1625        print "Rebasing the current branch onto %s" % upstream
1627        oldHead = read_pipe("git rev-parse HEAD").strip()
1628        system("git rebase %s" % upstream)
1629        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1630        return True
1631class P4Clone(P4Sync):
1633    def __init__(self):
1634        P4Sync.__init__(self)
1635        self.description = "Creates a new git repository and imports from Perforce into it"
1636        self.usage = "usage: %prog [options] //depot/path[@revRange]"
1637        self.options.append(
1638            optparse.make_option("--destination", dest="cloneDestination",
1639                                 action='store', default=None,
1640                                 help="where to leave result of the clone"))
1641        self.cloneDestination = None
1642        self.needsGit = False
1643    def defaultDestination(self, args):
1645        ## TODO: use common prefix of args?
1646        depotPath = args[0]
1647        depotDir = re.sub("(@[^@]*)$", "", depotPath)
1648        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1649        depotDir = re.sub(r"\.\.\.$", "", depotDir)
1650        depotDir = re.sub(r"/$", "", depotDir)
1651        return os.path.split(depotDir)[1]
1652    def run(self, args):
1654        if len(args) < 1:
1655            return False
1656        if self.keepRepoPath and not self.cloneDestination:
1658            sys.stderr.write("Must specify destination for --keep-path\n")
1659            sys.exit(1)
1660        depotPaths = args
1662        if not self.cloneDestination and len(depotPaths) > 1:
1664            self.cloneDestination = depotPaths[-1]
1665            depotPaths = depotPaths[:-1]
1666        for p in depotPaths:
1668            if not p.startswith("//"):
1669                return False
1670        if not self.cloneDestination:
1672            self.cloneDestination = self.defaultDestination(args)
1673        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1675        if not os.path.exists(self.cloneDestination):
1676            os.makedirs(self.cloneDestination)
1677        os.chdir(self.cloneDestination)
1678        system("git init")
1679        self.gitdir = os.getcwd() + "/.git"
1680        if not P4Sync.run(self, depotPaths):
1681            return False
1682        if self.branch != "master":
1683            if gitBranchExists("refs/remotes/p4/master"):
1684                system("git branch master refs/remotes/p4/master")
1685                system("git checkout -f")
1686            else:
1687                print "Could not detect main branch. No checkout/master branch created."
1688        return True
1690class P4Branches(Command):
1692    def __init__(self):
1693        Command.__init__(self)
1694        self.options = [ ]
1695        self.description = ("Shows the git branches that hold imports and their "
1696                            + "corresponding perforce depot paths")
1697        self.verbose = False
1698    def run(self, args):
1700        if originP4BranchesExist():
1701            createOrUpdateBranchesFromOrigin()
1702        cmdline = "git rev-parse --symbolic "
1704        cmdline += " --remotes"
1705        for line in read_pipe_lines(cmdline):
1707            line = line.strip()
1708            if not line.startswith('p4/') or line == "p4/HEAD":
1710                continue
1711            branch = line
1712            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1714            settings = extractSettingsGitLog(log)
1715            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1717        return True
1718class HelpFormatter(optparse.IndentedHelpFormatter):
1720    def __init__(self):
1721        optparse.IndentedHelpFormatter.__init__(self)
1722    def format_description(self, description):
1724        if description:
1725            return description + "\n"
1726        else:
1727            return ""
1728def printUsage(commands):
1730    print "usage: %s <command> [options]" % sys.argv[0]
1731    print ""
1732    print "valid commands: %s" % ", ".join(commands)
1733    print ""
1734    print "Try %s <command> --help for command specific help." % sys.argv[0]
1735    print ""
1736commands = {
1738    "debug" : P4Debug,
1739    "submit" : P4Submit,
1740    "commit" : P4Submit,
1741    "sync" : P4Sync,
1742    "rebase" : P4Rebase,
1743    "clone" : P4Clone,
1744    "rollback" : P4RollBack,
1745    "branches" : P4Branches
1746}
1747def main():
1750    if len(sys.argv[1:]) == 0:
1751        printUsage(commands.keys())
1752        sys.exit(2)
1753    cmd = ""
1755    cmdName = sys.argv[1]
1756    try:
1757        klass = commands[cmdName]
1758        cmd = klass()
1759    except KeyError:
1760        print "unknown command %s" % cmdName
1761        print ""
1762        printUsage(commands.keys())
1763        sys.exit(2)
1764    options = cmd.options
1766    cmd.gitdir = os.environ.get("GIT_DIR", None)
1767    args = sys.argv[2:]
1769    if len(options) > 0:
1771        options.append(optparse.make_option("--git-dir", dest="gitdir"))
1772        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1774                                       options,
1775                                       description = cmd.description,
1776                                       formatter = HelpFormatter())
1777        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1779    global verbose
1780    verbose = cmd.verbose
1781    if cmd.needsGit:
1782        if cmd.gitdir == None:
1783            cmd.gitdir = os.path.abspath(".git")
1784            if not isValidGitDir(cmd.gitdir):
1785                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1786                if os.path.exists(cmd.gitdir):
1787                    cdup = read_pipe("git rev-parse --show-cdup").strip()
1788                    if len(cdup) > 0:
1789                        os.chdir(cdup);
1790        if not isValidGitDir(cmd.gitdir):
1792            if isValidGitDir(cmd.gitdir + "/.git"):
1793                cmd.gitdir += "/.git"
1794            else:
1795                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1796        os.environ["GIT_DIR"] = cmd.gitdir
1798    if not cmd.run(args):
1800        parser.print_help()
1801if __name__ == '__main__':
1804    main()