593bda822fc955098b72bfcd1eeb618391b32668
   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# License: MIT <http://www.opensource.org/licenses/mit-license.php>
   7#
   8
   9import optparse, sys, os, marshal, popen2, shelve
  10import tempfile
  11
  12gitdir = os.environ.get("GIT_DIR", "")
  13
  14def p4CmdList(cmd):
  15    cmd = "p4 -G %s" % cmd
  16    pipe = os.popen(cmd, "rb")
  17
  18    result = []
  19    try:
  20        while True:
  21            entry = marshal.load(pipe)
  22            result.append(entry)
  23    except EOFError:
  24        pass
  25    pipe.close()
  26
  27    return result
  28
  29def p4Cmd(cmd):
  30    list = p4CmdList(cmd)
  31    result = {}
  32    for entry in list:
  33        result.update(entry)
  34    return result;
  35
  36def die(msg):
  37    sys.stderr.write(msg + "\n")
  38    sys.exit(1)
  39
  40def currentGitBranch():
  41    return os.popen("git-name-rev HEAD").read().split(" ")[1][:-1]
  42
  43def isValidGitDir(path):
  44    if os.path.exists(path + "/HEAD") and os.path.exists(path + "/refs") and os.path.exists(path + "/objects"):
  45        return True;
  46    return False
  47
  48def system(cmd):
  49    if os.system(cmd) != 0:
  50        die("command failed: %s" % cmd)
  51
  52class P4Debug:
  53    def __init__(self):
  54        self.options = [
  55        ]
  56        self.description = "A tool to debug the output of p4 -G."
  57
  58    def run(self, args):
  59        for output in p4CmdList(" ".join(args)):
  60            print output
  61
  62class P4CleanTags:
  63    def __init__(self):
  64        self.options = [
  65#                optparse.make_option("--branch", dest="branch", default="refs/heads/master")
  66        ]
  67        self.description = "A tool to remove stale unused tags from incremental perforce imports."
  68    def run(self, args):
  69        branch = currentGitBranch()
  70        print "Cleaning out stale p4 import tags..."
  71        sout, sin, serr = popen2.popen3("git-name-rev --tags `git-rev-parse %s`" % branch)
  72        output = sout.read()
  73        try:
  74            tagIdx = output.index(" tags/p4/")
  75        except:
  76            print "Cannot find any p4/* tag. Nothing to do."
  77            sys.exit(0)
  78
  79        try:
  80            caretIdx = output.index("^")
  81        except:
  82            caretIdx = len(output) - 1
  83        rev = int(output[tagIdx + 9 : caretIdx])
  84
  85        allTags = os.popen("git tag -l p4/").readlines()
  86        for i in range(len(allTags)):
  87            allTags[i] = int(allTags[i][3:-1])
  88
  89        allTags.sort()
  90
  91        allTags.remove(rev)
  92
  93        for rev in allTags:
  94            print os.popen("git tag -d p4/%s" % rev).read()
  95
  96        print "%s tags removed." % len(allTags)
  97
  98class P4Sync:
  99    def __init__(self):
 100        self.options = [
 101                optparse.make_option("--continue", action="store_false", dest="firstTime"),
 102                optparse.make_option("--origin", dest="origin"),
 103                optparse.make_option("--reset", action="store_true", dest="reset"),
 104                optparse.make_option("--master", dest="master"),
 105                optparse.make_option("--log-substitutions", dest="substFile"),
 106                optparse.make_option("--noninteractive", action="store_false"),
 107                optparse.make_option("--dry-run", action="store_true")
 108        ]
 109        self.description = "Submit changes from git to the perforce depot."
 110        self.firstTime = True
 111        self.reset = False
 112        self.interactive = True
 113        self.dryRun = False
 114        self.substFile = ""
 115        self.firstTime = True
 116        self.origin = "origin"
 117        self.master = ""
 118
 119        self.logSubstitutions = {}
 120        self.logSubstitutions["<enter description here>"] = "%log%"
 121        self.logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
 122
 123    def check(self):
 124        if len(p4CmdList("opened ...")) > 0:
 125            die("You have files opened with perforce! Close them before starting the sync.")
 126
 127    def start(self):
 128        if len(self.config) > 0 and not self.reset:
 129            die("Cannot start sync. Previous sync config found at %s" % self.configFile)
 130
 131        commits = []
 132        for line in os.popen("git-rev-list --no-merges %s..%s" % (self.origin, self.master)).readlines():
 133            commits.append(line[:-1])
 134        commits.reverse()
 135
 136        self.config["commits"] = commits
 137
 138        print "Creating temporary p4-sync branch from %s ..." % self.origin
 139        system("git checkout -f -b p4-sync %s" % self.origin)
 140
 141    def prepareLogMessage(self, template, message):
 142        result = ""
 143
 144        for line in template.split("\n"):
 145            if line.startswith("#"):
 146                result += line + "\n"
 147                continue
 148
 149            substituted = False
 150            for key in self.logSubstitutions.keys():
 151                if line.find(key) != -1:
 152                    value = self.logSubstitutions[key]
 153                    value = value.replace("%log%", message)
 154                    if value != "@remove@":
 155                        result += line.replace(key, value) + "\n"
 156                    substituted = True
 157                    break
 158
 159            if not substituted:
 160                result += line + "\n"
 161
 162        return result
 163
 164    def apply(self, id):
 165        print "Applying %s" % (os.popen("git-log --max-count=1 --pretty=oneline %s" % id).read())
 166        diff = os.popen("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)).readlines()
 167        filesToAdd = set()
 168        filesToDelete = set()
 169        for line in diff:
 170            modifier = line[0]
 171            path = line[1:].strip()
 172            if modifier == "M":
 173                system("p4 edit %s" % path)
 174            elif modifier == "A":
 175                filesToAdd.add(path)
 176                if path in filesToDelete:
 177                    filesToDelete.remove(path)
 178            elif modifier == "D":
 179                filesToDelete.add(path)
 180                if path in filesToAdd:
 181                    filesToAdd.remove(path)
 182            else:
 183                die("unknown modifier %s for %s" % (modifier, path))
 184
 185        system("git-diff-files --name-only -z | git-update-index --remove -z --stdin")
 186        system("git cherry-pick --no-commit \"%s\"" % id)
 187
 188        for f in filesToAdd:
 189            system("p4 add %s" % f)
 190        for f in filesToDelete:
 191            system("p4 revert %s" % f)
 192            system("p4 delete %s" % f)
 193
 194        logMessage = ""
 195        foundTitle = False
 196        for log in os.popen("git-cat-file commit %s" % id).readlines():
 197            if not foundTitle:
 198                if len(log) == 1:
 199                    foundTitle = 1
 200                continue
 201
 202            if len(logMessage) > 0:
 203                logMessage += "\t"
 204            logMessage += log
 205
 206        template = os.popen("p4 change -o").read()
 207
 208        if self.interactive:
 209            submitTemplate = self.prepareLogMessage(template, logMessage)
 210            diff = os.popen("p4 diff -du ...").read()
 211
 212            for newFile in filesToAdd:
 213                diff += "==== new file ====\n"
 214                diff += "--- /dev/null\n"
 215                diff += "+++ %s\n" % newFile
 216                f = open(newFile, "r")
 217                for line in f.readlines():
 218                    diff += "+" + line
 219                f.close()
 220
 221            pipe = os.popen("less", "w")
 222            pipe.write(submitTemplate + diff)
 223            pipe.close()
 224
 225            response = "e"
 226            while response == "e":
 227                response = raw_input("Do you want to submit this change (y/e/n)? ")
 228                if response == "e":
 229                    [handle, fileName] = tempfile.mkstemp()
 230                    tmpFile = os.fdopen(handle, "w+")
 231                    tmpFile.write(submitTemplate)
 232                    tmpFile.close()
 233                    editor = os.environ.get("EDITOR", "vi")
 234                    system(editor + " " + fileName)
 235                    tmpFile = open(fileName, "r")
 236                    submitTemplate = tmpFile.read()
 237                    tmpFile.close()
 238                    os.remove(fileName)
 239
 240            if response == "y" or response == "yes":
 241               if self.dryRun:
 242                   print submitTemplate
 243                   raw_input("Press return to continue...")
 244               else:
 245                    pipe = os.popen("p4 submit -i", "w")
 246                    pipe.write(submitTemplate)
 247                    pipe.close()
 248            else:
 249                print "Not submitting!"
 250                self.interactive = False
 251        else:
 252            fileName = "submit.txt"
 253            file = open(fileName, "w+")
 254            file.write(self.prepareLogMessage(template, logMessage))
 255            file.close()
 256            print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName, fileName)
 257
 258    def run(self, args):
 259        if self.reset:
 260            self.firstTime = True
 261
 262        if len(self.substFile) > 0:
 263            for line in open(self.substFile, "r").readlines():
 264                tokens = line[:-1].split("=")
 265                self.logSubstitutions[tokens[0]] = tokens[1]
 266
 267        if len(self.master) == 0:
 268            self.master = currentGitBranch()
 269            if len(self.master) == 0 or not os.path.exists("%s/refs/heads/%s" % (gitdir, self.master)):
 270                die("Detecting current git branch failed!")
 271
 272        self.check()
 273        self.configFile = gitdir + "/p4-git-sync.cfg"
 274        self.config = shelve.open(self.configFile, writeback=True)
 275
 276        if self.firstTime:
 277            self.start()
 278
 279        commits = self.config.get("commits", [])
 280
 281        while len(commits) > 0:
 282            self.firstTime = False
 283            commit = commits[0]
 284            commits = commits[1:]
 285            self.config["commits"] = commits
 286            self.apply(commit)
 287            if not self.interactive:
 288                break
 289
 290        self.config.close()
 291
 292        if len(commits) == 0:
 293            if self.firstTime:
 294                print "No changes found to apply between %s and current HEAD" % self.origin
 295            else:
 296                print "All changes applied!"
 297                print "Deleting temporary p4-sync branch and going back to %s" % self.master
 298                system("git checkout %s" % self.master)
 299                system("git branch -D p4-sync")
 300                print "Cleaning out your perforce checkout by doing p4 edit ... ; p4 revert ..."
 301                system("p4 edit ... >/dev/null")
 302                system("p4 revert ... >/dev/null")
 303            os.remove(self.configFile)
 304
 305
 306def printUsage(commands):
 307    print "usage: %s <command> [options]" % sys.argv[0]
 308    print ""
 309    print "valid commands: %s" % ", ".join(commands)
 310    print ""
 311    print "Try %s <command> --help for command specific help." % sys.argv[0]
 312    print ""
 313
 314commands = {
 315    "debug" : P4Debug(),
 316    "clean-tags" : P4CleanTags(),
 317    "sync-to-perforce" : P4Sync()
 318}
 319
 320if len(sys.argv[1:]) == 0:
 321    printUsage(commands.keys())
 322    sys.exit(2)
 323
 324cmd = ""
 325cmdName = sys.argv[1]
 326try:
 327    cmd = commands[cmdName]
 328except KeyError:
 329    print "unknown command %s" % cmdName
 330    print ""
 331    printUsage(commands.keys())
 332    sys.exit(2)
 333
 334options = cmd.options
 335cmd.gitdir = gitdir
 336options.append(optparse.make_option("--git-dir", dest="gitdir"))
 337
 338parser = optparse.OptionParser("usage: %prog " + cmdName + " [options]", options,
 339                               description = cmd.description)
 340
 341(cmd, args) = parser.parse_args(sys.argv[2:], cmd);
 342
 343gitdir = cmd.gitdir
 344if len(gitdir) == 0:
 345    gitdir = ".git"
 346
 347if not isValidGitDir(gitdir):
 348    if isValidGitDir(gitdir + "/.git"):
 349        gitdir += "/.git"
 350    else:
 351        dir("fatal: cannot locate git repository at %s" % gitdir)
 352
 353os.environ["GIT_DIR"] = gitdir
 354
 355cmd.run(args)