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