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