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