aboutsummaryrefslogtreecommitdiffstats
path: root/git-p4.py
diff options
context:
space:
mode:
authorLuke Diamand <luke@diamand.org>2018-05-23 23:20:26 +0100
committerJunio C Hamano <gitster@pobox.com>2018-05-24 08:58:28 +0900
commit123f631761dab8c37391ba1584122c2578f51923 (patch)
treeeb83d531d38c473363e1669496c91d21c781e57c /git-p4.py
parente3a80781f5932f5fea12a49eb06f3ade4ed8945c (diff)
downloadgit-123f631761dab8c37391ba1584122c2578f51923.tar.gz
git-p4: add unshelve command
This can be used to "unshelve" a shelved P4 commit into a git commit. For example: $ git p4 unshelve 12345 The resulting commit ends up in the branch: refs/remotes/p4/unshelved/12345 If that branch already exists, it is renamed - for example the above branch would be saved as p4/unshelved/12345.1. git-p4 checks that the shelved changelist is based on files which are at the same Perforce revision as the origin branch being used for the unshelve (HEAD by default). If they are not, it will refuse to unshelve. This is to ensure that the unshelved change does not contain other changes mixed-in. The reference branch can be changed manually with the "--origin" option. The change adds a new Unshelve command class. This just runs the existing P4Sync code tweaked to handle a shelved changelist. Signed-off-by: Luke Diamand <luke@diamand.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Diffstat (limited to 'git-p4.py')
-rwxr-xr-xgit-p4.py213
1 files changed, 177 insertions, 36 deletions
diff --git a/git-p4.py b/git-p4.py
index 7bb9cadc69..74d58afad3 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -316,12 +316,17 @@ def p4_last_change():
results = p4CmdList(["changes", "-m", "1"], skip_info=True)
return int(results[0]['change'])
-def p4_describe(change):
+def p4_describe(change, shelved=False):
"""Make sure it returns a valid result by checking for
the presence of field "time". Return a dict of the
results."""
- ds = p4CmdList(["describe", "-s", str(change)], skip_info=True)
+ cmd = ["describe", "-s"]
+ if shelved:
+ cmd += ["-S"]
+ cmd += [str(change)]
+
+ ds = p4CmdList(cmd, skip_info=True)
if len(ds) != 1:
die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
@@ -662,6 +667,12 @@ def gitBranchExists(branch):
stderr=subprocess.PIPE, stdout=subprocess.PIPE);
return proc.wait() == 0;
+def gitUpdateRef(ref, newvalue):
+ subprocess.check_call(["git", "update-ref", ref, newvalue])
+
+def gitDeleteRef(ref):
+ subprocess.check_call(["git", "update-ref", "-d", ref])
+
_gitConfig = {}
def gitConfig(key, typeSpecifier=None):
@@ -2411,6 +2422,7 @@ class P4Sync(Command, P4UserMap):
self.tempBranches = []
self.tempBranchLocation = "refs/git-p4-tmp"
self.largeFileSystem = None
+ self.suppress_meta_comment = False
if gitConfig('git-p4.largeFileSystem'):
largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
@@ -2421,6 +2433,18 @@ class P4Sync(Command, P4UserMap):
if gitConfig("git-p4.syncFromOrigin") == "false":
self.syncWithOrigin = False
+ self.depotPaths = []
+ self.changeRange = ""
+ self.previousDepotPaths = []
+ self.hasOrigin = False
+
+ # map from branch depot path to parent branch
+ self.knownBranches = {}
+ self.initialParents = {}
+
+ self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
+ self.labels = {}
+
# Force a checkpoint in fast-import and wait for it to finish
def checkpoint(self):
self.gitStream.write("checkpoint\n\n")
@@ -2429,7 +2453,20 @@ class P4Sync(Command, P4UserMap):
if self.verbose:
print "checkpoint finished: " + out
- def extractFilesFromCommit(self, commit):
+ def cmp_shelved(self, path, filerev, revision):
+ """ Determine if a path at revision #filerev is the same as the file
+ at revision @revision for a shelved changelist. If they don't match,
+ unshelving won't be safe (we will get other changes mixed in).
+
+ This is comparing the revision that the shelved changelist is *based* on, not
+ the shelved changelist itself.
+ """
+ ret = p4Cmd(["diff2", "{0}#{1}".format(path, filerev), "{0}@{1}".format(path, revision)])
+ if verbose:
+ print("p4 diff2 path %s filerev %s revision %s => %s" % (path, filerev, revision, ret))
+ return ret["status"] == "identical"
+
+ def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0, origin_revision = 0):
self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
for path in self.cloneExclude]
files = []
@@ -2452,6 +2489,19 @@ class P4Sync(Command, P4UserMap):
file["rev"] = commit["rev%s" % fnum]
file["action"] = commit["action%s" % fnum]
file["type"] = commit["type%s" % fnum]
+ if shelved:
+ file["shelved_cl"] = int(shelved_cl)
+
+ # For shelved changelists, check that the revision of each file that the
+ # shelve was based on matches the revision that we are using for the
+ # starting point for git-fast-import (self.initialParent). Otherwise
+ # the resulting diff will contain deltas from multiple commits.
+
+ if file["action"] != "add" and \
+ not self.cmp_shelved(path, file["rev"], origin_revision):
+ sys.exit("change {0} not based on {1} for {2}, cannot unshelve".format(
+ commit["change"], self.initialParent, path))
+
files.append(file)
fnum = fnum + 1
return files
@@ -2743,7 +2793,16 @@ class P4Sync(Command, P4UserMap):
def streamP4FilesCbSelf(entry):
self.streamP4FilesCb(entry)
- fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
+ fileArgs = []
+ for f in filesToRead:
+ if 'shelved_cl' in f:
+ # Handle shelved CLs using the "p4 print file@=N" syntax to print
+ # the contents
+ fileArg = '%s@=%d' % (f['path'], f['shelved_cl'])
+ else:
+ fileArg = '%s#%s' % (f['path'], f['rev'])
+
+ fileArgs.append(fileArg)
p4CmdList(["-x", "-", "print"],
stdin=fileArgs,
@@ -2844,11 +2903,15 @@ class P4Sync(Command, P4UserMap):
self.gitStream.write(details["desc"])
if len(jobs) > 0:
self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
- self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
- (','.join(self.branchPrefixes), details["change"]))
- if len(details['options']) > 0:
- self.gitStream.write(": options = %s" % details['options'])
- self.gitStream.write("]\nEOT\n\n")
+
+ if not self.suppress_meta_comment:
+ self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
+ (','.join(self.branchPrefixes), details["change"]))
+ if len(details['options']) > 0:
+ self.gitStream.write(": options = %s" % details['options'])
+ self.gitStream.write("]\n")
+
+ self.gitStream.write("EOT\n\n")
if len(parent) > 0:
if self.verbose:
@@ -3162,10 +3225,10 @@ class P4Sync(Command, P4UserMap):
else:
return None
- def importChanges(self, changes):
+ def importChanges(self, changes, shelved=False, origin_revision=0):
cnt = 1
for change in changes:
- description = p4_describe(change)
+ description = p4_describe(change, shelved)
self.updateOptionDict(description)
if not self.silent:
@@ -3235,7 +3298,7 @@ class P4Sync(Command, P4UserMap):
print "Parent of %s not found. Committing into head of %s" % (branch, parent)
self.commit(description, filesForCommit, branch, parent)
else:
- files = self.extractFilesFromCommit(description)
+ files = self.extractFilesFromCommit(description, shelved, change, origin_revision)
self.commit(description, files, self.branch,
self.initialParent)
# only needed once, to connect to the previous commit
@@ -3300,17 +3363,23 @@ class P4Sync(Command, P4UserMap):
print "IO error with git fast-import. Is your git version recent enough?"
print self.gitError.read()
+ def openStreams(self):
+ self.importProcess = subprocess.Popen(["git", "fast-import"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE);
+ self.gitOutput = self.importProcess.stdout
+ self.gitStream = self.importProcess.stdin
+ self.gitError = self.importProcess.stderr
- def run(self, args):
- self.depotPaths = []
- self.changeRange = ""
- self.previousDepotPaths = []
- self.hasOrigin = False
-
- # map from branch depot path to parent branch
- self.knownBranches = {}
- self.initialParents = {}
+ def closeStreams(self):
+ self.gitStream.close()
+ if self.importProcess.wait() != 0:
+ die("fast-import failed: %s" % self.gitError.read())
+ self.gitOutput.close()
+ self.gitError.close()
+ def run(self, args):
if self.importIntoRemotes:
self.refPrefix = "refs/remotes/p4/"
else:
@@ -3497,15 +3566,7 @@ class P4Sync(Command, P4UserMap):
b = b[len(self.projectName):]
self.createdBranches.add(b)
- self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
-
- self.importProcess = subprocess.Popen(["git", "fast-import"],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE);
- self.gitOutput = self.importProcess.stdout
- self.gitStream = self.importProcess.stdin
- self.gitError = self.importProcess.stderr
+ self.openStreams()
if revision:
self.importHeadRevision(revision)
@@ -3585,11 +3646,7 @@ class P4Sync(Command, P4UserMap):
missingP4Labels = p4Labels - gitTags
self.importP4Labels(self.gitStream, missingP4Labels)
- self.gitStream.close()
- if self.importProcess.wait() != 0:
- die("fast-import failed: %s" % self.gitError.read())
- self.gitOutput.close()
- self.gitError.close()
+ self.closeStreams()
# Cleanup temporary branches created during import
if self.tempBranches != []:
@@ -3721,6 +3778,89 @@ class P4Clone(P4Sync):
return True
+class P4Unshelve(Command):
+ def __init__(self):
+ Command.__init__(self)
+ self.options = []
+ self.origin = "HEAD"
+ self.description = "Unshelve a P4 changelist into a git commit"
+ self.usage = "usage: %prog [options] changelist"
+ self.options += [
+ optparse.make_option("--origin", dest="origin",
+ help="Use this base revision instead of the default (%s)" % self.origin),
+ ]
+ self.verbose = False
+ self.noCommit = False
+ self.destbranch = "refs/remotes/p4/unshelved"
+
+ def renameBranch(self, branch_name):
+ """ Rename the existing branch to branch_name.N
+ """
+
+ found = True
+ for i in range(0,1000):
+ backup_branch_name = "{0}.{1}".format(branch_name, i)
+ if not gitBranchExists(backup_branch_name):
+ gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
+ gitDeleteRef(branch_name)
+ found = True
+ print("renamed old unshelve branch to {0}".format(backup_branch_name))
+ break
+
+ if not found:
+ sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
+
+ def findLastP4Revision(self, starting_point):
+ """ Look back from starting_point for the first commit created by git-p4
+ to find the P4 commit we are based on, and the depot-paths.
+ """
+
+ for parent in (range(65535)):
+ log = extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))
+ settings = extractSettingsGitLog(log)
+ if settings.has_key('change'):
+ return settings
+
+ sys.exit("could not find git-p4 commits in {0}".format(self.origin))
+
+ def run(self, args):
+ if len(args) != 1:
+ return False
+
+ if not gitBranchExists(self.origin):
+ sys.exit("origin branch {0} does not exist".format(self.origin))
+
+ sync = P4Sync()
+ changes = args
+ sync.initialParent = self.origin
+
+ # use the first change in the list to construct the branch to unshelve into
+ change = changes[0]
+
+ # if the target branch already exists, rename it
+ branch_name = "{0}/{1}".format(self.destbranch, change)
+ if gitBranchExists(branch_name):
+ self.renameBranch(branch_name)
+ sync.branch = branch_name
+
+ sync.verbose = self.verbose
+ sync.suppress_meta_comment = True
+
+ settings = self.findLastP4Revision(self.origin)
+ origin_revision = settings['change']
+ sync.depotPaths = settings['depot-paths']
+ sync.branchPrefixes = sync.depotPaths
+
+ sync.openStreams()
+ sync.loadUserMapFromCache()
+ sync.silent = True
+ sync.importChanges(changes, shelved=True, origin_revision=origin_revision)
+ sync.closeStreams()
+
+ print("unshelved changelist {0} into {1}".format(change, branch_name))
+
+ return True
+
class P4Branches(Command):
def __init__(self):
Command.__init__(self)
@@ -3775,7 +3915,8 @@ commands = {
"rebase" : P4Rebase,
"clone" : P4Clone,
"rollback" : P4RollBack,
- "branches" : P4Branches
+ "branches" : P4Branches,
+ "unshelve" : P4Unshelve,
}