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