Option to pass start RevCommit to be blamed on to the BlameGenerator.

This can allow passing a FilteredRevCommit which is the filtered list of
commit graph making it easier for Blame to work on. This can
significantly improve blame performance since blame can skip expensive
RevWalk.

Change-Id: I5dab25301d6aef7df6a0bc25a4c553c730199272
This commit is contained in:
Ronald Bhuleskar 2022-08-02 17:17:46 -07:00 committed by Terry Parker
parent ceb51a5e0e
commit 59e8bec6e7
2 changed files with 339 additions and 41 deletions

View File

@ -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 {
/**
* <pre>
* (ts) OTHER_FILE INTERESTING_FILE
* 1 a
* 2 a, b
* 3 1, 2 c1 <--
* 4 a, b, c |
* 5 1, 2, 3 c2---
* </pre>
*/
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())) {
/**
*
*
* <pre>
* refs/heads/master
* A
* / \ refs/heads/side
* / ----------------> side
* / |
* merge <-------------------
* </pre>
*/
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<RevCommit> 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())) {
/**
*
*
* <pre>
* refs/heads/master
* A
* / \ refs/heads/side
* B ----------------> side
* / |
* merge <-------------------
* </pre>
*/
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 {
/**
* <pre>
* (ts) OTHER_FILE INTERESTING_FILE
* 1 a
* 2 a, b
* 3 1, 2 c1 <--
* 4 a, b, c |
* 6 3, 1, 2 c2---
* </pre>
*/
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());

View File

@ -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.
* <p>
* 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.
* <p>
@ -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()) {