#!/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 # # TODO: Add an option to sync/rebase to fetch and rebase from origin first. # 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 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 P4CleanTags(Command): def __init__(self): Command.__init__(self) self.options = [ # optparse.make_option("--branch", dest="branch", default="refs/heads/master") ] self.description = "A tool to remove stale unused tags from incremental perforce imports." def run(self, args): branch = currentGitBranch() print "Cleaning out stale p4 import tags..." sout, sin, serr = popen2.popen3("git name-rev --tags `git rev-parse %s`" % branch) output = sout.read() try: tagIdx = output.index(" tags/p4/") except: print "Cannot find any p4/* tag. Nothing to do." sys.exit(0) try: caretIdx = output.index("^") except: caretIdx = len(output) - 1 rev = int(output[tagIdx + 9 : caretIdx]) allTags = mypopen("git tag -l p4/").readlines() for i in range(len(allTags)): allTags[i] = int(allTags[i][3:-1]) allTags.sort() allTags.remove(rev) for rev in allTags: print mypopen("git tag -d p4/%s" % rev).read() print "%s tags removed." % len(allTags) 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("--known-branches", dest="knownBranches"), optparse.make_option("--data-cache", dest="dataCache", action="store_true"), optparse.make_option("--command-cache", dest="commandCache", action="store_true"), optparse.make_option("--detect-labels", dest="detectLabels", 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.dataCache = False self.commandCache = False self.silent = False self.knownBranches = Set() self.createdBranches = Set() self.committedChanges = Set() self.branch = "" self.detectBranches = False self.detectLabels = False self.changesFile = "" 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 isSubPathOf(self, first, second): if not first.startswith(second): return False if first == second: return True return first[len(second)] == "/" def branchesForCommit(self, files): branches = Set() for file in files: relativePath = file["path"][len(self.depotPath):] # strip off the filename relativePath = relativePath[0:relativePath.rfind("/")] # if len(branches) == 0: # branches.add(relativePath) # knownBranches.add(relativePath) # continue ###### this needs more testing :) knownBranch = False for branch in branches: if relativePath == branch: knownBranch = True break # if relativePath.startswith(branch): if self.isSubPathOf(relativePath, branch): knownBranch = True break # if branch.startswith(relativePath): if self.isSubPathOf(branch, relativePath): branches.remove(branch) break if knownBranch: continue for branch in self.knownBranches: #if relativePath.startswith(branch): if self.isSubPathOf(relativePath, branch): if len(branches) == 0: relativePath = branch else: knownBranch = True break if knownBranch: continue branches.add(relativePath) self.knownBranches.add(relativePath) return branches def findBranchParent(self, branchPrefix, files): for file in files: path = file["path"] if not path.startswith(branchPrefix): continue action = file["action"] if action != "integrate" and action != "branch": continue rev = file["rev"] depotPath = path + "#" + rev log = p4CmdList("filelog \"%s\"" % depotPath) if len(log) != 1: print "eek! I got confused by the filelog of %s" % depotPath sys.exit(1); log = log[0] if log["action0"] != action: print "eek! wrong action in filelog for %s : found %s, expected %s" % (depotPath, log["action0"], action) sys.exit(1); branchAction = log["how0,0"] # if branchAction == "branch into" or branchAction == "ignored": # continue # ignore for branching if not branchAction.endswith(" from"): continue # ignore for branching # print "eek! file %s was not branched from but instead: %s" % (depotPath, branchAction) # sys.exit(1); source = log["file0,0"] if source.startswith(branchPrefix): continue lastSourceRev = log["erev0,0"] sourceLog = p4CmdList("filelog -m 1 \"%s%s\"" % (source, lastSourceRev)) if len(sourceLog) != 1: print "eek! I got confused by the source filelog of %s%s" % (source, lastSourceRev) sys.exit(1); sourceLog = sourceLog[0] relPath = source[len(self.depotPath):] # strip off the filename relPath = relPath[0:relPath.rfind("/")] for branch in self.knownBranches: if self.isSubPathOf(relPath, branch): # print "determined parent branch branch %s due to change in file %s" % (branch, source) return branch # else: # print "%s is not a subpath of branch %s" % (relPath, branch) return "" def commit(self, details, files, branch, branchPrefix, parent = "", merged = ""): epoch = details["time"] author = details["user"] 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: self.gitStream.write("from %s\n" % parent) if len(merged) > 0: self.gitStream.write("merge %s\n" % merged) 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 < change: continue destRev = int(destFile["rev"]) startRev = integration["startFromRev"][1:] if startRev == "none": startRev = 0 else: startRev = int(startRev) endRev = integration["endFromRev"][1:] if endRev == "none": endRev = 0 else: endRev = int(endRev) initialBranch = (destRev == 1 and integration["how"] != "branch into") inRange = (destRev >= startRev and destRev <= endRev) newer = (destRev > startRev and destRev > endRev) if initialBranch or inRange or newer: integrations.append(integration) if deleted: continue if len(integrations) == 0 and integrationCount > 1: print "file %s was not integrated from %s into %s" % (fileName, sourceBranch, destinationBranch) return False return True def getUserMap(self): self.users = {} for output in p4CmdList("users"): if not output.has_key("User"): continue self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">" 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 run(self, args): self.depotPath = "" self.changeRange = "" self.initialParent = "" self.previousDepotPath = "" if len(self.branch) == 0: self.branch = "p4" 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 system("git branch %s origin" % self.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 = self.branch if not self.silent: print "Performing incremental import into %s git branch" % self.branch 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 len(self.changeRange) == 0: try: sout, sin, serr = popen2.popen3("git name-rev --tags `git rev-parse %s`" % self.branch) output = sout.read() if output.endswith("\n"): output = output[:-1] tagIdx = output.index(" tags/p4/") caretIdx = output.find("^") endPos = len(output) if caretIdx != -1: endPos = caretIdx self.rev = int(output[tagIdx + 9 : endPos]) + 1 self.changeRange = "@%s,#head" % self.rev self.initialParent = mypopen("git rev-parse %s" % self.branch).read()[:-1] except: pass 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): self.knownBranches.add(branch) branchPrefix = self.depotPath + branch + "/" filesForCommit = self.extractFilesInCommitToBranch(files, branchPrefix) merged = "" parent = "" ########### remove cnt!!! if branch not in self.createdBranches and cnt > 2: self.createdBranches.add(branch) parent = self.findBranchParent(branchPrefix, files) if parent == branch: parent = "" # elif len(parent) > 0: # print "%s branched off of %s" % (branch, parent) if len(parent) == 0: merged = self.findBranchSourceHeuristic(filesForCommit, branch, branchPrefix) if len(merged) > 0: print "change %s could be a merge from %s into %s" % (description["change"], merged, branch) if not self.changeIsBranchMerge(merged, branch, int(description["change"])): merged = "" branch = "refs/heads/" + branch if len(parent) > 0: parent = "refs/heads/" + parent if len(merged) > 0: merged = "refs/heads/" + merged self.commit(description, files, branch, branchPrefix, parent, merged) 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 = [ ] self.description = "Fetches the latest revision from perforce and rebases the current work (branch) against it" def run(self, args): sync = P4Sync() 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(), "clean-tags" : P4CleanTags(), "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()