diff options
Diffstat (limited to 'org.eclipse.jgit.test/tst/org/eclipse/jgit')
182 files changed, 13803 insertions, 1490 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java index db2d5d1404..226677229c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com> - * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others + * Copyright (C) 2010, 2025 Christian Halstrick <christian.halstrick@sap.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -13,6 +13,8 @@ package org.eclipse.jgit.api; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.util.FileUtils.RECURSIVE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -607,14 +609,14 @@ public class AddCommandTest extends RepositoryTestCase { try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); - dc.getEntry(0).getObjectId(); + ObjectId oid = dc.getEntry(0).getObjectId(); try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) { writer.print("other content"); } dc = git.add().addFilepattern("a.txt").call(); - + assertNotEquals(oid, dc.getEntry(0).getObjectId()); assertEquals( "[a.txt, mode:100644, content:other content]", indexState(CONTENT)); @@ -632,7 +634,7 @@ public class AddCommandTest extends RepositoryTestCase { try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); - dc.getEntry(0).getObjectId(); + ObjectId oid = dc.getEntry(0).getObjectId(); git.commit().setMessage("commit a.txt").call(); @@ -641,7 +643,7 @@ public class AddCommandTest extends RepositoryTestCase { } dc = git.add().addFilepattern("a.txt").call(); - + assertNotEquals(oid, dc.getEntry(0).getObjectId()); assertEquals( "[a.txt, mode:100644, content:other content]", indexState(CONTENT)); @@ -659,15 +661,17 @@ public class AddCommandTest extends RepositoryTestCase { try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); - dc.getEntry(0).getObjectId(); + ObjectId oid = dc.getEntry(0).getObjectId(); FileUtils.delete(file); // is supposed to do nothing - dc = git.add().addFilepattern("a.txt").call(); - + dc = git.add().addFilepattern("a.txt").setAll(false).call(); + assertEquals(oid, dc.getEntry(0).getObjectId()); assertEquals( "[a.txt, mode:100644, content:content]", indexState(CONTENT)); + git.add().addFilepattern("a.txt").call(); + assertEquals("", indexState(CONTENT)); } } @@ -684,15 +688,17 @@ public class AddCommandTest extends RepositoryTestCase { git.commit().setMessage("commit a.txt").call(); - dc.getEntry(0).getObjectId(); + ObjectId oid = dc.getEntry(0).getObjectId(); FileUtils.delete(file); // is supposed to do nothing - dc = git.add().addFilepattern("a.txt").call(); - + dc = git.add().addFilepattern("a.txt").setAll(false).call(); + assertEquals(oid, dc.getEntry(0).getObjectId()); assertEquals( "[a.txt, mode:100644, content:content]", indexState(CONTENT)); + git.add().addFilepattern("a.txt").call(); + assertEquals("", indexState(CONTENT)); } } @@ -878,7 +884,7 @@ public class AddCommandTest extends RepositoryTestCase { } } // Help null pointer analysis. - assert lastFile != null; + assertNotNull(lastFile); // Wait a bit. If entries are "racily clean", we'll recompute // hashes from the disk files, and then the second add is also slow. // We want to test the normal case. @@ -962,7 +968,7 @@ public class AddCommandTest extends RepositoryTestCase { // file sub/b.txt is deleted FileUtils.delete(file2); - git.add().addFilepattern("sub").call(); + git.add().addFilepattern("sub").setAll(false).call(); // change in sub/a.txt is staged // deletion of sub/b.txt is not staged // sub/c.txt is staged @@ -971,6 +977,12 @@ public class AddCommandTest extends RepositoryTestCase { "[sub/b.txt, mode:100644, content:content b]" + "[sub/c.txt, mode:100644, content:content c]", indexState(CONTENT)); + git.add().addFilepattern("sub").call(); + // deletion of sub/b.txt is staged + assertEquals( + "[sub/a.txt, mode:100644, content:modified content]" + + "[sub/c.txt, mode:100644, content:content c]", + indexState(CONTENT)); } } @@ -1259,7 +1271,7 @@ public class AddCommandTest extends RepositoryTestCase { "[git-link-dir, mode:160000]", indexState(0)); Set<String> untrackedFiles = git.status().call().getUntracked(); - assert (untrackedFiles.isEmpty()); + assertTrue(untrackedFiles.isEmpty()); } } @@ -1274,7 +1286,8 @@ public class AddCommandTest extends RepositoryTestCase { ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true); config.save(); - assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks()); + assertTrue( + db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks()); try (Git git = new Git(db)) { git.add().addFilepattern("nested-repo").call(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java index f2cceac4b3..9c2b16a0ae 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java @@ -254,24 +254,23 @@ public class ArchiveCommandTest extends RepositoryTestCase { } } - @SuppressWarnings({ "serial", "boxing" }) + @SuppressWarnings({ "boxing" }) private void archiveHeadAllFilesWithCompression(String fmt) throws Exception { try (Git git = new Git(db)) { createLargeTestContent(git); File archive = new File(getTemporaryDirectory(), "archive." + format); - archive(git, archive, fmt, new HashMap<String, Object>() {{ - put("compression-level", 1); - }}); + archive(git, archive, fmt, Map.of("compression-level", 1)); int sizeCompression1 = getNumBytes(archive); - archive(git, archive, fmt, new HashMap<String, Object>() {{ - put("compression-level", 9); - }}); + archive(git, archive, fmt, Map.of("compression-level", 9)); int sizeCompression9 = getNumBytes(archive); - assertTrue(sizeCompression1 > sizeCompression9); + assertTrue( + "Expected sizeCompression1 = " + sizeCompression1 + + " > sizeCompression9 = " + sizeCompression9, + sizeCompression1 > sizeCompression9); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java index 87be813c85..7c1cbc37d6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import java.util.List; @@ -160,6 +161,20 @@ public class BranchCommandTest extends RepositoryTestCase { - allBefore); } + @Test + public void testExistingNameInBothBranchesAndTags() throws Exception { + git.branchCreate().setName("test").call(); + git.tag().setName("test").call(); + + // existing name not allowed w/o force + assertThrows("Create branch with existing ref name should fail", + RefAlreadyExistsException.class, + () -> git.branchCreate().setName("test").call()); + + // existing name allowed with force option + git.branchCreate().setName("test").setForce(true).call(); + } + @Test(expected = InvalidRefNameException.class) public void testInvalidBranchHEAD() throws Exception { git.branchCreate().setName("HEAD").call(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java index 301d6be662..3f5c5da55a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL; +import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL_WITH_REFERENCE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -32,6 +34,7 @@ import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.merge.ContentMergeStrategy; @@ -527,10 +530,11 @@ public class CherryPickCommandTest extends RepositoryTestCase { assertEquals(RepositoryState.SAFE, db.getRepositoryState()); if (reason == null) { - ReflogReader reader = db.getReflogReader(Constants.HEAD); + RefDatabase refDb = db.getRefDatabase(); + ReflogReader reader = refDb.getReflogReader(Constants.HEAD); assertTrue(reader.getLastEntry().getComment() .startsWith("cherry-pick: ")); - reader = db.getReflogReader(db.getBranch()); + reader = refDb.getReflogReader(db.getFullBranch()); assertTrue(reader.getLastEntry().getComment() .startsWith("cherry-pick: ")); } @@ -590,4 +594,187 @@ public class CherryPickCommandTest extends RepositoryTestCase { checkFile(new File(db.getWorkTree(), "file"), "a\n2\n3\n"); } } + + private void doCherryPickWithCustomProviderBaseTest(Git git, + CherryPickCommitMessageProvider commitMessageProvider) + throws Exception { + writeTrashFile("fileA", "line 1\nline 2\nline 3\n"); + git.add().addFilepattern("fileA").call(); + RevCommit commitFirst = git.commit().setMessage("create fileA").call(); + + writeTrashFile("fileB", "content from file B\n"); + git.add().addFilepattern("fileB").call(); + RevCommit commitCreateFileB = git.commit() + .setMessage("create fileB\n\nsome commit details").call(); + + writeTrashFile("fileA", "line 1\nline 2\nline 3\nline 4\n"); + git.add().addFilepattern("fileA").call(); + RevCommit commitEditFileA1 = git.commit().setMessage("patch fileA 1") + .call(); + + writeTrashFile("fileA", "line 1\nline 2\nline 3\nline 4\nline 5\n"); + git.add().addFilepattern("fileA").call(); + RevCommit commitEditFileA2 = git.commit().setMessage("patch fileA 2") + .call(); + + git.branchCreate().setName("side").setStartPoint(commitFirst).call(); + checkoutBranch("refs/heads/side"); + + CherryPickResult pickResult = git.cherryPick() + .setCherryPickCommitMessageProvider(commitMessageProvider) + .include(commitCreateFileB).include(commitEditFileA1) + .include(commitEditFileA2).call(); + + assertEquals(CherryPickStatus.OK, pickResult.getStatus()); + + assertTrue(new File(db.getWorkTree(), "fileA").exists()); + assertTrue(new File(db.getWorkTree(), "fileB").exists()); + + checkFile(new File(db.getWorkTree(), "fileA"), + "line 1\nline 2\nline 3\nline 4\nline 5\n"); + checkFile(new File(db.getWorkTree(), "fileB"), "content from file B\n"); + } + + @Test + public void testCherryPickWithCustomCommitMessageProvider() + throws Exception { + try (Git git = new Git(db)) { + @SuppressWarnings("boxing") + CherryPickCommitMessageProvider messageProvider = srcCommit -> { + String message = srcCommit.getFullMessage(); + return String.format("%s (message length: %d)", message, + message.length()); + }; + doCherryPickWithCustomProviderBaseTest(git, messageProvider); + + Iterator<RevCommit> history = git.log().call().iterator(); + assertEquals("patch fileA 2 (message length: 13)", + history.next().getFullMessage()); + assertEquals("patch fileA 1 (message length: 13)", + history.next().getFullMessage()); + assertEquals( + "create fileB\n\nsome commit details (message length: 33)", + history.next().getFullMessage()); + assertEquals("create fileA", history.next().getFullMessage()); + assertFalse(history.hasNext()); + } + } + + @Test + public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL() + throws Exception { + try (Git git = new Git(db)) { + doCherryPickWithCustomProviderBaseTest(git, ORIGINAL); + + Iterator<RevCommit> history = git.log().call().iterator(); + assertEquals("patch fileA 2", history.next().getFullMessage()); + assertEquals("patch fileA 1", history.next().getFullMessage()); + assertEquals("create fileB\n\nsome commit details", + history.next().getFullMessage()); + assertEquals("create fileA", history.next().getFullMessage()); + assertFalse(history.hasNext()); + } + } + + @Test + public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL_WITH_REFERENCE() + throws Exception { + try (Git git = new Git(db)) { + doCherryPickWithCustomProviderBaseTest(git, + ORIGINAL_WITH_REFERENCE); + + Iterator<RevCommit> history = git.log().call().iterator(); + assertEquals("patch fileA 2\n\n(cherry picked from commit 1ac121e90b0fb6fb18bbb4307e3e9731ceeba9e1)", history.next().getFullMessage()); + assertEquals("patch fileA 1\n\n(cherry picked from commit 71475239df59076e18564fa360e3a74280926c2a)", history.next().getFullMessage()); + assertEquals("create fileB\n\nsome commit details\n\n(cherry picked from commit 29b4501297ccf8de9de9f451e7beb384b51f5378)", + history.next().getFullMessage()); + assertEquals("create fileA", history.next().getFullMessage()); + assertFalse(history.hasNext()); + } + } + + @Test + public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL_WITH_REFERENCE_DonNotAddNewLineAfterFooter() + throws Exception { + try (Git git = new Git(db)) { + CherryPickCommitMessageProvider commitMessageProvider = CherryPickCommitMessageProvider.ORIGINAL_WITH_REFERENCE; + + RevCommit commit1 = addFileAndCommit(git, "file1", "content 1", + "commit1: no footer line"); + RevCommit commit2 = addFileAndCommit(git, "file2", "content 2", + "commit2: simple single footer line" + + "\n\nSigned-off-by: Alice <alice@example.com>"); + RevCommit commit3 = addFileAndCommit(git, "file3", "content 3", + "commit3: multiple footer lines\n\n" + + "Signed-off-by: Alice <alice@example.com>\n" + + "Signed-off-by: Bob <bob@example.com>"); + RevCommit commit4 = addFileAndCommit(git, "file4", "content 4", + "commit4: extra commit text before footer line\n\n" + + "Commit message details\n\n" + + "Signed-off-by: Alice <alice@example.com>\n" + + "Signed-off-by: Bob <bob@example.com>"); + RevCommit commit5 = addFileAndCommit(git, "file5", "content 5", + "commit5: extra commit text after footer line\n\n" + + "Signed-off-by: Alice <alice@example.com>\n" + + "Signed-off-by: Bob <bob@example.com>\n\n" + + "some extra description after footer"); + + git.branchCreate().setName("side").setStartPoint(commit1).call(); + checkoutBranch("refs/heads/side"); + + CherryPickResult pickResult = git.cherryPick() + .setCherryPickCommitMessageProvider(commitMessageProvider) + .include(commit2).include(commit3).include(commit4) + .include(commit5).call(); + + assertEquals(CherryPickStatus.OK, pickResult.getStatus()); + + assertTrue(new File(db.getWorkTree(), "file1").exists()); + assertTrue(new File(db.getWorkTree(), "file2").exists()); + assertTrue(new File(db.getWorkTree(), "file3").exists()); + assertTrue(new File(db.getWorkTree(), "file4").exists()); + assertTrue(new File(db.getWorkTree(), "file5").exists()); + + Iterator<RevCommit> history = git.log().call().iterator(); + RevCommit cpCommit1 = history.next(); + RevCommit cpCommit2 = history.next(); + RevCommit cpCommit3 = history.next(); + RevCommit cpCommit4 = history.next(); + RevCommit cpCommitInit = history.next(); + assertFalse(history.hasNext()); + + assertEquals("commit5: extra commit text after footer line\n\n" + + "Signed-off-by: Alice <alice@example.com>\n" + + "Signed-off-by: Bob <bob@example.com>\n\n" + + "some extra description after footer\n\n" + + "(cherry picked from commit c3c9959207dc7ae7c83da5d36dc14ef2ca42d572)", + cpCommit1.getFullMessage()); + assertEquals("commit4: extra commit text before footer line\n\n" + + "Commit message details\n\n" + + "Signed-off-by: Alice <alice@example.com>\n" + + "Signed-off-by: Bob <bob@example.com>\n" + + "(cherry picked from commit af3e8106c12cb946a37b403ddb2dd6c11a883698)", + cpCommit2.getFullMessage()); + assertEquals("commit3: multiple footer lines\n\n" + + "Signed-off-by: Alice <alice@example.com>\n" + + "Signed-off-by: Bob <bob@example.com>\n" + + "(cherry picked from commit 6d60f1a70a11a32dff4402c157c4ac328c32ce6c)", + cpCommit3.getFullMessage()); + assertEquals("commit2: simple single footer line\n\n" + + "Signed-off-by: Alice <alice@example.com>\n" + + "(cherry picked from commit 92bf0ec458814ecc73da8e050e60547d2ea6cce5)", + cpCommit4.getFullMessage()); + + assertEquals("commit1: no footer line", + cpCommitInit.getFullMessage()); + } + } + + private RevCommit addFileAndCommit(Git git, String fileName, + String fileText, String commitMessage) + throws IOException, GitAPIException { + writeTrashFile(fileName, fileText); + git.add().addFilepattern(fileName).call(); + return git.commit().setMessage(commitMessage).call(); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java index 63ab8094ae..661878fa07 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java @@ -182,7 +182,8 @@ public class CloneCommandTest extends RepositoryTestCase { private static boolean hasRefLog(Repository repo, Ref ref) { try { - return repo.getReflogReader(ref.getName()).getLastEntry() != null; + return repo.getRefDatabase().getReflogReader(ref) + .getLastEntry() != null; } catch (IOException ioe) { throw new IllegalStateException(ioe); } @@ -647,7 +648,8 @@ public class CloneCommandTest extends RepositoryTestCase { new File(git.getRepository().getWorkTree(), walk.getPath()), subRepo.getWorkTree()); assertEquals(new File(new File(git.getRepository().getDirectory(), - "modules"), walk.getPath()), subRepo.getDirectory()); + "modules"), walk.getPath()).getCanonicalPath(), + subRepo.getDirectory().getCanonicalPath()); } File directory = createTempDirectory("testCloneRepositoryWithSubmodules"); @@ -681,8 +683,8 @@ public class CloneCommandTest extends RepositoryTestCase { walk.getPath()), clonedSub1.getWorkTree()); assertEquals( new File(new File(git2.getRepository().getDirectory(), - "modules"), walk.getPath()), - clonedSub1.getDirectory()); + "modules"), walk.getPath()).getCanonicalPath(), + clonedSub1.getDirectory().getCanonicalPath()); } } @@ -770,8 +772,8 @@ public class CloneCommandTest extends RepositoryTestCase { walk.getPath()), clonedSub1.getWorkTree()); assertEquals( new File(new File(git2.getRepository().getDirectory(), - "modules"), walk.getPath()), - clonedSub1.getDirectory()); + "modules"), walk.getPath()).getCanonicalPath(), + clonedSub1.getDirectory().getCanonicalPath()); status = new SubmoduleStatusCommand(clonedSub1); statuses = status.call(); } @@ -795,7 +797,7 @@ public class CloneCommandTest extends RepositoryTestCase { assertNull(git2.getRepository().getConfig().getEnum( BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, "test", - ConfigConstants.CONFIG_KEY_REBASE, null)); + ConfigConstants.CONFIG_KEY_REBASE)); StoredConfig userConfig = SystemReader.getInstance() .getUserConfig(); @@ -811,7 +813,6 @@ public class CloneCommandTest extends RepositoryTestCase { addRepoToClose(git2.getRepository()); assertEquals(BranchRebaseMode.REBASE, git2.getRepository().getConfig().getEnum( - BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, "test", ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE)); @@ -828,7 +829,6 @@ public class CloneCommandTest extends RepositoryTestCase { addRepoToClose(git2.getRepository()); assertEquals(BranchRebaseMode.REBASE, git2.getRepository().getConfig().getEnum( - BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, "test", ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java index b7abba4209..4e5f44e5a6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java @@ -26,6 +26,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.revwalk.RevCommit; @@ -69,10 +70,11 @@ public class CommitAndLogCommandTest extends RepositoryTestCase { l--; } assertEquals(l, -1); - ReflogReader reader = db.getReflogReader(Constants.HEAD); + RefDatabase refDb = db.getRefDatabase(); + ReflogReader reader = refDb.getReflogReader(Constants.HEAD); assertTrue( reader.getLastEntry().getComment().startsWith("commit:")); - reader = db.getReflogReader(db.getBranch()); + reader = refDb.getReflogReader(db.getFullBranch()); assertTrue( reader.getLastEntry().getComment().startsWith("commit:")); } @@ -248,10 +250,11 @@ public class CommitAndLogCommandTest extends RepositoryTestCase { c++; } assertEquals(1, c); - ReflogReader reader = db.getReflogReader(Constants.HEAD); + RefDatabase refDb = db.getRefDatabase(); + ReflogReader reader = refDb.getReflogReader(Constants.HEAD); assertTrue(reader.getLastEntry().getComment() .startsWith("commit (amend):")); - reader = db.getReflogReader(db.getBranch()); + reader = refDb.getReflogReader(db.getFullBranch()); assertTrue(reader.getLastEntry().getComment() .startsWith("commit (amend):")); } @@ -284,11 +287,10 @@ public class CommitAndLogCommandTest extends RepositoryTestCase { // template) chars = commit.getFullMessage().getBytes(UTF_8); int lineStart = 0; - int lineEnd = 0; for (int i = 0; i < 4; i++) { lineStart = RawParseUtils.nextLF(chars, lineStart); } - lineEnd = RawParseUtils.nextLF(chars, lineStart); + int lineEnd = RawParseUtils.nextLF(chars, lineStart); String line = RawParseUtils.decode(chars, lineStart, lineEnd); @@ -303,13 +305,12 @@ public class CommitAndLogCommandTest extends RepositoryTestCase { // we should find the untouched template chars = commit.getFullMessage().getBytes(UTF_8); lineStart = 0; - lineEnd = 0; for (int i = 0; i < 4; i++) { lineStart = RawParseUtils.nextLF(chars, lineStart); } lineEnd = RawParseUtils.nextLF(chars, lineStart); - line = RawParseUtils.decode(chars, lineStart, lineEnd); + RawParseUtils.decode(chars, lineStart, lineEnd); assertTrue(commit.getFullMessage() .contains("Change-Id: I" + ObjectId.zeroId().getName())); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java index 35de73e204..21cfcc4e34 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java @@ -19,14 +19,15 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import java.io.File; -import java.util.Date; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.List; -import java.util.TimeZone; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.EmptyCommitException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.dircache.DirCache; @@ -34,19 +35,23 @@ import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.time.TimeUtil; -import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; -import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.GpgConfig; +import org.eclipse.jgit.lib.GpgConfig.GpgFormat; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.ObjectBuilder; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Signer; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.submodule.SubmoduleWalk; @@ -430,10 +435,12 @@ public class CommitCommandTest extends RepositoryTestCase { assertEquals(1, squashedCommit.getParentCount()); assertNull(db.readSquashCommitMsg()); - assertEquals("commit: Squashed commit of the following:", db - .getReflogReader(Constants.HEAD).getLastEntry().getComment()); - assertEquals("commit: Squashed commit of the following:", db - .getReflogReader(db.getBranch()).getLastEntry().getComment()); + assertEquals("commit: Squashed commit of the following:", + db.getRefDatabase().getReflogReader(Constants.HEAD) + .getLastEntry().getComment()); + assertEquals("commit: Squashed commit of the following:", + db.getRefDatabase().getReflogReader(db.getFullBranch()) + .getLastEntry().getComment()); } } @@ -450,12 +457,15 @@ public class CommitCommandTest extends RepositoryTestCase { git.commit().setMessage("c3").setAll(true) .setReflogComment("testRl").call(); - db.getReflogReader(Constants.HEAD).getReverseEntries(); + db.getRefDatabase().getReflogReader(Constants.HEAD) + .getReverseEntries(); assertEquals("testRl;commit (initial): c1;", reflogComments( - db.getReflogReader(Constants.HEAD).getReverseEntries())); + db.getRefDatabase().getReflogReader(Constants.HEAD) + .getReverseEntries())); assertEquals("testRl;commit (initial): c1;", reflogComments( - db.getReflogReader(db.getBranch()).getReverseEntries())); + db.getRefDatabase().getReflogReader(db.getFullBranch()) + .getReverseEntries())); } } @@ -481,11 +491,11 @@ public class CommitCommandTest extends RepositoryTestCase { writeTrashFile("file1", "file1"); git.add().addFilepattern("file1").call(); - final String authorName = "First Author"; - final String authorEmail = "author@example.org"; - final Date authorDate = new Date(1349621117000L); + String authorName = "First Author"; + String authorEmail = "author@example.org"; + Instant authorDate = Instant.ofEpochSecond(1349621117L); PersonIdent firstAuthor = new PersonIdent(authorName, authorEmail, - authorDate, TimeZone.getTimeZone("UTC")); + authorDate, ZoneOffset.UTC); git.commit().setMessage("initial commit").setAuthor(firstAuthor).call(); RevCommit amended = git.commit().setAmend(true) @@ -494,7 +504,8 @@ public class CommitCommandTest extends RepositoryTestCase { PersonIdent amendedAuthor = amended.getAuthorIdent(); assertEquals(authorName, amendedAuthor.getName()); assertEquals(authorEmail, amendedAuthor.getEmailAddress()); - assertEquals(authorDate.getTime(), amendedAuthor.getWhen().getTime()); + assertEquals(authorDate.getEpochSecond(), + amendedAuthor.getWhenAsInstant().getEpochSecond()); } } @@ -839,21 +850,39 @@ public class CommitCommandTest extends RepositoryTestCase { String[] signingKey = new String[1]; PersonIdent[] signingCommitters = new PersonIdent[1]; AtomicInteger callCount = new AtomicInteger(); - GpgSigner.setDefault(new GpgSigner() { + // Since GpgFormat defaults to OpenPGP just set a new signer for + // that. + Signers.set(GpgFormat.OPENPGP, new Signer() { + @Override - public void sign(CommitBuilder commit, String gpgSigningKey, - PersonIdent signingCommitter, CredentialsProvider credentialsProvider) { - signingKey[0] = gpgSigningKey; + public void signObject(Repository repo, GpgConfig config, + ObjectBuilder builder, PersonIdent signingCommitter, + String signingKeySpec, + CredentialsProvider credentialsProvider) + throws CanceledException, + UnsupportedSigningFormatException { + signingKey[0] = signingKeySpec; signingCommitters[0] = signingCommitter; callCount.incrementAndGet(); } @Override - public boolean canLocateSigningKey(String gpgSigningKey, - PersonIdent signingCommitter, + public GpgSignature sign(Repository repo, GpgConfig config, + byte[] data, PersonIdent signingCommitter, + String signingKeySpec, + CredentialsProvider credentialsProvider) + throws CanceledException, + UnsupportedSigningFormatException { + throw new CanceledException("Unexpected call"); + } + + @Override + public boolean canLocateSigningKey(Repository repo, + GpgConfig config, PersonIdent signingCommitter, + String signingKeySpec, CredentialsProvider credentialsProvider) throws CanceledException { - return false; + throw new CanceledException("Unexpected call"); } }); @@ -904,19 +933,37 @@ public class CommitCommandTest extends RepositoryTestCase { git.add().addFilepattern("file1").call(); AtomicInteger callCount = new AtomicInteger(); - GpgSigner.setDefault(new GpgSigner() { + // Since GpgFormat defaults to OpenPGP just set a new signer for + // that. + Signers.set(GpgFormat.OPENPGP, new Signer() { + @Override - public void sign(CommitBuilder commit, String gpgSigningKey, - PersonIdent signingCommitter, CredentialsProvider credentialsProvider) { + public void signObject(Repository repo, GpgConfig config, + ObjectBuilder builder, PersonIdent signingCommitter, + String signingKeySpec, + CredentialsProvider credentialsProvider) + throws CanceledException, + UnsupportedSigningFormatException { callCount.incrementAndGet(); } @Override - public boolean canLocateSigningKey(String gpgSigningKey, - PersonIdent signingCommitter, + public GpgSignature sign(Repository repo, GpgConfig config, + byte[] data, PersonIdent signingCommitter, + String signingKeySpec, + CredentialsProvider credentialsProvider) + throws CanceledException, + UnsupportedSigningFormatException { + throw new CanceledException("Unexpected call"); + } + + @Override + public boolean canLocateSigningKey(Repository repo, + GpgConfig config, PersonIdent signingCommitter, + String signingKeySpec, CredentialsProvider credentialsProvider) throws CanceledException { - return false; + throw new CanceledException("Unexpected call"); } }); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java index ab87fa9662..060e6d3e84 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.api; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -87,6 +88,9 @@ public class DescribeCommandTest extends RepositoryTestCase { assertEquals("alice-t1", describe(c2, "alice*")); assertEquals("alice-t1", describe(c2, "a*", "b*", "c*")); + assertNotEquals("alice-t1", describeExcluding(c2, "alice*")); + assertNotEquals("alice-t1", describeCommand(c2).setMatch("*").setExclude("alice*").call()); + assertEquals("bob-t2", describe(c3)); assertEquals("bob-t2-0-g44579eb", describe(c3, true, false)); assertEquals("alice-t1-1-g44579eb", describe(c3, "alice*")); @@ -95,6 +99,15 @@ public class DescribeCommandTest extends RepositoryTestCase { assertEquals("bob-t2", describe(c3, "?ob*")); assertEquals("bob-t2", describe(c3, "a*", "b*", "c*")); + assertNotEquals("alice-t1-1-g44579eb", describeExcluding(c3, "alice*")); + assertNotEquals("alice-t1-1-g44579eb", describeCommand(c3).setMatch("*").setExclude("alice*").call()); + assertNotEquals("alice-t1-1-g44579eb", describeExcluding(c3, "a??c?-t*")); + assertNotEquals("alice-t1-1-g44579eb", describeCommand(c3).setMatch("bob*").setExclude("a??c?-t*").call()); + assertNotEquals("bob-t2", describeExcluding(c3, "bob*")); + assertNotEquals("bob-t2", describeCommand(c3).setMatch("alice*").setExclude("bob*")); + assertNotEquals("bob-t2", describeExcluding(c3, "?ob*")); + assertNotEquals("bob-t2", describeCommand(c3).setMatch("a??c?-t*").setExclude("?ob*")); + // the value verified with git-describe(1) assertEquals("bob-t2-1-g3e563c5", describe(c4)); assertEquals("bob-t2-1-g3e563c5", describe(c4, true, false)); @@ -518,6 +531,15 @@ public class DescribeCommandTest extends RepositoryTestCase { .setMatch(patterns).call(); } + private String describeExcluding(ObjectId c1, String... patterns) throws Exception { + return git.describe().setTarget(c1).setTags(describeUseAllTags) + .setExclude(patterns).call(); + } + + private DescribeCommand describeCommand(ObjectId c1) throws Exception { + return git.describe().setTarget(c1).setTags(describeUseAllTags); + } + private static void assertNameStartsWith(ObjectId c4, String prefix) { assertTrue(c4.name(), c4.name().startsWith(prefix)); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java index b937b1f6a9..4c971ffb6b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java @@ -559,7 +559,7 @@ public class EolRepositoryTest extends RepositoryTestCase { } if (infoAttributesContent != null) { - File f = new File(db.getDirectory(), Constants.INFO_ATTRIBUTES); + File f = new File(db.getCommonDirectory(), Constants.INFO_ATTRIBUTES); write(f, infoAttributesContent); } config.save(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java index 3ec454cfc3..3731347f11 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java @@ -92,8 +92,8 @@ public class FetchCommandTest extends RepositoryTestCase { assertTrue(remoteRef.getName().startsWith(Constants.R_REMOTES)); assertEquals(defaultBranchSha1, remoteRef.getObjectId()); - assertNotNull(git.getRepository().getReflogReader(remoteRef.getName()) - .getLastEntry()); + assertNotNull(git.getRepository().getRefDatabase() + .getReflogReader(remoteRef.getName()).getLastEntry()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java index f98db3497b..6090d5efbe 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java @@ -11,12 +11,11 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertTrue; -import java.util.Date; +import java.time.Instant; import java.util.Properties; import org.eclipse.jgit.junit.RepositoryTestCase; -import org.eclipse.jgit.util.GitDateParser; -import org.eclipse.jgit.util.SystemReader; +import org.eclipse.jgit.util.GitTimeParser; import org.junit.Before; import org.junit.Test; @@ -36,9 +35,8 @@ public class GarbageCollectCommandTest extends RepositoryTestCase { @Test public void testGConeCommit() throws Exception { - Date expire = GitDateParser.parse("now", null, SystemReader - .getInstance().getLocale()); - Properties res = git.gc().setExpire(expire).call(); + Instant expireNow = GitTimeParser.parseInstant("now"); + Properties res = git.gc().setExpire(expireNow).call(); assertTrue(res.size() == 8); } @@ -52,11 +50,8 @@ public class GarbageCollectCommandTest extends RepositoryTestCase { writeTrashFile("b.txt", "a couple of words for gc to pack more 2"); writeTrashFile("c.txt", "a couple of words for gc to pack more 3"); git.commit().setAll(true).setMessage("commit3").call(); - Properties res = git - .gc() - .setExpire( - GitDateParser.parse("now", null, SystemReader - .getInstance().getLocale())).call(); + Instant expireNow = GitTimeParser.parseInstant("now"); + Properties res = git.gc().setExpire(expireNow).call(); assertTrue(res.size() == 8); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java index 76934343da..e847e72415 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java @@ -14,6 +14,7 @@ import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; +import java.time.Instant; import org.eclipse.jgit.api.ListBranchCommand.ListMode; import org.eclipse.jgit.api.errors.GitAPIException; @@ -100,7 +101,7 @@ public class GitConstructionTest extends RepositoryTestCase { GitAPIException { File workTree = db.getWorkTree(); Git git = Git.open(workTree); - git.gc().setExpire(null).call(); + git.gc().setExpire((Instant) null).call(); git.checkout().setName(git.getRepository().resolve("HEAD^").getName()) .call(); try { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java new file mode 100644 index 0000000000..3b60e1b5c0 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2024, Broadcom and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.api; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; + +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ReflogEntry; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.junit.Test; + +public class LinkedWorktreeTest extends RepositoryTestCase { + + @Override + public void setUp() throws Exception { + super.setUp(); + + try (Git git = new Git(db)) { + git.commit().setMessage("Initial commit").call(); + } + } + + @Test + public void testWeCanReadFromLinkedWorktreeFromBare() throws Exception { + FS fs = db.getFS(); + File directory = trash.getParentFile(); + String dbDirName = db.getWorkTree().getName(); + cloneBare(fs, directory, dbDirName, "bare"); + File bareDirectory = new File(directory, "bare"); + worktreeAddExisting(fs, bareDirectory, "master"); + + File worktreesDir = new File(bareDirectory, "worktrees"); + File masterWorktreesDir = new File(worktreesDir, "master"); + + FileRepository repository = new FileRepository(masterWorktreesDir); + try (Git git = new Git(repository)) { + ObjectId objectId = repository.resolve(HEAD); + assertNotNull(objectId); + + Iterator<RevCommit> log = git.log().all().call().iterator(); + assertTrue(log.hasNext()); + assertTrue("Initial commit".equals(log.next().getShortMessage())); + + // we have reflog entry + // depending on git version we either have one or + // two entries where extra is zeroid entry with + // same message or no message + Collection<ReflogEntry> reflog = git.reflog().call(); + assertNotNull(reflog); + assertTrue(reflog.size() > 0); + ReflogEntry[] reflogs = reflog.toArray(new ReflogEntry[0]); + assertEquals(reflogs[reflogs.length - 1].getComment(), + "reset: moving to HEAD"); + + // index works with file changes + File masterDir = new File(directory, "master"); + File testFile = new File(masterDir, "test"); + + Status status = git.status().call(); + assertTrue(status.getUncommittedChanges().size() == 0); + assertTrue(status.getUntracked().size() == 0); + + JGitTestUtil.write(testFile, "test"); + status = git.status().call(); + assertTrue(status.getUncommittedChanges().size() == 0); + assertTrue(status.getUntracked().size() == 1); + + git.add().addFilepattern("test").call(); + status = git.status().call(); + assertTrue(status.getUncommittedChanges().size() == 1); + assertTrue(status.getUntracked().size() == 0); + } + } + + @Test + public void testWeCanReadFromLinkedWorktreeFromNonBare() throws Exception { + FS fs = db.getFS(); + worktreeAddNew(fs, db.getWorkTree(), "wt"); + + File worktreesDir = new File(db.getDirectory(), "worktrees"); + File masterWorktreesDir = new File(worktreesDir, "wt"); + + FileRepository repository = new FileRepository(masterWorktreesDir); + try (Git git = new Git(repository)) { + ObjectId objectId = repository.resolve(HEAD); + assertNotNull(objectId); + + Iterator<RevCommit> log = git.log().all().call().iterator(); + assertTrue(log.hasNext()); + assertTrue("Initial commit".equals(log.next().getShortMessage())); + + // we have reflog entry + Collection<ReflogEntry> reflog = git.reflog().call(); + assertNotNull(reflog); + assertTrue(reflog.size() > 0); + ReflogEntry[] reflogs = reflog.toArray(new ReflogEntry[0]); + assertEquals(reflogs[reflogs.length - 1].getComment(), + "reset: moving to HEAD"); + + // index works with file changes + File directory = trash.getParentFile(); + File wtDir = new File(directory, "wt"); + File testFile = new File(wtDir, "test"); + + Status status = git.status().call(); + assertTrue(status.getUncommittedChanges().size() == 0); + assertTrue(status.getUntracked().size() == 0); + + JGitTestUtil.write(testFile, "test"); + status = git.status().call(); + assertTrue(status.getUncommittedChanges().size() == 0); + assertTrue(status.getUntracked().size() == 1); + + git.add().addFilepattern("test").call(); + status = git.status().call(); + assertTrue(status.getUncommittedChanges().size() == 1); + assertTrue(status.getUntracked().size() == 0); + } + + } + + private static void cloneBare(FS fs, File directory, String from, String to) throws IOException, InterruptedException { + ProcessBuilder builder = fs.runInShell("git", + new String[] { "clone", "--bare", from, to }); + builder.directory(directory); + builder.environment().put("HOME", fs.userHome().getAbsolutePath()); + StringBuilder input = new StringBuilder(); + ExecutionResult result = fs.execute(builder, new ByteArrayInputStream( + input.toString().getBytes(StandardCharsets.UTF_8))); + String stdOut = toString(result.getStdout()); + String errorOut = toString(result.getStderr()); + assertNotNull(stdOut); + assertNotNull(errorOut); + } + + private static void worktreeAddExisting(FS fs, File directory, String name) throws IOException, InterruptedException { + ProcessBuilder builder = fs.runInShell("git", + new String[] { "worktree", "add", "../" + name, name }); + builder.directory(directory); + builder.environment().put("HOME", fs.userHome().getAbsolutePath()); + StringBuilder input = new StringBuilder(); + ExecutionResult result = fs.execute(builder, new ByteArrayInputStream( + input.toString().getBytes(StandardCharsets.UTF_8))); + String stdOut = toString(result.getStdout()); + String errorOut = toString(result.getStderr()); + assertNotNull(stdOut); + assertNotNull(errorOut); + } + + private static void worktreeAddNew(FS fs, File directory, String name) throws IOException, InterruptedException { + ProcessBuilder builder = fs.runInShell("git", + new String[] { "worktree", "add", "-b", name, "../" + name, "master"}); + builder.directory(directory); + builder.environment().put("HOME", fs.userHome().getAbsolutePath()); + StringBuilder input = new StringBuilder(); + ExecutionResult result = fs.execute(builder, new ByteArrayInputStream( + input.toString().getBytes(StandardCharsets.UTF_8))); + String stdOut = toString(result.getStdout()); + String errorOut = toString(result.getStderr()); + assertNotNull(stdOut); + assertNotNull(errorOut); + } + + private static String toString(TemporaryBuffer b) throws IOException { + return RawParseUtils.decode(b.toByteArray()); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java index 917b6c3297..1ec506798c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java @@ -21,6 +21,9 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import java.io.File; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Iterator; import java.util.regex.Pattern; @@ -33,6 +36,7 @@ import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.Sets; @@ -45,6 +49,7 @@ import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.GitDateFormatter; import org.eclipse.jgit.util.GitDateFormatter.Format; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.experimental.theories.DataPoints; @@ -76,12 +81,12 @@ public class MergeCommandTest extends RepositoryTestCase { assertEquals(MergeResult.MergeStatus.ALREADY_UP_TO_DATE, result.getMergeStatus()); } // no reflog entry written by merge - assertEquals("commit (initial): initial commit", - db + RefDatabase refDb = db.getRefDatabase(); + assertEquals("commit (initial): initial commit", refDb .getReflogReader(Constants.HEAD).getLastEntry().getComment()); - assertEquals("commit (initial): initial commit", - db - .getReflogReader(db.getBranch()).getLastEntry().getComment()); + assertEquals("commit (initial): initial commit", refDb + .getReflogReader(db.getFullBranch()).getLastEntry() + .getComment()); } @Test @@ -96,10 +101,11 @@ public class MergeCommandTest extends RepositoryTestCase { assertEquals(second, result.getNewHead()); } // no reflog entry written by merge - assertEquals("commit: second commit", db + assertEquals("commit: second commit", db.getRefDatabase() .getReflogReader(Constants.HEAD).getLastEntry().getComment()); - assertEquals("commit: second commit", db - .getReflogReader(db.getBranch()).getLastEntry().getComment()); + assertEquals("commit: second commit", db.getRefDatabase() + .getReflogReader(db.getFullBranch()).getLastEntry() + .getComment()); } @Test @@ -117,10 +123,13 @@ public class MergeCommandTest extends RepositoryTestCase { assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus()); assertEquals(second, result.getNewHead()); } + RefDatabase refDb = db.getRefDatabase(); assertEquals("merge refs/heads/master: Fast-forward", - db.getReflogReader(Constants.HEAD).getLastEntry().getComment()); + refDb.getReflogReader(Constants.HEAD) + .getLastEntry().getComment()); assertEquals("merge refs/heads/master: Fast-forward", - db.getReflogReader(db.getBranch()).getLastEntry().getComment()); + refDb.getReflogReader(db.getFullBranch()) + .getLastEntry().getComment()); } @Test @@ -140,10 +149,12 @@ public class MergeCommandTest extends RepositoryTestCase { result.getMergeStatus()); assertEquals(second, result.getNewHead()); } - assertEquals("merge refs/heads/master: Fast-forward", db + RefDatabase refDb = db.getRefDatabase(); + assertEquals("merge refs/heads/master: Fast-forward", refDb .getReflogReader(Constants.HEAD).getLastEntry().getComment()); - assertEquals("merge refs/heads/master: Fast-forward", db - .getReflogReader(db.getBranch()).getLastEntry().getComment()); + assertEquals("merge refs/heads/master: Fast-forward", refDb + .getReflogReader(db.getFullBranch()).getLastEntry() + .getComment()); } @Test @@ -171,10 +182,12 @@ public class MergeCommandTest extends RepositoryTestCase { assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus()); assertEquals(second, result.getNewHead()); } - assertEquals("merge refs/heads/master: Fast-forward", - db.getReflogReader(Constants.HEAD).getLastEntry().getComment()); - assertEquals("merge refs/heads/master: Fast-forward", - db.getReflogReader(db.getBranch()).getLastEntry().getComment()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals("merge refs/heads/master: Fast-forward", refDb + .getReflogReader(Constants.HEAD).getLastEntry().getComment()); + assertEquals("merge refs/heads/master: Fast-forward", refDb + .getReflogReader(db.getFullBranch()).getLastEntry() + .getComment()); } @Test @@ -229,14 +242,17 @@ public class MergeCommandTest extends RepositoryTestCase { .include(db.exactRef(R_HEADS + MASTER)).call(); assertEquals(MergeStatus.MERGED, result.getMergeStatus()); } + RefDatabase refDb = db.getRefDatabase(); assertEquals( "merge refs/heads/master: Merge made by " + mergeStrategy.getName() + ".", - db.getReflogReader(Constants.HEAD).getLastEntry().getComment()); + refDb.getReflogReader(Constants.HEAD).getLastEntry() + .getComment()); assertEquals( "merge refs/heads/master: Merge made by " + mergeStrategy.getName() + ".", - db.getReflogReader(db.getBranch()).getLastEntry().getComment()); + refDb.getReflogReader(db.getFullBranch()).getLastEntry() + .getComment()); } @Theory @@ -662,14 +678,17 @@ public class MergeCommandTest extends RepositoryTestCase { .setStrategy(MergeStrategy.RESOLVE).call(); assertEquals(MergeStatus.MERGED, result.getMergeStatus()); assertEquals("1\nb(1)\n3\n", read(new File(db.getWorkTree(), "b"))); - assertEquals("merge " + secondCommit.getId().getName() - + ": Merge made by resolve.", db - .getReflogReader(Constants.HEAD) - .getLastEntry().getComment()); - assertEquals("merge " + secondCommit.getId().getName() - + ": Merge made by resolve.", db - .getReflogReader(db.getBranch()) - .getLastEntry().getComment()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals( + "merge " + secondCommit.getId().getName() + + ": Merge made by resolve.", + refDb.getReflogReader(Constants.HEAD).getLastEntry() + .getComment()); + assertEquals( + "merge " + secondCommit.getId().getName() + + ": Merge made by resolve.", + refDb.getReflogReader(db.getFullBranch()).getLastEntry() + .getComment()); } } @@ -2086,6 +2105,94 @@ public class MergeCommandTest extends RepositoryTestCase { } } + @Test + public void testMergeCaseInsensitiveRename() throws Exception { + Assume.assumeTrue( + "Test makes only sense on a case-insensitive file system", + db.isWorkTreeCaseInsensitive()); + try (Git git = new Git(db)) { + writeTrashFile("a", "aaa"); + git.add().addFilepattern("a").call(); + RevCommit initialCommit = git.commit().setMessage("initial").call(); + // "Rename" "a" to "A" + git.rm().addFilepattern("a").call(); + writeTrashFile("A", "aaa"); + git.add().addFilepattern("A").call(); + RevCommit master = git.commit().setMessage("rename to A").call(); + + createBranch(initialCommit, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + + writeTrashFile("b", "bbb"); + git.add().addFilepattern("b").call(); + git.commit().setMessage("side").call(); + + // Merge master into side + MergeResult result = git.merge().include(master) + .setStrategy(MergeStrategy.RECURSIVE).call(); + assertEquals(MergeStatus.MERGED, result.getMergeStatus()); + assertTrue(new File(db.getWorkTree(), "A").isFile()); + // Double check + boolean found = true; + try (DirectoryStream<Path> dir = Files + .newDirectoryStream(db.getWorkTree().toPath())) { + for (Path p : dir) { + found = "A".equals(p.getFileName().toString()); + if (found) { + break; + } + } + } + assertTrue(found); + } + } + + @Test + public void testMergeCaseInsensitiveRenameConflict() throws Exception { + Assume.assumeTrue( + "Test makes only sense on a case-insensitive file system", + db.isWorkTreeCaseInsensitive()); + try (Git git = new Git(db)) { + writeTrashFile("a", "aaa"); + git.add().addFilepattern("a").call(); + RevCommit initialCommit = git.commit().setMessage("initial").call(); + // "Rename" "a" to "A" and change it + git.rm().addFilepattern("a").call(); + writeTrashFile("A", "yyy"); + git.add().addFilepattern("A").call(); + RevCommit master = git.commit().setMessage("rename to A").call(); + + createBranch(initialCommit, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + + writeTrashFile("a", "xxx"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("side").call(); + + // Merge master into side + MergeResult result = git.merge().include(master) + .setStrategy(MergeStrategy.RECURSIVE).call(); + assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); + File a = new File(db.getWorkTree(), "A"); + assertTrue(a.isFile()); + // Double check + boolean found = true; + try (DirectoryStream<Path> dir = Files + .newDirectoryStream(db.getWorkTree().toPath())) { + for (Path p : dir) { + found = "A".equals(p.getFileName().toString()); + if (found) { + break; + } + } + } + assertTrue(found); + assertEquals(1, result.getConflicts().size()); + assertTrue(result.getConflicts().containsKey("a")); + checkFile(a, "yyy"); + } + } + private static void setExecutable(Git git, String path, boolean executable) { FS.DETECTED.setExecute( new File(git.getRepository().getWorkTree(), path), executable); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java index f52b715d39..cf952d2b77 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java @@ -172,9 +172,9 @@ public class PathCheckoutCommandTest extends RepositoryTestCase { @Test public void testUpdateWorkingDirectoryFromIndex() throws Exception { CheckoutCommand co = git.checkout(); - File written = writeTrashFile(FILE1, "3a"); + writeTrashFile(FILE1, "3a"); git.add().addFilepattern(FILE1).call(); - written = writeTrashFile(FILE1, ""); + File written = writeTrashFile(FILE1, ""); assertEquals("", read(written)); co.addPath(FILE1).call(); assertEquals("3a", read(written)); @@ -185,9 +185,9 @@ public class PathCheckoutCommandTest extends RepositoryTestCase { public void testUpdateWorkingDirectoryFromHeadWithIndexChange() throws Exception { CheckoutCommand co = git.checkout(); - File written = writeTrashFile(FILE1, "3a"); + writeTrashFile(FILE1, "3a"); git.add().addFilepattern(FILE1).call(); - written = writeTrashFile(FILE1, ""); + File written = writeTrashFile(FILE1, ""); assertEquals("", read(written)); co.addPath(FILE1).setStartPoint("HEAD").call(); assertEquals("3", read(written)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java index 12300b3390..695681de8d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Map; import java.util.concurrent.Callable; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; @@ -29,6 +30,7 @@ import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.IndexDiff.StageState; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; @@ -117,6 +119,7 @@ public class PullCommandTest extends RepositoryTestCase { + db.getWorkTree().getAbsolutePath(); assertEquals(message, mergeCommit.getShortMessage()); } + assertTrue(target.status().call().isClean()); } @Test @@ -153,6 +156,10 @@ public class PullCommandTest extends RepositoryTestCase { assertFileContentsEqual(targetFile, result); assertEquals(RepositoryState.MERGING, target.getRepository() .getRepositoryState()); + Status status = target.status().call(); + Map<String, StageState> conflicting = status.getConflictingStageState(); + assertEquals(1, conflicting.size()); + assertEquals(StageState.BOTH_MODIFIED, conflicting.get("SomeFile.txt")); } @Test @@ -473,7 +480,7 @@ public class PullCommandTest extends RepositoryTestCase { @Test /** without config it should merge */ public void testPullWithoutConfig() throws Exception { - Callable<PullResult> setup = target.pull()::call; + Callable<PullResult> setup = target.pull(); doTestPullWithRebase(setup, TestPullMode.MERGE); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java index ff5f8b76cc..d1696d62a8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.PrintStream; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Properties; import org.eclipse.jgit.api.errors.DetachedHeadException; @@ -941,7 +942,7 @@ public class PushCommandTest extends RepositoryTestCase { } /** - * Check that branch.<name>.pushRemote overrides anything else. + * Check that branch.<name>.pushRemote overrides anything else. * * @throws Exception */ @@ -980,7 +981,7 @@ public class PushCommandTest extends RepositoryTestCase { } /** - * Check that remote.pushDefault overrides branch.<name>.remote + * Check that remote.pushDefault overrides branch.<name>.remote * * @throws Exception */ @@ -1146,7 +1147,7 @@ public class PushCommandTest extends RepositoryTestCase { RevCommit commit2 = git2.commit().setMessage("adding a").call(); // run a gc to ensure we have a bitmap index - Properties res = git1.gc().setExpire(null).call(); + Properties res = git1.gc().setExpire((Instant) null).call(); assertEquals(8, res.size()); // create another commit so we have something else to push diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java index d574e45f6f..4c8cf06a67 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java @@ -24,6 +24,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -55,6 +57,7 @@ import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.RebaseTodoLine.Action; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.RepositoryState; @@ -76,6 +79,10 @@ public class RebaseCommandTest extends RepositoryTestCase { private static final String FILE1 = "file1"; + private static final String FILE2 = "file2"; + + private static final String FILE3 = "file3"; + protected Git git; @Override @@ -127,11 +134,12 @@ public class RebaseCommandTest extends RepositoryTestCase { checkFile(file2, "file2"); assertEquals(Status.FAST_FORWARD, res.getStatus()); - List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) + RefDatabase refDb = db.getRefDatabase(); + List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD) .getReverseEntries(); - List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") + List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic") .getReverseEntries(); - List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") + List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals("rebase finished: returning to refs/heads/topic", headLog .get(0).getComment()); @@ -173,11 +181,12 @@ public class RebaseCommandTest extends RepositoryTestCase { checkFile(file2, "file2 new content"); assertEquals(Status.FAST_FORWARD, res.getStatus()); - List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) + RefDatabase refDb = db.getRefDatabase(); + List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD) .getReverseEntries(); - List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") + List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic") .getReverseEntries(); - List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") + List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals("rebase finished: returning to refs/heads/topic", headLog .get(0).getComment()); @@ -191,6 +200,177 @@ public class RebaseCommandTest extends RepositoryTestCase { } /** + * Rebase a single root commit onto an independent branch. + * + * <pre> + * A (master) + * + * B - C (orphan) + * </pre> + * + * to + * + * <pre> + * A + * + * B - C (orphan) - A' (master) + * </pre> + * + * @throws Exception + */ + @Test + public void testRebaseRootCommit() throws Exception { + writeTrashFile(FILE1, FILE1); + writeTrashFile(FILE2, FILE2); + git.add().addFilepattern(FILE1).addFilepattern(FILE2).call(); + RevCommit first = git.commit().setMessage("Add files").call(); + File file1 = new File(db.getWorkTree(), FILE1); + File file2 = new File(db.getWorkTree(), FILE2); + assertTrue(file1.exists()); + assertTrue(file2.exists()); + // Create an independent branch + git.checkout().setOrphan(true).setName("orphan").call(); + git.rm().addFilepattern(FILE1).addFilepattern(FILE2).call(); + assertFalse(file1.exists()); + assertFalse(file2.exists()); + writeTrashFile(FILE1, "something else"); + git.add().addFilepattern(FILE1).call(); + RevCommit orphanBase = git.commit().setMessage("Orphan base").call(); + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); + RevCommit orphanTop = git.commit().setMessage("Same file1").call(); + checkoutBranch("refs/heads/master"); + assertEquals(first.getId(), db.resolve("HEAD")); + RebaseResult res = git.rebase().setUpstream("refs/heads/orphan").call(); + assertEquals(Status.OK, res.getStatus()); + Iterable<RevCommit> log = git.log().add(db.resolve("HEAD")).call(); + ObjectId[] ids = { orphanTop.getId(), orphanBase.getId() }; + int nOfCommits = 0; + for (RevCommit c : log) { + nOfCommits++; + if (nOfCommits == 1) { + assertEquals("Add files", c.getFullMessage()); + } else { + assertEquals(ids[nOfCommits - 2], c.getId()); + } + } + assertEquals(3, nOfCommits); + assertTrue(file1.exists()); + checkFile(file1, FILE1); + assertTrue(file2.exists()); + checkFile(file2, FILE2); + } + + /** + * Rebase a branch onto an independent branch. + * + * <pre> + * A - B (master) + * + * C - D (orphan) + * </pre> + * + * to + * + * <pre> + * A - B + * + * C - D (orphan) - A' - B' (master) + * </pre> + * + * @throws Exception + */ + @Test + public void testRebaseNoMergeBase() throws Exception { + writeTrashFile(FILE1, FILE1); + writeTrashFile(FILE2, FILE2); + git.add().addFilepattern(FILE1).addFilepattern(FILE2).call(); + git.commit().setMessage("Add files").call(); + writeTrashFile(FILE3, FILE3); + git.add().addFilepattern(FILE3).call(); + RevCommit first = git.commit().setMessage("File3").call(); + File file1 = new File(db.getWorkTree(), FILE1); + File file2 = new File(db.getWorkTree(), FILE2); + File file3 = new File(db.getWorkTree(), FILE3); + assertTrue(file1.exists()); + assertTrue(file2.exists()); + assertTrue(file3.exists()); + // Create an independent branch + git.checkout().setOrphan(true).setName("orphan").call(); + git.rm() + .addFilepattern(FILE1) + .addFilepattern(FILE2) + .addFilepattern(FILE3) + .call(); + assertFalse(file1.exists()); + assertFalse(file2.exists()); + assertFalse(file3.exists()); + writeTrashFile(FILE1, "something else"); + git.add().addFilepattern(FILE1).call(); + RevCommit orphanBase = git.commit().setMessage("Orphan base").call(); + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); + RevCommit orphanTop = git.commit().setMessage("Same file1").call(); + checkoutBranch("refs/heads/master"); + assertEquals(first.getId(), db.resolve("HEAD")); + RebaseResult res = git.rebase().setUpstream("refs/heads/orphan").call(); + assertEquals(Status.OK, res.getStatus()); + Iterable<RevCommit> log = git.log().add(db.resolve("HEAD")).call(); + String[] msgs = { "File3", "Add files" }; + ObjectId[] ids = { orphanTop.getId(), orphanBase.getId() }; + int nOfCommits = 0; + for (RevCommit c : log) { + nOfCommits++; + if (nOfCommits <= msgs.length) { + assertEquals(msgs[nOfCommits - 1], c.getFullMessage()); + } else { + assertEquals(ids[nOfCommits - msgs.length - 1], c.getId()); + } + } + assertEquals(4, nOfCommits); + assertTrue(file1.exists()); + checkFile(file1, FILE1); + assertTrue(file2.exists()); + checkFile(file2, FILE2); + assertTrue(file3.exists()); + checkFile(file3, FILE3); + } + + /** + * Create a commit A and an unrelated commit B creating the same file with + * different content. Then rebase A onto B. The rebase should stop with a + * conflict. + * + * @throws Exception + * on errors + */ + @Test + public void testRebaseNoMergeBaseConflict() throws Exception { + writeTrashFile(FILE1, FILE1); + git.add().addFilepattern(FILE1).call(); + RevCommit first = git.commit().setMessage("Add file").call(); + File file1 = new File(db.getWorkTree(), FILE1); + assertTrue(file1.exists()); + // Create an independent branch + git.checkout().setOrphan(true).setName("orphan").call(); + git.rm().addFilepattern(FILE1).call(); + assertFalse(file1.exists()); + writeTrashFile(FILE1, "something else"); + git.add().addFilepattern(FILE1).call(); + git.commit().setMessage("Orphan").call(); + checkoutBranch("refs/heads/master"); + assertEquals(first.getId(), db.resolve("HEAD")); + RebaseResult res = git.rebase().setUpstream("refs/heads/orphan").call(); + assertEquals(Status.STOPPED, res.getStatus()); + assertEquals(first, res.getCurrentCommit()); + checkFile(file1, "<<<<<<< Upstream, based on orphan\n" + + "something else\n" + + "=======\n" + + "file1\n" + + ">>>>>>> " + first.abbreviate(7).name() + " Add file\n"); + } + + /** * Create the following commits and then attempt to rebase topic onto * master. This will serialize the branches. * @@ -270,13 +450,14 @@ public class RebaseCommandTest extends RepositoryTestCase { assertEquals(a, rw.next()); } - List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) + RefDatabase refDb = db.getRefDatabase(); + List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD) .getReverseEntries(); - List<ReflogEntry> sideLog = db.getReflogReader("refs/heads/side") + List<ReflogEntry> sideLog = refDb.getReflogReader("refs/heads/side") .getReverseEntries(); - List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") + List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic") .getReverseEntries(); - List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") + List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals("rebase finished: returning to refs/heads/topic", headLog .get(0).getComment()); @@ -591,9 +772,10 @@ public class RebaseCommandTest extends RepositoryTestCase { RebaseResult result = git.rebase().setUpstream(parent).call(); assertEquals(Status.UP_TO_DATE, result.getStatus()); - assertEquals(2, db.getReflogReader(Constants.HEAD).getReverseEntries() - .size()); - assertEquals(2, db.getReflogReader("refs/heads/master") + RefDatabase refDb = db.getRefDatabase(); + assertEquals(2, refDb.getReflogReader(Constants.HEAD) + .getReverseEntries().size()); + assertEquals(2, refDb.getReflogReader("refs/heads/master") .getReverseEntries().size()); } @@ -609,9 +791,10 @@ public class RebaseCommandTest extends RepositoryTestCase { RebaseResult res = git.rebase().setUpstream(first).call(); assertEquals(Status.UP_TO_DATE, res.getStatus()); - assertEquals(1, db.getReflogReader(Constants.HEAD).getReverseEntries() - .size()); - assertEquals(1, db.getReflogReader("refs/heads/master") + RefDatabase refDb = db.getRefDatabase(); + assertEquals(1, refDb.getReflogReader(Constants.HEAD) + .getReverseEntries().size()); + assertEquals(1, refDb.getReflogReader("refs/heads/master") .getReverseEntries().size()); } @@ -669,11 +852,12 @@ public class RebaseCommandTest extends RepositoryTestCase { db.resolve(Constants.HEAD)).getParent(0)); } assertEquals(origHead, db.readOrigHead()); - List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) + RefDatabase refDb = db.getRefDatabase(); + List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD) .getReverseEntries(); - List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic") + List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic") .getReverseEntries(); - List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master") + List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master") .getReverseEntries(); assertEquals(2, masterLog.size()); assertEquals(3, topicLog.size()); @@ -721,8 +905,8 @@ public class RebaseCommandTest extends RepositoryTestCase { db.resolve(Constants.HEAD)).getParent(0)); } - List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD) - .getReverseEntries(); + List<ReflogEntry> headLog = db.getRefDatabase() + .getReflogReader(Constants.HEAD).getReverseEntries(); assertEquals(8, headLog.size()); assertEquals("rebase: change file1 in topic", headLog.get(0) .getComment()); @@ -1428,7 +1612,7 @@ public class RebaseCommandTest extends RepositoryTestCase { public void testAuthorScriptConverter() throws Exception { // -1 h timezone offset PersonIdent ident = new PersonIdent("Author name", "a.mail@some.com", - 123456789123L, -60); + Instant.ofEpochMilli(123456789123L), ZoneOffset.ofHours(-1)); String convertedAuthor = git.rebase().toAuthorScript(ident); String[] lines = convertedAuthor.split("\n"); assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]); @@ -1440,12 +1624,14 @@ public class RebaseCommandTest extends RepositoryTestCase { assertEquals(ident.getName(), parsedIdent.getName()); assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress()); // this is rounded to the last second - assertEquals(123456789000L, parsedIdent.getWhen().getTime()); - assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset()); + assertEquals(123456789000L, + parsedIdent.getWhenAsInstant().toEpochMilli()); + assertEquals(ident.getZoneId(), parsedIdent.getZoneId()); // + 9.5h timezone offset ident = new PersonIdent("Author name", "a.mail@some.com", - 123456789123L, +570); + Instant.ofEpochMilli(123456789123L), + ZoneOffset.ofHoursMinutes(9, 30)); convertedAuthor = git.rebase().toAuthorScript(ident); lines = convertedAuthor.split("\n"); assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]); @@ -1456,8 +1642,9 @@ public class RebaseCommandTest extends RepositoryTestCase { convertedAuthor.getBytes(UTF_8)); assertEquals(ident.getName(), parsedIdent.getName()); assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress()); - assertEquals(123456789000L, parsedIdent.getWhen().getTime()); - assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset()); + assertEquals(123456789000L, + parsedIdent.getWhenAsInstant().toEpochMilli()); + assertEquals(ident.getZoneId(), parsedIdent.getZoneId()); } @Test @@ -2080,7 +2267,7 @@ public class RebaseCommandTest extends RepositoryTestCase { checkoutBranch("refs/heads/master"); writeTrashFile(FILE1, "modified file1"); git.add().addFilepattern(FILE1).call(); - git.commit().setMessage("commit3").call(); + git.commit().setMessage("commit2").call(); // checkout topic branch / modify file0 checkoutBranch("refs/heads/topic"); @@ -2099,6 +2286,57 @@ public class RebaseCommandTest extends RepositoryTestCase { assertEquals(RepositoryState.SAFE, db.getRepositoryState()); } + @Test + public void testFastForwardRebaseWithAutoStashConflict() throws Exception { + // create file0, add and commit + db.getConfig().setBoolean(ConfigConstants.CONFIG_REBASE_SECTION, null, + ConfigConstants.CONFIG_KEY_AUTOSTASH, true); + writeTrashFile("file0", "file0"); + git.add().addFilepattern("file0").call(); + git.commit().setMessage("commit0").call(); + // create file1, add and commit + writeTrashFile(FILE1, "file1"); + git.add().addFilepattern(FILE1).call(); + RevCommit commit = git.commit().setMessage("commit1").call(); + + // create topic branch + createBranch(commit, "refs/heads/topic"); + + // checkout master branch / modify file1, add and commit + checkoutBranch("refs/heads/master"); + writeTrashFile(FILE1, "modified file1"); + git.add().addFilepattern(FILE1).call(); + RevCommit master = git.commit().setMessage("commit2").call(); + + // checkout topic branch / modify file0 and file1 + checkoutBranch("refs/heads/topic"); + writeTrashFile("file0", "unstaged modified file0"); + writeTrashFile(FILE1, "unstaged modified file1"); + + // rebase + assertEquals(Status.STASH_APPLY_CONFLICTS, + git.rebase().setUpstream("refs/heads/master").call() + .getStatus()); + checkFile(new File(db.getWorkTree(), "file0"), + "unstaged modified file0"); + checkFile(new File(db.getWorkTree(), FILE1), + "<<<<<<< HEAD\n" + + "modified file1\n" + + "=======\n" + + "unstaged modified file1\n" + + ">>>>>>> stash\n"); + // If there is a merge conflict, the index is not reset, and thus file0 + // is staged here. This is the same behavior as in C git. + String expected = "[file0, mode:100644, content:unstaged modified file0]" + + "[file1, mode:100644, stage:1, content:file1]" + + "[file1, mode:100644, stage:2, content:modified file1]" + + "[file1, mode:100644, stage:3, content:unstaged modified file1]"; + assertEquals(expected, indexState(CONTENT)); + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + assertEquals(master, db.resolve(Constants.HEAD)); + assertEquals(master, db.resolve("refs/heads/topic")); + } + private List<DiffEntry> getStashedDiff() throws AmbiguousObjectException, IncorrectObjectTypeException, IOException, MissingObjectException { ObjectId stashId = db.resolve("stash@{0}"); @@ -2424,7 +2662,9 @@ public class RebaseCommandTest extends RepositoryTestCase { assertEquals("1111111", firstLine.getCommit().name()); assertEquals("pick", firstLine.getAction().toToken()); } catch (Exception e) { - fail("Valid parsable RebaseTodoLine that has been commented out should allow to change the action, but failed"); + throw new AssertionError( + "Valid parsable RebaseTodoLine that has been commented out should allow to change the action, but failed", + e); } assertEquals("2222222", steps.get(1).getCommit().name()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java index 534ebd9c61..add5886c2d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java @@ -118,23 +118,21 @@ public class RenameBranchCommandTest extends RepositoryTestCase { String branch = "b1"; assertEquals(BranchRebaseMode.REBASE, - config.getEnum(BranchRebaseMode.values(), - ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER, - ConfigConstants.CONFIG_KEY_REBASE, + config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, + Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE)); assertNull(config.getEnum(BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, branch, - ConfigConstants.CONFIG_KEY_REBASE, null)); + ConfigConstants.CONFIG_KEY_REBASE)); assertNotNull(git.branchRename().setNewName(branch).call()); config = git.getRepository().getConfig(); assertNull(config.getEnum(BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER, - ConfigConstants.CONFIG_KEY_REBASE, null)); + ConfigConstants.CONFIG_KEY_REBASE)); assertEquals(BranchRebaseMode.REBASE, - config.getEnum(BranchRebaseMode.values(), - ConfigConstants.CONFIG_BRANCH_SECTION, branch, + config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE)); } @@ -170,13 +168,12 @@ public class RenameBranchCommandTest extends RepositoryTestCase { String branch = "b1"; assertEquals(BranchRebaseMode.REBASE, - config.getEnum(BranchRebaseMode.values(), - ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER, - ConfigConstants.CONFIG_KEY_REBASE, + config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, + Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE)); assertNull(config.getEnum(BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, branch, - ConfigConstants.CONFIG_KEY_REBASE, null)); + ConfigConstants.CONFIG_KEY_REBASE)); assertTrue(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER, ConfigConstants.CONFIG_KEY_MERGE, true)); assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, @@ -187,10 +184,9 @@ public class RenameBranchCommandTest extends RepositoryTestCase { config = git.getRepository().getConfig(); assertNull(config.getEnum(BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER, - ConfigConstants.CONFIG_KEY_REBASE, null)); + ConfigConstants.CONFIG_KEY_REBASE)); assertEquals(BranchRebaseMode.REBASE, - config.getEnum(BranchRebaseMode.values(), - ConfigConstants.CONFIG_BRANCH_SECTION, branch, + config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE)); assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java index 8a479a0ca0..99873e1be1 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java @@ -36,11 +36,13 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.FileUtils; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; public class ResetCommandTest extends RepositoryTestCase { @@ -554,46 +556,73 @@ public class ResetCommandTest extends RepositoryTestCase { assertNull(db.resolve(Constants.HEAD)); } + @Test + public void testHardResetFileMode() throws Exception { + Assume.assumeTrue("Test must be able to set executable bit", + db.getFS().supportsExecute()); + git = new Git(db); + File a = writeTrashFile("a.txt", "aaa"); + File b = writeTrashFile("b.txt", "bbb"); + db.getFS().setExecute(b, true); + assertFalse(db.getFS().canExecute(a)); + assertTrue(db.getFS().canExecute(b)); + git.add().addFilepattern("a.txt").addFilepattern("b.txt").call(); + RevCommit commit = git.commit().setMessage("files created").call(); + db.getFS().setExecute(a, true); + db.getFS().setExecute(b, false); + assertTrue(db.getFS().canExecute(a)); + assertFalse(db.getFS().canExecute(b)); + git.add().addFilepattern("a.txt").addFilepattern("b.txt").call(); + git.commit().setMessage("change exe bits").call(); + Ref ref = git.reset().setRef(commit.getName()).setMode(HARD).call(); + assertSameAsHead(ref); + assertEquals(commit.getId(), ref.getObjectId()); + assertFalse(db.getFS().canExecute(a)); + assertTrue(db.getFS().canExecute(b)); + } + private void assertReflog(ObjectId prevHead, ObjectId head) throws IOException { // Check the reflog for HEAD - String actualHeadMessage = db.getReflogReader(Constants.HEAD) + RefDatabase refDb = db.getRefDatabase(); + String actualHeadMessage = refDb.getReflogReader(Constants.HEAD) .getLastEntry().getComment(); String expectedHeadMessage = head.getName() + ": updating HEAD"; assertEquals(expectedHeadMessage, actualHeadMessage); - assertEquals(head.getName(), db.getReflogReader(Constants.HEAD) + assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD) .getLastEntry().getNewId().getName()); - assertEquals(prevHead.getName(), db.getReflogReader(Constants.HEAD) + assertEquals(prevHead.getName(), refDb.getReflogReader(Constants.HEAD) .getLastEntry().getOldId().getName()); // The reflog for master contains the same as the one for HEAD - String actualMasterMessage = db.getReflogReader("refs/heads/master") + String actualMasterMessage = refDb.getReflogReader("refs/heads/master") .getLastEntry().getComment(); String expectedMasterMessage = head.getName() + ": updating HEAD"; // yes! assertEquals(expectedMasterMessage, actualMasterMessage); - assertEquals(head.getName(), db.getReflogReader(Constants.HEAD) + assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD) .getLastEntry().getNewId().getName()); - assertEquals(prevHead.getName(), db - .getReflogReader("refs/heads/master").getLastEntry().getOldId() - .getName()); + assertEquals(prevHead.getName(), + refDb.getReflogReader("refs/heads/master").getLastEntry() + .getOldId().getName()); } private void assertReflogDisabled(ObjectId head) throws IOException { + RefDatabase refDb = db.getRefDatabase(); // Check the reflog for HEAD - String actualHeadMessage = db.getReflogReader(Constants.HEAD) + String actualHeadMessage = refDb.getReflogReader(Constants.HEAD) .getLastEntry().getComment(); String expectedHeadMessage = "commit: adding a.txt and dir/b.txt"; assertEquals(expectedHeadMessage, actualHeadMessage); - assertEquals(head.getName(), db.getReflogReader(Constants.HEAD) + assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD) .getLastEntry().getOldId().getName()); // The reflog for master contains the same as the one for HEAD - String actualMasterMessage = db.getReflogReader("refs/heads/master") + String actualMasterMessage = refDb.getReflogReader("refs/heads/master") .getLastEntry().getComment(); String expectedMasterMessage = "commit: adding a.txt and dir/b.txt"; assertEquals(expectedMasterMessage, actualMasterMessage); - assertEquals(head.getName(), db.getReflogReader(Constants.HEAD) + assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD) .getLastEntry().getOldId().getName()); } /** diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java index 1c7b8d13a8..89fdb32220 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, Robin Rosenberg and others + * Copyright (C) 2011, 2024 Robin Rosenberg and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -29,6 +29,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; @@ -39,6 +40,7 @@ import org.junit.Test; * Test revert command */ public class RevertCommandTest extends RepositoryTestCase { + @Test public void testRevert() throws IOException, JGitInternalException, GitAPIException { @@ -58,7 +60,9 @@ public class RevertCommandTest extends RepositoryTestCase { writeTrashFile("a", "first line\nsecond line\nthird line\nfourth line\n"); git.add().addFilepattern("a").call(); - RevCommit fixingA = git.commit().setMessage("fixed a").call(); + // Commit message with a non-empty second line on purpose + RevCommit fixingA = git.commit().setMessage("fixed a\nsecond line") + .call(); writeTrashFile("b", "first line\n"); git.add().addFilepattern("b").call(); @@ -77,20 +81,58 @@ public class RevertCommandTest extends RepositoryTestCase { + "This reverts commit " + fixingA.getId().getName() + ".\n"; assertEquals(expectedMessage, revertCommit.getFullMessage()); assertEquals("fixed b", history.next().getFullMessage()); - assertEquals("fixed a", history.next().getFullMessage()); + assertEquals("fixed a\nsecond line", + history.next().getFullMessage()); assertEquals("enlarged a", history.next().getFullMessage()); assertEquals("create b", history.next().getFullMessage()); assertEquals("create a", history.next().getFullMessage()); assertFalse(history.hasNext()); - ReflogReader reader = db.getReflogReader(Constants.HEAD); + RefDatabase refDb = db.getRefDatabase(); + ReflogReader reader = refDb.getReflogReader(Constants.HEAD); assertTrue(reader.getLastEntry().getComment() .startsWith("revert: Revert \"")); - reader = db.getReflogReader(db.getBranch()); + reader = refDb.getReflogReader(db.getFullBranch()); assertTrue(reader.getLastEntry().getComment() .startsWith("revert: Revert \"")); } + } + @Test + public void testRevertWithChangeId() + throws IOException, JGitInternalException, GitAPIException { + try (Git git = new Git(db)) { + writeTrashFile("a", "first line\nthird line\n"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("create a").call(); + + writeTrashFile("a", "first line\nsecond line\nthird line\n"); + git.add().addFilepattern("a").call(); + RevCommit second = git.commit().setMessage("changed a").call(); + + writeTrashFile("a", + "first line\nsecond line\nthird line\nfourth line\n"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("changed a again").call(); + + git.revert().include(second).setInsertChangeId(true).call(); + + assertEquals(RepositoryState.SAFE, db.getRepositoryState()); + + checkFile(new File(db.getWorkTree(), "a"), + "first line\nthird line\nfourth line\n"); + Iterator<RevCommit> history = git.log().call().iterator(); + RevCommit revertCommit = history.next(); + String expectedMessage = "Revert \"changed a\"\n\n" + + "This reverts commit " + second.getId().getName() + ".\n"; + String commitMessage = revertCommit.getFullMessage(); + assertTrue(commitMessage.matches("^\\Q" + expectedMessage + + "\\E\nChange-Id: I[a-fA-F0-9]{40}\n$")); + assertEquals("changed a again", history.next().getFullMessage()); + assertEquals("changed a", history.next().getFullMessage()); + assertEquals("create a", history.next().getFullMessage()); + assertFalse(history.hasNext()); + } } @Test @@ -130,10 +172,11 @@ public class RevertCommandTest extends RepositoryTestCase { assertEquals("add first", history.next().getFullMessage()); assertFalse(history.hasNext()); - ReflogReader reader = db.getReflogReader(Constants.HEAD); + RefDatabase refDb = db.getRefDatabase(); + ReflogReader reader = refDb.getReflogReader(Constants.HEAD); assertTrue(reader.getLastEntry().getComment() .startsWith("revert: Revert \"")); - reader = db.getReflogReader(db.getBranch()); + reader = refDb.getReflogReader(db.getFullBranch()); assertTrue(reader.getLastEntry().getComment() .startsWith("revert: Revert \"")); } @@ -183,10 +226,11 @@ public class RevertCommandTest extends RepositoryTestCase { assertEquals("add first", history.next().getFullMessage()); assertFalse(history.hasNext()); - ReflogReader reader = db.getReflogReader(Constants.HEAD); + RefDatabase refDb = db.getRefDatabase(); + ReflogReader reader = refDb.getReflogReader(Constants.HEAD); assertTrue(reader.getLastEntry().getComment() .startsWith("revert: Revert \"")); - reader = db.getReflogReader(db.getBranch()); + reader = refDb.getReflogReader(db.getFullBranch()); assertTrue(reader.getLastEntry().getComment() .startsWith("revert: Revert \"")); } @@ -391,12 +435,13 @@ public class RevertCommandTest extends RepositoryTestCase { assertEquals(RepositoryState.SAFE, db.getRepositoryState()); if (reason == null) { - ReflogReader reader = db.getReflogReader(Constants.HEAD); - assertTrue(reader.getLastEntry().getComment() - .startsWith("revert: ")); - reader = db.getReflogReader(db.getBranch()); - assertTrue(reader.getLastEntry().getComment() - .startsWith("revert: ")); + RefDatabase refDb = db.getRefDatabase(); + ReflogReader reader = refDb.getReflogReader(Constants.HEAD); + assertTrue( + reader.getLastEntry().getComment().startsWith("revert: ")); + reader = refDb.getReflogReader(db.getFullBranch()); + assertTrue( + reader.getLastEntry().getComment().startsWith("revert: ")); } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java deleted file mode 100644 index d0fbdbd090..0000000000 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2019 Alex Jitianu <alex_jitianu@sync.ro> and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.api; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.Policy; -import java.util.Collections; - -import org.eclipse.jgit.junit.RepositoryTestCase; -import org.eclipse.jgit.util.FileUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -/** - * Tests that using a SecurityManager does not result in errors logged. - */ -public class SecurityManagerMissingPermissionsTest extends RepositoryTestCase { - - /** - * Collects all logging sent to the logging system. - */ - private final ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); - - private SecurityManager originalSecurityManager; - - private PrintStream defaultErrorOutput; - - @Override - @Before - public void setUp() throws Exception { - originalSecurityManager = System.getSecurityManager(); - - // slf4j-simple logs to System.err, redirect it to enable asserting - // logged errors - defaultErrorOutput = System.err; - System.setErr(new PrintStream(errorOutput)); - - refreshPolicyAllPermission(Policy.getPolicy()); - System.setSecurityManager(new SecurityManager()); - super.setUp(); - } - - /** - * If a SecurityManager is active a lot of {@link java.io.FilePermission} - * errors are thrown and logged while initializing a repository. - * - * @throws Exception - */ - @Test - public void testCreateNewRepos_MissingPermissions() throws Exception { - File wcTree = new File(getTemporaryDirectory(), - "CreateNewRepositoryTest_testCreateNewRepos"); - - File marker = new File(getTemporaryDirectory(), "marker"); - Files.write(marker.toPath(), Collections.singletonList("Can write")); - assertTrue("Can write in test directory", marker.isFile()); - FileUtils.delete(marker); - assertFalse("Can delete in test direcory", marker.exists()); - - Git git = Git.init().setBare(false) - .setDirectory(new File(wcTree.getAbsolutePath())).call(); - - addRepoToClose(git.getRepository()); - - assertEquals("", errorOutput.toString()); - } - - @Override - @After - public void tearDown() throws Exception { - System.setSecurityManager(originalSecurityManager); - System.setErr(defaultErrorOutput); - super.tearDown(); - } - - /** - * Refresh the Java Security Policy. - * - * @param policy - * the policy object - * - * @throws IOException - * if the temporary file that contains the policy could not be - * created - */ - private static void refreshPolicyAllPermission(Policy policy) - throws IOException { - // Starting with an all permissions policy. - String policyString = "grant { permission java.security.AllPermission; };"; - - // Do not use TemporaryFilesFactory, it will create a dependency cycle - Path policyFile = Files.createTempFile("testpolicy", ".txt"); - - try { - Files.write(policyFile, Collections.singletonList(policyString)); - System.setProperty("java.security.policy", - policyFile.toUri().toURL().toString()); - policy.refresh(); - } finally { - try { - Files.delete(policyFile); - } catch (IOException e) { - // Do not log; the test tests for no logging having occurred - e.printStackTrace(); - } - } - } - -} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java deleted file mode 100644 index 2b930a1133..0000000000 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2019 Nail Samatov <sanail@yandex.ru> and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.api; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.io.FilePermission; -import java.io.IOException; -import java.lang.reflect.ReflectPermission; -import java.nio.file.Files; -import java.security.Permission; -import java.security.SecurityPermission; -import java.util.ArrayList; -import java.util.List; -import java.util.PropertyPermission; -import java.util.logging.LoggingPermission; - -import javax.security.auth.AuthPermission; - -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.junit.JGitTestUtil; -import org.eclipse.jgit.junit.MockSystemReader; -import org.eclipse.jgit.junit.SeparateClassloaderTestRunner; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.util.FileUtils; -import org.eclipse.jgit.util.SystemReader; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * <p> - * Tests if jgit works if SecurityManager is enabled. - * </p> - * - * <p> - * Note: JGit's classes shouldn't be used before SecurityManager is configured. - * If you use some JGit's class before SecurityManager is replaced then part of - * the code can be invoked outside of our custom SecurityManager and this test - * becomes useless. - * </p> - * - * <p> - * For example the class {@link org.eclipse.jgit.util.FS} is used widely in jgit - * sources. It contains DETECTED static field. At the first usage of the class - * FS the field DETECTED is initialized and during initialization many system - * operations that SecurityManager can forbid are invoked. - * </p> - * - * <p> - * For this reason this test doesn't extend LocalDiskRepositoryTestCase (it uses - * JGit's classes in setUp() method) and other JGit's utility classes. It's done - * to affect SecurityManager as less as possible. - * </p> - * - * <p> - * We use SeparateClassloaderTestRunner to isolate FS.DETECTED field - * initialization between different tests run. - * </p> - */ -@RunWith(SeparateClassloaderTestRunner.class) -public class SecurityManagerTest { - private File root; - - private SecurityManager originalSecurityManager; - - private List<Permission> permissions = new ArrayList<>(); - - @Before - public void setUp() throws Exception { - // Create working directory - SystemReader.setInstance(new MockSystemReader()); - root = Files.createTempDirectory("jgit-security").toFile(); - - // Add system permissions - permissions.add(new RuntimePermission("*")); - permissions.add(new SecurityPermission("*")); - permissions.add(new AuthPermission("*")); - permissions.add(new ReflectPermission("*")); - permissions.add(new PropertyPermission("*", "read,write")); - permissions.add(new LoggingPermission("control", null)); - - permissions.add(new FilePermission( - System.getProperty("java.home") + "/-", "read")); - - String tempDir = System.getProperty("java.io.tmpdir"); - permissions.add(new FilePermission(tempDir, "read,write,delete")); - permissions - .add(new FilePermission(tempDir + "/-", "read,write,delete")); - - // Add permissions to dependent jar files. - String classPath = System.getProperty("java.class.path"); - if (classPath != null) { - for (String path : classPath.split(File.pathSeparator)) { - permissions.add(new FilePermission(path, "read")); - } - } - // Add permissions to jgit class files. - String jgitSourcesRoot = new File(System.getProperty("user.dir")) - .getParent(); - permissions.add(new FilePermission(jgitSourcesRoot + "/-", "read")); - - // Add permissions to working dir for jgit. Our git repositories will be - // initialized and cloned here. - permissions.add(new FilePermission(root.getPath() + "/-", - "read,write,delete,execute")); - - // Replace Security Manager - originalSecurityManager = System.getSecurityManager(); - System.setSecurityManager(new SecurityManager() { - - @Override - public void checkPermission(Permission requested) { - for (Permission permission : permissions) { - if (permission.implies(requested)) { - return; - } - } - - super.checkPermission(requested); - } - }); - } - - @After - public void tearDown() throws Exception { - System.setSecurityManager(originalSecurityManager); - - // Note: don't use this method before security manager is replaced in - // setUp() method. The method uses FS.DETECTED internally and can affect - // the test. - FileUtils.delete(root, FileUtils.RECURSIVE | FileUtils.RETRY); - } - - @Test - public void testInitAndClone() throws IOException, GitAPIException { - File remote = new File(root, "remote"); - File local = new File(root, "local"); - - try (Git git = Git.init().setDirectory(remote).call()) { - JGitTestUtil.write(new File(remote, "hello.txt"), "Hello world!"); - git.add().addFilepattern(".").call(); - git.commit().setMessage("Initial commit").call(); - } - - try (Git git = Git.cloneRepository().setURI(remote.toURI().toString()) - .setDirectory(local).call()) { - assertTrue(new File(local, ".git").exists()); - - JGitTestUtil.write(new File(local, "hi.txt"), "Hi!"); - git.add().addFilepattern(".").call(); - RevCommit commit1 = git.commit().setMessage("Commit on local repo") - .call(); - assertEquals("Commit on local repo", commit1.getFullMessage()); - assertNotNull(TreeWalk.forPath(git.getRepository(), "hello.txt", - commit1.getTree())); - } - - } - -} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java index 5d0ab05174..18cd21a5d7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java @@ -409,8 +409,8 @@ public class StashCreateCommandTest extends RepositoryTestCase { assertEquals("content", read(committedFile)); validateStashedCommit(stashed); - ReflogReader reader = git.getRepository().getReflogReader( - Constants.R_STASH); + ReflogReader reader = git.getRepository().getRefDatabase() + .getReflogReader(Constants.R_STASH); ReflogEntry entry = reader.getLastEntry(); assertNotNull(entry); assertEquals(ObjectId.zeroId(), entry.getOldId()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java index f9af968a7e..d937579283 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java @@ -69,8 +69,7 @@ public class StashDropCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); stashRef = git.getRepository().exactRef(Constants.R_STASH); - assertEquals(stashed, - git.getRepository().exactRef(Constants.R_STASH).getObjectId()); + assertEquals(stashed, stashRef.getObjectId()); try { assertNull(git.stashDrop().setStashRef(100).call()); fail("Exception not thrown"); @@ -88,14 +87,13 @@ public class StashDropCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); stashRef = git.getRepository().exactRef(Constants.R_STASH); - assertEquals(stashed, - git.getRepository().exactRef(Constants.R_STASH).getObjectId()); + assertEquals(stashed, stashRef.getObjectId()); assertNull(git.stashDrop().call()); stashRef = git.getRepository().exactRef(Constants.R_STASH); assertNull(stashRef); - ReflogReader reader = git.getRepository().getReflogReader( - Constants.R_STASH); + ReflogReader reader = git.getRepository().getRefDatabase() + .getReflogReader(Constants.R_STASH); assertNull(reader); } @@ -122,8 +120,8 @@ public class StashDropCommandTest extends RepositoryTestCase { assertNull(git.stashDrop().setAll(true).call()); assertNull(git.getRepository().exactRef(Constants.R_STASH)); - ReflogReader reader = git.getRepository().getReflogReader( - Constants.R_STASH); + ReflogReader reader = git.getRepository().getRefDatabase() + .getReflogReader(Constants.R_STASH); assertNull(reader); } @@ -152,8 +150,8 @@ public class StashDropCommandTest extends RepositoryTestCase { assertNotNull(stashRef); assertEquals(firstStash, stashRef.getObjectId()); - ReflogReader reader = git.getRepository().getReflogReader( - Constants.R_STASH); + ReflogReader reader = git.getRepository().getRefDatabase() + .getReflogReader(Constants.R_STASH); List<ReflogEntry> entries = reader.getReverseEntries(); assertEquals(1, entries.size()); assertEquals(ObjectId.zeroId(), entries.get(0).getOldId()); @@ -194,8 +192,8 @@ public class StashDropCommandTest extends RepositoryTestCase { assertNotNull(stashRef); assertEquals(thirdStash, stashRef.getObjectId()); - ReflogReader reader = git.getRepository().getReflogReader( - Constants.R_STASH); + ReflogReader reader = git.getRepository().getRefDatabase() + .getReflogReader(Constants.R_STASH); List<ReflogEntry> entries = reader.getReverseEntries(); assertEquals(2, entries.size()); assertEquals(ObjectId.zeroId(), entries.get(1).getOldId()); @@ -252,8 +250,8 @@ public class StashDropCommandTest extends RepositoryTestCase { assertNotNull(stashRef); assertEquals(thirdStash, stashRef.getObjectId()); - ReflogReader reader = git.getRepository().getReflogReader( - Constants.R_STASH); + ReflogReader reader = git.getRepository().getRefDatabase() + .getReflogReader(Constants.R_STASH); List<ReflogEntry> entries = reader.getReverseEntries(); assertEquals(2, entries.size()); assertEquals(ObjectId.zeroId(), entries.get(1).getOldId()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java index f47f447375..c2c06b2477 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java @@ -23,20 +23,22 @@ import org.junit.Test; /** Unit tests of {@link BlameGenerator}. */ public class BlameGeneratorTest extends RepositoryTestCase { + private static final String FILE = "file.txt"; + @Test public void testBoundLineDelete() throws Exception { try (Git git = new Git(db)) { String[] content1 = new String[] { "first", "second" }; - writeTrashFile("file.txt", join(content1)); - git.add().addFilepattern("file.txt").call(); + writeTrashFile(FILE, join(content1)); + git.add().addFilepattern(FILE).call(); RevCommit c1 = git.commit().setMessage("create file").call(); String[] content2 = new String[] { "third", "first", "second" }; - writeTrashFile("file.txt", join(content2)); - git.add().addFilepattern("file.txt").call(); + writeTrashFile(FILE, join(content2)); + git.add().addFilepattern(FILE).call(); RevCommit c2 = git.commit().setMessage("create file").call(); - try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) { + try (BlameGenerator generator = new BlameGenerator(db, FILE)) { generator.push(null, db.resolve(Constants.HEAD)); assertEquals(3, generator.getResultContents().size()); @@ -47,7 +49,7 @@ public class BlameGeneratorTest extends RepositoryTestCase { assertEquals(1, generator.getResultEnd()); assertEquals(0, generator.getSourceStart()); assertEquals(1, generator.getSourceEnd()); - assertEquals("file.txt", generator.getSourcePath()); + assertEquals(FILE, generator.getSourcePath()); assertTrue(generator.next()); assertEquals(c1, generator.getSourceCommit()); @@ -56,7 +58,7 @@ public class BlameGeneratorTest extends RepositoryTestCase { assertEquals(3, generator.getResultEnd()); assertEquals(0, generator.getSourceStart()); assertEquals(2, generator.getSourceEnd()); - assertEquals("file.txt", generator.getSourcePath()); + assertEquals(FILE, generator.getSourcePath()); assertFalse(generator.next()); } @@ -87,7 +89,8 @@ public class BlameGeneratorTest extends RepositoryTestCase { git.add().addFilepattern(FILENAME_2).call(); RevCommit c2 = git.commit().setMessage("change file2").call(); - try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) { + try (BlameGenerator generator = new BlameGenerator(db, + FILENAME_2)) { generator.push(null, db.resolve(Constants.HEAD)); assertEquals(3, generator.getResultContents().size()); @@ -113,7 +116,8 @@ public class BlameGeneratorTest extends RepositoryTestCase { } // and test again with other BlameGenerator API: - try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) { + try (BlameGenerator generator = new BlameGenerator(db, + FILENAME_2)) { generator.push(null, db.resolve(Constants.HEAD)); BlameResult result = generator.computeBlameResult(); @@ -136,21 +140,21 @@ public class BlameGeneratorTest extends RepositoryTestCase { try (Git git = new Git(db)) { String[] content1 = new String[] { "first", "second", "third" }; - writeTrashFile("file.txt", join(content1)); - git.add().addFilepattern("file.txt").call(); + writeTrashFile(FILE, join(content1)); + git.add().addFilepattern(FILE).call(); git.commit().setMessage("create file").call(); String[] content2 = new String[] { "" }; - writeTrashFile("file.txt", join(content2)); - git.add().addFilepattern("file.txt").call(); + writeTrashFile(FILE, join(content2)); + git.add().addFilepattern(FILE).call(); git.commit().setMessage("create file").call(); - writeTrashFile("file.txt", join(content1)); - git.add().addFilepattern("file.txt").call(); + writeTrashFile(FILE, join(content1)); + git.add().addFilepattern(FILE).call(); RevCommit c3 = git.commit().setMessage("create file").call(); - try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) { + try (BlameGenerator generator = new BlameGenerator(db, FILE)) { generator.push(null, db.resolve(Constants.HEAD)); assertEquals(3, generator.getResultContents().size()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java index 7fb98ec53b..c41dd81add 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java @@ -584,7 +584,7 @@ public class AttributesHandlerTest extends RepositoryTestCase { } if (infoAttributesContent != null) { - File f = new File(db.getDirectory(), Constants.INFO_ATTRIBUTES); + File f = new File(db.getCommonDirectory(), Constants.INFO_ATTRIBUTES); write(f, infoAttributesContent); } config.save(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java index f23469eda0..35b953320e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java @@ -26,6 +26,7 @@ import org.eclipse.jgit.attributes.Attribute.State; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.Before; import org.junit.Test; @@ -230,10 +231,10 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase { else { Attributes entryAttributes = new Attributes(); - new AttributesHandler(walk).mergeAttributes(attributesNode, - pathName, - false, - entryAttributes); + new AttributesHandler(walk, + () -> walk.getTree(CanonicalTreeParser.class)) + .mergeAttributes(attributesNode, pathName, false, + entryAttributes); if (nodeAttrs != null && !nodeAttrs.isEmpty()) { for (Attribute attribute : nodeAttrs) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java index 1fcfbaf0fa..dbbcb75da9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java @@ -20,6 +20,7 @@ import java.io.InputStream; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.After; import org.junit.Test; @@ -156,8 +157,9 @@ public class AttributesNodeTest { private void assertAttribute(String path, AttributesNode node, Attributes attrs) throws IOException { Attributes attributes = new Attributes(); - new AttributesHandler(DUMMY_WALK).mergeAttributes(node, path, false, - attributes); + new AttributesHandler(DUMMY_WALK, + () -> DUMMY_WALK.getTree(CanonicalTreeParser.class)) + .mergeAttributes(node, path, false, attributes); assertEquals(attrs, attributes); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java index 7b573e122e..c6c91386a2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java @@ -26,6 +26,7 @@ import org.eclipse.jgit.attributes.Attribute.State; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; @@ -194,9 +195,10 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { else { Attributes entryAttributes = new Attributes(); - new AttributesHandler(walk).mergeAttributes(attributesNode, - pathName, false, - entryAttributes); + new AttributesHandler(walk, + () -> walk.getTree(CanonicalTreeParser.class)) + .mergeAttributes(attributesNode, pathName, false, + entryAttributes); if (nodeAttrs != null && !nodeAttrs.isEmpty()) { for (Attribute attribute : nodeAttrs) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java index 5638c1f7d9..562a515721 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java @@ -104,10 +104,9 @@ public class CGitAttributesTest extends RepositoryTestCase { UTF_8))) { r.lines().forEach(line -> { // Parse the line and add to result map - int start = 0; int i = line.indexOf(':'); String path = line.substring(0, i).trim(); - start = i + 1; + int start = i + 1; i = line.indexOf(':', start); String key = line.substring(start, i).trim(); String value = line.substring(i + 1).trim(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java index 73abd2d37e..698fdb31a8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java @@ -791,7 +791,7 @@ public class TreeWalkAttributeTest extends RepositoryTestCase { for (Attribute a : attributes) { ret.add(a); } - return (ret); + return ret; } private File writeAttributesFile(String name, String... rules) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java index 795029188d..ac30c6c526 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java @@ -10,7 +10,6 @@ package org.eclipse.jgit.attributes.merge; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -20,6 +19,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.function.Consumer; @@ -42,7 +42,6 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; -import org.junit.Ignore; import org.junit.Test; public class MergeGitAttributeTest extends RepositoryTestCase { @@ -99,19 +98,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase { try { writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } })) { checkoutBranch(REFS_HEADS_LEFT); @@ -141,19 +140,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase { writeTrashFile(".gitattributes", "*.cat -merge"); writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } })) { // Check that the merge attribute is unset @@ -186,19 +185,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase { writeTrashFile(".gitattributes", "*.txt -merge"); writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } })) { // Check that the merge attribute is unset @@ -230,19 +229,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase { writeTrashFile(".gitattributes", "*.cat merge=binary"); writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } }, g -> { try { writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); } catch (IOException e) { - e.printStackTrace(); + throw new UncheckedIOException(e); } })) { // Check that the merge attribute is set to binary @@ -268,12 +267,52 @@ public class MergeGitAttributeTest extends RepositoryTestCase { } } - /* - * This test is commented because JGit add conflict markers in binary files. - * cf. https://www.eclipse.org/forums/index.php/t/1086511/ - */ @Test - @Ignore + public void mergeTextualFile_SetUnionMerge() throws NoWorkTreeException, + NoFilepatternException, GitAPIException, IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat merge=union"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "G\n" + "C\n" + "F\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + })) { + // Check that the merge attribute is set to union + assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat", "union"); + assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat", + "union"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/left -> refs/heads/right + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + // Check that the file is the union of both branches (no conflict + // marker added) + String result = read(writeTrashFile("res.cat", + "A\n" + "G\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile())); + } + } + + @Test public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException, IOException, NoHeadException, ConcurrentRefUpdateException, CheckoutConflictException, InvalidMergeHeadsException, @@ -433,7 +472,7 @@ public class MergeGitAttributeTest extends RepositoryTestCase { try (FileInputStream mergeResultFile = new FileInputStream( db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF) .toFile())) { - assertFalse(contentEquals( + assertTrue(contentEquals( getClass().getResourceAsStream(ENABLED_CHECKED_GIF), mergeResultFile)); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java new file mode 100644 index 0000000000..3e4ac1f993 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2025, Google LLC. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.blame; + +import static java.lang.String.join; +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.blame.cache.BlameCache; +import org.eclipse.jgit.blame.cache.CacheRegion; +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +public class BlameGeneratorCacheTest extends RepositoryTestCase { + private static final String FILE = "file.txt"; + + /** + * Simple history: + * + * <pre> + * C1 C2 C3 C4 C4 blame + * lines ---------------------------------- + * L1 | C1 C1 C1 C1 C1 + * L2 | C1 C1 *C3 *C4 C4 + * L3 | C1 C1 *C3 C3 C3 + * L4 | *C2 C2 *C4 C4 + * </pre> + * + * @throws Exception + * any error + */ + @Test + public void blame_simple_correctRegions() throws Exception { + RevCommit c1, c2, c3, c4; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = commit(r, lines("L1C1", "L2C1", "L3C1")); + c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1); + c3 = commit(r, lines("L1C1", "L2C3", "L3C3", "L4C2"), c2); + c4 = commit(r, lines("L1C1", "L2C4", "L3C3", "L4C4"), c3); + } + + List<EmittedRegion> expectedRegions = Arrays.asList( + new EmittedRegion(c1, 0, 1), + new EmittedRegion(c4, 1, 2), + new EmittedRegion(c3, 2, 3), + new EmittedRegion(c4, 3, 4)); + + assertRegions(c4, null, expectedRegions, 4); + assertRegions(c4, emptyCache(), expectedRegions, 4); + assertRegions(c4, blameAndCache(c4), expectedRegions, 4); + assertRegions(c4, blameAndCache(c3), expectedRegions, 4); + assertRegions(c4, blameAndCache(c2), expectedRegions, 4); + assertRegions(c4, blameAndCache(c1), expectedRegions, 4); + } + + @Test + public void blame_simple_cacheUsage() throws Exception { + RevCommit c1, c2, c3, c4; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = commit(r, lines("L1C1", "L2C1", "L3C1")); + c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1); + c3 = commit(r, lines("L1C1", "L2C3", "L3C3", "L4C2"), c2); + c4 = commit(r, lines("L1C1", "L2C4", "L3C3", "L4C4"), c3); + } + + assertCacheUsage(c4, null, false, 4); + assertCacheUsage(c4, emptyCache(), false, 4); + assertCacheUsage(c4, blameAndCache(c4), true, 1); + assertCacheUsage(c4, blameAndCache(c3), true, 2); + assertCacheUsage(c4, blameAndCache(c2), true, 3); + // Cache not needed because c1 doesn't have parents + assertCacheUsage(c4, blameAndCache(c1), false, 4); + } + + @Test + public void blame_simple_createdMidHistory_correctRegions() throws Exception { + String c1Content = lines("L1C1", "L2C1", "L3C1"); + String c2Content = lines("L1C1", "L2C1", "L3C1", "L4C2"); + String c3Content = lines("L1C1", "L2C3", "L3C3", "L4C2"); + String c4Content = lines("L1C1", "L2C4", "L3C3", "L4C4"); + + RevCommit c0, c1, c2, c3, c4; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c0 = r.commit().add("otherfile", "content").create(); + c1 = r.commit().parent(c0).add(FILE, c1Content).create(); + c2 = r.commit().parent(c1).add(FILE, c2Content).create(); + c3 = r.commit().parent(c2).add(FILE, c3Content).create(); + c4 = r.commit().parent(c3).add(FILE, c4Content).create(); + } + + List<EmittedRegion> expectedRegions = Arrays.asList( + new EmittedRegion(c1, 0, 1), + new EmittedRegion(c4, 1, 2), + new EmittedRegion(c3, 2, 3), + new EmittedRegion(c4, 3, 4)); + + assertRegions(c4, null, expectedRegions, 4); + assertRegions(c4, emptyCache(), expectedRegions, 4); + assertRegions(c4, blameAndCache(c4, FILE), expectedRegions, 4); + assertRegions(c4, blameAndCache(c3, FILE), expectedRegions, 4); + assertRegions(c4, blameAndCache(c2, FILE), expectedRegions, 4); + assertRegions(c4, blameAndCache(c1, FILE), expectedRegions, 4); + assertRegions(c4, blameAndCache(c0, FILE), expectedRegions, 4); + } + + @Test + public void blame_simple_createdMidHistory_cacheUsage() throws Exception { + String c1Content = lines("L1C1", "L2C1", "L3C1"); + String c2Content = lines("L1C1", "L2C1", "L3C1", "L4C2"); + String c3Content = lines("L1C1", "L2C3", "L3C3", "L4C2"); + String c4Content = lines("L1C1", "L2C4", "L3C3", "L4C4"); + + RevCommit c0, c1, c2, c3, c4; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c0 = r.commit().add("otherfile", "content").create(); + c1 = r.commit().parent(c0).add(FILE, c1Content).create(); + c2 = r.commit().parent(c1).add(FILE, c2Content).create(); + c3 = r.commit().parent(c2).add(FILE, c3Content).create(); + c4 = r.commit().parent(c3).add(FILE, c4Content).create(); + } + + assertCacheUsage(c4, null, false, 4); + assertCacheUsage(c4, emptyCache(), false, 4); + assertCacheUsage(c4, blameAndCache(c4, FILE), true, 1); + assertCacheUsage(c4, blameAndCache(c3, FILE), true, 2); + assertCacheUsage(c4, blameAndCache(c2, FILE), true, 3); + // Cache not needed because c1 created the file + assertCacheUsage(c4, blameAndCache(c1, FILE), false, 4); + } + + /** + * Overwrite: + * + * <pre> + * C1 C2 C3 C3 blame + * lines ---------------------------------- + * L1 | C1 C1 *C3 C3 + * L2 | C1 C1 *C3 C3 + * L3 | C1 C1 *C3 C3 + * L4 | *C2 + * </pre> + * + * @throws Exception + * any error + */ + @Test + public void blame_overwrite_correctRegions() throws Exception { + RevCommit c1, c2, c3; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = commit(r, lines("L1C1", "L2C1", "L3C1")); + c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1); + c3 = commit(r, lines("L1C3", "L2C3", "L3C3"), c2); + } + + List<EmittedRegion> expectedRegions = Arrays.asList( + new EmittedRegion(c3, 0, 3)); + + assertRegions(c3, null, expectedRegions, 3); + assertRegions(c3, emptyCache(), expectedRegions, 3); + assertRegions(c3, blameAndCache(c3), expectedRegions, 3); + assertRegions(c3, blameAndCache(c2), expectedRegions, 3); + assertRegions(c3, blameAndCache(c1), expectedRegions, 3); + } + + @Test + public void blame_overwrite_cacheUsage() throws Exception { + RevCommit c1, c2, c3; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = commit(r, lines("L1C1", "L2C1", "L3C1")); + c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1); + c3 = commit(r, lines("L1C3", "L2C3", "L3C3"), c2); + } + + assertCacheUsage(c3, null, false, 1); + assertCacheUsage(c3, emptyCache(), false, 1); + assertCacheUsage(c3, blameAndCache(c3), true, 1); + assertCacheUsage(c3, blameAndCache(c2), false, 1); + assertCacheUsage(c3, blameAndCache(c1), false, 1); + } + + /** + * Merge: + * + * <pre> + * root + * ---- + * L1 - + * L2 - + * L3 - + * / \ + * sideA sideB + * ----- ----- + * *L1 a L1 - + * *L2 a L2 - + * *L3 a L3 - + * *L4 a *L4 b + * L5 - *L5 b + * L6 - *L6 b + * L7 - *L7 b + * \ / + * merge + * ----- + * L1-L4 a (from sideA) + * L5-L7 - (common, from root) + * L8-L11 b (from sideB) + * </pre> + * + * @throws Exception + * any error + */ + @Test + public void blame_merge_correctRegions() throws Exception { + RevCommit root, sideA, sideB, mergedTip; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + root = commitAsLines(r, "---"); + sideA = commitAsLines(r, "aaaa---", root); + sideB = commitAsLines(r, "---bbbb", root); + mergedTip = commitAsLines(r, "aaaa---bbbb", sideA, sideB); + } + + List<EmittedRegion> expectedRegions = Arrays.asList( + new EmittedRegion(sideA, 0, 4), + new EmittedRegion(root, 4, 7), + new EmittedRegion(sideB, 7, 11)); + + assertRegions(mergedTip, null, expectedRegions, 11); + assertRegions(mergedTip, emptyCache(), expectedRegions, 11); + assertRegions(mergedTip, blameAndCache(root), expectedRegions, 11); + assertRegions(mergedTip, blameAndCache(sideA), expectedRegions, 11); + assertRegions(mergedTip, blameAndCache(sideB), expectedRegions, 11); + assertRegions(mergedTip, blameAndCache(mergedTip), expectedRegions, 11); + } + + @Test + public void blame_merge_cacheUsage() throws Exception { + RevCommit root, sideA, sideB, mergedTip; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + root = commitAsLines(r, "---"); + sideA = commitAsLines(r, "aaaa---", root); + sideB = commitAsLines(r, "---bbbb", root); + mergedTip = commitAsLines(r, "aaaa---bbbb", sideA, sideB); + } + + assertCacheUsage(mergedTip, null, /* cacheUsed */ false, + /* candidates */ 4); + assertCacheUsage(mergedTip, emptyCache(), false, 4); + + // While splitting unblamed regions to parents, sideA comes first + // and gets "aaaa----". Processing is by commit time, so sideB is + // explored first + assertCacheUsage(mergedTip, blameAndCache(sideA), true, 3); + assertCacheUsage(mergedTip, blameAndCache(sideB), true, 4); + assertCacheUsage(mergedTip, blameAndCache(root), false, 4); + } + + /** + * Moving block (insertion) + * + * <pre> + * C1 C2 C3 C3 blame + * lines ---------------------------------- + * L1 | C1 C1 C1 C1 + * L2 | C1 *C2 C2 C2 + * L3 | C1 *C3 C3 + * L4 | C1 C1 + * </pre> + * + * @throws Exception + * any error + */ + @Test + public void blame_movingBlock_correctRegions() throws Exception { + RevCommit c1, c2, c3; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = commit(r, lines("L1C1", "L2C1")); + c2 = commit(r, lines("L1C1", "middle", "L2C1"), c1); + c3 = commit(r, lines("L1C1", "middle", "extra", "L2C1"), c2); + } + + List<EmittedRegion> expectedRegions = Arrays.asList( + new EmittedRegion(c1, 0, 1), + new EmittedRegion(c2, 1, 2), + new EmittedRegion(c3, 2, 3), + new EmittedRegion(c1, 3, 4)); + + assertRegions(c3, null, expectedRegions, 4); + assertRegions(c3, emptyCache(), expectedRegions, 4); + assertRegions(c3, blameAndCache(c3), expectedRegions, 4); + assertRegions(c3, blameAndCache(c2), expectedRegions, 4); + assertRegions(c3, blameAndCache(c1), expectedRegions, 4); + } + + @Test + public void blame_movingBlock_cacheUsage() throws Exception { + RevCommit c1, c2, c3; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = commitAsLines(r, "root---"); + c2 = commitAsLines(r, "rootXXX---", c1); + c3 = commitAsLines(r, "rootYYYXXX---", c2); + } + + assertCacheUsage(c3, null, false, 3); + assertCacheUsage(c3, emptyCache(), false, 3); + assertCacheUsage(c3, blameAndCache(c3), true, 1); + assertCacheUsage(c3, blameAndCache(c2), true, 2); + assertCacheUsage(c3, blameAndCache(c1), false, 3); + } + + @Test + public void blame_cacheOnlyOnChange_unmodifiedInSomeCommits_cacheUsage() throws Exception { + String README = "README"; + String fileC1Content = lines("L1C1", "L2C1", "L3C1"); + String fileC2Content = lines("L1C1", "L2C1", "L3C1", "L4C2"); + String fileC3Content = lines("L1C1", "L2C3", "L3C3", "L4C2"); + String fileC4Content = lines("L1C1", "L2C4", "L3C3", "L4C4"); + + RevCommit c1, c2, c3, c4, ni; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = r.commit().add(FILE, fileC1Content).create(); + c2 = r.commit().parent(c1).add(FILE, fileC2Content).create(); + // Keep FILE and edit 100 times README + ni = c2; + for (int i = 0; i < 100; i++) { + ni = r.commit().parent(ni).add(README, "whatever " + i).create(); + } + c3 = r.commit().parent(ni).add(FILE, fileC3Content).create(); + c4 = r.commit().parent(c3).add(FILE, fileC4Content).create(); + r.branch("refs/heads/master").update(c4); + } + + InMemoryBlameCache empty = emptyCache(); + assertCacheUsage(c4, empty, false, 104); + assertEquals(3, empty.callCount); + + InMemoryBlameCache c4Cached = blameAndCache(c4, FILE); + assertCacheUsage(c4, c4Cached, true, 1); + assertEquals(1, c4Cached.callCount); + + InMemoryBlameCache c3Cached = blameAndCache(c3, FILE); + assertCacheUsage(c4, c3Cached, true, 2); + assertEquals(2, c3Cached.callCount); + + // This commit doesn't touch the file, shouldn't check the cache + InMemoryBlameCache niCached = blameAndCache(ni, FILE); + assertCacheUsage(c4, niCached, false, 104); + assertEquals(3, niCached.callCount); + + InMemoryBlameCache c2Cached = blameAndCache(c2, FILE); + assertCacheUsage(c4, c2Cached, true, 103); + assertEquals(3, c2Cached.callCount); + + // No parents, c1 doesn't need cache. + InMemoryBlameCache c1Cached = blameAndCache(c1, FILE); + assertCacheUsage(c4, c1Cached, false, 104); + assertEquals(3, c1Cached.callCount); + } + + @Test + public void blame_cacheOnlyOnChange_renameWithoutChange_cacheUsage() throws Exception { + String OTHER = "other.txt"; + String c1Content = lines("L1C1", "L2C1", "L3C1"); + String c2Content = lines("L1C1", "L2C1", "L3C1", "L4C2"); + + RevCommit c1, c2, c3; + try (TestRepository<FileRepository> r = new TestRepository<>(db)) { + c1 = r.commit().add(OTHER, c1Content).create(); + c2 = r.commit().parent(c1).add(OTHER, c2Content).create(); + c3 = r.commit().parent(c2).rm(OTHER).add(FILE, c2Content).create(); + r.branch("refs/heads/master").update(c3); + } + + assertCacheUsage(c3, null, false, 3); + assertCacheUsage(c3, emptyCache(), false, 3); + assertCacheUsage(c3, blameAndCache(c3, FILE), true, 1); + assertCacheUsage(c3, blameAndCache(c2, OTHER), true, 2); + assertCacheUsage(c3, blameAndCache(c1, OTHER), false, 3); + } + + private void assertRegions(RevCommit commit, InMemoryBlameCache cache, + List<EmittedRegion> expectedRegions, int resultLineCount) + throws IOException { + try (BlameGenerator gen = new BlameGenerator(db, FILE, cache)) { + gen.push(null, db.parseCommit(commit)); + List<EmittedRegion> regions = consume(gen); + assertRegionsEquals(expectedRegions, regions); + assertAllLinesCovered(/* lines= */ resultLineCount, regions); + } + } + + private void assertCacheUsage(RevCommit commit, InMemoryBlameCache cache, + boolean cacheHit, int candidatesVisited) throws IOException { + try (BlameGenerator gen = new BlameGenerator(db, FILE, cache)) { + gen.push(null, db.parseCommit(commit)); + consume(gen); + assertEquals(cacheHit, gen.getStats().isCacheHit()); + assertEquals(candidatesVisited, + gen.getStats().getCandidatesVisited()); + } + } + + private static void assertAllLinesCovered(int lines, + List<EmittedRegion> regions) { + Collections.sort(regions); + assertEquals("Starts in first line", 0, regions.get(0).resultStart()); + for (int i = 1; i < regions.size(); i++) { + assertEquals("No gaps", regions.get(i).resultStart(), + regions.get(i - 1).resultEnd()); + } + assertEquals("Ends in last line", lines, + regions.get(regions.size() - 1).resultEnd()); + } + + private static void assertRegionsEquals(List<EmittedRegion> expected, + List<EmittedRegion> actual) { + assertEquals(expected.size(), actual.size()); + Collections.sort(actual); + for (int i = 0; i < expected.size(); i++) { + assertEquals(String.format("List differ in element %d", i), + expected.get(i), actual.get(i)); + } + } + + private static InMemoryBlameCache emptyCache() { + return new InMemoryBlameCache("<empty>"); + } + + private List<EmittedRegion> consume(BlameGenerator generator) + throws IOException { + List<EmittedRegion> result = new ArrayList<>(); + while (generator.next()) { + EmittedRegion genRegion = new EmittedRegion( + generator.getSourceCommit().toObjectId(), + generator.getResultStart(), generator.getResultEnd()); + result.add(genRegion); + } + return result; + } + + private InMemoryBlameCache blameAndCache(RevCommit commit) + throws IOException { + return blameAndCache(commit, FILE); + } + + private InMemoryBlameCache blameAndCache(RevCommit commit, String path) + throws IOException { + List<CacheRegion> regions; + try (BlameGenerator generator = new BlameGenerator(db, path)) { + generator.push(null, commit); + regions = consume(generator).stream() + .map(EmittedRegion::asCacheRegion) + .toList(); + } + InMemoryBlameCache cache = new InMemoryBlameCache("<x>"); + cache.put(commit, path, regions); + return cache; + } + + private static RevCommit commitAsLines(TestRepository<?> r, + String charPerLine, RevCommit... parents) throws Exception { + return commit(r, charPerLine.replaceAll("\\S", "$0\n"), parents); + } + + private static RevCommit commit(TestRepository<?> r, String contents, + RevCommit... parents) throws Exception { + return commit(r, Map.of(FILE, contents), parents); + } + + private static RevCommit commit(TestRepository<?> r, + Map<String, String> fileContents, RevCommit... parents) + throws Exception { + TestRepository<?>.CommitBuilder builder = r.commit(); + for (RevCommit commit : parents) { + builder.parent(commit); + } + fileContents.forEach((path, content) -> { + try { + builder.add(path, content); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return builder.create(); + } + + private static String lines(String... l) { + return join("\n", l); + } + + private record EmittedRegion(ObjectId oid, int resultStart, int resultEnd) + implements Comparable<EmittedRegion> { + @Override + public int compareTo(EmittedRegion o) { + return resultStart - o.resultStart; + } + + CacheRegion asCacheRegion() { + return new CacheRegion(FILE, oid, resultStart, resultEnd); + } + } + + private static class InMemoryBlameCache implements BlameCache { + + private final Map<Key, List<CacheRegion>> cache = new HashMap<>(); + + private final String description; + + private int callCount; + + public InMemoryBlameCache(String description) { + this.description = description; + } + + @Override + public List<CacheRegion> get(Repository repo, ObjectId commitId, + String path) throws IOException { + callCount++; + return cache.get(new Key(commitId.name(), path)); + } + + public void put(ObjectId commitId, String path, + List<CacheRegion> cachedRegions) { + cache.put(new Key(commitId.name(), path), cachedRegions); + } + + @Override + public String toString() { + return "InMemoryCache: " + description; + } + + record Key(String commitId, String path) { + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java new file mode 100644 index 0000000000..1b28676fbf --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2025, Google LLC. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.blame; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.eclipse.jgit.blame.cache.CacheRegion; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +public class BlameRegionMergerTest extends RepositoryTestCase { + + private static final ObjectId O1 = ObjectId + .fromString("ff6dd8db6edc9aa0ac58fea1d14a55be46c3eb14"); + + private static final ObjectId O2 = ObjectId + .fromString("c3c7f680c6bee238617f25f6aa85d0b565fc8ecb"); + + private static final ObjectId O3 = ObjectId + .fromString("29e014aad0399fe8ede7c101d01b6e440ac9966b"); + + List<RevCommit> fakeCommits = List.of(new FakeRevCommit(O1), + new FakeRevCommit(O2), new FakeRevCommit(O3)); + + // In reverse order, so the code doesn't assume a sorted list + List<CacheRegion> cachedRegions = List.of( + new CacheRegion("README", O3, 20, 30), + new CacheRegion("README", O2, 10, 20), + new CacheRegion("README", O1, 0, 10)); + + BlameRegionMerger blamer = new BlameRegionMergerFakeCommits(fakeCommits, + cachedRegions); + + @Test + public void intersectRegions_allInside() { + Region unblamed = new Region(15, 18, 10); + CacheRegion blamed = new CacheRegion("README", O1, 10, 90); + + Region result = BlameRegionMerger.intersectRegions(unblamed, blamed); + // Same lines in result and source + assertEquals(15, result.resultStart); + assertEquals(18, result.sourceStart); + assertEquals(10, result.length); + assertNull(result.next); + } + + @Test + public void intersectRegions_startsBefore() { + // Intesecting [4, 14) with [10, 90) + Region unblamed = new Region(30, 4, 10); + CacheRegion blamed = new CacheRegion("README", O1, 10, 90); + + Region result = BlameRegionMerger.intersectRegions(unblamed, blamed); + + // The unblamed region starting at 4 (sourceStart), starts at 30 in the + // original file (resultStart). e.g. some commit introduced + // lines. If we take the second portion of the region, we need to move + // the result start accordingly. + assertEquals(36, result.resultStart); + assertEquals(10, result.sourceStart); + assertEquals(4, result.length); + assertNull(result.next); + } + + @Test + public void intersectRegions_endsAfter() { + // Intesecting [85, 95) with [10, 90) + Region unblamed = new Region(30, 85, 10); + CacheRegion blamed = new CacheRegion("README", O1, 10, 90); + + Region result = BlameRegionMerger.intersectRegions(unblamed, blamed); + + assertEquals(30, result.resultStart); + assertEquals(85, result.sourceStart); + assertEquals(5, result.length); + assertNull(result.next); + } + + @Test + public void intersectRegions_spillOverBothSides() { + // Intesecting [5, 100) with [10, 90) + Region unblamed = new Region(30, 5, 95); + CacheRegion blamed = new CacheRegion("README", O1, 10, 90); + + Region result = BlameRegionMerger.intersectRegions(unblamed, blamed); + + assertEquals(35, result.resultStart); + assertEquals(10, result.sourceStart); + assertEquals(80, result.length); + assertNull(result.next); + } + + @Test + public void intersectRegions_exactMatch() { + // Intesecting [5, 100) with [10, 90) + Region unblamed = new Region(30, 10, 80); + CacheRegion blamed = new CacheRegion("README", O1, 10, 90); + + Region result = BlameRegionMerger.intersectRegions(unblamed, blamed); + + assertEquals(30, result.resultStart); + assertEquals(10, result.sourceStart); + assertEquals(80, result.length); + assertNull(result.next); + } + + @Test + public void findOverlaps_allInside() { + Region unblamed = new Region(0, 11, 4); + List<CacheRegion> overlaps = blamer.findOverlaps(unblamed); + assertEquals(1, overlaps.size()); + assertEquals(10, overlaps.get(0).getStart()); + assertEquals(20, overlaps.get(0).getEnd()); + } + + @Test + public void findOverlaps_overTwoRegions() { + Region unblamed = new Region(0, 8, 4); + List<CacheRegion> overlaps = blamer.findOverlaps(unblamed); + assertEquals(2, overlaps.size()); + assertEquals(0, overlaps.get(0).getStart()); + assertEquals(10, overlaps.get(0).getEnd()); + assertEquals(10, overlaps.get(1).getStart()); + assertEquals(20, overlaps.get(1).getEnd()); + } + + @Test + public void findOverlaps_overThreeRegions() { + Region unblamed = new Region(0, 8, 15); + List<CacheRegion> overlaps = blamer.findOverlaps(unblamed); + assertEquals(3, overlaps.size()); + assertEquals(0, overlaps.get(0).getStart()); + assertEquals(10, overlaps.get(0).getEnd()); + assertEquals(10, overlaps.get(1).getStart()); + assertEquals(20, overlaps.get(1).getEnd()); + assertEquals(20, overlaps.get(2).getStart()); + assertEquals(30, overlaps.get(2).getEnd()); + } + + @Test + public void blame_exactOverlap() throws IOException { + Region unblamed = new Region(0, 10, 10); + List<Candidate> blamed = blamer.mergeOneRegion(unblamed); + + assertEquals(1, blamed.size()); + Candidate c = blamed.get(0); + assertEquals(c.sourceCommit.name(), O2.name()); + assertEquals(c.regionList.resultStart, unblamed.resultStart); + assertEquals(c.regionList.sourceStart, unblamed.sourceStart); + assertEquals(10, c.regionList.length); + assertNull(c.regionList.next); + } + + @Test + public void blame_corruptedIndex() { + Region outOfRange = new Region(0, 43, 4); + // This region is out of the blamed area + assertThrows(IOException.class, + () -> blamer.mergeOneRegion(outOfRange)); + } + + @Test + public void blame_allInsideOneBlamedRegion() throws IOException { + Region unblamed = new Region(0, 5, 3); + // This region if fully blamed to O1 + List<Candidate> blamed = blamer.mergeOneRegion(unblamed); + assertEquals(1, blamed.size()); + Candidate c = blamed.get(0); + assertEquals(c.sourceCommit.name(), O1.name()); + assertEquals(c.regionList.resultStart, unblamed.resultStart); + assertEquals(c.regionList.sourceStart, unblamed.sourceStart); + assertEquals(3, c.regionList.length); + assertNull(c.regionList.next); + } + + @Test + public void blame_overTwoBlamedRegions() throws IOException { + Region unblamed = new Region(0, 8, 5); + // (8, 10) belongs go C1, (10, 13) to C2 + List<Candidate> blamed = blamer.mergeOneRegion(unblamed); + assertEquals(2, blamed.size()); + Candidate c = blamed.get(0); + assertEquals(c.sourceCommit.name(), O1.name()); + assertEquals(unblamed.resultStart, c.regionList.resultStart); + assertEquals(unblamed.sourceStart, c.regionList.sourceStart); + assertEquals(2, c.regionList.length); + assertNull(c.regionList.next); + + c = blamed.get(1); + assertEquals(c.sourceCommit.name(), O2.name()); + assertEquals(2, c.regionList.resultStart); + assertEquals(10, c.regionList.sourceStart); + assertEquals(3, c.regionList.length); + assertNull(c.regionList.next); + } + + @Test + public void blame_all() throws IOException { + Region unblamed = new Region(0, 0, 30); + List<Candidate> blamed = blamer.mergeOneRegion(unblamed); + assertEquals(3, blamed.size()); + Candidate c = blamed.get(0); + assertEquals(c.sourceCommit.name(), O1.name()); + assertEquals(unblamed.resultStart, c.regionList.resultStart); + assertEquals(unblamed.sourceStart, c.regionList.sourceStart); + assertEquals(10, c.regionList.length); + assertNull(c.regionList.next); + + c = blamed.get(1); + assertEquals(c.sourceCommit.name(), O2.name()); + assertEquals(10, c.regionList.resultStart); + assertEquals(10, c.regionList.sourceStart); + assertEquals(10, c.regionList.length); + assertNull(c.regionList.next); + + c = blamed.get(2); + assertEquals(c.sourceCommit.name(), O3.name()); + assertEquals(20, c.regionList.resultStart); + assertEquals(20, c.regionList.sourceStart); + assertEquals(10, c.regionList.length); + assertNull(c.regionList.next); + } + + @Test + public void blame_fromCandidate() { + // We don't use anything from the candidate besides the + // regionList + Candidate c = new Candidate(null, null, null); + c.regionList = new Region(0, 8, 5); + c.regionList.next = new Region(22, 22, 4); + + Candidate blamed = blamer.mergeCandidate(c); + // Three candidates + assertNotNull(blamed); + assertNotNull(blamed.queueNext); + assertNotNull(blamed.queueNext.queueNext); + assertNull(blamed.queueNext.queueNext.queueNext); + + assertEquals(O1.name(), blamed.sourceCommit.name()); + + Candidate second = blamed.queueNext; + assertEquals(O2.name(), second.sourceCommit.name()); + + Candidate third = blamed.queueNext.queueNext; + assertEquals(O3.name(), third.sourceCommit.name()); + } + + @Test + public void blame_fromCandidate_twiceCandidateInOutput() { + Candidate c = new Candidate(null, null, null); + // This produces O1 and O2 + c.regionList = new Region(0, 8, 5); + // This produces O2 and O3 + c.regionList.next = new Region(20, 15, 7); + + Candidate blamed = blamer.mergeCandidate(c); + assertCandidateSingleRegion(O1, 2, blamed); + blamed = blamed.queueNext; + assertCandidateSingleRegion(O2, 3, blamed); + // We do not merge candidates afterwards, so these are + // two different candidates to the same source + blamed = blamed.queueNext; + assertCandidateSingleRegion(O2, 5, blamed); + blamed = blamed.queueNext; + assertCandidateSingleRegion(O3, 2, blamed); + assertNull(blamed.queueNext); + } + + private static void assertCandidateSingleRegion(ObjectId expectedOid, + int expectedLength, Candidate actual) { + assertNotNull("candidate", actual); + assertNotNull("region list not empty", actual.regionList); + assertNull("region list has only one element", actual.regionList.next); + assertEquals(expectedOid, actual.sourceCommit); + assertEquals(expectedLength, actual.regionList.length); + } + + private static final class BlameRegionMergerFakeCommits + extends BlameRegionMerger { + + private final Map<ObjectId, RevCommit> cache; + + BlameRegionMergerFakeCommits(List<RevCommit> commits, + List<CacheRegion> blamedRegions) { + super(null, null, blamedRegions); + cache = commits.stream().collect(Collectors + .toMap(RevCommit::toObjectId, Function.identity())); + } + + @Override + protected RevCommit parse(ObjectId oid) { + return cache.get(oid); + } + } + + private static final class FakeRevCommit extends RevCommit { + FakeRevCommit(AnyObjectId id) { + super(id); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java new file mode 100644 index 0000000000..1352871983 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2024 Qualcomm Innovation Center, Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.diff; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.stream.Collectors; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.junit.Test; + +public class DiffFormatterBuiltInDriverTest extends RepositoryTestCase { + @Test + public void testCppDriver() throws Exception { + String fileName = "greeting.c"; + String body = Files.readString( + Path.of(JGitTestUtil.getTestResourceFile(fileName) + .getAbsolutePath())); + RevCommit c1; + RevCommit c2; + try (Git git = new Git(db)) { + createCommit(git, ".gitattributes", "*.c diff=cpp"); + c1 = createCommit(git, fileName, body); + c2 = createCommit(git, fileName, + body.replace("Good day", "Greetings") + .replace("baz", "qux")); + } + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + DiffFormatter diffFormatter = new DiffFormatter(os)) { + String actual = getHunkHeaders(c1, c2, os, diffFormatter); + String expected = + "@@ -27,7 +27,7 @@ void getPersonalizedGreeting(char *result, const char *name, const char *timeOfD\n" + + "@@ -37,7 +37,7 @@ int main() {"; + assertEquals(expected, actual); + } + } + + @Test + public void testDtsDriver() throws Exception { + String fileName = "sample.dtsi"; + String body = Files.readString( + Path.of(JGitTestUtil.getTestResourceFile(fileName) + .getAbsolutePath())); + RevCommit c1; + RevCommit c2; + try (Git git = new Git(db)) { + createCommit(git, ".gitattributes", "*.dtsi diff=dts"); + c1 = createCommit(git, fileName, body); + c2 = createCommit(git, fileName, + body.replace("clock-frequency = <24000000>", + "clock-frequency = <48000000>")); + } + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + DiffFormatter diffFormatter = new DiffFormatter(os)) { + String actual = getHunkHeaders(c1, c2, os, diffFormatter); + String expected = "@@ -20,6 +20,6 @@ uart0: uart@101f1000 {"; + assertEquals(expected, actual); + } + } + + @Test + public void testJavaDriver() throws Exception { + String resourceName = "greeting.javasource"; + String body = Files.readString( + Path.of(JGitTestUtil.getTestResourceFile(resourceName) + .getAbsolutePath())); + RevCommit c1; + RevCommit c2; + try (Git git = new Git(db)) { + createCommit(git, ".gitattributes", "*.java diff=java"); + String fileName = "Greeting.java"; + c1 = createCommit(git, fileName, body); + c2 = createCommit(git, fileName, + body.replace("Good day", "Greetings") + .replace("baz", "qux")); + } + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + DiffFormatter diffFormatter = new DiffFormatter(os)) { + String actual = getHunkHeaders(c1, c2, os, diffFormatter); + String expected = + "@@ -22,7 +22,7 @@ public String getPersonalizedGreeting(String name, String timeOfDay) {\n" + + "@@ -32,6 +32,6 @@ public static void main(String[] args) {"; + assertEquals(expected, actual); + } + } + + @Test + public void testPythonDriver() throws Exception { + String fileName = "greeting.py"; + String body = Files.readString( + Path.of(JGitTestUtil.getTestResourceFile(fileName) + .getAbsolutePath())); + RevCommit c1; + RevCommit c2; + try (Git git = new Git(db)) { + createCommit(git, ".gitattributes", "*.py diff=python"); + c1 = createCommit(git, fileName, body); + c2 = createCommit(git, fileName, + body.replace("Good day", "Greetings")); + } + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + DiffFormatter diffFormatter = new DiffFormatter(os)) { + String actual = getHunkHeaders(c1, c2, os, diffFormatter); + String expected = "@@ -16,7 +16,7 @@ def get_personalized_greeting(self, name, time_of_day):"; + assertEquals(expected, actual); + } + } + + @Test + public void testRustDriver() throws Exception { + String fileName = "greeting.rs"; + String body = Files.readString( + Path.of(JGitTestUtil.getTestResourceFile(fileName) + .getAbsolutePath())); + RevCommit c1; + RevCommit c2; + try (Git git = new Git(db)) { + createCommit(git, ".gitattributes", "*.rs diff=rust"); + c1 = createCommit(git, fileName, body); + c2 = createCommit(git, fileName, + body.replace("Good day", "Greetings") + .replace("baz", "qux")); + } + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + DiffFormatter diffFormatter = new DiffFormatter(os)) { + String actual = getHunkHeaders(c1, c2, os, diffFormatter); + String expected = + "@@ -14,7 +14,7 @@ fn get_personalized_greeting(&self, name: &str, time_of_day: &str) -> String {\n" + + "@@ -23,5 +23,5 @@ fn main() {"; + assertEquals(expected, actual); + } + } + + private String getHunkHeaders(RevCommit c1, RevCommit c2, + ByteArrayOutputStream os, DiffFormatter diffFormatter) + throws IOException { + diffFormatter.setRepository(db); + diffFormatter.format(new CanonicalTreeParser(null, db.newObjectReader(), + c1.getTree()), + new CanonicalTreeParser(null, db.newObjectReader(), + c2.getTree())); + diffFormatter.flush(); + return Arrays.stream(os.toString(StandardCharsets.UTF_8).split("\n")) + .filter(line -> line.startsWith("@@")) + .collect(Collectors.joining("\n")); + } + + private RevCommit createCommit(Git git, String fileName, String body) + throws IOException, GitAPIException { + writeTrashFile(fileName, body); + git.add().addFilepattern(".").call(); + return git.commit().setMessage("message").call(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java index f657bab771..a2c20aaaba 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java @@ -28,7 +28,7 @@ public class EditListTest { assertTrue(l.isEmpty()); assertEquals("EditList[]", l.toString()); - assertEquals(l, l); + assertTrue(l.equals(l)); assertEquals(new EditList(), l); assertFalse(l.equals("")); assertEquals(l.hashCode(), new EditList().hashCode()); @@ -44,7 +44,7 @@ public class EditListTest { assertSame(e, l.get(0)); assertSame(e, l.iterator().next()); - assertEquals(l, l); + assertTrue(l.equals(l)); assertFalse(l.equals(new EditList())); final EditList l2 = new EditList(); @@ -69,7 +69,7 @@ public class EditListTest { assertSame(e1, i.next()); assertSame(e2, i.next()); - assertEquals(l, l); + assertTrue(l.equals(l)); assertFalse(l.equals(new EditList())); final EditList l2 = new EditList(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java index 8ab9bb12de..86c6d77cc6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java @@ -98,7 +98,7 @@ public class EditTest { final Edit e1 = new Edit(1, 2, 3, 4); final Edit e2 = new Edit(1, 2, 3, 4); - assertEquals(e1, e1); + assertTrue(e1.equals(e1)); assertEquals(e2, e1); assertEquals(e1, e2); assertEquals(e1.hashCode(), e2.hashCode()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java index 703d68b37c..61801106af 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java @@ -218,7 +218,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase { try { b.commit(); } catch (ReceivedEventMarkerException e) { - fail("unexpected IndexChangedEvent"); + throw new AssertionError("unexpected IndexChangedEvent", e); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java index 8e84dfa318..01d1e0282d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009, 2020 Google Inc. and others + * Copyright (C) 2009, 2023 Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -46,6 +46,16 @@ public class DirCacheEntryTest { assertFalse(isValidPath("a/")); assertFalse(isValidPath("ab/cd/ef/")); assertFalse(isValidPath("a\u0000b")); + assertFalse(isValidPath(".git")); + assertFalse(isValidPath(".GIT")); + assertFalse(isValidPath(".Git")); + assertFalse(isValidPath(".git/b")); + assertFalse(isValidPath(".GIT/b")); + assertFalse(isValidPath(".Git/b")); + assertFalse(isValidPath("x/y/.git/z/b")); + assertFalse(isValidPath("x/y/.GIT/z/b")); + assertFalse(isValidPath("x/y/.Git/z/b")); + assertTrue(isValidPath("git/b")); } @SuppressWarnings("unused") diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java new file mode 100644 index 0000000000..e3bc85a512 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.dircache; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.File; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +/** + * Tests for checking out with invalid paths. + */ +public class InvalidPathCheckoutTest extends RepositoryTestCase { + + private DirCacheEntry brokenEntry(String fileName, RevBlob blob) { + DirCacheEntry entry = new DirCacheEntry("XXXX/" + fileName); + entry.path[0] = '.'; + entry.path[1] = 'g'; + entry.path[2] = 'i'; + entry.path[3] = 't'; + entry.setFileMode(FileMode.REGULAR_FILE); + entry.setObjectId(blob); + return entry; + } + + @Test + public void testCheckoutIntoDotGit() throws Exception { + try (TestRepository<Repository> repo = new TestRepository<>(db)) { + db.incrementOpen(); + // DirCacheEntry does not allow any path component to contain + // ".git". C git also forbids this. But what if somebody creates + // such an entry explicitly? + RevCommit base = repo + .commit(repo.tree(brokenEntry("b", repo.blob("test")))); + try (Git git = new Git(db)) { + assertThrows(InvalidPathException.class, () -> git.reset() + .setMode(ResetType.HARD).setRef(base.name()).call()); + File b = new File(new File(trash, ".git"), "b"); + assertFalse(".git/b should not exist", b.exists()); + } + } + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java index c3b93879b2..5065b57840 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.gitrepo; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -22,6 +23,7 @@ import java.util.List; import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.BareWriterConfig; import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; @@ -68,6 +70,49 @@ public class BareSuperprojectWriterTest extends RepositoryTestCase { } @Test + public void write_setGitModulesContents_pinned() throws Exception { + try (Repository bareRepo = createBareRepository()) { + RepoProject pinWithUpstream = new RepoProject("pinWithUpstream", + "path/x", "cbc0fae7e1911d27e1de37d364698dba4411c78b", + "remote", ""); + pinWithUpstream.setUrl("http://example.com/a"); + pinWithUpstream.setUpstream("branchX"); + + RepoProject pinWithoutUpstream = new RepoProject( + "pinWithoutUpstream", "path/y", + "cbc0fae7e1911d27e1de37d364698dba4411c78b", "remote", ""); + pinWithoutUpstream.setUrl("http://example.com/b"); + + RemoteReader mockRemoteReader = mock(RemoteReader.class); + + BareSuperprojectWriter w = new BareSuperprojectWriter(bareRepo, + null, "refs/heads/master", author, mockRemoteReader, + BareWriterConfig.getDefault(), List.of()); + + RevCommit commit = w + .write(Arrays.asList(pinWithUpstream, pinWithoutUpstream)); + + String contents = readContents(bareRepo, commit, ".gitmodules"); + Config cfg = new Config(); + cfg.fromText(contents); + + assertThat(cfg.getString("submodule", "pinWithUpstream", "path"), + is("path/x")); + assertThat(cfg.getString("submodule", "pinWithUpstream", "url"), + is("http://example.com/a")); + assertThat(cfg.getString("submodule", "pinWithUpstream", "ref"), + is("branchX")); + + assertThat(cfg.getString("submodule", "pinWithoutUpstream", "path"), + is("path/y")); + assertThat(cfg.getString("submodule", "pinWithoutUpstream", "url"), + is("http://example.com/b")); + assertThat(cfg.getString("submodule", "pinWithoutUpstream", "ref"), + nullValue()); + } + } + + @Test public void write_setExtraContents() throws Exception { try (Repository bareRepo = createBareRepository()) { RepoProject repoProject = new RepoProject("subprojectX", "path/to", diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java index 20958a812c..0949d040e9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java @@ -11,14 +11,21 @@ package org.eclipse.jgit.gitrepo; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -138,6 +145,72 @@ public class ManifestParserTest { .collect(Collectors.toSet())); } + @Test + public void testPinProjectWithUpstream() throws Exception { + StringBuilder xmlContent = new StringBuilder(); + xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") + .append("<manifest>") + .append("<remote name=\"remote1\" fetch=\".\" />") + .append("<default revision=\"master\" remote=\"remote1\" />") + .append("<project path=\"foo\" name=\"pin-with-upstream\"") + .append(" revision=\"9b2fe85c0279f4d5ac69f07ddcd48566c3555405\"") + .append(" upstream=\"branchX\"/>") + .append("<project path=\"bar\" name=\"pin-without-upstream\"") + .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />") + .append("</manifest>"); + + ManifestParser parser = new ManifestParser(null, null, "master", + "https://git.google.com/", null, null); + parser.read(new ByteArrayInputStream( + xmlContent.toString().getBytes(UTF_8))); + + Map<String, RepoProject> repos = parser.getProjects().stream().collect( + Collectors.toMap(RepoProject::getName, Function.identity())); + assertEquals(2, repos.size()); + + RepoProject foo = repos.get("pin-with-upstream"); + assertEquals("pin-with-upstream", foo.getName()); + assertEquals("9b2fe85c0279f4d5ac69f07ddcd48566c3555405", + foo.getRevision()); + assertEquals("branchX", foo.getUpstream()); + + RepoProject bar = repos.get("pin-without-upstream"); + assertEquals("pin-without-upstream", bar.getName()); + assertEquals("76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0", + bar.getRevision()); + assertNull(bar.getUpstream()); + } + + @Test + public void testWithDestBranch() throws Exception { + StringBuilder xmlContent = new StringBuilder(); + xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") + .append("<manifest>") + .append("<remote name=\"remote1\" fetch=\".\" />") + .append("<default revision=\"master\" remote=\"remote1\" />") + .append("<project path=\"foo\" name=\"foo\"") + .append(" dest-branch=\"branchX\"/>") + .append("<project path=\"bar\" name=\"bar\"/>") + .append("</manifest>"); + + ManifestParser parser = new ManifestParser(null, null, "master", + "https://git.google.com/", null, null); + parser.read(new ByteArrayInputStream( + xmlContent.toString().getBytes(UTF_8))); + + Map<String, RepoProject> repos = parser.getProjects().stream().collect( + Collectors.toMap(RepoProject::getName, Function.identity())); + assertEquals(2, repos.size()); + + RepoProject foo = repos.get("foo"); + assertEquals("foo", foo.getName()); + assertEquals("branchX", foo.getDestBranch()); + + RepoProject bar = repos.get("bar"); + assertEquals("bar", bar.getName()); + assertNull(bar.getDestBranch()); + } + void testNormalize(String in, String want) { URI got = ManifestParser.normalizeEmptyPath(URI.create(in)); if (!got.toString().equals(want)) { @@ -152,4 +225,33 @@ public class ManifestParserTest { testNormalize("", ""); testNormalize("a/b", "a/b"); } + + @Test + public void testXXE() throws Exception { + File externalEntity = File.createTempFile("injected", "xml"); + externalEntity.deleteOnExit(); + Files.write(externalEntity.toPath(), + "<evil>injected xml</evil>" + .getBytes(UTF_8), + StandardOpenOption.WRITE); + String baseUrl = "https://git.google.com/"; + StringBuilder xmlContent = new StringBuilder(); + xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") + .append("<!DOCTYPE booo [ <!ENTITY foobar SYSTEM \"") + .append(externalEntity.getPath()).append("\"> ]>\n") + .append("<manifest>") + .append("<remote name=\"remote1\" fetch=\".\" />") + .append("<default revision=\"master\" remote=\"remote1\" />") + .append("&foobar;") + .append("<project path=\"foo\" name=\"foo\" groups=\"a,test\" />") + .append("</manifest>"); + + IOException e = assertThrows(IOException.class, + () -> new ManifestParser(null, null, "master", baseUrl, null, + null) + .read(new ByteArrayInputStream( + xmlContent.toString().getBytes(UTF_8)))); + assertTrue(e.getCause().getMessage().contains("DOCTYPE")); + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java index ca6f2e1053..3162e7910b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java @@ -1171,6 +1171,94 @@ public class RepoCommandTest extends RepositoryTestCase { } } + @Test + public void testRecordRemoteBranch_pinned() throws Exception { + Repository remoteDb = createBareRepository(); + Repository tempDb = createWorkRepository(); + + StringBuilder xmlContent = new StringBuilder(); + xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") + .append("<manifest>") + .append("<remote name=\"remote1\" fetch=\".\" />") + .append("<default revision=\"master\" remote=\"remote1\" />") + .append("<project path=\"pin-noupstream\"") + .append(" name=\"pin-noupstream\"") + .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />") + .append("<project path=\"pin-upstream\"") + .append(" name=\"pin-upstream\"") + .append(" upstream=\"branchX\"") + .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />") + .append("</manifest>"); + JGitTestUtil.writeTrashFile(tempDb, "manifest.xml", + xmlContent.toString()); + + RepoCommand command = new RepoCommand(remoteDb); + command.setPath( + tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml") + .setURI(rootUri).setRecordRemoteBranch(true).call(); + // Clone it + File directory = createTempDirectory("testBareRepo"); + try (Repository localDb = Git.cloneRepository().setDirectory(directory) + .setURI(remoteDb.getDirectory().toURI().toString()).call() + .getRepository();) { + // The .gitmodules file should exist + File gitmodules = new File(localDb.getWorkTree(), ".gitmodules"); + assertTrue("The .gitmodules file should exist", + gitmodules.exists()); + FileBasedConfig c = new FileBasedConfig(gitmodules, FS.DETECTED); + c.load(); + assertEquals("Pinned submodule with upstream records the ref", + "branchX", c.getString("submodule", "pin-upstream", "ref")); + assertNull("Pinned submodule without upstream don't have ref", + c.getString("submodule", "pin-noupstream", "ref")); + } + } + + @Test + public void testRecordRemoteBranch_pinned_nameConflict() throws Exception { + Repository remoteDb = createBareRepository(); + Repository tempDb = createWorkRepository(); + + StringBuilder xmlContent = new StringBuilder(); + xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") + .append("<manifest>") + .append("<remote name=\"remote1\" fetch=\".\" />") + .append("<default revision=\"master\" remote=\"remote1\" />") + .append("<project path=\"pin-upstream\"") + .append(" name=\"pin-upstream\"") + .append(" upstream=\"branchX\"") + .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />") + .append("<project path=\"pin-upstream-name-conflict\"") + .append(" name=\"pin-upstream\"") + .append(" upstream=\"branchX\"") + .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />") + .append("</manifest>"); + JGitTestUtil.writeTrashFile(tempDb, "manifest.xml", + xmlContent.toString()); + + RepoCommand command = new RepoCommand(remoteDb); + command.setPath( + tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml") + .setURI(rootUri).setRecordRemoteBranch(true).call(); + // Clone it + File directory = createTempDirectory("testBareRepo"); + try (Repository localDb = Git.cloneRepository().setDirectory(directory) + .setURI(remoteDb.getDirectory().toURI().toString()).call() + .getRepository();) { + // The .gitmodules file should exist + File gitmodules = new File(localDb.getWorkTree(), ".gitmodules"); + assertTrue("The .gitmodules file should exist", + gitmodules.exists()); + FileBasedConfig c = new FileBasedConfig(gitmodules, FS.DETECTED); + c.load(); + assertEquals("Upstream is preserved in name conflict", "branchX", + c.getString("submodule", "pin-upstream/pin-upstream", + "ref")); + assertEquals("Upstream is preserved in name conflict (other side)", + "branchX", c.getString("submodule", + "pin-upstream/pin-upstream-name-conflict", "ref")); + } + } @Test public void testRecordSubmoduleLabels() throws Exception { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java index 6112952549..6983eaa354 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java @@ -31,7 +31,7 @@ public class BasicRuleTest { assertFalse(rule1.getNegation()); assertTrue(rule3.getNegation()); assertNotEquals(rule1, null); - assertEquals(rule1, rule1); + assertTrue(rule1.equals(rule1)); assertEquals(rule1, rule2); assertNotEquals(rule1, rule3); assertNotEquals(rule1, rule4); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java index 05a953e081..ab08c99796 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java @@ -20,14 +20,18 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import org.eclipse.jgit.junit.MockSystemReader; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.After; @@ -698,6 +702,110 @@ public class IgnoreNodeTest extends RepositoryTestCase { } @Test + public void testUserGitIgnoreFound() throws IOException { + File homeDir = FS.DETECTED.userHome(); + Path userIgnore = homeDir.toPath().resolve(".config").resolve("git") + .resolve("ignore"); + Files.createDirectories(userIgnore.getParent()); + Files.writeString(userIgnore, "x"); + try { + writeTrashFile(".foo", ""); + writeTrashFile("a/x/file", ""); + writeTrashFile("b/x", ""); + writeTrashFile("x/file", ""); + + beginWalk(); + assertEntry(F, tracked, ".foo"); + assertEntry(D, tracked, "a"); + assertEntry(D, ignored, "a/x"); + assertEntry(F, ignored, "a/x/file"); + assertEntry(D, tracked, "b"); + assertEntry(F, ignored, "b/x"); + assertEntry(D, ignored, "x"); + assertEntry(F, ignored, "x/file"); + endWalk(); + } finally { + Files.deleteIfExists(userIgnore); + } + } + + @Test + public void testXdgIgnoreFound() throws IOException { + File tmp = getTemporaryDirectory(); + Path xdg = tmp.toPath().resolve("xdg"); + Path userIgnore = xdg.resolve("git").resolve("ignore"); + Files.createDirectories(userIgnore.getParent()); + Files.writeString(userIgnore, "x"); + SystemReader system = SystemReader.getInstance(); + assertTrue(system instanceof MockSystemReader); + ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME", + xdg.toAbsolutePath().toString()); + // Also create the one in the home directory -- it should not be active + File homeDir = FS.DETECTED.userHome(); + Path userIgnore2 = homeDir.toPath().resolve(".config").resolve("git") + .resolve("ignore"); + Files.createDirectories(userIgnore2.getParent()); + Files.writeString(userIgnore2, "a"); + try { + writeTrashFile(".foo", ""); + writeTrashFile("a/x/file", ""); + writeTrashFile("b/x", ""); + writeTrashFile("x/file", ""); + + beginWalk(); + assertEntry(F, tracked, ".foo"); + assertEntry(D, tracked, "a"); + assertEntry(D, ignored, "a/x"); + assertEntry(F, ignored, "a/x/file"); + assertEntry(D, tracked, "b"); + assertEntry(F, ignored, "b/x"); + assertEntry(D, ignored, "x"); + assertEntry(F, ignored, "x/file"); + endWalk(); + } finally { + ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME", null); + Files.deleteIfExists(userIgnore2); + } + } + + @Test + public void testXdgWrong() throws IOException { + File tmp = getTemporaryDirectory(); + Path xdg = tmp.toPath().resolve("xdg"); + SystemReader system = SystemReader.getInstance(); + assertTrue(system instanceof MockSystemReader); + // Valid value, but the directory doesn't exist + ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME", + xdg.toAbsolutePath().toString()); + // Also create the one in the home directory -- it should not be active + File homeDir = FS.DETECTED.userHome(); + Path userIgnore2 = homeDir.toPath().resolve(".config").resolve("git") + .resolve("ignore"); + Files.createDirectories(userIgnore2.getParent()); + Files.writeString(userIgnore2, "x"); + try { + writeTrashFile(".foo", ""); + writeTrashFile("a/x/file", ""); + writeTrashFile("b/x", ""); + writeTrashFile("x/file", ""); + + beginWalk(); + assertEntry(F, tracked, ".foo"); + assertEntry(D, tracked, "a"); + assertEntry(D, tracked, "a/x"); + assertEntry(F, tracked, "a/x/file"); + assertEntry(D, tracked, "b"); + assertEntry(F, tracked, "b/x"); + assertEntry(D, tracked, "x"); + assertEntry(F, tracked, "x/file"); + endWalk(); + } finally { + ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME", null); + Files.deleteIfExists(userIgnore2); + } + } + + @Test public void testToString() throws Exception { assertEquals(Arrays.asList("").toString(), new IgnoreNode().toString()); assertEquals(Arrays.asList("hello").toString(), diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java index d02bfcd3f6..1119db3712 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java @@ -132,7 +132,8 @@ public class IndexDiffWithSymlinkTest extends LocalDiskRepositoryTestCase { Writer writer = new OutputStreamWriter(out, UTF_8)) { writer.write("echo `which git` 1>&2\n"); writer.write("echo `git --version` 1>&2\n"); - writer.write("git init " + name + " && \\\n"); + writer.write("git -c init.defaultBranch=master init " + name + + " && \\\n"); writer.write("cd ./" + name + " && \\\n"); writer.write("git fast-import < ../" + name + ".txt && \\\n"); writer.write("git checkout -f\n"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java index 37ff40bdf7..0e73588c66 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java @@ -51,7 +51,6 @@ public abstract class ObjectReachabilityTestCase } } - /** {@inheritDoc} */ @Override @Before public void setUp() throws Exception { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java index 7679c11098..eeb13cc8b9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java @@ -32,7 +32,6 @@ public abstract class ReachabilityCheckerTestCase TestRepository<FileRepository> repo; - /** {@inheritDoc} */ @Override @Before public void setUp() throws Exception { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java index 97976564d8..4d05360252 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java @@ -10,24 +10,31 @@ package org.eclipse.jgit.internal.storage.commitgraph; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.lib.Constants.COMMIT_GENERATION_UNKNOWN; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.storage.file.FileRepository; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.junit.Before; import org.junit.Test; @@ -45,6 +52,7 @@ public class CommitGraphTest extends RepositoryTestCase { public void setUp() throws Exception { super.setUp(); tr = new TestRepository<>(db, new RevWalk(db), mockSystemReader); + mockSystemReader.setJGitConfig(new MockConfig()); } @Test @@ -196,11 +204,32 @@ public class CommitGraphTest extends RepositoryTestCase { assertEquals(getGenerationNumber(c8), 5); } + @Test + public void testGraphComputeChangedPaths() throws Exception { + RevCommit a = tr.commit(tr.tree(tr.file("d/f", tr.blob("a")))); + RevCommit b = tr.commit(tr.tree(tr.file("d/f", tr.blob("a"))), a); + RevCommit c = tr.commit(tr.tree(tr.file("d/f", tr.blob("b"))), b); + + writeAndReadCommitGraph(Collections.singleton(c)); + ChangedPathFilter acpf = commitGraph + .getChangedPathFilter(commitGraph.findGraphPosition(a)); + assertTrue(acpf.maybeContains("d".getBytes(UTF_8))); + assertTrue(acpf.maybeContains("d/f".getBytes(UTF_8))); + ChangedPathFilter bcpf = commitGraph + .getChangedPathFilter(commitGraph.findGraphPosition(b)); + assertFalse(bcpf.maybeContains("d".getBytes(UTF_8))); + assertFalse(bcpf.maybeContains("d/f".getBytes(UTF_8))); + ChangedPathFilter ccpf = commitGraph + .getChangedPathFilter(commitGraph.findGraphPosition(c)); + assertTrue(ccpf.maybeContains("d".getBytes(UTF_8))); + assertTrue(ccpf.maybeContains("d/f".getBytes(UTF_8))); + } + void writeAndReadCommitGraph(Set<ObjectId> wants) throws Exception { NullProgressMonitor m = NullProgressMonitor.INSTANCE; try (RevWalk walk = new RevWalk(db)) { CommitGraphWriter writer = new CommitGraphWriter( - GraphCommits.fromWalk(m, wants, walk)); + GraphCommits.fromWalk(m, wants, walk), true); ByteArrayOutputStream os = new ByteArrayOutputStream(); writer.write(m, os); InputStream inputStream = new ByteArrayInputStream( @@ -252,4 +281,41 @@ public class CommitGraphTest extends RepositoryTestCase { RevCommit commit(RevCommit... parents) throws Exception { return tr.commit(parents); } + + private static final class MockConfig extends FileBasedConfig { + private MockConfig() { + super(null, null); + } + + @Override + public void load() throws IOException, ConfigInvalidException { + // Do nothing + } + + @Override + public void save() throws IOException { + // Do nothing + } + + @Override + public boolean isOutdated() { + return false; + } + + @Override + public String toString() { + return "MockConfig"; + } + + @Override + public boolean getBoolean(final String section, final String name, + final boolean defaultValue) { + if (section.equals(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION) + && name.equals( + ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS)) { + return true; + } + return defaultValue; + } + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java index 6c5e5e5605..80a0f0cea5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java @@ -10,21 +10,38 @@ package org.eclipse.jgit.internal.storage.commitgraph; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; import java.util.Set; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.internal.storage.file.GC; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.NB; import org.junit.Before; import org.junit.Test; @@ -46,6 +63,7 @@ public class CommitGraphWriterTest extends RepositoryTestCase { os = new ByteArrayOutputStream(); tr = new TestRepository<>(db, new RevWalk(db), mockSystemReader); walk = new RevWalk(db); + mockSystemReader.setJGitConfig(new MockConfig()); } @Test @@ -68,7 +86,7 @@ public class CommitGraphWriterTest extends RepositoryTestCase { Set<ObjectId> wants = Collections.singleton(tip); NullProgressMonitor m = NullProgressMonitor.INSTANCE; GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); - writer = new CommitGraphWriter(graphCommits); + writer = new CommitGraphWriter(graphCommits, true); writer.write(m, os); assertEquals(5, graphCommits.size()); @@ -76,11 +94,20 @@ public class CommitGraphWriterTest extends RepositoryTestCase { assertTrue(data.length > 0); byte[] headers = new byte[8]; System.arraycopy(data, 0, headers, 0, 8); - assertArrayEquals(new byte[] {'C', 'G', 'P', 'H', 1, 1, 4, 0}, headers); - assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT, NB.decodeInt32(data, 8)); - assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP, NB.decodeInt32(data, 20)); - assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA, NB.decodeInt32(data, 32)); - assertEquals(CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST, NB.decodeInt32(data, 44)); + assertArrayEquals(new byte[] { 'C', 'G', 'P', 'H', 1, 1, 6, 0 }, + headers); + assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT, + NB.decodeInt32(data, 8)); + assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP, + NB.decodeInt32(data, 20)); + assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA, + NB.decodeInt32(data, 32)); + assertEquals(CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST, + NB.decodeInt32(data, 44)); + assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX, + NB.decodeInt32(data, 56)); + assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA, + NB.decodeInt32(data, 68)); } @Test @@ -93,7 +120,7 @@ public class CommitGraphWriterTest extends RepositoryTestCase { Set<ObjectId> wants = Collections.singleton(tip); NullProgressMonitor m = NullProgressMonitor.INSTANCE; GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); - writer = new CommitGraphWriter(graphCommits); + writer = new CommitGraphWriter(graphCommits, true); writer.write(m, os); assertEquals(4, graphCommits.size()); @@ -101,13 +128,355 @@ public class CommitGraphWriterTest extends RepositoryTestCase { assertTrue(data.length > 0); byte[] headers = new byte[8]; System.arraycopy(data, 0, headers, 0, 8); - assertArrayEquals(new byte[] {'C', 'G', 'P', 'H', 1, 1, 3, 0}, headers); - assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT, NB.decodeInt32(data, 8)); - assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP, NB.decodeInt32(data, 20)); - assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA, NB.decodeInt32(data, 32)); + assertArrayEquals(new byte[] { 'C', 'G', 'P', 'H', 1, 1, 5, 0 }, + headers); + assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT, + NB.decodeInt32(data, 8)); + assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP, + NB.decodeInt32(data, 20)); + assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA, + NB.decodeInt32(data, 32)); + assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX, + NB.decodeInt32(data, 44)); + assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA, + NB.decodeInt32(data, 56)); + } + + @Test + public void testProgressMonitor() throws Exception { + RevCommit root = commit(); + RevCommit a = commit(root); + RevCommit b = commit(root); + RevCommit tip = commit(a, b); + Set<ObjectId> wants = Collections.singleton(tip); + + NonNestedTasksProgressMonitor nonNested = new NonNestedTasksProgressMonitor(); + GraphCommits graphCommits = GraphCommits.fromWalk(nonNested, wants, + walk); + writer = new CommitGraphWriter(graphCommits, true); + writer.write(nonNested, os); + } + + private static class NonNestedTasksProgressMonitor + implements ProgressMonitor { + + boolean inTask; + + @Override + public void start(int totalTasks) { + // empty + } + + @Override + public void beginTask(String title, int totalWork) { + assertFalse("Previous monitoring task is not closed", inTask); + inTask = true; + } + + @Override + public void update(int completed) { + // empty + } + + @Override + public void endTask() { + assertTrue("Closing task that wasn't started", inTask); + inTask = false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void showDuration(boolean enabled) { + // empty + } + } + + static HashSet<String> changedPathStrings(byte[] data) { + int oidf_offset = -1; + int bidx_offset = -1; + int bdat_offset = -1; + for (int i = 8; i < data.length - 4; i += 12) { + switch (NB.decodeInt32(data, i)) { + case CommitGraphConstants.CHUNK_ID_OID_FANOUT: + oidf_offset = (int) NB.decodeInt64(data, i + 4); + break; + case CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX: + bidx_offset = (int) NB.decodeInt64(data, i + 4); + break; + case CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA: + bdat_offset = (int) NB.decodeInt64(data, i + 4); + break; + } + } + assertTrue(oidf_offset > 0); + assertTrue(bidx_offset > 0); + assertTrue(bdat_offset > 0); + bdat_offset += 12; // skip version, hash count, bits per entry + int commit_count = NB.decodeInt32(data, oidf_offset + 255 * 4); + int[] changed_path_length_cumuls = new int[commit_count]; + for (int i = 0; i < commit_count; i++) { + changed_path_length_cumuls[i] = NB.decodeInt32(data, + bidx_offset + i * 4); + } + HashSet<String> changed_paths = new HashSet<>(); + for (int i = 0; i < commit_count; i++) { + int prior_cumul = i == 0 ? 0 : changed_path_length_cumuls[i - 1]; + String changed_path = ""; + for (int j = prior_cumul; j < changed_path_length_cumuls[i]; j++) { + changed_path += data[bdat_offset + j] + ","; + } + changed_paths.add(changed_path); + } + return changed_paths; + } + + /** + * Expected value generated using the following: + * + * <pre> + * # apply into git-repo: https://lore.kernel.org/git/cover.1684790529.git.jonathantanmy@google.com/ + * (cd git-repo; make) + * git-repo/bin-wrappers/git init tested + * (cd tested; touch foo.txt; mkdir -p onedir/twodir; touch onedir/twodir/bar.txt) + * git-repo/bin-wrappers/git -C tested add foo.txt onedir + * git-repo/bin-wrappers/git -C tested commit -m first_commit + * (cd tested; mv foo.txt foo-new.txt; mv onedir/twodir/bar.txt onedir/twodir/bar-new.txt) + * git-repo/bin-wrappers/git -C tested add foo-new.txt onedir + * git-repo/bin-wrappers/git -C tested commit -a -m second_commit + * git-repo/bin-wrappers/git -C tested maintenance run + * git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths + * (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph) + * </pre> + * + * @throws Exception + */ + @Test + public void testChangedPathFilterRootAndNested() throws Exception { + RevBlob emptyBlob = tr.blob(new byte[] {}); + RevCommit root = tr.commit(tr.tree(tr.file("foo.txt", emptyBlob), + tr.file("onedir/twodir/bar.txt", emptyBlob))); + RevCommit tip = tr.commit(tr.tree(tr.file("foo-new.txt", emptyBlob), + tr.file("onedir/twodir/bar-new.txt", emptyBlob)), root); + + Set<ObjectId> wants = Collections.singleton(tip); + NullProgressMonitor m = NullProgressMonitor.INSTANCE; + GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); + writer = new CommitGraphWriter(graphCommits, true); + writer.write(m, os); + + HashSet<String> changedPaths = changedPathStrings(os.toByteArray()); + assertThat(changedPaths, containsInAnyOrder( + "109,-33,2,60,20,79,-11,116,", + "119,69,63,-8,0,")); + } + + /** + * Expected value generated using the following: + * + * <pre> + * git -C git-repo checkout todo get version number when it is merged + * (cd git-repo; make) + * git-repo/bin-wrappers/git init tested + * (cd tested; mkdir -p onedir/twodir; touch onedir/twodir/a.txt; touch onedir/twodir/b.txt) + * git-repo/bin-wrappers/git -C tested add onedir + * git-repo/bin-wrappers/git -C tested commit -m first_commit + * (cd tested; mv onedir/twodir/a.txt onedir/twodir/c.txt; mv onedir/twodir/b.txt onedir/twodir/d.txt) + * git-repo/bin-wrappers/git -C tested add onedir + * git-repo/bin-wrappers/git -C tested commit -a -m second_commit + * git-repo/bin-wrappers/git -C tested maintenance run + * git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths + * (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph) + * </pre> + * + * @throws Exception + */ + @Test + public void testChangedPathFilterOverlappingNested() throws Exception { + RevBlob emptyBlob = tr.blob(new byte[] {}); + RevCommit root = tr + .commit(tr.tree(tr.file("onedir/twodir/a.txt", emptyBlob), + tr.file("onedir/twodir/b.txt", emptyBlob))); + RevCommit tip = tr + .commit(tr.tree(tr.file("onedir/twodir/c.txt", emptyBlob), + tr.file("onedir/twodir/d.txt", emptyBlob)), root); + + Set<ObjectId> wants = Collections.singleton(tip); + NullProgressMonitor m = NullProgressMonitor.INSTANCE; + GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); + writer = new CommitGraphWriter(graphCommits, true); + writer.write(m, os); + + HashSet<String> changedPaths = changedPathStrings(os.toByteArray()); + assertThat(changedPaths, containsInAnyOrder("61,30,23,-24,1,", + "-58,-51,-46,60,29,-121,113,90,")); + } + + /** + * Expected value generated using the following: + * + * <pre> + * git -C git-repo checkout todo get version number when it is merged + * (cd git-repo; make) + * git-repo/bin-wrappers/git init tested + * (cd tested; touch ä½ å¥½) + * git-repo/bin-wrappers/git -C tested add ä½ å¥½ + * git-repo/bin-wrappers/git -C tested commit -m first_commit + * git-repo/bin-wrappers/git -C tested maintenance run + * git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths + * (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph) + * </pre> + * + * @throws Exception + */ + @Test + public void testChangedPathFilterHighBit() throws Exception { + RevBlob emptyBlob = tr.blob(new byte[] {}); + // tr.file encodes using UTF-8 + RevCommit root = tr.commit(tr.tree(tr.file("ä½ å¥½", emptyBlob))); + + Set<ObjectId> wants = Collections.singleton(root); + NullProgressMonitor m = NullProgressMonitor.INSTANCE; + GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); + writer = new CommitGraphWriter(graphCommits, true); + writer.write(m, os); + + HashSet<String> changedPaths = changedPathStrings(os.toByteArray()); + assertThat(changedPaths, containsInAnyOrder("16,16,")); + } + + @Test + public void testChangedPathFilterEmptyChange() throws Exception { + RevCommit root = commit(); + + Set<ObjectId> wants = Collections.singleton(root); + NullProgressMonitor m = NullProgressMonitor.INSTANCE; + GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); + writer = new CommitGraphWriter(graphCommits, true); + writer.write(m, os); + + HashSet<String> changedPaths = changedPathStrings(os.toByteArray()); + assertThat(changedPaths, containsInAnyOrder("0,")); + } + + @Test + public void testChangedPathFilterManyChanges() throws Exception { + RevBlob emptyBlob = tr.blob(new byte[] {}); + DirCacheEntry[] entries = new DirCacheEntry[513]; + for (int i = 0; i < entries.length; i++) { + entries[i] = tr.file(i + ".txt", emptyBlob); + } + + RevCommit root = tr.commit(tr.tree(entries)); + + Set<ObjectId> wants = Collections.singleton(root); + NullProgressMonitor m = NullProgressMonitor.INSTANCE; + GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); + writer = new CommitGraphWriter(graphCommits, true); + writer.write(m, os); + + HashSet<String> changedPaths = changedPathStrings(os.toByteArray()); + assertThat(changedPaths, containsInAnyOrder("-1,")); + } + + @Test + public void testReuseBloomFilters() throws Exception { + RevBlob emptyBlob = tr.blob(new byte[] {}); + RevCommit root = tr.commit(tr.tree(tr.file("foo.txt", emptyBlob), + tr.file("onedir/twodir/bar.txt", emptyBlob))); + tr.branch("master").update(root); + + db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_COMMIT_GRAPH, true); + db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null, + ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true); + db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null, + ConfigConstants.CONFIG_KEY_WRITE_CHANGED_PATHS, true); + GC gc = new GC(db); + gc.gc().get(); + + RevCommit tip = tr.commit(tr.tree(tr.file("foo-new.txt", emptyBlob), + tr.file("onedir/twodir/bar-new.txt", emptyBlob)), root); + + Set<ObjectId> wants = Collections.singleton(tip); + NullProgressMonitor m = NullProgressMonitor.INSTANCE; + GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk); + writer = new CommitGraphWriter(graphCommits, true); + CommitGraphWriter.Stats stats = writer.write(m, os); + + assertEquals(1, stats.getChangedPathFiltersReused()); + assertEquals(1, stats.getChangedPathFiltersComputed()); + + // Expected strings are the same as in + // #testChangedPathFilterRootAndNested + HashSet<String> changedPaths = changedPathStrings(os.toByteArray()); + assertThat(changedPaths, containsInAnyOrder( + "109,-33,2,60,20,79,-11,116,", + "119,69,63,-8,0,")); + } + + @Test + public void testPathDiffCalculator_skipUnchangedTree() throws Exception { + RevCommit root = tr.commit(tr.tree( + tr.file("d/sd1/f1", tr.blob("f1")), + tr.file("d/sd2/f2", tr.blob("f2")))); + RevCommit tip = tr.commit(tr.tree( + tr.file("d/sd1/f1", tr.blob("f1")), + tr.file("d/sd2/f2", tr.blob("f2B"))), root); + CommitGraphWriter.PathDiffCalculator c = new CommitGraphWriter.PathDiffCalculator(); + + Optional<HashSet<ByteBuffer>> byteBuffers = c.changedPaths(walk.getObjectReader(), tip); + + assertTrue(byteBuffers.isPresent()); + List<String> asString = byteBuffers.get().stream() + .map(b -> StandardCharsets.UTF_8.decode(b).toString()) + .collect(toList()); + assertThat(asString, containsInAnyOrder("d", "d/sd2", "d/sd2/f2")); + // We don't walk into d/sd1/f1 + assertEquals(1, c.stepCounter); } RevCommit commit(RevCommit... parents) throws Exception { return tr.commit(parents); } -} + + private static final class MockConfig extends FileBasedConfig { + private MockConfig() { + super(null, null); + } + + @Override + public void load() throws IOException, ConfigInvalidException { + // Do nothing + } + + @Override + public void save() throws IOException { + // Do nothing + } + + @Override + public boolean isOutdated() { + return false; + } + + @Override + public String toString() { + return "MockConfig"; + } + + @Override + public boolean getBoolean(final String section, final String name, + final boolean defaultValue) { + if (section.equals(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION) + && name.equals( + ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS)) { + return true; + } + return defaultValue; + } + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndexTest.java new file mode 100644 index 0000000000..b533d5c985 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndexTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.commitgraph; + +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class GraphObjectIndexTest { + + @Test + public void findGraphPosition_noObjInBucket() throws CommitGraphFormatException { + GraphObjectIndex idx = new GraphObjectIndex(100, + new byte[256 * 4], new byte[] {}); + int graphPosition = idx.findGraphPosition( + ObjectId.fromString("731dfd4c5eb6f88b98e983b9b0551b3562a0c46c")); + assertEquals(-1, graphPosition); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java new file mode 100644 index 0000000000..2c4b432a01 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2024, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertArrayEquals; + +import java.util.List; + +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.junit.Test; + +public class AggregatedBlockCacheStatsTest { + @Test + public void getName() { + BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats + .fromStatsList(List.of()); + + assertThat(aggregatedBlockCacheStats.getName(), + equalTo(AggregatedBlockCacheStats.class.getName())); + } + + @Test + public void getCurrentSize_aggregatesCurrentSizes() { + long[] currentSizes = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + packStats.addToLiveBytes(new TestKey(PackExt.PACK), 5); + currentSizes[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + bitmapStats.addToLiveBytes(new TestKey(PackExt.BITMAP_INDEX), 6); + currentSizes[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + indexStats.addToLiveBytes(new TestKey(PackExt.INDEX), 7); + currentSizes[PackExt.INDEX.getPosition()] = 7; + + BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats + .fromStatsList(List.of(packStats, bitmapStats, indexStats)); + + assertArrayEquals(aggregatedBlockCacheStats.getCurrentSize(), + currentSizes); + } + + @Test + public void getHitCount_aggregatesHitCounts() { + long[] hitCounts = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementHit(new TestKey(PackExt.PACK))); + hitCounts[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> bitmapStats + .incrementHit(new TestKey(PackExt.BITMAP_INDEX))); + hitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementHit(new TestKey(PackExt.INDEX))); + hitCounts[PackExt.INDEX.getPosition()] = 7; + + BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats + .fromStatsList(List.of(packStats, bitmapStats, indexStats)); + + assertArrayEquals(aggregatedBlockCacheStats.getHitCount(), hitCounts); + } + + @Test + public void getMissCount_aggregatesMissCounts() { + long[] missCounts = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementMiss(new TestKey(PackExt.PACK))); + missCounts[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> bitmapStats + .incrementMiss(new TestKey(PackExt.BITMAP_INDEX))); + missCounts[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX))); + missCounts[PackExt.INDEX.getPosition()] = 7; + + BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats + .fromStatsList(List.of(packStats, bitmapStats, indexStats)); + + assertArrayEquals(aggregatedBlockCacheStats.getMissCount(), missCounts); + } + + @Test + public void getTotalRequestCount_aggregatesRequestCounts() { + long[] totalRequestCounts = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, () -> { + packStats.incrementHit(new TestKey(PackExt.PACK)); + packStats.incrementMiss(new TestKey(PackExt.PACK)); + }); + totalRequestCounts[PackExt.PACK.getPosition()] = 10; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> { + bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX)); + bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX)); + }); + totalRequestCounts[PackExt.BITMAP_INDEX.getPosition()] = 12; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, () -> { + indexStats.incrementHit(new TestKey(PackExt.INDEX)); + indexStats.incrementMiss(new TestKey(PackExt.INDEX)); + }); + totalRequestCounts[PackExt.INDEX.getPosition()] = 14; + + BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats + .fromStatsList(List.of(packStats, bitmapStats, indexStats)); + + assertArrayEquals(aggregatedBlockCacheStats.getTotalRequestCount(), + totalRequestCounts); + } + + @Test + public void getHitRatio_aggregatesHitRatios() { + long[] hitRatios = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementHit(new TestKey(PackExt.PACK))); + hitRatios[PackExt.PACK.getPosition()] = 100; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> { + bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX)); + bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX)); + }); + hitRatios[PackExt.BITMAP_INDEX.getPosition()] = 50; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX))); + hitRatios[PackExt.INDEX.getPosition()] = 0; + + BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats + .fromStatsList(List.of(packStats, bitmapStats, indexStats)); + + assertArrayEquals(aggregatedBlockCacheStats.getHitRatio(), hitRatios); + } + + @Test + public void getEvictions_aggregatesEvictions() { + long[] evictions = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementEvict(new TestKey(PackExt.PACK))); + evictions[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> bitmapStats + .incrementEvict(new TestKey(PackExt.BITMAP_INDEX))); + evictions[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementEvict(new TestKey(PackExt.INDEX))); + evictions[PackExt.INDEX.getPosition()] = 7; + + BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats + .fromStatsList(List.of(packStats, bitmapStats, indexStats)); + + assertArrayEquals(aggregatedBlockCacheStats.getEvictions(), evictions); + } + + private static void incrementCounter(int amount, Runnable fn) { + for (int i = 0; i < amount; i++) { + fn.run(); + } + } + + private static long[] createEmptyStatsArray() { + return new long[PackExt.values().length]; + } + + private static class TestKey extends DfsStreamKey { + TestKey(PackExt packExt) { + super(0, packExt); + } + + @Override + public boolean equals(Object o) { + return false; + } + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java new file mode 100644 index 0000000000..2e2f86bf80 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java @@ -0,0 +1,67 @@ +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME; +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.isA; + +import java.util.List; + +import org.junit.Test; + +public class ClockBlockCacheTableTest { + private static final String NAME = "name"; + + @Test + public void getName_nameNotConfigured_returnsDefaultName() { + ClockBlockCacheTable cacheTable = new ClockBlockCacheTable( + createBlockCacheConfig()); + + assertThat(cacheTable.getName(), equalTo(DEFAULT_NAME)); + } + + @Test + public void getName_nameConfigured_returnsConfiguredName() { + ClockBlockCacheTable cacheTable = new ClockBlockCacheTable( + createBlockCacheConfig().setName(NAME)); + + assertThat(cacheTable.getName(), equalTo(NAME)); + } + + @Test + public void getBlockCacheStats_nameNotConfigured_returnsBlockCacheStatsWithDefaultName() { + ClockBlockCacheTable cacheTable = new ClockBlockCacheTable( + createBlockCacheConfig()); + + assertThat(cacheTable.getBlockCacheStats(), hasSize(1)); + assertThat(cacheTable.getBlockCacheStats().get(0).getName(), + equalTo(DEFAULT_NAME)); + } + + @Test + public void getBlockCacheStats_nameConfigured_returnsBlockCacheStatsWithConfiguredName() { + ClockBlockCacheTable cacheTable = new ClockBlockCacheTable( + createBlockCacheConfig().setName(NAME)); + + assertThat(cacheTable.getBlockCacheStats(), hasSize(1)); + assertThat(cacheTable.getBlockCacheStats().get(0).getName(), + equalTo(NAME)); + } + + @Test + public void getAllBlockCacheStats() { + ClockBlockCacheTable cacheTable = new ClockBlockCacheTable( + createBlockCacheConfig()); + + List<BlockCacheStats> blockCacheStats = cacheTable.getBlockCacheStats(); + assertThat(blockCacheStats, contains(isA(BlockCacheStats.class))); + } + + private static DfsBlockCacheConfig createBlockCacheConfig() { + return new DfsBlockCacheConfig().setBlockSize(512) + .setConcurrencyLevel(4).setBlockLimit(1024); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java index 2df0ba1b05..afa3179cde 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java @@ -38,13 +38,37 @@ package org.eclipse.jgit.internal.storage.dfs; +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_CACHE_PREFIX; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_LIMIT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_SIZE; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CONCURRENCY_LEVEL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_EXTENSIONS; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThrows; +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig; +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.lib.Config; import org.junit.Test; +@SuppressWarnings("boxing") public class DfsBlockCacheConfigTest { @Test @@ -55,7 +79,6 @@ public class DfsBlockCacheConfigTest { } @Test - @SuppressWarnings("boxing") public void negativeBlockSizeIsConvertedToDefault() { DfsBlockCacheConfig config = new DfsBlockCacheConfig(); config.setBlockSize(-1); @@ -64,7 +87,6 @@ public class DfsBlockCacheConfigTest { } @Test - @SuppressWarnings("boxing") public void tooSmallBlockSizeIsConvertedToDefault() { DfsBlockCacheConfig config = new DfsBlockCacheConfig(); config.setBlockSize(10); @@ -73,11 +95,295 @@ public class DfsBlockCacheConfigTest { } @Test - @SuppressWarnings("boxing") public void validBlockSize() { DfsBlockCacheConfig config = new DfsBlockCacheConfig(); config.setBlockSize(65536); assertThat(config.getBlockSize(), is(65536)); } + + @Test + public void fromConfigs() { + Config config = new Config(); + config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_BLOCK_LIMIT, 50 * 1024); + config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_BLOCK_SIZE, 1024); + config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_CONCURRENCY_LEVEL, 3); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_STREAM_RATIO, "0.5"); + + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig() + .fromConfig(config); + assertThat(cacheConfig.getBlockLimit(), is(50L * 1024L)); + assertThat(cacheConfig.getBlockSize(), is(1024)); + assertThat(cacheConfig.getConcurrencyLevel(), is(3)); + assertThat(cacheConfig.getStreamRatio(), closeTo(0.5, 0.0001)); + } + + @Test + public void fromConfig_blockLimitNotAMultipleOfBlockSize_throws() { + Config config = new Config(); + config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_BLOCK_LIMIT, 1025); + config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_BLOCK_SIZE, 1024); + + assertThrows(IllegalArgumentException.class, + () -> new DfsBlockCacheConfig().fromConfig(config)); + } + + @Test + public void fromConfig_streamRatioInvalidFormat_throws() { + Config config = new Config(); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_STREAM_RATIO, "0.a5"); + + assertThrows(IllegalArgumentException.class, + () -> new DfsBlockCacheConfig().fromConfig(config)); + } + + @Test + public void fromConfig_generatesDfsBlockCachePackExtConfigs() { + Config config = new Config(); + addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK), + /* blockLimit= */ 20 * 512, /* blockSize= */ 512); + + addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX), + /* blockLimit= */ 25 * 1024, /* blockSize= */ 1024); + + addPackExtConfigEntry(config, "index", + List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX, + PackExt.REVERSE_INDEX), + /* blockLimit= */ 30 * 1024, /* blockSize= */ 1024); + + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig() + .fromConfig(config); + var configs = cacheConfig.getPackExtCacheConfigurations(); + assertThat(configs, hasSize(3)); + var packConfig = getConfigForExt(configs, PackExt.PACK); + assertThat(packConfig.getBlockLimit(), is(20L * 512L)); + assertThat(packConfig.getBlockSize(), is(512)); + + var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX); + assertThat(bitmapConfig.getBlockLimit(), is(25L * 1024L)); + assertThat(bitmapConfig.getBlockSize(), is(1024)); + + var indexConfig = getConfigForExt(configs, PackExt.INDEX); + assertThat(indexConfig.getBlockLimit(), is(30L * 1024L)); + assertThat(indexConfig.getBlockSize(), is(1024)); + assertThat(getConfigForExt(configs, PackExt.OBJECT_SIZE_INDEX), + is(indexConfig)); + assertThat(getConfigForExt(configs, PackExt.REVERSE_INDEX), + is(indexConfig)); + } + + @Test + public void fromConfig_withExistingCacheHotMap_configWithPackExtConfigsHasHotMaps() { + Config config = new Config(); + addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK), + /* blockLimit= */ 20 * 512, /* blockSize= */ 512); + + addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX), + /* blockLimit= */ 25 * 1024, /* blockSize= */ 1024); + + addPackExtConfigEntry(config, "index", + List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX, + PackExt.REVERSE_INDEX), + /* blockLimit= */ 30 * 1024, /* blockSize= */ 1024); + + Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1, + PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4); + + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig(); + cacheConfig.setCacheHotMap(cacheHotMap); + cacheConfig.fromConfig(config); + + var configs = cacheConfig.getPackExtCacheConfigurations(); + assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap)); + assertThat(configs, hasSize(3)); + var packConfig = getConfigForExt(configs, PackExt.PACK); + assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1))); + + var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX); + assertThat(bitmapConfig.getCacheHotMap(), + is(Map.of(PackExt.BITMAP_INDEX, 2))); + + var indexConfig = getConfigForExt(configs, PackExt.INDEX); + assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3))); + } + + @Test + public void setCacheHotMap_configWithPackExtConfigs_setsHotMaps() { + Config config = new Config(); + addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK), + /* blockLimit= */ 20 * 512, /* blockSize= */ 512); + + addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX), + /* blockLimit= */ 25 * 1024, /* blockSize= */ 1024); + + addPackExtConfigEntry(config, "index", + List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX, + PackExt.REVERSE_INDEX), + /* blockLimit= */ 30 * 1024, /* blockSize= */ 1024); + + Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1, + PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4); + + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig() + .fromConfig(config); + cacheConfig.setCacheHotMap(cacheHotMap); + + var configs = cacheConfig.getPackExtCacheConfigurations(); + assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap)); + assertThat(configs, hasSize(3)); + var packConfig = getConfigForExt(configs, PackExt.PACK); + assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1))); + + var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX); + assertThat(bitmapConfig.getCacheHotMap(), + is(Map.of(PackExt.BITMAP_INDEX, 2))); + + var indexConfig = getConfigForExt(configs, PackExt.INDEX); + assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3))); + } + + @Test + public void fromConfigs_baseConfigOnly_nameSetFromConfigDfsSubSection() { + Config config = new Config(); + + DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig() + .fromConfig(config); + assertThat(blockCacheConfig.getName(), equalTo(DEFAULT_NAME)); + } + + @Test + public void fromConfigs_namesSetFromConfigDfsCachePrefixSubSections() { + Config config = new Config(); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_STREAM_RATIO, "0.5"); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name1", + CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name()); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name2", + CONFIG_KEY_PACK_EXTENSIONS, PackExt.BITMAP_INDEX.name()); + + DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig() + .fromConfig(config); + assertThat(blockCacheConfig.getName(), equalTo("dfs")); + assertThat( + blockCacheConfig.getPackExtCacheConfigurations().get(0) + .getPackExtCacheConfiguration().getName(), + equalTo("dfs.name1")); + assertThat( + blockCacheConfig.getPackExtCacheConfigurations().get(1) + .getPackExtCacheConfiguration().getName(), + equalTo("dfs.name2")); + } + + @Test + public void fromConfigs_dfsBlockCachePackExtConfigWithDuplicateExtensions_throws() { + Config config = new Config(); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1", + CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name()); + + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack2", + CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name()); + + assertThrows(IllegalArgumentException.class, + () -> new DfsBlockCacheConfig().fromConfig(config)); + } + + @Test + public void fromConfigs_dfsBlockCachePackExtConfigWithEmptyExtensions_throws() { + Config config = new Config(); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1", + CONFIG_KEY_PACK_EXTENSIONS, ""); + + assertThrows(IllegalArgumentException.class, + () -> new DfsBlockCacheConfig().fromConfig(config)); + } + + @Test + public void fromConfigs_dfsBlockCachePackExtConfigWithNoExtensions_throws() { + Config config = new Config(); + config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1", + CONFIG_KEY_BLOCK_SIZE, 0); + + assertThrows(IllegalArgumentException.class, + () -> new DfsBlockCacheConfig().fromConfig(config)); + } + + @Test + public void fromConfigs_dfsBlockCachePackExtConfigWithUnknownExtensions_throws() { + Config config = new Config(); + config.setString(CONFIG_CORE_SECTION, + CONFIG_DFS_CACHE_PREFIX + "unknownExt", + CONFIG_KEY_PACK_EXTENSIONS, "NotAKnownExt"); + + assertThrows(IllegalArgumentException.class, + () -> new DfsBlockCacheConfig().fromConfig(config)); + } + + @Test + public void writeConfigurationDebug_writesConfigsToWriter() + throws Exception { + Config config = new Config(); + config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_BLOCK_LIMIT, 50 * 1024); + config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_BLOCK_SIZE, 1024); + config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_CONCURRENCY_LEVEL, 3); + config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_STREAM_RATIO, "0.5"); + addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK), + /* blockLimit= */ 20 * 512, /* blockSize= */ 512); + + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig() + .fromConfig(config); + Map<PackExt, Integer> hotmap = Map.of(PackExt.PACK, 10); + cacheConfig.setCacheHotMap(hotmap); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + cacheConfig.print(new PrintWriter(byteArrayOutputStream, true, + StandardCharsets.UTF_8)); + + String writenConfig = byteArrayOutputStream + .toString(StandardCharsets.UTF_8); + + List<String> writenLines = Arrays.asList(writenConfig.split("\n")); + assertThat(writenLines, + equalTo(List.of("Name: dfs", " BlockLimit: " + (50 * 1024), + " BlockSize: 1024", " StreamRatio: 0.5", + " ConcurrencyLevel: 3", + " CacheHotMapEntry: " + PackExt.PACK + " : " + 10, + " Name: dfs.pack", " BlockLimit: " + 20 * 512, + " BlockSize: 512", " StreamRatio: 0.3", + " ConcurrencyLevel: 32", + " CacheHotMapEntry: " + PackExt.PACK + " : " + 10, + " PackExts: " + List.of(PackExt.PACK)))); + } + + private static void addPackExtConfigEntry(Config config, String configName, + List<PackExt> packExts, long blockLimit, int blockSize) { + String packExtConfigName = CONFIG_DFS_CACHE_PREFIX + configName; + config.setString(CONFIG_CORE_SECTION, packExtConfigName, + CONFIG_KEY_PACK_EXTENSIONS, packExts.stream().map(PackExt::name) + .collect(Collectors.joining(" "))); + config.setLong(CONFIG_CORE_SECTION, packExtConfigName, + CONFIG_KEY_BLOCK_LIMIT, blockLimit); + config.setInt(CONFIG_CORE_SECTION, packExtConfigName, + CONFIG_KEY_BLOCK_SIZE, blockSize); + } + + private static DfsBlockCacheConfig getConfigForExt( + List<DfsBlockCachePackExtConfig> configs, PackExt packExt) { + for (DfsBlockCachePackExtConfig config : configs) { + if (config.getPackExts().contains(packExt)) { + return config.getPackExtCacheConfiguration(); + } + } + return null; + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java index fef0563f48..3c7cc075d2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java @@ -13,20 +13,24 @@ package org.eclipse.jgit.internal.storage.dfs; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.LongStream; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.LongStream; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig; import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.IndexEventConsumer; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.junit.TestRepository; @@ -39,14 +43,35 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +@RunWith(Parameterized.class) public class DfsBlockCacheTest { @Rule public TestName testName = new TestName(); + private TestRng rng; + private DfsBlockCache cache; + private ExecutorService pool; + private enum CacheType { + SINGLE_TABLE_CLOCK_BLOCK_CACHE, EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE + } + + @Parameters(name = "cache type: {0}") + public static Iterable<? extends Object> data() { + return Arrays.asList(CacheType.SINGLE_TABLE_CLOCK_BLOCK_CACHE, + CacheType.EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE); + } + + @Parameter + public CacheType cacheType; + @Before public void setUp() { rng = new TestRng(testName.getMethodName()); @@ -448,8 +473,28 @@ public class DfsBlockCacheTest { } private void resetCache(int concurrencyLevel) { - DfsBlockCache.reconfigure(new DfsBlockCacheConfig().setBlockSize(512) - .setConcurrencyLevel(concurrencyLevel).setBlockLimit(1 << 20)); + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig() + .setBlockSize(512).setConcurrencyLevel(concurrencyLevel) + .setBlockLimit(1 << 20); + switch (cacheType) { + case SINGLE_TABLE_CLOCK_BLOCK_CACHE: + // SINGLE_TABLE_CLOCK_BLOCK_CACHE doesn't modify the config. + break; + case EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE: + List<DfsBlockCachePackExtConfig> packExtCacheConfigs = new ArrayList<>(); + for (PackExt packExt : PackExt.values()) { + DfsBlockCacheConfig extCacheConfig = new DfsBlockCacheConfig() + .setBlockSize(512).setConcurrencyLevel(concurrencyLevel) + .setBlockLimit(1 << 20) + .setPackExtCacheConfigurations(packExtCacheConfigs); + packExtCacheConfigs.add(new DfsBlockCachePackExtConfig( + EnumSet.of(packExt), extCacheConfig)); + } + cacheConfig.setPackExtCacheConfigurations(packExtCacheConfigs); + break; + } + assertNotNull(cacheConfig); + DfsBlockCache.reconfigure(cacheConfig); cache = DfsBlockCache.getInstance(); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java index ab998951f3..80bd689084 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java @@ -6,6 +6,7 @@ import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.IN import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -15,11 +16,18 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; +import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter; import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; +import org.eclipse.jgit.internal.storage.file.PackBitmapIndex; +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.internal.storage.reftable.LogCursor; import org.eclipse.jgit.internal.storage.reftable.RefCursor; import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; import org.eclipse.jgit.internal.storage.reftable.ReftableReader; @@ -28,9 +36,12 @@ import org.eclipse.jgit.junit.MockSystemReader; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevBlob; @@ -38,6 +49,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.util.GitTimeParser; import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Before; @@ -978,7 +990,7 @@ public class DfsGarbageCollectorTest { } @Test - public void produceCommitGraphAllRefsIncludedFromDisk() throws Exception { + public void produceCommitGraphOnlyHeadsAndTags() throws Exception { String tag = "refs/tags/tag1"; String head = "refs/heads/head1"; String nonHead = "refs/something/nonHead"; @@ -1000,19 +1012,20 @@ public class DfsGarbageCollectorTest { CommitGraph cg = gcPack.getCommitGraph(reader); assertNotNull(cg); - assertTrue("all commits in commit graph", cg.getCommitCnt() == 3); + assertTrue("Only heads and tags reachable commits in commit graph", + cg.getCommitCnt() == 2); // GC packed assertTrue("tag referenced commit is in graph", cg.findGraphPosition(rootCommitTagged) != -1); assertTrue("head referenced commit is in graph", cg.findGraphPosition(headTip) != -1); - // GC_REST packed - assertTrue("nonHead referenced commit is in graph", - cg.findGraphPosition(nonHeadTip) != -1); + // GC_REST not in commit graph + assertEquals("nonHead referenced commit is NOT in graph", + -1, cg.findGraphPosition(nonHeadTip)); } @Test - public void produceCommitGraphAllRefsIncludedFromCache() throws Exception { + public void produceCommitGraphOnlyHeadsAndTagsIncludedFromCache() throws Exception { String tag = "refs/tags/tag1"; String head = "refs/heads/head1"; String nonHead = "refs/something/nonHead"; @@ -1042,15 +1055,16 @@ public class DfsGarbageCollectorTest { assertTrue("commit graph read time is recorded", reader.stats.readCommitGraphMicros > 0); - assertTrue("all commits in commit graph", cachedCG.getCommitCnt() == 3); + assertTrue("Only heads and tags reachable commits in commit graph", + cachedCG.getCommitCnt() == 2); // GC packed assertTrue("tag referenced commit is in graph", cachedCG.findGraphPosition(rootCommitTagged) != -1); assertTrue("head referenced commit is in graph", cachedCG.findGraphPosition(headTip) != -1); - // GC_REST packed - assertTrue("nonHead referenced commit is in graph", - cachedCG.findGraphPosition(nonHeadTip) != -1); + // GC_REST not in commit graph + assertEquals("nonHead referenced commit is not in graph", + -1, cachedCG.findGraphPosition(nonHeadTip)); } @Test @@ -1100,6 +1114,270 @@ public class DfsGarbageCollectorTest { } } + @Test + public void produceCommitGraphAndBloomFilter() throws Exception { + String head = "refs/heads/head1"; + + git.branch(head).commit().message("0").noParents().create(); + + gcWithCommitGraphAndBloomFilter(); + + assertEquals(1, odb.getPacks().length); + DfsPackFile pack = odb.getPacks()[0]; + DfsPackDescription desc = pack.getPackDescription(); + CommitGraphWriter.Stats stats = desc.getCommitGraphStats(); + assertNotNull(stats); + assertEquals(1, stats.getChangedPathFiltersComputed()); + } + + @Test + public void testReadChangedPathConfigAsFalse() throws Exception { + String head = "refs/heads/head1"; + git.branch(head).commit().message("0").noParents().create(); + gcWithCommitGraphAndBloomFilter(); + + Config repoConfig = odb.getRepository().getConfig(); + repoConfig.setBoolean(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION, null, + ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS, false); + + DfsPackFile gcPack = odb.getPacks()[0]; + try (DfsReader reader = odb.newReader()) { + CommitGraph cg = gcPack.getCommitGraph(reader); + assertNull(cg.getChangedPathFilter(0)); + } + } + + @Test + public void testReadChangedPathConfigAsTrue() throws Exception { + String head = "refs/heads/head1"; + git.branch(head).commit().message("0").noParents().create(); + gcWithCommitGraphAndBloomFilter(); + + Config repoConfig = odb.getRepository().getConfig(); + repoConfig.setBoolean(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION, null, + ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS, true); + + DfsPackFile gcPack = odb.getPacks()[0]; + try (DfsReader reader = odb.newReader()) { + CommitGraph cg = gcPack.getCommitGraph(reader); + assertNotNull(cg.getChangedPathFilter(0)); + } + } + + @Test + public void objectSizeIdx_reachableBlob_bigEnough_indexed() throws Exception { + String master = "refs/heads/master"; + RevCommit root = git.branch(master).commit().message("root").noParents() + .create(); + RevBlob headsBlob = git.blob("twelve bytes"); + git.branch(master).commit() + .message("commit on head") + .add("file.txt", headsBlob) + .parent(root) + .create(); + + gcWithObjectSizeIndex(10); + + odb.getReaderOptions().setUseObjectSizeIndex(true); + DfsReader reader = odb.newReader(); + DfsPackFile gcPack = findFirstBySource(odb.getPacks(), GC); + assertTrue(gcPack.hasObjectSizeIndex(reader)); + assertEquals(12, gcPack.getIndexedObjectSize(reader, + gcPack.findIdxPosition(reader, headsBlob))); + } + + @Test + public void objectSizeIdx_reachableBlob_tooSmall_notIndexed() throws Exception { + String master = "refs/heads/master"; + RevCommit root = git.branch(master).commit().message("root").noParents() + .create(); + RevBlob tooSmallBlob = git.blob("small"); + git.branch(master).commit() + .message("commit on head") + .add("small.txt", tooSmallBlob) + .parent(root) + .create(); + + gcWithObjectSizeIndex(10); + + odb.getReaderOptions().setUseObjectSizeIndex(true); + DfsReader reader = odb.newReader(); + DfsPackFile gcPack = findFirstBySource(odb.getPacks(), GC); + assertTrue(gcPack.hasObjectSizeIndex(reader)); + assertEquals(-1, gcPack.getIndexedObjectSize(reader, + gcPack.findIdxPosition(reader, tooSmallBlob))); + } + + @Test + public void objectSizeIndex_unreachableGarbage_noIdx() throws Exception { + String master = "refs/heads/master"; + RevCommit root = git.branch(master).commit().message("root").noParents() + .create(); + git.branch(master).commit() + .message("commit on head") + .add("file.txt", git.blob("a blob")) + .parent(root) + .create(); + git.update(master, root); // blob is unreachable + gcWithObjectSizeIndex(0); + + DfsReader reader = odb.newReader(); + DfsPackFile gcRestPack = findFirstBySource(odb.getPacks(), UNREACHABLE_GARBAGE); + assertFalse(gcRestPack.hasObjectSizeIndex(reader)); + } + + @Test + public void bitmapIndexWrittenDuringGc() throws Exception { + int numBranches = 2; + int commitsPerBranch = 50; + + RevCommit commit0 = commit().message("0").create(); + git.update("branch0", commit0); + RevCommit branch1 = commitChain(commit0, commitsPerBranch); + git.update("branch1", branch1); + RevCommit branch2 = commitChain(commit0, commitsPerBranch); + git.update("branch2", branch2); + + int contiguousCommitCount = 5; + int recentCommitSpan = 2; + int recentCommitCount = 10; + int distantCommitSpan = 5; + + PackConfig packConfig = new PackConfig(); + packConfig.setBitmapContiguousCommitCount(contiguousCommitCount); + packConfig.setBitmapRecentCommitSpan(recentCommitSpan); + packConfig.setBitmapRecentCommitCount(recentCommitCount); + packConfig.setBitmapDistantCommitSpan(distantCommitSpan); + + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setPackConfig(packConfig); + run(gc); + + DfsPackFile pack = odb.getPacks()[0]; + PackBitmapIndex bitmapIndex = pack.getBitmapIndex(odb.newReader()); + assertTrue("pack file has bitmap index extension", + pack.getPackDescription().hasFileExt(PackExt.BITMAP_INDEX)); + + int recentCommitsPerBranch = (recentCommitCount - contiguousCommitCount + - 1) / recentCommitSpan; + assertEquals("expected recent commits", 2, recentCommitsPerBranch); + + int distantCommitsPerBranch = (commitsPerBranch - 1 - recentCommitCount) + / distantCommitSpan; + assertEquals("expected distant commits", 7, distantCommitsPerBranch); + + int branchBitmapsCount = contiguousCommitCount + + numBranches + * (recentCommitsPerBranch + + distantCommitsPerBranch); + assertEquals("expected bitmaps count", 23, branchBitmapsCount); + assertEquals("bitmap index has expected number of bitmaps", + branchBitmapsCount, + bitmapIndex.getBitmapCount()); + + // The count is just a function of whether any bitmaps happen to + // compress efficiently against the others in the index. We expect for + // this test that this there will be at least one like this, but the + // actual count is situation-specific + assertTrue("bitmap index has xor-compressed bitmaps", + bitmapIndex.getXorBitmapCount() > 0); + } + + @Test + public void gitGCWithRefLogExpire() throws Exception { + String master = "refs/heads/master"; + RevCommit commit0 = commit().message("0").create(); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + git.update(master, commit1); + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setReftableConfig(new ReftableConfig()); + run(gc); + DfsPackDescription t1 = odb.newPack(INSERT); + Ref next = new ObjectIdRef.PeeledNonTag(Ref.Storage.LOOSE, + "refs/heads/next", commit0.copy()); + Instant currentDay = Instant.now(); + Instant ten_days_ago = GitTimeParser.parseInstant("10 days ago"); + Instant twenty_days_ago = GitTimeParser.parseInstant("20 days ago"); + Instant thirty_days_ago = GitTimeParser.parseInstant("30 days ago"); + Instant fifty_days_ago = GitTimeParser.parseInstant("50 days ago"); + final ZoneOffset offset = ZoneOffset.ofHours(-8); + PersonIdent who2 = new PersonIdent("J.Author", "authemail", currentDay, + offset); + PersonIdent who3 = new PersonIdent("J.Author", "authemail", + ten_days_ago, offset); + PersonIdent who4 = new PersonIdent("J.Author", "authemail", + twenty_days_ago, offset); + PersonIdent who5 = new PersonIdent("J.Author", "authemail", + thirty_days_ago, offset); + PersonIdent who6 = new PersonIdent("J.Author", "authemail", + fifty_days_ago, offset); + + try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) { + ReftableWriter w = new ReftableWriter(out); + w.setMinUpdateIndex(42); + w.setMaxUpdateIndex(42); + w.begin(); + w.sortAndWriteRefs(Collections.singleton(next)); + w.writeLog("refs/heads/branch", 1, who2, ObjectId.zeroId(),id(2), "Branch Message"); + w.writeLog("refs/heads/branch1", 2, who3, ObjectId.zeroId(),id(3), "Branch Message1"); + w.writeLog("refs/heads/branch2", 2, who4, ObjectId.zeroId(),id(4), "Branch Message2"); + w.writeLog("refs/heads/branch3", 2, who5, ObjectId.zeroId(),id(5), "Branch Message3"); + w.writeLog("refs/heads/branch4", 2, who6, ObjectId.zeroId(),id(6), "Branch Message4"); + w.finish(); + t1.addFileExt(REFTABLE); + t1.setReftableStats(w.getStats()); + } + odb.commitPack(Collections.singleton(t1), null); + + gc = new DfsGarbageCollector(repo); + gc.setReftableConfig(new ReftableConfig()); + // Expire ref log entries older than 30 days + gc.setRefLogExpire(thirty_days_ago); + run(gc); + + // Single GC pack present with all objects. + assertEquals(1, odb.getPacks().length); + DfsPackFile pack = odb.getPacks()[0]; + DfsPackDescription desc = pack.getPackDescription(); + + DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc); + try (DfsReader ctx = odb.newReader(); + ReftableReader rr = table.open(ctx); + RefCursor rc = rr.allRefs(); + LogCursor lc = rr.allLogs()) { + assertTrue(rc.next()); + assertEquals(master, rc.getRef().getName()); + assertEquals(commit1, rc.getRef().getObjectId()); + assertTrue(rc.next()); + assertEquals(next.getName(), rc.getRef().getName()); + assertEquals(commit0, rc.getRef().getObjectId()); + assertFalse(rc.next()); + assertTrue(lc.next()); + assertEquals(lc.getRefName(),"refs/heads/branch"); + assertTrue(lc.next()); + assertEquals(lc.getRefName(),"refs/heads/branch1"); + assertTrue(lc.next()); + assertEquals(lc.getRefName(),"refs/heads/branch2"); + // Old entries are purged + assertFalse(lc.next()); + } + } + + + private RevCommit commitChain(RevCommit parent, int length) + throws Exception { + for (int i = 0; i < length; i++) { + parent = commit().message("" + i).parent(parent).create(); + } + return parent; + } + + private static DfsPackFile findFirstBySource(DfsPackFile[] packs, PackSource source) { + return Arrays.stream(packs) + .filter(p -> p.getPackDescription().getPackSource() == source) + .findFirst().get(); + } + private TestRepository<InMemoryRepository>.CommitBuilder commit() { return git.commit(); } @@ -1110,6 +1388,19 @@ public class DfsGarbageCollectorTest { run(gc); } + private void gcWithCommitGraphAndBloomFilter() throws IOException { + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setWriteCommitGraph(true); + gc.setWriteBloomFilter(true); + run(gc); + } + + private void gcWithObjectSizeIndex(int threshold) throws IOException { + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.getPackConfig().setMinBytesForObjSizeIndex(threshold); + run(gc); + } + private void gcNoTtl() throws IOException { DfsGarbageCollector gc = new DfsGarbageCollector(repo); gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); // disable TTL @@ -1162,4 +1453,12 @@ public class DfsGarbageCollectorTest { } return cnt; } + private static ObjectId id(int i) { + byte[] buf = new byte[OBJECT_ID_LENGTH]; + buf[0] = (byte) (i & 0xff); + buf[1] = (byte) ((i >>> 8) & 0xff); + buf[2] = (byte) ((i >>> 16) & 0xff); + buf[3] = (byte) (i >>> 24); + return ObjectId.fromRaw(buf); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java index adf577b0f7..efa98de549 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.internal.storage.dfs; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; @@ -29,11 +31,16 @@ import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.TestRng; import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.TagBuilder; +import org.eclipse.jgit.lib.TreeFormatter; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; import org.junit.Before; @@ -207,7 +214,7 @@ public class DfsInserterTest { } @Test - public void testNoCheckExisting() throws IOException { + public void testNoDuplicates() throws IOException { byte[] contents = Constants.encode("foo"); ObjectId fooId; try (ObjectInserter ins = db.newObjectInserter()) { @@ -217,21 +224,20 @@ public class DfsInserterTest { assertEquals(1, db.getObjectDatabase().listPacks().size()); try (ObjectInserter ins = db.newObjectInserter()) { - ((DfsInserter) ins).checkExisting(false); + ins.insert(Constants.OBJ_BLOB, Constants.encode("bar")); assertEquals(fooId, ins.insert(Constants.OBJ_BLOB, contents)); ins.flush(); } assertEquals(2, db.getObjectDatabase().listPacks().size()); - // Verify that we have a foo in both INSERT packs. + // Newer packs are first. Verify that foo is only in the second pack try (DfsReader reader = new DfsReader(db.getObjectDatabase())) { DfsPackFile packs[] = db.getObjectDatabase().getPacks(); - assertEquals(2, packs.length); DfsPackFile p1 = packs[0]; assertEquals(PackSource.INSERT, p1.getPackDescription().getPackSource()); - assertTrue(p1.hasObject(reader, fooId)); + assertFalse(p1.hasObject(reader, fooId)); DfsPackFile p2 = packs[1]; assertEquals(PackSource.INSERT, @@ -240,6 +246,73 @@ public class DfsInserterTest { } } + @Test + public void testObjectSizePopulated() throws IOException { + // Blob + byte[] contents = Constants.encode("foo"); + + // Commit + PersonIdent person = new PersonIdent("Committer a", "jgit@eclipse.org"); + CommitBuilder c = new CommitBuilder(); + c.setAuthor(person); + c.setCommitter(person); + c.setTreeId(ObjectId + .fromString("45c4c6767a3945815371a7016532751dd558be40")); + c.setMessage("commit message"); + + // Tree + TreeFormatter treeBuilder = new TreeFormatter(2); + treeBuilder.append("filea", FileMode.REGULAR_FILE, ObjectId + .fromString("45c4c6767a3945815371a7016532751dd558be40")); + treeBuilder.append("fileb", FileMode.GITLINK, ObjectId + .fromString("1c458e25ca624bb8d4735bec1379a4a29ba786d0")); + + // Tag + TagBuilder tagBuilder = new TagBuilder(); + tagBuilder.setObjectId( + ObjectId.fromString("c97fe131649e80de55bd153e9a8d8629f7ca6932"), + Constants.OBJ_COMMIT); + tagBuilder.setTag("short name"); + + try (DfsInserter ins = (DfsInserter) db.newObjectInserter()) { + ObjectId aBlob = ins.insert(Constants.OBJ_BLOB, contents); + assertEquals(contents.length, + ins.objectMap.get(aBlob).getFullSize()); + + ObjectId aCommit = ins.insert(c); + assertEquals(174, ins.objectMap.get(aCommit).getFullSize()); + + ObjectId tree = ins.insert(treeBuilder); + assertEquals(66, ins.objectMap.get(tree).getFullSize()); + + ObjectId tag = ins.insert(tagBuilder); + assertEquals(76, ins.objectMap.get(tag).getFullSize()); + } + } + + @Test + public void testObjectSizeIndexOnInsert() throws IOException { + db.getConfig().setInt(CONFIG_PACK_SECTION, null, + CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 0); + db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true); + + byte[] contents = Constants.encode("foo"); + ObjectId fooId; + try (ObjectInserter ins = db.newObjectInserter()) { + fooId = ins.insert(Constants.OBJ_BLOB, contents); + ins.flush(); + } + + DfsReader reader = db.getObjectDatabase().newReader(); + assertEquals(1, db.getObjectDatabase().listPacks().size()); + DfsPackFile insertPack = db.getObjectDatabase().getPacks()[0]; + assertEquals(PackSource.INSERT, + insertPack.getPackDescription().getPackSource()); + assertTrue(insertPack.hasObjectSizeIndex(reader)); + assertEquals(contents.length, insertPack.getIndexedObjectSize(reader, + insertPack.findIdxPosition(reader, fooId))); + } + private static String readString(ObjectLoader loader) throws IOException { return RawParseUtils.decode(readStream(loader)); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java index c516e30f50..c3b6aa85a2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java @@ -12,13 +12,18 @@ package org.eclipse.jgit.internal.storage.dfs; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT; +import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; import org.junit.Test; @@ -98,6 +103,40 @@ public class DfsPackCompacterTest { pack.getPackDescription().getEstimatedPackSize()); } + @Test + public void testObjectSizeIndexWritten() throws Exception { + writeObjectSizeIndex(repo, true); + RevCommit commit0 = commit().message("0").create(); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + git.update("master", commit1); + + compact(); + + Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks()) + .filter(pack -> pack.getPackDescription() + .getPackSource() == COMPACT) + .findFirst(); + assertTrue(compactPack.isPresent()); + assertTrue(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX)); + } + + @Test + public void testObjectSizeIndexNotWritten() throws Exception { + writeObjectSizeIndex(repo, false); + RevCommit commit0 = commit().message("0").create(); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + git.update("master", commit1); + + compact(); + + Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks()) + .filter(pack -> pack.getPackDescription() + .getPackSource() == COMPACT) + .findFirst(); + assertTrue(compactPack.isPresent()); + assertFalse(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX)); + } + private TestRepository<InMemoryRepository>.CommitBuilder commit() { return git.commit(); } @@ -108,4 +147,9 @@ public class DfsPackCompacterTest { compactor.compact(null); odb.clearCache(); } + + private static void writeObjectSizeIndex(DfsRepository repo, boolean should) { + repo.getConfig().setInt(ConfigConstants.CONFIG_PACK_SECTION, null, + ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, should ? 0 : -1); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java index ea5787309b..f2129fd3c5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java @@ -10,19 +10,39 @@ package org.eclipse.jgit.internal.storage.dfs; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.zip.Deflater; +import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; +import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; +import org.eclipse.jgit.internal.storage.dfs.DfsReader.PackLoadListener; +import org.eclipse.jgit.internal.storage.file.PackBitmapIndex; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackOutputStream; import org.eclipse.jgit.internal.storage.pack.PackWriter; import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRng; +import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.transport.ReceiveCommand; import org.junit.Before; import org.junit.Test; @@ -100,7 +120,178 @@ public class DfsPackFileTest { assertPackSize(); } - private void setupPack(int bs, int ps) throws IOException { + @Test + public void testLoadObjectSizeIndex() throws IOException { + bypassCache = false; + clearCache = true; + setObjectSizeIndexMinBytes(0); + ObjectId blobId = setupPack(512, 800); + + db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true); + DfsReader reader = db.getObjectDatabase().newReader(); + DfsPackFile pack = db.getObjectDatabase().getPacks()[0]; + assertTrue(pack.hasObjectSizeIndex(reader)); + assertEquals(800, pack.getIndexedObjectSize(reader, + pack.findIdxPosition(reader, blobId))); + } + + @Test + public void testGetBitmapIndex() throws IOException { + bypassCache = false; + clearCache = true; + ObjectId objectId = setupPack(512, 800); + + // Add a ref for GC + BatchRefUpdate batchRefUpdate = db.getRefDatabase().newBatchUpdate(); + batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), + objectId, "refs/heads/master")); + try (RevWalk rw = new RevWalk(db)) { + batchRefUpdate.execute(rw, NullProgressMonitor.INSTANCE); + } + DfsGarbageCollector gc = new DfsGarbageCollector(db); + gc.pack(NullProgressMonitor.INSTANCE); + + DfsReader reader = db.getObjectDatabase().newReader(); + PackBitmapIndex bitmapIndex = db.getObjectDatabase().getPacks()[0] + .getBitmapIndex(reader); + assertNotNull(bitmapIndex); + assertEquals(1, bitmapIndex.getObjectCount()); + } + + @Test + public void testGetBitmapIndex_noBitmaps() throws IOException { + bypassCache = false; + clearCache = true; + setupPack(512, 800); + + DfsReader reader = db.getObjectDatabase().newReader(); + PackBitmapIndex bitmapIndex = db.getObjectDatabase().getPacks()[0] + .getBitmapIndex(reader); + assertNull(bitmapIndex); + } + + @Test + public void testLoadObjectSizeIndex_noIndex() throws IOException { + bypassCache = false; + clearCache = true; + setObjectSizeIndexMinBytes(-1); + setupPack(512, 800); + + DfsReader reader = db.getObjectDatabase().newReader(); + DfsPackFile pack = db.getObjectDatabase().getPacks()[0]; + assertFalse(pack.hasObjectSizeIndex(reader)); + } + + private static class TestPackLoadListener implements PackLoadListener { + final Map<PackExt, Integer> indexLoadCount = new HashMap<>(); + + int blockLoadCount; + + @SuppressWarnings("boxing") + @Override + public void onIndexLoad(String packName, PackSource src, PackExt ext, + long size, Object loadedIdx) { + indexLoadCount.merge(ext, 1, Integer::sum); + } + + @Override + public void onBlockLoad(String packName, PackSource src, PackExt ext, long position, + DfsBlockData dfsBlockData) { + blockLoadCount += 1; + } + } + + @Test + public void testIndexLoadCallback_indexNotInCache() throws IOException { + bypassCache = false; + clearCache = true; + setObjectSizeIndexMinBytes(-1); + setupPack(512, 800); + + TestPackLoadListener tal = new TestPackLoadListener(); + DfsReader reader = db.getObjectDatabase().newReader(); + reader.addPackLoadListener(tal); + DfsPackFile pack = db.getObjectDatabase().getPacks()[0]; + pack.getPackIndex(reader); + + assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue()); + } + + @Test + public void testIndexLoadCallback_indexInCache() throws IOException { + bypassCache = false; + clearCache = false; + setObjectSizeIndexMinBytes(-1); + setupPack(512, 800); + + TestPackLoadListener tal = new TestPackLoadListener(); + DfsReader reader = db.getObjectDatabase().newReader(); + reader.addPackLoadListener(tal); + DfsPackFile pack = db.getObjectDatabase().getPacks()[0]; + pack.getPackIndex(reader); + pack.getPackIndex(reader); + pack.getPackIndex(reader); + + assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue()); + } + + @Test + public void testIndexLoadCallback_multipleReads() throws IOException { + bypassCache = false; + clearCache = true; + setObjectSizeIndexMinBytes(-1); + setupPack(512, 800); + + TestPackLoadListener tal = new TestPackLoadListener(); + DfsReader reader = db.getObjectDatabase().newReader(); + reader.addPackLoadListener(tal); + DfsPackFile pack = db.getObjectDatabase().getPacks()[0]; + pack.getPackIndex(reader); + pack.getPackIndex(reader); + pack.getPackIndex(reader); + + assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue()); + } + + + @Test + public void testBlockLoadCallback_loadInCache() throws IOException { + bypassCache = false; + clearCache = true; + setObjectSizeIndexMinBytes(-1); + setupPack(512, 800); + + TestPackLoadListener tal = new TestPackLoadListener(); + DfsReader reader = db.getObjectDatabase().newReader(); + reader.addPackLoadListener(tal); + DfsPackFile pack = db.getObjectDatabase().getPacks()[0]; + ObjectId anObject = pack.getPackIndex(reader).getObjectId(0); + pack.get(reader, anObject).getBytes(); + assertEquals(2, tal.blockLoadCount); + } + + @Test + public void testExistenceOfBloomFilterAlongWithCommitGraph() + throws Exception { + try (TestRepository<InMemoryRepository> repository = new TestRepository<>( + db)) { + repository.branch("/refs/heads/main").commit().add("blob1", "blob1") + .create(); + } + setReadChangedPaths(true); + DfsGarbageCollector gc = new DfsGarbageCollector(db); + gc.setWriteCommitGraph(true).setWriteBloomFilter(true) + .pack(NullProgressMonitor.INSTANCE); + + DfsReader reader = db.getObjectDatabase().newReader(); + CommitGraph cg = db.getObjectDatabase().getPacks()[0] + .getCommitGraph(reader); + assertNotNull(cg); + assertEquals(1, cg.getCommitCnt()); + assertNotNull(cg.getChangedPathFilter(0)); + } + + private ObjectId setupPack(int bs, int ps) throws IOException { DfsBlockCacheConfig cfg = new DfsBlockCacheConfig().setBlockSize(bs) .setBlockLimit(bs * 100).setStreamRatio(bypassCache ? 0F : 1F); DfsBlockCache.reconfigure(cfg); @@ -108,18 +299,19 @@ public class DfsPackFileTest { byte[] data = new TestRng(JGitTestUtil.getName()).nextBytes(ps); DfsInserter ins = (DfsInserter) db.newObjectInserter(); ins.setCompressionLevel(Deflater.NO_COMPRESSION); - ins.insert(Constants.OBJ_BLOB, data); + ObjectId blobId = ins.insert(Constants.OBJ_BLOB, data); ins.flush(); if (clearCache) { DfsBlockCache.reconfigure(cfg); db.getObjectDatabase().clearCache(); } + return blobId; } private void assertPackSize() throws IOException { try (DfsReader ctx = db.getObjectDatabase().newReader(); - PackWriter pw = new PackWriter(ctx); + PackWriter pw = new PackWriter(new PackConfig(), ctx); ByteArrayOutputStream os = new ByteArrayOutputStream(); PackOutputStream out = new PackOutputStream( NullProgressMonitor.INSTANCE, os, pw)) { @@ -129,4 +321,14 @@ public class DfsPackFileTest { assertEquals(packSize - (12 + 20), os.size()); } } + + private void setObjectSizeIndexMinBytes(int threshold) { + db.getConfig().setInt(CONFIG_PACK_SECTION, null, + CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, threshold); + } + + private void setReadChangedPaths(boolean enable) { + db.getConfig().setBoolean(CONFIG_COMMIT_GRAPH_SECTION, null, + CONFIG_KEY_READ_CHANGED_PATHS, enable); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java new file mode 100644 index 0000000000..9d26978d66 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023, Google LLC and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION; +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackList; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.transport.InMemoryPack; +import org.eclipse.jgit.transport.PackParser; +import org.junit.Before; +import org.junit.Test; + +public class DfsPackParserTest { + private InMemoryRepository repo; + + + @Before + public void setUp() throws Exception { + DfsRepositoryDescription desc = new DfsRepositoryDescription("test"); + repo = new InMemoryRepository(desc); + repo.getConfig().setInt(CONFIG_PACK_SECTION, null, + CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 0); + } + + @Test + public void parse_writeObjSizeIdx() throws IOException { + InMemoryPack pack = new InMemoryPack(); + + // Sha1 of the blob "a" + ObjectId blobA = ObjectId + .fromString("2e65efe2a145dda7ee51d1741299f848e5bf752e"); + + pack.header(2); + pack.write(Constants.OBJ_BLOB << 4 | 1); + pack.deflate(new byte[] { 'a' }); + + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + pack.copyRaw(blobA); + pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' }); + pack.digest(); + + try (ObjectInserter ins = repo.newObjectInserter()) { + PackParser parser = ins.newPackParser(pack.toInputStream()); + parser.parse(NullProgressMonitor.INSTANCE, + NullProgressMonitor.INSTANCE); + ins.flush(); + } + + repo.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true); + DfsReader reader = repo.getObjectDatabase().newReader(); + PackList packList = repo.getObjectDatabase().getPackList(); + assertEquals(1, packList.packs.length); + assertEquals(1, packList.packs[0].getIndexedObjectSize(reader, + packList.packs[0].findIdxPosition(reader, blobA))); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java new file mode 100644 index 0000000000..a0c228906e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2023, Google LLC. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; +import org.eclipse.jgit.internal.storage.dfs.DfsReader.PackLoadListener; +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.TestRng; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.junit.Before; +import org.junit.Test; + +public class DfsReaderTest { + InMemoryRepository db; + + @Before + public void setUp() { + db = new InMemoryRepository(new DfsRepositoryDescription("test")); + // These tests assume the object size index is enabled. + db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true); + } + + @Test + public void getObjectSize_noIndex_blob() throws IOException { + ObjectId obj = insertBlobWithSize(100); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + long size = ctx.getObjectSize(obj, OBJ_BLOB); + assertEquals(100, size); + } + } + + @Test + public void getObjectSize_noIndex_commit() throws IOException { + ObjectId obj = insertObjectWithSize(OBJ_COMMIT, 110); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + long size = ctx.getObjectSize(obj, OBJ_COMMIT); + assertEquals(110, size); + } + } + + @Test + public void getObjectSize_index_indexedBlob() throws IOException { + setObjectSizeIndexMinBytes(100); + ObjectId obj = insertBlobWithSize(200); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + long size = ctx.getObjectSize(obj, OBJ_BLOB); + assertEquals(200, size); + } + } + + @Test + public void getObjectSize_index_nonIndexedBlob() throws IOException { + setObjectSizeIndexMinBytes(100); + ObjectId obj = insertBlobWithSize(50); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + long size = ctx.getObjectSize(obj, OBJ_BLOB); + assertEquals(50, size); + } + } + + @Test + public void getObjectSize_index_commit() throws IOException { + setObjectSizeIndexMinBytes(100); + insertBlobWithSize(110); + ObjectId obj = insertObjectWithSize(OBJ_COMMIT, 120); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + long size = ctx.getObjectSize(obj, OBJ_COMMIT); + assertEquals(120, size); + } + } + + @Test + public void isNotLargerThan_objAboveThreshold() throws IOException { + setObjectSizeIndexMinBytes(100); + ObjectId obj = insertBlobWithSize(200); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + assertFalse("limit < threshold < obj", + ctx.isNotLargerThan(obj, OBJ_BLOB, 50)); + assertEquals(1, ctx.stats.isNotLargerThanCallCount); + assertEquals(1, ctx.stats.objectSizeIndexHit); + assertEquals(0, ctx.stats.objectSizeIndexMiss); + + assertFalse("limit = threshold < obj", + ctx.isNotLargerThan(obj, OBJ_BLOB, 100)); + assertEquals(2, ctx.stats.isNotLargerThanCallCount); + assertEquals(2, ctx.stats.objectSizeIndexHit); + assertEquals(0, ctx.stats.objectSizeIndexMiss); + + assertFalse("threshold < limit < obj", + ctx.isNotLargerThan(obj, OBJ_BLOB, 150)); + assertEquals(3, ctx.stats.isNotLargerThanCallCount); + assertEquals(3, ctx.stats.objectSizeIndexHit); + assertEquals(0, ctx.stats.objectSizeIndexMiss); + + assertTrue("threshold < limit = obj", + ctx.isNotLargerThan(obj, OBJ_BLOB, 200)); + assertEquals(4, ctx.stats.isNotLargerThanCallCount); + assertEquals(4, ctx.stats.objectSizeIndexHit); + assertEquals(0, ctx.stats.objectSizeIndexMiss); + + assertTrue("threshold < obj < limit", + ctx.isNotLargerThan(obj, OBJ_BLOB, 250)); + assertEquals(5, ctx.stats.isNotLargerThanCallCount); + assertEquals(5, ctx.stats.objectSizeIndexHit); + assertEquals(0, ctx.stats.objectSizeIndexMiss); + } + } + + @Test + public void isNotLargerThan_objBelowThreshold() throws IOException { + setObjectSizeIndexMinBytes(100); + insertBlobWithSize(1000); // index not empty + ObjectId obj = insertBlobWithSize(50); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + assertFalse("limit < obj < threshold", + ctx.isNotLargerThan(obj, OBJ_BLOB, 10)); + assertEquals(1, ctx.stats.isNotLargerThanCallCount); + assertEquals(0, ctx.stats.objectSizeIndexHit); + assertEquals(1, ctx.stats.objectSizeIndexMiss); + + assertTrue("limit = obj < threshold", + ctx.isNotLargerThan(obj, OBJ_BLOB, 50)); + assertEquals(2, ctx.stats.isNotLargerThanCallCount); + assertEquals(0, ctx.stats.objectSizeIndexHit); + assertEquals(2, ctx.stats.objectSizeIndexMiss); + + assertTrue("obj < limit < threshold", + ctx.isNotLargerThan(obj, OBJ_BLOB, 80)); + assertEquals(3, ctx.stats.isNotLargerThanCallCount); + assertEquals(0, ctx.stats.objectSizeIndexHit); + assertEquals(3, ctx.stats.objectSizeIndexMiss); + + assertTrue("obj < limit = threshold", + ctx.isNotLargerThan(obj, OBJ_BLOB, 100)); + assertEquals(4, ctx.stats.isNotLargerThanCallCount); + assertEquals(0, ctx.stats.objectSizeIndexHit); + assertEquals(4, ctx.stats.objectSizeIndexMiss); + + assertTrue("obj < threshold < limit", + ctx.isNotLargerThan(obj, OBJ_BLOB, 120)); + assertEquals(5, ctx.stats.isNotLargerThanCallCount); + assertEquals(0, ctx.stats.objectSizeIndexHit); + assertEquals(5, ctx.stats.objectSizeIndexMiss); + } + } + + @Test + public void isNotLargerThan_emptyIdx() throws IOException { + setObjectSizeIndexMinBytes(100); + ObjectId obj = insertBlobWithSize(10); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + assertFalse(ctx.isNotLargerThan(obj, OBJ_BLOB, 0)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 10)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 40)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 50)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 100)); + + assertEquals(5, ctx.stats.isNotLargerThanCallCount); + assertEquals(5, ctx.stats.objectSizeIndexMiss); + assertEquals(0, ctx.stats.objectSizeIndexHit); + } + } + + @Test + public void isNotLargerThan_noObjectSizeIndex() throws IOException { + setObjectSizeIndexMinBytes(-1); + ObjectId obj = insertBlobWithSize(10); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + assertFalse(ctx.isNotLargerThan(obj, OBJ_BLOB, 0)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 10)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 40)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 50)); + assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 100)); + + assertEquals(5, ctx.stats.isNotLargerThanCallCount); + assertEquals(0, ctx.stats.objectSizeIndexMiss); + assertEquals(0, ctx.stats.objectSizeIndexHit); + } + } + + @Test + public void packLoadListener_noInvocations() throws IOException { + insertBlobWithSize(100); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + CounterPackLoadListener listener = new CounterPackLoadListener(); + ctx.addPackLoadListener(listener); + assertEquals(null, listener.callsPerExt.get(PackExt.INDEX)); + } + } + + @Test + public void packLoadListener_has_openIdx() throws IOException { + ObjectId obj = insertBlobWithSize(100); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + CounterPackLoadListener listener = new CounterPackLoadListener(); + ctx.addPackLoadListener(listener); + boolean has = ctx.has(obj); + assertTrue(has); + assertEquals(Integer.valueOf(1), + listener.callsPerExt.get(PackExt.INDEX)); + } + } + + @Test + public void packLoadListener_notLargerThan_openMultipleIndices() + throws IOException { + setObjectSizeIndexMinBytes(100); + ObjectId obj = insertBlobWithSize(200); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + CounterPackLoadListener listener = new CounterPackLoadListener(); + ctx.addPackLoadListener(listener); + boolean notLargerThan = ctx.isNotLargerThan(obj, OBJ_BLOB, 1000); + assertTrue(notLargerThan); + assertEquals(Integer.valueOf(1), + listener.callsPerExt.get(PackExt.INDEX)); + assertEquals(Integer.valueOf(1), + listener.callsPerExt.get(PackExt.OBJECT_SIZE_INDEX)); + } + } + + @Test + public void packLoadListener_has_openMultipleIndices() throws IOException { + setObjectSizeIndexMinBytes(100); + insertBlobWithSize(200); + insertBlobWithSize(230); + insertBlobWithSize(100); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + CounterPackLoadListener listener = new CounterPackLoadListener(); + ctx.addPackLoadListener(listener); + ObjectId oid = ObjectId + .fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f129"); + boolean has = ctx.has(oid); + assertFalse(has); + // Open 3 indices trying to find the pack + assertEquals(Integer.valueOf(3), + listener.callsPerExt.get(PackExt.INDEX)); + } + } + + @Test + public void packLoadListener_has_repeatedCalls_openMultipleIndices() + throws IOException { + // Two objects NOT in the repo + ObjectId oid = ObjectId + .fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f129"); + ObjectId oid2 = ObjectId + .fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f130"); + + setObjectSizeIndexMinBytes(100); + insertBlobWithSize(200); + insertBlobWithSize(230); + insertBlobWithSize(100); + CounterPackLoadListener listener = new CounterPackLoadListener(); + try (DfsReader ctx = db.getObjectDatabase().newReader()) { + ctx.addPackLoadListener(listener); + boolean has = ctx.has(oid); + ctx.has(oid); + ctx.has(oid2); + assertFalse(has); + // The 3 indices were loaded only once each + assertEquals(Integer.valueOf(3), + listener.callsPerExt.get(PackExt.INDEX)); + } + } + + private static class CounterPackLoadListener implements PackLoadListener { + final Map<PackExt, Integer> callsPerExt = new HashMap<>(); + + @SuppressWarnings("boxing") + @Override + public void onIndexLoad(String packName, PackSource src, PackExt ext, + long size, Object loadedIdx) { + callsPerExt.merge(ext, 1, Integer::sum); + } + + @Override + public void onBlockLoad(String packName, PackSource src, PackExt ext, + long size, DfsBlockData dfsBlockData) { + // empty + } + } + + private ObjectId insertBlobWithSize(int size) throws IOException { + return insertObjectWithSize(OBJ_BLOB, size); + } + + private ObjectId insertObjectWithSize(int object_type, int size) + throws IOException { + TestRng testRng = new TestRng(JGitTestUtil.getName()); + ObjectId oid; + try (ObjectInserter ins = db.newObjectInserter()) { + oid = ins.insert(object_type, testRng.nextBytes(size)); + ins.flush(); + } + return oid; + } + + private void setObjectSizeIndexMinBytes(int threshold) { + db.getConfig().setInt(CONFIG_PACK_SECTION, null, + CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, threshold); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java new file mode 100644 index 0000000000..e7627bc4ab --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java @@ -0,0 +1,679 @@ +/* + * Copyright (c) 2024, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.Ref; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.RefLoader; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig; +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.junit.Test; +import org.mockito.Mockito; + +@SuppressWarnings({ "boxing", "unchecked" }) +public class PackExtBlockCacheTableTest { + private static final String CACHE_NAME = "CacheName"; + + @Test + public void fromBlockCacheConfigs_createsDfsPackExtBlockCacheTables() { + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig(); + cacheConfig.setPackExtCacheConfigurations( + List.of(new DfsBlockCachePackExtConfig(EnumSet.of(PackExt.PACK), + new DfsBlockCacheConfig()))); + assertNotNull( + PackExtBlockCacheTable.fromBlockCacheConfigs(cacheConfig)); + } + + @Test + public void fromBlockCacheConfigs_noPackExtConfigurationGiven_packExtCacheConfigurationsIsEmpty_throws() { + DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig(); + cacheConfig.setPackExtCacheConfigurations(List.of()); + assertThrows(IllegalArgumentException.class, + () -> PackExtBlockCacheTable + .fromBlockCacheConfigs(cacheConfig)); + } + + @Test + public void hasBlock0_packExtMapsToCacheTable_callsBitmapIndexCacheTable() { + DfsStreamKey streamKey = new TestKey(PackExt.BITMAP_INDEX); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.hasBlock0(any(DfsStreamKey.class))) + .thenReturn(true); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertTrue(tables.hasBlock0(streamKey)); + } + + @Test + public void hasBlock0_packExtDoesNotMapToCacheTable_callsDefaultCache() { + DfsStreamKey streamKey = new TestKey(PackExt.PACK); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.hasBlock0(any(DfsStreamKey.class))) + .thenReturn(true); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertTrue(tables.hasBlock0(streamKey)); + } + + @Test + public void getOrLoad_packExtMapsToCacheTable_callsBitmapIndexCacheTable() + throws Exception { + BlockBasedFile blockBasedFile = new BlockBasedFile(null, + mock(DfsPackDescription.class), PackExt.BITMAP_INDEX) { + // empty + }; + DfsBlock dfsBlock = mock(DfsBlock.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.getOrLoad(any(BlockBasedFile.class), + anyLong(), any(DfsReader.class), + any(DfsBlockCache.ReadableChannelSupplier.class))) + .thenReturn(mock(DfsBlock.class)); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.getOrLoad(any(BlockBasedFile.class), + anyLong(), any(DfsReader.class), + any(DfsBlockCache.ReadableChannelSupplier.class))) + .thenReturn(dfsBlock); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat( + tables.getOrLoad(blockBasedFile, 0, mock(DfsReader.class), + mock(DfsBlockCache.ReadableChannelSupplier.class)), + sameInstance(dfsBlock)); + } + + @Test + public void getOrLoad_packExtDoesNotMapToCacheTable_callsDefaultCache() + throws Exception { + BlockBasedFile blockBasedFile = new BlockBasedFile(null, + mock(DfsPackDescription.class), PackExt.PACK) { + // empty + }; + DfsBlock dfsBlock = mock(DfsBlock.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.getOrLoad(any(BlockBasedFile.class), + anyLong(), any(DfsReader.class), + any(DfsBlockCache.ReadableChannelSupplier.class))) + .thenReturn(dfsBlock); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.getOrLoad(any(BlockBasedFile.class), + anyLong(), any(DfsReader.class), + any(DfsBlockCache.ReadableChannelSupplier.class))) + .thenReturn(mock(DfsBlock.class)); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat( + tables.getOrLoad(blockBasedFile, 0, mock(DfsReader.class), + mock(DfsBlockCache.ReadableChannelSupplier.class)), + sameInstance(dfsBlock)); + } + + @Test + public void getOrLoadRef_packExtMapsToCacheTable_callsBitmapIndexCacheTable() + throws Exception { + Ref<Integer> ref = mock(Ref.class); + DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.getOrLoadRef(any(DfsStreamKey.class), + anyLong(), any(RefLoader.class))).thenReturn(mock(Ref.class)); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.getOrLoadRef(any(DfsStreamKey.class), + anyLong(), any(RefLoader.class))).thenReturn(ref); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.getOrLoadRef(dfsStreamKey, 0, mock(RefLoader.class)), + sameInstance(ref)); + } + + @Test + public void getOrLoadRef_packExtDoesNotMapToCacheTable_callsDefaultCache() + throws Exception { + Ref<Integer> ref = mock(Ref.class); + DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.getOrLoadRef(any(DfsStreamKey.class), + anyLong(), any(RefLoader.class))).thenReturn(ref); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.getOrLoadRef(any(DfsStreamKey.class), + anyLong(), any(RefLoader.class))).thenReturn(mock(Ref.class)); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.getOrLoadRef(dfsStreamKey, 0, mock(RefLoader.class)), + sameInstance(ref)); + } + + @Test + public void putDfsBlock_packExtMapsToCacheTable_callsBitmapIndexCacheTable() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX); + DfsBlock dfsBlock = new DfsBlock(dfsStreamKey, 0, new byte[0]); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + tables.put(dfsBlock); + Mockito.verify(bitmapIndexCacheTable, times(1)).put(dfsBlock); + } + + @Test + public void putDfsBlock_packExtDoesNotMapToCacheTable_callsDefaultCache() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK); + DfsBlock dfsBlock = new DfsBlock(dfsStreamKey, 0, new byte[0]); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + tables.put(dfsBlock); + Mockito.verify(defaultBlockCacheTable, times(1)).put(dfsBlock); + } + + @Test + public void putDfsStreamKey_packExtMapsToCacheTable_callsBitmapIndexCacheTable() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX); + Ref<Integer> ref = mock(Ref.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.put(any(DfsStreamKey.class), anyLong(), + anyLong(), anyInt())).thenReturn(mock(Ref.class)); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.put(any(DfsStreamKey.class), anyLong(), + anyLong(), anyInt())).thenReturn(ref); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.put(dfsStreamKey, 0, 0, 0), sameInstance(ref)); + } + + @Test + public void putDfsStreamKey_packExtDoesNotMapToCacheTable_callsDefaultCache() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK); + Ref<Integer> ref = mock(Ref.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.put(any(DfsStreamKey.class), anyLong(), + anyLong(), anyInt())).thenReturn(ref); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.put(any(DfsStreamKey.class), anyLong(), + anyLong(), anyInt())).thenReturn(mock(Ref.class)); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.put(dfsStreamKey, 0, 0, 0), sameInstance(ref)); + } + + @Test + public void putRef_packExtMapsToCacheTable_callsBitmapIndexCacheTable() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX); + Ref<Integer> ref = mock(Ref.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.putRef(any(DfsStreamKey.class), anyLong(), + anyInt())).thenReturn(mock(Ref.class)); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.putRef(any(DfsStreamKey.class), anyLong(), + anyInt())).thenReturn(ref); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.putRef(dfsStreamKey, 0, 0), sameInstance(ref)); + } + + @Test + public void putRef_packExtDoesNotMapToCacheTable_callsDefaultCache() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK); + Ref<Integer> ref = mock(Ref.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.putRef(any(DfsStreamKey.class), anyLong(), + anyInt())).thenReturn(ref); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.putRef(any(DfsStreamKey.class), anyLong(), + anyInt())).thenReturn(mock(Ref.class)); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.putRef(dfsStreamKey, 0, 0), sameInstance(ref)); + } + + @Test + public void contains_packExtMapsToCacheTable_callsBitmapIndexCacheTable() { + DfsStreamKey streamKey = new TestKey(PackExt.BITMAP_INDEX); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.contains(any(DfsStreamKey.class), anyLong())) + .thenReturn(true); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertTrue(tables.contains(streamKey, 0)); + } + + @Test + public void contains_packExtDoesNotMapToCacheTable_callsDefaultCache() { + DfsStreamKey streamKey = new TestKey(PackExt.PACK); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.contains(any(DfsStreamKey.class), + anyLong())).thenReturn(true); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertTrue(tables.contains(streamKey, 0)); + } + + @Test + public void get_packExtMapsToCacheTable_callsBitmapIndexCacheTable() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX); + Ref<Integer> ref = mock(Ref.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.get(any(DfsStreamKey.class), anyLong())) + .thenReturn(mock(Ref.class)); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.get(any(DfsStreamKey.class), anyLong())) + .thenReturn(ref); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.get(dfsStreamKey, 0), sameInstance(ref)); + } + + @Test + public void get_packExtDoesNotMapToCacheTable_callsDefaultCache() { + DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK); + Ref<Integer> ref = mock(Ref.class); + DfsBlockCacheTable defaultBlockCacheTable = mock( + DfsBlockCacheTable.class); + when(defaultBlockCacheTable.get(any(DfsStreamKey.class), anyLong())) + .thenReturn(ref); + DfsBlockCacheTable bitmapIndexCacheTable = mock( + DfsBlockCacheTable.class); + when(bitmapIndexCacheTable.get(any(DfsStreamKey.class), anyLong())) + .thenReturn(mock(Ref.class)); + + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + defaultBlockCacheTable, + Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable)); + + assertThat(tables.get(dfsStreamKey, 0), sameInstance(ref)); + } + + @Test + public void getName() { + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables( + cacheTableWithStats(/* name= */ "defaultName", packStats), + Map.of(PackExt.PACK, cacheTableWithStats(/* name= */ "packName", + packStats))); + + assertThat(tables.getName(), equalTo("defaultName,packName")); + } + + @Test + public void getAllBlockCacheStats() { + String defaultTableName = "default table"; + DfsBlockCacheStats defaultStats = new DfsBlockCacheStats( + defaultTableName); + incrementCounter(4, + () -> defaultStats.incrementHit(new TestKey(PackExt.REFTABLE))); + + String packTableName = "pack table"; + DfsBlockCacheStats packStats = new DfsBlockCacheStats(packTableName); + incrementCounter(5, + () -> packStats.incrementHit(new TestKey(PackExt.PACK))); + + String bitmapTableName = "bitmap table"; + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats( + bitmapTableName); + incrementCounter(6, () -> bitmapStats + .incrementHit(new TestKey(PackExt.BITMAP_INDEX))); + + DfsBlockCacheTable defaultTable = cacheTableWithStats(defaultStats); + DfsBlockCacheTable packTable = cacheTableWithStats(packStats); + DfsBlockCacheTable bitmapTable = cacheTableWithStats(bitmapStats); + PackExtBlockCacheTable tables = PackExtBlockCacheTable + .fromCacheTables(defaultTable, Map.of(PackExt.PACK, packTable, + PackExt.BITMAP_INDEX, bitmapTable)); + + List<BlockCacheStats> statsList = tables.getBlockCacheStats(); + assertThat(statsList, hasSize(3)); + + long[] defaultTableHitCounts = createEmptyStatsArray(); + defaultTableHitCounts[PackExt.REFTABLE.getPosition()] = 4; + assertArrayEquals( + getCacheStatsByName(statsList, defaultTableName).getHitCount(), + defaultTableHitCounts); + + long[] packTableHitCounts = createEmptyStatsArray(); + packTableHitCounts[PackExt.PACK.getPosition()] = 5; + assertArrayEquals( + getCacheStatsByName(statsList, packTableName).getHitCount(), + packTableHitCounts); + + long[] bitmapHitCounts = createEmptyStatsArray(); + bitmapHitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6; + assertArrayEquals( + getCacheStatsByName(statsList, bitmapTableName).getHitCount(), + bitmapHitCounts); + } + + @Test + public void getBlockCacheStats_getCurrentSize_consolidatesAllTableCurrentSizes() { + long[] currentSizes = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + packStats.addToLiveBytes(new TestKey(PackExt.PACK), 5); + currentSizes[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + bitmapStats.addToLiveBytes(new TestKey(PackExt.BITMAP_INDEX), 6); + currentSizes[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + indexStats.addToLiveBytes(new TestKey(PackExt.INDEX), 7); + currentSizes[PackExt.INDEX.getPosition()] = 7; + + PackExtBlockCacheTable tables = PackExtBlockCacheTable + .fromCacheTables(cacheTableWithStats(packStats), + Map.of(PackExt.BITMAP_INDEX, + cacheTableWithStats(bitmapStats), PackExt.INDEX, + cacheTableWithStats(indexStats))); + + assertArrayEquals(AggregatedBlockCacheStats + .fromStatsList(tables.getBlockCacheStats()).getCurrentSize(), + currentSizes); + } + + @Test + public void getBlockCacheStats_GetHitCount_consolidatesAllTableHitCounts() { + long[] hitCounts = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementHit(new TestKey(PackExt.PACK))); + hitCounts[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> bitmapStats + .incrementHit(new TestKey(PackExt.BITMAP_INDEX))); + hitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementHit(new TestKey(PackExt.INDEX))); + hitCounts[PackExt.INDEX.getPosition()] = 7; + + PackExtBlockCacheTable tables = PackExtBlockCacheTable + .fromCacheTables(cacheTableWithStats(packStats), + Map.of(PackExt.BITMAP_INDEX, + cacheTableWithStats(bitmapStats), PackExt.INDEX, + cacheTableWithStats(indexStats))); + + assertArrayEquals(AggregatedBlockCacheStats + .fromStatsList(tables.getBlockCacheStats()).getHitCount(), + hitCounts); + } + + @Test + public void getBlockCacheStats_getMissCount_consolidatesAllTableMissCounts() { + long[] missCounts = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementMiss(new TestKey(PackExt.PACK))); + missCounts[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> bitmapStats + .incrementMiss(new TestKey(PackExt.BITMAP_INDEX))); + missCounts[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX))); + missCounts[PackExt.INDEX.getPosition()] = 7; + + PackExtBlockCacheTable tables = PackExtBlockCacheTable + .fromCacheTables(cacheTableWithStats(packStats), + Map.of(PackExt.BITMAP_INDEX, + cacheTableWithStats(bitmapStats), PackExt.INDEX, + cacheTableWithStats(indexStats))); + + assertArrayEquals(AggregatedBlockCacheStats + .fromStatsList(tables.getBlockCacheStats()).getMissCount(), + missCounts); + } + + @Test + public void getBlockCacheStats_getTotalRequestCount_consolidatesAllTableTotalRequestCounts() { + long[] totalRequestCounts = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, () -> { + packStats.incrementHit(new TestKey(PackExt.PACK)); + packStats.incrementMiss(new TestKey(PackExt.PACK)); + }); + totalRequestCounts[PackExt.PACK.getPosition()] = 10; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> { + bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX)); + bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX)); + }); + totalRequestCounts[PackExt.BITMAP_INDEX.getPosition()] = 12; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, () -> { + indexStats.incrementHit(new TestKey(PackExt.INDEX)); + indexStats.incrementMiss(new TestKey(PackExt.INDEX)); + }); + totalRequestCounts[PackExt.INDEX.getPosition()] = 14; + + PackExtBlockCacheTable tables = PackExtBlockCacheTable + .fromCacheTables(cacheTableWithStats(packStats), + Map.of(PackExt.BITMAP_INDEX, + cacheTableWithStats(bitmapStats), PackExt.INDEX, + cacheTableWithStats(indexStats))); + + assertArrayEquals(AggregatedBlockCacheStats + .fromStatsList(tables.getBlockCacheStats()) + .getTotalRequestCount(), totalRequestCounts); + } + + @Test + public void getBlockCacheStats_getHitRatio_consolidatesAllTableHitRatios() { + long[] hitRatios = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementHit(new TestKey(PackExt.PACK))); + hitRatios[PackExt.PACK.getPosition()] = 100; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> { + bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX)); + bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX)); + }); + hitRatios[PackExt.BITMAP_INDEX.getPosition()] = 50; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX))); + hitRatios[PackExt.INDEX.getPosition()] = 0; + + PackExtBlockCacheTable tables = PackExtBlockCacheTable + .fromCacheTables(cacheTableWithStats(packStats), + Map.of(PackExt.BITMAP_INDEX, + cacheTableWithStats(bitmapStats), PackExt.INDEX, + cacheTableWithStats(indexStats))); + + assertArrayEquals(AggregatedBlockCacheStats + .fromStatsList(tables.getBlockCacheStats()).getHitRatio(), + hitRatios); + } + + @Test + public void getBlockCacheStats_getEvictions_consolidatesAllTableEvictions() { + long[] evictions = createEmptyStatsArray(); + + DfsBlockCacheStats packStats = new DfsBlockCacheStats(); + incrementCounter(5, + () -> packStats.incrementEvict(new TestKey(PackExt.PACK))); + evictions[PackExt.PACK.getPosition()] = 5; + + DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(); + incrementCounter(6, () -> bitmapStats + .incrementEvict(new TestKey(PackExt.BITMAP_INDEX))); + evictions[PackExt.BITMAP_INDEX.getPosition()] = 6; + + DfsBlockCacheStats indexStats = new DfsBlockCacheStats(); + incrementCounter(7, + () -> indexStats.incrementEvict(new TestKey(PackExt.INDEX))); + evictions[PackExt.INDEX.getPosition()] = 7; + + PackExtBlockCacheTable tables = PackExtBlockCacheTable + .fromCacheTables(cacheTableWithStats(packStats), + Map.of(PackExt.BITMAP_INDEX, + cacheTableWithStats(bitmapStats), PackExt.INDEX, + cacheTableWithStats(indexStats))); + + assertArrayEquals(AggregatedBlockCacheStats + .fromStatsList(tables.getBlockCacheStats()).getEvictions(), + evictions); + } + + private BlockCacheStats getCacheStatsByName( + List<BlockCacheStats> blockCacheStats, String name) { + for (BlockCacheStats entry : blockCacheStats) { + if (entry.getName().equals(name)) { + return entry; + } + } + return null; + } + + private static void incrementCounter(int amount, Runnable fn) { + for (int i = 0; i < amount; i++) { + fn.run(); + } + } + + private static long[] createEmptyStatsArray() { + return new long[PackExt.values().length]; + } + + private static DfsBlockCacheTable cacheTableWithStats( + BlockCacheStats dfsBlockCacheStats) { + return cacheTableWithStats(CACHE_NAME, dfsBlockCacheStats); + } + + private static DfsBlockCacheTable cacheTableWithStats(String name, + BlockCacheStats dfsBlockCacheStats) { + DfsBlockCacheTable cacheTable = mock(DfsBlockCacheTable.class); + when(cacheTable.getName()).thenReturn(name); + when(cacheTable.getBlockCacheStats()) + .thenReturn(List.of(dfsBlockCacheStats)); + return cacheTable; + } + + private static class TestKey extends DfsStreamKey { + TestKey(PackExt packExt) { + super(0, packExt); + } + + @Override + public boolean equals(Object o) { + return false; + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java index bd36337f35..41a33df0e4 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java @@ -29,6 +29,7 @@ import java.util.List; import org.eclipse.jgit.errors.AmbiguousObjectException; import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.internal.storage.pack.PackIndexWriter; import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.AbbreviatedObjectId; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndexTest.java new file mode 100644 index 0000000000..f47c385ab9 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndexTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.file; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.internal.storage.file.BasePackBitmapIndex.StoredBitmap; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdOwnerMap; +import org.junit.Before; +import org.junit.Test; + +import com.googlecode.javaewah.EWAHCompressedBitmap; + +public class BasePackBitmapIndexTest { + + private ObjectId baseOid; + + private StoredBitmap baseBitmap; + + private ObjectId xorOid; + + private StoredBitmap xorBitmap; + + private ObjectIdOwnerMap<StoredBitmap> bitmaps; + + @Before + public void setUp() { + baseOid = ObjectId + .fromString("c46f36f2bfc96d6d6f75bd71ee33625293aee690"); + baseBitmap = newBaseStoredBitmap(baseOid, bitmapOf(100)); + xorOid = ObjectId + .fromString("52c18ae15f8fa3787f920e68791367dae2e1af2d"); + xorBitmap = newXorStoredBitmap(xorOid, bitmapOf(200, 300), baseBitmap); + bitmaps = new ObjectIdOwnerMap<>(); + bitmaps.add(baseBitmap); + bitmaps.add(xorBitmap); + } + + @Test + public void testBitmapCounts() { + TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps); + + assertEquals(1, index.getBaseBitmapCount()); + assertEquals(1, index.getXorBitmapCount()); + assertEquals(2, index.getBitmapCount()); + } + + @Test + public void testBitmapCounts_xorResolved() { + TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps); + index.getBitmap(xorOid); + + assertEquals(2, index.getBaseBitmapCount()); + assertEquals(0, index.getXorBitmapCount()); + assertEquals(2, index.getBitmapCount()); + } + + @Test + public void testBitmapSizes() { + TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps); + + assertEquals(baseBitmap.getCurrentSizeInBytes(), + index.getBaseBitmapSizeInBytes()); + assertEquals(xorBitmap.getCurrentSizeInBytes(), + index.getXorBitmapSizeInBytes()); + } + + @Test + public void testBitmapSizes_xorResolved() { + TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps); + index.getBitmap(xorOid); + + assertTrue(baseBitmap.getCurrentSizeInBytes() < index + .getBaseBitmapSizeInBytes()); + assertEquals(0, index.getXorBitmapSizeInBytes()); + } + + private static final StoredBitmap newBaseStoredBitmap(ObjectId oid, + EWAHCompressedBitmap base) { + return new StoredBitmap(oid, base, null, 0); + } + + private static StoredBitmap newXorStoredBitmap(ObjectId oid, + EWAHCompressedBitmap xorMask, StoredBitmap base) { + return new StoredBitmap(oid, xorMask, base, 0); + } + + private static final EWAHCompressedBitmap bitmapOf(int... bits) { + EWAHCompressedBitmap b = new EWAHCompressedBitmap(); + for (int bit : bits) + b.set(bit); + return b; + } + + private static class TestPackBitmapIndex extends BasePackBitmapIndex { + TestPackBitmapIndex(ObjectIdOwnerMap<StoredBitmap> bitmaps) { + super(bitmaps); + } + + @Override + public int findPosition(AnyObjectId objectId) { + throw new IllegalStateException(); + } + + @Override + public ObjectId getObject(int position) + throws IllegalArgumentException { + throw new IllegalStateException(); + } + + @Override + public EWAHCompressedBitmap ofObjectType(EWAHCompressedBitmap bitmap, + int type) { + throw new IllegalStateException(); + } + + @Override + public int getObjectCount() { + throw new IllegalStateException(); + } + + @Override + public int getBitmapCount() { + return getBitmaps().size(); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackWriterTest.java index 2a403c7699..cd73c6ae83 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackWriterTest.java @@ -19,7 +19,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -66,7 +66,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -public class PackWriterTest extends SampleDataRepositoryTestCase { +public class BasePackWriterTest extends SampleDataRepositoryTestCase { private static final List<RevObject> EMPTY_LIST_REVS = Collections .<RevObject> emptyList(); @@ -542,6 +542,39 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { } @Test + public void testWriteReverseIndexConfig() { + assertFalse(config.isWriteReverseIndex()); + config.setWriteReverseIndex(true); + assertTrue(config.isWriteReverseIndex()); + } + + @Test + public void testWriteReverseIndexOff() throws Exception { + config.setWriteReverseIndex(false); + writer = new PackWriter(config, db.newObjectReader()); + ByteArrayOutputStream reverseIndexOutput = new ByteArrayOutputStream(); + + writer.writeReverseIndex(reverseIndexOutput); + + assertEquals(0, reverseIndexOutput.size()); + } + + @Test + public void testWriteReverseIndexOn() throws Exception { + config.setWriteReverseIndex(true); + writeVerifyPack4(false); + ByteArrayOutputStream reverseIndexOutput = new ByteArrayOutputStream(); + int headerBytes = 12; + int bodyBytes = 12; + int footerBytes = 40; + + writer.writeReverseIndex(reverseIndexOutput); + + assertTrue(reverseIndexOutput.size() == headerBytes + bodyBytes + + footerBytes); + } + + @Test public void testExclude() throws Exception { // TestRepository closes repo FileRepository repo = createBareRepository(); @@ -669,13 +702,29 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { } @Test - public void testTotalPackFilesScanWhenSearchForReuseTimeoutNotSet() + public void testTotalPackFilesScanWhenSearchForReuseTimeoutNotSetTrue() + throws Exception { + totalPackFilesScanWhenSearchForReuseTimeoutNotSet(true); + } + + @Test + public void testTotalPackFilesScanWhenSearchForReuseTimeoutNotSetFalse() throws Exception { + totalPackFilesScanWhenSearchForReuseTimeoutNotSet(false); + } + + public void totalPackFilesScanWhenSearchForReuseTimeoutNotSet(boolean doReturn) throws Exception { FileRepository fileRepository = setUpRepoWithMultiplePackfiles(); + int numberOfPackFiles = (int) new GC(fileRepository).getStatistics().numberOfPackFiles; + int objectsInMultiplePacks = 2; + int objectsInOnePacks = 1; + int expectedSelectCalls = objectsInMultiplePacks * (doReturn ? numberOfPackFiles : 1) + + objectsInOnePacks; + PackWriter mockedPackWriter = Mockito .spy(new PackWriter(config, fileRepository.newObjectReader())); - doNothing().when(mockedPackWriter).select(any(), any()); + doReturn(doReturn).when(mockedPackWriter).select(any(), any()); try (FileOutputStream packOS = new FileOutputStream( getPackFileToWrite(fileRepository, mockedPackWriter))) { @@ -683,27 +732,37 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { NullProgressMonitor.INSTANCE, packOS); } - long numberOfPackFiles = new GC(fileRepository) - .getStatistics().numberOfPackFiles; - int expectedSelectCalls = - // Objects contained in multiple packfiles * number of packfiles - 2 * (int) numberOfPackFiles + - // Objects in single packfile - 1; verify(mockedPackWriter, times(expectedSelectCalls)).select(any(), any()); } @Test - public void testTotalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck() + public void testTotalPackFilesScanWhenSkippingSearchForReuseTimeoutCheckTrue() + throws Exception { + totalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck(true); + } + + @Test + public void testTotalPackFilesScanWhenSkippingSearchForReuseTimeoutCheckFalse() throws Exception { + totalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck(false); + } + + public void totalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck( + boolean doReturn) throws Exception { FileRepository fileRepository = setUpRepoWithMultiplePackfiles(); + int numberOfPackFiles = (int) new GC(fileRepository).getStatistics().numberOfPackFiles; + int objectsInMultiplePacks = 2; + int objectsInOnePacks = 1; + int expectedSelectCalls = objectsInMultiplePacks * (doReturn ? numberOfPackFiles : 1) + + objectsInOnePacks; + PackConfig packConfig = new PackConfig(); packConfig.setSearchForReuseTimeout(Duration.ofSeconds(-1)); PackWriter mockedPackWriter = Mockito.spy( new PackWriter(packConfig, fileRepository.newObjectReader())); - doNothing().when(mockedPackWriter).select(any(), any()); + doReturn(doReturn).when(mockedPackWriter).select(any(), any()); try (FileOutputStream packOS = new FileOutputStream( getPackFileToWrite(fileRepository, mockedPackWriter))) { @@ -711,28 +770,31 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { NullProgressMonitor.INSTANCE, packOS); } - long numberOfPackFiles = new GC(fileRepository) - .getStatistics().numberOfPackFiles; - int expectedSelectCalls = - // Objects contained in multiple packfiles * number of packfiles - 2 * (int) numberOfPackFiles + - // Objects contained in single packfile - 1; verify(mockedPackWriter, times(expectedSelectCalls)).select(any(), any()); } @Test - public void testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck() + public void partialPackFilesScanWhenDoingSearchForReuseTimeoutCheck() throws Exception { + int objectsInMultiplePacks = 2; + int objectsInOnePacks = 1; + int expectedSelectCalls = objectsInMultiplePacks + objectsInOnePacks; + testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck(true, expectedSelectCalls); + testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck(false, expectedSelectCalls); + } + + public void testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck( + boolean doReturn, int expectedSelectCalls) throws Exception { FileRepository fileRepository = setUpRepoWithMultiplePackfiles(); + PackConfig packConfig = new PackConfig(); packConfig.setSearchForReuseTimeout(Duration.ofSeconds(-1)); PackWriter mockedPackWriter = Mockito.spy( new PackWriter(packConfig, fileRepository.newObjectReader())); mockedPackWriter.enableSearchForReuseTimeout(); - doNothing().when(mockedPackWriter).select(any(), any()); + doReturn(doReturn).when(mockedPackWriter).select(any(), any()); try (FileOutputStream packOS = new FileOutputStream( getPackFileToWrite(fileRepository, mockedPackWriter))) { @@ -740,7 +802,6 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { NullProgressMonitor.INSTANCE, packOS); } - int expectedSelectCalls = 3; // Objects in packfiles verify(mockedPackWriter, times(expectedSelectCalls)).select(any(), any()); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java index daf4382719..a0afc3ef13 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java @@ -171,7 +171,7 @@ public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase { assertEquals(c2.getResult(), ReceiveCommand.Result.OK); } - File packed = new File(diskRepo.getDirectory(), "packed-refs"); + File packed = new File(diskRepo.getCommonDirectory(), "packed-refs"); String packedStr = new String(Files.readAllBytes(packed.toPath()), UTF_8); @@ -1263,7 +1263,7 @@ public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase { } private ReflogEntry getLastReflog(String name) throws IOException { - ReflogReader r = diskRepo.getReflogReader(name); + ReflogReader r = diskRepo.getRefDatabase().getReflogReader(name); if (r == null) { return null; } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java index 6c7992716c..e8363ce21d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java @@ -81,8 +81,7 @@ public class FileReftableStackTest { } public void testCompaction(int N) throws Exception { - try (FileReftableStack stack = new FileReftableStack( - new File(reftableDir, "refs"), reftableDir, null, + try (FileReftableStack stack = new FileReftableStack(reftableDir, null, () -> new Config())) { writeBranches(stack, "refs/heads/branch%d", 0, N); MergedReftable table = stack.getMergedReftable(); @@ -124,8 +123,7 @@ public class FileReftableStackTest { // Can't delete in-use files on Windows. assumeFalse(SystemReader.getInstance().isWindows()); - try (FileReftableStack stack = new FileReftableStack( - new File(reftableDir, "refs"), reftableDir, null, + try (FileReftableStack stack = new FileReftableStack(reftableDir, null, () -> new Config())) { outer: for (int i = 0; i < 10; i++) { final long next = stack.getMergedReftable().maxUpdateIndex() @@ -152,8 +150,8 @@ public class FileReftableStackTest { } } assertThrows(FileNotFoundException.class, - () -> new FileReftableStack(new File(reftableDir, "refs"), - reftableDir, null, () -> new Config())); + () -> new FileReftableStack(reftableDir, null, + () -> new Config())); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java index 32342e3563..5756b41442 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import java.io.File; import java.io.FileOutputStream; @@ -33,8 +34,15 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; - import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; @@ -51,6 +59,10 @@ import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.TemporaryBuffer; import org.junit.Test; public class FileReftableTest extends SampleDataRepositoryTestCase { @@ -66,6 +78,30 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { @SuppressWarnings("boxing") @Test + public void testReloadIfNecessary() throws Exception { + ObjectId id = db.resolve("master"); + try (FileRepository repo1 = new FileRepository(db.getDirectory()); + FileRepository repo2 = new FileRepository(db.getDirectory())) { + ((FileReftableDatabase) repo1.getRefDatabase()) + .setAutoRefresh(true); + ((FileReftableDatabase) repo2.getRefDatabase()) + .setAutoRefresh(true); + FileRepository repos[] = { repo1, repo2 }; + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 2; j++) { + FileRepository repo = repos[j]; + RefUpdate u = repo.getRefDatabase().newUpdate( + String.format("branch%d", i * 10 + j), false); + u.setNewObjectId(id); + RefUpdate.Result r = u.update(); + assertEquals(Result.NEW, r); + } + } + } + } + + @SuppressWarnings("boxing") + @Test public void testRacyReload() throws Exception { ObjectId id = db.resolve("master"); int retry = 0; @@ -87,13 +123,61 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { u.setNewObjectId(id); r = u.update(); - assertEquals(r, Result.NEW); + assertEquals(Result.NEW, r); } } } // only the first one succeeds - assertEquals(retry, 19); + assertEquals(19, retry); + } + } + + @Test + public void testConcurrentRacyReload() throws Exception { + ObjectId id = db.resolve("master"); + final CyclicBarrier barrier = new CyclicBarrier(2); + + class UpdateRef implements Callable<RefUpdate.Result> { + + private RefUpdate u; + + UpdateRef(FileRepository repo, String branchName) + throws IOException { + u = repo.getRefDatabase().newUpdate(branchName, + false); + u.setNewObjectId(id); + } + + @Override + public RefUpdate.Result call() throws Exception { + barrier.await(); // wait for the other thread to prepare + return u.update(); + } + } + + ExecutorService pool = Executors.newFixedThreadPool(2); + try (FileRepository repo1 = new FileRepository(db.getDirectory()); + FileRepository repo2 = new FileRepository(db.getDirectory())) { + ((FileReftableDatabase) repo1.getRefDatabase()) + .setAutoRefresh(true); + ((FileReftableDatabase) repo2.getRefDatabase()) + .setAutoRefresh(true); + for (int i = 0; i < 10; i++) { + String branchName = String.format("branch%d", + Integer.valueOf(i)); + Future<RefUpdate.Result> ru1 = pool + .submit(new UpdateRef(repo1, branchName)); + Future<RefUpdate.Result> ru2 = pool + .submit(new UpdateRef(repo2, branchName)); + assertTrue((ru1.get() == Result.NEW + && ru2.get() == Result.LOCK_FAILURE) + || (ru1.get() == Result.LOCK_FAILURE + && ru2.get() == Result.NEW)); + } + } finally { + pool.shutdown(); + pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } } @@ -105,13 +189,13 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { RefUpdate u = db.updateRef("refs/heads/master"); u.setForceUpdate(true); u.setNewObjectId((i%2) == 0 ? c1 : c2); - assertEquals(u.update(), FORCED); + assertEquals(FORCED, u.update()); } File tableDir = new File(db.getDirectory(), Constants.REFTABLE); assertTrue(tableDir.listFiles().length > 2); ((FileReftableDatabase)db.getRefDatabase()).compactFully(); - assertEquals(tableDir.listFiles().length,2); + assertEquals(2, tableDir.listFiles().length); } @Test @@ -171,9 +255,10 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { v.update(); db.convertToPackedRefs(true, false); - List<ReflogEntry> logs = db.getReflogReader("refs/heads/master").getReverseEntries(2); - assertEquals(logs.get(0).getComment(), "banana"); - assertEquals(logs.get(1).getComment(), "apple"); + List<ReflogEntry> logs = db.getRefDatabase() + .getReflogReader("refs/heads/master").getReverseEntries(2); + assertEquals("banana", logs.get(0).getComment()); + assertEquals("apple", logs.get(1).getComment()); } @Test @@ -185,8 +270,9 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { ReceiveCommand rc1 = new ReceiveCommand(ObjectId.zeroId(), cur, "refs/heads/batch1"); ReceiveCommand rc2 = new ReceiveCommand(ObjectId.zeroId(), prev, "refs/heads/batch2"); String msg = "message"; + RefDatabase refDb = db.getRefDatabase(); try (RevWalk rw = new RevWalk(db)) { - db.getRefDatabase().newBatchUpdate() + refDb.newBatchUpdate() .addCommand(rc1, rc2) .setAtomic(true) .setRefLogIdent(person) @@ -194,15 +280,17 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { .execute(rw, NullProgressMonitor.INSTANCE); } - assertEquals(rc1.getResult(), ReceiveCommand.Result.OK); - assertEquals(rc2.getResult(), ReceiveCommand.Result.OK); + assertEquals(ReceiveCommand.Result.OK, rc1.getResult()); + assertEquals(ReceiveCommand.Result.OK, rc2.getResult()); - ReflogEntry e = db.getReflogReader("refs/heads/batch1").getLastEntry(); + ReflogEntry e = refDb.getReflogReader("refs/heads/batch1") + .getLastEntry(); assertEquals(msg, e.getComment()); assertEquals(person, e.getWho()); assertEquals(cur, e.getNewId()); - e = db.getReflogReader("refs/heads/batch2").getLastEntry(); + e = refDb.getReflogReader("refs/heads/batch2") + .getLastEntry(); assertEquals(msg, e.getComment()); assertEquals(person, e.getWho()); assertEquals(prev, e.getNewId()); @@ -267,7 +355,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { RefUpdate up = db.getRefDatabase().newUpdate("refs/heads/a", false); up.setForceUpdate(true); RefUpdate.Result res = up.delete(); - assertEquals(res, FORCED); + assertEquals(FORCED, res); assertNull(db.exactRef("refs/heads/a")); } @@ -309,7 +397,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { // the branch HEAD referred to is left untouched assertEquals(pid, db.resolve("refs/heads/master")); - ReflogReader reflogReader = db.getReflogReader("HEAD"); + ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD"); ReflogEntry e = reflogReader.getReverseEntries().get(0); assertEquals(ppid, e.getNewId()); assertEquals("GIT_COMMITTER_EMAIL", e.getWho().getEmailAddress()); @@ -330,12 +418,13 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { updateRef.setForceUpdate(true); RefUpdate.Result update = updateRef.update(); assertEquals(FORCED, update); // internal - ReflogReader r = db.getReflogReader("refs/heads/master"); + ReflogReader r = db.getRefDatabase() + .getReflogReader("refs/heads/master"); ReflogEntry e = r.getLastEntry(); - assertEquals(e.getNewId(), pid); - assertEquals(e.getComment(), "REFLOG!: FORCED"); - assertEquals(e.getWho(), person); + assertEquals(pid, e.getNewId()); + assertEquals("REFLOG!: FORCED", e.getComment()); + assertEquals(person, e.getWho()); } @Test @@ -352,10 +441,11 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { ref = db.updateRef(newRef); ref.setNewObjectId(db.resolve(Constants.HEAD)); - assertEquals(ref.delete(), RefUpdate.Result.NO_CHANGE); + assertEquals(RefUpdate.Result.NO_CHANGE, ref.delete()); // Differs from RefupdateTest. Deleting a loose ref leaves reflog trail. - ReflogReader reader = db.getReflogReader("refs/heads/abc"); + ReflogReader reader = db.getRefDatabase() + .getReflogReader("refs/heads/abc"); assertEquals(ObjectId.zeroId(), reader.getReverseEntry(1).getOldId()); assertEquals(nonZero, reader.getReverseEntry(1).getNewId()); assertEquals(nonZero, reader.getReverseEntry(0).getOldId()); @@ -382,8 +472,9 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { assertNotSame(newid, r.getObjectId()); assertSame(ObjectId.class, r.getObjectId().getClass()); assertEquals(newid, r.getObjectId()); - List<ReflogEntry> reverseEntries1 = db.getReflogReader("refs/heads/abc") - .getReverseEntries(); + RefDatabase refDb = db.getRefDatabase(); + List<ReflogEntry> reverseEntries1 = refDb + .getReflogReader("refs/heads/abc").getReverseEntries(); ReflogEntry entry1 = reverseEntries1.get(0); assertEquals(1, reverseEntries1.size()); assertEquals(ObjectId.zeroId(), entry1.getOldId()); @@ -392,7 +483,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { assertEquals(new PersonIdent(db).toString(), entry1.getWho().toString()); assertEquals("", entry1.getComment()); - List<ReflogEntry> reverseEntries2 = db.getReflogReader("HEAD") + List<ReflogEntry> reverseEntries2 = refDb.getReflogReader("HEAD") .getReverseEntries(); assertEquals(0, reverseEntries2.size()); } @@ -431,7 +522,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { Ref head = db.exactRef("HEAD"); assertTrue(head.isSymbolic()); - assertEquals(head.getTarget().getName(), "refs/heads/unborn"); + assertEquals("refs/heads/unborn", head.getTarget().getName()); } /** @@ -455,7 +546,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { // the branch HEAD referred to is left untouched assertNull(db.resolve("refs/heads/unborn")); - ReflogReader reflogReader = db.getReflogReader("HEAD"); + ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD"); ReflogEntry e = reflogReader.getReverseEntries().get(0); assertEquals(ObjectId.zeroId(), e.getOldId()); assertEquals(ppid, e.getNewId()); @@ -499,7 +590,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { names.add("refs/heads/new/name"); for (String nm : names) { - ReflogReader rd = db.getReflogReader(nm); + ReflogReader rd = db.getRefDatabase().getReflogReader(nm); assertNotNull(rd); ReflogEntry last = rd.getLastEntry(); ObjectId id = last.getNewId(); @@ -573,10 +664,10 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { assertTrue(res == Result.NEW || res == FORCED); } - assertEquals(refDb.exactRef(refName).getObjectId(), bId); + assertEquals(bId, refDb.exactRef(refName).getObjectId()); assertTrue(randomStr.equals(refDb.getReflogReader(refName).getReverseEntry(1).getComment())); refDb.compactFully(); - assertEquals(refDb.exactRef(refName).getObjectId(), bId); + assertEquals(bId, refDb.exactRef(refName).getObjectId()); assertTrue(randomStr.equals(refDb.getReflogReader(refName).getReverseEntry(1).getComment())); } @@ -644,6 +735,54 @@ public class FileReftableTest extends SampleDataRepositoryTestCase { checkContainsRef(refs, db.exactRef("HEAD")); } + @Test + public void testExternalUpdate_bug_102() throws Exception { + ((FileReftableDatabase) db.getRefDatabase()).setAutoRefresh(true); + assumeTrue(atLeastGitVersion(2, 45)); + Git git = Git.wrap(db); + git.tag().setName("foo").call(); + Ref ref = db.exactRef("refs/tags/foo"); + assertNotNull(ref); + runGitCommand("tag", "--force", "foo", "e"); + Ref e = db.exactRef("refs/heads/e"); + Ref foo = db.exactRef("refs/tags/foo"); + assertEquals(e.getObjectId(), foo.getObjectId()); + } + + private String toString(TemporaryBuffer b) throws IOException { + return RawParseUtils.decode(b.toByteArray()); + } + + private ExecutionResult runGitCommand(String... args) + throws IOException, InterruptedException { + FS fs = db.getFS(); + ProcessBuilder pb = fs.runInShell("git", args); + pb.directory(db.getWorkTree()); + System.err.println("PATH=" + pb.environment().get("PATH")); + ExecutionResult result = fs.execute(pb, null); + assertEquals(0, result.getRc()); + String err = toString(result.getStderr()); + if (!err.isEmpty()) { + System.err.println(err); + } + String out = toString(result.getStdout()); + if (!out.isEmpty()) { + System.out.println(out); + } + return result; + } + + private boolean atLeastGitVersion(int minMajor, int minMinor) + throws IOException, InterruptedException { + String version = toString(runGitCommand("version").getStdout()) + .split(" ")[2]; + System.out.println(version); + String[] digits = version.split("\\."); + int major = Integer.parseInt(digits[0]); + int minor = Integer.parseInt(digits[1]); + return (major >= minMajor) && (minor >= minMinor); + } + private RefUpdate updateRef(String name) throws IOException { final RefUpdate ref = db.updateRef(name); ref.setNewObjectId(db.resolve(Constants.HEAD)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java index 100bd32ad8..ed5a6990ac 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java @@ -18,7 +18,6 @@ import org.eclipse.jgit.util.SystemReader; import org.junit.Before; public class FileRepositoryBuilderAfterOpenConfigTest extends FileRepositoryBuilderTest { - /** {@inheritDoc} */ @Before @Override public void setUp() throws Exception { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java index 6cad8b6c62..434f7e4bef 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java @@ -16,9 +16,9 @@ import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; -import java.util.Date; import java.util.List; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; @@ -206,7 +206,7 @@ public class GcBasicPackingTest extends GcTestCase { // The old packfile is too young to be deleted. We should end up with // two pack files - gc.setExpire(new Date(oldPackfile.lastModified() - 1)); + gc.setExpire(Instant.ofEpochMilli(oldPackfile.lastModified() - 1)); gc.gc().get(); stats = gc.getStatistics(); assertEquals(0, stats.numberOfLooseObjects); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java index 1519873b62..8c1b4f7b65 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java @@ -10,7 +10,6 @@ package org.eclipse.jgit.internal.storage.file; -import static java.lang.Integer.valueOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; @@ -73,7 +72,7 @@ public class GcConcurrentTest extends GcTestCase { try { gc.setProgressMonitor(this); gc.repack(); - return valueOf(0); + return Integer.valueOf(0); } catch (IOException e) { // leave the syncPoint in broken state so any awaiting // threads and any threads that call await in the future get @@ -84,7 +83,7 @@ public class GcConcurrentTest extends GcTestCase { } catch (InterruptedException ignored) { // } - return valueOf(1); + return Integer.valueOf(1); } } } @@ -186,13 +185,12 @@ public class GcConcurrentTest extends GcTestCase { // make sure gc() has caused creation of a new packfile assertNotEquals(oldPackName, newPackName); - // Even when asking again for the set of packfiles outdated data - // will be returned. As long as the repository can work on cached data - // it will do so and not detect that a new packfile exists. - assertNotEquals(getSinglePack(repository).getPackName(), newPackName); + // When asking again for the set of packfiles the new updated data + // will be returned because of the rescan of the pack directory. + assertEquals(getSinglePack(repository).getPackName(), newPackName); - // Only when accessing object content it is required to rescan the pack - // directory and the new packfile will be detected. + // When accessing object content the new packfile refreshed from + // the rescan triggered from the list of packs. repository.getObjectDatabase().open(b).getSize(); assertEquals(getSinglePack(repository).getPackName(), newPackName); assertNotNull(getSinglePack(repository).getBitmapIndex()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java index 840c09896d..e1b6778c0e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java @@ -10,8 +10,13 @@ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BUILD_BITMAPS; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_KEPT_OBJECTS; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_REPACK_SECTION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.util.Iterator; @@ -19,9 +24,14 @@ import java.util.Iterator; import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.storage.pack.PackConfig; import org.junit.Test; public class GcKeepFilesTest extends GcTestCase { + private static final int COMMIT_AND_TREE_OBJECTS = 2; + @Test public void testKeepFiles() throws Exception { BranchBuilder bb = tr.branch("refs/heads/master"); @@ -48,11 +58,16 @@ public class GcKeepFilesTest extends GcTestCase { assertEquals(4, stats.numberOfLooseObjects); assertEquals(4, stats.numberOfPackedObjects); assertEquals(1, stats.numberOfPackFiles); + PackFile bitmapFile = singlePack.getPackFile().create(PackExt.BITMAP_INDEX); + assertTrue(keepFile.exists()); + assertTrue(bitmapFile.delete()); + gc.setPackKeptObjects(false); gc.gc().get(); stats = gc.getStatistics(); assertEquals(0, stats.numberOfLooseObjects); assertEquals(8, stats.numberOfPackedObjects); assertEquals(2, stats.numberOfPackFiles); + assertEquals(1, stats.numberOfBitmaps); // check that no object is packed twice Iterator<Pack> packs = repo.getObjectDatabase().getPacks() @@ -68,4 +83,149 @@ public class GcKeepFilesTest extends GcTestCase { + e.toObjectId(), ind2.hasObject(e.toObjectId())); } + + @Test + public void testKeptObjectsAreIncludedByDefault() throws Exception { + testKeptObjectsAreIncluded(); + } + + @Test + public void testKeptObjectsAreIncludedByDefaultWhenBuildBitmapsIsTrue() + throws Exception { + PackConfig packConfig = new PackConfig(); + Config repoConfig = repo.getObjectDatabase().getConfig(); + repoConfig.setBoolean(CONFIG_PACK_SECTION, null, + CONFIG_KEY_BUILD_BITMAPS, true); + packConfig.fromConfig(repoConfig); + gc.setPackConfig(packConfig); + + testKeptObjectsAreIncluded(); + } + + @Test + public void testKeptObjectsAreIncludedWhenPackKeptObjectsIsFalseButOverriddenViaCommandLine() + throws Exception { + PackConfig packConfig = new PackConfig(); + packConfig.setPackKeptObjects(false); + gc.setPackConfig(packConfig); + gc.setPackKeptObjects(true); + + testKeptObjectsAreIncluded(); + } + + @Test + public void testKeptObjectsAreNotIncludedByDefaultWhenBuildBitmapsIsFalse() + throws Exception { + PackConfig packConfig = new PackConfig(); + packConfig.setBuildBitmaps(false); + gc.setPackConfig(packConfig); + + testKeptObjectsAreNotIncluded(); + } + + @Test + public void testKeptObjectsAreIncludedWhenBuildBitmapsIsFalseButPackKeptObjectsIsTrue() + throws Exception { + PackConfig packConfig = new PackConfig(); + Config repoConfig = repo.getObjectDatabase().getConfig(); + repoConfig.setBoolean(CONFIG_PACK_SECTION, null, + CONFIG_KEY_BUILD_BITMAPS, false); + repoConfig.setBoolean(CONFIG_REPACK_SECTION, null, + CONFIG_KEY_PACK_KEPT_OBJECTS, true); + packConfig.fromConfig(repoConfig); + gc.setPackConfig(packConfig); + + testKeptObjectsAreIncluded(); + } + + @Test + public void testKeptObjectsAreNotIncludedWhenPackKeptObjectsIsTrueButOverriddenViaCommandLine() + throws Exception { + PackConfig packConfig = new PackConfig(); + packConfig.setPackKeptObjects(true); + gc.setPackConfig(packConfig); + gc.setPackKeptObjects(false); + + testKeptObjectsAreNotIncluded(); + } + + @Test + public void testKeptObjectsAreNotIncludedWhenPackKeptObjectsConfigIsFalse() + throws Exception { + PackConfig packConfig = new PackConfig(); + packConfig.setPackKeptObjects(false); + gc.setPackConfig(packConfig); + + testKeptObjectsAreNotIncluded(); + } + + private void testKeptObjectsAreIncluded() throws Exception { + BranchBuilder bb = tr.branch("refs/heads/master"); + ObjectId commitObjectInLockedPack = bb.commit().create().toObjectId(); + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(COMMIT_AND_TREE_OBJECTS, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + assertTrue(getSinglePack().getPackFile().create(PackExt.KEEP) + .createNewFile()); + + bb.commit().create(); + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(2 * COMMIT_AND_TREE_OBJECTS + 1, + stats.numberOfPackedObjects); + assertEquals(2, stats.numberOfPackFiles); + + PackIndex lockedPackIdx = null; + PackIndex newPackIdx = null; + for (Pack pack : repo.getObjectDatabase().getPacks()) { + if (pack.getObjectCount() == COMMIT_AND_TREE_OBJECTS) { + lockedPackIdx = pack.getIndex(); + } else { + newPackIdx = pack.getIndex(); + } + } + assertNotNull(lockedPackIdx); + assertTrue(lockedPackIdx.hasObject(commitObjectInLockedPack)); + assertNotNull(newPackIdx); + assertTrue(newPackIdx.hasObject(commitObjectInLockedPack)); + } + + private void testKeptObjectsAreNotIncluded() throws Exception { + BranchBuilder bb = tr.branch("refs/heads/master"); + ObjectId commitObjectInLockedPack = bb.commit().create().toObjectId(); + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(COMMIT_AND_TREE_OBJECTS, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + assertTrue(getSinglePack().getPackFile().create(PackExt.KEEP) + .createNewFile()); + + bb.commit().create(); + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(COMMIT_AND_TREE_OBJECTS + 1, stats.numberOfPackedObjects); + assertEquals(2, stats.numberOfPackFiles); + + PackIndex lockedPackIdx = null; + PackIndex newPackIdx = null; + for (Pack pack : repo.getObjectDatabase().getPacks()) { + if (pack.getObjectCount() == COMMIT_AND_TREE_OBJECTS) { + lockedPackIdx = pack.getIndex(); + } else { + newPackIdx = pack.getIndex(); + } + } + assertNotNull(lockedPackIdx); + assertTrue(lockedPackIdx.hasObject(commitObjectInLockedPack)); + assertNotNull(newPackIdx); + assertFalse(newPackIdx.hasObject(commitObjectInLockedPack)); + } + + private Pack getSinglePack() { + Iterator<Pack> packIt = repo.getObjectDatabase().getPacks().iterator(); + Pack singlePack = packIt.next(); + assertFalse(packIt.hasNext()); + return singlePack; + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java new file mode 100644 index 0000000000..cd1264ef55 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.file; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.stream.StreamSupport; + +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +public class GcNumberOfPackFilesSinceBitmapStatisticsTest extends GcTestCase { + @Test + public void testShouldReportZeroObjectsForInitializedRepo() + throws IOException { + assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap); + } + + @Test + public void testShouldReportAllPackFilesWhenNoGcWasPerformed() + throws Exception { + tr.packAndPrune(); + long result = gc.getStatistics().numberOfPackFilesSinceBitmap; + + assertEquals(repo.getObjectDatabase().getPacks().size(), result); + } + + @Test + public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception { + // given + addCommit(null); + gc.gc().get(); + assertEquals(1L, repositoryBitmapFiles()); + assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap); + } + + @Test + public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses() + throws Exception { + // commit & gc + RevCommit parent = addCommit(null); + gc.gc().get(); + assertEquals(1L, repositoryBitmapFiles()); + + // progress & pack + addCommit(parent); + tr.packAndPrune(); + + assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap); + } + + @Test + public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses() + throws Exception { + // commit & gc + RevCommit parent = addCommit(null); + gc.gc().get(); + assertEquals(1L, repositoryBitmapFiles()); + + // progress & gc + parent = addCommit(parent); + gc.gc().get(); + assertEquals(2L, repositoryBitmapFiles()); + + // progress & pack + addCommit(parent); + tr.packAndPrune(); + + assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap); + } + + private RevCommit addCommit(RevCommit parent) throws Exception { + PersonIdent ident = new PersonIdent("repo-metrics", "repo@metrics.com"); + TestRepository<FileRepository>.CommitBuilder builder = tr.commit() + .author(ident); + if (parent != null) { + builder.parent(parent); + } + RevCommit commit = builder.create(); + tr.update("master", commit); + parent = commit; + return parent; + } + + private long repositoryBitmapFiles() throws IOException { + return StreamSupport + .stream(Files + .newDirectoryStream(repo.getObjectDatabase() + .getPackDirectory().toPath(), "pack-*.bitmap") + .spliterator(), false) + .count(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcObjectSizeIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcObjectSizeIndexTest.java new file mode 100644 index 0000000000..1a05d88583 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcObjectSizeIndexTest.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2025, Google LLC. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +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 java.io.File; +import java.io.IOException; +import java.util.Collection; + +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.pack.PackConfig; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GcObjectSizeIndexTest extends GcTestCase { + + @Test + public void gc_2commits_noSizeLimit_blobsInIndex() throws Exception { + TestRepository<FileRepository>.BranchBuilder bb = tr + .branch("refs/heads/master"); + RevBlob blobA1 = tr.blob("7-bytes"); + RevBlob blobA2 = tr.blob("11-bytes xx"); + RevBlob blobB1 = tr.blob("B"); + RevBlob blobB2 = tr.blob("B2"); + bb.commit().add("A", blobA1).add("B", blobB1).create(); + bb.commit().add("A", blobA2).add("B", blobB2).create(); + + stats = gc.getStatistics(); + assertEquals(8, stats.numberOfLooseObjects); + assertEquals(0, stats.numberOfPackedObjects); + configureGc(gc, 0); + gc.gc().get(); + + stats = gc.getStatistics(); + assertEquals(1, stats.numberOfPackFiles); + assertEquals(4, stats.numberOfSizeIndexedObjects); + + assertTrue(getOnlyPack(repo).hasObjectSizeIndex()); + Pack pack = getOnlyPack(repo); + assertEquals(7, pack.getIndexedObjectSize(blobA1)); + assertEquals(11, pack.getIndexedObjectSize(blobA2)); + assertEquals(1, pack.getIndexedObjectSize(blobB1)); + assertEquals(2, pack.getIndexedObjectSize(blobB2)); + } + + @Test + public void gc_2commits_sizeLimit_biggerBlobsInIndex() throws Exception { + TestRepository<FileRepository>.BranchBuilder bb = tr + .branch("refs/heads/master"); + RevBlob blobA1 = tr.blob("7-bytes"); + RevBlob blobA2 = tr.blob("11-bytes xx"); + RevBlob blobB1 = tr.blob("B"); + RevBlob blobB2 = tr.blob("B2"); + bb.commit().add("A", blobA1).add("B", blobB1).create(); + bb.commit().add("A", blobA2).add("B", blobB2).create(); + + stats = gc.getStatistics(); + assertEquals(8, stats.numberOfLooseObjects); + assertEquals(0, stats.numberOfPackedObjects); + configureGc(gc, 5); + gc.gc().get(); + + stats = gc.getStatistics(); + assertEquals(1, stats.numberOfPackFiles); + assertEquals(2, stats.numberOfSizeIndexedObjects); + + assertTrue(getOnlyPack(repo).hasObjectSizeIndex()); + Pack pack = getOnlyPack(repo); + assertEquals(7, pack.getIndexedObjectSize(blobA1)); + assertEquals(11, pack.getIndexedObjectSize(blobA2)); + assertEquals(-1, pack.getIndexedObjectSize(blobB1)); + assertEquals(-1, pack.getIndexedObjectSize(blobB2)); + } + + @Test + public void gc_2commits_disableSizeIdx_noIdx() throws Exception { + TestRepository<FileRepository>.BranchBuilder bb = tr + .branch("refs/heads/master"); + RevBlob blobA1 = tr.blob("7-bytes"); + RevBlob blobA2 = tr.blob("11-bytes xx"); + RevBlob blobB1 = tr.blob("B"); + RevBlob blobB2 = tr.blob("B2"); + bb.commit().add("A", blobA1).add("B", blobB1).create(); + bb.commit().add("A", blobA2).add("B", blobB2).create(); + + stats = gc.getStatistics(); + assertEquals(8, stats.numberOfLooseObjects); + assertEquals(0, stats.numberOfPackedObjects); + configureGc(gc, -1); + gc.gc().get(); + + + stats = gc.getStatistics(); + assertEquals(1, stats.numberOfPackFiles); + assertEquals(0, stats.numberOfSizeIndexedObjects); + } + + @Test + public void gc_alreadyPacked_noChanges() + throws Exception { + tr.branch("refs/heads/master").commit().add("A", "A").add("B", "B") + .create(); + stats = gc.getStatistics(); + assertEquals(4, stats.numberOfLooseObjects); + assertEquals(0, stats.numberOfPackedObjects); + configureGc(gc, 0); + gc.gc().get(); + + stats = gc.getStatistics(); + assertEquals(4, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + assertTrue(getOnlyPack(repo).hasObjectSizeIndex()); + assertEquals(2, stats.numberOfSizeIndexedObjects); + + // Do the gc again and check that it hasn't changed anything + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(4, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + assertTrue(getOnlyPack(repo).hasObjectSizeIndex()); + assertEquals(2, stats.numberOfSizeIndexedObjects); + } + + @Test + public void gc_twoReachableCommits_oneUnreachable_twoPacks() + throws Exception { + TestRepository<FileRepository>.BranchBuilder bb = tr + .branch("refs/heads/master"); + RevCommit first = bb.commit().add("A", "A").add("B", "B").create(); + bb.commit().add("A", "A2").add("B", "B2").create(); + tr.update("refs/heads/master", first); + + stats = gc.getStatistics(); + assertEquals(8, stats.numberOfLooseObjects); + assertEquals(0, stats.numberOfPackedObjects); + configureGc(gc, 0); + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(0, stats.numberOfLooseObjects); + assertEquals(8, stats.numberOfPackedObjects); + assertEquals(2, stats.numberOfPackFiles); + assertEquals(4, stats.numberOfSizeIndexedObjects); + } + + @Test + public void gc_preserved_objSizeIdxIsPreserved() throws Exception { + Collection<Pack> oldPacks = preserveOldPacks(); + assertEquals(1, oldPacks.size()); + PackFile preserved = oldPacks.iterator().next().getPackFile() + .create(PackExt.OBJECT_SIZE_INDEX) + .createPreservedForDirectory( + repo.getObjectDatabase().getPreservedDirectory()); + assertTrue(preserved.exists()); + } + + @Test + public void gc_preserved_prune_noPreserves() throws Exception { + preserveOldPacks(); + configureGc(gc, 0).setPrunePreserved(true); + gc.gc().get(); + + assertFalse(repo.getObjectDatabase().getPreservedDirectory().exists()); + } + + private Collection<Pack> preserveOldPacks() throws Exception { + TestRepository<FileRepository>.BranchBuilder bb = tr + .branch("refs/heads/master"); + bb.commit().message("P").add("P", "P").create(); + + // pack loose object into packfile + configureGc(gc, 0); + gc.setExpireAgeMillis(0); + gc.gc().get(); + Collection<Pack> oldPacks = tr.getRepository().getObjectDatabase() + .getPacks(); + PackFile oldPackfile = oldPacks.iterator().next().getPackFile(); + assertTrue(oldPackfile.exists()); + + fsTick(); + bb.commit().message("B").add("B", "Q").create(); + + // repack again but now without a grace period for packfiles. We should + // end up with a new packfile and the old one should be placed in the + // preserved directory + gc.setPackExpireAgeMillis(0); + configureGc(gc, 0).setPreserveOldPacks(true); + gc.gc().get(); + + File preservedPackFile = oldPackfile.createPreservedForDirectory( + repo.getObjectDatabase().getPreservedDirectory()); + assertTrue(preservedPackFile.exists()); + return oldPacks; + } + + @Ignore + public void testPruneAndRestoreOldPacks() throws Exception { + String tempRef = "refs/heads/soon-to-be-unreferenced"; + TestRepository<FileRepository>.BranchBuilder bb = tr.branch(tempRef); + bb.commit().add("A", "A").add("B", "B").create(); + + // Verify setup conditions + stats = gc.getStatistics(); + assertEquals(4, stats.numberOfLooseObjects); + assertEquals(0, stats.numberOfPackedObjects); + + // Force all referenced objects into packs (to avoid having loose objects) + configureGc(gc, 0); + gc.setExpireAgeMillis(0); + gc.setPackExpireAgeMillis(0); + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(0, stats.numberOfLooseObjects); + assertEquals(4, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + + // Delete the temp ref, orphaning its commit + RefUpdate update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false); + update.setForceUpdate(true); + ObjectId objectId = update.getOldObjectId(); // remember it so we can restore it! + RefUpdate.Result result = update.delete(); + assertEquals(RefUpdate.Result.FORCED, result); + + fsTick(); + + // Repack with only orphaned commit, so packfile will be pruned + configureGc(gc, 0).setPreserveOldPacks(true); + gc.gc().get(); + stats = gc.getStatistics(); + assertEquals(0, stats.numberOfLooseObjects); + assertEquals(0, stats.numberOfPackedObjects); + assertEquals(0, stats.numberOfPackFiles); + + // Restore the temp ref to the deleted commit, should restore old-packs! + update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false); + update.setNewObjectId(objectId); + update.setExpectedOldObjectId(null); + result = update.update(); + assertEquals(RefUpdate.Result.NEW, result); + + stats = gc.getStatistics(); + assertEquals(4, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + } + + private PackConfig configureGc(GC myGc, int minSize) { + PackConfig pconfig = new PackConfig(repo); + pconfig.setMinBytesForObjSizeIndex(minSize); + myGc.setPackConfig(pconfig); + return pconfig; + } + + private Pack getOnlyPack(FileRepository fileRepo) + throws IOException { + Collection<Pack> packs = fileRepo.getObjectDatabase().getPacks(); + if (packs.size() != 1) { + throw new IOException("More than one pack"); + } + + return packs.iterator().next(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java index 8baa3cc341..f84be21e82 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import java.io.File; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.BrokenBarrierException; @@ -31,6 +30,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.PackRefsCommand; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; @@ -49,7 +50,7 @@ public class GcPackRefsTest extends GcTestCase { RevBlob a = tr.blob("a"); tr.lightweightTag("t", a); - gc.packRefs(); + packRefs(false); assertSame(repo.exactRef("refs/tags/t").getStorage(), Storage.PACKED); } @@ -58,9 +59,9 @@ public class GcPackRefsTest extends GcTestCase { String ref = "dir/ref"; tr.branch(ref).commit().create(); String name = repo.findRef(ref).getName(); - Path dir = repo.getDirectory().toPath().resolve(name).getParent(); + Path dir = repo.getCommonDirectory().toPath().resolve(name).getParent(); assertNotNull(dir); - gc.packRefs(); + packRefs(true); assertFalse(Files.exists(dir)); } @@ -75,9 +76,9 @@ public class GcPackRefsTest extends GcTestCase { Callable<Integer> packRefs = () -> { syncPoint.await(); try { - gc.packRefs(); + packRefs(false); return 0; - } catch (IOException e) { + } catch (GitAPIException e) { return 1; } }; @@ -102,7 +103,7 @@ public class GcPackRefsTest extends GcTestCase { "refs/tags/t1")); try { refLock.lock(); - gc.packRefs(); + packRefs(false); } finally { refLock.unlock(); } @@ -145,7 +146,7 @@ public class GcPackRefsTest extends GcTestCase { Future<Result> result2 = pool.submit(() -> { refUpdateLockedRef.await(); - gc.packRefs(); + packRefs(false); packRefsDone.await(); return null; }); @@ -173,19 +174,20 @@ public class GcPackRefsTest extends GcTestCase { assertEquals(repo.exactRef("HEAD").getTarget().getName(), "refs/heads/master"); assertNull(repo.exactRef("HEAD").getTarget().getObjectId()); - gc.packRefs(); + PackRefsCommand packRefsCommand = git.packRefs().setAll(true); + packRefsCommand.call(); assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE); assertEquals(repo.exactRef("HEAD").getTarget().getName(), "refs/heads/master"); assertNull(repo.exactRef("HEAD").getTarget().getObjectId()); git.checkout().setName("refs/heads/side").call(); - gc.packRefs(); + packRefsCommand.call(); assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE); // check for detached HEAD git.checkout().setName(first.getName()).call(); - gc.packRefs(); + packRefsCommand.call(); assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE); } @@ -208,7 +210,7 @@ public class GcPackRefsTest extends GcTestCase { assertEquals(repo.exactRef("HEAD").getTarget().getName(), "refs/heads/master"); assertNull(repo.exactRef("HEAD").getTarget().getObjectId()); - gc.packRefs(); + packRefs(true); assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE); assertEquals(repo.exactRef("HEAD").getTarget().getName(), "refs/heads/master"); @@ -216,9 +218,14 @@ public class GcPackRefsTest extends GcTestCase { // check for non-detached HEAD repo.updateRef(Constants.HEAD).link("refs/heads/side"); - gc.packRefs(); + packRefs(true); assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE); assertEquals(repo.exactRef("HEAD").getTarget().getObjectId(), second.getId()); } + + private void packRefs(boolean all) throws GitAPIException { + new PackRefsCommand(repo).setAll(all).call(); + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java index ca0f6842fc..84ec132e24 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java @@ -16,8 +16,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.File; +import java.time.Instant; import java.util.Collections; -import java.util.Date; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; import org.eclipse.jgit.lib.ObjectId; @@ -30,7 +30,7 @@ public class GcPruneNonReferencedTest extends GcTestCase { @Test public void nonReferencedNonExpiredObject_notPruned() throws Exception { RevBlob a = tr.blob("a"); - gc.setExpire(new Date(lastModified(a))); + gc.setExpire(Instant.ofEpochMilli(lastModified(a))); gc.prune(Collections.<ObjectId> emptySet()); assertTrue(repo.getObjectDatabase().has(a)); } @@ -58,7 +58,7 @@ public class GcPruneNonReferencedTest extends GcTestCase { @Test public void nonReferencedObjects_onlyExpiredPruned() throws Exception { RevBlob a = tr.blob("a"); - gc.setExpire(new Date(lastModified(a) + 1)); + gc.setExpire(Instant.ofEpochMilli(lastModified(a) + 1)); fsTick(); RevBlob b = tr.blob("b"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java index e6c1ee5fd6..29f180d76b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java @@ -30,7 +30,7 @@ public class GcReflogTest extends GcTestCase { BranchBuilder bb = tr.branch("refs/heads/master"); bb.commit().add("A", "A").add("B", "B").create(); bb.commit().add("A", "A2").add("B", "B2").create(); - new File(repo.getDirectory(), Constants.LOGS + "/refs/heads/master") + new File(repo.getCommonDirectory(), Constants.LOGS + "/refs/heads/master") .delete(); stats = gc.getStatistics(); assertEquals(8, stats.numberOfLooseObjects); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReverseIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReverseIndexTest.java new file mode 100644 index 0000000000..cbb0943426 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReverseIndexTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2023, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.file; + +import static org.eclipse.jgit.internal.storage.pack.PackExt.REVERSE_INDEX; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Collections; + +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.util.IO; +import org.junit.Test; + +public class GcReverseIndexTest extends GcTestCase { + + @Test + public void testWriteDefault() throws Exception { + PackConfig config = new PackConfig(repo); + gc.setPackConfig(config); + + RevCommit tip = commitChain(10); + TestRepository.BranchBuilder bb = tr.branch("refs/heads/main"); + bb.update(tip); + + gc.gc().get(); + assertRidxDoesNotExist(repo); + } + + @Test + public void testWriteDisabled() throws Exception { + PackConfig config = new PackConfig(repo); + config.setWriteReverseIndex(false); + gc.setPackConfig(config); + + RevCommit tip = commitChain(10); + TestRepository.BranchBuilder bb = tr.branch("refs/heads/main"); + bb.update(tip); + + gc.gc().get(); + assertRidxDoesNotExist(repo); + } + + @Test + public void testWriteEmptyRepo() throws Exception { + PackConfig config = new PackConfig(repo); + config.setWriteReverseIndex(true); + gc.setPackConfig(config); + + gc.gc().get(); + assertRidxDoesNotExist(repo); + } + + @Test + public void testWriteShallowRepo() throws Exception { + PackConfig config = new PackConfig(repo); + config.setWriteReverseIndex(true); + gc.setPackConfig(config); + + RevCommit tip = commitChain(2); + TestRepository.BranchBuilder bb = tr.branch("refs/heads/main"); + bb.update(tip); + repo.getObjectDatabase().setShallowCommits(Collections.singleton(tip)); + + gc.gc().get(); + assertValidRidxExists(repo); + } + + @Test + public void testWriteEnabled() throws Exception { + PackConfig config = new PackConfig(repo); + config.setWriteReverseIndex(true); + gc.setPackConfig(config); + + RevCommit tip = commitChain(10); + TestRepository.BranchBuilder bb = tr.branch("refs/heads/main"); + bb.update(tip); + + gc.gc().get(); + assertValidRidxExists(repo); + } + + private static void assertValidRidxExists(FileRepository repo) + throws Exception { + PackFile packFile = repo.getObjectDatabase().getPacks().iterator() + .next().getPackFile(); + File file = packFile.create(REVERSE_INDEX); + assertTrue(file.exists()); + try (InputStream os = new FileInputStream(file)) { + byte[] magic = new byte[4]; + IO.readFully(os, magic, 0, 4); + assertArrayEquals(new byte[] { 'R', 'I', 'D', 'X' }, magic); + } + } + + private static void assertRidxDoesNotExist(FileRepository repo) { + File packDir = repo.getObjectDatabase().getPackDirectory(); + String[] reverseIndexFilenames = packDir.list( + (dir, name) -> name.endsWith(REVERSE_INDEX.getExtension())); + assertEquals(0, reverseIndexFilenames.length); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java new file mode 100644 index 0000000000..af52e2cb85 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.file; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collection; +import java.util.stream.StreamSupport; + +import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.pack.PackConfig; +import org.junit.Test; + +public class GcSinceBitmapStatisticsTest extends GcTestCase { + @Test + public void testShouldReportZeroPacksAndObjectsForInitializedRepo() + throws IOException { + RepoStatistics s = gc.getStatistics(); + assertEquals(0L, s.numberOfPackFilesSinceBitmap); + assertEquals(0L, s.numberOfObjectsSinceBitmap); + } + + @Test + public void testShouldReportAllPackFilesWhenNoGcWasPerformed() + throws Exception { + tr.packAndPrune(); + long result = gc.getStatistics().numberOfPackFilesSinceBitmap; + + assertEquals(repo.getObjectDatabase().getPacks().size(), result); + } + + @Test + public void testShouldReportAllObjectsWhenNoGcWasPerformed() + throws Exception { + tr.packAndPrune(); + + assertEquals( + getNumberOfObjectsInPacks(repo.getObjectDatabase().getPacks()), + gc.getStatistics().numberOfObjectsSinceBitmap); + } + + @Test + public void testShouldReportNoPacksFilesSinceBitmapWhenPackfilesAreOlderThanBitmapFile() + throws Exception { + addCommit(null); + configureGC(/* buildBitmap */ false).gc().get(); + assertEquals(1L, gc.getStatistics().numberOfPackFiles); + assertEquals(0L, repositoryBitmapFiles()); + assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap); + + addCommit(null); + configureGC(/* buildBitmap */ true).gc().get(); + + assertEquals(1L, repositoryBitmapFiles()); + assertEquals(2L, gc.getStatistics().numberOfPackFiles); + assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap); + } + + @Test + public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception { + // given + addCommit(null); + assertEquals(2L, gc.getStatistics().numberOfObjectsSinceBitmap); + + gc.gc().get(); + assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap); + } + + @Test + public void testShouldReportNewPacksSinceGcWhenRepositoryProgresses() + throws Exception { + // commit & gc + RevCommit parent = addCommit(null); + gc.gc().get(); + assertEquals(1L, repositoryBitmapFiles()); + + // progress & pack + addCommit(parent); + assertEquals(1L, gc.getStatistics().numberOfPackFiles); + assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap); + + tr.packAndPrune(); + assertEquals(2L, gc.getStatistics().numberOfPackFiles); + assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap); + } + + @Test + public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses() + throws Exception { + // commit & gc + RevCommit parent = addCommit(null); + gc.gc().get(); + assertEquals(0L, gc.getStatistics().numberOfLooseObjects); + assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap); + + // progress & pack + addCommit(parent); + assertEquals(1L, gc.getStatistics().numberOfLooseObjects); + assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap); + + tr.packAndPrune(); + assertEquals(0L, gc.getStatistics().numberOfLooseObjects); + // Number of objects contained in the newly created PackFile + assertEquals(3L, gc.getStatistics().numberOfObjectsSinceBitmap); + } + + @Test + public void testShouldReportNewPacksFromTheLatestBitmapWhenRepositoryProgresses() + throws Exception { + // commit & gc + RevCommit parent = addCommit(null); + gc.gc().get(); + assertEquals(1L, repositoryBitmapFiles()); + + // progress & gc + parent = addCommit(parent); + gc.gc().get(); + assertEquals(2L, repositoryBitmapFiles()); + + // progress & pack + addCommit(parent); + tr.packAndPrune(); + + assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap); + } + + @Test + public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses() + throws Exception { + // commit & gc + RevCommit parent = addCommit(null); + gc.gc().get(); + + // progress & gc + parent = addCommit(parent); + gc.gc().get(); + assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap); + + // progress & pack + addCommit(parent); + assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap); + + tr.packAndPrune(); + assertEquals(4L, gc.getStatistics().numberOfObjectsSinceBitmap); + } + + private RevCommit addCommit(RevCommit parent) throws Exception { + return tr.branch("master").commit() + .author(new PersonIdent("repo-metrics", "repo@metrics.com")) + .parent(parent).create(); + } + + private long repositoryBitmapFiles() throws IOException { + return StreamSupport + .stream(Files + .newDirectoryStream(repo.getObjectDatabase() + .getPackDirectory().toPath(), "pack-*.bitmap") + .spliterator(), false) + .count(); + } + + private long getNumberOfObjectsInPacks(Collection<Pack> packs) { + return packs.stream().mapToLong(pack -> { + try { + return pack.getObjectCount(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).sum(); + } + + private GC configureGC(boolean buildBitmap) { + PackConfig pc = new PackConfig(repo.getObjectDatabase().getConfig()); + pc.setBuildBitmaps(buildBitmap); + gc.setPackConfig(pc); + return gc; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java index 48f6e06385..8b27b829b2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java @@ -61,6 +61,7 @@ public abstract class GcTestCase extends LocalDiskRepositoryTestCase { * the depth of the commit chain. * @return the commit that is the tip of the commit chain * @throws Exception + * if an error occurred */ protected RevCommit commitChain(int depth) throws Exception { if (depth <= 0) @@ -93,6 +94,7 @@ public abstract class GcTestCase extends LocalDiskRepositoryTestCase { * number of files added per commit * @return the commit that is the tip of the commit chain * @throws Exception + * if an error occurred */ protected RevCommit commitChain(int depth, int width) throws Exception { if (depth <= 0) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java index 7eab1dcb09..953d624bfe 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java @@ -209,7 +209,8 @@ public class LockFileTest extends RepositoryTestCase { lock.unlock(); lock.unlock(); } catch (Throwable e) { - fail("unlock should be noop if not locked at all."); + throw new AssertionError( + "unlock should be noop if not locked at all.", e); } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java index 746a0a1ff3..33cbc868ca 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java @@ -49,7 +49,10 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import java.io.File; @@ -66,6 +69,7 @@ import java.util.concurrent.Future; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; @@ -207,33 +211,35 @@ public class ObjectDirectoryTest extends RepositoryTestCase { .fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b"); WindowCursor curs = new WindowCursor(db.getObjectDatabase()); - LooseObjects mock = mock(LooseObjects.class); + Config config = new Config(); + config.setString("core", null, "trustLooseObjectStat", "ALWAYS"); + LooseObjects spy = Mockito.spy(new LooseObjects(config, trash)); UnpackedObjectCache unpackedObjectCacheMock = mock( UnpackedObjectCache.class); - Mockito.when(mock.getObjectLoader(any(), any(), any())) - .thenThrow(new IOException("Stale File Handle")); - Mockito.when(mock.open(curs, id)).thenCallRealMethod(); - Mockito.when(mock.unpackedObjectCache()) - .thenReturn(unpackedObjectCacheMock); + doThrow(new IOException("Stale File Handle")).when(spy) + .getObjectLoader(any(), any(), any()); + doReturn(unpackedObjectCacheMock).when(spy).unpackedObjectCache(); - assertNull(mock.open(curs, id)); + assertNull(spy.open(curs, id)); verify(unpackedObjectCacheMock).remove(id); } - @Test + @Test(expected = IOException.class) public void testOpenLooseObjectPropagatesIOExceptions() throws Exception { ObjectId id = ObjectId .fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b"); WindowCursor curs = new WindowCursor(db.getObjectDatabase()); - LooseObjects mock = mock(LooseObjects.class); + Config config = new Config(); + config.setString("core", null, "trustLooseObjectStat", "NEVER"); + LooseObjects spy = spy(new LooseObjects(config, + db.getObjectDatabase().getDirectory())); - Mockito.when(mock.getObjectLoader(any(), any(), any())) - .thenThrow(new IOException("some IO failure")); - Mockito.when(mock.open(curs, id)).thenCallRealMethod(); + doThrow(new IOException("some IO failure")).when(spy) + .getObjectLoader(any(), any(), any()); - assertThrows(IOException.class, () -> mock.open(curs, id)); + spy.open(curs, id); } @Test @@ -243,17 +249,18 @@ public class ObjectDirectoryTest extends RepositoryTestCase { db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null, ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true); - WindowCursor curs = new WindowCursor(db.getObjectDatabase()); - assertTrue(curs.getCommitGraph().isEmpty()); - commitFile("file.txt", "content", "master"); - GC gc = new GC(db); - gc.gc().get(); - assertTrue(curs.getCommitGraph().isPresent()); + try (WindowCursor curs = new WindowCursor(db.getObjectDatabase())) { + assertTrue(curs.getCommitGraph().isEmpty()); + commitFile("file.txt", "content", "master"); + GC gc = new GC(db); + gc.gc().get(); + assertTrue(curs.getCommitGraph().isPresent()); - db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, - ConfigConstants.CONFIG_COMMIT_GRAPH, false); + db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_COMMIT_GRAPH, false); - assertTrue(curs.getCommitGraph().isEmpty()); + assertTrue(curs.getCommitGraph().isEmpty()); + } } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java index 67bba18e2b..1f934acced 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java @@ -13,6 +13,7 @@ package org.eclipse.jgit.internal.storage.file; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; @@ -25,6 +26,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; @@ -66,7 +68,9 @@ public abstract class PackIndexTestCase extends RepositoryTestCase { * Verify CRC32 support. * * @throws MissingObjectException + * object is missing in the underlying index * @throws UnsupportedOperationException + * the index doesn't have CRC */ public abstract void testCRC32() throws MissingObjectException, UnsupportedOperationException; @@ -97,6 +101,39 @@ public abstract class PackIndexTestCase extends RepositoryTestCase { } } + @Test + public void testIteratorMutableEntryCompareTo() { + Iterator<PackIndex.MutableEntry> iterA = smallIdx.iterator(); + Iterator<PackIndex.MutableEntry> iterB = smallIdx.iterator(); + + MutableEntry aEntry = iterA.next(); + iterB.next(); + MutableEntry bEntry = iterB.next(); + // b is one ahead + assertTrue(aEntry.compareBySha1To(bEntry) < 0); + assertTrue(bEntry.compareBySha1To(aEntry) > 0); + + // advance a, now should be equal + assertEquals(0, iterA.next().compareBySha1To(bEntry)); + } + + @Test + public void testIteratorMutableEntryCopyTo() { + Iterator<PackIndex.MutableEntry> it = smallIdx.iterator(); + + MutableObjectId firstOidCopy = new MutableObjectId(); + MutableEntry next = it.next(); + next.copyOidTo(firstOidCopy); + ObjectId firstImmutable = next.toObjectId(); + + MutableEntry second = it.next(); + + // The copy has the right value after "next" + assertTrue(firstImmutable.equals(firstOidCopy)); + assertFalse("iterator has moved", + second.toObjectId().equals(firstImmutable)); + } + /** * Test results of iterator comparing to content of well-known (prepared) * small index. @@ -104,22 +141,22 @@ public abstract class PackIndexTestCase extends RepositoryTestCase { @Test public void testIteratorReturnedValues1() { Iterator<PackIndex.MutableEntry> iter = smallIdx.iterator(); - assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904", iter.next() - .name()); - assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab", iter.next() - .name()); - assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259", iter.next() - .name()); - assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3", iter.next() - .name()); - assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7", iter.next() - .name()); - assertEquals("902d5476fa249b7abc9d84c611577a81381f0327", iter.next() - .name()); - assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035", iter.next() - .name()); - assertEquals("c59759f143fb1fe21c197981df75a7ee00290799", iter.next() - .name()); + assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904", + iter.next().name()); + assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab", + iter.next().name()); + assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259", + iter.next().name()); + assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3", + iter.next().name()); + assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7", + iter.next().name()); + assertEquals("902d5476fa249b7abc9d84c611577a81381f0327", + iter.next().name()); + assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035", + iter.next().name()); + assertEquals("c59759f143fb1fe21c197981df75a7ee00290799", + iter.next().name()); assertFalse(iter.hasNext()); } @@ -196,16 +233,16 @@ public abstract class PackIndexTestCase extends RepositoryTestCase { @Test public void testIteratorReturnedValues2() { Iterator<PackIndex.MutableEntry> iter = denseIdx.iterator(); - while (!iter.next().name().equals( - "0a3d7772488b6b106fb62813c4d6d627918d9181")) { + while (!iter.next().name() + .equals("0a3d7772488b6b106fb62813c4d6d627918d9181")) { // just iterating } - assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3", iter.next() - .name()); // same level-1 - assertEquals("10da5895682013006950e7da534b705252b03be6", iter.next() - .name()); // same level-1 - assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8", iter.next() - .name()); + assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3", + iter.next().name()); // same level-1 + assertEquals("10da5895682013006950e7da534b705252b03be6", + iter.next().name()); // same level-1 + assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8", + iter.next().name()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java index 85043034aa..cc43d3c2bb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java @@ -53,6 +53,7 @@ import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; @@ -77,12 +78,14 @@ import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectStream; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.util.IO; @@ -489,6 +492,38 @@ public class PackInserterTest extends RepositoryTestCase { } } + @Test + public void createsObjectSizeIndex() throws Exception { + FileBasedConfig jGitConfig = mockSystemReader.getJGitConfig(); + jGitConfig.setInt( + ConfigConstants.CONFIG_PACK_SECTION, + null, + ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 10); + jGitConfig.save(); + byte[] oneBlob = Constants.encode("a blob with some content"); + byte[] anotherBlob = Constants.encode("some more contents"); + byte[] streamMeBlob = Constants.encode("some more content to write"); + + ObjectId oneBlobOid, anotherBlobOid, streamMeBlobOid; + try (PackInserter ins = newInserter()) { + oneBlobOid = ins.insert(OBJ_BLOB, oneBlob); + anotherBlobOid = ins.insert(OBJ_BLOB, anotherBlob); + streamMeBlobOid = ins.insert(OBJ_BLOB, streamMeBlob.length, + new ByteArrayInputStream(streamMeBlob)); + ins.flush(); + } + + List<Pack> listPacks = listPacks(db); + assertEquals(1, listPacks.size()); + Pack thePack = listPacks.get(0); + assertTrue(thePack.hasObjectSizeIndex()); + assertEquals(oneBlob.length, thePack.getIndexedObjectSize(oneBlobOid)); + assertEquals(anotherBlob.length, + thePack.getIndexedObjectSize(anotherBlobOid)); + assertEquals(streamMeBlob.length, + thePack.getIndexedObjectSize(streamMeBlobOid)); + } + private List<Pack> listPacks() throws Exception { List<Pack> fromOpenDb = listPacks(db); List<Pack> reopened; @@ -549,7 +584,8 @@ public class PackInserterTest extends RepositoryTestCase { } private void assertPacksOnly() throws Exception { - new BadFileCollector(f -> !f.endsWith(".pack") && !f.endsWith(".idx")) + new BadFileCollector(f -> !f.endsWith(".pack") && !f.endsWith(".idx") + && !f.endsWith(".objsize")) .assertNoBadFiles(db.getObjectDatabase().getDirectory()); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputedTest.java new file mode 100644 index 0000000000..ea5aaf5dd4 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputedTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com> + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.file; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.PackMismatchException; +import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.junit.Before; +import org.junit.Test; + +public class PackReverseIndexComputedTest extends RepositoryTestCase { + + private PackIndex idx; + + private PackReverseIndex reverseIdx; + + /** + * Set up tested class instance, test constructor by the way. + */ + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + // index with both small (< 2^31) and big offsets + idx = PackIndex.open(JGitTestUtil.getTestResourceFile("pack-huge.idx")); + reverseIdx = PackReverseIndexFactory.computeFromIndex(idx); + } + + /** + * Test findObject() for all index entries. + */ + @Test + public void testFindObject() { + for (MutableEntry me : idx) + assertEquals(me.toObjectId(), reverseIdx.findObject(me.getOffset())); + } + + /** + * Test findObject() with illegal argument. + */ + @Test + public void testFindObjectWrongOffset() { + assertNull(reverseIdx.findObject(0)); + } + + /** + * Test findNextOffset() for all index entries. + * + * @throws CorruptObjectException + */ + @Test + public void testFindNextOffset() throws CorruptObjectException { + long offset = findFirstOffset(); + assertTrue(offset > 0); + for (int i = 0; i < idx.getObjectCount(); i++) { + long newOffset = reverseIdx.findNextOffset(offset, Long.MAX_VALUE); + assertTrue(newOffset > offset); + if (i == idx.getObjectCount() - 1) + assertEquals(newOffset, Long.MAX_VALUE); + else + assertEquals(newOffset, idx.findOffset(reverseIdx + .findObject(newOffset))); + offset = newOffset; + } + } + + /** + * Test findNextOffset() with wrong illegal argument as offset. + */ + @Test + public void testFindNextOffsetWrongOffset() { + try { + reverseIdx.findNextOffset(0, Long.MAX_VALUE); + fail("findNextOffset() should throw exception"); + } catch (CorruptObjectException x) { + // expected + } + } + + @Test + public void testVerifyChecksum() throws PackMismatchException { + // ComputedReverseIndex doesn't have a file containing a checksum. + reverseIdx.verifyPackChecksum(null); + } + + private long findFirstOffset() { + long min = Long.MAX_VALUE; + for (MutableEntry me : idx) + min = Math.min(min, me.getOffset()); + return min; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java index 292e3e758a..f8fb4c15e7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java @@ -1,6 +1,5 @@ /* - * Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com> - * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others + * Copyright (C) 2022, Google LLC and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -8,95 +7,94 @@ * * SPDX-License-Identifier: BSD-3-Clause */ - package org.eclipse.jgit.internal.storage.file; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.junit.JGitTestUtil; -import org.eclipse.jgit.junit.RepositoryTestCase; -import org.junit.Before; import org.junit.Test; -public class PackReverseIndexTest extends RepositoryTestCase { +public class PackReverseIndexTest { - private PackIndex idx; - - private PackReverseIndex reverseIdx; + @Test + public void open_fallbackToComputed() throws IOException { + String noRevFilePrefix = "pack-3280af9c07ee18a87705ef50b0cc4cd20266cf12."; + PackReverseIndex computed = PackReverseIndexFactory.openOrCompute( + getResourceFileFor(noRevFilePrefix, PackExt.REVERSE_INDEX), 7, + () -> PackIndex.open( + getResourceFileFor(noRevFilePrefix, PackExt.INDEX))); - /** - * Set up tested class instance, test constructor by the way. - */ - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - // index with both small (< 2^31) and big offsets - idx = PackIndex.open(JGitTestUtil.getTestResourceFile( - "pack-huge.idx")); - reverseIdx = new PackReverseIndex(idx); + assertTrue(computed instanceof PackReverseIndexComputed); } - /** - * Test findObject() for all index entries. - */ @Test - public void testFindObject() { - for (MutableEntry me : idx) - assertEquals(me.toObjectId(), reverseIdx.findObject(me.getOffset())); + public void open_readGoodFile() throws IOException { + String hasRevFilePrefix = "pack-cbdeda40019ae0e6e789088ea0f51f164f489d14."; + PackReverseIndex version1 = PackReverseIndexFactory.openOrCompute( + getResourceFileFor(hasRevFilePrefix, PackExt.REVERSE_INDEX), 6, + () -> PackIndex.open( + getResourceFileFor(hasRevFilePrefix, PackExt.INDEX))); + + assertTrue(version1 instanceof PackReverseIndexV1); } - /** - * Test findObject() with illegal argument. - */ @Test - public void testFindObjectWrongOffset() { - assertNull(reverseIdx.findObject(0)); + public void open_readCorruptFile() { + String hasRevFilePrefix = "pack-cbdeda40019ae0e6e789088ea0f51f164f489d14."; + + assertThrows(IOException.class, + () -> PackReverseIndexFactory.openOrCompute( + getResourceFileFor(hasRevFilePrefix + "corrupt.", + PackExt.REVERSE_INDEX), + 6, () -> PackIndex.open(getResourceFileFor( + hasRevFilePrefix, PackExt.INDEX)))); } - /** - * Test findNextOffset() for all index entries. - * - * @throws CorruptObjectException - */ @Test - public void testFindNextOffset() throws CorruptObjectException { - long offset = findFirstOffset(); - assertTrue(offset > 0); - for (int i = 0; i < idx.getObjectCount(); i++) { - long newOffset = reverseIdx.findNextOffset(offset, Long.MAX_VALUE); - assertTrue(newOffset > offset); - if (i == idx.getObjectCount() - 1) - assertEquals(newOffset, Long.MAX_VALUE); - else - assertEquals(newOffset, idx.findOffset(reverseIdx - .findObject(newOffset))); - offset = newOffset; - } + public void read_badMagic() { + byte[] badMagic = new byte[] { 'R', 'B', 'A', 'D', // magic + 0x00, 0x00, 0x00, 0x01, // file version + 0x00, 0x00, 0x00, 0x01, // oid version + // pack checksum + 'P', 'A', 'C', 'K', 'C', 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3', + '4', '5', '6', '7', '8', '9', '0', + // checksum + 0x66, 0x01, (byte) 0xbc, (byte) 0xe8, 0x51, 0x4b, 0x2f, + (byte) 0xa1, (byte) 0xa9, (byte) 0xcd, (byte) 0xbe, (byte) 0xd6, + 0x4f, (byte) 0xa8, 0x7d, (byte) 0xab, 0x50, (byte) 0xa3, + (byte) 0xf7, (byte) 0xcc, }; + ByteArrayInputStream in = new ByteArrayInputStream(badMagic); + + assertThrows(IOException.class, + () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null)); } - /** - * Test findNextOffset() with wrong illegal argument as offset. - */ @Test - public void testFindNextOffsetWrongOffset() { - try { - reverseIdx.findNextOffset(0, Long.MAX_VALUE); - fail("findNextOffset() should throw exception"); - } catch (CorruptObjectException x) { - // expected - } + public void read_unsupportedVersion2() { + byte[] version2 = new byte[] { 'R', 'I', 'D', 'X', // magic + 0x00, 0x00, 0x00, 0x02, // file version + 0x00, 0x00, 0x00, 0x01, // oid version + // pack checksum + 'P', 'A', 'C', 'K', 'C', 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3', + '4', '5', '6', '7', '8', '9', '0', + // checksum + 0x70, 0x17, 0x10, 0x51, (byte) 0xfe, (byte) 0xab, (byte) 0x9b, + 0x68, (byte) 0xed, 0x3a, 0x3f, 0x27, 0x1d, (byte) 0xce, + (byte) 0xff, 0x38, 0x09, (byte) 0x9b, 0x29, 0x58, }; + ByteArrayInputStream in = new ByteArrayInputStream(version2); + + assertThrows(IOException.class, + () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null)); } - private long findFirstOffset() { - long min = Long.MAX_VALUE; - for (MutableEntry me : idx) - min = Math.min(min, me.getOffset()); - return min; + private File getResourceFileFor(String packFilePrefix, PackExt ext) { + return JGitTestUtil + .getTestResourceFile(packFilePrefix + ext.getExtension()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1Test.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1Test.java new file mode 100644 index 0000000000..38b28b501b --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1Test.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2022, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.file; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.PackMismatchException; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.PackedObjectInfo; +import org.junit.Before; +import org.junit.Test; + +public class PackReverseIndexV1Test { + private static final byte[] FAKE_PACK_CHECKSUM = new byte[] { 'P', 'A', 'C', + 'K', 'C', 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3', '4', '5', '6', + '7', '8', '9', '0', }; + + private static final byte[] NO_OBJECTS = new byte[] { 'R', 'I', 'D', 'X', // magic + 0x00, 0x00, 0x00, 0x01, // file version + 0x00, 0x00, 0x00, 0x01, // oid version + // pack checksum to copy into at byte 12 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // checksum + (byte) 0xd1, 0x1d, 0x17, (byte) 0xd5, (byte) 0xa1, 0x5c, + (byte) 0x8f, 0x45, 0x7e, 0x06, (byte) 0x91, (byte) 0xf2, 0x7e, 0x20, + 0x35, 0x2c, (byte) 0xdc, 0x4c, 0x46, (byte) 0xe4, }; + + private static final byte[] SMALL_PACK_CHECKSUM = new byte[] { (byte) 0xbb, + 0x1d, 0x25, 0x3d, (byte) 0xd3, (byte) 0xf0, 0x08, 0x75, (byte) 0xc8, + 0x04, (byte) 0xd0, 0x6f, 0x73, (byte) 0xe9, 0x00, (byte) 0x82, + (byte) 0xdb, 0x09, (byte) 0xc8, 0x13, }; + + private static final byte[] SMALL_CONTENTS = new byte[] { 'R', 'I', 'D', + 'X', // magic + 0x00, 0x00, 0x00, 0x01, // file version + 0x00, 0x00, 0x00, 0x01, // oid version + 0x00, 0x00, 0x00, 0x04, // offset 12: "68" -> index @ 4 + 0x00, 0x00, 0x00, 0x02, // offset 165: "5c" -> index @ 2 + 0x00, 0x00, 0x00, 0x03, // offset 257: "62" -> index @ 3 + 0x00, 0x00, 0x00, 0x01, // offset 450: "58" -> index @ 1 + 0x00, 0x00, 0x00, 0x05, // offset 556: "c5" -> index @ 5 + 0x00, 0x00, 0x00, 0x00, // offset 614: "2d" -> index @ 0 + // pack checksum to copy into at byte 36 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // checksum + (byte) 0xf0, 0x6d, 0x03, (byte) 0xd7, 0x6f, (byte) 0x9f, + (byte) 0xc1, 0x36, 0x26, (byte) 0xbc, (byte) 0xcb, 0x75, 0x36, + (byte) 0xa1, 0x26, 0x6a, 0x2b, (byte) 0x84, 0x16, (byte) 0x83, }; + + private PackReverseIndex emptyReverseIndex; + + /** + * Reverse index for the pack-cbdeda40019ae0e6e789088ea0f51f164f489d14.idx + * with contents `SHA-1 type size size-in-packfile offset-in-packfile` as + * shown by `verify-pack`: + * 2d04ee74dba30078c2dcdb713ddb8be4bc084d76 blob 8 17 614 + * 58728c938a9a8b9970cc09236caf94ada4689923 blob 140 106 450 + * 5ce00008cf3fb8f194f52742020bd40d78f3f1b3 commit 81 92 165 1 68cb1f232964f3cd698afc1dafe583937203c587 + * 62299a7ae290d685196e948a2fcb7d8c07f95c7d tree 198 193 257 + * 68cb1f232964f3cd698afc1dafe583937203c587 commit 220 153 12 + * c5ab27309491cf641eb11bb4b7a78641f280b482 tree 46 58 556 1 62299a7ae290d685196e948a2fcb7d8c07f95c7d + */ + private PackReverseIndex smallReverseIndex; + + private final PackedObjectInfo object614 = objectInfo( + "2d04ee74dba30078c2dcdb713ddb8be4bc084d76", OBJ_BLOB, 614); + + private final PackedObjectInfo object450 = objectInfo( + "58728c938a9a8b9970cc09236caf94ada4689923", OBJ_BLOB, 450); + + private final PackedObjectInfo object165 = objectInfo( + "5ce00008cf3fb8f194f52742020bd40d78f3f1b3", OBJ_COMMIT, 165); + + private final PackedObjectInfo object257 = objectInfo( + "62299a7ae290d685196e948a2fcb7d8c07f95c7d", OBJ_TREE, 257); + + private final PackedObjectInfo object12 = objectInfo( + "68cb1f232964f3cd698afc1dafe583937203c587", OBJ_COMMIT, 12); + + private final PackedObjectInfo object556 = objectInfo( + "c5ab27309491cf641eb11bb4b7a78641f280b482", OBJ_TREE, 556); + + // last object's offset + last object's length + private final long smallMaxOffset = 631; + + @Before + public void setUp() throws Exception { + System.arraycopy(SMALL_PACK_CHECKSUM, 0, SMALL_CONTENTS, 36, + SMALL_PACK_CHECKSUM.length); + ByteArrayInputStream smallIn = new ByteArrayInputStream(SMALL_CONTENTS); + smallReverseIndex = PackReverseIndexFactory.readFromFile(smallIn, 6, + () -> PackIndex.open(JGitTestUtil.getTestResourceFile( + "pack-cbdeda40019ae0e6e789088ea0f51f164f489d14.idx"))); + + System.arraycopy(FAKE_PACK_CHECKSUM, 0, NO_OBJECTS, 12, + FAKE_PACK_CHECKSUM.length); + ByteArrayInputStream emptyIn = new ByteArrayInputStream(NO_OBJECTS); + emptyReverseIndex = PackReverseIndexFactory.readFromFile(emptyIn, 0, + () -> null); + } + + @Test + public void read_unsupportedOidSHA256() { + byte[] version2 = new byte[] { 'R', 'I', 'D', 'X', // magic + 0x00, 0x00, 0x00, 0x01, // file version + 0x00, 0x00, 0x00, 0x02, // oid version + // pack checksum to copy into at byte 12 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // checksum + 0x6e, 0x78, 0x75, 0x67, (byte) 0x84, (byte) 0x89, (byte) 0xde, + (byte) 0xe3, (byte) 0x86, 0x6a, 0x3b, (byte) 0x98, 0x51, + (byte) 0xd8, (byte) 0x8c, (byte) 0xec, 0x50, (byte) 0xe7, + (byte) 0xfb, 0x22, }; + System.arraycopy(FAKE_PACK_CHECKSUM, 0, version2, 12, + FAKE_PACK_CHECKSUM.length); + ByteArrayInputStream in = new ByteArrayInputStream(version2); + + assertThrows(IOException.class, + () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null)); + } + + @Test + public void read_objectCountTooLarge() { + ByteArrayInputStream dummyInput = new ByteArrayInputStream(NO_OBJECTS); + long biggerThanInt = ((long) Integer.MAX_VALUE) + 1; + + assertThrows(IllegalArgumentException.class, + () -> PackReverseIndexFactory.readFromFile(dummyInput, + biggerThanInt, + () -> null)); + } + + @Test + public void read_incorrectChecksum() { + byte[] badChecksum = new byte[] { 'R', 'I', 'D', 'X', // magic + 0x00, 0x00, 0x00, 0x01, // file version + 0x00, 0x00, 0x00, 0x01, // oid version + // pack checksum to copy into at byte 12 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // checksum + (byte) 0xf2, 0x1a, 0x1a, (byte) 0xaa, 0x32, 0x2d, (byte) 0xb9, + (byte) 0xfd, 0x0f, (byte) 0xa5, 0x4c, (byte) 0xea, (byte) 0xcf, + (byte) 0xbb, (byte) 0x99, (byte) 0xde, (byte) 0xd3, 0x4e, + (byte) 0xb1, (byte) 0xee, // would be 0x74 if correct + }; + System.arraycopy(FAKE_PACK_CHECKSUM, 0, badChecksum, 12, + FAKE_PACK_CHECKSUM.length); + ByteArrayInputStream in = new ByteArrayInputStream(badChecksum); + assertThrows(CorruptObjectException.class, + () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null)); + } + + @Test + public void findObject_noObjects() { + assertNull(emptyReverseIndex.findObject(0)); + } + + @Test + public void findObject_multipleObjects() { + assertEquals(object614, smallReverseIndex.findObject(614)); + assertEquals(object450, smallReverseIndex.findObject(450)); + assertEquals(object165, smallReverseIndex.findObject(165)); + assertEquals(object257, smallReverseIndex.findObject(257)); + assertEquals(object12, smallReverseIndex.findObject(12)); + assertEquals(object556, smallReverseIndex.findObject(556)); + } + + @Test + public void findObject_badOffset() { + assertNull(smallReverseIndex.findObject(0)); + } + + @Test + public void findNextOffset_noObjects() { + assertThrows(IOException.class, + () -> emptyReverseIndex.findNextOffset(0, Long.MAX_VALUE)); + } + + @Test + public void findNextOffset_multipleObjects() throws CorruptObjectException { + assertEquals(smallMaxOffset, + smallReverseIndex.findNextOffset(614, smallMaxOffset)); + assertEquals(614, + smallReverseIndex.findNextOffset(556, smallMaxOffset)); + assertEquals(556, + smallReverseIndex.findNextOffset(450, smallMaxOffset)); + assertEquals(450, + smallReverseIndex.findNextOffset(257, smallMaxOffset)); + assertEquals(257, + smallReverseIndex.findNextOffset(165, smallMaxOffset)); + assertEquals(165, smallReverseIndex.findNextOffset(12, smallMaxOffset)); + } + + @Test + public void findNextOffset_badOffset() { + assertThrows(IOException.class, + () -> smallReverseIndex.findNextOffset(0, Long.MAX_VALUE)); + } + + @Test + public void findPosition_noObjects() { + assertEquals(-1, emptyReverseIndex.findPosition(0)); + } + + @Test + public void findPosition_multipleObjects() { + assertEquals(0, smallReverseIndex.findPosition(12)); + assertEquals(1, smallReverseIndex.findPosition(165)); + assertEquals(2, smallReverseIndex.findPosition(257)); + assertEquals(3, smallReverseIndex.findPosition(450)); + assertEquals(4, smallReverseIndex.findPosition(556)); + assertEquals(5, smallReverseIndex.findPosition(614)); + } + + @Test + public void findPosition_badOffset() { + assertEquals(-1, smallReverseIndex.findPosition(10)); + } + + @Test + public void findObjectByPosition_noObjects() { + assertThrows(AssertionError.class, + () -> emptyReverseIndex.findObjectByPosition(0)); + } + + @Test + public void findObjectByPosition_multipleObjects() { + assertEquals(object12, smallReverseIndex.findObjectByPosition(0)); + assertEquals(object165, smallReverseIndex.findObjectByPosition(1)); + assertEquals(object257, smallReverseIndex.findObjectByPosition(2)); + assertEquals(object450, smallReverseIndex.findObjectByPosition(3)); + assertEquals(object556, smallReverseIndex.findObjectByPosition(4)); + assertEquals(object614, smallReverseIndex.findObjectByPosition(5)); + } + + @Test + public void findObjectByPosition_badOffset() { + assertThrows(AssertionError.class, + () -> smallReverseIndex.findObjectByPosition(10)); + } + + @Test + public void verifyChecksum_match() throws IOException { + smallReverseIndex.verifyPackChecksum("smallPackFilePath"); + } + + @Test + public void verifyChecksum_mismatch() throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(NO_OBJECTS); + PackIndex mockForwardIndex = mock(PackIndex.class); + when(mockForwardIndex.getChecksum()).thenReturn( + new byte[] { 'D', 'I', 'F', 'F', 'P', 'A', 'C', 'K', 'C', 'H', + 'E', 'C', 'K', 'S', 'U', 'M', '7', '8', '9', '0', }); + PackReverseIndex reverseIndex = PackReverseIndexFactory.readFromFile(in, + 0, + () -> mockForwardIndex); + + assertThrows(PackMismatchException.class, + () -> reverseIndex.verifyPackChecksum("packFilePath")); + } + + private static PackedObjectInfo objectInfo(String objectId, int type, + long offset) { + PackedObjectInfo objectInfo = new PackedObjectInfo( + ObjectId.fromString(objectId)); + objectInfo.setType(type); + objectInfo.setOffset(offset); + return objectInfo; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1WriteReadTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1WriteReadTest.java new file mode 100644 index 0000000000..372a4c7cba --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1WriteReadTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.file; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.PackedObjectInfo; +import org.junit.Test; + +public class PackReverseIndexV1WriteReadTest { + + private static byte[] PACK_CHECKSUM = new byte[] { 'P', 'A', 'C', 'K', 'C', + 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3', '4', '5', '6', '7', '8', + '9', '0', }; + + @Test + public void writeThenRead_noObjects() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PackReverseIndexWriter writer = PackReverseIndexWriter.createWriter(out, + 1); + List<PackedObjectInfo> objectsSortedByName = new ArrayList<>(); + + // write + writer.write(objectsSortedByName, PACK_CHECKSUM); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + + // read + PackReverseIndex noObjectsReverseIndex = PackReverseIndexFactory + .readFromFile(in, 0, () -> null); + + // use + assertThrows(AssertionError.class, + () -> noObjectsReverseIndex.findObjectByPosition(0)); + } + + @Test + public void writeThenRead_oneObject() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PackReverseIndexWriter writer = PackReverseIndexWriter.createWriter(out, + 1); + PackedObjectInfo a = objectInfo("a", OBJ_COMMIT, 0); + List<PackedObjectInfo> objectsSortedByName = List.of(a); + + // write + writer.write(objectsSortedByName, PACK_CHECKSUM); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + PackIndex mockForwardIndex = mock(PackIndex.class); + when(mockForwardIndex.getObjectId(0)).thenReturn(a); + + // read + PackReverseIndex oneObjectReverseIndex = PackReverseIndexFactory + .readFromFile(in, 1, () -> mockForwardIndex); + + // use + assertEquals(a, oneObjectReverseIndex.findObjectByPosition(0)); + } + + @Test + public void writeThenRead_multipleObjectsLargeOffsets() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PackReverseIndexWriter writer = PackReverseIndexWriter.createWriter(out, + 1); + PackedObjectInfo a = objectInfo("a", OBJ_BLOB, 200000000); + PackedObjectInfo b = objectInfo("b", OBJ_COMMIT, 0); + PackedObjectInfo c = objectInfo("c", OBJ_COMMIT, 52000000000L); + PackedObjectInfo d = objectInfo("d", OBJ_TREE, 7); + PackedObjectInfo e = objectInfo("e", OBJ_COMMIT, 38000000000L); + List<PackedObjectInfo> objectsSortedByName = List.of(a, b, c, d, e); + + writer.write(objectsSortedByName, PACK_CHECKSUM); + + // write + writer.write(objectsSortedByName, PACK_CHECKSUM); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + PackIndex mockForwardIndex = mock(PackIndex.class); + when(mockForwardIndex.getObjectId(4)).thenReturn(e); + + // read + PackReverseIndex multipleObjectsReverseIndex = PackReverseIndexFactory + .readFromFile(in, 5, () -> mockForwardIndex); + + // use with minimal mocked forward index use + assertEquals(e, multipleObjectsReverseIndex.findObjectByPosition(3)); + } + + private static PackedObjectInfo objectInfo(String objectId, int type, + long offset) { + assert (objectId.length() == 1); + PackedObjectInfo objectInfo = new PackedObjectInfo( + ObjectId.fromString(objectId.repeat(OBJECT_ID_STRING_LENGTH))); + objectInfo.setType(type); + objectInfo.setOffset(offset); + return objectInfo; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java index a3596541fe..016a6afd70 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java @@ -10,6 +10,7 @@ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -29,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import java.util.zip.Deflater; import org.eclipse.jgit.errors.LargeObjectException; @@ -39,6 +41,7 @@ import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRng; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -47,6 +50,7 @@ import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectStream; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.transport.PackParser; import org.eclipse.jgit.transport.PackedObjectInfo; @@ -261,7 +265,7 @@ public class PackTest extends LocalDiskRepositoryTestCase { new PackIndexWriterV1(f).write(list, footer); } - Pack pack = new Pack(packName, null); + Pack pack = new Pack(repo.getConfig(), packName, null); try { pack.get(wc, b); fail("expected LargeObjectException.ExceedsByteArrayLimit"); @@ -295,6 +299,29 @@ public class PackTest extends LocalDiskRepositoryTestCase { } } + @Test + public void testObjectSize() throws Exception { + byte[] data = getRng().nextBytes(300); + RevBlob aBlob = tr.blob(data); + RevCommit aCommit = tr.branch("master").commit().add("A", aBlob).create(); + repo.getConfig().setInt(CONFIG_PACK_SECTION, null, ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 0); + tr.packAndPrune(); + + List<Pack> packs = repo.getObjectDatabase().getPacks().stream().collect(Collectors.toList()); + assertEquals(1, packs.size()); + // Indexed object + assertEquals(300, packs.get(0).getIndexedObjectSize(aBlob)); + assertEquals(300, packs.get(0).getObjectSize(wc, aBlob)); + // Non indexed object + assertEquals(-1, packs.get(0).getIndexedObjectSize(aCommit)); + assertEquals(168, packs.get(0).getObjectSize(wc, aCommit)); + // Object not in pack + assertEquals(-1, packs.get(0).getObjectSize(wc, + ObjectId.fromString("1111111111111111111111111111111111111111"))); + assertEquals(-1, packs.get(0).getIndexedObjectSize( + ObjectId.fromString("1111111111111111111111111111111111111111"))); + } + private static byte[] clone(int first, byte[] base) { byte[] r = new byte[base.length]; System.arraycopy(base, 1, r, 1, r.length - 1); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java index 42304e2253..3ea4a167cb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java @@ -17,7 +17,6 @@ import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.util.SystemReader; public class RefDirectoryAfterOpenConfigTest extends RefDirectoryTest { - /** {@inheritDoc} */ @Override public void refDirectorySetup() throws Exception { StoredConfig userConfig = SystemReader.getInstance().getUserConfig(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java index 619e585a90..baa0182b87 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java @@ -51,6 +51,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; import org.junit.Before; import org.junit.Test; @@ -89,25 +90,26 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { @Test public void testCreate() throws IOException { // setUp above created the directory. We just have to test it. - File d = diskRepo.getDirectory(); + File gitDir = diskRepo.getDirectory(); + File commonDir = diskRepo.getCommonDirectory(); assertSame(diskRepo, refdir.getRepository()); - assertTrue(new File(d, "refs").isDirectory()); - assertTrue(new File(d, "logs").isDirectory()); - assertTrue(new File(d, "logs/refs").isDirectory()); - assertFalse(new File(d, "packed-refs").exists()); + assertTrue(new File(commonDir, "refs").isDirectory()); + assertTrue(new File(commonDir, "logs").isDirectory()); + assertTrue(new File(commonDir, "logs/refs").isDirectory()); + assertFalse(new File(commonDir, "packed-refs").exists()); - assertTrue(new File(d, "refs/heads").isDirectory()); - assertTrue(new File(d, "refs/tags").isDirectory()); - assertEquals(2, new File(d, "refs").list().length); - assertEquals(0, new File(d, "refs/heads").list().length); - assertEquals(0, new File(d, "refs/tags").list().length); + assertTrue(new File(commonDir, "refs/heads").isDirectory()); + assertTrue(new File(commonDir, "refs/tags").isDirectory()); + assertEquals(2, new File(commonDir, "refs").list().length); + assertEquals(0, new File(commonDir, "refs/heads").list().length); + assertEquals(0, new File(commonDir, "refs/tags").list().length); - assertTrue(new File(d, "logs/refs/heads").isDirectory()); - assertFalse(new File(d, "logs/HEAD").exists()); - assertEquals(0, new File(d, "logs/refs/heads").list().length); + assertTrue(new File(commonDir, "logs/refs/heads").isDirectory()); + assertFalse(new File(gitDir, "logs/HEAD").exists()); + assertEquals(0, new File(commonDir, "logs/refs/heads").list().length); - assertEquals("ref: refs/heads/master\n", read(new File(d, HEAD))); + assertEquals("ref: refs/heads/master\n", read(new File(gitDir, HEAD))); } @Test(expected = UnsupportedOperationException.class) @@ -1349,6 +1351,18 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { assertEquals(Storage.LOOSE, ref.getStorage()); } + @Test + public void testCommonRefPrefix() { + assertEquals("", StringUtils.commonPrefix()); + assertEquals("HEAD", StringUtils.commonPrefix("HEAD")); + assertEquals("", StringUtils.commonPrefix("HEAD", "")); + assertEquals("", StringUtils.commonPrefix("HEAD", "refs/heads/")); + assertEquals("refs/heads/", + StringUtils.commonPrefix("refs/heads/master", "refs/heads/")); + assertEquals("refs/heads/", + StringUtils.commonPrefix("refs/heads/", "refs/heads/main")); + } + void writePackedRef(String name, AnyObjectId id) throws IOException { writePackedRefs(id.name() + " " + name + "\n"); } @@ -1369,7 +1383,7 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { } private void deleteLooseRef(String name) { - File path = new File(diskRepo.getDirectory(), name); + File path = new File(diskRepo.getCommonDirectory(), name); assertTrue("deleted " + name, path.delete()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java index 28d5ca726a..acc36d76f4 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java @@ -40,6 +40,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefRename; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; @@ -111,16 +112,17 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertNotSame(newid, r.getObjectId()); assertSame(ObjectId.class, r.getObjectId().getClass()); assertEquals(newid, r.getObjectId()); - List<ReflogEntry> reverseEntries1 = db + List<ReflogEntry> reverseEntries1 = db.getRefDatabase() .getReflogReader("refs/heads/abc").getReverseEntries(); ReflogEntry entry1 = reverseEntries1.get(0); assertEquals(1, reverseEntries1.size()); assertEquals(ObjectId.zeroId(), entry1.getOldId()); assertEquals(r.getObjectId(), entry1.getNewId()); - assertEquals(new PersonIdent(db).toString(), entry1.getWho().toString()); + assertEquals(new PersonIdent(db).toString(), + entry1.getWho().toString()); assertEquals("", entry1.getComment()); - List<ReflogEntry> reverseEntries2 = db.getReflogReader("HEAD") - .getReverseEntries(); + List<ReflogEntry> reverseEntries2 = db.getRefDatabase() + .getReflogReader("HEAD").getReverseEntries(); assertEquals(0, reverseEntries2.size()); } @@ -136,8 +138,11 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { final RefUpdate ru2 = updateRef(newRef2); Result update2 = ru2.update(); assertEquals(Result.LOCK_FAILURE, update2); - assertEquals(1, db.getReflogReader("refs/heads/z").getReverseEntries().size()); - assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(1, refDb.getReflogReader("refs/heads/z") + .getReverseEntries().size()); + assertEquals(0, + refDb.getReflogReader("HEAD").getReverseEntries().size()); } @Test @@ -147,8 +152,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { final RefUpdate ru = updateRef(newRef); Result update = ru.update(); assertEquals(Result.LOCK_FAILURE, update); - assertNull(db.getReflogReader("refs/heads/master/x")); - assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size()); + RefDatabase refDb = db.getRefDatabase(); + assertNull(refDb.getReflogReader("refs/heads/master/x")); + assertEquals(0, + refDb.getReflogReader("HEAD").getReverseEntries().size()); } @Test @@ -163,9 +170,12 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { final RefUpdate ru2 = updateRef(newRef2); Result update2 = ru2.update(); assertEquals(Result.LOCK_FAILURE, update2); - assertEquals(1, db.getReflogReader("refs/heads/z/a").getReverseEntries().size()); - assertNull(db.getReflogReader("refs/heads/z")); - assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(1, refDb.getReflogReader("refs/heads/z/a") + .getReverseEntries().size()); + assertNull(refDb.getReflogReader("refs/heads/z")); + assertEquals(0, + refDb.getReflogReader("HEAD").getReverseEntries().size()); } @Test @@ -175,8 +185,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { final RefUpdate ru = updateRef(newRef); Result update = ru.update(); assertEquals(Result.LOCK_FAILURE, update); - assertNull(db.getReflogReader("refs/heads/prefix")); - assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size()); + RefDatabase refDb = db.getRefDatabase(); + assertNull(refDb.getReflogReader("refs/heads/prefix")); + assertEquals(0, + refDb.getReflogReader("HEAD").getReverseEntries().size()); } /** @@ -197,8 +209,11 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { Result delete = updateRef2.delete(); assertEquals(Result.REJECTED_CURRENT_BRANCH, delete); assertEquals(pid, db.resolve("refs/heads/master")); - assertEquals(1,db.getReflogReader("refs/heads/master").getReverseEntries().size()); - assertEquals(0,db.getReflogReader("HEAD").getReverseEntries().size()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(1, refDb.getReflogReader("refs/heads/master") + .getReverseEntries().size()); + assertEquals(0, + refDb.getReflogReader("HEAD").getReverseEntries().size()); } @Test @@ -209,7 +224,8 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { updateRef.setForceUpdate(true); Result update = updateRef.update(); assertEquals(Result.FORCED, update); - assertEquals(1,db.getReflogReader("refs/heads/master").getReverseEntries().size()); + assertEquals(1, db.getRefDatabase().getReflogReader("refs/heads/master") + .getReverseEntries().size()); } @Test @@ -219,15 +235,18 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { ref.update(); // create loose ref ref = updateRef(newRef); // refresh delete(ref, Result.NO_CHANGE); - assertNull(db.getReflogReader("refs/heads/abc")); + assertNull(db.getRefDatabase().getReflogReader("refs/heads/abc")); } @Test public void testDeleteHead() throws IOException { final RefUpdate ref = updateRef(Constants.HEAD); delete(ref, Result.REJECTED_CURRENT_BRANCH, true, false); - assertEquals(0, db.getReflogReader("refs/heads/master").getReverseEntries().size()); - assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(0, refDb.getReflogReader("refs/heads/master") + .getReverseEntries().size()); + assertEquals(0, + refDb.getReflogReader("HEAD").getReverseEntries().size()); } @Test @@ -423,7 +442,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { // the branch HEAD referred to is left untouched assertEquals(pid, db.resolve("refs/heads/master")); - ReflogReader reflogReader = db.getReflogReader("HEAD"); + ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD"); ReflogEntry e = reflogReader.getReverseEntries().get(0); assertEquals(pid, e.getOldId()); assertEquals(ppid, e.getNewId()); @@ -453,7 +472,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { // the branch HEAD referred to is left untouched assertNull(db.resolve("refs/heads/unborn")); - ReflogReader reflogReader = db.getReflogReader("HEAD"); + ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD"); ReflogEntry e = reflogReader.getReverseEntries().get(0); assertEquals(ObjectId.zeroId(), e.getOldId()); assertEquals(ppid, e.getNewId()); @@ -513,7 +532,6 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { @Test public void testRefsCacheAfterUpdate() throws Exception { // Do not use the default repo for this case. - List<Ref> allRefs = db.getRefDatabase().getRefs(); ObjectId oldValue = db.resolve("HEAD"); ObjectId newValue = db.resolve("HEAD^"); // first make HEAD refer to loose ref @@ -529,7 +547,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { update = updateRef.update(); assertEquals(Result.FAST_FORWARD, update); - allRefs = db.getRefDatabase().getRefs(); + List<Ref> allRefs = db.getRefDatabase().getRefs(); Ref master = getRef(allRefs, "refs/heads/master").get(); Ref head = getRef(allRefs, "HEAD").get(); assertEquals("refs/heads/master", master.getName()); @@ -550,7 +568,6 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { @Test public void testRefsCacheAfterUpdateLooseOnly() throws Exception { // Do not use the default repo for this case. - List<Ref> allRefs = db.getRefDatabase().getRefs(); ObjectId oldValue = db.resolve("HEAD"); writeSymref(Constants.HEAD, "refs/heads/newref"); RefUpdate updateRef = db.updateRef(Constants.HEAD); @@ -559,7 +576,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { Result update = updateRef.update(); assertEquals(Result.NEW, update); - allRefs = db.getRefDatabase().getRefs(); + List<Ref> allRefs = db.getRefDatabase().getRefs(); Ref head = getRef(allRefs, "HEAD").get(); Ref newref = getRef(allRefs, "refs/heads/newref").get(); assertEquals("refs/heads/newref", newref.getName()); @@ -693,9 +710,12 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertEquals(Result.RENAMED, result); assertEquals(rb, db.resolve("refs/heads/new/name")); assertNull(db.resolve("refs/heads/b")); - assertEquals(1, db.getReflogReader("new/name").getReverseEntries().size()); - assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name") - .getLastEntry().getComment()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(1, refDb.getReflogReader("refs/heads/new/name") + .getReverseEntries().size()); + assertEquals("Branch: renamed b to new/name", + refDb.getReflogReader("refs/heads/new/name").getLastEntry() + .getComment()); assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists()); assertEquals(oldHead, db.resolve(Constants.HEAD)); // unchanged } @@ -715,11 +735,15 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertEquals(Result.RENAMED, result); assertEquals(rb, db.resolve("refs/heads/new/name")); assertNull(db.resolve("refs/heads/b")); - assertEquals(2, db.getReflogReader("new/name").getReverseEntries().size()); - assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name") - .getLastEntry().getComment()); - assertEquals("Just a message", db.getReflogReader("new/name") - .getReverseEntries().get(1).getComment()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(2, refDb.getReflogReader("refs/heads/new/name") + .getReverseEntries().size()); + assertEquals("Branch: renamed b to new/name", + refDb.getReflogReader("refs/heads/new/name").getLastEntry() + .getComment()); + assertEquals("Just a message", + refDb.getReflogReader("refs/heads/new/name").getReverseEntries() + .get(1).getComment()); assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists()); assertEquals(oldHead, db.resolve(Constants.HEAD)); // unchanged } @@ -739,13 +763,20 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertEquals(Result.RENAMED, result); assertEquals(rb, db.resolve("refs/heads/new/name")); assertNull(db.resolve("refs/heads/b")); - assertEquals("Branch: renamed b to new/name", db.getReflogReader( - "new/name").getLastEntry().getComment()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals("Branch: renamed b to new/name", + refDb.getReflogReader("refs/heads/new/name").getLastEntry() + .getComment()); assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists()); assertEquals(rb, db.resolve(Constants.HEAD)); - assertEquals(2, db.getReflogReader("new/name").getReverseEntries().size()); - assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name").getReverseEntries().get(0).getComment()); - assertEquals("Just a message", db.getReflogReader("new/name").getReverseEntries().get(1).getComment()); + assertEquals(2, refDb.getReflogReader("refs/heads/new/name") + .getReverseEntries().size()); + assertEquals("Branch: renamed b to new/name", + refDb.getReflogReader("refs/heads/new/name").getReverseEntries() + .get(0).getComment()); + assertEquals("Just a message", + refDb.getReflogReader("refs/heads/new/name").getReverseEntries() + .get(1).getComment()); } @Test @@ -768,11 +799,17 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertEquals(Result.RENAMED, result); assertEquals(rb2, db.resolve("refs/heads/new/name")); assertNull(db.resolve("refs/heads/b")); - assertEquals("Branch: renamed b to new/name", db.getReflogReader( - "new/name").getLastEntry().getComment()); - assertEquals(3, db.getReflogReader("refs/heads/new/name").getReverseEntries().size()); - assertEquals("Branch: renamed b to new/name", db.getReflogReader("refs/heads/new/name").getReverseEntries().get(0).getComment()); - assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals("Branch: renamed b to new/name", + refDb.getReflogReader("refs/heads/new/name").getLastEntry() + .getComment()); + assertEquals(3, refDb.getReflogReader("refs/heads/new/name") + .getReverseEntries().size()); + assertEquals("Branch: renamed b to new/name", + refDb.getReflogReader("refs/heads/new/name").getReverseEntries() + .get(0).getComment()); + assertEquals(0, + refDb.getReflogReader("HEAD").getReverseEntries().size()); // make sure b's log file is gone too. assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists()); @@ -791,9 +828,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { ObjectId oldfromId = db.resolve(fromName); ObjectId oldHeadId = db.resolve(Constants.HEAD); writeReflog(db, oldfromId, "Just a message", fromName); - List<ReflogEntry> oldFromLog = db + RefDatabase refDb = db.getRefDatabase(); + List<ReflogEntry> oldFromLog = refDb .getReflogReader(fromName).getReverseEntries(); - List<ReflogEntry> oldHeadLog = oldHeadId != null ? db + List<ReflogEntry> oldHeadLog = oldHeadId != null ? refDb .getReflogReader(Constants.HEAD).getReverseEntries() : null; assertTrue("internal check, we have a log", new File(db.getDirectory(), @@ -820,10 +858,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertEquals(oldHeadId, db.resolve(Constants.HEAD)); assertEquals(oldfromId, db.resolve(fromName)); assertNull(db.resolve(toName)); - assertEquals(oldFromLog.toString(), db.getReflogReader(fromName) + assertEquals(oldFromLog.toString(), refDb.getReflogReader(fromName) .getReverseEntries().toString()); if (oldHeadId != null && oldHeadLog != null) - assertEquals(oldHeadLog.toString(), db.getReflogReader( + assertEquals(oldHeadLog.toString(), refDb.getReflogReader( Constants.HEAD).getReverseEntries().toString()); } finally { lockFile.unlock(); @@ -944,15 +982,18 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertEquals(Result.RENAMED, result); assertNull(db.resolve("refs/heads/a")); assertEquals(rb, db.resolve("refs/heads/a/b")); - assertEquals(3, db.getReflogReader("a/b").getReverseEntries().size()); - assertEquals("Branch: renamed a to a/b", db.getReflogReader("a/b") - .getReverseEntries().get(0).getComment()); - assertEquals("Just a message", db.getReflogReader("a/b") + RefDatabase refDb = db.getRefDatabase(); + assertEquals(3, refDb.getReflogReader("refs/heads/a/b") + .getReverseEntries().size()); + assertEquals("Branch: renamed a to a/b", + refDb.getReflogReader("refs/heads/a/b").getReverseEntries() + .get(0).getComment()); + assertEquals("Just a message", refDb.getReflogReader("refs/heads/a/b") .getReverseEntries().get(1).getComment()); - assertEquals("Setup", db.getReflogReader("a/b").getReverseEntries() - .get(2).getComment()); + assertEquals("Setup", refDb.getReflogReader("refs/heads/a/b") + .getReverseEntries().get(2).getComment()); // same thing was logged to HEAD - assertEquals("Branch: renamed a to a/b", db.getReflogReader("HEAD") + assertEquals("Branch: renamed a to a/b", refDb.getReflogReader("HEAD") .getReverseEntries().get(0).getComment()); } @@ -980,15 +1021,20 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { assertNull(db.resolve("refs/heads/prefix/a")); assertEquals(rb, db.resolve("refs/heads/prefix")); - assertEquals(3, db.getReflogReader("prefix").getReverseEntries().size()); - assertEquals("Branch: renamed prefix/a to prefix", db.getReflogReader( - "prefix").getReverseEntries().get(0).getComment()); - assertEquals("Just a message", db.getReflogReader("prefix") - .getReverseEntries().get(1).getComment()); - assertEquals("Setup", db.getReflogReader("prefix").getReverseEntries() - .get(2).getComment()); - assertEquals("Branch: renamed prefix/a to prefix", db.getReflogReader( - "HEAD").getReverseEntries().get(0).getComment()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(3, refDb.getReflogReader("refs/heads/prefix") + .getReverseEntries().size()); + assertEquals("Branch: renamed prefix/a to prefix", + refDb.getReflogReader("refs/heads/prefix").getReverseEntries() + .get(0).getComment()); + assertEquals("Just a message", + refDb.getReflogReader("refs/heads/prefix").getReverseEntries() + .get(1).getComment()); + assertEquals("Setup", refDb.getReflogReader("refs/heads/prefix") + .getReverseEntries().get(2).getComment()); + assertEquals("Branch: renamed prefix/a to prefix", + refDb.getReflogReader("HEAD").getReverseEntries().get(0) + .getComment()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java index dc0e749373..16645cbcd7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java @@ -27,6 +27,7 @@ import org.eclipse.jgit.lib.CheckoutEntry; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; @@ -154,18 +155,22 @@ public class ReflogReaderTest extends SampleDataRepositoryTestCase { setupReflog("logs/refs/heads/a", aLine); setupReflog("logs/refs/heads/master", masterLine); setupReflog("logs/HEAD", headLine); - assertEquals("branch: change to master", db.getReflogReader("master") - .getLastEntry().getComment()); - assertEquals("branch: change to a", db.getReflogReader("a") - .getLastEntry().getComment()); - assertEquals("branch: change to HEAD", db.getReflogReader("HEAD") - .getLastEntry().getComment()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals("branch: change to master", + refDb.getReflogReader("refs/heads/master").getLastEntry() + .getComment()); + assertEquals("branch: change to a", + refDb.getReflogReader("refs/heads/a").getLastEntry() + .getComment()); + assertEquals("branch: change to HEAD", + refDb.getReflogReader("HEAD").getLastEntry().getComment()); } @Test public void testReadLineWithMissingComment() throws Exception { setupReflog("logs/refs/heads/master", oneLineWithoutComment); - final ReflogReader reader = db.getReflogReader("master"); + final ReflogReader reader = db.getRefDatabase() + .getReflogReader("refs/heads/master"); ReflogEntry e = reader.getLastEntry(); assertEquals(ObjectId .fromString("da85355dfc525c9f6f3927b876f379f46ccf826e"), e @@ -183,15 +188,18 @@ public class ReflogReaderTest extends SampleDataRepositoryTestCase { @Test public void testNoLog() throws Exception { - assertEquals(0, db.getReflogReader("master").getReverseEntries().size()); - assertNull(db.getReflogReader("master").getLastEntry()); + RefDatabase refDb = db.getRefDatabase(); + assertEquals(0, + refDb.getReflogReader("refs/heads/master").getReverseEntries() + .size()); + assertNull(refDb.getReflogReader("refs/heads/master").getLastEntry()); } @Test public void testCheckout() throws Exception { setupReflog("logs/HEAD", switchBranch); - List<ReflogEntry> entries = db.getReflogReader(Constants.HEAD) - .getReverseEntries(); + List<ReflogEntry> entries = db.getRefDatabase() + .getReflogReader(Constants.HEAD).getReverseEntries(); assertEquals(1, entries.size()); ReflogEntry entry = entries.get(0); CheckoutEntry checkout = entry.parseCheckout(); @@ -238,7 +246,7 @@ public class ReflogReaderTest extends SampleDataRepositoryTestCase { private void setupReflog(String logName, byte[] data) throws FileNotFoundException, IOException { - File logfile = new File(db.getDirectory(), logName); + File logfile = new File(db.getCommonDirectory(), logName); if (!logfile.getParentFile().mkdirs() && !logfile.getParentFile().isDirectory()) { throw new IOException( diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java index 8d0e99dea0..a8363336d9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java @@ -16,6 +16,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; @@ -32,7 +34,7 @@ public class ReflogWriterTest extends SampleDataRepositoryTestCase { ReflogWriter writer = new ReflogWriter((RefDirectory) db.getRefDatabase()); PersonIdent ident = new PersonIdent("John Doe", "john@doe.com", - 1243028200000L, 120); + Instant.ofEpochMilli(1243028200000L), ZoneOffset.ofHours(2)); ObjectId oldId = ObjectId .fromString("da85355dfc525c9f6f3927b876f379f46ccf826e"); ObjectId newId = ObjectId @@ -48,7 +50,7 @@ public class ReflogWriterTest extends SampleDataRepositoryTestCase { private void readReflog(byte[] buffer) throws FileNotFoundException, IOException { - File logfile = new File(db.getDirectory(), "logs/refs/heads/master"); + File logfile = new File(db.getCommonDirectory(), "logs/refs/heads/master"); if (!logfile.getParentFile().mkdirs() && !logfile.getParentFile().isDirectory()) { throw new IOException( diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java index c3dafe4aa2..90a2aa601e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java @@ -23,7 +23,6 @@ import static org.junit.Assert.assertEquals; public class SnapshottingRefDirectoryTest extends RefDirectoryTest { private RefDirectory originalRefDirectory; - /** {@inheritDoc} */ @Before @Override public void setUp() throws Exception { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java index f5c7c67c5d..684ee52b4a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java @@ -11,6 +11,8 @@ 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 org.eclipse.jgit.internal.storage.file.BasePackBitmapIndex.StoredBitmap; import org.eclipse.jgit.lib.ObjectId; @@ -44,6 +46,33 @@ public class StoredBitmapTest { assertEquals(bitmapOf(50, 90), sb.getBitmap()); } + @Test + public void testGetSizeWithoutXor() { + EWAHCompressedBitmap base = bitmapOf(100); + StoredBitmap sb = newStoredBitmap(base); + assertEquals(base.sizeInBytes(), sb.getCurrentSizeInBytes()); + sb.getBitmap(); + assertEquals(base.sizeInBytes(), sb.getCurrentSizeInBytes()); + } + + @Test + public void testGetSizeWithOneXor() { + EWAHCompressedBitmap base = bitmapOf(100, 101); + EWAHCompressedBitmap xor = bitmapOf(100); + StoredBitmap sb = newStoredBitmap(base, xor); + assertEquals(xor.sizeInBytes(), sb.getCurrentSizeInBytes()); + } + + @Test + public void testIsBase() { + EWAHCompressedBitmap one = bitmapOf(100, 101); + EWAHCompressedBitmap two = bitmapOf(100); + StoredBitmap baseBitmap = newStoredBitmap(one); + StoredBitmap xoredBitmap = newStoredBitmap(one, two); + assertTrue(baseBitmap.isBase()); + assertFalse(xoredBitmap.isBase()); + } + private static final StoredBitmap newStoredBitmap( EWAHCompressedBitmap... bitmaps) { StoredBitmap sb = null; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java index 49e8a7be66..e067beb317 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java @@ -28,6 +28,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.time.Instant; +import java.time.ZoneOffset; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -374,8 +375,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { public void test009_CreateCommitOldFormat() throws IOException { final ObjectId treeId = insertTree(new TreeFormatter()); final CommitBuilder c = new CommitBuilder(); - c.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60)); - c.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60)); + c.setAuthor(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); + c.setCommitter(new PersonIdent(committer, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); c.setMessage("A Commit\n"); c.setTreeId(treeId); assertEquals(treeId, c.getTreeId()); @@ -411,7 +414,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { final TagBuilder t = new TagBuilder(); t.setObjectId(emptyId, Constants.OBJ_BLOB); t.setTag("test020"); - t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60)); + t.setTagger(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); t.setMessage("test020 tagged\n"); ObjectId actid = insertTag(t); assertEquals("6759556b09fbb4fd8ae5e315134481cc25d46954", actid.name()); @@ -419,8 +423,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { RevTag mapTag = parseTag(actid); assertEquals(Constants.OBJ_BLOB, mapTag.getObject().getType()); assertEquals("test020 tagged\n", mapTag.getFullMessage()); - assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag - .getTaggerIdent()); + assertEquals(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)), + mapTag.getTaggerIdent()); assertEquals("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", mapTag .getObject().getId().name()); } @@ -434,7 +439,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { final TagBuilder t = new TagBuilder(); t.setObjectId(almostEmptyTreeId, Constants.OBJ_TREE); t.setTag("test021"); - t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60)); + t.setTagger(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); t.setMessage("test021 tagged\n"); ObjectId actid = insertTag(t); assertEquals("b0517bc8dbe2096b419d42424cd7030733f4abe5", actid.name()); @@ -442,8 +448,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { RevTag mapTag = parseTag(actid); assertEquals(Constants.OBJ_TREE, mapTag.getObject().getType()); assertEquals("test021 tagged\n", mapTag.getFullMessage()); - assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag - .getTaggerIdent()); + assertEquals(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)), + mapTag.getTaggerIdent()); assertEquals("417c01c8795a35b8e835113a85a5c0c1c77f67fb", mapTag .getObject().getId().name()); } @@ -455,17 +462,18 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId); final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree); final CommitBuilder almostEmptyCommit = new CommitBuilder(); - almostEmptyCommit.setAuthor(new PersonIdent(author, 1154236443000L, - -2 * 60)); // not exactly the same - almostEmptyCommit.setCommitter(new PersonIdent(author, 1154236443000L, - -2 * 60)); + almostEmptyCommit.setAuthor(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2))); + almostEmptyCommit.setCommitter(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2))); almostEmptyCommit.setMessage("test022\n"); almostEmptyCommit.setTreeId(almostEmptyTreeId); ObjectId almostEmptyCommitId = insertCommit(almostEmptyCommit); final TagBuilder t = new TagBuilder(); t.setObjectId(almostEmptyCommitId, Constants.OBJ_COMMIT); t.setTag("test022"); - t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60)); + t.setTagger(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); t.setMessage("test022 tagged\n"); ObjectId actid = insertTag(t); assertEquals("0ce2ebdb36076ef0b38adbe077a07d43b43e3807", actid.name()); @@ -473,8 +481,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { RevTag mapTag = parseTag(actid); assertEquals(Constants.OBJ_COMMIT, mapTag.getObject().getType()); assertEquals("test022 tagged\n", mapTag.getFullMessage()); - assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag - .getTaggerIdent()); + assertEquals(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)), + mapTag.getTaggerIdent()); assertEquals("b5d3b45a96b340441f5abb9080411705c51cc86c", mapTag .getObject().getId().name()); } @@ -488,9 +497,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { CommitBuilder commit = new CommitBuilder(); commit.setTreeId(almostEmptyTreeId); commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com", - 4294967295000L, 60)); + Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1))); commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com", - 4294967295000L, 60)); + Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1))); commit.setEncoding(UTF_8); commit.setMessage("\u00dcbergeeks"); ObjectId cid = insertCommit(commit); @@ -509,9 +518,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { CommitBuilder commit = new CommitBuilder(); commit.setTreeId(almostEmptyTreeId); commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com", - 4294967295000L, 60)); + Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1))); commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com", - 4294967295000L, 60)); + Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1))); commit.setEncoding(ISO_8859_1); commit.setMessage("\u00dcbergeeks"); ObjectId cid = insertCommit(commit); @@ -544,8 +553,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { .fromString("00b1f73724f493096d1ffa0b0f1f1482dbb8c936"), treeId); final CommitBuilder c1 = new CommitBuilder(); - c1.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60)); - c1.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60)); + c1.setAuthor(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); + c1.setCommitter(new PersonIdent(committer, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); c1.setMessage("A Commit\n"); c1.setTreeId(treeId); assertEquals(treeId, c1.getTreeId()); @@ -555,8 +566,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEquals(cmtid1, actid1); final CommitBuilder c2 = new CommitBuilder(); - c2.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60)); - c2.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60)); + c2.setAuthor(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); + c2.setCommitter(new PersonIdent(committer, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); c2.setMessage("A Commit 2\n"); c2.setTreeId(treeId); assertEquals(treeId, c2.getTreeId()); @@ -577,8 +590,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEquals(actid1, rm2.getParent(0)); final CommitBuilder c3 = new CommitBuilder(); - c3.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60)); - c3.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60)); + c3.setAuthor(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); + c3.setCommitter(new PersonIdent(committer, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); c3.setMessage("A Commit 3\n"); c3.setTreeId(treeId); assertEquals(treeId, c3.getTreeId()); @@ -600,8 +615,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEquals(actid2, rm3.getParent(1)); final CommitBuilder c4 = new CommitBuilder(); - c4.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60)); - c4.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60)); + c4.setAuthor(new PersonIdent(author, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); + c4.setCommitter(new PersonIdent(committer, + Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4))); c4.setMessage("A Commit 4\n"); c4.setTreeId(treeId); assertEquals(treeId, c3.getTreeId()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexBuilderTest.java new file mode 100644 index 0000000000..e6fefc623d --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexBuilderTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024, GerritForge Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.midx; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.eclipse.jgit.internal.storage.midx.MultiPackIndexLoader.MultiPackIndexBuilder; +import org.eclipse.jgit.internal.storage.midx.MultiPackIndexLoader.MultiPackIndexFormatException; +import org.junit.Test; + +public class MultiPackIndexBuilderTest { + + @Test + public void testRepeatedChunk() throws Exception { + byte[] buffer = new byte[2048]; + + MultiPackIndexBuilder builder1 = MultiPackIndexBuilder.builder(); + builder1.addOidFanout(buffer); + Exception e1 = assertThrows(MultiPackIndexFormatException.class, () -> { + builder1.addOidFanout(buffer); + }); + assertEquals("midx chunk id 0x4f494446 appears multiple times", + e1.getMessage()); + + MultiPackIndexBuilder builder2 = MultiPackIndexBuilder.builder(); + builder2.addOidLookUp(buffer); + Exception e2 = assertThrows(MultiPackIndexFormatException.class, () -> { + builder2.addOidLookUp(buffer); + }); + assertEquals("midx chunk id 0x4f49444c appears multiple times", + e2.getMessage()); + + MultiPackIndexBuilder builder3 = MultiPackIndexBuilder.builder(); + builder3.addObjectOffsets(buffer); + Exception e3 = assertThrows(MultiPackIndexFormatException.class, () -> { + builder3.addObjectOffsets(buffer); + }); + assertEquals("midx chunk id 0x4f4f4646 appears multiple times", + e3.getMessage()); + + MultiPackIndexBuilder builder4 = MultiPackIndexBuilder.builder(); + builder4.addPackNames(buffer); + Exception e4 = assertThrows(MultiPackIndexFormatException.class, () -> { + builder4.addPackNames(buffer); + }); + assertEquals("midx chunk id 0x504e414d appears multiple times", + e4.getMessage()); + + MultiPackIndexBuilder builder5 = MultiPackIndexBuilder.builder(); + builder5.addBitmappedPacks(buffer); + Exception e5 = assertThrows(MultiPackIndexFormatException.class, () -> { + builder5.addBitmappedPacks(buffer); + }); + assertEquals("midx chunk id 0x42544d50 appears multiple times", + e5.getMessage()); + + MultiPackIndexBuilder builder6 = MultiPackIndexBuilder.builder(); + builder6.addObjectLargeOffsets(buffer); + Exception e6 = assertThrows(MultiPackIndexFormatException.class, () -> { + builder6.addObjectLargeOffsets(buffer); + }); + assertEquals("midx chunk id 0x4c4f4646 appears multiple times", + e6.getMessage()); + + MultiPackIndexBuilder builder7 = MultiPackIndexBuilder.builder(); + builder7.addReverseIndex(buffer); + Exception e7 = assertThrows(MultiPackIndexFormatException.class, () -> { + builder7.addReverseIndex(buffer); + }); + assertEquals("midx chunk id 0x52494458 appears multiple times", + e7.getMessage()); + } + + @Test + public void testNeededChunk() { + byte[] buffer = new byte[2048]; + + Exception e1 = assertThrows(MultiPackIndexFormatException.class, () -> { + MultiPackIndexBuilder.builder().addOidLookUp(buffer).build(); + }); + assertEquals("midx 0x4f494446 chunk has not been loaded", + e1.getMessage()); + + Exception e2 = assertThrows(MultiPackIndexFormatException.class, () -> { + MultiPackIndexBuilder.builder().addOidFanout(buffer) + .addOidLookUp(buffer).build(); + }); + assertEquals("midx 0x504e414d chunk has not been loaded", + e2.getMessage()); + + Exception e3 = assertThrows(MultiPackIndexFormatException.class, () -> { + MultiPackIndexBuilder.builder().addOidFanout(buffer) + .addOidLookUp(buffer).addPackNames(buffer).build(); + }); + assertEquals("midx 0x4f4f4646 chunk has not been loaded", + e3.getMessage()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoaderTest.java new file mode 100644 index 0000000000..494f1d1137 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoaderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024, GerritForge Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.midx; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.junit.FakeIndexFactory; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.junit.Test; + +/** + * Test that the loader accepts valid files, discard broken files + * <p> + * Contents and lookups are covered in the MultiPackIndexTest + */ +public class MultiPackIndexLoaderTest { + + @Test + public void load_validFile_basic_upstream() throws Exception { + MultiPackIndex midx = MultiPackIndexLoader + .open(JGitTestUtil.getTestResourceFile("multi-pack-index.v1")); + assertNotNull(midx); + } + + @Test + public void load_validFile_basic_jgit() throws Exception { + PackIndex idxOne = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000001", 500), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000005", 12), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000010", 1500))); + PackIndex idxTwo = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000002", 501), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000003", 13), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000015", 1501))); + PackIndex idxThree = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000004", 502), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000007", 14), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000012", 1502))); + + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo, "p3", + idxThree); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(midx); + } + + @Test + public void load_emptyFile() { + assertThrows(IOException.class, () -> MultiPackIndexLoader + .read(new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void load_rubbishFile() { + assertThrows(MultiPackIndexLoader.MultiPackIndexFormatException.class, + () -> MultiPackIndexLoader.read(new ByteArrayInputStream( + "More than 12 bytes of not-midx".getBytes(UTF_8)))); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexTest.java new file mode 100644 index 0000000000..ab452854b2 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexTest.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2024, GerritForge Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.junit.FakeIndexFactory; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Test; + +public class MultiPackIndexTest { + + @Test + public void basic_upstream() throws IOException { + int knownPackId = 22; + int knowOffset = 258; + String knownOid = "3f4ee50f784c1e9550f09a67d2ffc1bc76917bdc"; + String knownPackName = "pack-e4b191e4343f2b7ff851026c2d8595a001077344.idx"; + String[] packNames = { + "pack-15d67b35f2b6a66ff995e09cedb36b101e0e0262.idx", + "pack-1a979514a5965e71523187a17806e03af44344ed.idx", + "pack-1de6731c035633ba8f5b41dacbc680a5a36ddd90.idx", + "pack-1ee98948e4e362c56f3cdec7f5837d06e152854f.idx", + "pack-1f6fe52ac3d33f3091d8eb8497474554bfa80bc4.idx", + "pack-34b1aa6b437a9d968412454204c2676a88dc55fa.idx", + "pack-3b245f7b4aff32a52d0520608f662bbf403792b9.idx", + "pack-47901f7f8d1c440492035c4165796a330c7f79e0.idx", + "pack-4e7f889b79aea8905a0062ce1bd68e5ef3af6a55.idx", + "pack-71ea652e4aea2cbc609545b4fbc3eda6325d88a1.idx", + "pack-723b1238411a4257c18167e91fbabed313ba332f.idx", + "pack-7bd57092a7daa4dc31277e1ec86f3de8d968ae17.idx", + "pack-883d4f469c5ea0f6d373ee623a758aeaf17715fc.idx", + "pack-8eadd378a011ddaa5ec751f2a6d9789ef501120f.idx", + "pack-92221b6f79a211944ccc6740fc22c9553ea1ba22.idx", + "pack-b139d0cae5f54c70d057a8f4d2cf99f0ae0c326c.idx", + "pack-b4f5c96d1fa6b1fac17a2a43710693c5514a9224.idx", + "pack-bed4bc1521f965e55a5a8a58dffaaefc70ea4753.idx", + "pack-cdc6baa7d90707a3c0dac4c188f797f0f79b97bb.idx", + "pack-d6d58a58fa24b74c8c082f4f63c4d2ddfb824cc9.idx", + "pack-daec59ae07f1091f3b81bd8266481bb5db3c868a.idx", + "pack-e2197d60e09ad9091407eff4e06d39ec940851e1.idx", + "pack-e4b191e4343f2b7ff851026c2d8595a001077344.idx", + "pack-eedf783b5da4caa57be33b08990fe57f245a7413.idx", + "pack-efb23e968801b9050bc70f0115a8a0eec88fb879.idx", + "pack-f919c0660c207ddf6bb0569a3041d682d19fb4f7.idx" }; + MultiPackIndex midx = MultiPackIndexLoader + .open(JGitTestUtil.getTestResourceFile("multi-pack-index.v1")); + assertNotNull(midx); + assertArrayEquals(packNames, midx.getPackNames()); + + MultiPackIndex.PackOffset oo = midx.find(ObjectId.fromString(knownOid)); + + assertEquals(knowOffset, oo.getOffset()); + assertEquals(knownPackId, oo.getPackId()); + assertEquals(knownPackName, midx.getPackNames()[oo.getPackId()]); + } + + @Test + public void basicMidx() throws IOException { + PackIndex idxOne = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000001", 500), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000005", 12), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000010", 1500))); + PackIndex idxTwo = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000002", 501), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000003", 13), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000015", 1501))); + PackIndex idxThree = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000004", 502), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000007", 14), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000012", 1502))); + + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo, "p3", + idxThree); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + assertEquals(3, midx.getPackNames().length); + assertInIndex(midx, 0, "0000000000000000000000000000000000000001", 500); + assertInIndex(midx, 0, "0000000000000000000000000000000000000005", 12); + assertInIndex(midx, 0, "0000000000000000000000000000000000000010", + 1500); + assertInIndex(midx, 1, "0000000000000000000000000000000000000002", 501); + assertInIndex(midx, 1, "0000000000000000000000000000000000000003", 13); + assertInIndex(midx, 1, "0000000000000000000000000000000000000015", + 1501); + assertInIndex(midx, 2, "0000000000000000000000000000000000000004", 502); + assertInIndex(midx, 2, "0000000000000000000000000000000000000007", 14); + assertInIndex(midx, 2, "0000000000000000000000000000000000000012", + 1502); + + assertNull(midx.find(ObjectId.zeroId())); + } + + @Test + public void jgit_largeOffsetChunk() throws IOException { + PackIndex idxOne = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000001", (1L << 34)), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000005", 12))); + PackIndex idxTwo = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000002", (1L << 35)), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000003", 13))); + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + assertEquals(2, midx.getPackNames().length); + assertInIndex(midx, 0, "0000000000000000000000000000000000000001", + (1L << 34)); + assertInIndex(midx, 0, "0000000000000000000000000000000000000005", 12); + assertInIndex(midx, 1, "0000000000000000000000000000000000000002", + (1L << 35)); + } + + @Test + public void jgit_largeOffset_noChunk() throws IOException { + // All offsets fit in 32 bits, no large offset chunk + // Most significant bit to 1 is still valid offset + PackIndex idxOne = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000001", + 0xff00_0000), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000005", 12))); + PackIndex idxTwo = FakeIndexFactory.indexOf(List.of( + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000002", 501), + new FakeIndexFactory.IndexObject( + "0000000000000000000000000000000000000003", 13))); + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + assertEquals(2, midx.getPackNames().length); + assertInIndex(midx, 0, "0000000000000000000000000000000000000001", + 0xff00_0000L); + assertInIndex(midx, 0, "0000000000000000000000000000000000000005", 12); + } + + @Test + public void jgit_resolve() throws IOException { + AbbreviatedObjectId abbrev = AbbreviatedObjectId + .fromString("32fe829a1c"); + + PackIndex idxOne = indexWith( + // Noise + "0000000000000000000000000000000000000001", + "3000000000000000000000000000000000000005", + // One before abbrev + "32fe829a1b000000000000000000000000000001", + // matches + "32fe829a1c000000000000000000000000000001", + "32fe829a1c000000000000000000000000000100", + // One after abbrev + "32fe829a1d000000000000000000000000000000"); + PackIndex idxTwo = indexWith( + // Noise + "8888880000000000000000000000000000000002", + "bbbbbb0000000000000000000000000000000003", + // Match + "32fe829a1c000000000000000000000000000010"); + + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + + + Set<ObjectId> results = new HashSet<>(); + midx.resolve(results, abbrev, 100); + + assertEquals(3, results.size()); + assertTrue(results.contains(ObjectId + .fromString("32fe829a1c000000000000000000000000000001"))); + assertTrue(results.contains(ObjectId + .fromString("32fe829a1c000000000000000000000000000010"))); + assertTrue(results.contains(ObjectId + .fromString("32fe829a1c000000000000000000000000000100"))); + + } + + @Test + public void jgit_resolve_matchLimit() throws IOException { + AbbreviatedObjectId abbrev = AbbreviatedObjectId + .fromString("32fe829a1c"); + + PackIndex idxOne = indexWith( + // Noise + "0000000000000000000000000000000000000001", + "3000000000000000000000000000000000000005", + // One before abbrev + "32fe829a1b000000000000000000000000000001", + // matches + "32fe829a1c000000000000000000000000000001", + "32fe829a1c000000000000000000000000000100", + // One after abbrev + "32fe829a1d000000000000000000000000000000"); + PackIndex idxTwo = indexWith( + // Noise + "8888880000000000000000000000000000000002", + "bbbbbb0000000000000000000000000000000003", + // Match + "32fe829a1c000000000000000000000000000010"); + + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + + + Set<ObjectId> results = new HashSet<>(); + midx.resolve(results, abbrev, 2); + + assertEquals(2, results.size()); + assertTrue(results.contains(ObjectId + .fromString("32fe829a1c000000000000000000000000000001"))); + assertTrue(results.contains(ObjectId + .fromString("32fe829a1c000000000000000000000000000010"))); + } + + @Test + public void jgit_resolve_noMatches() throws IOException { + AbbreviatedObjectId abbrev = AbbreviatedObjectId + .fromString("4400000000"); + + PackIndex idxOne = indexWith( + "0000000000000000000000000000000000000001", + "3000000000000000000000000000000000000005", + "32fe829a1b000000000000000000000000000001", + "32fe829a1c000000000000000000000000000001", + "32fe829a1c000000000000000000000000000100", + "32fe829a1d000000000000000000000000000000"); + PackIndex idxTwo = indexWith( + // Noise + "8888880000000000000000000000000000000002", + "bbbbbb0000000000000000000000000000000003", + "32fe829a1c000000000000000000000000000010"); + + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + + + Set<ObjectId> results = new HashSet<>(); + midx.resolve(results, abbrev, 200); + + assertEquals(0, results.size()); + } + + @Test + public void jgit_resolve_empty() throws IOException { + AbbreviatedObjectId abbrev = AbbreviatedObjectId + .fromString("4400000000"); + + PackIndex idxOne = FakeIndexFactory.indexOf(List.of()); + PackIndex idxTwo = FakeIndexFactory.indexOf(List.of()); + + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + MultiPackIndex midx = MultiPackIndexLoader + .read(new ByteArrayInputStream(out.toByteArray())); + + + Set<ObjectId> results = new HashSet<>(); + midx.resolve(results, abbrev, 200); + + assertEquals(0, results.size()); + } + + private static PackIndex indexWith(String... oids) { + List<FakeIndexFactory.IndexObject> idxObjs = new ArrayList<>( + oids.length); + int offset = 12; + for (String oid : oids) { + idxObjs.add(new FakeIndexFactory.IndexObject(oid, offset)); + offset += 10; + } + return FakeIndexFactory.indexOf(idxObjs); + } + + private static void assertInIndex(MultiPackIndex midx, int expectedPackId, + String oid, long expectedOffset) { + MultiPackIndex.PackOffset packOffset = midx + .find(ObjectId.fromString(oid)); + assertNotNull(packOffset); + assertEquals("Wrong packId for " + oid, expectedPackId, + packOffset.getPackId()); + assertEquals(expectedOffset, packOffset.getOffset()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java new file mode 100644 index 0000000000..8b57a2dcb4 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.junit.FakeIndexFactory; +import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.util.NB; +import org.junit.Test; + +public class MultiPackIndexWriterTest { + + @Test + public void write_allSmallOffsets() throws IOException { + PackIndex index1 = indexOf( + object("0000000000000000000000000000000000000001", 500), + object("0000000000000000000000000000000000000003", 1500), + object("0000000000000000000000000000000000000005", 3000)); + PackIndex index2 = indexOf( + object("0000000000000000000000000000000000000002", 500), + object("0000000000000000000000000000000000000004", 1500), + object("0000000000000000000000000000000000000006", 3000)); + + Map<String, PackIndex> data = Map.of("packname1", index1, "packname2", + index2); + + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, data); + // header (12 bytes) + // + chunkHeader (6 * 12 bytes) + // + fanout table (256 * 4 bytes) + // + OIDs (6 * 20 bytes) + // + (pack, offset) pairs (6 * 8) + // + RIDX (6 * 4 bytes) + // + packfile names (2 * 10) + // + checksum (20) + assertEquals(1340, out.size()); + List<Integer> chunkIds = readChunkIds(out); + assertEquals(5, chunkIds.size()); + assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT)); + assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP)); + assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS)); + assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX)); + assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES)); + } + + @Test + public void write_smallOffset_limit() throws IOException { + PackIndex index1 = indexOf( + object("0000000000000000000000000000000000000001", 500), + object("0000000000000000000000000000000000000003", 1500), + object("0000000000000000000000000000000000000005", (1L << 32) -1)); + PackIndex index2 = indexOf( + object("0000000000000000000000000000000000000002", 500), + object("0000000000000000000000000000000000000004", 1500), + object("0000000000000000000000000000000000000006", 3000)); + Map<String, PackIndex> data = + Map.of("packname1", index1, "packname2", index2); + + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, data); + // header (12 bytes) + // + chunkHeader (6 * 12 bytes) + // + fanout table (256 * 4 bytes) + // + OIDs (6 * 20 bytes) + // + (pack, offset) pairs (6 * 8) + // + RIDX (6 * 4 bytes) + // + packfile names (2 * 10) + // + checksum (20) + assertEquals(1340, out.size()); + List<Integer> chunkIds = readChunkIds(out); + assertEquals(5, chunkIds.size()); + assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT)); + assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP)); + assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS)); + assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX)); + assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES)); + } + + @Test + public void write_largeOffset() throws IOException { + PackIndex index1 = indexOf( + object("0000000000000000000000000000000000000001", 500), + object("0000000000000000000000000000000000000003", 1500), + object("0000000000000000000000000000000000000005", 1L << 32)); + PackIndex index2 = indexOf( + object("0000000000000000000000000000000000000002", 500), + object("0000000000000000000000000000000000000004", 1500), + object("0000000000000000000000000000000000000006", 3000)); + Map<String, PackIndex> data = + Map.of("packname1", index1, "packname2", index2); + + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, data); + // header (12 bytes) + // + chunkHeader (7 * 12 bytes) + // + fanout table (256 * 4 bytes) + // + OIDs (6 * 20 bytes) + // + (pack, offset) pairs (6 * 8) + // + (large-offset) (1 * 8) + // + RIDX (6 * 4 bytes) + // + packfile names (2 * 10) + // + checksum (20) + assertEquals(1360, out.size()); + List<Integer> chunkIds = readChunkIds(out); + assertEquals(6, chunkIds.size()); + assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT)); + assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP)); + assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS)); + assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_LARGEOFFSETS)); + assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX)); + assertEquals(5, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES)); + } + + @Test + public void jgit_emptyMidx() throws IOException { + PackIndex idxOne = FakeIndexFactory.indexOf(List.of()); + PackIndex idxTwo = FakeIndexFactory.indexOf(List.of()); + Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo); + MultiPackIndexWriter writer = new MultiPackIndexWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(NullProgressMonitor.INSTANCE, out, packs); + List<Integer> chunkIds = readChunkIds(out); + assertEquals(1134, out.size()); + assertEquals(5, chunkIds.size()); + assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT)); + assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP)); + assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS)); + assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX)); + assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES)); + } + + private List<Integer> readChunkIds(ByteArrayOutputStream out) { + List<Integer> chunkIds = new ArrayList<>(); + byte[] raw = out.toByteArray(); + int numChunks = raw[6]; + int position = 12; + for (int i = 0; i < numChunks; i++) { + chunkIds.add(NB.decodeInt32(raw, position)); + position += CHUNK_LOOKUP_WIDTH; + } + return chunkIds; + } + + private static PackIndex indexOf(IndexObject... objs) { + return FakeIndexFactory.indexOf(Arrays.asList(objs)); + } + + private static IndexObject object(String name, long offset) { + return new IndexObject(name, offset); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java new file mode 100644 index 0000000000..8218cbc20d --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.junit.FakeIndexFactory; +import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject; +import org.junit.Test; + +public class PackIndexMergerTest { + + @Test + public void rawIterator_noDuplicates() { + PackIndex idxOne = indexOf( + oidOffset("0000000000000000000000000000000000000001", 500), + oidOffset("0000000000000000000000000000000000000005", 12), + oidOffset("0000000000000000000000000000000000000010", 1500)); + PackIndex idxTwo = indexOf( + oidOffset("0000000000000000000000000000000000000002", 501), + oidOffset("0000000000000000000000000000000000000003", 13), + oidOffset("0000000000000000000000000000000000000015", 1501)); + PackIndex idxThree = indexOf( + oidOffset("0000000000000000000000000000000000000004", 502), + oidOffset("0000000000000000000000000000000000000007", 14), + oidOffset("0000000000000000000000000000000000000012", 1502)); + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree)); + assertEquals(9, merger.getUniqueObjectCount()); + assertEquals(3, merger.getPackCount()); + assertFalse(merger.needsLargeOffsetsChunk()); + Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator(); + assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500); + assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501); + assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13); + assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502); + assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12); + assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14); + assertNextEntry(it, "0000000000000000000000000000000000000010", 0, + 1500); + assertNextEntry(it, "0000000000000000000000000000000000000012", 2, + 1502); + assertNextEntry(it, "0000000000000000000000000000000000000015", 1, + 1501); + assertFalse(it.hasNext()); + } + + @Test + public void rawIterator_allDuplicates() { + PackIndex idxOne = indexOf( + oidOffset("0000000000000000000000000000000000000001", 500), + oidOffset("0000000000000000000000000000000000000005", 12), + oidOffset("0000000000000000000000000000000000000010", 1500)); + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne)); + assertEquals(3, merger.getUniqueObjectCount()); + assertEquals(3, merger.getPackCount()); + assertFalse(merger.needsLargeOffsetsChunk()); + Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator(); + assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500); + assertNextEntry(it, "0000000000000000000000000000000000000001", 1, 500); + assertNextEntry(it, "0000000000000000000000000000000000000001", 2, 500); + assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12); + assertNextEntry(it, "0000000000000000000000000000000000000005", 1, 12); + assertNextEntry(it, "0000000000000000000000000000000000000005", 2, 12); + assertNextEntry(it, "0000000000000000000000000000000000000010", 0, + 1500); + assertNextEntry(it, "0000000000000000000000000000000000000010", 1, + 1500); + assertNextEntry(it, "0000000000000000000000000000000000000010", 2, + 1500); + assertFalse(it.hasNext()); + } + + @Test + public void bySha1Iterator_noDuplicates() { + PackIndex idxOne = indexOf( + oidOffset("0000000000000000000000000000000000000001", 500), + oidOffset("0000000000000000000000000000000000000005", 12), + oidOffset("0000000000000000000000000000000000000010", 1500)); + PackIndex idxTwo = indexOf( + oidOffset("0000000000000000000000000000000000000002", 501), + oidOffset("0000000000000000000000000000000000000003", 13), + oidOffset("0000000000000000000000000000000000000015", 1501)); + PackIndex idxThree = indexOf( + oidOffset("0000000000000000000000000000000000000004", 502), + oidOffset("0000000000000000000000000000000000000007", 14), + oidOffset("0000000000000000000000000000000000000012", 1502)); + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree)); + assertEquals(9, merger.getUniqueObjectCount()); + assertEquals(3, merger.getPackCount()); + assertFalse(merger.needsLargeOffsetsChunk()); + Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator(); + assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500); + assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501); + assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13); + assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502); + assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12); + assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14); + assertNextEntry(it, "0000000000000000000000000000000000000010", 0, + 1500); + assertNextEntry(it, "0000000000000000000000000000000000000012", 2, + 1502); + assertNextEntry(it, "0000000000000000000000000000000000000015", 1, + 1501); + assertFalse(it.hasNext()); + } + + @Test + public void bySha1Iterator_allDuplicates() { + PackIndex idxOne = indexOf( + oidOffset("0000000000000000000000000000000000000001", 500), + oidOffset("0000000000000000000000000000000000000005", 12), + oidOffset("0000000000000000000000000000000000000010", 1500)); + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne)); + assertEquals(3, merger.getUniqueObjectCount()); + assertEquals(3, merger.getPackCount()); + assertFalse(merger.needsLargeOffsetsChunk()); + Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator(); + assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500); + assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12); + assertNextEntry(it, "0000000000000000000000000000000000000010", 0, + 1500); + assertFalse(it.hasNext()); + } + + @Test + public void bySha1Iterator_differentIndexSizes() { + PackIndex idxOne = indexOf( + oidOffset("0000000000000000000000000000000000000010", 1500)); + PackIndex idxTwo = indexOf( + oidOffset("0000000000000000000000000000000000000002", 500), + oidOffset("0000000000000000000000000000000000000003", 12)); + PackIndex idxThree = indexOf( + oidOffset("0000000000000000000000000000000000000004", 500), + oidOffset("0000000000000000000000000000000000000007", 12), + oidOffset("0000000000000000000000000000000000000012", 1500)); + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree)); + assertEquals(6, merger.getUniqueObjectCount()); + assertEquals(3, merger.getPackCount()); + assertFalse(merger.needsLargeOffsetsChunk()); + Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator(); + assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 500); + assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 12); + assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 500); + assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 12); + assertNextEntry(it, "0000000000000000000000000000000000000010", 0, + 1500); + assertNextEntry(it, "0000000000000000000000000000000000000012", 2, + 1500); + assertFalse(it.hasNext()); + } + + @Test + public void merger_noIndexes() { + PackIndexMerger merger = new PackIndexMerger(Map.of()); + assertEquals(0, merger.getUniqueObjectCount()); + assertFalse(merger.needsLargeOffsetsChunk()); + assertTrue(merger.getPackNames().isEmpty()); + assertEquals(0, merger.getPackCount()); + assertFalse(merger.bySha1Iterator().hasNext()); + } + + @Test + public void merger_emptyIndexes() { + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", indexOf(), "p2", indexOf())); + assertEquals(0, merger.getUniqueObjectCount()); + assertFalse(merger.needsLargeOffsetsChunk()); + assertEquals(2, merger.getPackNames().size()); + assertEquals(2, merger.getPackCount()); + assertFalse(merger.bySha1Iterator().hasNext()); + } + + @Test + public void bySha1Iterator_largeOffsets_needsChunk() { + PackIndex idx1 = indexOf( + oidOffset("0000000000000000000000000000000000000002", 1L << 32), + oidOffset("0000000000000000000000000000000000000004", 12)); + PackIndex idx2 = indexOf(oidOffset( + "0000000000000000000000000000000000000003", (1L << 31) + 10)); + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", idx1, "p2", idx2)); + assertTrue(merger.needsLargeOffsetsChunk()); + assertEquals(2, merger.getOffsetsOver31BitsCount()); + assertEquals(3, merger.getUniqueObjectCount()); + } + + @Test + public void bySha1Iterator_largeOffsets_noChunk() { + // If no value is over 2^32-1, then we don't need large offset + PackIndex idx1 = indexOf( + oidOffset("0000000000000000000000000000000000000002", + (1L << 31) + 15), + oidOffset("0000000000000000000000000000000000000004", 12)); + PackIndex idx2 = indexOf(oidOffset( + "0000000000000000000000000000000000000003", (1L << 31) + 10)); + PackIndexMerger merger = new PackIndexMerger( + Map.of("p1", idx1, "p2", idx2)); + assertFalse(merger.needsLargeOffsetsChunk()); + assertEquals(2, merger.getOffsetsOver31BitsCount()); + assertEquals(3, merger.getUniqueObjectCount()); + } + + private static void assertNextEntry( + Iterator<PackIndexMerger.MidxMutableEntry> it, String oid, + int packId, long offset) { + assertTrue(it.hasNext()); + PackIndexMerger.MidxMutableEntry e = it.next(); + assertEquals(oid, e.getObjectId().name()); + assertEquals(packId, e.getPackId()); + assertEquals(offset, e.getOffset()); + } + + private static IndexObject oidOffset(String oid, long offset) { + return new IndexObject(oid, offset); + } + + private static PackIndex indexOf(IndexObject... objs) { + return FakeIndexFactory.indexOf(Arrays.asList(objs)); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java new file mode 100644 index 0000000000..0b3ccacfc1 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; + +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.junit.FakeIndexFactory; +import org.junit.Test; + +public class PackIndexPeekIteratorTest { + @Test + public void next() { + PackIndex index1 = indexOf( + object("0000000000000000000000000000000000000001", 500), + object("0000000000000000000000000000000000000003", 1500), + object("0000000000000000000000000000000000000005", 3000)); + PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1); + assertEquals("0000000000000000000000000000000000000001", it.next().name()); + assertEquals("0000000000000000000000000000000000000003", it.next().name()); + assertEquals("0000000000000000000000000000000000000005", it.next().name()); + assertNull(it.next()); + } + + @Test + public void peek_doesNotAdvance() { + PackIndex index1 = indexOf( + object("0000000000000000000000000000000000000001", 500), + object("0000000000000000000000000000000000000003", 1500), + object("0000000000000000000000000000000000000005", 3000)); + PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1); + it.next(); + assertEquals("0000000000000000000000000000000000000001", it.peek().name()); + assertEquals("0000000000000000000000000000000000000001", it.peek().name()); + it.next(); + assertEquals("0000000000000000000000000000000000000003", it.peek().name()); + assertEquals("0000000000000000000000000000000000000003", it.peek().name()); + it.next(); + assertEquals("0000000000000000000000000000000000000005", it.peek().name()); + assertEquals("0000000000000000000000000000000000000005", it.peek().name()); + it.next(); + assertNull(it.peek()); + assertNull(it.peek()); + } + + @Test + public void empty() { + PackIndex index1 = indexOf(); + PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1); + assertNull(it.next()); + assertNull(it.peek()); + } + + private static PackIndex indexOf(FakeIndexFactory.IndexObject... objs) { + return FakeIndexFactory.indexOf(Arrays.asList(objs)); + } + + private static FakeIndexFactory.IndexObject object(String name, long offset) { + return new FakeIndexFactory.IndexObject(name, offset); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java index 0c09ad1510..ecf9a15b03 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java @@ -110,7 +110,7 @@ public class GcCommitSelectionTest extends GcTestCase { tr.branch(mainBranch).update(commit1); gc.setExpireAgeMillis(0); - gc.gc(); + gc.gc().get(); // Create only 2 bitmaps, for commit0 and commit1, excluding commit2 assertEquals(2, gc.getStatistics().numberOfBitmaps); @@ -227,7 +227,7 @@ public class GcCommitSelectionTest extends GcTestCase { PackConfig packConfig = new PackConfig(); packConfig.setBitmapExcludedRefsPrefixes(new String[] { "refs/heads/other" }); gc.setPackConfig(packConfig); - gc.gc(); + gc.gc().get(); assertEquals(1, gc.getStatistics().numberOfBitmaps); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java index 6fc7f25475..62dbda47fd 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java @@ -19,6 +19,8 @@ import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,6 +29,7 @@ import org.eclipse.jgit.internal.storage.io.BlockSource; import org.eclipse.jgit.internal.storage.reftable.ReftableWriter.Stats; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.junit.Test; @@ -279,6 +282,95 @@ public class ReftableCompactorTest { } } + @Test + public void reflog_all() throws IOException { + byte[] inTab; + try (ByteArrayOutputStream inBuf = new ByteArrayOutputStream()) { + ReftableWriter writer = new ReftableWriter(inBuf) + .setMinUpdateIndex(0).setMaxUpdateIndex(2).begin(); + writer.writeLog(MASTER, 2, person(Instant.ofEpochSecond(500)), + id(3), id(4), null); + writer.writeLog(MASTER, 1, person(Instant.ofEpochSecond(300)), + id(2), id(3), null); + writer.writeLog(MASTER, 0, person(Instant.ofEpochSecond(100)), + id(1), id(2), null); + writer.finish(); + inTab = inBuf.toByteArray(); + } + + ReftableCompactor compactor; + try (ByteArrayOutputStream outBuf = new ByteArrayOutputStream()) { + compactor = new ReftableCompactor(outBuf); + // No setReflogExpire time is set + List<ReftableReader> readers = new ArrayList<>(); + readers.add(read(inTab)); + compactor.addAll(readers); + compactor.compact(); + } + Stats stats = compactor.getStats(); + assertEquals(3, stats.logCount()); + } + + @Test + public void reflog_setExpireOlderThan() throws IOException { + byte[] inTab; + try (ByteArrayOutputStream inBuf = new ByteArrayOutputStream()) { + ReftableWriter writer = new ReftableWriter(inBuf) + .setMinUpdateIndex(0).setMaxUpdateIndex(2).begin(); + writer.writeLog(MASTER, 2, person(Instant.ofEpochSecond(500)), + id(3), id(4), null); + writer.writeLog(MASTER, 1, person(Instant.ofEpochSecond(300)), + id(2), id(3), null); + writer.writeLog(MASTER, 0, person(Instant.ofEpochSecond(100)), + id(1), id(2), null); + writer.finish(); + inTab = inBuf.toByteArray(); + } + + ReftableCompactor compactor; + try (ByteArrayOutputStream outBuf = new ByteArrayOutputStream()) { + compactor = new ReftableCompactor(outBuf); + compactor.setReflogExpireOlderThan(Instant.ofEpochSecond(300)); + List<ReftableReader> readers = new ArrayList<>(); + readers.add(read(inTab)); + compactor.addAll(readers); + compactor.compact(); + } + + Stats stats = compactor.getStats(); + assertEquals(2, stats.logCount()); + } + + @Test + public void reflog_disable() throws IOException { + byte[] inTab; + try (ByteArrayOutputStream inBuf = new ByteArrayOutputStream()) { + ReftableWriter writer = new ReftableWriter(inBuf) + .setMinUpdateIndex(0).setMaxUpdateIndex(2).begin(); + writer.writeLog(MASTER, 2, person(Instant.ofEpochSecond(500)), + id(3), id(4), null); + writer.writeLog(MASTER, 1, person(Instant.ofEpochSecond(300)), + id(2), id(3), null); + writer.writeLog(MASTER, 0, person(Instant.ofEpochSecond(100)), + id(1), id(2), null); + writer.finish(); + inTab = inBuf.toByteArray(); + } + + ReftableCompactor compactor; + try (ByteArrayOutputStream outBuf = new ByteArrayOutputStream()) { + compactor = new ReftableCompactor(outBuf); + compactor.setReflogExpireOlderThan(Instant.MAX); + List<ReftableReader> readers = new ArrayList<>(); + readers.add(read(inTab)); + compactor.addAll(readers); + compactor.compact(); + } + + Stats stats = compactor.getStats(); + assertEquals(0, stats.logCount()); + } + private static Ref ref(String name, int id) { return new ObjectIdRef.PeeledNonTag(PACKED, name, id(id)); } @@ -296,6 +388,10 @@ public class ReftableCompactorTest { return ObjectId.fromRaw(buf); } + private static PersonIdent person(Instant when) { + return new PersonIdent("a. u. thor", "author@jgit.com", when, ZoneId.systemDefault()); + } + private static ReftableReader read(byte[] table) { return new ReftableReader(BlockSource.from(table)); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java index ea0d92acfd..a54002bc74 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java @@ -29,6 +29,8 @@ import static org.junit.Assert.fail; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -175,7 +177,8 @@ public class ReftableTest { @Test public void hasObjLogs() throws IOException { - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); String msg = "test"; ReftableConfig cfg = new ReftableConfig(); cfg.setIndexObjects(false); @@ -617,7 +620,8 @@ public class ReftableTest { .setMinUpdateIndex(1) .setMaxUpdateIndex(2) .begin(); - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); String msg = "test"; writer.writeLog(MASTER, 1, who, ObjectId.zeroId(), id(1), msg); @@ -633,7 +637,8 @@ public class ReftableTest { .setMinUpdateIndex(1) .setMaxUpdateIndex(1) .begin(); - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); String msg = "test"; writer.writeLog(NEXT, 1, who, ObjectId.zeroId(), id(1), msg); @@ -647,7 +652,8 @@ public class ReftableTest { public void withReflog() throws IOException { Ref master = ref(MASTER, 1); Ref next = ref(NEXT, 2); - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); String msg = "test"; ByteArrayOutputStream buffer = new ByteArrayOutputStream(); @@ -712,11 +718,14 @@ public class ReftableTest { writer.writeRef(master); writer.writeRef(next); - PersonIdent who1 = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who1 = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); writer.writeLog(MASTER, 3, who1, ObjectId.zeroId(), id(1), "1"); - PersonIdent who2 = new PersonIdent("Log", "Ger", 1500079710, -8 * 60); + PersonIdent who2 = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); writer.writeLog(MASTER, 2, who2, id(1), id(2), "2"); - PersonIdent who3 = new PersonIdent("Log", "Ger", 1500079711, -8 * 60); + PersonIdent who3 = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); writer.writeLog(MASTER, 1, who3, id(2), id(3), "3"); writer.finish(); @@ -753,7 +762,8 @@ public class ReftableTest { .setMaxUpdateIndex(1) .setConfig(cfg) .begin(); - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); // Fill out the 1st ref block. List<String> names = new ArrayList<>(); @@ -782,7 +792,8 @@ public class ReftableTest { @Test public void reflogSeek() throws IOException { - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochSecond(1500079709), ZoneOffset.ofHours(-8)); String msg = "test"; String msgNext = "test next"; @@ -827,7 +838,8 @@ public class ReftableTest { @Test public void reflogSeekPrefix() throws IOException { - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ReftableWriter writer = new ReftableWriter(buffer) @@ -850,7 +862,8 @@ public class ReftableTest { @Test public void onlyReflog() throws IOException { - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); String msg = "test"; ByteArrayOutputStream buffer = new ByteArrayOutputStream(); @@ -916,7 +929,8 @@ public class ReftableTest { writer.writeRef(ref); } - PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + PersonIdent who = new PersonIdent("Log", "Ger", + Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8)); for (Ref ref : refs) { writer.writeLog(ref.getName(), 1, who, ObjectId.zeroId(), ref.getObjectId(), diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java index 450b753d94..1581d49797 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.junit; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.EPOCH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -18,7 +19,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import java.util.Date; import java.util.regex.Pattern; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; @@ -199,8 +199,8 @@ public class TestRepositoryTest { assertEquals(orig.getAuthorIdent(), amended.getAuthorIdent()); // Committer name/email is the same, but time was incremented. - assertEquals(new PersonIdent(orig.getCommitterIdent(), new Date(0)), - new PersonIdent(amended.getCommitterIdent(), new Date(0))); + assertEquals(new PersonIdent(orig.getCommitterIdent(), EPOCH), + new PersonIdent(amended.getCommitterIdent(), EPOCH)); assertTrue(orig.getCommitTime() < amended.getCommitTime()); assertEquals("foo contents", blobAsString(amended, "foo")); @@ -275,9 +275,9 @@ public class TestRepositoryTest { RevCommit toPick = tr.commit() .parent(tr.commit().create()) // Can't cherry-pick root. .author(new PersonIdent("Cherrypick Author", "cpa@example.com", - tr.getDate(), tr.getTimeZone())) + tr.getInstant(), tr.getTimeZoneId())) .author(new PersonIdent("Cherrypick Committer", "cpc@example.com", - tr.getDate(), tr.getTimeZone())) + tr.getInstant(), tr.getTimeZoneId())) .message("message to cherry-pick") .add("bar", "bar contents\n") .create(); @@ -294,8 +294,8 @@ public class TestRepositoryTest { assertEquals(toPick.getAuthorIdent(), result.getAuthorIdent()); // Committer name/email matches default, and time was incremented. - assertEquals(new PersonIdent(head.getCommitterIdent(), new Date(0)), - new PersonIdent(result.getCommitterIdent(), new Date(0))); + assertEquals(new PersonIdent(head.getCommitterIdent(), EPOCH), + new PersonIdent(result.getCommitterIdent(), EPOCH)); assertTrue(toPick.getCommitTime() < result.getCommitTime()); assertEquals("message to cherry-pick", result.getFullMessage()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/BitmapIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/BitmapIndexTest.java new file mode 100644 index 0000000000..ee4fa8bcc7 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/BitmapIndexTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.internal.storage.file.GC; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.junit.Before; +import org.junit.Test; + +public class BitmapIndexTest extends LocalDiskRepositoryTestCase { + + private static final String MAIN = "refs/heads/main"; + + TestRepository<FileRepository> repo; + + RevCommit tipWithBitmap; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + FileRepository db = createWorkRepository(); + repo = new TestRepository<>(db); + + RevCommit base = repo.commit().create(); + RevCommit one = repo.commit().parent(base).create(); + tipWithBitmap = repo.commit().parent(one).create(); + repo.update(MAIN, tipWithBitmap); + + GC gc = new GC(repo.getRepository()); + gc.setAuto(false); + gc.gc().get(); + + assertNotNull(repo.getRevWalk().getObjectReader().getBitmapIndex()); + } + + + @Test + public void listener_getBitmap_counted() throws Exception { + try (RevWalk rw = repo.getRevWalk(); + ObjectReader or = rw.getObjectReader()) { + BitmapLookupCounter counter = new BitmapLookupCounter(); + BitmapIndex bitmapIndex = or.getBitmapIndex(); + bitmapIndex.addBitmapLookupListener(counter); + + bitmapIndex.getBitmap(tipWithBitmap); + bitmapIndex.getBitmap(tipWithBitmap); + bitmapIndex.getBitmap(ObjectId.zeroId()); + + assertEquals(2, counter.bitmapFound); + assertEquals(1, counter.bitmapNotFound); + } + } + + private static class BitmapLookupCounter + implements BitmapIndex.BitmapLookupListener { + int bitmapFound = 0; + + int bitmapNotFound = 0; + + @Override + public void onBitmapFound(AnyObjectId oid) { + bitmapFound += 1; + } + + @Override + public void onBitmapNotFound(AnyObjectId oid) { + bitmapNotFound += 1; + } + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java index 42bafb60ca..3ccd0ef021 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java @@ -17,7 +17,9 @@ import java.io.IOException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.util.FS; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -27,7 +29,7 @@ import org.junit.rules.TemporaryFolder; * test using bazel which doesn't allow tests to create files in the home * directory */ -public class CommitTemplateConfigTest { +public class CommitTemplateConfigTest extends LocalDiskRepositoryTestCase { @Rule public TemporaryFolder tmp = new TemporaryFolder(); @@ -42,9 +44,11 @@ public class CommitTemplateConfigTest { String templateContent = "content of the template"; JGitTestUtil.write(tempFile, templateContent); // proper evaluation of the ~/ directory - String homeDir = System.getProperty("user.home"); + File homeDir = FS.DETECTED.userHome(); File tempFileInHomeDirectory = File.createTempFile("fileInHomeFolder", - ".tmp", new File(homeDir)); + ".tmp", homeDir); + // The home directory should be a mocked temporary directory, but + // still... tempFileInHomeDirectory.deleteOnExit(); JGitTestUtil.write(tempFileInHomeDirectory, templateContent); String expectedTemplatePath = "~/" + tempFileInHomeDirectory.getName(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java index 8f9d105319..06fee8ea71 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java @@ -42,7 +42,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -124,16 +123,16 @@ public class ConfigTest { @Test public void test005_PutGetStringList() { Config c = new Config(); - final LinkedList<String> values = new LinkedList<>(); + List<String> values = new ArrayList<>(); values.add("value1"); values.add("value2"); c.setStringList("my", null, "somename", values); - final Object[] expArr = values.toArray(); - final String[] actArr = c.getStringList("my", null, "somename"); + Object[] expArr = values.toArray(); + String[] actArr = c.getStringList("my", null, "somename"); assertArrayEquals(expArr, actArr); - final String expText = "[my]\n\tsomename = value1\n\tsomename = value2\n"; + String expText = "[my]\n\tsomename = value1\n\tsomename = value2\n"; assertEquals(expText, c.toText()); } @@ -507,6 +506,35 @@ public class ConfigTest { } @Test + public void testRemoveBranchSection() throws ConfigInvalidException { + Config c = parse("" // + + "[branch \"keep\"]\n" + + " merge = master.branch.to.keep.in.the.file\n" + + "\n" + + "[branch \"remove\"]\n" + + " merge = this.will.get.deleted\n" + + " remote = origin-for-some-long-gone-place\n" + + "\n" + + "\n" + + "[core-section-not-to-remove-in-test]\n" + + " packedGitLimit = 14\n" + + "\n" + + "[other]\n" + + " foo = bar\n"); + assertFalse(c.removeSection("branch", "does.not.exist")); + assertTrue(c.removeSection("branch", "remove")); + assertEquals("" // + + "[branch \"keep\"]\n" + + " merge = master.branch.to.keep.in.the.file\n" + + "\n" + + "[core-section-not-to-remove-in-test]\n" + + " packedGitLimit = 14\n" + + "\n" + + "[other]\n" + + " foo = bar\n", c.toText()); + } + + @Test public void testUnsetBranchSection() throws ConfigInvalidException { Config c = parse("" // + "[branch \"keep\"]\n" @@ -516,8 +544,12 @@ public class ConfigTest { + " merge = this.will.get.deleted\n" + " remote = origin-for-some-long-gone-place\n" + "\n" + + "\n" + "[core-section-not-to-remove-in-test]\n" - + " packedGitLimit = 14\n"); + + " packedGitLimit = 14\n" + + "\n" + + "[other]\n" + + " foo = bar\n"); c.unsetSection("branch", "does.not.exist"); c.unsetSection("branch", "remove"); assertEquals("" // @@ -525,7 +557,10 @@ public class ConfigTest { + " merge = master.branch.to.keep.in.the.file\n" + "\n" + "[core-section-not-to-remove-in-test]\n" - + " packedGitLimit = 14\n", c.toText()); + + " packedGitLimit = 14\n" + + "\n" + + "[other]\n" + + " foo = bar\n", c.toText()); } @Test @@ -1482,7 +1517,9 @@ public class ConfigTest { File workTree = tmp.newFolder("dummy-worktree"); File tempFile = tmp.newFile("testCommitTemplate-"); - Repository repo = FileRepositoryBuilder.create(workTree); + Repository repo = FileRepositoryBuilder + .create(new File(workTree, ".git")); + repo.create(); String templateContent = "content of the template"; JGitTestUtil.write(tempFile, templateContent); String expectedTemplatePath = tempFile.getPath(); @@ -1530,14 +1567,15 @@ public class ConfigTest { @Test public void testCommitTemplateEncoding() throws ConfigInvalidException, IOException { - Config config = new Config(null); File workTree = tmp.newFolder("dummy-worktree"); - Repository repo = FileRepositoryBuilder.create(workTree); + Repository repo = FileRepositoryBuilder + .create(new File(workTree, ".git")); + repo.create(); File tempFile = tmp.newFile("testCommitTemplate-"); String templateContent = "content of the template"; JGitTestUtil.write(tempFile, templateContent); String expectedTemplatePath = tempFile.getPath(); - config = parse("[i18n]\n\tcommitEncoding = utf-8\n" + Config config = parse("[i18n]\n\tcommitEncoding = utf-8\n" + "[commit]\n\ttemplate = " + Config.escapeValue(expectedTemplatePath) + "\n"); assertEquals(templateContent, @@ -1551,13 +1589,14 @@ public class ConfigTest { @Test(expected = ConfigInvalidException.class) public void testCommitTemplateWithInvalidEncoding() throws ConfigInvalidException, IOException { - Config config = new Config(null); File workTree = tmp.newFolder("dummy-worktree"); File tempFile = tmp.newFile("testCommitTemplate-"); - Repository repo = FileRepositoryBuilder.create(workTree); + Repository repo = FileRepositoryBuilder + .create(new File(workTree, ".git")); + repo.create(); String templateContent = "content of the template"; JGitTestUtil.write(tempFile, templateContent); - config = parse("[i18n]\n\tcommitEncoding = invalidEcoding\n" + Config config = parse("[i18n]\n\tcommitEncoding = invalidEcoding\n" + "[commit]\n\ttemplate = " + Config.escapeValue(tempFile.getPath()) + "\n"); config.get(CommitConfig.KEY).getCommitTemplateContent(repo); @@ -1566,15 +1605,17 @@ public class ConfigTest { @Test(expected = FileNotFoundException.class) public void testCommitTemplateWithInvalidPath() throws ConfigInvalidException, IOException { - Config config = new Config(null); File workTree = tmp.newFolder("dummy-worktree"); File tempFile = tmp.newFile("testCommitTemplate-"); - Repository repo = FileRepositoryBuilder.create(workTree); + Repository repo = FileRepositoryBuilder + .create(new File(workTree, ".git")); + repo.create(); String templateContent = "content of the template"; JGitTestUtil.write(tempFile, templateContent); // commit message encoding String expectedTemplatePath = "~/nonExistingTemplate"; - config = parse("[commit]\n\ttemplate = " + expectedTemplatePath + "\n"); + Config config = parse( + "[commit]\n\ttemplate = " + expectedTemplatePath + "\n"); String templatePath = config.get(CommitConfig.KEY) .getCommitTemplatePath(); assertEquals(expectedTemplatePath, templatePath); @@ -1595,6 +1636,47 @@ public class ConfigTest { assertFalse(config.get(CoreConfig.KEY).enableCommitGraph()); } + @Test + public void testGetNoDefaultBoolean() { + Config config = new Config(); + assertNull(config.getBoolean("foo", "bar")); + assertNull(config.getBoolean("foo", "bar", "baz")); + } + + @Test + public void testGetNoDefaultEnum() { + Config config = new Config(); + assertNull(config.getEnum(new TestEnum[] { TestEnum.ONE_TWO }, "foo", + "bar", "baz")); + } + + @Test + public void testGetNoDefaultInt() { + Config config = new Config(); + assertNull(config.getInt("foo", "bar")); + assertNull(config.getInt("foo", "bar", "baz")); + } + @Test + public void testGetNoDefaultIntInRange() { + Config config = new Config(); + assertNull(config.getIntInRange("foo", "bar", 1, 5)); + assertNull(config.getIntInRange("foo", "bar", "baz", 1, 5)); + } + + @Test + public void testGetNoDefaultLong() { + Config config = new Config(); + assertNull(config.getLong("foo", "bar")); + assertNull(config.getLong("foo", "bar", "baz")); + } + + @Test + public void testGetNoDefaultTimeUnit() { + Config config = new Config(); + assertNull(config.getTimeUnit("foo", "bar", "baz", + TimeUnit.SECONDS)); + } + private static void assertValueRoundTrip(String value) throws ConfigInvalidException { assertValueRoundTrip(value, value); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java index 32f6766d47..5c2b190777 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java @@ -96,6 +96,16 @@ public class GpgConfigTest { } @Test + public void testGetKeyFormat_ssh() throws Exception { + Config c = parse("" // + + "[gpg]\n" // + + " format = ssh\n" // + ); + + assertEquals(GpgConfig.GpgFormat.SSH, new GpgConfig(c).getKeyFormat()); + } + + @Test public void testGetSigningKey() throws Exception { Config c = parse("" // + "[user]\n" // diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java index 2b7b6ca76c..cd98606e53 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java @@ -2,7 +2,7 @@ * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> - * Copyright (C) 2013, Robin Stocker <robin@nibor.org> and others + * Copyright (C) 2013, 2025 Robin Stocker <robin@nibor.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -539,7 +539,7 @@ public class IndexDiffTest extends RepositoryTestCase { assertTrue(diff.getAssumeUnchanged().contains("file3")); assertTrue(diff.getModified().contains("file")); - git.add().addFilepattern(".").call(); + git.add().addFilepattern(".").setAll(false).call(); iterator = new FileTreeIterator(db); diff = new IndexDiff(db, Constants.HEAD, iterator); @@ -551,6 +551,18 @@ public class IndexDiffTest extends RepositoryTestCase { assertTrue(diff.getAssumeUnchanged().contains("file3")); assertTrue(diff.getChanged().contains("file")); assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders()); + + git.add().addFilepattern(".").call(); + + iterator = new FileTreeIterator(db); + diff = new IndexDiff(db, Constants.HEAD, iterator); + diff.diff(); + assertEquals(1, diff.getAssumeUnchanged().size()); + assertEquals(0, diff.getModified().size()); + assertEquals(1, diff.getChanged().size()); + assertTrue(diff.getAssumeUnchanged().contains("file2")); + assertTrue(diff.getChanged().contains("file")); + assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java index 5c44c9c44d..3ec4b6a073 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java @@ -11,7 +11,6 @@ package org.eclipse.jgit.lib; -import static java.lang.Integer.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.junit.JGitTestUtil.concat; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; @@ -92,7 +91,7 @@ public class ObjectCheckerTest { public void testInvalidType() { String msg = MessageFormat.format( JGitText.get().corruptObjectInvalidType2, - valueOf(OBJ_BAD)); + Integer.valueOf(OBJ_BAD)); assertCorrupt(msg, OBJ_BAD, new byte[0]); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java index 21032c341f..d6f0b038d2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java @@ -16,6 +16,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.nio.ByteBuffer; import java.util.Locale; import org.eclipse.jgit.errors.InvalidObjectIdException; @@ -153,4 +154,16 @@ public class ObjectIdTest { assertEquals(ObjectId.fromRaw(exp).name(), id.name()); } } + + @Test + public void test_toFromByteBuffer_raw() { + ObjectId oid = ObjectId + .fromString("ff00eedd003713bb1bb26b808ec9312548e73946"); + ByteBuffer anObject = ByteBuffer.allocate(Constants.OBJECT_ID_LENGTH); + oid.copyRawTo(anObject); + anObject.flip(); + + ObjectId actual = ObjectId.fromRaw(anObject); + assertEquals(oid.name(), actual.name()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java index 97da1757e0..943a68b82c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java @@ -55,7 +55,8 @@ public class PersonIdentTest { p.getWhenAsInstant()); assertEquals("A U Thor <author@example.com> 1142878501 -0500", p.toExternalString()); - assertEquals(ZoneId.of("GMT-05:00"), p.getZoneId()); + assertEquals(ZoneId.of("GMT-05:00").getRules().getOffset( + Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset()); } @Test @@ -69,7 +70,8 @@ public class PersonIdentTest { p.getWhenAsInstant()); assertEquals("A U Thor <author@example.com> 1142878501 +0530", p.toExternalString()); - assertEquals(ZoneId.of("GMT+05:30"), p.getZoneId()); + assertEquals(ZoneId.of("GMT+05:30").getRules().getOffset( + Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset()); } @SuppressWarnings("unused") diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java index b1d80c5c30..f25e5d10ff 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java @@ -30,6 +30,22 @@ import org.junit.Test; public class RacyGitTests extends RepositoryTestCase { + /** + * This test is inherently flaky in nature since using clocks in a computer + * to determine file modifications in a filesystem from Java is difficult + * and depends on many factors and we can't test all combinations + * + * If this test fails on your computer, don't worry but let us know if you + * are willing to provide details which may help to further improve handling + * of the racy git problem in JGit. + * + * Despite not being completely reproducible this test is still useful to + * detect regressions when running this test repeatedly on the same + * OS/filesystem/Java version (which we do on the CI used to build JGit). + * + * @see "https://git-scm.com/docs/racy-git" + * @see "https://www.youtube.com/watch?v=m44cAozuLNI" + */ @Test public void testRacyGitDetection() throws Exception { // Reset to force creation of index file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java index b02f245865..85f9612b6a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java @@ -71,6 +71,11 @@ public class RefDatabaseConflictingNamesTest { } @Override + public ReflogReader getReflogReader(Ref ref) throws IOException { + return null; + } + + @Override public void create() throws IOException { // Not needed } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java index 854180e3ea..a93937eeea 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java @@ -16,6 +16,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.storage.file.FileBasedConfig; @@ -24,22 +27,23 @@ import org.junit.Test; public class ReflogConfigTest extends RepositoryTestCase { @Test public void testlogAllRefUpdates() throws Exception { - long commitTime = 1154236443000L; - int tz = -4 * 60; + Instant commitTime = Instant.ofEpochSecond(1154236443L); + ZoneOffset tz = ZoneOffset.ofHours(-4); // check that there are no entries in the reflog and turn off writing // reflogs - assertTrue(db.getReflogReader(Constants.HEAD).getReverseEntries() + RefDatabase refDb = db.getRefDatabase(); + assertTrue(refDb.getReflogReader(Constants.HEAD).getReverseEntries() .isEmpty()); - final FileBasedConfig cfg = db.getConfig(); + FileBasedConfig cfg = db.getConfig(); cfg.setBoolean("core", null, "logallrefupdates", false); cfg.save(); // do one commit and check that reflog size is 0: no reflogs should be // written commit("A Commit\n", commitTime, tz); - commitTime += 60 * 1000; - assertTrue("Reflog for HEAD still contain no entry", db + commitTime = commitTime.plus(Duration.ofMinutes(1)); + assertTrue("Reflog for HEAD still contain no entry", refDb .getReflogReader(Constants.HEAD).getReverseEntries().isEmpty()); // set the logAllRefUpdates parameter to true and check it @@ -52,10 +56,10 @@ public class ReflogConfigTest extends RepositoryTestCase { // do one commit and check that reflog size is increased to 1 commit("A Commit\n", commitTime, tz); - commitTime += 60 * 1000; - assertTrue( - "Reflog for HEAD should contain one entry", - db.getReflogReader(Constants.HEAD).getReverseEntries().size() == 1); + commitTime = commitTime.plus(Duration.ofMinutes(1)); + assertTrue("Reflog for HEAD should contain one entry", + refDb.getReflogReader(Constants.HEAD).getReverseEntries() + .size() == 1); // set the logAllRefUpdates parameter to false and check it cfg.setBoolean("core", null, "logallrefupdates", false); @@ -67,10 +71,10 @@ public class ReflogConfigTest extends RepositoryTestCase { // do one commit and check that reflog size is 2 commit("A Commit\n", commitTime, tz); - commitTime += 60 * 1000; - assertTrue( - "Reflog for HEAD should contain two entries", - db.getReflogReader(Constants.HEAD).getReverseEntries().size() == 2); + commitTime = commitTime.plus(Duration.ofMinutes(1)); + assertTrue("Reflog for HEAD should contain two entries", + refDb.getReflogReader(Constants.HEAD).getReverseEntries() + .size() == 2); // set the logAllRefUpdates parameter to false and check it cfg.setEnum("core", null, "logallrefupdates", @@ -84,13 +88,13 @@ public class ReflogConfigTest extends RepositoryTestCase { // do one commit and check that reflog size is 3 commit("A Commit\n", commitTime, tz); assertTrue("Reflog for HEAD should contain three entries", - db.getReflogReader(Constants.HEAD).getReverseEntries() + refDb.getReflogReader(Constants.HEAD).getReverseEntries() .size() == 3); } - private void commit(String commitMsg, long commitTime, int tz) + private void commit(String commitMsg, Instant commitTime, ZoneOffset tz) throws IOException { - final CommitBuilder commit = new CommitBuilder(); + CommitBuilder commit = new CommitBuilder(); commit.setAuthor(new PersonIdent(author, commitTime, tz)); commit.setCommitter(new PersonIdent(committer, commitTime, tz)); commit.setMessage(commitMsg); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java index e21ff580bd..a5a6ce5d76 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java @@ -125,7 +125,7 @@ public class ThreadSafeProgressMonitorTest { try { assertTrue("latch released", cdl.await(1000, TimeUnit.MILLISECONDS)); } catch (InterruptedException ie) { - fail("Did not expect to be interrupted"); + throw new AssertionError("Did not expect to be interrupted", ie); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java index ae811f830f..8865ba9ebd 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java @@ -15,6 +15,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.time.Instant; +import java.time.ZoneOffset; + import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.junit.RepositoryTestCase; @@ -162,7 +165,8 @@ public class CherryPickTest extends RepositoryTestCase { final ObjectId[] parentIds) throws Exception { final CommitBuilder c = new CommitBuilder(); c.setTreeId(treeB.writeTree(odi)); - c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0)); + c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", + Instant.ofEpochSecond(1), ZoneOffset.UTC)); c.setCommitter(c.getAuthor()); c.setParentIds(parentIds); c.setMessage("Tree " + c.getTreeId().name()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java index f410960bec..b1998f30f8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java @@ -15,6 +15,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.dircache.DirCache; @@ -357,7 +359,8 @@ public class GitlinkMergeTest extends SampleDataRepositoryTestCase { ObjectId[] parentIds) throws Exception { CommitBuilder c = new CommitBuilder(); c.setTreeId(treeB.writeTree(odi)); - c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0)); + c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", + Instant.ofEpochSecond(1), ZoneOffset.UTC)); c.setCommitter(c.getAuthor()); c.setParentIds(parentIds); c.setMessage("Tree " + c.getTreeId().name()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java index 5f4331b04d..7a8a93e977 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java @@ -47,7 +47,10 @@ public class MergeAlgorithmTest { @Test public void testTwoConflictingModifications() throws IOException { assertEquals(t("a<b=Z>Zdefghij"), - merge("abcdefghij", "abZdefghij", "aZZdefghij")); + merge("abcdefghij", "abZdefghij", "aZZdefghij", false)); + + assertEquals(t("a<b|b=Z>Zdefghij"), + merge("abcdefghij", "abZdefghij", "aZZdefghij", true)); } /** @@ -60,7 +63,10 @@ public class MergeAlgorithmTest { @Test public void testOneAgainstTwoConflictingModifications() throws IOException { assertEquals(t("aZ<Z=c>Zefghij"), - merge("abcdefghij", "aZZZefghij", "aZcZefghij")); + merge("abcdefghij", "aZZZefghij", "aZcZefghij", false)); + + assertEquals(t("aZ<Z|c=c>Zefghij"), + merge("abcdefghij", "aZZZefghij", "aZcZefghij", true)); } /** @@ -72,7 +78,10 @@ public class MergeAlgorithmTest { @Test public void testNoAgainstOneModification() throws IOException { assertEquals(t("aZcZefghij"), - merge("abcdefghij", "abcdefghij", "aZcZefghij")); + merge("abcdefghij", "abcdefghij", "aZcZefghij", false)); + + assertEquals(t("aZcZefghij"), + merge("abcdefghij", "abcdefghij", "aZcZefghij", true)); } /** @@ -84,7 +93,10 @@ public class MergeAlgorithmTest { @Test public void testTwoNonConflictingModifications() throws IOException { assertEquals(t("YbZdefghij"), - merge("abcdefghij", "abZdefghij", "Ybcdefghij")); + merge("abcdefghij", "abZdefghij", "Ybcdefghij", false)); + + assertEquals(t("YbZdefghij"), + merge("abcdefghij", "abZdefghij", "Ybcdefghij", true)); } /** @@ -96,7 +108,10 @@ public class MergeAlgorithmTest { @Test public void testTwoComplicatedModifications() throws IOException { assertEquals(t("a<ZZZZfZhZj=bYdYYYYiY>"), - merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY")); + merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY", false)); + + assertEquals(t("a<ZZZZfZhZj|bcdefghij=bYdYYYYiY>"), + merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY", true)); } /** @@ -109,7 +124,9 @@ public class MergeAlgorithmTest { @Test public void testTwoModificationsWithSharedDelete() throws IOException { assertEquals(t("Cb}n}"), - merge("ab}n}n}", "ab}n}", "Cb}n}")); + merge("ab}n}n}", "ab}n}", "Cb}n}", false)); + + assertEquals(t("Cb}n}"), merge("ab}n}n}", "ab}n}", "Cb}n}", true)); } /** @@ -122,7 +139,11 @@ public class MergeAlgorithmTest { @Test public void testModificationsWithMiddleInsert() throws IOException { assertEquals(t("aBcd123123uvwxPq"), - merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq")); + merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq", + false)); + + assertEquals(t("aBcd123123uvwxPq"), merge("abcd123uvwxpq", + "aBcd123123uvwxPq", "abcd123123uvwxpq", true)); } /** @@ -135,7 +156,23 @@ public class MergeAlgorithmTest { @Test public void testModificationsWithMiddleDelete() throws IOException { assertEquals(t("Abz}z123Q"), - merge("abz}z}z123q", "Abz}z123Q", "abz}z123q")); + merge("abz}z}z123q", "Abz}z123Q", "abz}z123q", false)); + + assertEquals(t("Abz}z123Q"), + merge("abz}z}z123q", "Abz}z123Q", "abz}z123q", true)); + } + + @Test + public void testInsertionAfterDeletion() throws IOException { + assertEquals(t("a<=bc>d"), merge("abd", "ad", "abcd", false)); + assertEquals(t("a<|b=bc>d"), + merge("abd", "ad", "abcd", true)); + } + + @Test + public void testInsertionBeforeDeletion() throws IOException { + assertEquals(t("a<=cb>d"), merge("abd", "ad", "acbd", false)); + assertEquals(t("a<|b=cb>d"), merge("abd", "ad", "acbd", true)); } /** @@ -146,7 +183,10 @@ public class MergeAlgorithmTest { @Test public void testConflictAtStart() throws IOException { assertEquals(t("<Z=Y>bcdefghij"), - merge("abcdefghij", "Zbcdefghij", "Ybcdefghij")); + merge("abcdefghij", "Zbcdefghij", "Ybcdefghij", false)); + + assertEquals(t("<Z|a=Y>bcdefghij"), + merge("abcdefghij", "Zbcdefghij", "Ybcdefghij", true)); } /** @@ -157,7 +197,10 @@ public class MergeAlgorithmTest { @Test public void testConflictAtEnd() throws IOException { assertEquals(t("abcdefghi<Z=Y>"), - merge("abcdefghij", "abcdefghiZ", "abcdefghiY")); + merge("abcdefghij", "abcdefghiZ", "abcdefghiY", false)); + + assertEquals(t("abcdefghi<Z|j=Y>"), + merge("abcdefghij", "abcdefghiZ", "abcdefghiY", true)); } /** @@ -169,7 +212,10 @@ public class MergeAlgorithmTest { @Test public void testSameModification() throws IOException { assertEquals(t("abZdefghij"), - merge("abcdefghij", "abZdefghij", "abZdefghij")); + merge("abcdefghij", "abZdefghij", "abZdefghij", false)); + + assertEquals(t("abZdefghij"), + merge("abcdefghij", "abZdefghij", "abZdefghij", true)); } /** @@ -181,27 +227,36 @@ public class MergeAlgorithmTest { @Test public void testDeleteVsModify() throws IOException { assertEquals(t("ab<=Z>defghij"), - merge("abcdefghij", "abdefghij", "abZdefghij")); + merge("abcdefghij", "abdefghij", "abZdefghij", false)); + + assertEquals(t("ab<|c=Z>defghij"), + merge("abcdefghij", "abdefghij", "abZdefghij", true)); } @Test public void testInsertVsModify() throws IOException { - assertEquals(t("a<bZ=XY>"), merge("ab", "abZ", "aXY")); + assertEquals(t("a<bZ=XY>"), merge("ab", "abZ", "aXY", false)); + assertEquals(t("a<bZ|b=XY>"), merge("ab", "abZ", "aXY", true)); } @Test public void testAdjacentModifications() throws IOException { - assertEquals(t("a<Zc=bY>d"), merge("abcd", "aZcd", "abYd")); + assertEquals(t("a<Zc=bY>d"), merge("abcd", "aZcd", "abYd", false)); + assertEquals(t("a<Zc|bc=bY>d"), merge("abcd", "aZcd", "abYd", true)); } @Test public void testSeparateModifications() throws IOException { - assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe")); + assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe", false)); + assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe", true)); } @Test public void testBlankLines() throws IOException { - assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe")); + assertEquals(t("aZc\nYe"), + merge("abc\nde", "aZc\nde", "abc\nYe", false)); + assertEquals(t("aZc\nYe"), + merge("abc\nde", "aZc\nde", "abc\nYe", true)); } /** @@ -214,11 +269,22 @@ public class MergeAlgorithmTest { */ @Test public void testTwoSimilarModsAndOneInsert() throws IOException { - assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde")); - assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB")); - assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB")); + assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde", false)); + assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde", true)); + + assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB", false)); + assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB", true)); + + assertEquals(t("HIAAAJCAB"), + merge("HiACAB", "HIACAB", "HIAAAJCAB", false)); + assertEquals(t("HIAAAJCAB"), + merge("HiACAB", "HIACAB", "HIAAAJCAB", true)); + + assertEquals(t("AGADEFHIAAAJCAB"), + merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB", + false)); assertEquals(t("AGADEFHIAAAJCAB"), - merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB")); + merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB", true)); } /** @@ -232,18 +298,28 @@ public class MergeAlgorithmTest { @Test public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException { Assume.assumeTrue(newlineAtEnd); - assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ")); - assertEquals(t("IAJ"), merge("iA", "IA", "IAJ")); - assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ")); + assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ", false)); + assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ", true)); + + assertEquals(t("IAJ"), merge("iA", "IA", "IAJ", false)); + assertEquals(t("IAJ"), merge("iA", "IA", "IAJ", true)); + + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ", false)); + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ", true)); } @Test public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd() throws IOException { Assume.assumeFalse(newlineAtEnd); - assertEquals(t("I<A=AAJ>"), merge("iA", "IA", "IAAJ")); - assertEquals(t("I<A=AJ>"), merge("iA", "IA", "IAJ")); - assertEquals(t("I<A=AAAJ>"), merge("iA", "IA", "IAAAJ")); + assertEquals(t("I<A=AAJ>"), merge("iA", "IA", "IAAJ", false)); + assertEquals(t("I<A|A=AAJ>"), merge("iA", "IA", "IAAJ", true)); + + assertEquals(t("I<A=AJ>"), merge("iA", "IA", "IAJ", false)); + assertEquals(t("I<A|A=AJ>"), merge("iA", "IA", "IAJ", true)); + + assertEquals(t("I<A=AAAJ>"), merge("iA", "IA", "IAAAJ", false)); + assertEquals(t("I<A|A=AAAJ>"), merge("iA", "IA", "IAAAJ", true)); } /** @@ -254,22 +330,34 @@ public class MergeAlgorithmTest { @Test public void testEmptyTexts() throws IOException { // test modification against deletion - assertEquals(t("<AB=>"), merge("A", "AB", "")); - assertEquals(t("<=AB>"), merge("A", "", "AB")); + assertEquals(t("<AB=>"), merge("A", "AB", "", false)); + assertEquals(t("<AB|A=>"), merge("A", "AB", "", true)); + + assertEquals(t("<=AB>"), merge("A", "", "AB", false)); + assertEquals(t("<|A=AB>"), merge("A", "", "AB", true)); // test unmodified against deletion - assertEquals(t(""), merge("AB", "AB", "")); - assertEquals(t(""), merge("AB", "", "AB")); + assertEquals(t(""), merge("AB", "AB", "", false)); + assertEquals(t(""), merge("AB", "AB", "", true)); + + assertEquals(t(""), merge("AB", "", "AB", false)); + assertEquals(t(""), merge("AB", "", "AB", true)); // test deletion against deletion - assertEquals(t(""), merge("AB", "", "")); + assertEquals(t(""), merge("AB", "", "", false)); + assertEquals(t(""), merge("AB", "", "", true)); } - private String merge(String commonBase, String ours, String theirs) throws IOException { + private String merge(String commonBase, String ours, String theirs, + boolean diff3) throws IOException { MergeResult r = new MergeAlgorithm().merge(RawTextComparator.DEFAULT, T(commonBase), T(ours), T(theirs)); ByteArrayOutputStream bo=new ByteArrayOutputStream(50); - fmt.formatMerge(bo, r, "B", "O", "T", UTF_8); + if (diff3) { + fmt.formatMergeDiff3(bo, r, "B", "O", "T", UTF_8); + } else { + fmt.formatMerge(bo, r, "B", "O", "T", UTF_8); + } return new String(bo.toByteArray(), UTF_8); } @@ -284,6 +372,9 @@ public class MergeAlgorithmTest { case '=': r.append("=======\n"); break; + case '|': + r.append("||||||| B\n"); + break; case '>': r.append(">>>>>>> T\n"); break; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java new file mode 100644 index 0000000000..3a8af7a00e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2024 Qualcomm Innovation Center, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.merge; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.lib.Constants; +import org.junit.Assume; +import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class MergeAlgorithmUnionTest { + MergeFormatter fmt = new MergeFormatter(); + + private final boolean newlineAtEnd; + + @DataPoints + public static boolean[] newlineAtEndDataPoints = { false, true }; + + public MergeAlgorithmUnionTest(boolean newlineAtEnd) { + this.newlineAtEnd = newlineAtEnd; + } + + /** + * Check for a conflict where the second text was changed similar to the + * first one, but the second texts modification covers one more line. + * + * @throws java.io.IOException + */ + @Test + public void testTwoConflictingModifications() throws IOException { + assertEquals(t("abZZdefghij"), + merge("abcdefghij", "abZdefghij", "aZZdefghij")); + } + + /** + * Test a case where we have three consecutive chunks. The first text + * modifies all three chunks. The second text modifies the first and the + * last chunk. This should be reported as one conflicting region. + * + * @throws java.io.IOException + */ + @Test + public void testOneAgainstTwoConflictingModifications() throws IOException { + assertEquals(t("aZZcZefghij"), + merge("abcdefghij", "aZZZefghij", "aZcZefghij")); + } + + /** + * Test a merge where only the second text contains modifications. Expect as + * merge result the second text. + * + * @throws java.io.IOException + */ + @Test + public void testNoAgainstOneModification() throws IOException { + assertEquals(t("aZcZefghij"), + merge("abcdefghij", "abcdefghij", "aZcZefghij")); + } + + /** + * Both texts contain modifications but not on the same chunks. Expect a + * non-conflict merge result. + * + * @throws java.io.IOException + */ + @Test + public void testTwoNonConflictingModifications() throws IOException { + assertEquals(t("YbZdefghij"), + merge("abcdefghij", "abZdefghij", "Ybcdefghij")); + } + + /** + * Merge two complicated modifications. The merge algorithm has to extend + * and combine conflicting regions to get to the expected merge result. + * + * @throws java.io.IOException + */ + @Test + public void testTwoComplicatedModifications() throws IOException { + assertEquals(t("aZZZZfZhZjbYdYYYYiY"), + merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY")); + } + + /** + * Merge two modifications with a shared delete at the end. The underlying + * diff algorithm has to provide consistent edit results to get the expected + * merge result. + * + * @throws java.io.IOException + */ + @Test + public void testTwoModificationsWithSharedDelete() throws IOException { + assertEquals(t("Cb}n}"), merge("ab}n}n}", "ab}n}", "Cb}n}")); + } + + /** + * Merge modifications with a shared insert in the middle. The underlying + * diff algorithm has to provide consistent edit results to get the expected + * merge result. + * + * @throws java.io.IOException + */ + @Test + public void testModificationsWithMiddleInsert() throws IOException { + assertEquals(t("aBcd123123uvwxPq"), + merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq")); + } + + /** + * Merge modifications with a shared delete in the middle. The underlying + * diff algorithm has to provide consistent edit results to get the expected + * merge result. + * + * @throws java.io.IOException + */ + @Test + public void testModificationsWithMiddleDelete() throws IOException { + assertEquals(t("Abz}z123Q"), + merge("abz}z}z123q", "Abz}z123Q", "abz}z123q")); + } + + @Test + public void testInsertionAfterDeletion() throws IOException { + assertEquals(t("abcd"), merge("abd", "ad", "abcd")); + } + + @Test + public void testInsertionBeforeDeletion() throws IOException { + assertEquals(t("acbd"), merge("abd", "ad", "acbd")); + } + + /** + * Test a conflicting region at the very start of the text. + * + * @throws java.io.IOException + */ + @Test + public void testConflictAtStart() throws IOException { + assertEquals(t("ZYbcdefghij"), + merge("abcdefghij", "Zbcdefghij", "Ybcdefghij")); + } + + /** + * Test a conflicting region at the very end of the text. + * + * @throws java.io.IOException + */ + @Test + public void testConflictAtEnd() throws IOException { + assertEquals(t("abcdefghiZY"), + merge("abcdefghij", "abcdefghiZ", "abcdefghiY")); + } + + /** + * Check for a conflict where the second text was changed similar to the + * first one, but the second texts modification covers one more line. + * + * @throws java.io.IOException + */ + @Test + public void testSameModification() throws IOException { + assertEquals(t("abZdefghij"), + merge("abcdefghij", "abZdefghij", "abZdefghij")); + } + + /** + * Check that a deleted vs. a modified line shows up as conflict (see Bug + * 328551) + * + * @throws java.io.IOException + */ + @Test + public void testDeleteVsModify() throws IOException { + assertEquals(t("abZdefghij"), + merge("abcdefghij", "abdefghij", "abZdefghij")); + } + + @Test + public void testInsertVsModify() throws IOException { + assertEquals(t("abZXY"), merge("ab", "abZ", "aXY")); + } + + @Test + public void testAdjacentModifications() throws IOException { + assertEquals(t("aZcbYd"), merge("abcd", "aZcd", "abYd")); + } + + @Test + public void testSeparateModifications() throws IOException { + assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe")); + } + + @Test + public void testBlankLines() throws IOException { + assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe")); + } + + /** + * Test merging two contents which do one similar modification and one + * insertion is only done by one side, in the middle. Between modification + * and insertion is a block which is common between the two contents and the + * common base + * + * @throws java.io.IOException + */ + @Test + public void testTwoSimilarModsAndOneInsert() throws IOException { + assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde")); + + assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB")); + + assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB")); + + assertEquals(t("AGADEFHIAAAJCAB"), + merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB")); + } + + /** + * Test merging two contents which do one similar modification and one + * insertion is only done by one side, at the end. Between modification and + * insertion is a block which is common between the two contents and the + * common base + * + * @throws java.io.IOException + */ + @Test + public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException { + Assume.assumeTrue(newlineAtEnd); + assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ")); + + assertEquals(t("IAJ"), merge("iA", "IA", "IAJ")); + + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ")); + } + + @Test + public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd() + throws IOException { + Assume.assumeFalse(newlineAtEnd); + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAJ")); + + assertEquals(t("IAAJ"), merge("iA", "IA", "IAJ")); + + assertEquals(t("IAAAAJ"), merge("iA", "IA", "IAAAJ")); + } + + // Test situations where (at least) one input value is the empty text + + @Test + public void testEmptyTextModifiedAgainstDeletion() throws IOException { + // NOTE: git.git merge-file appends a '\n' to the end of the file even + // when the input files do not have a newline at the end. That appears + // to be a bug in git.git. + assertEquals(t("AB"), merge("A", "AB", "")); + assertEquals(t("AB"), merge("A", "", "AB")); + } + + @Test + public void testEmptyTextUnmodifiedAgainstDeletion() throws IOException { + assertEquals(t(""), merge("AB", "AB", "")); + + assertEquals(t(""), merge("AB", "", "AB")); + } + + @Test + public void testEmptyTextDeletionAgainstDeletion() throws IOException { + assertEquals(t(""), merge("AB", "", "")); + } + + private String merge(String commonBase, String ours, String theirs) + throws IOException { + MergeAlgorithm ma = new MergeAlgorithm(); + ma.setContentMergeStrategy(ContentMergeStrategy.UNION); + MergeResult<RawText> r = ma.merge(RawTextComparator.DEFAULT, + T(commonBase), T(ours), T(theirs)); + ByteArrayOutputStream bo = new ByteArrayOutputStream(50); + fmt.formatMerge(bo, r, "B", "O", "T", UTF_8); + return bo.toString(UTF_8); + } + + public String t(String text) { + StringBuilder r = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + switch (c) { + case '<': + r.append("<<<<<<< O\n"); + break; + case '=': + r.append("=======\n"); + break; + case '|': + r.append("||||||| B\n"); + break; + case '>': + r.append(">>>>>>> T\n"); + break; + default: + r.append(c); + if (newlineAtEnd || i < text.length() - 1) + r.append('\n'); + } + } + return r.toString(); + } + + public RawText T(String text) { + return new RawText(Constants.encode(t(text))); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java index 022e8cd55e..c6a6321cf8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java @@ -22,9 +22,12 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.file.Files; import java.time.Instant; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeResult; @@ -51,6 +54,7 @@ import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectStream; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; import org.eclipse.jgit.revwalk.RevCommit; @@ -1442,6 +1446,8 @@ public class MergerTest extends RepositoryTestCase { git.checkout().setName("master").call(); mergeResult = git.merge().include(commitX).setStrategy(strategy) .call(); + assertEquals(MergeResult.MergeStatus.MERGED, + mergeResult.getMergeStatus()); // Now, merge commit A and B (i.e. "master" and "second-branch"). // None of them have the file "a", so there is no conflict, BUT while @@ -1735,25 +1741,25 @@ public class MergerTest extends RepositoryTestCase { git.add().addFilepattern("c").call(); RevCommit commitI = git.commit().setMessage("Initial commit").call(); - File a = writeTrashFile("a", "content in Ancestor"); + writeTrashFile("a", "content in Ancestor"); git.add().addFilepattern("a").call(); RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call(); - a = writeTrashFile("a", "content in Child 1 (commited on master)"); + writeTrashFile("a", "content in Child 1 (commited on master)"); git.add().addFilepattern("a").call(); // commit C1M git.commit().setMessage("Child 1 on master").call(); git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call(); // "a" becomes executable in A2 - a = writeTrashFile("a", "content in Ancestor"); + File a = writeTrashFile("a", "content in Ancestor"); a.setExecutable(true); git.add().addFilepattern("a").call(); RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call(); // second branch git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call(); - a = writeTrashFile("a", "content in Child 2 (commited on second-branch)"); + writeTrashFile("a", "content in Child 2 (commited on second-branch)"); git.add().addFilepattern("a").call(); // commit C2S git.commit().setMessage("Child 2 on second-branch").call(); @@ -1786,7 +1792,259 @@ public class MergerTest extends RepositoryTestCase { // children mergeResult = git.merge().include(commitC3S).call(); assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED); + } + + /** + * Merging two commits when binary files have equal content, but conflicting content in the + * virtual ancestor. + * + * <p> + * This test has the same set up as + * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only + * with the content conflict in A1 and A2. + */ + @Theory + public void checkBinaryMergeConflictInVirtualAncestor(MergeStrategy strategy) throws Exception { + if (!strategy.equals(MergeStrategy.RECURSIVE)) { + return; + } + + Git git = Git.wrap(db); + + // master + writeTrashFile("c", "initial file"); + git.add().addFilepattern("c").call(); + RevCommit commitI = git.commit().setMessage("Initial commit").call(); + + writeTrashFile("a", "\0\1\1\1\1\0"); // content in Ancestor 1 + git.add().addFilepattern("a").call(); + RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call(); + + writeTrashFile("a", "\0\1\2\3\4\5\0"); // content in Child 1 (commited on master) + git.add().addFilepattern("a").call(); + // commit C1M + git.commit().setMessage("Child 1 on master").call(); + + git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call(); + writeTrashFile("a", "\0\2\2\2\2\0"); // content in Ancestor 1 + git.add().addFilepattern("a").call(); + RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call(); + + // second branch + git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call(); + writeTrashFile("a", "\0\5\4\3\2\1\0"); // content in Child 2 (commited on second-branch) + git.add().addFilepattern("a").call(); + // commit C2S + git.commit().setMessage("Child 2 on second-branch").call(); + + // Merge branch-to-merge into second-branch + MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call(); + assertEquals(mergeResult.getNewHead(), null); + assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING); + // Resolve the conflict manually + writeTrashFile("a", "\0\3\3\3\3\0"); // merge conflict resolution + git.add().addFilepattern("a").call(); + RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call(); + + // Merge branch-to-merge into master + git.checkout().setName("master").call(); + mergeResult = git.merge().include(commitA2).setStrategy(strategy).call(); + assertEquals(mergeResult.getNewHead(), null); + assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING); + + // Resolve the conflict manually - set the same value as in resolution above + writeTrashFile("a", "\0\3\3\3\3\0"); // merge conflict resolution + git.add().addFilepattern("a").call(); + // commit C4M + git.commit().setMessage("Child 4 on master - resolve merge conflict").call(); + + // Merge C4M (second-branch) into master (C3S) + // Conflict in virtual base should be here, but there are no conflicts in + // children + mergeResult = git.merge().include(commitC3S).call(); + assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED); + } + + /** + * File is binary in ours, theirs and base with different content in each of + * them. Content of the file should not change after the merge conflict as + * no conflict markers are added to the binary files + */ + @Theory + public void oursBinaryTheirsBinaryBaseBinary(MergeStrategy strategy) + throws Exception { + Git git = Git.wrap(db); + String binaryFile = "file"; + + writeTrashFile(binaryFile, "\u0000\u0001"); + git.add().addFilepattern(binaryFile).call(); + RevCommit parent = git.commit().setMessage("BASE COMMIT").call(); + String fileHashInBase = getFileHashInWorkTree(git, binaryFile); + + writeTrashFile(binaryFile, "\u0001\u0002"); + git.add().addFilepattern(binaryFile).call(); + RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call(); + String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile); + + git.checkout().setCreateBranch(true).setStartPoint(parent) + .setName("side").call(); + + writeTrashFile(binaryFile, "\u0002\u0000"); + git.add().addFilepattern(binaryFile).call(); + git.commit().setMessage("OURS COMMIT").call(); + String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile); + + MergeResult mergeResult = git.merge().setStrategy(strategy) + .include(child1).call(); + + // check if the merge caused a conflict + assertTrue(mergeResult.getConflicts() != null + && !mergeResult.getConflicts().isEmpty()); + String fileHashInChild2AfterMerge = getFileHashInWorkTree(git, + binaryFile); + + // check if the file content changed during a conflicting merge + assertEquals(fileHashInChild2AfterMerge, fileHashInChild2); + + Set<String> hashesInIndexFile = new HashSet<>(); + DirCache indexContent = git.getRepository().readDirCache(); + for (int i = 0; i < indexContent.getEntryCount(); ++i) { + DirCacheEntry indexEntry = indexContent.getEntry(i); + if (binaryFile.equals(indexEntry.getPathString())) { + hashesInIndexFile.add(indexEntry.getObjectId().name()); + } + } + + // check if all the three stages are added to index file + assertTrue(hashesInIndexFile.contains(fileHashInBase)); + assertTrue(hashesInIndexFile.contains(fileHashInChild1)); + assertTrue(hashesInIndexFile.contains(fileHashInChild2)); + } + + /** + * File is text in ours and theirs with different content but binary in + * base. Even in this case, file will be treated as a binary and no conflict + * markers are added to it + */ + @Theory + public void oursAndTheirsDifferentTextBaseBinary(MergeStrategy strategy) + throws Exception { + Git git = Git.wrap(db); + String binaryFile = "file"; + + writeTrashFile(binaryFile, "\u0000\u0001"); + git.add().addFilepattern(binaryFile).call(); + RevCommit parent = git.commit().setMessage("BASE COMMIT").call(); + String fileHashInBase = getFileHashInWorkTree(git, binaryFile); + + writeTrashFile(binaryFile, "TEXT1"); + git.add().addFilepattern(binaryFile).call(); + RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call(); + String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile); + + git.checkout().setCreateBranch(true).setStartPoint(parent) + .setName("side").call(); + + writeTrashFile(binaryFile, "TEXT2"); + git.add().addFilepattern(binaryFile).call(); + git.commit().setMessage("OURS COMMIT").call(); + String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile); + + MergeResult mergeResult = git.merge().setStrategy(strategy) + .include(child1).call(); + + assertTrue(mergeResult.getConflicts() != null + && !mergeResult.getConflicts().isEmpty()); + String fileHashInChild2AfterMerge = getFileHashInWorkTree(git, + binaryFile); + + assertEquals(fileHashInChild2AfterMerge, fileHashInChild2); + + Set<String> hashesInIndexFile = new HashSet<>(); + DirCache indexContent = git.getRepository().readDirCache(); + for (int i = 0; i < indexContent.getEntryCount(); ++i) { + DirCacheEntry indexEntry = indexContent.getEntry(i); + if (binaryFile.equals(indexEntry.getPathString())) { + hashesInIndexFile.add(indexEntry.getObjectId().name()); + } + } + + assertTrue(hashesInIndexFile.contains(fileHashInBase)); + assertTrue(hashesInIndexFile.contains(fileHashInChild1)); + assertTrue(hashesInIndexFile.contains(fileHashInChild2)); + } + + /** + * Tests the scenario where a file is expected to be treated as binary + * according to Git attributes + */ + @Theory + public void fileInBinaryInAttribute(MergeStrategy strategy) + throws Exception { + Git git = Git.wrap(db); + String binaryFile = "file.bin"; + + writeTrashFile(".gitattributes", binaryFile + " binary"); + git.add().addFilepattern(".gitattributes").call(); + git.commit().setMessage("ADDING GITATTRIBUTES").call(); + + writeTrashFile(binaryFile, "\u0000\u0001"); + git.add().addFilepattern(binaryFile).call(); + RevCommit parent = git.commit().setMessage("BASE COMMIT").call(); + String fileHashInBase = getFileHashInWorkTree(git, binaryFile); + + writeTrashFile(binaryFile, "\u0001\u0002"); + git.add().addFilepattern(binaryFile).call(); + RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call(); + String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile); + + git.checkout().setCreateBranch(true).setStartPoint(parent) + .setName("side").call(); + + writeTrashFile(binaryFile, "\u0002\u0000"); + git.add().addFilepattern(binaryFile).call(); + git.commit().setMessage("OURS COMMIT").call(); + String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile); + + MergeResult mergeResult = git.merge().setStrategy(strategy) + .include(child1).call(); + + // check if the merge caused a conflict + assertTrue(mergeResult.getConflicts() != null + && !mergeResult.getConflicts().isEmpty()); + String fileHashInChild2AfterMerge = getFileHashInWorkTree(git, + binaryFile); + + // check if the file content changed during a conflicting merge + assertEquals(fileHashInChild2AfterMerge, fileHashInChild2); + + Set<String> hashesInIndexFile = new HashSet<>(); + DirCache indexContent = git.getRepository().readDirCache(); + for (int i = 0; i < indexContent.getEntryCount(); ++i) { + DirCacheEntry indexEntry = indexContent.getEntry(i); + if (binaryFile.equals(indexEntry.getPathString())) { + hashesInIndexFile.add(indexEntry.getObjectId().name()); + } + } + + // check if all the three stages are added to index file + assertTrue(hashesInIndexFile.contains(fileHashInBase)); + assertTrue(hashesInIndexFile.contains(fileHashInChild1)); + assertTrue(hashesInIndexFile.contains(fileHashInChild2)); + } + + private String getFileHashInWorkTree(Git git, String filePath) + throws IOException { + Repository repository = git.getRepository(); + ObjectInserter objectInserter = repository.newObjectInserter(); + + File conflictingFile = new File(repository.getWorkTree(), filePath); + byte[] fileContent = Files.readAllBytes(conflictingFile.toPath()); + ObjectId blobId = objectInserter.insert(Constants.OBJ_BLOB, + fileContent); + objectInserter.flush(); + return blobId.name(); } private void writeSubmodule(String path, ObjectId commit) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java index 798aebe3b0..0016adfb66 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java @@ -16,6 +16,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; @@ -375,7 +377,8 @@ public class SimpleMergeTest extends SampleDataRepositoryTestCase { ObjectId[] parentIds) throws Exception { CommitBuilder c = new CommitBuilder(); c.setTreeId(treeB.writeTree(odi)); - c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0)); + c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", + Instant.ofEpochMilli(1L), ZoneOffset.UTC)); c.setCommitter(c.getAuthor()); c.setParentIds(parentIds); c.setMessage("Tree " + c.getTreeId().name()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java index e2637257c5..5507f8572d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; + import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.attributes.FilterCommand; @@ -47,8 +48,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) -@Suite.SuiteClasses({ - PatchApplierTest.WithWorktree. class, // +@Suite.SuiteClasses({ PatchApplierTest.WithWorktree.class, // PatchApplierTest.InCore.class, // }) public class PatchApplierTest { @@ -127,6 +127,20 @@ public class PatchApplierTest { } } + protected Result applyPatchAllowConflicts() throws IOException { + InputStream patchStream = getTestResource(name + ".patch"); + Patch patch = new Patch(); + patch.parse(patchStream); + if (inCore) { + try (ObjectInserter oi = db.newObjectInserter()) { + return new PatchApplier(db, baseTip, oi).allowConflicts() + .applyPatch(patch); + } + } + return new PatchApplier(db).allowConflicts() + .applyPatch(patch); + } + protected static InputStream getTestResource(String patchFile) { return PatchApplierTest.class.getClassLoader() .getResourceAsStream("org/eclipse/jgit/diff/" + patchFile); @@ -168,6 +182,13 @@ public class PatchApplierTest { verifyContent(result, aName, exists); } + void verifyChange(Result result, String aName, boolean exists, + int numConflicts) throws Exception { + assertEquals(numConflicts, result.getErrors().size()); + assertEquals(1, result.getPaths().size()); + verifyContent(result, aName, exists); + } + protected byte[] readBlob(ObjectId treeish, String path) throws Exception { try (TestRepository<?> tr = new TestRepository<>(db); @@ -345,6 +366,44 @@ public class PatchApplierTest { } @Test + public void testConflictMarkers() throws Exception { + init("allowconflict", true, true); + + Result result = applyPatchAllowConflicts(); + + assertEquals(result.getErrors().size(), 1); + PatchApplier.Result.Error error = result.getErrors().get(0); + assertEquals("cannot apply hunk", error.msg); + assertEquals("allowconflict", error.oldFileName); + assertTrue(error.isGitConflict()); + verifyChange(result, "allowconflict", true, 1); + } + + @Test + public void testConflictMarkersOutOfBounds() throws Exception { + init("ConflictOutOfBounds", true, true); + + Result result = applyPatchAllowConflicts(); + + assertEquals(result.getErrors().size(), 1); + PatchApplier.Result.Error error = result.getErrors().get(0); + assertEquals("cannot apply hunk", error.msg); + assertEquals("ConflictOutOfBounds", error.oldFileName); + assertTrue(error.isGitConflict()); + verifyChange(result, "ConflictOutOfBounds", true, 1); + } + + @Test + public void testConflictMarkersFileDeleted() throws Exception { + init("allowconflict_file_deleted", false, false); + + Result result = applyPatchAllowConflicts(); + + assertEquals(1, result.getErrors().size()); + assertEquals(0, result.getPaths().size()); + } + + @Test public void testShiftUp() throws Exception { init("ShiftUp"); @@ -495,6 +554,14 @@ public class PatchApplierTest { Result result = applyPatch(); verifyChange(result, "x_last_rm_nl"); } + + @Test + public void testVeryLongFile() throws Exception { + init("very_long_file"); + + Result result = applyPatch(); + verifyChange(result, "very_long_file"); + } } public static class WithWorktree extends Base { @@ -892,5 +959,30 @@ public class PatchApplierTest { FilterCommandRegistry.unregister("jgit://builtin/a2e/smudge"); } } + + private void dotGitTest(String fileName) throws Exception { + init(fileName, false, false); + Result result = null; + IOException ex = null; + try { + result = applyPatch(); + } catch (IOException e) { + ex = e; + } + assertTrue(ex != null + || (result != null && !result.getErrors().isEmpty())); + File b = new File(new File(trash, ".git"), "b"); + assertFalse(".git/b should not exist", b.exists()); + } + + @Test + public void testDotGit() throws Exception { + dotGitTest("dotgit"); + } + + @Test + public void testDotGit2() throws Exception { + dotGitTest("dotgit2"); + } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java index 71bda46ee5..8335c07b1f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java @@ -10,7 +10,6 @@ package org.eclipse.jgit.patch; -import static java.lang.Integer.valueOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -35,7 +34,7 @@ public class PatchCcErrorTest { assertSame(FormatError.Severity.ERROR, e.getSeverity()); assertEquals(MessageFormat.format( JGitText.get().truncatedHunkLinesMissingForAncestor, - valueOf(1), valueOf(1)), e.getMessage()); + Integer.valueOf(1), Integer.valueOf(1)), e.getMessage()); assertEquals(346, e.getOffset()); assertTrue(e.getLineText().startsWith( "@@@ -55,12 -163,13 +163,15 @@@ public ")); @@ -45,7 +44,7 @@ public class PatchCcErrorTest { assertSame(FormatError.Severity.ERROR, e.getSeverity()); assertEquals(MessageFormat.format( JGitText.get().truncatedHunkLinesMissingForAncestor, - valueOf(2), valueOf(2)), e.getMessage()); + Integer.valueOf(2), Integer.valueOf(2)), e.getMessage()); assertEquals(346, e.getOffset()); assertTrue(e.getLineText().startsWith( "@@@ -55,12 -163,13 +163,15 @@@ public ")); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java index 49f832a1aa..e4bd8506e3 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java @@ -11,7 +11,7 @@ package org.eclipse.jgit.revplot; import static org.junit.Assert.assertEquals; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.api.Git; @@ -70,7 +70,7 @@ public class AbstractPlotRendererTest extends RepositoryTestCase { private static class TestPlotRenderer extends AbstractPlotRenderer<PlotLane, Object> { - List<Integer> indentations = new LinkedList<>(); + List<Integer> indentations = new ArrayList<>(); @Override protected int drawLabel(int x, int y, Ref ref) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmapWalkerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmapWalkerTest.java new file mode 100644 index 0000000000..a59a71d19f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmapWalkerTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2023, Google Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.revwalk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; + +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.internal.storage.file.GC; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.BitmapIndex; +import org.eclipse.jgit.lib.BitmapIndex.Bitmap; +import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder; +import org.eclipse.jgit.lib.BitmapIndex.BitmapLookupListener; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectReader; +import org.junit.Before; +import org.junit.Test; + +public class BitmapWalkerTest extends LocalDiskRepositoryTestCase { + + private static final String MAIN = "refs/heads/main"; + + TestRepository<FileRepository> repo; + + RevCommit tipWithBitmap; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + FileRepository db = createWorkRepository(); + repo = new TestRepository<>(db); + + RevCommit base = repo.commit().create(); + RevCommit one = repo.commit().parent(base).create(); + tipWithBitmap = repo.commit().parent(one).create(); + repo.update(MAIN, tipWithBitmap); + + GC gc = new GC(repo.getRepository()); + gc.setAuto(false); + gc.gc().get(); + + assertNotNull(repo.getRevWalk().getObjectReader().getBitmapIndex()); + } + + private static class BitmapWalkCounter implements BitmapLookupListener { + int withBitmap; + + int withoutBitmap; + + @Override + public void onBitmapFound(AnyObjectId oid) { + withBitmap += 1; + } + + @Override + public void onBitmapNotFound(AnyObjectId oid) { + withoutBitmap += 1; + } + } + + @Test + public void counters_bitmapAtTip() throws Exception { + try (RevWalk rw = repo.getRevWalk(); + ObjectReader or = rw.getObjectReader()) { + BitmapWalkCounter counter = new BitmapWalkCounter(); + BitmapIndex bitmapIndex = or.getBitmapIndex(); + bitmapIndex.addBitmapLookupListener(counter); + BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(), + bitmapIndex, NullProgressMonitor.INSTANCE); + BitmapBuilder bitmap = bw.findObjects(List.of(tipWithBitmap), null, + true); + // First commit has a tree, so in total 4 objects + assertEquals(4, bitmap.cardinality()); + assertEquals(1, counter.withBitmap); + assertEquals(0, counter.withoutBitmap); + assertEquals(0, bw.getCountOfBitmapIndexMisses()); + } + } + + @Test + public void counters_bitmapAfterAStep() throws Exception { + System.out.println("Old tip: " + tipWithBitmap); + RevCommit newTip = repo.commit().parent(tipWithBitmap).create(); + System.out.println("New tip: " + newTip); + try (RevWalk rw = repo.getRevWalk(); + ObjectReader or = rw.getObjectReader()) { + BitmapWalkCounter counter = new BitmapWalkCounter(); + BitmapIndex bitmapIndex = or.getBitmapIndex(); + bitmapIndex.addBitmapLookupListener(counter); + BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(), + bitmapIndex, NullProgressMonitor.INSTANCE); + + bw.findObjects(List.of(newTip), null, true); + + assertEquals(1, counter.withBitmap); + // It checks bitmap before marking as interesting, and again in the + // walk + assertEquals(2, counter.withoutBitmap); + assertEquals(1, bw.getCountOfBitmapIndexMisses()); + } + } + + @Test + public void counters_bitmapAfterThreeSteps() throws Exception { + RevCommit newOne = repo.commit().parent(tipWithBitmap).create(); + RevCommit newTwo = repo.commit().parent(newOne).create(); + RevCommit newTip = repo.commit().parent(newTwo).create(); + + try (RevWalk rw = repo.getRevWalk(); + ObjectReader or = rw.getObjectReader()) { + BitmapWalkCounter counter = new BitmapWalkCounter(); + BitmapIndex bitmapIndex = or.getBitmapIndex(); + bitmapIndex.addBitmapLookupListener(counter); + BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(), + bitmapIndex, NullProgressMonitor.INSTANCE); + + bw.findObjects(List.of(newTip), null, true); + + assertEquals(1, counter.withBitmap); + assertEquals(4, counter.withoutBitmap); + assertEquals(3, bw.getCountOfBitmapIndexMisses()); + } + } + + @Test + public void counters_bitmapAfterThreeStepsWithSeen() throws Exception { + RevCommit newOne = repo.commit().parent(tipWithBitmap).create(); + RevCommit newTwo = repo.commit().parent(newOne).create(); + RevCommit newTip = repo.commit().parent(newTwo).create(); + + try (RevWalk rw = repo.getRevWalk(); + ObjectReader or = rw.getObjectReader()) { + BitmapIndex bitmapIndex = or.getBitmapIndex(); + Bitmap seen = bitmapIndex.getBitmap(tipWithBitmap); + BitmapBuilder seenBB = bitmapIndex.newBitmapBuilder().or(seen); + BitmapWalkCounter counter = new BitmapWalkCounter(); + bitmapIndex.addBitmapLookupListener(counter); + BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(), + bitmapIndex, NullProgressMonitor.INSTANCE); + + bw.findObjects(List.of(newTip), seenBB, true); + + assertEquals(0, counter.withBitmap); + assertEquals(4, counter.withoutBitmap); + assertEquals(3, bw.getCountOfBitmapIndexMisses()); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevPriorityQueueTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevPriorityQueueTest.java new file mode 100644 index 0000000000..369e2fae72 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevPriorityQueueTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023, GerritForge Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.revwalk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class DateRevPriorityQueueTest extends RevQueueTestCase<DateRevPriorityQueue> { + @Override + protected DateRevPriorityQueue create() { + return new DateRevPriorityQueue(); + } + + @Override + @Test + public void testEmpty() throws Exception { + super.testEmpty(); + assertNull(q.peek()); + assertEquals(Generator.SORT_COMMIT_TIME_DESC, q.outputType()); + } + + @Test + public void testCloneEmpty() throws Exception { + q = new DateRevPriorityQueue(AbstractRevQueue.EMPTY_QUEUE); + assertNull(q.next()); + } + + @Test + public void testInsertOutOfOrder() throws Exception { + final RevCommit a = parseBody(commit()); + final RevCommit b = parseBody(commit(10, a)); + final RevCommit c1 = parseBody(commit(5, b)); + final RevCommit c2 = parseBody(commit(-50, b)); + + q.add(c2); + q.add(a); + q.add(b); + q.add(c1); + + assertCommit(c1, q.next()); + assertCommit(b, q.next()); + assertCommit(a, q.next()); + assertCommit(c2, q.next()); + assertNull(q.next()); + } + + @Test + public void testInsertTie() throws Exception { + final RevCommit a = parseBody(commit()); + final RevCommit b = parseBody(commit(0, a)); + final RevCommit c = parseBody(commit(0, b)); + + { + q = create(); + q.add(a); + q.add(b); + q.add(c); + + assertCommit(a, q.next()); + assertCommit(b, q.next()); + assertCommit(c, q.next()); + assertNull(q.next()); + } + { + q = create(); + q.add(c); + q.add(b); + q.add(a); + + assertCommit(c, q.next()); + assertCommit(b, q.next()); + assertCommit(a, q.next()); + assertNull(q.next()); + } + } + + @Test + public void testCloneFIFO() throws Exception { + final RevCommit a = parseBody(commit()); + final RevCommit b = parseBody(commit(200, a)); + final RevCommit c = parseBody(commit(200, b)); + + final FIFORevQueue src = new FIFORevQueue(); + src.add(a); + src.add(b); + src.add(c); + + q = new DateRevPriorityQueue(src); + assertFalse(q.everbodyHasFlag(RevWalk.UNINTERESTING)); + assertFalse(q.anybodyHasFlag(RevWalk.UNINTERESTING)); + assertCommit(c, q.peek()); + assertCommit(c, q.peek()); + + assertCommit(c, q.next()); + assertCommit(b, q.next()); + assertCommit(a, q.next()); + assertNull(q.next()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java index 113f3bee82..657c3d242f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java @@ -16,76 +16,101 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import java.io.IOException; import java.util.List; import org.eclipse.jgit.junit.RepositoryTestCase; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; public class FooterLineTest extends RepositoryTestCase { @Test - public void testNoFooters_EmptyBody() throws IOException { - final RevCommit commit = parse(""); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_EmptyBody() { + String msg = buildMessage(""); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(0, footers.size()); } @Test - public void testNoFooters_NewlineOnlyBody1() throws IOException { - final RevCommit commit = parse("\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_NewlineOnlyBody1() { + String msg = buildMessage("\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(0, footers.size()); } @Test - public void testNoFooters_NewlineOnlyBody5() throws IOException { - final RevCommit commit = parse("\n\n\n\n\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_NewlineOnlyBody5() { + String msg = buildMessage("\n\n\n\n\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(0, footers.size()); } @Test - public void testNoFooters_OneLineBodyNoLF() throws IOException { - final RevCommit commit = parse("this is a commit"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_OneLineBodyNoLF() { + String msg = buildMessage("this is a commit"); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(0, footers.size()); } @Test - public void testNoFooters_OneLineBodyWithLF() throws IOException { - final RevCommit commit = parse("this is a commit\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_OneLineBodyWithLF() { + String msg = buildMessage("this is a commit\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(0, footers.size()); } @Test - public void testNoFooters_ShortBodyNoLF() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_ShortBodyNoLF() { + String msg = buildMessage("subject\n\nbody of commit"); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(0, footers.size()); } @Test - public void testNoFooters_ShortBodyWithLF() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_ShortBodyWithLF() { + String msg = buildMessage("subject\n\nbody of commit\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(0, footers.size()); } @Test - public void testSignedOffBy_OneUserNoLF() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "Signed-off-by: A. U. Thor <a@example.com>"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNoFooters_noRawMsg_SingleLineNoHeaders() { + String noRawMsg = "commit message with no header lines\n"; + List<FooterLine> footers = FooterLine.fromMessage(noRawMsg); + assertNotNull(footers); + assertEquals(0, footers.size()); + } + + @Test + public void testOneFooter_noRawMsg_MultiParagraphNoHeaders() { + String noRawMsg = "subject\n\n" + + "Not: footer\n\n" + + "Footer: value\n"; + List<FooterLine> footers = FooterLine.fromMessage(noRawMsg); + assertNotNull(footers); + assertEquals(1, footers.size()); + } + + @Test + public void testOneFooter_longSubject_NoHeaders() { + String noRawMsg = "50+ chars loooooooooooooong custom commit message.\n\n" + + "Footer: value\n"; + List<FooterLine> footers = FooterLine.fromMessage(noRawMsg); + assertNotNull(footers); + assertEquals(1, footers.size()); + } + + @Test + public void testSignedOffBy_OneUserNoLF() { + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "Signed-off-by: A. U. Thor <a@example.com>"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -98,10 +123,10 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testSignedOffBy_OneUserWithLF() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "Signed-off-by: A. U. Thor <a@example.com>\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testSignedOffBy_OneUserWithLF() { + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "Signed-off-by: A. U. Thor <a@example.com>\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -114,14 +139,13 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testSignedOffBy_IgnoreWhitespace() throws IOException { + public void testSignedOffBy_IgnoreWhitespace() { // We only ignore leading whitespace on the value, trailing // is assumed part of the value. // - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "Signed-off-by: A. U. Thor <a@example.com> \n"); - final List<FooterLine> footers = commit.getFooterLines(); - FooterLine f; + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "Signed-off-by: A. U. Thor <a@example.com> \n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); assertEquals(1, footers.size()); @@ -133,10 +157,10 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testEmptyValueNoLF() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "Signed-off-by:"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testEmptyValueNoLF() { + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "Signed-off-by:"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -149,10 +173,10 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testEmptyValueWithLF() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "Signed-off-by:\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testEmptyValueWithLF() { + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "Signed-off-by:\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -165,10 +189,10 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testShortKey() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "K:V\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testShortKey() { + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "K:V\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -181,10 +205,10 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testNonDelimtedEmail() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "Acked-by: re@example.com\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNonDelimtedEmail() { + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "Acked-by: re@example.com\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -197,10 +221,10 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testNotEmail() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n" - + "Acked-by: Main Tain Er\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testNotEmail() { + String msg = buildMessage("subject\n\nbody of commit\n" + "\n" + + "Acked-by: Main Tain Er\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -213,15 +237,15 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testSignedOffBy_ManyUsers() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" - + "Not-A-Footer-Line: this line must not be read as a footer\n" - + "\n" // paragraph break, now footers appear in final block - + "Signed-off-by: A. U. Thor <a@example.com>\n" - + "CC: <some.mailing.list@example.com>\n" - + "Acked-by: Some Reviewer <sr@example.com>\n" - + "Signed-off-by: Main Tain Er <mte@example.com>\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testSignedOffBy_ManyUsers() { + String msg = buildMessage("subject\n\nbody of commit\n" + + "Not-A-Footer-Line: this line must not be read as a footer\n" + + "\n" // paragraph break, now footers appear in final block + + "Signed-off-by: A. U. Thor <a@example.com>\n" + + "CC: <some.mailing.list@example.com>\n" + + "Acked-by: Some Reviewer <sr@example.com>\n" + + "Signed-off-by: Main Tain Er <mte@example.com>\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -249,16 +273,16 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testSignedOffBy_SkipNonFooter() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" - + "Not-A-Footer-Line: this line must not be read as a footer\n" - + "\n" // paragraph break, now footers appear in final block - + "Signed-off-by: A. U. Thor <a@example.com>\n" - + "CC: <some.mailing.list@example.com>\n" - + "not really a footer line but we'll skip it anyway\n" - + "Acked-by: Some Reviewer <sr@example.com>\n" - + "Signed-off-by: Main Tain Er <mte@example.com>\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testSignedOffBy_SkipNonFooter() { + String msg = buildMessage("subject\n\nbody of commit\n" + + "Not-A-Footer-Line: this line must not be read as a footer\n" + + "\n" // paragraph break, now footers appear in final block + + "Signed-off-by: A. U. Thor <a@example.com>\n" + + "CC: <some.mailing.list@example.com>\n" + + "not really a footer line but we'll skip it anyway\n" + + "Acked-by: Some Reviewer <sr@example.com>\n" + + "Signed-off-by: Main Tain Er <mte@example.com>\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f; assertNotNull(footers); @@ -267,30 +291,39 @@ public class FooterLineTest extends RepositoryTestCase { f = footers.get(0); assertEquals("Signed-off-by", f.getKey()); assertEquals("A. U. Thor <a@example.com>", f.getValue()); + assertEquals(217, f.getStartOffset()); + assertEquals(258, f.getEndOffset()); f = footers.get(1); assertEquals("CC", f.getKey()); assertEquals("<some.mailing.list@example.com>", f.getValue()); + assertEquals(259, f.getStartOffset()); + assertEquals(305, f.getEndOffset()); f = footers.get(2); assertEquals("Acked-by", f.getKey()); assertEquals("Some Reviewer <sr@example.com>", f.getValue()); + assertEquals(356, f.getStartOffset()); + assertEquals(396, f.getEndOffset()); f = footers.get(3); assertEquals("Signed-off-by", f.getKey()); assertEquals("Main Tain Er <mte@example.com>", f.getValue()); + assertEquals(397, f.getStartOffset()); + assertEquals(442, f.getEndOffset()); } @Test - public void testFilterFootersIgnoreCase() throws IOException { - final RevCommit commit = parse("subject\n\nbody of commit\n" - + "Not-A-Footer-Line: this line must not be read as a footer\n" - + "\n" // paragraph break, now footers appear in final block - + "Signed-Off-By: A. U. Thor <a@example.com>\n" - + "CC: <some.mailing.list@example.com>\n" - + "Acked-by: Some Reviewer <sr@example.com>\n" - + "signed-off-by: Main Tain Er <mte@example.com>\n"); - final List<String> footers = commit.getFooterLines("signed-off-by"); + public void testFilterFootersIgnoreCase() { + String msg = buildMessage("subject\n\nbody of commit\n" + + "Not-A-Footer-Line: this line must not be read as a footer\n" + + "\n" // paragraph break, now footers appear in final block + + "Signed-Off-By: A. U. Thor <a@example.com>\n" + + "CC: <some.mailing.list@example.com>\n" + + "Acked-by: Some Reviewer <sr@example.com>\n" + + "signed-off-by: Main Tain Er <mte@example.com>\n"); + List<String> footers = FooterLine.getValues( + FooterLine.fromMessage(msg), "signed-off-by"); assertNotNull(footers); assertEquals(2, footers.size()); @@ -300,38 +333,104 @@ public class FooterLineTest extends RepositoryTestCase { } @Test - public void testMatchesBugId() throws IOException { - final RevCommit commit = parse("this is a commit subject for test\n" - + "\n" // paragraph break, now footers appear in final block - + "Simple-Bug-Id: 42\n"); - final List<FooterLine> footers = commit.getFooterLines(); + public void testMatchesBugId() { + String msg = buildMessage("this is a commit subject for test\n" + + "\n" // paragraph break, now footers appear in final block + + "Simple-Bug-Id: 42\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); assertNotNull(footers); assertEquals(1, footers.size()); - final FooterLine line = footers.get(0); + FooterLine line = footers.get(0); assertNotNull(line); assertEquals("Simple-Bug-Id", line.getKey()); assertEquals("42", line.getValue()); - final FooterKey bugid = new FooterKey("Simple-Bug-Id"); + FooterKey bugid = new FooterKey("Simple-Bug-Id"); assertTrue("matches Simple-Bug-Id", line.matches(bugid)); assertFalse("not Signed-off-by", line.matches(FooterKey.SIGNED_OFF_BY)); assertFalse("not CC", line.matches(FooterKey.CC)); } - private RevCommit parse(String msg) throws IOException { - final StringBuilder buf = new StringBuilder(); + @Test + public void testMultilineFooters() { + String msg = buildMessage("subject\n\nbody of commit\n" + + "Not-A-Footer-Line: this line must not be read as a footer\n" + + "\n" // paragraph break, now footers appear in final block + + "Notes: The change must not be merged until dependency ABC is\n" + + " updated.\n" + + "CC: <some.mailing.list@example.com>\n" + + "not really a footer line but we'll skip it anyway\n" + + "Acked-by: Some Reviewer <sr@example.com>\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); + FooterLine f; + + assertNotNull(footers); + assertEquals(3, footers.size()); + + f = footers.get(0); + assertEquals("Notes", f.getKey()); + assertEquals( + "The change must not be merged until dependency ABC is updated.", + f.getValue()); + + f = footers.get(1); + assertEquals("CC", f.getKey()); + assertEquals("<some.mailing.list@example.com>", f.getValue()); + + f = footers.get(2); + assertEquals("Acked-by", f.getKey()); + assertEquals("Some Reviewer <sr@example.com>", f.getValue()); + } + + @Test + public void testMultilineFooters_multipleWhitespaceAreAllowed() { + String msg = buildMessage("subject\n\nbody of commit\n" + + "Not-A-Footer-Line: this line must not be read as a footer\n" + + "\n" // paragraph break, now footers appear in final block + + "Notes: The change must not be merged until dependency ABC is\n" + + " updated.\n"); + List<FooterLine> footers = FooterLine.fromMessage(msg); + FooterLine f; + + assertNotNull(footers); + assertEquals(1, footers.size()); + + f = footers.get(0); + assertEquals("Notes", f.getKey()); + assertEquals( + "The change must not be merged until dependency ABC is updated.", + f.getValue()); + } + + @Test + public void testFirstLineNeverFooter() { + String msg = buildMessage( + String.join("\n", "First-Line: is never a footer", "Foo: ter", + "1-is: also a footer")); + List<FooterLine> footers = FooterLine.fromMessage(msg); + assertNotNull(footers); + assertEquals(2, footers.size()); + } + + @Test + public void testLineAfterFooters() { + String msg = buildMessage( + String.join("\n", "Subject line: is never a footer", "Foo: ter", + "1-is: also a footer", "this is not a footer")); + List<FooterLine> footers = FooterLine.fromMessage(msg); + assertNotNull(footers); + assertEquals(2, footers.size()); + } + + private String buildMessage(String msg) { + StringBuilder buf = new StringBuilder(); buf.append("tree " + ObjectId.zeroId().name() + "\n"); buf.append("author A. U. Thor <a@example.com> 1 +0000\n"); buf.append("committer A. U. Thor <a@example.com> 1 +0000\n"); buf.append("\n"); buf.append(msg); - - try (RevWalk walk = new RevWalk(db)) { - RevCommit c = new RevCommit(ObjectId.zeroId()); - c.parseCanonical(walk, Constants.encode(buf.toString())); - return c; - } + return buf.toString(); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java index 82af34ded2..014ff928a8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2009, Google Inc. and others + * Copyright (C) 2008, 2024 Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -23,7 +23,9 @@ import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; -import java.util.TimeZone; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.CommitBuilder; @@ -94,18 +96,17 @@ public class RevCommitParseTest extends RepositoryTestCase { assertNotNull(cAuthor); assertEquals(authorName, cAuthor.getName()); assertEquals(authorEmail, cAuthor.getEmailAddress()); - assertEquals((long) authorTime * 1000, cAuthor.getWhen().getTime()); - assertEquals(TimeZone.getTimeZone("GMT" + authorTimeZone), - cAuthor.getTimeZone()); + assertEquals(Instant.ofEpochSecond(authorTime), + cAuthor.getWhenAsInstant()); + assertEquals(ZoneId.of(authorTimeZone), cAuthor.getZoneId()); final PersonIdent cCommitter = c.getCommitterIdent(); assertNotNull(cCommitter); assertEquals(committerName, cCommitter.getName()); assertEquals(committerEmail, cCommitter.getEmailAddress()); - assertEquals((long) committerTime * 1000, - cCommitter.getWhen().getTime()); - assertEquals(TimeZone.getTimeZone("GMT" + committerTimeZone), - cCommitter.getTimeZone()); + assertEquals(Instant.ofEpochSecond(committerTime), + cCommitter.getWhenAsInstant()); + assertEquals(ZoneId.of(committerTimeZone), cCommitter.getZoneId()); } private RevCommit create(String msg) throws Exception { @@ -153,9 +154,13 @@ public class RevCommitParseTest extends RepositoryTestCase { c.parseCanonical(rw, b.toString().getBytes(UTF_8)); } assertEquals( - new PersonIdent("", "a_u_thor@example.com", 1218123387000l, 7), + new PersonIdent("", "a_u_thor@example.com", + Instant.ofEpochMilli(1218123387000L), + ZoneOffset.ofHoursMinutes(0, 7)), c.getAuthorIdent()); - assertEquals(new PersonIdent("", "", 1218123390000l, -5), + assertEquals( + new PersonIdent("", "", Instant.ofEpochMilli(1218123390000L), + ZoneOffset.ofHoursMinutes(0, -5)), c.getCommitterIdent()); } @@ -408,6 +413,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create(msg); assertEquals(msg, c.getFullMessage()); assertEquals(msg, c.getShortMessage()); + assertEquals(msg, c.getFirstMessageLine()); } @Test @@ -415,6 +421,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create("\n"); assertEquals("\n", c.getFullMessage()); assertEquals("", c.getShortMessage()); + assertEquals("", c.getFirstMessageLine()); } @Test @@ -423,6 +430,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create(shortMsg); assertEquals(shortMsg, c.getFullMessage()); assertEquals(shortMsg, c.getShortMessage()); + assertEquals(shortMsg, c.getFirstMessageLine()); } @Test @@ -432,6 +440,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create(fullMsg); assertEquals(fullMsg, c.getFullMessage()); assertEquals(shortMsg, c.getShortMessage()); + assertEquals(shortMsg, c.getFirstMessageLine()); } @Test @@ -441,6 +450,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create(fullMsg); assertEquals(fullMsg, c.getFullMessage()); assertEquals(shortMsg, c.getShortMessage()); + assertEquals("This is a", c.getFirstMessageLine()); } @Test @@ -450,6 +460,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create(fullMsg); assertEquals(fullMsg, c.getFullMessage()); assertEquals(shortMsg, c.getShortMessage()); + assertEquals("This is a", c.getFirstMessageLine()); } @Test @@ -461,6 +472,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create(fullMsg); assertEquals(fullMsg, c.getFullMessage()); assertEquals(shortMsg, c.getShortMessage()); + assertEquals(shortMsg, c.getFirstMessageLine()); } @Test @@ -480,6 +492,7 @@ public class RevCommitParseTest extends RepositoryTestCase { assertEquals(author, p.getAuthorIdent()); assertEquals(committer, p.getCommitterIdent()); assertEquals("Test commit", p.getShortMessage()); + assertEquals("Test commit", p.getFirstMessageLine()); assertEquals(src.getMessage(), p.getFullMessage()); } @@ -494,6 +507,7 @@ public class RevCommitParseTest extends RepositoryTestCase { final RevCommit c = create(fullMsg); assertEquals(fullMsg, c.getFullMessage()); assertEquals(shortMsg, c.getShortMessage()); + assertEquals("This fixes a", c.getFirstMessageLine()); } private static ObjectId id(String str) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java index 8c25e05986..529d5a9f09 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java @@ -39,7 +39,7 @@ public class RevWalkCarryFlagsTest extends RevWalkTestCase { /** * Similar to {@link #testRevWalkCarryUninteresting_fastClock()} but the * last merge commit is created so fast that he has the same creationdate as - * the previous commit. This will cause the underlying {@link DateRevQueue} + * the previous commit. This will cause the underlying {@link AbstractRevQueue} * is not able to sort the commits in a way matching the topology. A parent * (one of the commits which are merged) is handled before the child (the * merge commit). This makes carrying over flags more complicated diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java index 3cc0368943..e47dd898b0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java @@ -26,6 +26,8 @@ import java.util.Comparator; import java.util.List; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.diff.DiffConfig; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.storage.file.GC; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ConfigConstants; @@ -33,7 +35,10 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.filter.MessageRevFilter; import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; +import org.eclipse.jgit.treewalk.filter.ChangedPathTreeFilter; +import org.eclipse.jgit.treewalk.filter.OrTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.junit.Test; @@ -46,6 +51,7 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase { public void setUp() throws Exception { super.setUp(); rw = new RevWalk(db); + mockSystemReader.setJGitConfig(new MockConfig()); } @Test @@ -167,6 +173,428 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase { } @Test + public void testChangedPathFilter_allModify() throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit(tree(file("file2", blob("2"))), c1); + RevCommit c3 = commit(tree(file("file1", blob("3"))), c2); + RevCommit c4 = commit(tree(file("file2", blob("4"))), c3); + + branch(c4, "master"); + + enableAndWriteCommitGraph(); + + TreeRevFilter trf = new TreeRevFilter(rw, + ChangedPathTreeFilter.create("file1")); + rw.markStart(rw.lookupCommit(c4)); + rw.setRevFilter(trf); + assertEquals(c4, rw.next()); + assertEquals(c3, rw.next()); + assertEquals(c2, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // all commits modified file1 but c1 did not have a parent + assertEquals(3, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // No negatives because all 4 commits had modified file1 + assertEquals(0, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_someModify() throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit(tree(file("file1", blob("1"))), c1); + RevCommit c3 = commit(tree(file("file1", blob("2"))), c2); + RevCommit c4 = commit(tree(file("file1", blob("1"))), c3); + + branch(c4, "master"); + + enableAndWriteCommitGraph(); + + TreeRevFilter trf = new TreeRevFilter(rw, + ChangedPathTreeFilter.create("file1")); + rw.markStart(rw.lookupCommit(c4)); + rw.setRevFilter(trf); + assertEquals(c4, rw.next()); + assertEquals(c3, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // c4 and c3 modified file1. c1 did not have a parent + assertEquals(2, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // c2 did not modify file1 + assertEquals(1, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilterWithMultiPaths() throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit(tree(file("file1", blob("2"))), c1); + RevCommit c3 = commit(tree(file("file2", blob("3"))), c2); + RevCommit c4 = commit(tree(file("file3", blob("4"))), c3); + + branch(c4, "master"); + + enableAndWriteCommitGraph(); + + TreeRevFilter trf = new TreeRevFilter(rw, + ChangedPathTreeFilter.create("file1", "file2")); + rw.markStart(rw.lookupCommit(c4)); + rw.setRevFilter(trf); + assertEquals(c4, rw.next()); + assertEquals(c3, rw.next()); + assertEquals(c2, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // all commits have modified either file1 or file2, c1 did not have a + // parent + assertEquals(3, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // No negative + assertEquals(0, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilterWithFollowFilter() throws Exception { + RevCommit c0 = commit(tree()); + RevCommit c1 = commit(tree(file("file", blob("contents"))), c0); + RevCommit c2 = commit(tree(file("file", blob("contents")), + file("unrelated", blob("unrelated change"))), c1); + RevCommit c3 = commit(tree(file("renamed-file", blob("contents")), + file("unrelated", blob("unrelated change"))), c2); + RevCommit c4 = commit( + tree(file("renamed-file", blob("contents")), + file("unrelated", blob("another unrelated change"))), + c3); + branch(c4, "master"); + + enableAndWriteCommitGraph(); + + db.getConfig().setString(ConfigConstants.CONFIG_DIFF_SECTION, null, + ConfigConstants.CONFIG_KEY_RENAMES, "true"); + + TreeRevFilter trf = new TreeRevFilter(rw, FollowFilter + .create("renamed-file", db.getConfig().get(DiffConfig.KEY))); + rw.markStart(rw.lookupCommit(c4)); + rw.setRevFilter(trf); + assertEquals(c3, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // Path "renamed-file" is in c3's bloom filter, and another path "file" + // is in c1's bloom filter (we know of "file" because the rev walk + // detected that "renamed-file" is a renaming of "file") + assertEquals(2, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // 2 commits that have exactly one parent and don't match path + assertEquals(2, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_pathFilter_or_pathFilter_binaryOperation() + throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit( + tree(file("file1", blob("1")), file("file2", blob("2"))), c1); + RevCommit c3 = commit(tree(file("file2", blob("2"))), c2); + RevCommit c4 = commit( + tree(file("file2", blob("2")), file("file3", blob("3"))), c3); + RevCommit c5 = commit( + tree(file("file2", blob("2")), file("file3", blob("3"))), c4); + + branch(c5, "master"); + + enableAndWriteCommitGraph(); + + ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1"); + ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2"); + + TreeFilter tf = OrTreeFilter + .create(new ChangedPathTreeFilter[] { pf1, pf2 }); + + TreeRevFilter trf = new TreeRevFilter(rw, tf); + rw.markStart(rw.lookupCommit(c5)); + rw.setRevFilter(trf); + assertEquals(c3, rw.next()); + assertEquals(c2, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // c2 and c3 has either file1 or file2, c1 is not counted as + // ChangedPathFilter only applies to commits with 1 parent + assertEquals(2, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // c4 and c5 did not modify file1 or file2 + assertEquals(2, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_pathFilter_or_pathFilter_or_pathFilter_listOperation() + throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit( + tree(file("file1", blob("1")), file("file2", blob("2"))), c1); + RevCommit c3 = commit(tree(file("file2", blob("2"))), c2); + RevCommit c4 = commit(tree(file("file3", blob("3"))), c3); + RevCommit c5 = commit(tree(file("file3", blob("3"))), c4); + + branch(c5, "master"); + + enableAndWriteCommitGraph(); + + ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1"); + ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2"); + ChangedPathTreeFilter pf3 = ChangedPathTreeFilter.create("file3"); + + TreeFilter tf = OrTreeFilter + .create(new ChangedPathTreeFilter[] { pf1, pf2, pf3 }); + + TreeRevFilter trf = new TreeRevFilter(rw, tf); + rw.markStart(rw.lookupCommit(c5)); + rw.setRevFilter(trf); + assertEquals(c4, rw.next()); + assertEquals(c3, rw.next()); + assertEquals(c2, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // c2 and c3 has either modified file1 or file2 or file3, c1 is not + // counted as ChangedPathFilter only applies to commits with 1 parent + assertEquals(3, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // c5 does not modify either file1 or file2 or file3 + assertEquals(1, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_pathFilter_or_nonPathFilter_binaryOperation() + throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit(tree(file("file2", blob("2"))), c1); + RevCommit c3 = commit(tree(file("file2", blob("3"))), c2); + RevCommit c4 = commit(tree(file("file2", blob("3"))), c3); + + branch(c4, "master"); + + enableAndWriteCommitGraph(); + + ChangedPathTreeFilter pf = ChangedPathTreeFilter.create("file1"); + TreeFilter npf = TreeFilter.ANY_DIFF; + + TreeFilter tf = OrTreeFilter.create(new TreeFilter[] { pf, npf }); + + TreeRevFilter trf = new TreeRevFilter(rw, tf); + rw.markStart(rw.lookupCommit(c4)); + rw.setRevFilter(trf); + assertEquals(c3, rw.next()); + assertEquals(c2, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // c2 modified file1, c3 defaulted positive due to ANY_DIFF, c1 is not + // counted as ChangedPathFilter only applies to commits with 1 parent + assertEquals(2, trf.getChangedPathFilterTruePositive()); + + // c4 defaulted positive due to ANY_DIFF, but didn't no diff with its + // parent c3 + assertEquals(1, trf.getChangedPathFilterFalsePositive()); + + // No negative due to the OrTreeFilter + assertEquals(0, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_nonPathFilter_or_nonPathFilter_binaryOperation() + throws Exception { + RevCommit c1 = commitFile("file1", "1", "master"); + RevCommit c2 = commitFile("file2", "2", "master"); + RevCommit c3 = commitFile("file3", "3", "master"); + RevCommit c4 = commitFile("file4", "4", "master"); + + enableAndWriteCommitGraph(); + + TreeFilter npf1 = TreeFilter.ANY_DIFF; + TreeFilter npf2 = TreeFilter.ANY_DIFF; + + TreeFilter tf = OrTreeFilter.create(new TreeFilter[] { npf1, npf2 }); + + TreeRevFilter trf = new TreeRevFilter(rw, tf); + rw.markStart(rw.lookupCommit(c4)); + rw.setRevFilter(trf); + assertEquals(c4, rw.next()); + assertEquals(c3, rw.next()); + assertEquals(c2, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // No true positives since there's no pathFilter + assertEquals(0, trf.getChangedPathFilterTruePositive()); + + // No false positives since there's no pathFilter + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // No negative since there's no pathFilter + assertEquals(0, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_pathFilter_and_pathFilter_binaryOperation() + throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit(tree(file("file2", blob("2"))), c1); + + branch(c2, "master"); + + enableAndWriteCommitGraph(); + + ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1"); + ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2"); + + TreeFilter atf = AndTreeFilter + .create(new ChangedPathTreeFilter[] { pf1, pf2 }); + TreeRevFilter trf = new TreeRevFilter(rw, atf); + + rw.markStart(rw.lookupCommit(c2)); + rw.setRevFilter(trf); + + assertNull(rw.next()); + + // c1 is not counted as ChangedPathFilter only applies to commits with 1 + // parent + assertEquals(0, trf.getChangedPathFilterTruePositive()); + + // c2 has modified both file 1 and file2, + // however nothing is returned from TreeWalk since a TreeHead + // cannot be two paths at once + assertEquals(1, trf.getChangedPathFilterFalsePositive()); + + // No negatives + assertEquals(0, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_pathFilter_and_pathFilter_and_pathFilter_listOperation() + throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit(tree(file("file2", blob("2"))), c1); + RevCommit c3 = commit(tree(file("file3", blob("3"))), c2); + + branch(c3, "master"); + + enableAndWriteCommitGraph(); + + ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1"); + ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2"); + ChangedPathTreeFilter pf3 = ChangedPathTreeFilter.create("file3"); + + TreeFilter tf = AndTreeFilter + .create(new ChangedPathTreeFilter[] { pf1, pf2, pf3 }); + + TreeRevFilter trf = new TreeRevFilter(rw, tf); + rw.markStart(rw.lookupCommit(c3)); + rw.setRevFilter(trf); + assertNull(rw.next()); + + // c1 is not counted as ChangedPathFilter only applies to commits with 1 + // parent + assertEquals(0, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // c2 and c3 can not possibly have both file1, file2, and file3 as + // treeHead at once + assertEquals(2, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_pathFilter_and_nonPathFilter_binaryOperation() + throws Exception { + RevCommit c1 = commit(tree(file("file1", blob("1")))); + RevCommit c2 = commit(tree(file("file1", blob("2"))), c1); + RevCommit c3 = commit(tree(file("file1", blob("2"))), c2); + + branch(c3, "master"); + + enableAndWriteCommitGraph(); + + ChangedPathTreeFilter pf = ChangedPathTreeFilter.create("file1"); + TreeFilter npf = TreeFilter.ANY_DIFF; + + TreeFilter tf = AndTreeFilter.create(new TreeFilter[] { pf, npf }); + + TreeRevFilter trf = new TreeRevFilter(rw, tf); + rw.markStart(rw.lookupCommit(c3)); + rw.setRevFilter(trf); + assertEquals(c2, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // c2 modified file1 and c1 is not counted as ChangedPathFilter only + // applies to commits with 1 parent + assertEquals(1, trf.getChangedPathFilterTruePositive()); + + // No false positives + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // c3 did not modify file1 + assertEquals(1, trf.getChangedPathFilterNegative()); + } + + @Test + public void testChangedPathFilter_nonPathFilter_and_nonPathFilter_binaryOperation() + throws Exception { + RevCommit c1 = commitFile("file1", "1", "master"); + commitFile("file1", "1", "master"); + RevCommit c3 = commitFile("file3", "3", "master"); + RevCommit c4 = commitFile("file4", "4", "master"); + + enableAndWriteCommitGraph(); + + TreeFilter npf1 = TreeFilter.ANY_DIFF; + TreeFilter npf2 = TreeFilter.ANY_DIFF; + + TreeFilter tf = AndTreeFilter.create(new TreeFilter[] { npf1, npf2 }); + + TreeRevFilter trf = new TreeRevFilter(rw, tf); + rw.markStart(rw.lookupCommit(c4)); + rw.setRevFilter(trf); + assertEquals(c4, rw.next()); + assertEquals(c3, rw.next()); + assertEquals(c1, rw.next()); + assertNull(rw.next()); + + // No true positives since there's no path + assertEquals(0, trf.getChangedPathFilterTruePositive()); + + // No false positives since there's no path + assertEquals(0, trf.getChangedPathFilterFalsePositive()); + + // No negative since there's no path + assertEquals(0, trf.getChangedPathFilterNegative()); + } + + @Test public void testWalkWithCommitMessageFilter() throws Exception { RevCommit a = commit(); RevCommit b = commitBuilder().parent(a) @@ -437,6 +865,8 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase { ConfigConstants.CONFIG_COMMIT_GRAPH, true); db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null, ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true); + db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null, + ConfigConstants.CONFIG_KEY_WRITE_CHANGED_PATHS, true); GC gc = new GC(db); gc.gc().get(); } @@ -445,4 +875,41 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase { rw.close(); rw = new RevWalk(db); } + + private static final class MockConfig extends FileBasedConfig { + private MockConfig() { + super(null, null); + } + + @Override + public void load() throws IOException, ConfigInvalidException { + // Do nothing + } + + @Override + public void save() throws IOException { + // Do nothing + } + + @Override + public boolean isOutdated() { + return false; + } + + @Override + public String toString() { + return "MockConfig"; + } + + @Override + public boolean getBoolean(final String section, final String name, + final boolean defaultValue) { + if (section.equals(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION) + && name.equals( + ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS)) { + return true; + } + return defaultValue; + } + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java index 81ff4a2f92..7fece66bf0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java @@ -14,6 +14,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import java.io.IOException; +import java.time.Instant; import java.util.Date; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -217,14 +218,132 @@ public class RevWalkFilterTest extends RevWalkTestCase { final RevCommit b = commit(a); tick(100); - Date since = getDate(); + Instant since = getInstant(); final RevCommit c1 = commit(b); tick(100); final RevCommit c2 = commit(b); tick(100); - Date until = getDate(); + Instant until = getInstant(); + final RevCommit d = commit(c1, c2); + tick(100); + + final RevCommit e = commit(d); + + { + RevFilter after = CommitTimeRevFilter.after(since); + assertNotNull(after); + rw.setRevFilter(after); + markStart(e); + assertCommit(e, rw.next()); + assertCommit(d, rw.next()); + assertCommit(c2, rw.next()); + assertCommit(c1, rw.next()); + assertNull(rw.next()); + } + + { + RevFilter before = CommitTimeRevFilter.before(until); + assertNotNull(before); + rw.reset(); + rw.setRevFilter(before); + markStart(e); + assertCommit(c2, rw.next()); + assertCommit(c1, rw.next()); + assertCommit(b, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + { + RevFilter between = CommitTimeRevFilter.between(since, until); + assertNotNull(between); + rw.reset(); + rw.setRevFilter(between); + markStart(e); + assertCommit(c2, rw.next()); + assertCommit(c1, rw.next()); + assertNull(rw.next()); + } + } + + @Test + public void testCommitTimeRevFilter_date() throws Exception { + // Using deprecated Date api for the commit time rev filter. + // Delete this tests when method is removed. + final RevCommit a = commit(); + tick(100); + + final RevCommit b = commit(a); + tick(100); + + Date since = Date.from(getInstant()); + final RevCommit c1 = commit(b); + tick(100); + + final RevCommit c2 = commit(b); + tick(100); + + Date until = Date.from(getInstant()); + final RevCommit d = commit(c1, c2); + tick(100); + + final RevCommit e = commit(d); + + { + RevFilter after = CommitTimeRevFilter.after(since); + assertNotNull(after); + rw.setRevFilter(after); + markStart(e); + assertCommit(e, rw.next()); + assertCommit(d, rw.next()); + assertCommit(c2, rw.next()); + assertCommit(c1, rw.next()); + assertNull(rw.next()); + } + + { + RevFilter before = CommitTimeRevFilter.before(until); + assertNotNull(before); + rw.reset(); + rw.setRevFilter(before); + markStart(e); + assertCommit(c2, rw.next()); + assertCommit(c1, rw.next()); + assertCommit(b, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + { + RevFilter between = CommitTimeRevFilter.between(since, until); + assertNotNull(between); + rw.reset(); + rw.setRevFilter(between); + markStart(e); + assertCommit(c2, rw.next()); + assertCommit(c1, rw.next()); + assertNull(rw.next()); + } + } + + @Test + public void testCommitTimeRevFilter_long() throws Exception { + final RevCommit a = commit(); + tick(100); + + final RevCommit b = commit(a); + tick(100); + + long since = getInstant().toEpochMilli(); + final RevCommit c1 = commit(b); + tick(100); + + final RevCommit c2 = commit(b); + tick(100); + + long until = getInstant().toEpochMilli(); final RevCommit d = commit(c1, c2); tick(100); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java index c62136e64d..5203e3fbea 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java @@ -27,9 +27,17 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase { private static class DiffCollector extends RenameCallback { List<DiffEntry> diffs = new ArrayList<>(); + List<RevCommit> commits = new ArrayList<>(); + @Override public void renamed(DiffEntry diff) { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public void renamed(DiffEntry diff, RevCommit commit) { diffs.add(diff); + commits.add(commit); } } @@ -77,6 +85,7 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase { assertNull(rw.next()); assertRenames("a->b"); + assertRenameCommits(renameCommit); } @Test @@ -108,6 +117,7 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase { assertNull(rw.next()); assertRenames("c->a", "b->c", "a->b"); + assertRenameCommits(renameCommit3, renameCommit2, renameCommit1); } /** @@ -136,6 +146,20 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase { } } + protected void assertRenameCommits(RevCommit... expectedCommits) { + Assert.assertEquals( + "Unexpected number of rename commits. Expected: " + + expectedCommits.length + ", actual: " + + diffCollector.diffs.size(), + expectedCommits.length, diffCollector.diffs.size()); + + for (int i = 0; i < expectedCommits.length; i++) { + RevCommit renameCommit = diffCollector.commits.get(i); + Assert.assertNotNull(renameCommit); + Assert.assertEquals(expectedCommits[i], renameCommit); + } + } + protected void assertNoRenames() { Assert.assertEquals("Found unexpected rename/copy diff", 0, diffCollector.diffs.size()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java index ec0c0e7e84..8fa6a83670 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.revwalk; import static org.junit.Assert.assertSame; +import java.time.Instant; import java.util.Date; import org.eclipse.jgit.dircache.DirCacheEntry; @@ -38,8 +39,14 @@ public abstract class RevWalkTestCase extends RepositoryTestCase { return new RevWalk(db); } + // Use getInstant() instead + @Deprecated protected Date getDate() { - return util.getDate(); + return Date.from(util.getInstant()); + } + + protected Instant getInstant() { + return util.getInstant(); } protected void tick(int secDelta) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java index 0a045c917b..ffc7c96f69 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java @@ -14,6 +14,7 @@ import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import java.util.Collection; +import java.util.HashSet; import java.util.List; import org.eclipse.jgit.api.Git; @@ -121,7 +122,7 @@ public class RevWalkUtilsReachableTest extends RevWalkTestCase { Collection<Ref> sortedRefs = RefComparator.sort(allRefs); List<Ref> actual = RevWalkUtils.findBranchesReachableFrom(commit, rw, sortedRefs); - assertEquals(refsThatShouldContainCommit, actual); + assertEquals(new HashSet<>(refsThatShouldContainCommit), new HashSet<>(actual)); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RewriteGeneratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RewriteGeneratorTest.java new file mode 100644 index 0000000000..04e372998c --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RewriteGeneratorTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023, HIS eG + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.revwalk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Test; + +public class RewriteGeneratorTest extends RevWalkTestCase { + + @Test + public void testRewriteGeneratorDoesNotExhaustPreviousGenerator() + throws Exception { + RevCommit a = commit(); + a.flags |= RevWalk.TREE_REV_FILTER_APPLIED; + RevCommit b = commit(a); + + LIFORevQueue q = new LIFORevQueue(); + q.add(a); + q.add(b); + + /* + * Since the TREE_REV_FILTER has been applied to commit a and the + * REWRITE flag has not been applied to commit a, the RewriteGenerator + * must not rewrite the parent of b and thus must not call the previous + * generator (since b already has its correct parent). + */ + RewriteGenerator rewriteGenerator = new RewriteGenerator(q); + rewriteGenerator.next(); + + assertNotNull( + "Previous generator was unnecessarily exhausted by RewriteGenerator", + q.next()); + } + + @Test + public void testRewriteGeneratorRewritesParent() throws Exception { + RevCommit a = commit(); + a.flags |= RevWalk.TREE_REV_FILTER_APPLIED; + a.flags |= RevWalk.REWRITE; + RevCommit b = commit(a); + assertEquals(1, b.getParentCount()); + + LIFORevQueue q = new LIFORevQueue(); + /* + * We are only adding commit b (and not a), because PendingGenerator + * should never emit a commit that has the REWRITE flag set. + */ + q.add(b); + + RewriteGenerator rewriteGenerator = new RewriteGenerator(q); + RevCommit returnedB = rewriteGenerator.next(); + assertEquals(b.getId(), returnedB.getId()); + assertEquals(0, returnedB.getParentCount()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java index 298facfd15..ddbb19cb8f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java @@ -20,17 +20,33 @@ import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.junit.Test; public class TreeRevFilterTest extends RevWalkTestCase { - private RevFilter treeRevFilter() { - return new TreeRevFilter(rw, TreeFilter.ANY_DIFF); - } + @Test + public void testStringOfPearls_FilePath1_treeRevFilter() + throws Exception { + RevCommit a = commit(tree(file("d/f", blob("a")))); + RevCommit b = commit(tree(file("d/f", blob("a"))), a); + RevCommit c = commit(tree(file("d/f", blob("b"))), b); + rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF)); + markStart(c); + + assertCommit(c, rw.next()); + assertEquals(1, c.getParentCount()); + assertCommit(b, c.getParent(0)); + + assertCommit(a, rw.next()); // b was skipped + assertEquals(0, a.getParentCount()); + assertNull(rw.next()); + } @Test - public void testStringOfPearls_FilePath1() + public void testStringOfPearls_FilePath1_noRewriteParents() throws Exception { RevCommit a = commit(tree(file("d/f", blob("a")))); RevCommit b = commit(tree(file("d/f", blob("a"))), a); RevCommit c = commit(tree(file("d/f", blob("b"))), b); - rw.setRevFilter(treeRevFilter()); + + rw.setRewriteParents(false); + rw.setTreeFilter(TreeFilter.ANY_DIFF); markStart(c); assertCommit(c, rw.next()); @@ -43,12 +59,74 @@ public class TreeRevFilterTest extends RevWalkTestCase { } @Test + public void testStringOfPearls_FilePath1_RewriteParents() + throws Exception { + RevCommit a = commit(tree(file("d/f", blob("a")))); + RevCommit b = commit(tree(file("d/f", blob("a"))), a); + RevCommit c = commit(tree(file("d/f", blob("b"))), b); + + rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF)); + rw.setTreeFilter(TreeFilter.ANY_DIFF); + markStart(c); + + assertCommit(c, rw.next()); + assertEquals(1, c.getParentCount()); + assertCommit(a, c.getParent(0)); + + assertCommit(a, rw.next()); // b was skipped + assertEquals(0, a.getParentCount()); + assertNull(rw.next()); + } + + @Test public void testStringOfPearls_FilePath2() throws Exception { RevCommit a = commit(tree(file("d/f", blob("a")))); RevCommit b = commit(tree(file("d/f", blob("a"))), a); RevCommit c = commit(tree(file("d/f", blob("b"))), b); RevCommit d = commit(tree(file("d/f", blob("b"))), c); - rw.setRevFilter(treeRevFilter()); + rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF)); + markStart(d); + + // d was skipped + assertCommit(c, rw.next()); + assertEquals(1, c.getParentCount()); + assertCommit(b, c.getParent(0)); + + // b was skipped + assertCommit(a, rw.next()); + assertEquals(0, a.getParentCount()); + assertNull(rw.next()); + } + + @Test + public void testStringOfPearls_FilePath2_RewriteParents() throws Exception { + RevCommit a = commit(tree(file("d/f", blob("a")))); + RevCommit b = commit(tree(file("d/f", blob("a"))), a); + RevCommit c = commit(tree(file("d/f", blob("b"))), b); + RevCommit d = commit(tree(file("d/f", blob("b"))), c); + rw.setTreeFilter(TreeFilter.ANY_DIFF); + markStart(d); + + // d was skipped + assertCommit(c, rw.next()); + assertEquals(1, c.getParentCount()); + assertCommit(a, c.getParent(0)); + + // b was skipped + assertCommit(a, rw.next()); + assertEquals(0, a.getParentCount()); + assertNull(rw.next()); + } + + @Test + public void testStringOfPearls_FilePath2_RewriteParents_False() throws Exception { + RevCommit a = commit(tree(file("d/f", blob("a")))); + RevCommit b = commit(tree(file("d/f", blob("a"))), a); + RevCommit c = commit(tree(file("d/f", blob("b"))), b); + RevCommit d = commit(tree(file("d/f", blob("b"))), c); + rw.setRewriteParents(false); + rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF)); + rw.setTreeFilter(TreeFilter.ANY_DIFF); markStart(d); // d was skipped @@ -68,7 +146,7 @@ public class TreeRevFilterTest extends RevWalkTestCase { RevCommit b = commit(tree(file("d/f", blob("a"))), a); RevCommit c = commit(tree(file("d/f", blob("b"))), b); RevCommit d = commit(tree(file("d/f", blob("b"))), c); - rw.setRevFilter(treeRevFilter()); + rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF)); markStart(d); // d was skipped @@ -93,7 +171,9 @@ public class TreeRevFilterTest extends RevWalkTestCase { RevCommit g = commit(tree(file("d/f", blob("b"))), f); RevCommit h = commit(tree(file("d/f", blob("b"))), g); RevCommit i = commit(tree(file("d/f", blob("c"))), h); - rw.setRevFilter(treeRevFilter()); + + // Doesn't rewrite parents since no TreeFilter is set + rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF)); markStart(i); assertCommit(i, rw.next()); @@ -112,8 +192,39 @@ public class TreeRevFilterTest extends RevWalkTestCase { } @Test + public void testStringOfPearls_FilePath3_RewriteParents() throws Exception { + RevCommit a = commit(tree(file("d/f", blob("a")))); + RevCommit b = commit(tree(file("d/f", blob("a"))), a); + RevCommit c = commit(tree(file("d/f", blob("b"))), b); + RevCommit d = commit(tree(file("d/f", blob("b"))), c); + RevCommit e = commit(tree(file("d/f", blob("b"))), d); + RevCommit f = commit(tree(file("d/f", blob("b"))), e); + RevCommit g = commit(tree(file("d/f", blob("b"))), f); + RevCommit h = commit(tree(file("d/f", blob("b"))), g); + RevCommit i = commit(tree(file("d/f", blob("c"))), h); + + rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF)); + rw.setTreeFilter(TreeFilter.ANY_DIFF); + markStart(i); + + assertCommit(i, rw.next()); + assertEquals(1, i.getParentCount()); + assertCommit(c, i.getParent(0)); + + // h..d was skipped + assertCommit(c, rw.next()); + assertEquals(1, c.getParentCount()); + assertCommit(a, c.getParent(0)); + + // b was skipped + assertCommit(a, rw.next()); + assertEquals(0, a.getParentCount()); + assertNull(rw.next()); + } + + @Test public void testPathFilterOrOtherFilter() throws Exception { - RevFilter pathFilter = treeRevFilter(); + RevFilter pathFilter = new TreeRevFilter(rw, TreeFilter.ANY_DIFF); RevFilter skipFilter = SkipRevFilter.create(1); RevFilter orFilter = OrRevFilter.create(skipFilter, pathFilter); @@ -125,6 +236,9 @@ public class TreeRevFilterTest extends RevWalkTestCase { rw.setRevFilter(pathFilter); markStart(c); assertCommit(c, rw.next()); + assertEquals(1, c.getParentCount()); + assertCommit(b, c.getParent(0)); + assertCommit(a, rw.next()); // Skip filter matches b, a. diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java new file mode 100644 index 0000000000..7d212d540f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.storage.file; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jgit.util.FS; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class UserConfigFileTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void testParentOnlyLoad() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + } + + @Test + public void testLoadBoth() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Files.writeString(user, "[user]\n\temail = a.u.thor@example.com"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + } + + @Test + public void testOverwriteChild() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Files.writeString(user, "[user]\n\temail = a.u.thor@example.com"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + config.setString("user", null, "name", "A U Thor"); + assertEquals("A U Thor", config.getString("user", null, "name")); + config.save(); + UserConfigFile config2 = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config2.load(); + assertEquals("A U Thor", config2.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + FileBasedConfig cfg = new FileBasedConfig(null, xdg.toFile(), + FS.DETECTED); + cfg.load(); + assertEquals("Archibald Ulysses Thor", + cfg.getString("user", null, "name")); + assertNull(cfg.getString("user", null, "email")); + } + + @Test + public void testUnset() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Files.writeString(user, "[user]\n\temail = a.u.thor@example.com"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + config.setString("user", null, "name", "A U Thor"); + assertEquals("A U Thor", config.getString("user", null, "name")); + config.unset("user", null, "name"); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + config.save(); + UserConfigFile config2 = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config2.load(); + assertEquals("Archibald Ulysses Thor", + config2.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + FileBasedConfig cfg = new FileBasedConfig(null, user.toFile(), + FS.DETECTED); + cfg.load(); + assertNull(cfg.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + cfg.getString("user", null, "email")); + } + + @Test + public void testUnsetSection() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Files.writeString(user, "[user]\n\temail = a.u.thor@example.com"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + config.unsetSection("user", null); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + config.save(); + assertTrue(Files.readString(user).strip().isEmpty()); + } + + @Test + public void testNoChild() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertNull(config.getString("user", null, "email")); + config.setString("user", null, "email", "a.u.thor@example.com"); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + config.save(); + assertFalse(Files.exists(user)); + UserConfigFile config2 = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config2.load(); + assertEquals("Archibald Ulysses Thor", + config2.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config2.getString("user", null, "email")); + } + + @Test + public void testNoFiles() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertNull(config.getString("user", null, "name")); + assertNull(config.getString("user", null, "email")); + config.setString("user", null, "name", "Archibald Ulysses Thor"); + config.setString("user", null, "email", "a.u.thor@example.com"); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + config.save(); + assertTrue(Files.exists(user)); + assertFalse(Files.exists(xdg)); + UserConfigFile config2 = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config2.load(); + assertEquals("Archibald Ulysses Thor", + config2.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config2.getString("user", null, "email")); + } + + @Test + public void testSetInXdg() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + config.setString("user", null, "email", "a.u.thor@example.com"); + config.save(); + assertFalse(Files.exists(user)); + FileBasedConfig cfg = new FileBasedConfig(null, xdg.toFile(), + FS.DETECTED); + cfg.load(); + assertEquals("Archibald Ulysses Thor", + cfg.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + cfg.getString("user", null, "email")); + } + + @Test + public void testUserConfigCreated() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Thread.sleep(3000); // Avoid racily clean isOutdated() below. + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + Files.writeString(user, + "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor"); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertTrue(config.isOutdated()); + config.load(); + assertEquals("A U Thor", config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + } + + @Test + public void testUserConfigDeleted() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Files.writeString(user, + "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor"); + Thread.sleep(3000); // Avoid racily clean isOutdated() below. + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("A U Thor", config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + Files.delete(user); + assertEquals("A U Thor", config.getString("user", null, "name")); + assertEquals("a.u.thor@example.com", + config.getString("user", null, "email")); + assertTrue(config.isOutdated()); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertNull(config.getString("user", null, "email")); + } + + @Test + public void testXdgConfigDeleted() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Thread.sleep(3000); // Avoid racily clean isOutdated() below. + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + Files.delete(xdg); + assertEquals("Archibald Ulysses Thor", + config.getString("user", null, "name")); + assertTrue(config.isOutdated()); + config.load(); + assertNull(config.getString("user", null, "name")); + } + + @Test + public void testXdgConfigDeletedUserConfigExists() throws Exception { + Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg"); + Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor"); + Path user = tmp.getRoot().toPath().resolve("user.cfg"); + Files.writeString(user, + "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor"); + Thread.sleep(3000); // Avoid racily clean isOutdated() below. + UserConfigFile config = new UserConfigFile(null, user.toFile(), + xdg.toFile(), FS.DETECTED); + config.load(); + assertEquals("A U Thor", config.getString("user", null, "name")); + Files.delete(xdg); + assertTrue(config.isOutdated()); + config.load(); + assertEquals("A U Thor", config.getString("user", null, "name")); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java index 300c869b78..4306975f7b 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java @@ -114,6 +114,13 @@ public class SubmoduleAddTest extends RepositoryTestCase { try (Repository subModRepo = generator.getRepository()) { assertNotNull(subModRepo); assertEquals(subCommit, commit); + String worktreeDir = subModRepo.getConfig().getString( + ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_WORKTREE); + assertEquals("../../../sub", worktreeDir); + String gitdir = read(new File(subModRepo.getWorkTree(), + Constants.DOT_GIT)); + assertEquals("gitdir: ../.git/modules/sub", gitdir); } } Status status = Git.wrap(db).status().call(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java index b10bd73208..d54117005d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java @@ -17,21 +17,25 @@ import java.io.File; import java.io.IOException; import java.util.Collection; +import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.InitCommand; +import org.eclipse.jgit.api.SubmoduleAddCommand; import org.eclipse.jgit.api.SubmoduleUpdateCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEditor; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.junit.Test; @@ -40,6 +44,91 @@ import org.junit.Test; */ public class SubmoduleUpdateTest extends RepositoryTestCase { + private Repository submoduleRepo; + + private Git git; + + private AnyObjectId subRepoCommit2; + + private void createSubmoduleRepo() throws IOException, GitAPIException { + File directory = createTempDirectory("submodule_repo"); + InitCommand init = Git.init(); + init.setDirectory(directory); + init.call(); + submoduleRepo = Git.open(directory).getRepository(); + try (Git sub = Git.wrap(submoduleRepo)) { + // commit something + JGitTestUtil.writeTrashFile(submoduleRepo, "commit1.txt", + "commit 1"); + sub.add().addFilepattern("commit1.txt").call(); + sub.commit().setMessage("commit 1").call().getId(); + + JGitTestUtil.writeTrashFile(submoduleRepo, "commit2.txt", + "commit 2"); + sub.add().addFilepattern("commit2.txt").call(); + subRepoCommit2 = sub.commit().setMessage("commit 2").call().getId(); + } + } + + private void addSubmodule(String path) throws GitAPIException { + SubmoduleAddCommand command = new SubmoduleAddCommand(db); + command.setPath(path); + String uri = submoduleRepo.getDirectory().toURI().toString(); + command.setURI(uri); + try (Repository repo = command.call()) { + assertNotNull(repo); + } + git.add().addFilepattern(path).addFilepattern(Constants.DOT_GIT_MODULES) + .call(); + git.commit().setMessage("adding submodule").call(); + recursiveDelete(new File(git.getRepository().getWorkTree(), path)); + recursiveDelete( + new File(new File(git.getRepository().getCommonDirectory(), + Constants.MODULES), path)); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + createSubmoduleRepo(); + + git = Git.wrap(db); + // commit something + writeTrashFile("initial.txt", "initial"); + git.add().addFilepattern("initial.txt").call(); + git.commit().setMessage("initial commit").call(); + } + + public void updateModeClonedRestoredSubmoduleTemplate(String mode) + throws Exception { + String path = "sub"; + addSubmodule(path); + + StoredConfig cfg = git.getRepository().getConfig(); + if (mode != null) { + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, mode); + cfg.save(); + } + SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db); + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + update.call(); + assertEquals(subRepoCommit2.getName(), + subGit.getRepository().getBranch()); + } + + recursiveDelete(new File(db.getWorkTree(), path)); + + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + update.call(); + assertEquals(subRepoCommit2.getName(), + subGit.getRepository().getBranch()); + } + } + @Test public void repositoryWithNoSubmodules() throws GitAPIException { SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db); @@ -50,35 +139,9 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { @Test public void repositoryWithSubmodule() throws Exception { - writeTrashFile("file.txt", "content"); - Git git = Git.wrap(db); - git.add().addFilepattern("file.txt").call(); - final RevCommit commit = git.commit().setMessage("create file").call(); final String path = "sub"; - DirCache cache = db.lockDirCache(); - DirCacheEditor editor = cache.editor(); - editor.add(new PathEdit(path) { - - @Override - public void apply(DirCacheEntry ent) { - ent.setFileMode(FileMode.GITLINK); - ent.setObjectId(commit); - } - }); - editor.commit(); - - StoredConfig config = db.getConfig(); - config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, - ConfigConstants.CONFIG_KEY_URL, db.getDirectory().toURI() - .toString()); - config.save(); - - FileBasedConfig modulesConfig = new FileBasedConfig(new File( - db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS()); - modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, - ConfigConstants.CONFIG_KEY_PATH, path); - modulesConfig.save(); + addSubmodule(path); SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db); Collection<String> updated = command.call(); @@ -90,14 +153,22 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { assertTrue(generator.next()); try (Repository subRepo = generator.getRepository()) { assertNotNull(subRepo); - assertEquals(commit, subRepo.resolve(Constants.HEAD)); + assertEquals(subRepoCommit2, subRepo.resolve(Constants.HEAD)); + String worktreeDir = subRepo.getConfig().getString( + ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_WORKTREE); + assertEquals("../../../sub", worktreeDir); + String gitdir = read( + new File(subRepo.getWorkTree(), Constants.DOT_GIT)); + assertEquals("gitdir: ../.git/modules/sub", gitdir); + } } } @Test - public void repositoryWithUnconfiguredSubmodule() throws IOException, - GitAPIException { + public void repositoryWithUnconfiguredSubmodule() + throws IOException, GitAPIException { final ObjectId id = ObjectId .fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); final String path = "sub"; @@ -113,16 +184,14 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { }); editor.commit(); - FileBasedConfig modulesConfig = new FileBasedConfig(new File( - db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS()); + FileBasedConfig modulesConfig = new FileBasedConfig( + new File(db.getWorkTree(), Constants.DOT_GIT_MODULES), + db.getFS()); modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, ConfigConstants.CONFIG_KEY_PATH, path); String url = "git://server/repo.git"; modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, ConfigConstants.CONFIG_KEY_URL, url); - String update = "rebase"; - modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, - ConfigConstants.CONFIG_KEY_UPDATE, update); modulesConfig.save(); SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db); @@ -132,8 +201,8 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { } @Test - public void repositoryWithInitializedSubmodule() throws IOException, - GitAPIException { + public void repositoryWithInitializedSubmodule() + throws IOException, GitAPIException { final ObjectId id = ObjectId .fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); final String path = "sub"; @@ -160,4 +229,77 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { assertNotNull(updated); assertTrue(updated.isEmpty()); } + + @Test + public void updateModeMergeClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate( + ConfigConstants.CONFIG_KEY_MERGE); + } + + @Test + public void updateModeRebaseClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate( + ConfigConstants.CONFIG_KEY_REBASE); + } + + @Test + public void updateModeCheckoutClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate( + ConfigConstants.CONFIG_KEY_CHECKOUT); + } + + @Test + public void updateModeMissingClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate(null); + } + + @Test + public void updateMode() throws Exception { + String path = "sub"; + addSubmodule(path); + + StoredConfig cfg = git.getRepository().getConfig(); + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, + ConfigConstants.CONFIG_KEY_REBASE); + cfg.save(); + + SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db); + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + CheckoutCommand checkout = subGit.checkout(); + checkout.setName("master"); + checkout.call(); + update.call(); + assertEquals("master", subGit.getRepository().getBranch()); + } + + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, + ConfigConstants.CONFIG_KEY_CHECKOUT); + cfg.save(); + + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + assertEquals(subRepoCommit2.getName(), + subGit.getRepository().getBranch()); + } + + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, + ConfigConstants.CONFIG_KEY_MERGE); + cfg.save(); + + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + CheckoutCommand checkout = subGit.checkout(); + checkout.setName("master"); + checkout.call(); + update.call(); + assertEquals("master", subGit.getRepository().getBranch()); + } + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java new file mode 100644 index 0000000000..490c45b558 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2023 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.symlinks; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.patch.Patch; +import org.eclipse.jgit.patch.PatchApplier; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DirectoryTest extends RepositoryTestCase { + + @BeforeClass + public static void checkPrecondition() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + Path tempDir = Files.createTempDirectory("jgit"); + try { + Path a = tempDir.resolve("a"); + Files.writeString(a, "test"); + Path b = tempDir.resolve("A"); + Assume.assumeTrue(Files.exists(b)); + } finally { + FileUtils.delete(tempDir.toFile(), + FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS); + } + } + + @Parameters(name = "core.symlinks={0}") + public static Boolean[] parameters() { + return new Boolean[] { Boolean.TRUE, Boolean.FALSE }; + } + + @Parameter(0) + public boolean useSymlinks; + + private void checkFiles() throws Exception { + File a = new File(trash, "a"); + assertTrue("a should be a directory", + Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS)); + File b = new File(a, "b"); + assertTrue("a/b should exist", b.isFile()); + File x = new File(trash, "x"); + assertTrue("x should be a directory", + Files.isDirectory(x.toPath(), LinkOption.NOFOLLOW_LINKS)); + File y = new File(x, "y"); + assertTrue("x/y should exist", y.isFile()); + } + + @Test + public void testCheckout() throws Exception { + StoredConfig config = db.getConfig(); + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks); + config.save(); + try (TestRepository<Repository> repo = new TestRepository<>(db)) { + db.incrementOpen(); + // Create links directly in the git repo, then use a hard reset + // to get them into the workspace. + RevCommit base = repo.commit( + repo.tree( + repo.link("A", repo.blob(".git")), + repo.file("a/b", repo.blob("test")), + repo.file("x/y", repo.blob("test2")))); + try (Git git = new Git(db)) { + git.reset().setMode(ResetType.HARD).setRef(base.name()).call(); + File b = new File(new File(trash, ".git"), "b"); + assertFalse(".git/b should not exist", b.exists()); + checkFiles(); + } + } + } + + @Test + public void testCheckout2() throws Exception { + StoredConfig config = db.getConfig(); + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks); + config.save(); + try (TestRepository<Repository> repo = new TestRepository<>(db)) { + db.incrementOpen(); + RevCommit base = repo.commit( + repo.tree( + repo.link("A/B", repo.blob("../.git")), + repo.file("a/b/a/b", repo.blob("test")), + repo.file("x/y", repo.blob("test2")))); + try (Git git = new Git(db)) { + boolean testFiles = true; + try { + git.reset().setMode(ResetType.HARD).setRef(base.name()) + .call(); + } catch (Exception e) { + if (!useSymlinks) { + // There is a file in the middle of the path where we'd + // expect a directory. This case is not handled + // anywhere. What would be a better reply than an IOE? + testFiles = false; + } else { + throw e; + } + } + File a = new File(new File(trash, ".git"), "a"); + assertFalse(".git/a should not exist", a.exists()); + if (testFiles) { + a = new File(trash, "a"); + assertTrue("a should be a directory", Files.isDirectory( + a.toPath(), LinkOption.NOFOLLOW_LINKS)); + File b = new File(a, "b"); + assertTrue("a/b should be a directory", Files.isDirectory( + a.toPath(), LinkOption.NOFOLLOW_LINKS)); + a = new File(b, "a"); + assertTrue("a/b/a should be a directory", Files.isDirectory( + a.toPath(), LinkOption.NOFOLLOW_LINKS)); + b = new File(a, "b"); + assertTrue("a/b/a/b should exist", b.isFile()); + File x = new File(trash, "x"); + assertTrue("x should be a directory", Files.isDirectory( + x.toPath(), LinkOption.NOFOLLOW_LINKS)); + File y = new File(x, "y"); + assertTrue("x/y should exist", y.isFile()); + } + } + } + } + + @Test + public void testMerge() throws Exception { + StoredConfig config = db.getConfig(); + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks); + config.save(); + try (TestRepository<Repository> repo = new TestRepository<>(db)) { + db.incrementOpen(); + RevCommit base = repo.commit( + repo.tree(repo.file("q", repo.blob("test")))); + RevCommit side = repo.commit( + repo.tree( + repo.link("A", repo.blob(".git")), + repo.file("a/b", repo.blob("test")), + repo.file("x/y", repo.blob("test2")))); + try (Git git = new Git(db)) { + git.reset().setMode(ResetType.HARD).setRef(base.name()).call(); + git.merge().include(side) + .setMessage("merged").call(); + File b = new File(new File(trash, ".git"), "b"); + assertFalse(".git/b should not exist", b.exists()); + checkFiles(); + } + } + } + + @Test + public void testMerge2() throws Exception { + StoredConfig config = db.getConfig(); + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks); + config.save(); + try (TestRepository<Repository> repo = new TestRepository<>(db)) { + db.incrementOpen(); + RevCommit base = repo.commit( + repo.tree( + repo.file("q", repo.blob("test")), + repo.link("A", repo.blob(".git")))); + RevCommit side = repo.commit( + repo.tree( + repo.file("a/b", repo.blob("test")), + repo.file("x/y", repo.blob("test2")))); + try (Git git = new Git(db)) { + git.reset().setMode(ResetType.HARD).setRef(base.name()).call(); + git.merge().include(side) + .setMessage("merged").call(); + File b = new File(new File(trash, ".git"), "b"); + assertFalse(".git/b should not exist", b.exists()); + checkFiles(); + } + } + } + + @Test + public void testApply() throws Exception { + StoredConfig config = db.getConfig(); + config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks); + config.save(); + // PatchApplier doesn't do symlinks yet. + try (TestRepository<Repository> repo = new TestRepository<>(db)) { + db.incrementOpen(); + RevCommit base = repo.commit( + repo.tree( + repo.file("x", repo.blob("test")), + repo.link("A", repo.blob(".git")))); + try (Git git = new Git(db)) { + git.reset().setMode(ResetType.HARD).setRef(base.name()).call(); + Patch patch = new Patch(); + try (InputStream patchStream = this.getClass() + .getResourceAsStream("dirtest.patch")) { + patch.parse(patchStream); + } + boolean testFiles = true; + try { + PatchApplier.Result result = new PatchApplier(db) + .applyPatch(patch); + assertNotNull(result); + } catch (IOException e) { + if (!useSymlinks) { + // There is a file there, so the patch won't apply. + // Unclear whether an IOE is the correct response, + // though. Probably some negative PatchApplier.Result is + // more appropriate. + testFiles = false; + } else { + throw e; + } + } + File b = new File(new File(trash, ".git"), "b"); + assertFalse(".git/b should not exist", b.exists()); + if (testFiles) { + File a = new File(trash, "a"); + assertTrue("a should be a directory", + Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS)); + b = new File(a, "b"); + assertTrue("a/b should exist", b.isFile()); + } + } + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java index 578128326f..4f5e35f129 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java @@ -35,6 +35,7 @@ public abstract class SampleDataRepositoryTestCase extends RepositoryTestCase { * @param repo * test repository to receive packfile copies * @throws IOException + * an error occurred */ public static void copyCGitTestPacks(FileRepository repo) throws IOException { final String[] packs = { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java index c47e591445..0ba8926a7f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java @@ -25,10 +25,6 @@ import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -46,16 +42,8 @@ public class AtomicPushTest { public void setUp() throws Exception { server = newRepo("server"); client = newRepo("client"); - testProtocol = new TestProtocol<>( - null, - new ReceivePackFactory<Object>() { - @Override - public ReceivePack create(Object req, Repository db) - throws ServiceNotEnabledException, - ServiceNotAuthorizedException { - return new ReceivePack(db); - } - }); + testProtocol = new TestProtocol<>(null, + (req, db) -> new ReceivePack(db)); uri = testProtocol.register(ctx, server); try (TestRepository<?> clientRepo = new TestRepository<>(client)) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java index 947ca97615..c1ab43eadf 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java @@ -160,7 +160,7 @@ public class HttpAuthTest { String value = header.substring(i + 1).trim(); if (!headerFields.containsKey(key)) - headerFields.put(key, new ArrayList<String>()); + headerFields.put(key, new ArrayList<>()); List<String> values = headerFields.get(key); values.add(value); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/InMemoryPack.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/InMemoryPack.java new file mode 100644 index 0000000000..cad80ef707 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/InMemoryPack.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023, Google LLC. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.util.zip.Deflater; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.eclipse.jgit.util.TemporaryBuffer.Heap; + +/** + * Helper class to create packs for tests. + */ +public class InMemoryPack { + + private final Heap tinyPack; + + public InMemoryPack() { + this(1024); + } + + public InMemoryPack(int size) { + this.tinyPack = new TemporaryBuffer.Heap(size); + } + + public InMemoryPack header(int cnt) + throws IOException { + final byte[] hdr = new byte[8]; + NB.encodeInt32(hdr, 0, 2); + NB.encodeInt32(hdr, 4, cnt); + + tinyPack.write(Constants.PACK_SIGNATURE); + tinyPack.write(hdr, 0, 8); + return this; + } + + public InMemoryPack write(int i) throws IOException { + tinyPack.write(i); + return this; + } + + public InMemoryPack deflate(byte[] content) + throws IOException { + Deflater deflater = new Deflater(); + byte[] buf = new byte[128]; + deflater.setInput(content, 0, content.length); + deflater.finish(); + do { + final int n = deflater.deflate(buf, 0, buf.length); + if (n > 0) + tinyPack.write(buf, 0, n); + } while (!deflater.finished()); + return this; + } + + public InMemoryPack copyRaw(AnyObjectId o) throws IOException { + o.copyRawTo(tinyPack); + return this; + } + + public InMemoryPack digest() throws IOException { + MessageDigest md = Constants.newMessageDigest(); + md.update(tinyPack.toByteArray()); + tinyPack.write(md.digest()); + return this; + } + + public InputStream toInputStream() throws IOException { + return new ByteArrayInputStream(tinyPack.toByteArray()); + } + + public byte[] toByteArray() throws IOException { + return tinyPack.toByteArray(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ObjectDirectoryPackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ObjectDirectoryPackParserTest.java new file mode 100644 index 0000000000..b17c577087 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ObjectDirectoryPackParserTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2021, Google LLC. and others + * Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com> + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.util.zip.Deflater; + +import org.eclipse.jgit.internal.storage.file.ObjectDirectoryPackParser; +import org.eclipse.jgit.internal.storage.file.Pack; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Pack parsing is covered in {@link PackParserTest}. + * + * Here we test ObjectDirectoryPackParser specific parts. e.g. that is creates + * the object-size index. + */ +public class ObjectDirectoryPackParserTest extends RepositoryTestCase { + + @Before + public void setup() throws IOException { + FileBasedConfig jGitConfig = mockSystemReader.getJGitConfig(); + jGitConfig.setInt(ConfigConstants.CONFIG_PACK_SECTION, null, + ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 7); + jGitConfig.save(); + } + + /** + * Test indexing one of the test packs in the egit repo. It has deltas. + * + * @throws IOException + */ + @Test + public void testGitPack() throws IOException { + File packFile = JGitTestUtil.getTestResourceFile("pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.pack"); + try (InputStream is = new FileInputStream(packFile)) { + ObjectDirectoryPackParser p = index(is); + p.parse(NullProgressMonitor.INSTANCE); + + Pack pack = p.getPack(); + assertTrue(pack.hasObjectSizeIndex()); + + // Only blobs in the pack + ObjectId blob1 = ObjectId + .fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3"); + ObjectId blob2 = ObjectId + .fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259"); + assertEquals(18787, pack.getIndexedObjectSize(blob1)); + assertEquals(18009, pack.getIndexedObjectSize(blob2)); + + // Indexed sizes match object db sizes + assertEquals(db.getObjectDatabase().open(blob1).getSize(), + pack.getIndexedObjectSize(blob1)); + assertEquals(db.getObjectDatabase().open(blob2).getSize(), + pack.getIndexedObjectSize(blob2)); + + } + } + + /** + * This is just another pack. It so happens that we have two convenient pack to + * test with in the repository. + * + * @throws IOException + */ + @Test + public void testAnotherGitPack() throws IOException { + File packFile = JGitTestUtil.getTestResourceFile("pack-df2982f284bbabb6bdb59ee3fcc6eb0983e20371.pack"); + try (InputStream is = new FileInputStream(packFile)) { + ObjectDirectoryPackParser p = index(is); + p.parse(NullProgressMonitor.INSTANCE); + Pack pack = p.getPack(); + + // Blob smaller than threshold: + assertEquals(-1, pack.getIndexedObjectSize(ObjectId + .fromString("15fae9e651043de0fd1deef588aa3fbf5a7a41c6"))); + + // Blob bigger than threshold + assertEquals(10, pack.getIndexedObjectSize(ObjectId + .fromString("8230f48330e0055d9e0bc5a2a77718f6dd9324b8"))); + + // A commit (not indexed) + assertEquals(-1, pack.getIndexedObjectSize(ObjectId + .fromString("d0114ab8ac326bab30e3a657a0397578c5a1af88"))); + + // Object not in pack + assertEquals(-1, pack.getIndexedObjectSize(ObjectId + .fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + } + } + + @Test + public void testTinyThinPack() throws Exception { + // less than 16 bytes, so its length fits in a single byte later + String base = "abcdefghijklmn"; + RevBlob a; + try (TestRepository d = new TestRepository<Repository>(db)) { + a = d.blob(base); + } + + TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); + + packHeader(pack, 1); + + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + a.copyRawTo(pack); + deflate(pack, new byte[] { (byte) base.length(), // size of the base + (byte) (base.length() + 1), // size after reconstruction + 0x1, 'b' }); // append one byte + + digest(pack); + + ObjectDirectoryPackParser p = index(new ByteArrayInputStream(pack.toByteArray())); + p.setAllowThin(true); + p.parse(NullProgressMonitor.INSTANCE); + + Pack writtenPack = p.getPack(); + // base + assertEquals(base.length(), writtenPack.getIndexedObjectSize(a)); + // undeltified blob + assertEquals(base.length() + 1, + writtenPack.getIndexedObjectSize(ObjectId.fromString( + "f177875498138143c9657cc52b049ad4d20d5223"))); + } + + @Test + public void testPackWithDuplicateBlob() throws Exception { + final byte[] data = Constants.encode("0123456789abcdefg"); + RevBlob blob; + try (TestRepository<Repository> d = new TestRepository<>(db)) { + blob = d.blob(data); + assertTrue(db.getObjectDatabase().has(blob)); + } + + TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); + packHeader(pack, 1); + pack.write(Constants.OBJ_BLOB << 4 | 0x80 | 1); + pack.write(1); + deflate(pack, data); + digest(pack); + + ObjectDirectoryPackParser p = index( + new ByteArrayInputStream(pack.toByteArray())); + p.setAllowThin(false); + p.parse(NullProgressMonitor.INSTANCE); + + assertEquals(data.length, p.getPack().getIndexedObjectSize(blob)); + } + + private static void packHeader(TemporaryBuffer.Heap tinyPack, int cnt) + throws IOException { + final byte[] hdr = new byte[8]; + NB.encodeInt32(hdr, 0, 2); + NB.encodeInt32(hdr, 4, cnt); + + tinyPack.write(Constants.PACK_SIGNATURE); + tinyPack.write(hdr, 0, 8); + } + + private static void deflate(TemporaryBuffer.Heap tinyPack, + final byte[] content) + throws IOException { + final Deflater deflater = new Deflater(); + final byte[] buf = new byte[128]; + deflater.setInput(content, 0, content.length); + deflater.finish(); + do { + final int n = deflater.deflate(buf, 0, buf.length); + if (n > 0) + tinyPack.write(buf, 0, n); + } while (!deflater.finished()); + } + + private static void digest(TemporaryBuffer.Heap buf) throws IOException { + MessageDigest md = Constants.newMessageDigest(); + md.update(buf.toByteArray()); + buf.write(md.digest()); + } + + private ObjectInserter inserter; + + @After + public void release() { + if (inserter != null) { + inserter.close(); + } + } + + private ObjectDirectoryPackParser index(InputStream in) throws IOException { + if (inserter == null) + inserter = db.newObjectInserter(); + return (ObjectDirectoryPackParser) inserter.newPackParser(in); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java index f02428efc9..6148df99f3 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java @@ -22,12 +22,10 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.security.MessageDigest; import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.zip.Deflater; import org.eclipse.jgit.errors.TooLargeObjectInPackException; import org.eclipse.jgit.internal.JGitText; @@ -42,8 +40,6 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevBlob; -import org.eclipse.jgit.util.NB; -import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.io.UnionInputStream; import org.junit.After; import org.junit.Test; @@ -193,17 +189,14 @@ public class PackParserTest extends RepositoryTestCase { a = d.blob("a"); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + pack.copyRaw(a); + pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' }); + pack.digest(); - packHeader(pack, 1); - - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); - a.copyRawTo(pack); - deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' }); - - digest(pack); - - PackParser p = index(new ByteArrayInputStream(pack.toByteArray())); + PackParser p = index(pack.toInputStream()); p.setAllowThin(true); p.parse(NullProgressMonitor.INSTANCE); } @@ -216,14 +209,14 @@ public class PackParserTest extends RepositoryTestCase { assertTrue(db.getObjectDatabase().has(d.blob(data))); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); - packHeader(pack, 1); - pack.write((Constants.OBJ_BLOB) << 4 | 0x80 | 1); + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_BLOB << 4 | 0x80 | 1); pack.write(1); - deflate(pack, data); - digest(pack); + pack.deflate(data); + pack.digest(); - PackParser p = index(new ByteArrayInputStream(pack.toByteArray())); + PackParser p = index(pack.toInputStream()); p.setAllowThin(false); p.parse(NullProgressMonitor.INSTANCE); } @@ -236,16 +229,16 @@ public class PackParserTest extends RepositoryTestCase { assertTrue(db.getObjectDatabase().has(d.blob(data))); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); - packHeader(pack, 2); - pack.write((Constants.OBJ_BLOB) << 4 | 10); // offset 12 - deflate(pack, data); - pack.write((Constants.OBJ_OFS_DELTA) << 4 | 4); // offset 31 + InMemoryPack pack = new InMemoryPack(); + pack.header(2); + pack.write(Constants.OBJ_BLOB << 4 | 10); // offset 12 + pack.deflate(data); + pack.write(Constants.OBJ_OFS_DELTA << 4 | 4); // offset 31 pack.write(19); - deflate(pack, new byte[] { 0xA, 0xB, 0x1, 'b' }); - digest(pack); + pack.deflate(new byte[] { 0xA, 0xB, 0x1, 'b' }); + pack.digest(); - PackParser p = index(new ByteArrayInputStream(pack.toByteArray())); + PackParser p = index(pack.toInputStream()); p.parse(NullProgressMonitor.INSTANCE); List<PackedObjectInfo> sortedObjectList = p.getSortedObjectList(null); @@ -275,15 +268,15 @@ public class PackParserTest extends RepositoryTestCase { a = d.blob("a"); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); - packHeader(pack, 1); - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); - a.copyRawTo(pack); - deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' }); - digest(pack); + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + pack.copyRaw(a); + pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' }); + pack.digest(); PackParser p = index(new UnionInputStream( - new ByteArrayInputStream(pack.toByteArray()), + pack.toInputStream(), new ByteArrayInputStream(new byte[] { 0x7e }))); p.setAllowThin(true); p.setCheckEofAfterPackFooter(true); @@ -305,22 +298,21 @@ public class PackParserTest extends RepositoryTestCase { d.blob(data); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); - - packHeader(pack, 1); - pack.write((Constants.OBJ_BLOB) << 4 | 10); - deflate(pack, data); - digest(pack); + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_BLOB << 4 | 10); + pack.deflate(data); + pack.digest(); - PackParser p = index(new ByteArrayInputStream(pack.toByteArray())); + PackParser p = index(pack.toInputStream()); p.setMaxObjectSizeLimit(11); p.parse(NullProgressMonitor.INSTANCE); - p = index(new ByteArrayInputStream(pack.toByteArray())); + p = index(pack.toInputStream()); p.setMaxObjectSizeLimit(10); p.parse(NullProgressMonitor.INSTANCE); - p = index(new ByteArrayInputStream(pack.toByteArray())); + p = index(pack.toInputStream()); p.setMaxObjectSizeLimit(9); try { p.parse(NullProgressMonitor.INSTANCE); @@ -339,21 +331,20 @@ public class PackParserTest extends RepositoryTestCase { a = d.blob("a"); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); - - packHeader(pack, 1); - pack.write((Constants.OBJ_REF_DELTA) << 4 | 14); - a.copyRawTo(pack); - deflate(pack, new byte[] { 1, 11, 11, 'a', '0', '1', '2', '3', '4', + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_REF_DELTA << 4 | 14); + pack.copyRaw(a); + pack.deflate(new byte[] { 1, 11, 11, 'a', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }); - digest(pack); + pack.digest(); - PackParser p = index(new ByteArrayInputStream(pack.toByteArray())); + PackParser p = index(pack.toInputStream()); p.setAllowThin(true); p.setMaxObjectSizeLimit(14); p.parse(NullProgressMonitor.INSTANCE); - p = index(new ByteArrayInputStream(pack.toByteArray())); + p = index(pack.toInputStream()); p.setAllowThin(true); p.setMaxObjectSizeLimit(13); try { @@ -373,20 +364,19 @@ public class PackParserTest extends RepositoryTestCase { a = d.blob("0123456789"); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); - - packHeader(pack, 1); - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); - a.copyRawTo(pack); - deflate(pack, new byte[] { 10, 11, 1, 'a' }); - digest(pack); + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + pack.copyRaw(a); + pack.deflate(new byte[] { 10, 11, 1, 'a' }); + pack.digest(); - PackParser p = index(new ByteArrayInputStream(pack.toByteArray())); + PackParser p = index(pack.toInputStream()); p.setAllowThin(true); p.setMaxObjectSizeLimit(11); p.parse(NullProgressMonitor.INSTANCE); - p = index(new ByteArrayInputStream(pack.toByteArray())); + p = index(pack.toInputStream()); p.setAllowThin(true); p.setMaxObjectSizeLimit(10); try { @@ -406,12 +396,12 @@ public class PackParserTest extends RepositoryTestCase { a = d.blob("a"); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); - packHeader(pack, 1); - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); - a.copyRawTo(pack); - deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' }); - digest(pack); + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + pack.copyRaw(a); + pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' }); + pack.digest(); InputStream in = new ByteArrayInputStream(pack.toByteArray()) { @Override @@ -447,12 +437,12 @@ public class PackParserTest extends RepositoryTestCase { a = d.blob("a"); } - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(32*1024); - packHeader(pack, 1); - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); - a.copyRawTo(pack); - deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' }); - digest(pack); + InMemoryPack pack = new InMemoryPack(); + pack.header(1); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + pack.copyRaw(a); + pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' }); + pack.digest(); byte packData[] = pack.toByteArray(); byte streamData[] = new byte[packData.length + 1]; @@ -476,14 +466,14 @@ public class PackParserTest extends RepositoryTestCase { // Build a pack ~17k int objects = 900; - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(32 * 1024); - packHeader(pack, objects); + InMemoryPack pack = new InMemoryPack(32 * 1024); + pack.header(objects); for (int i = 0; i < objects; i++) { - pack.write((Constants.OBJ_BLOB) << 4 | 10); - deflate(pack, data); + pack.write(Constants.OBJ_BLOB << 4 | 10); + pack.deflate(data); } - digest(pack); + pack.digest(); byte packData[] = pack.toByteArray(); byte streamData[] = new byte[packData.length + 1]; @@ -510,14 +500,15 @@ public class PackParserTest extends RepositoryTestCase { } int objects = 248; - TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(32 * 1024); - packHeader(pack, objects + 1); + InMemoryPack pack = new InMemoryPack(32 * 1024); + pack.header(objects + 1); + int offset = 13; StringBuilder sb = new StringBuilder(); for (int i = 0; i < offset; i++) sb.append(i); offset = sb.toString().length(); - int lenByte = (Constants.OBJ_BLOB) << 4 | (offset & 0x0F); + int lenByte = Constants.OBJ_BLOB << 4 | (offset & 0x0F); offset >>= 4; if (offset > 0) lenByte |= 1 << 7; @@ -529,16 +520,16 @@ public class PackParserTest extends RepositoryTestCase { lenByte |= 1 << 7; pack.write(lenByte); } - deflate(pack, Constants.encode(sb.toString())); + pack.deflate(Constants.encode(sb.toString())); for (int i = 0; i < objects; i++) { // The last pack header written falls across the 8192 byte boundary // between [8189:8210] - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); - b.copyRawTo(pack); - deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' }); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); + pack.copyRaw(b); + pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' }); } - digest(pack); + pack.digest(); byte packData[] = pack.toByteArray(); byte streamData[] = new byte[packData.length + 1]; @@ -555,36 +546,6 @@ public class PackParserTest extends RepositoryTestCase { assertEquals(0x7e, in.read()); } - private static void packHeader(TemporaryBuffer.Heap tinyPack, int cnt) - throws IOException { - final byte[] hdr = new byte[8]; - NB.encodeInt32(hdr, 0, 2); - NB.encodeInt32(hdr, 4, cnt); - - tinyPack.write(Constants.PACK_SIGNATURE); - tinyPack.write(hdr, 0, 8); - } - - private static void deflate(TemporaryBuffer.Heap tinyPack, - final byte[] content) - throws IOException { - final Deflater deflater = new Deflater(); - final byte[] buf = new byte[128]; - deflater.setInput(content, 0, content.length); - deflater.finish(); - do { - final int n = deflater.deflate(buf, 0, buf.length); - if (n > 0) - tinyPack.write(buf, 0, n); - } while (!deflater.finished()); - } - - private static void digest(TemporaryBuffer.Heap buf) throws IOException { - MessageDigest md = Constants.newMessageDigest(); - md.update(buf.toByteArray()); - buf.write(md.digest()); - } - private ObjectInserter inserter; @After diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java index f5658abceb..6290b7978e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java @@ -73,7 +73,7 @@ public class PushCertificateIdentTest { @Test public void fuzzyCasesMatchPersonIdent() throws Exception { // See RawParseUtils_ParsePersonIdentTest#testParsePersonIdent_fuzzyCases() - Date when = new Date(1234567890000l); + Date when = new Date(1234567890000L); TimeZone tz = TimeZone.getTimeZone("GMT-7"); assertMatchesPersonIdent( @@ -89,7 +89,7 @@ public class PushCertificateIdentTest { @Test public void incompleteCasesMatchPersonIdent() throws Exception { // See RawParseUtils_ParsePersonIdentTest#testParsePersonIdent_incompleteCases() - Date when = new Date(1234567890000l); + Date when = new Date(1234567890000L); TimeZone tz = TimeZone.getTimeZone("GMT-7"); assertMatchesPersonIdent( @@ -138,6 +138,51 @@ public class PushCertificateIdentTest { "Me <me@example.com>"); } + @Test + public void timezoneRange_hours() { + int HOUR_TO_MS = 60 * 60 * 1000; + + // java.util.TimeZone: Hours must be between 0 to 23 + PushCertificateIdent hourLimit = PushCertificateIdent + .parse("A U. Thor <a_u_thor@example.com> 1218123387 +2300"); + assertEquals(1380, hourLimit.getTimeZoneOffset()); + assertEquals(23 * HOUR_TO_MS, + hourLimit.getTimeZone().getOffset(1218123387)); + + PushCertificateIdent hourDubious = PushCertificateIdent + .parse("A U. Thor <a_u_thor@example.com> 1218123387 +2400"); + assertEquals(1440, hourDubious.getTimeZoneOffset()); + assertEquals(0, hourDubious.getTimeZone().getOffset(1218123387)); + } + + @Test + public void timezoneRange_minutes() { + PushCertificateIdent hourLimit = PushCertificateIdent + .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0059"); + assertEquals(59, hourLimit.getTimeZoneOffset()); + assertEquals(59 * 60 * 1000, + hourLimit.getTimeZone().getOffset(1218123387)); + + // This becomes one hour and one minute (!) + PushCertificateIdent hourDubious = PushCertificateIdent + .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0061"); + assertEquals(61, hourDubious.getTimeZoneOffset()); + assertEquals(61 * 60 * 1000, + hourDubious.getTimeZone().getOffset(1218123387)); + + PushCertificateIdent weirdCase = PushCertificateIdent + .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0099"); + assertEquals(99, weirdCase.getTimeZoneOffset()); + assertEquals(99 * 60 * 1000, + weirdCase.getTimeZone().getOffset(1218123387)); + + PushCertificateIdent weirdCase2 = PushCertificateIdent + .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0199"); + assertEquals(60 + 99, weirdCase2.getTimeZoneOffset()); + assertEquals((60 + 99) * 60 * 1000, + weirdCase2.getTimeZone().getOffset(1218123387)); + } + private static void assertMatchesPersonIdent(String raw, PersonIdent expectedPersonIdent, String expectedUserId) { PushCertificateIdent certIdent = PushCertificateIdent.parse(raw); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java index 4f01e4d445..a03222be0c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java @@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -318,8 +320,8 @@ public class PushCertificateStoreTest { } private PersonIdent newIdent() { - return new PersonIdent( - "A U. Thor", "author@example.com", ts.getAndIncrement(), 0); + return new PersonIdent("A U. Thor", "author@example.com", + Instant.ofEpochMilli(ts.getAndIncrement()), ZoneOffset.UTC); } private PushCertificateStore newStore() { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java index a91bc95c8d..7dac27f612 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java @@ -187,10 +187,10 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024); packHeader(pack, 2); - pack.write((Constants.OBJ_BLOB) << 4 | 1); + pack.write(Constants.OBJ_BLOB << 4 | 1); deflate(pack, new byte[] { 'a' }); - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); a.copyRawTo(pack); deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' }); @@ -296,7 +296,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas packHeader(pack, 3); copy(pack, src.open(N)); copy(pack, src.open(s.parseBody(N).getTree())); - pack.write((Constants.OBJ_REF_DELTA) << 4 | 4); + pack.write(Constants.OBJ_REF_DELTA << 4 | 4); b.copyRawTo(pack); deflate(pack, delta); digest(pack); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java index cc910b3b9f..f589a399a4 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java @@ -70,20 +70,30 @@ public abstract class RequestValidatorTestCase { } /** + * Check if the validator accepts a reachable commit + * * @return true if a commit reachable from a visible tip (but not directly * the tip) is valid */ protected abstract boolean isReachableCommitValid(); - /** @return true if a commit not reachable from any tip is valid */ + /** + * Check if the validator accepts an unreachable commit + * + * @return true if a commit not reachable from any tip is valid + **/ protected abstract boolean isUnreachableCommitValid(); /** + * Check if the validator accepts a previously advertised tip + * * @return true if the commit directly pointed by an advertised ref is valid */ protected abstract boolean isAdvertisedTipValid(); /** + * Check if the validator accepts a previous unadvertised tip + * * @return true if the object directly pointed by a non-advertised ref is * valid */ @@ -92,17 +102,23 @@ public abstract class RequestValidatorTestCase { // UploadPack doesn't allow to ask for blobs when there is no // bitmap. Test both cases separately. /** + * Check if the validator accepts a reachable blob (repo with bitmaps) + * * @return true if a reachable blob is valid (and the repo has bitmaps) */ protected abstract boolean isReachableBlobValid_withBitmaps(); /** + * Check if the validator accepts a reachable blob (repo without bitmaps) + * * @return true if a reachable blob is valid (and the repo does NOT have * bitmaps) */ protected abstract boolean isReachableBlobValid_withoutBitmaps(); /** + * Check if the validator accepts an unreachable blob + * * @return true if a blob unreachable from any tip is valid */ protected abstract boolean isUnreachableBlobValid(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java index 3516ed01fc..444e958ae8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java @@ -10,7 +10,6 @@ package org.eclipse.jgit.transport; -import static java.lang.Integer.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR; @@ -224,7 +223,8 @@ public class SideBandOutputStreamTest { } catch (IllegalArgumentException e) { assertEquals(MessageFormat.format( JGitText.get().packetSizeMustBeAtMost, - valueOf(Integer.MAX_VALUE), valueOf(65520)), e.getMessage()); + Integer.valueOf(Integer.MAX_VALUE), Integer.valueOf(65520)), + e.getMessage()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java index 029b45e1e6..96d3a5835a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java @@ -14,10 +14,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import java.io.File; import java.io.IOException; import java.net.HttpCookie; +import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -101,7 +101,7 @@ public class TransportHttpTest extends SampleDataRepositoryTestCase { .singletonList("cookie2=some value; Max-Age=1234; Path=/")); try (TransportHttp transportHttp = new TransportHttp(db, uri)) { - Date creationDate = new Date(); + Instant creationDate = Instant.now(); transportHttp.processResponseCookies(connection); // evaluate written cookie file @@ -112,8 +112,9 @@ public class TransportHttpTest extends SampleDataRepositoryTestCase { cookie.setPath("/u/2/"); cookie.setMaxAge( - (Instant.parse("2100-01-01T11:00:00.000Z").toEpochMilli() - - creationDate.getTime()) / 1000); + Duration.between(creationDate, + Instant.parse("2100-01-01T11:00:00.000Z")) + .getSeconds()); cookie.setSecure(true); cookie.setHttpOnly(true); expectedCookies.add(cookie); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java index d403624b71..67920029d4 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java @@ -82,6 +82,43 @@ public class URIishTest { } @Test + public void testBrokenFilePath() throws Exception { + String str = "D:\\\\my\\\\x"; + URIish u = new URIish(str); + assertNull(u.getScheme()); + assertFalse(u.isRemote()); + assertEquals(str, u.getPath()); + assertEquals(u, new URIish(str)); + } + + @Test + public void testStackOverflow() throws Exception { + StringBuilder b = new StringBuilder("D:\\"); + for (int i = 0; i < 4000; i++) { + b.append("x\\"); + } + String str = b.toString(); + URIish u = new URIish(str); + assertNull(u.getScheme()); + assertFalse(u.isRemote()); + assertEquals(str, u.getPath()); + } + + @Test + public void testStackOverflow2() throws Exception { + StringBuilder b = new StringBuilder("D:\\"); + for (int i = 0; i < 4000; i++) { + b.append("x\\"); + } + b.append('y'); + String str = b.toString(); + URIish u = new URIish(str); + assertNull(u.getScheme()); + assertFalse(u.isRemote()); + assertEquals(str, u.getPath()); + } + + @Test public void testRelativePath() throws Exception { final String str = "../../foo/bar"; URIish u = new URIish(str); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackHandleDeletedPackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackHandleDeletedPackFileTest.java new file mode 100644 index 0000000000..272c5ea5b5 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackHandleDeletedPackFileTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023, Dariusz Luksza <dariusz.luksza@gmail.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.internal.storage.file.GC; +import org.eclipse.jgit.internal.storage.file.Pack; +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.junit.TestRepository.CommitBuilder; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.UploadPack.RequestPolicy; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class UploadPackHandleDeletedPackFileTest + extends LocalDiskRepositoryTestCase { + + private FileRepository server; + + private TestRepository<FileRepository> remote; + + private Repository client; + + private RevCommit head; + + @Parameter + public boolean emptyCommit; + + @Parameters(name="empty commit: {0}") + public static Collection<Boolean[]> initTestData() { + return Arrays.asList( + new Boolean[][] { { Boolean.TRUE }, { Boolean.FALSE } }); + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + server = createBareRepository(); + server.getConfig().setString("protocol", null, "version", "2"); + + remote = new TestRepository<>(server); + client = new InMemoryRepository(new DfsRepositoryDescription("client")); + + setupServerRepo(); + head = server.parseCommit(server.resolve(HEAD)); + } + + @Test + public void testV2PackFileRemovedDuringUploadPack() throws Exception { + doRemovePackFileDuringUploadPack(PackExt.PACK); + } + + @Test + public void testV2IdxFileRemovedDuringUploadPack() throws Exception { + doRemovePackFileDuringUploadPack(PackExt.INDEX); + } + + @Test + public void testV2BitmapFileRemovedDuringUploadPack() throws Exception { + doRemovePackFileDuringUploadPack(PackExt.BITMAP_INDEX); + } + + private void doRemovePackFileDuringUploadPack(PackExt packExt) + throws Exception { + Object ctx = new Object(); + TestProtocol<Object> testProtocol = new TestProtocol<>( + (Object req, Repository db) -> { + UploadPack up = new UploadPack(db); + up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT); + Collection<Pack> packs = server.getObjectDatabase() + .getPacks(); + assertEquals("single pack expected", 1, packs.size()); + Pack pack = packs.iterator().next(); + + try { + addNewCommit(); + + new GC(remote.getRepository()).gc().get(); + + pack.getPackFile().create(packExt).delete(); + } catch (Exception e) { + throw new AssertionError( + "GC or pack file removal failed", e); + } + + return up; + }, null); + + URIish uri = testProtocol.register(ctx, server); + + try (Transport tn = testProtocol.open(uri, client, "server")) { + tn.fetch(NullProgressMonitor.INSTANCE, + Collections.singletonList(new RefSpec(head.name()))); + assertTrue(client.getObjectDatabase().has(head)); + } + } + + private void addNewCommit() throws Exception { + CommitBuilder commit = remote.commit().message("2"); + if (!emptyCommit) { + commit = commit.add("test2.txt", remote.blob("2")); + } + remote.update("master", commit.parent(head).create()); + } + + private void setupServerRepo() throws Exception { + RevCommit commit0 = remote.commit().message("0") + .add("test.txt", remote.blob("0")) + .create(); + remote.update("master", commit0); + + new GC(remote.getRepository()).gc().get(); // create pack files + + head = remote.commit().message("1").parent(commit0) + .add("test1.txt", remote.blob("1")) + .create(); + remote.update("master", head); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java index 2711762640..a5507c8f81 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java @@ -27,9 +27,6 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.UploadPack.RequestPolicy; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -264,15 +261,10 @@ public class UploadPackReachabilityTest { } private static TestProtocol<Object> generateReachableCommitUploadPackProtocol() { - return new TestProtocol<>(new UploadPackFactory<Object>() { - @Override - public UploadPack create(Object req, Repository db) - throws ServiceNotEnabledException, - ServiceNotAuthorizedException { - UploadPack up = new UploadPack(db); - up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT); - return up; - } + return new TestProtocol<>((req, db) -> { + UploadPack up = new UploadPack(db); + up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT); + return up; }, null); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java index 9755ed1b69..5c2f0e5c7d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java @@ -1,5 +1,6 @@ package org.eclipse.jgit.transport; +import static java.time.ZoneOffset.UTC; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -11,12 +12,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -501,15 +504,15 @@ public class UploadPackTest { assertThat(hook.capabilitiesRequest, notNullValue()); assertThat(pckIn.readString(), is("version 2")); assertThat( - Arrays.asList(pckIn.readString(), pckIn.readString(), - pckIn.readString()), + Arrays.asList(pckIn.readString(),pckIn.readString(), + pckIn.readString(), pckIn.readString()), // TODO(jonathantanmy) This check is written this way // to make it simple to see that we expect this list of // capabilities, but probably should be loosened to // allow additional commands to be added to the list, // and additional capabilities to be added to existing // commands without requiring test changes. - hasItems("ls-refs", "fetch=shallow", "server-option")); + hasItems("agent=" + UserAgent.get() ,"ls-refs", "fetch=shallow", "server-option")); assertTrue(PacketLineIn.isEnd(pckIn.readString())); } @@ -535,7 +538,7 @@ public class UploadPackTest { lines.add(line); } } - assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option")); + assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option", "agent=" + UserAgent.get())); } private void checkUnadvertisedIfUnallowed(String configSection, @@ -564,6 +567,47 @@ public class UploadPackTest { } @Test + public void testV0CapabilitiesAllowAnySha1InWant() throws Exception { + checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack", + "allowanysha1inwant", "allow-reachable-sha1-in-want", + "allow-tip-sha1-in-want"); + } + + @Test + public void testV0CapabilitiesAllowReachableSha1InWant() throws Exception { + checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack", + "allowreachablesha1inwant", "allow-reachable-sha1-in-want"); + } + + @Test + public void testV0CapabilitiesAllowTipSha1InWant() throws Exception { + checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack", + "allowtipsha1inwant", "allow-tip-sha1-in-want"); + } + + private void checkAvertisedCapabilityProtocolV0IfAllowed( + String configSection, String configName, String... capabilities) + throws Exception { + server.getConfig().setBoolean(configSection, null, configName, true); + ByteArrayInputStream recvStream = uploadPackSetup( + TransferConfig.ProtocolVersion.V0.version(), null, + PacketLineIn.end()); + PacketLineIn pckIn = new PacketLineIn(recvStream); + + String line; + while (!PacketLineIn.isEnd((line = pckIn.readString()))) { + if (line.contains("capabilities")) { + List<String> linesCapabilities = Arrays.asList(line.substring( + line.indexOf(" ", line.indexOf("capabilities")) + 1) + .split(" ")); + assertThat(linesCapabilities, hasItems(capabilities)); + return; + } + } + fail("Server side protocol did not contain any capabilities'"); + } + + @Test public void testV2CapabilitiesAllowFilter() throws Exception { checkAdvertisedIfAllowed("uploadpack", "allowfilter", "filter"); checkUnadvertisedIfUnallowed("uploadpack", "allowfilter", "filter"); @@ -601,9 +645,9 @@ public class UploadPackTest { assertThat(pckIn.readString(), is("version 2")); assertThat( - Arrays.asList(pckIn.readString(), pckIn.readString(), + Arrays.asList(pckIn.readString(),pckIn.readString(), pckIn.readString(), pckIn.readString()), - hasItems("ls-refs", "fetch=shallow", "server-option")); + hasItems("agent="+ UserAgent.get(),"ls-refs", "fetch=shallow", "server-option")); assertTrue(PacketLineIn.isEnd(pckIn.readString())); } @@ -1464,14 +1508,19 @@ public class UploadPackTest { public void testV2FetchShallowSince() throws Exception { PersonIdent person = new PersonIdent(remote.getRepository()); - RevCommit beyondBoundary = remote.commit() - .committer(new PersonIdent(person, 1510000000, 0)).create(); - RevCommit boundary = remote.commit().parent(beyondBoundary) - .committer(new PersonIdent(person, 1520000000, 0)).create(); - RevCommit tooOld = remote.commit() - .committer(new PersonIdent(person, 1500000000, 0)).create(); + RevCommit beyondBoundary = remote.commit().committer( + new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC)) + .create(); + RevCommit boundary = remote.commit().parent(beyondBoundary).committer( + new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC)) + .create(); + RevCommit tooOld = remote.commit().committer( + new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC)) + .create(); RevCommit merge = remote.commit().parent(boundary).parent(tooOld) - .committer(new PersonIdent(person, 1530000000, 0)).create(); + .committer(new PersonIdent(person, + Instant.ofEpochSecond(1530000), UTC)) + .create(); remote.update("branch1", merge); @@ -1517,12 +1566,15 @@ public class UploadPackTest { public void testV2FetchShallowSince_excludedParentWithMultipleChildren() throws Exception { PersonIdent person = new PersonIdent(remote.getRepository()); - RevCommit base = remote.commit() - .committer(new PersonIdent(person, 1500000000, 0)).create(); - RevCommit child1 = remote.commit().parent(base) - .committer(new PersonIdent(person, 1510000000, 0)).create(); - RevCommit child2 = remote.commit().parent(base) - .committer(new PersonIdent(person, 1520000000, 0)).create(); + RevCommit base = remote.commit().committer( + new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC)) + .create(); + RevCommit child1 = remote.commit().parent(base).committer( + new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC)) + .create(); + RevCommit child2 = remote.commit().parent(base).committer( + new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC)) + .create(); remote.update("branch1", child1); remote.update("branch2", child2); @@ -1559,8 +1611,9 @@ public class UploadPackTest { public void testV2FetchShallowSince_noCommitsSelected() throws Exception { PersonIdent person = new PersonIdent(remote.getRepository()); - RevCommit tooOld = remote.commit() - .committer(new PersonIdent(person, 1500000000, 0)).create(); + RevCommit tooOld = remote.commit().committer( + new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC)) + .create(); remote.update("branch1", tooOld); @@ -1684,12 +1737,15 @@ public class UploadPackTest { public void testV2FetchDeepenNot_excludedParentWithMultipleChildren() throws Exception { PersonIdent person = new PersonIdent(remote.getRepository()); - RevCommit base = remote.commit() - .committer(new PersonIdent(person, 1500000000, 0)).create(); - RevCommit child1 = remote.commit().parent(base) - .committer(new PersonIdent(person, 1510000000, 0)).create(); - RevCommit child2 = remote.commit().parent(base) - .committer(new PersonIdent(person, 1520000000, 0)).create(); + RevCommit base = remote.commit().committer( + new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC)) + .create(); + RevCommit child1 = remote.commit().parent(base).committer( + new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC)) + .create(); + RevCommit child2 = remote.commit().parent(base).committer( + new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC)) + .create(); remote.update("base", base); remote.update("branch1", child1); @@ -1800,14 +1856,15 @@ public class UploadPackTest { RevBlob blobHighDepth = remote.blob("hi"); RevTree subtree = remote.tree(remote.file("1", blobHighDepth)); - RevTree rootTree = (new TreeBuilder() { + + RevTree rootTree = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.add(remote.file("1", blobLowDepth)); dcBuilder.addTree(new byte[] {'2'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree); } - }).build(); + }.build(); RevCommit commit = remote.commit(rootTree); DeepTreePreparator() throws Exception {} @@ -1904,21 +1961,23 @@ public class UploadPackTest { class RepeatedSubtreePreparator { RevBlob foo = remote.blob("foo"); RevTree subtree3 = remote.tree(remote.file("foo", foo)); - RevTree subtree2 = (new TreeBuilder() { + RevTree subtree2 = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'b'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree3); } - }).build(); - RevTree subtree1 = (new TreeBuilder() { + }.build(); + + RevTree subtree1 = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree2); } - }).build(); - RevTree rootTree = (new TreeBuilder() { + }.build(); + + RevTree rootTree = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'a'}, DirCacheEntry.STAGE_0, @@ -1926,7 +1985,7 @@ public class UploadPackTest { dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree2); } - }).build(); + }.build(); RevCommit commit = remote.commit(rootTree); RepeatedSubtreePreparator() throws Exception {} @@ -1970,22 +2029,22 @@ public class UploadPackTest { RevTree subtree1 = remote.tree(remote.file("foo", foo)); /** b/foo */ - RevTree subtree2 = (new TreeBuilder() { + RevTree subtree2 = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'b'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree1); } - }).build(); + }.build(); /** x/b/foo */ - RevTree subtree3 = (new TreeBuilder() { + RevTree subtree3 = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree2); } - }).build(); + }.build(); RevBlob baz = remote.blob("baz"); @@ -1993,33 +2052,33 @@ public class UploadPackTest { RevTree subtree4 = remote.tree(remote.file("baz", baz)); /** c/baz */ - RevTree subtree5 = (new TreeBuilder() { + RevTree subtree5 = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'c'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree4); } - }).build(); + }.build(); /** u/c/baz */ - RevTree subtree6 = (new TreeBuilder() { + RevTree subtree6 = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'u'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree5); } - }).build(); + }.build(); /** v/c/baz */ - RevTree subtree7 = (new TreeBuilder() { + RevTree subtree7 = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'v'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree5); } - }).build(); + }.build(); - RevTree rootTree = (new TreeBuilder() { + RevTree rootTree = new TreeBuilder() { @Override void addElements(DirCacheBuilder dcBuilder) throws Exception { dcBuilder.addTree(new byte[] {'a'}, DirCacheEntry.STAGE_0, @@ -2031,7 +2090,7 @@ public class UploadPackTest { dcBuilder.addTree(new byte[] {'z'}, DirCacheEntry.STAGE_0, remote.getRevWalk().getObjectReader(), subtree7); } - }).build(); + }.build(); RevCommit commit = remote.commit(rootTree); RepeatedSubtreeAtSameLevelPreparator() throws Exception {} @@ -2168,12 +2227,12 @@ public class UploadPackTest { /** * <pre> * remote: - * foo <- foofoo <-- branchFoo - * bar <- barbar <-- branchBar + * foo <- foofoo <-- branchFoo + * bar <- barbar <-- branchBar * * client: - * foo <-- branchFoo - * bar <-- branchBar + * foo <-- branchFoo + * bar <-- branchBar * * fetch(branchFoo) should send exactly 1 have (i.e. foo) from branchFoo * </pre> @@ -2215,7 +2274,8 @@ public class UploadPackTest { uri = testProtocol.register(ctx, server); - TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES, true)); + TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES, + /* useNegotiationTip= */true)); try (Transport tn = testProtocol.open(uri, clientRepo.getRepository(), "server")) { @@ -2335,7 +2395,8 @@ public class UploadPackTest { }, null); uri = testProtocol.register(ctx, server); - TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES, true)); + TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES, + /* useNegotiationTip= */true)); try (Transport tn = testProtocol.open(uri, clientRepo.getRepository(), "server")) { @@ -2362,6 +2423,67 @@ public class UploadPackTest { } } + /** + * <pre> + * remote: + * foo <- foofoo <-- branchFoo + * bar <- barbar <-- branchBar + * + * client: + * none + * + * fetch(branchFoo) should not send have and should get only branchFoo back + * </pre> + */ + @Test + public void testNegotiationTipDoesNotDoFullClone() throws Exception { + RevCommit fooParent = remote.commit().message("foo").create(); + RevCommit fooChild = remote.commit().message("foofoo").parent(fooParent) + .create(); + RevCommit barParent = remote.commit().message("bar").create(); + RevCommit barChild = remote.commit().message("barbar").parent(barParent) + .create(); + + // Remote has branchFoo at fooChild and branchBar at barChild + remote.update("branchFoo", fooChild); + remote.update("branchBar", barChild); + + AtomicReference<UploadPack> uploadPack = new AtomicReference<>(); + CountHavesPreUploadHook countHavesHook = new CountHavesPreUploadHook(); + + // Client does not have branchFoo & branchBar + try (TestRepository<InMemoryRepository> clientRepo = new TestRepository<>( + client)) { + testProtocol = new TestProtocol<>((Object req, Repository db) -> { + UploadPack up = new UploadPack(db); + up.setPreUploadHook(countHavesHook); + uploadPack.set(up); + return up; + }, null); + + uri = testProtocol.register(ctx, server); + + TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES, + /* useNegotiationTip= */true)); + try (Transport tn = testProtocol.open(uri, + clientRepo.getRepository(), "server")) { + + tn.fetch(NullProgressMonitor.INSTANCE, + Collections.singletonList( + new RefSpec("refs/heads/branchFoo")), + "branchFoo"); + } + } + + assertTrue(client.getObjectDatabase().has(fooParent.toObjectId())); + assertTrue(client.getObjectDatabase().has(fooChild.toObjectId())); + assertFalse(client.getObjectDatabase().has(barParent.toObjectId())); + assertFalse(client.getObjectDatabase().has(barChild.toObjectId())); + + assertEquals(0, uploadPack.get().getStatistics().getHaves()); + assertTrue(countHavesHook.havesSentDuringNegotiation.isEmpty()); + } + private static class CountHavesPreUploadHook implements PreUploadHook { Set<ObjectId> havesSentDuringNegotiation = new HashSet<>(); @@ -2754,7 +2876,7 @@ public class UploadPackTest { RevTag heavyTag2 = remote.tag("middleTagRing", heavyTag1); remote.lightweightTag("refTagRing", heavyTag2); - UploadPack uploadPack = new UploadPack(remote.getRepository()); + try (UploadPack uploadPack = new UploadPack(remote.getRepository())) { ByteArrayOutputStream cli = new ByteArrayOutputStream(); PacketLineOut clientWant = new PacketLineOut(cli); @@ -2764,7 +2886,6 @@ public class UploadPackTest { clientWant.writeString("done\n"); try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) { - uploadPack.setPreUploadHook(new PreUploadHook() { @Override public void onBeginNegotiateRound(UploadPack up, @@ -2817,6 +2938,7 @@ public class UploadPackTest { assertTrue(objDb.has(heavyTag2.toObjectId())); } } +} @Test public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exception { @@ -2828,7 +2950,7 @@ public class UploadPackTest { RevTag tag3 = remote.tag("t3", tag2); remote.lightweightTag("t3", tag3); - UploadPack uploadPack = new UploadPack(remote.getRepository()); + try (UploadPack uploadPack = new UploadPack(remote.getRepository())) { ByteArrayOutputStream cli = new ByteArrayOutputStream(); PacketLineOut clientWant = new PacketLineOut(cli); @@ -2838,7 +2960,6 @@ public class UploadPackTest { clientWant.writeString("done\n"); try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) { - uploadPack.setPreUploadHook(new PreUploadHook() { @Override public void onBeginNegotiateRound(UploadPack up, @@ -2886,6 +3007,7 @@ public class UploadPackTest { assertTrue(objDb.has(one.toObjectId())); } } +} @Test public void testSafeToClearRefsInFetchV0() throws Exception { @@ -2937,7 +3059,70 @@ public class UploadPackTest { assertThat(pckIn.readString(), is("packfile")); parsePack(recvStream); assertTrue(client.getObjectDatabase().has(one.toObjectId())); - assertEquals(1, ((RefCallsCountingRepository)server).numRefCalls()); + assertEquals(0, ((RefCallsCountingRepository)server).numRefCalls()); + } + + /* + * Invokes UploadPack with specified protocol version and sends it the given + * lines, and returns UploadPack statistics (use uploadPackSetup to get the + * output stream) + */ + private PackStatistics uploadPackV2SetupStats(String... inputLines) + throws Exception { + + ByteArrayInputStream send = linesAsInputStream(inputLines); + String version = TransferConfig.ProtocolVersion.V2.version(); + server.getConfig().setString(ConfigConstants.CONFIG_PROTOCOL_SECTION, + null, ConfigConstants.CONFIG_KEY_VERSION, version); + try (UploadPack up = new UploadPack(server)) { + up.setExtraParameters(Sets.of("version=".concat(version))); + + ByteArrayOutputStream recv = new ByteArrayOutputStream(); + up.upload(send, recv, null); + return up.getStatistics(); + } + } + + @Test + public void testUseWantedRefsAsAdvertisedSetV2_onlyWantedRefs() + throws Exception { + server = new RefCallsCountingRepository( + new DfsRepositoryDescription("server")); + remote = new TestRepository<>(server); + RevCommit one = remote.commit().message("1").create(); + RevCommit two = remote.commit().message("2").create(); + RevCommit three = remote.commit().message("3").create(); + remote.update("one", one); + remote.update("two", two); + remote.update("three", three); + server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", + true); + PackStatistics packStats = uploadPackV2SetupStats("command=fetch\n", + PacketLineIn.delimiter(), "want-ref refs/heads/one\n", + "want-ref refs/heads/two\n", "done\n", PacketLineIn.end()); + assertEquals("only wanted-refs", 2, packStats.getAdvertised()); + assertEquals(0, ((RefCallsCountingRepository) server).numRefCalls()); + } + + @Test + public void testUseWantedRefsAsAdvertisedSetV2_withWantId() + throws Exception { + server = new RefCallsCountingRepository( + new DfsRepositoryDescription("server")); + remote = new TestRepository<>(server); + RevCommit one = remote.commit().message("1").create(); + RevCommit two = remote.commit().message("2").create(); + RevCommit three = remote.commit().message("3").create(); + remote.update("one", one); + remote.update("two", two); + remote.update("three", three); + server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", + true); + PackStatistics packStats = uploadPackV2SetupStats("command=fetch\n", + PacketLineIn.delimiter(), "want-ref refs/heads/one\n", + "want " + one.getName() + "\n", "done\n", PacketLineIn.end()); + assertEquals("all refs", 3, packStats.getAdvertised()); + assertEquals(1, ((RefCallsCountingRepository) server).numRefCalls()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java index 37f9514494..f71781dcf3 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java @@ -16,9 +16,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.net.HttpURLConnection; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -74,7 +74,7 @@ public class JDKHttpConnectionTest { } private void assertValues(String key, String... values) { - List<String> l = new LinkedList<>(); + List<String> l = new ArrayList<>(); List<String> hf = c.getHeaderFields(key); if (hf != null) { l.addAll(hf); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java index e463e9070a..7b9e70d4da 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java @@ -25,8 +25,9 @@ import java.time.Instant; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.dircache.Checkout; import org.eclipse.jgit.dircache.DirCache; -import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheEditor; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; import org.eclipse.jgit.dircache.DirCacheEntry; @@ -38,6 +39,7 @@ import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; @@ -303,11 +305,12 @@ public class FileTreeIteratorTest extends RepositoryTestCase { DirCacheEntry dce = db.readDirCache().getEntry("symlink"); dce.setFileMode(FileMode.SYMLINK); try (ObjectReader objectReader = db.newObjectReader()) { + Checkout checkout = new Checkout(db).setRecursiveDeletion(false); + checkout.checkout(dce, + new CheckoutMetadata(EolStreamType.DIRECT, null), + objectReader, null); WorkingTreeOptions options = db.getConfig() .get(WorkingTreeOptions.KEY); - DirCacheCheckout.checkoutEntry(db, dce, objectReader, false, null, - options); - FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(), options); while (!fti.getEntryPathString().equals("symlink")) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilterTest.java new file mode 100644 index 0000000000..88f6b75c6b --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilterTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.treewalk.filter; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.eclipse.jgit.internal.storage.commitgraph.ChangedPathFilter; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.filter.TreeFilter.MutableBoolean; +import org.junit.Test; + +public class ChangedPathTreeFilterTest { + + @Test + public void shouldTreeWalk_no_usingCpf() { + ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b"); + MutableBoolean cfpUsed = new MutableBoolean(); + + boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("c"), null, + cfpUsed); + + assertFalse(result); + assertTrue(cfpUsed.get()); + } + + @Test + public void shouldTreeWalk_yes_usingCpf() { + ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b"); + MutableBoolean cfpUsed = new MutableBoolean(); + + boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("a/b"), null, + cfpUsed); + + assertTrue(result); + assertTrue(cfpUsed.get()); + } + + @Test + public void shouldTreeWalk_yes_noCpf() { + ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b"); + MutableBoolean cfpUsed = new MutableBoolean(); + + boolean result = f.shouldTreeWalk(FakeRevCommit.noCpf(), null, + cfpUsed); + + assertTrue(result); + assertFalse(cfpUsed.get()); + } + + @Test + public void shouldTreeWalk_no_usingCpf_noReport() { + ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b"); + boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("c"), null, + null); + + assertFalse(result); + } + + @Test + public void shouldTreeWalk_yes_usingCpf_noReport() { + ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b"); + boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("a/b"), null, + null); + assertTrue(result); + } + + @Test + public void shouldTreeWalk_yes_noCpf_noReport() { + ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b"); + boolean result = f.shouldTreeWalk(FakeRevCommit.noCpf(), null, + null); + + assertTrue(result); + } + + private static class FakeRevCommit extends RevCommit { + + static RevCommit withCpfFor(String... paths) { + return new FakeRevCommit( + ChangedPathFilter.fromPaths(Arrays.stream(paths) + .map(str -> ByteBuffer.wrap(str.getBytes(UTF_8))) + .collect(Collectors.toSet()))); + } + + static RevCommit noCpf() { + return new FakeRevCommit(null); + } + + private final ChangedPathFilter cpf; + + /** + * Create a new commit reference. + * + * @param cpf + * changedPathFilter + */ + protected FakeRevCommit(ChangedPathFilter cpf) { + super(ObjectId.zeroId()); + this.cpf = cpf; + } + + @Override + public ChangedPathFilter getChangedPathFilter(RevWalk rw) { + return cpf; + } + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java index 32bd40312f..1bb4939c85 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.treewalk.filter; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -21,7 +22,9 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEditor; @@ -30,6 +33,7 @@ import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Sets; @@ -143,6 +147,29 @@ public class PathFilterGroupTest { } @Test + public void testGetPathsBestEffort() { + String[] paths = { "path1", "path2", "path3" }; + Set<byte[]> expected = Arrays.stream(paths).map(Constants::encode) + .collect(Collectors.toSet()); + TreeFilter pathFilterGroup = PathFilterGroup.createFromStrings(paths); + Optional<Set<byte[]>> bestEffortPaths = pathFilterGroup + .getPathsBestEffort(); + assertTrue(bestEffortPaths.isPresent()); + Set<byte[]> actual = bestEffortPaths.get(); + assertEquals(expected.size(), actual.size()); + for (byte[] actualPath : actual) { + boolean findMatch = false; + for (byte[] expectedPath : expected) { + if (Arrays.equals(actualPath, expectedPath)) { + findMatch = true; + break; + } + } + assertTrue(findMatch); + } + } + + @Test public void testStopWalk() throws MissingObjectException, IncorrectObjectTypeException, IOException { // Obvious diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java index 0b5a7356c8..d6f4f18f30 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java @@ -53,8 +53,8 @@ public class PathSuffixFilterTest extends RepositoryTestCase { @Test public void testEdgeCases() throws IOException { ObjectId treeId = createTree("abc", "abcd", "bcd", "c"); - assertEquals(new ArrayList<String>(), getMatchingPaths("xbcd", treeId)); - assertEquals(new ArrayList<String>(), getMatchingPaths("abcx", treeId)); + assertEquals(new ArrayList<>(), getMatchingPaths("xbcd", treeId)); + assertEquals(new ArrayList<>(), getMatchingPaths("abcx", treeId)); assertEquals(Arrays.asList("abcd"), getMatchingPaths("abcd", treeId)); assertEquals(Arrays.asList("abcd", "bcd"), getMatchingPaths("bcd", treeId)); assertEquals(Arrays.asList("abc", "c"), getMatchingPaths("c", treeId)); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java index a270ca8861..b8b9cbe558 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java @@ -296,6 +296,7 @@ public class BlockListTest { public void testAddRejectsBadIndexes() { BlockList<Integer> list = new BlockList<>(4); list.add(Integer.valueOf(41)); + assertEquals(Integer.valueOf(41), list.get(0)); try { list.add(-1, Integer.valueOf(42)); @@ -316,6 +317,7 @@ public class BlockListTest { public void testRemoveRejectsBadIndexes() { BlockList<Integer> list = new BlockList<>(4); list.add(Integer.valueOf(41)); + assertEquals(Integer.valueOf(41), list.get(0)); try { list.remove(-1); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java index 32652494d2..44e8632228 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java @@ -12,7 +12,9 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; -import java.util.concurrent.TimeUnit; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; import org.eclipse.jgit.junit.MockSystemReader; import org.eclipse.jgit.lib.ObjectId; @@ -53,9 +55,9 @@ public class ChangeIdUtilTest { MockSystemReader mockSystemReader = new MockSystemReader(); - final long when = mockSystemReader.getCurrentTime(); + Instant when = mockSystemReader.now(); - final int tz = new MockSystemReader().getTimezone(when); + ZoneId tz = new MockSystemReader().getTimeZoneAt(when); PersonIdent author = new PersonIdent("J. Author", "ja@example.com"); { @@ -218,23 +220,23 @@ public class ChangeIdUtilTest { @Test public void testACommitWithSubjectBodyBugBrackersAndSob() throws Exception { assertEquals( - "a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\nChange-Id: I90ecb589bef766302532c3e00915e10114b00f62\n[bracket]\nSigned-off-by: me@you.too\n", - call("a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\n[bracket]\nSigned-off-by: me@you.too\n\n")); + "a commit with subject body, bug, brackers and sob\n\nText\n\nBug: 33\n[bracket]\nChange-Id: I94dc6ed919a4baaa7c1bf8712717b888c6b90363\nSigned-off-by: me@you.too\n", + call("a commit with subject body, bug, brackers and sob\n\nText\n\nBug: 33\n[bracket]\nSigned-off-by: me@you.too\n\n")); } @Test public void testACommitWithSubjectBodyBugLineWithASpaceAndSob() throws Exception { assertEquals( - "a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\nChange-Id: I864e2218bdee033c8ce9a7f923af9e0d5dc16863\n \nSigned-off-by: me@you.too\n", - call("a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n")); + "a commit with subject body, bug, line with a space and sob\n\nText\n\nBug: 33\n \nChange-Id: I126b472d2e0e64ad8187d61857f0169f9ccdae86\nSigned-off-by: me@you.too\n", + call("a commit with subject body, bug, line with a space and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n")); } @Test public void testACommitWithSubjectBodyBugEmptyLineAndSob() throws Exception { assertEquals( - "a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\nChange-Id: I33f119f533313883e6ada3df600c4f0d4db23a76\n \nSigned-off-by: me@you.too\n", - call("a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n")); + "a commit with subject body, bug, empty line and sob\n\nText\n\nBug: 33\n\nChange-Id: Ic3b61b6e39a0815669b65302e9e75e6a5a019a26\nSigned-off-by: me@you.too\n", + call("a commit with subject body, bug, empty line and sob\n\nText\n\nBug: 33\n\nSigned-off-by: me@you.too\n\n")); } @Test @@ -342,9 +344,7 @@ public class ChangeIdUtilTest { /** Increment the {@link #author} and {@link #committer} times. */ protected void tick() { - final long delta = TimeUnit.MILLISECONDS.convert(5 * 60, - TimeUnit.SECONDS); - final long now = author.getWhen().getTime() + delta; + Instant now = author.getWhenAsInstant().plus(Duration.ofMinutes(5)); author = new PersonIdent(author, now, tz); committer = new PersonIdent(committer, now, tz); @@ -528,7 +528,7 @@ public class ChangeIdUtilTest { } @Test - public void testChangeIdAfterBugOrIssue() throws Exception { + public void testChangeIdAfterOtherFooters() throws Exception { assertEquals("a\n" + // "\n" + // "Bug: 42\n" + // @@ -541,6 +541,18 @@ public class ChangeIdUtilTest { assertEquals("a\n" + // "\n" + // + "Bug: 42\n" + // + " multi-line Bug footer\n" + // + "Change-Id: Icc953ef35f1a4ee5eb945132aefd603ae3d9dd9f\n" + // + SOB1,// + call("a\n" + // + "\n" + // + "Bug: 42\n" + // + " multi-line Bug footer\n" + // + SOB1)); + + assertEquals("a\n" + // + "\n" + // "Issue: 42\n" + // "Change-Id: Ie66e07d89ae5b114c0975b49cf326e90331dd822\n" + // SOB1,// @@ -548,6 +560,14 @@ public class ChangeIdUtilTest { "\n" + // "Issue: 42\n" + // SOB1)); + + assertEquals("a\n" + // + "\n" + // + "Other: none\n" + // + "Change-Id: Ide70e625dea61854206378a377dd12e462ae720f\n",// + call("a\n" + // + "\n" + // + "Other: none\n")); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java index 171d80c3da..3a7fa2388e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java @@ -13,6 +13,7 @@ package org.eclipse.jgit.util; import static java.time.Instant.EPOCH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeNoException; import static org.junit.Assume.assumeTrue; @@ -28,8 +29,12 @@ import java.nio.file.attribute.PosixFilePermission; import java.time.Duration; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.errors.CommandFailedException; @@ -195,6 +200,23 @@ public class FSTest { } @Test + @SuppressWarnings("boxing") + public void testConcurrentSymlinkSupport() + throws ExecutionException, InterruptedException { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + int n = 3; + List<CompletableFuture<Boolean>> futures = new ArrayList<>(); + for (int i = 0; i < n; i++) { + futures.add(CompletableFuture.supplyAsync( + () -> FS.DETECTED.newInstance().supportsSymlinks())); + } + + for (int i = 0; i < n; i++) { + assertTrue(futures.get(i).get()); + } + } + + @Test public void testFsTimestampResolution() throws Exception { DateTimeFormatter formatter = DateTimeFormatter .ofPattern("uuuu-MMM-dd HH:mm:ss.nnnnnnnnn", Locale.ENGLISH) @@ -233,4 +255,26 @@ public class FSTest { assertFalse(RepositoryCache.FileKey .isGitRepository(new File("repo.git"), FS.DETECTED)); } + + @Test + public void testSearchPath() throws IOException { + File f1 = new File(trash, "file1"); + FileUtils.createNewFile(f1); + f1.setExecutable(true); + File f2 = new File(trash, "file2"); + FileUtils.createNewFile(f2); + assertEquals(f1, FS.searchPath(trash.getAbsolutePath(), "file1")); + assertNull(FS.searchPath(trash.getAbsolutePath(), "file2")); + } + + @Test + public void testSearchPathEmptyPath() { + assertNull(FS.searchPath("", "file1")); + assertNull(FS.searchPath(File.pathSeparator, "file1")); + assertNull(FS.searchPath(File.pathSeparator + File.pathSeparator, + "file1")); + assertNull(FS.searchPath( + " " + File.pathSeparator + " " + File.pathSeparator + " \t", + "file1")); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java index 2b1fb2ef04..5106540227 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java @@ -74,7 +74,9 @@ public class FileUtilsTest { try { FileUtils.delete(f, FileUtils.SKIP_MISSING); } catch (IOException e) { - fail("deletion of non-existing file must not fail with option SKIP_MISSING"); + throw new AssertionError( + "deletion of non-existing file must not fail with option SKIP_MISSING", + e); } } @@ -108,7 +110,9 @@ public class FileUtilsTest { try { FileUtils.delete(d, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); } catch (IOException e) { - fail("recursive deletion of non-existing directory must not fail with option SKIP_MISSING"); + throw new AssertionError( + "recursive deletion of non-existing directory must not fail with option SKIP_MISSING", + e); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java index 0bd7e0bd61..7ef386f6ee 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java @@ -90,7 +90,8 @@ public class GitDateFormatterTest { public void LOCALE() { String date = new GitDateFormatter(Format.LOCALE).formatDate(ident); assertTrue("Sep 20, 2011 7:09:25 PM -0400".equals(date) - || "Sep 20, 2011, 7:09:25 PM -0400".equals(date)); // JDK-8206961 + || "Sep 20, 2011, 7:09:25 PM -0400".equals(date) // JDK-8206961 + || "Sep 20, 2011, 7:09:25\u202FPM -0400".equals(date)); // JDK-8304925 } @Test @@ -98,6 +99,7 @@ public class GitDateFormatterTest { String date = new GitDateFormatter(Format.LOCALELOCAL) .formatDate(ident); assertTrue("Sep 20, 2011 7:39:25 PM".equals(date) - || "Sep 20, 2011, 7:39:25 PM".equals(date)); // JDK-8206961 + || "Sep 20, 2011, 7:39:25 PM".equals(date) // JDK-8206961 + || "Sep 20, 2011, 7:39:25\u202FPM".equals(date)); // JDK-8304925 } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java new file mode 100644 index 0000000000..a59d7bc7bb --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012, Christian Halstrick and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertThrows; + +import java.text.ParseException; + +import org.eclipse.jgit.junit.MockSystemReader; +import org.junit.After; +import org.junit.Before; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; + +/** + * Tests which assert that unparseable Strings lead to ParseExceptions + */ +@RunWith(Theories.class) +public class GitTimeParserBadlyFormattedTest { + private String dateStr; + + @Before + public void setUp() { + MockSystemReader mockSystemReader = new MockSystemReader(); + SystemReader.setInstance(mockSystemReader); + } + + @After + public void tearDown() { + SystemReader.setInstance(null); + } + + public GitTimeParserBadlyFormattedTest(String dateStr) { + this.dateStr = dateStr; + } + + @DataPoints + public static String[] getDataPoints() { + return new String[] { "", ".", "...", "1970", "3000.3000.3000", "3 yesterday ago", + "now yesterday ago", "yesterdays", "3.day. 2.week.ago", + "day ago", "Gra Feb 21 15:35:00 2007 +0100", + "Sun Feb 21 15:35:00 2007 +0100", + "Wed Feb 21 15:35:00 Grand +0100" }; + } + + @Theory + public void badlyFormattedWithoutRef() { + assertThrows( + "The expected ParseException while parsing '" + dateStr + + "' did not occur.", + ParseException.class, () -> GitTimeParser.parse(dateStr)); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java new file mode 100644 index 0000000000..0e5eb283a4 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2024, Christian Halstrick and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.text.ParseException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; + +import org.eclipse.jgit.junit.MockSystemReader; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class GitTimeParserTest { + MockSystemReader mockSystemReader; + + @Before + public void setUp() { + mockSystemReader = new MockSystemReader(); + SystemReader.setInstance(mockSystemReader); + } + + @After + public void tearDown() { + SystemReader.setInstance(null); + } + + @Test + public void yesterday() throws ParseException { + LocalDateTime parse = GitTimeParser.parse("yesterday"); + + LocalDateTime now = SystemReader.getInstance().civilNow(); + assertEquals(Period.between(parse.toLocalDate(), now.toLocalDate()), + Period.ofDays(1)); + } + + @Test + public void never() throws ParseException { + LocalDateTime parse = GitTimeParser.parse("never"); + assertEquals(LocalDateTime.MAX, parse); + } + + @Test + public void now_pointInTime() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100"); + + LocalDateTime parsedNow = GitTimeParser.parse("now", aTime); + + assertEquals(aTime, parsedNow); + } + + @Test + public void now_systemTime() throws ParseException { + LocalDateTime firstNow = GitTimeParser.parse("now"); + assertEquals(SystemReader.getInstance().civilNow(), firstNow); + mockSystemReader.tick(10); + LocalDateTime secondNow = GitTimeParser.parse("now"); + assertTrue(secondNow.isAfter(firstNow)); + } + + @Test + public void weeksAgo() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100"); + + LocalDateTime parse = GitTimeParser.parse("2 weeks ago", aTime); + assertEquals(asLocalDateTime("2007-02-07 15:35:00 +0100"), parse); + } + + @Test + public void daysAndWeeksAgo() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100"); + + LocalDateTime twoWeeksAgoActual = GitTimeParser.parse("2 weeks ago", + aTime); + + LocalDateTime twoWeeksAgoExpected = asLocalDateTime( + "2007-02-07 15:35:00 +0100"); + assertEquals(twoWeeksAgoExpected, twoWeeksAgoActual); + + LocalDateTime combinedWhitespace = GitTimeParser + .parse("3 days 2 weeks ago", aTime); + LocalDateTime combinedWhitespaceExpected = asLocalDateTime( + "2007-02-04 15:35:00 +0100"); + assertEquals(combinedWhitespaceExpected, combinedWhitespace); + + LocalDateTime combinedDots = GitTimeParser.parse("3.day.2.week.ago", + aTime); + LocalDateTime combinedDotsExpected = asLocalDateTime( + "2007-02-04 15:35:00 +0100"); + assertEquals(combinedDotsExpected, combinedDots); + } + + @Test + public void hoursAgo() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:00 +0100"); + + LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago", + aTime); + + LocalDateTime twoHoursAgoExpected = asLocalDateTime( + "2007-02-21 15:35:00 +0100"); + assertEquals(twoHoursAgoExpected, twoHoursAgoActual); + } + + @Test + public void hoursAgo_acrossDay() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:00 +0100"); + + LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago", + aTime); + + LocalDateTime twoHoursAgoExpected = asLocalDateTime( + "2007-02-20 22:35:00 +0100"); + assertEquals(twoHoursAgoExpected, twoHoursAgoActual); + } + + @Test + public void minutesHoursAgoCombined() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-04 15:35:00 +0100"); + + LocalDateTime combinedWhitespace = GitTimeParser + .parse("3 hours 2 minutes ago", aTime); + LocalDateTime combinedWhitespaceExpected = asLocalDateTime( + "2007-02-04 12:33:00 +0100"); + assertEquals(combinedWhitespaceExpected, combinedWhitespace); + + LocalDateTime combinedDots = GitTimeParser + .parse("3.hours.2.minutes.ago", aTime); + LocalDateTime combinedDotsExpected = asLocalDateTime( + "2007-02-04 12:33:00 +0100"); + assertEquals(combinedDotsExpected, combinedDots); + } + + @Test + public void minutesAgo() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:10 +0100"); + + LocalDateTime twoMinutesAgo = GitTimeParser.parse("2 minutes ago", + aTime); + + LocalDateTime twoMinutesAgoExpected = asLocalDateTime( + "2007-02-21 17:33:10 +0100"); + assertEquals(twoMinutesAgoExpected, twoMinutesAgo); + } + + @Test + public void minutesAgo_acrossDay() throws ParseException { + LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:10 +0100"); + + LocalDateTime minutesAgoActual = GitTimeParser.parse("40 minutes ago", + aTime); + + LocalDateTime minutesAgoExpected = asLocalDateTime( + "2007-02-20 23:55:10 +0100"); + assertEquals(minutesAgoExpected, minutesAgoActual); + } + + @Test + public void iso() throws ParseException { + String dateStr = "2007-02-21 15:35:00 +0100"; + + LocalDateTime actual = GitTimeParser.parse(dateStr); + + LocalDateTime expected = asLocalDateTime(dateStr); + assertEquals(expected, actual); + } + + @Test + public void rfc() throws ParseException { + String dateStr = "Wed, 21 Feb 2007 15:35:00 +0100"; + + LocalDateTime actual = GitTimeParser.parse(dateStr); + + LocalDateTime expected = asLocalDateTime(dateStr, + "EEE, dd MMM yyyy HH:mm:ss Z"); + assertEquals(expected, actual); + } + + @Test + public void shortFmt() throws ParseException { + assertParsing("2007-02-21", "yyyy-MM-dd"); + } + + @Test + public void shortWithDots() throws ParseException { + assertParsing("2007.02.21", "yyyy.MM.dd"); + } + + @Test + public void shortWithSlash() throws ParseException { + assertParsing("02/21/2007", "MM/dd/yyyy"); + } + + @Test + public void shortWithDotsReverse() throws ParseException { + assertParsing("21.02.2007", "dd.MM.yyyy"); + } + + @Test + public void defaultFmt() throws ParseException { + assertParsing("Wed Feb 21 15:35:00 2007 +0100", + "EEE MMM dd HH:mm:ss yyyy Z"); + } + + @Test + public void local() throws ParseException { + assertParsing("Wed Feb 21 15:35:00 2007", "EEE MMM dd HH:mm:ss yyyy"); + } + + private static void assertParsing(String dateStr, String format) + throws ParseException { + LocalDateTime actual = GitTimeParser.parse(dateStr); + + LocalDateTime expected = asLocalDateTime(dateStr, format); + assertEquals(expected, actual); + } + + private static LocalDateTime asLocalDateTime(String dateStr) { + return asLocalDateTime(dateStr, "yyyy-MM-dd HH:mm:ss Z"); + } + + private static LocalDateTime asLocalDateTime(String dateStr, + String pattern) { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern); + TemporalAccessor ta = fmt + .withZone(SystemReader.getInstance().getTimeZoneId()) + .withLocale(SystemReader.getInstance().getLocale()) + .parse(dateStr); + return ta.isSupported(ChronoField.HOUR_OF_DAY) ? LocalDateTime.from(ta) + : LocalDate.from(ta).atStartOfDay(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java index 1231aefee0..b7490f0b1f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java @@ -157,7 +157,7 @@ public class HookTest extends RepositoryTestCase { git.commit().setMessage("commit") .setHookOutputStream(new PrintStream(out)).call(); } catch (AbortedByHookException e) { - fail("unexpected hook failure"); + throw new AssertionError("unexpected hook failure", e); } assertEquals("unexpected hook output", "test pre-commit\ntest commit-msg .git/COMMIT_EDITMSG\ntest post-commit\n", diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java index cbe4eb2eb0..a3a5697ef4 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java @@ -47,18 +47,6 @@ public class HttpSupportTest { } @Test - public void testMalformedUri() throws Exception { - // Valid URL, but backslash is not allowed in a URI in the userinfo part - // per RFC 3986: https://tools.ietf.org/html/rfc3986#section-3.2.1 . - // Test that conversion to URI to call the ProxySelector does not throw - // an exception. - Proxy proxy = HttpSupport.proxyFor(new TestProxySelector(), new URL( - "http://infor\\c.jones@somehost/somewhere/someproject.git")); - assertNotNull(proxy); - assertEquals(Proxy.Type.HTTP, proxy.type()); - } - - @Test public void testCorrectUri() throws Exception { // Backslash escaped as %5C is correct. Proxy proxy = HttpSupport.proxyFor(new TestProxySelector(), new URL( diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java index e80c07509a..e4ef302359 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, Leonard Broman <leonard.broman@gmail.com> and others + * Copyright (C) 2011, 2024 Leonard Broman <leonard.broman@gmail.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -15,6 +15,7 @@ import static org.junit.Assert.fail; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; +import java.util.List; import org.eclipse.jgit.lib.Constants; import org.junit.Test; @@ -24,7 +25,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; public class RawParseUtilsTest { String commit = "tree e3a1035abd2b319bb01e57d69b0ba6cab289297e\n" + "parent 54e895b87c0768d2317a2b17062e3ad9f76a8105\n" + - "committer A U Thor <author@xample.com 1528968566 +0200\n" + + "committer A U Thor <author@example.com> 1528968566 +0200\n" + "gpgsig -----BEGIN PGP SIGNATURE-----\n" + " \n" + " wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n" + @@ -67,21 +68,24 @@ public class RawParseUtilsTest { public void testHeaderStart() { byte[] headerName = "some".getBytes(UTF_8); byte[] commitBytes = commit.getBytes(UTF_8); - assertEquals(625, RawParseUtils.headerStart(headerName, commitBytes, 0)); - assertEquals(625, RawParseUtils.headerStart(headerName, commitBytes, 4)); + assertEquals(627, + RawParseUtils.headerStart(headerName, commitBytes, 0)); + assertEquals(627, + RawParseUtils.headerStart(headerName, commitBytes, 4)); byte[] missingHeaderName = "missing".getBytes(UTF_8); assertEquals(-1, RawParseUtils.headerStart(missingHeaderName, commitBytes, 0)); byte[] fauxHeaderName = "other".getBytes(UTF_8); - assertEquals(-1, RawParseUtils.headerStart(fauxHeaderName, commitBytes, 625 + 4)); + assertEquals(-1, RawParseUtils.headerStart(fauxHeaderName, commitBytes, + 627 + 4)); } @Test public void testHeaderEnd() { byte[] commitBytes = commit.getBytes(UTF_8); - int[] expected = new int[] {45, 93, 148, 619, 637}; + int[] expected = new int[] { 45, 93, 150, 621, 639 }; int start = 0; for (int i = 0; i < expected.length; i++) { start = RawParseUtils.headerEnd(commitBytes, start); @@ -89,4 +93,44 @@ public class RawParseUtilsTest { start += 1; } } + + @Test + public void testHeaderValue() { + byte[] commitBytes = commit.getBytes(UTF_8); + List<String> headers = List.of( + "e3a1035abd2b319bb01e57d69b0ba6cab289297e", + "54e895b87c0768d2317a2b17062e3ad9f76a8105", + "A U Thor <author@example.com> 1528968566 +0200", + "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n" + + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n" + + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n" + + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n" + + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n" + + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n" + + "=TClh\n" + + "-----END PGP SIGNATURE-----", + "other header"); + int start = 0; + for (String header : headers) { + int endOfTag = RawParseUtils.next(commitBytes, start, ' '); + int end = RawParseUtils.headerEnd(commitBytes, start); + + assertEquals(header, new String( + RawParseUtils.headerValue(commitBytes, endOfTag, end), + UTF_8)); + start = end + 1; + } + } + + @Test + public void testLastHeaderEnd() { + byte[] raw = "headerA A header\nheaderB Another header".getBytes(UTF_8); + int bStart = RawParseUtils.headerStart("headerB".getBytes(UTF_8), raw, + 0); + assertEquals(25, bStart); + int bEnd = RawParseUtils.nextLfSkippingSplitLines(raw, bStart); + assertEquals(raw.length, bEnd); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java index ee3ce8d98c..6d23db81d8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java @@ -10,10 +10,13 @@ package org.eclipse.jgit.util; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; import static org.junit.Assert.assertEquals; -import java.util.Date; -import java.util.TimeZone; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; import org.eclipse.jgit.lib.PersonIdent; import org.junit.Test; @@ -22,8 +25,8 @@ public class RawParseUtils_ParsePersonIdentTest { @Test public void testParsePersonIdent_legalCases() { - final Date when = new Date(1234567890000l); - final TimeZone tz = TimeZone.getTimeZone("GMT-7"); + Instant when = Instant.ofEpochMilli(1234567890000L); + ZoneId tz = ZoneOffset.ofHours(-7); assertPersonIdent("Me <me@example.com> 1234567890 -0700", new PersonIdent("Me", "me@example.com", when, tz)); @@ -50,8 +53,8 @@ public class RawParseUtils_ParsePersonIdentTest { @Test public void testParsePersonIdent_fuzzyCases() { - final Date when = new Date(1234567890000l); - final TimeZone tz = TimeZone.getTimeZone("GMT-7"); + Instant when = Instant.ofEpochMilli(1234567890000L); + ZoneId tz = ZoneOffset.ofHours(-7); assertPersonIdent( "A U Thor <author@example.com>, C O. Miter <comiter@example.com> 1234567890 -0700", @@ -64,8 +67,8 @@ public class RawParseUtils_ParsePersonIdentTest { @Test public void testParsePersonIdent_incompleteCases() { - final Date when = new Date(1234567890000l); - final TimeZone tz = TimeZone.getTimeZone("GMT-7"); + Instant when = Instant.ofEpochMilli(1234567890000L); + ZoneId tz = ZoneOffset.ofHours(-7); assertPersonIdent("Me <> 1234567890 -0700", new PersonIdent("Me", "", when, tz)); @@ -76,26 +79,26 @@ public class RawParseUtils_ParsePersonIdentTest { assertPersonIdent(" <> 1234567890 -0700", new PersonIdent("", "", when, tz)); - assertPersonIdent("<>", new PersonIdent("", "", 0, 0)); + assertPersonIdent("<>", new PersonIdent("", "", EPOCH, UTC)); - assertPersonIdent(" <>", new PersonIdent("", "", 0, 0)); + assertPersonIdent(" <>", new PersonIdent("", "", EPOCH, UTC)); assertPersonIdent("<me@example.com>", new PersonIdent("", - "me@example.com", 0, 0)); + "me@example.com", EPOCH, UTC)); assertPersonIdent(" <me@example.com>", new PersonIdent("", - "me@example.com", 0, 0)); + "me@example.com", EPOCH, UTC)); - assertPersonIdent("Me <>", new PersonIdent("Me", "", 0, 0)); + assertPersonIdent("Me <>", new PersonIdent("Me", "", EPOCH, UTC)); assertPersonIdent("Me <me@example.com>", new PersonIdent("Me", - "me@example.com", 0, 0)); + "me@example.com", EPOCH, UTC)); assertPersonIdent("Me <me@example.com> 1234567890", new PersonIdent( - "Me", "me@example.com", 0, 0)); + "Me", "me@example.com", EPOCH, UTC)); assertPersonIdent("Me <me@example.com> 1234567890 ", new PersonIdent( - "Me", "me@example.com", 0, 0)); + "Me", "me@example.com", EPOCH, UTC)); } @Test @@ -104,6 +107,21 @@ public class RawParseUtils_ParsePersonIdentTest { assertPersonIdent("Me <me@example.com 1234567890 -0700", null); } + @Test + public void testParsePersonIdent_badTz() { + PersonIdent tooBig = RawParseUtils + .parsePersonIdent("Me <me@example.com> 1234567890 +8315"); + assertEquals(tooBig.getZoneOffset().getTotalSeconds(), 0); + + PersonIdent tooSmall = RawParseUtils + .parsePersonIdent("Me <me@example.com> 1234567890 -8315"); + assertEquals(tooSmall.getZoneOffset().getTotalSeconds(), 0); + + PersonIdent notATime = RawParseUtils + .parsePersonIdent("Me <me@example.com> 1234567890 -0370"); + assertEquals(notATime.getZoneOffset().getTotalSeconds(), 0); + } + private static void assertPersonIdent(String line, PersonIdent expected) { PersonIdent actual = RawParseUtils.parsePersonIdent(line); assertEquals(expected, actual); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java index 627417d462..0c9cb2d2b0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java @@ -268,6 +268,7 @@ public class RefMapTest { assertFalse(itr.hasNext()); } + @SuppressWarnings("ModifiedButNotUsed") @Test public void testPut_KeyMustMatchName_NoPrefix() { final Ref refA = newRef("refs/heads/A", ID_ONE); @@ -280,6 +281,7 @@ public class RefMapTest { } } + @SuppressWarnings("ModifiedButNotUsed") @Test public void testPut_KeyMustMatchName_WithPrefix() { final Ref refA = newRef("refs/heads/A", ID_ONE); @@ -421,7 +423,7 @@ public class RefMapTest { Map.Entry<String, Ref> ent_b = itr.next(); assertEquals(ent_a.hashCode(), "A".hashCode()); - assertEquals(ent_a, ent_a); + assertTrue(ent_a.equals(ent_a)); assertFalse(ent_a.equals(ent_b)); assertEquals(a.toString(), ent_a.toString()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java index 214bbca944..a927d8dbef 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java @@ -16,7 +16,7 @@ import static org.eclipse.jgit.util.RelativeDateFormatter.SECOND_IN_MILLIS; import static org.eclipse.jgit.util.RelativeDateFormatter.YEAR_IN_MILLIS; import static org.junit.Assert.assertEquals; -import java.util.Date; +import java.time.Instant; import org.eclipse.jgit.junit.MockSystemReader; import org.junit.After; @@ -37,9 +37,9 @@ public class RelativeDateFormatterTest { private static void assertFormat(long ageFromNow, long timeUnit, String expectedFormat) { - Date d = new Date(SystemReader.getInstance().getCurrentTime() - - ageFromNow * timeUnit); - String s = RelativeDateFormatter.format(d); + long millis = ageFromNow * timeUnit; + Instant aTime = SystemReader.getInstance().now().minusMillis(millis); + String s = RelativeDateFormatter.format(aTime); assertEquals(expectedFormat, s); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java index aa7247e105..9a1c710752 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -153,4 +154,41 @@ public class StringUtilsTest { () -> StringUtils.parseLongWithSuffix("8000000000000000000G", false)); } + + @Test + public void testCommonPrefix() { + assertEquals("", StringUtils.commonPrefix((String[]) null)); + assertEquals("", StringUtils.commonPrefix(new String[] {})); + assertEquals("", StringUtils.commonPrefix(new String[] { null })); + assertEquals("", StringUtils.commonPrefix(null, null)); + assertEquals("", StringUtils.commonPrefix("", "")); + assertEquals("", StringUtils.commonPrefix(null, "")); + assertEquals("", StringUtils.commonPrefix("abcd", null, null)); + assertEquals("", StringUtils.commonPrefix(null, null, "abcd")); + assertEquals("", StringUtils.commonPrefix("", "abcd")); + assertEquals("", StringUtils.commonPrefix("abcd", "efgh")); + assertEquals("abcd", StringUtils.commonPrefix("abcd")); + assertEquals("ab", StringUtils.commonPrefix("abcd", "ab")); + assertEquals("abcd", StringUtils.commonPrefix("abcd", "abcdefgh")); + assertEquals("foo bar ", + StringUtils.commonPrefix("foo bar 42", "foo bar 24")); + } + + @Test + public void testTrim() { + assertEquals("a", StringUtils.trim("a", '/')); + assertEquals("aaaa", StringUtils.trim("aaaa", '/')); + assertEquals("aaa", StringUtils.trim("/aaa", '/')); + assertEquals("aaa", StringUtils.trim("aaa/", '/')); + assertEquals("aaa", StringUtils.trim("/aaa/", '/')); + assertEquals("aa/aa", StringUtils.trim("/aa/aa/", '/')); + assertEquals("aa/aa", StringUtils.trim("aa/aa", '/')); + + assertEquals("", StringUtils.trim("", '/')); + assertEquals("", StringUtils.trim("/", '/')); + assertEquals("", StringUtils.trim("//", '/')); + assertEquals("", StringUtils.trim("///", '/')); + + assertNull(StringUtils.trim(null, '/')); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java index 5a2bd976c7..c1c72804da 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java @@ -10,7 +10,7 @@ package org.eclipse.jgit.util.http; import java.net.HttpCookie; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import org.hamcrest.Description; @@ -26,7 +26,7 @@ public final class HttpCookiesMatcher { public static Matcher<Iterable<? extends HttpCookie>> containsInOrder( Iterable<HttpCookie> expectedCookies, int allowedMaxAgeDelta) { - final List<Matcher<? super HttpCookie>> cookieMatchers = new LinkedList<>(); + final List<Matcher<? super HttpCookie>> cookieMatchers = new ArrayList<>(); for (HttpCookie cookie : expectedCookies) { cookieMatchers .add(new HttpCookieMatcher(cookie, allowedMaxAgeDelta)); @@ -92,7 +92,7 @@ public final class HttpCookiesMatcher { } @SuppressWarnings("boxing") - protected static void describeCookie(Description description, + private static void describeCookie(Description description, HttpCookie cookie) { description.appendText("HttpCookie["); description.appendText("name: ").appendValue(cookie.getName()) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java new file mode 100644 index 0000000000..ec9f96ed96 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023, SAP SE and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.util.io; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.eclipse.jgit.internal.JGitText; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ByteBufferInputStreamTest { + + private static final byte data[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; + + private ByteBuffer buf; + + private ByteBufferInputStream is; + + @Before + public void setup() { + buf = ByteBuffer.wrap(data); + is = new ByteBufferInputStream(buf); + } + + @After + public void tearDown() { + is.close(); + } + + @Test + public void testRead() throws IOException { + assertEquals(0x00, is.read()); + assertEquals(0x01, is.read()); + assertEquals(0x02, is.read()); + assertEquals(0x03, is.read()); + assertEquals(0x04, is.read()); + assertEquals(0x05, is.read()); + assertEquals(0x06, is.read()); + assertEquals(0x07, is.read()); + assertEquals(0x08, is.read()); + assertEquals(0x09, is.read()); + assertEquals(0x0A, is.read()); + assertEquals(0x0B, is.read()); + assertEquals(0x0C, is.read()); + assertEquals(0x0D, is.read()); + assertEquals(0x0E, is.read()); + assertEquals(0x0F, is.read()); + assertEquals(-1, is.read()); + } + + @Test + public void testReadMultiple() throws IOException { + byte[] x = new byte[5]; + int n = is.read(x); + assertEquals(5, n); + assertArrayEquals(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, x); + } + + @Test + public void testReadMultipleOffset() throws IOException { + byte[] x = new byte[7]; + int n = is.read(x, 4, 3); + assertEquals(3, n); + assertArrayEquals( + new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02 }, + x); + } + + @Test + public void testReadAll() throws IOException { + byte[] x = is.readAllBytes(); + assertEquals(16, x.length); + assertArrayEquals(data, x); + } + + @Test + public void testMarkReset() throws IOException { + byte[] x = new byte[5]; + int n = is.read(x); + assertEquals(11, is.available()); + assertTrue(is.markSupported()); + is.mark(is.available()); + is.reset(); + byte[] y = new byte[5]; + int m = is.read(y); + assertEquals(n, m); + assertArrayEquals(new byte[] { 0x05, 0x06, 0x07, 0x08, 0x09 }, y); + } + + @Test + public void testClosed() { + is.close(); + Exception e = assertThrows(IOException.class, () -> is.read()); + assertEquals(JGitText.get().inputStreamClosed, e.getMessage()); + } + + @Test + public void testReadNBytes() throws IOException { + byte[] x = is.readNBytes(4); + assertArrayEquals(new byte[] { 0x00, 0x01, 0x02, 0x03 }, x); + } + + @Test + public void testReadNBytesOffset() throws IOException { + byte[] x = new byte[10]; + Arrays.fill(x, (byte) 0x0F); + is.readNBytes(x, 3, 4); + assertArrayEquals(new byte[] { 0x0F, 0x0F, 0x0F, 0x00, 0x01, 0x02, 0x03, + 0x0F, 0x0F, 0x0F }, x); + } + + @Test + public void testRead0() throws IOException { + byte[] x = new byte[7]; + int n = is.read(x, 4, 0); + assertEquals(0, n); + + is.readAllBytes(); + n = is.read(x, 4, 3); + assertEquals(-1, n); + } + + @Test + public void testSkip() throws IOException { + assertEquals(15, is.skip(15)); + assertEquals(0x0F, is.read()); + assertEquals(-1, is.read()); + } + + @Test + public void testSkip0() throws IOException { + assertEquals(0, is.skip(0)); + assertEquals(0x00, is.read()); + } +} |