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