{"thread":{"id":"48475","subject":"[PATCHv2 0/1] add git-p4 unshelve command","startedAt":"2018-05-12T21:24:51Z","lastAt":"2018-05-16T20:48:56Z","messageCount":3,"participants":["Luke Diamand"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"347476","messageId":"20180512212434.28012-1-luke@diamand.org","threadId":"48475","inReplyTo":null,"subject":"[PATCHv2 0/1] add git-p4 unshelve command","fromName":"Luke Diamand","fromEmail":"luke@diamand.org","sentAt":"2018-05-12T21:24:33Z","receivedAt":"2018-05-12T21:24:51Z","isPatch":false,"sender":{"key":"luke@diamand.org","avatar":"https://avatars.githubusercontent.com/u/5330967?v=4"},"body":"This is another attempt to make a \"git p4 unshelve\" command.\n\nUnshelving in p4 is a bit like a cross between cherry-pick\nand \"am\", and is very commonly used for review.\n\nThis command helps git users who want to try out a shelved\np4 change from some other repo:\n\ne.g.\n\n   $ git p4 unshelve 12345\n   unshelved CL12345 into refs/remotes/p4/unshelved/12345\n   $ git show refs/remotes/p4/unshelved/12345\n\nI abandoned an earlier attempt because it seemed like there\nis no way to get around a rather nasty problem: git-p4\njust constructs the commit and passes the file contents to\ngit-fastimport. But there's no easy way to construct the\n*prior* commit, because Perforce doesn't record this\ninformation, and so you can end up with other changes\nmixed into the unshelved commit - these are the differences\nbetween your tree and the other tree, for each file that\nhas been modified.\n\nHowever, I think the command is sufficiently useful that\nit's worth supporting anyway, even with that caveat.\n\nI also tried to use \"p4 describe\" to get the deltas, but\nthat's very unsatisfactory: I found myself writing a\nsecond-rate version of git's diff tool to try to make\nup for the deficiencies in Perforce's diff tool.\n\nIt might be possible to reconstruct the missing base\ncommit information, but that's a reasonably tricky task.\n\nI have incorporated some of the comments from the earlier\nreview rounds, in particular:\n\n- no longer adds the [git-p4...] annotation in unshelve\n- try to use .format() in place of %\n- rename the target branch if it already exists\n\nLuke Diamand (1):\n  git-p4: add unshelve command\n\n Documentation/git-p4.txt |  26 ++++++\n git-p4.py                | 171 ++++++++++++++++++++++++++++++---------\n t/t9832-unshelve.sh      |  99 +++++++++++++++++++++++\n 3 files changed, 260 insertions(+), 36 deletions(-)\n create mode 100755 t/t9832-unshelve.sh\n\n-- \n2.17.0.392.gdeb1a6e9b7\n\n"},{"id":"347477","messageId":"20180512212434.28012-2-luke@diamand.org","threadId":"48475","inReplyTo":"20180512212434.28012-1-luke@diamand.org","subject":"[PATCH 1/1] git-p4: add unshelve command","fromName":"Luke Diamand","fromEmail":"luke@diamand.org","sentAt":"2018-05-12T21:24:34Z","receivedAt":"2018-05-12T21:24:57Z","isPatch":true,"sender":{"key":"luke@diamand.org","avatar":"https://avatars.githubusercontent.com/u/5330967?v=4"},"body":"This can be used to \"unshelve\" a shelved P4 commit into\na git commit.\n\nFor example:\n\n  $ git p4 unshelve 12345\n\nThe resulting commit ends up in the branch:\n   refs/remotes/p4/unshelved/12345\n\nIf that branch already exists, it is renamed - for example\nthe above branch would be saved as p4/unshelved/12345.1.\n\nCaveat:\n\nThe unshelving is done against the current \"p4/master\" branch;\ngit-p4 uses \"p4 print\" to get the file contents at the requested\nrevision, and then fast-import creates a commit relative to p4/master.\n\nIdeally what you would want is for fast-import to create the\ncommit based on the Perforce \"revision\" prior to the shelved commit,\nbut Perforce doesn't have such a concept - to do this, git-p4\nwould need to figure out the revisions of the individual files\nbefore the shelved changelist, and then construct a temporary\ngit branch which matched this.\n\nIt's possible to do this, but doing so makes this change a lot more\ncomplicated.\n\nThis limitation means that if you unshelve a change where some\nof the changed files were not based on p4/master, you will get\nan amalgam of the change you wanted, and these other changes.\n\nThe reference branch can be changed manually with the \"--origin\"\noption.\n\nThe change adds a new Unshelve command class. This just runs the\nexisting P4Sync code tweaked to handle a shelved changelist.\n\nSigned-off-by: Luke Diamand <luke@diamand.org>\n---\n Documentation/git-p4.txt |  26 ++++++\n git-p4.py                | 171 ++++++++++++++++++++++++++++++---------\n t/t9832-unshelve.sh      |  99 +++++++++++++++++++++++\n 3 files changed, 260 insertions(+), 36 deletions(-)\n create mode 100755 t/t9832-unshelve.sh\n\ndiff --git a/Documentation/git-p4.txt b/Documentation/git-p4.txt\nindex d8c8f11c9f..2d768eec10 100644\n--- a/Documentation/git-p4.txt\n+++ b/Documentation/git-p4.txt\n@@ -164,6 +164,25 @@ $ git p4 submit --shelve\n $ git p4 submit --update-shelve 1234 --update-shelve 2345\n ----\n \n+\n+Unshelve\n+~~~~~~~~\n+Unshelving will take a shelved P4 changelist, and produce the equivalent git commit\n+in the branch refs/remotes/p4/unshelved/<changelist>.\n+\n+The git commit is created relative to the current p4/master, so if this\n+is behind Perforce itself, it may include more changes than you expected. You can\n+change the reference branch with the \"--origin\" option.\n+\n+If the target branch in refs/remotes/p4/unshelved already exists, the old one will\n+be renamed.\n+\n+----\n+$ git p4 sync\n+$ git p4 unshelve 12345\n+$ git show refs/remotes/p4/unshelved/12345\n+----\n+\n OPTIONS\n -------\n \n@@ -337,6 +356,13 @@ These options can be used to modify 'git p4 rebase' behavior.\n --import-labels::\n \tImport p4 labels.\n \n+Unshelve options\n+~~~~~~~~~~~~~~~~\n+\n+--origin::\n+    Sets the git refspec against which the shelved P4 changelist is compared.\n+    Defaults to p4/master.\n+\n DEPOT PATH SYNTAX\n -----------------\n The p4 depot path argument to 'git p4 sync' and 'git p4 clone' can\ndiff --git a/git-p4.py b/git-p4.py\nindex 7bb9cadc69..dcf6dc9f4f 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -316,12 +316,17 @@ def p4_last_change():\n     results = p4CmdList([\"changes\", \"-m\", \"1\"], skip_info=True)\n     return int(results[0]['change'])\n \n-def p4_describe(change):\n+def p4_describe(change, shelved=False):\n     \"\"\"Make sure it returns a valid result by checking for\n        the presence of field \"time\".  Return a dict of the\n        results.\"\"\"\n \n-    ds = p4CmdList([\"describe\", \"-s\", str(change)], skip_info=True)\n+    cmd = [\"describe\", \"-s\"]\n+    if shelved:\n+        cmd += [\"-S\"]\n+    cmd += [str(change)]\n+\n+    ds = p4CmdList(cmd, skip_info=True)\n     if len(ds) != 1:\n         die(\"p4 describe -s %d did not return 1 result: %s\" % (change, str(ds)))\n \n@@ -662,6 +667,12 @@ def gitBranchExists(branch):\n                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);\n     return proc.wait() == 0;\n \n+def gitUpdateRef(ref, newvalue):\n+    subprocess.check_call([\"git\", \"update-ref\", ref, newvalue])\n+\n+def gitDeleteRef(ref):\n+    subprocess.check_call([\"git\", \"update-ref\", \"-d\", ref])\n+\n _gitConfig = {}\n \n def gitConfig(key, typeSpecifier=None):\n@@ -2411,6 +2422,7 @@ class P4Sync(Command, P4UserMap):\n         self.tempBranches = []\n         self.tempBranchLocation = \"refs/git-p4-tmp\"\n         self.largeFileSystem = None\n+        self.suppress_meta_comment = False\n \n         if gitConfig('git-p4.largeFileSystem'):\n             largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]\n@@ -2421,6 +2433,18 @@ class P4Sync(Command, P4UserMap):\n         if gitConfig(\"git-p4.syncFromOrigin\") == \"false\":\n             self.syncWithOrigin = False\n \n+        self.depotPaths = []\n+        self.changeRange = \"\"\n+        self.previousDepotPaths = []\n+        self.hasOrigin = False\n+\n+        # map from branch depot path to parent branch\n+        self.knownBranches = {}\n+        self.initialParents = {}\n+\n+        self.tz = \"%+03d%02d\" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))\n+        self.labels = {}\n+\n     # Force a checkpoint in fast-import and wait for it to finish\n     def checkpoint(self):\n         self.gitStream.write(\"checkpoint\\n\\n\")\n@@ -2429,7 +2453,7 @@ class P4Sync(Command, P4UserMap):\n         if self.verbose:\n             print \"checkpoint finished: \" + out\n \n-    def extractFilesFromCommit(self, commit):\n+    def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0):\n         self.cloneExclude = [re.sub(r\"\\.\\.\\.$\", \"\", path)\n                              for path in self.cloneExclude]\n         files = []\n@@ -2452,6 +2476,9 @@ class P4Sync(Command, P4UserMap):\n             file[\"rev\"] = commit[\"rev%s\" % fnum]\n             file[\"action\"] = commit[\"action%s\" % fnum]\n             file[\"type\"] = commit[\"type%s\" % fnum]\n+            if shelved:\n+                file[\"shelved_cl\"] = int(shelved_cl)\n+\n             files.append(file)\n             fnum = fnum + 1\n         return files\n@@ -2743,7 +2770,16 @@ class P4Sync(Command, P4UserMap):\n             def streamP4FilesCbSelf(entry):\n                 self.streamP4FilesCb(entry)\n \n-            fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]\n+            fileArgs = []\n+            for f in filesToRead:\n+                if 'shelved_cl' in f:\n+                    # Handle shelved CLs using the \"p4 print file@=N\" syntax to print\n+                    # the contents\n+                    fileArg = '%s@=%d' % (f['path'], f['shelved_cl'])\n+                else:\n+                    fileArg = '%s#%s' % (f['path'], f['rev'])\n+\n+                fileArgs.append(fileArg)\n \n             p4CmdList([\"-x\", \"-\", \"print\"],\n                       stdin=fileArgs,\n@@ -2844,11 +2880,15 @@ class P4Sync(Command, P4UserMap):\n         self.gitStream.write(details[\"desc\"])\n         if len(jobs) > 0:\n             self.gitStream.write(\"\\nJobs: %s\" % (' '.join(jobs)))\n-        self.gitStream.write(\"\\n[git-p4: depot-paths = \\\"%s\\\": change = %s\" %\n-                             (','.join(self.branchPrefixes), details[\"change\"]))\n-        if len(details['options']) > 0:\n-            self.gitStream.write(\": options = %s\" % details['options'])\n-        self.gitStream.write(\"]\\nEOT\\n\\n\")\n+\n+        if not self.suppress_meta_comment:\n+            self.gitStream.write(\"\\n[git-p4: depot-paths = \\\"%s\\\": change = %s\" %\n+                                (','.join(self.branchPrefixes), details[\"change\"]))\n+            if len(details['options']) > 0:\n+                self.gitStream.write(\": options = %s\" % details['options'])\n+            self.gitStream.write(\"]\\n\")\n+\n+        self.gitStream.write(\"EOT\\n\\n\")\n \n         if len(parent) > 0:\n             if self.verbose:\n@@ -3162,10 +3202,10 @@ class P4Sync(Command, P4UserMap):\n         else:\n             return None\n \n-    def importChanges(self, changes):\n+    def importChanges(self, changes, shelved=False):\n         cnt = 1\n         for change in changes:\n-            description = p4_describe(change)\n+            description = p4_describe(change, shelved)\n             self.updateOptionDict(description)\n \n             if not self.silent:\n@@ -3235,7 +3275,7 @@ class P4Sync(Command, P4UserMap):\n                                 print \"Parent of %s not found. Committing into head of %s\" % (branch, parent)\n                             self.commit(description, filesForCommit, branch, parent)\n                 else:\n-                    files = self.extractFilesFromCommit(description)\n+                    files = self.extractFilesFromCommit(description, shelved, change)\n                     self.commit(description, files, self.branch,\n                                 self.initialParent)\n                     # only needed once, to connect to the previous commit\n@@ -3300,17 +3340,23 @@ class P4Sync(Command, P4UserMap):\n             print \"IO error with git fast-import. Is your git version recent enough?\"\n             print self.gitError.read()\n \n+    def openStreams(self):\n+        self.importProcess = subprocess.Popen([\"git\", \"fast-import\"],\n+                                              stdin=subprocess.PIPE,\n+                                              stdout=subprocess.PIPE,\n+                                              stderr=subprocess.PIPE);\n+        self.gitOutput = self.importProcess.stdout\n+        self.gitStream = self.importProcess.stdin\n+        self.gitError = self.importProcess.stderr\n \n-    def run(self, args):\n-        self.depotPaths = []\n-        self.changeRange = \"\"\n-        self.previousDepotPaths = []\n-        self.hasOrigin = False\n-\n-        # map from branch depot path to parent branch\n-        self.knownBranches = {}\n-        self.initialParents = {}\n+    def closeStreams(self):\n+        self.gitStream.close()\n+        if self.importProcess.wait() != 0:\n+            die(\"fast-import failed: %s\" % self.gitError.read())\n+        self.gitOutput.close()\n+        self.gitError.close()\n \n+    def run(self, args):\n         if self.importIntoRemotes:\n             self.refPrefix = \"refs/remotes/p4/\"\n         else:\n@@ -3497,15 +3543,7 @@ class P4Sync(Command, P4UserMap):\n                     b = b[len(self.projectName):]\n                 self.createdBranches.add(b)\n \n-        self.tz = \"%+03d%02d\" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))\n-\n-        self.importProcess = subprocess.Popen([\"git\", \"fast-import\"],\n-                                              stdin=subprocess.PIPE,\n-                                              stdout=subprocess.PIPE,\n-                                              stderr=subprocess.PIPE);\n-        self.gitOutput = self.importProcess.stdout\n-        self.gitStream = self.importProcess.stdin\n-        self.gitError = self.importProcess.stderr\n+        self.openStreams()\n \n         if revision:\n             self.importHeadRevision(revision)\n@@ -3585,11 +3623,7 @@ class P4Sync(Command, P4UserMap):\n             missingP4Labels = p4Labels - gitTags\n             self.importP4Labels(self.gitStream, missingP4Labels)\n \n-        self.gitStream.close()\n-        if self.importProcess.wait() != 0:\n-            die(\"fast-import failed: %s\" % self.gitError.read())\n-        self.gitOutput.close()\n-        self.gitError.close()\n+        self.closeStreams()\n \n         # Cleanup temporary branches created during import\n         if self.tempBranches != []:\n@@ -3721,6 +3755,70 @@ class P4Clone(P4Sync):\n \n         return True\n \n+class P4Unshelve(Command):\n+    def __init__(self):\n+        Command.__init__(self)\n+        self.options = []\n+        self.description = \"Unshelve a P4 changelist into a git commit\"\n+        self.usage = \"usage: %prog [options] changelist\"\n+        self.options += [\n+                optparse.make_option(\"--origin\", dest=\"origin\"),\n+        ]\n+        self.verbose = False\n+        self.noCommit = False\n+        self.origin = \"HEAD\"\n+        self.destbranch = \"refs/remotes/p4/unshelved\"\n+\n+    def run(self, args):\n+        if len(args) != 1:\n+            return False\n+\n+        if not gitBranchExists(self.origin):\n+            sys.exit(\"origin branch {} does not exist\".format(self.origin))\n+\n+        sync = P4Sync()\n+        changes = args\n+        sync.initialParent = self.origin\n+\n+        # use the first change in the list to construct the branch to unshelve into\n+        change = changes[0]\n+\n+        # if it already exists, rename it\n+        branch_name = \"{}/{}\".format(self.destbranch, change)\n+        if gitBranchExists(branch_name):\n+            found = True\n+            for i in range(0,1000):\n+                backup_branch_name = \"{}.{}\".format(branch_name, i)\n+                if not gitBranchExists(backup_branch_name):\n+                    gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup\n+                    gitDeleteRef(branch_name)\n+                    found = True\n+                    print(\"renamed old unshelve branch to {}\".format(backup_branch_name))\n+                    break\n+\n+            if not found:\n+                sys.exit(\"gave up trying to rename existing branch {}\".format(sync.branch))\n+        sync.branch = branch_name\n+\n+        sync.verbose = self.verbose\n+        sync.suppress_meta_comment = True\n+\n+        print(\"getting log message from {}\".format(self.origin))\n+        log = extractLogMessageFromGitCommit(\"refs/remotes/p4/master\")\n+        settings = extractSettingsGitLog(log)\n+        sync.depotPaths = settings['depot-paths']\n+        sync.branchPrefixes = sync.depotPaths\n+\n+        sync.openStreams()\n+        sync.loadUserMapFromCache()\n+        sync.silent = True\n+        sync.importChanges(changes, shelved=True)\n+        sync.closeStreams()\n+\n+        print(\"unshelved CL{} into {}\".format(change, branch_name))\n+\n+        return True\n+\n class P4Branches(Command):\n     def __init__(self):\n         Command.__init__(self)\n@@ -3775,7 +3873,8 @@ commands = {\n     \"rebase\" : P4Rebase,\n     \"clone\" : P4Clone,\n     \"rollback\" : P4RollBack,\n-    \"branches\" : P4Branches\n+    \"branches\" : P4Branches,\n+    \"unshelve\" : P4Unshelve,\n }\n \n \ndiff --git a/t/t9832-unshelve.sh b/t/t9832-unshelve.sh\nnew file mode 100755\nindex 0000000000..332d30ece9\n--- /dev/null\n+++ b/t/t9832-unshelve.sh\n@@ -0,0 +1,99 @@\n+#!/bin/sh\n+\n+last_shelved_change() {\n+\tp4 changes -s shelved -m1 | cut -d \" \" -f 2\n+}\n+\n+test_description='git p4 unshelve'\n+\n+. ./lib-git-p4.sh\n+\n+test_expect_success 'start p4d' '\n+\tstart_p4d\n+'\n+\n+test_expect_success 'init depot' '\n+\t(\n+\t\tcd \"$cli\" &&\n+\t\techo file1 >file1 &&\n+\t\tp4 add file1 &&\n+\t\tp4 submit -d \"change 1\"\n+\t\t: >file_to_delete &&\n+\t\tp4 add file_to_delete &&\n+\t\tp4 submit -d \"file to delete\"\n+\t)\n+'\n+\n+test_expect_success 'initial clone' '\n+\tgit p4 clone --dest=\"$git\" //depot/@all\n+'\n+\n+test_expect_success 'create shelved changelist' '\n+\t(\n+\t\tcd \"$cli\" &&\n+\t\tp4 edit file1 &&\n+\t\techo \"a change\" >>file1 &&\n+\t\techo \"new file\" >file2 &&\n+\t\tp4 add file2 &&\n+\t\tp4 delete file_to_delete &&\n+\t\tp4 opened &&\n+\t\tp4 shelve -i <<EOF\n+Change: new\n+Description:\n+\tTest commit\n+\n+\tFurther description\n+Files:\n+\t//depot/file1\n+\t//depot/file2\n+\t//depot/file_to_delete\n+EOF\n+\n+\t) &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tchange=$(last_shelved_change) &&\n+\t\tgit p4 unshelve $change &&\n+\t\tgit show refs/remotes/p4/unshelved/$change | grep -q \"Further description\" &&\n+\t\tgit cherry-pick refs/remotes/p4/unshelved/$change &&\n+\t\ttest_path_is_file file2 &&\n+\t\ttest_cmp file1 \"$cli\"/file1 &&\n+\t\ttest_cmp file2 \"$cli\"/file2 &&\n+\t\ttest_path_is_missing file_to_delete\n+\t)\n+'\n+\n+test_expect_success 'update shelved changelist and re-unshelve' '\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$cli\" &&\n+\t\tchange=$(last_shelved_change) &&\n+\t\techo \"file3\" >file3 &&\n+\t\tp4 add -c $change file3 &&\n+\t\tp4 shelve -i -r <<EOF &&\n+Change: $change\n+Description:\n+\tTest commit\n+\n+\tFurther description\n+Files:\n+\t//depot/file1\n+\t//depot/file2\n+\t//depot/file3\n+\t//depot/file_to_delete\n+EOF\n+\t\tp4 describe $change\n+\t) &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tchange=$(last_shelved_change) &&\n+\t\tgit p4 unshelve $change &&\n+\t\tgit diff refs/remotes/p4/unshelved/$change.0 refs/remotes/p4/unshelved/$change | grep -q file3\n+\t)\n+'\n+\n+test_expect_success 'kill p4d' '\n+\tkill_p4d\n+'\n+\n+test_done\n-- \n2.17.0.392.gdeb1a6e9b7\n\n"},{"id":"347823","messageId":"CAE5ih79MTtDKRUYVhB7miuqYxhxZGrVtstG6Tn02LBv2kXrCfA@mail.gmail.com","threadId":"48475","inReplyTo":"1378098185.1161438.1526219571066@mail.yahoo.com","subject":"Re: [PATCH 1/1] git-p4: add unshelve command","fromName":"Luke Diamand","fromEmail":"luke@diamand.org","sentAt":"2018-05-16T20:48:51Z","receivedAt":"2018-05-16T20:48:56Z","isPatch":true,"sender":{"key":"luke@diamand.org","avatar":"https://avatars.githubusercontent.com/u/5330967?v=4"},"body":"On 13 May 2018 at 14:52, Merland Romain <merlorom@yahoo.fr> wrote:\n> Hello Luke,\n>\n> Very interseting\n> This is indeed an option we are waiting since the introduction of option --shelve for git p4 submit\n> What I like most in your approach is the preservation of link to p4/master inducing small changes of git-p4 existing functions already heavily tested.\n> Also I like the dedicated branch you create, it is cleaner and then we can cherry-pick in other git branches.\n\nThanks, I'd be interested to know how you get on with it!\n\n> We made some basic tries on our side, just adding an option --unshelve to P4Submit (for simplicity, but I like much more your P4Unshelve class)\n> and trying to use the diff of p4 to generate a patch when possible, then apply it in the current git branch on top of HEAD.\n> Here it is, hope it can help a bit.\n> Note it also uses p4 print -o for binary files.\n\nI did try something like this earlier this year (if you look in the\narchives you'll find it) but I found that it was getting quite\ncomplicated trying to construct a sensible looking patch file from the\noutput of p4 describe. Better to let git's existing tools do that, as\nthey're going to be better than any half-baked attempt I might manage\nin Python!\n\nThanks!\nLuke\n\n\n\n>\n> diff --git a/git-p4.py b/git-p4.py\n> index f4a6f3b4c..b466b46e1 100755\n> --- a/git-p4.py\n> +++ b/git-p4.py\n> @@ -1353,6 +1353,8 @@ class P4Submit(Command, P4UserMap):\n>                                       metavar=\"CHANGELIST\",\n>                                       help=\"update an existing shelved changelist, implies --shelve, \"\n>                                             \"repeat in-order for multiple shelved changelists\"),\n> +                optparse.make_option(\"--unshelve\", dest=\"unshelve\",\n> +                                     help=\"unshelve speficied ChangeList into current BRANCH.\"),\n>                  optparse.make_option(\"--commit\", dest=\"commit\", metavar=\"COMMIT\",\n>                                       help=\"submit only the specified commit(s), one commit or xxx..xxx\"),\n>                  optparse.make_option(\"--disable-rebase\", dest=\"disable_rebase\", action=\"store_true\",\n> @@ -1367,6 +1369,7 @@ class P4Submit(Command, P4UserMap):\n>          self.dry_run = False\n>          self.shelve = False\n>          self.update_shelve = list()\n> +        self.unshelve = \"\"\n>          self.commit = \"\"\n>          self.disable_rebase = False\n>          self.prepare_p4_only = False\n> @@ -2083,6 +2086,66 @@ class P4Submit(Command, P4UserMap):\n>          if self.clientPath == \"\":\n>              die(\"Error: Cannot locate perforce checkout of %s in client view\" % self.depotPath)\n>\n> +        # special case of unshelving\n> +        # todo: put this code in a class like P4Sync or P4Rebase\n> +        if self.unshelve != \"\":\n> +            git_dir = os.getcwd() + '/'\n> +            print \"Importing shelved CL %s into current git branch %s\" % (self.unshelve, self.master)\n> +            description = p4_describe(self.unshelve)\n> +\n> +            # get changed files\n> +            files = p4CmdList(['files', \"@=%s\" % self.unshelve])\n> +            editedFiles = []\n> +            filesToAdd = []\n> +            filesToDelete = []\n> +            binaryFiles = []\n> +            something_to_commit = False\n> +            for f in files:\n> +                if not f[\"depotFile\"].startswith(self.depotPath):\n> +                    print \"WARNING: file %s not in this p4 depot - skipping\" % f[\"depotFile\"]\n> +                    continue\n> +\n> +                elif f[\"action\"] == 'delete':\n> +                    filesToDelete.append(f)\n> +                    something_to_commit = True\n> +                elif f[\"action\"] == 'add':\n> +                    filesToAdd.append(f)\n> +                    something_to_commit = True\n> +                elif f[\"type\"] == 'binary':\n> +                    binaryFiles.append(f)\n> +                    something_to_commit = True\n> +                elif f[\"action\"] == 'edit':\n> +                    editedFiles.append(f)\n> +                    something_to_commit = True\n> +\n> +                f[\"clientFile\"] = f[\"depotFile\"].replace(self.depotPath,self.clientPath)\n> +                f[\"gitFile\"] = f[\"depotFile\"].replace(self.depotPath,git_dir)\n> +\n> +            if not something_to_commit:\n> +                print \"Nothing to commit. Exiting\"\n> +                return True\n> +\n> +            # get the diff and copy to diff directory\n> +            for f in editedFiles:\n> +                p4diff = p4_read_pipe(['diff2', '-du', f[\"depotFile\"]+'#'+f[\"rev\"], f[\"depotFile\"]+'@='+self.unshelve])\n> +                p4diff = \"\\n\".join(p4diff.split(\"\\n\")[1:])\n> +                p4diff = '--- '+f[\"gitFile\"]+'\\n' + '+++ '+f[\"gitFile\"]+'\\n' + p4diff\n> +                write_pipe(['patch', '-d/', '-p0'], p4diff)\n> +                write_pipe(['git', 'add', '-f', f[\"gitFile\"]], \"\")\n> +            for f in filesToAdd:\n> +                p4_write_pipe(['print', '-o', f[\"gitFile\"], f[\"depotFile\"]+'@='+self.unshelve], \"\")\n> +                write_pipe(['git', 'add', '-f', f[\"gitFile\"]], \"\")\n> +            for f in filesToDelete:\n> +                os.remove(f[\"gitFile\"])\n> +                write_pipe(['git', 'rm', f[\"gitFile\"]], \"\")\n> +            for f in binaryFiles:\n> +                p4_write_pipe(['print', '-o', f[\"gitFile\"], f[\"depotFile\"]+'@='+self.unshelve], \"\")\n> +                write_pipe(['git', 'add', '-f', f[\"gitFile\"]], \"\")\n> +\n> +            # finalize: commit in git\n> +            write_pipe(['git', 'commit', '-m', description[\"desc\"]], \"\")\n> +            return True\n> +\n>          print \"Perforce checkout for depot path %s located at %s\" % (self.depotPath, self.clientPath)\n>          self.oldWorkingDirectory = os.getcwd()\n>\n> Romain\n>\n>\n>\n> Le samedi 12 mai 2018 à 23:24:48 UTC+2, Luke Diamand <luke@diamand.org> a écrit :\n>\n>\n>\n>\n>\n> This can be used to \"unshelve\" a shelved P4 commit into\n> a git commit.\n>\n> For example:\n>\n>   $ git p4 unshelve 12345\n>\n> The resulting commit ends up in the branch:\n>   refs/remotes/p4/unshelved/12345\n>\n> If that branch already exists, it is renamed - for example\n> the above branch would be saved as p4/unshelved/12345.1.\n>\n> Caveat:\n>\n> The unshelving is done against the current \"p4/master\" branch;\n> git-p4 uses \"p4 print\" to get the file contents at the requested\n> revision, and then fast-import creates a commit relative to p4/master.\n>\n> Ideally what you would want is for fast-import to create the\n> commit based on the Perforce \"revision\" prior to the shelved commit,\n> but Perforce doesn't have such a concept - to do this, git-p4\n> would need to figure out the revisions of the individual files\n> before the shelved changelist, and then construct a temporary\n> git branch which matched this.\n>\n> It's possible to do this, but doing so makes this change a lot more\n> complicated.\n>\n> This limitation means that if you unshelve a change where some\n> of the changed files were not based on p4/master, you will get\n> an amalgam of the change you wanted, and these other changes.\n>\n> The reference branch can be changed manually with the \"--origin\"\n> option.\n>\n> The change adds a new Unshelve command class. This just runs the\n> existing P4Sync code tweaked to handle a shelved changelist.\n>\n> Signed-off-by: Luke Diamand <luke@diamand.org>\n"}]}