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("-M", dest="detectRename", action="store_true"),
473 self.description = "Submit changes from git to the perforce depot."
474 self.usage += " [name of git branch to submit into perforce depot]"
475 self.firstTime = True
477 self.interactive = True
478 self.firstTime = True
480 self.detectRename = False
482 self.isWindows = (platform.system() == "Windows")
485 if len(p4CmdList("opened ...")) > 0:
486 die("You have files opened with perforce! Close them before starting the sync.")
489 if len(self.config) > 0 and not self.reset:
490 die("Cannot start sync. Previous sync config found at %s\n"
491 "If you want to start submitting again from scratch "
492 "maybe you want to call git-p4 submit --reset" % self.configFile)
495 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
496 commits.append(line.strip())
499 self.config["commits"] = commits
501 # replaces everything between 'Description:' and the next P4 submit template field with the
503 def prepareLogMessage(self, template, message):
506 inDescriptionSection = False
508 for line in template.split("\n"):
509 if line.startswith("#"):
510 result += line + "\n"
513 if inDescriptionSection:
514 if line.startswith("Files:"):
515 inDescriptionSection = False
519 if line.startswith("Description:"):
520 inDescriptionSection = True
522 for messageLine in message.split("\n"):
523 line += "\t" + messageLine + "\n"
525 result += line + "\n"
529 def prepareSubmitTemplate(self):
530 # remove lines in the Files section that show changes to files outside the depot path we're committing into
532 inFilesSection = False
533 for line in read_pipe_lines("p4 change -o"):
535 if line.startswith("\t"):
536 # path starts and ends with a tab
538 lastTab = path.rfind("\t")
540 path = path[:lastTab]
541 if not path.startswith(self.depotPath):
544 inFilesSection = False
546 if line.startswith("Files:"):
547 inFilesSection = True
553 def applyCommit(self, id):
554 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
555 diffOpts = ("", "-M")[self.detectRename]
556 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
558 filesToDelete = set()
560 filesToChangeExecBit = {}
562 diff = parseDiffTreeEntry(line)
563 modifier = diff['status']
566 system("p4 edit \"%s\"" % path)
567 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
568 filesToChangeExecBit[path] = diff['dst_mode']
569 editedFiles.add(path)
570 elif modifier == "A":
572 filesToChangeExecBit[path] = diff['dst_mode']
573 if path in filesToDelete:
574 filesToDelete.remove(path)
575 elif modifier == "D":
576 filesToDelete.add(path)
577 if path in filesToAdd:
578 filesToAdd.remove(path)
579 elif modifier == "R":
580 src, dest = diff['src'], diff['dst']
581 system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
582 system("p4 edit \"%s\"" % (dest))
583 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
584 filesToChangeExecBit[dest] = diff['dst_mode']
586 editedFiles.add(dest)
587 filesToDelete.add(src)
589 die("unknown modifier %s for %s" % (modifier, path))
591 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
592 patchcmd = diffcmd + " | git apply "
593 tryPatchCmd = patchcmd + "--check -"
594 applyPatchCmd = patchcmd + "--check --apply -"
596 if os.system(tryPatchCmd) != 0:
597 print "Unfortunately applying the change failed!"
598 print "What do you want to do?"
600 while response != "s" and response != "a" and response != "w":
601 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
602 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
604 print "Skipping! Good luck with the next patches..."
605 for f in editedFiles:
606 system("p4 revert \"%s\"" % f);
610 elif response == "a":
611 os.system(applyPatchCmd)
612 if len(filesToAdd) > 0:
613 print "You may also want to call p4 add on the following files:"
614 print " ".join(filesToAdd)
615 if len(filesToDelete):
616 print "The following files should be scheduled for deletion with p4 delete:"
617 print " ".join(filesToDelete)
618 die("Please resolve and submit the conflict manually and "
619 + "continue afterwards with git-p4 submit --continue")
620 elif response == "w":
621 system(diffcmd + " > patch.txt")
622 print "Patch saved to patch.txt in %s !" % self.clientPath
623 die("Please resolve and submit the conflict manually and "
624 "continue afterwards with git-p4 submit --continue")
626 system(applyPatchCmd)
629 system("p4 add \"%s\"" % f)
630 for f in filesToDelete:
631 system("p4 revert \"%s\"" % f)
632 system("p4 delete \"%s\"" % f)
634 # Set/clear executable bits
635 for f in filesToChangeExecBit.keys():
636 mode = filesToChangeExecBit[f]
637 setP4ExecBit(f, mode)
639 logMessage = extractLogMessageFromGitCommit(id)
641 logMessage = logMessage.replace("\n", "\r\n")
642 logMessage = logMessage.strip()
644 template = self.prepareSubmitTemplate()
647 submitTemplate = self.prepareLogMessage(template, logMessage)
648 diff = read_pipe("p4 diff -du ...")
650 for newFile in filesToAdd:
651 diff += "==== new file ====\n"
652 diff += "--- /dev/null\n"
653 diff += "+++ %s\n" % newFile
654 f = open(newFile, "r")
655 for line in f.readlines():
659 separatorLine = "######## everything below this line is just the diff #######"
660 if platform.system() == "Windows":
661 separatorLine += "\r"
662 separatorLine += "\n"
664 [handle, fileName] = tempfile.mkstemp()
665 tmpFile = os.fdopen(handle, "w+")
666 tmpFile.write(submitTemplate + separatorLine + diff)
669 if platform.system() == "Windows":
670 defaultEditor = "notepad"
671 editor = os.environ.get("EDITOR", defaultEditor);
672 system(editor + " " + fileName)
673 tmpFile = open(fileName, "rb")
674 message = tmpFile.read()
677 submitTemplate = message[:message.index(separatorLine)]
679 submitTemplate = submitTemplate.replace("\r\n", "\n")
681 write_pipe("p4 submit -i", submitTemplate)
683 fileName = "submit.txt"
684 file = open(fileName, "w+")
685 file.write(self.prepareLogMessage(template, logMessage))
687 print ("Perforce submit template written as %s. "
688 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
689 % (fileName, fileName))
693 self.master = currentGitBranch()
694 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
695 die("Detecting current git branch failed!")
697 self.master = args[0]
701 [upstream, settings] = findUpstreamBranchPoint()
702 self.depotPath = settings['depot-paths'][0]
703 if len(self.origin) == 0:
704 self.origin = upstream
707 print "Origin branch is " + self.origin
709 if len(self.depotPath) == 0:
710 print "Internal error: cannot locate perforce depot path from existing branches"
713 self.clientPath = p4Where(self.depotPath)
715 if len(self.clientPath) == 0:
716 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
719 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
720 self.oldWorkingDirectory = os.getcwd()
722 os.chdir(self.clientPath)
723 print "Syncronizing p4 checkout..."
724 system("p4 sync ...")
727 self.firstTime = True
730 self.configFile = self.gitdir + "/p4-git-sync.cfg"
731 self.config = shelve.open(self.configFile, writeback=True)
736 commits = self.config.get("commits", [])
738 while len(commits) > 0:
739 self.firstTime = False
741 commits = commits[1:]
742 self.config["commits"] = commits
743 self.applyCommit(commit)
744 if not self.interactive:
749 if len(commits) == 0:
751 print "No changes found to apply between %s and current HEAD" % self.origin
753 print "All changes applied!"
754 os.chdir(self.oldWorkingDirectory)
761 os.remove(self.configFile)
765 class P4Sync(Command):
767 Command.__init__(self)
769 optparse.make_option("--branch", dest="branch"),
770 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
771 optparse.make_option("--changesfile", dest="changesFile"),
772 optparse.make_option("--silent", dest="silent", action="store_true"),
773 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
774 optparse.make_option("--verbose", dest="verbose", action="store_true"),
775 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
776 help="Import into refs/heads/ , not refs/remotes"),
777 optparse.make_option("--max-changes", dest="maxChanges"),
778 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
779 help="Keep entire BRANCH/DIR/SUBDIR prefix during import")
781 self.description = """Imports from Perforce into a git repository.\n
783 //depot/my/project/ -- to import the current head
784 //depot/my/project/@all -- to import everything
785 //depot/my/project/@1,6 -- to import only from revision 1 to 6
787 (a ... is not needed in the path p4 specification, it's added implicitly)"""
789 self.usage += " //depot/path[@revRange]"
791 self.createdBranches = Set()
792 self.committedChanges = Set()
794 self.detectBranches = False
795 self.detectLabels = False
796 self.changesFile = ""
797 self.syncWithOrigin = True
799 self.importIntoRemotes = True
801 self.isWindows = (platform.system() == "Windows")
802 self.keepRepoPath = False
803 self.depotPaths = None
804 self.p4BranchesInGit = []
805 self.cloneExclude = []
807 if gitConfig("git-p4.syncFromOrigin") == "false":
808 self.syncWithOrigin = False
810 def extractFilesFromCommit(self, commit):
811 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
812 for path in self.cloneExclude]
815 while commit.has_key("depotFile%s" % fnum):
816 path = commit["depotFile%s" % fnum]
818 if [p for p in self.cloneExclude
819 if path.startswith (p)]:
822 found = [p for p in self.depotPaths
823 if path.startswith (p)]
830 file["rev"] = commit["rev%s" % fnum]
831 file["action"] = commit["action%s" % fnum]
832 file["type"] = commit["type%s" % fnum]
837 def stripRepoPath(self, path, prefixes):
838 if self.keepRepoPath:
839 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
842 if path.startswith(p):
847 def splitFilesIntoBranches(self, commit):
850 while commit.has_key("depotFile%s" % fnum):
851 path = commit["depotFile%s" % fnum]
852 found = [p for p in self.depotPaths
853 if path.startswith (p)]
860 file["rev"] = commit["rev%s" % fnum]
861 file["action"] = commit["action%s" % fnum]
862 file["type"] = commit["type%s" % fnum]
865 relPath = self.stripRepoPath(path, self.depotPaths)
867 for branch in self.knownBranches.keys():
869 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
870 if relPath.startswith(branch + "/"):
871 if branch not in branches:
872 branches[branch] = []
873 branches[branch].append(file)
878 ## Should move this out, doesn't use SELF.
879 def readP4Files(self, files):
880 files = [f for f in files
881 if f['action'] != 'delete']
886 filedata = p4CmdList('-x - print',
887 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
890 if "p4ExitCode" in filedata[0]:
891 die("Problems executing p4. Error: [%d]."
892 % (filedata[0]['p4ExitCode']));
896 while j < len(filedata):
900 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
901 tmp = filedata[j]['data']
902 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
903 tmp = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', tmp)
904 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
905 tmp = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', tmp)
910 if not stat.has_key('depotFile'):
911 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
914 contents[stat['depotFile']] = text
917 assert not f.has_key('data')
918 f['data'] = contents[f['path']]
920 def commit(self, details, files, branch, branchPrefixes, parent = ""):
921 epoch = details["time"]
922 author = details["user"]
925 print "commit into %s" % branch
927 # start with reading files; if that fails, we should not
931 if [p for p in branchPrefixes if f['path'].startswith(p)]:
934 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
936 self.readP4Files(files)
941 self.gitStream.write("commit %s\n" % branch)
942 # gitStream.write("mark :%s\n" % details["change"])
943 self.committedChanges.add(int(details["change"]))
945 if author not in self.users:
946 self.getUserMapFromPerforceServer()
947 if author in self.users:
948 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
950 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
952 self.gitStream.write("committer %s\n" % committer)
954 self.gitStream.write("data <<EOT\n")
955 self.gitStream.write(details["desc"])
956 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
957 % (','.join (branchPrefixes), details["change"]))
958 if len(details['options']) > 0:
959 self.gitStream.write(": options = %s" % details['options'])
960 self.gitStream.write("]\nEOT\n\n")
964 print "parent %s" % parent
965 self.gitStream.write("from %s\n" % parent)
968 if file["type"] == "apple":
969 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
972 relPath = self.stripRepoPath(file['path'], branchPrefixes)
973 if file["action"] == "delete":
974 self.gitStream.write("D %s\n" % relPath)
979 if isP4Exec(file["type"]):
981 elif file["type"] == "symlink":
983 # p4 print on a symlink contains "target\n", so strip it off
986 if self.isWindows and file["type"].endswith("text"):
987 data = data.replace("\r\n", "\n")
989 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
990 self.gitStream.write("data %s\n" % len(data))
991 self.gitStream.write(data)
992 self.gitStream.write("\n")
994 self.gitStream.write("\n")
996 change = int(details["change"])
998 if self.labels.has_key(change):
999 label = self.labels[change]
1000 labelDetails = label[0]
1001 labelRevisions = label[1]
1003 print "Change %s is labelled %s" % (change, labelDetails)
1005 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1006 for p in branchPrefixes]))
1008 if len(files) == len(labelRevisions):
1012 if info["action"] == "delete":
1014 cleanedFiles[info["depotFile"]] = info["rev"]
1016 if cleanedFiles == labelRevisions:
1017 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1018 self.gitStream.write("from %s\n" % branch)
1020 owner = labelDetails["Owner"]
1022 if author in self.users:
1023 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1025 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1026 self.gitStream.write("tagger %s\n" % tagger)
1027 self.gitStream.write("data <<EOT\n")
1028 self.gitStream.write(labelDetails["Description"])
1029 self.gitStream.write("EOT\n\n")
1033 print ("Tag %s does not match with change %s: files do not match."
1034 % (labelDetails["label"], change))
1038 print ("Tag %s does not match with change %s: file count is different."
1039 % (labelDetails["label"], change))
1041 def getUserCacheFilename(self):
1042 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1043 return home + "/.gitp4-usercache.txt"
1045 def getUserMapFromPerforceServer(self):
1046 if self.userMapFromPerforceServer:
1050 for output in p4CmdList("users"):
1051 if not output.has_key("User"):
1053 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1057 for (key, val) in self.users.items():
1058 s += "%s\t%s\n" % (key, val)
1060 open(self.getUserCacheFilename(), "wb").write(s)
1061 self.userMapFromPerforceServer = True
1063 def loadUserMapFromCache(self):
1065 self.userMapFromPerforceServer = False
1067 cache = open(self.getUserCacheFilename(), "rb")
1068 lines = cache.readlines()
1071 entry = line.strip().split("\t")
1072 self.users[entry[0]] = entry[1]
1074 self.getUserMapFromPerforceServer()
1076 def getLabels(self):
1079 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1080 if len(l) > 0 and not self.silent:
1081 print "Finding files belonging to labels in %s" % `self.depotPaths`
1084 label = output["label"]
1088 print "Querying files for label %s" % label
1089 for file in p4CmdList("files "
1090 + ' '.join (["%s...@%s" % (p, label)
1091 for p in self.depotPaths])):
1092 revisions[file["depotFile"]] = file["rev"]
1093 change = int(file["change"])
1094 if change > newestChange:
1095 newestChange = change
1097 self.labels[newestChange] = [output, revisions]
1100 print "Label changes: %s" % self.labels.keys()
1102 def guessProjectName(self):
1103 for p in self.depotPaths:
1106 p = p[p.strip().rfind("/") + 1:]
1107 if not p.endswith("/"):
1111 def getBranchMapping(self):
1112 lostAndFoundBranches = set()
1114 for info in p4CmdList("branches"):
1115 details = p4Cmd("branch -o %s" % info["branch"])
1117 while details.has_key("View%s" % viewIdx):
1118 paths = details["View%s" % viewIdx].split(" ")
1119 viewIdx = viewIdx + 1
1120 # require standard //depot/foo/... //depot/bar/... mapping
1121 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1124 destination = paths[1]
1126 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1127 source = source[len(self.depotPaths[0]):-4]
1128 destination = destination[len(self.depotPaths[0]):-4]
1130 if destination in self.knownBranches:
1132 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1133 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1136 self.knownBranches[destination] = source
1138 lostAndFoundBranches.discard(destination)
1140 if source not in self.knownBranches:
1141 lostAndFoundBranches.add(source)
1144 for branch in lostAndFoundBranches:
1145 self.knownBranches[branch] = branch
1147 def getBranchMappingFromGitBranches(self):
1148 branches = p4BranchesInGit(self.importIntoRemotes)
1149 for branch in branches.keys():
1150 if branch == "master":
1153 branch = branch[len(self.projectName):]
1154 self.knownBranches[branch] = branch
1156 def listExistingP4GitBranches(self):
1157 # branches holds mapping from name to commit
1158 branches = p4BranchesInGit(self.importIntoRemotes)
1159 self.p4BranchesInGit = branches.keys()
1160 for branch in branches.keys():
1161 self.initialParents[self.refPrefix + branch] = branches[branch]
1163 def updateOptionDict(self, d):
1165 if self.keepRepoPath:
1166 option_keys['keepRepoPath'] = 1
1168 d["options"] = ' '.join(sorted(option_keys.keys()))
1170 def readOptions(self, d):
1171 self.keepRepoPath = (d.has_key('options')
1172 and ('keepRepoPath' in d['options']))
1174 def gitRefForBranch(self, branch):
1175 if branch == "main":
1176 return self.refPrefix + "master"
1178 if len(branch) <= 0:
1181 return self.refPrefix + self.projectName + branch
1183 def gitCommitByP4Change(self, ref, change):
1185 print "looking in ref " + ref + " for change %s using bisect..." % change
1188 latestCommit = parseRevision(ref)
1192 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1193 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1198 log = extractLogMessageFromGitCommit(next)
1199 settings = extractSettingsGitLog(log)
1200 currentChange = int(settings['change'])
1202 print "current change %s" % currentChange
1204 if currentChange == change:
1206 print "found %s" % next
1209 if currentChange < change:
1210 earliestCommit = "^%s" % next
1212 latestCommit = "%s" % next
1216 def importNewBranch(self, branch, maxChange):
1217 # make fast-import flush all changes to disk and update the refs using the checkpoint
1218 # command so that we can try to find the branch parent in the git history
1219 self.gitStream.write("checkpoint\n\n");
1220 self.gitStream.flush();
1221 branchPrefix = self.depotPaths[0] + branch + "/"
1222 range = "@1,%s" % maxChange
1223 #print "prefix" + branchPrefix
1224 changes = p4ChangesForPaths([branchPrefix], range)
1225 if len(changes) <= 0:
1227 firstChange = changes[0]
1228 #print "first change in branch: %s" % firstChange
1229 sourceBranch = self.knownBranches[branch]
1230 sourceDepotPath = self.depotPaths[0] + sourceBranch
1231 sourceRef = self.gitRefForBranch(sourceBranch)
1232 #print "source " + sourceBranch
1234 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1235 #print "branch parent: %s" % branchParentChange
1236 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1237 if len(gitParent) > 0:
1238 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1239 #print "parent git commit: %s" % gitParent
1241 self.importChanges(changes)
1244 def importChanges(self, changes):
1246 for change in changes:
1247 description = p4Cmd("describe %s" % change)
1248 self.updateOptionDict(description)
1251 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1256 if self.detectBranches:
1257 branches = self.splitFilesIntoBranches(description)
1258 for branch in branches.keys():
1260 branchPrefix = self.depotPaths[0] + branch + "/"
1264 filesForCommit = branches[branch]
1267 print "branch is %s" % branch
1269 self.updatedBranches.add(branch)
1271 if branch not in self.createdBranches:
1272 self.createdBranches.add(branch)
1273 parent = self.knownBranches[branch]
1274 if parent == branch:
1277 fullBranch = self.projectName + branch
1278 if fullBranch not in self.p4BranchesInGit:
1280 print("\n Importing new branch %s" % fullBranch);
1281 if self.importNewBranch(branch, change - 1):
1283 self.p4BranchesInGit.append(fullBranch)
1285 print("\n Resuming with change %s" % change);
1288 print "parent determined through known branches: %s" % parent
1290 branch = self.gitRefForBranch(branch)
1291 parent = self.gitRefForBranch(parent)
1294 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1296 if len(parent) == 0 and branch in self.initialParents:
1297 parent = self.initialParents[branch]
1298 del self.initialParents[branch]
1300 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1302 files = self.extractFilesFromCommit(description)
1303 self.commit(description, files, self.branch, self.depotPaths,
1305 self.initialParent = ""
1307 print self.gitError.read()
1310 def importHeadRevision(self, revision):
1311 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1313 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1314 details["desc"] = ("Initial import of %s from the state at revision %s"
1315 % (' '.join(self.depotPaths), revision))
1316 details["change"] = revision
1320 for info in p4CmdList("files "
1321 + ' '.join(["%s...%s"
1323 for p in self.depotPaths])):
1325 if info['code'] == 'error':
1326 sys.stderr.write("p4 returned an error: %s\n"
1331 change = int(info["change"])
1332 if change > newestRevision:
1333 newestRevision = change
1335 if info["action"] == "delete":
1336 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1337 #fileCnt = fileCnt + 1
1340 for prop in ["depotFile", "rev", "action", "type" ]:
1341 details["%s%s" % (prop, fileCnt)] = info[prop]
1343 fileCnt = fileCnt + 1
1345 details["change"] = newestRevision
1346 self.updateOptionDict(details)
1348 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1350 print "IO error with git fast-import. Is your git version recent enough?"
1351 print self.gitError.read()
1354 def run(self, args):
1355 self.depotPaths = []
1356 self.changeRange = ""
1357 self.initialParent = ""
1358 self.previousDepotPaths = []
1360 # map from branch depot path to parent branch
1361 self.knownBranches = {}
1362 self.initialParents = {}
1363 self.hasOrigin = originP4BranchesExist()
1364 if not self.syncWithOrigin:
1365 self.hasOrigin = False
1367 if self.importIntoRemotes:
1368 self.refPrefix = "refs/remotes/p4/"
1370 self.refPrefix = "refs/heads/p4/"
1372 if self.syncWithOrigin and self.hasOrigin:
1374 print "Syncing with origin first by calling git fetch origin"
1375 system("git fetch origin")
1377 if len(self.branch) == 0:
1378 self.branch = self.refPrefix + "master"
1379 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1380 system("git update-ref %s refs/heads/p4" % self.branch)
1381 system("git branch -D p4");
1382 # create it /after/ importing, when master exists
1383 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1384 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1386 # TODO: should always look at previous commits,
1387 # merge with previous imports, if possible.
1390 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1391 self.listExistingP4GitBranches()
1393 if len(self.p4BranchesInGit) > 1:
1395 print "Importing from/into multiple branches"
1396 self.detectBranches = True
1399 print "branches: %s" % self.p4BranchesInGit
1402 for branch in self.p4BranchesInGit:
1403 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1405 settings = extractSettingsGitLog(logMsg)
1407 self.readOptions(settings)
1408 if (settings.has_key('depot-paths')
1409 and settings.has_key ('change')):
1410 change = int(settings['change']) + 1
1411 p4Change = max(p4Change, change)
1413 depotPaths = sorted(settings['depot-paths'])
1414 if self.previousDepotPaths == []:
1415 self.previousDepotPaths = depotPaths
1418 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1419 for i in range(0, min(len(cur), len(prev))):
1420 if cur[i] <> prev[i]:
1424 paths.append (cur[:i + 1])
1426 self.previousDepotPaths = paths
1429 self.depotPaths = sorted(self.previousDepotPaths)
1430 self.changeRange = "@%s,#head" % p4Change
1431 if not self.detectBranches:
1432 self.initialParent = parseRevision(self.branch)
1433 if not self.silent and not self.detectBranches:
1434 print "Performing incremental import into %s git branch" % self.branch
1436 if not self.branch.startswith("refs/"):
1437 self.branch = "refs/heads/" + self.branch
1439 if len(args) == 0 and self.depotPaths:
1441 print "Depot paths: %s" % ' '.join(self.depotPaths)
1443 if self.depotPaths and self.depotPaths != args:
1444 print ("previous import used depot path %s and now %s was specified. "
1445 "This doesn't work!" % (' '.join (self.depotPaths),
1449 self.depotPaths = sorted(args)
1455 for p in self.depotPaths:
1456 if p.find("@") != -1:
1457 atIdx = p.index("@")
1458 self.changeRange = p[atIdx:]
1459 if self.changeRange == "@all":
1460 self.changeRange = ""
1461 elif ',' not in self.changeRange:
1462 revision = self.changeRange
1463 self.changeRange = ""
1465 elif p.find("#") != -1:
1466 hashIdx = p.index("#")
1467 revision = p[hashIdx:]
1469 elif self.previousDepotPaths == []:
1472 p = re.sub ("\.\.\.$", "", p)
1473 if not p.endswith("/"):
1478 self.depotPaths = newPaths
1481 self.loadUserMapFromCache()
1483 if self.detectLabels:
1486 if self.detectBranches:
1487 ## FIXME - what's a P4 projectName ?
1488 self.projectName = self.guessProjectName()
1491 self.getBranchMappingFromGitBranches()
1493 self.getBranchMapping()
1495 print "p4-git branches: %s" % self.p4BranchesInGit
1496 print "initial parents: %s" % self.initialParents
1497 for b in self.p4BranchesInGit:
1501 b = b[len(self.projectName):]
1502 self.createdBranches.add(b)
1504 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1506 importProcess = subprocess.Popen(["git", "fast-import"],
1507 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1508 stderr=subprocess.PIPE);
1509 self.gitOutput = importProcess.stdout
1510 self.gitStream = importProcess.stdin
1511 self.gitError = importProcess.stderr
1514 self.importHeadRevision(revision)
1518 if len(self.changesFile) > 0:
1519 output = open(self.changesFile).readlines()
1522 changeSet.add(int(line))
1524 for change in changeSet:
1525 changes.append(change)
1530 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1532 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1534 if len(self.maxChanges) > 0:
1535 changes = changes[:min(int(self.maxChanges), len(changes))]
1537 if len(changes) == 0:
1539 print "No changes to import!"
1542 if not self.silent and not self.detectBranches:
1543 print "Import destination: %s" % self.branch
1545 self.updatedBranches = set()
1547 self.importChanges(changes)
1551 if len(self.updatedBranches) > 0:
1552 sys.stdout.write("Updated branches: ")
1553 for b in self.updatedBranches:
1554 sys.stdout.write("%s " % b)
1555 sys.stdout.write("\n")
1557 self.gitStream.close()
1558 if importProcess.wait() != 0:
1559 die("fast-import failed: %s" % self.gitError.read())
1560 self.gitOutput.close()
1561 self.gitError.close()
1565 class P4Rebase(Command):
1567 Command.__init__(self)
1569 self.description = ("Fetches the latest revision from perforce and "
1570 + "rebases the current work (branch) against it")
1571 self.verbose = False
1573 def run(self, args):
1577 return self.rebase()
1580 if os.system("git update-index --refresh") != 0:
1581 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.");
1582 if len(read_pipe("git diff-index HEAD --")) > 0:
1583 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1585 [upstream, settings] = findUpstreamBranchPoint()
1586 if len(upstream) == 0:
1587 die("Cannot find upstream branchpoint for rebase")
1589 # the branchpoint may be p4/foo~3, so strip off the parent
1590 upstream = re.sub("~[0-9]+$", "", upstream)
1592 print "Rebasing the current branch onto %s" % upstream
1593 oldHead = read_pipe("git rev-parse HEAD").strip()
1594 system("git rebase %s" % upstream)
1595 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1598 class P4Clone(P4Sync):
1600 P4Sync.__init__(self)
1601 self.description = "Creates a new git repository and imports from Perforce into it"
1602 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1604 optparse.make_option("--destination", dest="cloneDestination",
1605 action='store', default=None,
1606 help="where to leave result of the clone"),
1607 optparse.make_option("-/", dest="cloneExclude",
1608 action="append", type="string",
1609 help="exclude depot path")
1611 self.cloneDestination = None
1612 self.needsGit = False
1614 # This is required for the "append" cloneExclude action
1615 def ensure_value(self, attr, value):
1616 if not hasattr(self, attr) or getattr(self, attr) is None:
1617 setattr(self, attr, value)
1618 return getattr(self, attr)
1620 def defaultDestination(self, args):
1621 ## TODO: use common prefix of args?
1623 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1624 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1625 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1626 depotDir = re.sub(r"/$", "", depotDir)
1627 return os.path.split(depotDir)[1]
1629 def run(self, args):
1633 if self.keepRepoPath and not self.cloneDestination:
1634 sys.stderr.write("Must specify destination for --keep-path\n")
1639 if not self.cloneDestination and len(depotPaths) > 1:
1640 self.cloneDestination = depotPaths[-1]
1641 depotPaths = depotPaths[:-1]
1643 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1644 for p in depotPaths:
1645 if not p.startswith("//"):
1648 if not self.cloneDestination:
1649 self.cloneDestination = self.defaultDestination(args)
1651 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1652 if not os.path.exists(self.cloneDestination):
1653 os.makedirs(self.cloneDestination)
1654 os.chdir(self.cloneDestination)
1656 self.gitdir = os.getcwd() + "/.git"
1657 if not P4Sync.run(self, depotPaths):
1659 if self.branch != "master":
1660 if gitBranchExists("refs/remotes/p4/master"):
1661 system("git branch master refs/remotes/p4/master")
1662 system("git checkout -f")
1664 print "Could not detect main branch. No checkout/master branch created."
1668 class P4Branches(Command):
1670 Command.__init__(self)
1672 self.description = ("Shows the git branches that hold imports and their "
1673 + "corresponding perforce depot paths")
1674 self.verbose = False
1676 def run(self, args):
1677 if originP4BranchesExist():
1678 createOrUpdateBranchesFromOrigin()
1680 cmdline = "git rev-parse --symbolic "
1681 cmdline += " --remotes"
1683 for line in read_pipe_lines(cmdline):
1686 if not line.startswith('p4/') or line == "p4/HEAD":
1690 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1691 settings = extractSettingsGitLog(log)
1693 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1696 class HelpFormatter(optparse.IndentedHelpFormatter):
1698 optparse.IndentedHelpFormatter.__init__(self)
1700 def format_description(self, description):
1702 return description + "\n"
1706 def printUsage(commands):
1707 print "usage: %s <command> [options]" % sys.argv[0]
1709 print "valid commands: %s" % ", ".join(commands)
1711 print "Try %s <command> --help for command specific help." % sys.argv[0]
1716 "submit" : P4Submit,
1717 "commit" : P4Submit,
1719 "rebase" : P4Rebase,
1721 "rollback" : P4RollBack,
1722 "branches" : P4Branches
1727 if len(sys.argv[1:]) == 0:
1728 printUsage(commands.keys())
1732 cmdName = sys.argv[1]
1734 klass = commands[cmdName]
1737 print "unknown command %s" % cmdName
1739 printUsage(commands.keys())
1742 options = cmd.options
1743 cmd.gitdir = os.environ.get("GIT_DIR", None)
1747 if len(options) > 0:
1748 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1750 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1752 description = cmd.description,
1753 formatter = HelpFormatter())
1755 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1757 verbose = cmd.verbose
1759 if cmd.gitdir == None:
1760 cmd.gitdir = os.path.abspath(".git")
1761 if not isValidGitDir(cmd.gitdir):
1762 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1763 if os.path.exists(cmd.gitdir):
1764 cdup = read_pipe("git rev-parse --show-cdup").strip()
1768 if not isValidGitDir(cmd.gitdir):
1769 if isValidGitDir(cmd.gitdir + "/.git"):
1770 cmd.gitdir += "/.git"
1772 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1774 os.environ["GIT_DIR"] = cmd.gitdir
1776 if not cmd.run(args):
1780 if __name__ == '__main__':