Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.test/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BlameCommandTest.java271
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java143
-rw-r--r--org.eclipse.jgit/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java227
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java960
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java356
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java386
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java133
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java113
13 files changed, 2606 insertions, 0 deletions
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 8ec0b6582f..b1736472ee 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 0000000000..1f67807e46
--- /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 0000000000..657cf38708
--- /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 e2d1a1e0ab..0788b8c621 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 8eab4122c6..5a40edd7dc 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 366028a2a7..02538a2dce 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 0000000000..400d94bc88
--- /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<BlameResult> {
+
+ private String path;
+
+ private DiffAlgorithm diffAlgorithm;
+
+ private RawTextComparator textComparator;
+
+ private ObjectId startCommit;
+
+ private Collection<ObjectId> 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.
+ * <p>
+ * 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<ObjectId> end)
+ throws IOException {
+ startCommit = start.toObjectId();
+ reverseEndCommits = new ArrayList<ObjectId>(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 682455077f..4d81e86ef6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -456,6 +456,19 @@ public class Git {
}
/**
+ * Returns a command object to execute a {@code blame} command
+ *
+ * @see <a
+ * href="http://www.kernel.org/pub/software/scm/git/docs/git-blame.html"
+ * >Git documentation about Blame</a>
+ * @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
*/
public Repository getRepository() {
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 0000000000..3b8e41eb16
--- /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.
+ * <p>
+ * 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.
+ * <p>
+ * 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()}.
+ * <p>
+ * This class is not thread-safe.
+ * <p>
+ * An instance of BlameGenerator can only be used once. To blame multiple files
+ * the application must create a new BlameGenerator.
+ * <p>
+ * During blame processing there are two files involved:
+ * <ul>
+ * <li>result - The file whose lines are being examined. This is the revision
+ * the user is trying to view blame/annotation information alongside of.</li>
+ * <li>source - The file that was blamed with supplying one or more lines of
+ * data into result. The source may be a different file path (due to copy or
+ * rename). Source line numbers may differ from result line numbers due to lines
+ * being added/removed in intermediate revisions.</li>
+ * </ul>
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <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 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.
+ * <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 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.
+ * <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 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).
+ * <p>
+ * 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.
+ * <p>
+ * 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).
+ * <p>
+ * 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.
+ * <p>
+ * 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<? extends ObjectId> 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.
+ * <p>
+ * 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 0000000000..d7a958feeb
--- /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.
+ * <p>
+ * 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.
+ * <p>
+ * This class is not thread-safe.
+ * <p>
+ * During blame processing there are two files involved:
+ * <ul>
+ * <li>result - The file whose lines are being examined. This is the revision
+ * the user is trying to view blame/annotation information alongside of.</li>
+ * <li>source - The file that was blamed with supplying one or more lines of
+ * data into result. The source may be a different file path (due to copy or
+ * rename). Source line numbers may differ from result line numbers due to lines
+ * being added/removed in intermediate revisions.</li>
+ * </ul>
+ */
+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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 0000000000..5f20ce9577
--- /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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 0000000000..9ea346b894
--- /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.
+ * <p>
+ * 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 0000000000..5b59804cf1
--- /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];
+ }
+ }
+}

Back to the top