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 p4_build_cmd(cmd):
  21    """Build a suitable p4 command line.
  22    This consolidates building and returning a p4 command line into one
  24    location. It means that hooking into the environment, or other configuration
  25    can be done more easily.
  26    """
  27    real_cmd = "%s " % "p4"
  28    user = gitConfig("git-p4.user")
  30    if len(user) > 0:
  31        real_cmd += "-u %s " % user
  32    password = gitConfig("git-p4.password")
  34    if len(password) > 0:
  35        real_cmd += "-P %s " % password
  36    port = gitConfig("git-p4.port")
  38    if len(port) > 0:
  39        real_cmd += "-p %s " % port
  40    host = gitConfig("git-p4.host")
  42    if len(host) > 0:
  43        real_cmd += "-h %s " % host
  44    client = gitConfig("git-p4.client")
  46    if len(client) > 0:
  47        real_cmd += "-c %s " % client
  48    real_cmd += "%s" % (cmd)
  50    if verbose:
  51        print real_cmd
  52    return real_cmd
  53def chdir(dir):
  55    if os.name == 'nt':
  56        os.environ['PWD']=dir
  57    os.chdir(dir)
  58def die(msg):
  60    if verbose:
  61        raise Exception(msg)
  62    else:
  63        sys.stderr.write(msg + "\n")
  64        sys.exit(1)
  65def write_pipe(c, str):
  67    if verbose:
  68        sys.stderr.write('Writing pipe: %s\n' % c)
  69    pipe = os.popen(c, 'w')
  71    val = pipe.write(str)
  72    if pipe.close():
  73        die('Command failed: %s' % c)
  74    return val
  76def p4_write_pipe(c, str):
  78    real_cmd = p4_build_cmd(c)
  79    return write_pipe(real_cmd, str)
  80def read_pipe(c, ignore_error=False):
  82    if verbose:
  83        sys.stderr.write('Reading pipe: %s\n' % c)
  84    pipe = os.popen(c, 'rb')
  86    val = pipe.read()
  87    if pipe.close() and not ignore_error:
  88        die('Command failed: %s' % c)
  89    return val
  91def p4_read_pipe(c, ignore_error=False):
  93    real_cmd = p4_build_cmd(c)
  94    return read_pipe(real_cmd, ignore_error)
  95def read_pipe_lines(c):
  97    if verbose:
  98        sys.stderr.write('Reading pipe: %s\n' % c)
  99    ## todo: check return status
 100    pipe = os.popen(c, 'rb')
 101    val = pipe.readlines()
 102    if pipe.close():
 103        die('Command failed: %s' % c)
 104    return val
 106def p4_read_pipe_lines(c):
 108    """Specifically invoke p4 on the command supplied. """
 109    real_cmd = p4_build_cmd(c)
 110    return read_pipe_lines(real_cmd)
 111def system(cmd):
 113    if verbose:
 114        sys.stderr.write("executing %s\n" % cmd)
 115    if os.system(cmd) != 0:
 116        die("command failed: %s" % cmd)
 117def p4_system(cmd):
 119    """Specifically invoke p4 as the system command. """
 120    real_cmd = p4_build_cmd(cmd)
 121    return system(real_cmd)
 122def isP4Exec(kind):
 124    """Determine if a Perforce 'kind' should have execute permission
 125    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
 127    or x follows one of a few letters.  Otherwise, if there is an 'x' after
 128    a plus sign, it is also executable"""
 129    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
 130def setP4ExecBit(file, mode):
 132    # Reopens an already open file and changes the execute bit to match
 133    # the execute bit setting in the passed in mode.
 134    p4Type = "+x"
 136    if not isModeExec(mode):
 138        p4Type = getP4OpenedType(file)
 139        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
 140        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
 141        if p4Type[-1] == "+":
 142            p4Type = p4Type[0:-1]
 143    p4_system("reopen -t %s %s" % (p4Type, file))
 145def getP4OpenedType(file):
 147    # Returns the perforce file type for the given file.
 148    result = p4_read_pipe("opened %s" % file)
 150    match = re.match(".*\((.+)\)\r?$", result)
 151    if match:
 152        return match.group(1)
 153    else:
 154        die("Could not determine file type for %s (result: '%s')" % (file, result))
 155def diffTreePattern():
 157    # This is a simple generator for the diff tree regex pattern. This could be
 158    # a class variable if this and parseDiffTreeEntry were a part of a class.
 159    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 160    while True:
 161        yield pattern
 162def parseDiffTreeEntry(entry):
 164    """Parses a single diff tree entry into its component elements.
 165    See git-diff-tree(1) manpage for details about the format of the diff
 167    output. This method returns a dictionary with the following elements:
 168    src_mode - The mode of the source file
 170    dst_mode - The mode of the destination file
 171    src_sha1 - The sha1 for the source file
 172    dst_sha1 - The sha1 fr the destination file
 173    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 174    status_score - The score for the status (applicable for 'C' and 'R'
 175                   statuses). This is None if there is no score.
 176    src - The path for the source file.
 177    dst - The path for the destination file. This is only present for
 178          copy or renames. If it is not present, this is None.
 179    If the pattern is not matched, None is returned."""
 181    match = diffTreePattern().next().match(entry)
 183    if match:
 184        return {
 185            'src_mode': match.group(1),
 186            'dst_mode': match.group(2),
 187            'src_sha1': match.group(3),
 188            'dst_sha1': match.group(4),
 189            'status': match.group(5),
 190            'status_score': match.group(6),
 191            'src': match.group(7),
 192            'dst': match.group(10)
 193        }
 194    return None
 195def isModeExec(mode):
 197    # Returns True if the given git mode represents an executable file,
 198    # otherwise False.
 199    return mode[-3:] == "755"
 200def isModeExecChanged(src_mode, dst_mode):
 202    return isModeExec(src_mode) != isModeExec(dst_mode)
 203def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
 205    cmd = p4_build_cmd("-G %s" % (cmd))
 206    if verbose:
 207        sys.stderr.write("Opening pipe: %s\n" % cmd)
 208    # Use a temporary file to avoid deadlocks without
 210    # subprocess.communicate(), which would put another copy
 211    # of stdout into memory.
 212    stdin_file = None
 213    if stdin is not None:
 214        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 215        stdin_file.write(stdin)
 216        stdin_file.flush()
 217        stdin_file.seek(0)
 218    p4 = subprocess.Popen(cmd, shell=True,
 220                          stdin=stdin_file,
 221                          stdout=subprocess.PIPE)
 222    result = []
 224    try:
 225        while True:
 226            entry = marshal.load(p4.stdout)
 227            result.append(entry)
 228    except EOFError:
 229        pass
 230    exitCode = p4.wait()
 231    if exitCode != 0:
 232        entry = {}
 233        entry["p4ExitCode"] = exitCode
 234        result.append(entry)
 235    return result
 237def p4Cmd(cmd):
 239    list = p4CmdList(cmd)
 240    result = {}
 241    for entry in list:
 242        result.update(entry)
 243    return result;
 244def p4Where(depotPath):
 246    if not depotPath.endswith("/"):
 247        depotPath += "/"
 248    depotPath = depotPath + "..."
 249    outputList = p4CmdList("where %s" % depotPath)
 250    output = None
 251    for entry in outputList:
 252        if "depotFile" in entry:
 253            if entry["depotFile"] == depotPath:
 254                output = entry
 255                break
 256        elif "data" in entry:
 257            data = entry.get("data")
 258            space = data.find(" ")
 259            if data[:space] == depotPath:
 260                output = entry
 261                break
 262    if output == None:
 263        return ""
 264    if output["code"] == "error":
 265        return ""
 266    clientPath = ""
 267    if "path" in output:
 268        clientPath = output.get("path")
 269    elif "data" in output:
 270        data = output.get("data")
 271        lastSpace = data.rfind(" ")
 272        clientPath = data[lastSpace + 1:]
 273    if clientPath.endswith("..."):
 275        clientPath = clientPath[:-3]
 276    return clientPath
 277def currentGitBranch():
 279    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 280def isValidGitDir(path):
 282    if (os.path.exists(path + "/HEAD")
 283        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 284        return True;
 285    return False
 286def parseRevision(ref):
 288    return read_pipe("git rev-parse %s" % ref).strip()
 289def extractLogMessageFromGitCommit(commit):
 291    logMessage = ""
 292    ## fixme: title is first line of commit, not 1st paragraph.
 294    foundTitle = False
 295    for log in read_pipe_lines("git cat-file commit %s" % commit):
 296       if not foundTitle:
 297           if len(log) == 1:
 298               foundTitle = True
 299           continue
 300       logMessage += log
 302    return logMessage
 303def extractSettingsGitLog(log):
 305    values = {}
 306    for line in log.split("\n"):
 307        line = line.strip()
 308        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 309        if not m:
 310            continue
 311        assignments = m.group(1).split (':')
 313        for a in assignments:
 314            vals = a.split ('=')
 315            key = vals[0].strip()
 316            val = ('='.join (vals[1:])).strip()
 317            if val.endswith ('\"') and val.startswith('"'):
 318                val = val[1:-1]
 319            values[key] = val
 321    paths = values.get("depot-paths")
 323    if not paths:
 324        paths = values.get("depot-path")
 325    if paths:
 326        values['depot-paths'] = paths.split(',')
 327    return values
 328def gitBranchExists(branch):
 330    proc = subprocess.Popen(["git", "rev-parse", branch],
 331                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 332    return proc.wait() == 0;
 333_gitConfig = {}
 335def gitConfig(key):
 336    if not _gitConfig.has_key(key):
 337        _gitConfig[key] = read_pipe("git config %s" % key, ignore_error=True).strip()
 338    return _gitConfig[key]
 339def p4BranchesInGit(branchesAreInRemotes = True):
 341    branches = {}
 342    cmdline = "git rev-parse --symbolic "
 344    if branchesAreInRemotes:
 345        cmdline += " --remotes"
 346    else:
 347        cmdline += " --branches"
 348    for line in read_pipe_lines(cmdline):
 350        line = line.strip()
 351        ## only import to p4/
 353        if not line.startswith('p4/') or line == "p4/HEAD":
 354            continue
 355        branch = line
 356        # strip off p4
 358        branch = re.sub ("^p4/", "", line)
 359        branches[branch] = parseRevision(line)
 361    return branches
 362def findUpstreamBranchPoint(head = "HEAD"):
 364    branches = p4BranchesInGit()
 365    # map from depot-path to branch name
 366    branchByDepotPath = {}
 367    for branch in branches.keys():
 368        tip = branches[branch]
 369        log = extractLogMessageFromGitCommit(tip)
 370        settings = extractSettingsGitLog(log)
 371        if settings.has_key("depot-paths"):
 372            paths = ",".join(settings["depot-paths"])
 373            branchByDepotPath[paths] = "remotes/p4/" + branch
 374    settings = None
 376    parent = 0
 377    while parent < 65535:
 378        commit = head + "~%s" % parent
 379        log = extractLogMessageFromGitCommit(commit)
 380        settings = extractSettingsGitLog(log)
 381        if settings.has_key("depot-paths"):
 382            paths = ",".join(settings["depot-paths"])
 383            if branchByDepotPath.has_key(paths):
 384                return [branchByDepotPath[paths], settings]
 385        parent = parent + 1
 387    return ["", settings]
 389def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 391    if not silent:
 392        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 393               % localRefPrefix)
 394    originPrefix = "origin/p4/"
 396    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 398        line = line.strip()
 399        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 400            continue
 401        headName = line[len(originPrefix):]
 403        remoteHead = localRefPrefix + headName
 404        originHead = line
 405        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 407        if (not original.has_key('depot-paths')
 408            or not original.has_key('change')):
 409            continue
 410        update = False
 412        if not gitBranchExists(remoteHead):
 413            if verbose:
 414                print "creating %s" % remoteHead
 415            update = True
 416        else:
 417            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 418            if settings.has_key('change') > 0:
 419                if settings['depot-paths'] == original['depot-paths']:
 420                    originP4Change = int(original['change'])
 421                    p4Change = int(settings['change'])
 422                    if originP4Change > p4Change:
 423                        print ("%s (%s) is newer than %s (%s). "
 424                               "Updating p4 branch from origin."
 425                               % (originHead, originP4Change,
 426                                  remoteHead, p4Change))
 427                        update = True
 428                else:
 429                    print ("Ignoring: %s was imported from %s while "
 430                           "%s was imported from %s"
 431                           % (originHead, ','.join(original['depot-paths']),
 432                              remoteHead, ','.join(settings['depot-paths'])))
 433        if update:
 435            system("git update-ref %s %s" % (remoteHead, originHead))
 436def originP4BranchesExist():
 438        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 439def p4ChangesForPaths(depotPaths, changeRange):
 441    assert depotPaths
 442    output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
 443                                                        for p in depotPaths]))
 444    changes = {}
 446    for line in output:
 447        changeNum = int(line.split(" ")[1])
 448        changes[changeNum] = True
 449    changelist = changes.keys()
 451    changelist.sort()
 452    return changelist
 453class Command:
 455    def __init__(self):
 456        self.usage = "usage: %prog [options]"
 457        self.needsGit = True
 458class P4Debug(Command):
 460    def __init__(self):
 461        Command.__init__(self)
 462        self.options = [
 463            optparse.make_option("--verbose", dest="verbose", action="store_true",
 464                                 default=False),
 465            ]
 466        self.description = "A tool to debug the output of p4 -G."
 467        self.needsGit = False
 468        self.verbose = False
 469    def run(self, args):
 471        j = 0
 472        for output in p4CmdList(" ".join(args)):
 473            print 'Element: %d' % j
 474            j += 1
 475            print output
 476        return True
 477class P4RollBack(Command):
 479    def __init__(self):
 480        Command.__init__(self)
 481        self.options = [
 482            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 483            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 484        ]
 485        self.description = "A tool to debug the multi-branch import. Don't use :)"
 486        self.verbose = False
 487        self.rollbackLocalBranches = False
 488    def run(self, args):
 490        if len(args) != 1:
 491            return False
 492        maxChange = int(args[0])
 493        if "p4ExitCode" in p4Cmd("changes -m 1"):
 495            die("Problems executing p4");
 496        if self.rollbackLocalBranches:
 498            refPrefix = "refs/heads/"
 499            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 500        else:
 501            refPrefix = "refs/remotes/"
 502            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 503        for line in lines:
 505            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 506                line = line.strip()
 507                ref = refPrefix + line
 508                log = extractLogMessageFromGitCommit(ref)
 509                settings = extractSettingsGitLog(log)
 510                depotPaths = settings['depot-paths']
 512                change = settings['change']
 513                changed = False
 515                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 517                                                           for p in depotPaths]))) == 0:
 518                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 519                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 520                    continue
 521                while change and int(change) > maxChange:
 523                    changed = True
 524                    if self.verbose:
 525                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 526                    system("git update-ref %s \"%s^\"" % (ref, ref))
 527                    log = extractLogMessageFromGitCommit(ref)
 528                    settings =  extractSettingsGitLog(log)
 529                    depotPaths = settings['depot-paths']
 532                    change = settings['change']
 533                if changed:
 535                    print "%s rewound to %s" % (ref, change)
 536        return True
 538class P4Submit(Command):
 540    def __init__(self):
 541        Command.__init__(self)
 542        self.options = [
 543                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 544                optparse.make_option("--origin", dest="origin"),
 545                optparse.make_option("-M", dest="detectRename", action="store_true"),
 546        ]
 547        self.description = "Submit changes from git to the perforce depot."
 548        self.usage += " [name of git branch to submit into perforce depot]"
 549        self.interactive = True
 550        self.origin = ""
 551        self.detectRename = False
 552        self.verbose = False
 553        self.isWindows = (platform.system() == "Windows")
 554    def check(self):
 556        if len(p4CmdList("opened ...")) > 0:
 557            die("You have files opened with perforce! Close them before starting the sync.")
 558    # replaces everything between 'Description:' and the next P4 submit template field with the
 560    # commit message
 561    def prepareLogMessage(self, template, message):
 562        result = ""
 563        inDescriptionSection = False
 565        for line in template.split("\n"):
 567            if line.startswith("#"):
 568                result += line + "\n"
 569                continue
 570            if inDescriptionSection:
 572                if line.startswith("Files:"):
 573                    inDescriptionSection = False
 574                else:
 575                    continue
 576            else:
 577                if line.startswith("Description:"):
 578                    inDescriptionSection = True
 579                    line += "\n"
 580                    for messageLine in message.split("\n"):
 581                        line += "\t" + messageLine + "\n"
 582            result += line + "\n"
 584        return result
 586    def prepareSubmitTemplate(self):
 588        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 589        template = ""
 590        inFilesSection = False
 591        for line in p4_read_pipe_lines("change -o"):
 592            if line.endswith("\r\n"):
 593                line = line[:-2] + "\n"
 594            if inFilesSection:
 595                if line.startswith("\t"):
 596                    # path starts and ends with a tab
 597                    path = line[1:]
 598                    lastTab = path.rfind("\t")
 599                    if lastTab != -1:
 600                        path = path[:lastTab]
 601                        if not path.startswith(self.depotPath):
 602                            continue
 603                else:
 604                    inFilesSection = False
 605            else:
 606                if line.startswith("Files:"):
 607                    inFilesSection = True
 608            template += line
 610        return template
 612    def applyCommit(self, id):
 614        print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 615        diffOpts = ("", "-M")[self.detectRename]
 616        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 617        filesToAdd = set()
 618        filesToDelete = set()
 619        editedFiles = set()
 620        filesToChangeExecBit = {}
 621        for line in diff:
 622            diff = parseDiffTreeEntry(line)
 623            modifier = diff['status']
 624            path = diff['src']
 625            if modifier == "M":
 626                p4_system("edit \"%s\"" % path)
 627                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 628                    filesToChangeExecBit[path] = diff['dst_mode']
 629                editedFiles.add(path)
 630            elif modifier == "A":
 631                filesToAdd.add(path)
 632                filesToChangeExecBit[path] = diff['dst_mode']
 633                if path in filesToDelete:
 634                    filesToDelete.remove(path)
 635            elif modifier == "D":
 636                filesToDelete.add(path)
 637                if path in filesToAdd:
 638                    filesToAdd.remove(path)
 639            elif modifier == "R":
 640                src, dest = diff['src'], diff['dst']
 641                p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
 642                p4_system("edit \"%s\"" % (dest))
 643                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 644                    filesToChangeExecBit[dest] = diff['dst_mode']
 645                os.unlink(dest)
 646                editedFiles.add(dest)
 647                filesToDelete.add(src)
 648            else:
 649                die("unknown modifier %s for %s" % (modifier, path))
 650        diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 652        patchcmd = diffcmd + " | git apply "
 653        tryPatchCmd = patchcmd + "--check -"
 654        applyPatchCmd = patchcmd + "--check --apply -"
 655        if os.system(tryPatchCmd) != 0:
 657            print "Unfortunately applying the change failed!"
 658            print "What do you want to do?"
 659            response = "x"
 660            while response != "s" and response != "a" and response != "w":
 661                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 662                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 663            if response == "s":
 664                print "Skipping! Good luck with the next patches..."
 665                for f in editedFiles:
 666                    p4_system("revert \"%s\"" % f);
 667                for f in filesToAdd:
 668                    system("rm %s" %f)
 669                return
 670            elif response == "a":
 671                os.system(applyPatchCmd)
 672                if len(filesToAdd) > 0:
 673                    print "You may also want to call p4 add on the following files:"
 674                    print " ".join(filesToAdd)
 675                if len(filesToDelete):
 676                    print "The following files should be scheduled for deletion with p4 delete:"
 677                    print " ".join(filesToDelete)
 678                die("Please resolve and submit the conflict manually and "
 679                    + "continue afterwards with git-p4 submit --continue")
 680            elif response == "w":
 681                system(diffcmd + " > patch.txt")
 682                print "Patch saved to patch.txt in %s !" % self.clientPath
 683                die("Please resolve and submit the conflict manually and "
 684                    "continue afterwards with git-p4 submit --continue")
 685        system(applyPatchCmd)
 687        for f in filesToAdd:
 689            p4_system("add \"%s\"" % f)
 690        for f in filesToDelete:
 691            p4_system("revert \"%s\"" % f)
 692            p4_system("delete \"%s\"" % f)
 693        # Set/clear executable bits
 695        for f in filesToChangeExecBit.keys():
 696            mode = filesToChangeExecBit[f]
 697            setP4ExecBit(f, mode)
 698        logMessage = extractLogMessageFromGitCommit(id)
 700        logMessage = logMessage.strip()
 701        template = self.prepareSubmitTemplate()
 703        if self.interactive:
 705            submitTemplate = self.prepareLogMessage(template, logMessage)
 706            if os.environ.has_key("P4DIFF"):
 707                del(os.environ["P4DIFF"])
 708            diff = p4_read_pipe("diff -du ...")
 709            newdiff = ""
 711            for newFile in filesToAdd:
 712                newdiff += "==== new file ====\n"
 713                newdiff += "--- /dev/null\n"
 714                newdiff += "+++ %s\n" % newFile
 715                f = open(newFile, "r")
 716                for line in f.readlines():
 717                    newdiff += "+" + line
 718                f.close()
 719            separatorLine = "######## everything below this line is just the diff #######\n"
 721            [handle, fileName] = tempfile.mkstemp()
 723            tmpFile = os.fdopen(handle, "w+")
 724            if self.isWindows:
 725                submitTemplate = submitTemplate.replace("\n", "\r\n")
 726                separatorLine = separatorLine.replace("\n", "\r\n")
 727                newdiff = newdiff.replace("\n", "\r\n")
 728            tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
 729            tmpFile.close()
 730            mtime = os.stat(fileName).st_mtime
 731            defaultEditor = "vi"
 732            if platform.system() == "Windows":
 733                defaultEditor = "notepad"
 734            if os.environ.has_key("P4EDITOR"):
 735                editor = os.environ.get("P4EDITOR")
 736            else:
 737                editor = os.environ.get("EDITOR", defaultEditor);
 738            system(editor + " " + fileName)
 739            response = "y"
 741            if os.stat(fileName).st_mtime <= mtime:
 742                response = "x"
 743                while response != "y" and response != "n":
 744                    response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
 745            if response == "y":
 747                tmpFile = open(fileName, "rb")
 748                message = tmpFile.read()
 749                tmpFile.close()
 750                submitTemplate = message[:message.index(separatorLine)]
 751                if self.isWindows:
 752                    submitTemplate = submitTemplate.replace("\r\n", "\n")
 753                p4_write_pipe("submit -i", submitTemplate)
 754            else:
 755                for f in editedFiles:
 756                    p4_system("revert \"%s\"" % f);
 757                for f in filesToAdd:
 758                    p4_system("revert \"%s\"" % f);
 759                    system("rm %s" %f)
 760            os.remove(fileName)
 762        else:
 763            fileName = "submit.txt"
 764            file = open(fileName, "w+")
 765            file.write(self.prepareLogMessage(template, logMessage))
 766            file.close()
 767            print ("Perforce submit template written as %s. "
 768                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 769                   % (fileName, fileName))
 770    def run(self, args):
 772        if len(args) == 0:
 773            self.master = currentGitBranch()
 774            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 775                die("Detecting current git branch failed!")
 776        elif len(args) == 1:
 777            self.master = args[0]
 778        else:
 779            return False
 780        allowSubmit = gitConfig("git-p4.allowSubmit")
 782        if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
 783            die("%s is not in git-p4.allowSubmit" % self.master)
 784        [upstream, settings] = findUpstreamBranchPoint()
 786        self.depotPath = settings['depot-paths'][0]
 787        if len(self.origin) == 0:
 788            self.origin = upstream
 789        if self.verbose:
 791            print "Origin branch is " + self.origin
 792        if len(self.depotPath) == 0:
 794            print "Internal error: cannot locate perforce depot path from existing branches"
 795            sys.exit(128)
 796        self.clientPath = p4Where(self.depotPath)
 798        if len(self.clientPath) == 0:
 800            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
 801            sys.exit(128)
 802        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
 804        self.oldWorkingDirectory = os.getcwd()
 805        chdir(self.clientPath)
 807        print "Syncronizing p4 checkout..."
 808        p4_system("sync ...")
 809        self.check()
 811        commits = []
 813        for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
 814            commits.append(line.strip())
 815        commits.reverse()
 816        while len(commits) > 0:
 818            commit = commits[0]
 819            commits = commits[1:]
 820            self.applyCommit(commit)
 821            if not self.interactive:
 822                break
 823        if len(commits) == 0:
 825            print "All changes applied!"
 826            chdir(self.oldWorkingDirectory)
 827            sync = P4Sync()
 829            sync.run([])
 830            rebase = P4Rebase()
 832            rebase.rebase()
 833        return True
 835class P4Sync(Command):
 837    def __init__(self):
 838        Command.__init__(self)
 839        self.options = [
 840                optparse.make_option("--branch", dest="branch"),
 841                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 842                optparse.make_option("--changesfile", dest="changesFile"),
 843                optparse.make_option("--silent", dest="silent", action="store_true"),
 844                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
 845                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 846                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
 847                                     help="Import into refs/heads/ , not refs/remotes"),
 848                optparse.make_option("--max-changes", dest="maxChanges"),
 849                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
 850                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
 851                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
 852                                     help="Only sync files that are included in the Perforce Client Spec")
 853        ]
 854        self.description = """Imports from Perforce into a git repository.\n
 855    example:
 856    //depot/my/project/ -- to import the current head
 857    //depot/my/project/@all -- to import everything
 858    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 859    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 861        self.usage += " //depot/path[@revRange]"
 863        self.silent = False
 864        self.createdBranches = Set()
 865        self.committedChanges = Set()
 866        self.branch = ""
 867        self.detectBranches = False
 868        self.detectLabels = False
 869        self.changesFile = ""
 870        self.syncWithOrigin = True
 871        self.verbose = False
 872        self.importIntoRemotes = True
 873        self.maxChanges = ""
 874        self.isWindows = (platform.system() == "Windows")
 875        self.keepRepoPath = False
 876        self.depotPaths = None
 877        self.p4BranchesInGit = []
 878        self.cloneExclude = []
 879        self.useClientSpec = False
 880        self.clientSpecDirs = []
 881        if gitConfig("git-p4.syncFromOrigin") == "false":
 883            self.syncWithOrigin = False
 884    def extractFilesFromCommit(self, commit):
 886        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
 887                             for path in self.cloneExclude]
 888        files = []
 889        fnum = 0
 890        while commit.has_key("depotFile%s" % fnum):
 891            path =  commit["depotFile%s" % fnum]
 892            if [p for p in self.cloneExclude
 894                if path.startswith (p)]:
 895                found = False
 896            else:
 897                found = [p for p in self.depotPaths
 898                         if path.startswith (p)]
 899            if not found:
 900                fnum = fnum + 1
 901                continue
 902            file = {}
 904            file["path"] = path
 905            file["rev"] = commit["rev%s" % fnum]
 906            file["action"] = commit["action%s" % fnum]
 907            file["type"] = commit["type%s" % fnum]
 908            files.append(file)
 909            fnum = fnum + 1
 910        return files
 911    def stripRepoPath(self, path, prefixes):
 913        if self.keepRepoPath:
 914            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
 915        for p in prefixes:
 917            if path.startswith(p):
 918                path = path[len(p):]
 919        return path
 921    def splitFilesIntoBranches(self, commit):
 923        branches = {}
 924        fnum = 0
 925        while commit.has_key("depotFile%s" % fnum):
 926            path =  commit["depotFile%s" % fnum]
 927            found = [p for p in self.depotPaths
 928                     if path.startswith (p)]
 929            if not found:
 930                fnum = fnum + 1
 931                continue
 932            file = {}
 934            file["path"] = path
 935            file["rev"] = commit["rev%s" % fnum]
 936            file["action"] = commit["action%s" % fnum]
 937            file["type"] = commit["type%s" % fnum]
 938            fnum = fnum + 1
 939            relPath = self.stripRepoPath(path, self.depotPaths)
 941            for branch in self.knownBranches.keys():
 943                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
 945                if relPath.startswith(branch + "/"):
 946                    if branch not in branches:
 947                        branches[branch] = []
 948                    branches[branch].append(file)
 949                    break
 950        return branches
 952    ## Should move this out, doesn't use SELF.
 954    def readP4Files(self, files):
 955        filesForCommit = []
 956        filesToRead = []
 957        for f in files:
 959            includeFile = True
 960            for val in self.clientSpecDirs:
 961                if f['path'].startswith(val[0]):
 962                    if val[1] <= 0:
 963                        includeFile = False
 964                    break
 965            if includeFile:
 967                filesForCommit.append(f)
 968                if f['action'] not in ('delete', 'purge'):
 969                    filesToRead.append(f)
 970        filedata = []
 972        if len(filesToRead) > 0:
 973            filedata = p4CmdList('-x - print',
 974                                 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
 975                                                  for f in filesToRead]),
 976                                 stdin_mode='w+')
 977            if "p4ExitCode" in filedata[0]:
 979                die("Problems executing p4. Error: [%d]."
 980                    % (filedata[0]['p4ExitCode']));
 981        j = 0;
 983        contents = {}
 984        while j < len(filedata):
 985            stat = filedata[j]
 986            j += 1
 987            text = ''
 988            while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
 989                text += filedata[j]['data']
 990                del filedata[j]['data']
 991                j += 1
 992            if not stat.has_key('depotFile'):
 994                sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
 995                continue
 996            if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
 998                text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
 999            elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1000                text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text)
1001            contents[stat['depotFile']] = text
1003        for f in filesForCommit:
1005            path = f['path']
1006            if contents.has_key(path):
1007                f['data'] = contents[path]
1008        return filesForCommit
1010    def commit(self, details, files, branch, branchPrefixes, parent = ""):
1012        epoch = details["time"]
1013        author = details["user"]
1014        if self.verbose:
1016            print "commit into %s" % branch
1017        # start with reading files; if that fails, we should not
1019        # create a commit.
1020        new_files = []
1021        for f in files:
1022            if [p for p in branchPrefixes if f['path'].startswith(p)]:
1023                new_files.append (f)
1024            else:
1025                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
1026        files = self.readP4Files(new_files)
1027        self.gitStream.write("commit %s\n" % branch)
1029#        gitStream.write("mark :%s\n" % details["change"])
1030        self.committedChanges.add(int(details["change"]))
1031        committer = ""
1032        if author not in self.users:
1033            self.getUserMapFromPerforceServer()
1034        if author in self.users:
1035            committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1036        else:
1037            committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1038        self.gitStream.write("committer %s\n" % committer)
1040        self.gitStream.write("data <<EOT\n")
1042        self.gitStream.write(details["desc"])
1043        self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1044                             % (','.join (branchPrefixes), details["change"]))
1045        if len(details['options']) > 0:
1046            self.gitStream.write(": options = %s" % details['options'])
1047        self.gitStream.write("]\nEOT\n\n")
1048        if len(parent) > 0:
1050            if self.verbose:
1051                print "parent %s" % parent
1052            self.gitStream.write("from %s\n" % parent)
1053        for file in files:
1055            if file["type"] == "apple":
1056                print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1057                continue
1058            relPath = self.stripRepoPath(file['path'], branchPrefixes)
1060            if file["action"] in ("delete", "purge"):
1061                self.gitStream.write("D %s\n" % relPath)
1062            else:
1063                data = file['data']
1064                mode = "644"
1066                if isP4Exec(file["type"]):
1067                    mode = "755"
1068                elif file["type"] == "symlink":
1069                    mode = "120000"
1070                    # p4 print on a symlink contains "target\n", so strip it off
1071                    data = data[:-1]
1072                if self.isWindows and file["type"].endswith("text"):
1074                    data = data.replace("\r\n", "\n")
1075                self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1077                self.gitStream.write("data %s\n" % len(data))
1078                self.gitStream.write(data)
1079                self.gitStream.write("\n")
1080        self.gitStream.write("\n")
1082        change = int(details["change"])
1084        if self.labels.has_key(change):
1086            label = self.labels[change]
1087            labelDetails = label[0]
1088            labelRevisions = label[1]
1089            if self.verbose:
1090                print "Change %s is labelled %s" % (change, labelDetails)
1091            files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1093                                                    for p in branchPrefixes]))
1094            if len(files) == len(labelRevisions):
1096                cleanedFiles = {}
1098                for info in files:
1099                    if info["action"] in ("delete", "purge"):
1100                        continue
1101                    cleanedFiles[info["depotFile"]] = info["rev"]
1102                if cleanedFiles == labelRevisions:
1104                    self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1105                    self.gitStream.write("from %s\n" % branch)
1106                    owner = labelDetails["Owner"]
1108                    tagger = ""
1109                    if author in self.users:
1110                        tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1111                    else:
1112                        tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1113                    self.gitStream.write("tagger %s\n" % tagger)
1114                    self.gitStream.write("data <<EOT\n")
1115                    self.gitStream.write(labelDetails["Description"])
1116                    self.gitStream.write("EOT\n\n")
1117                else:
1119                    if not self.silent:
1120                        print ("Tag %s does not match with change %s: files do not match."
1121                               % (labelDetails["label"], change))
1122            else:
1124                if not self.silent:
1125                    print ("Tag %s does not match with change %s: file count is different."
1126                           % (labelDetails["label"], change))
1127    def getUserCacheFilename(self):
1129        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1130        return home + "/.gitp4-usercache.txt"
1131    def getUserMapFromPerforceServer(self):
1133        if self.userMapFromPerforceServer:
1134            return
1135        self.users = {}
1136        for output in p4CmdList("users"):
1138            if not output.has_key("User"):
1139                continue
1140            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1141        s = ''
1144        for (key, val) in self.users.items():
1145            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1146        open(self.getUserCacheFilename(), "wb").write(s)
1148        self.userMapFromPerforceServer = True
1149    def loadUserMapFromCache(self):
1151        self.users = {}
1152        self.userMapFromPerforceServer = False
1153        try:
1154            cache = open(self.getUserCacheFilename(), "rb")
1155            lines = cache.readlines()
1156            cache.close()
1157            for line in lines:
1158                entry = line.strip().split("\t")
1159                self.users[entry[0]] = entry[1]
1160        except IOError:
1161            self.getUserMapFromPerforceServer()
1162    def getLabels(self):
1164        self.labels = {}
1165        l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1167        if len(l) > 0 and not self.silent:
1168            print "Finding files belonging to labels in %s" % `self.depotPaths`
1169        for output in l:
1171            label = output["label"]
1172            revisions = {}
1173            newestChange = 0
1174            if self.verbose:
1175                print "Querying files for label %s" % label
1176            for file in p4CmdList("files "
1177                                  +  ' '.join (["%s...@%s" % (p, label)
1178                                                for p in self.depotPaths])):
1179                revisions[file["depotFile"]] = file["rev"]
1180                change = int(file["change"])
1181                if change > newestChange:
1182                    newestChange = change
1183            self.labels[newestChange] = [output, revisions]
1185        if self.verbose:
1187            print "Label changes: %s" % self.labels.keys()
1188    def guessProjectName(self):
1190        for p in self.depotPaths:
1191            if p.endswith("/"):
1192                p = p[:-1]
1193            p = p[p.strip().rfind("/") + 1:]
1194            if not p.endswith("/"):
1195               p += "/"
1196            return p
1197    def getBranchMapping(self):
1199        lostAndFoundBranches = set()
1200        for info in p4CmdList("branches"):
1202            details = p4Cmd("branch -o %s" % info["branch"])
1203            viewIdx = 0
1204            while details.has_key("View%s" % viewIdx):
1205                paths = details["View%s" % viewIdx].split(" ")
1206                viewIdx = viewIdx + 1
1207                # require standard //depot/foo/... //depot/bar/... mapping
1208                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1209                    continue
1210                source = paths[0]
1211                destination = paths[1]
1212                ## HACK
1213                if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1214                    source = source[len(self.depotPaths[0]):-4]
1215                    destination = destination[len(self.depotPaths[0]):-4]
1216                    if destination in self.knownBranches:
1218                        if not self.silent:
1219                            print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1220                            print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1221                        continue
1222                    self.knownBranches[destination] = source
1224                    lostAndFoundBranches.discard(destination)
1226                    if source not in self.knownBranches:
1228                        lostAndFoundBranches.add(source)
1229        for branch in lostAndFoundBranches:
1232            self.knownBranches[branch] = branch
1233    def getBranchMappingFromGitBranches(self):
1235        branches = p4BranchesInGit(self.importIntoRemotes)
1236        for branch in branches.keys():
1237            if branch == "master":
1238                branch = "main"
1239            else:
1240                branch = branch[len(self.projectName):]
1241            self.knownBranches[branch] = branch
1242    def listExistingP4GitBranches(self):
1244        # branches holds mapping from name to commit
1245        branches = p4BranchesInGit(self.importIntoRemotes)
1246        self.p4BranchesInGit = branches.keys()
1247        for branch in branches.keys():
1248            self.initialParents[self.refPrefix + branch] = branches[branch]
1249    def updateOptionDict(self, d):
1251        option_keys = {}
1252        if self.keepRepoPath:
1253            option_keys['keepRepoPath'] = 1
1254        d["options"] = ' '.join(sorted(option_keys.keys()))
1256    def readOptions(self, d):
1258        self.keepRepoPath = (d.has_key('options')
1259                             and ('keepRepoPath' in d['options']))
1260    def gitRefForBranch(self, branch):
1262        if branch == "main":
1263            return self.refPrefix + "master"
1264        if len(branch) <= 0:
1266            return branch
1267        return self.refPrefix + self.projectName + branch
1269    def gitCommitByP4Change(self, ref, change):
1271        if self.verbose:
1272            print "looking in ref " + ref + " for change %s using bisect..." % change
1273        earliestCommit = ""
1275        latestCommit = parseRevision(ref)
1276        while True:
1278            if self.verbose:
1279                print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1280            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1281            if len(next) == 0:
1282                if self.verbose:
1283                    print "argh"
1284                return ""
1285            log = extractLogMessageFromGitCommit(next)
1286            settings = extractSettingsGitLog(log)
1287            currentChange = int(settings['change'])
1288            if self.verbose:
1289                print "current change %s" % currentChange
1290            if currentChange == change:
1292                if self.verbose:
1293                    print "found %s" % next
1294                return next
1295            if currentChange < change:
1297                earliestCommit = "^%s" % next
1298            else:
1299                latestCommit = "%s" % next
1300        return ""
1302    def importNewBranch(self, branch, maxChange):
1304        # make fast-import flush all changes to disk and update the refs using the checkpoint
1305        # command so that we can try to find the branch parent in the git history
1306        self.gitStream.write("checkpoint\n\n");
1307        self.gitStream.flush();
1308        branchPrefix = self.depotPaths[0] + branch + "/"
1309        range = "@1,%s" % maxChange
1310        #print "prefix" + branchPrefix
1311        changes = p4ChangesForPaths([branchPrefix], range)
1312        if len(changes) <= 0:
1313            return False
1314        firstChange = changes[0]
1315        #print "first change in branch: %s" % firstChange
1316        sourceBranch = self.knownBranches[branch]
1317        sourceDepotPath = self.depotPaths[0] + sourceBranch
1318        sourceRef = self.gitRefForBranch(sourceBranch)
1319        #print "source " + sourceBranch
1320        branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1322        #print "branch parent: %s" % branchParentChange
1323        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1324        if len(gitParent) > 0:
1325            self.initialParents[self.gitRefForBranch(branch)] = gitParent
1326            #print "parent git commit: %s" % gitParent
1327        self.importChanges(changes)
1329        return True
1330    def importChanges(self, changes):
1332        cnt = 1
1333        for change in changes:
1334            description = p4Cmd("describe %s" % change)
1335            self.updateOptionDict(description)
1336            if not self.silent:
1338                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1339                sys.stdout.flush()
1340            cnt = cnt + 1
1341            try:
1343                if self.detectBranches:
1344                    branches = self.splitFilesIntoBranches(description)
1345                    for branch in branches.keys():
1346                        ## HACK  --hwn
1347                        branchPrefix = self.depotPaths[0] + branch + "/"
1348                        parent = ""
1350                        filesForCommit = branches[branch]
1352                        if self.verbose:
1354                            print "branch is %s" % branch
1355                        self.updatedBranches.add(branch)
1357                        if branch not in self.createdBranches:
1359                            self.createdBranches.add(branch)
1360                            parent = self.knownBranches[branch]
1361                            if parent == branch:
1362                                parent = ""
1363                            else:
1364                                fullBranch = self.projectName + branch
1365                                if fullBranch not in self.p4BranchesInGit:
1366                                    if not self.silent:
1367                                        print("\n    Importing new branch %s" % fullBranch);
1368                                    if self.importNewBranch(branch, change - 1):
1369                                        parent = ""
1370                                        self.p4BranchesInGit.append(fullBranch)
1371                                    if not self.silent:
1372                                        print("\n    Resuming with change %s" % change);
1373                                if self.verbose:
1375                                    print "parent determined through known branches: %s" % parent
1376                        branch = self.gitRefForBranch(branch)
1378                        parent = self.gitRefForBranch(parent)
1379                        if self.verbose:
1381                            print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1382                        if len(parent) == 0 and branch in self.initialParents:
1384                            parent = self.initialParents[branch]
1385                            del self.initialParents[branch]
1386                        self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1388                else:
1389                    files = self.extractFilesFromCommit(description)
1390                    self.commit(description, files, self.branch, self.depotPaths,
1391                                self.initialParent)
1392                    self.initialParent = ""
1393            except IOError:
1394                print self.gitError.read()
1395                sys.exit(1)
1396    def importHeadRevision(self, revision):
1398        print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1399        details = { "user" : "git perforce import user", "time" : int(time.time()) }
1401        details["desc"] = ("Initial import of %s from the state at revision %s"
1402                           % (' '.join(self.depotPaths), revision))
1403        details["change"] = revision
1404        newestRevision = 0
1405        fileCnt = 0
1407        for info in p4CmdList("files "
1408                              +  ' '.join(["%s...%s"
1409                                           % (p, revision)
1410                                           for p in self.depotPaths])):
1411            if info['code'] == 'error':
1413                sys.stderr.write("p4 returned an error: %s\n"
1414                                 % info['data'])
1415                sys.exit(1)
1416            change = int(info["change"])
1419            if change > newestRevision:
1420                newestRevision = change
1421            if info["action"] in ("delete", "purge"):
1423                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1424                #fileCnt = fileCnt + 1
1425                continue
1426            for prop in ["depotFile", "rev", "action", "type" ]:
1428                details["%s%s" % (prop, fileCnt)] = info[prop]
1429            fileCnt = fileCnt + 1
1431        details["change"] = newestRevision
1433        self.updateOptionDict(details)
1434        try:
1435            self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1436        except IOError:
1437            print "IO error with git fast-import. Is your git version recent enough?"
1438            print self.gitError.read()
1439    def getClientSpec(self):
1442        specList = p4CmdList( "client -o" )
1443        temp = {}
1444        for entry in specList:
1445            for k,v in entry.iteritems():
1446                if k.startswith("View"):
1447                    if v.startswith('"'):
1448                        start = 1
1449                    else:
1450                        start = 0
1451                    index = v.find("...")
1452                    v = v[start:index]
1453                    if v.startswith("-"):
1454                        v = v[1:]
1455                        temp[v] = -len(v)
1456                    else:
1457                        temp[v] = len(v)
1458        self.clientSpecDirs = temp.items()
1459        self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1460    def run(self, args):
1462        self.depotPaths = []
1463        self.changeRange = ""
1464        self.initialParent = ""
1465        self.previousDepotPaths = []
1466        # map from branch depot path to parent branch
1468        self.knownBranches = {}
1469        self.initialParents = {}
1470        self.hasOrigin = originP4BranchesExist()
1471        if not self.syncWithOrigin:
1472            self.hasOrigin = False
1473        if self.importIntoRemotes:
1475            self.refPrefix = "refs/remotes/p4/"
1476        else:
1477            self.refPrefix = "refs/heads/p4/"
1478        if self.syncWithOrigin and self.hasOrigin:
1480            if not self.silent:
1481                print "Syncing with origin first by calling git fetch origin"
1482            system("git fetch origin")
1483        if len(self.branch) == 0:
1485            self.branch = self.refPrefix + "master"
1486            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1487                system("git update-ref %s refs/heads/p4" % self.branch)
1488                system("git branch -D p4");
1489            # create it /after/ importing, when master exists
1490            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1491                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1492        if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1494            self.getClientSpec()
1495        # TODO: should always look at previous commits,
1497        # merge with previous imports, if possible.
1498        if args == []:
1499            if self.hasOrigin:
1500                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1501            self.listExistingP4GitBranches()
1502            if len(self.p4BranchesInGit) > 1:
1504                if not self.silent:
1505                    print "Importing from/into multiple branches"
1506                self.detectBranches = True
1507            if self.verbose:
1509                print "branches: %s" % self.p4BranchesInGit
1510            p4Change = 0
1512            for branch in self.p4BranchesInGit:
1513                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1514                settings = extractSettingsGitLog(logMsg)
1516                self.readOptions(settings)
1518                if (settings.has_key('depot-paths')
1519                    and settings.has_key ('change')):
1520                    change = int(settings['change']) + 1
1521                    p4Change = max(p4Change, change)
1522                    depotPaths = sorted(settings['depot-paths'])
1524                    if self.previousDepotPaths == []:
1525                        self.previousDepotPaths = depotPaths
1526                    else:
1527                        paths = []
1528                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1529                            for i in range(0, min(len(cur), len(prev))):
1530                                if cur[i] <> prev[i]:
1531                                    i = i - 1
1532                                    break
1533                            paths.append (cur[:i + 1])
1535                        self.previousDepotPaths = paths
1537            if p4Change > 0:
1539                self.depotPaths = sorted(self.previousDepotPaths)
1540                self.changeRange = "@%s,#head" % p4Change
1541                if not self.detectBranches:
1542                    self.initialParent = parseRevision(self.branch)
1543                if not self.silent and not self.detectBranches:
1544                    print "Performing incremental import into %s git branch" % self.branch
1545        if not self.branch.startswith("refs/"):
1547            self.branch = "refs/heads/" + self.branch
1548        if len(args) == 0 and self.depotPaths:
1550            if not self.silent:
1551                print "Depot paths: %s" % ' '.join(self.depotPaths)
1552        else:
1553            if self.depotPaths and self.depotPaths != args:
1554                print ("previous import used depot path %s and now %s was specified. "
1555                       "This doesn't work!" % (' '.join (self.depotPaths),
1556                                               ' '.join (args)))
1557                sys.exit(1)
1558            self.depotPaths = sorted(args)
1560        revision = ""
1562        self.users = {}
1563        newPaths = []
1565        for p in self.depotPaths:
1566            if p.find("@") != -1:
1567                atIdx = p.index("@")
1568                self.changeRange = p[atIdx:]
1569                if self.changeRange == "@all":
1570                    self.changeRange = ""
1571                elif ',' not in self.changeRange:
1572                    revision = self.changeRange
1573                    self.changeRange = ""
1574                p = p[:atIdx]
1575            elif p.find("#") != -1:
1576                hashIdx = p.index("#")
1577                revision = p[hashIdx:]
1578                p = p[:hashIdx]
1579            elif self.previousDepotPaths == []:
1580                revision = "#head"
1581            p = re.sub ("\.\.\.$", "", p)
1583            if not p.endswith("/"):
1584                p += "/"
1585            newPaths.append(p)
1587        self.depotPaths = newPaths
1589        self.loadUserMapFromCache()
1592        self.labels = {}
1593        if self.detectLabels:
1594            self.getLabels();
1595        if self.detectBranches:
1597            ## FIXME - what's a P4 projectName ?
1598            self.projectName = self.guessProjectName()
1599            if self.hasOrigin:
1601                self.getBranchMappingFromGitBranches()
1602            else:
1603                self.getBranchMapping()
1604            if self.verbose:
1605                print "p4-git branches: %s" % self.p4BranchesInGit
1606                print "initial parents: %s" % self.initialParents
1607            for b in self.p4BranchesInGit:
1608                if b != "master":
1609                    ## FIXME
1611                    b = b[len(self.projectName):]
1612                self.createdBranches.add(b)
1613        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1615        importProcess = subprocess.Popen(["git", "fast-import"],
1617                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1618                                         stderr=subprocess.PIPE);
1619        self.gitOutput = importProcess.stdout
1620        self.gitStream = importProcess.stdin
1621        self.gitError = importProcess.stderr
1622        if revision:
1624            self.importHeadRevision(revision)
1625        else:
1626            changes = []
1627            if len(self.changesFile) > 0:
1629                output = open(self.changesFile).readlines()
1630                changeSet = Set()
1631                for line in output:
1632                    changeSet.add(int(line))
1633                for change in changeSet:
1635                    changes.append(change)
1636                changes.sort()
1638            else:
1639                if self.verbose:
1640                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1641                                                              self.changeRange)
1642                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1643                if len(self.maxChanges) > 0:
1645                    changes = changes[:min(int(self.maxChanges), len(changes))]
1646            if len(changes) == 0:
1648                if not self.silent:
1649                    print "No changes to import!"
1650                return True
1651            if not self.silent and not self.detectBranches:
1653                print "Import destination: %s" % self.branch
1654            self.updatedBranches = set()
1656            self.importChanges(changes)
1658            if not self.silent:
1660                print ""
1661                if len(self.updatedBranches) > 0:
1662                    sys.stdout.write("Updated branches: ")
1663                    for b in self.updatedBranches:
1664                        sys.stdout.write("%s " % b)
1665                    sys.stdout.write("\n")
1666        self.gitStream.close()
1668        if importProcess.wait() != 0:
1669            die("fast-import failed: %s" % self.gitError.read())
1670        self.gitOutput.close()
1671        self.gitError.close()
1672        return True
1674class P4Rebase(Command):
1676    def __init__(self):
1677        Command.__init__(self)
1678        self.options = [ ]
1679        self.description = ("Fetches the latest revision from perforce and "
1680                            + "rebases the current work (branch) against it")
1681        self.verbose = False
1682    def run(self, args):
1684        sync = P4Sync()
1685        sync.run([])
1686        return self.rebase()
1688    def rebase(self):
1690        if os.system("git update-index --refresh") != 0:
1691            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.");
1692        if len(read_pipe("git diff-index HEAD --")) > 0:
1693            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1694        [upstream, settings] = findUpstreamBranchPoint()
1696        if len(upstream) == 0:
1697            die("Cannot find upstream branchpoint for rebase")
1698        # the branchpoint may be p4/foo~3, so strip off the parent
1700        upstream = re.sub("~[0-9]+$", "", upstream)
1701        print "Rebasing the current branch onto %s" % upstream
1703        oldHead = read_pipe("git rev-parse HEAD").strip()
1704        system("git rebase %s" % upstream)
1705        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1706        return True
1707class P4Clone(P4Sync):
1709    def __init__(self):
1710        P4Sync.__init__(self)
1711        self.description = "Creates a new git repository and imports from Perforce into it"
1712        self.usage = "usage: %prog [options] //depot/path[@revRange]"
1713        self.options += [
1714            optparse.make_option("--destination", dest="cloneDestination",
1715                                 action='store', default=None,
1716                                 help="where to leave result of the clone"),
1717            optparse.make_option("-/", dest="cloneExclude",
1718                                 action="append", type="string",
1719                                 help="exclude depot path")
1720        ]
1721        self.cloneDestination = None
1722        self.needsGit = False
1723    # This is required for the "append" cloneExclude action
1725    def ensure_value(self, attr, value):
1726        if not hasattr(self, attr) or getattr(self, attr) is None:
1727            setattr(self, attr, value)
1728        return getattr(self, attr)
1729    def defaultDestination(self, args):
1731        ## TODO: use common prefix of args?
1732        depotPath = args[0]
1733        depotDir = re.sub("(@[^@]*)$", "", depotPath)
1734        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1735        depotDir = re.sub(r"\.\.\.$", "", depotDir)
1736        depotDir = re.sub(r"/$", "", depotDir)
1737        return os.path.split(depotDir)[1]
1738    def run(self, args):
1740        if len(args) < 1:
1741            return False
1742        if self.keepRepoPath and not self.cloneDestination:
1744            sys.stderr.write("Must specify destination for --keep-path\n")
1745            sys.exit(1)
1746        depotPaths = args
1748        if not self.cloneDestination and len(depotPaths) > 1:
1750            self.cloneDestination = depotPaths[-1]
1751            depotPaths = depotPaths[:-1]
1752        self.cloneExclude = ["/"+p for p in self.cloneExclude]
1754        for p in depotPaths:
1755            if not p.startswith("//"):
1756                return False
1757        if not self.cloneDestination:
1759            self.cloneDestination = self.defaultDestination(args)
1760        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1762        if not os.path.exists(self.cloneDestination):
1763            os.makedirs(self.cloneDestination)
1764        chdir(self.cloneDestination)
1765        system("git init")
1766        self.gitdir = os.getcwd() + "/.git"
1767        if not P4Sync.run(self, depotPaths):
1768            return False
1769        if self.branch != "master":
1770            if self.importIntoRemotes:
1771                masterbranch = "refs/remotes/p4/master"
1772            else:
1773                masterbranch = "refs/heads/p4/master"
1774            if gitBranchExists(masterbranch):
1775                system("git branch master %s" % masterbranch)
1776                system("git checkout -f")
1777            else:
1778                print "Could not detect main branch. No checkout/master branch created."
1779        return True
1781class P4Branches(Command):
1783    def __init__(self):
1784        Command.__init__(self)
1785        self.options = [ ]
1786        self.description = ("Shows the git branches that hold imports and their "
1787                            + "corresponding perforce depot paths")
1788        self.verbose = False
1789    def run(self, args):
1791        if originP4BranchesExist():
1792            createOrUpdateBranchesFromOrigin()
1793        cmdline = "git rev-parse --symbolic "
1795        cmdline += " --remotes"
1796        for line in read_pipe_lines(cmdline):
1798            line = line.strip()
1799            if not line.startswith('p4/') or line == "p4/HEAD":
1801                continue
1802            branch = line
1803            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1805            settings = extractSettingsGitLog(log)
1806            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1808        return True
1809class HelpFormatter(optparse.IndentedHelpFormatter):
1811    def __init__(self):
1812        optparse.IndentedHelpFormatter.__init__(self)
1813    def format_description(self, description):
1815        if description:
1816            return description + "\n"
1817        else:
1818            return ""
1819def printUsage(commands):
1821    print "usage: %s <command> [options]" % sys.argv[0]
1822    print ""
1823    print "valid commands: %s" % ", ".join(commands)
1824    print ""
1825    print "Try %s <command> --help for command specific help." % sys.argv[0]
1826    print ""
1827commands = {
1829    "debug" : P4Debug,
1830    "submit" : P4Submit,
1831    "commit" : P4Submit,
1832    "sync" : P4Sync,
1833    "rebase" : P4Rebase,
1834    "clone" : P4Clone,
1835    "rollback" : P4RollBack,
1836    "branches" : P4Branches
1837}
1838def main():
1841    if len(sys.argv[1:]) == 0:
1842        printUsage(commands.keys())
1843        sys.exit(2)
1844    cmd = ""
1846    cmdName = sys.argv[1]
1847    try:
1848        klass = commands[cmdName]
1849        cmd = klass()
1850    except KeyError:
1851        print "unknown command %s" % cmdName
1852        print ""
1853        printUsage(commands.keys())
1854        sys.exit(2)
1855    options = cmd.options
1857    cmd.gitdir = os.environ.get("GIT_DIR", None)
1858    args = sys.argv[2:]
1860    if len(options) > 0:
1862        options.append(optparse.make_option("--git-dir", dest="gitdir"))
1863        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1865                                       options,
1866                                       description = cmd.description,
1867                                       formatter = HelpFormatter())
1868        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1870    global verbose
1871    verbose = cmd.verbose
1872    if cmd.needsGit:
1873        if cmd.gitdir == None:
1874            cmd.gitdir = os.path.abspath(".git")
1875            if not isValidGitDir(cmd.gitdir):
1876                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1877                if os.path.exists(cmd.gitdir):
1878                    cdup = read_pipe("git rev-parse --show-cdup").strip()
1879                    if len(cdup) > 0:
1880                        chdir(cdup);
1881        if not isValidGitDir(cmd.gitdir):
1883            if isValidGitDir(cmd.gitdir + "/.git"):
1884                cmd.gitdir += "/.git"
1885            else:
1886                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1887        os.environ["GIT_DIR"] = cmd.gitdir
1889    if not cmd.run(args):
1891        parser.print_help()
1892if __name__ == '__main__':
1895    main()