diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java index 0e25c494e..a1433e9fe 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java @@ -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 void testSamePackDifferentCompressionDetectChecksumChanged() 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 packs) { + Iterator pIt = packs.iterator(); + PackFile p = pIt.next(); + assertFalse(pIt.hasNext()); + return p; } private Collection gc(int compressionLevel)