Skip to content

Commit 123f631

Browse files
luked99gitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent e3a8078 commit 123f631

File tree

3 files changed

+347
-36
lines changed

3 files changed

+347
-36
lines changed

Documentation/git-p4.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,31 @@ $ git p4 submit --shelve
164164
$ git p4 submit --update-shelve 1234 --update-shelve 2345
165165
----
166166

167+
168+
Unshelve
169+
~~~~~~~~
170+
Unshelving will take a shelved P4 changelist, and produce the equivalent git commit
171+
in the branch refs/remotes/p4/unshelved/<changelist>.
172+
173+
The git commit is created relative to the current origin revision (HEAD by default).
174+
If the shelved changelist's parent revisions differ, git-p4 will refuse to unshelve;
175+
you need to be unshelving onto an equivalent tree.
176+
177+
The origin revision can be changed with the "--origin" option.
178+
179+
If the target branch in refs/remotes/p4/unshelved already exists, the old one will
180+
be renamed.
181+
182+
----
183+
$ git p4 sync
184+
$ git p4 unshelve 12345
185+
$ git show refs/remotes/p4/unshelved/12345
186+
<submit more changes via p4 to the same files>
187+
$ git p4 unshelve 12345
188+
<refuses to unshelve until git is in sync with p4 again>
189+
190+
----
191+
167192
OPTIONS
168193
-------
169194

@@ -337,6 +362,13 @@ These options can be used to modify 'git p4 rebase' behavior.
337362
--import-labels::
338363
Import p4 labels.
339364

365+
Unshelve options
366+
~~~~~~~~~~~~~~~~
367+
368+
--origin::
369+
Sets the git refspec against which the shelved P4 changelist is compared.
370+
Defaults to p4/master.
371+
340372
DEPOT PATH SYNTAX
341373
-----------------
342374
The p4 depot path argument to 'git p4 sync' and 'git p4 clone' can

git-p4.py

Lines changed: 177 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -316,12 +316,17 @@ def p4_last_change():
316316
results = p4CmdList(["changes", "-m", "1"], skip_info=True)
317317
return int(results[0]['change'])
318318

319-
def p4_describe(change):
319+
def p4_describe(change, shelved=False):
320320
"""Make sure it returns a valid result by checking for
321321
the presence of field "time". Return a dict of the
322322
results."""
323323

324-
ds = p4CmdList(["describe", "-s", str(change)], skip_info=True)
324+
cmd = ["describe", "-s"]
325+
if shelved:
326+
cmd += ["-S"]
327+
cmd += [str(change)]
328+
329+
ds = p4CmdList(cmd, skip_info=True)
325330
if len(ds) != 1:
326331
die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
327332

@@ -662,6 +667,12 @@ def gitBranchExists(branch):
662667
stderr=subprocess.PIPE, stdout=subprocess.PIPE);
663668
return proc.wait() == 0;
664669

670+
def gitUpdateRef(ref, newvalue):
671+
subprocess.check_call(["git", "update-ref", ref, newvalue])
672+
673+
def gitDeleteRef(ref):
674+
subprocess.check_call(["git", "update-ref", "-d", ref])
675+
665676
_gitConfig = {}
666677

667678
def gitConfig(key, typeSpecifier=None):
@@ -2411,6 +2422,7 @@ def __init__(self):
24112422
self.tempBranches = []
24122423
self.tempBranchLocation = "refs/git-p4-tmp"
24132424
self.largeFileSystem = None
2425+
self.suppress_meta_comment = False
24142426

24152427
if gitConfig('git-p4.largeFileSystem'):
24162428
largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
@@ -2421,6 +2433,18 @@ def __init__(self):
24212433
if gitConfig("git-p4.syncFromOrigin") == "false":
24222434
self.syncWithOrigin = False
24232435

2436+
self.depotPaths = []
2437+
self.changeRange = ""
2438+
self.previousDepotPaths = []
2439+
self.hasOrigin = False
2440+
2441+
# map from branch depot path to parent branch
2442+
self.knownBranches = {}
2443+
self.initialParents = {}
2444+
2445+
self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2446+
self.labels = {}
2447+
24242448
# Force a checkpoint in fast-import and wait for it to finish
24252449
def checkpoint(self):
24262450
self.gitStream.write("checkpoint\n\n")
@@ -2429,7 +2453,20 @@ def checkpoint(self):
24292453
if self.verbose:
24302454
print "checkpoint finished: " + out
24312455

2432-
def extractFilesFromCommit(self, commit):
2456+
def cmp_shelved(self, path, filerev, revision):
2457+
""" Determine if a path at revision #filerev is the same as the file
2458+
at revision @revision for a shelved changelist. If they don't match,
2459+
unshelving won't be safe (we will get other changes mixed in).
2460+
2461+
This is comparing the revision that the shelved changelist is *based* on, not
2462+
the shelved changelist itself.
2463+
"""
2464+
ret = p4Cmd(["diff2", "{0}#{1}".format(path, filerev), "{0}@{1}".format(path, revision)])
2465+
if verbose:
2466+
print("p4 diff2 path %s filerev %s revision %s => %s" % (path, filerev, revision, ret))
2467+
return ret["status"] == "identical"
2468+
2469+
def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0, origin_revision = 0):
24332470
self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
24342471
for path in self.cloneExclude]
24352472
files = []
@@ -2452,6 +2489,19 @@ def extractFilesFromCommit(self, commit):
24522489
file["rev"] = commit["rev%s" % fnum]
24532490
file["action"] = commit["action%s" % fnum]
24542491
file["type"] = commit["type%s" % fnum]
2492+
if shelved:
2493+
file["shelved_cl"] = int(shelved_cl)
2494+
2495+
# For shelved changelists, check that the revision of each file that the
2496+
# shelve was based on matches the revision that we are using for the
2497+
# starting point for git-fast-import (self.initialParent). Otherwise
2498+
# the resulting diff will contain deltas from multiple commits.
2499+
2500+
if file["action"] != "add" and \
2501+
not self.cmp_shelved(path, file["rev"], origin_revision):
2502+
sys.exit("change {0} not based on {1} for {2}, cannot unshelve".format(
2503+
commit["change"], self.initialParent, path))
2504+
24552505
files.append(file)
24562506
fnum = fnum + 1
24572507
return files
@@ -2743,7 +2793,16 @@ def streamP4Files(self, files):
27432793
def streamP4FilesCbSelf(entry):
27442794
self.streamP4FilesCb(entry)
27452795

2746-
fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2796+
fileArgs = []
2797+
for f in filesToRead:
2798+
if 'shelved_cl' in f:
2799+
# Handle shelved CLs using the "p4 print file@=N" syntax to print
2800+
# the contents
2801+
fileArg = '%s@=%d' % (f['path'], f['shelved_cl'])
2802+
else:
2803+
fileArg = '%s#%s' % (f['path'], f['rev'])
2804+
2805+
fileArgs.append(fileArg)
27472806

27482807
p4CmdList(["-x", "-", "print"],
27492808
stdin=fileArgs,
@@ -2844,11 +2903,15 @@ def commit(self, details, files, branch, parent = ""):
28442903
self.gitStream.write(details["desc"])
28452904
if len(jobs) > 0:
28462905
self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
2847-
self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2848-
(','.join(self.branchPrefixes), details["change"]))
2849-
if len(details['options']) > 0:
2850-
self.gitStream.write(": options = %s" % details['options'])
2851-
self.gitStream.write("]\nEOT\n\n")
2906+
2907+
if not self.suppress_meta_comment:
2908+
self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2909+
(','.join(self.branchPrefixes), details["change"]))
2910+
if len(details['options']) > 0:
2911+
self.gitStream.write(": options = %s" % details['options'])
2912+
self.gitStream.write("]\n")
2913+
2914+
self.gitStream.write("EOT\n\n")
28522915

28532916
if len(parent) > 0:
28542917
if self.verbose:
@@ -3162,10 +3225,10 @@ def searchParent(self, parent, branch, target):
31623225
else:
31633226
return None
31643227

3165-
def importChanges(self, changes):
3228+
def importChanges(self, changes, shelved=False, origin_revision=0):
31663229
cnt = 1
31673230
for change in changes:
3168-
description = p4_describe(change)
3231+
description = p4_describe(change, shelved)
31693232
self.updateOptionDict(description)
31703233

31713234
if not self.silent:
@@ -3235,7 +3298,7 @@ def importChanges(self, changes):
32353298
print "Parent of %s not found. Committing into head of %s" % (branch, parent)
32363299
self.commit(description, filesForCommit, branch, parent)
32373300
else:
3238-
files = self.extractFilesFromCommit(description)
3301+
files = self.extractFilesFromCommit(description, shelved, change, origin_revision)
32393302
self.commit(description, files, self.branch,
32403303
self.initialParent)
32413304
# only needed once, to connect to the previous commit
@@ -3300,17 +3363,23 @@ def importHeadRevision(self, revision):
33003363
print "IO error with git fast-import. Is your git version recent enough?"
33013364
print self.gitError.read()
33023365

3366+
def openStreams(self):
3367+
self.importProcess = subprocess.Popen(["git", "fast-import"],
3368+
stdin=subprocess.PIPE,
3369+
stdout=subprocess.PIPE,
3370+
stderr=subprocess.PIPE);
3371+
self.gitOutput = self.importProcess.stdout
3372+
self.gitStream = self.importProcess.stdin
3373+
self.gitError = self.importProcess.stderr
33033374

3304-
def run(self, args):
3305-
self.depotPaths = []
3306-
self.changeRange = ""
3307-
self.previousDepotPaths = []
3308-
self.hasOrigin = False
3309-
3310-
# map from branch depot path to parent branch
3311-
self.knownBranches = {}
3312-
self.initialParents = {}
3375+
def closeStreams(self):
3376+
self.gitStream.close()
3377+
if self.importProcess.wait() != 0:
3378+
die("fast-import failed: %s" % self.gitError.read())
3379+
self.gitOutput.close()
3380+
self.gitError.close()
33133381

3382+
def run(self, args):
33143383
if self.importIntoRemotes:
33153384
self.refPrefix = "refs/remotes/p4/"
33163385
else:
@@ -3497,15 +3566,7 @@ def run(self, args):
34973566
b = b[len(self.projectName):]
34983567
self.createdBranches.add(b)
34993568

3500-
self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3501-
3502-
self.importProcess = subprocess.Popen(["git", "fast-import"],
3503-
stdin=subprocess.PIPE,
3504-
stdout=subprocess.PIPE,
3505-
stderr=subprocess.PIPE);
3506-
self.gitOutput = self.importProcess.stdout
3507-
self.gitStream = self.importProcess.stdin
3508-
self.gitError = self.importProcess.stderr
3569+
self.openStreams()
35093570

35103571
if revision:
35113572
self.importHeadRevision(revision)
@@ -3585,11 +3646,7 @@ def run(self, args):
35853646
missingP4Labels = p4Labels - gitTags
35863647
self.importP4Labels(self.gitStream, missingP4Labels)
35873648

3588-
self.gitStream.close()
3589-
if self.importProcess.wait() != 0:
3590-
die("fast-import failed: %s" % self.gitError.read())
3591-
self.gitOutput.close()
3592-
self.gitError.close()
3649+
self.closeStreams()
35933650

35943651
# Cleanup temporary branches created during import
35953652
if self.tempBranches != []:
@@ -3721,6 +3778,89 @@ def run(self, args):
37213778

37223779
return True
37233780

3781+
class P4Unshelve(Command):
3782+
def __init__(self):
3783+
Command.__init__(self)
3784+
self.options = []
3785+
self.origin = "HEAD"
3786+
self.description = "Unshelve a P4 changelist into a git commit"
3787+
self.usage = "usage: %prog [options] changelist"
3788+
self.options += [
3789+
optparse.make_option("--origin", dest="origin",
3790+
help="Use this base revision instead of the default (%s)" % self.origin),
3791+
]
3792+
self.verbose = False
3793+
self.noCommit = False
3794+
self.destbranch = "refs/remotes/p4/unshelved"
3795+
3796+
def renameBranch(self, branch_name):
3797+
""" Rename the existing branch to branch_name.N
3798+
"""
3799+
3800+
found = True
3801+
for i in range(0,1000):
3802+
backup_branch_name = "{0}.{1}".format(branch_name, i)
3803+
if not gitBranchExists(backup_branch_name):
3804+
gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
3805+
gitDeleteRef(branch_name)
3806+
found = True
3807+
print("renamed old unshelve branch to {0}".format(backup_branch_name))
3808+
break
3809+
3810+
if not found:
3811+
sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
3812+
3813+
def findLastP4Revision(self, starting_point):
3814+
""" Look back from starting_point for the first commit created by git-p4
3815+
to find the P4 commit we are based on, and the depot-paths.
3816+
"""
3817+
3818+
for parent in (range(65535)):
3819+
log = extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))
3820+
settings = extractSettingsGitLog(log)
3821+
if settings.has_key('change'):
3822+
return settings
3823+
3824+
sys.exit("could not find git-p4 commits in {0}".format(self.origin))
3825+
3826+
def run(self, args):
3827+
if len(args) != 1:
3828+
return False
3829+
3830+
if not gitBranchExists(self.origin):
3831+
sys.exit("origin branch {0} does not exist".format(self.origin))
3832+
3833+
sync = P4Sync()
3834+
changes = args
3835+
sync.initialParent = self.origin
3836+
3837+
# use the first change in the list to construct the branch to unshelve into
3838+
change = changes[0]
3839+
3840+
# if the target branch already exists, rename it
3841+
branch_name = "{0}/{1}".format(self.destbranch, change)
3842+
if gitBranchExists(branch_name):
3843+
self.renameBranch(branch_name)
3844+
sync.branch = branch_name
3845+
3846+
sync.verbose = self.verbose
3847+
sync.suppress_meta_comment = True
3848+
3849+
settings = self.findLastP4Revision(self.origin)
3850+
origin_revision = settings['change']
3851+
sync.depotPaths = settings['depot-paths']
3852+
sync.branchPrefixes = sync.depotPaths
3853+
3854+
sync.openStreams()
3855+
sync.loadUserMapFromCache()
3856+
sync.silent = True
3857+
sync.importChanges(changes, shelved=True, origin_revision=origin_revision)
3858+
sync.closeStreams()
3859+
3860+
print("unshelved changelist {0} into {1}".format(change, branch_name))
3861+
3862+
return True
3863+
37243864
class P4Branches(Command):
37253865
def __init__(self):
37263866
Command.__init__(self)
@@ -3775,7 +3915,8 @@ def printUsage(commands):
37753915
"rebase" : P4Rebase,
37763916
"clone" : P4Clone,
37773917
"rollback" : P4RollBack,
3778-
"branches" : P4Branches
3918+
"branches" : P4Branches,
3919+
"unshelve" : P4Unshelve,
37793920
}
37803921

37813922

0 commit comments

Comments
 (0)