diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target index b33003010..76aafcce7 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd index 973f6660a..8c28b96d1 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd @@ -1,7 +1,7 @@ target "jgit-4.17" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/releases/2020-09/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target index 4e6c153b1..f48ed6e52 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd index 260db7c9d..a0cf7f71d 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd @@ -1,7 +1,7 @@ target "jgit-4.18" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/releases/2020-12/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target index 60acb0df7..d30352c15 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target @@ -1,7 +1,7 @@ - + @@ -87,11 +87,11 @@ - + - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd index 1649ff7bb..ec33a0873 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd @@ -1,8 +1,8 @@ target "jgit-4.19-staging" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" -location "https://download.eclipse.org/staging/2021-03/" { +location "https://download.eclipse.org/releases/2021-03/" { org.eclipse.osgi lazy } diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target index 0660134ba..e20ef5b59 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd index 82f1a3b55..ccca715b1 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd @@ -1,7 +1,7 @@ target "jgit-4.20" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/releases/2021-06/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target index 605c07d52..b98b23b0b 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd index 39c0303a8..2652598df 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd @@ -1,7 +1,7 @@ target "jgit-4.21" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/releases/2021-09/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target index 7f26ca340..718660985 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd index 3e5e98ab7..bc87048da 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd @@ -1,7 +1,7 @@ target "jgit-4.22" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/releases/2021-12/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target index c401ad43b..670d61d5d 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd index f2d01e92b..2d15b9ff5 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd @@ -1,7 +1,7 @@ target "jgit-4.23" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/releases/2022-03/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target index 8592bc6c1..4d78a592e 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd index 48b6f7d38..0b7176a30 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd @@ -1,7 +1,7 @@ target "jgit-4.24" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/releases/2022-06/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target index 71c26ae6c..568da28e5 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target @@ -1,7 +1,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd index 81b8abc12..0005893d6 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd @@ -1,7 +1,7 @@ target "jgit-4.25" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/S20220726152247.tpd" +include "orbit/R20220830213456-2022-09.tpd" location "https://download.eclipse.org/staging/2022-09/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/S20220726152247.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220830213456-2022-09.tpd similarity index 98% rename from org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/S20220726152247.tpd rename to org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220830213456-2022-09.tpd index 8e62642e9..8db1018ff 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/S20220726152247.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220830213456-2022-09.tpd @@ -1,7 +1,7 @@ -target "S20220726152247" with source configurePhase +target "R20220830213456-2022-09" with source configurePhase // see https://download.eclipse.org/tools/orbit/downloads/ -location "https://download.eclipse.org/tools/orbit/downloads/drops/S20220726152247/repository" { +location "https://download.eclipse.org/tools/orbit/downloads/drops/R20220830213456/repository" { com.google.gson [2.8.9.v20220111-1409,2.8.9.v20220111-1409] com.google.gson.source [2.8.9.v20220111-1409,2.8.9.v20220111-1409] com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902] diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java index 584d14963..40764b739 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java @@ -525,9 +525,9 @@ public void testNonASCIIDel() throws Exception { @Test public void testRenameNoHunks() throws Exception { ApplyResult result = init("RenameNoHunks", true, true); - assertEquals(1, result.getUpdatedFiles().size()); - assertEquals(new File(db.getWorkTree(), "RenameNoHunks"), result.getUpdatedFiles() - .get(0)); + assertEquals(2, result.getUpdatedFiles().size()); + assertTrue(result.getUpdatedFiles().contains(new File(db.getWorkTree(), "RenameNoHunks"))); + assertTrue(result.getUpdatedFiles().contains(new File(db.getWorkTree(), "nested/subdir/Renamed"))); checkFile(new File(db.getWorkTree(), "nested/subdir/Renamed"), b.getString(0, b.size(), false)); } @@ -535,9 +535,9 @@ public void testRenameNoHunks() throws Exception { @Test public void testRenameWithHunks() throws Exception { ApplyResult result = init("RenameWithHunks", true, true); - assertEquals(1, result.getUpdatedFiles().size()); - assertEquals(new File(db.getWorkTree(), "RenameWithHunks"), result.getUpdatedFiles() - .get(0)); + assertEquals(2, result.getUpdatedFiles().size()); + assertTrue(result.getUpdatedFiles().contains(new File(db.getWorkTree(), "RenameWithHunks"))); + assertTrue(result.getUpdatedFiles().contains(new File(db.getWorkTree(), "nested/subdir/Renamed"))); checkFile(new File(db.getWorkTree(), "nested/subdir/Renamed"), b.getString(0, b.size(), false)); } @@ -546,7 +546,7 @@ public void testRenameWithHunks() throws Exception { public void testCopyWithHunks() throws Exception { ApplyResult result = init("CopyWithHunks", true, true); assertEquals(1, result.getUpdatedFiles().size()); - assertEquals(new File(db.getWorkTree(), "CopyWithHunks"), result.getUpdatedFiles() + assertEquals(new File(db.getWorkTree(), "CopyResult"), result.getUpdatedFiles() .get(0)); checkFile(new File(db.getWorkTree(), "CopyResult"), b.getString(0, b.size(), false)); 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 f47f44737..b175ead8e 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 @@ -13,56 +13,330 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.Iterator; + import org.eclipse.jgit.api.Git; import org.eclipse.jgit.blame.BlameGenerator; import org.eclipse.jgit.blame.BlameResult; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.revwalk.FilteredRevCommit; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.junit.Test; /** Unit tests of {@link BlameGenerator}. */ public class BlameGeneratorTest extends RepositoryTestCase { + + public static final String OTHER_FILE = "other_file.txt"; + + public static final String INTERESTING_FILE = "interesting_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(); + public void testSingleBlame() throws Exception { + + /** + *
+		 * (ts) 	OTHER_FILE			INTERESTING_FILE
+		 * 1 		a
+		 * 2	 	a, b
+		 * 3							1, 2				c1 <--
+		 * 4	 	a, b, c										 |
+		 * 5							1, 2, 3				c2---
+		 * 
+ */ + try (Git git = new Git(db); + RevWalk revWalk = new RevWalk(git.getRepository())) { + writeTrashFile(OTHER_FILE, join("a")); + git.add().addFilepattern(OTHER_FILE).call(); + git.commit().setMessage("create file").call(); + + writeTrashFile(OTHER_FILE, join("a", "b")); + git.add().addFilepattern(OTHER_FILE).call(); + git.commit().setMessage("amend file").call(); + + writeTrashFile(INTERESTING_FILE, join("1", "2")); + git.add().addFilepattern(INTERESTING_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(); - RevCommit c2 = git.commit().setMessage("create file").call(); + writeTrashFile(OTHER_FILE, join("a", "b", "c")); + git.add().addFilepattern(OTHER_FILE).call(); + git.commit().setMessage("amend file").call(); - try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) { - generator.push(null, db.resolve(Constants.HEAD)); + writeTrashFile(INTERESTING_FILE, join("1", "2", "3")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit c2 = git.commit().setMessage("amend file").call(); + + RevCommit filteredC1 = new FilteredRevCommit(c1); + RevCommit filteredC2 = new FilteredRevCommit(c2, filteredC1); + + revWalk.parseHeaders(filteredC2); + + try (BlameGenerator generator = new BlameGenerator(db, + INTERESTING_FILE)) { + generator.push(filteredC2); assertEquals(3, generator.getResultContents().size()); assertTrue(generator.next()); assertEquals(c2, generator.getSourceCommit()); assertEquals(1, generator.getRegionLength()); - assertEquals(0, generator.getResultStart()); - assertEquals(1, generator.getResultEnd()); - assertEquals(0, generator.getSourceStart()); - assertEquals(1, generator.getSourceEnd()); - assertEquals("file.txt", generator.getSourcePath()); + assertEquals(2, generator.getResultStart()); + assertEquals(3, generator.getResultEnd()); + assertEquals(2, generator.getSourceStart()); + assertEquals(3, generator.getSourceEnd()); + assertEquals(INTERESTING_FILE, generator.getSourcePath()); assertTrue(generator.next()); assertEquals(c1, generator.getSourceCommit()); assertEquals(2, generator.getRegionLength()); - assertEquals(1, generator.getResultStart()); - assertEquals(3, generator.getResultEnd()); + assertEquals(0, generator.getResultStart()); + assertEquals(2, generator.getResultEnd()); assertEquals(0, generator.getSourceStart()); assertEquals(2, generator.getSourceEnd()); - assertEquals("file.txt", generator.getSourcePath()); + assertEquals(INTERESTING_FILE, generator.getSourcePath()); assertFalse(generator.next()); } } } + @Test + public void testMergeSingleBlame() throws Exception { + try (Git git = new Git(db); + RevWalk revWalk = new RevWalk(git.getRepository())) { + + /** + * + * + *
+			 *  refs/heads/master
+			 *      A
+			 *     / \       		 refs/heads/side
+			 *    /   ---------------->  side
+			 *   /                        |
+			 *  merge <-------------------
+			 * 
+ */ + + writeTrashFile(INTERESTING_FILE, join("1", "2")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit c1 = git.commit().setMessage("create file").call(); + + createBranch(c1, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + writeTrashFile(INTERESTING_FILE, join("1", "2", "3", "4")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit sideCommit = git.commit() + .setMessage("amend file in another branch").call(); + + checkoutBranch("refs/heads/master"); + git.merge().setMessage("merge").include(sideCommit) + .setStrategy(MergeStrategy.RESOLVE).call(); + + Iterator it = git.log().call().iterator(); + RevCommit mergeCommit = it.next(); + + RevCommit filteredC1 = new FilteredRevCommit(c1); + RevCommit filteredSide = new FilteredRevCommit(sideCommit, + filteredC1); + RevCommit filteredMerge = new FilteredRevCommit(mergeCommit, + filteredSide, filteredC1); + + revWalk.parseHeaders(filteredMerge); + + try (BlameGenerator generator = new BlameGenerator(db, + INTERESTING_FILE)) { + generator.push(filteredMerge); + assertEquals(4, generator.getResultContents().size()); + + assertTrue(generator.next()); + assertEquals(mergeCommit, generator.getSourceCommit()); + assertEquals(2, generator.getRegionLength()); + assertEquals(2, generator.getResultStart()); + assertEquals(4, generator.getResultEnd()); + assertEquals(2, generator.getSourceStart()); + assertEquals(4, generator.getSourceEnd()); + assertEquals(INTERESTING_FILE, generator.getSourcePath()); + + assertTrue(generator.next()); + assertEquals(filteredC1, generator.getSourceCommit()); + assertEquals(2, generator.getRegionLength()); + assertEquals(0, generator.getResultStart()); + assertEquals(2, generator.getResultEnd()); + assertEquals(0, generator.getSourceStart()); + assertEquals(2, generator.getSourceEnd()); + assertEquals(INTERESTING_FILE, generator.getSourcePath()); + + assertFalse(generator.next()); + } + } + } + + @Test + public void testMergeBlame() throws Exception { + try (Git git = new Git(db); + RevWalk revWalk = new RevWalk(git.getRepository())) { + + /** + * + * + *
+			 *  refs/heads/master
+			 *      A
+			 *     / \       		 refs/heads/side
+			 *    B   ---------------->  side
+			 *   /                        |
+			 *  merge <-------------------
+			 * 
+ */ + writeTrashFile(INTERESTING_FILE, join("1", "2")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit c1 = git.commit().setMessage("create file").call(); + + createBranch(c1, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + writeTrashFile(INTERESTING_FILE, join("1", "2", "3")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit sideCommit = git.commit().setMessage("amend file").call(); + + checkoutBranch("refs/heads/master"); + writeTrashFile(INTERESTING_FILE, join("1", "2", "4")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit c2 = git.commit().setMessage("delete and amend file") + .call(); + + git.merge().setMessage("merge").include(sideCommit) + .setStrategy(MergeStrategy.RESOLVE).call(); + writeTrashFile(INTERESTING_FILE, join("1", "2", "3", "4")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit mergeCommit = git.commit().setMessage("merge commit") + .call(); + + RevCommit filteredC1 = new FilteredRevCommit(c1); + RevCommit filteredSide = new FilteredRevCommit(sideCommit, + filteredC1); + RevCommit filteredC2 = new FilteredRevCommit(c2, filteredC1); + + RevCommit filteredMerge = new FilteredRevCommit(mergeCommit, + filteredSide, filteredC2); + + revWalk.parseHeaders(filteredMerge); + + try (BlameGenerator generator = new BlameGenerator(db, + INTERESTING_FILE)) { + generator.push(filteredMerge); + assertEquals(4, generator.getResultContents().size()); + + assertTrue(generator.next()); + assertEquals(filteredC2, generator.getSourceCommit()); + assertEquals(1, generator.getRegionLength()); + assertEquals(3, generator.getResultStart()); + assertEquals(4, generator.getResultEnd()); + assertEquals(2, generator.getSourceStart()); + assertEquals(3, generator.getSourceEnd()); + assertEquals(INTERESTING_FILE, generator.getSourcePath()); + + assertTrue(generator.next()); + assertEquals(filteredSide, generator.getSourceCommit()); + assertEquals(1, generator.getRegionLength()); + assertEquals(2, generator.getResultStart()); + assertEquals(3, generator.getResultEnd()); + assertEquals(2, generator.getSourceStart()); + assertEquals(3, generator.getSourceEnd()); + assertEquals(INTERESTING_FILE, generator.getSourcePath()); + + assertTrue(generator.next()); + assertEquals(filteredC1, generator.getSourceCommit()); + assertEquals(2, generator.getRegionLength()); + assertEquals(0, generator.getResultStart()); + assertEquals(2, generator.getResultEnd()); + assertEquals(0, generator.getSourceStart()); + assertEquals(2, generator.getSourceEnd()); + assertEquals(INTERESTING_FILE, generator.getSourcePath()); + + assertFalse(generator.next()); + } + } + } + + @Test + public void testSingleBlame_compareWithWalk() throws Exception { + /** + *
+		 * (ts) 	OTHER_FILE			INTERESTING_FILE
+		 * 1 		a
+		 * 2	 	a, b
+		 * 3							1, 2				c1 <--
+		 * 4	 	a, b, c										 |
+		 * 6							3, 1, 2				c2---
+		 * 
+ */ + try (Git git = new Git(db); + RevWalk revWalk = new RevWalk(git.getRepository())) { + writeTrashFile(OTHER_FILE, join("a")); + git.add().addFilepattern(OTHER_FILE).call(); + git.commit().setMessage("create file").call(); + + writeTrashFile(OTHER_FILE, join("a", "b")); + git.add().addFilepattern(OTHER_FILE).call(); + git.commit().setMessage("amend file").call(); + + writeTrashFile(INTERESTING_FILE, join("1", "2")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit c1 = git.commit().setMessage("create file").call(); + + writeTrashFile(OTHER_FILE, join("a", "b", "c")); + git.add().addFilepattern(OTHER_FILE).call(); + git.commit().setMessage("amend file").call(); + + writeTrashFile(INTERESTING_FILE, join("3", "1", "2")); + git.add().addFilepattern(INTERESTING_FILE).call(); + RevCommit c2 = git.commit().setMessage("prepend").call(); + + RevCommit filteredC1 = new FilteredRevCommit(c1); + RevCommit filteredC2 = new FilteredRevCommit(c2, filteredC1); + + revWalk.parseHeaders(filteredC2); + + try (BlameGenerator g1 = new BlameGenerator(db, INTERESTING_FILE); + BlameGenerator g2 = new BlameGenerator(db, + INTERESTING_FILE)) { + g1.push(null, c2); + g2.push(null, filteredC2); + + assertEquals(g1.getResultContents().size(), + g2.getResultContents().size()); // 3 + + assertTrue(g1.next()); + assertTrue(g2.next()); + + assertEquals(g1.getSourceCommit(), g2.getSourceCommit()); // c2 + assertEquals(INTERESTING_FILE, g1.getSourcePath()); + assertEquals(g1.getRegionLength(), g2.getRegionLength()); // 1 + assertEquals(g1.getResultStart(), g2.getResultStart()); // 0 + assertEquals(g1.getResultEnd(), g2.getResultEnd()); // 1 + assertEquals(g1.getSourceStart(), g2.getSourceStart()); // 0 + assertEquals(g1.getSourceEnd(), g2.getSourceEnd()); // 1 + assertEquals(g1.getSourcePath(), g2.getSourcePath()); // INTERESTING_FILE + + assertTrue(g1.next()); + assertTrue(g2.next()); + + assertEquals(g1.getSourceCommit(), g2.getSourceCommit()); // c1 + assertEquals(g1.getRegionLength(), g2.getRegionLength()); // 2 + assertEquals(g1.getResultStart(), g2.getResultStart()); // 1 + assertEquals(g1.getResultEnd(), g2.getResultEnd()); // 3 + assertEquals(g1.getSourceStart(), g2.getSourceStart()); // 0 + assertEquals(g1.getSourceEnd(), g2.getSourceEnd()); // 2 + assertEquals(g1.getSourcePath(), g2.getSourcePath()); // INTERESTING_FILE + + assertFalse(g1.next()); + assertFalse(g2.next()); + } + } + } + @Test public void testRenamedBoundLineDelete() throws Exception { try (Git git = new Git(db)) { @@ -87,7 +361,8 @@ public void testRenamedBoundLineDelete() throws Exception { 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 +388,8 @@ public void testRenamedBoundLineDelete() throws Exception { } // 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 +412,22 @@ public void testLinesAllDeletedShortenedWalk() throws Exception { 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(INTERESTING_FILE, join(content1)); + git.add().addFilepattern(INTERESTING_FILE).call(); git.commit().setMessage("create file").call(); String[] content2 = new String[] { "" }; - writeTrashFile("file.txt", join(content2)); - git.add().addFilepattern("file.txt").call(); + writeTrashFile(INTERESTING_FILE, join(content2)); + git.add().addFilepattern(INTERESTING_FILE).call(); git.commit().setMessage("create file").call(); - writeTrashFile("file.txt", join(content1)); - git.add().addFilepattern("file.txt").call(); + writeTrashFile(INTERESTING_FILE, join(content1)); + git.add().addFilepattern(INTERESTING_FILE).call(); RevCommit c3 = git.commit().setMessage("create file").call(); - try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) { + try (BlameGenerator generator = new BlameGenerator(db, + INTERESTING_FILE)) { generator.push(null, db.resolve(Constants.HEAD)); assertEquals(3, generator.getResultContents().size()); 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 3e6d13a67..ca6f2e105 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 @@ -13,6 +13,7 @@ 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; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -32,6 +33,7 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.junit.JGitTestUtil; @@ -1337,6 +1339,28 @@ public void testTwoPathUseTheSameName() throws Exception { } } + @Test + public void testInvalidPath() throws Exception { + Repository remoteDb = createBareRepository(); + Repository tempDb = createWorkRepository(); + + StringBuilder xmlContent = new StringBuilder(); + xmlContent.append("\n") + .append("") + .append("") + .append("") + .append("").append(""); + JGitTestUtil.writeTrashFile(tempDb, "manifest.xml", + xmlContent.toString()); + + RepoCommand command = new RepoCommand(remoteDb); + command.setPath( + tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml") + .setURI(rootUri).setRecommendShallow(true); + assertThrows(ManifestErrorException.class, () -> command.call()); + } + private void resolveRelativeUris() { // Find the longest common prefix ends with "/" as rootUri. defaultUri = defaultDb.getDirectory().toURI().toString(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FilteredRevCommitTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FilteredRevCommitTest.java new file mode 100644 index 000000000..49ce47ef4 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FilteredRevCommitTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2022, 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 + * + * @since 6.3 + */ +package org.eclipse.jgit.revwalk; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; + +import java.util.Arrays; + +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.junit.Before; +import org.junit.Test; + +public class FilteredRevCommitTest { + private TestRepository tr; + + private RevWalk rw; + + @Before + public void setUp() throws Exception { + tr = new TestRepository<>( + new InMemoryRepository(new DfsRepositoryDescription("test"))); + rw = tr.getRevWalk(); + } + + @Test + public void testParseHeaders_noParent() throws Exception { + RevCommit root = tr.commit().add("todelete", "to be deleted").create(); + RevCommit orig = tr.commit().parent(root).rm("todelete") + .add("foo", "foo contents").add("bar", "bar contents") + .add("dir/baz", "baz contents").create(); + FilteredRevCommit filteredRevCommit = new FilteredRevCommit(orig); + filteredRevCommit.parseHeaders(rw); + tr.branch("master").update(filteredRevCommit); + assertEquals("foo contents", blobAsString(filteredRevCommit, "foo")); + assertEquals("bar contents", blobAsString(filteredRevCommit, "bar")); + assertEquals("baz contents", + blobAsString(filteredRevCommit, "dir/baz")); + } + + @Test + public void testParents() throws Exception { + RevCommit commit1 = tr.commit().add("foo", "foo contents\n").create(); + RevCommit commit2 = tr.commit().parent(commit1) + .message("original message").add("bar", "bar contents") + .create(); + RevCommit commit3 = tr.commit().parent(commit2).message("commit3") + .add("foo", "foo contents\n new line\n").create(); + + FilteredRevCommit filteredCommitHead = new FilteredRevCommit(commit3, + commit1); + + assertEquals(commit1, Arrays.stream(filteredCommitHead.getParents()) + .findFirst().get()); + assertEquals("commit3", filteredCommitHead.getFullMessage()); + assertEquals("foo contents\n new line\n", + blobAsString(filteredCommitHead, "foo")); + assertEquals(filteredCommitHead.getTree(), commit3.getTree()); + + } + + @Test + public void testFlag() throws Exception { + RevCommit root = tr.commit().add("todelete", "to be deleted").create(); + RevCommit orig = tr.commit().parent(root).rm("todelete") + .add("foo", "foo contents").add("bar", "bar contents") + .add("dir/baz", "baz contents").create(); + + FilteredRevCommit filteredRevCommit = new FilteredRevCommit(orig, root); + assertEquals(RevObject.PARSED, orig.flags); + assertEquals(RevObject.PARSED, filteredRevCommit.flags); + } + + @Test + public void testCommitState() throws Exception { + RevCommit root = tr.commit().add("todelete", "to be deleted").create(); + RevCommit orig = tr.commit().parent(root).rm("todelete") + .add("foo", "foo contents").add("bar", "bar contents") + .add("dir/baz", "baz contents").create(); + + FilteredRevCommit filteredRevCommit = new FilteredRevCommit(orig, root); + assertEquals(filteredRevCommit.getParentCount(), 1); + assertSame(filteredRevCommit.getRawBuffer(), orig.getRawBuffer()); + assertSame(filteredRevCommit.getTree(), orig.getTree()); + assertEquals(filteredRevCommit.getFullMessage(), orig.getFullMessage()); + assertEquals(filteredRevCommit.commitTime, orig.commitTime); + assertSame(filteredRevCommit.parents, RevCommit.NO_PARENTS); + } + + @Test + public void testParseCommit_withParents_parsesRealParents() + throws Exception { + RevCommit commit1 = tr.commit().add("foo", "foo contents\n").create(); + RevCommit commit2 = tr.commit().parent(commit1) + .message("original message").add("bar", "bar contents") + .create(); + RevCommit commit3 = tr.commit().parent(commit2).message("commit3") + .add("foo", "foo contents\n new line\n").create(); + + FilteredRevCommit filteredCommitHead = new FilteredRevCommit(commit3, + commit1); + + RevCommit parsedCommit = rw.parseCommit(filteredCommitHead.getId()); + assertEquals(filteredCommitHead.getId(), commit3.getId()); + // This is an intended behavior as revWalk#parseCommit doesn't parse + // through the overridden parents rather uses the real parents. + assertNotEquals( + Arrays.stream(parsedCommit.getParents()).findFirst().get(), + Arrays.stream(filteredCommitHead.getParents()).findFirst() + .get()); + } + + private String blobAsString(AnyObjectId treeish, String path) + throws Exception { + RevObject obj = tr.get(rw.parseTree(treeish), path); + assertSame(RevBlob.class, obj.getClass()); + ObjectLoader loader = rw.getObjectReader().open(obj); + return new String(loader.getCachedBytes(), UTF_8); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FilteredRevWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FilteredRevWalkTest.java new file mode 100644 index 000000000..b1f8c0c0e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FilteredRevWalkTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022, 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.revwalk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.junit.Before; +import org.junit.Test; + +public class FilteredRevWalkTest extends RevWalkTestCase { + private TestRepository repository; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + repository = new TestRepository<>(db); + } + + @Test + public void testWalk() throws Exception { + writeTrashFile("a.txt", "content"); + repository.git().add().addFilepattern("a.txt").call(); + RevCommit c1 = repository.git().commit().setMessage("first commit") + .call(); + + writeTrashFile("b.txt", "new file added"); + repository.git().add().addFilepattern("b.txt").call(); + repository.git().commit().setMessage("second commit").call(); + + writeTrashFile("a.txt", "content added"); + repository.git().add().addFilepattern("a.txt").call(); + RevCommit c3 = repository.git().commit().setMessage("third commit") + .call(); + + RevWalk revWalk = repository.getRevWalk(); + FilteredRevCommit filteredRevCommit = new FilteredRevCommit(c3, c1); + + revWalk.markStart(filteredRevCommit); + assertEquals(c3, revWalk.next()); + assertEquals(c1, revWalk.next()); + } + + @Test + public void testParseBody() throws Exception { + writeTrashFile("a.txt", "content"); + repository.git().add().addFilepattern("a.txt").call(); + RevCommit c1 = repository.git().commit().setMessage("first commit") + .call(); + + writeTrashFile("b.txt", "new file added"); + repository.git().add().addFilepattern("b.txt").call(); + repository.git().commit().setMessage("second commit").call(); + + writeTrashFile("a.txt", "content added"); + repository.git().add().addFilepattern("a.txt").call(); + RevCommit c3 = repository.git().commit().setMessage("third commit") + .call(); + + FilteredRevCommit filteredRevCommit = new FilteredRevCommit(c3, c1); + filteredRevCommit.disposeBody(); + + RevWalk revWalk = repository.getRevWalk(); + + revWalk.parseBody(filteredRevCommit); + assertEquals(filteredRevCommit.getFullMessage(), c3.getFullMessage()); + assertEquals(filteredRevCommit.getShortMessage(), c3.getShortMessage()); + assertEquals(filteredRevCommit.commitTime, c3.commitTime); + assertSame(filteredRevCommit.getTree(), c3.getTree()); + assertSame(filteredRevCommit.parents, RevCommit.NO_PARENTS); + + } + + /** + * Test that the uninteresting flag is carried over correctly. Every commit + * should have the uninteresting flag resulting in a RevWalk returning no + * commit. + * + * @throws Exception + */ + @Test + public void testRevWalkCarryUninteresting() throws Exception { + writeTrashFile("a.txt", "content"); + repository.git().add().addFilepattern("a.txt").call(); + RevCommit c1 = repository.git().commit().setMessage("first commit") + .call(); + + writeTrashFile("b.txt", "new file added"); + repository.git().add().addFilepattern("b.txt").call(); + RevCommit c2 = repository.git().commit().setMessage("second commit") + .call(); + + writeTrashFile("a.txt", "content added"); + repository.git().add().addFilepattern("a.txt").call(); + RevCommit c3 = repository.git().commit().setMessage("third commit") + .call(); + + RevWalk revWalk = repository.getRevWalk(); + FilteredRevCommit filteredCommit1 = new FilteredRevCommit(c1); + FilteredRevCommit filteredCommit2 = new FilteredRevCommit(c2, + filteredCommit1); + FilteredRevCommit filteredCommit3 = new FilteredRevCommit(c3, + filteredCommit2); + + revWalk.markStart(filteredCommit2); + markUninteresting(filteredCommit3); + assertNull("Found an unexpected commit", rw.next()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java index c8256b89c..146d16953 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java @@ -12,6 +12,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.filter.MessageRevFilter; @@ -423,9 +424,41 @@ public void testWithTopoSortAndTreeFilter() throws Exception { rw.sort(RevSort.TOPO, true); rw.setTreeFilter(PathFilterGroup.createFromStrings("0")); markStart(d); - assertCommit(d, rw.next()); - assertCommit(c, rw.next()); - assertCommit(b, rw.next()); + + assertEquals(d, rw.next()); + assertEquals(c, rw.next()); + assertEquals(b, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testWithTopoSortAndTreeFilter_shouldUseFilteredRevCommits() + throws Exception { + RevCommit a = commit(); + RevCommit b = commit(tree(file("0", blob("b"))), a); + RevCommit c = commit(tree(file("0", blob("c"))), b, a); + RevCommit d = commit(tree(file("0", blob("d"))), c); + + rw.reset(); + rw.setFirstParent(true); + rw.sort(RevSort.TOPO, true); + rw.setTreeFilter(PathFilterGroup.createFromStrings("0")); + markStart(d); + + RevCommit x = rw.next(); + assertTrue(x instanceof FilteredRevCommit); + assertEquals(1, x.getParentCount()); + assertEquals(c, x.getParent(0)); + + RevCommit y = rw.next(); + assertTrue(y instanceof FilteredRevCommit); + assertEquals(1, y.getParentCount()); + assertEquals(b, y.getParent(0)); + + RevCommit z = rw.next(); + assertTrue(z instanceof FilteredRevCommit); + assertEquals(0, z.getParentCount()); + assertNull(rw.next()); } @@ -441,8 +474,8 @@ public void testWithTopoSortAndTreeFilter2() throws Exception { rw.sort(RevSort.TOPO, true); rw.setTreeFilter(PathFilterGroup.createFromStrings("0")); markStart(d); - assertCommit(d, rw.next()); - assertCommit(c, rw.next()); + assertEquals(d, rw.next()); + assertEquals(c, rw.next()); assertNull(rw.next()); } @@ -458,9 +491,9 @@ public void testWithTopoNonIntermixSortAndTreeFilter() throws Exception { rw.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true); rw.setTreeFilter(PathFilterGroup.createFromStrings("0")); markStart(d); - assertCommit(d, rw.next()); - assertCommit(c, rw.next()); - assertCommit(b, rw.next()); + assertEquals(d, rw.next()); + assertEquals(c, rw.next()); + assertEquals(b, rw.next()); assertNull(rw.next()); } @@ -476,8 +509,8 @@ public void testWithTopoNonIntermixSortAndTreeFilter2() throws Exception { rw.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true); rw.setTreeFilter(PathFilterGroup.createFromStrings("0")); markStart(d); - assertCommit(d, rw.next()); - assertCommit(c, rw.next()); + assertEquals(d, rw.next()); + assertEquals(c, rw.next()); assertNull(rw.next()); } } 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 c62136e64..20478ef70 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 @@ -9,6 +9,7 @@ */ package org.eclipse.jgit.revwalk; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.util.ArrayList; @@ -55,7 +56,7 @@ public void testNoRename() throws Exception { final RevCommit a = commit(tree(file("0", blob("0")))); follow("0"); markStart(a); - assertCommit(a, rw.next()); + assertEquals(a, rw.next()); assertNull(rw.next()); assertNoRenames(); @@ -72,8 +73,8 @@ public void testSingleRename() throws Exception { follow("b"); markStart(renameCommit); - assertCommit(renameCommit, rw.next()); - assertCommit(a, rw.next()); + assertEquals(renameCommit, rw.next()); + assertEquals(a, rw.next()); assertNull(rw.next()); assertRenames("a->b"); @@ -101,10 +102,10 @@ public void testMultiRename() throws Exception { follow("a"); markStart(renameCommit3); - assertCommit(renameCommit3, rw.next()); - assertCommit(renameCommit2, rw.next()); - assertCommit(renameCommit1, rw.next()); - assertCommit(a, rw.next()); + assertEquals(renameCommit3, rw.next()); + assertEquals(renameCommit2, rw.next()); + assertEquals(renameCommit1, rw.next()); + assertEquals(a, rw.next()); assertNull(rw.next()); assertRenames("c->a", "b->c", "a->b"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkPathFilter1Test.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkPathFilter1Test.java index 5cce11aa1..d933a6fc7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkPathFilter1Test.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkPathFilter1Test.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.revwalk; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import java.util.Collections; @@ -23,8 +24,8 @@ public class RevWalkPathFilter1Test extends RevWalkTestCase { protected void filter(String path) { - rw.setTreeFilter(AndTreeFilter.create(PathFilterGroup - .createFromStrings(Collections.singleton(path)), + rw.setTreeFilter(AndTreeFilter.create( + PathFilterGroup.createFromStrings(Collections.singleton(path)), TreeFilter.ANY_DIFF)); } @@ -49,7 +50,7 @@ public void testSimple1() throws Exception { final RevCommit a = commit(tree(file("0", blob("0")))); filter("0"); markStart(a); - assertCommit(a, rw.next()); + assertEquals(a, rw.next()); assertNull(rw.next()); } @@ -72,10 +73,10 @@ public void testEdits_MatchAll() throws Exception { final RevCommit d = commit(tree(file("0", blob("d"))), c); filter("0"); markStart(d); - assertCommit(d, rw.next()); - assertCommit(c, rw.next()); - assertCommit(b, rw.next()); - assertCommit(a, rw.next()); + assertEquals(d, rw.next()); + assertEquals(c, rw.next()); + assertEquals(b, rw.next()); + assertEquals(a, rw.next()); assertNull(rw.next()); } @@ -87,11 +88,11 @@ public void testStringOfPearls_FilePath1() throws Exception { filter("d/f"); markStart(c); - assertCommit(c, rw.next()); + assertEquals(c, rw.next()); assertEquals(1, c.getParentCount()); - assertCommit(a, c.getParent(0)); // b was skipped + assertEquals(a, c.getParent(0)); // b was skipped - assertCommit(a, rw.next()); + assertEquals(a, rw.next()); assertEquals(0, a.getParentCount()); assertNull(rw.next()); } @@ -106,11 +107,11 @@ public void testStringOfPearls_FilePath1_NoParentRewriting() markStart(c); rw.setRewriteParents(false); - assertCommit(c, rw.next()); + assertEquals(c, rw.next()); assertEquals(1, c.getParentCount()); - assertCommit(b, c.getParent(0)); + assertEquals(b, c.getParent(0)); - assertCommit(a, rw.next()); // b was skipped + assertEquals(a, rw.next()); // b was skipped assertEquals(0, a.getParentCount()); assertNull(rw.next()); } @@ -125,18 +126,18 @@ public void testStringOfPearls_FilePath2() throws Exception { markStart(d); // d was skipped - assertCommit(c, rw.next()); + assertEquals(c, rw.next()); assertEquals(1, c.getParentCount()); - assertCommit(a, c.getParent(0)); // b was skipped + assertEquals(a, c.getParent(0)); // b was skipped - assertCommit(a, rw.next()); + assertEquals(a, rw.next()); assertEquals(0, a.getParentCount()); assertNull(rw.next()); } @Test public void testStringOfPearls_FilePath2_NoParentRewriting() - throws Exception { + throws Exception { final RevCommit a = commit(tree(file("d/f", blob("a")))); final RevCommit b = commit(tree(file("d/f", blob("a"))), a); final RevCommit c = commit(tree(file("d/f", blob("b"))), b); @@ -146,12 +147,12 @@ public void testStringOfPearls_FilePath2_NoParentRewriting() rw.setRewriteParents(false); // d was skipped - assertCommit(c, rw.next()); + assertEquals(c, rw.next()); assertEquals(1, c.getParentCount()); - assertCommit(b, c.getParent(0)); + assertEquals(b, c.getParent(0)); // b was skipped - assertCommit(a, rw.next()); + assertEquals(a, rw.next()); assertEquals(0, a.getParentCount()); assertNull(rw.next()); } @@ -166,11 +167,11 @@ public void testStringOfPearls_DirPath2() throws Exception { markStart(d); // d was skipped - assertCommit(c, rw.next()); + assertEquals(c, rw.next()); assertEquals(1, c.getParentCount()); - assertCommit(a, c.getParent(0)); // b was skipped + assertEquals(a, c.getParent(0)); // b was skipped - assertCommit(a, rw.next()); + assertEquals(a, rw.next()); assertEquals(0, a.getParentCount()); assertNull(rw.next()); } @@ -211,15 +212,15 @@ public void testStringOfPearls_FilePath3() throws Exception { filter("d/f"); markStart(i); - assertCommit(i, rw.next()); + assertEquals(i, rw.next()); assertEquals(1, i.getParentCount()); - assertCommit(c, i.getParent(0)); // h..d was skipped + assertEquals(c, i.getParent(0)); // h..d was skipped - assertCommit(c, rw.next()); + assertEquals(c, rw.next()); assertEquals(1, c.getParentCount()); - assertCommit(a, c.getParent(0)); // b was skipped + assertEquals(a, c.getParent(0)); // b was skipped - assertCommit(a, rw.next()); + assertEquals(a, rw.next()); assertEquals(0, a.getParentCount()); assertNull(rw.next()); } @@ -273,4 +274,49 @@ public void testStopWhenPathDisappears() throws Exception { assertCommit(b, rw.next()); assertCommit(a, rw.next()); } + + @Test + public void testCommitHeaders_rewrittenParents() throws Exception { + final RevCommit a = commit(tree(file("d/f", blob("a")))); + final RevCommit b = commit(tree(file("d/f", blob("a"))), a); + final RevCommit c = commit(tree(file("d/f", blob("b"))), b); + filter("d/f"); + markStart(c); + + RevCommit cBar = rw.next(); + assertNotNull(cBar.getShortMessage()); + assertEquals(cBar.getCommitTime(), c.getCommitTime()); + + RevCommit aBar = rw.next(); + assertNotNull(aBar.getShortMessage()); + assertEquals(aBar.getCommitTime(), a.getCommitTime()); + + assertNull(rw.next()); + } + + @Test + public void testFlags_rewrittenParents() throws Exception { + final RevCommit a = commit(tree(file("d/f", blob("a")))); + final RevCommit b = commit(tree(file("d/f", blob("a"))), a); + final RevCommit c = commit(tree(file("d/f", blob("b"))), b); + + final RevFlag flag1 = rw.newFlag("flag1"); + final RevFlag flag2 = rw.newFlag("flag2"); + + a.add(flag1); + c.add(flag2); + + filter("d/f"); + markStart(c); + + RevCommit cBar = rw.next(); + assertEquals(cBar.flags & RevObject.PARSED, 1); + assertEquals(cBar.flags & flag2.mask, flag2.mask); + + RevCommit aBar = rw.next(); + assertEquals(aBar.flags & RevObject.PARSED, 1); + assertEquals(aBar.flags & flag1.mask, flag1.mask); + + assertNull(rw.next()); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java index 88bc7ddd2..64fba98b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java @@ -157,13 +157,14 @@ public ApplyResult call() throws GitAPIException, PatchFormatException, JGitText.get().renameFileFailed, f, dest), e); } apply(repository, fh.getOldPath(), cache, dest, fh); + r.addUpdatedFile(dest); break; case COPY: - f = getFile(fh.getOldPath(), false); - File target = getFile(fh.getNewPath(), false); - FileUtils.mkdirs(target.getParentFile(), true); - Files.copy(f.toPath(), target.toPath()); - apply(repository, fh.getOldPath(), cache, target, fh); + File src = getFile(fh.getOldPath(), false); + f = getFile(fh.getNewPath(), false); + FileUtils.mkdirs(f.getParentFile(), true); + Files.copy(src.toPath(), f.toPath()); + apply(repository, fh.getOldPath(), cache, f, fh); } r.addUpdatedFile(f); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java index 77967df2e..93ddfc660 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java @@ -129,6 +129,7 @@ public class BlameGenerator implements AutoCloseable { /** Blame is currently assigned to this source. */ private Candidate outCandidate; + private Region outRegion; /** @@ -395,6 +396,35 @@ private static byte[] getBytes(String path, InputStream in, long maxLength) return copy; } + /** + * Push a candidate object onto the generator's traversal stack. + *

+ * Candidates should be pushed in history order from oldest-to-newest. + * Applications should push the starting commit first, then the index + * revision (if the index is interesting), and finally the working tree copy + * (if the working tree is interesting). + * + * @param blameCommit + * ordered commits to use instead of RevWalk. + * @return {@code this} + * @throws java.io.IOException + * the repository cannot be read. + * @since 6.3 + */ + public BlameGenerator push(RevCommit blameCommit) throws IOException { + if (!find(blameCommit, resultPath)) { + return this; + } + + Candidate c = new Candidate(getRepository(), blameCommit, resultPath); + c.sourceBlob = idBuf.toObjectId(); + c.loadText(reader); + c.regionList = new Region(0, 0, c.sourceText.size()); + remaining = c.sourceText.size(); + push(c); + return this; + } + /** * Push a candidate object onto the generator's traversal stack. *

@@ -428,16 +458,7 @@ public BlameGenerator push(String description, AnyObjectId id) } RevCommit commit = revPool.parseCommit(id); - if (!find(commit, resultPath)) - return this; - - Candidate c = new Candidate(getRepository(), commit, resultPath); - c.sourceBlob = idBuf.toObjectId(); - c.loadText(reader); - c.regionList = new Region(0, 0, c.sourceText.size()); - remaining = c.sourceText.size(); - push(c); - return this; + return push(commit); } /** @@ -605,7 +626,7 @@ public boolean next() throws IOException { // Do not generate a tip of a reverse. The region // survives and should not appear to be deleted. - } else /* if (pCnt == 0) */{ + } else /* if (pCnt == 0) */ { // Root commit, with at least one surviving region. // Assign the remaining blame here. return result(n); @@ -846,8 +867,8 @@ private boolean processMerge(Candidate n) throws IOException { editList = new EditList(0); } else { p.loadText(reader); - editList = diffAlgorithm.diff(textComparator, - p.sourceText, n.sourceText); + editList = diffAlgorithm.diff(textComparator, p.sourceText, + n.sourceText); } if (editList.isEmpty()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java index e6626aece..f63cc6d64 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java @@ -23,6 +23,7 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.InvalidPathException; import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile; import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; @@ -138,7 +139,7 @@ RevCommit write(List repoProjects) } // In the last try, just propagate the exceptions return commitTreeOnCurrentTip(inserter, rw, treeId); - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | InvalidPathException e) { throw new ManifestErrorException(e); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index ab82dd0c3..d79f5d410 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -1181,12 +1181,12 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree, workTreeUpdater.writeWorkTreeChanges(true); if (getUnmergedPaths().isEmpty() && !failed()) { WorkTreeUpdater.Result result = workTreeUpdater.writeIndexChanges(); - resultTree = result.treeId; - modifiedFiles = result.modifiedFiles; - for (String f : result.failedToDelete) { + resultTree = result.getTreeId(); + modifiedFiles = result.getModifiedFiles(); + for (String f : result.getFailedToDelete()) { failingPaths.put(f, MergeFailureReason.COULD_NOT_DELETE); } - return result.failedToDelete.isEmpty(); + return result.getFailedToDelete().isEmpty(); } resultTree = null; return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FilteredRevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FilteredRevCommit.java new file mode 100644 index 000000000..16beac390 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FilteredRevCommit.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022, 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.revwalk; + +/** + * A filtered commit reference that overrides its parent in the DAG. + * + * @since 6.3 + */ +public class FilteredRevCommit extends RevCommit { + private RevCommit[] overriddenParents; + + /** + * Create a new commit reference wrapping an underlying commit reference. + * + * @param commit + * commit that is being wrapped + */ + public FilteredRevCommit(RevCommit commit) { + this(commit, NO_PARENTS); + } + + /** + * Create a new commit reference wrapping an underlying commit reference. + * + * @param commit + * commit that is being wrapped + * @param parents + * overridden parents for the commit + */ + public FilteredRevCommit(RevCommit commit, RevCommit... parents) { + super(commit); + this.overriddenParents = parents; + this.parents = NO_PARENTS; + } + + /** + * Update parents on the commit + * + * @param overriddenParents + * parents to be overwritten + */ + public void setParents(RevCommit... overriddenParents) { + this.overriddenParents = overriddenParents; + } + + /** + * Get the number of parent commits listed in this commit. + * + * @return number of parents; always a positive value but can be 0 if it has + * no parents. + */ + @Override + public int getParentCount() { + return overriddenParents.length; + } + + /** + * Get the nth parent from this commit's parent list. + * + * @param nth + * parent index to obtain. Must be in the range 0 through + * {@link #getParentCount()}-1. + * @return the specified parent. + * @throws java.lang.ArrayIndexOutOfBoundsException + * an invalid parent index was specified. + */ + @Override + public RevCommit getParent(int nth) { + return overriddenParents[nth]; + } + + /** + * Obtain an array of all parents (NOTE - THIS IS NOT A COPY). + * + *

+ * This method is exposed only to provide very fast, efficient access to + * this commit's parent list. Applications relying on this list should be + * very careful to ensure they do not modify its contents during their use + * of it. + * + * @return the array of parents. + */ + @Override + public RevCommit[] getParents() { + return overriddenParents; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java index 70490eec7..a7c21e3f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java @@ -36,6 +36,11 @@ /** * A commit reference to a commit in the DAG. + * + * The state of the RevCommit isn't populated until the commit is parsed. The + * newly created RevCommit is unparsed and only has an objectId reference. Other + * states like parents, trees, commit ident, commit message, etc. are + * populated/available when the commit is parsed. */ public class RevCommit extends RevObject { private static final int STACK_DEPTH = 500; @@ -109,7 +114,7 @@ public static RevCommit parse(RevWalk rw, byte[] raw) throws IOException { * * @since 6.3 */ - protected RevCommit[] parents; + RevCommit[] parents; int commitTime; // An int here for performance, overflows in 2038 @@ -127,6 +132,22 @@ protected RevCommit(AnyObjectId id) { super(id); } + /** + * Create a new commit reference. + * + * @param orig + * commit to be copied from. + */ + RevCommit(RevCommit orig) { + super(orig.getId()); + this.buffer = orig.buffer; + this.commitTime = orig.commitTime; + this.flags = orig.flags; + this.parents = orig.parents; + this.tree = orig.tree; + this.inDegree = orig.inDegree; + } + @Override void parseHeaders(RevWalk walk) throws MissingObjectException, IncorrectObjectTypeException, IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java index 2c88bb872..9ec331b69 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java @@ -11,6 +11,8 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -38,10 +40,13 @@ class RewriteGenerator extends Generator { private final FIFORevQueue pending; + private final Map transformedCommits; + RewriteGenerator(Generator s) { super(s.firstParent); source = s; pending = new FIFORevQueue(s.firstParent); + transformedCommits = new HashMap<>(); } @Override @@ -58,10 +63,10 @@ int outputType() { @Override RevCommit next() throws MissingObjectException, IncorrectObjectTypeException, IOException { - RevCommit c = pending.next(); + FilteredRevCommit c = (FilteredRevCommit) pending.next(); if (c == null) { - c = source.next(); + c = transform(source.next()); if (c == null) { // We are done: Both the source generator and our internal list // are completely exhausted. @@ -79,9 +84,9 @@ RevCommit next() throws MissingObjectException, final RevCommit newp = rewrite(oldp); if (firstParent) { if (newp == null) { - c.parents = RevCommit.NO_PARENTS; + c.setParents(RevCommit.NO_PARENTS); } else { - c.parents = new RevCommit[] { newp }; + c.setParents(newp); } return c; } @@ -91,7 +96,7 @@ RevCommit next() throws MissingObjectException, } } if (rewrote) { - c.parents = cleanup(pList); + c.setParents(cleanup(pList)); } return c; } @@ -111,7 +116,7 @@ private void applyFilterToParents(RevCommit c) for (RevCommit parent : c.getParents()) { while ((parent.flags & RevWalk.TREE_REV_FILTER_APPLIED) == 0) { - RevCommit n = source.next(); + FilteredRevCommit n = transform(source.next()); if (n != null) { pending.add(n); @@ -130,6 +135,8 @@ private RevCommit rewrite(RevCommit p) throws MissingObjectException, IncorrectObjectTypeException, IOException { for (;;) { + p = transform(p); + if (p.getParentCount() > 1) { // This parent is a merge, so keep it. // @@ -158,11 +165,27 @@ private RevCommit rewrite(RevCommit p) throws MissingObjectException, } applyFilterToParents(p.getParent(0)); - p = p.getParent(0); + p = transform(p.getParent(0)); } } + private FilteredRevCommit transform(RevCommit c) { + if (c == null) { + return null; + } + + if (c instanceof FilteredRevCommit) { + return (FilteredRevCommit) c; + } + + if (!transformedCommits.containsKey(c)) { + transformedCommits.put(c, new FilteredRevCommit(c, c.getParents())); + } + + return transformedCommits.get(c); + } + private RevCommit[] cleanup(RevCommit[] oldList) { // Remove any duplicate parents caused due to rewrites (e.g. a merge // with two sides that both simplified back into the merge base). diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java index 88529ba09..e5de2b4a7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java @@ -25,7 +25,10 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.TreeMap; + +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute; import org.eclipse.jgit.attributes.Attributes; @@ -71,20 +74,33 @@ public class WorkTreeUpdater implements Closeable { */ public static class Result { - /** - * Files modified during this operation. - */ - public List modifiedFiles = new LinkedList<>(); + private final List modifiedFiles = new LinkedList<>(); + + + private final List failedToDelete = new LinkedList<>(); + + private ObjectId treeId = null; /** - * Files in this list were failed to be deleted. + * @return Modified tree ID if any, or null otherwise. */ - public List failedToDelete = new LinkedList<>(); + public ObjectId getTreeId() { + return treeId; + } /** - * Modified tree ID if any, or null otherwise. + * @return Files that couldn't be deleted. */ - public ObjectId treeId = null; + public List getFailedToDelete() { + return failedToDelete; + } + + /** + * @return Files modified during this operation. + */ + public List getModifiedFiles() { + return modifiedFiles; + } } Result result = new Result(); @@ -96,97 +112,112 @@ public static class Result { private final Repository repo; /** - * Set to true if this operation should work in-memory. The repo's dircache and - * workingtree are not touched by this method. Eventually needed files are - * created as temporary files and a new empty, in-memory dircache will be - * used instead the repo's one. Often used for bare repos where the repo + * Set to true if this operation should work in-memory. The repo's dircache + * and workingtree are not touched by this method. Eventually needed files + * are created as temporary files and a new empty, in-memory dircache will + * be used instead the repo's one. Often used for bare repos where the repo * doesn't even have a workingtree and dircache. */ private final boolean inCore; private final ObjectInserter inserter; + private final ObjectReader reader; + private DirCache dirCache; + private boolean implicitDirCache = false; /** * Builder to update the dir cache during this operation. */ - private DirCacheBuilder builder = null; + private DirCacheBuilder builder; /** - * The {@link WorkingTreeOptions} are needed to determine line endings for affected files. + * The {@link WorkingTreeOptions} are needed to determine line endings for + * affected files. */ private WorkingTreeOptions workingTreeOptions; /** - * The size limit (bytes) which controls a file to be stored in {@code Heap} or {@code LocalFile} - * during the operation. + * The size limit (bytes) which controls a file to be stored in {@code Heap} + * or {@code LocalFile} during the operation. */ private int inCoreFileSizeLimit; /** - * If the operation has nothing to do for a file but check it out at the end of the operation, it - * can be added here. + * If the operation has nothing to do for a file but check it out at the end + * of the operation, it can be added here. */ private final Map toBeCheckedOut = new HashMap<>(); /** - * Files in this list will be deleted from the local copy at the end of the operation. + * Files in this list will be deleted from the local copy at the end of the + * operation. */ private final TreeMap toBeDeleted = new TreeMap<>(); /** * Keeps {@link CheckoutMetadata} for {@link #checkout()}. */ - private Map checkoutMetadata; + private Map checkoutMetadataByPath; /** * Keeps {@link CheckoutMetadata} for {@link #revertModifiedFiles()}. */ - private Map cleanupMetadata; + private Map cleanupMetadataByPath; /** - * Whether the changes were successfully written + * Whether the changes were successfully written. */ - private boolean indexChangesWritten = false; + private boolean indexChangesWritten; /** - * @param repo the {@link org.eclipse.jgit.lib.Repository}. - * @param dirCache if set, use the provided dir cache. Otherwise, use the default repository one + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, use the default + * repository one */ - private WorkTreeUpdater( - Repository repo, - DirCache dirCache) { + private WorkTreeUpdater(Repository repo, DirCache dirCache) { this.repo = repo; this.dirCache = dirCache; this.inCore = false; this.inserter = repo.newObjectInserter(); this.reader = inserter.newReader(); - this.workingTreeOptions = repo.getConfig().get(WorkingTreeOptions.KEY); - this.checkoutMetadata = new HashMap<>(); - this.cleanupMetadata = new HashMap<>(); - this.inCoreFileSizeLimit = setInCoreFileSizeLimit(repo.getConfig()); + Config config = repo.getConfig(); + this.workingTreeOptions = config.get(WorkingTreeOptions.KEY); + this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config); + this.checkoutMetadataByPath = new HashMap<>(); + this.cleanupMetadataByPath = new HashMap<>(); } /** - * @param repo the {@link org.eclipse.jgit.lib.Repository}. - * @param dirCache if set, use the provided dir cache. Otherwise, use the default repository one - * @return an IO handler. + * Creates a new {@link WorkTreeUpdater} for the given repository. + * + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, use the default + * repository one + * @return the {@link WorkTreeUpdater}. */ - public static WorkTreeUpdater createWorkTreeUpdater(Repository repo, DirCache dirCache) { + public static WorkTreeUpdater createWorkTreeUpdater(Repository repo, + DirCache dirCache) { return new WorkTreeUpdater(repo, dirCache); } /** - * @param repo the {@link org.eclipse.jgit.lib.Repository}. - * @param dirCache if set, use the provided dir cache. Otherwise, creates a new one - * @param oi to use for writing the modified objects with. + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, creates a new + * one + * @param oi + * to use for writing the modified objects with. */ - private WorkTreeUpdater( - Repository repo, - DirCache dirCache, + private WorkTreeUpdater(Repository repo, DirCache dirCache, ObjectInserter oi) { this.repo = repo; this.dirCache = dirCache; @@ -195,18 +226,24 @@ private WorkTreeUpdater( this.inCore = true; this.reader = oi.newReader(); if (repo != null) { - this.inCoreFileSizeLimit = setInCoreFileSizeLimit(repo.getConfig()); + this.inCoreFileSizeLimit = getInCoreFileSizeLimit(repo.getConfig()); } } /** - * @param repo the {@link org.eclipse.jgit.lib.Repository}. - * @param dirCache if set, use the provided dir cache. Otherwise, creates a new one - * @param oi to use for writing the modified objects with. - * @return an IO handler. + * Creates a new {@link WorkTreeUpdater} that works in memory only. + * + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, creates a new + * one + * @param oi + * to use for writing the modified objects with. + * @return the {@link WorkTreeUpdater} */ - public static WorkTreeUpdater createInCoreWorkTreeUpdater(Repository repo, DirCache dirCache, - ObjectInserter oi) { + public static WorkTreeUpdater createInCoreWorkTreeUpdater(Repository repo, + DirCache dirCache, ObjectInserter oi) { return new WorkTreeUpdater(repo, dirCache, oi); } @@ -219,17 +256,15 @@ public interface StreamSupplier { * Loads the input stream. * * @return the loaded stream - * @throws IOException if any reading error occurs + * @throws IOException + * if any reading error occurs */ InputStream load() throws IOException; } /** - * We write the patch result to a {@link org.eclipse.jgit.util.TemporaryBuffer} and then use - * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF and smudge filters. - * DirCacheCheckout needs an ObjectLoader, not a TemporaryBuffer, so this class bridges between - * the two, making any Stream provided by a {@link StreamSupplier} look like an ordinary git blob - * to DirCacheCheckout. + * We want to use DirCacheCheckout for its CR-LF and smudge filters, but DirCacheCheckout needs an + * ObjectLoader rather than InputStream. This class provides a bridge between the two. */ public static class StreamLoader extends ObjectLoader { @@ -264,24 +299,28 @@ public byte[] getCachedBytes() throws LargeObjectException { @Override public ObjectStream openStream() throws IOException { - return new ObjectStream.Filter(getType(), getSize(), new BufferedInputStream(data.load())); + return new ObjectStream.Filter(getType(), getSize(), + new BufferedInputStream(data.load())); } } /** * Creates stream loader for the given supplier. * - * @param supplier to wrap - * @param length of the supplied content + * @param supplier + * to wrap + * @param length + * of the supplied content * @return the result stream loader */ - public static StreamLoader createStreamLoader(StreamSupplier supplier, long length) { + public static StreamLoader createStreamLoader(StreamSupplier supplier, + long length) { return new StreamLoader(supplier, length); } - private static int setInCoreFileSizeLimit(Config config) { - return config.getInt( - ConfigConstants.CONFIG_MERGE_SECTION, ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20); + private static int getInCoreFileSizeLimit(Config config) { + return config.getInt(ConfigConstants.CONFIG_MERGE_SECTION, + ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20); } /** @@ -297,7 +336,8 @@ public int getInCoreFileSizeLimit() { * Gets dir cache for the repo. Locked if not inCore. * * @return the result dir cache - * @throws IOException is case the dir cache cannot be read + * @throws IOException + * is case the dir cache cannot be read */ public DirCache getLockedDirCache() throws IOException { if (dirCache == null) { @@ -305,7 +345,7 @@ public DirCache getLockedDirCache() throws IOException { if (inCore) { dirCache = DirCache.newInCore(); } else { - dirCache = nonNullNonBareRepo().lockDirCache(); + dirCache = nonNullRepo().lockDirCache(); } } if (builder == null) { @@ -315,21 +355,25 @@ public DirCache getLockedDirCache() throws IOException { } /** - * Creates build iterator for the handler's builder. + * Creates a {@link DirCacheBuildIterator} for the builder of this + * {@link WorkTreeUpdater}. * - * @return the iterator + * @return the {@link DirCacheBuildIterator} */ public DirCacheBuildIterator createDirCacheBuildIterator() { return new DirCacheBuildIterator(builder); } /** - * Writes the changes to the WorkTree (but not the index). + * Writes the changes to the working tree (but not to the index). * - * @param shouldCheckoutTheirs before committing the changes - * @throws IOException if any of the writes fail + * @param shouldCheckoutTheirs + * before committing the changes + * @throws IOException + * if any of the writes fail */ - public void writeWorkTreeChanges(boolean shouldCheckoutTheirs) throws IOException { + public void writeWorkTreeChanges(boolean shouldCheckoutTheirs) + throws IOException { handleDeletedFiles(); if (inCore) { @@ -356,8 +400,9 @@ public void writeWorkTreeChanges(boolean shouldCheckoutTheirs) throws IOExceptio /** * Writes the changes to the index. * - * @return the Result of the operation. - * @throws IOException if any of the writes fail + * @return the {@link Result} of the operation. + * @throws IOException + * if any of the writes fail */ public Result writeIndexChanges() throws IOException { result.treeId = getLockedDirCache().writeTree(inserter); @@ -366,32 +411,42 @@ public Result writeIndexChanges() throws IOException { } /** - * Adds a {@link DirCacheEntry} for direct checkout and remembers its {@link CheckoutMetadata}. + * Adds a {@link DirCacheEntry} for direct checkout and remembers its + * {@link CheckoutMetadata}. * - * @param path of the entry - * @param entry to add - * @param cleanupStreamType to use for the cleanup metadata - * @param cleanupSmudgeCommand to use for the cleanup metadata - * @param checkoutStreamType to use for the checkout metadata - * @param checkoutSmudgeCommand to use for the checkout metadata - * @since 6.1 + * @param path + * of the entry + * @param entry + * to add + * @param cleanupStreamType + * to use for the cleanup metadata + * @param cleanupSmudgeCommand + * to use for the cleanup metadata + * @param checkoutStreamType + * to use for the checkout metadata + * @param checkoutSmudgeCommand + * to use for the checkout metadata */ - public void addToCheckout( - String path, DirCacheEntry entry, EolStreamType cleanupStreamType, - String cleanupSmudgeCommand, EolStreamType checkoutStreamType, String checkoutSmudgeCommand) { + public void addToCheckout(String path, DirCacheEntry entry, + EolStreamType cleanupStreamType, String cleanupSmudgeCommand, + EolStreamType checkoutStreamType, String checkoutSmudgeCommand) { if (entry != null) { // In some cases, we just want to add the metadata. toBeCheckedOut.put(path, entry); } - addCheckoutMetadata(cleanupMetadata, path, cleanupStreamType, cleanupSmudgeCommand); - addCheckoutMetadata(checkoutMetadata, path, checkoutStreamType, checkoutSmudgeCommand); + addCheckoutMetadata(cleanupMetadataByPath, path, cleanupStreamType, + cleanupSmudgeCommand); + addCheckoutMetadata(checkoutMetadataByPath, path, checkoutStreamType, + checkoutSmudgeCommand); } /** - * Get a map which maps the paths of files which have to be checked out because the operation - * created new fully-merged content for this file into the index. - * - *

This means: the operation wrote a new stage 0 entry for this path.

+ * Gets a map which maps the paths of files which have to be checked out + * because the operation created new fully-merged content for this file into + * the index. + *

+ * This means: the operation wrote a new stage 0 entry for this path. + *

* * @return the map */ @@ -400,37 +455,43 @@ public Map getToBeCheckedOut() { } /** - * Deletes the given file + * Remembers the given file to be deleted. *

- * Note the actual deletion is only done in {@link #writeWorkTreeChanges} + * Note the actual deletion is only done in {@link #writeWorkTreeChanges}. * - * @param path of the file to be deleted - * @param file to be deleted - * @param streamType to use for cleanup metadata - * @param smudgeCommand to use for cleanup metadata - * @throws IOException if the file cannot be deleted + * @param path + * of the file to be deleted + * @param file + * to be deleted + * @param streamType + * to use for cleanup metadata + * @param smudgeCommand + * to use for cleanup metadata */ - public void deleteFile(String path, File file, EolStreamType streamType, String smudgeCommand) - throws IOException { + public void deleteFile(String path, File file, EolStreamType streamType, + String smudgeCommand) { toBeDeleted.put(path, file); if (file != null && file.isFile()) { - addCheckoutMetadata(cleanupMetadata, path, streamType, smudgeCommand); + addCheckoutMetadata(cleanupMetadataByPath, path, streamType, + smudgeCommand); } } /** - * Remembers the {@link CheckoutMetadata} for the given path; it may be needed in {@link - * #checkout()} or in {@link #revertModifiedFiles()}. + * Remembers the {@link CheckoutMetadata} for the given path; it may be + * needed in {@link #checkout()} or in {@link #revertModifiedFiles()}. * - * @param map to add the metadata to - * @param path of the current node - * @param streamType to use for the metadata - * @param smudgeCommand to use for the metadata - * @since 6.1 + * @param map + * to add the metadata to + * @param path + * of the current node + * @param streamType + * to use for the metadata + * @param smudgeCommand + * to use for the metadata */ - private void addCheckoutMetadata( - Map map, String path, EolStreamType streamType, - String smudgeCommand) { + private void addCheckoutMetadata(Map map, + String path, EolStreamType streamType, String smudgeCommand) { if (inCore || map == null) { return; } @@ -439,18 +500,20 @@ private void addCheckoutMetadata( /** * Detects if CRLF conversion has been configured. - *

+ *

+ *

* See {@link EolStreamTypeUtil#detectStreamType} for more info. * - * @param attributes of the file for which the type is to be detected + * @param attributes + * of the file for which the type is to be detected * @return the detected type */ public EolStreamType detectCheckoutStreamType(Attributes attributes) { if (inCore) { return null; } - return EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, attributes); + return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP, + workingTreeOptions, attributes); } private void handleDeletedFiles() { @@ -470,7 +533,8 @@ private void handleDeletedFiles() { /** * Marks the given path as modified in the operation. * - * @param path to mark as modified + * @param path + * to mark as modified */ public void markAsModified(String path) { result.modifiedFiles.add(path); @@ -486,17 +550,15 @@ public List getModifiedFiles() { } private void checkout() throws NoWorkTreeException, IOException { - // Iterate in reverse so that "folder/file" is deleted before - // "folder". Otherwise, this could result in a failing path because - // of a non-empty directory, for which delete() would fail. - for (Map.Entry entry : toBeCheckedOut.entrySet()) { + for (Map.Entry entry : toBeCheckedOut + .entrySet()) { DirCacheEntry dirCacheEntry = entry.getValue(); if (dirCacheEntry.getFileMode() == FileMode.GITLINK) { - new File(nonNullNonBareRepo().getWorkTree(), entry.getKey()).mkdirs(); + new File(nonNullRepo().getWorkTree(), entry.getKey()) + .mkdirs(); } else { - DirCacheCheckout.checkoutEntry( - repo, dirCacheEntry, reader, false, - checkoutMetadata.get(entry.getKey()), + DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader, + false, checkoutMetadataByPath.get(entry.getKey()), workingTreeOptions); result.modifiedFiles.add(entry.getKey()); } @@ -504,11 +566,13 @@ private void checkout() throws NoWorkTreeException, IOException { } /** - * Reverts any uncommitted changes in the worktree. We know that for all modified files the - * old content was in the old index and the index contained only stage 0. In case if inCore - * operation just clear the history of modified files. + * Reverts any uncommitted changes in the worktree. We know that for all + * modified files the old content was in the old index and the index + * contained only stage 0. In case of inCore operation just clear the + * history of modified files. * - * @throws java.io.IOException in case the cleaning up failed + * @throws IOException + * in case the cleaning up failed */ public void revertModifiedFiles() throws IOException { if (inCore) { @@ -521,9 +585,8 @@ public void revertModifiedFiles() throws IOException { for (String path : result.modifiedFiles) { DirCacheEntry entry = dirCache.getEntry(path); if (entry != null) { - DirCacheCheckout.checkoutEntry( - repo, entry, reader, false, cleanupMetadata.get(path), - workingTreeOptions); + DirCacheCheckout.checkoutEntry(repo, entry, reader, false, + cleanupMetadataByPath.get(path), workingTreeOptions); } } } @@ -538,22 +601,24 @@ public void close() throws IOException { /** * Updates the file in the checkout with the given content. * - * @param resultStreamLoader with the content to be updated - * @param streamType for parsing the content - * @param smudgeCommand for formatting the content - * @param path of the file to be updated - * @param file to be updated - * @param safeWrite whether the content should be written to a buffer first - * @throws IOException if the {@link CheckoutMetadata} cannot be determined + * @param resultStreamLoader + * with the content to be updated + * @param streamType + * for parsing the content + * @param smudgeCommand + * for formatting the content + * @param path + * of the file to be updated + * @param file + * to be updated + * @param safeWrite + * whether the content should be written to a buffer first + * @throws IOException + * if the file cannot be updated */ - public void updateFileWithContent( - StreamLoader resultStreamLoader, - EolStreamType streamType, - String smudgeCommand, - String path, - File file, - boolean safeWrite) - throws IOException { + public void updateFileWithContent(StreamLoader resultStreamLoader, + EolStreamType streamType, String smudgeCommand, String path, + File file, boolean safeWrite) throws IOException { if (inCore) { return; } @@ -584,74 +649,85 @@ public void updateFileWithContent( } /** - * Creates a path with the given content, and adds it to the specified stage to the index builder + * Creates a path with the given content, and adds it to the specified stage + * to the index builder. * - * @param inputStream with the content to be updated - * @param path of the file to be updated - * @param fileMode of the modified file - * @param entryStage of the new entry - * @param lastModified instant of the modified file - * @param len of the content - * @param lfsAttribute for checking for LFS enablement + * @param inputStream + * with the content to be updated + * @param path + * of the file to be updated + * @param fileMode + * of the modified file + * @param entryStage + * of the new entry + * @param lastModified + * instant of the modified file + * @param len + * of the content + * @param lfsAttribute + * for checking for LFS enablement * @return the entry which was added to the index - * @throws IOException if inserting the content fails + * @throws IOException + * if inserting the content fails */ - public DirCacheEntry insertToIndex( - InputStream inputStream, - byte[] path, - FileMode fileMode, - int entryStage, - Instant lastModified, - int len, + public DirCacheEntry insertToIndex(InputStream inputStream, byte[] path, + FileMode fileMode, int entryStage, Instant lastModified, int len, Attribute lfsAttribute) throws IOException { StreamLoader contentLoader = createStreamLoader(() -> inputStream, len); - return insertToIndex(contentLoader, path, fileMode, entryStage, lastModified, len, - lfsAttribute); + return insertToIndex(contentLoader, path, fileMode, entryStage, + lastModified, len, lfsAttribute); } /** - * Creates a path with the given content, and adds it to the specified stage to the index builder + * Creates a path with the given content, and adds it to the specified stage + * to the index builder. * - * @param resultStreamLoader with the content to be updated - * @param path of the file to be updated - * @param fileMode of the modified file - * @param entryStage of the new entry - * @param lastModified instant of the modified file - * @param len of the content - * @param lfsAttribute for checking for LFS enablement + * @param resultStreamLoader + * with the content to be updated + * @param path + * of the file to be updated + * @param fileMode + * of the modified file + * @param entryStage + * of the new entry + * @param lastModified + * instant of the modified file + * @param len + * of the content + * @param lfsAttribute + * for checking for LFS enablement * @return the entry which was added to the index - * @throws IOException if inserting the content fails + * @throws IOException + * if inserting the content fails */ - public DirCacheEntry insertToIndex( - StreamLoader resultStreamLoader, - byte[] path, - FileMode fileMode, - int entryStage, - Instant lastModified, - int len, - Attribute lfsAttribute) throws IOException { - return addExistingToIndex(insertResult(resultStreamLoader, lfsAttribute), - path, fileMode, entryStage, lastModified, len); + public DirCacheEntry insertToIndex(StreamLoader resultStreamLoader, + byte[] path, FileMode fileMode, int entryStage, + Instant lastModified, int len, Attribute lfsAttribute) + throws IOException { + return addExistingToIndex( + insertResult(resultStreamLoader, lfsAttribute), path, fileMode, + entryStage, lastModified, len); } /** - * Adds a path with the specified stage to the index builder + * Adds a path with the specified stage to the index builder. * - * @param objectId of the existing object to add - * @param path of the modified file - * @param fileMode of the modified file - * @param entryStage of the new entry - * @param lastModified instant of the modified file - * @param len of the modified file content + * @param objectId + * of the existing object to add + * @param path + * of the modified file + * @param fileMode + * of the modified file + * @param entryStage + * of the new entry + * @param lastModified + * instant of the modified file + * @param len + * of the modified file content * @return the entry which was added to the index */ - public DirCacheEntry addExistingToIndex( - ObjectId objectId, - byte[] path, - FileMode fileMode, - int entryStage, - Instant lastModified, - int len) { + public DirCacheEntry addExistingToIndex(ObjectId objectId, byte[] path, + FileMode fileMode, int entryStage, Instant lastModified, int len) { DirCacheEntry dce = new DirCacheEntry(path, entryStage); dce.setFileMode(fileMode); if (lastModified != null) { @@ -664,44 +740,25 @@ public DirCacheEntry addExistingToIndex( return dce; } - private ObjectId insertResult(StreamLoader resultStreamLoader, Attribute lfsAttribute) - throws IOException { - try (LfsInputStream is = - org.eclipse.jgit.util.LfsFactory.getInstance() - .applyCleanFilter( - repo, - resultStreamLoader.data.load(), - resultStreamLoader.size, - lfsAttribute)) { + private ObjectId insertResult(StreamLoader resultStreamLoader, + Attribute lfsAttribute) throws IOException { + try (LfsInputStream is = LfsFactory.getInstance().applyCleanFilter(repo, + resultStreamLoader.data.load(), resultStreamLoader.size, + lfsAttribute)) { return inserter.insert(OBJ_BLOB, is.getLength(), is); } } /** - * Gets non-null repository instance + * Gets the non-null repository instance of this {@link WorkTreeUpdater}. * * @return non-null repository instance - * @throws java.lang.NullPointerException if the handler was constructed without a repository. + * @throws NullPointerException + * if the handler was constructed without a repository. */ + @NonNull private Repository nonNullRepo() throws NullPointerException { - if (repo == null) { - throw new NullPointerException(JGitText.get().repositoryIsRequired); - } - return repo; + return Objects.requireNonNull(repo, + () -> JGitText.get().repositoryIsRequired); } - - - /** - * Gets non-null and non-bare repository instance - * - * @return non-null and non-bare repository instance - * @throws java.lang.NullPointerException if the handler was constructed without a repository. - * @throws NoWorkTreeException if the handler was constructed with a bare repository - */ - private Repository nonNullNonBareRepo() throws NullPointerException, NoWorkTreeException { - if (nonNullRepo().isBare()) { - throw new NoWorkTreeException(); - } - return repo; - } -} \ No newline at end of file +}