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 |
||||
|
@ -1,3 +1,3 @@
|
||||
#Properties file for org.eclipse.jgit.ant |
||||
Bundle-Name = JGit Ant Tasks (Incubation) |
||||
Bundle-Name = JGit Ant Tasks |
||||
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 |
||||
|
@ -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 |
||||
|
@ -1,2 +1,2 @@
|
||||
plugin_name=JGit HTTP Server (Incubation) |
||||
plugin_name=JGit HTTP Server |
||||
provider_name=Eclipse.org |
||||
|
@ -1,2 +1,2 @@
|
||||
plugin_name=JGit HTTP Tests (Incubation) |
||||
plugin_name=JGit HTTP Tests |
||||
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 |
||||
|
@ -1,2 +1,2 @@
|
||||
plugin_name=JGit JUnit Http Utility Classes (Incubation) |
||||
plugin_name=JGit JUnit Http Utility Classes |
||||
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 |
||||
|
@ -1,2 +1,2 @@
|
||||
plugin_name=JGit Command Line Interface (Incubation) |
||||
plugin_name=JGit Command Line Interface |
||||
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 |
||||
|
@ -1,2 +1,2 @@
|
||||
plugin_name=JGit DHT Storage (Incubation) |
||||
plugin_name=JGit DHT Storage |
||||
provider_name=Eclipse.org |
||||
|
@ -1,2 +1,2 @@
|
||||
plugin_name=JGit Core Tests (Incubation) |
||||
plugin_name=JGit Core Tests |
||||
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 |
||||
|
@ -1,2 +1,2 @@
|
||||
plugin_name=JGit Core (Incubation) |
||||
plugin_name=JGit Core |
||||
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