3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
11 import optparse, sys, os, marshal, popen2, subprocess, shelve
12 import tempfile, getopt, sha, os.path, time, platform
23 sys.stderr.write(msg + "\n")
26 def write_pipe(c, str):
28 sys.stderr.write('Writing pipe: %s\n' % c)
30 pipe = os.popen(c, 'w')
33 die('Command failed: %s' % c)
37 def read_pipe(c, ignore_error=False):
39 sys.stderr.write('Reading pipe: %s\n' % c)
41 pipe = os.popen(c, 'rb')
43 if pipe.close() and not ignore_error:
44 die('Command failed: %s' % c)
49 def read_pipe_lines(c):
51 sys.stderr.write('Reading pipe: %s\n' % c)
52 ## todo: check return status
53 pipe = os.popen(c, 'rb')
54 val = pipe.readlines()
56 die('Command failed: %s' % c)
62 sys.stderr.write("executing %s\n" % cmd)
63 if os.system(cmd) != 0:
64 die("command failed: %s" % cmd)
67 """Determine if a Perforce 'kind' should have execute permission
69 'p4 help filetypes' gives a list of the types. If it starts with 'x',
70 or x follows one of a few letters. Otherwise, if there is an 'x' after
71 a plus sign, it is also executable"""
72 return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
74 def diffTreePattern():
75 # This is a simple generator for the diff tree regex pattern. This could be
76 # a class variable if this and parseDiffTreeEntry were a part of a class.
77 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
81 def parseDiffTreeEntry(entry):
82 """Parses a single diff tree entry into its component elements.
84 See git-diff-tree(1) manpage for details about the format of the diff
85 output. This method returns a dictionary with the following elements:
87 src_mode - The mode of the source file
88 dst_mode - The mode of the destination file
89 src_sha1 - The sha1 for the source file
90 dst_sha1 - The sha1 fr the destination file
91 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
92 status_score - The score for the status (applicable for 'C' and 'R'
93 statuses). This is None if there is no score.
94 src - The path for the source file.
95 dst - The path for the destination file. This is only present for
96 copy or renames. If it is not present, this is None.
98 If the pattern is not matched, None is returned."""
100 match = diffTreePattern().next().match(entry)
103 'src_mode': match.group(1),
104 'dst_mode': match.group(2),
105 'src_sha1': match.group(3),
106 'dst_sha1': match.group(4),
107 'status': match.group(5),
108 'status_score': match.group(6),
109 'src': match.group(7),
110 'dst': match.group(10)
114 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
115 cmd = "p4 -G %s" % cmd
117 sys.stderr.write("Opening pipe: %s\n" % cmd)
119 # Use a temporary file to avoid deadlocks without
120 # subprocess.communicate(), which would put another copy
121 # of stdout into memory.
123 if stdin is not None:
124 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
125 stdin_file.write(stdin)
129 p4 = subprocess.Popen(cmd, shell=True,
131 stdout=subprocess.PIPE)
136 entry = marshal.load(p4.stdout)
143 entry["p4ExitCode"] = exitCode
149 list = p4CmdList(cmd)
155 def p4Where(depotPath):
156 if not depotPath.endswith("/"):
158 output = p4Cmd("where %s..." % depotPath)
159 if output["code"] == "error":
163 clientPath = output.get("path")
164 elif "data" in output:
165 data = output.get("data")
166 lastSpace = data.rfind(" ")
167 clientPath = data[lastSpace + 1:]
169 if clientPath.endswith("..."):
170 clientPath = clientPath[:-3]
173 def currentGitBranch():
174 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
176 def isValidGitDir(path):
177 if (os.path.exists(path + "/HEAD")
178 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
182 def parseRevision(ref):
183 return read_pipe("git rev-parse %s" % ref).strip()
185 def extractLogMessageFromGitCommit(commit):
188 ## fixme: title is first line of commit, not 1st paragraph.
190 for log in read_pipe_lines("git cat-file commit %s" % commit):
199 def extractSettingsGitLog(log):
201 for line in log.split("\n"):
203 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
207 assignments = m.group(1).split (':')
208 for a in assignments:
210 key = vals[0].strip()
211 val = ('='.join (vals[1:])).strip()
212 if val.endswith ('\"') and val.startswith('"'):
217 paths = values.get("depot-paths")
219 paths = values.get("depot-path")
221 values['depot-paths'] = paths.split(',')
224 def gitBranchExists(branch):
225 proc = subprocess.Popen(["git", "rev-parse", branch],
226 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
227 return proc.wait() == 0;
230 return read_pipe("git config %s" % key, ignore_error=True).strip()
232 def p4BranchesInGit(branchesAreInRemotes = True):
235 cmdline = "git rev-parse --symbolic "
236 if branchesAreInRemotes:
237 cmdline += " --remotes"
239 cmdline += " --branches"
241 for line in read_pipe_lines(cmdline):
244 ## only import to p4/
245 if not line.startswith('p4/') or line == "p4/HEAD":
250 branch = re.sub ("^p4/", "", line)
252 branches[branch] = parseRevision(line)
255 def findUpstreamBranchPoint(head = "HEAD"):
256 branches = p4BranchesInGit()
257 # map from depot-path to branch name
258 branchByDepotPath = {}
259 for branch in branches.keys():
260 tip = branches[branch]
261 log = extractLogMessageFromGitCommit(tip)
262 settings = extractSettingsGitLog(log)
263 if settings.has_key("depot-paths"):
264 paths = ",".join(settings["depot-paths"])
265 branchByDepotPath[paths] = "remotes/p4/" + branch
269 while parent < 65535:
270 commit = head + "~%s" % parent
271 log = extractLogMessageFromGitCommit(commit)
272 settings = extractSettingsGitLog(log)
273 if settings.has_key("depot-paths"):
274 paths = ",".join(settings["depot-paths"])
275 if branchByDepotPath.has_key(paths):
276 return [branchByDepotPath[paths], settings]
280 return ["", settings]
282 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
284 print ("Creating/updating branch(es) in %s based on origin branch(es)"
287 originPrefix = "origin/p4/"
289 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
291 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
294 headName = line[len(originPrefix):]
295 remoteHead = localRefPrefix + headName
298 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
299 if (not original.has_key('depot-paths')
300 or not original.has_key('change')):
304 if not gitBranchExists(remoteHead):
306 print "creating %s" % remoteHead
309 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
310 if settings.has_key('change') > 0:
311 if settings['depot-paths'] == original['depot-paths']:
312 originP4Change = int(original['change'])
313 p4Change = int(settings['change'])
314 if originP4Change > p4Change:
315 print ("%s (%s) is newer than %s (%s). "
316 "Updating p4 branch from origin."
317 % (originHead, originP4Change,
318 remoteHead, p4Change))
321 print ("Ignoring: %s was imported from %s while "
322 "%s was imported from %s"
323 % (originHead, ','.join(original['depot-paths']),
324 remoteHead, ','.join(settings['depot-paths'])))
327 system("git update-ref %s %s" % (remoteHead, originHead))
329 def originP4BranchesExist():
330 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
332 def p4ChangesForPaths(depotPaths, changeRange):
334 output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange)
335 for p in depotPaths]))
339 changeNum = line.split(" ")[1]
340 changes.append(int(changeNum))
347 self.usage = "usage: %prog [options]"
350 class P4Debug(Command):
352 Command.__init__(self)
354 optparse.make_option("--verbose", dest="verbose", action="store_true",
357 self.description = "A tool to debug the output of p4 -G."
358 self.needsGit = False
363 for output in p4CmdList(" ".join(args)):
364 print 'Element: %d' % j
369 class P4RollBack(Command):
371 Command.__init__(self)
373 optparse.make_option("--verbose", dest="verbose", action="store_true"),
374 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
376 self.description = "A tool to debug the multi-branch import. Don't use :)"
378 self.rollbackLocalBranches = False
383 maxChange = int(args[0])
385 if "p4ExitCode" in p4Cmd("changes -m 1"):
386 die("Problems executing p4");
388 if self.rollbackLocalBranches:
389 refPrefix = "refs/heads/"
390 lines = read_pipe_lines("git rev-parse --symbolic --branches")
392 refPrefix = "refs/remotes/"
393 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
396 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
398 ref = refPrefix + line
399 log = extractLogMessageFromGitCommit(ref)
400 settings = extractSettingsGitLog(log)
402 depotPaths = settings['depot-paths']
403 change = settings['change']
407 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
408 for p in depotPaths]))) == 0:
409 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
410 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
413 while change and int(change) > maxChange:
416 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
417 system("git update-ref %s \"%s^\"" % (ref, ref))
418 log = extractLogMessageFromGitCommit(ref)
419 settings = extractSettingsGitLog(log)
422 depotPaths = settings['depot-paths']
423 change = settings['change']
426 print "%s rewound to %s" % (ref, change)
430 class P4Submit(Command):
432 Command.__init__(self)
434 optparse.make_option("--continue", action="store_false", dest="firstTime"),
435 optparse.make_option("--verbose", dest="verbose", action="store_true"),
436 optparse.make_option("--origin", dest="origin"),
437 optparse.make_option("--reset", action="store_true", dest="reset"),
438 optparse.make_option("--log-substitutions", dest="substFile"),
439 optparse.make_option("--dry-run", action="store_true"),
440 optparse.make_option("--direct", dest="directSubmit", action="store_true"),
441 optparse.make_option("--trust-me-like-a-fool", dest="trustMeLikeAFool", action="store_true"),
442 optparse.make_option("-M", dest="detectRename", action="store_true"),
444 self.description = "Submit changes from git to the perforce depot."
445 self.usage += " [name of git branch to submit into perforce depot]"
446 self.firstTime = True
448 self.interactive = True
451 self.firstTime = True
453 self.directSubmit = False
454 self.trustMeLikeAFool = False
455 self.detectRename = False
457 self.isWindows = (platform.system() == "Windows")
459 self.logSubstitutions = {}
460 self.logSubstitutions["<enter description here>"] = "%log%"
461 self.logSubstitutions["\tDetails:"] = "\tDetails: %log%"
464 if len(p4CmdList("opened ...")) > 0:
465 die("You have files opened with perforce! Close them before starting the sync.")
468 if len(self.config) > 0 and not self.reset:
469 die("Cannot start sync. Previous sync config found at %s\n"
470 "If you want to start submitting again from scratch "
471 "maybe you want to call git-p4 submit --reset" % self.configFile)
474 if self.directSubmit:
477 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
478 commits.append(line.strip())
481 self.config["commits"] = commits
483 def prepareLogMessage(self, template, message):
486 for line in template.split("\n"):
487 if line.startswith("#"):
488 result += line + "\n"
492 for key in self.logSubstitutions.keys():
493 if line.find(key) != -1:
494 value = self.logSubstitutions[key]
495 value = value.replace("%log%", message)
496 if value != "@remove@":
497 result += line.replace(key, value) + "\n"
502 result += line + "\n"
506 def prepareSubmitTemplate(self):
507 # remove lines in the Files section that show changes to files outside the depot path we're committing into
509 inFilesSection = False
510 for line in read_pipe_lines("p4 change -o"):
512 if line.startswith("\t"):
513 # path starts and ends with a tab
515 lastTab = path.rfind("\t")
517 path = path[:lastTab]
518 if not path.startswith(self.depotPath):
521 inFilesSection = False
523 if line.startswith("Files:"):
524 inFilesSection = True
530 def applyCommit(self, id):
531 if self.directSubmit:
532 print "Applying local change in working directory/index"
533 diff = self.diffStatus
535 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
536 diffOpts = ("", "-M")[self.detectRename]
537 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
539 filesToDelete = set()
542 diff = parseDiffTreeEntry(line)
543 modifier = diff['status']
546 system("p4 edit \"%s\"" % path)
547 editedFiles.add(path)
548 elif modifier == "A":
550 if path in filesToDelete:
551 filesToDelete.remove(path)
552 elif modifier == "D":
553 filesToDelete.add(path)
554 if path in filesToAdd:
555 filesToAdd.remove(path)
556 elif modifier == "R":
557 src, dest = diff['src'], diff['dst']
558 system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
559 system("p4 edit \"%s\"" % (dest))
561 editedFiles.add(dest)
562 filesToDelete.add(src)
564 die("unknown modifier %s for %s" % (modifier, path))
566 if self.directSubmit:
567 diffcmd = "cat \"%s\"" % self.diffFile
569 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
570 patchcmd = diffcmd + " | git apply "
571 tryPatchCmd = patchcmd + "--check -"
572 applyPatchCmd = patchcmd + "--check --apply -"
574 if os.system(tryPatchCmd) != 0:
575 print "Unfortunately applying the change failed!"
576 print "What do you want to do?"
578 while response != "s" and response != "a" and response != "w":
579 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
580 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
582 print "Skipping! Good luck with the next patches..."
583 for f in editedFiles:
584 system("p4 revert \"%s\"" % f);
588 elif response == "a":
589 os.system(applyPatchCmd)
590 if len(filesToAdd) > 0:
591 print "You may also want to call p4 add on the following files:"
592 print " ".join(filesToAdd)
593 if len(filesToDelete):
594 print "The following files should be scheduled for deletion with p4 delete:"
595 print " ".join(filesToDelete)
596 die("Please resolve and submit the conflict manually and "
597 + "continue afterwards with git-p4 submit --continue")
598 elif response == "w":
599 system(diffcmd + " > patch.txt")
600 print "Patch saved to patch.txt in %s !" % self.clientPath
601 die("Please resolve and submit the conflict manually and "
602 "continue afterwards with git-p4 submit --continue")
604 system(applyPatchCmd)
607 system("p4 add \"%s\"" % f)
608 for f in filesToDelete:
609 system("p4 revert \"%s\"" % f)
610 system("p4 delete \"%s\"" % f)
613 if not self.directSubmit:
614 logMessage = extractLogMessageFromGitCommit(id)
615 logMessage = logMessage.replace("\n", "\n\t")
617 logMessage = logMessage.replace("\n", "\r\n")
618 logMessage = logMessage.strip()
620 template = self.prepareSubmitTemplate()
623 submitTemplate = self.prepareLogMessage(template, logMessage)
624 diff = read_pipe("p4 diff -du ...")
626 for newFile in filesToAdd:
627 diff += "==== new file ====\n"
628 diff += "--- /dev/null\n"
629 diff += "+++ %s\n" % newFile
630 f = open(newFile, "r")
631 for line in f.readlines():
635 separatorLine = "######## everything below this line is just the diff #######"
636 if platform.system() == "Windows":
637 separatorLine += "\r"
638 separatorLine += "\n"
641 if self.trustMeLikeAFool:
644 firstIteration = True
645 while response == "e":
646 if not firstIteration:
647 response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ")
648 firstIteration = False
650 [handle, fileName] = tempfile.mkstemp()
651 tmpFile = os.fdopen(handle, "w+")
652 tmpFile.write(submitTemplate + separatorLine + diff)
655 if platform.system() == "Windows":
656 defaultEditor = "notepad"
657 editor = os.environ.get("EDITOR", defaultEditor);
658 system(editor + " " + fileName)
659 tmpFile = open(fileName, "rb")
660 message = tmpFile.read()
663 submitTemplate = message[:message.index(separatorLine)]
665 submitTemplate = submitTemplate.replace("\r\n", "\n")
667 if response == "y" or response == "yes":
670 raw_input("Press return to continue...")
672 if self.directSubmit:
673 print "Submitting to git first"
674 os.chdir(self.oldWorkingDirectory)
675 write_pipe("git commit -a -F -", submitTemplate)
676 os.chdir(self.clientPath)
678 write_pipe("p4 submit -i", submitTemplate)
679 elif response == "s":
680 for f in editedFiles:
681 system("p4 revert \"%s\"" % f);
683 system("p4 revert \"%s\"" % f);
685 for f in filesToDelete:
686 system("p4 delete \"%s\"" % f);
689 print "Not submitting!"
690 self.interactive = False
692 fileName = "submit.txt"
693 file = open(fileName, "w+")
694 file.write(self.prepareLogMessage(template, logMessage))
696 print ("Perforce submit template written as %s. "
697 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
698 % (fileName, fileName))
702 self.master = currentGitBranch()
703 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
704 die("Detecting current git branch failed!")
706 self.master = args[0]
710 [upstream, settings] = findUpstreamBranchPoint()
711 self.depotPath = settings['depot-paths'][0]
712 if len(self.origin) == 0:
713 self.origin = upstream
716 print "Origin branch is " + self.origin
718 if len(self.depotPath) == 0:
719 print "Internal error: cannot locate perforce depot path from existing branches"
722 self.clientPath = p4Where(self.depotPath)
724 if len(self.clientPath) == 0:
725 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
728 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
729 self.oldWorkingDirectory = os.getcwd()
731 if self.directSubmit:
732 self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD")
733 if len(self.diffStatus) == 0:
734 print "No changes in working directory to submit."
736 patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD")
737 self.diffFile = self.gitdir + "/p4-git-diff"
738 f = open(self.diffFile, "wb")
742 os.chdir(self.clientPath)
743 print "Syncronizing p4 checkout..."
744 system("p4 sync ...")
747 self.firstTime = True
749 if len(self.substFile) > 0:
750 for line in open(self.substFile, "r").readlines():
751 tokens = line.strip().split("=")
752 self.logSubstitutions[tokens[0]] = tokens[1]
755 self.configFile = self.gitdir + "/p4-git-sync.cfg"
756 self.config = shelve.open(self.configFile, writeback=True)
761 commits = self.config.get("commits", [])
763 while len(commits) > 0:
764 self.firstTime = False
766 commits = commits[1:]
767 self.config["commits"] = commits
768 self.applyCommit(commit)
769 if not self.interactive:
774 if self.directSubmit:
775 os.remove(self.diffFile)
777 if len(commits) == 0:
779 print "No changes found to apply between %s and current HEAD" % self.origin
781 print "All changes applied!"
782 os.chdir(self.oldWorkingDirectory)
787 response = raw_input("Do you want to rebase current HEAD from Perforce now using git-p4 rebase? [y]es/[n]o ")
788 if response == "y" or response == "yes":
791 os.remove(self.configFile)
795 class P4Sync(Command):
797 Command.__init__(self)
799 optparse.make_option("--branch", dest="branch"),
800 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
801 optparse.make_option("--changesfile", dest="changesFile"),
802 optparse.make_option("--silent", dest="silent", action="store_true"),
803 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
804 optparse.make_option("--verbose", dest="verbose", action="store_true"),
805 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
806 help="Import into refs/heads/ , not refs/remotes"),
807 optparse.make_option("--max-changes", dest="maxChanges"),
808 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
809 help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
811 self.description = """Imports from Perforce into a git repository.\n
813 //depot/my/project/ -- to import the current head
814 //depot/my/project/@all -- to import everything
815 //depot/my/project/@1,6 -- to import only from revision 1 to 6
817 (a ... is not needed in the path p4 specification, it's added implicitly)"""
819 self.usage += " //depot/path[@revRange]"
821 self.createdBranches = Set()
822 self.committedChanges = Set()
824 self.detectBranches = False
825 self.detectLabels = False
826 self.changesFile = ""
827 self.syncWithOrigin = True
829 self.importIntoRemotes = True
831 self.isWindows = (platform.system() == "Windows")
832 self.keepRepoPath = False
833 self.depotPaths = None
834 self.p4BranchesInGit = []
836 if gitConfig("git-p4.syncFromOrigin") == "false":
837 self.syncWithOrigin = False
839 def extractFilesFromCommit(self, commit):
842 while commit.has_key("depotFile%s" % fnum):
843 path = commit["depotFile%s" % fnum]
845 found = [p for p in self.depotPaths
846 if path.startswith (p)]
853 file["rev"] = commit["rev%s" % fnum]
854 file["action"] = commit["action%s" % fnum]
855 file["type"] = commit["type%s" % fnum]
860 def stripRepoPath(self, path, prefixes):
861 if self.keepRepoPath:
862 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
865 if path.startswith(p):
870 def splitFilesIntoBranches(self, commit):
873 while commit.has_key("depotFile%s" % fnum):
874 path = commit["depotFile%s" % fnum]
875 found = [p for p in self.depotPaths
876 if path.startswith (p)]
883 file["rev"] = commit["rev%s" % fnum]
884 file["action"] = commit["action%s" % fnum]
885 file["type"] = commit["type%s" % fnum]
888 relPath = self.stripRepoPath(path, self.depotPaths)
890 for branch in self.knownBranches.keys():
892 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
893 if relPath.startswith(branch + "/"):
894 if branch not in branches:
895 branches[branch] = []
896 branches[branch].append(file)
901 ## Should move this out, doesn't use SELF.
902 def readP4Files(self, files):
903 files = [f for f in files
904 if f['action'] != 'delete']
909 filedata = p4CmdList('-x - print',
910 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
913 if "p4ExitCode" in filedata[0]:
914 die("Problems executing p4. Error: [%d]."
915 % (filedata[0]['p4ExitCode']));
919 while j < len(filedata):
923 while j < len(filedata) and filedata[j]['code'] in ('text',
925 text += filedata[j]['data']
929 if not stat.has_key('depotFile'):
930 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
933 contents[stat['depotFile']] = text
936 assert not f.has_key('data')
937 f['data'] = contents[f['path']]
939 def commit(self, details, files, branch, branchPrefixes, parent = ""):
940 epoch = details["time"]
941 author = details["user"]
944 print "commit into %s" % branch
946 # start with reading files; if that fails, we should not
950 if [p for p in branchPrefixes if f['path'].startswith(p)]:
953 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
955 self.readP4Files(files)
960 self.gitStream.write("commit %s\n" % branch)
961 # gitStream.write("mark :%s\n" % details["change"])
962 self.committedChanges.add(int(details["change"]))
964 if author not in self.users:
965 self.getUserMapFromPerforceServer()
966 if author in self.users:
967 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
969 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
971 self.gitStream.write("committer %s\n" % committer)
973 self.gitStream.write("data <<EOT\n")
974 self.gitStream.write(details["desc"])
975 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
976 % (','.join (branchPrefixes), details["change"]))
977 if len(details['options']) > 0:
978 self.gitStream.write(": options = %s" % details['options'])
979 self.gitStream.write("]\nEOT\n\n")
983 print "parent %s" % parent
984 self.gitStream.write("from %s\n" % parent)
987 if file["type"] == "apple":
988 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
991 relPath = self.stripRepoPath(file['path'], branchPrefixes)
992 if file["action"] == "delete":
993 self.gitStream.write("D %s\n" % relPath)
998 if isP4Exec(file["type"]):
1000 elif file["type"] == "symlink":
1002 # p4 print on a symlink contains "target\n", so strip it off
1005 if self.isWindows and file["type"].endswith("text"):
1006 data = data.replace("\r\n", "\n")
1008 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1009 self.gitStream.write("data %s\n" % len(data))
1010 self.gitStream.write(data)
1011 self.gitStream.write("\n")
1013 self.gitStream.write("\n")
1015 change = int(details["change"])
1017 if self.labels.has_key(change):
1018 label = self.labels[change]
1019 labelDetails = label[0]
1020 labelRevisions = label[1]
1022 print "Change %s is labelled %s" % (change, labelDetails)
1024 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1025 for p in branchPrefixes]))
1027 if len(files) == len(labelRevisions):
1031 if info["action"] == "delete":
1033 cleanedFiles[info["depotFile"]] = info["rev"]
1035 if cleanedFiles == labelRevisions:
1036 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1037 self.gitStream.write("from %s\n" % branch)
1039 owner = labelDetails["Owner"]
1041 if author in self.users:
1042 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1044 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1045 self.gitStream.write("tagger %s\n" % tagger)
1046 self.gitStream.write("data <<EOT\n")
1047 self.gitStream.write(labelDetails["Description"])
1048 self.gitStream.write("EOT\n\n")
1052 print ("Tag %s does not match with change %s: files do not match."
1053 % (labelDetails["label"], change))
1057 print ("Tag %s does not match with change %s: file count is different."
1058 % (labelDetails["label"], change))
1060 def getUserCacheFilename(self):
1061 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1062 return home + "/.gitp4-usercache.txt"
1064 def getUserMapFromPerforceServer(self):
1065 if self.userMapFromPerforceServer:
1069 for output in p4CmdList("users"):
1070 if not output.has_key("User"):
1072 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1076 for (key, val) in self.users.items():
1077 s += "%s\t%s\n" % (key, val)
1079 open(self.getUserCacheFilename(), "wb").write(s)
1080 self.userMapFromPerforceServer = True
1082 def loadUserMapFromCache(self):
1084 self.userMapFromPerforceServer = False
1086 cache = open(self.getUserCacheFilename(), "rb")
1087 lines = cache.readlines()
1090 entry = line.strip().split("\t")
1091 self.users[entry[0]] = entry[1]
1093 self.getUserMapFromPerforceServer()
1095 def getLabels(self):
1098 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1099 if len(l) > 0 and not self.silent:
1100 print "Finding files belonging to labels in %s" % `self.depotPath`
1103 label = output["label"]
1107 print "Querying files for label %s" % label
1108 for file in p4CmdList("files "
1109 + ' '.join (["%s...@%s" % (p, label)
1110 for p in self.depotPaths])):
1111 revisions[file["depotFile"]] = file["rev"]
1112 change = int(file["change"])
1113 if change > newestChange:
1114 newestChange = change
1116 self.labels[newestChange] = [output, revisions]
1119 print "Label changes: %s" % self.labels.keys()
1121 def guessProjectName(self):
1122 for p in self.depotPaths:
1125 p = p[p.strip().rfind("/") + 1:]
1126 if not p.endswith("/"):
1130 def getBranchMapping(self):
1131 lostAndFoundBranches = set()
1133 for info in p4CmdList("branches"):
1134 details = p4Cmd("branch -o %s" % info["branch"])
1136 while details.has_key("View%s" % viewIdx):
1137 paths = details["View%s" % viewIdx].split(" ")
1138 viewIdx = viewIdx + 1
1139 # require standard //depot/foo/... //depot/bar/... mapping
1140 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1143 destination = paths[1]
1145 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1146 source = source[len(self.depotPaths[0]):-4]
1147 destination = destination[len(self.depotPaths[0]):-4]
1149 if destination in self.knownBranches:
1151 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1152 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1155 self.knownBranches[destination] = source
1157 lostAndFoundBranches.discard(destination)
1159 if source not in self.knownBranches:
1160 lostAndFoundBranches.add(source)
1163 for branch in lostAndFoundBranches:
1164 self.knownBranches[branch] = branch
1166 def listExistingP4GitBranches(self):
1167 # branches holds mapping from name to commit
1168 branches = p4BranchesInGit(self.importIntoRemotes)
1169 self.p4BranchesInGit = branches.keys()
1170 for branch in branches.keys():
1171 self.initialParents[self.refPrefix + branch] = branches[branch]
1173 def updateOptionDict(self, d):
1175 if self.keepRepoPath:
1176 option_keys['keepRepoPath'] = 1
1178 d["options"] = ' '.join(sorted(option_keys.keys()))
1180 def readOptions(self, d):
1181 self.keepRepoPath = (d.has_key('options')
1182 and ('keepRepoPath' in d['options']))
1184 def gitRefForBranch(self, branch):
1185 if branch == "main":
1186 return self.refPrefix + "master"
1188 if len(branch) <= 0:
1191 return self.refPrefix + self.projectName + branch
1193 def gitCommitByP4Change(self, ref, change):
1195 print "looking in ref " + ref + " for change %s using bisect..." % change
1198 latestCommit = parseRevision(ref)
1202 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1203 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1208 log = extractLogMessageFromGitCommit(next)
1209 settings = extractSettingsGitLog(log)
1210 currentChange = int(settings['change'])
1212 print "current change %s" % currentChange
1214 if currentChange == change:
1216 print "found %s" % next
1219 if currentChange < change:
1220 earliestCommit = "^%s" % next
1222 latestCommit = "%s" % next
1226 def importNewBranch(self, branch, maxChange):
1227 # make fast-import flush all changes to disk and update the refs using the checkpoint
1228 # command so that we can try to find the branch parent in the git history
1229 self.gitStream.write("checkpoint\n\n");
1230 self.gitStream.flush();
1231 branchPrefix = self.depotPaths[0] + branch + "/"
1232 range = "@1,%s" % maxChange
1233 #print "prefix" + branchPrefix
1234 changes = p4ChangesForPaths([branchPrefix], range)
1235 if len(changes) <= 0:
1237 firstChange = changes[0]
1238 #print "first change in branch: %s" % firstChange
1239 sourceBranch = self.knownBranches[branch]
1240 sourceDepotPath = self.depotPaths[0] + sourceBranch
1241 sourceRef = self.gitRefForBranch(sourceBranch)
1242 #print "source " + sourceBranch
1244 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1245 #print "branch parent: %s" % branchParentChange
1246 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1247 if len(gitParent) > 0:
1248 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1249 #print "parent git commit: %s" % gitParent
1251 self.importChanges(changes)
1254 def importChanges(self, changes):
1256 for change in changes:
1257 description = p4Cmd("describe %s" % change)
1258 self.updateOptionDict(description)
1261 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1266 if self.detectBranches:
1267 branches = self.splitFilesIntoBranches(description)
1268 for branch in branches.keys():
1270 branchPrefix = self.depotPaths[0] + branch + "/"
1274 filesForCommit = branches[branch]
1277 print "branch is %s" % branch
1279 self.updatedBranches.add(branch)
1281 if branch not in self.createdBranches:
1282 self.createdBranches.add(branch)
1283 parent = self.knownBranches[branch]
1284 if parent == branch:
1287 fullBranch = self.projectName + branch
1288 if fullBranch not in self.p4BranchesInGit:
1290 print("\n Importing new branch %s" % fullBranch);
1291 if self.importNewBranch(branch, change - 1):
1293 self.p4BranchesInGit.append(fullBranch)
1295 print("\n Resuming with change %s" % change);
1298 print "parent determined through known branches: %s" % parent
1300 branch = self.gitRefForBranch(branch)
1301 parent = self.gitRefForBranch(parent)
1304 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1306 if len(parent) == 0 and branch in self.initialParents:
1307 parent = self.initialParents[branch]
1308 del self.initialParents[branch]
1310 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1312 files = self.extractFilesFromCommit(description)
1313 self.commit(description, files, self.branch, self.depotPaths,
1315 self.initialParent = ""
1317 print self.gitError.read()
1320 def importHeadRevision(self, revision):
1321 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1323 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1324 details["desc"] = ("Initial import of %s from the state at revision %s"
1325 % (' '.join(self.depotPaths), revision))
1326 details["change"] = revision
1330 for info in p4CmdList("files "
1331 + ' '.join(["%s...%s"
1333 for p in self.depotPaths])):
1335 if info['code'] == 'error':
1336 sys.stderr.write("p4 returned an error: %s\n"
1341 change = int(info["change"])
1342 if change > newestRevision:
1343 newestRevision = change
1345 if info["action"] == "delete":
1346 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1347 #fileCnt = fileCnt + 1
1350 for prop in ["depotFile", "rev", "action", "type" ]:
1351 details["%s%s" % (prop, fileCnt)] = info[prop]
1353 fileCnt = fileCnt + 1
1355 details["change"] = newestRevision
1356 self.updateOptionDict(details)
1358 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1360 print "IO error with git fast-import. Is your git version recent enough?"
1361 print self.gitError.read()
1364 def run(self, args):
1365 self.depotPaths = []
1366 self.changeRange = ""
1367 self.initialParent = ""
1368 self.previousDepotPaths = []
1370 # map from branch depot path to parent branch
1371 self.knownBranches = {}
1372 self.initialParents = {}
1373 self.hasOrigin = originP4BranchesExist()
1374 if not self.syncWithOrigin:
1375 self.hasOrigin = False
1377 if self.importIntoRemotes:
1378 self.refPrefix = "refs/remotes/p4/"
1380 self.refPrefix = "refs/heads/p4/"
1382 if self.syncWithOrigin and self.hasOrigin:
1384 print "Syncing with origin first by calling git fetch origin"
1385 system("git fetch origin")
1387 if len(self.branch) == 0:
1388 self.branch = self.refPrefix + "master"
1389 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1390 system("git update-ref %s refs/heads/p4" % self.branch)
1391 system("git branch -D p4");
1392 # create it /after/ importing, when master exists
1393 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1394 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1396 # TODO: should always look at previous commits,
1397 # merge with previous imports, if possible.
1400 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1401 self.listExistingP4GitBranches()
1403 if len(self.p4BranchesInGit) > 1:
1405 print "Importing from/into multiple branches"
1406 self.detectBranches = True
1409 print "branches: %s" % self.p4BranchesInGit
1412 for branch in self.p4BranchesInGit:
1413 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1415 settings = extractSettingsGitLog(logMsg)
1417 self.readOptions(settings)
1418 if (settings.has_key('depot-paths')
1419 and settings.has_key ('change')):
1420 change = int(settings['change']) + 1
1421 p4Change = max(p4Change, change)
1423 depotPaths = sorted(settings['depot-paths'])
1424 if self.previousDepotPaths == []:
1425 self.previousDepotPaths = depotPaths
1428 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1429 for i in range(0, min(len(cur), len(prev))):
1430 if cur[i] <> prev[i]:
1434 paths.append (cur[:i + 1])
1436 self.previousDepotPaths = paths
1439 self.depotPaths = sorted(self.previousDepotPaths)
1440 self.changeRange = "@%s,#head" % p4Change
1441 if not self.detectBranches:
1442 self.initialParent = parseRevision(self.branch)
1443 if not self.silent and not self.detectBranches:
1444 print "Performing incremental import into %s git branch" % self.branch
1446 if not self.branch.startswith("refs/"):
1447 self.branch = "refs/heads/" + self.branch
1449 if len(args) == 0 and self.depotPaths:
1451 print "Depot paths: %s" % ' '.join(self.depotPaths)
1453 if self.depotPaths and self.depotPaths != args:
1454 print ("previous import used depot path %s and now %s was specified. "
1455 "This doesn't work!" % (' '.join (self.depotPaths),
1459 self.depotPaths = sorted(args)
1465 for p in self.depotPaths:
1466 if p.find("@") != -1:
1467 atIdx = p.index("@")
1468 self.changeRange = p[atIdx:]
1469 if self.changeRange == "@all":
1470 self.changeRange = ""
1471 elif ',' not in self.changeRange:
1472 revision = self.changeRange
1473 self.changeRange = ""
1475 elif p.find("#") != -1:
1476 hashIdx = p.index("#")
1477 revision = p[hashIdx:]
1479 elif self.previousDepotPaths == []:
1482 p = re.sub ("\.\.\.$", "", p)
1483 if not p.endswith("/"):
1488 self.depotPaths = newPaths
1491 self.loadUserMapFromCache()
1493 if self.detectLabels:
1496 if self.detectBranches:
1497 ## FIXME - what's a P4 projectName ?
1498 self.projectName = self.guessProjectName()
1500 if not self.hasOrigin:
1501 self.getBranchMapping();
1503 print "p4-git branches: %s" % self.p4BranchesInGit
1504 print "initial parents: %s" % self.initialParents
1505 for b in self.p4BranchesInGit:
1509 b = b[len(self.projectName):]
1510 self.createdBranches.add(b)
1512 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1514 importProcess = subprocess.Popen(["git", "fast-import"],
1515 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1516 stderr=subprocess.PIPE);
1517 self.gitOutput = importProcess.stdout
1518 self.gitStream = importProcess.stdin
1519 self.gitError = importProcess.stderr
1522 self.importHeadRevision(revision)
1526 if len(self.changesFile) > 0:
1527 output = open(self.changesFile).readlines()
1530 changeSet.add(int(line))
1532 for change in changeSet:
1533 changes.append(change)
1538 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1540 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1542 if len(self.maxChanges) > 0:
1543 changes = changes[:min(int(self.maxChanges), len(changes))]
1545 if len(changes) == 0:
1547 print "No changes to import!"
1550 if not self.silent and not self.detectBranches:
1551 print "Import destination: %s" % self.branch
1553 self.updatedBranches = set()
1555 self.importChanges(changes)
1559 if len(self.updatedBranches) > 0:
1560 sys.stdout.write("Updated branches: ")
1561 for b in self.updatedBranches:
1562 sys.stdout.write("%s " % b)
1563 sys.stdout.write("\n")
1565 self.gitStream.close()
1566 if importProcess.wait() != 0:
1567 die("fast-import failed: %s" % self.gitError.read())
1568 self.gitOutput.close()
1569 self.gitError.close()
1573 class P4Rebase(Command):
1575 Command.__init__(self)
1577 self.description = ("Fetches the latest revision from perforce and "
1578 + "rebases the current work (branch) against it")
1579 self.verbose = False
1581 def run(self, args):
1585 return self.rebase()
1588 [upstream, settings] = findUpstreamBranchPoint()
1589 if len(upstream) == 0:
1590 die("Cannot find upstream branchpoint for rebase")
1592 # the branchpoint may be p4/foo~3, so strip off the parent
1593 upstream = re.sub("~[0-9]+$", "", upstream)
1595 print "Rebasing the current branch onto %s" % upstream
1596 oldHead = read_pipe("git rev-parse HEAD").strip()
1597 system("git rebase %s" % upstream)
1598 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1601 class P4Clone(P4Sync):
1603 P4Sync.__init__(self)
1604 self.description = "Creates a new git repository and imports from Perforce into it"
1605 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1606 self.options.append(
1607 optparse.make_option("--destination", dest="cloneDestination",
1608 action='store', default=None,
1609 help="where to leave result of the clone"))
1610 self.cloneDestination = None
1611 self.needsGit = False
1613 def defaultDestination(self, args):
1614 ## TODO: use common prefix of args?
1616 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1617 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1618 depotDir = re.sub(r"\.\.\.$,", "", depotDir)
1619 depotDir = re.sub(r"/$", "", depotDir)
1620 return os.path.split(depotDir)[1]
1622 def run(self, args):
1626 if self.keepRepoPath and not self.cloneDestination:
1627 sys.stderr.write("Must specify destination for --keep-path\n")
1632 if not self.cloneDestination and len(depotPaths) > 1:
1633 self.cloneDestination = depotPaths[-1]
1634 depotPaths = depotPaths[:-1]
1636 for p in depotPaths:
1637 if not p.startswith("//"):
1640 if not self.cloneDestination:
1641 self.cloneDestination = self.defaultDestination(args)
1643 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1644 if not os.path.exists(self.cloneDestination):
1645 os.makedirs(self.cloneDestination)
1646 os.chdir(self.cloneDestination)
1648 self.gitdir = os.getcwd() + "/.git"
1649 if not P4Sync.run(self, depotPaths):
1651 if self.branch != "master":
1652 if gitBranchExists("refs/remotes/p4/master"):
1653 system("git branch master refs/remotes/p4/master")
1654 system("git checkout -f")
1656 print "Could not detect main branch. No checkout/master branch created."
1660 class P4Branches(Command):
1662 Command.__init__(self)
1664 self.description = ("Shows the git branches that hold imports and their "
1665 + "corresponding perforce depot paths")
1666 self.verbose = False
1668 def run(self, args):
1669 if originP4BranchesExist():
1670 createOrUpdateBranchesFromOrigin()
1672 cmdline = "git rev-parse --symbolic "
1673 cmdline += " --remotes"
1675 for line in read_pipe_lines(cmdline):
1678 if not line.startswith('p4/') or line == "p4/HEAD":
1682 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1683 settings = extractSettingsGitLog(log)
1685 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1688 class HelpFormatter(optparse.IndentedHelpFormatter):
1690 optparse.IndentedHelpFormatter.__init__(self)
1692 def format_description(self, description):
1694 return description + "\n"
1698 def printUsage(commands):
1699 print "usage: %s <command> [options]" % sys.argv[0]
1701 print "valid commands: %s" % ", ".join(commands)
1703 print "Try %s <command> --help for command specific help." % sys.argv[0]
1708 "submit" : P4Submit,
1709 "commit" : P4Submit,
1711 "rebase" : P4Rebase,
1713 "rollback" : P4RollBack,
1714 "branches" : P4Branches
1719 if len(sys.argv[1:]) == 0:
1720 printUsage(commands.keys())
1724 cmdName = sys.argv[1]
1726 klass = commands[cmdName]
1729 print "unknown command %s" % cmdName
1731 printUsage(commands.keys())
1734 options = cmd.options
1735 cmd.gitdir = os.environ.get("GIT_DIR", None)
1739 if len(options) > 0:
1740 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1742 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1744 description = cmd.description,
1745 formatter = HelpFormatter())
1747 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1749 verbose = cmd.verbose
1751 if cmd.gitdir == None:
1752 cmd.gitdir = os.path.abspath(".git")
1753 if not isValidGitDir(cmd.gitdir):
1754 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1755 if os.path.exists(cmd.gitdir):
1756 cdup = read_pipe("git rev-parse --show-cdup").strip()
1760 if not isValidGitDir(cmd.gitdir):
1761 if isValidGitDir(cmd.gitdir + "/.git"):
1762 cmd.gitdir += "/.git"
1764 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1766 os.environ["GIT_DIR"] = cmd.gitdir
1768 if not cmd.run(args):
1772 if __name__ == '__main__':