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