You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PackFileSnapshotTest.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. /*
  2. * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.internal.storage.file;
  11. import static org.junit.Assert.assertEquals;
  12. import static org.junit.Assert.assertFalse;
  13. import static org.junit.Assert.assertNotNull;
  14. import static org.junit.Assert.assertTrue;
  15. import static org.junit.Assume.assumeFalse;
  16. import static org.junit.Assume.assumeTrue;
  17. import java.io.File;
  18. import java.io.IOException;
  19. import java.io.OutputStream;
  20. import java.io.Writer;
  21. import java.nio.file.Files;
  22. import java.nio.file.Path;
  23. import java.nio.file.Paths;
  24. import java.nio.file.StandardCopyOption;
  25. import java.nio.file.StandardOpenOption;
  26. //import java.nio.file.attribute.BasicFileAttributes;
  27. import java.text.ParseException;
  28. import java.time.Instant;
  29. import java.util.Collection;
  30. import java.util.Iterator;
  31. import java.util.Random;
  32. import java.util.zip.Deflater;
  33. import org.eclipse.jgit.api.GarbageCollectCommand;
  34. import org.eclipse.jgit.api.Git;
  35. import org.eclipse.jgit.api.errors.AbortedByHookException;
  36. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  37. import org.eclipse.jgit.api.errors.GitAPIException;
  38. import org.eclipse.jgit.api.errors.NoFilepatternException;
  39. import org.eclipse.jgit.api.errors.NoHeadException;
  40. import org.eclipse.jgit.api.errors.NoMessageException;
  41. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  42. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  43. import org.eclipse.jgit.junit.RepositoryTestCase;
  44. import org.eclipse.jgit.lib.AnyObjectId;
  45. import org.eclipse.jgit.lib.ConfigConstants;
  46. import org.eclipse.jgit.lib.ObjectId;
  47. import org.eclipse.jgit.storage.file.FileBasedConfig;
  48. import org.eclipse.jgit.storage.pack.PackConfig;
  49. import org.eclipse.jgit.util.FS;
  50. import org.junit.Test;
  51. public class PackFileSnapshotTest extends RepositoryTestCase {
  52. private static ObjectId unknownID = ObjectId
  53. .fromString("1234567890123456789012345678901234567890");
  54. @Test
  55. public void testSamePackDifferentCompressionDetectChecksumChanged()
  56. throws Exception {
  57. Git git = Git.wrap(db);
  58. File f = writeTrashFile("file", "foobar ");
  59. for (int i = 0; i < 10; i++) {
  60. appendRandomLine(f);
  61. git.add().addFilepattern("file").call();
  62. git.commit().setMessage("message" + i).call();
  63. }
  64. FileBasedConfig c = db.getConfig();
  65. c.setInt(ConfigConstants.CONFIG_GC_SECTION, null,
  66. ConfigConstants.CONFIG_KEY_AUTOPACKLIMIT, 1);
  67. c.save();
  68. Collection<Pack> packs = gc(Deflater.NO_COMPRESSION);
  69. assertEquals("expected 1 packfile after gc", 1, packs.size());
  70. Pack p1 = packs.iterator().next();
  71. PackFileSnapshot snapshot = p1.getFileSnapshot();
  72. packs = gc(Deflater.BEST_COMPRESSION);
  73. assertEquals("expected 1 packfile after gc", 1, packs.size());
  74. Pack p2 = packs.iterator().next();
  75. File pf = p2.getPackFile();
  76. // changing compression level with aggressive gc may change size,
  77. // fileKey (on *nix) and checksum. Hence FileSnapshot.isModified can
  78. // return true already based on size or fileKey.
  79. // So the only thing we can test here is that we ensure that checksum
  80. // also changed when we read it here in this test
  81. assertTrue("expected snapshot to detect modified pack",
  82. snapshot.isModified(pf));
  83. assertTrue("expected checksum changed", snapshot.isChecksumChanged(pf));
  84. }
  85. private void appendRandomLine(File f, int length, Random r)
  86. throws IOException {
  87. try (Writer w = Files.newBufferedWriter(f.toPath(),
  88. StandardOpenOption.APPEND)) {
  89. appendRandomLine(w, length, r);
  90. }
  91. }
  92. private void appendRandomLine(File f) throws IOException {
  93. appendRandomLine(f, 5, new Random());
  94. }
  95. private void appendRandomLine(Writer w, int len, Random r)
  96. throws IOException {
  97. final int c1 = 32; // ' '
  98. int c2 = 126; // '~'
  99. for (int i = 0; i < len; i++) {
  100. w.append((char) (c1 + r.nextInt(1 + c2 - c1)));
  101. }
  102. }
  103. private ObjectId createTestRepo(int testDataSeed, int testDataLength)
  104. throws IOException, GitAPIException, NoFilepatternException,
  105. NoHeadException, NoMessageException, UnmergedPathsException,
  106. ConcurrentRefUpdateException, WrongRepositoryStateException,
  107. AbortedByHookException {
  108. // Create a repo with two commits and one file. Each commit adds
  109. // testDataLength number of bytes. Data are random bytes. Since the
  110. // seed for the random number generator is specified we will get
  111. // the same set of bytes for every run and for every platform
  112. Random r = new Random(testDataSeed);
  113. Git git = Git.wrap(db);
  114. File f = writeTrashFile("file", "foobar ");
  115. appendRandomLine(f, testDataLength, r);
  116. git.add().addFilepattern("file").call();
  117. git.commit().setMessage("message1").call();
  118. appendRandomLine(f, testDataLength, r);
  119. git.add().addFilepattern("file").call();
  120. return git.commit().setMessage("message2").call().getId();
  121. }
  122. // Try repacking so fast that you get two new packs which differ only in
  123. // content/chksum but have same name, size and lastmodified.
  124. // Since this is done with standard gc (which creates new tmp files and
  125. // renames them) the filekeys of the new packfiles differ helping jgit
  126. // to detect the fast modification
  127. @Test
  128. public void testDetectModificationAlthoughSameSizeAndModificationtime()
  129. throws Exception {
  130. int testDataSeed = 1;
  131. int testDataLength = 100;
  132. FileBasedConfig config = db.getConfig();
  133. // don't use mtime of the parent folder to detect pack file
  134. // modification.
  135. config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
  136. ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, false);
  137. config.save();
  138. createTestRepo(testDataSeed, testDataLength);
  139. // repack to create initial packfile
  140. Pack p = repackAndCheck(5, null, null, null);
  141. Path packFilePath = p.getPackFile().toPath();
  142. AnyObjectId chk1 = p.getPackChecksum();
  143. String name = p.getPackName();
  144. Long length = Long.valueOf(p.getPackFile().length());
  145. FS fs = db.getFS();
  146. Instant m1 = fs.lastModifiedInstant(packFilePath);
  147. // Wait for a filesystem timer tick to enhance probability the rest of
  148. // this test is done before the filesystem timer ticks again.
  149. fsTick(packFilePath.toFile());
  150. // Repack to create packfile with same name, length. Lastmodified and
  151. // content and checksum are different since compression level differs
  152. AnyObjectId chk2 = repackAndCheck(6, name, length, chk1)
  153. .getPackChecksum();
  154. Instant m2 = fs.lastModifiedInstant(packFilePath);
  155. assumeFalse(m2.equals(m1));
  156. // Repack to create packfile with same name, length. Lastmodified is
  157. // equal to the previous one because we are in the same filesystem timer
  158. // slot. Content and its checksum are different
  159. AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
  160. .getPackChecksum();
  161. Instant m3 = fs.lastModifiedInstant(packFilePath);
  162. // ask for an unknown git object to force jgit to rescan the list of
  163. // available packs. If we would ask for a known objectid then JGit would
  164. // skip searching for new/modified packfiles
  165. db.getObjectDatabase().has(unknownID);
  166. assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
  167. .getPackChecksum());
  168. assumeTrue(m3.equals(m2));
  169. }
  170. // Try repacking so fast that we get two new packs which differ only in
  171. // content and checksum but have same name, size and lastmodified.
  172. // To avoid that JGit detects modification by checking the filekey create
  173. // two new packfiles upfront and create copies of them. Then modify the
  174. // packfiles in-place by opening them for write and then copying the
  175. // content.
  176. @Test
  177. public void testDetectModificationAlthoughSameSizeAndModificationtimeAndFileKey()
  178. throws Exception {
  179. int testDataSeed = 1;
  180. int testDataLength = 100;
  181. FileBasedConfig config = db.getConfig();
  182. config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
  183. ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, false);
  184. config.save();
  185. createTestRepo(testDataSeed, testDataLength);
  186. // Repack to create initial packfile. Make a copy of it
  187. Pack p = repackAndCheck(5, null, null, null);
  188. Path packFilePath = p.getPackFile().toPath();
  189. Path fn = packFilePath.getFileName();
  190. assertNotNull(fn);
  191. String packFileName = fn.toString();
  192. Path packFileBasePath = packFilePath
  193. .resolveSibling(packFileName.replaceAll(".pack", ""));
  194. AnyObjectId chk1 = p.getPackChecksum();
  195. String name = p.getPackName();
  196. Long length = Long.valueOf(p.getPackFile().length());
  197. copyPack(packFileBasePath, "", ".copy1");
  198. // Repack to create second packfile. Make a copy of it
  199. AnyObjectId chk2 = repackAndCheck(6, name, length, chk1)
  200. .getPackChecksum();
  201. copyPack(packFileBasePath, "", ".copy2");
  202. // Repack to create third packfile
  203. AnyObjectId chk3 = repackAndCheck(7, name, length, chk2)
  204. .getPackChecksum();
  205. FS fs = db.getFS();
  206. Instant m3 = fs.lastModifiedInstant(packFilePath);
  207. db.getObjectDatabase().has(unknownID);
  208. assertEquals(chk3, getSinglePack(db.getObjectDatabase().getPacks())
  209. .getPackChecksum());
  210. // Wait for a filesystem timer tick to enhance probability the rest of
  211. // this test is done before the filesystem timer ticks.
  212. fsTick(packFilePath.toFile());
  213. // Copy copy2 to packfile data to force modification of packfile without
  214. // changing the packfile's filekey.
  215. copyPack(packFileBasePath, ".copy2", "");
  216. Instant m2 = fs.lastModifiedInstant(packFilePath);
  217. assumeFalse(m3.equals(m2));
  218. db.getObjectDatabase().has(unknownID);
  219. assertEquals(chk2, getSinglePack(db.getObjectDatabase().getPacks())
  220. .getPackChecksum());
  221. // Copy copy2 to packfile data to force modification of packfile without
  222. // changing the packfile's filekey.
  223. copyPack(packFileBasePath, ".copy1", "");
  224. Instant m1 = fs.lastModifiedInstant(packFilePath);
  225. assumeTrue(m2.equals(m1));
  226. db.getObjectDatabase().has(unknownID);
  227. assertEquals(chk1, getSinglePack(db.getObjectDatabase().getPacks())
  228. .getPackChecksum());
  229. }
  230. // Copy file from src to dst but avoid creating a new File (with new
  231. // FileKey) if dst already exists
  232. private Path copyFile(Path src, Path dst) throws IOException {
  233. if (Files.exists(dst)) {
  234. dst.toFile().setWritable(true);
  235. try (OutputStream dstOut = Files.newOutputStream(dst)) {
  236. Files.copy(src, dstOut);
  237. return dst;
  238. }
  239. }
  240. return Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
  241. }
  242. private Path copyPack(Path base, String srcSuffix, String dstSuffix)
  243. throws IOException {
  244. copyFile(Paths.get(base + ".idx" + srcSuffix),
  245. Paths.get(base + ".idx" + dstSuffix));
  246. copyFile(Paths.get(base + ".bitmap" + srcSuffix),
  247. Paths.get(base + ".bitmap" + dstSuffix));
  248. return copyFile(Paths.get(base + ".pack" + srcSuffix),
  249. Paths.get(base + ".pack" + dstSuffix));
  250. }
  251. private Pack repackAndCheck(int compressionLevel, String oldName,
  252. Long oldLength, AnyObjectId oldChkSum)
  253. throws IOException, ParseException {
  254. Pack p = getSinglePack(gc(compressionLevel));
  255. File pf = p.getPackFile();
  256. // The following two assumptions should not cause the test to fail. If
  257. // on a certain platform we get packfiles (containing the same git
  258. // objects) where the lengths differ or the checksums don't differ we
  259. // just skip this test. A reason for that could be that compression
  260. // works differently or random number generator works differently. Then
  261. // we have to search for more consistent test data or checkin these
  262. // packfiles as test resources
  263. assumeTrue(oldLength == null || pf.length() == oldLength.longValue());
  264. assumeTrue(oldChkSum == null || !p.getPackChecksum().equals(oldChkSum));
  265. assertTrue(oldName == null || p.getPackName().equals(oldName));
  266. return p;
  267. }
  268. private Pack getSinglePack(Collection<Pack> packs) {
  269. Iterator<Pack> pIt = packs.iterator();
  270. Pack p = pIt.next();
  271. assertFalse(pIt.hasNext());
  272. return p;
  273. }
  274. private Collection<Pack> gc(int compressionLevel)
  275. throws IOException, ParseException {
  276. GC gc = new GC(db);
  277. PackConfig pc = new PackConfig(db.getConfig());
  278. pc.setCompressionLevel(compressionLevel);
  279. pc.setSinglePack(true);
  280. // --aggressive
  281. pc.setDeltaSearchWindowSize(
  282. GarbageCollectCommand.DEFAULT_GC_AGGRESSIVE_WINDOW);
  283. pc.setMaxDeltaDepth(GarbageCollectCommand.DEFAULT_GC_AGGRESSIVE_DEPTH);
  284. pc.setReuseObjects(false);
  285. gc.setPackConfig(pc);
  286. gc.setExpireAgeMillis(0);
  287. gc.setPackExpireAgeMillis(0);
  288. return gc.gc();
  289. }
  290. }