Browse Source
* stable-1.0: Prepare post v1.0.0.201106011211-rc3 builds JGit v1.0.0.201106011211-rc3 Remove incubation marker blame: Compute the origin of lines in a result filestable-1.1
Matthias Sohn
14 years ago
37 changed files with 2637 additions and 31 deletions
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit Ant Tasks Tests (Incubation) |
plugin_name=JGit Ant Tasks Tests |
||||||
provider_name=Eclipse JGit |
provider_name=Eclipse JGit |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
#Properties file for org.eclipse.jgit.ant |
#Properties file for org.eclipse.jgit.ant |
||||||
Bundle-Name = JGit Ant Tasks (Incubation) |
Bundle-Name = JGit Ant Tasks |
||||||
Provider-Name = Eclipse JGit |
Provider-Name = Eclipse JGit |
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit Console User Interface (Incubation) |
plugin_name=JGit Console User Interface |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit DHT Storage Protocol Buffer Messages (Incubation) |
plugin_name=JGit DHT Storage Protocol Buffer Messages |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit HTTP Server (Incubation) |
plugin_name=JGit HTTP Server |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit HTTP Tests (Incubation) |
plugin_name=JGit HTTP Tests |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit IP Log Generator (Incubation) |
plugin_name=JGit IP Log Generator |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit JUnit Http Utility Classes (Incubation) |
plugin_name=JGit JUnit Http Utility Classes |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit JUnit Utility Classes (Incubation) |
plugin_name=JGit JUnit Utility Classes |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit Command Line Interface (Incubation) |
plugin_name=JGit Command Line Interface |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit DHT Storage Tests (Incubation) |
plugin_name=JGit DHT Storage Tests |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit DHT Storage (Incubation) |
plugin_name=JGit DHT Storage |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit Core Tests (Incubation) |
plugin_name=JGit Core Tests |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -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)); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit AWT User Interface (Incubation) |
plugin_name=JGit AWT User Interface |
||||||
provider_name=Eclipse.org |
provider_name=Eclipse.org |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
plugin_name=JGit Core (Incubation) |
plugin_name=JGit Core |
||||||
provider_name=Eclipse JGit |
provider_name=Eclipse JGit |
||||||
|
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue