<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.googlecode.java-diff-utils</groupId>
+ <artifactId>diffutils</artifactId>
+ <version>1.2</version>
+ </dependency>
<dependency>
<groupId>org.picocontainer</groupId>
<artifactId>picocontainer</artifactId>
*/
package org.sonar.server.computation.task.projectanalysis.scm;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
return new GeneratedScmInfo(changesets);
}
- public static ScmInfo create(long analysisDate, Set<Integer> lines, ScmInfo dbScmInfo) {
- checkState(!lines.isEmpty(), "No changesets");
-
+ public static ScmInfo create(long analysisDate, int[] matches, ScmInfo dbScmInfo) {
Changeset changeset = Changeset.newChangesetBuilder()
.setDate(analysisDate)
.build();
- Map<Integer, Changeset> changesets = lines.stream()
- .collect(Collectors.toMap(x -> x, i -> changeset));
- dbScmInfo.getAllChangesets().entrySet().stream()
- .filter(e -> !lines.contains(e.getKey()))
- .forEach(e -> changesets.put(e.getKey(), e.getValue()));
+ Map<Integer, Changeset> dbChangesets = dbScmInfo.getAllChangesets();
+ Map<Integer, Changeset> changesets = new LinkedHashMap<>(matches.length);
+ for (int i = 0; i < matches.length; i++) {
+ if (matches[i] > 0) {
+ changesets.put(i + 1, dbChangesets.get(matches[i]));
+ } else {
+ changesets.put(i + 1, changeset);
+ }
+ }
return new GeneratedScmInfo(changesets);
}
return Optional.of(GeneratedScmInfo.create(analysisMetadata.getAnalysisDate(), newOrChangedLines));
}
- private ScmInfo removeAuthorAndRevision(ScmInfo info) {
+ private static ScmInfo removeAuthorAndRevision(ScmInfo info) {
Map<Integer, Changeset> cleanedScmInfo = info.getAllChangesets().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> removeAuthorAndRevision(e.getValue())));
return new ScmInfoImpl(cleanedScmInfo);
}
// generate date for new/changed lines
- Set<Integer> newOrChangedLines = sourceLinesDiff.getNewOrChangedLines(file);
- if (newOrChangedLines.isEmpty()) {
- return Optional.of(scmInfo);
- }
- return Optional.of(GeneratedScmInfo.create(analysisMetadata.getAnalysisDate(), newOrChangedLines, scmInfo));
+ int[] matchingLines = sourceLinesDiff.getMatchingLines(file);
+
+ return Optional.of(GeneratedScmInfo.create(analysisMetadata.getAnalysisDate(), matchingLines, scmInfo));
}
}
*/
package org.sonar.server.computation.task.projectanalysis.source;
-import java.util.Set;
import org.sonar.server.computation.task.projectanalysis.component.Component;
public interface SourceLinesDiff {
- Set<Integer> getNewOrChangedLines(Component component);
+ int[] getMatchingLines(Component component);
}
*/
package org.sonar.server.computation.task.projectanalysis.source;
-import java.util.HashSet;
+import difflib.myers.MyersDiff;
+import difflib.myers.PathNode;
import java.util.List;
-import java.util.Set;
public class SourceLinesDiffFinder {
this.report = report;
}
- public Set<Integer> findNewOrChangedLines() {
- return walk(0, 0, new HashSet<>());
- }
-
- private Set<Integer> walk(int r, int db, HashSet<Integer> acc) {
+ /**
+ * Creates a diff between the file in the database and the file in the report using Myers' algorithm, and links matching lines between
+ * both files.
+ * @return an array with one entry for each line in the report. Those entries point either to a line in the database, or to 0,
+ * in which case it means the line was added.
+ */
+ public int[] findMatchingLines() {
+ int[] index = new int[report.size()];
- if (r >= report.size()) {
- return acc;
- }
+ int dbLine = database.size();
+ int reportLine = report.size();
+ try {
+ PathNode node = MyersDiff.buildPath(database.toArray(), report.toArray());
- if (db < database.size()) {
-
- if (report.get(r).equals(database.get(db))) {
- walk(stepIndex(r), stepIndex(db), acc);
- return acc;
- }
+ while (node.prev != null) {
+ PathNode prevNode = node.prev;
- List<String> remainingDatabase = database.subList(db, database.size());
- if (remainingDatabase.contains(report.get(r))) {
- int nextDb = db + remainingDatabase.indexOf(report.get(r));
- walk(r, nextDb, acc);
- return acc;
+ if (!node.isSnake()) {
+ // additions
+ reportLine -= (node.j - prevNode.j);
+ // removals
+ dbLine -= (node.i - prevNode.i);
+ } else {
+ // matches
+ for (int i = node.i; i > prevNode.i; i--) {
+ index[reportLine - 1] = dbLine;
+ reportLine--;
+ dbLine--;
+ }
+ }
+ node = prevNode;
}
-
+ } catch (Exception e) {
+ return index;
}
-
- acc.add(r+1);
- walk(stepIndex(r), db, acc);
- return acc;
- }
-
- private static int stepIndex(int r) {
- return ++r;
+ return index;
}
}
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
import org.sonar.core.hash.SourceLinesHashesComputer;
import org.sonar.core.util.CloseableIterator;
import org.sonar.db.DbClient;
}
@Override
- public Set<Integer> getNewOrChangedLines(Component component) {
+ public int[] getMatchingLines(Component component) {
- List<String> database = new ArrayList<>();
+ List<String> database;
try (DbSession dbSession = dbClient.openSession(false)) {
- database.addAll(fileSourceDao.selectLineHashes(dbSession, component.getUuid()));
+ database = fileSourceDao.selectLineHashes(dbSession, component.getUuid());
+ if (database == null) {
+ database = new ArrayList<>();
+ }
}
- List<String> report = new ArrayList<>();
+ List<String> report;
SourceLinesHashesComputer linesHashesComputer = new SourceLinesHashesComputer();
try (CloseableIterator<String> lineIterator = sourceLinesRepository.readLines(component)) {
while (lineIterator.hasNext()) {
linesHashesComputer.addLine(line);
}
}
- report.addAll(linesHashesComputer.getLineHashes());
+ report = linesHashesComputer.getLineHashes();
- return new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ return new SourceLinesDiffFinder(database, report).findMatchingLines();
}
package org.sonar.server.computation.task.projectanalysis.scm;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
@Test
public void generate_scm_info_for_new_and_changed_lines_when_report_is_empty() {
createDbScmInfoWithOneLine("hash");
- when(diff.getNewOrChangedLines(FILE)).thenReturn(ImmutableSet.of(2, 3));
+ when(diff.getMatchingLines(FILE)).thenReturn(new int[] {1, 0, 0});
addFileSourceInReport(3);
ScmInfo scmInfo = underTest.getScmInfo(FILE).get();
assertThat(scmInfo.getAllChangesets()).hasSize(3);
assertChangeset(scmInfo.getChangesetForLine(3), null, null, analysisDate.getTime());
verify(dbLoader).getScmInfo(FILE);
- verify(diff).getNewOrChangedLines(FILE);
+ verify(diff).getMatchingLines(FILE);
verifyNoMoreInteractions(dbLoader);
verifyZeroInteractions(sourceHashRepository);
verifyNoMoreInteractions(diff);
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
report.add("line - 3");
report.add("line - 4");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).isEmpty();
+ assertThat(diff).containsExactly(1, 2, 3, 4, 5);
}
report.add(" }\n");
report.add("}\n");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
-
- assertThat(diff).containsExactlyInAnyOrder(5, 6, 7, 8, 10, 11, 12);
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
+ assertThat(diff).containsExactly(1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 5, 6, 7);
}
report.add("line - 2");
report.add("line - 3");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).containsExactlyInAnyOrder(1, 2);
+ assertThat(diff).containsExactly(0, 0, 3, 4);
}
report.add("line - 2 - modified");
report.add("line - 3 - modified");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).containsExactlyInAnyOrder(3, 4);
+ assertThat(diff).containsExactly(1, 2, 0, 0);
}
report.add("line - 4");
report.add("line - 5");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).containsExactlyInAnyOrder(3, 4);
+ assertThat(diff).containsExactly(1, 2, 0, 0, 5, 6);
}
report.add("line - 1");
report.add("line - 2");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).containsExactlyInAnyOrder(1, 2);
+ assertThat(diff).containsExactly(0, 0, 1, 2, 3);
}
report.add("line - 2");
report.add("line - 3");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).containsExactlyInAnyOrder(3, 4);
+ assertThat(diff).containsExactly(1, 2, 0, 0, 3, 4);
}
report.add("line - new");
report.add("line - new");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).containsExactlyInAnyOrder(4, 5);
+ assertThat(diff).containsExactly(1, 2, 3, 0, 0);
}
report.add("line - 1");
report.add("line - 2");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).isEmpty();
+ assertThat(diff).containsExactly(1, 2, 3);
}
report.add("line - 4");
report.add("line - 5");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
-
- assertThat(diff).isEmpty();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
+ assertThat(diff).containsExactly(1, 2, 5, 6);
}
@Test
report.add("line - 2");
report.add("line - 3");
- Set<Integer> diff = new SourceLinesDiffFinder(database, report).findNewOrChangedLines();
+ int[] diff = new SourceLinesDiffFinder(database, report).findMatchingLines();
- assertThat(diff).isEmpty();
+ assertThat(diff).containsExactly(3, 4);
}
setFileContentInReport(FILE_REF, CONTENT);
Component component = fileComponent(FILE_REF);
- assertThat(underTest.getNewOrChangedLines(component)).isEmpty();
+ assertThat(underTest.getMatchingLines(component)).containsExactly(1, 2, 3, 4, 5, 6, 7);
}
orchestrator.executeBuild(scanner);
scmData = ws.getScmData(FILE_TO_ANALYSE);
- assertThat(scmData.size()).isEqualTo(4);
+ assertThat(scmData.size()).isEqualTo(3);
assertThat(scmData.get(1).revision).isEmpty();
assertThat(scmData.get(1).author).isEmpty();
assertThat(scmData.get(1).date).isInSameMinuteWindowAs(new Date());
assertThat(scmData.get(5).revision).isEmpty();
assertThat(scmData.get(5).author).isEmpty();
assertThat(scmData.get(5).date).isAfter(scmData.get(1).date);
-
+
+ assertThat(scmData.get(11).revision).isEmpty();
+ assertThat(scmData.get(11).author).isEmpty();
+ assertThat(scmData.get(11).date).isInSameMinuteWindowAs(new Date());
+
+ tester.openBrowser()
+ .openCode("sample-without-scm", "sample-without-scm:src/main/xoo/sample/Sample.xoo")
+ .getSourceViewer()
+ .shouldHaveNewLines(5, 6, 7, 8, 9, 10)
+ .shouldNotHaveNewLines(1, 2, 3, 4, 11, 12, 13);
}
private File disposableWorkspaceFor(String project) throws IOException {