Merge branch 'ld/p4-unshelve'
authorJunio C Hamano <gitster@pobox.com>
Fri, 1 Jun 2018 06:06:38 +0000 (15:06 +0900)
committerJunio C Hamano <gitster@pobox.com>
Fri, 1 Jun 2018 06:06:38 +0000 (15:06 +0900)
"git p4" learned to "unshelve" shelved commit from P4.

* ld/p4-unshelve:
  git-p4: add unshelve command

1  2 
Documentation/git-p4.txt
git-p4.py

diff --combined Documentation/git-p4.txt
@@@ -29,8 -29,8 +29,8 @@@ Submit Git changes back to p4 using 'gi
  the updated p4 remote branch.
  
  
 -EXAMPLE
 --------
 +EXAMPLES
 +--------
  * Clone a repository:
  +
  ------------
@@@ -164,6 -164,31 +164,31 @@@ $ git p4 submit --shelv
  $ git p4 submit --update-shelve 1234 --update-shelve 2345
  ----
  
+ Unshelve
+ ~~~~~~~~
+ Unshelving will take a shelved P4 changelist, and produce the equivalent git commit
+ in the branch refs/remotes/p4/unshelved/<changelist>.
+ The git commit is created relative to the current origin revision (HEAD by default).
+ If the shelved changelist's parent revisions differ, git-p4 will refuse to unshelve;
+ you need to be unshelving onto an equivalent tree.
+ The origin revision can be changed with the "--origin" option.
+ If the target branch in refs/remotes/p4/unshelved already exists, the old one will
+ be renamed.
+ ----
+ $ git p4 sync
+ $ git p4 unshelve 12345
+ $ git show refs/remotes/p4/unshelved/12345
+ <submit more changes via p4 to the same files>
+ $ git p4 unshelve 12345
+ <refuses to unshelve until git is in sync with p4 again>
+ ----
  OPTIONS
  -------
  
@@@ -337,6 -362,13 +362,13 @@@ These options can be used to modify 'gi
  --import-labels::
        Import p4 labels.
  
+ Unshelve options
+ ~~~~~~~~~~~~~~~~
+ --origin::
+     Sets the git refspec against which the shelved P4 changelist is compared.
+     Defaults to p4/master.
  DEPOT PATH SYNTAX
  -----------------
  The p4 depot path argument to 'git p4 sync' and 'git p4 clone' can
diff --combined git-p4.py
+++ b/git-p4.py
@@@ -316,12 -316,17 +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 +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):
@@@ -2099,11 -2110,11 +2110,11 @@@ class P4Submit(Command, P4UserMap)
  
          commits = []
          if self.master:
 -            commitish = self.master
 +            committish = self.master
          else:
 -            commitish = 'HEAD'
 +            committish = 'HEAD'
  
 -        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
 +        for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
              commits.append(line.strip())
          commits.reverse()
  
@@@ -2411,6 -2422,7 +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')]
          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")
          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 = []
              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
              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,
          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:
          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:
                                  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
              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:
                      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)
              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 +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 +3915,8 @@@ commands = 
      "rebase" : P4Rebase,
      "clone" : P4Clone,
      "rollback" : P4RollBack,
-     "branches" : P4Branches
+     "branches" : P4Branches,
+     "unshelve" : P4Unshelve,
  }