diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 8ec0b6582..b1736472e 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -11,6 +11,7 @@ Import-Package: org.eclipse.jgit;version="[1.0.0,1.1.0)", org.eclipse.jgit.api;version="[1.0.0,1.1.0)", org.eclipse.jgit.api.errors;version="[1.0.0,1.1.0)", org.eclipse.jgit.awtui;version="[1.0.0,1.1.0)", + org.eclipse.jgit.blame;version="[1.0.0,1.1.0)", org.eclipse.jgit.console;version="[1.0.0,1.1.0)", org.eclipse.jgit.diff;version="[1.0.0,1.1.0)", org.eclipse.jgit.dircache;version="[1.0.0,1.1.0)", diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BlameCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BlameCommandTest.java new file mode 100644 index 000000000..1f67807e4 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BlameCommandTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2011, GitHub Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.eclipse.jgit.blame.BlameResult; +import org.eclipse.jgit.lib.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +/** + * Unit tests of {@link BlameCommand} + */ +public class BlameCommandTest extends RepositoryTestCase { + + private String join(String... lines) { + StringBuilder joined = new StringBuilder(); + for (String line : lines) + joined.append(line).append('\n'); + return joined.toString(); + } + + @Test + public void testSingleRevision() throws Exception { + Git git = new Git(db); + + String[] content = new String[] { "first", "second", "third" }; + + writeTrashFile("file.txt", join(content)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit = git.commit().setMessage("create file").call(); + + BlameCommand command = new BlameCommand(db); + command.setFilePath("file.txt"); + BlameResult lines = command.call(); + assertNotNull(lines); + assertEquals(3, lines.getResultContents().size()); + + for (int i = 0; i < 3; i++) { + assertEquals(commit, lines.getSourceCommit(i)); + assertEquals(i, lines.getSourceLine(i)); + } + } + + @Test + public void testTwoRevisions() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "first", "second" }; + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit1 = git.commit().setMessage("create file").call(); + + String[] content2 = new String[] { "first", "second", "third" }; + writeTrashFile("file.txt", join(content2)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit2 = git.commit().setMessage("create file").call(); + + BlameCommand command = new BlameCommand(db); + command.setFilePath("file.txt"); + BlameResult lines = command.call(); + assertEquals(3, lines.getResultContents().size()); + + assertEquals(commit1, lines.getSourceCommit(0)); + assertEquals(0, lines.getSourceLine(0)); + + assertEquals(commit1, lines.getSourceCommit(1)); + assertEquals(1, lines.getSourceLine(1)); + + assertEquals(commit2, lines.getSourceCommit(2)); + assertEquals(2, lines.getSourceLine(2)); + } + + @Test + public void testRename() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "a", "b", "c" }; + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit1 = git.commit().setMessage("create file").call(); + + writeTrashFile("file1.txt", join(content1)); + git.add().addFilepattern("file1.txt").call(); + git.rm().addFilepattern("file.txt").call(); + git.commit().setMessage("moving file").call(); + + String[] content2 = new String[] { "a", "b", "c2" }; + writeTrashFile("file1.txt", join(content2)); + git.add().addFilepattern("file1.txt").call(); + RevCommit commit3 = git.commit().setMessage("editing file").call(); + + BlameCommand command = new BlameCommand(db); + command.setFollowFileRenames(true); + command.setFilePath("file1.txt"); + BlameResult lines = command.call(); + + assertEquals(commit1, lines.getSourceCommit(0)); + assertEquals(0, lines.getSourceLine(0)); + assertEquals("file.txt", lines.getSourcePath(0)); + + assertEquals(commit1, lines.getSourceCommit(1)); + assertEquals(1, lines.getSourceLine(1)); + assertEquals("file.txt", lines.getSourcePath(1)); + + assertEquals(commit3, lines.getSourceCommit(2)); + assertEquals(2, lines.getSourceLine(2)); + assertEquals("file1.txt", lines.getSourcePath(2)); + } + + @Test + public void testDeleteTrailingLines() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "a", "b", "c", "d" }; + String[] content2 = new String[] { "a", "b" }; + + writeTrashFile("file.txt", join(content2)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit1 = git.commit().setMessage("create file").call(); + + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("edit file").call(); + + writeTrashFile("file.txt", join(content2)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("edit file").call(); + + BlameCommand command = new BlameCommand(db); + + command.setFilePath("file.txt"); + BlameResult lines = command.call(); + assertEquals(content2.length, lines.getResultContents().size()); + + assertEquals(commit1, lines.getSourceCommit(0)); + assertEquals(commit1, lines.getSourceCommit(1)); + + assertEquals(0, lines.getSourceLine(0)); + assertEquals(1, lines.getSourceLine(1)); + } + + @Test + public void testDeleteMiddleLines() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "a", "b", "c", "d", "e" }; + String[] content2 = new String[] { "a", "c", "e" }; + + writeTrashFile("file.txt", join(content2)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit1 = git.commit().setMessage("edit file").call(); + + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("edit file").call(); + + writeTrashFile("file.txt", join(content2)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("edit file").call(); + + BlameCommand command = new BlameCommand(db); + + command.setFilePath("file.txt"); + BlameResult lines = command.call(); + assertEquals(content2.length, lines.getResultContents().size()); + + assertEquals(commit1, lines.getSourceCommit(0)); + assertEquals(0, lines.getSourceLine(0)); + + assertEquals(commit1, lines.getSourceCommit(1)); + assertEquals(1, lines.getSourceLine(1)); + + assertEquals(commit1, lines.getSourceCommit(2)); + assertEquals(2, lines.getSourceLine(2)); + } + + @Test + public void testEditAllLines() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "a", "1" }; + String[] content2 = new String[] { "b", "2" }; + + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("edit file").call(); + + writeTrashFile("file.txt", join(content2)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit2 = git.commit().setMessage("create file").call(); + + BlameCommand command = new BlameCommand(db); + + command.setFilePath("file.txt"); + BlameResult lines = command.call(); + assertEquals(content2.length, lines.getResultContents().size()); + assertEquals(commit2, lines.getSourceCommit(0)); + assertEquals(commit2, lines.getSourceCommit(1)); + } + + @Test + public void testMiddleClearAllLines() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "a", "b", "c" }; + + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("edit file").call(); + + writeTrashFile("file.txt", ""); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("create file").call(); + + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + RevCommit commit3 = git.commit().setMessage("edit file").call(); + + BlameCommand command = new BlameCommand(db); + + command.setFilePath("file.txt"); + BlameResult lines = command.call(); + assertEquals(content1.length, lines.getResultContents().size()); + assertEquals(commit3, lines.getSourceCommit(0)); + assertEquals(commit3, lines.getSourceCommit(1)); + assertEquals(commit3, lines.getSourceCommit(2)); + } +} 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 new file mode 100644 index 000000000..657cf3870 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2011, GitHub Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api.blame; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.blame.BlameGenerator; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +/** Unit tests of {@link BlameGenerator}. */ +public class BlameGeneratorTest extends RepositoryTestCase { + @Test + public void testBoundLineDelete() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "first", "second" }; + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").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(); + + BlameGenerator generator = new BlameGenerator(db, "file.txt"); + try { + generator.push(null, db.resolve(Constants.HEAD)); + 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()); + + assertTrue(generator.next()); + assertEquals(c1, generator.getSourceCommit()); + assertEquals(2, generator.getRegionLength()); + assertEquals(1, generator.getResultStart()); + assertEquals(3, generator.getResultEnd()); + assertEquals(0, generator.getSourceStart()); + assertEquals(2, generator.getSourceEnd()); + assertEquals("file.txt", generator.getSourcePath()); + + assertFalse(generator.next()); + } finally { + generator.release(); + } + } + + @Test + public void testLinesAllDeletedShortenedWalk() throws Exception { + Git git = new Git(db); + + String[] content1 = new String[] { "first", "second", "third" }; + + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("create file").call(); + + String[] content2 = new String[] { "" }; + + writeTrashFile("file.txt", join(content2)); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("create file").call(); + + writeTrashFile("file.txt", join(content1)); + git.add().addFilepattern("file.txt").call(); + RevCommit c3 = git.commit().setMessage("create file").call(); + + BlameGenerator generator = new BlameGenerator(db, "file.txt"); + try { + generator.push(null, db.resolve(Constants.HEAD)); + assertEquals(3, generator.getResultContents().size()); + + assertTrue(generator.next()); + assertEquals(c3, generator.getSourceCommit()); + assertEquals(0, generator.getResultStart()); + assertEquals(3, generator.getResultEnd()); + + assertFalse(generator.next()); + } finally { + generator.release(); + } + } + + private static String join(String... lines) { + StringBuilder joined = new StringBuilder(); + for (String line : lines) + joined.append(line).append('\n'); + return joined.toString(); + } +} diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index e2d1a1e0a..0788b8c62 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -8,6 +8,7 @@ Bundle-Vendor: %provider_name Export-Package: org.eclipse.jgit;version="1.0.0", org.eclipse.jgit.api;version="1.0.0", org.eclipse.jgit.api.errors;version="1.0.0", + org.eclipse.jgit.blame;version="1.0.0", org.eclipse.jgit.diff;version="1.0.0", org.eclipse.jgit.dircache;version="1.0.0", org.eclipse.jgit.errors;version="1.0.0", diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties index 8eab4122c..5a40edd7d 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties @@ -32,6 +32,7 @@ badSectionEntry=Bad section entry: {0} base64InputNotProperlyPadded=Base64 input not properly padded. baseLengthIncorrect=base length incorrect bareRepositoryNoWorkdirAndIndex=Bare Repository has neither a working tree, nor an index +blameNotCommittedYet=Not Committed Yet blobNotFound=Blob not found: {0} branchNameInvalid=Branch name {0} is not allowed blobNotFoundForPath=Blob not found: {0} for path: {1} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java index 366028a2a..02538a2dc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java @@ -92,6 +92,7 @@ public class JGitText extends TranslationBundle { /***/ public String base64InputNotProperlyPadded; /***/ public String baseLengthIncorrect; /***/ public String bareRepositoryNoWorkdirAndIndex; + /***/ public String blameNotCommittedYet; /***/ public String blobNotFound; /***/ public String blobNotFoundForPath; /***/ public String branchNameInvalid; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java new file mode 100644 index 000000000..400d94bc8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2011, GitHub Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.blame.BlameGenerator; +import org.eclipse.jgit.blame.BlameResult; +import org.eclipse.jgit.diff.DiffAlgorithm; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +/** + * Blame command for building a {@link BlameResult} for a file path. + */ +public class BlameCommand extends GitCommand { + + private String path; + + private DiffAlgorithm diffAlgorithm; + + private RawTextComparator textComparator; + + private ObjectId startCommit; + + private Collection reverseEndCommits; + + private Boolean followFileRenames; + + /** + * @param repo + */ + public BlameCommand(Repository repo) { + super(repo); + } + + /** + * Set file path + * + * @param filePath + * @return this command + */ + public BlameCommand setFilePath(String filePath) { + this.path = filePath; + return this; + } + + /** + * Set diff algorithm + * + * @param diffAlgorithm + * @return this command + */ + public BlameCommand setDiffAlgorithm(DiffAlgorithm diffAlgorithm) { + this.diffAlgorithm = diffAlgorithm; + return this; + } + + /** + * Set raw text comparator + * + * @param textComparator + * @return this command + */ + public BlameCommand setTextComparator(RawTextComparator textComparator) { + this.textComparator = textComparator; + return this; + } + + /** + * Set start commit id + * + * @param commit + * @return this command + */ + public BlameCommand setStartCommit(AnyObjectId commit) { + this.startCommit = commit.toObjectId(); + return this; + } + + /** + * Enable (or disable) following file renames. + *

+ * If true renames are followed using the standard FollowFilter behavior + * used by RevWalk (which matches {@code git log --follow} in the C + * implementation). This is not the same as copy/move detection as + * implemented by the C implementation's of {@code git blame -M -C}. + * + * @param follow + * enable following. + * @return {@code this} + */ + public BlameCommand setFollowFileRenames(boolean follow) { + followFileRenames = Boolean.valueOf(follow); + return this; + } + + /** + * Configure the command to compute reverse blame (history of deletes). + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commit to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameCommand reverse(AnyObjectId start, AnyObjectId end) + throws IOException { + return reverse(start, Collections.singleton(end.toObjectId())); + } + + /** + * Configure the generator to compute reverse blame (history of deletes). + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commits to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameCommand reverse(AnyObjectId start, Collection end) + throws IOException { + startCommit = start.toObjectId(); + reverseEndCommits = new ArrayList(end); + return this; + } + + /** + * Generate a list of lines with information about when the lines were + * introduced into the file path. + * + * @return list of lines + */ + public BlameResult call() throws JGitInternalException { + checkCallable(); + BlameGenerator gen = new BlameGenerator(repo, path); + try { + if (diffAlgorithm != null) + gen.setDiffAlgorithm(diffAlgorithm); + if (textComparator != null) + gen.setTextComparator(textComparator); + if (followFileRenames != null) + gen.setFollowFileRenames(followFileRenames.booleanValue()); + + if (reverseEndCommits != null) + gen.reverse(startCommit, reverseEndCommits); + else if (startCommit != null) + gen.push(null, startCommit); + else { + gen.push(null, repo.resolve(Constants.HEAD)); + if (!repo.isBare()) { + DirCache dc = repo.readDirCache(); + int entry = dc.findEntry(path); + if (0 <= entry) + gen.push(null, dc.getEntry(entry).getObjectId()); + + File inTree = new File(repo.getWorkTree(), path); + if (inTree.isFile()) + gen.push(null, new RawText(inTree)); + } + } + return gen.computeBlameResult(); + } catch (IOException e) { + throw new JGitInternalException(e.getMessage(), e); + } finally { + gen.release(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index 682455077..4d81e86ef 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -455,6 +455,19 @@ public class Git { return new CleanCommand(repo); } + /** + * Returns a command object to execute a {@code blame} command + * + * @see Git documentation about Blame + * @return a {@link BlameCommand} used to collect all optional parameters + * and to finally execute the {@code blame} command + */ + public BlameCommand blame() { + return new BlameCommand(repo); + } + /** * @return the git repository this class is interacting with */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java new file mode 100644 index 000000000..3b8e41eb1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java @@ -0,0 +1,960 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jgit.JGitText; +import org.eclipse.jgit.blame.Candidate.BlobCandidate; +import org.eclipse.jgit.blame.Candidate.ReverseCandidate; +import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; +import org.eclipse.jgit.diff.DiffAlgorithm; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.diff.HistogramDiff; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.diff.RenameDetector; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +/** + * Generate author information for lines based on introduction to the file. + *

+ * Applications that want a simple one-shot computation of blame for a file + * should use {@link #computeBlameResult()} to prepare the entire result in one + * method call. This may block for significant time as the history of the + * repository must be traversed until information is gathered for every line. + *

+ * Applications that want more incremental update behavior may use either the + * raw {@link #next()} streaming approach supported by this class, or construct + * a {@link BlameResult} using {@link BlameResult#create(BlameGenerator)} and + * incrementally construct the result with {@link BlameResult#computeNext()}. + *

+ * This class is not thread-safe. + *

+ * An instance of BlameGenerator can only be used once. To blame multiple files + * the application must create a new BlameGenerator. + *

+ * During blame processing there are two files involved: + *

+ *

+ * The blame algorithm is implemented by initially assigning responsibility for + * all lines of the result to the starting commit. A difference against the + * commit's ancestor is computed, and responsibility is passed to the ancestor + * commit for any lines that are common. The starting commit is blamed only for + * the lines that do not appear in the ancestor, if any. The loop repeats using + * the ancestor, until there are no more lines to acquire information on, or the + * file's creation point is discovered in history. + */ +public class BlameGenerator { + private final Repository repository; + + private final PathFilter resultPath; + + private final MutableObjectId idBuf; + + /** Revision pool used to acquire commits from. */ + private RevWalk revPool; + + /** Indicates the commit has already been processed. */ + private RevFlag SEEN; + + private ObjectReader reader; + + private TreeWalk treeWalk; + + private DiffAlgorithm diffAlgorithm = new HistogramDiff(); + + private RawTextComparator textComparator = RawTextComparator.DEFAULT; + + private RenameDetector renameDetector; + + /** Potential candidates, sorted by commit time descending. */ + private Candidate queue; + + /** Number of lines that still need to be discovered. */ + private int remaining; + + /** Blame is currently assigned to this source. */ + private Candidate currentSource; + + /** + * Create a blame generator for the repository and path + * + * @param repository + * repository to access revision data from. + * @param path + * initial path of the file to start scanning. + */ + public BlameGenerator(Repository repository, String path) { + this.repository = repository; + this.resultPath = PathFilter.create(path); + + idBuf = new MutableObjectId(); + setFollowFileRenames(true); + initRevPool(false); + + remaining = -1; + } + + private void initRevPool(boolean reverse) { + if (queue != null) + throw new IllegalStateException(); + + if (revPool != null) + revPool.release(); + + if (reverse) + revPool = new ReverseWalk(getRepository()); + else + revPool = new RevWalk(getRepository()); + + revPool.setRetainBody(true); + SEEN = revPool.newFlag("SEEN"); + reader = revPool.getObjectReader(); + treeWalk = new TreeWalk(reader); + } + + /** @return repository being scanned for revision history. */ + public Repository getRepository() { + return repository; + } + + /** @return path file path being processed. */ + public String getResultPath() { + return resultPath.getPath(); + } + + /** + * Difference algorithm to use when comparing revisions. + * + * @param algorithm + * @return {@code this} + */ + public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) { + diffAlgorithm = algorithm; + return this; + } + + /** + * Text comparator to use when comparing revisions. + * + * @param comparator + * @return {@code this} + */ + public BlameGenerator setTextComparator(RawTextComparator comparator) { + textComparator = comparator; + return this; + } + + /** + * Enable (or disable) following file renames, on by default. + *

+ * If true renames are followed using the standard FollowFilter behavior + * used by RevWalk (which matches {@code git log --follow} in the C + * implementation). This is not the same as copy/move detection as + * implemented by the C implementation's of {@code git blame -M -C}. + * + * @param follow + * enable following. + * @return {@code this} + */ + public BlameGenerator setFollowFileRenames(boolean follow) { + if (follow) + renameDetector = new RenameDetector(getRepository()); + else + renameDetector = null; + return this; + } + + /** + * Obtain the RenameDetector if {@code setFollowFileRenames(true)}. + * + * @return the rename detector, allowing the application to configure its + * settings for rename score and breaking behavior. + */ + public RenameDetector getRenameDetector() { + return renameDetector; + } + + /** + * Push a candidate blob 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 description + * description of the blob revision, such as "Working Tree". + * @param contents + * contents of the file. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator push(String description, byte[] contents) + throws IOException { + return push(description, new RawText(contents)); + } + + /** + * Push a candidate blob 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 description + * description of the blob revision, such as "Working Tree". + * @param contents + * contents of the file. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator push(String description, RawText contents) + throws IOException { + if (description == null) + description = JGitText.get().blameNotCommittedYet; + BlobCandidate c = new BlobCandidate(description, resultPath); + c.sourceText = contents; + c.regionList = new Region(0, 0, contents.size()); + remaining = contents.size(); + push(c); + return this; + } + + /** + * 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 description + * description of the blob revision, such as "Working Tree". + * @param id + * may be a commit or a blob. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator push(String description, AnyObjectId id) + throws IOException { + ObjectLoader ldr = reader.open(id); + if (ldr.getType() == OBJ_BLOB) { + if (description == null) + description = JGitText.get().blameNotCommittedYet; + BlobCandidate c = new BlobCandidate(description, resultPath); + c.sourceBlob = id.toObjectId(); + c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); + c.regionList = new Region(0, 0, c.sourceText.size()); + remaining = c.sourceText.size(); + push(c); + return this; + } + + RevCommit commit = revPool.parseCommit(id); + if (!find(commit, resultPath)) + return this; + + Candidate c = new Candidate(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; + } + + /** + * Configure the generator to compute reverse blame (history of deletes). + *

+ * This method is expensive as it immediately runs a RevWalk over the + * history spanning the expression {@code start..end} (end being more recent + * than start) and then performs the equivalent operation as + * {@link #push(String, AnyObjectId)} to begin blame traversal from the + * commit named by {@code start} walking forwards through history until + * {@code end} blaming line deletions. + *

+ * A reverse blame may produce multiple sources for the same result line, + * each of these is a descendant commit that removed the line, typically + * this occurs when the same deletion appears in multiple side branches such + * as due to a cherry-pick. Applications relying on reverse should use + * {@link BlameResult} as it filters these duplicate sources and only + * remembers the first (oldest) deletion. + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commit to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator reverse(AnyObjectId start, AnyObjectId end) + throws IOException { + return reverse(start, Collections.singleton(end.toObjectId())); + } + + /** + * Configure the generator to compute reverse blame (history of deletes). + *

+ * This method is expensive as it immediately runs a RevWalk over the + * history spanning the expression {@code start..end} (end being more recent + * than start) and then performs the equivalent operation as + * {@link #push(String, AnyObjectId)} to begin blame traversal from the + * commit named by {@code start} walking forwards through history until + * {@code end} blaming line deletions. + *

+ * A reverse blame may produce multiple sources for the same result line, + * each of these is a descendant commit that removed the line, typically + * this occurs when the same deletion appears in multiple side branches such + * as due to a cherry-pick. Applications relying on reverse should use + * {@link BlameResult} as it filters these duplicate sources and only + * remembers the first (oldest) deletion. + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commits to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator reverse(AnyObjectId start, + Collection end) throws IOException { + initRevPool(true); + + ReverseCommit result = (ReverseCommit) revPool.parseCommit(start); + if (!find(result, resultPath)) + return this; + + revPool.markUninteresting(result); + for (ObjectId id : end) + revPool.markStart(revPool.parseCommit(id)); + + while (revPool.next() != null) { + // just pump the queue + } + + ReverseCandidate c = new ReverseCandidate(result, 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; + } + + /** + * Execute the generator in a blocking fashion until all data is ready. + * + * @return the complete result. Null if no file exists for the given path. + * @throws IOException + * the repository cannot be read. + */ + public BlameResult computeBlameResult() throws IOException { + try { + BlameResult r = BlameResult.create(this); + if (r != null) + r.computeAll(); + return r; + } finally { + release(); + } + } + + /** + * Step the blame algorithm one iteration. + * + * @return true if the generator has found a region's source. The getSource* + * and {@link #getResultStart()}, {@link #getResultEnd()} methods + * can be used to inspect the region found. False if there are no + * more regions to describe. + * @throws IOException + * repository cannot be read. + */ + public boolean next() throws IOException { + // If there is a source still pending, produce the next region. + if (currentSource != null) { + Region r = currentSource.regionList; + Region n = r.next; + remaining -= r.length; + if (n != null) { + currentSource.regionList = n; + return true; + } + + if (currentSource.queueNext != null) + return result(currentSource.queueNext); + + currentSource = null; + } + + // If there are no lines remaining, the entire result is done, + // even if there are revisions still available for the path. + if (remaining == 0) + return done(); + + for (;;) { + Candidate n = pop(); + if (n == null) + return done(); + + int pCnt = n.getParentCount(); + if (pCnt == 1) { + if (processOne(n)) + return true; + + } else if (1 < pCnt) { + if (processMerge(n)) + return true; + + } else if (n instanceof ReverseCandidate) { + // Do not generate a tip of a reverse. The region + // survives and should not appear to be deleted. + + } else /* if (pCnt == 0) */{ + // Root commit, with at least one surviving region. + // Assign the remaining blame here. + return result(n); + } + } + } + + private boolean done() { + release(); + return false; + } + + private boolean result(Candidate n) throws IOException { + if (n.sourceCommit != null) + revPool.parseBody(n.sourceCommit); + currentSource = n; + return true; + } + + private boolean reverseResult(Candidate parent, Candidate source) + throws IOException { + // On a reverse blame present the application the parent + // (as this is what did the removals), however the region + // list to enumerate is the source's surviving list. + Candidate res = parent.copy(parent.sourceCommit); + res.regionList = source.regionList; + return result(res); + } + + private Candidate pop() { + Candidate n = queue; + if (n != null) { + queue = n.queueNext; + n.queueNext = null; + } + return n; + } + + private void push(BlobCandidate toInsert) { + Candidate c = queue; + if (c != null) { + c.regionList = null; + toInsert.parent = c; + } + queue = toInsert; + } + + private void push(Candidate toInsert) { + // Mark sources to ensure they get discarded (above) if + // another path to the same commit. + toInsert.add(SEEN); + + // Insert into the queue using descending commit time, so + // the most recent commit will pop next. + int time = toInsert.getTime(); + Candidate n = queue; + if (n == null || time >= n.getTime()) { + toInsert.queueNext = n; + queue = toInsert; + return; + } + + for (Candidate p = n;; p = n) { + n = p.queueNext; + if (n == null || time >= n.getTime()) { + toInsert.queueNext = n; + p.queueNext = toInsert; + return; + } + } + } + + private boolean processOne(Candidate n) throws IOException { + RevCommit parent = n.getParent(0); + if (parent == null) + return split(n.getNextCandidate(0), n); + if (parent.has(SEEN)) + return false; + revPool.parseHeaders(parent); + + if (find(parent, n.sourcePath)) { + if (idBuf.equals(n.sourceBlob)) { + // The common case of the file not being modified in + // a simple string-of-pearls history. Blame parent. + n.sourceCommit = parent; + push(n); + return false; + } + + Candidate next = n.create(parent, n.sourcePath); + next.sourceBlob = idBuf.toObjectId(); + next.loadText(reader); + return split(next, n); + } + + if (n.sourceCommit == null) + return result(n); + + DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath); + if (r == null) + return result(n); + + if (0 == r.getOldId().prefixCompare(n.sourceBlob)) { + // A 100% rename without any content change can also + // skip directly to the parent. + n.sourceCommit = parent; + n.sourcePath = PathFilter.create(r.getOldPath()); + push(n); + return false; + } + + Candidate next = n.create(parent, PathFilter.create(r.getOldPath())); + next.sourceBlob = r.getOldId().toObjectId(); + next.renameScore = r.getScore(); + next.loadText(reader); + return split(next, n); + } + + private boolean split(Candidate parent, Candidate source) + throws IOException { + EditList editList = diffAlgorithm.diff(textComparator, + parent.sourceText, source.sourceText); + if (editList.isEmpty()) { + // Ignoring whitespace (or some other special comparator) can + // cause non-identical blobs to have an empty edit list. In + // a case like this push the parent alone. + parent.regionList = source.regionList; + push(parent); + return false; + } + + parent.takeBlame(editList, source); + if (parent.regionList != null) + push(parent); + if (source.regionList != null) { + if (source instanceof ReverseCandidate) + return reverseResult(parent, source); + return result(source); + } + return false; + } + + private boolean processMerge(Candidate n) throws IOException { + int pCnt = n.getParentCount(); + + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + revPool.parseHeaders(parent); + } + + // If any single parent exactly matches the merge, follow only + // that one parent through history. + ObjectId[] ids = null; + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + if (!find(parent, n.sourcePath)) + continue; + if (!(n instanceof ReverseCandidate) && idBuf.equals(n.sourceBlob)) { + n.sourceCommit = parent; + push(n); + return false; + } + if (ids == null) + ids = new ObjectId[pCnt]; + ids[pIdx] = idBuf.toObjectId(); + } + + // If rename detection is enabled, search for any relevant names. + DiffEntry[] renames = null; + if (renameDetector != null) { + renames = new DiffEntry[pCnt]; + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + if (ids != null && ids[pIdx] != null) + continue; + + DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath); + if (r == null) + continue; + + if (n instanceof ReverseCandidate) { + if (ids == null) + ids = new ObjectId[pCnt]; + ids[pCnt] = r.getOldId().toObjectId(); + } else if (0 == r.getOldId().prefixCompare(n.sourceBlob)) { + // A 100% rename without any content change can also + // skip directly to the parent. Note this bypasses an + // earlier parent that had the path (above) but did not + // have an exact content match. For performance reasons + // we choose to follow the one parent over trying to do + // possibly both parents. + n.sourceCommit = parent; + n.sourcePath = PathFilter.create(r.getOldPath()); + push(n); + return false; + } + + renames[pIdx] = r; + } + } + + // Construct the candidate for each parent. + Candidate[] parents = new Candidate[pCnt]; + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + + Candidate p; + if (renames != null && renames[pIdx] != null) { + p = n.create(parent, + PathFilter.create(renames[pIdx].getOldPath())); + p.renameScore = renames[pIdx].getScore(); + p.sourceBlob = renames[pIdx].getOldId().toObjectId(); + } else if (ids != null && ids[pIdx] != null) { + p = n.create(parent, n.sourcePath); + p.sourceBlob = ids[pIdx]; + } else { + continue; + } + + EditList editList; + if (n instanceof ReverseCandidate + && p.sourceBlob.equals(n.sourceBlob)) { + // This special case happens on ReverseCandidate forks. + p.sourceText = n.sourceText; + editList = new EditList(0); + } else { + p.loadText(reader); + editList = diffAlgorithm.diff(textComparator, + p.sourceText, n.sourceText); + } + + if (editList.isEmpty()) { + // Ignoring whitespace (or some other special comparator) can + // cause non-identical blobs to have an empty edit list. In + // a case like this push the parent alone. + if (n instanceof ReverseCandidate) { + parents[pIdx] = p; + continue; + } + + p.regionList = n.regionList; + push(p); + return false; + } + + p.takeBlame(editList, n); + + // Only remember this parent candidate if there is at least + // one region that was blamed on the parent. + if (p.regionList != null) { + // Reverse blame requires inverting the regions. This puts + // the regions the parent deleted from us into the parent, + // and retains the common regions to look at other parents + // for deletions. + if (n instanceof ReverseCandidate) { + Region r = p.regionList; + p.regionList = n.regionList; + n.regionList = r; + } + + parents[pIdx] = p; + } + } + + if (n instanceof ReverseCandidate) { + // On a reverse blame report all deletions found in the children, + // and pass on to them a copy of our region list. + Candidate resultHead = null; + Candidate resultTail = null; + + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + Candidate p = parents[pIdx]; + if (p == null) + continue; + + if (p.regionList != null) { + Candidate r = p.copy(p.sourceCommit); + if (resultTail != null) { + resultTail.queueNext = r; + resultTail = r; + } else { + resultHead = r; + resultTail = r; + } + } + + if (n.regionList != null) { + p.regionList = n.regionList.deepCopy(); + push(p); + } + } + + if (resultHead != null) + return result(resultHead); + return false; + } + + // Push any parents that are still candidates. + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + if (parents[pIdx] != null) + push(parents[pIdx]); + } + + if (n.regionList != null) + return result(n); + return false; + } + + /** + * Get the revision blamed for the current region. + *

+ * The source commit may be null if the line was blamed to an uncommitted + * revision, such as the working tree copy, or during a reverse blame if the + * line survives to the end revision (e.g. the branch tip). + * + * @return current revision being blamed. + */ + public RevCommit getSourceCommit() { + return currentSource.sourceCommit; + } + + /** @return current author being blamed. */ + public PersonIdent getSourceAuthor() { + return currentSource.getAuthor(); + } + + /** @return current committer being blamed. */ + public PersonIdent getSourceCommitter() { + RevCommit c = getSourceCommit(); + return c != null ? c.getCommitterIdent() : null; + } + + /** @return path of the file being blamed. */ + public String getSourcePath() { + return currentSource.sourcePath.getPath(); + } + + /** @return rename score if a rename occurred in {@link #getSourceCommit}. */ + public int getRenameScore() { + return currentSource.renameScore; + } + + /** + * @return first line of the source data that has been blamed for the + * current region. This is line number of where the region was added + * during {@link #getSourceCommit()} in file + * {@link #getSourcePath()}. + */ + public int getSourceStart() { + return currentSource.regionList.sourceStart; + } + + /** + * @return one past the range of the source data that has been blamed for + * the current region. This is line number of where the region was + * added during {@link #getSourceCommit()} in file + * {@link #getSourcePath()}. + */ + public int getSourceEnd() { + Region r = currentSource.regionList; + return r.sourceStart + r.length; + } + + /** + * @return first line of the result that {@link #getSourceCommit()} has been + * blamed for providing. Line numbers use 0 based indexing. + */ + public int getResultStart() { + return currentSource.regionList.resultStart; + } + + /** + * @return one past the range of the result that {@link #getSourceCommit()} + * has been blamed for providing. Line numbers use 0 based indexing. + * Because a source cannot be blamed for an empty region of the + * result, {@link #getResultEnd()} is always at least one larger + * than {@link #getResultStart()}. + */ + public int getResultEnd() { + Region r = currentSource.regionList; + return r.resultStart + r.length; + } + + /** + * @return number of lines in the current region being blamed to + * {@link #getSourceCommit()}. This is always the value of the + * expression {@code getResultEnd() - getResultStart()}, but also + * {@code getSourceEnd() - getSourceStart()}. + */ + public int getRegionLength() { + return currentSource.regionList.length; + } + + /** + * @return complete contents of the source file blamed for the current + * output region. This is the contents of {@link #getSourcePath()} + * within {@link #getSourceCommit()}. The source contents is + * temporarily available as an artifact of the blame algorithm. Most + * applications will want the result contents for display to users. + */ + public RawText getSourceContents() { + return currentSource.sourceText; + } + + /** + * @return complete file contents of the result file blame is annotating. + * This value is accessible only after being configured and only + * immediately before the first call to {@link #next()}. Returns + * null if the path does not exist. + * @throws IOException + * repository cannot be read. + * @throws IllegalStateException + * {@link #next()} has already been invoked. + */ + public RawText getResultContents() throws IOException { + return queue != null ? queue.sourceText : null; + } + + /** Release the current blame session. */ + public void release() { + revPool.release(); + queue = null; + currentSource = null; + } + + private boolean find(RevCommit commit, PathFilter path) throws IOException { + treeWalk.setFilter(path); + treeWalk.reset(commit.getTree()); + while (treeWalk.next()) { + if (path.isDone(treeWalk)) { + if (treeWalk.getFileMode(0).getObjectType() != OBJ_BLOB) + return false; + treeWalk.getObjectId(idBuf, 0); + return true; + } + + if (treeWalk.isSubtree()) + treeWalk.enterSubtree(); + } + return false; + } + + private DiffEntry findRename(RevCommit parent, RevCommit commit, + PathFilter path) throws IOException { + if (renameDetector == null) + return null; + + treeWalk.setFilter(TreeFilter.ANY_DIFF); + treeWalk.reset(parent.getTree(), commit.getTree()); + renameDetector.addAll(DiffEntry.scan(treeWalk)); + for (DiffEntry ent : renameDetector.compute()) { + if (isRename(ent) && ent.getNewPath().equals(path.getPath())) + return ent; + } + return null; + } + + private static boolean isRename(DiffEntry ent) { + return ent.getChangeType() == ChangeType.RENAME + || ent.getChangeType() == ChangeType.COPY; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java new file mode 100644 index 000000000..d7a958fee --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import java.io.IOException; + +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * Collects line annotations for inspection by applications. + *

+ * A result is usually updated incrementally as the BlameGenerator digs back + * further through history. Applications that want to lay annotations down text + * to the original source file in a viewer may find the BlameResult structure an + * easy way to acquire the information, at the expense of keeping tables in + * memory tracking every line of the result file. + *

+ * This class is not thread-safe. + *

+ * During blame processing there are two files involved: + *

+ */ +public class BlameResult { + /** + * Construct a new BlameResult for a generator. + * + * @param gen + * the generator the result will consume records from. + * @return the new result object. null if the generator cannot find the path + * it starts from. + * @throws IOException + * the repository cannot be read. + */ + public static BlameResult create(BlameGenerator gen) throws IOException { + String path = gen.getResultPath(); + RawText contents = gen.getResultContents(); + if (contents == null) { + gen.release(); + return null; + } + return new BlameResult(gen, path, contents); + } + + private final String resultPath; + + private final RevCommit[] sourceCommits; + + private final PersonIdent[] sourceAuthors; + + private final PersonIdent[] sourceCommitters; + + private final String[] sourcePaths; + + /** Warning: these are actually 1-based. */ + private final int[] sourceLines; + + private RawText resultContents; + + private BlameGenerator generator; + + private int lastLength; + + BlameResult(BlameGenerator bg, String path, RawText text) { + generator = bg; + resultPath = path; + resultContents = text; + + int cnt = text.size(); + sourceCommits = new RevCommit[cnt]; + sourceAuthors = new PersonIdent[cnt]; + sourceCommitters = new PersonIdent[cnt]; + sourceLines = new int[cnt]; + sourcePaths = new String[cnt]; + } + + /** @return path of the file this result annotates. */ + public String getResultPath() { + return resultPath; + } + + /** @return contents of the result file, available for display. */ + public RawText getResultContents() { + return resultContents; + } + + /** Throw away the {@link #getResultContents()}. */ + public void discardResultContents() { + resultContents = null; + } + + /** + * Check if the given result line has been annotated yet. + * + * @param idx + * line to read data of, 0 based. + * @return true if the data has been annotated, false otherwise. + */ + public boolean hasSourceData(int idx) { + return sourceLines[idx] != 0; + } + + /** + * Check if the given result line has been annotated yet. + * + * @param start + * first index to examine. + * @param end + * last index to examine. + * @return true if the data has been annotated, false otherwise. + */ + public boolean hasSourceData(int start, int end) { + for (; start < end; start++) + if (sourceLines[start] == 0) + return false; + return true; + } + + /** + * Get the commit that provided the specified line of the result. + *

+ * The source commit may be null if the line was blamed to an uncommitted + * revision, such as the working tree copy, or during a reverse blame if the + * line survives to the end revision (e.g. the branch tip). + * + * @param idx + * line to read data of, 0 based. + * @return commit that provided line {@code idx}. May be null. + */ + public RevCommit getSourceCommit(int idx) { + return sourceCommits[idx]; + } + + /** + * Get the author that provided the specified line of the result. + * + * @param idx + * line to read data of, 0 based. + * @return author that provided line {@code idx}. May be null. + */ + public PersonIdent getSourceAuthor(int idx) { + return sourceAuthors[idx]; + } + + /** + * Get the committer that provided the specified line of the result. + * + * @param idx + * line to read data of, 0 based. + * @return committer that provided line {@code idx}. May be null. + */ + public PersonIdent getSourceCommitter(int idx) { + return sourceCommitters[idx]; + } + + /** + * Get the file path that provided the specified line of the result. + * + * @param idx + * line to read data of, 0 based. + * @return source file path that provided line {@code idx}. + */ + public String getSourcePath(int idx) { + return sourcePaths[idx]; + } + + /** + * Get the corresponding line number in the source file. + * + * @param idx + * line to read data of, 0 based. + * @return matching line number in the source file. + */ + public int getSourceLine(int idx) { + return sourceLines[idx] - 1; + } + + /** + * Compute all pending information. + * + * @throws IOException + * the repository cannot be read. + */ + public void computeAll() throws IOException { + BlameGenerator gen = generator; + if (gen == null) + return; + + try { + while (gen.next()) + loadFrom(gen); + } finally { + gen.release(); + generator = null; + } + } + + /** + * Compute the next available segment and return the first index. + *

+ * Computes one segment and returns to the caller the first index that is + * available. After return the caller can also inspect {@link #lastLength()} + * to determine how many lines of the result were computed. + * + * @return index that is now available. -1 if no more are available. + * @throws IOException + * the repository cannot be read. + */ + public int computeNext() throws IOException { + BlameGenerator gen = generator; + if (gen == null) + return -1; + + if (gen.next()) { + loadFrom(gen); + lastLength = gen.getRegionLength(); + return gen.getResultStart(); + } else { + gen.release(); + generator = null; + return -1; + } + } + + /** @return length of the last segment found by {@link #computeNext()}. */ + public int lastLength() { + return lastLength; + } + + /** + * Compute until the entire range has been populated. + * + * @param start + * first index to examine. + * @param end + * last index to examine. + * @throws IOException + * the repository cannot be read. + */ + public void computeRange(int start, int end) throws IOException { + BlameGenerator gen = generator; + if (gen == null) + return; + + while (start < end) { + if (hasSourceData(start, end)) + return; + + if (!gen.next()) { + gen.release(); + generator = null; + return; + } + + loadFrom(gen); + + // If the result contains either end of our current range bounds, + // update the bounds to avoid scanning that section during the + // next loop iteration. + + int resLine = gen.getResultStart(); + int resEnd = gen.getResultEnd(); + + if (resLine <= start && start < resEnd) + start = resEnd; + + if (resLine <= end && end < resEnd) + end = resLine; + } + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append("BlameResult: "); + r.append(getResultPath()); + return r.toString(); + } + + private void loadFrom(BlameGenerator gen) { + RevCommit srcCommit = gen.getSourceCommit(); + PersonIdent srcAuthor = gen.getSourceAuthor(); + PersonIdent srcCommitter = gen.getSourceCommitter(); + String srcPath = gen.getSourcePath(); + int srcLine = gen.getSourceStart(); + int resLine = gen.getResultStart(); + int resEnd = gen.getResultEnd(); + + for (; resLine < resEnd; resLine++) { + // Reverse blame can generate multiple results for the same line. + // Favor the first one selected, as this is the oldest and most + // likely to be nearest to the inquiry made by the user. + if (sourceLines[resLine] != 0) + continue; + + sourceCommits[resLine] = srcCommit; + sourceAuthors[resLine] = srcAuthor; + sourceCommitters[resLine] = srcCommitter; + sourcePaths[resLine] = srcPath; + + // Since sourceLines is 1-based to permit hasSourceData to use 0 to + // mean the line has not been annotated yet, pre-increment instead + // of the traditional post-increment when making the assignment. + sourceLines[resLine] = ++srcLine; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java new file mode 100644 index 000000000..5f20ce957 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import java.io.IOException; + +import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; +import org.eclipse.jgit.diff.Edit; +import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +/** + * A source that may have supplied some (or all) of the result file. + *

+ * Candidates are kept in a queue by BlameGenerator, allowing the generator to + * perform a parallel search down the parents of any merges that are discovered + * during the history traversal. Each candidate retains a {@link #regionList} + * describing sections of the result file the candidate has taken responsibility + * for either directly or indirectly through its history. Actual blame from this + * region list will be assigned to the candidate when its ancestor commit(s) are + * themselves converted into Candidate objects and the ancestor's candidate uses + * {@link #takeBlame(EditList, Candidate)} to accept responsibility for sections + * of the result. + */ +class Candidate { + /** Next candidate in the candidate queue. */ + Candidate queueNext; + + /** Commit being considered (or blamed, depending on state). */ + RevCommit sourceCommit; + + /** Path of the candidate file in {@link #sourceCommit}. */ + PathFilter sourcePath; + + /** Unique name of the candidate blob in {@link #sourceCommit}. */ + ObjectId sourceBlob; + + /** Complete contents of the file in {@link #sourceCommit}. */ + RawText sourceText; + + /** + * Chain of regions this candidate may be blamed for. + *

+ * This list is always kept sorted by resultStart order, making it simple to + * merge-join with the sorted EditList during blame assignment. + */ + Region regionList; + + /** + * Score assigned to the rename to this candidate. + *

+ * Consider the history "A<-B<-C". If the result file S in C was renamed to + * R in B, the rename score for this rename will be held in this field by + * the candidate object for B. By storing the score with B, the application + * can see what the rename score was as it makes the transition from C/S to + * B/R. This may seem backwards since it was C that performed the rename, + * but the application doesn't learn about path R until B. + */ + int renameScore; + + Candidate(RevCommit commit, PathFilter path) { + sourceCommit = commit; + sourcePath = path; + } + + int getParentCount() { + return sourceCommit.getParentCount(); + } + + RevCommit getParent(int idx) { + return sourceCommit.getParent(idx); + } + + Candidate getNextCandidate(@SuppressWarnings("unused") int idx) { + return null; + } + + void add(RevFlag flag) { + sourceCommit.add(flag); + } + + int getTime() { + return sourceCommit.getCommitTime(); + } + + PersonIdent getAuthor() { + return sourceCommit.getAuthorIdent(); + } + + Candidate create(RevCommit commit, PathFilter path) { + return new Candidate(commit, path); + } + + Candidate copy(RevCommit commit) { + Candidate r = create(commit, sourcePath); + r.sourceBlob = sourceBlob; + r.sourceText = sourceText; + r.regionList = regionList; + r.renameScore = renameScore; + return r; + } + + void loadText(ObjectReader reader) throws IOException { + ObjectLoader ldr = reader.open(sourceBlob, Constants.OBJ_BLOB); + sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); + } + + void takeBlame(EditList editList, Candidate child) { + blame(editList, this, child); + } + + private static void blame(EditList editList, Candidate a, Candidate b) { + Region r = b.clearRegionList(); + Region aTail = null; + Region bTail = null; + + for (int eIdx = 0; eIdx < editList.size();) { + // If there are no more regions left, neither side has any + // more responsibility for the result. Remaining edits can + // be safely ignored. + if (r == null) + return; + + Edit e = editList.get(eIdx); + + // Edit ends before the next candidate region. Skip the edit. + if (e.getEndB() <= r.sourceStart) { + eIdx++; + continue; + } + + // Next candidate region starts before the edit. Assign some + // of the blame onto A, but possibly split and also on B. + if (r.sourceStart < e.getBeginB()) { + int d = e.getBeginB() - r.sourceStart; + if (r.length <= d) { + // Pass the blame for this region onto A. + Region next = r.next; + r.sourceStart = e.getBeginA() - d; + aTail = add(aTail, a, r); + r = next; + continue; + } + + // Split the region and assign some to A, some to B. + aTail = add(aTail, a, r.splitFirst(e.getBeginA() - d, d)); + r.slideAndShrink(d); + } + + // At this point e.getBeginB() <= r.sourceStart. + + // An empty edit on the B side isn't relevant to this split, + // as it does not overlap any candidate region. + if (e.getLengthB() == 0) { + eIdx++; + continue; + } + + // If the region ends before the edit, blame on B. + int rEnd = r.sourceStart + r.length; + if (rEnd <= e.getEndB()) { + Region next = r.next; + bTail = add(bTail, b, r); + r = next; + if (rEnd == e.getEndB()) + eIdx++; + continue; + } + + // This region extends beyond the edit. Blame the first + // half of the region on B, and process the rest after. + int len = e.getEndB() - r.sourceStart; + bTail = add(bTail, b, r.splitFirst(r.sourceStart, len)); + r.slideAndShrink(len); + eIdx++; + } + + if (r == null) + return; + + // For any remaining region, pass the blame onto A after shifting + // the source start to account for the difference between the two. + Edit e = editList.get(editList.size() - 1); + int endB = e.getEndB(); + int d = endB - e.getEndA(); + if (aTail == null) + a.regionList = r; + else + aTail.next = r; + do { + if (endB <= r.sourceStart) + r.sourceStart -= d; + r = r.next; + } while (r != null); + } + + private static Region add(Region aTail, Candidate a, Region n) { + // If there is no region on the list, use only this one. + if (aTail == null) { + a.regionList = n; + n.next = null; + return n; + } + + // If the prior region ends exactly where the new region begins + // in both the result and the source, combine these together into + // one contiguous region. This occurs when intermediate commits + // have inserted and deleted lines in the middle of a region. Try + // to report this region as a single region to the application, + // rather than in fragments. + if (aTail.resultStart + aTail.length == n.resultStart + && aTail.sourceStart + aTail.length == n.sourceStart) { + aTail.length += n.length; + return aTail; + } + + // Append the region onto the end of the list. + aTail.next = n; + n.next = null; + return n; + } + + private Region clearRegionList() { + Region r = regionList; + regionList = null; + return r; + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append("Candidate["); + r.append(sourcePath.getPath()); + if (sourceCommit != null) + r.append(" @ ").append(sourceCommit.abbreviate(6).name()); + if (regionList != null) + r.append(" regions:").append(regionList); + r.append("]"); + return r.toString(); + } + + /** + * Special candidate type used for reverse blame. + *

+ * Reverse blame inverts the commit history graph to follow from a commit to + * its descendant children, rather than the normal history direction of + * child to parent. These types require a {@link ReverseCommit} which keeps + * children pointers, allowing reverse navigation of history. + */ + static final class ReverseCandidate extends Candidate { + ReverseCandidate(ReverseCommit commit, PathFilter path) { + super(commit, path); + } + + @Override + int getParentCount() { + return ((ReverseCommit) sourceCommit).getChildCount(); + } + + @Override + RevCommit getParent(int idx) { + return ((ReverseCommit) sourceCommit).getChild(idx); + } + + @Override + int getTime() { + // Invert the timestamp so newer dates sort older. + return -sourceCommit.getCommitTime(); + } + + @Override + Candidate create(RevCommit commit, PathFilter path) { + return new ReverseCandidate((ReverseCommit) commit, path); + } + + @Override + public String toString() { + return "Reverse" + super.toString(); + } + } + + /** + * Candidate loaded from a file source, and not a commit. + *

+ * The {@link Candidate#sourceCommit} field is always null on this type of + * candidate. Instead history traversal follows the single {@link #parent} + * field to discover the next Candidate. Often this is a normal Candidate + * type that has a valid sourceCommit. + */ + static final class BlobCandidate extends Candidate { + /** + * Next candidate to pass blame onto. + *

+ * When computing the differences that this candidate introduced to the + * file content, the parent's sourceText is used as the base. + */ + Candidate parent; + + /** Author name to refer to this blob with. */ + String description; + + BlobCandidate(String name, PathFilter path) { + super(null, path); + description = name; + } + + @Override + int getParentCount() { + return parent != null ? 1 : 0; + } + + @Override + RevCommit getParent(int idx) { + return null; + } + + @Override + Candidate getNextCandidate(int idx) { + return parent; + } + + @Override + void add(RevFlag flag) { + // Do nothing, sourceCommit is null. + } + + @Override + int getTime() { + return Integer.MAX_VALUE; + } + + @Override + PersonIdent getAuthor() { + return new PersonIdent(description, null); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java new file mode 100644 index 000000000..9ea346b89 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +/** + * Region of the result that still needs to be computed. + *

+ * Regions are held in a singly-linked-list by {@link Candidate} using the + * {@link Candidate#regionList} field. The list is kept in sorted order by + * {@link #resultStart}. + */ +class Region { + /** Next entry in the region linked list. */ + Region next; + + /** First position of this region in the result file blame is computing. */ + int resultStart; + + /** First position in the {@link Candidate} that owns this Region. */ + int sourceStart; + + /** Length of the region, always >= 1. */ + int length; + + Region(int rs, int ss, int len) { + resultStart = rs; + sourceStart = ss; + length = len; + } + + /** + * Copy the entire result region, but at a new source position. + * + * @param newSource + * the new source position. + * @return the same result region, but offset for a new source. + */ + Region copy(int newSource) { + return new Region(resultStart, newSource, length); + } + + /** + * Split the region, assigning a new source position to the first half. + * + * @param newSource + * the new source position. + * @param newLen + * length of the new region. + * @return the first half of the region, at the new source. + */ + Region splitFirst(int newSource, int newLen) { + return new Region(resultStart, newSource, newLen); + } + + /** + * Edit this region to remove the first {@code d} elements. + * + * @param d + * number of elements to remove from the start of this region. + */ + void slideAndShrink(int d) { + resultStart += d; + sourceStart += d; + length -= d; + } + + Region deepCopy() { + Region head = new Region(resultStart, sourceStart, length); + Region tail = head; + for (Region n = next; n != null; n = n.next) { + Region q = new Region(n.resultStart, n.sourceStart, n.length); + tail.next = q; + tail = q; + } + return head; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + Region r = this; + do { + if (r != this) + buf.append(','); + buf.append(r.resultStart); + buf.append('-'); + buf.append(r.resultStart + r.length); + r = r.next; + } while (r != null); + return buf.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java new file mode 100644 index 000000000..5b59804cf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +final class ReverseWalk extends RevWalk { + ReverseWalk(Repository repo) { + super(repo); + } + + @Override + public ReverseCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + ReverseCommit c = (ReverseCommit) super.next(); + if (c == null) + return null; + for (int pIdx = 0; pIdx < c.getParentCount(); pIdx++) + ((ReverseCommit) c.getParent(pIdx)).addChild(c); + return c; + } + + @Override + protected RevCommit createCommit(AnyObjectId id) { + return new ReverseCommit(id); + } + + static final class ReverseCommit extends RevCommit { + private static final ReverseCommit[] NO_CHILDREN = {}; + + private ReverseCommit[] children = NO_CHILDREN; + + ReverseCommit(AnyObjectId id) { + super(id); + } + + void addChild(ReverseCommit c) { + // Always put the most recent child onto the front of the list. + // This works correctly because our ReverseWalk parent (above) + // runs in COMMIT_TIME_DESC order. Older commits will be popped + // later and should go in front of the children list so they are + // visited first by BlameGenerator when considering candidates. + + int cnt = children.length; + if (cnt == 0) + children = new ReverseCommit[] { c }; + else if (cnt == 1) + children = new ReverseCommit[] { c, children[0] }; + else { + ReverseCommit[] n = new ReverseCommit[1 + cnt]; + n[0] = c; + System.arraycopy(children, 0, n, 1, cnt); + children = n; + } + } + + int getChildCount() { + return children.length; + } + + ReverseCommit getChild(final int nth) { + return children[nth]; + } + } +}