{"thread":{"id":"40389","subject":"[PATCH v6 0/7] git-p4: add support for large file systems","startedAt":"2015-09-20T20:26:20Z","lastAt":"2015-09-21T20:05:10Z","messageCount":10,"participants":["larsxschneider@gmail.com","Luke Diamand","Junio C Hamano"],"isPatch":true,"patchVersion":6,"patchTotal":7},"messages":[{"id":"270357","messageId":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":null,"subject":"[PATCH v6 0/7] git-p4: add support for large file systems","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:20Z","receivedAt":"2015-09-20T20:26:20Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\ndiff to v5:\n* remove superfluous flush call (thanks Luke!)\n* keep &&-chain intact in test cases (thanks Luke, Junio and Eric!)\n* add mock LFS test case for deleting files (thanks Luke!)\n* add mock LFS test case for git-p4.largeFileExtensions option\n* add Luke's ACK to commit \"git-p4: add optional type specifier to gitConfig reader\" (thanks Luke!)\n* replace substring extraction in test case ... \"${VAR:X:Y}\" only works in bash, \"$(echo \"$VAR\" | cut -cX-Y)\" works in sh\n\nCheers,\nLars\n\nLars Schneider (7):\n  git-p4: add optional type specifier to gitConfig reader\n  git-p4: add gitConfigInt reader\n  git-p4: return an empty list if a list config has no values\n  git-p4: add file streaming progress in verbose mode\n  git-p4: check free space during streaming\n  git-p4: add support for large file systems\n  git-p4: add Git LFS backend for large file system\n\n Documentation/git-p4.txt   |  32 +++++\n git-p4.py                  | 278 ++++++++++++++++++++++++++++++++++++++++---\n t/t9823-git-p4-mock-lfs.sh | 170 ++++++++++++++++++++++++++\n t/t9824-git-p4-git-lfs.sh  | 288 +++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 751 insertions(+), 17 deletions(-)\n create mode 100755 t/t9823-git-p4-mock-lfs.sh\n create mode 100755 t/t9824-git-p4-git-lfs.sh\n\n--\n2.5.1\n"},{"id":"270363","messageId":"1442780787-65166-2-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"[PATCH v6 1/7] git-p4: add optional type specifier to gitConfig reader","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:21Z","receivedAt":"2015-09-20T20:26:21Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\nThe functions “gitConfig” and “gitConfigBool” are almost identical. Make “gitConfig” more generic by adding an optional type specifier. Use the type specifier “—bool” with “gitConfig” to implement “gitConfigBool. This prepares the implementation of other type specifiers such as “—int”.\n\nSigned-off-by: Lars Schneider <larsxschneider@gmail.com>\nAcked-by: Luke Diamand <luke@diamand.org>\n---\n git-p4.py | 12 ++++++------\n 1 file changed, 6 insertions(+), 6 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 073f87b..c139cab 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -604,9 +604,12 @@ def gitBranchExists(branch):\n \n _gitConfig = {}\n \n-def gitConfig(key):\n+def gitConfig(key, typeSpecifier=None):\n     if not _gitConfig.has_key(key):\n-        cmd = [ \"git\", \"config\", key ]\n+        cmd = [ \"git\", \"config\" ]\n+        if typeSpecifier:\n+            cmd += [ typeSpecifier ]\n+        cmd += [ key ]\n         s = read_pipe(cmd, ignore_error=True)\n         _gitConfig[key] = s.strip()\n     return _gitConfig[key]\n@@ -617,10 +620,7 @@ def gitConfigBool(key):\n        in the config.\"\"\"\n \n     if not _gitConfig.has_key(key):\n-        cmd = [ \"git\", \"config\", \"--bool\", key ]\n-        s = read_pipe(cmd, ignore_error=True)\n-        v = s.strip()\n-        _gitConfig[key] = v == \"true\"\n+        _gitConfig[key] = gitConfig(key, '--bool') == \"true\"\n     return _gitConfig[key]\n \n def gitConfigList(key):\n-- \n2.5.1\n"},{"id":"270359","messageId":"1442780787-65166-3-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"[PATCH v6 2/7] git-p4: add gitConfigInt reader","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:22Z","receivedAt":"2015-09-20T20:26:22Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\nAdd a git config reader for integer variables. Please note that the git config implementation automatically supports k, m, and g suffixes.\n\nSigned-off-by: Lars Schneider <larsxschneider@gmail.com>\n---\n git-p4.py | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex c139cab..40ad4ae 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -623,6 +623,17 @@ def gitConfigBool(key):\n         _gitConfig[key] = gitConfig(key, '--bool') == \"true\"\n     return _gitConfig[key]\n \n+def gitConfigInt(key):\n+    if not _gitConfig.has_key(key):\n+        cmd = [ \"git\", \"config\", \"--int\", key ]\n+        s = read_pipe(cmd, ignore_error=True)\n+        v = s.strip()\n+        try:\n+            _gitConfig[key] = int(gitConfig(key, '--int'))\n+        except ValueError:\n+            _gitConfig[key] = None\n+    return _gitConfig[key]\n+\n def gitConfigList(key):\n     if not _gitConfig.has_key(key):\n         s = read_pipe([\"git\", \"config\", \"--get-all\", key], ignore_error=True)\n-- \n2.5.1\n"},{"id":"270362","messageId":"1442780787-65166-4-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"[PATCH v6 3/7] git-p4: return an empty list if a list config has no values","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:23Z","receivedAt":"2015-09-20T20:26:23Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\nSigned-off-by: Lars Schneider <larsxschneider@gmail.com>\n---\n git-p4.py | 2 ++\n 1 file changed, 2 insertions(+)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 40ad4ae..90d3b90 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -638,6 +638,8 @@ def gitConfigList(key):\n     if not _gitConfig.has_key(key):\n         s = read_pipe([\"git\", \"config\", \"--get-all\", key], ignore_error=True)\n         _gitConfig[key] = s.strip().split(os.linesep)\n+        if _gitConfig[key] == ['']:\n+            _gitConfig[key] = []\n     return _gitConfig[key]\n \n def p4BranchesInGit(branchesAreInRemotes=True):\n-- \n2.5.1\n"},{"id":"270358","messageId":"1442780787-65166-5-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"[PATCH v6 4/7] git-p4: add file streaming progress in verbose mode","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:24Z","receivedAt":"2015-09-20T20:26:24Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\nIf a file is streamed from P4 to Git then the verbose mode prints\ncontinuously the progress as percentage like this:\n//depot/file.bin 20% (10 MB)\n\nUpon completion the progress is overwritten with depot source, local\nfile and size like this:\n//depot/file.bin --> local/file.bin (10 MB)\n\nSigned-off-by: Lars Schneider <larsxschneider@gmail.com>\n---\n git-p4.py | 20 ++++++++++++++++++--\n 1 file changed, 18 insertions(+), 2 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 90d3b90..e7a7ea4 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -2164,7 +2164,9 @@ class P4Sync(Command, P4UserMap):\n     def streamOneP4File(self, file, contents):\n         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)\n         if verbose:\n-            sys.stderr.write(\"%s\\n\" % relPath)\n+            size = int(self.stream_file['fileSize'])\n+            sys.stdout.write('\\r%s --> %s (%i MB)\\n' % (file['depotFile'], relPath, size/1024/1024))\n+            sys.stdout.flush()\n \n         (type_base, type_mods) = split_p4_type(file[\"type\"])\n \n@@ -2241,7 +2243,8 @@ class P4Sync(Command, P4UserMap):\n     def streamOneP4Deletion(self, file):\n         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)\n         if verbose:\n-            sys.stderr.write(\"delete %s\\n\" % relPath)\n+            sys.stdout.write(\"delete %s\\n\" % relPath)\n+            sys.stdout.flush()\n         self.gitStream.write(\"D %s\\n\" % relPath)\n \n     # handle another chunk of streaming data\n@@ -2281,10 +2284,23 @@ class P4Sync(Command, P4UserMap):\n         # 'data' field we need to append to our array\n         for k in marshalled.keys():\n             if k == 'data':\n+                if 'streamContentSize' not in self.stream_file:\n+                    self.stream_file['streamContentSize'] = 0\n+                self.stream_file['streamContentSize'] += len(marshalled['data'])\n                 self.stream_contents.append(marshalled['data'])\n             else:\n                 self.stream_file[k] = marshalled[k]\n \n+        if (verbose and\n+            'streamContentSize' in self.stream_file and\n+            'fileSize' in self.stream_file and\n+            'depotFile' in self.stream_file):\n+            size = int(self.stream_file[\"fileSize\"])\n+            if size > 0:\n+                progress = 100*self.stream_file['streamContentSize']/size\n+                sys.stdout.write('\\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))\n+                sys.stdout.flush()\n+\n         self.stream_have_file_info = True\n \n     # Stream directly from \"p4 files\" into \"git fast-import\"\n-- \n2.5.1\n"},{"id":"270360","messageId":"1442780787-65166-6-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"[PATCH v6 5/7] git-p4: check free space during streaming","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:25Z","receivedAt":"2015-09-20T20:26:25Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\ngit-p4 will just halt if there is not enough disk space while\nstreaming content from P4 to Git. Add a check to ensure at least\n4 times (arbitrarily chosen) the size of a streamed file is available.\n\nSigned-off-by: Lars Schneider <larsxschneider@gmail.com>\n---\n git-p4.py | 27 ++++++++++++++++++++++++++-\n 1 file changed, 26 insertions(+), 1 deletion(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex e7a7ea4..b465356 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -104,6 +104,16 @@ def chdir(path, is_client_path=False):\n         path = os.getcwd()\n     os.environ['PWD'] = path\n \n+def calcDiskFree(dirname):\n+    \"\"\"Return free space in bytes on the disk of the given dirname.\"\"\"\n+    if platform.system() == 'Windows':\n+        free_bytes = ctypes.c_ulonglong(0)\n+        ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(dirname), None, None, ctypes.pointer(free_bytes))\n+        return free_bytes.value\n+    else:\n+        st = os.statvfs(dirname)\n+        return st.f_bavail * st.f_frsize\n+\n def die(msg):\n     if verbose:\n         raise Exception(msg)\n@@ -2038,6 +2048,14 @@ class P4Sync(Command, P4UserMap):\n         self.clientSpecDirs = None\n         self.tempBranches = []\n         self.tempBranchLocation = \"git-p4-tmp\"\n+        self.largeFileSystem = None\n+        self.cloneDestination = None\n+\n+        if gitConfig('git-p4.largeFileSystem'):\n+            largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]\n+            self.largeFileSystem = largeFileSystemConstructor(\n+                lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)\n+            )\n \n         if gitConfig(\"git-p4.syncFromOrigin\") == \"false\":\n             self.syncWithOrigin = False\n@@ -2256,6 +2274,14 @@ class P4Sync(Command, P4UserMap):\n             if marshalled[\"code\"] == \"error\":\n                 if \"data\" in marshalled:\n                     err = marshalled[\"data\"].rstrip()\n+\n+        if not err and self.cloneDestination and 'fileSize' in self.stream_file:\n+            required_bytes = int((4 * int(self.stream_file[\"fileSize\"])) - calcDiskFree(self.cloneDestination))\n+            if required_bytes > 0:\n+                err = 'Not enough space left on %s! Free at least %i MB.' % (\n+                    os.path.abspath(self.cloneDestination), required_bytes/1024/1024\n+                )\n+\n         if err:\n             f = None\n             if self.stream_have_file_info:\n@@ -3218,7 +3244,6 @@ class P4Clone(P4Sync):\n             optparse.make_option(\"--bare\", dest=\"cloneBare\",\n                                  action=\"store_true\", default=False),\n         ]\n-        self.cloneDestination = None\n         self.needsGit = False\n         self.cloneBare = False\n \n-- \n2.5.1\n"},{"id":"270364","messageId":"1442780787-65166-7-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"[PATCH v6 6/7] git-p4: add support for large file systems","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:26Z","receivedAt":"2015-09-20T20:26:26Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\nPerforce repositories can contain large (binary) files. Migrating these\nrepositories to Git generates very large local clones. External storage\nsystems such as Git LFS [1], Git Fat [2], Git Media [3], git-annex [4]\ntry to address this problem.\n\nAdd a generic mechanism to detect large files based on extension,\nuncompressed size, and/or compressed size.\n\n[1] https://git-lfs.github.com/\n[2] https://github.com/jedbrown/git-fat\n[3] https://github.com/alebedev/git-media\n[4] https://git-annex.branchable.com/\n\nSigned-off-by: Lars Schneider <larsxschneider@gmail.com>\n---\n Documentation/git-p4.txt   |  32 +++++++++\n git-p4.py                  | 138 +++++++++++++++++++++++++++++++++---\n t/t9823-git-p4-mock-lfs.sh | 170 +++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 330 insertions(+), 10 deletions(-)\n create mode 100755 t/t9823-git-p4-mock-lfs.sh\n\ndiff --git a/Documentation/git-p4.txt b/Documentation/git-p4.txt\nindex 82aa5d6..3f21e95 100644\n--- a/Documentation/git-p4.txt\n+++ b/Documentation/git-p4.txt\n@@ -510,6 +510,38 @@ git-p4.useClientSpec::\n \toption '--use-client-spec'.  See the \"CLIENT SPEC\" section above.\n \tThis variable is a boolean, not the name of a p4 client.\n \n+git-p4.largeFileSystem::\n+\tSpecify the system that is used for large (binary) files. Please note\n+\tthat large file systems do not support the 'git p4 submit' command.\n+\tOnly Git LFS [1] is implemented right now. Download\n+\tand install the Git LFS command line extension to use this option\n+\tand configure it like this:\n++\n+-------------\n+git config       git-p4.largeFileSystem GitLFS\n+-------------\n++\n+\t[1] https://git-lfs.github.com/\n+\n+git-p4.largeFileExtensions::\n+\tAll files matching a file extension in the list will be processed\n+\tby the large file system. Do not prefix the extensions with '.'.\n+\n+git-p4.largeFileThreshold::\n+\tAll files with an uncompressed size exceeding the threshold will be\n+\tprocessed by the large file system. By default the threshold is\n+\tdefined in bytes. Add the suffix k, m, or g to change the unit.\n+\n+git-p4.largeFileCompressedThreshold::\n+\tAll files with a compressed size exceeding the threshold will be\n+\tprocessed by the large file system. This option might slow down\n+\tyour clone/sync process. By default the threshold is defined in\n+\tbytes. Add the suffix k, m, or g to change the unit.\n+\n+git-p4.pushLargeFiles::\n+\tBoolean variable which defines if large files are automatically\n+\tpushed to a server.\n+\n Submit variables\n ~~~~~~~~~~~~~~~~\n git-p4.detectRenames::\ndiff --git a/git-p4.py b/git-p4.py\nindex b465356..05aeb2c 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -22,6 +22,8 @@ import platform\n import re\n import shutil\n import stat\n+import zipfile\n+import zlib\n \n try:\n     from subprocess import CalledProcessError\n@@ -932,6 +934,110 @@ def wildcard_present(path):\n     m = re.search(\"[*#@%]\", path)\n     return m is not None\n \n+class LargeFileSystem(object):\n+    \"\"\"Base class for large file system support.\"\"\"\n+\n+    def __init__(self, writeToGitStream):\n+        self.largeFiles = set()\n+        self.writeToGitStream = writeToGitStream\n+\n+    def generatePointer(self, cloneDestination, contentFile):\n+        \"\"\"Return the content of a pointer file that is stored in Git instead of\n+           the actual content.\"\"\"\n+        assert False, \"Method 'generatePointer' required in \" + self.__class__.__name__\n+\n+    def pushFile(self, localLargeFile):\n+        \"\"\"Push the actual content which is not stored in the Git repository to\n+           a server.\"\"\"\n+        assert False, \"Method 'pushFile' required in \" + self.__class__.__name__\n+\n+    def hasLargeFileExtension(self, relPath):\n+        return reduce(\n+            lambda a, b: a or b,\n+            [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],\n+            False\n+        )\n+\n+    def generateTempFile(self, contents):\n+        contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)\n+        for d in contents:\n+            contentFile.write(d)\n+        contentFile.close()\n+        return contentFile.name\n+\n+    def exceedsLargeFileThreshold(self, relPath, contents):\n+        if gitConfigInt('git-p4.largeFileThreshold'):\n+            contentsSize = sum(len(d) for d in contents)\n+            if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):\n+                return True\n+        if gitConfigInt('git-p4.largeFileCompressedThreshold'):\n+            contentsSize = sum(len(d) for d in contents)\n+            if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):\n+                return False\n+            contentTempFile = self.generateTempFile(contents)\n+            compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)\n+            zf = zipfile.ZipFile(compressedContentFile.name, mode='w')\n+            zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)\n+            zf.close()\n+            compressedContentsSize = zf.infolist()[0].compress_size\n+            os.remove(contentTempFile)\n+            os.remove(compressedContentFile.name)\n+            if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):\n+                return True\n+        return False\n+\n+    def addLargeFile(self, relPath):\n+        self.largeFiles.add(relPath)\n+\n+    def removeLargeFile(self, relPath):\n+        self.largeFiles.remove(relPath)\n+\n+    def isLargeFile(self, relPath):\n+        return relPath in self.largeFiles\n+\n+    def processContent(self, cloneDestination, git_mode, relPath, contents):\n+        \"\"\"Processes the content of git fast import. This method decides if a\n+           file is stored in the large file system and handles all necessary\n+           steps.\"\"\"\n+        if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):\n+            contentTempFile = self.generateTempFile(contents)\n+            (git_mode, contents, localLargeFile) = self.generatePointer(cloneDestination, contentTempFile)\n+\n+            # Move temp file to final location in large file system\n+            largeFileDir = os.path.dirname(localLargeFile)\n+            if not os.path.isdir(largeFileDir):\n+                os.makedirs(largeFileDir)\n+            shutil.move(contentTempFile, localLargeFile)\n+            self.addLargeFile(relPath)\n+            if gitConfigBool('git-p4.pushLargeFiles'):\n+                self.pushFile(localLargeFile)\n+            if verbose:\n+                sys.stderr.write(\"%s moved to large file system (%s)\\n\" % (relPath, localLargeFile))\n+        return (git_mode, contents)\n+\n+class MockLFS(LargeFileSystem):\n+    \"\"\"Mock large file system for testing.\"\"\"\n+\n+    def generatePointer(self, cloneDestination, contentFile):\n+        \"\"\"The pointer content is the original content prefixed with \"pointer-\".\n+           The local filename of the large file storage is derived from the file content.\n+           \"\"\"\n+        with open(contentFile, 'r') as f:\n+            content = next(f)\n+            gitMode = '100644'\n+            pointerContents = 'pointer-' + content\n+            localLargeFile = os.path.join(cloneDestination, '.git', 'mock-storage', 'local', content[:-1])\n+            return (gitMode, pointerContents, localLargeFile)\n+\n+    def pushFile(self, localLargeFile):\n+        \"\"\"The remote filename of the large file storage is the same as the local\n+           one but in a different directory.\n+           \"\"\"\n+        remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')\n+        if not os.path.exists(remotePath):\n+            os.makedirs(remotePath)\n+        shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))\n+\n class Command:\n     def __init__(self):\n         self.usage = \"usage: %prog [options]\"\n@@ -1105,6 +1211,9 @@ class P4Submit(Command, P4UserMap):\n         self.p4HasMoveCommand = p4_has_move_command()\n         self.branch = None\n \n+        if gitConfig('git-p4.largeFileSystem'):\n+            die(\"Large file system not supported for git-p4 submit command. Please remove it from config.\")\n+\n     def check(self):\n         if len(p4CmdList(\"opened ...\")) > 0:\n             die(\"You have files opened with perforce! Close them before starting the sync.\")\n@@ -2057,6 +2166,12 @@ class P4Sync(Command, P4UserMap):\n                 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)\n             )\n \n+        if gitConfig('git-p4.largeFileSystem'):\n+            largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]\n+            self.largeFileSystem = largeFileSystemConstructor(\n+                lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)\n+            )\n+\n         if gitConfig(\"git-p4.syncFromOrigin\") == \"false\":\n             self.syncWithOrigin = False\n \n@@ -2176,6 +2291,13 @@ class P4Sync(Command, P4UserMap):\n \n         return branches\n \n+    def writeToGitStream(self, gitMode, relPath, contents):\n+        self.gitStream.write('M %s inline %s\\n' % (gitMode, relPath))\n+        self.gitStream.write('data %d\\n' % sum(len(d) for d in contents))\n+        for d in contents:\n+            self.gitStream.write(d)\n+        self.gitStream.write('\\n')\n+\n     # output one file from the P4 stream\n     # - helper for streamP4Files\n \n@@ -2246,17 +2368,10 @@ class P4Sync(Command, P4UserMap):\n             text = regexp.sub(r'$\\1$', text)\n             contents = [ text ]\n \n-        self.gitStream.write(\"M %s inline %s\\n\" % (git_mode, relPath))\n+        if self.largeFileSystem:\n+            (git_mode, contents) = self.largeFileSystem.processContent(self.cloneDestination, git_mode, relPath, contents)\n \n-        # total length...\n-        length = 0\n-        for d in contents:\n-            length = length + len(d)\n-\n-        self.gitStream.write(\"data %d\\n\" % length)\n-        for d in contents:\n-            self.gitStream.write(d)\n-        self.gitStream.write(\"\\n\")\n+        self.writeToGitStream(git_mode, relPath, contents)\n \n     def streamOneP4Deletion(self, file):\n         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)\n@@ -2265,6 +2380,9 @@ class P4Sync(Command, P4UserMap):\n             sys.stdout.flush()\n         self.gitStream.write(\"D %s\\n\" % relPath)\n \n+        if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):\n+            self.largeFileSystem.removeLargeFile(relPath)\n+\n     # handle another chunk of streaming data\n     def streamP4FilesCb(self, marshalled):\n \ndiff --git a/t/t9823-git-p4-mock-lfs.sh b/t/t9823-git-p4-mock-lfs.sh\nnew file mode 100755\nindex 0000000..5684ee3\n--- /dev/null\n+++ b/t/t9823-git-p4-mock-lfs.sh\n@@ -0,0 +1,170 @@\n+#!/bin/sh\n+\n+test_description='Clone repositories and store files in Mock LFS'\n+\n+. ./lib-git-p4.sh\n+\n+test_file_is_not_in_mock_lfs () {\n+\tFILE=\"$1\" &&\n+\tCONTENT=\"$2\" &&\n+\techo \"$CONTENT\" >expect_content &&\n+\ttest_path_is_file \"$FILE\" &&\n+\ttest_cmp expect_content \"$FILE\"\n+}\n+\n+test_file_is_in_mock_lfs () {\n+\tFILE=\"$1\" &&\n+\tCONTENT=\"$2\" &&\n+\tLOCAL_STORAGE=\".git/mock-storage/local/$CONTENT\" &&\n+\tSERVER_STORAGE=\".git/mock-storage/remote/$CONTENT\" &&\n+\techo \"pointer-$CONTENT\" >expect_pointer &&\n+\techo \"$CONTENT\" >expect_content &&\n+\ttest_path_is_file \"$FILE\" &&\n+\ttest_path_is_file \"$LOCAL_STORAGE\" &&\n+\ttest_path_is_file \"$SERVER_STORAGE\" &&\n+\ttest_cmp expect_pointer \"$FILE\" &&\n+\ttest_cmp expect_content \"$LOCAL_STORAGE\" &&\n+\ttest_cmp expect_content \"$SERVER_STORAGE\"\n+}\n+\n+test_file_is_deleted_in_mock_lfs () {\n+\tFILE=\"$1\" &&\n+\tCONTENT=\"$2\" &&\n+\tLOCAL_STORAGE=\".git/mock-storage/local/$CONTENT\" &&\n+\tSERVER_STORAGE=\".git/mock-storage/remote/$CONTENT\" &&\n+\techo \"pointer-$CONTENT\" >expect_pointer &&\n+\techo \"$CONTENT\" >expect_content &&\n+\t! test_path_is_file \"$FILE\" &&\n+\ttest_path_is_file \"$LOCAL_STORAGE\" &&\n+\ttest_path_is_file \"$SERVER_STORAGE\" &&\n+\ttest_cmp expect_content \"$LOCAL_STORAGE\" &&\n+\ttest_cmp expect_content \"$SERVER_STORAGE\"\n+}\n+\n+test_file_count_in_dir () {\n+\tDIR=\"$1\" &&\n+\tEXPECTED_COUNT=\"$2\" &&\n+\tfind \"$DIR\" -type f >actual &&\n+\ttest_line_count = $EXPECTED_COUNT actual\n+}\n+\n+test_expect_success 'start p4d' '\n+\tstart_p4d\n+'\n+\n+test_expect_success 'Create repo with binary files' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\t(\n+\t\tcd \"$cli\" &&\n+\n+\t\techo \"content 1 txt 23 bytes\" >file1.txt &&\n+\t\tp4 add file1.txt &&\n+\t\techo \"content 2-3 bin 25 bytes\" >file2.dat &&\n+\t\tp4 add file2.dat &&\n+\t\tp4 submit -d \"Add text and binary file\" &&\n+\n+\t\tmkdir \"path with spaces\" &&\n+\t\techo \"content 2-3 bin 25 bytes\" >\"path with spaces/file3.bin\" &&\n+\t\tp4 add \"path with spaces/file3.bin\" &&\n+\t\tp4 submit -d \"Add another binary file with same content and spaces in path\" &&\n+\n+\t\techo \"content 4 bin 26 bytes XX\" >file4.bin &&\n+\t\tp4 add file4.bin &&\n+\t\tp4 submit -d \"Add another binary file with different content\"\n+\t)\n+'\n+\n+test_expect_success 'Store files in Mock LFS based on size (>24 bytes)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem MockLFS &&\n+\t\tgit config git-p4.largeFileThreshold 24 &&\n+\t\tgit config git-p4.pushLargeFiles True &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_is_not_in_mock_lfs file1.txt \"content 1 txt 23 bytes\" &&\n+\t\ttest_file_is_in_mock_lfs file2.dat \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_is_in_mock_lfs \"path with spaces/file3.bin\" \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_is_in_mock_lfs file4.bin \"content 4 bin 26 bytes XX\" &&\n+\n+\t\ttest_file_count_in_dir \".git/mock-storage/local\" 2 &&\n+\t\ttest_file_count_in_dir \".git/mock-storage/remote\" 2\n+\t)\n+'\n+\n+test_expect_success 'Store files in Mock LFS based on extension (dat)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem MockLFS &&\n+\t\tgit config git-p4.largeFileExtensions dat &&\n+\t\tgit config git-p4.pushLargeFiles True &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_is_not_in_mock_lfs file1.txt \"content 1 txt 23 bytes\" &&\n+\t\ttest_file_is_in_mock_lfs file2.dat \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_is_not_in_mock_lfs \"path with spaces/file3.bin\" \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_is_not_in_mock_lfs file4.bin \"content 4 bin 26 bytes XX\" &&\n+\n+\t\ttest_file_count_in_dir \".git/mock-storage/local\" 1 &&\n+\t\ttest_file_count_in_dir \".git/mock-storage/remote\" 1\n+\t)\n+'\n+\n+test_expect_success 'Remove file from repo and store files in Mock LFS based on size (>24 bytes)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\t(\n+\t\tcd \"$cli\" &&\n+\t\tp4 delete file4.bin &&\n+\t\tp4 submit -d \"Remove file\"\n+\t) &&\n+\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem MockLFS &&\n+\t\tgit config git-p4.largeFileThreshold 24 &&\n+\t\tgit config git-p4.pushLargeFiles True &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_is_not_in_mock_lfs file1.txt \"content 1 txt 23 bytes\" &&\n+\t\ttest_file_is_in_mock_lfs file2.dat \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_is_in_mock_lfs \"path with spaces/file3.bin\" \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_is_deleted_in_mock_lfs file4.bin \"content 4 bin 26 bytes XX\" &&\n+\n+\t\ttest_file_count_in_dir \".git/mock-storage/local\" 2 &&\n+\t\ttest_file_count_in_dir \".git/mock-storage/remote\" 2\n+\t)\n+'\n+\n+test_expect_success 'Run git p4 submit in repo configured with large file system' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem MockLFS &&\n+\t\tgit config git-p4.largeFileThreshold 24 &&\n+\t\tgit config git-p4.pushLargeFiles True &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_must_fail git p4 submit\n+\t)\n+'\n+\n+test_expect_success 'kill p4d' '\n+\tkill_p4d\n+'\n+\n+test_done\n-- \n2.5.1\n"},{"id":"270361","messageId":"1442780787-65166-8-git-send-email-larsxschneider@gmail.com","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"[PATCH v6 7/7] git-p4: add Git LFS backend for large file system","fromName":"","fromEmail":"larsxschneider@gmail.com","sentAt":"2015-09-20T20:26:27Z","receivedAt":"2015-09-20T20:26:27Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"From: Lars Schneider <larsxschneider@gmail.com>\n\nAdd example implementation including test cases for the large file\nsystem using Git LFS.\n\nPushing files to the Git LFS server is not tested.\n\nSigned-off-by: Lars Schneider <larsxschneider@gmail.com>\n---\n git-p4.py                 |  72 ++++++++++++\n t/t9824-git-p4-git-lfs.sh | 288 ++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 360 insertions(+)\n create mode 100755 t/t9824-git-p4-git-lfs.sh\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 05aeb2c..4e7cac5 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -1038,6 +1038,78 @@ class MockLFS(LargeFileSystem):\n             os.makedirs(remotePath)\n         shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))\n \n+class GitLFS(LargeFileSystem):\n+    \"\"\"Git LFS as backend for the git-p4 large file system.\n+       See https://git-lfs.github.com/ for details.\"\"\"\n+\n+    def __init__(self, *args):\n+        LargeFileSystem.__init__(self, *args)\n+        self.baseGitAttributes = []\n+\n+    def generatePointer(self, cloneDestination, contentFile):\n+        \"\"\"Generate a Git LFS pointer for the content. Return LFS Pointer file\n+           mode and content which is stored in the Git repository instead of\n+           the actual content. Return also the new location of the actual\n+           content.\n+           \"\"\"\n+        pointerProcess = subprocess.Popen(\n+            ['git', 'lfs', 'pointer', '--file=' + contentFile],\n+            stdout=subprocess.PIPE\n+        )\n+        pointerFile = pointerProcess.stdout.read()\n+        if pointerProcess.wait():\n+            os.remove(contentFile)\n+            die('git-lfs pointer command failed. Did you install the extension?')\n+        pointerContents = [i+'\\n' for i in pointerFile.split('\\n')[2:][:-1]]\n+        oid = pointerContents[1].split(' ')[1].split(':')[1][:-1]\n+        localLargeFile = os.path.join(\n+            cloneDestination,\n+            '.git', 'lfs', 'objects', oid[:2], oid[2:4],\n+            oid,\n+        )\n+        # LFS Spec states that pointer files should not have the executable bit set.\n+        gitMode = '100644'\n+        return (gitMode, pointerContents, localLargeFile)\n+\n+    def pushFile(self, localLargeFile):\n+        uploadProcess = subprocess.Popen(\n+            ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]\n+        )\n+        if uploadProcess.wait():\n+            die('git-lfs push command failed. Did you define a remote?')\n+\n+    def generateGitAttributes(self):\n+        return (\n+            self.baseGitAttributes +\n+            [\n+                '\\n',\n+                '#\\n',\n+                '# Git LFS (see https://git-lfs.github.com/)\\n',\n+                '#\\n',\n+            ] +\n+            ['*.' + f.replace(' ', '[:space:]') + ' filter=lfs -text\\n'\n+                for f in sorted(gitConfigList('git-p4.largeFileExtensions'))\n+            ] +\n+            ['/' + f.replace(' ', '[:space:]') + ' filter=lfs -text\\n'\n+                for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)\n+            ]\n+        )\n+\n+    def addLargeFile(self, relPath):\n+        LargeFileSystem.addLargeFile(self, relPath)\n+        self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())\n+\n+    def removeLargeFile(self, relPath):\n+        LargeFileSystem.removeLargeFile(self, relPath)\n+        self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())\n+\n+    def processContent(self, cloneDestination, git_mode, relPath, contents):\n+        if relPath == '.gitattributes':\n+            self.baseGitAttributes = contents\n+            return (git_mode, self.generateGitAttributes())\n+        else:\n+            return LargeFileSystem.processContent(self, cloneDestination, git_mode, relPath, contents)\n+\n class Command:\n     def __init__(self):\n         self.usage = \"usage: %prog [options]\"\ndiff --git a/t/t9824-git-p4-git-lfs.sh b/t/t9824-git-p4-git-lfs.sh\nnew file mode 100755\nindex 0000000..63039ef\n--- /dev/null\n+++ b/t/t9824-git-p4-git-lfs.sh\n@@ -0,0 +1,288 @@\n+#!/bin/sh\n+\n+test_description='Clone repositories and store files in Git LFS'\n+\n+. ./lib-git-p4.sh\n+\n+git lfs help >/dev/null 2>&1 || {\n+\tskip_all='skipping git p4 Git LFS tests; Git LFS not found'\n+\ttest_done\n+}\n+\n+test_file_in_lfs () {\n+\tFILE=\"$1\" &&\n+\tSIZE=\"$2\" &&\n+\tEXPECTED_CONTENT=\"$3\" &&\n+\tcat \"$FILE\" | grep \"size $SIZE\" &&\n+\tHASH=$(cat \"$FILE\" | grep \"oid sha256:\" | sed -e \"s/oid sha256://g\") &&\n+\tLFS_FILE=\".git/lfs/objects/$(echo \"$HASH\" | cut -c1-2)/$(echo \"$HASH\" | cut -c3-4)/$HASH\" &&\n+\techo $EXPECTED_CONTENT >expect &&\n+\ttest_path_is_file \"$FILE\" &&\n+\ttest_path_is_file \"$LFS_FILE\" &&\n+\ttest_cmp expect \"$LFS_FILE\"\n+}\n+\n+test_file_count_in_dir () {\n+\tDIR=\"$1\" &&\n+\tEXPECTED_COUNT=\"$2\" &&\n+\tfind \"$DIR\" -type f >actual &&\n+\ttest_line_count = $EXPECTED_COUNT actual\n+}\n+\n+test_expect_success 'start p4d' '\n+\tstart_p4d\n+'\n+\n+test_expect_success 'Create repo with binary files' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\t(\n+\t\tcd \"$cli\" &&\n+\n+\t\techo \"content 1 txt 23 bytes\" >file1.txt &&\n+\t\tp4 add file1.txt &&\n+\t\techo \"content 2-3 bin 25 bytes\" >file2.dat &&\n+\t\tp4 add file2.dat &&\n+\t\tp4 submit -d \"Add text and binary file\" &&\n+\n+\t\tmkdir \"path with spaces\" &&\n+\t\techo \"content 2-3 bin 25 bytes\" >\"path with spaces/file3.bin\" &&\n+\t\tp4 add \"path with spaces/file3.bin\" &&\n+\t\tp4 submit -d \"Add another binary file with same content and spaces in path\" &&\n+\n+\t\techo \"content 4 bin 26 bytes XX\" >file4.bin &&\n+\t\tp4 add file4.bin &&\n+\t\tp4 submit -d \"Add another binary file with different content\"\n+\t)\n+'\n+\n+test_expect_success 'Store files in LFS based on size (>24 bytes)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem GitLFS &&\n+\t\tgit config git-p4.largeFileThreshold 24 &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_in_lfs file2.dat 25 \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_in_lfs \"path with spaces/file3.bin\" 25 \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_in_lfs file4.bin 26 \"content 4 bin 26 bytes XX\" &&\n+\n+\t\ttest_file_count_in_dir \".git/lfs/objects\" 2 &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\n+\t\t#\n+\t\t# Git LFS (see https://git-lfs.github.com/)\n+\t\t#\n+\t\t/file2.dat filter=lfs -text\n+\t\t/file4.bin filter=lfs -text\n+\t\t/path[:space:]with[:space:]spaces/file3.bin filter=lfs -text\n+\t\tEOF\n+\t\ttest_path_is_file .gitattributes &&\n+\t\ttest_cmp expect .gitattributes\n+\t)\n+'\n+\n+test_expect_success 'Store files in LFS based on size (>25 bytes)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem GitLFS &&\n+\t\tgit config git-p4.largeFileThreshold 25 &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_in_lfs file4.bin 26 \"content 4 bin 26 bytes XX\" &&\n+\t\ttest_file_count_in_dir \".git/lfs/objects\" 1 &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\n+\t\t#\n+\t\t# Git LFS (see https://git-lfs.github.com/)\n+\t\t#\n+\t\t/file4.bin filter=lfs -text\n+\t\tEOF\n+\t\ttest_path_is_file .gitattributes &&\n+\t\ttest_cmp expect .gitattributes\n+\t)\n+'\n+\n+test_expect_success 'Store files in LFS based on extension (dat)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem GitLFS &&\n+\t\tgit config git-p4.largeFileExtensions dat &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_in_lfs file2.dat 25 \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_count_in_dir \".git/lfs/objects\" 1 &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\n+\t\t#\n+\t\t# Git LFS (see https://git-lfs.github.com/)\n+\t\t#\n+\t\t*.dat filter=lfs -text\n+\t\tEOF\n+\t\ttest_path_is_file .gitattributes &&\n+\t\ttest_cmp expect .gitattributes\n+\t)\n+'\n+\n+test_expect_success 'Store files in LFS based on size (>25 bytes) and extension (dat)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem GitLFS &&\n+\t\tgit config git-p4.largeFileExtensions dat &&\n+\t\tgit config git-p4.largeFileThreshold 25 &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_in_lfs file2.dat 25 \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_in_lfs file4.bin 26 \"content 4 bin 26 bytes XX\" &&\n+\t\ttest_file_count_in_dir \".git/lfs/objects\" 2 &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\n+\t\t#\n+\t\t# Git LFS (see https://git-lfs.github.com/)\n+\t\t#\n+\t\t*.dat filter=lfs -text\n+\t\t/file4.bin filter=lfs -text\n+\t\tEOF\n+\t\ttest_path_is_file .gitattributes &&\n+\t\ttest_cmp expect .gitattributes\n+\t)\n+'\n+\n+test_expect_success 'Remove file from repo and store files in LFS based on size (>24 bytes)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\t(\n+\t\tcd \"$cli\" &&\n+\t\tp4 delete file4.bin &&\n+\t\tp4 submit -d \"Remove file\"\n+\t) &&\n+\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem GitLFS &&\n+\t\tgit config git-p4.largeFileThreshold 24 &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_in_lfs file2.dat 25 \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_in_lfs \"path with spaces/file3.bin\" 25 \"content 2-3 bin 25 bytes\" &&\n+\t\t! test_path_is_file file4.bin &&\n+\t\ttest_file_count_in_dir \".git/lfs/objects\" 2 &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\n+\t\t#\n+\t\t# Git LFS (see https://git-lfs.github.com/)\n+\t\t#\n+\t\t/file2.dat filter=lfs -text\n+\t\t/path[:space:]with[:space:]spaces/file3.bin filter=lfs -text\n+\t\tEOF\n+\t\ttest_path_is_file .gitattributes &&\n+\t\ttest_cmp expect .gitattributes\n+\t)\n+'\n+\n+test_expect_success 'Add .gitattributes and store files in LFS based on size (>24 bytes)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\t(\n+\t\tcd \"$cli\" &&\n+\t\techo \"*.txt text\" >.gitattributes &&\n+\t\tp4 add .gitattributes &&\n+\t\tp4 submit -d \"Add .gitattributes\"\n+\t) &&\n+\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem GitLFS &&\n+\t\tgit config git-p4.largeFileThreshold 24 &&\n+\t\tgit p4 clone --destination=\"$git\" //depot@all &&\n+\n+\t\ttest_file_in_lfs file2.dat 25 \"content 2-3 bin 25 bytes\" &&\n+\t\ttest_file_in_lfs \"path with spaces/file3.bin\" 25 \"content 2-3 bin 25 bytes\" &&\n+\t\t! test_path_is_file file4.bin &&\n+\t\ttest_file_count_in_dir \".git/lfs/objects\" 2 &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t*.txt text\n+\n+\t\t#\n+\t\t# Git LFS (see https://git-lfs.github.com/)\n+\t\t#\n+\t\t/file2.dat filter=lfs -text\n+\t\t/path[:space:]with[:space:]spaces/file3.bin filter=lfs -text\n+\t\tEOF\n+\t\ttest_path_is_file .gitattributes &&\n+\t\ttest_cmp expect .gitattributes\n+\t)\n+'\n+\n+test_expect_success 'Add big files to repo and store files in LFS based on compressed size (>28 bytes)' '\n+\tclient_view \"//depot/... //client/...\" &&\n+\t(\n+\t\tcd \"$cli\" &&\n+\t\techo \"content 5 bin 40 bytes XXXXXXXXXXXXXXXX\" >file5.bin &&\n+\t\tp4 add file5.bin &&\n+\t\tp4 submit -d \"Add file with small footprint after compression\" &&\n+\n+\t\techo \"content 6 bin 39 bytes XXXXXYYYYYZZZZZ\" >file6.bin &&\n+\t\tp4 add file6.bin &&\n+\t\tp4 submit -d \"Add file with large footprint after compression\"\n+\t) &&\n+\n+\tclient_view \"//depot/... //client/...\" &&\n+\ttest_when_finished cleanup_git &&\n+\t(\n+\t\tcd \"$git\" &&\n+\t\tgit init . &&\n+\t\tgit config git-p4.useClientSpec true &&\n+\t\tgit config git-p4.largeFileSystem GitLFS &&\n+\t\tgit config git-p4.largeFileCompressedThreshold 28 &&\n+\t\t# We only import HEAD here (\"@all\" is missing!)\n+\t\tgit p4 clone --destination=\"$git\" //depot &&\n+\n+\t\ttest_file_in_lfs file6.bin 13 \"content 6 bin 39 bytes XXXXXYYYYYZZZZZ\"\n+\t\ttest_file_count_in_dir \".git/lfs/objects\" 1 &&\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t*.txt text\n+\n+\t\t#\n+\t\t# Git LFS (see https://git-lfs.github.com/)\n+\t\t#\n+\t\t/file6.bin filter=lfs -text\n+\t\tEOF\n+\t\ttest_path_is_file .gitattributes &&\n+\t\ttest_cmp expect .gitattributes\n+\t)\n+'\n+\n+test_expect_success 'kill p4d' '\n+\tkill_p4d\n+'\n+\n+test_done\n-- \n2.5.1\n"},{"id":"270420","messageId":"56005E6A.4000308@diamand.org","threadId":"40389","inReplyTo":"1442780787-65166-1-git-send-email-larsxschneider@gmail.com","subject":"Re: [PATCH v6 0/7] git-p4: add support for large file systems","fromName":"Luke Diamand","fromEmail":"luke@diamand.org","sentAt":"2015-09-21T19:45:46Z","receivedAt":"2015-09-21T19:45:46Z","isPatch":true,"sender":{"key":"luke@diamand.org","avatar":"https://avatars.githubusercontent.com/u/5330967?v=4"},"body":"On 20/09/15 21:26, larsxschneider@gmail.com wrote:\n> From: Lars Schneider <larsxschneider@gmail.com>\n>\n> diff to v5:\n> * remove superfluous flush call (thanks Luke!)\n> * keep &&-chain intact in test cases (thanks Luke, Junio and Eric!)\n> * add mock LFS test case for deleting files (thanks Luke!)\n> * add mock LFS test case for git-p4.largeFileExtensions option\n> * add Luke's ACK to commit \"git-p4: add optional type specifier to gitConfig reader\" (thanks Luke!)\n> * replace substring extraction in test case ... \"${VAR:X:Y}\" only works in bash, \"$(echo \"$VAR\" | cut -cX-Y)\" works in sh\n\nSorry about this, but patch 6 no longer seems to apply cleanly to next \n(or at least not for me).\n\nLuke\n"},{"id":"270422","messageId":"xmqq1tdra689.fsf@gitster.mtv.corp.google.com","threadId":"40389","inReplyTo":"56005E6A.4000308@diamand.org","subject":"Re: [PATCH v6 0/7] git-p4: add support for large file systems","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2015-09-21T20:05:10Z","receivedAt":"2015-09-21T20:05:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Luke Diamand <luke@diamand.org> writes:\n\n> On 20/09/15 21:26, larsxschneider@gmail.com wrote:\n>> From: Lars Schneider <larsxschneider@gmail.com>\n>>\n>> diff to v5:\n>> * remove superfluous flush call (thanks Luke!)\n>> * keep &&-chain intact in test cases (thanks Luke, Junio and Eric!)\n>> * add mock LFS test case for deleting files (thanks Luke!)\n>> * add mock LFS test case for git-p4.largeFileExtensions option\n>> * add Luke's ACK to commit \"git-p4: add optional type specifier to\n>> gitConfig reader\" (thanks Luke!)\n>> * replace substring extraction in test case ... \"${VAR:X:Y}\" only\n>> works in bash, \"$(echo \"$VAR\" | cut -cX-Y)\" works in sh\n>\n> Sorry about this, but patch 6 no longer seems to apply cleanly to next\n> (or at least not for me).\n\nPlease check the conflict resolution when I push today's result out\non 'pu'.\n"}]}