Refactor EditTransformer to take new inputs

EditTransformer is the class that we use to identify the edits due to
rebase between 2 commits. These rebase edits are a function of the edits
between these 2 commits, each of the commits and its parent, and the
edits between the parent commits.

The previous implementation of EditTransformer accepted a list of
PatchListEntry(s) corresponding to multiple changed files between 2
commits. This was not optimal, since EditTransformer uses only a small
subset of fields of PatchListEntry.

This change introduces a new entity class "FileEdits" containing only
the fields needed by EditTransformer, namely the list of edits and the
old/new file paths.

New methods are introduced in EditTransformer to use the FileEdits
entities, also some of the methods using the PatchListEntry are
eliminated. PatchListLoader (the old diff cache implementation) is
modified to use the new methods. Subsequent changes will introduce the
git file diff and the file diff caches which should use the new methods.

Change-Id: I3d31180b44d2495c4fa9ee8902f979f23c47cf4b
This commit is contained in:
Youssef Elghareeb
2020-09-02 12:23:21 +02:00
parent 97d2bcdb22
commit e48e9c2558
6 changed files with 230 additions and 67 deletions

View File

@@ -15,12 +15,17 @@
package com.google.gerrit.server.patch;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.server.patch.GitPositionTransformer.FileMapping;
import com.google.gerrit.server.patch.GitPositionTransformer.Mapping;
import com.google.gerrit.server.patch.GitPositionTransformer.Range;
import com.google.gerrit.server.patch.GitPositionTransformer.RangeMapping;
import com.google.gerrit.server.patch.entities.Edit;
import com.google.gerrit.server.patch.entities.FileEdits;
import java.util.List;
/** Mappings derived from diffs. */
public class DiffMappings {
@@ -33,31 +38,47 @@ public class DiffMappings {
return Mapping.create(fileMapping, rangeMappings);
}
private static FileMapping toFileMapping(PatchListEntry patchListEntry) {
switch (patchListEntry.getChangeType()) {
public static Mapping toMapping(FileEdits fileEdits) {
FileMapping fileMapping = FileMapping.forFile(fileEdits.oldPath(), fileEdits.newPath());
ImmutableSet<RangeMapping> rangeMappings = toRangeMappings(fileEdits.edits());
return Mapping.create(fileMapping, rangeMappings);
}
private static FileMapping toFileMapping(PatchListEntry ple) {
return toFileMapping(ple.getChangeType(), ple.getOldName(), ple.getNewName());
}
private static FileMapping toFileMapping(
Patch.ChangeType changeType, String oldName, String newName) {
switch (changeType) {
case ADDED:
return FileMapping.forAddedFile(patchListEntry.getNewName());
return FileMapping.forAddedFile(newName);
case MODIFIED:
case REWRITE:
return FileMapping.forModifiedFile(patchListEntry.getNewName());
return FileMapping.forModifiedFile(newName);
case DELETED:
// Name of deleted file is mentioned as newName.
return FileMapping.forDeletedFile(patchListEntry.getNewName());
return FileMapping.forDeletedFile(newName);
case RENAMED:
case COPIED:
return FileMapping.forRenamedFile(patchListEntry.getOldName(), patchListEntry.getNewName());
return FileMapping.forRenamedFile(oldName, newName);
default:
throw new IllegalStateException("Unmapped diff type: " + patchListEntry.getChangeType());
throw new IllegalStateException("Unmapped diff type: " + changeType);
}
}
private static ImmutableSet<RangeMapping> toRangeMappings(PatchListEntry patchListEntry) {
return patchListEntry.getEdits().stream()
return toRangeMappings(
patchListEntry.getEdits().stream().map(Edit::fromJGitEdit).collect(toList()));
}
private static ImmutableSet<RangeMapping> toRangeMappings(List<Edit> edits) {
return edits.stream()
.map(
edit ->
RangeMapping.create(
Range.create(edit.getBeginA(), edit.getEndA()),
Range.create(edit.getBeginB(), edit.getEndB())))
Range.create(edit.beginA(), edit.endA()),
Range.create(edit.beginB(), edit.endB())))
.collect(toImmutableSet());
}
}

View File

@@ -19,7 +19,6 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Multimaps.toMultimap;
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.ImmutableSet;
@@ -31,12 +30,13 @@ import com.google.gerrit.server.patch.GitPositionTransformer.OmitPositionOnConfl
import com.google.gerrit.server.patch.GitPositionTransformer.Position;
import com.google.gerrit.server.patch.GitPositionTransformer.PositionedEntity;
import com.google.gerrit.server.patch.GitPositionTransformer.Range;
import com.google.gerrit.server.patch.entities.Edit;
import com.google.gerrit.server.patch.entities.FileEdits;
import java.util.List;
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
@@ -54,40 +54,42 @@ class EditTransformer {
/**
* Creates a new {@code EditTransformer} for the edits contained in the specified {@code
* PatchListEntry}s.
* FileEdits}s.
*
* @param patchListEntries a list of {@code PatchListEntry}s containing the edits
* @param fileEdits a list of {@code FileEdits}s containing the edits
*/
public EditTransformer(List<PatchListEntry> patchListEntries) {
edits = patchListEntries.stream().flatMap(EditTransformer::toEdits).collect(toImmutableList());
public EditTransformer(List<FileEdits> fileEdits) {
// TODO(ghareeb): Can we replace FileEdits with another entity from the new refactored
// diff cache implementation? e.g. one of the GitFileDiffCache entities
edits = fileEdits.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.
* {@code treeA} and {@code treeB} and the specified {@code FileEdits}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'}
* @param transformingEntries a list of {@code FileEdits}s defining the transformation of {@code
* treeA} to {@code treeA'}
*/
public void transformReferencesOfSideA(List<PatchListEntry> transformationEntries) {
transformEdits(transformationEntries, SideAStrategy.INSTANCE);
public void transformReferencesOfSideA(ImmutableList<FileEdits> transformingEntries) {
transformEdits(transformingEntries, 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.
* {@code treeA} and {@code treeB} and the specified {@code FileEdits}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
* @param transformingEntries 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);
public void transformReferencesOfSideB(ImmutableList<FileEdits> transformingEntries) {
transformEdits(transformingEntries, SideBStrategy.INSTANCE);
}
/**
@@ -99,25 +101,33 @@ class EditTransformer {
return edits.stream()
.collect(
toMultimap(
ContextAwareEdit::getNewFilePath, Function.identity(), ArrayListMultimap::create));
c -> {
String path =
c.getNewFilePath().isPresent()
? c.getNewFilePath().get()
: c.getOldFilePath().get();
return path;
},
Function.identity(),
ArrayListMultimap::create));
}
public static Stream<ContextAwareEdit> toEdits(PatchListEntry patchListEntry) {
ImmutableList<Edit> edits = patchListEntry.getEdits();
public static Stream<ContextAwareEdit> toEdits(FileEdits in) {
List<Edit> edits = in.edits();
if (edits.isEmpty()) {
return Stream.of(ContextAwareEdit.createForNoContentEdit(patchListEntry));
return Stream.of(ContextAwareEdit.createForNoContentEdit(in.oldPath(), in.newPath()));
}
return edits.stream().map(edit -> ContextAwareEdit.create(patchListEntry, edit));
return edits.stream().map(edit -> ContextAwareEdit.create(in.oldPath(), in.newPath(), edit));
}
private void transformEdits(List<PatchListEntry> transformingEntries, SideStrategy sideStrategy) {
private void transformEdits(List<FileEdits> inputs, SideStrategy sideStrategy) {
ImmutableList<PositionedEntity<ContextAwareEdit>> positionedEdits =
edits.stream()
.map(edit -> toPositionedEntity(edit, sideStrategy))
.collect(toImmutableList());
ImmutableSet<Mapping> mappings =
transformingEntries.stream().map(DiffMappings::toMapping).collect(toImmutableSet());
inputs.stream().map(DiffMappings::toMapping).collect(toImmutableSet());
edits =
positionTransformer.transform(positionedEdits, mappings).stream()
@@ -133,41 +143,41 @@ class EditTransformer {
@AutoValue
abstract static class ContextAwareEdit {
static ContextAwareEdit create(PatchListEntry patchListEntry, Edit edit) {
static ContextAwareEdit create(Optional<String> oldPath, Optional<String> newPath, Edit edit) {
// TODO(ghareeb): Look if the new FileEdits class is capable of representing renames/copies
// and in this case we can get rid of the ContextAwareEdit class.
return create(
patchListEntry.getOldName(),
patchListEntry.getNewName(),
edit.getBeginA(),
edit.getEndA(),
edit.getBeginB(),
edit.getEndB(),
false);
oldPath, newPath, edit.beginA(), edit.endA(), edit.beginB(), edit.endB(), false);
}
static ContextAwareEdit createForNoContentEdit(PatchListEntry patchListEntry) {
static ContextAwareEdit createForNoContentEdit(
Optional<String> oldPath, Optional<String> newPath) {
// Remove the warning in createEditAtNewPosition() if we switch to an empty range instead of
// (-1:-1, -1:-1) in the future.
return create(
patchListEntry.getOldName(), patchListEntry.getNewName(), -1, -1, -1, -1, false);
return create(oldPath, newPath, -1, -1, -1, -1, false);
}
static ContextAwareEdit create(
String oldFilePath,
String newFilePath,
Optional<String> oldFilePath,
Optional<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;
Optional<String> adjustedFilePath = oldFilePath.isPresent() ? oldFilePath : newFilePath;
boolean implicitRename =
newFilePath.isPresent()
&& oldFilePath.isPresent()
&& !Objects.equals(oldFilePath.get(), newFilePath.get())
&& filePathAdjusted;
return new AutoValue_EditTransformer_ContextAwareEdit(
adjustedOldFilePath, newFilePath, beginA, endA, beginB, endB, implicitRename);
adjustedFilePath, newFilePath, beginA, endA, beginB, endB, implicitRename);
}
public abstract String getOldFilePath();
public abstract Optional<String> getOldFilePath();
public abstract String getNewFilePath();
public abstract Optional<String> getNewFilePath();
public abstract int getBeginA();
@@ -180,12 +190,13 @@ class EditTransformer {
// Used for equals(), for which this value is important.
public abstract boolean isImplicitRename();
public Optional<Edit> toEdit() {
public Optional<org.eclipse.jgit.diff.Edit> toEdit() {
if (getBeginA() < 0) {
return Optional.empty();
}
return Optional.of(new Edit(getBeginA(), getEndA(), getBeginB(), getEndB()));
return Optional.of(
new org.eclipse.jgit.diff.Edit(getBeginA(), getEndA(), getBeginB(), getEndB()));
}
}
@@ -200,8 +211,12 @@ class EditTransformer {
@Override
public Position extractPosition(ContextAwareEdit edit) {
String filePath =
edit.getOldFilePath().isPresent()
? edit.getOldFilePath().get()
: edit.getNewFilePath().get();
return Position.builder()
.filePath(edit.getOldFilePath())
.filePath(filePath)
.lineRange(Range.create(edit.getBeginA(), edit.getEndA()))
.build();
}
@@ -227,13 +242,13 @@ class EditTransformer {
newPosition);
}
return ContextAwareEdit.create(
updatedFilePath,
Optional.of(updatedFilePath),
edit.getNewFilePath(),
updatedRange.start(),
updatedRange.end(),
edit.getBeginB(),
edit.getEndB(),
!Objects.equals(edit.getOldFilePath(), updatedFilePath));
!Objects.equals(edit.getOldFilePath(), Optional.of(updatedFilePath)));
}
}
@@ -242,8 +257,12 @@ class EditTransformer {
@Override
public Position extractPosition(ContextAwareEdit edit) {
String filePath =
edit.getNewFilePath().isPresent()
? edit.getNewFilePath().get()
: edit.getOldFilePath().get();
return Position.builder()
.filePath(edit.getNewFilePath())
.filePath(filePath)
.lineRange(Range.create(edit.getBeginB(), edit.getEndB()))
.build();
}
@@ -255,7 +274,8 @@ class EditTransformer {
// in the future.
Range updatedRange = newPosition.lineRange().orElseGet(() -> Range.create(-1, -1));
// Same as far the range above. PATCHSET_LEVEL is a safe fallback.
String updatedFilePath = newPosition.filePath().orElse(Patch.PATCHSET_LEVEL);
Optional<String> updatedFilePath =
Optional.of(newPosition.filePath().orElse(Patch.PATCHSET_LEVEL));
return ContextAwareEdit.create(
edit.getOldFilePath(),
updatedFilePath,

View File

@@ -330,6 +330,11 @@ public class GitPositionTransformer {
return new AutoValue_GitPositionTransformer_FileMapping(
Optional.of(oldPath), Optional.of(newPath));
}
/** Creates a {@link FileMapping} using the old and new paths. */
public static FileMapping forFile(Optional<String> oldPath, Optional<String> newPath) {
return new AutoValue_GitPositionTransformer_FileMapping(oldPath, newPath);
}
}
/**

View File

@@ -39,6 +39,7 @@ import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.patch.EditTransformer.ContextAwareEdit;
import com.google.gerrit.server.patch.entities.FileEdits;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
@@ -306,13 +307,39 @@ public class PatchListLoader implements Callable<PatchList> {
getRelevantPatchListEntries(
parentDiffEntries, parentCommitA, parentCommitB, touchedFilePaths, df);
EditTransformer editTransformer = new EditTransformer(parentPatchListEntries);
editTransformer.transformReferencesOfSideA(oldPatches);
editTransformer.transformReferencesOfSideB(newPatches);
EditTransformer editTransformer = new EditTransformer(toFileEditsList(parentPatchListEntries));
editTransformer.transformReferencesOfSideA(toFileEditsList(oldPatches));
editTransformer.transformReferencesOfSideB(toFileEditsList(newPatches));
return EditsDueToRebaseResult.create(
relevantDiffEntries, editTransformer.getEditsPerFilePath());
}
private ImmutableList<FileEdits> toFileEditsList(List<PatchListEntry> entries) {
return entries.stream().map(PatchListLoader::toFileEdits).collect(toImmutableList());
}
private static FileEdits toFileEdits(PatchListEntry patchListEntry) {
Optional<String> oldName = Optional.empty();
Optional<String> newName = Optional.empty();
switch (patchListEntry.getChangeType()) {
case DELETED:
oldName = Optional.of(patchListEntry.getNewName());
break;
case ADDED:
case MODIFIED:
case REWRITE:
newName = Optional.of(patchListEntry.getNewName());
break;
case COPIED:
case RENAMED:
oldName = Optional.of(patchListEntry.getOldName());
newName = Optional.of(patchListEntry.getNewName());
break;
}
return FileEdits.create(patchListEntry.getEdits(), oldName, newName);
}
private static boolean isRootOrMergeCommit(RevCommit commit) {
return commit.getParentCount() != 1;
}
@@ -405,7 +432,7 @@ public class PatchListLoader implements Callable<PatchList> {
PatchListEntry patchListEntry =
newEntry(treeA, fileHeader, contentEditsDueToRebase, newSize, newSize - oldSize);
// All edits in a file are due to rebase -> exclude the file from the diff.
if (EditTransformer.toEdits(patchListEntry).allMatch(editsDueToRebase::contains)) {
if (EditTransformer.toEdits(toFileEdits(patchListEntry)).allMatch(editsDueToRebase::contains)) {
return Optional.empty();
}
return Optional.of(patchListEntry);

View File

@@ -0,0 +1,46 @@
// Copyright (C) 2020 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.entities;
import com.google.auto.value.AutoValue;
/**
* A modified region between 2 versions of the same content. This is the Gerrit entity class
* corresponding to {@link org.eclipse.jgit.diff.Edit} and is needed to ensure immutability when
* included as fields of the diff persisted caches.
*/
@AutoValue
public abstract class Edit {
static Edit create(int beginA, int endA, int beginB, int endB) {
return new AutoValue_Edit(beginA, endA, beginB, endB);
}
public static Edit fromJGitEdit(org.eclipse.jgit.diff.Edit jgitEdit) {
return create(
jgitEdit.getBeginA(), jgitEdit.getEndA(), jgitEdit.getBeginB(), jgitEdit.getEndB());
}
/** Start of a region in sequence A. */
public abstract int beginA();
/** End of a region in sequence A. */
public abstract int endA();
/** Start of a region in sequence B. */
public abstract int beginB();
/** End of a region in sequence B. */
public abstract int endB();
}

View File

@@ -0,0 +1,44 @@
// Copyright (C) 2020 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.entities;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Optional;
/**
* An entity class containing the list of edits between 2 commits for a file, and the old and new
* paths.
*/
@AutoValue
public abstract class FileEdits {
public static FileEdits create(
List<org.eclipse.jgit.diff.Edit> jgitEdits,
Optional<String> oldPath,
Optional<String> newPath) {
ImmutableList<Edit> edits =
jgitEdits.stream().map(Edit::fromJGitEdit).collect(toImmutableList());
return new AutoValue_FileEdits(edits, oldPath, newPath);
}
public abstract ImmutableList<Edit> edits();
public abstract Optional<String> oldPath();
public abstract Optional<String> newPath();
}