Browse Source

Merge branch 'stable-1.0'

* 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 file
stable-1.1
Matthias Sohn 14 years ago
parent
commit
f0d62fc609
  1. 2
      org.eclipse.jgit.ant.test/plugin.properties
  2. 2
      org.eclipse.jgit.ant/plugin.properties
  3. 2
      org.eclipse.jgit.console/plugin.properties
  4. 2
      org.eclipse.jgit.generated.storage.dht.proto/plugin.properties
  5. 2
      org.eclipse.jgit.http.server/plugin.properties
  6. 2
      org.eclipse.jgit.http.test/plugin.properties
  7. 2
      org.eclipse.jgit.iplog/plugin.properties
  8. 2
      org.eclipse.jgit.junit.http/plugin.properties
  9. 2
      org.eclipse.jgit.junit/plugin.properties
  10. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.properties
  11. 2
      org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
  12. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.properties
  13. 2
      org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
  14. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.properties
  15. 2
      org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
  16. 2
      org.eclipse.jgit.packaging/org.eclipse.jgit.updatesite/pom.xml
  17. 10
      org.eclipse.jgit.packaging/org.eclipse.jgit.updatesite/site.xml
  18. 2
      org.eclipse.jgit.packaging/pom.xml
  19. 2
      org.eclipse.jgit.pgm/plugin.properties
  20. 2
      org.eclipse.jgit.storage.dht.test/plugin.properties
  21. 2
      org.eclipse.jgit.storage.dht/plugin.properties
  22. 1
      org.eclipse.jgit.test/META-INF/MANIFEST.MF
  23. 2
      org.eclipse.jgit.test/plugin.properties
  24. 271
      org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BlameCommandTest.java
  25. 143
      org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
  26. 2
      org.eclipse.jgit.ui/plugin.properties
  27. 1
      org.eclipse.jgit/META-INF/MANIFEST.MF
  28. 2
      org.eclipse.jgit/plugin.properties
  29. 1
      org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
  30. 1
      org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
  31. 227
      org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java
  32. 13
      org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
  33. 960
      org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java
  34. 356
      org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java
  35. 386
      org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java
  36. 133
      org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java
  37. 113
      org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java

2
org.eclipse.jgit.ant.test/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit Ant Tasks Tests (Incubation)
plugin_name=JGit Ant Tasks Tests
provider_name=Eclipse JGit

2
org.eclipse.jgit.ant/plugin.properties

@ -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

2
org.eclipse.jgit.console/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit Console User Interface (Incubation)
plugin_name=JGit Console User Interface
provider_name=Eclipse.org

2
org.eclipse.jgit.generated.storage.dht.proto/plugin.properties

@ -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

2
org.eclipse.jgit.http.server/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit HTTP Server (Incubation)
plugin_name=JGit HTTP Server
provider_name=Eclipse.org

2
org.eclipse.jgit.http.test/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit HTTP Tests (Incubation)
plugin_name=JGit HTTP Tests
provider_name=Eclipse.org

2
org.eclipse.jgit.iplog/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit IP Log Generator (Incubation)
plugin_name=JGit IP Log Generator
provider_name=Eclipse.org

2
org.eclipse.jgit.junit.http/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit JUnit Http Utility Classes (Incubation)
plugin_name=JGit JUnit Http Utility Classes
provider_name=Eclipse.org

2
org.eclipse.jgit.junit/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit JUnit Utility Classes (Incubation)
plugin_name=JGit JUnit Utility Classes
provider_name=Eclipse.org

4
org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.properties

@ -8,10 +8,10 @@
#
###############################################################################
featureName=Eclipse JGit (Incubation)
featureName=Eclipse JGit
providerName=Eclipse JGit
updateSiteName=Eclipse JGit Update Site (Incubation)
updateSiteName=Eclipse JGit Update Site
# description property - text of the "Feature Description"
description=\

2
org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml

@ -56,7 +56,7 @@
<artifactId>org.eclipse.jgit.feature</artifactId>
<packaging>eclipse-feature</packaging>
<name>JGit Feature (Incubation)</name>
<name>JGit Feature</name>
<dependencies>
<dependency>

4
org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.properties

@ -9,10 +9,10 @@
#
###############################################################################
featureName=Eclipse JGit JUnit Test Support (Incubation)
featureName=Eclipse JGit JUnit Test Support
providerName=Eclipse JGit
updateSiteName=Eclipse JGit Update Site (Incubation)
updateSiteName=Eclipse JGit Update Site
# description property - text of the "Feature Description"
description=\

2
org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml

@ -56,7 +56,7 @@
<artifactId>org.eclipse.jgit.junit.feature</artifactId>
<packaging>eclipse-feature</packaging>
<name>JGit JUnit Feature (Incubation)</name>
<name>JGit JUnit Feature</name>
<dependencies>
<dependency>

4
org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.properties

@ -8,10 +8,10 @@
#
###############################################################################
featureName=Eclipse JGit - Source (Incubation)
featureName=Eclipse JGit - Source
providerName=Eclipse JGit
updateSiteName=Eclipse JGit Update Site (Incubation)
updateSiteName=Eclipse JGit Update Site
# description property - text of the "Feature Description"
description=\

2
org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml

@ -56,6 +56,6 @@
<artifactId>org.eclipse.jgit.source.feature</artifactId>
<packaging>eclipse-feature</packaging>
<name>JGit Source Feature (Incubation)</name>
<name>JGit Source Feature</name>
</project>

2
org.eclipse.jgit.packaging/org.eclipse.jgit.updatesite/pom.xml

@ -56,7 +56,7 @@
<artifactId>org.eclipse.jgit.updatesite</artifactId>
<packaging>eclipse-update-site</packaging>
<name>Eclipse JGit Plugin Update Site (Incubation)</name>
<name>Eclipse JGit Plugin Update Site</name>
<!-- see http://issues.sonatype.org/browse/TYCHO-313 -->
<version>1.0.0-SNAPSHOT</version>

10
org.eclipse.jgit.packaging/org.eclipse.jgit.updatesite/site.xml

@ -4,17 +4,17 @@
This is a pure Java implementation of the Git version control system. The native Git version is also required in this version.
</description>
<feature url="features/org.eclipse.jgit_0.0.0.qualifier.jar" id="org.eclipse.jgit" version="0.0.0" patch="true">
<category name="JGit (Incubation)"/>
<category name="JGit"/>
</feature>
<feature url="features/org.eclipse.jgit.source_0.0.0.qualifier.jar" id="org.eclipse.jgit.source" version="0.0.0" patch="true">
<category name="JGit (Incubation)"/>
<category name="JGit"/>
</feature>
<feature url="features/org.eclipse.jgit.junit_0.0.0.qualifier.jar" id="org.eclipse.jgit.junit" version="0.0.0" patch="true">
<category name="JGit (Incubation)"/>
<category name="JGit"/>
</feature>
<category-def name="JGit (Incubation)" label="JGit (Incubation)">
<category-def name="JGit" label="JGit">
<description>
JGit (Incubation)
JGit
</description>
</category-def>
</site>

2
org.eclipse.jgit.packaging/pom.xml

@ -56,7 +56,7 @@
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>JGit Tycho Parent (Incubation)</name>
<name>JGit Tycho Parent</name>
<properties>
<tycho-version>0.12.0</tycho-version>

2
org.eclipse.jgit.pgm/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit Command Line Interface (Incubation)
plugin_name=JGit Command Line Interface
provider_name=Eclipse.org

2
org.eclipse.jgit.storage.dht.test/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit DHT Storage Tests (Incubation)
plugin_name=JGit DHT Storage Tests
provider_name=Eclipse.org

2
org.eclipse.jgit.storage.dht/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit DHT Storage (Incubation)
plugin_name=JGit DHT Storage
provider_name=Eclipse.org

1
org.eclipse.jgit.test/META-INF/MANIFEST.MF

@ -11,6 +11,7 @@ Import-Package: org.eclipse.jgit;version="[1.0.0,1.1.0)",
org.eclipse.jgit.api;version="[1.0.0,1.1.0)",
org.eclipse.jgit.api.errors;version="[1.0.0,1.1.0)",
org.eclipse.jgit.awtui;version="[1.0.0,1.1.0)",
org.eclipse.jgit.blame;version="[1.0.0,1.1.0)",
org.eclipse.jgit.console;version="[1.0.0,1.1.0)",
org.eclipse.jgit.diff;version="[1.0.0,1.1.0)",
org.eclipse.jgit.dircache;version="[1.0.0,1.1.0)",

2
org.eclipse.jgit.test/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit Core Tests (Incubation)
plugin_name=JGit Core Tests
provider_name=Eclipse.org

271
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BlameCommandTest.java

@ -0,0 +1,271 @@
/*
* Copyright (C) 2011, GitHub Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.api;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.eclipse.jgit.blame.BlameResult;
import org.eclipse.jgit.lib.RepositoryTestCase;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
/**
* Unit tests of {@link BlameCommand}
*/
public class BlameCommandTest extends RepositoryTestCase {
private String join(String... lines) {
StringBuilder joined = new StringBuilder();
for (String line : lines)
joined.append(line).append('\n');
return joined.toString();
}
@Test
public void testSingleRevision() throws Exception {
Git git = new Git(db);
String[] content = new String[] { "first", "second", "third" };
writeTrashFile("file.txt", join(content));
git.add().addFilepattern("file.txt").call();
RevCommit commit = git.commit().setMessage("create file").call();
BlameCommand command = new BlameCommand(db);
command.setFilePath("file.txt");
BlameResult lines = command.call();
assertNotNull(lines);
assertEquals(3, lines.getResultContents().size());
for (int i = 0; i < 3; i++) {
assertEquals(commit, lines.getSourceCommit(i));
assertEquals(i, lines.getSourceLine(i));
}
}
@Test
public void testTwoRevisions() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "first", "second" };
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
RevCommit commit1 = git.commit().setMessage("create file").call();
String[] content2 = new String[] { "first", "second", "third" };
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
RevCommit commit2 = git.commit().setMessage("create file").call();
BlameCommand command = new BlameCommand(db);
command.setFilePath("file.txt");
BlameResult lines = command.call();
assertEquals(3, lines.getResultContents().size());
assertEquals(commit1, lines.getSourceCommit(0));
assertEquals(0, lines.getSourceLine(0));
assertEquals(commit1, lines.getSourceCommit(1));
assertEquals(1, lines.getSourceLine(1));
assertEquals(commit2, lines.getSourceCommit(2));
assertEquals(2, lines.getSourceLine(2));
}
@Test
public void testRename() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "a", "b", "c" };
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
RevCommit commit1 = git.commit().setMessage("create file").call();
writeTrashFile("file1.txt", join(content1));
git.add().addFilepattern("file1.txt").call();
git.rm().addFilepattern("file.txt").call();
git.commit().setMessage("moving file").call();
String[] content2 = new String[] { "a", "b", "c2" };
writeTrashFile("file1.txt", join(content2));
git.add().addFilepattern("file1.txt").call();
RevCommit commit3 = git.commit().setMessage("editing file").call();
BlameCommand command = new BlameCommand(db);
command.setFollowFileRenames(true);
command.setFilePath("file1.txt");
BlameResult lines = command.call();
assertEquals(commit1, lines.getSourceCommit(0));
assertEquals(0, lines.getSourceLine(0));
assertEquals("file.txt", lines.getSourcePath(0));
assertEquals(commit1, lines.getSourceCommit(1));
assertEquals(1, lines.getSourceLine(1));
assertEquals("file.txt", lines.getSourcePath(1));
assertEquals(commit3, lines.getSourceCommit(2));
assertEquals(2, lines.getSourceLine(2));
assertEquals("file1.txt", lines.getSourcePath(2));
}
@Test
public void testDeleteTrailingLines() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "a", "b", "c", "d" };
String[] content2 = new String[] { "a", "b" };
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
RevCommit commit1 = git.commit().setMessage("create file").call();
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("edit file").call();
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("edit file").call();
BlameCommand command = new BlameCommand(db);
command.setFilePath("file.txt");
BlameResult lines = command.call();
assertEquals(content2.length, lines.getResultContents().size());
assertEquals(commit1, lines.getSourceCommit(0));
assertEquals(commit1, lines.getSourceCommit(1));
assertEquals(0, lines.getSourceLine(0));
assertEquals(1, lines.getSourceLine(1));
}
@Test
public void testDeleteMiddleLines() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "a", "b", "c", "d", "e" };
String[] content2 = new String[] { "a", "c", "e" };
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
RevCommit commit1 = git.commit().setMessage("edit file").call();
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("edit file").call();
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("edit file").call();
BlameCommand command = new BlameCommand(db);
command.setFilePath("file.txt");
BlameResult lines = command.call();
assertEquals(content2.length, lines.getResultContents().size());
assertEquals(commit1, lines.getSourceCommit(0));
assertEquals(0, lines.getSourceLine(0));
assertEquals(commit1, lines.getSourceCommit(1));
assertEquals(1, lines.getSourceLine(1));
assertEquals(commit1, lines.getSourceCommit(2));
assertEquals(2, lines.getSourceLine(2));
}
@Test
public void testEditAllLines() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "a", "1" };
String[] content2 = new String[] { "b", "2" };
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("edit file").call();
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
RevCommit commit2 = git.commit().setMessage("create file").call();
BlameCommand command = new BlameCommand(db);
command.setFilePath("file.txt");
BlameResult lines = command.call();
assertEquals(content2.length, lines.getResultContents().size());
assertEquals(commit2, lines.getSourceCommit(0));
assertEquals(commit2, lines.getSourceCommit(1));
}
@Test
public void testMiddleClearAllLines() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "a", "b", "c" };
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("edit file").call();
writeTrashFile("file.txt", "");
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("create file").call();
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
RevCommit commit3 = git.commit().setMessage("edit file").call();
BlameCommand command = new BlameCommand(db);
command.setFilePath("file.txt");
BlameResult lines = command.call();
assertEquals(content1.length, lines.getResultContents().size());
assertEquals(commit3, lines.getSourceCommit(0));
assertEquals(commit3, lines.getSourceCommit(1));
assertEquals(commit3, lines.getSourceCommit(2));
}
}

143
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java

@ -0,0 +1,143 @@
/*
* Copyright (C) 2011, GitHub Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.api.blame;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.blame.BlameGenerator;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.RepositoryTestCase;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
/** Unit tests of {@link BlameGenerator}. */
public class BlameGeneratorTest extends RepositoryTestCase {
@Test
public void testBoundLineDelete() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "first", "second" };
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
RevCommit c1 = git.commit().setMessage("create file").call();
String[] content2 = new String[] { "third", "first", "second" };
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
RevCommit c2 = git.commit().setMessage("create file").call();
BlameGenerator generator = new BlameGenerator(db, "file.txt");
try {
generator.push(null, db.resolve(Constants.HEAD));
assertEquals(3, generator.getResultContents().size());
assertTrue(generator.next());
assertEquals(c2, generator.getSourceCommit());
assertEquals(1, generator.getRegionLength());
assertEquals(0, generator.getResultStart());
assertEquals(1, generator.getResultEnd());
assertEquals(0, generator.getSourceStart());
assertEquals(1, generator.getSourceEnd());
assertEquals("file.txt", generator.getSourcePath());
assertTrue(generator.next());
assertEquals(c1, generator.getSourceCommit());
assertEquals(2, generator.getRegionLength());
assertEquals(1, generator.getResultStart());
assertEquals(3, generator.getResultEnd());
assertEquals(0, generator.getSourceStart());
assertEquals(2, generator.getSourceEnd());
assertEquals("file.txt", generator.getSourcePath());
assertFalse(generator.next());
} finally {
generator.release();
}
}
@Test
public void testLinesAllDeletedShortenedWalk() throws Exception {
Git git = new Git(db);
String[] content1 = new String[] { "first", "second", "third" };
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("create file").call();
String[] content2 = new String[] { "" };
writeTrashFile("file.txt", join(content2));
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("create file").call();
writeTrashFile("file.txt", join(content1));
git.add().addFilepattern("file.txt").call();
RevCommit c3 = git.commit().setMessage("create file").call();
BlameGenerator generator = new BlameGenerator(db, "file.txt");
try {
generator.push(null, db.resolve(Constants.HEAD));
assertEquals(3, generator.getResultContents().size());
assertTrue(generator.next());
assertEquals(c3, generator.getSourceCommit());
assertEquals(0, generator.getResultStart());
assertEquals(3, generator.getResultEnd());
assertFalse(generator.next());
} finally {
generator.release();
}
}
private static String join(String... lines) {
StringBuilder joined = new StringBuilder();
for (String line : lines)
joined.append(line).append('\n');
return joined.toString();
}
}

2
org.eclipse.jgit.ui/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit AWT User Interface (Incubation)
plugin_name=JGit AWT User Interface
provider_name=Eclipse.org

1
org.eclipse.jgit/META-INF/MANIFEST.MF

@ -8,6 +8,7 @@ Bundle-Vendor: %provider_name
Export-Package: org.eclipse.jgit;version="1.0.0",
org.eclipse.jgit.api;version="1.0.0",
org.eclipse.jgit.api.errors;version="1.0.0",
org.eclipse.jgit.blame;version="1.0.0",
org.eclipse.jgit.diff;version="1.0.0",
org.eclipse.jgit.dircache;version="1.0.0",
org.eclipse.jgit.errors;version="1.0.0",

2
org.eclipse.jgit/plugin.properties

@ -1,2 +1,2 @@
plugin_name=JGit Core (Incubation)
plugin_name=JGit Core
provider_name=Eclipse JGit

1
org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties

@ -32,6 +32,7 @@ badSectionEntry=Bad section entry: {0}
base64InputNotProperlyPadded=Base64 input not properly padded.
baseLengthIncorrect=base length incorrect
bareRepositoryNoWorkdirAndIndex=Bare Repository has neither a working tree, nor an index
blameNotCommittedYet=Not Committed Yet
blobNotFound=Blob not found: {0}
branchNameInvalid=Branch name {0} is not allowed
blobNotFoundForPath=Blob not found: {0} for path: {1}

1
org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java

@ -92,6 +92,7 @@ public class JGitText extends TranslationBundle {
/***/ public String base64InputNotProperlyPadded;
/***/ public String baseLengthIncorrect;
/***/ public String bareRepositoryNoWorkdirAndIndex;
/***/ public String blameNotCommittedYet;
/***/ public String blobNotFound;
/***/ public String blobNotFoundForPath;
/***/ public String branchNameInvalid;

227
org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java

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

13
org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java

@ -455,6 +455,19 @@ public class Git {
return new CleanCommand(repo);
}
/**
* Returns a command object to execute a {@code blame} command
*
* @see <a
* href="http://www.kernel.org/pub/software/scm/git/docs/git-blame.html"
* >Git documentation about Blame</a>
* @return a {@link BlameCommand} used to collect all optional parameters
* and to finally execute the {@code blame} command
*/
public BlameCommand blame() {
return new BlameCommand(repo);
}
/**
* @return the git repository this class is interacting with
*/

960
org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java

@ -0,0 +1,960 @@
/*
* Copyright (C) 2011, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.blame;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.blame.Candidate.BlobCandidate;
import org.eclipse.jgit.blame.Candidate.ReverseCandidate;
import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
import org.eclipse.jgit.diff.DiffAlgorithm;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.HistogramDiff;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.diff.RenameDetector;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
/**
* Generate author information for lines based on introduction to the file.
* <p>
* Applications that want a simple one-shot computation of blame for a file
* should use {@link #computeBlameResult()} to prepare the entire result in one
* method call. This may block for significant time as the history of the
* repository must be traversed until information is gathered for every line.
* <p>
* Applications that want more incremental update behavior may use either the
* raw {@link #next()} streaming approach supported by this class, or construct
* a {@link BlameResult} using {@link BlameResult#create(BlameGenerator)} and
* incrementally construct the result with {@link BlameResult#computeNext()}.
* <p>
* This class is not thread-safe.
* <p>
* An instance of BlameGenerator can only be used once. To blame multiple files
* the application must create a new BlameGenerator.
* <p>
* During blame processing there are two files involved:
* <ul>
* <li>result - The file whose lines are being examined. This is the revision
* the user is trying to view blame/annotation information alongside of.</li>
* <li>source - The file that was blamed with supplying one or more lines of
* data into result. The source may be a different file path (due to copy or
* rename). Source line numbers may differ from result line numbers due to lines
* being added/removed in intermediate revisions.</li>
* </ul>
* <p>
* The blame algorithm is implemented by initially assigning responsibility for
* all lines of the result to the starting commit. A difference against the
* commit's ancestor is computed, and responsibility is passed to the ancestor
* commit for any lines that are common. The starting commit is blamed only for
* the lines that do not appear in the ancestor, if any. The loop repeats using
* the ancestor, until there are no more lines to acquire information on, or the
* file's creation point is discovered in history.
*/
public class BlameGenerator {
private final Repository repository;
private final PathFilter resultPath;
private final MutableObjectId idBuf;
/** Revision pool used to acquire commits from. */
private RevWalk revPool;
/** Indicates the commit has already been processed. */
private RevFlag SEEN;
private ObjectReader reader;
private TreeWalk treeWalk;
private DiffAlgorithm diffAlgorithm = new HistogramDiff();
private RawTextComparator textComparator = RawTextComparator.DEFAULT;
private RenameDetector renameDetector;
/** Potential candidates, sorted by commit time descending. */
private Candidate queue;
/** Number of lines that still need to be discovered. */
private int remaining;
/** Blame is currently assigned to this source. */
private Candidate currentSource;
/**
* Create a blame generator for the repository and path
*
* @param repository
* repository to access revision data from.
* @param path
* initial path of the file to start scanning.
*/
public BlameGenerator(Repository repository, String path) {
this.repository = repository;
this.resultPath = PathFilter.create(path);
idBuf = new MutableObjectId();
setFollowFileRenames(true);
initRevPool(false);
remaining = -1;
}
private void initRevPool(boolean reverse) {
if (queue != null)
throw new IllegalStateException();
if (revPool != null)
revPool.release();
if (reverse)
revPool = new ReverseWalk(getRepository());
else
revPool = new RevWalk(getRepository());
revPool.setRetainBody(true);
SEEN = revPool.newFlag("SEEN");
reader = revPool.getObjectReader();
treeWalk = new TreeWalk(reader);
}
/** @return repository being scanned for revision history. */
public Repository getRepository() {
return repository;
}
/** @return path file path being processed. */
public String getResultPath() {
return resultPath.getPath();
}
/**
* Difference algorithm to use when comparing revisions.
*
* @param algorithm
* @return {@code this}
*/
public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) {
diffAlgorithm = algorithm;
return this;
}
/**
* Text comparator to use when comparing revisions.
*
* @param comparator
* @return {@code this}
*/
public BlameGenerator setTextComparator(RawTextComparator comparator) {
textComparator = comparator;
return this;
}
/**
* Enable (or disable) following file renames, on by default.
* <p>
* If true renames are followed using the standard FollowFilter behavior
* used by RevWalk (which matches {@code git log --follow} in the C
* implementation). This is not the same as copy/move detection as
* implemented by the C implementation's of {@code git blame -M -C}.
*
* @param follow
* enable following.
* @return {@code this}
*/
public BlameGenerator setFollowFileRenames(boolean follow) {
if (follow)
renameDetector = new RenameDetector(getRepository());
else
renameDetector = null;
return this;
}
/**
* Obtain the RenameDetector if {@code setFollowFileRenames(true)}.
*
* @return the rename detector, allowing the application to configure its
* settings for rename score and breaking behavior.
*/
public RenameDetector getRenameDetector() {
return renameDetector;
}
/**
* Push a candidate blob onto the generator's traversal stack.
* <p>
* Candidates should be pushed in history order from oldest-to-newest.
* Applications should push the starting commit first, then the index
* revision (if the index is interesting), and finally the working tree
* copy (if the working tree is interesting).
*
* @param description
* description of the blob revision, such as "Working Tree".
* @param contents
* contents of the file.
* @return {@code this}
* @throws IOException
* the repository cannot be read.
*/
public BlameGenerator push(String description, byte[] contents)
throws IOException {
return push(description, new RawText(contents));
}
/**
* Push a candidate blob onto the generator's traversal stack.
* <p>
* Candidates should be pushed in history order from oldest-to-newest.
* Applications should push the starting commit first, then the index
* revision (if the index is interesting), and finally the working tree copy
* (if the working tree is interesting).
*
* @param description
* description of the blob revision, such as "Working Tree".
* @param contents
* contents of the file.
* @return {@code this}
* @throws IOException
* the repository cannot be read.
*/
public BlameGenerator push(String description, RawText contents)
throws IOException {
if (description == null)
description = JGitText.get().blameNotCommittedYet;
BlobCandidate c = new BlobCandidate(description, resultPath);
c.sourceText = contents;
c.regionList = new Region(0, 0, contents.size());
remaining = contents.size();
push(c);
return this;
}
/**
* Push a candidate object onto the generator's traversal stack.
* <p>
* Candidates should be pushed in history order from oldest-to-newest.
* Applications should push the starting commit first, then the index
* revision (if the index is interesting), and finally the working tree copy
* (if the working tree is interesting).
*
* @param description
* description of the blob revision, such as "Working Tree".
* @param id
* may be a commit or a blob.
* @return {@code this}
* @throws IOException
* the repository cannot be read.
*/
public BlameGenerator push(String description, AnyObjectId id)
throws IOException {
ObjectLoader ldr = reader.open(id);
if (ldr.getType() == OBJ_BLOB) {
if (description == null)
description = JGitText.get().blameNotCommittedYet;
BlobCandidate c = new BlobCandidate(description, resultPath);
c.sourceBlob = id.toObjectId();
c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
c.regionList = new Region(0, 0, c.sourceText.size());
remaining = c.sourceText.size();
push(c);
return this;
}
RevCommit commit = revPool.parseCommit(id);
if (!find(commit, resultPath))
return this;
Candidate c = new Candidate(commit, resultPath);
c.sourceBlob = idBuf.toObjectId();
c.loadText(reader);
c.regionList = new Region(0, 0, c.sourceText.size());
remaining = c.sourceText.size();
push(c);
return this;
}
/**
* Configure the generator to compute reverse blame (history of deletes).
* <p>
* This method is expensive as it immediately runs a RevWalk over the
* history spanning the expression {@code start..end} (end being more recent
* than start) and then performs the equivalent operation as
* {@link #push(String, AnyObjectId)} to begin blame traversal from the
* commit named by {@code start} walking forwards through history until
* {@code end} blaming line deletions.
* <p>
* A reverse blame may produce multiple sources for the same result line,
* each of these is a descendant commit that removed the line, typically
* this occurs when the same deletion appears in multiple side branches such
* as due to a cherry-pick. Applications relying on reverse should use
* {@link BlameResult} as it filters these duplicate sources and only
* remembers the first (oldest) deletion.
*
* @param start
* oldest commit to traverse from. The result file will be loaded
* from this commit's tree.
* @param end
* most recent commit to stop traversal at. Usually an active
* branch tip, tag, or HEAD.
* @return {@code this}
* @throws IOException
* the repository cannot be read.
*/
public BlameGenerator reverse(AnyObjectId start, AnyObjectId end)
throws IOException {
return reverse(start, Collections.singleton(end.toObjectId()));
}
/**
* Configure the generator to compute reverse blame (history of deletes).
* <p>
* This method is expensive as it immediately runs a RevWalk over the
* history spanning the expression {@code start..end} (end being more recent
* than start) and then performs the equivalent operation as
* {@link #push(String, AnyObjectId)} to begin blame traversal from the
* commit named by {@code start} walking forwards through history until
* {@code end} blaming line deletions.
* <p>
* A reverse blame may produce multiple sources for the same result line,
* each of these is a descendant commit that removed the line, typically
* this occurs when the same deletion appears in multiple side branches such
* as due to a cherry-pick. Applications relying on reverse should use
* {@link BlameResult} as it filters these duplicate sources and only
* remembers the first (oldest) deletion.
*
* @param start
* oldest commit to traverse from. The result file will be loaded
* from this commit's tree.
* @param end
* most recent commits to stop traversal at. Usually an active
* branch tip, tag, or HEAD.
* @return {@code this}
* @throws IOException
* the repository cannot be read.
*/
public BlameGenerator reverse(AnyObjectId start,
Collection<? extends ObjectId> end) throws IOException {
initRevPool(true);
ReverseCommit result = (ReverseCommit) revPool.parseCommit(start);
if (!find(result, resultPath))
return this;
revPool.markUninteresting(result);
for (ObjectId id : end)
revPool.markStart(revPool.parseCommit(id));
while (revPool.next() != null) {
// just pump the queue
}
ReverseCandidate c = new ReverseCandidate(result, resultPath);
c.sourceBlob = idBuf.toObjectId();
c.loadText(reader);
c.regionList = new Region(0, 0, c.sourceText.size());
remaining = c.sourceText.size();
push(c);
return this;
}
/**
* Execute the generator in a blocking fashion until all data is ready.
*
* @return the complete result. Null if no file exists for the given path.
* @throws IOException
* the repository cannot be read.
*/
public BlameResult computeBlameResult() throws IOException {
try {
BlameResult r = BlameResult.create(this);
if (r != null)
r.computeAll();
return r;
} finally {
release();
}
}
/**
* Step the blame algorithm one iteration.
*
* @return true if the generator has found a region's source. The getSource*
* and {@link #getResultStart()}, {@link #getResultEnd()} methods
* can be used to inspect the region found. False if there are no
* more regions to describe.
* @throws IOException
* repository cannot be read.
*/
public boolean next() throws IOException {
// If there is a source still pending, produce the next region.
if (currentSource != null) {
Region r = currentSource.regionList;
Region n = r.next;
remaining -= r.length;
if (n != null) {
currentSource.regionList = n;
return true;
}
if (currentSource.queueNext != null)
return result(currentSource.queueNext);
currentSource = null;
}
// If there are no lines remaining, the entire result is done,
// even if there are revisions still available for the path.
if (remaining == 0)
return done();
for (;;) {
Candidate n = pop();
if (n == null)
return done();
int pCnt = n.getParentCount();
if (pCnt == 1) {
if (processOne(n))
return true;
} else if (1 < pCnt) {
if (processMerge(n))
return true;
} else if (n instanceof ReverseCandidate) {
// Do not generate a tip of a reverse. The region
// survives and should not appear to be deleted.
} else /* if (pCnt == 0) */{
// Root commit, with at least one surviving region.
// Assign the remaining blame here.
return result(n);
}
}
}
private boolean done() {
release();
return false;
}
private boolean result(Candidate n) throws IOException {
if (n.sourceCommit != null)
revPool.parseBody(n.sourceCommit);
currentSource = n;
return true;
}
private boolean reverseResult(Candidate parent, Candidate source)
throws IOException {
// On a reverse blame present the application the parent
// (as this is what did the removals), however the region
// list to enumerate is the source's surviving list.
Candidate res = parent.copy(parent.sourceCommit);
res.regionList = source.regionList;
return result(res);
}
private Candidate pop() {
Candidate n = queue;
if (n != null) {
queue = n.queueNext;
n.queueNext = null;
}
return n;
}
private void push(BlobCandidate toInsert) {
Candidate c = queue;
if (c != null) {
c.regionList = null;
toInsert.parent = c;
}
queue = toInsert;
}
private void push(Candidate toInsert) {
// Mark sources to ensure they get discarded (above) if
// another path to the same commit.
toInsert.add(SEEN);
// Insert into the queue using descending commit time, so
// the most recent commit will pop next.
int time = toInsert.getTime();
Candidate n = queue;
if (n == null || time >= n.getTime()) {
toInsert.queueNext = n;
queue = toInsert;
return;
}
for (Candidate p = n;; p = n) {
n = p.queueNext;
if (n == null || time >= n.getTime()) {
toInsert.queueNext = n;
p.queueNext = toInsert;
return;
}
}
}
private boolean processOne(Candidate n) throws IOException {
RevCommit parent = n.getParent(0);
if (parent == null)
return split(n.getNextCandidate(0), n);
if (parent.has(SEEN))
return false;
revPool.parseHeaders(parent);
if (find(parent, n.sourcePath)) {
if (idBuf.equals(n.sourceBlob)) {
// The common case of the file not being modified in
// a simple string-of-pearls history. Blame parent.
n.sourceCommit = parent;
push(n);
return false;
}
Candidate next = n.create(parent, n.sourcePath);
next.sourceBlob = idBuf.toObjectId();
next.loadText(reader);
return split(next, n);
}
if (n.sourceCommit == null)
return result(n);
DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
if (r == null)
return result(n);
if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
// A 100% rename without any content change can also
// skip directly to the parent.
n.sourceCommit = parent;
n.sourcePath = PathFilter.create(r.getOldPath());
push(n);
return false;
}
Candidate next = n.create(parent, PathFilter.create(r.getOldPath()));
next.sourceBlob = r.getOldId().toObjectId();
next.renameScore = r.getScore();
next.loadText(reader);
return split(next, n);
}
private boolean split(Candidate parent, Candidate source)
throws IOException {
EditList editList = diffAlgorithm.diff(textComparator,
parent.sourceText, source.sourceText);
if (editList.isEmpty()) {
// Ignoring whitespace (or some other special comparator) can
// cause non-identical blobs to have an empty edit list. In
// a case like this push the parent alone.
parent.regionList = source.regionList;
push(parent);
return false;
}
parent.takeBlame(editList, source);
if (parent.regionList != null)
push(parent);
if (source.regionList != null) {
if (source instanceof ReverseCandidate)
return reverseResult(parent, source);
return result(source);
}
return false;
}
private boolean processMerge(Candidate n) throws IOException {
int pCnt = n.getParentCount();
for (int pIdx = 0; pIdx < pCnt; pIdx++) {
RevCommit parent = n.getParent(pIdx);
if (parent.has(SEEN))
continue;
revPool.parseHeaders(parent);
}
// If any single parent exactly matches the merge, follow only
// that one parent through history.
ObjectId[] ids = null;
for (int pIdx = 0; pIdx < pCnt; pIdx++) {
RevCommit parent = n.getParent(pIdx);
if (parent.has(SEEN))
continue;
if (!find(parent, n.sourcePath))
continue;
if (!(n instanceof ReverseCandidate) && idBuf.equals(n.sourceBlob)) {
n.sourceCommit = parent;
push(n);
return false;
}
if (ids == null)
ids = new ObjectId[pCnt];
ids[pIdx] = idBuf.toObjectId();
}
// If rename detection is enabled, search for any relevant names.
DiffEntry[] renames = null;
if (renameDetector != null) {
renames = new DiffEntry[pCnt];
for (int pIdx = 0; pIdx < pCnt; pIdx++) {
RevCommit parent = n.getParent(pIdx);
if (parent.has(SEEN))
continue;
if (ids != null && ids[pIdx] != null)
continue;
DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
if (r == null)
continue;
if (n instanceof ReverseCandidate) {
if (ids == null)
ids = new ObjectId[pCnt];
ids[pCnt] = r.getOldId().toObjectId();
} else if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
// A 100% rename without any content change can also
// skip directly to the parent. Note this bypasses an
// earlier parent that had the path (above) but did not
// have an exact content match. For performance reasons
// we choose to follow the one parent over trying to do
// possibly both parents.
n.sourceCommit = parent;
n.sourcePath = PathFilter.create(r.getOldPath());
push(n);
return false;
}
renames[pIdx] = r;
}
}
// Construct the candidate for each parent.
Candidate[] parents = new Candidate[pCnt];
for (int pIdx = 0; pIdx < pCnt; pIdx++) {
RevCommit parent = n.getParent(pIdx);
if (parent.has(SEEN))
continue;
Candidate p;
if (renames != null && renames[pIdx] != null) {
p = n.create(parent,
PathFilter.create(renames[pIdx].getOldPath()));
p.renameScore = renames[pIdx].getScore();
p.sourceBlob = renames[pIdx].getOldId().toObjectId();
} else if (ids != null && ids[pIdx] != null) {
p = n.create(parent, n.sourcePath);
p.sourceBlob = ids[pIdx];
} else {
continue;
}
EditList editList;
if (n instanceof ReverseCandidate
&& p.sourceBlob.equals(n.sourceBlob)) {
// This special case happens on ReverseCandidate forks.
p.sourceText = n.sourceText;
editList = new EditList(0);
} else {
p.loadText(reader);
editList = diffAlgorithm.diff(textComparator,
p.sourceText, n.sourceText);
}
if (editList.isEmpty()) {
// Ignoring whitespace (or some other special comparator) can
// cause non-identical blobs to have an empty edit list. In
// a case like this push the parent alone.
if (n instanceof ReverseCandidate) {
parents[pIdx] = p;
continue;
}
p.regionList = n.regionList;
push(p);
return false;
}
p.takeBlame(editList, n);
// Only remember this parent candidate if there is at least
// one region that was blamed on the parent.
if (p.regionList != null) {
// Reverse blame requires inverting the regions. This puts
// the regions the parent deleted from us into the parent,
// and retains the common regions to look at other parents
// for deletions.
if (n instanceof ReverseCandidate) {
Region r = p.regionList;
p.regionList = n.regionList;
n.regionList = r;
}
parents[pIdx] = p;
}
}
if (n instanceof ReverseCandidate) {
// On a reverse blame report all deletions found in the children,
// and pass on to them a copy of our region list.
Candidate resultHead = null;
Candidate resultTail = null;
for (int pIdx = 0; pIdx < pCnt; pIdx++) {
Candidate p = parents[pIdx];
if (p == null)
continue;
if (p.regionList != null) {
Candidate r = p.copy(p.sourceCommit);
if (resultTail != null) {
resultTail.queueNext = r;
resultTail = r;
} else {
resultHead = r;
resultTail = r;
}
}
if (n.regionList != null) {
p.regionList = n.regionList.deepCopy();
push(p);
}
}
if (resultHead != null)
return result(resultHead);
return false;
}
// Push any parents that are still candidates.
for (int pIdx = 0; pIdx < pCnt; pIdx++) {
if (parents[pIdx] != null)
push(parents[pIdx]);
}
if (n.regionList != null)
return result(n);
return false;
}
/**
* Get the revision blamed for the current region.
* <p>
* The source commit may be null if the line was blamed to an uncommitted
* revision, such as the working tree copy, or during a reverse blame if the
* line survives to the end revision (e.g. the branch tip).
*
* @return current revision being blamed.
*/
public RevCommit getSourceCommit() {
return currentSource.sourceCommit;
}
/** @return current author being blamed. */
public PersonIdent getSourceAuthor() {
return currentSource.getAuthor();
}
/** @return current committer being blamed. */
public PersonIdent getSourceCommitter() {
RevCommit c = getSourceCommit();
return c != null ? c.getCommitterIdent() : null;
}
/** @return path of the file being blamed. */
public String getSourcePath() {
return currentSource.sourcePath.getPath();
}
/** @return rename score if a rename occurred in {@link #getSourceCommit}. */
public int getRenameScore() {
return currentSource.renameScore;
}
/**
* @return first line of the source data that has been blamed for the
* current region. This is line number of where the region was added
* during {@link #getSourceCommit()} in file
* {@link #getSourcePath()}.
*/
public int getSourceStart() {
return currentSource.regionList.sourceStart;
}
/**
* @return one past the range of the source data that has been blamed for
* the current region. This is line number of where the region was
* added during {@link #getSourceCommit()} in file
* {@link #getSourcePath()}.
*/
public int getSourceEnd() {
Region r = currentSource.regionList;
return r.sourceStart + r.length;
}
/**
* @return first line of the result that {@link #getSourceCommit()} has been
* blamed for providing. Line numbers use 0 based indexing.
*/
public int getResultStart() {
return currentSource.regionList.resultStart;
}
/**
* @return one past the range of the result that {@link #getSourceCommit()}
* has been blamed for providing. Line numbers use 0 based indexing.
* Because a source cannot be blamed for an empty region of the
* result, {@link #getResultEnd()} is always at least one larger
* than {@link #getResultStart()}.
*/
public int getResultEnd() {
Region r = currentSource.regionList;
return r.resultStart + r.length;
}
/**
* @return number of lines in the current region being blamed to
* {@link #getSourceCommit()}. This is always the value of the
* expression {@code getResultEnd() - getResultStart()}, but also
* {@code getSourceEnd() - getSourceStart()}.
*/
public int getRegionLength() {
return currentSource.regionList.length;
}
/**
* @return complete contents of the source file blamed for the current
* output region. This is the contents of {@link #getSourcePath()}
* within {@link #getSourceCommit()}. The source contents is
* temporarily available as an artifact of the blame algorithm. Most
* applications will want the result contents for display to users.
*/
public RawText getSourceContents() {
return currentSource.sourceText;
}
/**
* @return complete file contents of the result file blame is annotating.
* This value is accessible only after being configured and only
* immediately before the first call to {@link #next()}. Returns
* null if the path does not exist.
* @throws IOException
* repository cannot be read.
* @throws IllegalStateException
* {@link #next()} has already been invoked.
*/
public RawText getResultContents() throws IOException {
return queue != null ? queue.sourceText : null;
}
/** Release the current blame session. */
public void release() {
revPool.release();
queue = null;
currentSource = null;
}
private boolean find(RevCommit commit, PathFilter path) throws IOException {
treeWalk.setFilter(path);
treeWalk.reset(commit.getTree());
while (treeWalk.next()) {
if (path.isDone(treeWalk)) {
if (treeWalk.getFileMode(0).getObjectType() != OBJ_BLOB)
return false;
treeWalk.getObjectId(idBuf, 0);
return true;
}
if (treeWalk.isSubtree())
treeWalk.enterSubtree();
}
return false;
}
private DiffEntry findRename(RevCommit parent, RevCommit commit,
PathFilter path) throws IOException {
if (renameDetector == null)
return null;
treeWalk.setFilter(TreeFilter.ANY_DIFF);
treeWalk.reset(parent.getTree(), commit.getTree());
renameDetector.addAll(DiffEntry.scan(treeWalk));
for (DiffEntry ent : renameDetector.compute()) {
if (isRename(ent) && ent.getNewPath().equals(path.getPath()))
return ent;
}
return null;
}
private static boolean isRename(DiffEntry ent) {
return ent.getChangeType() == ChangeType.RENAME
|| ent.getChangeType() == ChangeType.COPY;
}
}

356
org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java

@ -0,0 +1,356 @@
/*
* Copyright (C) 2011, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.blame;
import java.io.IOException;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
/**
* Collects line annotations for inspection by applications.
* <p>
* A result is usually updated incrementally as the BlameGenerator digs back
* further through history. Applications that want to lay annotations down text
* to the original source file in a viewer may find the BlameResult structure an
* easy way to acquire the information, at the expense of keeping tables in
* memory tracking every line of the result file.
* <p>
* This class is not thread-safe.
* <p>
* During blame processing there are two files involved:
* <ul>
* <li>result - The file whose lines are being examined. This is the revision
* the user is trying to view blame/annotation information alongside of.</li>
* <li>source - The file that was blamed with supplying one or more lines of
* data into result. The source may be a different file path (due to copy or
* rename). Source line numbers may differ from result line numbers due to lines
* being added/removed in intermediate revisions.</li>
* </ul>
*/
public class BlameResult {
/**
* Construct a new BlameResult for a generator.
*
* @param gen
* the generator the result will consume records from.
* @return the new result object. null if the generator cannot find the path
* it starts from.
* @throws IOException
* the repository cannot be read.
*/
public static BlameResult create(BlameGenerator gen) throws IOException {
String path = gen.getResultPath();
RawText contents = gen.getResultContents();
if (contents == null) {
gen.release();
return null;
}
return new BlameResult(gen, path, contents);
}
private final String resultPath;
private final RevCommit[] sourceCommits;
private final PersonIdent[] sourceAuthors;
private final PersonIdent[] sourceCommitters;
private final String[] sourcePaths;
/** Warning: these are actually 1-based. */
private final int[] sourceLines;
private RawText resultContents;
private BlameGenerator generator;
private int lastLength;
BlameResult(BlameGenerator bg, String path, RawText text) {
generator = bg;
resultPath = path;
resultContents = text;
int cnt = text.size();
sourceCommits = new RevCommit[cnt];
sourceAuthors = new PersonIdent[cnt];
sourceCommitters = new PersonIdent[cnt];
sourceLines = new int[cnt];
sourcePaths = new String[cnt];
}
/** @return path of the file this result annotates. */
public String getResultPath() {
return resultPath;
}
/** @return contents of the result file, available for display. */
public RawText getResultContents() {
return resultContents;
}
/** Throw away the {@link #getResultContents()}. */
public void discardResultContents() {
resultContents = null;
}
/**
* Check if the given result line has been annotated yet.
*
* @param idx
* line to read data of, 0 based.
* @return true if the data has been annotated, false otherwise.
*/
public boolean hasSourceData(int idx) {
return sourceLines[idx] != 0;
}
/**
* Check if the given result line has been annotated yet.
*
* @param start
* first index to examine.
* @param end
* last index to examine.
* @return true if the data has been annotated, false otherwise.
*/
public boolean hasSourceData(int start, int end) {
for (; start < end; start++)
if (sourceLines[start] == 0)
return false;
return true;
}
/**
* Get the commit that provided the specified line of the result.
* <p>
* The source commit may be null if the line was blamed to an uncommitted
* revision, such as the working tree copy, or during a reverse blame if the
* line survives to the end revision (e.g. the branch tip).
*
* @param idx
* line to read data of, 0 based.
* @return commit that provided line {@code idx}. May be null.
*/
public RevCommit getSourceCommit(int idx) {
return sourceCommits[idx];
}
/**
* Get the author that provided the specified line of the result.
*
* @param idx
* line to read data of, 0 based.
* @return author that provided line {@code idx}. May be null.
*/
public PersonIdent getSourceAuthor(int idx) {
return sourceAuthors[idx];
}
/**
* Get the committer that provided the specified line of the result.
*
* @param idx
* line to read data of, 0 based.
* @return committer that provided line {@code idx}. May be null.
*/
public PersonIdent getSourceCommitter(int idx) {
return sourceCommitters[idx];
}
/**
* Get the file path that provided the specified line of the result.
*
* @param idx
* line to read data of, 0 based.
* @return source file path that provided line {@code idx}.
*/
public String getSourcePath(int idx) {
return sourcePaths[idx];
}
/**
* Get the corresponding line number in the source file.
*
* @param idx
* line to read data of, 0 based.
* @return matching line number in the source file.
*/
public int getSourceLine(int idx) {
return sourceLines[idx] - 1;
}
/**
* Compute all pending information.
*
* @throws IOException
* the repository cannot be read.
*/
public void computeAll() throws IOException {
BlameGenerator gen = generator;
if (gen == null)
return;
try {
while (gen.next())
loadFrom(gen);
} finally {
gen.release();
generator = null;
}
}
/**
* Compute the next available segment and return the first index.
* <p>
* Computes one segment and returns to the caller the first index that is
* available. After return the caller can also inspect {@link #lastLength()}
* to determine how many lines of the result were computed.
*
* @return index that is now available. -1 if no more are available.
* @throws IOException
* the repository cannot be read.
*/
public int computeNext() throws IOException {
BlameGenerator gen = generator;
if (gen == null)
return -1;
if (gen.next()) {
loadFrom(gen);
lastLength = gen.getRegionLength();
return gen.getResultStart();
} else {
gen.release();
generator = null;
return -1;
}
}
/** @return length of the last segment found by {@link #computeNext()}. */
public int lastLength() {
return lastLength;
}
/**
* Compute until the entire range has been populated.
*
* @param start
* first index to examine.
* @param end
* last index to examine.
* @throws IOException
* the repository cannot be read.
*/
public void computeRange(int start, int end) throws IOException {
BlameGenerator gen = generator;
if (gen == null)
return;
while (start < end) {
if (hasSourceData(start, end))
return;
if (!gen.next()) {
gen.release();
generator = null;
return;
}
loadFrom(gen);
// If the result contains either end of our current range bounds,
// update the bounds to avoid scanning that section during the
// next loop iteration.
int resLine = gen.getResultStart();
int resEnd = gen.getResultEnd();
if (resLine <= start && start < resEnd)
start = resEnd;
if (resLine <= end && end < resEnd)
end = resLine;
}
}
@Override
public String toString() {
StringBuilder r = new StringBuilder();
r.append("BlameResult: ");
r.append(getResultPath());
return r.toString();
}
private void loadFrom(BlameGenerator gen) {
RevCommit srcCommit = gen.getSourceCommit();
PersonIdent srcAuthor = gen.getSourceAuthor();
PersonIdent srcCommitter = gen.getSourceCommitter();
String srcPath = gen.getSourcePath();
int srcLine = gen.getSourceStart();
int resLine = gen.getResultStart();
int resEnd = gen.getResultEnd();
for (; resLine < resEnd; resLine++) {
// Reverse blame can generate multiple results for the same line.
// Favor the first one selected, as this is the oldest and most
// likely to be nearest to the inquiry made by the user.
if (sourceLines[resLine] != 0)
continue;
sourceCommits[resLine] = srcCommit;
sourceAuthors[resLine] = srcAuthor;
sourceCommitters[resLine] = srcCommitter;
sourcePaths[resLine] = srcPath;
// Since sourceLines is 1-based to permit hasSourceData to use 0 to
// mean the line has not been annotated yet, pre-increment instead
// of the traditional post-increment when making the assignment.
sourceLines[resLine] = ++srcLine;
}
}
}

386
org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java

@ -0,0 +1,386 @@
/*
* Copyright (C) 2011, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.blame;
import java.io.IOException;
import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.treewalk.filter.PathFilter;
/**
* A source that may have supplied some (or all) of the result file.
* <p>
* Candidates are kept in a queue by BlameGenerator, allowing the generator to
* perform a parallel search down the parents of any merges that are discovered
* during the history traversal. Each candidate retains a {@link #regionList}
* describing sections of the result file the candidate has taken responsibility
* for either directly or indirectly through its history. Actual blame from this
* region list will be assigned to the candidate when its ancestor commit(s) are
* themselves converted into Candidate objects and the ancestor's candidate uses
* {@link #takeBlame(EditList, Candidate)} to accept responsibility for sections
* of the result.
*/
class Candidate {
/** Next candidate in the candidate queue. */
Candidate queueNext;
/** Commit being considered (or blamed, depending on state). */
RevCommit sourceCommit;
/** Path of the candidate file in {@link #sourceCommit}. */
PathFilter sourcePath;
/** Unique name of the candidate blob in {@link #sourceCommit}. */
ObjectId sourceBlob;
/** Complete contents of the file in {@link #sourceCommit}. */
RawText sourceText;
/**
* Chain of regions this candidate may be blamed for.
* <p>
* This list is always kept sorted by resultStart order, making it simple to
* merge-join with the sorted EditList during blame assignment.
*/
Region regionList;
/**
* Score assigned to the rename to this candidate.
* <p>
* Consider the history "A<-B<-C". If the result file S in C was renamed to
* R in B, the rename score for this rename will be held in this field by
* the candidate object for B. By storing the score with B, the application
* can see what the rename score was as it makes the transition from C/S to
* B/R. This may seem backwards since it was C that performed the rename,
* but the application doesn't learn about path R until B.
*/
int renameScore;
Candidate(RevCommit commit, PathFilter path) {
sourceCommit = commit;
sourcePath = path;
}
int getParentCount() {
return sourceCommit.getParentCount();
}
RevCommit getParent(int idx) {
return sourceCommit.getParent(idx);
}
Candidate getNextCandidate(@SuppressWarnings("unused") int idx) {
return null;
}
void add(RevFlag flag) {
sourceCommit.add(flag);
}
int getTime() {
return sourceCommit.getCommitTime();
}
PersonIdent getAuthor() {
return sourceCommit.getAuthorIdent();
}
Candidate create(RevCommit commit, PathFilter path) {
return new Candidate(commit, path);
}
Candidate copy(RevCommit commit) {
Candidate r = create(commit, sourcePath);
r.sourceBlob = sourceBlob;
r.sourceText = sourceText;
r.regionList = regionList;
r.renameScore = renameScore;
return r;
}
void loadText(ObjectReader reader) throws IOException {
ObjectLoader ldr = reader.open(sourceBlob, Constants.OBJ_BLOB);
sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
}
void takeBlame(EditList editList, Candidate child) {
blame(editList, this, child);
}
private static void blame(EditList editList, Candidate a, Candidate b) {
Region r = b.clearRegionList();
Region aTail = null;
Region bTail = null;
for (int eIdx = 0; eIdx < editList.size();) {
// If there are no more regions left, neither side has any
// more responsibility for the result. Remaining edits can
// be safely ignored.
if (r == null)
return;
Edit e = editList.get(eIdx);
// Edit ends before the next candidate region. Skip the edit.
if (e.getEndB() <= r.sourceStart) {
eIdx++;
continue;
}
// Next candidate region starts before the edit. Assign some
// of the blame onto A, but possibly split and also on B.
if (r.sourceStart < e.getBeginB()) {
int d = e.getBeginB() - r.sourceStart;
if (r.length <= d) {
// Pass the blame for this region onto A.
Region next = r.next;
r.sourceStart = e.getBeginA() - d;
aTail = add(aTail, a, r);
r = next;
continue;
}
// Split the region and assign some to A, some to B.
aTail = add(aTail, a, r.splitFirst(e.getBeginA() - d, d));
r.slideAndShrink(d);
}
// At this point e.getBeginB() <= r.sourceStart.
// An empty edit on the B side isn't relevant to this split,
// as it does not overlap any candidate region.
if (e.getLengthB() == 0) {
eIdx++;
continue;
}
// If the region ends before the edit, blame on B.
int rEnd = r.sourceStart + r.length;
if (rEnd <= e.getEndB()) {
Region next = r.next;
bTail = add(bTail, b, r);
r = next;
if (rEnd == e.getEndB())
eIdx++;
continue;
}
// This region extends beyond the edit. Blame the first
// half of the region on B, and process the rest after.
int len = e.getEndB() - r.sourceStart;
bTail = add(bTail, b, r.splitFirst(r.sourceStart, len));
r.slideAndShrink(len);
eIdx++;
}
if (r == null)
return;
// For any remaining region, pass the blame onto A after shifting
// the source start to account for the difference between the two.
Edit e = editList.get(editList.size() - 1);
int endB = e.getEndB();
int d = endB - e.getEndA();
if (aTail == null)
a.regionList = r;
else
aTail.next = r;
do {
if (endB <= r.sourceStart)
r.sourceStart -= d;
r = r.next;
} while (r != null);
}
private static Region add(Region aTail, Candidate a, Region n) {
// If there is no region on the list, use only this one.
if (aTail == null) {
a.regionList = n;
n.next = null;
return n;
}
// If the prior region ends exactly where the new region begins
// in both the result and the source, combine these together into
// one contiguous region. This occurs when intermediate commits
// have inserted and deleted lines in the middle of a region. Try
// to report this region as a single region to the application,
// rather than in fragments.
if (aTail.resultStart + aTail.length == n.resultStart
&& aTail.sourceStart + aTail.length == n.sourceStart) {
aTail.length += n.length;
return aTail;
}
// Append the region onto the end of the list.
aTail.next = n;
n.next = null;
return n;
}
private Region clearRegionList() {
Region r = regionList;
regionList = null;
return r;
}
@Override
public String toString() {
StringBuilder r = new StringBuilder();
r.append("Candidate[");
r.append(sourcePath.getPath());
if (sourceCommit != null)
r.append(" @ ").append(sourceCommit.abbreviate(6).name());
if (regionList != null)
r.append(" regions:").append(regionList);
r.append("]");
return r.toString();
}
/**
* Special candidate type used for reverse blame.
* <p>
* Reverse blame inverts the commit history graph to follow from a commit to
* its descendant children, rather than the normal history direction of
* child to parent. These types require a {@link ReverseCommit} which keeps
* children pointers, allowing reverse navigation of history.
*/
static final class ReverseCandidate extends Candidate {
ReverseCandidate(ReverseCommit commit, PathFilter path) {
super(commit, path);
}
@Override
int getParentCount() {
return ((ReverseCommit) sourceCommit).getChildCount();
}
@Override
RevCommit getParent(int idx) {
return ((ReverseCommit) sourceCommit).getChild(idx);
}
@Override
int getTime() {
// Invert the timestamp so newer dates sort older.
return -sourceCommit.getCommitTime();
}
@Override
Candidate create(RevCommit commit, PathFilter path) {
return new ReverseCandidate((ReverseCommit) commit, path);
}
@Override
public String toString() {
return "Reverse" + super.toString();
}
}
/**
* Candidate loaded from a file source, and not a commit.
* <p>
* The {@link Candidate#sourceCommit} field is always null on this type of
* candidate. Instead history traversal follows the single {@link #parent}
* field to discover the next Candidate. Often this is a normal Candidate
* type that has a valid sourceCommit.
*/
static final class BlobCandidate extends Candidate {
/**
* Next candidate to pass blame onto.
* <p>
* When computing the differences that this candidate introduced to the
* file content, the parent's sourceText is used as the base.
*/
Candidate parent;
/** Author name to refer to this blob with. */
String description;
BlobCandidate(String name, PathFilter path) {
super(null, path);
description = name;
}
@Override
int getParentCount() {
return parent != null ? 1 : 0;
}
@Override
RevCommit getParent(int idx) {
return null;
}
@Override
Candidate getNextCandidate(int idx) {
return parent;
}
@Override
void add(RevFlag flag) {
// Do nothing, sourceCommit is null.
}
@Override
int getTime() {
return Integer.MAX_VALUE;
}
@Override
PersonIdent getAuthor() {
return new PersonIdent(description, null);
}
}
}

133
org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java

@ -0,0 +1,133 @@
/*
* Copyright (C) 2011, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.blame;
/**
* Region of the result that still needs to be computed.
* <p>
* Regions are held in a singly-linked-list by {@link Candidate} using the
* {@link Candidate#regionList} field. The list is kept in sorted order by
* {@link #resultStart}.
*/
class Region {
/** Next entry in the region linked list. */
Region next;
/** First position of this region in the result file blame is computing. */
int resultStart;
/** First position in the {@link Candidate} that owns this Region. */
int sourceStart;
/** Length of the region, always >= 1. */
int length;
Region(int rs, int ss, int len) {
resultStart = rs;
sourceStart = ss;
length = len;
}
/**
* Copy the entire result region, but at a new source position.
*
* @param newSource
* the new source position.
* @return the same result region, but offset for a new source.
*/
Region copy(int newSource) {
return new Region(resultStart, newSource, length);
}
/**
* Split the region, assigning a new source position to the first half.
*
* @param newSource
* the new source position.
* @param newLen
* length of the new region.
* @return the first half of the region, at the new source.
*/
Region splitFirst(int newSource, int newLen) {
return new Region(resultStart, newSource, newLen);
}
/**
* Edit this region to remove the first {@code d} elements.
*
* @param d
* number of elements to remove from the start of this region.
*/
void slideAndShrink(int d) {
resultStart += d;
sourceStart += d;
length -= d;
}
Region deepCopy() {
Region head = new Region(resultStart, sourceStart, length);
Region tail = head;
for (Region n = next; n != null; n = n.next) {
Region q = new Region(n.resultStart, n.sourceStart, n.length);
tail.next = q;
tail = q;
}
return head;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
Region r = this;
do {
if (r != this)
buf.append(',');
buf.append(r.resultStart);
buf.append('-');
buf.append(r.resultStart + r.length);
r = r.next;
} while (r != null);
return buf.toString();
}
}

113
org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java

@ -0,0 +1,113 @@
/*
* Copyright (C) 2011, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.blame;
import java.io.IOException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
final class ReverseWalk extends RevWalk {
ReverseWalk(Repository repo) {
super(repo);
}
@Override
public ReverseCommit next() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
ReverseCommit c = (ReverseCommit) super.next();
if (c == null)
return null;
for (int pIdx = 0; pIdx < c.getParentCount(); pIdx++)
((ReverseCommit) c.getParent(pIdx)).addChild(c);
return c;
}
@Override
protected RevCommit createCommit(AnyObjectId id) {
return new ReverseCommit(id);
}
static final class ReverseCommit extends RevCommit {
private static final ReverseCommit[] NO_CHILDREN = {};
private ReverseCommit[] children = NO_CHILDREN;
ReverseCommit(AnyObjectId id) {
super(id);
}
void addChild(ReverseCommit c) {
// Always put the most recent child onto the front of the list.
// This works correctly because our ReverseWalk parent (above)
// runs in COMMIT_TIME_DESC order. Older commits will be popped
// later and should go in front of the children list so they are
// visited first by BlameGenerator when considering candidates.
int cnt = children.length;
if (cnt == 0)
children = new ReverseCommit[] { c };
else if (cnt == 1)
children = new ReverseCommit[] { c, children[0] };
else {
ReverseCommit[] n = new ReverseCommit[1 + cnt];
n[0] = c;
System.arraycopy(children, 0, n, 1, cnt);
children = n;
}
}
int getChildCount() {
return children.length;
}
ReverseCommit getChild(final int nth) {
return children[nth];
}
}
}
Loading…
Cancel
Save