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("--direct", dest="directSubmit", action="store_true"),
472 optparse.make_option("-M", dest="detectRename", action="store_true"),
474 self.description = "Submit changes from git to the perforce depot."
475 self.usage += " [name of git branch to submit into perforce depot]"
476 self.firstTime = True
478 self.interactive = True
479 self.firstTime = True
481 self.directSubmit = False
482 self.detectRename = False
484 self.isWindows = (platform.system() == "Windows")
486 self.logSubstitutions = {}
487 self.logSubstitutions["<enter description here>"] = "%log%"
488 self.logSubstitutions["\tDetails:"] = "\tDetails: %log%"
491 if len(p4CmdList("opened ...")) > 0:
492 die("You have files opened with perforce! Close them before starting the sync.")
495 if len(self.config) > 0 and not self.reset:
496 die("Cannot start sync. Previous sync config found at %s\n"
497 "If you want to start submitting again from scratch "
498 "maybe you want to call git-p4 submit --reset" % self.configFile)
501 if self.directSubmit:
504 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
505 commits.append(line.strip())
508 self.config["commits"] = commits
510 def prepareLogMessage(self, template, message):
513 for line in template.split("\n"):
514 if line.startswith("#"):
515 result += line + "\n"
519 for key in self.logSubstitutions.keys():
520 if line.find(key) != -1:
521 value = self.logSubstitutions[key]
522 value = value.replace("%log%", message)
523 if value != "@remove@":
524 result += line.replace(key, value) + "\n"
529 result += line + "\n"
533 def prepareSubmitTemplate(self):
534 # remove lines in the Files section that show changes to files outside the depot path we're committing into
536 inFilesSection = False
537 for line in read_pipe_lines("p4 change -o"):
539 if line.startswith("\t"):
540 # path starts and ends with a tab
542 lastTab = path.rfind("\t")
544 path = path[:lastTab]
545 if not path.startswith(self.depotPath):
548 inFilesSection = False
550 if line.startswith("Files:"):
551 inFilesSection = True
557 def applyCommit(self, id):
558 if self.directSubmit:
559 print "Applying local change in working directory/index"
560 diff = self.diffStatus
562 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
563 diffOpts = ("", "-M")[self.detectRename]
564 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
566 filesToDelete = set()
568 filesToChangeExecBit = {}
570 diff = parseDiffTreeEntry(line)
571 modifier = diff['status']
574 system("p4 edit \"%s\"" % path)
575 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
576 filesToChangeExecBit[path] = diff['dst_mode']
577 editedFiles.add(path)
578 elif modifier == "A":
580 filesToChangeExecBit[path] = diff['dst_mode']
581 if path in filesToDelete:
582 filesToDelete.remove(path)
583 elif modifier == "D":
584 filesToDelete.add(path)
585 if path in filesToAdd:
586 filesToAdd.remove(path)
587 elif modifier == "R":
588 src, dest = diff['src'], diff['dst']
589 system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
590 system("p4 edit \"%s\"" % (dest))
591 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
592 filesToChangeExecBit[dest] = diff['dst_mode']
594 editedFiles.add(dest)
595 filesToDelete.add(src)
597 die("unknown modifier %s for %s" % (modifier, path))
599 if self.directSubmit:
600 diffcmd = "cat \"%s\"" % self.diffFile
602 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
603 patchcmd = diffcmd + " | git apply "
604 tryPatchCmd = patchcmd + "--check -"
605 applyPatchCmd = patchcmd + "--check --apply -"
607 if os.system(tryPatchCmd) != 0:
608 print "Unfortunately applying the change failed!"
609 print "What do you want to do?"
611 while response != "s" and response != "a" and response != "w":
612 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
613 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
615 print "Skipping! Good luck with the next patches..."
616 for f in editedFiles:
617 system("p4 revert \"%s\"" % f);
621 elif response == "a":
622 os.system(applyPatchCmd)
623 if len(filesToAdd) > 0:
624 print "You may also want to call p4 add on the following files:"
625 print " ".join(filesToAdd)
626 if len(filesToDelete):
627 print "The following files should be scheduled for deletion with p4 delete:"
628 print " ".join(filesToDelete)
629 die("Please resolve and submit the conflict manually and "
630 + "continue afterwards with git-p4 submit --continue")
631 elif response == "w":
632 system(diffcmd + " > patch.txt")
633 print "Patch saved to patch.txt in %s !" % self.clientPath
634 die("Please resolve and submit the conflict manually and "
635 "continue afterwards with git-p4 submit --continue")
637 system(applyPatchCmd)
640 system("p4 add \"%s\"" % f)
641 for f in filesToDelete:
642 system("p4 revert \"%s\"" % f)
643 system("p4 delete \"%s\"" % f)
645 # Set/clear executable bits
646 for f in filesToChangeExecBit.keys():
647 mode = filesToChangeExecBit[f]
648 setP4ExecBit(f, mode)
651 if not self.directSubmit:
652 logMessage = extractLogMessageFromGitCommit(id)
653 logMessage = logMessage.replace("\n", "\n\t")
655 logMessage = logMessage.replace("\n", "\r\n")
656 logMessage = logMessage.strip()
658 template = self.prepareSubmitTemplate()
661 submitTemplate = self.prepareLogMessage(template, logMessage)
662 diff = read_pipe("p4 diff -du ...")
664 for newFile in filesToAdd:
665 diff += "==== new file ====\n"
666 diff += "--- /dev/null\n"
667 diff += "+++ %s\n" % newFile
668 f = open(newFile, "r")
669 for line in f.readlines():
673 separatorLine = "######## everything below this line is just the diff #######"
674 if platform.system() == "Windows":
675 separatorLine += "\r"
676 separatorLine += "\n"
678 [handle, fileName] = tempfile.mkstemp()
679 tmpFile = os.fdopen(handle, "w+")
680 tmpFile.write(submitTemplate + separatorLine + diff)
683 if platform.system() == "Windows":
684 defaultEditor = "notepad"
685 editor = os.environ.get("EDITOR", defaultEditor);
686 system(editor + " " + fileName)
687 tmpFile = open(fileName, "rb")
688 message = tmpFile.read()
691 submitTemplate = message[:message.index(separatorLine)]
693 submitTemplate = submitTemplate.replace("\r\n", "\n")
695 if self.directSubmit:
696 print "Submitting to git first"
697 os.chdir(self.oldWorkingDirectory)
698 write_pipe("git commit -a -F -", submitTemplate)
699 os.chdir(self.clientPath)
701 write_pipe("p4 submit -i", submitTemplate)
703 fileName = "submit.txt"
704 file = open(fileName, "w+")
705 file.write(self.prepareLogMessage(template, logMessage))
707 print ("Perforce submit template written as %s. "
708 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
709 % (fileName, fileName))
713 self.master = currentGitBranch()
714 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
715 die("Detecting current git branch failed!")
717 self.master = args[0]
721 [upstream, settings] = findUpstreamBranchPoint()
722 self.depotPath = settings['depot-paths'][0]
723 if len(self.origin) == 0:
724 self.origin = upstream
727 print "Origin branch is " + self.origin
729 if len(self.depotPath) == 0:
730 print "Internal error: cannot locate perforce depot path from existing branches"
733 self.clientPath = p4Where(self.depotPath)
735 if len(self.clientPath) == 0:
736 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
739 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
740 self.oldWorkingDirectory = os.getcwd()
742 if self.directSubmit:
743 self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD")
744 if len(self.diffStatus) == 0:
745 print "No changes in working directory to submit."
747 patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD")
748 self.diffFile = self.gitdir + "/p4-git-diff"
749 f = open(self.diffFile, "wb")
753 os.chdir(self.clientPath)
754 print "Syncronizing p4 checkout..."
755 system("p4 sync ...")
758 self.firstTime = True
761 self.configFile = self.gitdir + "/p4-git-sync.cfg"
762 self.config = shelve.open(self.configFile, writeback=True)
767 commits = self.config.get("commits", [])
769 while len(commits) > 0:
770 self.firstTime = False
772 commits = commits[1:]
773 self.config["commits"] = commits
774 self.applyCommit(commit)
775 if not self.interactive:
780 if self.directSubmit:
781 os.remove(self.diffFile)
783 if len(commits) == 0:
785 print "No changes found to apply between %s and current HEAD" % self.origin
787 print "All changes applied!"
788 os.chdir(self.oldWorkingDirectory)
795 os.remove(self.configFile)
799 class P4Sync(Command):
801 Command.__init__(self)
803 optparse.make_option("--branch", dest="branch"),
804 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
805 optparse.make_option("--changesfile", dest="changesFile"),
806 optparse.make_option("--silent", dest="silent", action="store_true"),
807 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
808 optparse.make_option("--verbose", dest="verbose", action="store_true"),
809 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
810 help="Import into refs/heads/ , not refs/remotes"),
811 optparse.make_option("--max-changes", dest="maxChanges"),
812 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
813 help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
815 self.description = """Imports from Perforce into a git repository.\n
817 //depot/my/project/ -- to import the current head
818 //depot/my/project/@all -- to import everything
819 //depot/my/project/@1,6 -- to import only from revision 1 to 6
821 (a ... is not needed in the path p4 specification, it's added implicitly)"""
823 self.usage += " //depot/path[@revRange]"
825 self.createdBranches = Set()
826 self.committedChanges = Set()
828 self.detectBranches = False
829 self.detectLabels = False
830 self.changesFile = ""
831 self.syncWithOrigin = True
833 self.importIntoRemotes = True
835 self.isWindows = (platform.system() == "Windows")
836 self.keepRepoPath = False
837 self.depotPaths = None
838 self.p4BranchesInGit = []
839 self.cloneExclude = []
841 if gitConfig("git-p4.syncFromOrigin") == "false":
842 self.syncWithOrigin = False
844 def extractFilesFromCommit(self, commit):
845 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
846 for path in self.cloneExclude]
849 while commit.has_key("depotFile%s" % fnum):
850 path = commit["depotFile%s" % fnum]
852 if [p for p in self.cloneExclude
853 if path.startswith (p)]:
856 found = [p for p in self.depotPaths
857 if path.startswith (p)]
864 file["rev"] = commit["rev%s" % fnum]
865 file["action"] = commit["action%s" % fnum]
866 file["type"] = commit["type%s" % fnum]
871 def stripRepoPath(self, path, prefixes):
872 if self.keepRepoPath:
873 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
876 if path.startswith(p):
881 def splitFilesIntoBranches(self, commit):
884 while commit.has_key("depotFile%s" % fnum):
885 path = commit["depotFile%s" % fnum]
886 found = [p for p in self.depotPaths
887 if path.startswith (p)]
894 file["rev"] = commit["rev%s" % fnum]
895 file["action"] = commit["action%s" % fnum]
896 file["type"] = commit["type%s" % fnum]
899 relPath = self.stripRepoPath(path, self.depotPaths)
901 for branch in self.knownBranches.keys():
903 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
904 if relPath.startswith(branch + "/"):
905 if branch not in branches:
906 branches[branch] = []
907 branches[branch].append(file)
912 ## Should move this out, doesn't use SELF.
913 def readP4Files(self, files):
914 files = [f for f in files
915 if f['action'] != 'delete']
920 filedata = p4CmdList('-x - print',
921 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
924 if "p4ExitCode" in filedata[0]:
925 die("Problems executing p4. Error: [%d]."
926 % (filedata[0]['p4ExitCode']));
930 while j < len(filedata):
934 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
935 tmp = filedata[j]['data']
936 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
937 tmp = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', tmp)
938 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
939 tmp = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', tmp)
944 if not stat.has_key('depotFile'):
945 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
948 contents[stat['depotFile']] = text
951 assert not f.has_key('data')
952 f['data'] = contents[f['path']]
954 def commit(self, details, files, branch, branchPrefixes, parent = ""):
955 epoch = details["time"]
956 author = details["user"]
959 print "commit into %s" % branch
961 # start with reading files; if that fails, we should not
965 if [p for p in branchPrefixes if f['path'].startswith(p)]:
968 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
970 self.readP4Files(files)
975 self.gitStream.write("commit %s\n" % branch)
976 # gitStream.write("mark :%s\n" % details["change"])
977 self.committedChanges.add(int(details["change"]))
979 if author not in self.users:
980 self.getUserMapFromPerforceServer()
981 if author in self.users:
982 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
984 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
986 self.gitStream.write("committer %s\n" % committer)
988 self.gitStream.write("data <<EOT\n")
989 self.gitStream.write(details["desc"])
990 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
991 % (','.join (branchPrefixes), details["change"]))
992 if len(details['options']) > 0:
993 self.gitStream.write(": options = %s" % details['options'])
994 self.gitStream.write("]\nEOT\n\n")
998 print "parent %s" % parent
999 self.gitStream.write("from %s\n" % parent)
1002 if file["type"] == "apple":
1003 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
1006 relPath = self.stripRepoPath(file['path'], branchPrefixes)
1007 if file["action"] == "delete":
1008 self.gitStream.write("D %s\n" % relPath)
1013 if isP4Exec(file["type"]):
1015 elif file["type"] == "symlink":
1017 # p4 print on a symlink contains "target\n", so strip it off
1020 if self.isWindows and file["type"].endswith("text"):
1021 data = data.replace("\r\n", "\n")
1023 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1024 self.gitStream.write("data %s\n" % len(data))
1025 self.gitStream.write(data)
1026 self.gitStream.write("\n")
1028 self.gitStream.write("\n")
1030 change = int(details["change"])
1032 if self.labels.has_key(change):
1033 label = self.labels[change]
1034 labelDetails = label[0]
1035 labelRevisions = label[1]
1037 print "Change %s is labelled %s" % (change, labelDetails)
1039 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1040 for p in branchPrefixes]))
1042 if len(files) == len(labelRevisions):
1046 if info["action"] == "delete":
1048 cleanedFiles[info["depotFile"]] = info["rev"]
1050 if cleanedFiles == labelRevisions:
1051 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1052 self.gitStream.write("from %s\n" % branch)
1054 owner = labelDetails["Owner"]
1056 if author in self.users:
1057 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1059 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1060 self.gitStream.write("tagger %s\n" % tagger)
1061 self.gitStream.write("data <<EOT\n")
1062 self.gitStream.write(labelDetails["Description"])
1063 self.gitStream.write("EOT\n\n")
1067 print ("Tag %s does not match with change %s: files do not match."
1068 % (labelDetails["label"], change))
1072 print ("Tag %s does not match with change %s: file count is different."
1073 % (labelDetails["label"], change))
1075 def getUserCacheFilename(self):
1076 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1077 return home + "/.gitp4-usercache.txt"
1079 def getUserMapFromPerforceServer(self):
1080 if self.userMapFromPerforceServer:
1084 for output in p4CmdList("users"):
1085 if not output.has_key("User"):
1087 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1091 for (key, val) in self.users.items():
1092 s += "%s\t%s\n" % (key, val)
1094 open(self.getUserCacheFilename(), "wb").write(s)
1095 self.userMapFromPerforceServer = True
1097 def loadUserMapFromCache(self):
1099 self.userMapFromPerforceServer = False
1101 cache = open(self.getUserCacheFilename(), "rb")
1102 lines = cache.readlines()
1105 entry = line.strip().split("\t")
1106 self.users[entry[0]] = entry[1]
1108 self.getUserMapFromPerforceServer()
1110 def getLabels(self):
1113 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1114 if len(l) > 0 and not self.silent:
1115 print "Finding files belonging to labels in %s" % `self.depotPaths`
1118 label = output["label"]
1122 print "Querying files for label %s" % label
1123 for file in p4CmdList("files "
1124 + ' '.join (["%s...@%s" % (p, label)
1125 for p in self.depotPaths])):
1126 revisions[file["depotFile"]] = file["rev"]
1127 change = int(file["change"])
1128 if change > newestChange:
1129 newestChange = change
1131 self.labels[newestChange] = [output, revisions]
1134 print "Label changes: %s" % self.labels.keys()
1136 def guessProjectName(self):
1137 for p in self.depotPaths:
1140 p = p[p.strip().rfind("/") + 1:]
1141 if not p.endswith("/"):
1145 def getBranchMapping(self):
1146 lostAndFoundBranches = set()
1148 for info in p4CmdList("branches"):
1149 details = p4Cmd("branch -o %s" % info["branch"])
1151 while details.has_key("View%s" % viewIdx):
1152 paths = details["View%s" % viewIdx].split(" ")
1153 viewIdx = viewIdx + 1
1154 # require standard //depot/foo/... //depot/bar/... mapping
1155 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1158 destination = paths[1]
1160 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1161 source = source[len(self.depotPaths[0]):-4]
1162 destination = destination[len(self.depotPaths[0]):-4]
1164 if destination in self.knownBranches:
1166 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1167 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1170 self.knownBranches[destination] = source
1172 lostAndFoundBranches.discard(destination)
1174 if source not in self.knownBranches:
1175 lostAndFoundBranches.add(source)
1178 for branch in lostAndFoundBranches:
1179 self.knownBranches[branch] = branch
1181 def getBranchMappingFromGitBranches(self):
1182 branches = p4BranchesInGit(self.importIntoRemotes)
1183 for branch in branches.keys():
1184 if branch == "master":
1187 branch = branch[len(self.projectName):]
1188 self.knownBranches[branch] = branch
1190 def listExistingP4GitBranches(self):
1191 # branches holds mapping from name to commit
1192 branches = p4BranchesInGit(self.importIntoRemotes)
1193 self.p4BranchesInGit = branches.keys()
1194 for branch in branches.keys():
1195 self.initialParents[self.refPrefix + branch] = branches[branch]
1197 def updateOptionDict(self, d):
1199 if self.keepRepoPath:
1200 option_keys['keepRepoPath'] = 1
1202 d["options"] = ' '.join(sorted(option_keys.keys()))
1204 def readOptions(self, d):
1205 self.keepRepoPath = (d.has_key('options')
1206 and ('keepRepoPath' in d['options']))
1208 def gitRefForBranch(self, branch):
1209 if branch == "main":
1210 return self.refPrefix + "master"
1212 if len(branch) <= 0:
1215 return self.refPrefix + self.projectName + branch
1217 def gitCommitByP4Change(self, ref, change):
1219 print "looking in ref " + ref + " for change %s using bisect..." % change
1222 latestCommit = parseRevision(ref)
1226 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1227 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1232 log = extractLogMessageFromGitCommit(next)
1233 settings = extractSettingsGitLog(log)
1234 currentChange = int(settings['change'])
1236 print "current change %s" % currentChange
1238 if currentChange == change:
1240 print "found %s" % next
1243 if currentChange < change:
1244 earliestCommit = "^%s" % next
1246 latestCommit = "%s" % next
1250 def importNewBranch(self, branch, maxChange):
1251 # make fast-import flush all changes to disk and update the refs using the checkpoint
1252 # command so that we can try to find the branch parent in the git history
1253 self.gitStream.write("checkpoint\n\n");
1254 self.gitStream.flush();
1255 branchPrefix = self.depotPaths[0] + branch + "/"
1256 range = "@1,%s" % maxChange
1257 #print "prefix" + branchPrefix
1258 changes = p4ChangesForPaths([branchPrefix], range)
1259 if len(changes) <= 0:
1261 firstChange = changes[0]
1262 #print "first change in branch: %s" % firstChange
1263 sourceBranch = self.knownBranches[branch]
1264 sourceDepotPath = self.depotPaths[0] + sourceBranch
1265 sourceRef = self.gitRefForBranch(sourceBranch)
1266 #print "source " + sourceBranch
1268 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1269 #print "branch parent: %s" % branchParentChange
1270 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1271 if len(gitParent) > 0:
1272 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1273 #print "parent git commit: %s" % gitParent
1275 self.importChanges(changes)
1278 def importChanges(self, changes):
1280 for change in changes:
1281 description = p4Cmd("describe %s" % change)
1282 self.updateOptionDict(description)
1285 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1290 if self.detectBranches:
1291 branches = self.splitFilesIntoBranches(description)
1292 for branch in branches.keys():
1294 branchPrefix = self.depotPaths[0] + branch + "/"
1298 filesForCommit = branches[branch]
1301 print "branch is %s" % branch
1303 self.updatedBranches.add(branch)
1305 if branch not in self.createdBranches:
1306 self.createdBranches.add(branch)
1307 parent = self.knownBranches[branch]
1308 if parent == branch:
1311 fullBranch = self.projectName + branch
1312 if fullBranch not in self.p4BranchesInGit:
1314 print("\n Importing new branch %s" % fullBranch);
1315 if self.importNewBranch(branch, change - 1):
1317 self.p4BranchesInGit.append(fullBranch)
1319 print("\n Resuming with change %s" % change);
1322 print "parent determined through known branches: %s" % parent
1324 branch = self.gitRefForBranch(branch)
1325 parent = self.gitRefForBranch(parent)
1328 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1330 if len(parent) == 0 and branch in self.initialParents:
1331 parent = self.initialParents[branch]
1332 del self.initialParents[branch]
1334 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1336 files = self.extractFilesFromCommit(description)
1337 self.commit(description, files, self.branch, self.depotPaths,
1339 self.initialParent = ""
1341 print self.gitError.read()
1344 def importHeadRevision(self, revision):
1345 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1347 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1348 details["desc"] = ("Initial import of %s from the state at revision %s"
1349 % (' '.join(self.depotPaths), revision))
1350 details["change"] = revision
1354 for info in p4CmdList("files "
1355 + ' '.join(["%s...%s"
1357 for p in self.depotPaths])):
1359 if info['code'] == 'error':
1360 sys.stderr.write("p4 returned an error: %s\n"
1365 change = int(info["change"])
1366 if change > newestRevision:
1367 newestRevision = change
1369 if info["action"] == "delete":
1370 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1371 #fileCnt = fileCnt + 1
1374 for prop in ["depotFile", "rev", "action", "type" ]:
1375 details["%s%s" % (prop, fileCnt)] = info[prop]
1377 fileCnt = fileCnt + 1
1379 details["change"] = newestRevision
1380 self.updateOptionDict(details)
1382 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1384 print "IO error with git fast-import. Is your git version recent enough?"
1385 print self.gitError.read()
1388 def run(self, args):
1389 self.depotPaths = []
1390 self.changeRange = ""
1391 self.initialParent = ""
1392 self.previousDepotPaths = []
1394 # map from branch depot path to parent branch
1395 self.knownBranches = {}
1396 self.initialParents = {}
1397 self.hasOrigin = originP4BranchesExist()
1398 if not self.syncWithOrigin:
1399 self.hasOrigin = False
1401 if self.importIntoRemotes:
1402 self.refPrefix = "refs/remotes/p4/"
1404 self.refPrefix = "refs/heads/p4/"
1406 if self.syncWithOrigin and self.hasOrigin:
1408 print "Syncing with origin first by calling git fetch origin"
1409 system("git fetch origin")
1411 if len(self.branch) == 0:
1412 self.branch = self.refPrefix + "master"
1413 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1414 system("git update-ref %s refs/heads/p4" % self.branch)
1415 system("git branch -D p4");
1416 # create it /after/ importing, when master exists
1417 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1418 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1420 # TODO: should always look at previous commits,
1421 # merge with previous imports, if possible.
1424 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1425 self.listExistingP4GitBranches()
1427 if len(self.p4BranchesInGit) > 1:
1429 print "Importing from/into multiple branches"
1430 self.detectBranches = True
1433 print "branches: %s" % self.p4BranchesInGit
1436 for branch in self.p4BranchesInGit:
1437 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1439 settings = extractSettingsGitLog(logMsg)
1441 self.readOptions(settings)
1442 if (settings.has_key('depot-paths')
1443 and settings.has_key ('change')):
1444 change = int(settings['change']) + 1
1445 p4Change = max(p4Change, change)
1447 depotPaths = sorted(settings['depot-paths'])
1448 if self.previousDepotPaths == []:
1449 self.previousDepotPaths = depotPaths
1452 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1453 for i in range(0, min(len(cur), len(prev))):
1454 if cur[i] <> prev[i]:
1458 paths.append (cur[:i + 1])
1460 self.previousDepotPaths = paths
1463 self.depotPaths = sorted(self.previousDepotPaths)
1464 self.changeRange = "@%s,#head" % p4Change
1465 if not self.detectBranches:
1466 self.initialParent = parseRevision(self.branch)
1467 if not self.silent and not self.detectBranches:
1468 print "Performing incremental import into %s git branch" % self.branch
1470 if not self.branch.startswith("refs/"):
1471 self.branch = "refs/heads/" + self.branch
1473 if len(args) == 0 and self.depotPaths:
1475 print "Depot paths: %s" % ' '.join(self.depotPaths)
1477 if self.depotPaths and self.depotPaths != args:
1478 print ("previous import used depot path %s and now %s was specified. "
1479 "This doesn't work!" % (' '.join (self.depotPaths),
1483 self.depotPaths = sorted(args)
1489 for p in self.depotPaths:
1490 if p.find("@") != -1:
1491 atIdx = p.index("@")
1492 self.changeRange = p[atIdx:]
1493 if self.changeRange == "@all":
1494 self.changeRange = ""
1495 elif ',' not in self.changeRange:
1496 revision = self.changeRange
1497 self.changeRange = ""
1499 elif p.find("#") != -1:
1500 hashIdx = p.index("#")
1501 revision = p[hashIdx:]
1503 elif self.previousDepotPaths == []:
1506 p = re.sub ("\.\.\.$", "", p)
1507 if not p.endswith("/"):
1512 self.depotPaths = newPaths
1515 self.loadUserMapFromCache()
1517 if self.detectLabels:
1520 if self.detectBranches:
1521 ## FIXME - what's a P4 projectName ?
1522 self.projectName = self.guessProjectName()
1525 self.getBranchMappingFromGitBranches()
1527 self.getBranchMapping()
1529 print "p4-git branches: %s" % self.p4BranchesInGit
1530 print "initial parents: %s" % self.initialParents
1531 for b in self.p4BranchesInGit:
1535 b = b[len(self.projectName):]
1536 self.createdBranches.add(b)
1538 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1540 importProcess = subprocess.Popen(["git", "fast-import"],
1541 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1542 stderr=subprocess.PIPE);
1543 self.gitOutput = importProcess.stdout
1544 self.gitStream = importProcess.stdin
1545 self.gitError = importProcess.stderr
1548 self.importHeadRevision(revision)
1552 if len(self.changesFile) > 0:
1553 output = open(self.changesFile).readlines()
1556 changeSet.add(int(line))
1558 for change in changeSet:
1559 changes.append(change)
1564 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1566 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1568 if len(self.maxChanges) > 0:
1569 changes = changes[:min(int(self.maxChanges), len(changes))]
1571 if len(changes) == 0:
1573 print "No changes to import!"
1576 if not self.silent and not self.detectBranches:
1577 print "Import destination: %s" % self.branch
1579 self.updatedBranches = set()
1581 self.importChanges(changes)
1585 if len(self.updatedBranches) > 0:
1586 sys.stdout.write("Updated branches: ")
1587 for b in self.updatedBranches:
1588 sys.stdout.write("%s " % b)
1589 sys.stdout.write("\n")
1591 self.gitStream.close()
1592 if importProcess.wait() != 0:
1593 die("fast-import failed: %s" % self.gitError.read())
1594 self.gitOutput.close()
1595 self.gitError.close()
1599 class P4Rebase(Command):
1601 Command.__init__(self)
1603 self.description = ("Fetches the latest revision from perforce and "
1604 + "rebases the current work (branch) against it")
1605 self.verbose = False
1607 def run(self, args):
1611 return self.rebase()
1614 if os.system("git update-index --refresh") != 0:
1615 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.");
1616 if len(read_pipe("git diff-index HEAD --")) > 0:
1617 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1619 [upstream, settings] = findUpstreamBranchPoint()
1620 if len(upstream) == 0:
1621 die("Cannot find upstream branchpoint for rebase")
1623 # the branchpoint may be p4/foo~3, so strip off the parent
1624 upstream = re.sub("~[0-9]+$", "", upstream)
1626 print "Rebasing the current branch onto %s" % upstream
1627 oldHead = read_pipe("git rev-parse HEAD").strip()
1628 system("git rebase %s" % upstream)
1629 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1632 class P4Clone(P4Sync):
1634 P4Sync.__init__(self)
1635 self.description = "Creates a new git repository and imports from Perforce into it"
1636 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1638 optparse.make_option("--destination", dest="cloneDestination",
1639 action='store', default=None,
1640 help="where to leave result of the clone"),
1641 optparse.make_option("-/", dest="cloneExclude",
1642 action="append", type="string",
1643 help="exclude depot path")
1645 self.cloneDestination = None
1646 self.needsGit = False
1648 # This is required for the "append" cloneExclude action
1649 def ensure_value(self, attr, value):
1650 if not hasattr(self, attr) or getattr(self, attr) is None:
1651 setattr(self, attr, value)
1652 return getattr(self, attr)
1654 def defaultDestination(self, args):
1655 ## TODO: use common prefix of args?
1657 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1658 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1659 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1660 depotDir = re.sub(r"/$", "", depotDir)
1661 return os.path.split(depotDir)[1]
1663 def run(self, args):
1667 if self.keepRepoPath and not self.cloneDestination:
1668 sys.stderr.write("Must specify destination for --keep-path\n")
1673 if not self.cloneDestination and len(depotPaths) > 1:
1674 self.cloneDestination = depotPaths[-1]
1675 depotPaths = depotPaths[:-1]
1677 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1678 for p in depotPaths:
1679 if not p.startswith("//"):
1682 if not self.cloneDestination:
1683 self.cloneDestination = self.defaultDestination(args)
1685 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1686 if not os.path.exists(self.cloneDestination):
1687 os.makedirs(self.cloneDestination)
1688 os.chdir(self.cloneDestination)
1690 self.gitdir = os.getcwd() + "/.git"
1691 if not P4Sync.run(self, depotPaths):
1693 if self.branch != "master":
1694 if gitBranchExists("refs/remotes/p4/master"):
1695 system("git branch master refs/remotes/p4/master")
1696 system("git checkout -f")
1698 print "Could not detect main branch. No checkout/master branch created."
1702 class P4Branches(Command):
1704 Command.__init__(self)
1706 self.description = ("Shows the git branches that hold imports and their "
1707 + "corresponding perforce depot paths")
1708 self.verbose = False
1710 def run(self, args):
1711 if originP4BranchesExist():
1712 createOrUpdateBranchesFromOrigin()
1714 cmdline = "git rev-parse --symbolic "
1715 cmdline += " --remotes"
1717 for line in read_pipe_lines(cmdline):
1720 if not line.startswith('p4/') or line == "p4/HEAD":
1724 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1725 settings = extractSettingsGitLog(log)
1727 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1730 class HelpFormatter(optparse.IndentedHelpFormatter):
1732 optparse.IndentedHelpFormatter.__init__(self)
1734 def format_description(self, description):
1736 return description + "\n"
1740 def printUsage(commands):
1741 print "usage: %s <command> [options]" % sys.argv[0]
1743 print "valid commands: %s" % ", ".join(commands)
1745 print "Try %s <command> --help for command specific help." % sys.argv[0]
1750 "submit" : P4Submit,
1751 "commit" : P4Submit,
1753 "rebase" : P4Rebase,
1755 "rollback" : P4RollBack,
1756 "branches" : P4Branches
1761 if len(sys.argv[1:]) == 0:
1762 printUsage(commands.keys())
1766 cmdName = sys.argv[1]
1768 klass = commands[cmdName]
1771 print "unknown command %s" % cmdName
1773 printUsage(commands.keys())
1776 options = cmd.options
1777 cmd.gitdir = os.environ.get("GIT_DIR", None)
1781 if len(options) > 0:
1782 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1784 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1786 description = cmd.description,
1787 formatter = HelpFormatter())
1789 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1791 verbose = cmd.verbose
1793 if cmd.gitdir == None:
1794 cmd.gitdir = os.path.abspath(".git")
1795 if not isValidGitDir(cmd.gitdir):
1796 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1797 if os.path.exists(cmd.gitdir):
1798 cdup = read_pipe("git rev-parse --show-cdup").strip()
1802 if not isValidGitDir(cmd.gitdir):
1803 if isValidGitDir(cmd.gitdir + "/.git"):
1804 cmd.gitdir += "/.git"
1806 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1808 os.environ["GIT_DIR"] = cmd.gitdir
1810 if not cmd.run(args):
1814 if __name__ == '__main__':