{"thread":{"id":"50417","subject":"[PATCH 1/1] git-p4: recover from inconsistent perforce history","startedAt":"2019-02-06T20:08:31Z","lastAt":"2019-02-11T08:54:08Z","messageCount":5,"participants":["andrew@adoakley.name","Luke Diamand","Andrew Oakley"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"368637","messageId":"20190206194219.15507-1-andrew@adoakley.name","threadId":"50417","inReplyTo":null,"subject":"[PATCH 1/1] git-p4: recover from inconsistent perforce history","fromName":"","fromEmail":"andrew@adoakley.name","sentAt":"2019-02-06T19:42:19Z","receivedAt":"2019-02-06T20:08:31Z","isPatch":true,"sender":{"key":"andrew@adoakley.name","avatar":"https://avatars.githubusercontent.com/u/1107440?v=4"},"body":"From: Andrew Oakley <andrew@adoakley.name>\n\nPerforce allows you commit files and directories with the same name, so\nyou could have files //depot/foo and //depot/foo/bar both checked in.  A\np4 sync of a repository in this state fails.  Deleting one of the files\nrecovers the repository.\n\nWhen this happens we want git-p4 to recover in the same way as perforce.\n\nSigned-off-by: Andrew Oakley <andrew@adoakley.name>\n---\n git-p4.py                      | 41 ++++++++++++++++++++++--\n t/t9834-git-p4-file-dir-bug.sh | 58 ++++++++++++++++++++++++++++++++++\n 2 files changed, 97 insertions(+), 2 deletions(-)\n create mode 100755 t/t9834-git-p4-file-dir-bug.sh\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 3e12774f96..6bf2bbbcec 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -3012,6 +3012,43 @@ def hasBranchPrefix(self, path):\n             print('Ignoring file outside of prefix: {0}'.format(path))\n         return hasPrefix\n \n+    def isIncluded(self, path):\n+        return self.inClientSpec(path) and self.hasBranchPrefix(path)\n+\n+    def findShadowedFiles(self, files, change):\n+        # Perforce allows you commit files and directories with the same name,\n+        # so you could have files //depot/foo and //depot/foo/bar both checked\n+        # in.  A p4 sync of a repository in this state fails.  Deleting one of\n+        # the files recovers the repository.\n+        #\n+        # Git will not allow the broken state to exist and only the most recent\n+        # of the conflicting names is left in the repository.  When one of the\n+        # conflicting files is deleted we need to re-add the other one to make\n+        # sure the git repository recovers in the same way as perforce.\n+        deleted = [f for f in files if f['action'] in self.delete_actions]\n+        to_check = set()\n+        for f in deleted:\n+            path = f['path']\n+            to_check.add(path + '/...')\n+            while True:\n+                path = path.rsplit(\"/\", 1)[0]\n+                if path == \"/\" or path in to_check:\n+                    break\n+                to_check.add(path)\n+        to_check = [p for p in to_check if self.isIncluded(p)]\n+        if to_check:\n+            stat_result = p4CmdList(\n+                [\"fstat\", \"-T\", \"depotFile,headRev,headType\"] +\n+                    [\"%s@%s\" % (p, change) for p in to_check])\n+            for record in stat_result:\n+                if record['code'] != 'stat':\n+                    continue\n+                files.append({\n+                    'action': 'add',\n+                    'path': record['depotFile'],\n+                    'rev': record['headRev'],\n+                    'type': record['headType']})\n+\n     def commit(self, details, files, branch, parent = \"\", allow_empty=False):\n         epoch = details[\"time\"]\n         author = details[\"user\"]\n@@ -3023,8 +3060,8 @@ def commit(self, details, files, branch, parent = \"\", allow_empty=False):\n         if self.clientSpecDirs:\n             self.clientSpecDirs.update_client_spec_path_cache(files)\n \n-        files = [f for f in files\n-            if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]\n+        files = [f for f in files if self.isIncluded(f['path'])]\n+        self.findShadowedFiles(files, details[\"change\"])\n \n         if gitConfigBool('git-p4.keepEmptyCommits'):\n             allow_empty = True\ndiff --git a/t/t9834-git-p4-file-dir-bug.sh b/t/t9834-git-p4-file-dir-bug.sh\nnew file mode 100755\nindex 0000000000..9839a3d2bb\n--- /dev/null\n+++ b/t/t9834-git-p4-file-dir-bug.sh\n@@ -0,0 +1,58 @@\n+#!/bin/sh\n+\n+test_description='git p4 directory/file bug handling\n+\n+This test creates files and directories with the same name in perforce and\n+checks that git-p4 recovers from the error at the same time as the perforce\n+repository.'\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+\n+\t\ttouch add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\tp4 add add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\tmkdir add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\ttouch add_dir_add_file_del_file/file add_dir_add_file_del_dir/file &&\n+\t\tp4 add add_dir_add_file_del_file/file add_dir_add_file_del_dir/file &&\n+\t\tp4 submit -d \"add initial\" &&\n+\n+\t\trm -f add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\tmkdir add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\ttouch add_file_add_dir_del_file/file add_file_add_dir_del_dir/file &&\n+\t\tp4 add add_file_add_dir_del_file/file add_file_add_dir_del_dir/file &&\n+\t\trm -rf add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\ttouch add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\tp4 add add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\tp4 submit -d \"add conflicting\" &&\n+\n+\t\tp4 delete -k add_file_add_dir_del_file &&\n+\t\tp4 delete -k add_file_add_dir_del_dir/file &&\n+\t\tp4 delete -k add_dir_add_file_del_file &&\n+\t\tp4 delete -k add_dir_add_file_del_dir/file &&\n+\t\tp4 submit -d \"delete conflicting\"\n+\t)\n+'\n+\n+test_expect_success 'clone with git-p4' '\n+\tgit p4 clone --dest=\"$git\" //depot/@all\n+'\n+\n+test_expect_success 'check final contents' '\n+\ttest_path_is_dir \"$git/add_file_add_dir_del_file\" &&\n+\ttest_path_is_file \"$git/add_file_add_dir_del_dir\" &&\n+\ttest_path_is_dir \"$git/add_dir_add_file_del_file\" &&\n+\ttest_path_is_file \"$git/add_dir_add_file_del_dir\"\n+'\n+\n+test_expect_success 'kill p4d' '\n+\tkill_p4d\n+'\n+\n+test_done\n-- \n2.19.2\n\n"},{"id":"368671","messageId":"20190207080420.395c521be017dafb6561784b@diamand.org","threadId":"50417","inReplyTo":"20190206194219.15507-1-andrew@adoakley.name","subject":"Re: [PATCH 1/1] git-p4: recover from inconsistent perforce history","fromName":"Luke Diamand","fromEmail":"luke@diamand.org","sentAt":"2019-02-07T08:04:20Z","receivedAt":"2019-02-07T08:04:26Z","isPatch":true,"sender":{"key":"luke@diamand.org","avatar":"https://avatars.githubusercontent.com/u/5330967?v=4"},"body":"On Wed,  6 Feb 2019 19:42:19 +0000\nandrew@adoakley.name wrote:\n\n> From: Andrew Oakley <andrew@adoakley.name>\n> \n> Perforce allows you commit files and directories with the same name, so\n> you could have files //depot/foo and //depot/foo/bar both checked in.  A\n> p4 sync of a repository in this state fails.  Deleting one of the files\n> recovers the repository.\n> \n> When this happens we want git-p4 to recover in the same way as perforce.\n\nI'm finding the test fails for me on a clean git repo, although I can't see any obvious reason why.\n\nHaving the ability to detect when Perforce users submit a change which creates a file-inside-a-file will be really very useful.\n\nLuke\n\n\n> \n> Signed-off-by: Andrew Oakley <andrew@adoakley.name>\n> ---\n>  git-p4.py                      | 41 ++++++++++++++++++++++--\n>  t/t9834-git-p4-file-dir-bug.sh | 58 ++++++++++++++++++++++++++++++++++\n>  2 files changed, 97 insertions(+), 2 deletions(-)\n>  create mode 100755 t/t9834-git-p4-file-dir-bug.sh\n> \n> diff --git a/git-p4.py b/git-p4.py\n> index 3e12774f96..6bf2bbbcec 100755\n> --- a/git-p4.py\n> +++ b/git-p4.py\n> @@ -3012,6 +3012,43 @@ def hasBranchPrefix(self, path):\n>              print('Ignoring file outside of prefix: {0}'.format(path))\n>          return hasPrefix\n>  \n> +    def isIncluded(self, path):\n> +        return self.inClientSpec(path) and self.hasBranchPrefix(path)\n> +\n> +    def findShadowedFiles(self, files, change):\n> +        # Perforce allows you commit files and directories with the same name,\n> +        # so you could have files //depot/foo and //depot/foo/bar both checked\n> +        # in.  A p4 sync of a repository in this state fails.  Deleting one of\n> +        # the files recovers the repository.\n> +        #\n> +        # Git will not allow the broken state to exist and only the most recent\n> +        # of the conflicting names is left in the repository.  When one of the\n> +        # conflicting files is deleted we need to re-add the other one to make\n> +        # sure the git repository recovers in the same way as perforce.\n> +        deleted = [f for f in files if f['action'] in self.delete_actions]\n> +        to_check = set()\n> +        for f in deleted:\n> +            path = f['path']\n> +            to_check.add(path + '/...')\n> +            while True:\n> +                path = path.rsplit(\"/\", 1)[0]\n> +                if path == \"/\" or path in to_check:\n> +                    break\n> +                to_check.add(path)\n> +        to_check = [p for p in to_check if self.isIncluded(p)]\n> +        if to_check:\n> +            stat_result = p4CmdList(\n> +                [\"fstat\", \"-T\", \"depotFile,headRev,headType\"] +\n> +                    [\"%s@%s\" % (p, change) for p in to_check])\n> +            for record in stat_result:\n> +                if record['code'] != 'stat':\n> +                    continue\n> +                files.append({\n> +                    'action': 'add',\n> +                    'path': record['depotFile'],\n> +                    'rev': record['headRev'],\n> +                    'type': record['headType']})\n> +\n>      def commit(self, details, files, branch, parent = \"\", allow_empty=False):\n>          epoch = details[\"time\"]\n>          author = details[\"user\"]\n> @@ -3023,8 +3060,8 @@ def commit(self, details, files, branch, parent = \"\", allow_empty=False):\n>          if self.clientSpecDirs:\n>              self.clientSpecDirs.update_client_spec_path_cache(files)\n>  \n> -        files = [f for f in files\n> -            if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]\n> +        files = [f for f in files if self.isIncluded(f['path'])]\n> +        self.findShadowedFiles(files, details[\"change\"])\n>  \n>          if gitConfigBool('git-p4.keepEmptyCommits'):\n>              allow_empty = True\n> diff --git a/t/t9834-git-p4-file-dir-bug.sh b/t/t9834-git-p4-file-dir-bug.sh\n> new file mode 100755\n> index 0000000000..9839a3d2bb\n> --- /dev/null\n> +++ b/t/t9834-git-p4-file-dir-bug.sh\n> @@ -0,0 +1,58 @@\n> +#!/bin/sh\n> +\n> +test_description='git p4 directory/file bug handling\n> +\n> +This test creates files and directories with the same name in perforce and\n> +checks that git-p4 recovers from the error at the same time as the perforce\n> +repository.'\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> +\n> +\t\ttouch add_file_add_dir_del_file add_file_add_dir_del_dir &&\n> +\t\tp4 add add_file_add_dir_del_file add_file_add_dir_del_dir &&\n> +\t\tmkdir add_dir_add_file_del_file add_dir_add_file_del_dir &&\n> +\t\ttouch add_dir_add_file_del_file/file add_dir_add_file_del_dir/file &&\n> +\t\tp4 add add_dir_add_file_del_file/file add_dir_add_file_del_dir/file &&\n> +\t\tp4 submit -d \"add initial\" &&\n> +\n> +\t\trm -f add_file_add_dir_del_file add_file_add_dir_del_dir &&\n> +\t\tmkdir add_file_add_dir_del_file add_file_add_dir_del_dir &&\n> +\t\ttouch add_file_add_dir_del_file/file add_file_add_dir_del_dir/file &&\n> +\t\tp4 add add_file_add_dir_del_file/file add_file_add_dir_del_dir/file &&\n> +\t\trm -rf add_dir_add_file_del_file add_dir_add_file_del_dir &&\n> +\t\ttouch add_dir_add_file_del_file add_dir_add_file_del_dir &&\n> +\t\tp4 add add_dir_add_file_del_file add_dir_add_file_del_dir &&\n> +\t\tp4 submit -d \"add conflicting\" &&\n> +\n> +\t\tp4 delete -k add_file_add_dir_del_file &&\n> +\t\tp4 delete -k add_file_add_dir_del_dir/file &&\n> +\t\tp4 delete -k add_dir_add_file_del_file &&\n> +\t\tp4 delete -k add_dir_add_file_del_dir/file &&\n> +\t\tp4 submit -d \"delete conflicting\"\n> +\t)\n> +'\n> +\n> +test_expect_success 'clone with git-p4' '\n> +\tgit p4 clone --dest=\"$git\" //depot/@all\n> +'\n> +\n> +test_expect_success 'check final contents' '\n> +\ttest_path_is_dir \"$git/add_file_add_dir_del_file\" &&\n> +\ttest_path_is_file \"$git/add_file_add_dir_del_dir\" &&\n> +\ttest_path_is_dir \"$git/add_dir_add_file_del_file\" &&\n> +\ttest_path_is_file \"$git/add_dir_add_file_del_dir\"\n> +'\n> +\n> +test_expect_success 'kill p4d' '\n> +\tkill_p4d\n> +'\n> +\n> +test_done\n> -- \n> 2.19.2\n> \n\n\n-- \nLuke Diamand <luke@diamand.org>\n"},{"id":"368806","messageId":"20190208090704.68154885@ado-amd-gentoo.home.adoakley.name","threadId":"50417","inReplyTo":"20190207080420.395c521be017dafb6561784b@diamand.org","subject":"Re: [PATCH 1/1] git-p4: recover from inconsistent perforce history","fromName":"Andrew Oakley","fromEmail":"andrew@adoakley.name","sentAt":"2019-02-08T09:07:04Z","receivedAt":"2019-02-08T09:07:09Z","isPatch":true,"sender":{"key":"andrew@adoakley.name","avatar":"https://avatars.githubusercontent.com/u/1107440?v=4"},"body":"On Thu, 7 Feb 2019 08:04:20 +0000\nLuke Diamand <luke@diamand.org> wrote:\n\n> On Wed,  6 Feb 2019 19:42:19 +0000\n> andrew@adoakley.name wrote:\n> \n> > From: Andrew Oakley <andrew@adoakley.name>\n> > \n> > Perforce allows you commit files and directories with the same\n> > name, so you could have files //depot/foo and //depot/foo/bar both\n> > checked in.  A p4 sync of a repository in this state fails.\n> > Deleting one of the files recovers the repository.\n> > \n> > When this happens we want git-p4 to recover in the same way as\n> > perforce.  \n> \n> I'm finding the test fails for me on a clean git repo, although I\n> can't see any obvious reason why.\n\nThis introduced a failure when client specs are being used.  I wasn't\npopulating the cache (update_client_spec_path_cache) before checking if\na file should be included (inClientSpec).  I can rearrange the code to\nfix this.\n\nThanks\n\n-- \nAndrew Oakley\n"},{"id":"368807","messageId":"20190208091033.12617-1-andrew@adoakley.name","threadId":"50417","inReplyTo":"20190208090704.68154885@ado-amd-gentoo.home.adoakley.name","subject":"[PATCH v2] git-p4: recover from inconsistent perforce history","fromName":"","fromEmail":"andrew@adoakley.name","sentAt":"2019-02-08T09:10:33Z","receivedAt":"2019-02-08T09:10:59Z","isPatch":true,"sender":{"key":"andrew@adoakley.name","avatar":"https://avatars.githubusercontent.com/u/1107440?v=4"},"body":"From: Andrew Oakley <andrew@adoakley.name>\n\nPerforce allows you commit files and directories with the same name, so\nyou could have files //depot/foo and //depot/foo/bar both checked in.  A\np4 sync of a repository in this state fails.  Deleting one of the files\nrecovers the repository.\n\nWhen this happens we want git-p4 to recover in the same way as perforce.\n\nSigned-off-by: Andrew Oakley <andrew@adoakley.name>\n---\n git-p4.py                      | 41 ++++++++++++++++++++++--\n t/t9834-git-p4-file-dir-bug.sh | 58 ++++++++++++++++++++++++++++++++++\n 2 files changed, 96 insertions(+), 3 deletions(-)\n create mode 100755 t/t9834-git-p4-file-dir-bug.sh\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 3e12774f96..42f6805641 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -3012,6 +3012,40 @@ def hasBranchPrefix(self, path):\n             print('Ignoring file outside of prefix: {0}'.format(path))\n         return hasPrefix\n \n+    def findShadowedFiles(self, files, change):\n+        # Perforce allows you commit files and directories with the same name,\n+        # so you could have files //depot/foo and //depot/foo/bar both checked\n+        # in.  A p4 sync of a repository in this state fails.  Deleting one of\n+        # the files recovers the repository.\n+        #\n+        # Git will not allow the broken state to exist and only the most recent\n+        # of the conflicting names is left in the repository.  When one of the\n+        # conflicting files is deleted we need to re-add the other one to make\n+        # sure the git repository recovers in the same way as perforce.\n+        deleted = [f for f in files if f['action'] in self.delete_actions]\n+        to_check = set()\n+        for f in deleted:\n+            path = f['path']\n+            to_check.add(path + '/...')\n+            while True:\n+                path = path.rsplit(\"/\", 1)[0]\n+                if path == \"/\" or path in to_check:\n+                    break\n+                to_check.add(path)\n+        to_check = [p for p in to_check if self.hasBranchPrefix(p)]\n+        if to_check:\n+            stat_result = p4CmdList(\n+                [\"fstat\", \"-T\", \"depotFile,headRev,headType\"] +\n+                    [\"%s@%s\" % (p, change) for p in to_check])\n+            for record in stat_result:\n+                if record['code'] != 'stat':\n+                    continue\n+                files.append({\n+                    'action': 'add',\n+                    'path': record['depotFile'],\n+                    'rev': record['headRev'],\n+                    'type': record['headType']})\n+\n     def commit(self, details, files, branch, parent = \"\", allow_empty=False):\n         epoch = details[\"time\"]\n         author = details[\"user\"]\n@@ -3020,11 +3054,12 @@ def commit(self, details, files, branch, parent = \"\", allow_empty=False):\n         if self.verbose:\n             print('commit into {0}'.format(branch))\n \n+        files = [f for f in files if self.hasBranchPrefix(f['path'])]\n+        self.findShadowedFiles(files, details[\"change\"])\n+\n         if self.clientSpecDirs:\n             self.clientSpecDirs.update_client_spec_path_cache(files)\n-\n-        files = [f for f in files\n-            if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]\n+            files = [f for f in files if self.inClientSpec(f['path'])]\n \n         if gitConfigBool('git-p4.keepEmptyCommits'):\n             allow_empty = True\ndiff --git a/t/t9834-git-p4-file-dir-bug.sh b/t/t9834-git-p4-file-dir-bug.sh\nnew file mode 100755\nindex 0000000000..9839a3d2bb\n--- /dev/null\n+++ b/t/t9834-git-p4-file-dir-bug.sh\n@@ -0,0 +1,58 @@\n+#!/bin/sh\n+\n+test_description='git p4 directory/file bug handling\n+\n+This test creates files and directories with the same name in perforce and\n+checks that git-p4 recovers from the error at the same time as the perforce\n+repository.'\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+\n+\t\ttouch add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\tp4 add add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\tmkdir add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\ttouch add_dir_add_file_del_file/file add_dir_add_file_del_dir/file &&\n+\t\tp4 add add_dir_add_file_del_file/file add_dir_add_file_del_dir/file &&\n+\t\tp4 submit -d \"add initial\" &&\n+\n+\t\trm -f add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\tmkdir add_file_add_dir_del_file add_file_add_dir_del_dir &&\n+\t\ttouch add_file_add_dir_del_file/file add_file_add_dir_del_dir/file &&\n+\t\tp4 add add_file_add_dir_del_file/file add_file_add_dir_del_dir/file &&\n+\t\trm -rf add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\ttouch add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\tp4 add add_dir_add_file_del_file add_dir_add_file_del_dir &&\n+\t\tp4 submit -d \"add conflicting\" &&\n+\n+\t\tp4 delete -k add_file_add_dir_del_file &&\n+\t\tp4 delete -k add_file_add_dir_del_dir/file &&\n+\t\tp4 delete -k add_dir_add_file_del_file &&\n+\t\tp4 delete -k add_dir_add_file_del_dir/file &&\n+\t\tp4 submit -d \"delete conflicting\"\n+\t)\n+'\n+\n+test_expect_success 'clone with git-p4' '\n+\tgit p4 clone --dest=\"$git\" //depot/@all\n+'\n+\n+test_expect_success 'check final contents' '\n+\ttest_path_is_dir \"$git/add_file_add_dir_del_file\" &&\n+\ttest_path_is_file \"$git/add_file_add_dir_del_dir\" &&\n+\ttest_path_is_dir \"$git/add_dir_add_file_del_file\" &&\n+\ttest_path_is_file \"$git/add_dir_add_file_del_dir\"\n+'\n+\n+test_expect_success 'kill p4d' '\n+\tkill_p4d\n+'\n+\n+test_done\n-- \n2.19.2\n\n"},{"id":"369006","messageId":"20190211085405.51d752b7@ado-amd-gentoo.home.adoakley.name","threadId":"50417","inReplyTo":"20190208091033.12617-1-andrew@adoakley.name","subject":"Re: [PATCH v2] git-p4: recover from inconsistent perforce history","fromName":"Andrew Oakley","fromEmail":"andrew@adoakley.name","sentAt":"2019-02-11T08:54:05Z","receivedAt":"2019-02-11T08:54:08Z","isPatch":true,"sender":{"key":"andrew@adoakley.name","avatar":"https://avatars.githubusercontent.com/u/1107440?v=4"},"body":"On Fri,  8 Feb 2019 09:10:33 +0000\nandrew@adoakley.name wrote:\n\n> From: Andrew Oakley <andrew@adoakley.name>\n> \n> Perforce allows you commit files and directories with the same name,\n> so you could have files //depot/foo and //depot/foo/bar both checked\n> in.  A p4 sync of a repository in this state fails.  Deleting one of\n> the files recovers the repository.\n> \n> When this happens we want git-p4 to recover in the same way as\n> perforce.\n\nUnfortunately this still has a few issues:\n- newer p4d versions don't allow this by default so the tests fail\n- deleted files get recreated in some cases\n- command line too long when there are lots of deletions\n\nThese are all easily fixable, but I'll test more on a real repository\nwith before sharing another patch.\n\n-- \nAndrew Oakley\n"}]}