3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
5 # Author: Simon Hausmann <hausmann@kde.org>
6 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
9 import optparse
, sys
, os
, marshal
, popen2
, shelve
12 gitdir
= os
.environ
.get("GIT_DIR", "")
15 cmd
= "p4 -G %s" % cmd
16 pipe
= os
.popen(cmd
, "rb")
21 entry
= marshal
.load(pipe
)
37 sys
.stderr
.write(msg
+ "\n")
40 def currentGitBranch():
41 return os
.popen("git-name-rev HEAD").read().split(" ")[1][:-1]
43 def isValidGitDir(path
):
44 if os
.path
.exists(path
+ "/HEAD") and os
.path
.exists(path
+ "/refs") and os
.path
.exists(path
+ "/objects"):
49 if os
.system(cmd
) != 0:
50 die("command failed: %s" % cmd
)
56 self
.description
= "A tool to debug the output of p4 -G."
59 for output
in p4CmdList(" ".join(args
)):
65 # optparse.make_option("--branch", dest="branch", default="refs/heads/master")
67 self
.description
= "A tool to remove stale unused tags from incremental perforce imports."
69 branch
= currentGitBranch()
70 print "Cleaning out stale p4 import tags..."
71 sout
, sin
, serr
= popen2
.popen3("git-name-rev --tags `git-rev-parse %s`" % branch
)
74 tagIdx
= output
.index(" tags/p4/")
76 print "Cannot find any p4/* tag. Nothing to do."
80 caretIdx
= output
.index("^")
82 caretIdx
= len(output
) - 1
83 rev
= int(output
[tagIdx
+ 9 : caretIdx
])
85 allTags
= os
.popen("git tag -l p4/").readlines()
86 for i
in range(len(allTags
)):
87 allTags
[i
] = int(allTags
[i
][3:-1])
94 print os
.popen("git tag -d p4/%s" % rev
).read()
96 print "%s tags removed." % len(allTags
)
101 optparse
.make_option("--continue", action
="store_false", dest
="firstTime"),
102 optparse
.make_option("--origin", dest
="origin"),
103 optparse
.make_option("--reset", action
="store_true", dest
="reset"),
104 optparse
.make_option("--master", dest
="master"),
105 optparse
.make_option("--log-substitutions", dest
="substFile"),
106 optparse
.make_option("--noninteractive", action
="store_false"),
107 optparse
.make_option("--dry-run", action
="store_true")
109 self
.description
= "Submit changes from git to the perforce depot."
110 self
.firstTime
= True
112 self
.interactive
= True
115 self
.firstTime
= True
116 self
.origin
= "origin"
119 self
.logSubstitutions
= {}
120 self
.logSubstitutions
["<enter description here>"] = "%log%"
121 self
.logSubstitutions
["\tDetails:"] = "\tDetails: %log%"
124 if len(p4CmdList("opened ...")) > 0:
125 die("You have files opened with perforce! Close them before starting the sync.")
128 if len(self
.config
) > 0 and not self
.reset
:
129 die("Cannot start sync. Previous sync config found at %s" % self
.configFile
)
132 for line
in os
.popen("git-rev-list --no-merges %s..%s" % (self
.origin
, self
.master
)).readlines():
133 commits
.append(line
[:-1])
136 self
.config
["commits"] = commits
138 print "Creating temporary p4-sync branch from %s ..." % self
.origin
139 system("git checkout -f -b p4-sync %s" % self
.origin
)
141 def prepareLogMessage(self
, template
, message
):
144 for line
in template
.split("\n"):
145 if line
.startswith("#"):
146 result
+= line
+ "\n"
150 for key
in self
.logSubstitutions
.keys():
151 if line
.find(key
) != -1:
152 value
= self
.logSubstitutions
[key
]
153 value
= value
.replace("%log%", message
)
154 if value
!= "@remove@":
155 result
+= line
.replace(key
, value
) + "\n"
160 result
+= line
+ "\n"
165 print "Applying %s" % (os
.popen("git-log --max-count=1 --pretty=oneline %s" % id).read())
166 diff
= os
.popen("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)).readlines()
168 filesToDelete
= set()
171 path
= line
[1:].strip()
173 system("p4 edit %s" % path
)
174 elif modifier
== "A":
176 if path
in filesToDelete
:
177 filesToDelete
.remove(path
)
178 elif modifier
== "D":
179 filesToDelete
.add(path
)
180 if path
in filesToAdd
:
181 filesToAdd
.remove(path
)
183 die("unknown modifier %s for %s" % (modifier
, path
))
185 system("git-diff-files --name-only -z | git-update-index --remove -z --stdin")
186 system("git cherry-pick --no-commit \"%s\"" % id)
189 system("p4 add %s" % f
)
190 for f
in filesToDelete
:
191 system("p4 revert %s" % f
)
192 system("p4 delete %s" % f
)
196 for log
in os
.popen("git-cat-file commit %s" % id).readlines():
202 if len(logMessage
) > 0:
206 template
= os
.popen("p4 change -o").read()
209 submitTemplate
= self
.prepareLogMessage(template
, logMessage
)
210 diff
= os
.popen("p4 diff -du ...").read()
212 for newFile
in filesToAdd
:
213 diff
+= "==== new file ====\n"
214 diff
+= "--- /dev/null\n"
215 diff
+= "+++ %s\n" % newFile
216 f
= open(newFile
, "r")
217 for line
in f
.readlines():
221 pipe
= os
.popen("less", "w")
222 pipe
.write(submitTemplate
+ diff
)
226 while response
== "e":
227 response
= raw_input("Do you want to submit this change (y/e/n)? ")
229 [handle
, fileName
] = tempfile
.mkstemp()
230 tmpFile
= os
.fdopen(handle
, "w+")
231 tmpFile
.write(submitTemplate
)
233 editor
= os
.environ
.get("EDITOR", "vi")
234 system(editor
+ " " + fileName
)
235 tmpFile
= open(fileName
, "r")
236 submitTemplate
= tmpFile
.read()
240 if response
== "y" or response
== "yes":
243 raw_input("Press return to continue...")
245 pipe
= os
.popen("p4 submit -i", "w")
246 pipe
.write(submitTemplate
)
249 print "Not submitting!"
250 self
.interactive
= False
252 fileName
= "submit.txt"
253 file = open(fileName
, "w+")
254 file.write(self
.prepareLogMessage(template
, logMessage
))
256 print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName
, fileName
)
260 self
.firstTime
= True
262 if len(self
.substFile
) > 0:
263 for line
in open(self
.substFile
, "r").readlines():
264 tokens
= line
[:-1].split("=")
265 self
.logSubstitutions
[tokens
[0]] = tokens
[1]
267 if len(self
.master
) == 0:
268 self
.master
= currentGitBranch()
269 if len(self
.master
) == 0 or not os
.path
.exists("%s/refs/heads/%s" % (gitdir
, self
.master
)):
270 die("Detecting current git branch failed!")
273 self
.configFile
= gitdir
+ "/p4-git-sync.cfg"
274 self
.config
= shelve
.open(self
.configFile
, writeback
=True)
279 commits
= self
.config
.get("commits", [])
281 while len(commits
) > 0:
282 self
.firstTime
= False
284 commits
= commits
[1:]
285 self
.config
["commits"] = commits
287 if not self
.interactive
:
292 if len(commits
) == 0:
294 print "No changes found to apply between %s and current HEAD" % self
.origin
296 print "All changes applied!"
297 print "Deleting temporary p4-sync branch and going back to %s" % self
.master
298 system("git checkout %s" % self
.master
)
299 system("git branch -D p4-sync")
300 print "Cleaning out your perforce checkout by doing p4 edit ... ; p4 revert ..."
301 system("p4 edit ... >/dev/null")
302 system("p4 revert ... >/dev/null")
303 os
.remove(self
.configFile
)
306 def printUsage(commands
):
307 print "usage: %s <command> [options]" % sys
.argv
[0]
309 print "valid commands: %s" % ", ".join(commands
)
311 print "Try %s <command> --help for command specific help." % sys
.argv
[0]
316 "clean-tags" : P4CleanTags(),
317 "sync-to-perforce" : P4Sync()
320 if len(sys
.argv
[1:]) == 0:
321 printUsage(commands
.keys())
325 cmdName
= sys
.argv
[1]
327 cmd
= commands
[cmdName
]
329 print "unknown command %s" % cmdName
331 printUsage(commands
.keys())
334 options
= cmd
.options
336 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
338 parser
= optparse
.OptionParser("usage: %prog " + cmdName
+ " [options]", options
,
339 description
= cmd
.description
)
341 (cmd
, args
) = parser
.parse_args(sys
.argv
[2:], cmd
);
347 if not isValidGitDir(gitdir
):
348 if isValidGitDir(gitdir
+ "/.git"):
351 dir("fatal: cannot locate git repository at %s" % gitdir
)
353 os
.environ
["GIT_DIR"] = gitdir