contrib / fast-import / git-p4on commit git-p4: support exclude paths (354081d)
   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
  19def die(msg):
  20    if verbose:
  21        raise Exception(msg)
  22    else:
  23        sys.stderr.write(msg + "\n")
  24        sys.exit(1)
  25
  26def write_pipe(c, str):
  27    if verbose:
  28        sys.stderr.write('Writing pipe: %s\n' % c)
  29
  30    pipe = os.popen(c, 'w')
  31    val = pipe.write(str)
  32    if pipe.close():
  33        die('Command failed: %s' % c)
  34
  35    return val
  36
  37def read_pipe(c, ignore_error=False):
  38    if verbose:
  39        sys.stderr.write('Reading pipe: %s\n' % c)
  40
  41    pipe = os.popen(c, 'rb')
  42    val = pipe.read()
  43    if pipe.close() and not ignore_error:
  44        die('Command failed: %s' % c)
  45
  46    return val
  47
  48
  49def read_pipe_lines(c):
  50    if verbose:
  51        sys.stderr.write('Reading pipe: %s\n' % c)
  52    ## todo: check return status
  53    pipe = os.popen(c, 'rb')
  54    val = pipe.readlines()
  55    if pipe.close():
  56        die('Command failed: %s' % c)
  57
  58    return val
  59
  60def system(cmd):
  61    if verbose:
  62        sys.stderr.write("executing %s\n" % cmd)
  63    if os.system(cmd) != 0:
  64        die("command failed: %s" % cmd)
  65
  66def isP4Exec(kind):
  67    """Determine if a Perforce 'kind' should have execute permission
  68
  69    'p4 help filetypes' gives a list of the types.  If it starts with 'x',
  70    or x follows one of a few letters.  Otherwise, if there is an 'x' after
  71    a plus sign, it is also executable"""
  72    return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
  73
  74def setP4ExecBit(file, mode):
  75    # Reopens an already open file and changes the execute bit to match
  76    # the execute bit setting in the passed in mode.
  77
  78    p4Type = "+x"
  79
  80    if not isModeExec(mode):
  81        p4Type = getP4OpenedType(file)
  82        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
  83        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
  84        if p4Type[-1] == "+":
  85            p4Type = p4Type[0:-1]
  86
  87    system("p4 reopen -t %s %s" % (p4Type, file))
  88
  89def getP4OpenedType(file):
  90    # Returns the perforce file type for the given file.
  91
  92    result = read_pipe("p4 opened %s" % file)
  93    match = re.match(".*\((.+)\)$", result)
  94    if match:
  95        return match.group(1)
  96    else:
  97        die("Could not determine file type for %s" % file)
  98
  99def diffTreePattern():
 100    # This is a simple generator for the diff tree regex pattern. This could be
 101    # a class variable if this and parseDiffTreeEntry were a part of a class.
 102    pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
 103    while True:
 104        yield pattern
 105
 106def parseDiffTreeEntry(entry):
 107    """Parses a single diff tree entry into its component elements.
 108
 109    See git-diff-tree(1) manpage for details about the format of the diff
 110    output. This method returns a dictionary with the following elements:
 111
 112    src_mode - The mode of the source file
 113    dst_mode - The mode of the destination file
 114    src_sha1 - The sha1 for the source file
 115    dst_sha1 - The sha1 fr the destination file
 116    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
 117    status_score - The score for the status (applicable for 'C' and 'R'
 118                   statuses). This is None if there is no score.
 119    src - The path for the source file.
 120    dst - The path for the destination file. This is only present for
 121          copy or renames. If it is not present, this is None.
 122
 123    If the pattern is not matched, None is returned."""
 124
 125    match = diffTreePattern().next().match(entry)
 126    if match:
 127        return {
 128            'src_mode': match.group(1),
 129            'dst_mode': match.group(2),
 130            'src_sha1': match.group(3),
 131            'dst_sha1': match.group(4),
 132            'status': match.group(5),
 133            'status_score': match.group(6),
 134            'src': match.group(7),
 135            'dst': match.group(10)
 136        }
 137    return None
 138
 139def isModeExec(mode):
 140    # Returns True if the given git mode represents an executable file,
 141    # otherwise False.
 142    return mode[-3:] == "755"
 143
 144def isModeExecChanged(src_mode, dst_mode):
 145    return isModeExec(src_mode) != isModeExec(dst_mode)
 146
 147def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
 148    cmd = "p4 -G %s" % cmd
 149    if verbose:
 150        sys.stderr.write("Opening pipe: %s\n" % cmd)
 151
 152    # Use a temporary file to avoid deadlocks without
 153    # subprocess.communicate(), which would put another copy
 154    # of stdout into memory.
 155    stdin_file = None
 156    if stdin is not None:
 157        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
 158        stdin_file.write(stdin)
 159        stdin_file.flush()
 160        stdin_file.seek(0)
 161
 162    p4 = subprocess.Popen(cmd, shell=True,
 163                          stdin=stdin_file,
 164                          stdout=subprocess.PIPE)
 165
 166    result = []
 167    try:
 168        while True:
 169            entry = marshal.load(p4.stdout)
 170            result.append(entry)
 171    except EOFError:
 172        pass
 173    exitCode = p4.wait()
 174    if exitCode != 0:
 175        entry = {}
 176        entry["p4ExitCode"] = exitCode
 177        result.append(entry)
 178
 179    return result
 180
 181def p4Cmd(cmd):
 182    list = p4CmdList(cmd)
 183    result = {}
 184    for entry in list:
 185        result.update(entry)
 186    return result;
 187
 188def p4Where(depotPath):
 189    if not depotPath.endswith("/"):
 190        depotPath += "/"
 191    output = p4Cmd("where %s..." % depotPath)
 192    if output["code"] == "error":
 193        return ""
 194    clientPath = ""
 195    if "path" in output:
 196        clientPath = output.get("path")
 197    elif "data" in output:
 198        data = output.get("data")
 199        lastSpace = data.rfind(" ")
 200        clientPath = data[lastSpace + 1:]
 201
 202    if clientPath.endswith("..."):
 203        clientPath = clientPath[:-3]
 204    return clientPath
 205
 206def currentGitBranch():
 207    return read_pipe("git name-rev HEAD").split(" ")[1].strip()
 208
 209def isValidGitDir(path):
 210    if (os.path.exists(path + "/HEAD")
 211        and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
 212        return True;
 213    return False
 214
 215def parseRevision(ref):
 216    return read_pipe("git rev-parse %s" % ref).strip()
 217
 218def extractLogMessageFromGitCommit(commit):
 219    logMessage = ""
 220
 221    ## fixme: title is first line of commit, not 1st paragraph.
 222    foundTitle = False
 223    for log in read_pipe_lines("git cat-file commit %s" % commit):
 224       if not foundTitle:
 225           if len(log) == 1:
 226               foundTitle = True
 227           continue
 228
 229       logMessage += log
 230    return logMessage
 231
 232def extractSettingsGitLog(log):
 233    values = {}
 234    for line in log.split("\n"):
 235        line = line.strip()
 236        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
 237        if not m:
 238            continue
 239
 240        assignments = m.group(1).split (':')
 241        for a in assignments:
 242            vals = a.split ('=')
 243            key = vals[0].strip()
 244            val = ('='.join (vals[1:])).strip()
 245            if val.endswith ('\"') and val.startswith('"'):
 246                val = val[1:-1]
 247
 248            values[key] = val
 249
 250    paths = values.get("depot-paths")
 251    if not paths:
 252        paths = values.get("depot-path")
 253    if paths:
 254        values['depot-paths'] = paths.split(',')
 255    return values
 256
 257def gitBranchExists(branch):
 258    proc = subprocess.Popen(["git", "rev-parse", branch],
 259                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
 260    return proc.wait() == 0;
 261
 262def gitConfig(key):
 263    return read_pipe("git config %s" % key, ignore_error=True).strip()
 264
 265def p4BranchesInGit(branchesAreInRemotes = True):
 266    branches = {}
 267
 268    cmdline = "git rev-parse --symbolic "
 269    if branchesAreInRemotes:
 270        cmdline += " --remotes"
 271    else:
 272        cmdline += " --branches"
 273
 274    for line in read_pipe_lines(cmdline):
 275        line = line.strip()
 276
 277        ## only import to p4/
 278        if not line.startswith('p4/') or line == "p4/HEAD":
 279            continue
 280        branch = line
 281
 282        # strip off p4
 283        branch = re.sub ("^p4/", "", line)
 284
 285        branches[branch] = parseRevision(line)
 286    return branches
 287
 288def findUpstreamBranchPoint(head = "HEAD"):
 289    branches = p4BranchesInGit()
 290    # map from depot-path to branch name
 291    branchByDepotPath = {}
 292    for branch in branches.keys():
 293        tip = branches[branch]
 294        log = extractLogMessageFromGitCommit(tip)
 295        settings = extractSettingsGitLog(log)
 296        if settings.has_key("depot-paths"):
 297            paths = ",".join(settings["depot-paths"])
 298            branchByDepotPath[paths] = "remotes/p4/" + branch
 299
 300    settings = None
 301    parent = 0
 302    while parent < 65535:
 303        commit = head + "~%s" % parent
 304        log = extractLogMessageFromGitCommit(commit)
 305        settings = extractSettingsGitLog(log)
 306        if settings.has_key("depot-paths"):
 307            paths = ",".join(settings["depot-paths"])
 308            if branchByDepotPath.has_key(paths):
 309                return [branchByDepotPath[paths], settings]
 310
 311        parent = parent + 1
 312
 313    return ["", settings]
 314
 315def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
 316    if not silent:
 317        print ("Creating/updating branch(es) in %s based on origin branch(es)"
 318               % localRefPrefix)
 319
 320    originPrefix = "origin/p4/"
 321
 322    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 323        line = line.strip()
 324        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
 325            continue
 326
 327        headName = line[len(originPrefix):]
 328        remoteHead = localRefPrefix + headName
 329        originHead = line
 330
 331        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
 332        if (not original.has_key('depot-paths')
 333            or not original.has_key('change')):
 334            continue
 335
 336        update = False
 337        if not gitBranchExists(remoteHead):
 338            if verbose:
 339                print "creating %s" % remoteHead
 340            update = True
 341        else:
 342            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
 343            if settings.has_key('change') > 0:
 344                if settings['depot-paths'] == original['depot-paths']:
 345                    originP4Change = int(original['change'])
 346                    p4Change = int(settings['change'])
 347                    if originP4Change > p4Change:
 348                        print ("%s (%s) is newer than %s (%s). "
 349                               "Updating p4 branch from origin."
 350                               % (originHead, originP4Change,
 351                                  remoteHead, p4Change))
 352                        update = True
 353                else:
 354                    print ("Ignoring: %s was imported from %s while "
 355                           "%s was imported from %s"
 356                           % (originHead, ','.join(original['depot-paths']),
 357                              remoteHead, ','.join(settings['depot-paths'])))
 358
 359        if update:
 360            system("git update-ref %s %s" % (remoteHead, originHead))
 361
 362def originP4BranchesExist():
 363        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
 364
 365def p4ChangesForPaths(depotPaths, changeRange):
 366    assert depotPaths
 367    output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange)
 368                                                        for p in depotPaths]))
 369
 370    changes = []
 371    for line in output:
 372        changeNum = line.split(" ")[1]
 373        changes.append(int(changeNum))
 374
 375    changes.sort()
 376    return changes
 377
 378class Command:
 379    def __init__(self):
 380        self.usage = "usage: %prog [options]"
 381        self.needsGit = True
 382
 383class P4Debug(Command):
 384    def __init__(self):
 385        Command.__init__(self)
 386        self.options = [
 387            optparse.make_option("--verbose", dest="verbose", action="store_true",
 388                                 default=False),
 389            ]
 390        self.description = "A tool to debug the output of p4 -G."
 391        self.needsGit = False
 392        self.verbose = False
 393
 394    def run(self, args):
 395        j = 0
 396        for output in p4CmdList(" ".join(args)):
 397            print 'Element: %d' % j
 398            j += 1
 399            print output
 400        return True
 401
 402class P4RollBack(Command):
 403    def __init__(self):
 404        Command.__init__(self)
 405        self.options = [
 406            optparse.make_option("--verbose", dest="verbose", action="store_true"),
 407            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 408        ]
 409        self.description = "A tool to debug the multi-branch import. Don't use :)"
 410        self.verbose = False
 411        self.rollbackLocalBranches = False
 412
 413    def run(self, args):
 414        if len(args) != 1:
 415            return False
 416        maxChange = int(args[0])
 417
 418        if "p4ExitCode" in p4Cmd("changes -m 1"):
 419            die("Problems executing p4");
 420
 421        if self.rollbackLocalBranches:
 422            refPrefix = "refs/heads/"
 423            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 424        else:
 425            refPrefix = "refs/remotes/"
 426            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 427
 428        for line in lines:
 429            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 430                line = line.strip()
 431                ref = refPrefix + line
 432                log = extractLogMessageFromGitCommit(ref)
 433                settings = extractSettingsGitLog(log)
 434
 435                depotPaths = settings['depot-paths']
 436                change = settings['change']
 437
 438                changed = False
 439
 440                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 441                                                           for p in depotPaths]))) == 0:
 442                    print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
 443                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 444                    continue
 445
 446                while change and int(change) > maxChange:
 447                    changed = True
 448                    if self.verbose:
 449                        print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
 450                    system("git update-ref %s \"%s^\"" % (ref, ref))
 451                    log = extractLogMessageFromGitCommit(ref)
 452                    settings =  extractSettingsGitLog(log)
 453
 454
 455                    depotPaths = settings['depot-paths']
 456                    change = settings['change']
 457
 458                if changed:
 459                    print "%s rewound to %s" % (ref, change)
 460
 461        return True
 462
 463class P4Submit(Command):
 464    def __init__(self):
 465        Command.__init__(self)
 466        self.options = [
 467                optparse.make_option("--continue", action="store_false", dest="firstTime"),
 468                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 469                optparse.make_option("--origin", dest="origin"),
 470                optparse.make_option("--reset", action="store_true", dest="reset"),
 471                optparse.make_option("--log-substitutions", dest="substFile"),
 472                optparse.make_option("--direct", dest="directSubmit", action="store_true"),
 473                optparse.make_option("-M", dest="detectRename", action="store_true"),
 474        ]
 475        self.description = "Submit changes from git to the perforce depot."
 476        self.usage += " [name of git branch to submit into perforce depot]"
 477        self.firstTime = True
 478        self.reset = False
 479        self.interactive = True
 480        self.substFile = ""
 481        self.firstTime = True
 482        self.origin = ""
 483        self.directSubmit = False
 484        self.detectRename = False
 485        self.verbose = False
 486        self.isWindows = (platform.system() == "Windows")
 487
 488        self.logSubstitutions = {}
 489        self.logSubstitutions["<enter description here>"] = "%log%"
 490        self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
 491
 492    def check(self):
 493        if len(p4CmdList("opened ...")) > 0:
 494            die("You have files opened with perforce! Close them before starting the sync.")
 495
 496    def start(self):
 497        if len(self.config) > 0 and not self.reset:
 498            die("Cannot start sync. Previous sync config found at %s\n"
 499                "If you want to start submitting again from scratch "
 500                "maybe you want to call git-p4 submit --reset" % self.configFile)
 501
 502        commits = []
 503        if self.directSubmit:
 504            commits.append("0")
 505        else:
 506            for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
 507                commits.append(line.strip())
 508            commits.reverse()
 509
 510        self.config["commits"] = commits
 511
 512    def prepareLogMessage(self, template, message):
 513        result = ""
 514
 515        for line in template.split("\n"):
 516            if line.startswith("#"):
 517                result += line + "\n"
 518                continue
 519
 520            substituted = False
 521            for key in self.logSubstitutions.keys():
 522                if line.find(key) != -1:
 523                    value = self.logSubstitutions[key]
 524                    value = value.replace("%log%", message)
 525                    if value != "@remove@":
 526                        result += line.replace(key, value) + "\n"
 527                    substituted = True
 528                    break
 529
 530            if not substituted:
 531                result += line + "\n"
 532
 533        return result
 534
 535    def prepareSubmitTemplate(self):
 536        # remove lines in the Files section that show changes to files outside the depot path we're committing into
 537        template = ""
 538        inFilesSection = False
 539        for line in read_pipe_lines("p4 change -o"):
 540            if inFilesSection:
 541                if line.startswith("\t"):
 542                    # path starts and ends with a tab
 543                    path = line[1:]
 544                    lastTab = path.rfind("\t")
 545                    if lastTab != -1:
 546                        path = path[:lastTab]
 547                        if not path.startswith(self.depotPath):
 548                            continue
 549                else:
 550                    inFilesSection = False
 551            else:
 552                if line.startswith("Files:"):
 553                    inFilesSection = True
 554
 555            template += line
 556
 557        return template
 558
 559    def applyCommit(self, id):
 560        if self.directSubmit:
 561            print "Applying local change in working directory/index"
 562            diff = self.diffStatus
 563        else:
 564            print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
 565            diffOpts = ("", "-M")[self.detectRename]
 566            diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
 567        filesToAdd = set()
 568        filesToDelete = set()
 569        editedFiles = set()
 570        filesToChangeExecBit = {}
 571        for line in diff:
 572            diff = parseDiffTreeEntry(line)
 573            modifier = diff['status']
 574            path = diff['src']
 575            if modifier == "M":
 576                system("p4 edit \"%s\"" % path)
 577                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 578                    filesToChangeExecBit[path] = diff['dst_mode']
 579                editedFiles.add(path)
 580            elif modifier == "A":
 581                filesToAdd.add(path)
 582                filesToChangeExecBit[path] = diff['dst_mode']
 583                if path in filesToDelete:
 584                    filesToDelete.remove(path)
 585            elif modifier == "D":
 586                filesToDelete.add(path)
 587                if path in filesToAdd:
 588                    filesToAdd.remove(path)
 589            elif modifier == "R":
 590                src, dest = diff['src'], diff['dst']
 591                system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
 592                system("p4 edit \"%s\"" % (dest))
 593                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
 594                    filesToChangeExecBit[dest] = diff['dst_mode']
 595                os.unlink(dest)
 596                editedFiles.add(dest)
 597                filesToDelete.add(src)
 598            else:
 599                die("unknown modifier %s for %s" % (modifier, path))
 600
 601        if self.directSubmit:
 602            diffcmd = "cat \"%s\"" % self.diffFile
 603        else:
 604            diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
 605        patchcmd = diffcmd + " | git apply "
 606        tryPatchCmd = patchcmd + "--check -"
 607        applyPatchCmd = patchcmd + "--check --apply -"
 608
 609        if os.system(tryPatchCmd) != 0:
 610            print "Unfortunately applying the change failed!"
 611            print "What do you want to do?"
 612            response = "x"
 613            while response != "s" and response != "a" and response != "w":
 614                response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
 615                                     "and with .rej files / [w]rite the patch to a file (patch.txt) ")
 616            if response == "s":
 617                print "Skipping! Good luck with the next patches..."
 618                for f in editedFiles:
 619                    system("p4 revert \"%s\"" % f);
 620                for f in filesToAdd:
 621                    system("rm %s" %f)
 622                return
 623            elif response == "a":
 624                os.system(applyPatchCmd)
 625                if len(filesToAdd) > 0:
 626                    print "You may also want to call p4 add on the following files:"
 627                    print " ".join(filesToAdd)
 628                if len(filesToDelete):
 629                    print "The following files should be scheduled for deletion with p4 delete:"
 630                    print " ".join(filesToDelete)
 631                die("Please resolve and submit the conflict manually and "
 632                    + "continue afterwards with git-p4 submit --continue")
 633            elif response == "w":
 634                system(diffcmd + " > patch.txt")
 635                print "Patch saved to patch.txt in %s !" % self.clientPath
 636                die("Please resolve and submit the conflict manually and "
 637                    "continue afterwards with git-p4 submit --continue")
 638
 639        system(applyPatchCmd)
 640
 641        for f in filesToAdd:
 642            system("p4 add \"%s\"" % f)
 643        for f in filesToDelete:
 644            system("p4 revert \"%s\"" % f)
 645            system("p4 delete \"%s\"" % f)
 646
 647        # Set/clear executable bits
 648        for f in filesToChangeExecBit.keys():
 649            mode = filesToChangeExecBit[f]
 650            setP4ExecBit(f, mode)
 651
 652        logMessage = ""
 653        if not self.directSubmit:
 654            logMessage = extractLogMessageFromGitCommit(id)
 655            logMessage = logMessage.replace("\n", "\n\t")
 656            if self.isWindows:
 657                logMessage = logMessage.replace("\n", "\r\n")
 658            logMessage = logMessage.strip()
 659
 660        template = self.prepareSubmitTemplate()
 661
 662        if self.interactive:
 663            submitTemplate = self.prepareLogMessage(template, logMessage)
 664            diff = read_pipe("p4 diff -du ...")
 665
 666            for newFile in filesToAdd:
 667                diff += "==== new file ====\n"
 668                diff += "--- /dev/null\n"
 669                diff += "+++ %s\n" % newFile
 670                f = open(newFile, "r")
 671                for line in f.readlines():
 672                    diff += "+" + line
 673                f.close()
 674
 675            separatorLine = "######## everything below this line is just the diff #######"
 676            if platform.system() == "Windows":
 677                separatorLine += "\r"
 678            separatorLine += "\n"
 679
 680            [handle, fileName] = tempfile.mkstemp()
 681            tmpFile = os.fdopen(handle, "w+")
 682            tmpFile.write(submitTemplate + separatorLine + diff)
 683            tmpFile.close()
 684            defaultEditor = "vi"
 685            if platform.system() == "Windows":
 686                defaultEditor = "notepad"
 687            editor = os.environ.get("EDITOR", defaultEditor);
 688            system(editor + " " + fileName)
 689            tmpFile = open(fileName, "rb")
 690            message = tmpFile.read()
 691            tmpFile.close()
 692            os.remove(fileName)
 693            submitTemplate = message[:message.index(separatorLine)]
 694            if self.isWindows:
 695                submitTemplate = submitTemplate.replace("\r\n", "\n")
 696
 697            if self.directSubmit:
 698                print "Submitting to git first"
 699                os.chdir(self.oldWorkingDirectory)
 700                write_pipe("git commit -a -F -", submitTemplate)
 701                os.chdir(self.clientPath)
 702
 703            write_pipe("p4 submit -i", submitTemplate)
 704        else:
 705            fileName = "submit.txt"
 706            file = open(fileName, "w+")
 707            file.write(self.prepareLogMessage(template, logMessage))
 708            file.close()
 709            print ("Perforce submit template written as %s. "
 710                   + "Please review/edit and then use p4 submit -i < %s to submit directly!"
 711                   % (fileName, fileName))
 712
 713    def run(self, args):
 714        if len(args) == 0:
 715            self.master = currentGitBranch()
 716            if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
 717                die("Detecting current git branch failed!")
 718        elif len(args) == 1:
 719            self.master = args[0]
 720        else:
 721            return False
 722
 723        [upstream, settings] = findUpstreamBranchPoint()
 724        self.depotPath = settings['depot-paths'][0]
 725        if len(self.origin) == 0:
 726            self.origin = upstream
 727
 728        if self.verbose:
 729            print "Origin branch is " + self.origin
 730
 731        if len(self.depotPath) == 0:
 732            print "Internal error: cannot locate perforce depot path from existing branches"
 733            sys.exit(128)
 734
 735        self.clientPath = p4Where(self.depotPath)
 736
 737        if len(self.clientPath) == 0:
 738            print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
 739            sys.exit(128)
 740
 741        print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
 742        self.oldWorkingDirectory = os.getcwd()
 743
 744        if self.directSubmit:
 745            self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD")
 746            if len(self.diffStatus) == 0:
 747                print "No changes in working directory to submit."
 748                return True
 749            patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD")
 750            self.diffFile = self.gitdir + "/p4-git-diff"
 751            f = open(self.diffFile, "wb")
 752            f.write(patch)
 753            f.close();
 754
 755        os.chdir(self.clientPath)
 756        print "Syncronizing p4 checkout..."
 757        system("p4 sync ...")
 758
 759        if self.reset:
 760            self.firstTime = True
 761
 762        if len(self.substFile) > 0:
 763            for line in open(self.substFile, "r").readlines():
 764                tokens = line.strip().split("=")
 765                self.logSubstitutions[tokens[0]] = tokens[1]
 766
 767        self.check()
 768        self.configFile = self.gitdir + "/p4-git-sync.cfg"
 769        self.config = shelve.open(self.configFile, writeback=True)
 770
 771        if self.firstTime:
 772            self.start()
 773
 774        commits = self.config.get("commits", [])
 775
 776        while len(commits) > 0:
 777            self.firstTime = False
 778            commit = commits[0]
 779            commits = commits[1:]
 780            self.config["commits"] = commits
 781            self.applyCommit(commit)
 782            if not self.interactive:
 783                break
 784
 785        self.config.close()
 786
 787        if self.directSubmit:
 788            os.remove(self.diffFile)
 789
 790        if len(commits) == 0:
 791            if self.firstTime:
 792                print "No changes found to apply between %s and current HEAD" % self.origin
 793            else:
 794                print "All changes applied!"
 795                os.chdir(self.oldWorkingDirectory)
 796
 797                sync = P4Sync()
 798                sync.run([])
 799
 800                rebase = P4Rebase()
 801                rebase.rebase()
 802            os.remove(self.configFile)
 803
 804        return True
 805
 806class P4Sync(Command):
 807    def __init__(self):
 808        Command.__init__(self)
 809        self.options = [
 810                optparse.make_option("--branch", dest="branch"),
 811                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 812                optparse.make_option("--changesfile", dest="changesFile"),
 813                optparse.make_option("--silent", dest="silent", action="store_true"),
 814                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
 815                optparse.make_option("--verbose", dest="verbose", action="store_true"),
 816                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
 817                                     help="Import into refs/heads/ , not refs/remotes"),
 818                optparse.make_option("--max-changes", dest="maxChanges"),
 819                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
 820                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
 821        ]
 822        self.description = """Imports from Perforce into a git repository.\n
 823    example:
 824    //depot/my/project/ -- to import the current head
 825    //depot/my/project/@all -- to import everything
 826    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 827
 828    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 829
 830        self.usage += " //depot/path[@revRange]"
 831        self.silent = False
 832        self.createdBranches = Set()
 833        self.committedChanges = Set()
 834        self.branch = ""
 835        self.detectBranches = False
 836        self.detectLabels = False
 837        self.changesFile = ""
 838        self.syncWithOrigin = True
 839        self.verbose = False
 840        self.importIntoRemotes = True
 841        self.maxChanges = ""
 842        self.isWindows = (platform.system() == "Windows")
 843        self.keepRepoPath = False
 844        self.depotPaths = None
 845        self.p4BranchesInGit = []
 846        self.cloneExclude = []
 847
 848        if gitConfig("git-p4.syncFromOrigin") == "false":
 849            self.syncWithOrigin = False
 850
 851    def extractFilesFromCommit(self, commit):
 852        self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
 853                             for path in self.cloneExclude]
 854        files = []
 855        fnum = 0
 856        while commit.has_key("depotFile%s" % fnum):
 857            path =  commit["depotFile%s" % fnum]
 858
 859            if [p for p in self.cloneExclude
 860                if path.startswith (p)]:
 861                found = False
 862            else:
 863                found = [p for p in self.depotPaths
 864                         if path.startswith (p)]
 865            if not found:
 866                fnum = fnum + 1
 867                continue
 868
 869            file = {}
 870            file["path"] = path
 871            file["rev"] = commit["rev%s" % fnum]
 872            file["action"] = commit["action%s" % fnum]
 873            file["type"] = commit["type%s" % fnum]
 874            files.append(file)
 875            fnum = fnum + 1
 876        return files
 877
 878    def stripRepoPath(self, path, prefixes):
 879        if self.keepRepoPath:
 880            prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
 881
 882        for p in prefixes:
 883            if path.startswith(p):
 884                path = path[len(p):]
 885
 886        return path
 887
 888    def splitFilesIntoBranches(self, commit):
 889        branches = {}
 890        fnum = 0
 891        while commit.has_key("depotFile%s" % fnum):
 892            path =  commit["depotFile%s" % fnum]
 893            found = [p for p in self.depotPaths
 894                     if path.startswith (p)]
 895            if not found:
 896                fnum = fnum + 1
 897                continue
 898
 899            file = {}
 900            file["path"] = path
 901            file["rev"] = commit["rev%s" % fnum]
 902            file["action"] = commit["action%s" % fnum]
 903            file["type"] = commit["type%s" % fnum]
 904            fnum = fnum + 1
 905
 906            relPath = self.stripRepoPath(path, self.depotPaths)
 907
 908            for branch in self.knownBranches.keys():
 909
 910                # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
 911                if relPath.startswith(branch + "/"):
 912                    if branch not in branches:
 913                        branches[branch] = []
 914                    branches[branch].append(file)
 915                    break
 916
 917        return branches
 918
 919    ## Should move this out, doesn't use SELF.
 920    def readP4Files(self, files):
 921        files = [f for f in files
 922                 if f['action'] != 'delete']
 923
 924        if not files:
 925            return
 926
 927        filedata = p4CmdList('-x - print',
 928                             stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
 929                                              for f in files]),
 930                             stdin_mode='w+')
 931        if "p4ExitCode" in filedata[0]:
 932            die("Problems executing p4. Error: [%d]."
 933                % (filedata[0]['p4ExitCode']));
 934
 935        j = 0;
 936        contents = {}
 937        while j < len(filedata):
 938            stat = filedata[j]
 939            j += 1
 940            text = ''
 941            while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
 942                tmp = filedata[j]['data']
 943                if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
 944                    tmp = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', tmp)
 945                elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
 946                    tmp = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', tmp)
 947                text += tmp
 948                j += 1
 949
 950
 951            if not stat.has_key('depotFile'):
 952                sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
 953                continue
 954
 955            contents[stat['depotFile']] = text
 956
 957        for f in files:
 958            assert not f.has_key('data')
 959            f['data'] = contents[f['path']]
 960
 961    def commit(self, details, files, branch, branchPrefixes, parent = ""):
 962        epoch = details["time"]
 963        author = details["user"]
 964
 965        if self.verbose:
 966            print "commit into %s" % branch
 967
 968        # start with reading files; if that fails, we should not
 969        # create a commit.
 970        new_files = []
 971        for f in files:
 972            if [p for p in branchPrefixes if f['path'].startswith(p)]:
 973                new_files.append (f)
 974            else:
 975                sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
 976        files = new_files
 977        self.readP4Files(files)
 978
 979
 980
 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 run(self, args):
1396        self.depotPaths = []
1397        self.changeRange = ""
1398        self.initialParent = ""
1399        self.previousDepotPaths = []
1400
1401        # map from branch depot path to parent branch
1402        self.knownBranches = {}
1403        self.initialParents = {}
1404        self.hasOrigin = originP4BranchesExist()
1405        if not self.syncWithOrigin:
1406            self.hasOrigin = False
1407
1408        if self.importIntoRemotes:
1409            self.refPrefix = "refs/remotes/p4/"
1410        else:
1411            self.refPrefix = "refs/heads/p4/"
1412
1413        if self.syncWithOrigin and self.hasOrigin:
1414            if not self.silent:
1415                print "Syncing with origin first by calling git fetch origin"
1416            system("git fetch origin")
1417
1418        if len(self.branch) == 0:
1419            self.branch = self.refPrefix + "master"
1420            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1421                system("git update-ref %s refs/heads/p4" % self.branch)
1422                system("git branch -D p4");
1423            # create it /after/ importing, when master exists
1424            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1425                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1426
1427        # TODO: should always look at previous commits,
1428        # merge with previous imports, if possible.
1429        if args == []:
1430            if self.hasOrigin:
1431                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1432            self.listExistingP4GitBranches()
1433
1434            if len(self.p4BranchesInGit) > 1:
1435                if not self.silent:
1436                    print "Importing from/into multiple branches"
1437                self.detectBranches = True
1438
1439            if self.verbose:
1440                print "branches: %s" % self.p4BranchesInGit
1441
1442            p4Change = 0
1443            for branch in self.p4BranchesInGit:
1444                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1445
1446                settings = extractSettingsGitLog(logMsg)
1447
1448                self.readOptions(settings)
1449                if (settings.has_key('depot-paths')
1450                    and settings.has_key ('change')):
1451                    change = int(settings['change']) + 1
1452                    p4Change = max(p4Change, change)
1453
1454                    depotPaths = sorted(settings['depot-paths'])
1455                    if self.previousDepotPaths == []:
1456                        self.previousDepotPaths = depotPaths
1457                    else:
1458                        paths = []
1459                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1460                            for i in range(0, min(len(cur), len(prev))):
1461                                if cur[i] <> prev[i]:
1462                                    i = i - 1
1463                                    break
1464
1465                            paths.append (cur[:i + 1])
1466
1467                        self.previousDepotPaths = paths
1468
1469            if p4Change > 0:
1470                self.depotPaths = sorted(self.previousDepotPaths)
1471                self.changeRange = "@%s,#head" % p4Change
1472                if not self.detectBranches:
1473                    self.initialParent = parseRevision(self.branch)
1474                if not self.silent and not self.detectBranches:
1475                    print "Performing incremental import into %s git branch" % self.branch
1476
1477        if not self.branch.startswith("refs/"):
1478            self.branch = "refs/heads/" + self.branch
1479
1480        if len(args) == 0 and self.depotPaths:
1481            if not self.silent:
1482                print "Depot paths: %s" % ' '.join(self.depotPaths)
1483        else:
1484            if self.depotPaths and self.depotPaths != args:
1485                print ("previous import used depot path %s and now %s was specified. "
1486                       "This doesn't work!" % (' '.join (self.depotPaths),
1487                                               ' '.join (args)))
1488                sys.exit(1)
1489
1490            self.depotPaths = sorted(args)
1491
1492        revision = ""
1493        self.users = {}
1494
1495        newPaths = []
1496        for p in self.depotPaths:
1497            if p.find("@") != -1:
1498                atIdx = p.index("@")
1499                self.changeRange = p[atIdx:]
1500                if self.changeRange == "@all":
1501                    self.changeRange = ""
1502                elif ',' not in self.changeRange:
1503                    revision = self.changeRange
1504                    self.changeRange = ""
1505                p = p[:atIdx]
1506            elif p.find("#") != -1:
1507                hashIdx = p.index("#")
1508                revision = p[hashIdx:]
1509                p = p[:hashIdx]
1510            elif self.previousDepotPaths == []:
1511                revision = "#head"
1512
1513            p = re.sub ("\.\.\.$", "", p)
1514            if not p.endswith("/"):
1515                p += "/"
1516
1517            newPaths.append(p)
1518
1519        self.depotPaths = newPaths
1520
1521
1522        self.loadUserMapFromCache()
1523        self.labels = {}
1524        if self.detectLabels:
1525            self.getLabels();
1526
1527        if self.detectBranches:
1528            ## FIXME - what's a P4 projectName ?
1529            self.projectName = self.guessProjectName()
1530
1531            if self.hasOrigin:
1532                self.getBranchMappingFromGitBranches()
1533            else:
1534                self.getBranchMapping()
1535            if self.verbose:
1536                print "p4-git branches: %s" % self.p4BranchesInGit
1537                print "initial parents: %s" % self.initialParents
1538            for b in self.p4BranchesInGit:
1539                if b != "master":
1540
1541                    ## FIXME
1542                    b = b[len(self.projectName):]
1543                self.createdBranches.add(b)
1544
1545        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1546
1547        importProcess = subprocess.Popen(["git", "fast-import"],
1548                                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1549                                         stderr=subprocess.PIPE);
1550        self.gitOutput = importProcess.stdout
1551        self.gitStream = importProcess.stdin
1552        self.gitError = importProcess.stderr
1553
1554        if revision:
1555            self.importHeadRevision(revision)
1556        else:
1557            changes = []
1558
1559            if len(self.changesFile) > 0:
1560                output = open(self.changesFile).readlines()
1561                changeSet = Set()
1562                for line in output:
1563                    changeSet.add(int(line))
1564
1565                for change in changeSet:
1566                    changes.append(change)
1567
1568                changes.sort()
1569            else:
1570                if self.verbose:
1571                    print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1572                                                              self.changeRange)
1573                changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1574
1575                if len(self.maxChanges) > 0:
1576                    changes = changes[:min(int(self.maxChanges), len(changes))]
1577
1578            if len(changes) == 0:
1579                if not self.silent:
1580                    print "No changes to import!"
1581                return True
1582
1583            if not self.silent and not self.detectBranches:
1584                print "Import destination: %s" % self.branch
1585
1586            self.updatedBranches = set()
1587
1588            self.importChanges(changes)
1589
1590            if not self.silent:
1591                print ""
1592                if len(self.updatedBranches) > 0:
1593                    sys.stdout.write("Updated branches: ")
1594                    for b in self.updatedBranches:
1595                        sys.stdout.write("%s " % b)
1596                    sys.stdout.write("\n")
1597
1598        self.gitStream.close()
1599        if importProcess.wait() != 0:
1600            die("fast-import failed: %s" % self.gitError.read())
1601        self.gitOutput.close()
1602        self.gitError.close()
1603
1604        return True
1605
1606class P4Rebase(Command):
1607    def __init__(self):
1608        Command.__init__(self)
1609        self.options = [ ]
1610        self.description = ("Fetches the latest revision from perforce and "
1611                            + "rebases the current work (branch) against it")
1612        self.verbose = False
1613
1614    def run(self, args):
1615        sync = P4Sync()
1616        sync.run([])
1617
1618        return self.rebase()
1619
1620    def rebase(self):
1621        if os.system("git update-index --refresh") != 0:
1622            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.");
1623        if len(read_pipe("git diff-index HEAD --")) > 0:
1624            die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1625
1626        [upstream, settings] = findUpstreamBranchPoint()
1627        if len(upstream) == 0:
1628            die("Cannot find upstream branchpoint for rebase")
1629
1630        # the branchpoint may be p4/foo~3, so strip off the parent
1631        upstream = re.sub("~[0-9]+$", "", upstream)
1632
1633        print "Rebasing the current branch onto %s" % upstream
1634        oldHead = read_pipe("git rev-parse HEAD").strip()
1635        system("git rebase %s" % upstream)
1636        system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1637        return True
1638
1639class P4Clone(P4Sync):
1640    def __init__(self):
1641        P4Sync.__init__(self)
1642        self.description = "Creates a new git repository and imports from Perforce into it"
1643        self.usage = "usage: %prog [options] //depot/path[@revRange]"
1644        self.options += [
1645            optparse.make_option("--destination", dest="cloneDestination",
1646                                 action='store', default=None,
1647                                 help="where to leave result of the clone"),
1648            optparse.make_option("-/", dest="cloneExclude",
1649                                 action="append", type="string",
1650                                 help="exclude depot path")
1651        ]
1652        self.cloneDestination = None
1653        self.needsGit = False
1654
1655    # This is required for the "append" cloneExclude action
1656    def ensure_value(self, attr, value):
1657        if not hasattr(self, attr) or getattr(self, attr) is None:
1658            setattr(self, attr, value)
1659        return getattr(self, attr)
1660
1661    def defaultDestination(self, args):
1662        ## TODO: use common prefix of args?
1663        depotPath = args[0]
1664        depotDir = re.sub("(@[^@]*)$", "", depotPath)
1665        depotDir = re.sub("(#[^#]*)$", "", depotDir)
1666        depotDir = re.sub(r"\.\.\.$", "", depotDir)
1667        depotDir = re.sub(r"/$", "", depotDir)
1668        return os.path.split(depotDir)[1]
1669
1670    def run(self, args):
1671        if len(args) < 1:
1672            return False
1673
1674        if self.keepRepoPath and not self.cloneDestination:
1675            sys.stderr.write("Must specify destination for --keep-path\n")
1676            sys.exit(1)
1677
1678        depotPaths = args
1679
1680        if not self.cloneDestination and len(depotPaths) > 1:
1681            self.cloneDestination = depotPaths[-1]
1682            depotPaths = depotPaths[:-1]
1683
1684        self.cloneExclude = ["/"+p for p in self.cloneExclude]
1685        for p in depotPaths:
1686            if not p.startswith("//"):
1687                return False
1688
1689        if not self.cloneDestination:
1690            self.cloneDestination = self.defaultDestination(args)
1691
1692        print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1693        if not os.path.exists(self.cloneDestination):
1694            os.makedirs(self.cloneDestination)
1695        os.chdir(self.cloneDestination)
1696        system("git init")
1697        self.gitdir = os.getcwd() + "/.git"
1698        if not P4Sync.run(self, depotPaths):
1699            return False
1700        if self.branch != "master":
1701            if gitBranchExists("refs/remotes/p4/master"):
1702                system("git branch master refs/remotes/p4/master")
1703                system("git checkout -f")
1704            else:
1705                print "Could not detect main branch. No checkout/master branch created."
1706
1707        return True
1708
1709class P4Branches(Command):
1710    def __init__(self):
1711        Command.__init__(self)
1712        self.options = [ ]
1713        self.description = ("Shows the git branches that hold imports and their "
1714                            + "corresponding perforce depot paths")
1715        self.verbose = False
1716
1717    def run(self, args):
1718        if originP4BranchesExist():
1719            createOrUpdateBranchesFromOrigin()
1720
1721        cmdline = "git rev-parse --symbolic "
1722        cmdline += " --remotes"
1723
1724        for line in read_pipe_lines(cmdline):
1725            line = line.strip()
1726
1727            if not line.startswith('p4/') or line == "p4/HEAD":
1728                continue
1729            branch = line
1730
1731            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1732            settings = extractSettingsGitLog(log)
1733
1734            print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1735        return True
1736
1737class HelpFormatter(optparse.IndentedHelpFormatter):
1738    def __init__(self):
1739        optparse.IndentedHelpFormatter.__init__(self)
1740
1741    def format_description(self, description):
1742        if description:
1743            return description + "\n"
1744        else:
1745            return ""
1746
1747def printUsage(commands):
1748    print "usage: %s <command> [options]" % sys.argv[0]
1749    print ""
1750    print "valid commands: %s" % ", ".join(commands)
1751    print ""
1752    print "Try %s <command> --help for command specific help." % sys.argv[0]
1753    print ""
1754
1755commands = {
1756    "debug" : P4Debug,
1757    "submit" : P4Submit,
1758    "commit" : P4Submit,
1759    "sync" : P4Sync,
1760    "rebase" : P4Rebase,
1761    "clone" : P4Clone,
1762    "rollback" : P4RollBack,
1763    "branches" : P4Branches
1764}
1765
1766
1767def main():
1768    if len(sys.argv[1:]) == 0:
1769        printUsage(commands.keys())
1770        sys.exit(2)
1771
1772    cmd = ""
1773    cmdName = sys.argv[1]
1774    try:
1775        klass = commands[cmdName]
1776        cmd = klass()
1777    except KeyError:
1778        print "unknown command %s" % cmdName
1779        print ""
1780        printUsage(commands.keys())
1781        sys.exit(2)
1782
1783    options = cmd.options
1784    cmd.gitdir = os.environ.get("GIT_DIR", None)
1785
1786    args = sys.argv[2:]
1787
1788    if len(options) > 0:
1789        options.append(optparse.make_option("--git-dir", dest="gitdir"))
1790
1791        parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1792                                       options,
1793                                       description = cmd.description,
1794                                       formatter = HelpFormatter())
1795
1796        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1797    global verbose
1798    verbose = cmd.verbose
1799    if cmd.needsGit:
1800        if cmd.gitdir == None:
1801            cmd.gitdir = os.path.abspath(".git")
1802            if not isValidGitDir(cmd.gitdir):
1803                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1804                if os.path.exists(cmd.gitdir):
1805                    cdup = read_pipe("git rev-parse --show-cdup").strip()
1806                    if len(cdup) > 0:
1807                        os.chdir(cdup);
1808
1809        if not isValidGitDir(cmd.gitdir):
1810            if isValidGitDir(cmd.gitdir + "/.git"):
1811                cmd.gitdir += "/.git"
1812            else:
1813                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1814
1815        os.environ["GIT_DIR"] = cmd.gitdir
1816
1817    if not cmd.run(args):
1818        parser.print_help()
1819
1820
1821if __name__ == '__main__':
1822    main()