Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

PackFileSnapshotTest.java 12KB

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