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