* stable-2.15: Detect RawInput correctly Remove unnecessary 'algorithm' parameter of PatchListKey Remove some more false negatives for edits due to rebase Remove some false negatives for edits due to rebase Change-Id: I1a95b209c2091d4993e7cc590f5177a877136be5
314 lines
11 KiB
Java
314 lines
11 KiB
Java
// Copyright (C) 2017 The Android Open Source Project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package com.google.gerrit.server.patch;
|
|
|
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
|
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 com.google.auto.value.AutoValue;
|
|
import com.google.common.base.MoreObjects;
|
|
import com.google.common.collect.ArrayListMultimap;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.Multimap;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.function.Function;
|
|
import java.util.stream.Stream;
|
|
import org.eclipse.jgit.diff.Edit;
|
|
|
|
/**
|
|
* Transformer of edits regarding their base trees. An edit describes a difference between {@code
|
|
* treeA} and {@code treeB}. This class allows to describe the edit as a difference between {@code
|
|
* treeA'} and {@code treeB'} given the transformation of {@code treeA} to {@code treeA'} and {@code
|
|
* treeB} to {@code treeB'}. Edits which can't be transformed due to conflicts with the
|
|
* transformation are omitted.
|
|
*/
|
|
class EditTransformer {
|
|
|
|
private List<ContextAwareEdit> edits;
|
|
|
|
/**
|
|
* Creates a new {@code EditTransformer} for the edits contained in the specified {@code
|
|
* PatchListEntry}s.
|
|
*
|
|
* @param patchListEntries a list of {@code PatchListEntry}s containing the edits
|
|
*/
|
|
public EditTransformer(List<PatchListEntry> patchListEntries) {
|
|
edits = patchListEntries.stream().flatMap(EditTransformer::toEdits).collect(toImmutableList());
|
|
}
|
|
|
|
/**
|
|
* Transforms the references of side A of the edits. If the edits describe differences between
|
|
* {@code treeA} and {@code treeB} and the specified {@code PatchListEntry}s define a
|
|
* transformation from {@code treeA} to {@code treeA'}, the resulting edits will be defined as
|
|
* differences between {@code treeA'} and {@code treeB}. Edits which can't be transformed due to
|
|
* conflicts with the transformation are omitted.
|
|
*
|
|
* @param transformationEntries a list of {@code PatchListEntry}s defining the transformation of
|
|
* {@code treeA} to {@code treeA'}
|
|
*/
|
|
public void transformReferencesOfSideA(List<PatchListEntry> transformationEntries) {
|
|
transformEdits(transformationEntries, SideAStrategy.INSTANCE);
|
|
}
|
|
|
|
/**
|
|
* Transforms the references of side B of the edits. If the edits describe differences between
|
|
* {@code treeA} and {@code treeB} and the specified {@code PatchListEntry}s define a
|
|
* transformation from {@code treeB} to {@code treeB'}, the resulting edits will be defined as
|
|
* differences between {@code treeA} and {@code treeB'}. Edits which can't be transformed due to
|
|
* conflicts with the transformation are omitted.
|
|
*
|
|
* @param transformationEntries a list of {@code PatchListEntry}s defining the transformation of
|
|
* {@code treeB} to {@code treeB'}
|
|
*/
|
|
public void transformReferencesOfSideB(List<PatchListEntry> transformationEntries) {
|
|
transformEdits(transformationEntries, SideBStrategy.INSTANCE);
|
|
}
|
|
|
|
/**
|
|
* Returns the transformed edits per file path they modify in {@code treeB'}.
|
|
*
|
|
* @return the transformed edits per file path
|
|
*/
|
|
public Multimap<String, ContextAwareEdit> getEditsPerFilePath() {
|
|
return edits
|
|
.stream()
|
|
.collect(
|
|
toMultimap(
|
|
ContextAwareEdit::getNewFilePath, Function.identity(), ArrayListMultimap::create));
|
|
}
|
|
|
|
public static Stream<ContextAwareEdit> toEdits(PatchListEntry patchListEntry) {
|
|
ImmutableList<Edit> edits = patchListEntry.getEdits();
|
|
if (edits.isEmpty()) {
|
|
return Stream.of(ContextAwareEdit.createForNoContentEdit(patchListEntry));
|
|
}
|
|
|
|
return edits.stream().map(edit -> ContextAwareEdit.create(patchListEntry, edit));
|
|
}
|
|
|
|
private void transformEdits(List<PatchListEntry> transformingEntries, SideStrategy sideStrategy) {
|
|
Map<String, List<ContextAwareEdit>> editsPerFilePath =
|
|
edits.stream().collect(groupingBy(sideStrategy::getFilePath));
|
|
Map<String, List<PatchListEntry>> transEntriesPerPath =
|
|
transformingEntries.stream().collect(groupingBy(EditTransformer::getOldFilePath));
|
|
|
|
edits =
|
|
editsPerFilePath
|
|
.entrySet()
|
|
.stream()
|
|
.flatMap(
|
|
pathAndEdits -> {
|
|
List<PatchListEntry> transEntries =
|
|
transEntriesPerPath.getOrDefault(pathAndEdits.getKey(), ImmutableList.of());
|
|
return transformEdits(sideStrategy, pathAndEdits.getValue(), transEntries);
|
|
})
|
|
.collect(toList());
|
|
}
|
|
|
|
private static String getOldFilePath(PatchListEntry patchListEntry) {
|
|
return MoreObjects.firstNonNull(patchListEntry.getOldName(), patchListEntry.getNewName());
|
|
}
|
|
|
|
private static Stream<ContextAwareEdit> transformEdits(
|
|
SideStrategy sideStrategy,
|
|
List<ContextAwareEdit> originalEdits,
|
|
List<PatchListEntry> transformingEntries) {
|
|
if (transformingEntries.isEmpty()) {
|
|
return originalEdits.stream();
|
|
}
|
|
|
|
// 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(
|
|
SideStrategy sideStrategy,
|
|
List<ContextAwareEdit> unorderedOriginalEdits,
|
|
List<Edit> unorderedTransformingEdits,
|
|
String adjustedFilePath) {
|
|
List<ContextAwareEdit> originalEdits = new ArrayList<>(unorderedOriginalEdits);
|
|
originalEdits.sort(comparing(sideStrategy::getBegin).thenComparing(sideStrategy::getEnd));
|
|
List<Edit> transformingEdits = new ArrayList<>(unorderedTransformingEdits);
|
|
transformingEdits.sort(comparing(Edit::getBeginA).thenComparing(Edit::getEndA));
|
|
|
|
int shiftedAmount = 0;
|
|
int transIndex = 0;
|
|
int origIndex = 0;
|
|
List<ContextAwareEdit> resultingEdits = new ArrayList<>(originalEdits.size());
|
|
while (origIndex < originalEdits.size() && transIndex < transformingEdits.size()) {
|
|
ContextAwareEdit originalEdit = originalEdits.get(origIndex);
|
|
Edit transformingEdit = transformingEdits.get(transIndex);
|
|
if (transformingEdit.getEndA() <= sideStrategy.getBegin(originalEdit)) {
|
|
shiftedAmount = transformingEdit.getEndB() - transformingEdit.getEndA();
|
|
transIndex++;
|
|
} else if (sideStrategy.getEnd(originalEdit) <= transformingEdit.getBeginA()) {
|
|
resultingEdits.add(sideStrategy.create(originalEdit, shiftedAmount, adjustedFilePath));
|
|
origIndex++;
|
|
} else {
|
|
// Overlapping -> ignore.
|
|
origIndex++;
|
|
}
|
|
}
|
|
for (int i = origIndex; i < originalEdits.size(); i++) {
|
|
resultingEdits.add(
|
|
sideStrategy.create(originalEdits.get(i), shiftedAmount, adjustedFilePath));
|
|
}
|
|
return resultingEdits;
|
|
}
|
|
|
|
@AutoValue
|
|
abstract static class ContextAwareEdit {
|
|
static ContextAwareEdit create(PatchListEntry patchListEntry, Edit edit) {
|
|
return create(
|
|
patchListEntry.getOldName(),
|
|
patchListEntry.getNewName(),
|
|
edit.getBeginA(),
|
|
edit.getEndA(),
|
|
edit.getBeginB(),
|
|
edit.getEndB(),
|
|
false);
|
|
}
|
|
|
|
static ContextAwareEdit createForNoContentEdit(PatchListEntry patchListEntry) {
|
|
return create(
|
|
patchListEntry.getOldName(), patchListEntry.getNewName(), -1, -1, -1, -1, false);
|
|
}
|
|
|
|
static ContextAwareEdit create(
|
|
String oldFilePath,
|
|
String newFilePath,
|
|
int beginA,
|
|
int endA,
|
|
int beginB,
|
|
int endB,
|
|
boolean filePathAdjusted) {
|
|
String adjustedOldFilePath = MoreObjects.firstNonNull(oldFilePath, newFilePath);
|
|
boolean implicitRename = !Objects.equals(oldFilePath, newFilePath) && filePathAdjusted;
|
|
return new AutoValue_EditTransformer_ContextAwareEdit(
|
|
adjustedOldFilePath, newFilePath, beginA, endA, beginB, endB, implicitRename);
|
|
}
|
|
|
|
public abstract String getOldFilePath();
|
|
|
|
public abstract String getNewFilePath();
|
|
|
|
public abstract int getBeginA();
|
|
|
|
public abstract int getEndA();
|
|
|
|
public abstract int getBeginB();
|
|
|
|
public abstract int getEndB();
|
|
|
|
// Used for equals(), for which this value is important.
|
|
public abstract boolean isImplicitRename();
|
|
|
|
public Optional<Edit> toEdit() {
|
|
if (getBeginA() < 0) {
|
|
return Optional.empty();
|
|
}
|
|
|
|
return Optional.of(new Edit(getBeginA(), getEndA(), getBeginB(), getEndB()));
|
|
}
|
|
}
|
|
|
|
private interface SideStrategy {
|
|
String getFilePath(ContextAwareEdit edit);
|
|
|
|
int getBegin(ContextAwareEdit edit);
|
|
|
|
int getEnd(ContextAwareEdit edit);
|
|
|
|
ContextAwareEdit create(ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath);
|
|
}
|
|
|
|
private enum SideAStrategy implements SideStrategy {
|
|
INSTANCE;
|
|
|
|
@Override
|
|
public String getFilePath(ContextAwareEdit edit) {
|
|
return edit.getOldFilePath();
|
|
}
|
|
|
|
@Override
|
|
public int getBegin(ContextAwareEdit edit) {
|
|
return edit.getBeginA();
|
|
}
|
|
|
|
@Override
|
|
public int getEnd(ContextAwareEdit edit) {
|
|
return edit.getEndA();
|
|
}
|
|
|
|
@Override
|
|
public ContextAwareEdit create(
|
|
ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) {
|
|
return ContextAwareEdit.create(
|
|
adjustedFilePath,
|
|
edit.getNewFilePath(),
|
|
edit.getBeginA() + shiftedAmount,
|
|
edit.getEndA() + shiftedAmount,
|
|
edit.getBeginB(),
|
|
edit.getEndB(),
|
|
!Objects.equals(edit.getOldFilePath(), adjustedFilePath));
|
|
}
|
|
}
|
|
|
|
private enum SideBStrategy implements SideStrategy {
|
|
INSTANCE;
|
|
|
|
@Override
|
|
public String getFilePath(ContextAwareEdit edit) {
|
|
return edit.getNewFilePath();
|
|
}
|
|
|
|
@Override
|
|
public int getBegin(ContextAwareEdit edit) {
|
|
return edit.getBeginB();
|
|
}
|
|
|
|
@Override
|
|
public int getEnd(ContextAwareEdit edit) {
|
|
return edit.getEndB();
|
|
}
|
|
|
|
@Override
|
|
public ContextAwareEdit create(
|
|
ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) {
|
|
return ContextAwareEdit.create(
|
|
edit.getOldFilePath(),
|
|
adjustedFilePath,
|
|
edit.getBeginA(),
|
|
edit.getEndA(),
|
|
edit.getBeginB() + shiftedAmount,
|
|
edit.getEndB() + shiftedAmount,
|
|
!Objects.equals(edit.getNewFilePath(), adjustedFilePath));
|
|
}
|
|
}
|
|
}
|