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
13
14gitdir = os.environ.get("GIT_DIR", "")
15
16def p4CmdList(cmd):
17 cmd = "p4 -G %s" % cmd
18 pipe = os.popen(cmd, "rb")
19
20 result = []
21 try:
22 while True:
23 entry = marshal.load(pipe)
24 result.append(entry)
25 except EOFError:
26 pass
27 pipe.close()
28
29 return result
30
31def p4Cmd(cmd):
32 list = p4CmdList(cmd)
33 result = {}
34 for entry in list:
35 result.update(entry)
36 return result;
37
38def die(msg):
39 sys.stderr.write(msg + "\n")
40 sys.exit(1)
41
42def currentGitBranch():
43 return os.popen("git-name-rev HEAD").read().split(" ")[1][:-1]
44
45def 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
49
50def system(cmd):
51 if os.system(cmd) != 0:
52 die("command failed: %s" % cmd)
53
54class P4Debug:
55 def __init__(self):
56 self.options = [
57 ]
58 self.description = "A tool to debug the output of p4 -G."
59
60 def run(self, args):
61 for output in p4CmdList(" ".join(args)):
62 print output
63
64class P4CleanTags:
65 def __init__(self):
66 self.options = [
67# optparse.make_option("--branch", dest="branch", default="refs/heads/master")
68 ]
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)
80
81 try:
82 caretIdx = output.index("^")
83 except:
84 caretIdx = len(output) - 1
85 rev = int(output[tagIdx + 9 : caretIdx])
86
87 allTags = os.popen("git tag -l p4/").readlines()
88 for i in range(len(allTags)):
89 allTags[i] = int(allTags[i][3:-1])
90
91 allTags.sort()
92
93 allTags.remove(rev)
94
95 for rev in allTags:
96 print os.popen("git tag -d p4/%s" % rev).read()
97
98 print "%s tags removed." % len(allTags)
99
100class 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")
110 ]
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 = ""
120
121 self.logSubstitutions = {}
122 self.logSubstitutions["<enter description here>"] = "%log%"
123 self.logSubstitutions["\tDetails:"] = "\tDetails: %log%"
124
125 def check(self):
126 if len(p4CmdList("opened ...")) > 0:
127 die("You have files opened with perforce! Close them before starting the sync.")
128
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)
132
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()
137
138 self.config["commits"] = commits
139
140 print "Creating temporary p4-sync branch from %s ..." % self.origin
141 system("git checkout -f -b p4-sync %s" % self.origin)
142
143 def prepareLogMessage(self, template, message):
144 result = ""
145
146 for line in template.split("\n"):
147 if line.startswith("#"):
148 result += line + "\n"
149 continue
150
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
160
161 if not substituted:
162 result += line + "\n"
163
164 return result
165
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))
186
187 system("git-diff-files --name-only -z | git-update-index --remove -z --stdin")
188 system("git cherry-pick --no-commit \"%s\"" % id)
189
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)
195
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
203
204 if len(logMessage) > 0:
205 logMessage += "\t"
206 logMessage += log
207
208 template = os.popen("p4 change -o").read()
209
210 if self.interactive:
211 submitTemplate = self.prepareLogMessage(template, logMessage)
212 diff = os.popen("p4 diff -du ...").read()
213
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()
222
223 pipe = os.popen("less", "w")
224 pipe.write(submitTemplate + diff)
225 pipe.close()
226
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)
241
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)
259
260 def run(self, args):
261 if self.reset:
262 self.firstTime = True
263
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]
268
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!")
273
274 self.check()
275 self.configFile = gitdir + "/p4-git-sync.cfg"
276 self.config = shelve.open(self.configFile, writeback=True)
277
278 if self.firstTime:
279 self.start()
280
281 commits = self.config.get("commits", [])
282
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
291
292 self.config.close()
293
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)
306
307
308def 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 ""
315
316commands = {
317 "debug" : P4Debug(),
318 "clean-tags" : P4CleanTags(),
319 "sync-to-perforce" : P4Sync()
320}
321
322if len(sys.argv[1:]) == 0:
323 printUsage(commands.keys())
324 sys.exit(2)
325
326cmd = ""
327cmdName = sys.argv[1]
328try:
329 cmd = commands[cmdName]
330except KeyError:
331 print "unknown command %s" % cmdName
332 print ""
333 printUsage(commands.keys())
334 sys.exit(2)
335
336options = cmd.options
337cmd.gitdir = gitdir
338options.append(optparse.make_option("--git-dir", dest="gitdir"))
339
340parser = optparse.OptionParser("usage: %prog " + cmdName + " [options]", options,
341 description = cmd.description)
342
343(cmd, args) = parser.parse_args(sys.argv[2:], cmd);
344
345gitdir = cmd.gitdir
346if len(gitdir) == 0:
347 gitdir = ".git"
348
349if not isValidGitDir(gitdir):
350 if isValidGitDir(gitdir + "/.git"):
351 gitdir += "/.git"
352 else:
353 dir("fatal: cannot locate git repository at %s" % gitdir)
354
355os.environ["GIT_DIR"] = gitdir
356
357cmd.run(args)