e07fcee42c13e98a8a34de982eb3cc2f5bb14b60
   1#!/usr/bin/env python
   2#
   3# p4-git-sync.py
   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 os, string, shelve, stat
  12import getopt, sys, marshal, tempfile
  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
  40try:
  41    opts, args = getopt.getopt(sys.argv[1:], "", [ "continue", "git-dir=", "origin=", "reset", "master=",
  42                                                   "submit-log-subst=", "log-substitutions=", "interactive",
  43                                                   "dry-run" ])
  44except getopt.GetoptError:
  45    print "fixme, syntax error"
  46    sys.exit(1)
  47
  48logSubstitutions = {}
  49logSubstitutions["<enter description here>"] = "%log%"
  50logSubstitutions["\tDetails:"] = "\tDetails:  %log%"
  51gitdir = os.environ.get("GIT_DIR", "")
  52origin = "origin"
  53master = ""
  54firstTime = True
  55reset = False
  56interactive = False
  57dryRun = False
  58
  59for o, a in opts:
  60    if o == "--git-dir":
  61        gitdir = a
  62    elif o == "--origin":
  63        origin = a
  64    elif o == "--master":
  65        master = a
  66    elif o == "--continue":
  67        firstTime = False
  68    elif o == "--reset":
  69        reset = True
  70        firstTime = True
  71    elif o == "--submit-log-subst":
  72        key = a.split("%")[0]
  73        value = a.split("%")[1]
  74        logSubstitutions[key] = value
  75    elif o == "--log-substitutions":
  76        for line in open(a, "r").readlines():
  77            tokens = line[:-1].split("=")
  78            logSubstitutions[tokens[0]] = tokens[1]
  79    elif o == "--interactive":
  80        interactive = True
  81    elif o == "--dry-run":
  82        dryRun = True
  83
  84if len(gitdir) == 0:
  85    gitdir = ".git"
  86else:
  87    os.environ["GIT_DIR"] = gitdir
  88
  89configFile = gitdir + "/p4-git-sync.cfg"
  90
  91origin = "origin"
  92if len(args) == 1:
  93    origin = args[0]
  94
  95if len(master) == 0:
  96    sys.stdout.write("Auto-detecting current branch: ")
  97    master = os.popen("git-name-rev HEAD").read().split(" ")[1][:-1]
  98    if len(master) == 0 or not os.path.exists("%s/refs/heads/%s" % (gitdir, master)):
  99        die("\nFailed to detect current branch! Aborting!");
 100    sys.stdout.write("%s\n" % master)
 101
 102def system(cmd):
 103    if os.system(cmd) != 0:
 104        die("command failed: %s" % cmd)
 105
 106def check():
 107    if len(p4CmdList("opened ...")) > 0:
 108        die("You have files opened with perforce! Close them before starting the sync.")
 109
 110def start(config):
 111    if len(config) > 0 and not reset:
 112        die("Cannot start sync. Previous sync config found at %s" % configFile)
 113
 114    #if len(os.popen("git-update-index --refresh").read()) > 0:
 115    #    die("Your working tree is not clean. Check with git status!")
 116
 117    commits = []
 118    for line in os.popen("git-rev-list --no-merges %s..%s" % (origin, master)).readlines():
 119        commits.append(line[:-1])
 120    commits.reverse()
 121
 122    config["commits"] = commits
 123
 124    print "Creating temporary p4-sync branch from %s ..." % origin
 125    system("git checkout -f -b p4-sync %s" % origin)
 126
 127#    print "Cleaning index..."
 128#    system("git checkout -f")
 129
 130def prepareLogMessage(template, message):
 131    result = ""
 132
 133    for line in template.split("\n"):
 134        if line.startswith("#"):
 135            result += line + "\n"
 136            continue
 137
 138        substituted = False
 139        for key in logSubstitutions.keys():
 140            if line.find(key) != -1:
 141                value = logSubstitutions[key]
 142                value = value.replace("%log%", message)
 143                if value != "@remove@":
 144                    result += line.replace(key, value) + "\n"
 145                substituted = True
 146                break
 147
 148        if not substituted:
 149            result += line + "\n"
 150
 151    return result
 152
 153def apply(id):
 154    global interactive
 155    print "Applying %s" % (os.popen("git-log --max-count=1 --pretty=oneline %s" % id).read())
 156    diff = os.popen("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)).readlines()
 157    filesToAdd = set()
 158    filesToDelete = set()
 159    for line in diff:
 160        modifier = line[0]
 161        path = line[1:].strip()
 162        if modifier == "M":
 163            system("p4 edit %s" % path)
 164        elif modifier == "A":
 165            filesToAdd.add(path)
 166            if path in filesToDelete:
 167                filesToDelete.remove(path)
 168        elif modifier == "D":
 169            filesToDelete.add(path)
 170            if path in filesToAdd:
 171                filesToAdd.remove(path)
 172        else:
 173            die("unknown modifier %s for %s" % (modifier, path))
 174
 175    system("git-diff-files --name-only -z | git-update-index --remove -z --stdin")
 176    system("git cherry-pick --no-commit \"%s\"" % id)
 177    #system("git format-patch --stdout -k \"%s^\"..\"%s\" | git-am -k" % (id, id))
 178    #system("git branch -D tmp")
 179    #system("git checkout -f -b tmp \"%s^\"" % id)
 180
 181    for f in filesToAdd:
 182        system("p4 add %s" % f)
 183    for f in filesToDelete:
 184        system("p4 revert %s" % f)
 185        system("p4 delete %s" % f)
 186
 187    logMessage = ""
 188    foundTitle = False
 189    for log in os.popen("git-cat-file commit %s" % id).readlines():
 190        if not foundTitle:
 191            if len(log) == 1:
 192                foundTitle = 1
 193            continue
 194
 195        if len(logMessage) > 0:
 196            logMessage += "\t"
 197        logMessage += log
 198
 199    template = os.popen("p4 change -o").read()
 200
 201    if interactive:
 202        submitTemplate = prepareLogMessage(template, logMessage)
 203        diff = os.popen("p4 diff -du ...").read()
 204
 205        for newFile in filesToAdd:
 206            diff += "==== new file ====\n"
 207            diff += "--- /dev/null\n"
 208            diff += "+++ %s\n" % newFile
 209            f = open(newFile, "r")
 210            for line in f.readlines():
 211                diff += "+" + line
 212            f.close()
 213
 214        pipe = os.popen("less", "w")
 215        pipe.write(submitTemplate + diff)
 216        pipe.close()
 217
 218        response = "e"
 219        while response == "e":
 220            response = raw_input("Do you want to submit this change (y/e/n)? ")
 221            if response == "e":
 222                [handle, fileName] = tempfile.mkstemp()
 223                tmpFile = os.fdopen(handle, "w+")
 224                tmpFile.write(submitTemplate)
 225                tmpFile.close()
 226                editor = os.environ.get("EDITOR", "vi")
 227                system(editor + " " + fileName)
 228                tmpFile = open(fileName, "r")
 229                submitTemplate = tmpFile.read()
 230                tmpFile.close()
 231                os.remove(fileName)
 232
 233        if response == "y" or response == "yes":
 234           if dryRun:
 235               print submitTemplate
 236               raw_input("Press return to continue...")
 237           else:
 238                pipe = os.popen("p4 submit -i", "w")
 239                pipe.write(submitTemplate)
 240                pipe.close()
 241        else:
 242            print "Not submitting!"
 243            interactive = False
 244    else:
 245        fileName = "submit.txt"
 246        file = open(fileName, "w+")
 247        file.write(prepareLogMessage(template, logMessage))
 248        file.close()
 249        print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName, fileName)
 250
 251check()
 252
 253config = shelve.open(configFile, writeback=True)
 254
 255if firstTime:
 256    start(config)
 257
 258commits = config.get("commits", [])
 259
 260while len(commits) > 0:
 261    firstTime = False
 262    commit = commits[0]
 263    commits = commits[1:]
 264    config["commits"] = commits
 265    apply(commit)
 266    if not interactive:
 267        break
 268
 269config.close()
 270
 271if len(commits) == 0:
 272    if firstTime:
 273        print "No changes found to apply between %s and current HEAD" % origin
 274    else:
 275        print "All changes applied!"
 276        print "Deleting temporary p4-sync branch and going back to %s" % master
 277        system("git checkout %s" % master)
 278        system("git branch -D p4-sync")
 279        print "Cleaning out your perforce checkout by doing p4 edit ... ; p4 revert ..."
 280        system("p4 edit ... >/dev/null")
 281        system("p4 revert ... >/dev/null")
 282    os.remove(configFile)
 283