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 <simon@lst.de>
6# Copyright: 2007 Simon Hausmann <simon@lst.de>
7# 2007 Trolltech ASA
8# License: MIT <http://www.opensource.org/licenses/mit-license.php>
9#
10
11import optparse, sys, os, marshal, popen2, subprocess, shelve
12import tempfile, getopt, sha, os.path, time, platform
13import re
14
15from sets import Set;
16
17verbose = False
18
19def die(msg):
20 if verbose:
21 raise Exception(msg)
22 else:
23 sys.stderr.write(msg + "\n")
24 sys.exit(1)
25
26def write_pipe(c, str):
27 if verbose:
28 sys.stderr.write('Writing pipe: %s\n' % c)
29
30 pipe = os.popen(c, 'w')
31 val = pipe.write(str)
32 if pipe.close():
33 die('Command failed: %s' % c)
34
35 return val
36
37def read_pipe(c, ignore_error=False):
38 if verbose:
39 sys.stderr.write('Reading pipe: %s\n' % c)
40
41 pipe = os.popen(c, 'rb')
42 val = pipe.read()
43 if pipe.close() and not ignore_error:
44 die('Command failed: %s' % c)
45
46 return val
47
48
49def read_pipe_lines(c):
50 if verbose:
51 sys.stderr.write('Reading pipe: %s\n' % c)
52 ## todo: check return status
53 pipe = os.popen(c, 'rb')
54 val = pipe.readlines()
55 if pipe.close():
56 die('Command failed: %s' % c)
57
58 return val
59
60def p4_read_pipe_lines(c):
61 """Specifically invoke p4 on the command supplied. """
62 real_cmd = "%s %s" % ("p4", c)
63 if verbose:
64 print real_cmd
65 return read_pipe_lines(real_cmd)
66
67def system(cmd):
68 if verbose:
69 sys.stderr.write("executing %s\n" % cmd)
70 if os.system(cmd) != 0:
71 die("command failed: %s" % cmd)
72
73def isP4Exec(kind):
74 """Determine if a Perforce 'kind' should have execute permission
75
76 'p4 help filetypes' gives a list of the types. If it starts with 'x',
77 or x follows one of a few letters. Otherwise, if there is an 'x' after
78 a plus sign, it is also executable"""
79 return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
80
81def setP4ExecBit(file, mode):
82 # Reopens an already open file and changes the execute bit to match
83 # the execute bit setting in the passed in mode.
84
85 p4Type = "+x"
86
87 if not isModeExec(mode):
88 p4Type = getP4OpenedType(file)
89 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
90 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
91 if p4Type[-1] == "+":
92 p4Type = p4Type[0:-1]
93
94 system("p4 reopen -t %s %s" % (p4Type, file))
95
96def getP4OpenedType(file):
97 # Returns the perforce file type for the given file.
98
99 result = read_pipe("p4 opened %s" % file)
100 match = re.match(".*\((.+)\)\r?$", result)
101 if match:
102 return match.group(1)
103 else:
104 die("Could not determine file type for %s (result: '%s')" % (file, result))
105
106def diffTreePattern():
107 # This is a simple generator for the diff tree regex pattern. This could be
108 # a class variable if this and parseDiffTreeEntry were a part of a class.
109 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
110 while True:
111 yield pattern
112
113def parseDiffTreeEntry(entry):
114 """Parses a single diff tree entry into its component elements.
115
116 See git-diff-tree(1) manpage for details about the format of the diff
117 output. This method returns a dictionary with the following elements:
118
119 src_mode - The mode of the source file
120 dst_mode - The mode of the destination file
121 src_sha1 - The sha1 for the source file
122 dst_sha1 - The sha1 fr the destination file
123 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
124 status_score - The score for the status (applicable for 'C' and 'R'
125 statuses). This is None if there is no score.
126 src - The path for the source file.
127 dst - The path for the destination file. This is only present for
128 copy or renames. If it is not present, this is None.
129
130 If the pattern is not matched, None is returned."""
131
132 match = diffTreePattern().next().match(entry)
133 if match:
134 return {
135 'src_mode': match.group(1),
136 'dst_mode': match.group(2),
137 'src_sha1': match.group(3),
138 'dst_sha1': match.group(4),
139 'status': match.group(5),
140 'status_score': match.group(6),
141 'src': match.group(7),
142 'dst': match.group(10)
143 }
144 return None
145
146def isModeExec(mode):
147 # Returns True if the given git mode represents an executable file,
148 # otherwise False.
149 return mode[-3:] == "755"
150
151def isModeExecChanged(src_mode, dst_mode):
152 return isModeExec(src_mode) != isModeExec(dst_mode)
153
154def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
155 cmd = "p4 -G %s" % cmd
156 if verbose:
157 sys.stderr.write("Opening pipe: %s\n" % cmd)
158
159 # Use a temporary file to avoid deadlocks without
160 # subprocess.communicate(), which would put another copy
161 # of stdout into memory.
162 stdin_file = None
163 if stdin is not None:
164 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
165 stdin_file.write(stdin)
166 stdin_file.flush()
167 stdin_file.seek(0)
168
169 p4 = subprocess.Popen(cmd, shell=True,
170 stdin=stdin_file,
171 stdout=subprocess.PIPE)
172
173 result = []
174 try:
175 while True:
176 entry = marshal.load(p4.stdout)
177 result.append(entry)
178 except EOFError:
179 pass
180 exitCode = p4.wait()
181 if exitCode != 0:
182 entry = {}
183 entry["p4ExitCode"] = exitCode
184 result.append(entry)
185
186 return result
187
188def p4Cmd(cmd):
189 list = p4CmdList(cmd)
190 result = {}
191 for entry in list:
192 result.update(entry)
193 return result;
194
195def p4Where(depotPath):
196 if not depotPath.endswith("/"):
197 depotPath += "/"
198 output = p4Cmd("where %s..." % depotPath)
199 if output["code"] == "error":
200 return ""
201 clientPath = ""
202 if "path" in output:
203 clientPath = output.get("path")
204 elif "data" in output:
205 data = output.get("data")
206 lastSpace = data.rfind(" ")
207 clientPath = data[lastSpace + 1:]
208
209 if clientPath.endswith("..."):
210 clientPath = clientPath[:-3]
211 return clientPath
212
213def currentGitBranch():
214 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
215
216def isValidGitDir(path):
217 if (os.path.exists(path + "/HEAD")
218 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
219 return True;
220 return False
221
222def parseRevision(ref):
223 return read_pipe("git rev-parse %s" % ref).strip()
224
225def extractLogMessageFromGitCommit(commit):
226 logMessage = ""
227
228 ## fixme: title is first line of commit, not 1st paragraph.
229 foundTitle = False
230 for log in read_pipe_lines("git cat-file commit %s" % commit):
231 if not foundTitle:
232 if len(log) == 1:
233 foundTitle = True
234 continue
235
236 logMessage += log
237 return logMessage
238
239def extractSettingsGitLog(log):
240 values = {}
241 for line in log.split("\n"):
242 line = line.strip()
243 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
244 if not m:
245 continue
246
247 assignments = m.group(1).split (':')
248 for a in assignments:
249 vals = a.split ('=')
250 key = vals[0].strip()
251 val = ('='.join (vals[1:])).strip()
252 if val.endswith ('\"') and val.startswith('"'):
253 val = val[1:-1]
254
255 values[key] = val
256
257 paths = values.get("depot-paths")
258 if not paths:
259 paths = values.get("depot-path")
260 if paths:
261 values['depot-paths'] = paths.split(',')
262 return values
263
264def gitBranchExists(branch):
265 proc = subprocess.Popen(["git", "rev-parse", branch],
266 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
267 return proc.wait() == 0;
268
269def gitConfig(key):
270 return read_pipe("git config %s" % key, ignore_error=True).strip()
271
272def p4BranchesInGit(branchesAreInRemotes = True):
273 branches = {}
274
275 cmdline = "git rev-parse --symbolic "
276 if branchesAreInRemotes:
277 cmdline += " --remotes"
278 else:
279 cmdline += " --branches"
280
281 for line in read_pipe_lines(cmdline):
282 line = line.strip()
283
284 ## only import to p4/
285 if not line.startswith('p4/') or line == "p4/HEAD":
286 continue
287 branch = line
288
289 # strip off p4
290 branch = re.sub ("^p4/", "", line)
291
292 branches[branch] = parseRevision(line)
293 return branches
294
295def findUpstreamBranchPoint(head = "HEAD"):
296 branches = p4BranchesInGit()
297 # map from depot-path to branch name
298 branchByDepotPath = {}
299 for branch in branches.keys():
300 tip = branches[branch]
301 log = extractLogMessageFromGitCommit(tip)
302 settings = extractSettingsGitLog(log)
303 if settings.has_key("depot-paths"):
304 paths = ",".join(settings["depot-paths"])
305 branchByDepotPath[paths] = "remotes/p4/" + branch
306
307 settings = None
308 parent = 0
309 while parent < 65535:
310 commit = head + "~%s" % parent
311 log = extractLogMessageFromGitCommit(commit)
312 settings = extractSettingsGitLog(log)
313 if settings.has_key("depot-paths"):
314 paths = ",".join(settings["depot-paths"])
315 if branchByDepotPath.has_key(paths):
316 return [branchByDepotPath[paths], settings]
317
318 parent = parent + 1
319
320 return ["", settings]
321
322def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
323 if not silent:
324 print ("Creating/updating branch(es) in %s based on origin branch(es)"
325 % localRefPrefix)
326
327 originPrefix = "origin/p4/"
328
329 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
330 line = line.strip()
331 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
332 continue
333
334 headName = line[len(originPrefix):]
335 remoteHead = localRefPrefix + headName
336 originHead = line
337
338 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
339 if (not original.has_key('depot-paths')
340 or not original.has_key('change')):
341 continue
342
343 update = False
344 if not gitBranchExists(remoteHead):
345 if verbose:
346 print "creating %s" % remoteHead
347 update = True
348 else:
349 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
350 if settings.has_key('change') > 0:
351 if settings['depot-paths'] == original['depot-paths']:
352 originP4Change = int(original['change'])
353 p4Change = int(settings['change'])
354 if originP4Change > p4Change:
355 print ("%s (%s) is newer than %s (%s). "
356 "Updating p4 branch from origin."
357 % (originHead, originP4Change,
358 remoteHead, p4Change))
359 update = True
360 else:
361 print ("Ignoring: %s was imported from %s while "
362 "%s was imported from %s"
363 % (originHead, ','.join(original['depot-paths']),
364 remoteHead, ','.join(settings['depot-paths'])))
365
366 if update:
367 system("git update-ref %s %s" % (remoteHead, originHead))
368
369def originP4BranchesExist():
370 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
371
372def p4ChangesForPaths(depotPaths, changeRange):
373 assert depotPaths
374 output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
375 for p in depotPaths]))
376
377 changes = []
378 for line in output:
379 changeNum = line.split(" ")[1]
380 changes.append(int(changeNum))
381
382 changes.sort()
383 return changes
384
385class Command:
386 def __init__(self):
387 self.usage = "usage: %prog [options]"
388 self.needsGit = True
389
390class P4Debug(Command):
391 def __init__(self):
392 Command.__init__(self)
393 self.options = [
394 optparse.make_option("--verbose", dest="verbose", action="store_true",
395 default=False),
396 ]
397 self.description = "A tool to debug the output of p4 -G."
398 self.needsGit = False
399 self.verbose = False
400
401 def run(self, args):
402 j = 0
403 for output in p4CmdList(" ".join(args)):
404 print 'Element: %d' % j
405 j += 1
406 print output
407 return True
408
409class P4RollBack(Command):
410 def __init__(self):
411 Command.__init__(self)
412 self.options = [
413 optparse.make_option("--verbose", dest="verbose", action="store_true"),
414 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
415 ]
416 self.description = "A tool to debug the multi-branch import. Don't use :)"
417 self.verbose = False
418 self.rollbackLocalBranches = False
419
420 def run(self, args):
421 if len(args) != 1:
422 return False
423 maxChange = int(args[0])
424
425 if "p4ExitCode" in p4Cmd("changes -m 1"):
426 die("Problems executing p4");
427
428 if self.rollbackLocalBranches:
429 refPrefix = "refs/heads/"
430 lines = read_pipe_lines("git rev-parse --symbolic --branches")
431 else:
432 refPrefix = "refs/remotes/"
433 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
434
435 for line in lines:
436 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
437 line = line.strip()
438 ref = refPrefix + line
439 log = extractLogMessageFromGitCommit(ref)
440 settings = extractSettingsGitLog(log)
441
442 depotPaths = settings['depot-paths']
443 change = settings['change']
444
445 changed = False
446
447 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
448 for p in depotPaths]))) == 0:
449 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
450 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
451 continue
452
453 while change and int(change) > maxChange:
454 changed = True
455 if self.verbose:
456 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
457 system("git update-ref %s \"%s^\"" % (ref, ref))
458 log = extractLogMessageFromGitCommit(ref)
459 settings = extractSettingsGitLog(log)
460
461
462 depotPaths = settings['depot-paths']
463 change = settings['change']
464
465 if changed:
466 print "%s rewound to %s" % (ref, change)
467
468 return True
469
470class P4Submit(Command):
471 def __init__(self):
472 Command.__init__(self)
473 self.options = [
474 optparse.make_option("--verbose", dest="verbose", action="store_true"),
475 optparse.make_option("--origin", dest="origin"),
476 optparse.make_option("-M", dest="detectRename", action="store_true"),
477 ]
478 self.description = "Submit changes from git to the perforce depot."
479 self.usage += " [name of git branch to submit into perforce depot]"
480 self.interactive = True
481 self.origin = ""
482 self.detectRename = False
483 self.verbose = False
484 self.isWindows = (platform.system() == "Windows")
485
486 def check(self):
487 if len(p4CmdList("opened ...")) > 0:
488 die("You have files opened with perforce! Close them before starting the sync.")
489
490 # replaces everything between 'Description:' and the next P4 submit template field with the
491 # commit message
492 def prepareLogMessage(self, template, message):
493 result = ""
494
495 inDescriptionSection = False
496
497 for line in template.split("\n"):
498 if line.startswith("#"):
499 result += line + "\n"
500 continue
501
502 if inDescriptionSection:
503 if line.startswith("Files:"):
504 inDescriptionSection = False
505 else:
506 continue
507 else:
508 if line.startswith("Description:"):
509 inDescriptionSection = True
510 line += "\n"
511 for messageLine in message.split("\n"):
512 line += "\t" + messageLine + "\n"
513
514 result += line + "\n"
515
516 return result
517
518 def prepareSubmitTemplate(self):
519 # remove lines in the Files section that show changes to files outside the depot path we're committing into
520 template = ""
521 inFilesSection = False
522 for line in p4_read_pipe_lines("change -o"):
523 if line.endswith("\r\n"):
524 line = line[:-2] + "\n"
525 if inFilesSection:
526 if line.startswith("\t"):
527 # path starts and ends with a tab
528 path = line[1:]
529 lastTab = path.rfind("\t")
530 if lastTab != -1:
531 path = path[:lastTab]
532 if not path.startswith(self.depotPath):
533 continue
534 else:
535 inFilesSection = False
536 else:
537 if line.startswith("Files:"):
538 inFilesSection = True
539
540 template += line
541
542 return template
543
544 def applyCommit(self, id):
545 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
546 diffOpts = ("", "-M")[self.detectRename]
547 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
548 filesToAdd = set()
549 filesToDelete = set()
550 editedFiles = set()
551 filesToChangeExecBit = {}
552 for line in diff:
553 diff = parseDiffTreeEntry(line)
554 modifier = diff['status']
555 path = diff['src']
556 if modifier == "M":
557 system("p4 edit \"%s\"" % path)
558 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
559 filesToChangeExecBit[path] = diff['dst_mode']
560 editedFiles.add(path)
561 elif modifier == "A":
562 filesToAdd.add(path)
563 filesToChangeExecBit[path] = diff['dst_mode']
564 if path in filesToDelete:
565 filesToDelete.remove(path)
566 elif modifier == "D":
567 filesToDelete.add(path)
568 if path in filesToAdd:
569 filesToAdd.remove(path)
570 elif modifier == "R":
571 src, dest = diff['src'], diff['dst']
572 system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
573 system("p4 edit \"%s\"" % (dest))
574 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
575 filesToChangeExecBit[dest] = diff['dst_mode']
576 os.unlink(dest)
577 editedFiles.add(dest)
578 filesToDelete.add(src)
579 else:
580 die("unknown modifier %s for %s" % (modifier, path))
581
582 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
583 patchcmd = diffcmd + " | git apply "
584 tryPatchCmd = patchcmd + "--check -"
585 applyPatchCmd = patchcmd + "--check --apply -"
586
587 if os.system(tryPatchCmd) != 0:
588 print "Unfortunately applying the change failed!"
589 print "What do you want to do?"
590 response = "x"
591 while response != "s" and response != "a" and response != "w":
592 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
593 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
594 if response == "s":
595 print "Skipping! Good luck with the next patches..."
596 for f in editedFiles:
597 system("p4 revert \"%s\"" % f);
598 for f in filesToAdd:
599 system("rm %s" %f)
600 return
601 elif response == "a":
602 os.system(applyPatchCmd)
603 if len(filesToAdd) > 0:
604 print "You may also want to call p4 add on the following files:"
605 print " ".join(filesToAdd)
606 if len(filesToDelete):
607 print "The following files should be scheduled for deletion with p4 delete:"
608 print " ".join(filesToDelete)
609 die("Please resolve and submit the conflict manually and "
610 + "continue afterwards with git-p4 submit --continue")
611 elif response == "w":
612 system(diffcmd + " > patch.txt")
613 print "Patch saved to patch.txt in %s !" % self.clientPath
614 die("Please resolve and submit the conflict manually and "
615 "continue afterwards with git-p4 submit --continue")
616
617 system(applyPatchCmd)
618
619 for f in filesToAdd:
620 system("p4 add \"%s\"" % f)
621 for f in filesToDelete:
622 system("p4 revert \"%s\"" % f)
623 system("p4 delete \"%s\"" % f)
624
625 # Set/clear executable bits
626 for f in filesToChangeExecBit.keys():
627 mode = filesToChangeExecBit[f]
628 setP4ExecBit(f, mode)
629
630 logMessage = extractLogMessageFromGitCommit(id)
631 logMessage = logMessage.strip()
632
633 template = self.prepareSubmitTemplate()
634
635 if self.interactive:
636 submitTemplate = self.prepareLogMessage(template, logMessage)
637 if os.environ.has_key("P4DIFF"):
638 del(os.environ["P4DIFF"])
639 diff = read_pipe("p4 diff -du ...")
640
641 newdiff = ""
642 for newFile in filesToAdd:
643 newdiff += "==== new file ====\n"
644 newdiff += "--- /dev/null\n"
645 newdiff += "+++ %s\n" % newFile
646 f = open(newFile, "r")
647 for line in f.readlines():
648 newdiff += "+" + line
649 f.close()
650
651 separatorLine = "######## everything below this line is just the diff #######\n"
652
653 [handle, fileName] = tempfile.mkstemp()
654 tmpFile = os.fdopen(handle, "w+")
655 if self.isWindows:
656 submitTemplate = submitTemplate.replace("\n", "\r\n")
657 separatorLine = separatorLine.replace("\n", "\r\n")
658 newdiff = newdiff.replace("\n", "\r\n")
659 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
660 tmpFile.close()
661 defaultEditor = "vi"
662 if platform.system() == "Windows":
663 defaultEditor = "notepad"
664 if os.environ.has_key("P4EDITOR"):
665 editor = os.environ.get("P4EDITOR")
666 else:
667 editor = os.environ.get("EDITOR", defaultEditor);
668 system(editor + " " + fileName)
669 tmpFile = open(fileName, "rb")
670 message = tmpFile.read()
671 tmpFile.close()
672 os.remove(fileName)
673 submitTemplate = message[:message.index(separatorLine)]
674 if self.isWindows:
675 submitTemplate = submitTemplate.replace("\r\n", "\n")
676
677 write_pipe("p4 submit -i", submitTemplate)
678 else:
679 fileName = "submit.txt"
680 file = open(fileName, "w+")
681 file.write(self.prepareLogMessage(template, logMessage))
682 file.close()
683 print ("Perforce submit template written as %s. "
684 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
685 % (fileName, fileName))
686
687 def run(self, args):
688 if len(args) == 0:
689 self.master = currentGitBranch()
690 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
691 die("Detecting current git branch failed!")
692 elif len(args) == 1:
693 self.master = args[0]
694 else:
695 return False
696
697 allowSubmit = gitConfig("git-p4.allowSubmit")
698 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
699 die("%s is not in git-p4.allowSubmit" % self.master)
700
701 [upstream, settings] = findUpstreamBranchPoint()
702 self.depotPath = settings['depot-paths'][0]
703 if len(self.origin) == 0:
704 self.origin = upstream
705
706 if self.verbose:
707 print "Origin branch is " + self.origin
708
709 if len(self.depotPath) == 0:
710 print "Internal error: cannot locate perforce depot path from existing branches"
711 sys.exit(128)
712
713 self.clientPath = p4Where(self.depotPath)
714
715 if len(self.clientPath) == 0:
716 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
717 sys.exit(128)
718
719 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
720 self.oldWorkingDirectory = os.getcwd()
721
722 os.chdir(self.clientPath)
723 print "Syncronizing p4 checkout..."
724 system("p4 sync ...")
725
726 self.check()
727
728 commits = []
729 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
730 commits.append(line.strip())
731 commits.reverse()
732
733 while len(commits) > 0:
734 commit = commits[0]
735 commits = commits[1:]
736 self.applyCommit(commit)
737 if not self.interactive:
738 break
739
740 if len(commits) == 0:
741 print "All changes applied!"
742 os.chdir(self.oldWorkingDirectory)
743
744 sync = P4Sync()
745 sync.run([])
746
747 rebase = P4Rebase()
748 rebase.rebase()
749
750 return True
751
752class P4Sync(Command):
753 def __init__(self):
754 Command.__init__(self)
755 self.options = [
756 optparse.make_option("--branch", dest="branch"),
757 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
758 optparse.make_option("--changesfile", dest="changesFile"),
759 optparse.make_option("--silent", dest="silent", action="store_true"),
760 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
761 optparse.make_option("--verbose", dest="verbose", action="store_true"),
762 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
763 help="Import into refs/heads/ , not refs/remotes"),
764 optparse.make_option("--max-changes", dest="maxChanges"),
765 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
766 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
767 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
768 help="Only sync files that are included in the Perforce Client Spec")
769 ]
770 self.description = """Imports from Perforce into a git repository.\n
771 example:
772 //depot/my/project/ -- to import the current head
773 //depot/my/project/@all -- to import everything
774 //depot/my/project/@1,6 -- to import only from revision 1 to 6
775
776 (a ... is not needed in the path p4 specification, it's added implicitly)"""
777
778 self.usage += " //depot/path[@revRange]"
779 self.silent = False
780 self.createdBranches = Set()
781 self.committedChanges = Set()
782 self.branch = ""
783 self.detectBranches = False
784 self.detectLabels = False
785 self.changesFile = ""
786 self.syncWithOrigin = True
787 self.verbose = False
788 self.importIntoRemotes = True
789 self.maxChanges = ""
790 self.isWindows = (platform.system() == "Windows")
791 self.keepRepoPath = False
792 self.depotPaths = None
793 self.p4BranchesInGit = []
794 self.cloneExclude = []
795 self.useClientSpec = False
796 self.clientSpecDirs = []
797
798 if gitConfig("git-p4.syncFromOrigin") == "false":
799 self.syncWithOrigin = False
800
801 def extractFilesFromCommit(self, commit):
802 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
803 for path in self.cloneExclude]
804 files = []
805 fnum = 0
806 while commit.has_key("depotFile%s" % fnum):
807 path = commit["depotFile%s" % fnum]
808
809 if [p for p in self.cloneExclude
810 if path.startswith (p)]:
811 found = False
812 else:
813 found = [p for p in self.depotPaths
814 if path.startswith (p)]
815 if not found:
816 fnum = fnum + 1
817 continue
818
819 file = {}
820 file["path"] = path
821 file["rev"] = commit["rev%s" % fnum]
822 file["action"] = commit["action%s" % fnum]
823 file["type"] = commit["type%s" % fnum]
824 files.append(file)
825 fnum = fnum + 1
826 return files
827
828 def stripRepoPath(self, path, prefixes):
829 if self.keepRepoPath:
830 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
831
832 for p in prefixes:
833 if path.startswith(p):
834 path = path[len(p):]
835
836 return path
837
838 def splitFilesIntoBranches(self, commit):
839 branches = {}
840 fnum = 0
841 while commit.has_key("depotFile%s" % fnum):
842 path = commit["depotFile%s" % fnum]
843 found = [p for p in self.depotPaths
844 if path.startswith (p)]
845 if not found:
846 fnum = fnum + 1
847 continue
848
849 file = {}
850 file["path"] = path
851 file["rev"] = commit["rev%s" % fnum]
852 file["action"] = commit["action%s" % fnum]
853 file["type"] = commit["type%s" % fnum]
854 fnum = fnum + 1
855
856 relPath = self.stripRepoPath(path, self.depotPaths)
857
858 for branch in self.knownBranches.keys():
859
860 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
861 if relPath.startswith(branch + "/"):
862 if branch not in branches:
863 branches[branch] = []
864 branches[branch].append(file)
865 break
866
867 return branches
868
869 ## Should move this out, doesn't use SELF.
870 def readP4Files(self, files):
871 filesForCommit = []
872 filesToRead = []
873
874 for f in files:
875 includeFile = True
876 for val in self.clientSpecDirs:
877 if f['path'].startswith(val[0]):
878 if val[1] <= 0:
879 includeFile = False
880 break
881
882 if includeFile:
883 filesForCommit.append(f)
884 if f['action'] != 'delete':
885 filesToRead.append(f)
886
887 filedata = []
888 if len(filesToRead) > 0:
889 filedata = p4CmdList('-x - print',
890 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
891 for f in filesToRead]),
892 stdin_mode='w+')
893
894 if "p4ExitCode" in filedata[0]:
895 die("Problems executing p4. Error: [%d]."
896 % (filedata[0]['p4ExitCode']));
897
898 j = 0;
899 contents = {}
900 while j < len(filedata):
901 stat = filedata[j]
902 j += 1
903 text = [];
904 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
905 text.append(filedata[j]['data'])
906 j += 1
907 text = ''.join(text)
908
909 if not stat.has_key('depotFile'):
910 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
911 continue
912
913 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
914 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
915 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
916 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
917
918 contents[stat['depotFile']] = text
919
920 for f in filesForCommit:
921 path = f['path']
922 if contents.has_key(path):
923 f['data'] = contents[path]
924
925 return filesForCommit
926
927 def commit(self, details, files, branch, branchPrefixes, parent = ""):
928 epoch = details["time"]
929 author = details["user"]
930
931 if self.verbose:
932 print "commit into %s" % branch
933
934 # start with reading files; if that fails, we should not
935 # create a commit.
936 new_files = []
937 for f in files:
938 if [p for p in branchPrefixes if f['path'].startswith(p)]:
939 new_files.append (f)
940 else:
941 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
942 files = self.readP4Files(new_files)
943
944 self.gitStream.write("commit %s\n" % branch)
945# gitStream.write("mark :%s\n" % details["change"])
946 self.committedChanges.add(int(details["change"]))
947 committer = ""
948 if author not in self.users:
949 self.getUserMapFromPerforceServer()
950 if author in self.users:
951 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
952 else:
953 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
954
955 self.gitStream.write("committer %s\n" % committer)
956
957 self.gitStream.write("data <<EOT\n")
958 self.gitStream.write(details["desc"])
959 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
960 % (','.join (branchPrefixes), details["change"]))
961 if len(details['options']) > 0:
962 self.gitStream.write(": options = %s" % details['options'])
963 self.gitStream.write("]\nEOT\n\n")
964
965 if len(parent) > 0:
966 if self.verbose:
967 print "parent %s" % parent
968 self.gitStream.write("from %s\n" % parent)
969
970 for file in files:
971 if file["type"] == "apple":
972 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
973 continue
974
975 relPath = self.stripRepoPath(file['path'], branchPrefixes)
976 if file["action"] == "delete":
977 self.gitStream.write("D %s\n" % relPath)
978 else:
979 data = file['data']
980
981 mode = "644"
982 if isP4Exec(file["type"]):
983 mode = "755"
984 elif file["type"] == "symlink":
985 mode = "120000"
986 # p4 print on a symlink contains "target\n", so strip it off
987 data = data[:-1]
988
989 if self.isWindows and file["type"].endswith("text"):
990 data = data.replace("\r\n", "\n")
991
992 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
993 self.gitStream.write("data %s\n" % len(data))
994 self.gitStream.write(data)
995 self.gitStream.write("\n")
996
997 self.gitStream.write("\n")
998
999 change = int(details["change"])
1000
1001 if self.labels.has_key(change):
1002 label = self.labels[change]
1003 labelDetails = label[0]
1004 labelRevisions = label[1]
1005 if self.verbose:
1006 print "Change %s is labelled %s" % (change, labelDetails)
1007
1008 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1009 for p in branchPrefixes]))
1010
1011 if len(files) == len(labelRevisions):
1012
1013 cleanedFiles = {}
1014 for info in files:
1015 if info["action"] == "delete":
1016 continue
1017 cleanedFiles[info["depotFile"]] = info["rev"]
1018
1019 if cleanedFiles == labelRevisions:
1020 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1021 self.gitStream.write("from %s\n" % branch)
1022
1023 owner = labelDetails["Owner"]
1024 tagger = ""
1025 if author in self.users:
1026 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1027 else:
1028 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1029 self.gitStream.write("tagger %s\n" % tagger)
1030 self.gitStream.write("data <<EOT\n")
1031 self.gitStream.write(labelDetails["Description"])
1032 self.gitStream.write("EOT\n\n")
1033
1034 else:
1035 if not self.silent:
1036 print ("Tag %s does not match with change %s: files do not match."
1037 % (labelDetails["label"], change))
1038
1039 else:
1040 if not self.silent:
1041 print ("Tag %s does not match with change %s: file count is different."
1042 % (labelDetails["label"], change))
1043
1044 def getUserCacheFilename(self):
1045 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1046 return home + "/.gitp4-usercache.txt"
1047
1048 def getUserMapFromPerforceServer(self):
1049 if self.userMapFromPerforceServer:
1050 return
1051 self.users = {}
1052
1053 for output in p4CmdList("users"):
1054 if not output.has_key("User"):
1055 continue
1056 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1057
1058
1059 s = ''
1060 for (key, val) in self.users.items():
1061 s += "%s\t%s\n" % (key, val)
1062
1063 open(self.getUserCacheFilename(), "wb").write(s)
1064 self.userMapFromPerforceServer = True
1065
1066 def loadUserMapFromCache(self):
1067 self.users = {}
1068 self.userMapFromPerforceServer = False
1069 try:
1070 cache = open(self.getUserCacheFilename(), "rb")
1071 lines = cache.readlines()
1072 cache.close()
1073 for line in lines:
1074 entry = line.strip().split("\t")
1075 self.users[entry[0]] = entry[1]
1076 except IOError:
1077 self.getUserMapFromPerforceServer()
1078
1079 def getLabels(self):
1080 self.labels = {}
1081
1082 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1083 if len(l) > 0 and not self.silent:
1084 print "Finding files belonging to labels in %s" % `self.depotPaths`
1085
1086 for output in l:
1087 label = output["label"]
1088 revisions = {}
1089 newestChange = 0
1090 if self.verbose:
1091 print "Querying files for label %s" % label
1092 for file in p4CmdList("files "
1093 + ' '.join (["%s...@%s" % (p, label)
1094 for p in self.depotPaths])):
1095 revisions[file["depotFile"]] = file["rev"]
1096 change = int(file["change"])
1097 if change > newestChange:
1098 newestChange = change
1099
1100 self.labels[newestChange] = [output, revisions]
1101
1102 if self.verbose:
1103 print "Label changes: %s" % self.labels.keys()
1104
1105 def guessProjectName(self):
1106 for p in self.depotPaths:
1107 if p.endswith("/"):
1108 p = p[:-1]
1109 p = p[p.strip().rfind("/") + 1:]
1110 if not p.endswith("/"):
1111 p += "/"
1112 return p
1113
1114 def getBranchMapping(self):
1115 lostAndFoundBranches = set()
1116
1117 for info in p4CmdList("branches"):
1118 details = p4Cmd("branch -o %s" % info["branch"])
1119 viewIdx = 0
1120 while details.has_key("View%s" % viewIdx):
1121 paths = details["View%s" % viewIdx].split(" ")
1122 viewIdx = viewIdx + 1
1123 # require standard //depot/foo/... //depot/bar/... mapping
1124 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1125 continue
1126 source = paths[0]
1127 destination = paths[1]
1128 ## HACK
1129 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1130 source = source[len(self.depotPaths[0]):-4]
1131 destination = destination[len(self.depotPaths[0]):-4]
1132
1133 if destination in self.knownBranches:
1134 if not self.silent:
1135 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1136 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1137 continue
1138
1139 self.knownBranches[destination] = source
1140
1141 lostAndFoundBranches.discard(destination)
1142
1143 if source not in self.knownBranches:
1144 lostAndFoundBranches.add(source)
1145
1146
1147 for branch in lostAndFoundBranches:
1148 self.knownBranches[branch] = branch
1149
1150 def getBranchMappingFromGitBranches(self):
1151 branches = p4BranchesInGit(self.importIntoRemotes)
1152 for branch in branches.keys():
1153 if branch == "master":
1154 branch = "main"
1155 else:
1156 branch = branch[len(self.projectName):]
1157 self.knownBranches[branch] = branch
1158
1159 def listExistingP4GitBranches(self):
1160 # branches holds mapping from name to commit
1161 branches = p4BranchesInGit(self.importIntoRemotes)
1162 self.p4BranchesInGit = branches.keys()
1163 for branch in branches.keys():
1164 self.initialParents[self.refPrefix + branch] = branches[branch]
1165
1166 def updateOptionDict(self, d):
1167 option_keys = {}
1168 if self.keepRepoPath:
1169 option_keys['keepRepoPath'] = 1
1170
1171 d["options"] = ' '.join(sorted(option_keys.keys()))
1172
1173 def readOptions(self, d):
1174 self.keepRepoPath = (d.has_key('options')
1175 and ('keepRepoPath' in d['options']))
1176
1177 def gitRefForBranch(self, branch):
1178 if branch == "main":
1179 return self.refPrefix + "master"
1180
1181 if len(branch) <= 0:
1182 return branch
1183
1184 return self.refPrefix + self.projectName + branch
1185
1186 def gitCommitByP4Change(self, ref, change):
1187 if self.verbose:
1188 print "looking in ref " + ref + " for change %s using bisect..." % change
1189
1190 earliestCommit = ""
1191 latestCommit = parseRevision(ref)
1192
1193 while True:
1194 if self.verbose:
1195 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1196 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1197 if len(next) == 0:
1198 if self.verbose:
1199 print "argh"
1200 return ""
1201 log = extractLogMessageFromGitCommit(next)
1202 settings = extractSettingsGitLog(log)
1203 currentChange = int(settings['change'])
1204 if self.verbose:
1205 print "current change %s" % currentChange
1206
1207 if currentChange == change:
1208 if self.verbose:
1209 print "found %s" % next
1210 return next
1211
1212 if currentChange < change:
1213 earliestCommit = "^%s" % next
1214 else:
1215 latestCommit = "%s" % next
1216
1217 return ""
1218
1219 def importNewBranch(self, branch, maxChange):
1220 # make fast-import flush all changes to disk and update the refs using the checkpoint
1221 # command so that we can try to find the branch parent in the git history
1222 self.gitStream.write("checkpoint\n\n");
1223 self.gitStream.flush();
1224 branchPrefix = self.depotPaths[0] + branch + "/"
1225 range = "@1,%s" % maxChange
1226 #print "prefix" + branchPrefix
1227 changes = p4ChangesForPaths([branchPrefix], range)
1228 if len(changes) <= 0:
1229 return False
1230 firstChange = changes[0]
1231 #print "first change in branch: %s" % firstChange
1232 sourceBranch = self.knownBranches[branch]
1233 sourceDepotPath = self.depotPaths[0] + sourceBranch
1234 sourceRef = self.gitRefForBranch(sourceBranch)
1235 #print "source " + sourceBranch
1236
1237 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1238 #print "branch parent: %s" % branchParentChange
1239 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1240 if len(gitParent) > 0:
1241 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1242 #print "parent git commit: %s" % gitParent
1243
1244 self.importChanges(changes)
1245 return True
1246
1247 def importChanges(self, changes):
1248 cnt = 1
1249 for change in changes:
1250 description = p4Cmd("describe %s" % change)
1251 self.updateOptionDict(description)
1252
1253 if not self.silent:
1254 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1255 sys.stdout.flush()
1256 cnt = cnt + 1
1257
1258 try:
1259 if self.detectBranches:
1260 branches = self.splitFilesIntoBranches(description)
1261 for branch in branches.keys():
1262 ## HACK --hwn
1263 branchPrefix = self.depotPaths[0] + branch + "/"
1264
1265 parent = ""
1266
1267 filesForCommit = branches[branch]
1268
1269 if self.verbose:
1270 print "branch is %s" % branch
1271
1272 self.updatedBranches.add(branch)
1273
1274 if branch not in self.createdBranches:
1275 self.createdBranches.add(branch)
1276 parent = self.knownBranches[branch]
1277 if parent == branch:
1278 parent = ""
1279 else:
1280 fullBranch = self.projectName + branch
1281 if fullBranch not in self.p4BranchesInGit:
1282 if not self.silent:
1283 print("\n Importing new branch %s" % fullBranch);
1284 if self.importNewBranch(branch, change - 1):
1285 parent = ""
1286 self.p4BranchesInGit.append(fullBranch)
1287 if not self.silent:
1288 print("\n Resuming with change %s" % change);
1289
1290 if self.verbose:
1291 print "parent determined through known branches: %s" % parent
1292
1293 branch = self.gitRefForBranch(branch)
1294 parent = self.gitRefForBranch(parent)
1295
1296 if self.verbose:
1297 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1298
1299 if len(parent) == 0 and branch in self.initialParents:
1300 parent = self.initialParents[branch]
1301 del self.initialParents[branch]
1302
1303 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1304 else:
1305 files = self.extractFilesFromCommit(description)
1306 self.commit(description, files, self.branch, self.depotPaths,
1307 self.initialParent)
1308 self.initialParent = ""
1309 except IOError:
1310 print self.gitError.read()
1311 sys.exit(1)
1312
1313 def importHeadRevision(self, revision):
1314 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1315
1316 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1317 details["desc"] = ("Initial import of %s from the state at revision %s"
1318 % (' '.join(self.depotPaths), revision))
1319 details["change"] = revision
1320 newestRevision = 0
1321
1322 fileCnt = 0
1323 for info in p4CmdList("files "
1324 + ' '.join(["%s...%s"
1325 % (p, revision)
1326 for p in self.depotPaths])):
1327
1328 if info['code'] == 'error':
1329 sys.stderr.write("p4 returned an error: %s\n"
1330 % info['data'])
1331 sys.exit(1)
1332
1333
1334 change = int(info["change"])
1335 if change > newestRevision:
1336 newestRevision = change
1337
1338 if info["action"] == "delete":
1339 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1340 #fileCnt = fileCnt + 1
1341 continue
1342
1343 for prop in ["depotFile", "rev", "action", "type" ]:
1344 details["%s%s" % (prop, fileCnt)] = info[prop]
1345
1346 fileCnt = fileCnt + 1
1347
1348 details["change"] = newestRevision
1349 self.updateOptionDict(details)
1350 try:
1351 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1352 except IOError:
1353 print "IO error with git fast-import. Is your git version recent enough?"
1354 print self.gitError.read()
1355
1356
1357 def getClientSpec(self):
1358 specList = p4CmdList( "client -o" )
1359 temp = {}
1360 for entry in specList:
1361 for k,v in entry.iteritems():
1362 if k.startswith("View"):
1363 if v.startswith('"'):
1364 start = 1
1365 else:
1366 start = 0
1367 index = v.find("...")
1368 v = v[start:index]
1369 if v.startswith("-"):
1370 v = v[1:]
1371 temp[v] = -len(v)
1372 else:
1373 temp[v] = len(v)
1374 self.clientSpecDirs = temp.items()
1375 self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1376
1377 def run(self, args):
1378 self.depotPaths = []
1379 self.changeRange = ""
1380 self.initialParent = ""
1381 self.previousDepotPaths = []
1382
1383 # map from branch depot path to parent branch
1384 self.knownBranches = {}
1385 self.initialParents = {}
1386 self.hasOrigin = originP4BranchesExist()
1387 if not self.syncWithOrigin:
1388 self.hasOrigin = False
1389
1390 if self.importIntoRemotes:
1391 self.refPrefix = "refs/remotes/p4/"
1392 else:
1393 self.refPrefix = "refs/heads/p4/"
1394
1395 if self.syncWithOrigin and self.hasOrigin:
1396 if not self.silent:
1397 print "Syncing with origin first by calling git fetch origin"
1398 system("git fetch origin")
1399
1400 if len(self.branch) == 0:
1401 self.branch = self.refPrefix + "master"
1402 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1403 system("git update-ref %s refs/heads/p4" % self.branch)
1404 system("git branch -D p4");
1405 # create it /after/ importing, when master exists
1406 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1407 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1408
1409 if self.useClientSpec or gitConfig("p4.useclientspec") == "true":
1410 self.getClientSpec()
1411
1412 # TODO: should always look at previous commits,
1413 # merge with previous imports, if possible.
1414 if args == []:
1415 if self.hasOrigin:
1416 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1417 self.listExistingP4GitBranches()
1418
1419 if len(self.p4BranchesInGit) > 1:
1420 if not self.silent:
1421 print "Importing from/into multiple branches"
1422 self.detectBranches = True
1423
1424 if self.verbose:
1425 print "branches: %s" % self.p4BranchesInGit
1426
1427 p4Change = 0
1428 for branch in self.p4BranchesInGit:
1429 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1430
1431 settings = extractSettingsGitLog(logMsg)
1432
1433 self.readOptions(settings)
1434 if (settings.has_key('depot-paths')
1435 and settings.has_key ('change')):
1436 change = int(settings['change']) + 1
1437 p4Change = max(p4Change, change)
1438
1439 depotPaths = sorted(settings['depot-paths'])
1440 if self.previousDepotPaths == []:
1441 self.previousDepotPaths = depotPaths
1442 else:
1443 paths = []
1444 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1445 for i in range(0, min(len(cur), len(prev))):
1446 if cur[i] <> prev[i]:
1447 i = i - 1
1448 break
1449
1450 paths.append (cur[:i + 1])
1451
1452 self.previousDepotPaths = paths
1453
1454 if p4Change > 0:
1455 self.depotPaths = sorted(self.previousDepotPaths)
1456 self.changeRange = "@%s,#head" % p4Change
1457 if not self.detectBranches:
1458 self.initialParent = parseRevision(self.branch)
1459 if not self.silent and not self.detectBranches:
1460 print "Performing incremental import into %s git branch" % self.branch
1461
1462 if not self.branch.startswith("refs/"):
1463 self.branch = "refs/heads/" + self.branch
1464
1465 if len(args) == 0 and self.depotPaths:
1466 if not self.silent:
1467 print "Depot paths: %s" % ' '.join(self.depotPaths)
1468 else:
1469 if self.depotPaths and self.depotPaths != args:
1470 print ("previous import used depot path %s and now %s was specified. "
1471 "This doesn't work!" % (' '.join (self.depotPaths),
1472 ' '.join (args)))
1473 sys.exit(1)
1474
1475 self.depotPaths = sorted(args)
1476
1477 revision = ""
1478 self.users = {}
1479
1480 newPaths = []
1481 for p in self.depotPaths:
1482 if p.find("@") != -1:
1483 atIdx = p.index("@")
1484 self.changeRange = p[atIdx:]
1485 if self.changeRange == "@all":
1486 self.changeRange = ""
1487 elif ',' not in self.changeRange:
1488 revision = self.changeRange
1489 self.changeRange = ""
1490 p = p[:atIdx]
1491 elif p.find("#") != -1:
1492 hashIdx = p.index("#")
1493 revision = p[hashIdx:]
1494 p = p[:hashIdx]
1495 elif self.previousDepotPaths == []:
1496 revision = "#head"
1497
1498 p = re.sub ("\.\.\.$", "", p)
1499 if not p.endswith("/"):
1500 p += "/"
1501
1502 newPaths.append(p)
1503
1504 self.depotPaths = newPaths
1505
1506
1507 self.loadUserMapFromCache()
1508 self.labels = {}
1509 if self.detectLabels:
1510 self.getLabels();
1511
1512 if self.detectBranches:
1513 ## FIXME - what's a P4 projectName ?
1514 self.projectName = self.guessProjectName()
1515
1516 if self.hasOrigin:
1517 self.getBranchMappingFromGitBranches()
1518 else:
1519 self.getBranchMapping()
1520 if self.verbose:
1521 print "p4-git branches: %s" % self.p4BranchesInGit
1522 print "initial parents: %s" % self.initialParents
1523 for b in self.p4BranchesInGit:
1524 if b != "master":
1525
1526 ## FIXME
1527 b = b[len(self.projectName):]
1528 self.createdBranches.add(b)
1529
1530 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1531
1532 importProcess = subprocess.Popen(["git", "fast-import"],
1533 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1534 stderr=subprocess.PIPE);
1535 self.gitOutput = importProcess.stdout
1536 self.gitStream = importProcess.stdin
1537 self.gitError = importProcess.stderr
1538
1539 if revision:
1540 self.importHeadRevision(revision)
1541 else:
1542 changes = []
1543
1544 if len(self.changesFile) > 0:
1545 output = open(self.changesFile).readlines()
1546 changeSet = Set()
1547 for line in output:
1548 changeSet.add(int(line))
1549
1550 for change in changeSet:
1551 changes.append(change)
1552
1553 changes.sort()
1554 else:
1555 if self.verbose:
1556 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1557 self.changeRange)
1558 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1559
1560 if len(self.maxChanges) > 0:
1561 changes = changes[:min(int(self.maxChanges), len(changes))]
1562
1563 if len(changes) == 0:
1564 if not self.silent:
1565 print "No changes to import!"
1566 return True
1567
1568 if not self.silent and not self.detectBranches:
1569 print "Import destination: %s" % self.branch
1570
1571 self.updatedBranches = set()
1572
1573 self.importChanges(changes)
1574
1575 if not self.silent:
1576 print ""
1577 if len(self.updatedBranches) > 0:
1578 sys.stdout.write("Updated branches: ")
1579 for b in self.updatedBranches:
1580 sys.stdout.write("%s " % b)
1581 sys.stdout.write("\n")
1582
1583 self.gitStream.close()
1584 if importProcess.wait() != 0:
1585 die("fast-import failed: %s" % self.gitError.read())
1586 self.gitOutput.close()
1587 self.gitError.close()
1588
1589 return True
1590
1591class P4Rebase(Command):
1592 def __init__(self):
1593 Command.__init__(self)
1594 self.options = [ ]
1595 self.description = ("Fetches the latest revision from perforce and "
1596 + "rebases the current work (branch) against it")
1597 self.verbose = False
1598
1599 def run(self, args):
1600 sync = P4Sync()
1601 sync.run([])
1602
1603 return self.rebase()
1604
1605 def rebase(self):
1606 if os.system("git update-index --refresh") != 0:
1607 die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up-to-date or stash away all your changes with git stash.");
1608 if len(read_pipe("git diff-index HEAD --")) > 0:
1609 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1610
1611 [upstream, settings] = findUpstreamBranchPoint()
1612 if len(upstream) == 0:
1613 die("Cannot find upstream branchpoint for rebase")
1614
1615 # the branchpoint may be p4/foo~3, so strip off the parent
1616 upstream = re.sub("~[0-9]+$", "", upstream)
1617
1618 print "Rebasing the current branch onto %s" % upstream
1619 oldHead = read_pipe("git rev-parse HEAD").strip()
1620 system("git rebase %s" % upstream)
1621 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1622 return True
1623
1624class P4Clone(P4Sync):
1625 def __init__(self):
1626 P4Sync.__init__(self)
1627 self.description = "Creates a new git repository and imports from Perforce into it"
1628 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1629 self.options += [
1630 optparse.make_option("--destination", dest="cloneDestination",
1631 action='store', default=None,
1632 help="where to leave result of the clone"),
1633 optparse.make_option("-/", dest="cloneExclude",
1634 action="append", type="string",
1635 help="exclude depot path")
1636 ]
1637 self.cloneDestination = None
1638 self.needsGit = False
1639
1640 # This is required for the "append" cloneExclude action
1641 def ensure_value(self, attr, value):
1642 if not hasattr(self, attr) or getattr(self, attr) is None:
1643 setattr(self, attr, value)
1644 return getattr(self, attr)
1645
1646 def defaultDestination(self, args):
1647 ## TODO: use common prefix of args?
1648 depotPath = args[0]
1649 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1650 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1651 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1652 depotDir = re.sub(r"/$", "", depotDir)
1653 return os.path.split(depotDir)[1]
1654
1655 def run(self, args):
1656 if len(args) < 1:
1657 return False
1658
1659 if self.keepRepoPath and not self.cloneDestination:
1660 sys.stderr.write("Must specify destination for --keep-path\n")
1661 sys.exit(1)
1662
1663 depotPaths = args
1664
1665 if not self.cloneDestination and len(depotPaths) > 1:
1666 self.cloneDestination = depotPaths[-1]
1667 depotPaths = depotPaths[:-1]
1668
1669 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1670 for p in depotPaths:
1671 if not p.startswith("//"):
1672 return False
1673
1674 if not self.cloneDestination:
1675 self.cloneDestination = self.defaultDestination(args)
1676
1677 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1678 if not os.path.exists(self.cloneDestination):
1679 os.makedirs(self.cloneDestination)
1680 os.chdir(self.cloneDestination)
1681 system("git init")
1682 self.gitdir = os.getcwd() + "/.git"
1683 if not P4Sync.run(self, depotPaths):
1684 return False
1685 if self.branch != "master":
1686 if gitBranchExists("refs/remotes/p4/master"):
1687 system("git branch master refs/remotes/p4/master")
1688 system("git checkout -f")
1689 else:
1690 print "Could not detect main branch. No checkout/master branch created."
1691
1692 return True
1693
1694class P4Branches(Command):
1695 def __init__(self):
1696 Command.__init__(self)
1697 self.options = [ ]
1698 self.description = ("Shows the git branches that hold imports and their "
1699 + "corresponding perforce depot paths")
1700 self.verbose = False
1701
1702 def run(self, args):
1703 if originP4BranchesExist():
1704 createOrUpdateBranchesFromOrigin()
1705
1706 cmdline = "git rev-parse --symbolic "
1707 cmdline += " --remotes"
1708
1709 for line in read_pipe_lines(cmdline):
1710 line = line.strip()
1711
1712 if not line.startswith('p4/') or line == "p4/HEAD":
1713 continue
1714 branch = line
1715
1716 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1717 settings = extractSettingsGitLog(log)
1718
1719 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1720 return True
1721
1722class HelpFormatter(optparse.IndentedHelpFormatter):
1723 def __init__(self):
1724 optparse.IndentedHelpFormatter.__init__(self)
1725
1726 def format_description(self, description):
1727 if description:
1728 return description + "\n"
1729 else:
1730 return ""
1731
1732def printUsage(commands):
1733 print "usage: %s <command> [options]" % sys.argv[0]
1734 print ""
1735 print "valid commands: %s" % ", ".join(commands)
1736 print ""
1737 print "Try %s <command> --help for command specific help." % sys.argv[0]
1738 print ""
1739
1740commands = {
1741 "debug" : P4Debug,
1742 "submit" : P4Submit,
1743 "commit" : P4Submit,
1744 "sync" : P4Sync,
1745 "rebase" : P4Rebase,
1746 "clone" : P4Clone,
1747 "rollback" : P4RollBack,
1748 "branches" : P4Branches
1749}
1750
1751
1752def main():
1753 if len(sys.argv[1:]) == 0:
1754 printUsage(commands.keys())
1755 sys.exit(2)
1756
1757 cmd = ""
1758 cmdName = sys.argv[1]
1759 try:
1760 klass = commands[cmdName]
1761 cmd = klass()
1762 except KeyError:
1763 print "unknown command %s" % cmdName
1764 print ""
1765 printUsage(commands.keys())
1766 sys.exit(2)
1767
1768 options = cmd.options
1769 cmd.gitdir = os.environ.get("GIT_DIR", None)
1770
1771 args = sys.argv[2:]
1772
1773 if len(options) > 0:
1774 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1775
1776 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1777 options,
1778 description = cmd.description,
1779 formatter = HelpFormatter())
1780
1781 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1782 global verbose
1783 verbose = cmd.verbose
1784 if cmd.needsGit:
1785 if cmd.gitdir == None:
1786 cmd.gitdir = os.path.abspath(".git")
1787 if not isValidGitDir(cmd.gitdir):
1788 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1789 if os.path.exists(cmd.gitdir):
1790 cdup = read_pipe("git rev-parse --show-cdup").strip()
1791 if len(cdup) > 0:
1792 os.chdir(cdup);
1793
1794 if not isValidGitDir(cmd.gitdir):
1795 if isValidGitDir(cmd.gitdir + "/.git"):
1796 cmd.gitdir += "/.git"
1797 else:
1798 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1799
1800 os.environ["GIT_DIR"] = cmd.gitdir
1801
1802 if not cmd.run(args):
1803 parser.print_help()
1804
1805
1806if __name__ == '__main__':
1807 main()