477238fa1c1017bfd90d5084e28962c310db03cb
   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 <hausmann@kde.org>
   6# Copyright: 2007 Simon Hausmann <hausmann@kde.org>
   7#            2007 Trolltech ASA
   8# License: MIT <http://www.opensource.org/licenses/mit-license.php>
   9#
  10
  11import optparse, sys, os, marshal, popen2, shelve
  12import tempfile, getopt, sha, os.path, time
  13from sets import Set;
  14
  15gitdir = os.environ.get("GIT_DIR", "")
  16
  17def p4CmdList(cmd):
  18    cmd = "p4 -G %s" % cmd
  19    pipe = os.popen(cmd, "rb")
  20
  21    result = []
  22    try:
  23        while True:
  24            entry = marshal.load(pipe)
  25            result.append(entry)
  26    except EOFError:
  27        pass
  28    pipe.close()
  29
  30    return result
  31
  32def p4Cmd(cmd):
  33    list = p4CmdList(cmd)
  34    result = {}
  35    for entry in list:
  36        result.update(entry)
  37    return result;
  38
  39def die(msg):
  40    sys.stderr.write(msg + "\n")
  41    sys.exit(1)
  42
  43def currentGitBranch():
  44    return os.popen("git-name-rev HEAD").read().split(" ")[1][:-1]
  45
  46def isValidGitDir(path):
  47    if os.path.exists(path + "/HEAD") and os.path.exists(path + "/refs") and os.path.exists(path + "/objects"):
  48        return True;
  49    return False
  50
  51def system(cmd):
  52    if os.system(cmd) != 0:
  53        die("command failed: %s" % cmd)
  54
  55class Command:
  56    def __init__(self):
  57        self.usage = "usage: %prog [options]"
  58
  59class P4Debug(Command):
  60    def __init__(self):
  61        self.options = [
  62        ]
  63        self.description = "A tool to debug the output of p4 -G."
  64
  65    def run(self, args):
  66        for output in p4CmdList(" ".join(args)):
  67            print output
  68        return True
  69
  70class P4CleanTags(Command):
  71    def __init__(self):
  72        Command.__init__(self)
  73        self.options = [
  74#                optparse.make_option("--branch", dest="branch", default="refs/heads/master")
  75        ]
  76        self.description = "A tool to remove stale unused tags from incremental perforce imports."
  77    def run(self, args):
  78        branch = currentGitBranch()
  79        print "Cleaning out stale p4 import tags..."
  80        sout, sin, serr = popen2.popen3("git-name-rev --tags `git-rev-parse %s`" % branch)
  81        output = sout.read()
  82        try:
  83            tagIdx = output.index(" tags/p4/")
  84        except:
  85            print "Cannot find any p4/* tag. Nothing to do."
  86            sys.exit(0)
  87
  88        try:
  89            caretIdx = output.index("^")
  90        except:
  91            caretIdx = len(output) - 1
  92        rev = int(output[tagIdx + 9 : caretIdx])
  93
  94        allTags = os.popen("git tag -l p4/").readlines()
  95        for i in range(len(allTags)):
  96            allTags[i] = int(allTags[i][3:-1])
  97
  98        allTags.sort()
  99
 100        allTags.remove(rev)
 101
 102        for rev in allTags:
 103            print os.popen("git tag -d p4/%s" % rev).read()
 104
 105        print "%s tags removed." % len(allTags)
 106        return True
 107
 108class P4Sync(Command):
 109    def __init__(self):
 110        Command.__init__(self)
 111        self.options = [
 112                optparse.make_option("--continue", action="store_false", dest="firstTime"),
 113                optparse.make_option("--origin", dest="origin"),
 114                optparse.make_option("--reset", action="store_true", dest="reset"),
 115                optparse.make_option("--master", dest="master"),
 116                optparse.make_option("--log-substitutions", dest="substFile"),
 117                optparse.make_option("--noninteractive", action="store_false"),
 118                optparse.make_option("--dry-run", action="store_true")
 119        ]
 120        self.description = "Submit changes from git to the perforce depot."
 121        self.firstTime = True
 122        self.reset = False
 123        self.interactive = True
 124        self.dryRun = False
 125        self.substFile = ""
 126        self.firstTime = True
 127        self.origin = "origin"
 128        self.master = ""
 129
 130        self.logSubstitutions = {}
 131        self.logSubstitutions["<enter description here>"] = "%log%"
 132        self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
 133
 134    def check(self):
 135        if len(p4CmdList("opened ...")) > 0:
 136            die("You have files opened with perforce! Close them before starting the sync.")
 137
 138    def start(self):
 139        if len(self.config) > 0 and not self.reset:
 140            die("Cannot start sync. Previous sync config found at %s" % self.configFile)
 141
 142        commits = []
 143        for line in os.popen("git-rev-list --no-merges %s..%s" % (self.origin, self.master)).readlines():
 144            commits.append(line[:-1])
 145        commits.reverse()
 146
 147        self.config["commits"] = commits
 148
 149        print "Creating temporary p4-sync branch from %s ..." % self.origin
 150        system("git checkout -f -b p4-sync %s" % self.origin)
 151
 152    def prepareLogMessage(self, template, message):
 153        result = ""
 154
 155        for line in template.split("\n"):
 156            if line.startswith("#"):
 157                result += line + "\n"
 158                continue
 159
 160            substituted = False
 161            for key in self.logSubstitutions.keys():
 162                if line.find(key) != -1:
 163                    value = self.logSubstitutions[key]
 164                    value = value.replace("%log%", message)
 165                    if value != "@remove@":
 166                        result += line.replace(key, value) + "\n"
 167                    substituted = True
 168                    break
 169
 170            if not substituted:
 171                result += line + "\n"
 172
 173        return result
 174
 175    def apply(self, id):
 176        print "Applying %s" % (os.popen("git-log --max-count=1 --pretty=oneline %s" % id).read())
 177        diff = os.popen("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)).readlines()
 178        filesToAdd = set()
 179        filesToDelete = set()
 180        for line in diff:
 181            modifier = line[0]
 182            path = line[1:].strip()
 183            if modifier == "M":
 184                system("p4 edit %s" % path)
 185            elif modifier == "A":
 186                filesToAdd.add(path)
 187                if path in filesToDelete:
 188                    filesToDelete.remove(path)
 189            elif modifier == "D":
 190                filesToDelete.add(path)
 191                if path in filesToAdd:
 192                    filesToAdd.remove(path)
 193            else:
 194                die("unknown modifier %s for %s" % (modifier, path))
 195
 196        system("git-diff-files --name-only -z | git-update-index --remove -z --stdin")
 197        system("git cherry-pick --no-commit \"%s\"" % id)
 198
 199        for f in filesToAdd:
 200            system("p4 add %s" % f)
 201        for f in filesToDelete:
 202            system("p4 revert %s" % f)
 203            system("p4 delete %s" % f)
 204
 205        logMessage = ""
 206        foundTitle = False
 207        for log in os.popen("git-cat-file commit %s" % id).readlines():
 208            if not foundTitle:
 209                if len(log) == 1:
 210                    foundTitle = 1
 211                continue
 212
 213            if len(logMessage) > 0:
 214                logMessage += "\t"
 215            logMessage += log
 216
 217        template = os.popen("p4 change -o").read()
 218
 219        if self.interactive:
 220            submitTemplate = self.prepareLogMessage(template, logMessage)
 221            diff = os.popen("p4 diff -du ...").read()
 222
 223            for newFile in filesToAdd:
 224                diff += "==== new file ====\n"
 225                diff += "--- /dev/null\n"
 226                diff += "+++ %s\n" % newFile
 227                f = open(newFile, "r")
 228                for line in f.readlines():
 229                    diff += "+" + line
 230                f.close()
 231
 232            pipe = os.popen("less", "w")
 233            pipe.write(submitTemplate + diff)
 234            pipe.close()
 235
 236            response = "e"
 237            while response == "e":
 238                response = raw_input("Do you want to submit this change (y/e/n)? ")
 239                if response == "e":
 240                    [handle, fileName] = tempfile.mkstemp()
 241                    tmpFile = os.fdopen(handle, "w+")
 242                    tmpFile.write(submitTemplate)
 243                    tmpFile.close()
 244                    editor = os.environ.get("EDITOR", "vi")
 245                    system(editor + " " + fileName)
 246                    tmpFile = open(fileName, "r")
 247                    submitTemplate = tmpFile.read()
 248                    tmpFile.close()
 249                    os.remove(fileName)
 250
 251            if response == "y" or response == "yes":
 252               if self.dryRun:
 253                   print submitTemplate
 254                   raw_input("Press return to continue...")
 255               else:
 256                    pipe = os.popen("p4 submit -i", "w")
 257                    pipe.write(submitTemplate)
 258                    pipe.close()
 259            else:
 260                print "Not submitting!"
 261                self.interactive = False
 262        else:
 263            fileName = "submit.txt"
 264            file = open(fileName, "w+")
 265            file.write(self.prepareLogMessage(template, logMessage))
 266            file.close()
 267            print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName, fileName)
 268
 269    def run(self, args):
 270        if self.reset:
 271            self.firstTime = True
 272
 273        if len(self.substFile) > 0:
 274            for line in open(self.substFile, "r").readlines():
 275                tokens = line[:-1].split("=")
 276                self.logSubstitutions[tokens[0]] = tokens[1]
 277
 278        if len(self.master) == 0:
 279            self.master = currentGitBranch()
 280            if len(self.master) == 0 or not os.path.exists("%s/refs/heads/%s" % (gitdir, self.master)):
 281                die("Detecting current git branch failed!")
 282
 283        self.check()
 284        self.configFile = gitdir + "/p4-git-sync.cfg"
 285        self.config = shelve.open(self.configFile, writeback=True)
 286
 287        if self.firstTime:
 288            self.start()
 289
 290        commits = self.config.get("commits", [])
 291
 292        while len(commits) > 0:
 293            self.firstTime = False
 294            commit = commits[0]
 295            commits = commits[1:]
 296            self.config["commits"] = commits
 297            self.apply(commit)
 298            if not self.interactive:
 299                break
 300
 301        self.config.close()
 302
 303        if len(commits) == 0:
 304            if self.firstTime:
 305                print "No changes found to apply between %s and current HEAD" % self.origin
 306            else:
 307                print "All changes applied!"
 308                print "Deleting temporary p4-sync branch and going back to %s" % self.master
 309                system("git checkout %s" % self.master)
 310                system("git branch -D p4-sync")
 311                print "Cleaning out your perforce checkout by doing p4 edit ... ; p4 revert ..."
 312                system("p4 edit ... >/dev/null")
 313                system("p4 revert ... >/dev/null")
 314            os.remove(self.configFile)
 315
 316        return True
 317
 318class GitSync(Command):
 319    def __init__(self):
 320        Command.__init__(self)
 321        self.options = [
 322                optparse.make_option("--branch", dest="branch"),
 323                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
 324                optparse.make_option("--changesfile", dest="changesFile"),
 325                optparse.make_option("--silent", dest="silent", action="store_true"),
 326                optparse.make_option("--known-branches", dest="knownBranches"),
 327                optparse.make_option("--cache", dest="doCache", action="store_true"),
 328                optparse.make_option("--command-cache", dest="commandCache", action="store_true")
 329        ]
 330        self.description = """Imports from Perforce into a git repository.\n
 331    example:
 332    //depot/my/project/ -- to import the current head
 333    //depot/my/project/@all -- to import everything
 334    //depot/my/project/@1,6 -- to import only from revision 1 to 6
 335
 336    (a ... is not needed in the path p4 specification, it's added implicitly)"""
 337
 338        self.usage += " //depot/path[@revRange]"
 339
 340        self.dataCache = False
 341        self.commandCache = False
 342        self.silent = False
 343        self.knownBranches = Set()
 344        self.createdBranches = Set()
 345        self.committedChanges = Set()
 346        self.branch = "master"
 347        self.detectBranches = False
 348        self.changesFile = ""
 349
 350    def p4File(self, depotPath):
 351        return os.popen("p4 print -q \"%s\"" % depotPath, "rb").read()
 352
 353    def extractFilesFromCommit(self, commit):
 354        files = []
 355        fnum = 0
 356        while commit.has_key("depotFile%s" % fnum):
 357            path =  commit["depotFile%s" % fnum]
 358            if not path.startswith(self.globalPrefix):
 359    #            if not self.silent:
 360    #                print "\nchanged files: ignoring path %s outside of %s in change %s" % (path, self.globalPrefix, change)
 361                fnum = fnum + 1
 362                continue
 363
 364            file = {}
 365            file["path"] = path
 366            file["rev"] = commit["rev%s" % fnum]
 367            file["action"] = commit["action%s" % fnum]
 368            file["type"] = commit["type%s" % fnum]
 369            files.append(file)
 370            fnum = fnum + 1
 371        return files
 372
 373    def isSubPathOf(self, first, second):
 374        if not first.startswith(second):
 375            return False
 376        if first == second:
 377            return True
 378        return first[len(second)] == "/"
 379
 380    def branchesForCommit(self, files):
 381        branches = Set()
 382
 383        for file in files:
 384            relativePath = file["path"][len(self.globalPrefix):]
 385            # strip off the filename
 386            relativePath = relativePath[0:relativePath.rfind("/")]
 387
 388    #        if len(branches) == 0:
 389    #            branches.add(relativePath)
 390    #            knownBranches.add(relativePath)
 391    #            continue
 392
 393            ###### this needs more testing :)
 394            knownBranch = False
 395            for branch in branches:
 396                if relativePath == branch:
 397                    knownBranch = True
 398                    break
 399    #            if relativePath.startswith(branch):
 400                if self.isSubPathOf(relativePath, branch):
 401                    knownBranch = True
 402                    break
 403    #            if branch.startswith(relativePath):
 404                if self.isSubPathOf(branch, relativePath):
 405                    branches.remove(branch)
 406                    break
 407
 408            if knownBranch:
 409                continue
 410
 411            for branch in knownBranches:
 412                #if relativePath.startswith(branch):
 413                if self.isSubPathOf(relativePath, branch):
 414                    if len(branches) == 0:
 415                        relativePath = branch
 416                    else:
 417                        knownBranch = True
 418                    break
 419
 420            if knownBranch:
 421                continue
 422
 423            branches.add(relativePath)
 424            self.knownBranches.add(relativePath)
 425
 426        return branches
 427
 428    def findBranchParent(self, branchPrefix, files):
 429        for file in files:
 430            path = file["path"]
 431            if not path.startswith(branchPrefix):
 432                continue
 433            action = file["action"]
 434            if action != "integrate" and action != "branch":
 435                continue
 436            rev = file["rev"]
 437            depotPath = path + "#" + rev
 438
 439            log = p4CmdList("filelog \"%s\"" % depotPath)
 440            if len(log) != 1:
 441                print "eek! I got confused by the filelog of %s" % depotPath
 442                sys.exit(1);
 443
 444            log = log[0]
 445            if log["action0"] != action:
 446                print "eek! wrong action in filelog for %s : found %s, expected %s" % (depotPath, log["action0"], action)
 447                sys.exit(1);
 448
 449            branchAction = log["how0,0"]
 450    #        if branchAction == "branch into" or branchAction == "ignored":
 451    #            continue # ignore for branching
 452
 453            if not branchAction.endswith(" from"):
 454                continue # ignore for branching
 455    #            print "eek! file %s was not branched from but instead: %s" % (depotPath, branchAction)
 456    #            sys.exit(1);
 457
 458            source = log["file0,0"]
 459            if source.startswith(branchPrefix):
 460                continue
 461
 462            lastSourceRev = log["erev0,0"]
 463
 464            sourceLog = p4CmdList("filelog -m 1 \"%s%s\"" % (source, lastSourceRev))
 465            if len(sourceLog) != 1:
 466                print "eek! I got confused by the source filelog of %s%s" % (source, lastSourceRev)
 467                sys.exit(1);
 468            sourceLog = sourceLog[0]
 469
 470            relPath = source[len(self.globalPrefix):]
 471            # strip off the filename
 472            relPath = relPath[0:relPath.rfind("/")]
 473
 474            for branch in self.knownBranches:
 475                if self.isSubPathOf(relPath, branch):
 476    #                print "determined parent branch branch %s due to change in file %s" % (branch, source)
 477                    return branch
 478    #            else:
 479    #                print "%s is not a subpath of branch %s" % (relPath, branch)
 480
 481        return ""
 482
 483    def commit(self, details, files, branch, branchPrefix, parent, merged = ""):
 484        epoch = details["time"]
 485        author = details["user"]
 486
 487        self.gitStream.write("commit %s\n" % branch)
 488    #    gitStream.write("mark :%s\n" % details["change"])
 489        self.committedChanges.add(int(details["change"]))
 490        committer = ""
 491        if author in self.users:
 492            committer = "%s %s %s" % (self.users[author], epoch, tz)
 493        else:
 494            committer = "%s <a@b> %s %s" % (author, epoch, tz)
 495
 496        self.gitStream.write("committer %s\n" % committer)
 497
 498        self.gitStream.write("data <<EOT\n")
 499        self.gitStream.write(details["desc"])
 500        self.gitStream.write("\n[ imported from %s; change %s ]\n" % (branchPrefix, details["change"]))
 501        self.gitStream.write("EOT\n\n")
 502
 503        if len(parent) > 0:
 504            self.gitStream.write("from %s\n" % parent)
 505
 506        if len(merged) > 0:
 507            self.gitStream.write("merge %s\n" % merged)
 508
 509        for file in files:
 510            path = file["path"]
 511            if not path.startswith(branchPrefix):
 512    #            if not silent:
 513    #                print "\nchanged files: ignoring path %s outside of branch prefix %s in change %s" % (path, branchPrefix, details["change"])
 514                continue
 515            rev = file["rev"]
 516            depotPath = path + "#" + rev
 517            relPath = path[len(branchPrefix):]
 518            action = file["action"]
 519
 520            if file["type"] == "apple":
 521                print "\nfile %s is a strange apple file that forks. Ignoring!" % path
 522                continue
 523
 524            if action == "delete":
 525                self.gitStream.write("D %s\n" % relPath)
 526            else:
 527                mode = 644
 528                if file["type"].startswith("x"):
 529                    mode = 755
 530
 531                data = self.p4File(depotPath)
 532
 533                self.gitStream.write("M %s inline %s\n" % (mode, relPath))
 534                self.gitStream.write("data %s\n" % len(data))
 535                self.gitStream.write(data)
 536                self.gitStream.write("\n")
 537
 538        self.gitStream.write("\n")
 539
 540        self.lastChange = int(details["change"])
 541
 542    def extractFilesInCommitToBranch(self, files, branchPrefix):
 543        newFiles = []
 544
 545        for file in files:
 546            path = file["path"]
 547            if path.startswith(branchPrefix):
 548                newFiles.append(file)
 549
 550        return newFiles
 551
 552    def findBranchSourceHeuristic(self, files, branch, branchPrefix):
 553        for file in files:
 554            action = file["action"]
 555            if action != "integrate" and action != "branch":
 556                continue
 557            path = file["path"]
 558            rev = file["rev"]
 559            depotPath = path + "#" + rev
 560
 561            log = p4CmdList("filelog \"%s\"" % depotPath)
 562            if len(log) != 1:
 563                print "eek! I got confused by the filelog of %s" % depotPath
 564                sys.exit(1);
 565
 566            log = log[0]
 567            if log["action0"] != action:
 568                print "eek! wrong action in filelog for %s : found %s, expected %s" % (depotPath, log["action0"], action)
 569                sys.exit(1);
 570
 571            branchAction = log["how0,0"]
 572
 573            if not branchAction.endswith(" from"):
 574                continue # ignore for branching
 575    #            print "eek! file %s was not branched from but instead: %s" % (depotPath, branchAction)
 576    #            sys.exit(1);
 577
 578            source = log["file0,0"]
 579            if source.startswith(branchPrefix):
 580                continue
 581
 582            lastSourceRev = log["erev0,0"]
 583
 584            sourceLog = p4CmdList("filelog -m 1 \"%s%s\"" % (source, lastSourceRev))
 585            if len(sourceLog) != 1:
 586                print "eek! I got confused by the source filelog of %s%s" % (source, lastSourceRev)
 587                sys.exit(1);
 588            sourceLog = sourceLog[0]
 589
 590            relPath = source[len(self.globalPrefix):]
 591            # strip off the filename
 592            relPath = relPath[0:relPath.rfind("/")]
 593
 594            for candidate in self.knownBranches:
 595                if self.isSubPathOf(relPath, candidate) and candidate != branch:
 596                    return candidate
 597
 598        return ""
 599
 600    def changeIsBranchMerge(self, sourceBranch, destinationBranch, change):
 601        sourceFiles = {}
 602        for file in p4CmdList("files %s...@%s" % (self.globalPrefix + sourceBranch + "/", change)):
 603            if file["action"] == "delete":
 604                continue
 605            sourceFiles[file["depotFile"]] = file
 606
 607        destinationFiles = {}
 608        for file in p4CmdList("files %s...@%s" % (self.globalPrefix + destinationBranch + "/", change)):
 609            destinationFiles[file["depotFile"]] = file
 610
 611        for fileName in sourceFiles.keys():
 612            integrations = []
 613            deleted = False
 614            integrationCount = 0
 615            for integration in p4CmdList("integrated \"%s\"" % fileName):
 616                toFile = integration["fromFile"] # yes, it's true, it's fromFile
 617                if not toFile in destinationFiles:
 618                    continue
 619                destFile = destinationFiles[toFile]
 620                if destFile["action"] == "delete":
 621    #                print "file %s has been deleted in %s" % (fileName, toFile)
 622                    deleted = True
 623                    break
 624                integrationCount += 1
 625                if integration["how"] == "branch from":
 626                    continue
 627
 628                if int(integration["change"]) == change:
 629                    integrations.append(integration)
 630                    continue
 631                if int(integration["change"]) > change:
 632                    continue
 633
 634                destRev = int(destFile["rev"])
 635
 636                startRev = integration["startFromRev"][1:]
 637                if startRev == "none":
 638                    startRev = 0
 639                else:
 640                    startRev = int(startRev)
 641
 642                endRev = integration["endFromRev"][1:]
 643                if endRev == "none":
 644                    endRev = 0
 645                else:
 646                    endRev = int(endRev)
 647
 648                initialBranch = (destRev == 1 and integration["how"] != "branch into")
 649                inRange = (destRev >= startRev and destRev <= endRev)
 650                newer = (destRev > startRev and destRev > endRev)
 651
 652                if initialBranch or inRange or newer:
 653                    integrations.append(integration)
 654
 655            if deleted:
 656                continue
 657
 658            if len(integrations) == 0 and integrationCount > 1:
 659                print "file %s was not integrated from %s into %s" % (fileName, sourceBranch, destinationBranch)
 660                return False
 661
 662        return True
 663
 664    def getUserMap(self):
 665        self.users = {}
 666
 667        for output in p4CmdList("users"):
 668            if not output.has_key("User"):
 669                continue
 670            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
 671
 672    def run(self, args):
 673        self.branch = "refs/heads/" + self.branch
 674        self.globalPrefix = self.previousDepotPath = os.popen("git-repo-config --get p4.depotpath").read()
 675        if len(self.globalPrefix) != 0:
 676            self.globalPrefix = self.globalPrefix[:-1]
 677
 678        if len(args) == 0 and len(self.globalPrefix) != 0:
 679            if not self.silent:
 680                print "[using previously specified depot path %s]" % self.globalPrefix
 681        elif len(args) != 1:
 682            return False
 683        else:
 684            if len(self.globalPrefix) != 0 and self.globalPrefix != args[0]:
 685                print "previous import used depot path %s and now %s was specified. this doesn't work!" % (self.globalPrefix, args[0])
 686                sys.exit(1)
 687            self.globalPrefix = args[0]
 688
 689        self.changeRange = ""
 690        self.revision = ""
 691        self.users = {}
 692        self.initialParent = ""
 693        self.lastChange = 0
 694        self.initialTag = ""
 695
 696        if self.globalPrefix.find("@") != -1:
 697            atIdx = self.globalPrefix.index("@")
 698            self.changeRange = self.globalPrefix[atIdx:]
 699            if self.changeRange == "@all":
 700                self.changeRange = ""
 701            elif self.changeRange.find(",") == -1:
 702                self.revision = self.changeRange
 703                self.changeRange = ""
 704            self.globalPrefix = self.globalPrefix[0:atIdx]
 705        elif self.globalPrefix.find("#") != -1:
 706            hashIdx = self.globalPrefix.index("#")
 707            self.revision = self.globalPrefix[hashIdx:]
 708            self.globalPrefix = self.globalPrefix[0:hashIdx]
 709        elif len(self.previousDepotPath) == 0:
 710            self.revision = "#head"
 711
 712        if self.globalPrefix.endswith("..."):
 713            self.globalPrefix = self.globalPrefix[:-3]
 714
 715        if not self.globalPrefix.endswith("/"):
 716            self.globalPrefix += "/"
 717
 718        self.getUserMap()
 719
 720        if len(self.changeRange) == 0:
 721            try:
 722                sout, sin, serr = popen2.popen3("git-name-rev --tags `git-rev-parse %s`" % self.branch)
 723                output = sout.read()
 724                if output.endswith("\n"):
 725                    output = output[:-1]
 726                tagIdx = output.index(" tags/p4/")
 727                caretIdx = output.find("^")
 728                endPos = len(output)
 729                if caretIdx != -1:
 730                    endPos = caretIdx
 731                self.rev = int(output[tagIdx + 9 : endPos]) + 1
 732                self.changeRange = "@%s,#head" % self.rev
 733                self.initialParent = os.popen("git-rev-parse %s" % self.branch).read()[:-1]
 734                self.initialTag = "p4/%s" % (int(self.rev) - 1)
 735            except:
 736                pass
 737
 738        tz = - time.timezone / 36
 739        tzsign = ("%s" % tz)[0]
 740        if tzsign != '+' and tzsign != '-':
 741            tz = "+" + ("%s" % tz)
 742
 743        self.gitOutput, self.gitStream, self.gitError = popen2.popen3("git-fast-import")
 744
 745        if len(self.revision) > 0:
 746            print "Doing initial import of %s from revision %s" % (self.globalPrefix, self.revision)
 747
 748            details = { "user" : "git perforce import user", "time" : int(time.time()) }
 749            details["desc"] = "Initial import of %s from the state at revision %s" % (self.globalPrefix, self.revision)
 750            details["change"] = self.revision
 751            newestRevision = 0
 752
 753            fileCnt = 0
 754            for info in p4CmdList("files %s...%s" % (self.globalPrefix, self.revision)):
 755                change = int(info["change"])
 756                if change > newestRevision:
 757                    newestRevision = change
 758
 759                if info["action"] == "delete":
 760                    continue
 761
 762                for prop in [ "depotFile", "rev", "action", "type" ]:
 763                    details["%s%s" % (prop, fileCnt)] = info[prop]
 764
 765                fileCnt = fileCnt + 1
 766
 767            details["change"] = newestRevision
 768
 769            try:
 770                self.commit(details, self.extractFilesFromCommit(details), self.branch, self.globalPrefix)
 771            except:
 772                print self.gitError.read()
 773
 774        else:
 775            changes = []
 776
 777            if len(changesFile) > 0:
 778                output = open(self.changesFile).readlines()
 779                changeSet = Set()
 780                for line in output:
 781                    changeSet.add(int(line))
 782
 783                for change in changeSet:
 784                    changes.append(change)
 785
 786                changes.sort()
 787            else:
 788                output = os.popen("p4 changes %s...%s" % (self.globalPrefix, self.changeRange)).readlines()
 789
 790                for line in output:
 791                    changeNum = line.split(" ")[1]
 792                    changes.append(changeNum)
 793
 794                changes.reverse()
 795
 796            if len(changes) == 0:
 797                if not silent:
 798                    print "no changes to import!"
 799                sys.exit(1)
 800
 801            cnt = 1
 802            for change in changes:
 803                description = p4Cmd("describe %s" % change)
 804
 805                if not silent:
 806                    sys.stdout.write("\rimporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
 807                    sys.stdout.flush()
 808                cnt = cnt + 1
 809
 810                try:
 811                    files = self.extractFilesFromCommit(description)
 812                    if self.detectBranches:
 813                        for branch in self.branchesForCommit(files):
 814                            self.knownBranches.add(branch)
 815                            branchPrefix = self.globalPrefix + branch + "/"
 816
 817                            filesForCommit = self.extractFilesInCommitToBranch(files, branchPrefix)
 818
 819                            merged = ""
 820                            parent = ""
 821                            ########### remove cnt!!!
 822                            if branch not in self.createdBranches and cnt > 2:
 823                                self.createdBranches.add(branch)
 824                                parent = self.findBranchParent(branchPrefix, files)
 825                                if parent == branch:
 826                                    parent = ""
 827            #                    elif len(parent) > 0:
 828            #                        print "%s branched off of %s" % (branch, parent)
 829
 830                            if len(parent) == 0:
 831                                merged = self.findBranchSourceHeuristic(filesForCommit, branch, branchPrefix)
 832                                if len(merged) > 0:
 833                                    print "change %s could be a merge from %s into %s" % (description["change"], merged, branch)
 834                                    if not self.changeIsBranchMerge(merged, branch, int(description["change"])):
 835                                        merged = ""
 836
 837                            branch = "refs/heads/" + branch
 838                            if len(parent) > 0:
 839                                parent = "refs/heads/" + parent
 840                            if len(merged) > 0:
 841                                merged = "refs/heads/" + merged
 842                            self.commit(description, files, branch, branchPrefix, parent, merged)
 843                    else:
 844                        self.commit(description, files, branch, globalPrefix, initialParent)
 845                        self.initialParent = ""
 846                except IOError:
 847                    print self.gitError.read()
 848                    sys.exit(1)
 849
 850        if not self.silent:
 851            print ""
 852
 853        self.gitStream.write("reset refs/tags/p4/%s\n" % self.lastChange)
 854        self.gitStream.write("from %s\n\n" % self.branch);
 855
 856
 857        self.gitStream.close()
 858        self.gitOutput.close()
 859        self.gitError.close()
 860
 861        os.popen("git-repo-config p4.depotpath %s" % self.globalPrefix).read()
 862        if len(self.initialTag) > 0:
 863            os.popen("git tag -d %s" % self.initialTag).read()
 864
 865        return True
 866
 867class HelpFormatter(optparse.IndentedHelpFormatter):
 868    def __init__(self):
 869        optparse.IndentedHelpFormatter.__init__(self)
 870
 871    def format_description(self, description):
 872        if description:
 873            return description + "\n"
 874        else:
 875            return ""
 876
 877def printUsage(commands):
 878    print "usage: %s <command> [options]" % sys.argv[0]
 879    print ""
 880    print "valid commands: %s" % ", ".join(commands)
 881    print ""
 882    print "Try %s <command> --help for command specific help." % sys.argv[0]
 883    print ""
 884
 885commands = {
 886    "debug" : P4Debug(),
 887    "clean-tags" : P4CleanTags(),
 888    "submit" : P4Sync(),
 889    "sync" : GitSync()
 890}
 891
 892if len(sys.argv[1:]) == 0:
 893    printUsage(commands.keys())
 894    sys.exit(2)
 895
 896cmd = ""
 897cmdName = sys.argv[1]
 898try:
 899    cmd = commands[cmdName]
 900except KeyError:
 901    print "unknown command %s" % cmdName
 902    print ""
 903    printUsage(commands.keys())
 904    sys.exit(2)
 905
 906options = cmd.options
 907cmd.gitdir = gitdir
 908options.append(optparse.make_option("--git-dir", dest="gitdir"))
 909
 910parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
 911                               options,
 912                               description = cmd.description,
 913                               formatter = HelpFormatter())
 914
 915(cmd, args) = parser.parse_args(sys.argv[2:], cmd);
 916
 917gitdir = cmd.gitdir
 918if len(gitdir) == 0:
 919    gitdir = ".git"
 920
 921if not isValidGitDir(gitdir):
 922    if isValidGitDir(gitdir + "/.git"):
 923        gitdir += "/.git"
 924    else:
 925        die("fatal: cannot locate git repository at %s" % gitdir)
 926
 927os.environ["GIT_DIR"] = gitdir
 928
 929if not cmd.run(args):
 930    parser.print_help()
 931