22
22
import re
23
23
import shutil
24
24
import stat
25
+ import zipfile
26
+ import zlib
25
27
26
28
try :
27
29
from subprocess import CalledProcessError
@@ -932,6 +934,110 @@ def wildcard_present(path):
932
934
m = re .search ("[*#@%]" , path )
933
935
return m is not None
934
936
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
+
935
1041
class Command :
936
1042
def __init__ (self ):
937
1043
self .usage = "usage: %prog [options]"
@@ -1105,6 +1211,9 @@ def __init__(self):
1105
1211
self .p4HasMoveCommand = p4_has_move_command ()
1106
1212
self .branch = None
1107
1213
1214
+ if gitConfig ('git-p4.largeFileSystem' ):
1215
+ die ("Large file system not supported for git-p4 submit command. Please remove it from config." )
1216
+
1108
1217
def check (self ):
1109
1218
if len (p4CmdList ("opened ..." )) > 0 :
1110
1219
die ("You have files opened with perforce! Close them before starting the sync." )
@@ -2055,6 +2164,13 @@ def __init__(self):
2055
2164
self .clientSpecDirs = None
2056
2165
self .tempBranches = []
2057
2166
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
+ )
2058
2174
2059
2175
if gitConfig ("git-p4.syncFromOrigin" ) == "false" :
2060
2176
self .syncWithOrigin = False
@@ -2175,6 +2291,13 @@ def splitFilesIntoBranches(self, commit):
2175
2291
2176
2292
return branches
2177
2293
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
+
2178
2301
# output one file from the P4 stream
2179
2302
# - helper for streamP4Files
2180
2303
@@ -2245,17 +2368,10 @@ def streamOneP4File(self, file, contents):
2245
2368
text = regexp .sub (r'$\1$' , text )
2246
2369
contents = [ text ]
2247
2370
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 )
2249
2373
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 )
2259
2375
2260
2376
def streamOneP4Deletion (self , file ):
2261
2377
relPath = self .stripRepoPath (file ['path' ], self .branchPrefixes )
@@ -2264,6 +2380,9 @@ def streamOneP4Deletion(self, file):
2264
2380
sys .stdout .flush ()
2265
2381
self .gitStream .write ("D %s\n " % relPath )
2266
2382
2383
+ if self .largeFileSystem and self .largeFileSystem .isLargeFile (relPath ):
2384
+ self .largeFileSystem .removeLargeFile (relPath )
2385
+
2267
2386
# handle another chunk of streaming data
2268
2387
def streamP4FilesCb (self , marshalled ):
2269
2388
0 commit comments