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 setP4ExecBit(file, mode):
75 # Reopens an already open file and changes the execute bit to match
76 # the execute bit setting in the passed in mode.
80 if not isModeExec(mode):
81 p4Type = getP4OpenedType(file)
82 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
83 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
87 system("p4 reopen -t %s %s" % (p4Type, file))
89 def getP4OpenedType(file):
90 # Returns the perforce file type for the given file.
92 result = read_pipe("p4 opened %s" % file)
93 match = re.match(".*\((.+)\)$", result)
97 die("Could not determine file type for %s" % file)
99 def diffTreePattern():
100 # This is a simple generator for the diff tree regex pattern. This could be
101 # a class variable if this and parseDiffTreeEntry were a part of a class.
102 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
106 def parseDiffTreeEntry(entry):
107 """Parses a single diff tree entry into its component elements.
109 See git-diff-tree(1) manpage for details about the format of the diff
110 output. This method returns a dictionary with the following elements:
112 src_mode - The mode of the source file
113 dst_mode - The mode of the destination file
114 src_sha1 - The sha1 for the source file
115 dst_sha1 - The sha1 fr the destination file
116 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
117 status_score - The score for the status (applicable for 'C' and 'R'
118 statuses). This is None if there is no score.
119 src - The path for the source file.
120 dst - The path for the destination file. This is only present for
121 copy or renames. If it is not present, this is None.
123 If the pattern is not matched, None is returned."""
125 match = diffTreePattern().next().match(entry)
128 'src_mode': match.group(1),
129 'dst_mode': match.group(2),
130 'src_sha1': match.group(3),
131 'dst_sha1': match.group(4),
132 'status': match.group(5),
133 'status_score': match.group(6),
134 'src': match.group(7),
135 'dst': match.group(10)
139 def isModeExec(mode):
140 # Returns True if the given git mode represents an executable file,
142 return mode[-3:] == "755"
144 def isModeExecChanged(src_mode, dst_mode):
145 return isModeExec(src_mode) != isModeExec(dst_mode)
147 def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
148 cmd = "p4 -G %s" % cmd
150 sys.stderr.write("Opening pipe: %s\n" % cmd)
152 # Use a temporary file to avoid deadlocks without
153 # subprocess.communicate(), which would put another copy
154 # of stdout into memory.
156 if stdin is not None:
157 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
158 stdin_file.write(stdin)
162 p4 = subprocess.Popen(cmd, shell=True,
164 stdout=subprocess.PIPE)
169 entry = marshal.load(p4.stdout)
176 entry["p4ExitCode"] = exitCode
182 list = p4CmdList(cmd)
188 def p4Where(depotPath):
189 if not depotPath.endswith("/"):
191 output = p4Cmd("where %s..." % depotPath)
192 if output["code"] == "error":
196 clientPath = output.get("path")
197 elif "data" in output:
198 data = output.get("data")
199 lastSpace = data.rfind(" ")
200 clientPath = data[lastSpace + 1:]
202 if clientPath.endswith("..."):
203 clientPath = clientPath[:-3]
206 def currentGitBranch():
207 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
209 def isValidGitDir(path):
210 if (os.path.exists(path + "/HEAD")
211 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
215 def parseRevision(ref):
216 return read_pipe("git rev-parse %s" % ref).strip()
218 def extractLogMessageFromGitCommit(commit):
221 ## fixme: title is first line of commit, not 1st paragraph.
223 for log in read_pipe_lines("git cat-file commit %s" % commit):
232 def extractSettingsGitLog(log):
234 for line in log.split("\n"):
236 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
240 assignments = m.group(1).split (':')
241 for a in assignments:
243 key = vals[0].strip()
244 val = ('='.join (vals[1:])).strip()
245 if val.endswith ('\"') and val.startswith('"'):
250 paths = values.get("depot-paths")
252 paths = values.get("depot-path")
254 values['depot-paths'] = paths.split(',')
257 def gitBranchExists(branch):
258 proc = subprocess.Popen(["git", "rev-parse", branch],
259 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
260 return proc.wait() == 0;
263 return read_pipe("git config %s" % key, ignore_error=True).strip()
265 def p4BranchesInGit(branchesAreInRemotes = True):
268 cmdline = "git rev-parse --symbolic "
269 if branchesAreInRemotes:
270 cmdline += " --remotes"
272 cmdline += " --branches"
274 for line in read_pipe_lines(cmdline):
277 ## only import to p4/
278 if not line.startswith('p4/') or line == "p4/HEAD":
283 branch = re.sub ("^p4/", "", line)
285 branches[branch] = parseRevision(line)
288 def findUpstreamBranchPoint(head = "HEAD"):
289 branches = p4BranchesInGit()
290 # map from depot-path to branch name
291 branchByDepotPath = {}
292 for branch in branches.keys():
293 tip = branches[branch]
294 log = extractLogMessageFromGitCommit(tip)
295 settings = extractSettingsGitLog(log)
296 if settings.has_key("depot-paths"):
297 paths = ",".join(settings["depot-paths"])
298 branchByDepotPath[paths] = "remotes/p4/" + branch
302 while parent < 65535:
303 commit = head + "~%s" % parent
304 log = extractLogMessageFromGitCommit(commit)
305 settings = extractSettingsGitLog(log)
306 if settings.has_key("depot-paths"):
307 paths = ",".join(settings["depot-paths"])
308 if branchByDepotPath.has_key(paths):
309 return [branchByDepotPath[paths], settings]
313 return ["", settings]
315 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
317 print ("Creating/updating branch(es) in %s based on origin branch(es)"
320 originPrefix = "origin/p4/"
322 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
324 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
327 headName = line[len(originPrefix):]
328 remoteHead = localRefPrefix + headName
331 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
332 if (not original.has_key('depot-paths')
333 or not original.has_key('change')):
337 if not gitBranchExists(remoteHead):
339 print "creating %s" % remoteHead
342 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
343 if settings.has_key('change') > 0:
344 if settings['depot-paths'] == original['depot-paths']:
345 originP4Change = int(original['change'])
346 p4Change = int(settings['change'])
347 if originP4Change > p4Change:
348 print ("%s (%s) is newer than %s (%s). "
349 "Updating p4 branch from origin."
350 % (originHead, originP4Change,
351 remoteHead, p4Change))
354 print ("Ignoring: %s was imported from %s while "
355 "%s was imported from %s"
356 % (originHead, ','.join(original['depot-paths']),
357 remoteHead, ','.join(settings['depot-paths'])))
360 system("git update-ref %s %s" % (remoteHead, originHead))
362 def originP4BranchesExist():
363 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
365 def p4ChangesForPaths(depotPaths, changeRange):
367 output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange)
368 for p in depotPaths]))
372 changeNum = line.split(" ")[1]
373 changes.append(int(changeNum))
380 self.usage = "usage: %prog [options]"
383 class P4Debug(Command):
385 Command.__init__(self)
387 optparse.make_option("--verbose", dest="verbose", action="store_true",
390 self.description = "A tool to debug the output of p4 -G."
391 self.needsGit = False
396 for output in p4CmdList(" ".join(args)):
397 print 'Element: %d' % j
402 class P4RollBack(Command):
404 Command.__init__(self)
406 optparse.make_option("--verbose", dest="verbose", action="store_true"),
407 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
409 self.description = "A tool to debug the multi-branch import. Don't use :)"
411 self.rollbackLocalBranches = False
416 maxChange = int(args[0])
418 if "p4ExitCode" in p4Cmd("changes -m 1"):
419 die("Problems executing p4");
421 if self.rollbackLocalBranches:
422 refPrefix = "refs/heads/"
423 lines = read_pipe_lines("git rev-parse --symbolic --branches")
425 refPrefix = "refs/remotes/"
426 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
429 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
431 ref = refPrefix + line
432 log = extractLogMessageFromGitCommit(ref)
433 settings = extractSettingsGitLog(log)
435 depotPaths = settings['depot-paths']
436 change = settings['change']
440 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
441 for p in depotPaths]))) == 0:
442 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
443 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
446 while change and int(change) > maxChange:
449 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
450 system("git update-ref %s \"%s^\"" % (ref, ref))
451 log = extractLogMessageFromGitCommit(ref)
452 settings = extractSettingsGitLog(log)
455 depotPaths = settings['depot-paths']
456 change = settings['change']
459 print "%s rewound to %s" % (ref, change)
463 class P4Submit(Command):
465 Command.__init__(self)
467 optparse.make_option("--continue", action="store_false", dest="firstTime"),
468 optparse.make_option("--verbose", dest="verbose", action="store_true"),
469 optparse.make_option("--origin", dest="origin"),
470 optparse.make_option("--reset", action="store_true", dest="reset"),
471 optparse.make_option("--log-substitutions", dest="substFile"),
472 optparse.make_option("--direct", dest="directSubmit", action="store_true"),
473 optparse.make_option("-M", dest="detectRename", action="store_true"),
475 self.description = "Submit changes from git to the perforce depot."
476 self.usage += " [name of git branch to submit into perforce depot]"
477 self.firstTime = True
479 self.interactive = True
481 self.firstTime = True
483 self.directSubmit = False
484 self.detectRename = False
486 self.isWindows = (platform.system() == "Windows")
488 self.logSubstitutions = {}
489 self.logSubstitutions["<enter description here>"] = "%log%"
490 self.logSubstitutions["\tDetails:"] = "\tDetails: %log%"
493 if len(p4CmdList("opened ...")) > 0:
494 die("You have files opened with perforce! Close them before starting the sync.")
497 if len(self.config) > 0 and not self.reset:
498 die("Cannot start sync. Previous sync config found at %s\n"
499 "If you want to start submitting again from scratch "
500 "maybe you want to call git-p4 submit --reset" % self.configFile)
503 if self.directSubmit:
506 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
507 commits.append(line.strip())
510 self.config["commits"] = commits
512 def prepareLogMessage(self, template, message):
515 for line in template.split("\n"):
516 if line.startswith("#"):
517 result += line + "\n"
521 for key in self.logSubstitutions.keys():
522 if line.find(key) != -1:
523 value = self.logSubstitutions[key]
524 value = value.replace("%log%", message)
525 if value != "@remove@":
526 result += line.replace(key, value) + "\n"
531 result += line + "\n"
535 def prepareSubmitTemplate(self):
536 # remove lines in the Files section that show changes to files outside the depot path we're committing into
538 inFilesSection = False
539 for line in read_pipe_lines("p4 change -o"):
541 if line.startswith("\t"):
542 # path starts and ends with a tab
544 lastTab = path.rfind("\t")
546 path = path[:lastTab]
547 if not path.startswith(self.depotPath):
550 inFilesSection = False
552 if line.startswith("Files:"):
553 inFilesSection = True
559 def applyCommit(self, id):
560 if self.directSubmit:
561 print "Applying local change in working directory/index"
562 diff = self.diffStatus
564 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
565 diffOpts = ("", "-M")[self.detectRename]
566 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
568 filesToDelete = set()
570 filesToChangeExecBit = {}
572 diff = parseDiffTreeEntry(line)
573 modifier = diff['status']
576 system("p4 edit \"%s\"" % path)
577 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
578 filesToChangeExecBit[path] = diff['dst_mode']
579 editedFiles.add(path)
580 elif modifier == "A":
582 filesToChangeExecBit[path] = diff['dst_mode']
583 if path in filesToDelete:
584 filesToDelete.remove(path)
585 elif modifier == "D":
586 filesToDelete.add(path)
587 if path in filesToAdd:
588 filesToAdd.remove(path)
589 elif modifier == "R":
590 src, dest = diff['src'], diff['dst']
591 system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
592 system("p4 edit \"%s\"" % (dest))
593 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
594 filesToChangeExecBit[dest] = diff['dst_mode']
596 editedFiles.add(dest)
597 filesToDelete.add(src)
599 die("unknown modifier %s for %s" % (modifier, path))
601 if self.directSubmit:
602 diffcmd = "cat \"%s\"" % self.diffFile
604 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
605 patchcmd = diffcmd + " | git apply "
606 tryPatchCmd = patchcmd + "--check -"
607 applyPatchCmd = patchcmd + "--check --apply -"
609 if os.system(tryPatchCmd) != 0:
610 print "Unfortunately applying the change failed!"
611 print "What do you want to do?"
613 while response != "s" and response != "a" and response != "w":
614 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
615 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
617 print "Skipping! Good luck with the next patches..."
618 for f in editedFiles:
619 system("p4 revert \"%s\"" % f);
623 elif response == "a":
624 os.system(applyPatchCmd)
625 if len(filesToAdd) > 0:
626 print "You may also want to call p4 add on the following files:"
627 print " ".join(filesToAdd)
628 if len(filesToDelete):
629 print "The following files should be scheduled for deletion with p4 delete:"
630 print " ".join(filesToDelete)
631 die("Please resolve and submit the conflict manually and "
632 + "continue afterwards with git-p4 submit --continue")
633 elif response == "w":
634 system(diffcmd + " > patch.txt")
635 print "Patch saved to patch.txt in %s !" % self.clientPath
636 die("Please resolve and submit the conflict manually and "
637 "continue afterwards with git-p4 submit --continue")
639 system(applyPatchCmd)
642 system("p4 add \"%s\"" % f)
643 for f in filesToDelete:
644 system("p4 revert \"%s\"" % f)
645 system("p4 delete \"%s\"" % f)
647 # Set/clear executable bits
648 for f in filesToChangeExecBit.keys():
649 mode = filesToChangeExecBit[f]
650 setP4ExecBit(f, mode)
653 if not self.directSubmit:
654 logMessage = extractLogMessageFromGitCommit(id)
655 logMessage = logMessage.replace("\n", "\n\t")
657 logMessage = logMessage.replace("\n", "\r\n")
658 logMessage = logMessage.strip()
660 template = self.prepareSubmitTemplate()
663 submitTemplate = self.prepareLogMessage(template, logMessage)
664 diff = read_pipe("p4 diff -du ...")
666 for newFile in filesToAdd:
667 diff += "==== new file ====\n"
668 diff += "--- /dev/null\n"
669 diff += "+++ %s\n" % newFile
670 f = open(newFile, "r")
671 for line in f.readlines():
675 separatorLine = "######## everything below this line is just the diff #######"
676 if platform.system() == "Windows":
677 separatorLine += "\r"
678 separatorLine += "\n"
680 [handle, fileName] = tempfile.mkstemp()
681 tmpFile = os.fdopen(handle, "w+")
682 tmpFile.write(submitTemplate + separatorLine + diff)
685 if platform.system() == "Windows":
686 defaultEditor = "notepad"
687 editor = os.environ.get("EDITOR", defaultEditor);
688 system(editor + " " + fileName)
689 tmpFile = open(fileName, "rb")
690 message = tmpFile.read()
693 submitTemplate = message[:message.index(separatorLine)]
695 submitTemplate = submitTemplate.replace("\r\n", "\n")
697 if self.directSubmit:
698 print "Submitting to git first"
699 os.chdir(self.oldWorkingDirectory)
700 write_pipe("git commit -a -F -", submitTemplate)
701 os.chdir(self.clientPath)
703 write_pipe("p4 submit -i", submitTemplate)
705 fileName = "submit.txt"
706 file = open(fileName, "w+")
707 file.write(self.prepareLogMessage(template, logMessage))
709 print ("Perforce submit template written as %s. "
710 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
711 % (fileName, fileName))
715 self.master = currentGitBranch()
716 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
717 die("Detecting current git branch failed!")
719 self.master = args[0]
723 [upstream, settings] = findUpstreamBranchPoint()
724 self.depotPath = settings['depot-paths'][0]
725 if len(self.origin) == 0:
726 self.origin = upstream
729 print "Origin branch is " + self.origin
731 if len(self.depotPath) == 0:
732 print "Internal error: cannot locate perforce depot path from existing branches"
735 self.clientPath = p4Where(self.depotPath)
737 if len(self.clientPath) == 0:
738 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
741 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
742 self.oldWorkingDirectory = os.getcwd()
744 if self.directSubmit:
745 self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD")
746 if len(self.diffStatus) == 0:
747 print "No changes in working directory to submit."
749 patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD")
750 self.diffFile = self.gitdir + "/p4-git-diff"
751 f = open(self.diffFile, "wb")
755 os.chdir(self.clientPath)
756 print "Syncronizing p4 checkout..."
757 system("p4 sync ...")
760 self.firstTime = True
762 if len(self.substFile) > 0:
763 for line in open(self.substFile, "r").readlines():
764 tokens = line.strip().split("=")
765 self.logSubstitutions[tokens[0]] = tokens[1]
768 self.configFile = self.gitdir + "/p4-git-sync.cfg"
769 self.config = shelve.open(self.configFile, writeback=True)
774 commits = self.config.get("commits", [])
776 while len(commits) > 0:
777 self.firstTime = False
779 commits = commits[1:]
780 self.config["commits"] = commits
781 self.applyCommit(commit)
782 if not self.interactive:
787 if self.directSubmit:
788 os.remove(self.diffFile)
790 if len(commits) == 0:
792 print "No changes found to apply between %s and current HEAD" % self.origin
794 print "All changes applied!"
795 os.chdir(self.oldWorkingDirectory)
802 os.remove(self.configFile)
806 class P4Sync(Command):
808 Command.__init__(self)
810 optparse.make_option("--branch", dest="branch"),
811 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
812 optparse.make_option("--changesfile", dest="changesFile"),
813 optparse.make_option("--silent", dest="silent", action="store_true"),
814 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
815 optparse.make_option("--verbose", dest="verbose", action="store_true"),
816 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
817 help="Import into refs/heads/ , not refs/remotes"),
818 optparse.make_option("--max-changes", dest="maxChanges"),
819 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
820 help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
822 self.description = """Imports from Perforce into a git repository.\n
824 //depot/my/project/ -- to import the current head
825 //depot/my/project/@all -- to import everything
826 //depot/my/project/@1,6 -- to import only from revision 1 to 6
828 (a ... is not needed in the path p4 specification, it's added implicitly)"""
830 self.usage += " //depot/path[@revRange]"
832 self.createdBranches = Set()
833 self.committedChanges = Set()
835 self.detectBranches = False
836 self.detectLabels = False
837 self.changesFile = ""
838 self.syncWithOrigin = True
840 self.importIntoRemotes = True
842 self.isWindows = (platform.system() == "Windows")
843 self.keepRepoPath = False
844 self.depotPaths = None
845 self.p4BranchesInGit = []
846 self.cloneExclude = []
848 if gitConfig("git-p4.syncFromOrigin") == "false":
849 self.syncWithOrigin = False
851 def extractFilesFromCommit(self, commit):
852 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
853 for path in self.cloneExclude]
856 while commit.has_key("depotFile%s" % fnum):
857 path = commit["depotFile%s" % fnum]
859 if [p for p in self.cloneExclude
860 if path.startswith (p)]:
863 found = [p for p in self.depotPaths
864 if path.startswith (p)]
871 file["rev"] = commit["rev%s" % fnum]
872 file["action"] = commit["action%s" % fnum]
873 file["type"] = commit["type%s" % fnum]
878 def stripRepoPath(self, path, prefixes):
879 if self.keepRepoPath:
880 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
883 if path.startswith(p):
888 def splitFilesIntoBranches(self, commit):
891 while commit.has_key("depotFile%s" % fnum):
892 path = commit["depotFile%s" % fnum]
893 found = [p for p in self.depotPaths
894 if path.startswith (p)]
901 file["rev"] = commit["rev%s" % fnum]
902 file["action"] = commit["action%s" % fnum]
903 file["type"] = commit["type%s" % fnum]
906 relPath = self.stripRepoPath(path, self.depotPaths)
908 for branch in self.knownBranches.keys():
910 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
911 if relPath.startswith(branch + "/"):
912 if branch not in branches:
913 branches[branch] = []
914 branches[branch].append(file)
919 ## Should move this out, doesn't use SELF.
920 def readP4Files(self, files):
921 files = [f for f in files
922 if f['action'] != 'delete']
927 filedata = p4CmdList('-x - print',
928 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
931 if "p4ExitCode" in filedata[0]:
932 die("Problems executing p4. Error: [%d]."
933 % (filedata[0]['p4ExitCode']));
937 while j < len(filedata):
941 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
942 tmp = filedata[j]['data']
943 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
944 tmp = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', tmp)
945 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
946 tmp = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', tmp)
951 if not stat.has_key('depotFile'):
952 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
955 contents[stat['depotFile']] = text
958 assert not f.has_key('data')
959 f['data'] = contents[f['path']]
961 def commit(self, details, files, branch, branchPrefixes, parent = ""):
962 epoch = details["time"]
963 author = details["user"]
966 print "commit into %s" % branch
968 # start with reading files; if that fails, we should not
972 if [p for p in branchPrefixes if f['path'].startswith(p)]:
975 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
977 self.readP4Files(files)
982 self.gitStream.write("commit %s\n" % branch)
983 # gitStream.write("mark :%s\n" % details["change"])
984 self.committedChanges.add(int(details["change"]))
986 if author not in self.users:
987 self.getUserMapFromPerforceServer()
988 if author in self.users:
989 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
991 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
993 self.gitStream.write("committer %s\n" % committer)
995 self.gitStream.write("data <<EOT\n")
996 self.gitStream.write(details["desc"])
997 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
998 % (','.join (branchPrefixes), details["change"]))
999 if len(details['options']) > 0:
1000 self.gitStream.write(": options = %s" % details['options'])
1001 self.gitStream.write("]\nEOT\n\n")
1005 print "parent %s" % parent
1006 self.gitStream.write("from %s\n" % parent)
1009 if file["type"] == "apple":
1010 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1013 relPath = self.stripRepoPath(file['path'], branchPrefixes)
1014 if file["action"] == "delete":
1015 self.gitStream.write("D %s\n" % relPath)
1020 if isP4Exec(file["type"]):
1022 elif file["type"] == "symlink":
1024 # p4 print on a symlink contains "target\n", so strip it off
1027 if self.isWindows and file["type"].endswith("text"):
1028 data = data.replace("\r\n", "\n")
1030 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1031 self.gitStream.write("data %s\n" % len(data))
1032 self.gitStream.write(data)
1033 self.gitStream.write("\n")
1035 self.gitStream.write("\n")
1037 change = int(details["change"])
1039 if self.labels.has_key(change):
1040 label = self.labels[change]
1041 labelDetails = label[0]
1042 labelRevisions = label[1]
1044 print "Change %s is labelled %s" % (change, labelDetails)
1046 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1047 for p in branchPrefixes]))
1049 if len(files) == len(labelRevisions):
1053 if info["action"] == "delete":
1055 cleanedFiles[info["depotFile"]] = info["rev"]
1057 if cleanedFiles == labelRevisions:
1058 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1059 self.gitStream.write("from %s\n" % branch)
1061 owner = labelDetails["Owner"]
1063 if author in self.users:
1064 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1066 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1067 self.gitStream.write("tagger %s\n" % tagger)
1068 self.gitStream.write("data <<EOT\n")
1069 self.gitStream.write(labelDetails["Description"])
1070 self.gitStream.write("EOT\n\n")
1074 print ("Tag %s does not match with change %s: files do not match."
1075 % (labelDetails["label"], change))
1079 print ("Tag %s does not match with change %s: file count is different."
1080 % (labelDetails["label"], change))
1082 def getUserCacheFilename(self):
1083 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1084 return home + "/.gitp4-usercache.txt"
1086 def getUserMapFromPerforceServer(self):
1087 if self.userMapFromPerforceServer:
1091 for output in p4CmdList("users"):
1092 if not output.has_key("User"):
1094 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1098 for (key, val) in self.users.items():
1099 s += "%s\t%s\n" % (key, val)
1101 open(self.getUserCacheFilename(), "wb").write(s)
1102 self.userMapFromPerforceServer = True
1104 def loadUserMapFromCache(self):
1106 self.userMapFromPerforceServer = False
1108 cache = open(self.getUserCacheFilename(), "rb")
1109 lines = cache.readlines()
1112 entry = line.strip().split("\t")
1113 self.users[entry[0]] = entry[1]
1115 self.getUserMapFromPerforceServer()
1117 def getLabels(self):
1120 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1121 if len(l) > 0 and not self.silent:
1122 print "Finding files belonging to labels in %s" % `self.depotPaths`
1125 label = output["label"]
1129 print "Querying files for label %s" % label
1130 for file in p4CmdList("files "
1131 + ' '.join (["%s...@%s" % (p, label)
1132 for p in self.depotPaths])):
1133 revisions[file["depotFile"]] = file["rev"]
1134 change = int(file["change"])
1135 if change > newestChange:
1136 newestChange = change
1138 self.labels[newestChange] = [output, revisions]
1141 print "Label changes: %s" % self.labels.keys()
1143 def guessProjectName(self):
1144 for p in self.depotPaths:
1147 p = p[p.strip().rfind("/") + 1:]
1148 if not p.endswith("/"):
1152 def getBranchMapping(self):
1153 lostAndFoundBranches = set()
1155 for info in p4CmdList("branches"):
1156 details = p4Cmd("branch -o %s" % info["branch"])
1158 while details.has_key("View%s" % viewIdx):
1159 paths = details["View%s" % viewIdx].split(" ")
1160 viewIdx = viewIdx + 1
1161 # require standard //depot/foo/... //depot/bar/... mapping
1162 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1165 destination = paths[1]
1167 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1168 source = source[len(self.depotPaths[0]):-4]
1169 destination = destination[len(self.depotPaths[0]):-4]
1171 if destination in self.knownBranches:
1173 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1174 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1177 self.knownBranches[destination] = source
1179 lostAndFoundBranches.discard(destination)
1181 if source not in self.knownBranches:
1182 lostAndFoundBranches.add(source)
1185 for branch in lostAndFoundBranches:
1186 self.knownBranches[branch] = branch
1188 def getBranchMappingFromGitBranches(self):
1189 branches = p4BranchesInGit(self.importIntoRemotes)
1190 for branch in branches.keys():
1191 if branch == "master":
1194 branch = branch[len(self.projectName):]
1195 self.knownBranches[branch] = branch
1197 def listExistingP4GitBranches(self):
1198 # branches holds mapping from name to commit
1199 branches = p4BranchesInGit(self.importIntoRemotes)
1200 self.p4BranchesInGit = branches.keys()
1201 for branch in branches.keys():
1202 self.initialParents[self.refPrefix + branch] = branches[branch]
1204 def updateOptionDict(self, d):
1206 if self.keepRepoPath:
1207 option_keys['keepRepoPath'] = 1
1209 d["options"] = ' '.join(sorted(option_keys.keys()))
1211 def readOptions(self, d):
1212 self.keepRepoPath = (d.has_key('options')
1213 and ('keepRepoPath' in d['options']))
1215 def gitRefForBranch(self, branch):
1216 if branch == "main":
1217 return self.refPrefix + "master"
1219 if len(branch) <= 0:
1222 return self.refPrefix + self.projectName + branch
1224 def gitCommitByP4Change(self, ref, change):
1226 print "looking in ref " + ref + " for change %s using bisect..." % change
1229 latestCommit = parseRevision(ref)
1233 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1234 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1239 log = extractLogMessageFromGitCommit(next)
1240 settings = extractSettingsGitLog(log)
1241 currentChange = int(settings['change'])
1243 print "current change %s" % currentChange
1245 if currentChange == change:
1247 print "found %s" % next
1250 if currentChange < change:
1251 earliestCommit = "^%s" % next
1253 latestCommit = "%s" % next
1257 def importNewBranch(self, branch, maxChange):
1258 # make fast-import flush all changes to disk and update the refs using the checkpoint
1259 # command so that we can try to find the branch parent in the git history
1260 self.gitStream.write("checkpoint\n\n");
1261 self.gitStream.flush();
1262 branchPrefix = self.depotPaths[0] + branch + "/"
1263 range = "@1,%s" % maxChange
1264 #print "prefix" + branchPrefix
1265 changes = p4ChangesForPaths([branchPrefix], range)
1266 if len(changes) <= 0:
1268 firstChange = changes[0]
1269 #print "first change in branch: %s" % firstChange
1270 sourceBranch = self.knownBranches[branch]
1271 sourceDepotPath = self.depotPaths[0] + sourceBranch
1272 sourceRef = self.gitRefForBranch(sourceBranch)
1273 #print "source " + sourceBranch
1275 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1276 #print "branch parent: %s" % branchParentChange
1277 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1278 if len(gitParent) > 0:
1279 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1280 #print "parent git commit: %s" % gitParent
1282 self.importChanges(changes)
1285 def importChanges(self, changes):
1287 for change in changes:
1288 description = p4Cmd("describe %s" % change)
1289 self.updateOptionDict(description)
1292 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1297 if self.detectBranches:
1298 branches = self.splitFilesIntoBranches(description)
1299 for branch in branches.keys():
1301 branchPrefix = self.depotPaths[0] + branch + "/"
1305 filesForCommit = branches[branch]
1308 print "branch is %s" % branch
1310 self.updatedBranches.add(branch)
1312 if branch not in self.createdBranches:
1313 self.createdBranches.add(branch)
1314 parent = self.knownBranches[branch]
1315 if parent == branch:
1318 fullBranch = self.projectName + branch
1319 if fullBranch not in self.p4BranchesInGit:
1321 print("\n Importing new branch %s" % fullBranch);
1322 if self.importNewBranch(branch, change - 1):
1324 self.p4BranchesInGit.append(fullBranch)
1326 print("\n Resuming with change %s" % change);
1329 print "parent determined through known branches: %s" % parent
1331 branch = self.gitRefForBranch(branch)
1332 parent = self.gitRefForBranch(parent)
1335 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1337 if len(parent) == 0 and branch in self.initialParents:
1338 parent = self.initialParents[branch]
1339 del self.initialParents[branch]
1341 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1343 files = self.extractFilesFromCommit(description)
1344 self.commit(description, files, self.branch, self.depotPaths,
1346 self.initialParent = ""
1348 print self.gitError.read()
1351 def importHeadRevision(self, revision):
1352 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1354 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1355 details["desc"] = ("Initial import of %s from the state at revision %s"
1356 % (' '.join(self.depotPaths), revision))
1357 details["change"] = revision
1361 for info in p4CmdList("files "
1362 + ' '.join(["%s...%s"
1364 for p in self.depotPaths])):
1366 if info['code'] == 'error':
1367 sys.stderr.write("p4 returned an error: %s\n"
1372 change = int(info["change"])
1373 if change > newestRevision:
1374 newestRevision = change
1376 if info["action"] == "delete":
1377 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1378 #fileCnt = fileCnt + 1
1381 for prop in ["depotFile", "rev", "action", "type" ]:
1382 details["%s%s" % (prop, fileCnt)] = info[prop]
1384 fileCnt = fileCnt + 1
1386 details["change"] = newestRevision
1387 self.updateOptionDict(details)
1389 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1391 print "IO error with git fast-import. Is your git version recent enough?"
1392 print self.gitError.read()
1395 def run(self, args):
1396 self.depotPaths = []
1397 self.changeRange = ""
1398 self.initialParent = ""
1399 self.previousDepotPaths = []
1401 # map from branch depot path to parent branch
1402 self.knownBranches = {}
1403 self.initialParents = {}
1404 self.hasOrigin = originP4BranchesExist()
1405 if not self.syncWithOrigin:
1406 self.hasOrigin = False
1408 if self.importIntoRemotes:
1409 self.refPrefix = "refs/remotes/p4/"
1411 self.refPrefix = "refs/heads/p4/"
1413 if self.syncWithOrigin and self.hasOrigin:
1415 print "Syncing with origin first by calling git fetch origin"
1416 system("git fetch origin")
1418 if len(self.branch) == 0:
1419 self.branch = self.refPrefix + "master"
1420 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1421 system("git update-ref %s refs/heads/p4" % self.branch)
1422 system("git branch -D p4");
1423 # create it /after/ importing, when master exists
1424 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1425 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1427 # TODO: should always look at previous commits,
1428 # merge with previous imports, if possible.
1431 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1432 self.listExistingP4GitBranches()
1434 if len(self.p4BranchesInGit) > 1:
1436 print "Importing from/into multiple branches"
1437 self.detectBranches = True
1440 print "branches: %s" % self.p4BranchesInGit
1443 for branch in self.p4BranchesInGit:
1444 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1446 settings = extractSettingsGitLog(logMsg)
1448 self.readOptions(settings)
1449 if (settings.has_key('depot-paths')
1450 and settings.has_key ('change')):
1451 change = int(settings['change']) + 1
1452 p4Change = max(p4Change, change)
1454 depotPaths = sorted(settings['depot-paths'])
1455 if self.previousDepotPaths == []:
1456 self.previousDepotPaths = depotPaths
1459 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1460 for i in range(0, min(len(cur), len(prev))):
1461 if cur[i] <> prev[i]:
1465 paths.append (cur[:i + 1])
1467 self.previousDepotPaths = paths
1470 self.depotPaths = sorted(self.previousDepotPaths)
1471 self.changeRange = "@%s,#head" % p4Change
1472 if not self.detectBranches:
1473 self.initialParent = parseRevision(self.branch)
1474 if not self.silent and not self.detectBranches:
1475 print "Performing incremental import into %s git branch" % self.branch
1477 if not self.branch.startswith("refs/"):
1478 self.branch = "refs/heads/" + self.branch
1480 if len(args) == 0 and self.depotPaths:
1482 print "Depot paths: %s" % ' '.join(self.depotPaths)
1484 if self.depotPaths and self.depotPaths != args:
1485 print ("previous import used depot path %s and now %s was specified. "
1486 "This doesn't work!" % (' '.join (self.depotPaths),
1490 self.depotPaths = sorted(args)
1496 for p in self.depotPaths:
1497 if p.find("@") != -1:
1498 atIdx = p.index("@")
1499 self.changeRange = p[atIdx:]
1500 if self.changeRange == "@all":
1501 self.changeRange = ""
1502 elif ',' not in self.changeRange:
1503 revision = self.changeRange
1504 self.changeRange = ""
1506 elif p.find("#") != -1:
1507 hashIdx = p.index("#")
1508 revision = p[hashIdx:]
1510 elif self.previousDepotPaths == []:
1513 p = re.sub ("\.\.\.$", "", p)
1514 if not p.endswith("/"):
1519 self.depotPaths = newPaths
1522 self.loadUserMapFromCache()
1524 if self.detectLabels:
1527 if self.detectBranches:
1528 ## FIXME - what's a P4 projectName ?
1529 self.projectName = self.guessProjectName()
1532 self.getBranchMappingFromGitBranches()
1534 self.getBranchMapping()
1536 print "p4-git branches: %s" % self.p4BranchesInGit
1537 print "initial parents: %s" % self.initialParents
1538 for b in self.p4BranchesInGit:
1542 b = b[len(self.projectName):]
1543 self.createdBranches.add(b)
1545 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1547 importProcess = subprocess.Popen(["git", "fast-import"],
1548 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1549 stderr=subprocess.PIPE);
1550 self.gitOutput = importProcess.stdout
1551 self.gitStream = importProcess.stdin
1552 self.gitError = importProcess.stderr
1555 self.importHeadRevision(revision)
1559 if len(self.changesFile) > 0:
1560 output = open(self.changesFile).readlines()
1563 changeSet.add(int(line))
1565 for change in changeSet:
1566 changes.append(change)
1571 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1573 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1575 if len(self.maxChanges) > 0:
1576 changes = changes[:min(int(self.maxChanges), len(changes))]
1578 if len(changes) == 0:
1580 print "No changes to import!"
1583 if not self.silent and not self.detectBranches:
1584 print "Import destination: %s" % self.branch
1586 self.updatedBranches = set()
1588 self.importChanges(changes)
1592 if len(self.updatedBranches) > 0:
1593 sys.stdout.write("Updated branches: ")
1594 for b in self.updatedBranches:
1595 sys.stdout.write("%s " % b)
1596 sys.stdout.write("\n")
1598 self.gitStream.close()
1599 if importProcess.wait() != 0:
1600 die("fast-import failed: %s" % self.gitError.read())
1601 self.gitOutput.close()
1602 self.gitError.close()
1606 class P4Rebase(Command):
1608 Command.__init__(self)
1610 self.description = ("Fetches the latest revision from perforce and "
1611 + "rebases the current work (branch) against it")
1612 self.verbose = False
1614 def run(self, args):
1618 return self.rebase()
1621 if os.system("git update-index --refresh") != 0:
1622 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.");
1623 if len(read_pipe("git diff-index HEAD --")) > 0:
1624 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1626 [upstream, settings] = findUpstreamBranchPoint()
1627 if len(upstream) == 0:
1628 die("Cannot find upstream branchpoint for rebase")
1630 # the branchpoint may be p4/foo~3, so strip off the parent
1631 upstream = re.sub("~[0-9]+$", "", upstream)
1633 print "Rebasing the current branch onto %s" % upstream
1634 oldHead = read_pipe("git rev-parse HEAD").strip()
1635 system("git rebase %s" % upstream)
1636 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1639 class P4Clone(P4Sync):
1641 P4Sync.__init__(self)
1642 self.description = "Creates a new git repository and imports from Perforce into it"
1643 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1645 optparse.make_option("--destination", dest="cloneDestination",
1646 action='store', default=None,
1647 help="where to leave result of the clone"),
1648 optparse.make_option("-/", dest="cloneExclude",
1649 action="append", type="string",
1650 help="exclude depot path")
1652 self.cloneDestination = None
1653 self.needsGit = False
1655 # This is required for the "append" cloneExclude action
1656 def ensure_value(self, attr, value):
1657 if not hasattr(self, attr) or getattr(self, attr) is None:
1658 setattr(self, attr, value)
1659 return getattr(self, attr)
1661 def defaultDestination(self, args):
1662 ## TODO: use common prefix of args?
1664 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1665 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1666 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1667 depotDir = re.sub(r"/$", "", depotDir)
1668 return os.path.split(depotDir)[1]
1670 def run(self, args):
1674 if self.keepRepoPath and not self.cloneDestination:
1675 sys.stderr.write("Must specify destination for --keep-path\n")
1680 if not self.cloneDestination and len(depotPaths) > 1:
1681 self.cloneDestination = depotPaths[-1]
1682 depotPaths = depotPaths[:-1]
1684 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1685 for p in depotPaths:
1686 if not p.startswith("//"):
1689 if not self.cloneDestination:
1690 self.cloneDestination = self.defaultDestination(args)
1692 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1693 if not os.path.exists(self.cloneDestination):
1694 os.makedirs(self.cloneDestination)
1695 os.chdir(self.cloneDestination)
1697 self.gitdir = os.getcwd() + "/.git"
1698 if not P4Sync.run(self, depotPaths):
1700 if self.branch != "master":
1701 if gitBranchExists("refs/remotes/p4/master"):
1702 system("git branch master refs/remotes/p4/master")
1703 system("git checkout -f")
1705 print "Could not detect main branch. No checkout/master branch created."
1709 class P4Branches(Command):
1711 Command.__init__(self)
1713 self.description = ("Shows the git branches that hold imports and their "
1714 + "corresponding perforce depot paths")
1715 self.verbose = False
1717 def run(self, args):
1718 if originP4BranchesExist():
1719 createOrUpdateBranchesFromOrigin()
1721 cmdline = "git rev-parse --symbolic "
1722 cmdline += " --remotes"
1724 for line in read_pipe_lines(cmdline):
1727 if not line.startswith('p4/') or line == "p4/HEAD":
1731 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1732 settings = extractSettingsGitLog(log)
1734 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1737 class HelpFormatter(optparse.IndentedHelpFormatter):
1739 optparse.IndentedHelpFormatter.__init__(self)
1741 def format_description(self, description):
1743 return description + "\n"
1747 def printUsage(commands):
1748 print "usage: %s <command> [options]" % sys.argv[0]
1750 print "valid commands: %s" % ", ".join(commands)
1752 print "Try %s <command> --help for command specific help." % sys.argv[0]
1757 "submit" : P4Submit,
1758 "commit" : P4Submit,
1760 "rebase" : P4Rebase,
1762 "rollback" : P4RollBack,
1763 "branches" : P4Branches
1768 if len(sys.argv[1:]) == 0:
1769 printUsage(commands.keys())
1773 cmdName = sys.argv[1]
1775 klass = commands[cmdName]
1778 print "unknown command %s" % cmdName
1780 printUsage(commands.keys())
1783 options = cmd.options
1784 cmd.gitdir = os.environ.get("GIT_DIR", None)
1788 if len(options) > 0:
1789 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1791 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1793 description = cmd.description,
1794 formatter = HelpFormatter())
1796 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1798 verbose = cmd.verbose
1800 if cmd.gitdir == None:
1801 cmd.gitdir = os.path.abspath(".git")
1802 if not isValidGitDir(cmd.gitdir):
1803 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1804 if os.path.exists(cmd.gitdir):
1805 cdup = read_pipe("git rev-parse --show-cdup").strip()
1809 if not isValidGitDir(cmd.gitdir):
1810 if isValidGitDir(cmd.gitdir + "/.git"):
1811 cmd.gitdir += "/.git"
1813 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1815 os.environ["GIT_DIR"] = cmd.gitdir
1817 if not cmd.run(args):
1821 if __name__ == '__main__':