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("--verbose", dest="verbose", action="store_true"),
468 optparse.make_option("--origin", dest="origin"),
469 optparse.make_option("-M", dest="detectRename", action="store_true"),
471 self.description = "Submit changes from git to the perforce depot."
472 self.usage += " [name of git branch to submit into perforce depot]"
473 self.interactive = True
475 self.detectRename = False
477 self.isWindows = (platform.system() == "Windows")
480 if len(p4CmdList("opened ...")) > 0:
481 die("You have files opened with perforce! Close them before starting the sync.")
483 # replaces everything between 'Description:' and the next P4 submit template field with the
485 def prepareLogMessage(self, template, message):
488 inDescriptionSection = False
490 for line in template.split("\n"):
491 if line.startswith("#"):
492 result += line + "\n"
495 if inDescriptionSection:
496 if line.startswith("Files:"):
497 inDescriptionSection = False
501 if line.startswith("Description:"):
502 inDescriptionSection = True
504 for messageLine in message.split("\n"):
505 line += "\t" + messageLine + "\n"
507 result += line + "\n"
511 def prepareSubmitTemplate(self):
512 # remove lines in the Files section that show changes to files outside the depot path we're committing into
514 inFilesSection = False
515 for line in read_pipe_lines("p4 change -o"):
517 if line.startswith("\t"):
518 # path starts and ends with a tab
520 lastTab = path.rfind("\t")
522 path = path[:lastTab]
523 if not path.startswith(self.depotPath):
526 inFilesSection = False
528 if line.startswith("Files:"):
529 inFilesSection = True
535 def applyCommit(self, id):
536 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
537 diffOpts = ("", "-M")[self.detectRename]
538 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
540 filesToDelete = set()
542 filesToChangeExecBit = {}
544 diff = parseDiffTreeEntry(line)
545 modifier = diff['status']
548 system("p4 edit \"%s\"" % path)
549 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
550 filesToChangeExecBit[path] = diff['dst_mode']
551 editedFiles.add(path)
552 elif modifier == "A":
554 filesToChangeExecBit[path] = diff['dst_mode']
555 if path in filesToDelete:
556 filesToDelete.remove(path)
557 elif modifier == "D":
558 filesToDelete.add(path)
559 if path in filesToAdd:
560 filesToAdd.remove(path)
561 elif modifier == "R":
562 src, dest = diff['src'], diff['dst']
563 system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest))
564 system("p4 edit \"%s\"" % (dest))
565 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
566 filesToChangeExecBit[dest] = diff['dst_mode']
568 editedFiles.add(dest)
569 filesToDelete.add(src)
571 die("unknown modifier %s for %s" % (modifier, path))
573 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
574 patchcmd = diffcmd + " | git apply "
575 tryPatchCmd = patchcmd + "--check -"
576 applyPatchCmd = patchcmd + "--check --apply -"
578 if os.system(tryPatchCmd) != 0:
579 print "Unfortunately applying the change failed!"
580 print "What do you want to do?"
582 while response != "s" and response != "a" and response != "w":
583 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
584 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
586 print "Skipping! Good luck with the next patches..."
587 for f in editedFiles:
588 system("p4 revert \"%s\"" % f);
592 elif response == "a":
593 os.system(applyPatchCmd)
594 if len(filesToAdd) > 0:
595 print "You may also want to call p4 add on the following files:"
596 print " ".join(filesToAdd)
597 if len(filesToDelete):
598 print "The following files should be scheduled for deletion with p4 delete:"
599 print " ".join(filesToDelete)
600 die("Please resolve and submit the conflict manually and "
601 + "continue afterwards with git-p4 submit --continue")
602 elif response == "w":
603 system(diffcmd + " > patch.txt")
604 print "Patch saved to patch.txt in %s !" % self.clientPath
605 die("Please resolve and submit the conflict manually and "
606 "continue afterwards with git-p4 submit --continue")
608 system(applyPatchCmd)
611 system("p4 add \"%s\"" % f)
612 for f in filesToDelete:
613 system("p4 revert \"%s\"" % f)
614 system("p4 delete \"%s\"" % f)
616 # Set/clear executable bits
617 for f in filesToChangeExecBit.keys():
618 mode = filesToChangeExecBit[f]
619 setP4ExecBit(f, mode)
621 logMessage = extractLogMessageFromGitCommit(id)
623 logMessage = logMessage.replace("\n", "\r\n")
624 logMessage = logMessage.strip()
626 template = self.prepareSubmitTemplate()
629 submitTemplate = self.prepareLogMessage(template, logMessage)
630 if os.environ.has_key("P4DIFF"):
631 del(os.environ["P4DIFF"])
632 diff = read_pipe("p4 diff -du ...")
634 for newFile in filesToAdd:
635 diff += "==== new file ====\n"
636 diff += "--- /dev/null\n"
637 diff += "+++ %s\n" % newFile
638 f = open(newFile, "r")
639 for line in f.readlines():
643 separatorLine = "######## everything below this line is just the diff #######"
644 if platform.system() == "Windows":
645 separatorLine += "\r"
646 separatorLine += "\n"
648 [handle, fileName] = tempfile.mkstemp()
649 tmpFile = os.fdopen(handle, "w+")
650 tmpFile.write(submitTemplate + separatorLine + diff)
653 if platform.system() == "Windows":
654 defaultEditor = "notepad"
655 if os.environ.has_key("P4EDITOR"):
656 editor = os.environ.get("P4EDITOR")
658 editor = os.environ.get("EDITOR", defaultEditor);
659 system(editor + " " + fileName)
660 tmpFile = open(fileName, "rb")
661 message = tmpFile.read()
664 submitTemplate = message[:message.index(separatorLine)]
666 submitTemplate = submitTemplate.replace("\r\n", "\n")
668 write_pipe("p4 submit -i", submitTemplate)
670 fileName = "submit.txt"
671 file = open(fileName, "w+")
672 file.write(self.prepareLogMessage(template, logMessage))
674 print ("Perforce submit template written as %s. "
675 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
676 % (fileName, fileName))
680 self.master = currentGitBranch()
681 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
682 die("Detecting current git branch failed!")
684 self.master = args[0]
688 [upstream, settings] = findUpstreamBranchPoint()
689 self.depotPath = settings['depot-paths'][0]
690 if len(self.origin) == 0:
691 self.origin = upstream
694 print "Origin branch is " + self.origin
696 if len(self.depotPath) == 0:
697 print "Internal error: cannot locate perforce depot path from existing branches"
700 self.clientPath = p4Where(self.depotPath)
702 if len(self.clientPath) == 0:
703 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
706 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
707 self.oldWorkingDirectory = os.getcwd()
709 os.chdir(self.clientPath)
710 print "Syncronizing p4 checkout..."
711 system("p4 sync ...")
716 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
717 commits.append(line.strip())
720 while len(commits) > 0:
722 commits = commits[1:]
723 self.applyCommit(commit)
724 if not self.interactive:
727 if len(commits) == 0:
728 print "All changes applied!"
729 os.chdir(self.oldWorkingDirectory)
739 class P4Sync(Command):
741 Command.__init__(self)
743 optparse.make_option("--branch", dest="branch"),
744 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
745 optparse.make_option("--changesfile", dest="changesFile"),
746 optparse.make_option("--silent", dest="silent", action="store_true"),
747 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
748 optparse.make_option("--verbose", dest="verbose", action="store_true"),
749 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
750 help="Import into refs/heads/ , not refs/remotes"),
751 optparse.make_option("--max-changes", dest="maxChanges"),
752 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
753 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
754 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
755 help="Only sync files that are included in the Perforce Client Spec")
757 self.description = """Imports from Perforce into a git repository.\n
759 //depot/my/project/ -- to import the current head
760 //depot/my/project/@all -- to import everything
761 //depot/my/project/@1,6 -- to import only from revision 1 to 6
763 (a ... is not needed in the path p4 specification, it's added implicitly)"""
765 self.usage += " //depot/path[@revRange]"
767 self.createdBranches = Set()
768 self.committedChanges = Set()
770 self.detectBranches = False
771 self.detectLabels = False
772 self.changesFile = ""
773 self.syncWithOrigin = True
775 self.importIntoRemotes = True
777 self.isWindows = (platform.system() == "Windows")
778 self.keepRepoPath = False
779 self.depotPaths = None
780 self.p4BranchesInGit = []
781 self.cloneExclude = []
782 self.useClientSpec = False
783 self.clientSpecDirs = []
785 if gitConfig("git-p4.syncFromOrigin") == "false":
786 self.syncWithOrigin = False
788 def extractFilesFromCommit(self, commit):
789 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
790 for path in self.cloneExclude]
793 while commit.has_key("depotFile%s" % fnum):
794 path = commit["depotFile%s" % fnum]
796 if [p for p in self.cloneExclude
797 if path.startswith (p)]:
800 found = [p for p in self.depotPaths
801 if path.startswith (p)]
808 file["rev"] = commit["rev%s" % fnum]
809 file["action"] = commit["action%s" % fnum]
810 file["type"] = commit["type%s" % fnum]
815 def stripRepoPath(self, path, prefixes):
816 if self.keepRepoPath:
817 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
820 if path.startswith(p):
825 def splitFilesIntoBranches(self, commit):
828 while commit.has_key("depotFile%s" % fnum):
829 path = commit["depotFile%s" % fnum]
830 found = [p for p in self.depotPaths
831 if path.startswith (p)]
838 file["rev"] = commit["rev%s" % fnum]
839 file["action"] = commit["action%s" % fnum]
840 file["type"] = commit["type%s" % fnum]
843 relPath = self.stripRepoPath(path, self.depotPaths)
845 for branch in self.knownBranches.keys():
847 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
848 if relPath.startswith(branch + "/"):
849 if branch not in branches:
850 branches[branch] = []
851 branches[branch].append(file)
856 ## Should move this out, doesn't use SELF.
857 def readP4Files(self, files):
863 for val in self.clientSpecDirs:
864 if f['path'].startswith(val[0]):
870 filesForCommit.append(f)
871 if f['action'] != 'delete':
872 filesToRead.append(f)
875 if len(filesToRead) > 0:
876 filedata = p4CmdList('-x - print',
877 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
878 for f in filesToRead]),
881 if "p4ExitCode" in filedata[0]:
882 die("Problems executing p4. Error: [%d]."
883 % (filedata[0]['p4ExitCode']));
887 while j < len(filedata):
891 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
892 text.append(filedata[j]['data'])
896 if not stat.has_key('depotFile'):
897 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
900 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
901 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
902 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
903 text = re.sub(r'(?i)\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
905 contents[stat['depotFile']] = text
907 for f in filesForCommit:
909 if contents.has_key(path):
910 f['data'] = contents[path]
912 return filesForCommit
914 def commit(self, details, files, branch, branchPrefixes, parent = ""):
915 epoch = details["time"]
916 author = details["user"]
919 print "commit into %s" % branch
921 # start with reading files; if that fails, we should not
925 if [p for p in branchPrefixes if f['path'].startswith(p)]:
928 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
929 files = self.readP4Files(new_files)
931 self.gitStream.write("commit %s\n" % branch)
932 # gitStream.write("mark :%s\n" % details["change"])
933 self.committedChanges.add(int(details["change"]))
935 if author not in self.users:
936 self.getUserMapFromPerforceServer()
937 if author in self.users:
938 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
940 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
942 self.gitStream.write("committer %s\n" % committer)
944 self.gitStream.write("data <<EOT\n")
945 self.gitStream.write(details["desc"])
946 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
947 % (','.join (branchPrefixes), details["change"]))
948 if len(details['options']) > 0:
949 self.gitStream.write(": options = %s" % details['options'])
950 self.gitStream.write("]\nEOT\n\n")
954 print "parent %s" % parent
955 self.gitStream.write("from %s\n" % parent)
958 if file["type"] == "apple":
959 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
962 relPath = self.stripRepoPath(file['path'], branchPrefixes)
963 if file["action"] == "delete":
964 self.gitStream.write("D %s\n" % relPath)
969 if isP4Exec(file["type"]):
971 elif file["type"] == "symlink":
973 # p4 print on a symlink contains "target\n", so strip it off
976 if self.isWindows and file["type"].endswith("text"):
977 data = data.replace("\r\n", "\n")
979 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
980 self.gitStream.write("data %s\n" % len(data))
981 self.gitStream.write(data)
982 self.gitStream.write("\n")
984 self.gitStream.write("\n")
986 change = int(details["change"])
988 if self.labels.has_key(change):
989 label = self.labels[change]
990 labelDetails = label[0]
991 labelRevisions = label[1]
993 print "Change %s is labelled %s" % (change, labelDetails)
995 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
996 for p in branchPrefixes]))
998 if len(files) == len(labelRevisions):
1002 if info["action"] == "delete":
1004 cleanedFiles[info["depotFile"]] = info["rev"]
1006 if cleanedFiles == labelRevisions:
1007 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1008 self.gitStream.write("from %s\n" % branch)
1010 owner = labelDetails["Owner"]
1012 if author in self.users:
1013 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1015 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1016 self.gitStream.write("tagger %s\n" % tagger)
1017 self.gitStream.write("data <<EOT\n")
1018 self.gitStream.write(labelDetails["Description"])
1019 self.gitStream.write("EOT\n\n")
1023 print ("Tag %s does not match with change %s: files do not match."
1024 % (labelDetails["label"], change))
1028 print ("Tag %s does not match with change %s: file count is different."
1029 % (labelDetails["label"], change))
1031 def getUserCacheFilename(self):
1032 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1033 return home + "/.gitp4-usercache.txt"
1035 def getUserMapFromPerforceServer(self):
1036 if self.userMapFromPerforceServer:
1040 for output in p4CmdList("users"):
1041 if not output.has_key("User"):
1043 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1047 for (key, val) in self.users.items():
1048 s += "%s\t%s\n" % (key, val)
1050 open(self.getUserCacheFilename(), "wb").write(s)
1051 self.userMapFromPerforceServer = True
1053 def loadUserMapFromCache(self):
1055 self.userMapFromPerforceServer = False
1057 cache = open(self.getUserCacheFilename(), "rb")
1058 lines = cache.readlines()
1061 entry = line.strip().split("\t")
1062 self.users[entry[0]] = entry[1]
1064 self.getUserMapFromPerforceServer()
1066 def getLabels(self):
1069 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1070 if len(l) > 0 and not self.silent:
1071 print "Finding files belonging to labels in %s" % `self.depotPaths`
1074 label = output["label"]
1078 print "Querying files for label %s" % label
1079 for file in p4CmdList("files "
1080 + ' '.join (["%s...@%s" % (p, label)
1081 for p in self.depotPaths])):
1082 revisions[file["depotFile"]] = file["rev"]
1083 change = int(file["change"])
1084 if change > newestChange:
1085 newestChange = change
1087 self.labels[newestChange] = [output, revisions]
1090 print "Label changes: %s" % self.labels.keys()
1092 def guessProjectName(self):
1093 for p in self.depotPaths:
1096 p = p[p.strip().rfind("/") + 1:]
1097 if not p.endswith("/"):
1101 def getBranchMapping(self):
1102 lostAndFoundBranches = set()
1104 for info in p4CmdList("branches"):
1105 details = p4Cmd("branch -o %s" % info["branch"])
1107 while details.has_key("View%s" % viewIdx):
1108 paths = details["View%s" % viewIdx].split(" ")
1109 viewIdx = viewIdx + 1
1110 # require standard //depot/foo/... //depot/bar/... mapping
1111 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1114 destination = paths[1]
1116 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1117 source = source[len(self.depotPaths[0]):-4]
1118 destination = destination[len(self.depotPaths[0]):-4]
1120 if destination in self.knownBranches:
1122 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1123 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1126 self.knownBranches[destination] = source
1128 lostAndFoundBranches.discard(destination)
1130 if source not in self.knownBranches:
1131 lostAndFoundBranches.add(source)
1134 for branch in lostAndFoundBranches:
1135 self.knownBranches[branch] = branch
1137 def getBranchMappingFromGitBranches(self):
1138 branches = p4BranchesInGit(self.importIntoRemotes)
1139 for branch in branches.keys():
1140 if branch == "master":
1143 branch = branch[len(self.projectName):]
1144 self.knownBranches[branch] = branch
1146 def listExistingP4GitBranches(self):
1147 # branches holds mapping from name to commit
1148 branches = p4BranchesInGit(self.importIntoRemotes)
1149 self.p4BranchesInGit = branches.keys()
1150 for branch in branches.keys():
1151 self.initialParents[self.refPrefix + branch] = branches[branch]
1153 def updateOptionDict(self, d):
1155 if self.keepRepoPath:
1156 option_keys['keepRepoPath'] = 1
1158 d["options"] = ' '.join(sorted(option_keys.keys()))
1160 def readOptions(self, d):
1161 self.keepRepoPath = (d.has_key('options')
1162 and ('keepRepoPath' in d['options']))
1164 def gitRefForBranch(self, branch):
1165 if branch == "main":
1166 return self.refPrefix + "master"
1168 if len(branch) <= 0:
1171 return self.refPrefix + self.projectName + branch
1173 def gitCommitByP4Change(self, ref, change):
1175 print "looking in ref " + ref + " for change %s using bisect..." % change
1178 latestCommit = parseRevision(ref)
1182 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1183 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1188 log = extractLogMessageFromGitCommit(next)
1189 settings = extractSettingsGitLog(log)
1190 currentChange = int(settings['change'])
1192 print "current change %s" % currentChange
1194 if currentChange == change:
1196 print "found %s" % next
1199 if currentChange < change:
1200 earliestCommit = "^%s" % next
1202 latestCommit = "%s" % next
1206 def importNewBranch(self, branch, maxChange):
1207 # make fast-import flush all changes to disk and update the refs using the checkpoint
1208 # command so that we can try to find the branch parent in the git history
1209 self.gitStream.write("checkpoint\n\n");
1210 self.gitStream.flush();
1211 branchPrefix = self.depotPaths[0] + branch + "/"
1212 range = "@1,%s" % maxChange
1213 #print "prefix" + branchPrefix
1214 changes = p4ChangesForPaths([branchPrefix], range)
1215 if len(changes) <= 0:
1217 firstChange = changes[0]
1218 #print "first change in branch: %s" % firstChange
1219 sourceBranch = self.knownBranches[branch]
1220 sourceDepotPath = self.depotPaths[0] + sourceBranch
1221 sourceRef = self.gitRefForBranch(sourceBranch)
1222 #print "source " + sourceBranch
1224 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1225 #print "branch parent: %s" % branchParentChange
1226 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1227 if len(gitParent) > 0:
1228 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1229 #print "parent git commit: %s" % gitParent
1231 self.importChanges(changes)
1234 def importChanges(self, changes):
1236 for change in changes:
1237 description = p4Cmd("describe %s" % change)
1238 self.updateOptionDict(description)
1241 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1246 if self.detectBranches:
1247 branches = self.splitFilesIntoBranches(description)
1248 for branch in branches.keys():
1250 branchPrefix = self.depotPaths[0] + branch + "/"
1254 filesForCommit = branches[branch]
1257 print "branch is %s" % branch
1259 self.updatedBranches.add(branch)
1261 if branch not in self.createdBranches:
1262 self.createdBranches.add(branch)
1263 parent = self.knownBranches[branch]
1264 if parent == branch:
1267 fullBranch = self.projectName + branch
1268 if fullBranch not in self.p4BranchesInGit:
1270 print("\n Importing new branch %s" % fullBranch);
1271 if self.importNewBranch(branch, change - 1):
1273 self.p4BranchesInGit.append(fullBranch)
1275 print("\n Resuming with change %s" % change);
1278 print "parent determined through known branches: %s" % parent
1280 branch = self.gitRefForBranch(branch)
1281 parent = self.gitRefForBranch(parent)
1284 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1286 if len(parent) == 0 and branch in self.initialParents:
1287 parent = self.initialParents[branch]
1288 del self.initialParents[branch]
1290 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1292 files = self.extractFilesFromCommit(description)
1293 self.commit(description, files, self.branch, self.depotPaths,
1295 self.initialParent = ""
1297 print self.gitError.read()
1300 def importHeadRevision(self, revision):
1301 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1303 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1304 details["desc"] = ("Initial import of %s from the state at revision %s"
1305 % (' '.join(self.depotPaths), revision))
1306 details["change"] = revision
1310 for info in p4CmdList("files "
1311 + ' '.join(["%s...%s"
1313 for p in self.depotPaths])):
1315 if info['code'] == 'error':
1316 sys.stderr.write("p4 returned an error: %s\n"
1321 change = int(info["change"])
1322 if change > newestRevision:
1323 newestRevision = change
1325 if info["action"] == "delete":
1326 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1327 #fileCnt = fileCnt + 1
1330 for prop in ["depotFile", "rev", "action", "type" ]:
1331 details["%s%s" % (prop, fileCnt)] = info[prop]
1333 fileCnt = fileCnt + 1
1335 details["change"] = newestRevision
1336 self.updateOptionDict(details)
1338 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1340 print "IO error with git fast-import. Is your git version recent enough?"
1341 print self.gitError.read()
1344 def getClientSpec(self):
1345 specList = p4CmdList( "client -o" )
1347 for entry in specList:
1348 for k,v in entry.iteritems():
1349 if k.startswith("View"):
1350 if v.startswith('"'):
1354 index = v.find("...")
1356 if v.startswith("-"):
1361 self.clientSpecDirs = temp.items()
1362 self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1364 def run(self, args):
1365 self.depotPaths = []
1366 self.changeRange = ""
1367 self.initialParent = ""
1368 self.previousDepotPaths = []
1370 # map from branch depot path to parent branch
1371 self.knownBranches = {}
1372 self.initialParents = {}
1373 self.hasOrigin = originP4BranchesExist()
1374 if not self.syncWithOrigin:
1375 self.hasOrigin = False
1377 if self.importIntoRemotes:
1378 self.refPrefix = "refs/remotes/p4/"
1380 self.refPrefix = "refs/heads/p4/"
1382 if self.syncWithOrigin and self.hasOrigin:
1384 print "Syncing with origin first by calling git fetch origin"
1385 system("git fetch origin")
1387 if len(self.branch) == 0:
1388 self.branch = self.refPrefix + "master"
1389 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1390 system("git update-ref %s refs/heads/p4" % self.branch)
1391 system("git branch -D p4");
1392 # create it /after/ importing, when master exists
1393 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1394 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1396 if self.useClientSpec or gitConfig("p4.useclientspec") == "true":
1397 self.getClientSpec()
1399 # TODO: should always look at previous commits,
1400 # merge with previous imports, if possible.
1403 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1404 self.listExistingP4GitBranches()
1406 if len(self.p4BranchesInGit) > 1:
1408 print "Importing from/into multiple branches"
1409 self.detectBranches = True
1412 print "branches: %s" % self.p4BranchesInGit
1415 for branch in self.p4BranchesInGit:
1416 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
1418 settings = extractSettingsGitLog(logMsg)
1420 self.readOptions(settings)
1421 if (settings.has_key('depot-paths')
1422 and settings.has_key ('change')):
1423 change = int(settings['change']) + 1
1424 p4Change = max(p4Change, change)
1426 depotPaths = sorted(settings['depot-paths'])
1427 if self.previousDepotPaths == []:
1428 self.previousDepotPaths = depotPaths
1431 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1432 for i in range(0, min(len(cur), len(prev))):
1433 if cur[i] <> prev[i]:
1437 paths.append (cur[:i + 1])
1439 self.previousDepotPaths = paths
1442 self.depotPaths = sorted(self.previousDepotPaths)
1443 self.changeRange = "@%s,#head" % p4Change
1444 if not self.detectBranches:
1445 self.initialParent = parseRevision(self.branch)
1446 if not self.silent and not self.detectBranches:
1447 print "Performing incremental import into %s git branch" % self.branch
1449 if not self.branch.startswith("refs/"):
1450 self.branch = "refs/heads/" + self.branch
1452 if len(args) == 0 and self.depotPaths:
1454 print "Depot paths: %s" % ' '.join(self.depotPaths)
1456 if self.depotPaths and self.depotPaths != args:
1457 print ("previous import used depot path %s and now %s was specified. "
1458 "This doesn't work!" % (' '.join (self.depotPaths),
1462 self.depotPaths = sorted(args)
1468 for p in self.depotPaths:
1469 if p.find("@") != -1:
1470 atIdx = p.index("@")
1471 self.changeRange = p[atIdx:]
1472 if self.changeRange == "@all":
1473 self.changeRange = ""
1474 elif ',' not in self.changeRange:
1475 revision = self.changeRange
1476 self.changeRange = ""
1478 elif p.find("#") != -1:
1479 hashIdx = p.index("#")
1480 revision = p[hashIdx:]
1482 elif self.previousDepotPaths == []:
1485 p = re.sub ("\.\.\.$", "", p)
1486 if not p.endswith("/"):
1491 self.depotPaths = newPaths
1494 self.loadUserMapFromCache()
1496 if self.detectLabels:
1499 if self.detectBranches:
1500 ## FIXME - what's a P4 projectName ?
1501 self.projectName = self.guessProjectName()
1504 self.getBranchMappingFromGitBranches()
1506 self.getBranchMapping()
1508 print "p4-git branches: %s" % self.p4BranchesInGit
1509 print "initial parents: %s" % self.initialParents
1510 for b in self.p4BranchesInGit:
1514 b = b[len(self.projectName):]
1515 self.createdBranches.add(b)
1517 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1519 importProcess = subprocess.Popen(["git", "fast-import"],
1520 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1521 stderr=subprocess.PIPE);
1522 self.gitOutput = importProcess.stdout
1523 self.gitStream = importProcess.stdin
1524 self.gitError = importProcess.stderr
1527 self.importHeadRevision(revision)
1531 if len(self.changesFile) > 0:
1532 output = open(self.changesFile).readlines()
1535 changeSet.add(int(line))
1537 for change in changeSet:
1538 changes.append(change)
1543 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1545 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1547 if len(self.maxChanges) > 0:
1548 changes = changes[:min(int(self.maxChanges), len(changes))]
1550 if len(changes) == 0:
1552 print "No changes to import!"
1555 if not self.silent and not self.detectBranches:
1556 print "Import destination: %s" % self.branch
1558 self.updatedBranches = set()
1560 self.importChanges(changes)
1564 if len(self.updatedBranches) > 0:
1565 sys.stdout.write("Updated branches: ")
1566 for b in self.updatedBranches:
1567 sys.stdout.write("%s " % b)
1568 sys.stdout.write("\n")
1570 self.gitStream.close()
1571 if importProcess.wait() != 0:
1572 die("fast-import failed: %s" % self.gitError.read())
1573 self.gitOutput.close()
1574 self.gitError.close()
1578 class P4Rebase(Command):
1580 Command.__init__(self)
1582 self.description = ("Fetches the latest revision from perforce and "
1583 + "rebases the current work (branch) against it")
1584 self.verbose = False
1586 def run(self, args):
1590 return self.rebase()
1593 if os.system("git update-index --refresh") != 0:
1594 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.");
1595 if len(read_pipe("git diff-index HEAD --")) > 0:
1596 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1598 [upstream, settings] = findUpstreamBranchPoint()
1599 if len(upstream) == 0:
1600 die("Cannot find upstream branchpoint for rebase")
1602 # the branchpoint may be p4/foo~3, so strip off the parent
1603 upstream = re.sub("~[0-9]+$", "", upstream)
1605 print "Rebasing the current branch onto %s" % upstream
1606 oldHead = read_pipe("git rev-parse HEAD").strip()
1607 system("git rebase %s" % upstream)
1608 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1611 class P4Clone(P4Sync):
1613 P4Sync.__init__(self)
1614 self.description = "Creates a new git repository and imports from Perforce into it"
1615 self.usage = "usage: %prog [options] //depot/path[@revRange]"
1617 optparse.make_option("--destination", dest="cloneDestination",
1618 action='store', default=None,
1619 help="where to leave result of the clone"),
1620 optparse.make_option("-/", dest="cloneExclude",
1621 action="append", type="string",
1622 help="exclude depot path")
1624 self.cloneDestination = None
1625 self.needsGit = False
1627 # This is required for the "append" cloneExclude action
1628 def ensure_value(self, attr, value):
1629 if not hasattr(self, attr) or getattr(self, attr) is None:
1630 setattr(self, attr, value)
1631 return getattr(self, attr)
1633 def defaultDestination(self, args):
1634 ## TODO: use common prefix of args?
1636 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1637 depotDir = re.sub("(#[^#]*)$", "", depotDir)
1638 depotDir = re.sub(r"\.\.\.$", "", depotDir)
1639 depotDir = re.sub(r"/$", "", depotDir)
1640 return os.path.split(depotDir)[1]
1642 def run(self, args):
1646 if self.keepRepoPath and not self.cloneDestination:
1647 sys.stderr.write("Must specify destination for --keep-path\n")
1652 if not self.cloneDestination and len(depotPaths) > 1:
1653 self.cloneDestination = depotPaths[-1]
1654 depotPaths = depotPaths[:-1]
1656 self.cloneExclude = ["/"+p for p in self.cloneExclude]
1657 for p in depotPaths:
1658 if not p.startswith("//"):
1661 if not self.cloneDestination:
1662 self.cloneDestination = self.defaultDestination(args)
1664 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
1665 if not os.path.exists(self.cloneDestination):
1666 os.makedirs(self.cloneDestination)
1667 os.chdir(self.cloneDestination)
1669 self.gitdir = os.getcwd() + "/.git"
1670 if not P4Sync.run(self, depotPaths):
1672 if self.branch != "master":
1673 if gitBranchExists("refs/remotes/p4/master"):
1674 system("git branch master refs/remotes/p4/master")
1675 system("git checkout -f")
1677 print "Could not detect main branch. No checkout/master branch created."
1681 class P4Branches(Command):
1683 Command.__init__(self)
1685 self.description = ("Shows the git branches that hold imports and their "
1686 + "corresponding perforce depot paths")
1687 self.verbose = False
1689 def run(self, args):
1690 if originP4BranchesExist():
1691 createOrUpdateBranchesFromOrigin()
1693 cmdline = "git rev-parse --symbolic "
1694 cmdline += " --remotes"
1696 for line in read_pipe_lines(cmdline):
1699 if not line.startswith('p4/') or line == "p4/HEAD":
1703 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1704 settings = extractSettingsGitLog(log)
1706 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1709 class HelpFormatter(optparse.IndentedHelpFormatter):
1711 optparse.IndentedHelpFormatter.__init__(self)
1713 def format_description(self, description):
1715 return description + "\n"
1719 def printUsage(commands):
1720 print "usage: %s <command> [options]" % sys.argv[0]
1722 print "valid commands: %s" % ", ".join(commands)
1724 print "Try %s <command> --help for command specific help." % sys.argv[0]
1729 "submit" : P4Submit,
1730 "commit" : P4Submit,
1732 "rebase" : P4Rebase,
1734 "rollback" : P4RollBack,
1735 "branches" : P4Branches
1740 if len(sys.argv[1:]) == 0:
1741 printUsage(commands.keys())
1745 cmdName = sys.argv[1]
1747 klass = commands[cmdName]
1750 print "unknown command %s" % cmdName
1752 printUsage(commands.keys())
1755 options = cmd.options
1756 cmd.gitdir = os.environ.get("GIT_DIR", None)
1760 if len(options) > 0:
1761 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1763 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1765 description = cmd.description,
1766 formatter = HelpFormatter())
1768 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1770 verbose = cmd.verbose
1772 if cmd.gitdir == None:
1773 cmd.gitdir = os.path.abspath(".git")
1774 if not isValidGitDir(cmd.gitdir):
1775 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1776 if os.path.exists(cmd.gitdir):
1777 cdup = read_pipe("git rev-parse --show-cdup").strip()
1781 if not isValidGitDir(cmd.gitdir):
1782 if isValidGitDir(cmd.gitdir + "/.git"):
1783 cmd.gitdir += "/.git"
1785 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
1787 os.environ["GIT_DIR"] = cmd.gitdir
1789 if not cmd.run(args):
1793 if __name__ == '__main__':