#!/usr/bin/env python # # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. # # Author: Simon Hausmann # Copyright: 2007 Simon Hausmann # 2007 Trolltech ASA # License: MIT # import optparse, sys, os, marshal, popen2, subprocess, shelve import tempfile, getopt, sha, os.path, time, platform from sets import Set; gitdir = os.environ.get("GIT_DIR", "") def mypopen(command): return os.popen(command, "rb"); def p4CmdList(cmd): cmd = "p4 -G %s" % cmd pipe = os.popen(cmd, "rb") result = [] try: while True: entry = marshal.load(pipe) result.append(entry) except EOFError: pass pipe.close() return result def p4Cmd(cmd): list = p4CmdList(cmd) result = {} for entry in list: result.update(entry) return result; def p4Where(depotPath): if not depotPath.endswith("/"): depotPath += "/" output = p4Cmd("where %s..." % depotPath) clientPath = "" if "path" in output: clientPath = output.get("path") elif "data" in output: data = output.get("data") lastSpace = data.rfind(" ") clientPath = data[lastSpace + 1:] if clientPath.endswith("..."): clientPath = clientPath[:-3] return clientPath def die(msg): sys.stderr.write(msg + "\n") sys.exit(1) def currentGitBranch(): return mypopen("git name-rev HEAD").read().split(" ")[1][:-1] def isValidGitDir(path): if os.path.exists(path + "/HEAD") and os.path.exists(path + "/refs") and os.path.exists(path + "/objects"): return True; return False def parseRevision(ref): return mypopen("git rev-parse %s" % ref).read()[:-1] def system(cmd): if os.system(cmd) != 0: die("command failed: %s" % cmd) def extractLogMessageFromGitCommit(commit): logMessage = "" foundTitle = False for log in mypopen("git cat-file commit %s" % commit).readlines(): if not foundTitle: if len(log) == 1: foundTitle = True continue logMessage += log return logMessage def extractDepotPathAndChangeFromGitLog(log): values = {} for line in log.split("\n"): line = line.strip() if line.startswith("[git-p4:") and line.endswith("]"): line = line[8:-1].strip() for assignment in line.split(":"): variable = assignment.strip() value = "" equalPos = assignment.find("=") if equalPos != -1: variable = assignment[:equalPos].strip() value = assignment[equalPos + 1:].strip() if value.startswith("\"") and value.endswith("\""): value = value[1:-1] values[variable] = value return values.get("depot-path"), values.get("change") def gitBranchExists(branch): proc = subprocess.Popen(["git", "rev-parse", branch], stderr=subprocess.PIPE, stdout=subprocess.PIPE); return proc.wait() == 0; class Command: def __init__(self): self.usage = "usage: %prog [options]" self.needsGit = True class P4Debug(Command): def __init__(self): Command.__init__(self) self.options = [ ] self.description = "A tool to debug the output of p4 -G." self.needsGit = False def run(self, args): for output in p4CmdList(" ".join(args)): print output return True class P4Submit(Command): def __init__(self): Command.__init__(self) self.options = [ optparse.make_option("--continue", action="store_false", dest="firstTime"), optparse.make_option("--origin", dest="origin"), optparse.make_option("--reset", action="store_true", dest="reset"), optparse.make_option("--log-substitutions", dest="substFile"), optparse.make_option("--noninteractive", action="store_false"), optparse.make_option("--dry-run", action="store_true"), ] self.description = "Submit changes from git to the perforce depot." self.usage += " [name of git branch to submit into perforce depot]" self.firstTime = True self.reset = False self.interactive = True self.dryRun = False self.substFile = "" self.firstTime = True self.origin = "" self.logSubstitutions = {} self.logSubstitutions[""] = "%log%" self.logSubstitutions["\tDetails:"] = "\tDetails: %log%" def check(self): if len(p4CmdList("opened ...")) > 0: die("You have files opened with perforce! Close them before starting the sync.") def start(self): if len(self.config) > 0 and not self.reset: die("Cannot start sync. Previous sync config found at %s\nIf you want to start submitting again from scratch maybe you want to call git-p4 submit --reset" % self.configFile) commits = [] for line in mypopen("git rev-list --no-merges %s..%s" % (self.origin, self.master)).readlines(): commits.append(line[:-1]) commits.reverse() self.config["commits"] = commits def prepareLogMessage(self, template, message): result = "" for line in template.split("\n"): if line.startswith("#"): result += line + "\n" continue substituted = False for key in self.logSubstitutions.keys(): if line.find(key) != -1: value = self.logSubstitutions[key] value = value.replace("%log%", message) if value != "@remove@": result += line.replace(key, value) + "\n" substituted = True break if not substituted: result += line + "\n" return result def apply(self, id): print "Applying %s" % (mypopen("git log --max-count=1 --pretty=oneline %s" % id).read()) diff = mypopen("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)).readlines() filesToAdd = set() filesToDelete = set() editedFiles = set() for line in diff: modifier = line[0] path = line[1:].strip() if modifier == "M": system("p4 edit \"%s\"" % path) editedFiles.add(path) elif modifier == "A": filesToAdd.add(path) if path in filesToDelete: filesToDelete.remove(path) elif modifier == "D": filesToDelete.add(path) if path in filesToAdd: filesToAdd.remove(path) else: die("unknown modifier %s for %s" % (modifier, path)) diffcmd = "git diff-tree -p --diff-filter=ACMRTUXB \"%s^\" \"%s\"" % (id, id) patchcmd = diffcmd + " | patch -p1" if os.system(patchcmd + " --dry-run --silent") != 0: print "Unfortunately applying the change failed!" print "What do you want to do?" response = "x" while response != "s" and response != "a" and response != "w": response = raw_input("[s]kip this patch / [a]pply the patch forcibly and with .rej files / [w]rite the patch to a file (patch.txt) ") if response == "s": print "Skipping! Good luck with the next patches..." return elif response == "a": os.system(patchcmd) if len(filesToAdd) > 0: print "You may also want to call p4 add on the following files:" print " ".join(filesToAdd) if len(filesToDelete): print "The following files should be scheduled for deletion with p4 delete:" print " ".join(filesToDelete) die("Please resolve and submit the conflict manually and continue afterwards with git-p4 submit --continue") elif response == "w": system(diffcmd + " > patch.txt") print "Patch saved to patch.txt in %s !" % self.clientPath die("Please resolve and submit the conflict manually and continue afterwards with git-p4 submit --continue") system(patchcmd) for f in filesToAdd: system("p4 add %s" % f) for f in filesToDelete: system("p4 revert %s" % f) system("p4 delete %s" % f) logMessage = extractLogMessageFromGitCommit(id) logMessage = logMessage.replace("\n", "\n\t") logMessage = logMessage[:-1] template = mypopen("p4 change -o").read() if self.interactive: submitTemplate = self.prepareLogMessage(template, logMessage) diff = mypopen("p4 diff -du ...").read() for newFile in filesToAdd: diff += "==== new file ====\n" diff += "--- /dev/null\n" diff += "+++ %s\n" % newFile f = open(newFile, "r") for line in f.readlines(): diff += "+" + line f.close() separatorLine = "######## everything below this line is just the diff #######" if platform.system() == "Windows": separatorLine += "\r" separatorLine += "\n" response = "e" firstIteration = True while response == "e": if not firstIteration: response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ") firstIteration = False if response == "e": [handle, fileName] = tempfile.mkstemp() tmpFile = os.fdopen(handle, "w+") tmpFile.write(submitTemplate + separatorLine + diff) tmpFile.close() defaultEditor = "vi" if platform.system() == "Windows": defaultEditor = "notepad" editor = os.environ.get("EDITOR", defaultEditor); system(editor + " " + fileName) tmpFile = open(fileName, "rb") message = tmpFile.read() tmpFile.close() os.remove(fileName) submitTemplate = message[:message.index(separatorLine)] if response == "y" or response == "yes": if self.dryRun: print submitTemplate raw_input("Press return to continue...") else: pipe = os.popen("p4 submit -i", "wb") pipe.write(submitTemplate) pipe.close() elif response == "s": for f in editedFiles: system("p4 revert \"%s\"" % f); for f in filesToAdd: system("p4 revert \"%s\"" % f); system("rm %s" %f) for f in filesToDelete: system("p4 delete \"%s\"" % f); return else: print "Not submitting!" self.interactive = False else: fileName = "submit.txt" file = open(fileName, "w+") file.write(self.prepareLogMessage(template, logMessage)) file.close() print "Perforce submit template written as %s. Please review/edit and then use p4 submit -i < %s to submit directly!" % (fileName, fileName) def run(self, args): global gitdir # make gitdir absolute so we can cd out into the perforce checkout gitdir = os.path.abspath(gitdir) os.environ["GIT_DIR"] = gitdir if len(args) == 0: self.master = currentGitBranch() if len(self.master) == 0 or not os.path.exists("%s/refs/heads/%s" % (gitdir, self.master)): die("Detecting current git branch failed!") elif len(args) == 1: self.master = args[0] else: return False depotPath = "" if gitBranchExists("p4"): [depotPath, dummy] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("p4")) if len(depotPath) == 0 and gitBranchExists("origin"): [depotPath, dummy] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("origin")) if len(depotPath) == 0: print "Internal error: cannot locate perforce depot path from existing branches" sys.exit(128) self.clientPath = p4Where(depotPath) if len(self.clientPath) == 0: print "Error: Cannot locate perforce checkout of %s in client view" % depotPath sys.exit(128) print "Perforce checkout for depot path %s located at %s" % (depotPath, self.clientPath) oldWorkingDirectory = os.getcwd() os.chdir(self.clientPath) response = raw_input("Do you want to sync %s with p4 sync? [y]es/[n]o " % self.clientPath) if response == "y" or response == "yes": system("p4 sync ...") if len(self.origin) == 0: if gitBranchExists("p4"): self.origin = "p4" else: self.origin = "origin" if self.reset: self.firstTime = True if len(self.substFile) > 0: for line in open(self.substFile, "r").readlines(): tokens = line[:-1].split("=") self.logSubstitutions[tokens[0]] = tokens[1] self.check() self.configFile = gitdir + "/p4-git-sync.cfg" self.config = shelve.open(self.configFile, writeback=True) if self.firstTime: self.start() commits = self.config.get("commits", []) while len(commits) > 0: self.firstTime = False commit = commits[0] commits = commits[1:] self.config["commits"] = commits self.apply(commit) if not self.interactive: break self.config.close() if len(commits) == 0: if self.firstTime: print "No changes found to apply between %s and current HEAD" % self.origin else: print "All changes applied!" response = raw_input("Do you want to sync from Perforce now using git-p4 rebase? [y]es/[n]o ") if response == "y" or response == "yes": os.chdir(oldWorkingDirectory) rebase = P4Rebase() rebase.run([]) os.remove(self.configFile) return True class P4Sync(Command): def __init__(self): Command.__init__(self) self.options = [ optparse.make_option("--branch", dest="branch"), optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"), optparse.make_option("--changesfile", dest="changesFile"), optparse.make_option("--silent", dest="silent", action="store_true"), optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"), optparse.make_option("--with-origin", dest="syncWithOrigin", action="store_true"), optparse.make_option("--verbose", dest="verbose", action="store_true") ] self.description = """Imports from Perforce into a git repository.\n example: //depot/my/project/ -- to import the current head //depot/my/project/@all -- to import everything //depot/my/project/@1,6 -- to import only from revision 1 to 6 (a ... is not needed in the path p4 specification, it's added implicitly)""" self.usage += " //depot/path[@revRange]" self.silent = False self.createdBranches = Set() self.committedChanges = Set() self.branch = "" self.detectBranches = False self.detectLabels = False self.changesFile = "" self.syncWithOrigin = False self.verbose = False def p4File(self, depotPath): return os.popen("p4 print -q \"%s\"" % depotPath, "rb").read() def extractFilesFromCommit(self, commit): files = [] fnum = 0 while commit.has_key("depotFile%s" % fnum): path = commit["depotFile%s" % fnum] if not path.startswith(self.depotPath): # if not self.silent: # print "\nchanged files: ignoring path %s outside of %s in change %s" % (path, self.depotPath, change) fnum = fnum + 1 continue file = {} file["path"] = path file["rev"] = commit["rev%s" % fnum] file["action"] = commit["action%s" % fnum] file["type"] = commit["type%s" % fnum] files.append(file) fnum = fnum + 1 return files def branchesForCommit(self, files): branches = Set() for file in files: path = file["path"][len(self.depotPath):] for branch in self.knownBranches.keys(): if path.startswith(branch): branches.add(branch) return branches def commit(self, details, files, branch, branchPrefix, parent = ""): epoch = details["time"] author = details["user"] if self.verbose: print "commit into %s" % branch self.gitStream.write("commit %s\n" % branch) # gitStream.write("mark :%s\n" % details["change"]) self.committedChanges.add(int(details["change"])) committer = "" if author in self.users: committer = "%s %s %s" % (self.users[author], epoch, self.tz) else: committer = "%s %s %s" % (author, epoch, self.tz) self.gitStream.write("committer %s\n" % committer) self.gitStream.write("data < 0: if self.verbose: print "parent %s" % parent self.gitStream.write("from %s\n" % parent) for file in files: path = file["path"] if not path.startswith(branchPrefix): # if not silent: # print "\nchanged files: ignoring path %s outside of branch prefix %s in change %s" % (path, branchPrefix, details["change"]) continue rev = file["rev"] depotPath = path + "#" + rev relPath = path[len(branchPrefix):] action = file["action"] if file["type"] == "apple": print "\nfile %s is a strange apple file that forks. Ignoring!" % path continue if action == "delete": self.gitStream.write("D %s\n" % relPath) else: mode = 644 if file["type"].startswith("x"): mode = 755 data = self.p4File(depotPath) self.gitStream.write("M %s inline %s\n" % (mode, relPath)) self.gitStream.write("data %s\n" % len(data)) self.gitStream.write(data) self.gitStream.write("\n") self.gitStream.write("\n") change = int(details["change"]) self.lastChange = change if change in self.labels: label = self.labels[change] labelDetails = label[0] labelRevisions = label[1] files = p4CmdList("files %s...@%s" % (branchPrefix, change)) if len(files) == len(labelRevisions): cleanedFiles = {} for info in files: if info["action"] == "delete": continue cleanedFiles[info["depotFile"]] = info["rev"] if cleanedFiles == labelRevisions: self.gitStream.write("tag tag_%s\n" % labelDetails["label"]) self.gitStream.write("from %s\n" % branch) owner = labelDetails["Owner"] tagger = "" if author in self.users: tagger = "%s %s %s" % (self.users[owner], epoch, self.tz) else: tagger = "%s %s %s" % (owner, epoch, self.tz) self.gitStream.write("tagger %s\n" % tagger) self.gitStream.write("data <" def getLabels(self): self.labels = {} l = p4CmdList("labels %s..." % self.depotPath) if len(l) > 0 and not self.silent: print "Finding files belonging to labels in %s" % self.depotPath for output in l: label = output["label"] revisions = {} newestChange = 0 for file in p4CmdList("files //...@%s" % label): revisions[file["depotFile"]] = file["rev"] change = int(file["change"]) if change > newestChange: newestChange = change self.labels[newestChange] = [output, revisions] def getBranchMapping(self): # map from branch depot path to parent branch self.knownBranches = {} for info in p4CmdList("branches"): details = p4Cmd("branch -o %s" % info["branch"]) viewIdx = 0 while details.has_key("View%s" % viewIdx): paths = details["View%s" % viewIdx].split(" ") viewIdx = viewIdx + 1 # require standard //depot/foo/... //depot/bar/... mapping if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."): continue source = paths[0] destination = paths[1] if source.startswith(self.depotPath) and destination.startswith(self.depotPath): source = source[len(self.depotPath):-4] destination = destination[len(self.depotPath):-4] self.knownBranches[destination] = source self.knownBranches[source] = source def run(self, args): self.depotPath = "" self.changeRange = "" self.initialParent = "" self.previousDepotPath = "" if self.syncWithOrigin and gitBranchExists("origin") and gitBranchExists("refs/remotes/p4/master"): print "Syncing with origin first as requested by calling git fetch origin" system("git fetch origin") [originPreviousDepotPath, originP4Change] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("origin")) [p4PreviousDepotPath, p4Change] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit("p4")) if len(originPreviousDepotPath) > 0 and len(originP4Change) > 0 and len(p4Change) > 0: if originPreviousDepotPath == p4PreviousDepotPath: originP4Change = int(originP4Change) p4Change = int(p4Change) if originP4Change > p4Change: print "origin (%s) is newer than p4 (%s). Updating p4 branch from origin." % (originP4Change, p4Change) system("git update-ref refs/remotes/p4/master origin"); else: print "Cannot sync with origin. It was imported from %s while remotes/p4 was imported from %s" % (originPreviousDepotPath, p4PreviousDepotPath) if len(self.branch) == 0: self.branch = "refs/remotes/p4/master" if gitBranchExists("refs/heads/p4"): system("git update-ref %s refs/heads/p4" % self.branch) system("git branch -D p4"); if not gitBranchExists("refs/remotes/p4/HEAD"): system("git symbolic-ref refs/remotes/p4/HEAD %s" % self.branch) if len(args) == 0: if not gitBranchExists(self.branch) and gitBranchExists("origin"): if not self.silent: print "Creating %s branch in git repository based on origin" % self.branch branch = self.branch if not branch.startswith("refs"): branch = "refs/heads/" + branch system("git update-ref %s origin" % branch) [self.previousDepotPath, p4Change] = extractDepotPathAndChangeFromGitLog(extractLogMessageFromGitCommit(self.branch)) if len(self.previousDepotPath) > 0 and len(p4Change) > 0: p4Change = int(p4Change) + 1 self.depotPath = self.previousDepotPath self.changeRange = "@%s,#head" % p4Change self.initialParent = parseRevision(self.branch) if not self.silent: print "Performing incremental import into %s git branch" % self.branch if not self.branch.startswith("refs/"): self.branch = "refs/heads/" + self.branch if len(self.depotPath) != 0: self.depotPath = self.depotPath[:-1] if len(args) == 0 and len(self.depotPath) != 0: if not self.silent: print "Depot path: %s" % self.depotPath elif len(args) != 1: return False else: if len(self.depotPath) != 0 and self.depotPath != args[0]: print "previous import used depot path %s and now %s was specified. this doesn't work!" % (self.depotPath, args[0]) sys.exit(1) self.depotPath = args[0] self.revision = "" self.users = {} self.lastChange = 0 if self.depotPath.find("@") != -1: atIdx = self.depotPath.index("@") self.changeRange = self.depotPath[atIdx:] if self.changeRange == "@all": self.changeRange = "" elif self.changeRange.find(",") == -1: self.revision = self.changeRange self.changeRange = "" self.depotPath = self.depotPath[0:atIdx] elif self.depotPath.find("#") != -1: hashIdx = self.depotPath.index("#") self.revision = self.depotPath[hashIdx:] self.depotPath = self.depotPath[0:hashIdx] elif len(self.previousDepotPath) == 0: self.revision = "#head" if self.depotPath.endswith("..."): self.depotPath = self.depotPath[:-3] if not self.depotPath.endswith("/"): self.depotPath += "/" self.getUserMap() self.labels = {} if self.detectLabels: self.getLabels(); if self.detectBranches: self.getBranchMapping(); self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60)) importProcess = subprocess.Popen(["git", "fast-import"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE); self.gitOutput = importProcess.stdout self.gitStream = importProcess.stdin self.gitError = importProcess.stderr if len(self.revision) > 0: print "Doing initial import of %s from revision %s" % (self.depotPath, self.revision) details = { "user" : "git perforce import user", "time" : int(time.time()) } details["desc"] = "Initial import of %s from the state at revision %s" % (self.depotPath, self.revision) details["change"] = self.revision newestRevision = 0 fileCnt = 0 for info in p4CmdList("files %s...%s" % (self.depotPath, self.revision)): change = int(info["change"]) if change > newestRevision: newestRevision = change if info["action"] == "delete": # don't increase the file cnt, otherwise details["depotFile123"] will have gaps! #fileCnt = fileCnt + 1 continue for prop in [ "depotFile", "rev", "action", "type" ]: details["%s%s" % (prop, fileCnt)] = info[prop] fileCnt = fileCnt + 1 details["change"] = newestRevision try: self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPath) except IOError: print "IO error with git fast-import. Is your git version recent enough?" print self.gitError.read() else: changes = [] if len(self.changesFile) > 0: output = open(self.changesFile).readlines() changeSet = Set() for line in output: changeSet.add(int(line)) for change in changeSet: changes.append(change) changes.sort() else: output = mypopen("p4 changes %s...%s" % (self.depotPath, self.changeRange)).readlines() for line in output: changeNum = line.split(" ")[1] changes.append(changeNum) changes.reverse() if len(changes) == 0: if not self.silent: print "no changes to import!" return True cnt = 1 for change in changes: description = p4Cmd("describe %s" % change) if not self.silent: sys.stdout.write("\rimporting revision %s (%s%%)" % (change, cnt * 100 / len(changes))) sys.stdout.flush() cnt = cnt + 1 try: files = self.extractFilesFromCommit(description) if self.detectBranches: for branch in self.branchesForCommit(files): branchPrefix = self.depotPath + branch + "/" parent = "" filesForCommit = self.extractFilesInCommitToBranch(files, branch) if branch not in self.createdBranches : self.createdBranches.add(branch) parent = self.knownBranches[branch] if parent == branch: parent = "" branch = "refs/remotes/p4/" + branch if len(parent) > 0: parent = "refs/remotes/p4/" + parent self.commit(description, files, branch, branchPrefix, parent) else: self.commit(description, files, self.branch, self.depotPath, self.initialParent) self.initialParent = "" except IOError: print self.gitError.read() sys.exit(1) if not self.silent: print "" self.gitStream.close() self.gitOutput.close() self.gitError.close() importProcess.wait() return True class P4Rebase(Command): def __init__(self): Command.__init__(self) self.options = [ optparse.make_option("--with-origin", dest="syncWithOrigin", action="store_true") ] self.description = "Fetches the latest revision from perforce and rebases the current work (branch) against it" self.syncWithOrigin = False def run(self, args): sync = P4Sync() sync.syncWithOrigin = self.syncWithOrigin sync.run([]) print "Rebasing the current branch" oldHead = mypopen("git rev-parse HEAD").read()[:-1] system("git rebase p4") system("git diff-tree --stat --summary -M %s HEAD" % oldHead) return True class P4Clone(P4Sync): def __init__(self): P4Sync.__init__(self) self.description = "Creates a new git repository and imports from Perforce into it" self.usage = "usage: %prog [options] //depot/path[@revRange] [directory]" self.needsGit = False def run(self, args): if len(args) < 1: return False depotPath = args[0] dir = "" if len(args) == 2: dir = args[1] elif len(args) > 2: return False if not depotPath.startswith("//"): return False if len(dir) == 0: dir = depotPath atPos = dir.rfind("@") if atPos != -1: dir = dir[0:atPos] hashPos = dir.rfind("#") if hashPos != -1: dir = dir[0:hashPos] if dir.endswith("..."): dir = dir[:-3] if dir.endswith("/"): dir = dir[:-1] slashPos = dir.rfind("/") if slashPos != -1: dir = dir[slashPos + 1:] print "Importing from %s into %s" % (depotPath, dir) os.makedirs(dir) os.chdir(dir) system("git init") if not P4Sync.run(self, [depotPath]): return False if self.branch != "master": system("git branch master p4") system("git checkout -f") return True class HelpFormatter(optparse.IndentedHelpFormatter): def __init__(self): optparse.IndentedHelpFormatter.__init__(self) def format_description(self, description): if description: return description + "\n" else: return "" def printUsage(commands): print "usage: %s [options]" % sys.argv[0] print "" print "valid commands: %s" % ", ".join(commands) print "" print "Try %s --help for command specific help." % sys.argv[0] print "" commands = { "debug" : P4Debug(), "submit" : P4Submit(), "sync" : P4Sync(), "rebase" : P4Rebase(), "clone" : P4Clone() } if len(sys.argv[1:]) == 0: printUsage(commands.keys()) sys.exit(2) cmd = "" cmdName = sys.argv[1] try: cmd = commands[cmdName] except KeyError: print "unknown command %s" % cmdName print "" printUsage(commands.keys()) sys.exit(2) options = cmd.options cmd.gitdir = gitdir args = sys.argv[2:] if len(options) > 0: options.append(optparse.make_option("--git-dir", dest="gitdir")) parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName), options, description = cmd.description, formatter = HelpFormatter()) (cmd, args) = parser.parse_args(sys.argv[2:], cmd); if cmd.needsGit: gitdir = cmd.gitdir if len(gitdir) == 0: gitdir = ".git" if not isValidGitDir(gitdir): gitdir = mypopen("git rev-parse --git-dir").read()[:-1] if os.path.exists(gitdir): cdup = mypopen("git rev-parse --show-cdup").read()[:-1]; if len(cdup) > 0: os.chdir(cdup); if not isValidGitDir(gitdir): if isValidGitDir(gitdir + "/.git"): gitdir += "/.git" else: die("fatal: cannot locate git repository at %s" % gitdir) os.environ["GIT_DIR"] = gitdir if not cmd.run(args): parser.print_help()