@ -43,28 +43,50 @@
package org.eclipse.jgit.internal.storage.file ;
import static org.junit.Assert.assertEquals ;
import static org.junit.Assert.assertFalse ;
import static org.junit.Assert.assertTrue ;
import static org.junit.Assume.assumeFalse ;
import static org.junit.Assume.assumeTrue ;
import java.io.File ;
import java.io.IOException ;
import java.io.OutputStream ;
import java.io.Writer ;
import java.nio.file.Files ;
import java.nio.file.Path ;
import java.nio.file.Paths ;
import java.nio.file.StandardCopyOption ;
import java.nio.file.StandardOpenOption ;
//import java.nio.file.attribute.BasicFileAttributes;
import java.text.ParseException ;
import java.util.Collection ;
import java.util.Iterator ;
import java.util.Random ;
import java.util.zip.Deflater ;
import org.eclipse.jgit.api.GarbageCollectCommand ;
import org.eclipse.jgit.api.Git ;
import org.eclipse.jgit.api.errors.AbortedByHookException ;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException ;
import org.eclipse.jgit.api.errors.GitAPIException ;
import org.eclipse.jgit.api.errors.NoFilepatternException ;
import org.eclipse.jgit.api.errors.NoHeadException ;
import org.eclipse.jgit.api.errors.NoMessageException ;
import org.eclipse.jgit.api.errors.UnmergedPathsException ;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException ;
import org.eclipse.jgit.junit.RepositoryTestCase ;
import org.eclipse.jgit.lib.AnyObjectId ;
import org.eclipse.jgit.lib.ConfigConstants ;
import org.eclipse.jgit.lib.ObjectId ;
import org.eclipse.jgit.storage.file.FileBasedConfig ;
import org.eclipse.jgit.storage.pack.PackConfig ;
import org.junit.Test ;
public class PackFileSnapshotTest extends RepositoryTestCase {
private static ObjectId unknownID = ObjectId
. fromString ( "1234567890123456789012345678901234567890" ) ;
@Test
public void testSamePackDifferentCompressionDetectChecksumChanged ( )
throws Exception {
@ -100,24 +122,213 @@ public class PackFileSnapshotTest extends RepositoryTestCase {
assertTrue ( "expected checksum changed" , snapshot . isChecksumChanged ( pf ) ) ;
}
private void appendRandomLine ( File f ) throws IOException {
private void appendRandomLine ( File f , int length , Random r )
throws IOException {
try ( Writer w = Files . newBufferedWriter ( f . toPath ( ) ,
StandardOpenOption . APPEND ) ) {
w . append ( randomLine ( 20 ) ) ;
appendRandomLine ( w , length , r ) ;
}
}
private String randomLine ( int len ) {
final int a = 97 ; // 'a'
int z = 122 ; // 'z'
Random random = new Random ( ) ;
StringBuilder buffer = new StringBuilder ( len ) ;
private void appendRandomLine ( File f ) throws IOException {
appendRandomLine ( f , 5 , new Random ( ) ) ;
}
private void appendRandomLine ( Writer w , int len , Random r )
throws IOException {
final int c1 = 32 ; // ' '
int c2 = 126 ; // '~'
for ( int i = 0 ; i < len ; i + + ) {
int rnd = a + ( int ) ( random . nextFloat ( ) * ( z - a + 1 ) ) ;
buffer . append ( ( char ) rnd ) ;
w . append ( ( char ) ( c1 + r . nextInt ( 1 + c2 - c1 ) ) ) ;
}
buffer . append ( '\n' ) ;
return buffer . toString ( ) ;
}
private ObjectId createTestRepo ( int testDataSeed , int testDataLength )
throws IOException , GitAPIException , NoFilepatternException ,
NoHeadException , NoMessageException , UnmergedPathsException ,
ConcurrentRefUpdateException , WrongRepositoryStateException ,
AbortedByHookException {
// Create a repo with two commits and one file. Each commit adds
// testDataLength number of bytes. Data are random bytes. Since the
// seed for the random number generator is specified we will get
// the same set of bytes for every run and for every platform
Random r = new Random ( testDataSeed ) ;
Git git = Git . wrap ( db ) ;
File f = writeTrashFile ( "file" , "foobar " ) ;
appendRandomLine ( f , testDataLength , r ) ;
git . add ( ) . addFilepattern ( "file" ) . call ( ) ;
git . commit ( ) . setMessage ( "message1" ) . call ( ) ;
appendRandomLine ( f , testDataLength , r ) ;
git . add ( ) . addFilepattern ( "file" ) . call ( ) ;
return git . commit ( ) . setMessage ( "message2" ) . call ( ) . getId ( ) ;
}
// Try repacking so fast that you get two new packs which differ only in
// content/chksum but have same name, size and lastmodified.
// Since this is done with standard gc (which creates new tmp files and
// renames them) the filekeys of the new packfiles differ helping jgit
// to detect the fast modification
@Test
public void testDetectModificationAlthoughSameSizeAndModificationtime ( )
throws Exception {
int testDataSeed = 1 ;
int testDataLength = 100 ;
FileBasedConfig config = db . getConfig ( ) ;
// don't use mtime of the parent folder to detect pack file
// modification.
config . setBoolean ( ConfigConstants . CONFIG_CORE_SECTION , null ,
ConfigConstants . CONFIG_KEY_TRUSTFOLDERSTAT , false ) ;
config . save ( ) ;
createTestRepo ( testDataSeed , testDataLength ) ;
// repack to create initial packfile
PackFile pf = repackAndCheck ( 5 , null , null , null ) ;
Path packFilePath = pf . getPackFile ( ) . toPath ( ) ;
AnyObjectId chk1 = pf . getPackChecksum ( ) ;
String name = pf . getPackName ( ) ;
Long length = Long . valueOf ( pf . getPackFile ( ) . length ( ) ) ;
long m1 = packFilePath . toFile ( ) . lastModified ( ) ;
// Wait for a filesystem timer tick to enhance probability the rest of
// this test is done before the filesystem timer ticks again.
fsTick ( packFilePath . toFile ( ) ) ;
// Repack to create packfile with same name, length. Lastmodified and
// content and checksum are different since compression level differs
AnyObjectId chk2 = repackAndCheck ( 6 , name , length , chk1 )
. getPackChecksum ( ) ;
long m2 = packFilePath . toFile ( ) . lastModified ( ) ;
assumeFalse ( m2 = = m1 ) ;
// Repack to create packfile with same name, length. Lastmodified is
// equal to the previous one because we are in the same filesystem timer
// slot. Content and its checksum are different
AnyObjectId chk3 = repackAndCheck ( 7 , name , length , chk2 )
. getPackChecksum ( ) ;
long m3 = packFilePath . toFile ( ) . lastModified ( ) ;
// ask for an unknown git object to force jgit to rescan the list of
// available packs. If we would ask for a known objectid then JGit would
// skip searching for new/modified packfiles
db . getObjectDatabase ( ) . has ( unknownID ) ;
assertEquals ( chk3 , getSinglePack ( db . getObjectDatabase ( ) . getPacks ( ) )
. getPackChecksum ( ) ) ;
assumeTrue ( m3 = = m2 ) ;
}
// Try repacking so fast that we get two new packs which differ only in
// content and checksum but have same name, size and lastmodified.
// To avoid that JGit detects modification by checking the filekey create
// two new packfiles upfront and create copies of them. Then modify the
// packfiles in-place by opening them for write and then copying the
// content.
@Test
public void testDetectModificationAlthoughSameSizeAndModificationtimeAndFileKey ( )
throws Exception {
int testDataSeed = 1 ;
int testDataLength = 100 ;
FileBasedConfig config = db . getConfig ( ) ;
config . setBoolean ( ConfigConstants . CONFIG_CORE_SECTION , null ,
ConfigConstants . CONFIG_KEY_TRUSTFOLDERSTAT , false ) ;
config . save ( ) ;
createTestRepo ( testDataSeed , testDataLength ) ;
// Repack to create initial packfile. Make a copy of it
PackFile pf = repackAndCheck ( 5 , null , null , null ) ;
Path packFilePath = pf . getPackFile ( ) . toPath ( ) ;
Path packFileBasePath = packFilePath . resolveSibling (
packFilePath . getFileName ( ) . toString ( ) . replaceAll ( ".pack" , "" ) ) ;
AnyObjectId chk1 = pf . getPackChecksum ( ) ;
String name = pf . getPackName ( ) ;
Long length = Long . valueOf ( pf . getPackFile ( ) . length ( ) ) ;
copyPack ( packFileBasePath , "" , ".copy1" ) ;
// Repack to create second packfile. Make a copy of it
AnyObjectId chk2 = repackAndCheck ( 6 , name , length , chk1 )
. getPackChecksum ( ) ;
copyPack ( packFileBasePath , "" , ".copy2" ) ;
// Repack to create third packfile
AnyObjectId chk3 = repackAndCheck ( 7 , name , length , chk2 )
. getPackChecksum ( ) ;
long m3 = packFilePath . toFile ( ) . lastModified ( ) ;
db . getObjectDatabase ( ) . has ( unknownID ) ;
assertEquals ( chk3 , getSinglePack ( db . getObjectDatabase ( ) . getPacks ( ) )
. getPackChecksum ( ) ) ;
// Wait for a filesystem timer tick to enhance probability the rest of
// this test is done before the filesystem timer ticks.
fsTick ( packFilePath . toFile ( ) ) ;
// Copy copy2 to packfile data to force modification of packfile without
// changing the packfile's filekey.
copyPack ( packFileBasePath , ".copy2" , "" ) ;
long m2 = packFilePath . toFile ( ) . lastModified ( ) ;
assumeFalse ( m3 = = m2 ) ;
db . getObjectDatabase ( ) . has ( unknownID ) ;
assertEquals ( chk2 , getSinglePack ( db . getObjectDatabase ( ) . getPacks ( ) )
. getPackChecksum ( ) ) ;
// Copy copy2 to packfile data to force modification of packfile without
// changing the packfile's filekey.
copyPack ( packFileBasePath , ".copy1" , "" ) ;
long m1 = packFilePath . toFile ( ) . lastModified ( ) ;
assumeTrue ( m2 = = m1 ) ;
db . getObjectDatabase ( ) . has ( unknownID ) ;
assertEquals ( chk1 , getSinglePack ( db . getObjectDatabase ( ) . getPacks ( ) )
. getPackChecksum ( ) ) ;
}
// Copy file from src to dst but avoid creating a new File (with new
// FileKey) if dst already exists
private Path copyFile ( Path src , Path dst ) throws IOException {
if ( Files . exists ( dst ) ) {
dst . toFile ( ) . setWritable ( true ) ;
try ( OutputStream dstOut = Files . newOutputStream ( dst ) ) {
Files . copy ( src , dstOut ) ;
return dst ;
}
} else {
return Files . copy ( src , dst , StandardCopyOption . REPLACE_EXISTING ) ;
}
}
private Path copyPack ( Path base , String srcSuffix , String dstSuffix )
throws IOException {
copyFile ( Paths . get ( base + ".idx" + srcSuffix ) ,
Paths . get ( base + ".idx" + dstSuffix ) ) ;
copyFile ( Paths . get ( base + ".bitmap" + srcSuffix ) ,
Paths . get ( base + ".bitmap" + dstSuffix ) ) ;
return copyFile ( Paths . get ( base + ".pack" + srcSuffix ) ,
Paths . get ( base + ".pack" + dstSuffix ) ) ;
}
private PackFile repackAndCheck ( int compressionLevel , String oldName ,
Long oldLength , AnyObjectId oldChkSum )
throws IOException , ParseException {
PackFile p = getSinglePack ( gc ( compressionLevel ) ) ;
File pf = p . getPackFile ( ) ;
// The following two assumptions should not cause the test to fail. If
// on a certain platform we get packfiles (containing the same git
// objects) where the lengths differ or the checksums don't differ we
// just skip this test. A reason for that could be that compression
// works differently or random number generator works differently. Then
// we have to search for more consistent test data or checkin these
// packfiles as test resources
assumeTrue ( oldLength = = null | | pf . length ( ) = = oldLength . longValue ( ) ) ;
assumeTrue ( oldChkSum = = null | | ! p . getPackChecksum ( ) . equals ( oldChkSum ) ) ;
assertTrue ( oldName = = null | | p . getPackName ( ) . equals ( oldName ) ) ;
return p ;
}
private PackFile getSinglePack ( Collection < PackFile > packs ) {
Iterator < PackFile > pIt = packs . iterator ( ) ;
PackFile p = pIt . next ( ) ;
assertFalse ( pIt . hasNext ( ) ) ;
return p ;
}
private Collection < PackFile > gc ( int compressionLevel )