# 2007 Trolltech ASA
# License: MIT <http://www.opensource.org/licenses/mit-license.php>
#
+# TODO: * implement git-p4 rollback <perforce change number> for debugging
+# to roll back all p4 remote branches to a commit older or equal to
+# the specified change.
+# * for git-p4 submit --direct it would be nice to still create a
+# git commit without updating HEAD before submitting to perforce.
+# With the commit sha1 printed (or recoded in a .git/foo file?)
+# it's possible to recover if anything goes wrong instead of potentially
+# loosing a change entirely because it was never comitted to git and
+# the p4 submit failed (or resulted in lots of conflicts, etc.)
+# * Consider making --with-origin the default, assuming that the git
+# protocol is always more efficient. (needs manual testing first :)
+#
import optparse, sys, os, marshal, popen2, subprocess, shelve
import tempfile, getopt, sha, os.path, time, platform
if not depotPath.endswith("/"):
depotPath += "/"
output = p4Cmd("where %s..." % depotPath)
+ if output["code"] == "error":
+ return ""
clientPath = ""
if "path" in output:
clientPath = output.get("path")
optparse.make_option("--log-substitutions", dest="substFile"),
optparse.make_option("--noninteractive", action="store_false"),
optparse.make_option("--dry-run", action="store_true"),
+ optparse.make_option("--direct", dest="directSubmit", 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.substFile = ""
self.firstTime = True
self.origin = ""
+ self.directSubmit = False
self.logSubstitutions = {}
self.logSubstitutions["<enter description here>"] = "%log%"
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()
+ if self.directSubmit:
+ commits.append("0")
+ else:
+ 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
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()
+ if self.directSubmit:
+ print "Applying local change in working directory/index"
+ diff = self.diffStatus
+ else:
+ 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()
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 self.directSubmit:
+ diffcmd = "cat \"%s\"" % self.diffFile
+ else:
+ diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
+ patchcmd = diffcmd + " | git apply "
+ tryPatchCmd = patchcmd + "--check -"
+ applyPatchCmd = patchcmd + "--check --apply -"
- if os.system(patchcmd + " --dry-run --silent") != 0:
+ if os.system(tryPatchCmd) != 0:
print "Unfortunately applying the change failed!"
print "What do you want to do?"
response = "x"
print "Skipping! Good luck with the next patches..."
return
elif response == "a":
- os.system(patchcmd)
+ os.system(applyPatchCmd)
if len(filesToAdd) > 0:
print "You may also want to call p4 add on the following files:"
print " ".join(filesToAdd)
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)
+ system(applyPatchCmd)
for f in filesToAdd:
system("p4 add %s" % f)
system("p4 revert %s" % f)
system("p4 delete %s" % f)
- logMessage = extractLogMessageFromGitCommit(id)
- logMessage = logMessage.replace("\n", "\n\t")
- logMessage = logMessage[:-1]
+ logMessage = ""
+ if not self.directSubmit:
+ logMessage = extractLogMessageFromGitCommit(id)
+ logMessage = logMessage.replace("\n", "\n\t")
+ logMessage = logMessage[:-1]
template = mypopen("p4 change -o").read()
print "Perforce checkout for depot path %s located at %s" % (depotPath, self.clientPath)
oldWorkingDirectory = os.getcwd()
+
+ if self.directSubmit:
+ self.diffStatus = mypopen("git diff -r --name-status HEAD").readlines()
+ patch = mypopen("git diff -p --binary --diff-filter=ACMRTUXB HEAD").read()
+ self.diffFile = gitdir + "/p4-git-diff"
+ f = open(self.diffFile, "wb")
+ f.write(patch)
+ f.close();
+
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":
self.config.close()
+ if self.directSubmit:
+ os.remove(self.diffFile)
+
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 ")
+ response = ""
+ os.chdir(oldWorkingDirectory)
+
+ if self.directSubmit:
+ response = raw_input("Do you want to DISCARD your git WORKING DIRECTORY CHANGES and sync from Perforce now using git-p4 rebase? [y]es/[n]o ")
+ if response == "y" or response == "yes":
+ system("git reset --hard")
+
+ if len(response) == 0:
+ 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)
# gitStream.write("mark :%s\n" % details["change"])
self.committedChanges.add(int(details["change"]))
committer = ""
+ if author not in self.users:
+ self.getUserMapFromPerforceServer()
if author in self.users:
committer = "%s %s %s" % (self.users[author], epoch, self.tz)
else:
change = int(details["change"])
- self.lastChange = change
-
- if change in self.labels:
+ if self.labels.has_key(change):
label = self.labels[change]
labelDetails = label[0]
labelRevisions = label[1]
if not self.silent:
print "Tag %s does not match with change %s: file count is different." % (labelDetails["label"], change)
- def getUserMap(self):
+ def getUserMapFromPerforceServer(self):
self.users = {}
for output in p4CmdList("users"):
continue
self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
+ cache = open(gitdir + "/p4-usercache.txt", "wb")
+ for user in self.users.keys():
+ cache.write("%s\t%s\n" % (user, self.users[user]))
+ cache.close();
+
+ def loadUserMapFromCache(self):
+ self.users = {}
+ try:
+ cache = open(gitdir + "/p4-usercache.txt", "rb")
+ lines = cache.readlines()
+ cache.close()
+ for line in lines:
+ entry = line[:-1].split("\t")
+ self.users[entry[0]] = entry[1]
+ except IOError:
+ self.getUserMapFromPerforceServer()
+
def getLabels(self):
self.labels = {}
if change > newestChange:
newestChange = change
- self.labels[int(newestChange)] = [output, revisions]
+ self.labels[newestChange] = [output, revisions]
+
+ if self.verbose:
+ print "Label changes: %s" % self.labels.keys()
def getBranchMapping(self):
self.projectName = self.depotPath[self.depotPath[:-1].rfind("/") + 1:]
self.knownBranches = {}
self.initialParents = {}
- self.listExistingP4GitBranches()
-
if self.syncWithOrigin and gitBranchExists("origin") and gitBranchExists("refs/remotes/p4/master") and not self.detectBranches:
### needs to be ported to multi branch import
if not gitBranchExists("refs/remotes/p4/HEAD"):
system("git symbolic-ref refs/remotes/p4/HEAD %s" % self.branch)
+ # this needs to be called after the conversion from heads/p4 to remotes/p4/master
+ self.listExistingP4GitBranches()
+ if len(self.p4BranchesInGit) > 1 and not self.silent:
+ print "Importing from/into multiple branches"
+ self.detectBranches = True
+
if len(args) == 0:
if not gitBranchExists(self.branch) and gitBranchExists("origin") and not self.detectBranches:
### needs to be ported to multi branch import
self.depotPath = self.previousDepotPath
self.changeRange = "@%s,#head" % p4Change
self.initialParent = parseRevision(self.branch)
- if not self.silent:
+ if not self.silent and not self.detectBranches:
print "Performing incremental import into %s git branch" % self.branch
if not self.branch.startswith("refs/"):
self.revision = ""
self.users = {}
- self.lastChange = 0
if self.depotPath.find("@") != -1:
atIdx = self.depotPath.index("@")
if not self.depotPath.endswith("/"):
self.depotPath += "/"
- self.getUserMap()
+ self.loadUserMapFromCache()
self.labels = {}
if self.detectLabels:
self.getLabels();
if len(changes) == 0:
if not self.silent:
- print "no changes to import!"
+ print "No changes to import!"
return True
+ self.updatedBranches = set()
+
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.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
sys.stdout.flush()
cnt = cnt + 1
if self.verbose:
print "branch is %s" % branch
+ self.updatedBranches.add(branch)
+
if branch not in self.createdBranches:
self.createdBranches.add(branch)
parent = self.knownBranches[branch]
print self.gitError.read()
sys.exit(1)
- if not self.silent:
- print ""
+ if not self.silent:
+ print ""
+ if len(self.updatedBranches) > 0:
+ sys.stdout.write("Updated branches: ")
+ for b in self.updatedBranches:
+ sys.stdout.write("%s " % b)
+ sys.stdout.write("\n")
self.gitStream.close()
self.needsGit = False
def run(self, args):
+ global gitdir
+
if len(args) < 1:
return False
depotPath = args[0]
os.makedirs(dir)
os.chdir(dir)
system("git init")
+ gitdir = os.getcwd() + "/.git"
if not P4Sync.run(self, [depotPath]):
return False
if self.branch != "master":