Skip to content

Commit a5db4b1

Browse files
larsxschneidergitster
authored andcommitted
git-p4: add support for large file systems
Perforce repositories can contain large (binary) files. Migrating these repositories to Git generates very large local clones. External storage systems such as Git LFS [1], Git Fat [2], Git Media [3], git-annex [4] try to address this problem. Add a generic mechanism to detect large files based on extension, uncompressed size, and/or compressed size. [1] https://git-lfs.github.com/ [2] https://github.com/jedbrown/git-fat [3] https://github.com/alebedev/git-media [4] https://git-annex.branchable.com/ Signed-off-by: Lars Schneider <[email protected]> Conflicts: Documentation/git-p4.txt git-p4.py Signed-off-by: Junio C Hamano <[email protected]>
1 parent 4d25dc4 commit a5db4b1

File tree

3 files changed

+353
-10
lines changed

3 files changed

+353
-10
lines changed

Documentation/git-p4.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,38 @@ git-p4.useClientSpec::
510510
option '--use-client-spec'. See the "CLIENT SPEC" section above.
511511
This variable is a boolean, not the name of a p4 client.
512512

513+
git-p4.largeFileSystem::
514+
Specify the system that is used for large (binary) files. Please note
515+
that large file systems do not support the 'git p4 submit' command.
516+
Only Git LFS [1] is implemented right now. Download
517+
and install the Git LFS command line extension to use this option
518+
and configure it like this:
519+
+
520+
-------------
521+
git config git-p4.largeFileSystem GitLFS
522+
-------------
523+
+
524+
[1] https://git-lfs.github.com/
525+
526+
git-p4.largeFileExtensions::
527+
All files matching a file extension in the list will be processed
528+
by the large file system. Do not prefix the extensions with '.'.
529+
530+
git-p4.largeFileThreshold::
531+
All files with an uncompressed size exceeding the threshold will be
532+
processed by the large file system. By default the threshold is
533+
defined in bytes. Add the suffix k, m, or g to change the unit.
534+
535+
git-p4.largeFileCompressedThreshold::
536+
All files with a compressed size exceeding the threshold will be
537+
processed by the large file system. This option might slow down
538+
your clone/sync process. By default the threshold is defined in
539+
bytes. Add the suffix k, m, or g to change the unit.
540+
541+
git-p4.largeFilePush::
542+
Boolean variable which defines if large files are automatically
543+
pushed to a server.
544+
513545
Submit variables
514546
~~~~~~~~~~~~~~~~
515547
git-p4.detectRenames::

git-p4.py

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import re
2323
import shutil
2424
import stat
25+
import zipfile
26+
import zlib
2527

2628
try:
2729
from subprocess import CalledProcessError
@@ -932,6 +934,110 @@ def wildcard_present(path):
932934
m = re.search("[*#@%]", path)
933935
return m is not None
934936

937+
class LargeFileSystem(object):
938+
"""Base class for large file system support."""
939+
940+
def __init__(self, writeToGitStream):
941+
self.largeFiles = set()
942+
self.writeToGitStream = writeToGitStream
943+
944+
def generatePointer(self, cloneDestination, contentFile):
945+
"""Return the content of a pointer file that is stored in Git instead of
946+
the actual content."""
947+
assert False, "Method 'generatePointer' required in " + self.__class__.__name__
948+
949+
def pushFile(self, localLargeFile):
950+
"""Push the actual content which is not stored in the Git repository to
951+
a server."""
952+
assert False, "Method 'pushFile' required in " + self.__class__.__name__
953+
954+
def hasLargeFileExtension(self, relPath):
955+
return reduce(
956+
lambda a, b: a or b,
957+
[relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
958+
False
959+
)
960+
961+
def generateTempFile(self, contents):
962+
contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
963+
for d in contents:
964+
contentFile.write(d)
965+
contentFile.close()
966+
return contentFile.name
967+
968+
def exceedsLargeFileThreshold(self, relPath, contents):
969+
if gitConfigInt('git-p4.largeFileThreshold'):
970+
contentsSize = sum(len(d) for d in contents)
971+
if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
972+
return True
973+
if gitConfigInt('git-p4.largeFileCompressedThreshold'):
974+
contentsSize = sum(len(d) for d in contents)
975+
if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
976+
return False
977+
contentTempFile = self.generateTempFile(contents)
978+
compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
979+
zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
980+
zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
981+
zf.close()
982+
compressedContentsSize = zf.infolist()[0].compress_size
983+
os.remove(contentTempFile)
984+
os.remove(compressedContentFile.name)
985+
if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
986+
return True
987+
return False
988+
989+
def addLargeFile(self, relPath):
990+
self.largeFiles.add(relPath)
991+
992+
def removeLargeFile(self, relPath):
993+
self.largeFiles.remove(relPath)
994+
995+
def isLargeFile(self, relPath):
996+
return relPath in self.largeFiles
997+
998+
def processContent(self, git_mode, relPath, contents):
999+
"""Processes the content of git fast import. This method decides if a
1000+
file is stored in the large file system and handles all necessary
1001+
steps."""
1002+
if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1003+
contentTempFile = self.generateTempFile(contents)
1004+
(git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1005+
1006+
# Move temp file to final location in large file system
1007+
largeFileDir = os.path.dirname(localLargeFile)
1008+
if not os.path.isdir(largeFileDir):
1009+
os.makedirs(largeFileDir)
1010+
shutil.move(contentTempFile, localLargeFile)
1011+
self.addLargeFile(relPath)
1012+
if gitConfigBool('git-p4.largeFilePush'):
1013+
self.pushFile(localLargeFile)
1014+
if verbose:
1015+
sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1016+
return (git_mode, contents)
1017+
1018+
class MockLFS(LargeFileSystem):
1019+
"""Mock large file system for testing."""
1020+
1021+
def generatePointer(self, contentFile):
1022+
"""The pointer content is the original content prefixed with "pointer-".
1023+
The local filename of the large file storage is derived from the file content.
1024+
"""
1025+
with open(contentFile, 'r') as f:
1026+
content = next(f)
1027+
gitMode = '100644'
1028+
pointerContents = 'pointer-' + content
1029+
localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1030+
return (gitMode, pointerContents, localLargeFile)
1031+
1032+
def pushFile(self, localLargeFile):
1033+
"""The remote filename of the large file storage is the same as the local
1034+
one but in a different directory.
1035+
"""
1036+
remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1037+
if not os.path.exists(remotePath):
1038+
os.makedirs(remotePath)
1039+
shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1040+
9351041
class Command:
9361042
def __init__(self):
9371043
self.usage = "usage: %prog [options]"
@@ -1105,6 +1211,9 @@ def __init__(self):
11051211
self.p4HasMoveCommand = p4_has_move_command()
11061212
self.branch = None
11071213

1214+
if gitConfig('git-p4.largeFileSystem'):
1215+
die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1216+
11081217
def check(self):
11091218
if len(p4CmdList("opened ...")) > 0:
11101219
die("You have files opened with perforce! Close them before starting the sync.")
@@ -2055,6 +2164,13 @@ def __init__(self):
20552164
self.clientSpecDirs = None
20562165
self.tempBranches = []
20572166
self.tempBranchLocation = "git-p4-tmp"
2167+
self.largeFileSystem = None
2168+
2169+
if gitConfig('git-p4.largeFileSystem'):
2170+
largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2171+
self.largeFileSystem = largeFileSystemConstructor(
2172+
lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2173+
)
20582174

20592175
if gitConfig("git-p4.syncFromOrigin") == "false":
20602176
self.syncWithOrigin = False
@@ -2175,6 +2291,13 @@ def splitFilesIntoBranches(self, commit):
21752291

21762292
return branches
21772293

2294+
def writeToGitStream(self, gitMode, relPath, contents):
2295+
self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2296+
self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2297+
for d in contents:
2298+
self.gitStream.write(d)
2299+
self.gitStream.write('\n')
2300+
21782301
# output one file from the P4 stream
21792302
# - helper for streamP4Files
21802303

@@ -2245,17 +2368,10 @@ def streamOneP4File(self, file, contents):
22452368
text = regexp.sub(r'$\1$', text)
22462369
contents = [ text ]
22472370

2248-
self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2371+
if self.largeFileSystem:
2372+
(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
22492373

2250-
# total length...
2251-
length = 0
2252-
for d in contents:
2253-
length = length + len(d)
2254-
2255-
self.gitStream.write("data %d\n" % length)
2256-
for d in contents:
2257-
self.gitStream.write(d)
2258-
self.gitStream.write("\n")
2374+
self.writeToGitStream(git_mode, relPath, contents)
22592375

22602376
def streamOneP4Deletion(self, file):
22612377
relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
@@ -2264,6 +2380,9 @@ def streamOneP4Deletion(self, file):
22642380
sys.stdout.flush()
22652381
self.gitStream.write("D %s\n" % relPath)
22662382

2383+
if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2384+
self.largeFileSystem.removeLargeFile(relPath)
2385+
22672386
# handle another chunk of streaming data
22682387
def streamP4FilesCb(self, marshalled):
22692388

0 commit comments

Comments
 (0)