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>
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
11 import optparse
, sys
, os
, marshal
, popen2
, shelve
14 gitdir
= os
.environ
.get("GIT_DIR", "")
17 cmd
= "p4 -G %s" % cmd
18 pipe
= os
.popen(cmd
, "rb")
23 entry
= marshal
.load(pipe
)
39 sys
.stderr
.write(msg
+ "\n")
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"):
51 if os
.system(cmd
) != 0:
52 die("command failed: %s" % cmd
)
58 self
.description
= "A tool to debug the output of p4 -G."
61 for output
in p4CmdList(" ".join(args
)):
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."
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
)
76 tagIdx
= output
.index(" tags/p4/")
78 print "Cannot find any p4/* tag. Nothing to do."
82 caretIdx
= output
.index("^")
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])
96 print os
.popen("git tag -d p4/%s" % rev
).read()
98 print "%s tags removed." % len(allTags
)
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
114 self
.interactive
= True
117 self
.firstTime
= True
118 self
.origin
= "origin"
121 self
.logSubstitutions
= {}
122 self
.logSubstitutions
["<enter description here>"] = "%log%"
123 self
.logSubstitutions
["\tDetails:"] = "\tDetails: %log%"
126 if len(p4CmdList("opened ...")) > 0:
127 die("You have files opened with perforce! Close them before starting the sync.")
130 if len(self
.config
) > 0 and not self
.reset
:
131 die("Cannot start sync. Previous sync config found at %s" % self
.configFile
)
134 for line
in os
.popen("git-rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)).readlines():
135 commits
.append(line
[:-1])
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
):
146 for line
in template
.split("\n"):
147 if line
.startswith("#"):
148 result
+= line
+ "\n"
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"
162 result
+= line
+ "\n"
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()
170 filesToDelete
= set()
173 path
= line
[1:].strip()
175 system("p4 edit %s" % path
)
176 elif modifier
== "A":
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
)
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)
191 system("p4 add %s" % f
)
192 for f
in filesToDelete
:
193 system("p4 revert %s" % f
)
194 system("p4 delete %s" % f
)
198 for log
in os
.popen("git-cat-file commit %s" % id).readlines():
204 if len(logMessage
) > 0:
208 template
= os
.popen("p4 change -o").read()
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():
223 pipe
= os
.popen("less", "w")
224 pipe
.write(submitTemplate
+ diff
)
228 while response
== "e":
229 response
= raw_input("Do you want to submit this change (y/e/n)? ")
231 [handle
, fileName
] = tempfile
.mkstemp()
232 tmpFile
= os
.fdopen(handle
, "w+")
233 tmpFile
.write(submitTemplate
)
235 editor
= os
.environ
.get("EDITOR", "vi")
236 system(editor
+ " " + fileName
)
237 tmpFile
= open(fileName
, "r")
238 submitTemplate
= tmpFile
.read()
242 if response
== "y" or response
== "yes":
245 raw_input("Press return to continue...")
247 pipe
= os
.popen("p4 submit -i", "w")
248 pipe
.write(submitTemplate
)
251 print "Not submitting!"
252 self
.interactive
= False
254 fileName
= "submit.txt"
255 file = open(fileName
, "w+")
256 file.write(self
.prepareLogMessage(template
, logMessage
))
258 print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName
, fileName
)
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!")
275 self
.configFile
= gitdir
+ "/p4-git-sync.cfg"
276 self
.config
= shelve
.open(self
.configFile
, writeback
=True)
281 commits
= self
.config
.get("commits", [])
283 while len(commits
) > 0:
284 self
.firstTime
= False
286 commits
= commits
[1:]
287 self
.config
["commits"] = commits
289 if not self
.interactive
:
294 if len(commits
) == 0:
296 print "No changes found to apply between %s and current HEAD" % self
.origin
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]
311 print "valid commands: %s" % ", ".join(commands
)
313 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
318 "clean-tags" : P4CleanTags(),
322 if len(sys
.argv
[1:]) == 0:
323 printUsage(commands
.keys())
327 cmdName
= sys
.argv
[1]
329 cmd
= commands
[cmdName
]
331 print "unknown command %s" % cmdName
333 printUsage(commands
.keys())
336 options
= cmd
.options
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
);
349 if not isValidGitDir(gitdir
):
350 if isValidGitDir(gitdir
+ "/.git"):
353 die("fatal: cannot locate git repository at %s" % gitdir
)
355 os
.environ
["GIT_DIR"] = gitdir