Fix exception due to duplicate keys in EditTransformer

As copies are treated as additions in the current version of JGit,
having multiple PatchListEntries referring to the same file path
didn't seem possible. However, when a file is renamed alongside
those copies, JGit identifies the copies as such.

The logic which transforms the edits from the parents' trees to the
children's trees assumed that one edit was only mapped to a
transformed edit. With the existence of copies, we need to map one
edit to several ones instead.

All in all, we compute the cross product of all possible combinations.
When computing a real diff between two trees, not all of them would
be present at the same time. As it seems difficult to figure out
exactly which edits are 'necessary' to keep, we rather keep all of
them for the moment and accept that we possibly waste some resources.

Change-Id: I6d57fb6a49b8dea58aab9565ac41301c7365159f
This commit is contained in:
Alice Kober-Sotzek 2017-07-13 18:05:52 +02:00
parent a29041ce24
commit 264143d5b5
3 changed files with 76 additions and 15 deletions

View File

@ -138,6 +138,20 @@ public class RevisionDiffIT extends AbstractDaemonTest {
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, newFilePath);
}
@Test
public void copiedFileTreatedAsAddedFileInDiff() throws Exception {
String copyFilePath = "copy_of_some_file.txt";
gApi.changes().id(changeId).edit().modifyFile(copyFilePath, RawInputUtil.create(FILE_CONTENT));
gApi.changes().id(changeId).edit().publish();
Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files();
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, copyFilePath);
// If this ever changes, please add tests which cover copied files.
assertThat(changedFiles.get(copyFilePath)).status().isEqualTo('A');
assertThat(changedFiles.get(copyFilePath)).linesInserted().isEqualTo(100);
assertThat(changedFiles.get(copyFilePath)).linesDeleted().isNull();
}
@Test
public void addedBinaryFileIsIncludedInDiff() throws Exception {
String imageFileName = "an_image.png";
@ -1039,6 +1053,43 @@ public class RevisionDiffIT extends AbstractDaemonTest {
assertThat(diffInfo).content().element(2).commonLines().hasSize(95);
}
@Test
public void copiedAndRenamedFilesWithOnlyRebaseHunksAreIdentified() throws Exception {
String newFileContent = FILE_CONTENT.replace("Line 5\n", "Line five\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
// Copies are only identified by JGit when paired with renaming.
String copyFileName = "copy_of_some_file.txt";
String renamedFileName = "renamed_some_file.txt";
gApi.changes()
.id(changeId)
.edit()
.modifyFile(copyFileName, RawInputUtil.create(newFileContent));
gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFileName);
gApi.changes().id(changeId).edit().publish();
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, copyFileName, renamedFileName);
DiffInfo renamedFileDiffInfo =
getDiffRequest(changeId, CURRENT, renamedFileName).withBase(initialPatchSetId).get();
assertThat(renamedFileDiffInfo).content().element(0).commonLines().hasSize(4);
assertThat(renamedFileDiffInfo).content().element(1).linesOfA().containsExactly("Line 5");
assertThat(renamedFileDiffInfo).content().element(1).linesOfB().containsExactly("Line five");
assertThat(renamedFileDiffInfo).content().element(1).isDueToRebase();
assertThat(renamedFileDiffInfo).content().element(2).commonLines().hasSize(95);
DiffInfo copiedFileDiffInfo =
getDiffRequest(changeId, CURRENT, copyFileName).withBase(initialPatchSetId).get();
assertThat(copiedFileDiffInfo).content().element(0).commonLines().hasSize(4);
assertThat(copiedFileDiffInfo).content().element(1).linesOfA().containsExactly("Line 5");
assertThat(copiedFileDiffInfo).content().element(1).linesOfB().containsExactly("Line five");
assertThat(copiedFileDiffInfo).content().element(1).isDueToRebase();
assertThat(copiedFileDiffInfo).content().element(2).commonLines().hasSize(95);
}
/*
* change PS B
* |

View File

@ -19,7 +19,6 @@ import static com.google.common.collect.Multimaps.toMultimap;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
@ -109,10 +108,8 @@ class EditTransformer {
private void transformEdits(List<PatchListEntry> transformingEntries, SideStrategy sideStrategy) {
Map<String, List<ContextAwareEdit>> editsPerFilePath =
edits.stream().collect(groupingBy(sideStrategy::getFilePath));
Map<String, PatchListEntry> transformingEntryPerPath =
transformingEntries
.stream()
.collect(toMap(EditTransformer::getOldFilePath, Function.identity()));
Map<String, List<PatchListEntry>> transEntriesPerPath =
transformingEntries.stream().collect(groupingBy(EditTransformer::getOldFilePath));
edits =
editsPerFilePath
@ -120,10 +117,9 @@ class EditTransformer {
.stream()
.flatMap(
pathAndEdits -> {
PatchListEntry transformingEntry =
transformingEntryPerPath.get(pathAndEdits.getKey());
return transformEdits(sideStrategy, pathAndEdits.getValue(), transformingEntry)
.stream();
List<PatchListEntry> transEntries =
transEntriesPerPath.getOrDefault(pathAndEdits.getKey(), ImmutableList.of());
return transformEdits(sideStrategy, pathAndEdits.getValue(), transEntries);
})
.collect(toList());
}
@ -132,15 +128,22 @@ class EditTransformer {
return MoreObjects.firstNonNull(patchListEntry.getOldName(), patchListEntry.getNewName());
}
private static List<ContextAwareEdit> transformEdits(
private static Stream<ContextAwareEdit> transformEdits(
SideStrategy sideStrategy,
List<ContextAwareEdit> originalEdits,
PatchListEntry transformingEntry) {
if (transformingEntry == null) {
return originalEdits;
List<PatchListEntry> transformingEntries) {
if (transformingEntries.isEmpty()) {
return originalEdits.stream();
}
return transformEdits(
sideStrategy, originalEdits, transformingEntry.getEdits(), transformingEntry.getNewName());
// TODO(aliceks): Find a way to prevent an explosion of the number of entries.
return transformingEntries
.stream()
.flatMap(
transEntry ->
transformEdits(
sideStrategy, originalEdits, transEntry.getEdits(), transEntry.getNewName())
.stream());
}
private static List<ContextAwareEdit> transformEdits(

View File

@ -16,6 +16,7 @@ package com.google.gerrit.extensions.common;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.ComparableSubject;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.IntegerSubject;
import com.google.common.truth.Subject;
@ -51,4 +52,10 @@ public class FileInfoSubject extends Subject<FileInfoSubject, FileInfo> {
FileInfo fileInfo = actual();
return Truth.assertThat(fileInfo.linesDeleted).named("linesDeleted");
}
public ComparableSubject<?, Character> status() {
isNotNull();
FileInfo fileInfo = actual();
return Truth.assertThat(fileInfo.status).named("status");
}
}