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