be4fd36c361e263ff4d7bddc3df0ba29a5b92e8e
   1#!/usr/bin/python
   2#
   3# p4-fast-export.py
   4#
   5# Author: Simon Hausmann <hausmann@kde.org>
   6# License: MIT <http://www.opensource.org/licenses/mit-license.php>
   7#
   8# TODO:
   9#       - support integrations (at least p4i)
  10#       - support p4 submit (hah!)
  11#
  12import os, string, sys, time
  13import marshal, popen2
  14
  15branch = "refs/heads/p4"
  16prefix = os.popen("git-repo-config --get p4.depotpath").read()
  17if len(prefix) != 0:
  18    prefix = prefix[:-1]
  19
  20if len(sys.argv) == 1 and len(prefix) != 0:
  21    print "[using previously specified depot path %s]" % prefix
  22elif len(sys.argv) != 2:
  23    print "usage: %s //depot/path[@revRange]" % sys.argv[0]
  24    print "\n    example:"
  25    print "    %s //depot/my/project/ -- to import everything"
  26    print "    %s //depot/my/project/@1,6 -- to import only from revision 1 to 6"
  27    print ""
  28    print "    (a ... is not needed in the path p4 specification, it's added implicitly)"
  29    print ""
  30    sys.exit(1)
  31else:
  32    if len(prefix) != 0 and prefix != sys.argv[1]:
  33        print "previous import used depot path %s and now %s was specified. this doesn't work!" % (prefix, sys.argv[1])
  34        sys.exit(1)
  35    prefix = sys.argv[1]
  36
  37changeRange = ""
  38revision = ""
  39users = {}
  40initialParent = ""
  41
  42if prefix.find("@") != -1:
  43    atIdx = prefix.index("@")
  44    changeRange = prefix[atIdx:]
  45    if changeRange.find(",") == -1:
  46        revision = changeRange
  47        changeRange = ""
  48    prefix = prefix[0:atIdx]
  49elif prefix.find("#") != -1:
  50    hashIdx = prefix.index("#")
  51    revision = prefix[hashIdx:]
  52    prefix = prefix[0:hashIdx]
  53
  54if prefix.endswith("..."):
  55    prefix = prefix[:-3]
  56
  57if not prefix.endswith("/"):
  58    prefix += "/"
  59
  60def p4CmdList(cmd):
  61    pipe = os.popen("p4 -G %s" % cmd, "rb")
  62    result = []
  63    try:
  64        while True:
  65            entry = marshal.load(pipe)
  66            result.append(entry)
  67    except EOFError:
  68        pass
  69    pipe.close()
  70    return result
  71
  72def p4Cmd(cmd):
  73    list = p4CmdList(cmd)
  74    result = {}
  75    for entry in list:
  76        result.update(entry)
  77    return result;
  78
  79def commit(details):
  80    global initialParent
  81    global users
  82
  83    epoch = details["time"]
  84    author = details["user"]
  85
  86    gitStream.write("commit %s\n" % branch)
  87    committer = ""
  88    if author in users:
  89        committer = "%s %s %s" % (users[author], epoch, tz)
  90    else:
  91        committer = "%s <a@b> %s %s" % (author, epoch, tz)
  92
  93    gitStream.write("committer %s\n" % committer)
  94
  95    gitStream.write("data <<EOT\n")
  96    gitStream.write(details["desc"])
  97    gitStream.write("\n[ imported from %s; change %s ]\n" % (prefix, details["change"]))
  98    gitStream.write("EOT\n\n")
  99
 100    if len(initialParent) > 0:
 101        gitStream.write("from %s\n" % initialParent)
 102        initialParent = ""
 103
 104    fnum = 0
 105    while details.has_key("depotFile%s" % fnum):
 106        path = details["depotFile%s" % fnum]
 107        if not path.startswith(prefix):
 108            print "\nchanged files: ignoring path %s outside of %s in change %s" % (path, prefix, change)
 109            fnum = fnum + 1
 110            continue
 111
 112        rev = details["rev%s" % fnum]
 113        depotPath = path + "#" + rev
 114        relPath = path[len(prefix):]
 115        action = details["action%s" % fnum]
 116
 117        if action == "delete":
 118            gitStream.write("D %s\n" % relPath)
 119        else:
 120            mode = 644
 121            if details["type%s" % fnum].startswith("x"):
 122                mode = 755
 123
 124            data = os.popen("p4 print -q \"%s\"" % depotPath, "rb").read()
 125
 126            gitStream.write("M %s inline %s\n" % (mode, relPath))
 127            gitStream.write("data %s\n" % len(data))
 128            gitStream.write(data)
 129            gitStream.write("\n")
 130
 131        fnum = fnum + 1
 132
 133    gitStream.write("\n")
 134
 135    gitStream.write("tag p4/%s\n" % details["change"])
 136    gitStream.write("from %s\n" % branch);
 137    gitStream.write("tagger %s\n" % committer);
 138    gitStream.write("data 0\n\n")
 139
 140
 141def getUserMap():
 142    users = {}
 143
 144    for output in p4CmdList("users"):
 145        if not output.has_key("User"):
 146            continue
 147        users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
 148    return users
 149
 150users = getUserMap()
 151
 152if len(changeRange) == 0:
 153    try:
 154        sout, sin, serr = popen2.popen3("git-name-rev --tags `git-rev-parse %s`" % branch)
 155        output = sout.read()
 156        tagIdx = output.index(" tags/p4/")
 157        caretIdx = output.index("^")
 158        rev = int(output[tagIdx + 9 : caretIdx]) + 1
 159        changeRange = "@%s,#head" % rev
 160        initialParent = os.popen("git-rev-parse %s" % branch).read()[:-1]
 161    except:
 162        pass
 163
 164sys.stderr.write("\n")
 165
 166tz = - time.timezone / 36
 167tzsign = ("%s" % tz)[0]
 168if tzsign != '+' and tzsign != '-':
 169    tz = "+" + ("%s" % tz)
 170
 171if len(revision) > 0:
 172    print "Doing initial import of %s from revision %s" % (prefix, revision)
 173
 174    details = { "user" : "git perforce import user", "time" : int(time.time()) }
 175    details["desc"] = "Initial import of %s from the state at revision %s" % (prefix, revision)
 176    details["change"] = revision
 177    newestRevision = 0
 178
 179    fileCnt = 0
 180    for info in p4CmdList("files %s...%s" % (prefix, revision)):
 181        change = info["change"]
 182        if change > newestRevision:
 183            newestRevision = change
 184
 185        if info["action"] == "delete":
 186            continue
 187
 188        for prop in [ "depotFile", "rev", "action", "type" ]:
 189            details["%s%s" % (prop, fileCnt)] = info[prop]
 190
 191        fileCnt = fileCnt + 1
 192
 193    details["change"] = newestRevision
 194
 195    gitOutput, gitStream, gitError = popen2.popen3("git-fast-import")
 196    try:
 197        commit(details)
 198    except:
 199        print gitError.read()
 200
 201    gitStream.close()
 202    gitOutput.close()
 203    gitError.close()
 204else:
 205    output = os.popen("p4 changes %s...%s" % (prefix, changeRange)).readlines()
 206
 207    changes = []
 208    for line in output:
 209        changeNum = line.split(" ")[1]
 210        changes.append(changeNum)
 211
 212    changes.reverse()
 213
 214    if len(changes) == 0:
 215        print "no changes to import!"
 216        sys.exit(1)
 217
 218    gitOutput, gitStream, gitError = popen2.popen3("git-fast-import")
 219
 220    cnt = 1
 221    for change in changes:
 222        description = p4Cmd("describe %s" % change)
 223
 224        sys.stdout.write("\rimporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
 225        sys.stdout.flush()
 226        cnt = cnt + 1
 227
 228        commit(description)
 229
 230    gitStream.close()
 231    gitOutput.close()
 232    gitError.close()
 233
 234print ""
 235
 236os.popen("git-repo-config p4.depotpath %s" % prefix).read()
 237