2222import re
2323import shutil
2424import stat
25+ import zipfile
26+ import zlib
2527
2628try :
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+
9351041class 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