| // 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; | |
| import static com.google.common.collect.Comparators.emptiesFirst; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; | |
| import static java.util.Comparator.comparing; | |
| import static java.util.stream.Collectors.collectingAndThen; | |
| import static java.util.stream.Collectors.groupingBy; | |
| import com.google.auto.value.AutoValue; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Sets; | |
| import com.google.common.collect.Streams; | |
| import java.util.Collection; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import java.util.function.BiFunction; | |
| import java.util.function.Function; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| /** | |
| * Transformer of {@link Position}s in one Git tree to {@link Position}s in another Git tree given | |
| * the {@link Mapping}s between the trees. | |
| * | |
| * <p>The base idea is that a {@link Position} in the source tree can be translated/mapped to a | |
| * corresponding {@link Position} in the target tree when we know how the target tree changed | |
| * compared to the source tree. As long as {@link Position}s are only defined via file path and line | |
| * range, we only need to know which file path in the source tree corresponds to which file path in | |
| * the target tree and how the lines within that file changed from the source to the target tree. | |
| * | |
| * <p>The algorithm is roughly: | |
| * | |
| * <ol> | |
| * <li>Go over all positions and replace the file path for each of them with the corresponding one | |
| * in the target tree. If a file path maps to two file paths in the target tree (copied file), | |
| * duplicate the position entry and use each of the new file paths with it. If a file path | |
| * maps to no file in the target tree (deleted file), apply the specified conflict strategy | |
| * (e.g. drop position completely or map to next best guess). | |
| * <li>Per file path, go through the file from top to bottom and keep track of how the range | |
| * mappings for that file shift the lines. Derive the shifted amount by comparing the number | |
| * of lines between source and target in the range mapping. While going through the file, | |
| * shift each encountered position by the currently tracked amount. If a position overlaps | |
| * with the lines of a range mapping, apply the specified conflict strategy (e.g. drop | |
| * position completely or map to next best guess). | |
| * </ol> | |
| */ | |
| public class GitPositionTransformer { | |
| private final PositionConflictStrategy positionConflictStrategy; | |
| /** | |
| * Creates a new {@code GitPositionTransformer} which uses the specified strategy for conflicts. | |
| */ | |
| public GitPositionTransformer(PositionConflictStrategy positionConflictStrategy) { | |
| this.positionConflictStrategy = positionConflictStrategy; | |
| } | |
| /** | |
| * Transforms the {@link Position}s of the specified entities as indicated via the {@link | |
| * Mapping}s. | |
| * | |
| * <p>This is typically used to transform the {@link Position}s in one Git tree (source) to the | |
| * corresponding {@link Position}s in another Git tree (target). The {@link Mapping}s need to | |
| * indicate all relevant changes between the source and target tree. {@link Mapping}s for files | |
| * not referenced by the given {@link Position}s need not be specified. They can be included, | |
| * though, as they aren't harmful. | |
| * | |
| * @param entities the entities whose {@link Position} should be mapped to the target tree | |
| * @param mappings the mappings describing all relevant changes between the source and the target | |
| * tree | |
| * @param <T> an entity which has a {@link Position} | |
| * @return a list of entities with transformed positions. There are no guarantees about the order | |
| * of the returned elements. | |
| */ | |
| public <T> ImmutableList<PositionedEntity<T>> transform( | |
| Collection<PositionedEntity<T>> entities, Set<Mapping> mappings) { | |
| // Update the file paths first as copied files might exist. For copied files, this operation | |
| // will duplicate the PositionedEntity instances of the original file. | |
| List<PositionedEntity<T>> filePathUpdatedEntities = updateFilePaths(entities, mappings); | |
| return shiftRanges(filePathUpdatedEntities, mappings); | |
| } | |
| private <T> ImmutableList<PositionedEntity<T>> updateFilePaths( | |
| Collection<PositionedEntity<T>> entities, Set<Mapping> mappings) { | |
| Map<String, ImmutableSet<String>> newFilesPerOldFile = groupNewFilesByOldFiles(mappings); | |
| return entities.stream() | |
| .flatMap(entity -> mapToNewFileIfChanged(newFilesPerOldFile, entity)) | |
| .collect(toImmutableList()); | |
| } | |
| private static Map<String, ImmutableSet<String>> groupNewFilesByOldFiles(Set<Mapping> mappings) { | |
| return mappings.stream() | |
| .map(Mapping::file) | |
| // Ignore file additions (irrelevant for mappings). | |
| .filter(mapping -> mapping.oldPath().isPresent()) | |
| .collect( | |
| groupingBy( | |
| mapping -> mapping.oldPath().orElse(""), | |
| collectingAndThen( | |
| Collectors.mapping(FileMapping::newPath, toImmutableSet()), | |
| // File deletion (empty Optional) -> empty set. | |
| GitPositionTransformer::unwrapOptionals))); | |
| } | |
| private static ImmutableSet<String> unwrapOptionals(ImmutableSet<Optional<String>> optionals) { | |
| return optionals.stream().flatMap(Streams::stream).collect(toImmutableSet()); | |
| } | |
| private <T> Stream<PositionedEntity<T>> mapToNewFileIfChanged( | |
| Map<String, ? extends Set<String>> newFilesPerOldFile, PositionedEntity<T> entity) { | |
| if (!entity.position().filePath().isPresent()) { | |
| // No mapping of file paths necessary if no file path is set. -> Keep existing entry. | |
| return Stream.of(entity); | |
| } | |
| String oldFilePath = entity.position().filePath().get(); | |
| if (!newFilesPerOldFile.containsKey(oldFilePath)) { | |
| // Unchanged files don't have a mapping. -> Keep existing entries. | |
| return Stream.of(entity); | |
| } | |
| Set<String> newFiles = newFilesPerOldFile.get(oldFilePath); | |
| if (newFiles.isEmpty()) { | |
| // File was deleted. | |
| return Streams.stream( | |
| positionConflictStrategy.getOnFileConflict(entity.position()).map(entity::withPosition)); | |
| } | |
| return newFiles.stream().map(entity::withFilePath); | |
| } | |
| private <T> ImmutableList<PositionedEntity<T>> shiftRanges( | |
| List<PositionedEntity<T>> filePathUpdatedEntities, Set<Mapping> mappings) { | |
| Map<String, ImmutableSet<RangeMapping>> mappingsPerNewFilePath = | |
| groupRangeMappingsByNewFilePath(mappings); | |
| return Stream.concat( | |
| // Keep positions without a file. | |
| filePathUpdatedEntities.stream() | |
| .filter(entity -> !entity.position().filePath().isPresent()), | |
| // Shift ranges per file. | |
| groupByFilePath(filePathUpdatedEntities).entrySet().stream() | |
| .flatMap( | |
| newFilePathAndEntities -> | |
| shiftRangesInOneFileIfChanged( | |
| mappingsPerNewFilePath, | |
| newFilePathAndEntities.getKey(), | |
| newFilePathAndEntities.getValue()) | |
| .stream())) | |
| .collect(toImmutableList()); | |
| } | |
| private static Map<String, ImmutableSet<RangeMapping>> groupRangeMappingsByNewFilePath( | |
| Set<Mapping> mappings) { | |
| return mappings.stream() | |
| // Ignore range mappings of deleted files. | |
| .filter(mapping -> mapping.file().newPath().isPresent()) | |
| .collect( | |
| groupingBy( | |
| mapping -> mapping.file().newPath().orElse(""), | |
| collectingAndThen( | |
| Collectors.<Mapping, Set<RangeMapping>>reducing( | |
| new HashSet<>(), Mapping::ranges, Sets::union), | |
| ImmutableSet::copyOf))); | |
| } | |
| private static <T> Map<String, ImmutableList<PositionedEntity<T>>> groupByFilePath( | |
| List<PositionedEntity<T>> fileUpdatedEntities) { | |
| return fileUpdatedEntities.stream() | |
| .filter(entity -> entity.position().filePath().isPresent()) | |
| .collect(groupingBy(entity -> entity.position().filePath().orElse(""), toImmutableList())); | |
| } | |
| private <T> ImmutableList<PositionedEntity<T>> shiftRangesInOneFileIfChanged( | |
| Map<String, ImmutableSet<RangeMapping>> mappingsPerNewFilePath, | |
| String newFilePath, | |
| ImmutableList<PositionedEntity<T>> sameFileEntities) { | |
| ImmutableSet<RangeMapping> sameFileRangeMappings = | |
| mappingsPerNewFilePath.getOrDefault(newFilePath, ImmutableSet.of()); | |
| if (sameFileRangeMappings.isEmpty()) { | |
| // Unchanged files and pure renames/copies don't have range mappings. -> Keep existing | |
| // entries. | |
| return sameFileEntities; | |
| } | |
| return shiftRangesInOneFile(sameFileEntities, sameFileRangeMappings); | |
| } | |
| private <T> ImmutableList<PositionedEntity<T>> shiftRangesInOneFile( | |
| List<PositionedEntity<T>> sameFileEntities, Set<RangeMapping> sameFileRangeMappings) { | |
| ImmutableList<PositionedEntity<T>> sortedEntities = sortByStartEnd(sameFileEntities); | |
| ImmutableList<RangeMapping> sortedMappings = sortByOldStartEnd(sameFileRangeMappings); | |
| int shiftedAmount = 0; | |
| int mappingIndex = 0; | |
| int entityIndex = 0; | |
| ImmutableList.Builder<PositionedEntity<T>> resultingEntities = | |
| ImmutableList.builderWithExpectedSize(sortedEntities.size()); | |
| while (entityIndex < sortedEntities.size() && mappingIndex < sortedMappings.size()) { | |
| PositionedEntity<T> entity = sortedEntities.get(entityIndex); | |
| if (entity.position().lineRange().isPresent()) { | |
| Range range = entity.position().lineRange().get(); | |
| RangeMapping mapping = sortedMappings.get(mappingIndex); | |
| if (mapping.oldLineRange().end() <= range.start()) { | |
| shiftedAmount = mapping.newLineRange().end() - mapping.oldLineRange().end(); | |
| mappingIndex++; | |
| } else if (range.end() <= mapping.oldLineRange().start()) { | |
| resultingEntities.add(entity.shiftPositionBy(shiftedAmount)); | |
| entityIndex++; | |
| } else { | |
| positionConflictStrategy | |
| .getOnRangeConflict(entity.position()) | |
| .map(entity::withPosition) | |
| .ifPresent(resultingEntities::add); | |
| entityIndex++; | |
| } | |
| } else { | |
| // No range -> no need to shift. | |
| resultingEntities.add(entity); | |
| entityIndex++; | |
| } | |
| } | |
| for (int i = entityIndex; i < sortedEntities.size(); i++) { | |
| resultingEntities.add(sortedEntities.get(i).shiftPositionBy(shiftedAmount)); | |
| } | |
| return resultingEntities.build(); | |
| } | |
| private static <T> ImmutableList<PositionedEntity<T>> sortByStartEnd( | |
| List<PositionedEntity<T>> entities) { | |
| return entities.stream() | |
| .sorted( | |
| comparing( | |
| entity -> entity.position().lineRange(), | |
| emptiesFirst(comparing(Range::start).thenComparing(Range::end)))) | |
| .collect(toImmutableList()); | |
| } | |
| private static ImmutableList<RangeMapping> sortByOldStartEnd(Set<RangeMapping> mappings) { | |
| return mappings.stream() | |
| .sorted( | |
| comparing( | |
| RangeMapping::oldLineRange, comparing(Range::start).thenComparing(Range::end))) | |
| .collect(toImmutableList()); | |
| } | |
| /** | |
| * A mapping from a {@link Position} in one Git commit/tree (source) to a {@link Position} in | |
| * another Git commit/tree (target). | |
| */ | |
| @AutoValue | |
| public abstract static class Mapping { | |
| /** A mapping describing how the attributes of one file are mapped from source to target. */ | |
| public abstract FileMapping file(); | |
| /** | |
| * Mappings describing how line ranges within the file indicated by {@link #file()} are mapped | |
| * from source to target. | |
| */ | |
| public abstract ImmutableSet<RangeMapping> ranges(); | |
| public static Mapping create(FileMapping fileMapping, Iterable<RangeMapping> rangeMappings) { | |
| return new AutoValue_GitPositionTransformer_Mapping( | |
| fileMapping, ImmutableSet.copyOf(rangeMappings)); | |
| } | |
| } | |
| /** | |
| * A mapping of attributes from a file in one Git tree (source) to a file in another Git tree | |
| * (target). | |
| * | |
| * <p>At the moment, only the file path is considered. Other attributes like file mode would be | |
| * imaginable too but are currently not supported. | |
| */ | |
| @AutoValue | |
| public abstract static class FileMapping { | |
| /** File path in the source tree. For file additions, this is an empty {@link Optional}. */ | |
| public abstract Optional<String> oldPath(); | |
| /** | |
| * File path in the target tree. Can be the same as {@link #oldPath()} if unchanged. For file | |
| * deletions, this is an empty {@link Optional}. | |
| */ | |
| public abstract Optional<String> newPath(); | |
| /** | |
| * Creates a {@link FileMapping} for a file addition. | |
| * | |
| * <p>In the context of {@link GitPositionTransformer}, file additions are irrelevant as no | |
| * given position in the source tree can refer to such a new file in the target tree. We still | |
| * provide this factory method so that code outside of {@link GitPositionTransformer} doesn't | |
| * have to care about such details and can simply create {@link FileMapping}s for any | |
| * modifications between the trees. | |
| */ | |
| public static FileMapping forAddedFile(String filePath) { | |
| return new AutoValue_GitPositionTransformer_FileMapping( | |
| Optional.empty(), Optional.of(filePath)); | |
| } | |
| /** Creates a {@link FileMapping} for a file deletion. */ | |
| public static FileMapping forDeletedFile(String filePath) { | |
| return new AutoValue_GitPositionTransformer_FileMapping( | |
| Optional.of(filePath), Optional.empty()); | |
| } | |
| /** Creates a {@link FileMapping} for a file modification. */ | |
| public static FileMapping forModifiedFile(String filePath) { | |
| return new AutoValue_GitPositionTransformer_FileMapping( | |
| Optional.of(filePath), Optional.of(filePath)); | |
| } | |
| /** Creates a {@link FileMapping} for a file renaming. */ | |
| public static FileMapping forRenamedFile(String oldPath, String newPath) { | |
| 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); | |
| } | |
| } | |
| /** | |
| * A mapping of a line range in one Git tree (source) to the corresponding line range in another | |
| * Git tree (target). | |
| */ | |
| @AutoValue | |
| public abstract static class RangeMapping { | |
| /** Range in the source tree. */ | |
| public abstract Range oldLineRange(); | |
| /** Range in the target tree. */ | |
| public abstract Range newLineRange(); | |
| /** | |
| * Creates a new {@code RangeMapping}. | |
| * | |
| * @param oldRange see {@link #oldLineRange()} | |
| * @param newRange see {@link #newLineRange()} | |
| */ | |
| public static RangeMapping create(Range oldRange, Range newRange) { | |
| return new AutoValue_GitPositionTransformer_RangeMapping(oldRange, newRange); | |
| } | |
| } | |
| /** | |
| * A position within the tree of a Git commit. | |
| * | |
| * <p>The term 'position' is our own invention. The underlying idea is that a Gerrit comment is at | |
| * a specific position within the commit of a patchset. That position is defined by the attributes | |
| * defined in this class. | |
| * | |
| * <p>The same thinking can be applied to diff hunks (= JGit edits). Each diff hunk maps a | |
| * position in one commit (e.g. in the parent of the patchset) to a position in another commit | |
| * (e.g. in the commit of the patchset). | |
| * | |
| * <p>We only refer to lines and not character offsets within the lines here as Git only works | |
| * with line precision. In theory, we could do better in Gerrit as we also have intraline diffs. | |
| * Incorporating those requires careful considerations, though. | |
| */ | |
| @AutoValue | |
| public abstract static class Position { | |
| /** Absolute file path. */ | |
| public abstract Optional<String> filePath(); | |
| /** | |
| * Affected lines. An empty {@link Optional} indicates that this position does not refer to any | |
| * specific lines (e.g. used for a file comment). | |
| */ | |
| public abstract Optional<Range> lineRange(); | |
| /** | |
| * Creates a copy of this {@code Position} whose range is shifted by the indicated amount. | |
| * | |
| * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance. | |
| * | |
| * @param amount number of lines to shift. Negative values mean moving the range up, positive | |
| * values mean moving the range down. | |
| * @return a {@code Position} instance with the updated range | |
| */ | |
| public Position shiftBy(int amount) { | |
| return lineRange() | |
| .map(range -> toBuilder().lineRange(range.shiftBy(amount)).build()) | |
| .orElse(this); | |
| } | |
| /** | |
| * Creates a copy of this {@code Position} which doesn't refer to any specific lines. | |
| * | |
| * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance. | |
| * | |
| * @return a {@code Position} instance without a line range | |
| */ | |
| public Position withoutLineRange() { | |
| return toBuilder().lineRange(Optional.empty()).build(); | |
| } | |
| /** | |
| * Creates a copy of this {@code Position} whose file path is adjusted to the indicated value. | |
| * | |
| * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance. | |
| * | |
| * @param filePath the new file path to use | |
| * @return a {@code Position} instance with the indicated file path | |
| */ | |
| public Position withFilePath(String filePath) { | |
| return toBuilder().filePath(filePath).build(); | |
| } | |
| abstract Builder toBuilder(); | |
| public static Builder builder() { | |
| return new AutoValue_GitPositionTransformer_Position.Builder(); | |
| } | |
| /** Builder of a {@link Position}. */ | |
| @AutoValue.Builder | |
| public abstract static class Builder { | |
| /** See {@link #filePath()}. */ | |
| public abstract Builder filePath(String filePath); | |
| /** See {@link #lineRange()}. */ | |
| public abstract Builder lineRange(Range lineRange); | |
| /** See {@link #lineRange()}. */ | |
| public abstract Builder lineRange(Optional<Range> lineRange); | |
| public abstract Position build(); | |
| } | |
| } | |
| /** A range. In the context of {@link GitPositionTransformer}, this is a line range. */ | |
| @AutoValue | |
| public abstract static class Range { | |
| /** Start of the range. (inclusive) */ | |
| public abstract int start(); | |
| /** End of the range. (exclusive) */ | |
| public abstract int end(); | |
| /** | |
| * Creates a copy of this {@code Range} which is shifted by the indicated amount. A shift | |
| * equally applies to both {@link #start()} end {@link #end()}. | |
| * | |
| * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance. | |
| * | |
| * @param amount amount to shift. Negative values mean moving the range up, positive values mean | |
| * moving the range down. | |
| * @return a {@code Range} instance with updated start/end | |
| */ | |
| public Range shiftBy(int amount) { | |
| return create(start() + amount, end() + amount); | |
| } | |
| public static Range create(int start, int end) { | |
| return new AutoValue_GitPositionTransformer_Range(start, end); | |
| } | |
| } | |
| /** | |
| * Wrapper around an instance of {@code T} which annotates it with a {@link Position}. Methods | |
| * such as {@link #shiftPositionBy(int)} and {@link #withFilePath(String)} allow to update the | |
| * associated {@link Position}. Afterwards, use {@link #getEntityAtUpdatedPosition()} to get an | |
| * updated version of the {@code T} instance. | |
| * | |
| * @param <T> an object/entity type which has a {@link Position} | |
| */ | |
| public static class PositionedEntity<T> { | |
| private final T entity; | |
| private final Position position; | |
| private final BiFunction<T, Position, T> updatedEntityCreator; | |
| /** | |
| * Creates a new {@code PositionedEntity}. | |
| * | |
| * @param entity an instance which should be annotated with a {@link Position} | |
| * @param positionExtractor a function describing how a {@link Position} can be derived from the | |
| * given entity | |
| * @param updatedEntityCreator a function to create a new entity of type {@code T} from an | |
| * existing entity and a given {@link Position}. This must return a new instance of type | |
| * {@code T}! The existing instance must not be modified! | |
| * @param <T> an object/entity type which has a {@link Position} | |
| */ | |
| public static <T> PositionedEntity<T> create( | |
| T entity, | |
| Function<T, Position> positionExtractor, | |
| BiFunction<T, Position, T> updatedEntityCreator) { | |
| Position position = positionExtractor.apply(entity); | |
| return new PositionedEntity<>(entity, position, updatedEntityCreator); | |
| } | |
| private PositionedEntity( | |
| T entity, Position position, BiFunction<T, Position, T> updatedEntityCreator) { | |
| this.entity = entity; | |
| this.position = position; | |
| this.updatedEntityCreator = updatedEntityCreator; | |
| } | |
| /** | |
| * Returns the original underlying entity. | |
| * | |
| * @return the original instance of {@code T} | |
| */ | |
| public T getEntity() { | |
| return entity; | |
| } | |
| /** | |
| * Returns an updated version of the entity to which the internally stored {@link Position} was | |
| * written back to. | |
| * | |
| * @return an updated instance of {@code T} | |
| */ | |
| public T getEntityAtUpdatedPosition() { | |
| return updatedEntityCreator.apply(entity, position); | |
| } | |
| Position position() { | |
| return position; | |
| } | |
| /** | |
| * Shifts the tracked {@link Position} by the specified amount. | |
| * | |
| * @param amount number of lines to shift. Negative values mean moving the range up, positive | |
| * values mean moving the range down. | |
| * @return a {@code PositionedEntity} with updated {@link Position} | |
| */ | |
| public PositionedEntity<T> shiftPositionBy(int amount) { | |
| return new PositionedEntity<>(entity, position.shiftBy(amount), updatedEntityCreator); | |
| } | |
| /** | |
| * Updates the file path of the tracked {@link Position}. | |
| * | |
| * @param filePath the new file path to use | |
| * @return a {@code PositionedEntity} with updated {@link Position} | |
| */ | |
| public PositionedEntity<T> withFilePath(String filePath) { | |
| return new PositionedEntity<>(entity, position.withFilePath(filePath), updatedEntityCreator); | |
| } | |
| /** | |
| * Updates the tracked {@link Position}. | |
| * | |
| * @return a {@code PositionedEntity} with updated {@link Position} | |
| */ | |
| public PositionedEntity<T> withPosition(Position newPosition) { | |
| return new PositionedEntity<>(entity, newPosition, updatedEntityCreator); | |
| } | |
| } | |
| /** | |
| * Strategy indicating how to handle {@link Position}s for which mapping conflicts exist. A | |
| * mapping conflict means that a {@link Position} can't be transformed such that it still refers | |
| * to exactly the same commit content afterwards. | |
| * | |
| * <p>Example: A {@link Position} refers to file foo.txt and lines 5-6 which contain the text | |
| * "Line 5\nLine 6". One of the {@link Mapping}s given to {@link #transform(Collection, Set)} | |
| * indicates that line 5 of foo.txt was modified to "Line five\nLine 5.1\n". We could derive a | |
| * transformed {@link Position} (foo.txt, lines 5-7) but that {@link Position} would then refer to | |
| * the content "Line five\nLine 5.1\nLine 6". If the modification started already in line 4, we | |
| * could even only guess what the transformed {@link Position} would be. | |
| */ | |
| public interface PositionConflictStrategy { | |
| /** | |
| * Determines an alternate {@link Position} when the range of the position can't be mapped | |
| * without a conflict. | |
| * | |
| * @param oldPosition position in the source tree | |
| * @return the new {@link Position} or an empty {@link Optional} if the position should be | |
| * dropped | |
| */ | |
| Optional<Position> getOnRangeConflict(Position oldPosition); | |
| /** | |
| * Determines an alternate {@link Position} when there is no file for the position (= file | |
| * deletion) in the target tree. | |
| * | |
| * @param oldPosition position in the source tree | |
| * @return the new {@link Position} or an empty {@link Optional} if the position should be * | |
| * dropped | |
| */ | |
| Optional<Position> getOnFileConflict(Position oldPosition); | |
| } | |
| /** | |
| * A strategy which drops any {@link Position}s on a conflicting mapping. Such a strategy is | |
| * useful if it's important that any mapped {@link Position} still refers to exactly the same | |
| * commit content as before. See more details at {@link PositionConflictStrategy}. | |
| * | |
| * <p>We need this strategy for computing edits due to rebase. | |
| */ | |
| public enum OmitPositionOnConflict implements PositionConflictStrategy { | |
| INSTANCE; | |
| @Override | |
| public Optional<Position> getOnRangeConflict(Position oldPosition) { | |
| return Optional.empty(); | |
| } | |
| @Override | |
| public Optional<Position> getOnFileConflict(Position oldPosition) { | |
| return Optional.empty(); | |
| } | |
| } | |
| /** | |
| * A strategy which tries to select the next suitable {@link Position} on a conflicting mapping. | |
| * At the moment, this strategy is very basic and only defers to the next higher level (e.g. range | |
| * unclear -> drop range but keep file reference). This could be improved in the future. | |
| * | |
| * <p>We need this strategy for ported comments. | |
| * | |
| * <p><strong>Warning:</strong> With this strategy, mapped {@link Position}s are not guaranteed to | |
| * refer to exactly the same commit content as before. See more details at {@link | |
| * PositionConflictStrategy}. | |
| * | |
| * <p>Contract: This strategy will never drop any {@link Position}. | |
| */ | |
| public enum BestPositionOnConflict implements PositionConflictStrategy { | |
| INSTANCE; | |
| @Override | |
| public Optional<Position> getOnRangeConflict(Position oldPosition) { | |
| return Optional.of(oldPosition.withoutLineRange()); | |
| } | |
| @Override | |
| public Optional<Position> getOnFileConflict(Position oldPosition) { | |
| // If there isn't a target file, we can also drop any ranges. | |
| return Optional.of(Position.builder().build()); | |
| } | |
| } | |
| } |