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