Skip to content

Commit caf0c98

Browse files
committed
Merge branch 'ld/p4-unshelve'
"git p4" learned to "unshelve" shelved commit from P4. * ld/p4-unshelve: git-p4: add unshelve command
2 parents e1149fd + 123f631 commit caf0c98

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)