LCOV - code coverage report
Current view: top level - server/patch - GitPositionTransformer.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 136 143 95.1 %
Date: 2022-11-19 15:00:39 Functions: 53 57 93.0 %

          Line data    Source code
       1             : // Copyright (C) 2020 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.patch;
      16             : 
      17             : import static com.google.common.collect.Comparators.emptiesFirst;
      18             : import static com.google.common.collect.ImmutableList.toImmutableList;
      19             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      20             : import static java.util.Comparator.comparing;
      21             : import static java.util.stream.Collectors.collectingAndThen;
      22             : import static java.util.stream.Collectors.groupingBy;
      23             : 
      24             : import com.google.auto.value.AutoValue;
      25             : import com.google.common.collect.ImmutableList;
      26             : import com.google.common.collect.ImmutableSet;
      27             : import com.google.common.collect.Sets;
      28             : import com.google.common.collect.Streams;
      29             : import java.util.Collection;
      30             : import java.util.HashSet;
      31             : import java.util.List;
      32             : import java.util.Map;
      33             : import java.util.Optional;
      34             : import java.util.Set;
      35             : import java.util.function.BiFunction;
      36             : import java.util.function.Function;
      37             : import java.util.stream.Collectors;
      38             : import java.util.stream.Stream;
      39             : 
      40             : /**
      41             :  * Transformer of {@link Position}s in one Git tree to {@link Position}s in another Git tree given
      42             :  * the {@link Mapping}s between the trees.
      43             :  *
      44             :  * <p>The base idea is that a {@link Position} in the source tree can be translated/mapped to a
      45             :  * corresponding {@link Position} in the target tree when we know how the target tree changed
      46             :  * compared to the source tree. As long as {@link Position}s are only defined via file path and line
      47             :  * range, we only need to know which file path in the source tree corresponds to which file path in
      48             :  * the target tree and how the lines within that file changed from the source to the target tree.
      49             :  *
      50             :  * <p>The algorithm is roughly:
      51             :  *
      52             :  * <ol>
      53             :  *   <li>Go over all positions and replace the file path for each of them with the corresponding one
      54             :  *       in the target tree. If a file path maps to two file paths in the target tree (copied file),
      55             :  *       duplicate the position entry and use each of the new file paths with it. If a file path
      56             :  *       maps to no file in the target tree (deleted file), apply the specified conflict strategy
      57             :  *       (e.g. drop position completely or map to next best guess).
      58             :  *   <li>Per file path, go through the file from top to bottom and keep track of how the range
      59             :  *       mappings for that file shift the lines. Derive the shifted amount by comparing the number
      60             :  *       of lines between source and target in the range mapping. While going through the file,
      61             :  *       shift each encountered position by the currently tracked amount. If a position overlaps
      62             :  *       with the lines of a range mapping, apply the specified conflict strategy (e.g. drop
      63             :  *       position completely or map to next best guess).
      64             :  * </ol>
      65             :  */
      66             : public class GitPositionTransformer {
      67             :   private final PositionConflictStrategy positionConflictStrategy;
      68             : 
      69             :   /**
      70             :    * Creates a new {@code GitPositionTransformer} which uses the specified strategy for conflicts.
      71             :    */
      72         145 :   public GitPositionTransformer(PositionConflictStrategy positionConflictStrategy) {
      73         145 :     this.positionConflictStrategy = positionConflictStrategy;
      74         145 :   }
      75             : 
      76             :   /**
      77             :    * Transforms the {@link Position}s of the specified entities as indicated via the {@link
      78             :    * Mapping}s.
      79             :    *
      80             :    * <p>This is typically used to transform the {@link Position}s in one Git tree (source) to the
      81             :    * corresponding {@link Position}s in another Git tree (target). The {@link Mapping}s need to
      82             :    * indicate all relevant changes between the source and target tree. {@link Mapping}s for files
      83             :    * not referenced by the given {@link Position}s need not be specified. They can be included,
      84             :    * though, as they aren't harmful.
      85             :    *
      86             :    * @param entities the entities whose {@link Position} should be mapped to the target tree
      87             :    * @param mappings the mappings describing all relevant changes between the source and the target
      88             :    *     tree
      89             :    * @param <T> an entity which has a {@link Position}
      90             :    * @return a list of entities with transformed positions. There are no guarantees about the order
      91             :    *     of the returned elements.
      92             :    */
      93             :   public <T> ImmutableList<PositionedEntity<T>> transform(
      94             :       Collection<PositionedEntity<T>> entities, Set<Mapping> mappings) {
      95             :     // Update the file paths first as copied files might exist. For copied files, this operation
      96             :     // will duplicate the PositionedEntity instances of the original file.
      97           4 :     List<PositionedEntity<T>> filePathUpdatedEntities = updateFilePaths(entities, mappings);
      98             : 
      99           4 :     return shiftRanges(filePathUpdatedEntities, mappings);
     100             :   }
     101             : 
     102             :   private <T> ImmutableList<PositionedEntity<T>> updateFilePaths(
     103             :       Collection<PositionedEntity<T>> entities, Set<Mapping> mappings) {
     104           4 :     Map<String, ImmutableSet<String>> newFilesPerOldFile = groupNewFilesByOldFiles(mappings);
     105           4 :     return entities.stream()
     106           4 :         .flatMap(entity -> mapToNewFileIfChanged(newFilesPerOldFile, entity))
     107           4 :         .collect(toImmutableList());
     108             :   }
     109             : 
     110             :   private static Map<String, ImmutableSet<String>> groupNewFilesByOldFiles(Set<Mapping> mappings) {
     111           4 :     return mappings.stream()
     112           4 :         .map(Mapping::file)
     113             :         // Ignore file additions (irrelevant for mappings).
     114           4 :         .filter(mapping -> mapping.oldPath().isPresent())
     115           4 :         .collect(
     116           4 :             groupingBy(
     117           4 :                 mapping -> mapping.oldPath().orElse(""),
     118           4 :                 collectingAndThen(
     119           4 :                     Collectors.mapping(FileMapping::newPath, toImmutableSet()),
     120             :                     // File deletion (empty Optional) -> empty set.
     121             :                     GitPositionTransformer::unwrapOptionals)));
     122             :   }
     123             : 
     124             :   private static ImmutableSet<String> unwrapOptionals(ImmutableSet<Optional<String>> optionals) {
     125           4 :     return optionals.stream().flatMap(Streams::stream).collect(toImmutableSet());
     126             :   }
     127             : 
     128             :   private <T> Stream<PositionedEntity<T>> mapToNewFileIfChanged(
     129             :       Map<String, ? extends Set<String>> newFilesPerOldFile, PositionedEntity<T> entity) {
     130           4 :     if (!entity.position().filePath().isPresent()) {
     131             :       // No mapping of file paths necessary if no file path is set. -> Keep existing entry.
     132           1 :       return Stream.of(entity);
     133             :     }
     134           4 :     String oldFilePath = entity.position().filePath().get();
     135           4 :     if (!newFilesPerOldFile.containsKey(oldFilePath)) {
     136             :       // Unchanged files don't have a mapping. -> Keep existing entries.
     137           2 :       return Stream.of(entity);
     138             :     }
     139           4 :     Set<String> newFiles = newFilesPerOldFile.get(oldFilePath);
     140           4 :     if (newFiles.isEmpty()) {
     141             :       // File was deleted.
     142           2 :       return Streams.stream(
     143           2 :           positionConflictStrategy.getOnFileConflict(entity.position()).map(entity::withPosition));
     144             :     }
     145           3 :     return newFiles.stream().map(entity::withFilePath);
     146             :   }
     147             : 
     148             :   private <T> ImmutableList<PositionedEntity<T>> shiftRanges(
     149             :       List<PositionedEntity<T>> filePathUpdatedEntities, Set<Mapping> mappings) {
     150           4 :     Map<String, ImmutableSet<RangeMapping>> mappingsPerNewFilePath =
     151           4 :         groupRangeMappingsByNewFilePath(mappings);
     152           4 :     return Stream.concat(
     153             :             // Keep positions without a file.
     154           4 :             filePathUpdatedEntities.stream()
     155           4 :                 .filter(entity -> !entity.position().filePath().isPresent()),
     156             :             // Shift ranges per file.
     157           4 :             groupByFilePath(filePathUpdatedEntities).entrySet().stream()
     158           4 :                 .flatMap(
     159             :                     newFilePathAndEntities ->
     160           4 :                         shiftRangesInOneFileIfChanged(
     161             :                             mappingsPerNewFilePath,
     162           4 :                             newFilePathAndEntities.getKey(),
     163           4 :                             newFilePathAndEntities.getValue())
     164           4 :                             .stream()))
     165           4 :         .collect(toImmutableList());
     166             :   }
     167             : 
     168             :   private static Map<String, ImmutableSet<RangeMapping>> groupRangeMappingsByNewFilePath(
     169             :       Set<Mapping> mappings) {
     170           4 :     return mappings.stream()
     171             :         // Ignore range mappings of deleted files.
     172           4 :         .filter(mapping -> mapping.file().newPath().isPresent())
     173           4 :         .collect(
     174           4 :             groupingBy(
     175           3 :                 mapping -> mapping.file().newPath().orElse(""),
     176           4 :                 collectingAndThen(
     177           4 :                     Collectors.<Mapping, Set<RangeMapping>>reducing(
     178             :                         new HashSet<>(), Mapping::ranges, Sets::union),
     179             :                     ImmutableSet::copyOf)));
     180             :   }
     181             : 
     182             :   private static <T> Map<String, ImmutableList<PositionedEntity<T>>> groupByFilePath(
     183             :       List<PositionedEntity<T>> fileUpdatedEntities) {
     184           4 :     return fileUpdatedEntities.stream()
     185           4 :         .filter(entity -> entity.position().filePath().isPresent())
     186           4 :         .collect(groupingBy(entity -> entity.position().filePath().orElse(""), toImmutableList()));
     187             :   }
     188             : 
     189             :   private <T> ImmutableList<PositionedEntity<T>> shiftRangesInOneFileIfChanged(
     190             :       Map<String, ImmutableSet<RangeMapping>> mappingsPerNewFilePath,
     191             :       String newFilePath,
     192             :       ImmutableList<PositionedEntity<T>> sameFileEntities) {
     193           4 :     ImmutableSet<RangeMapping> sameFileRangeMappings =
     194           4 :         mappingsPerNewFilePath.getOrDefault(newFilePath, ImmutableSet.of());
     195           4 :     if (sameFileRangeMappings.isEmpty()) {
     196             :       // Unchanged files and pure renames/copies don't have range mappings. -> Keep existing
     197             :       // entries.
     198           2 :       return sameFileEntities;
     199             :     }
     200           3 :     return shiftRangesInOneFile(sameFileEntities, sameFileRangeMappings);
     201             :   }
     202             : 
     203             :   private <T> ImmutableList<PositionedEntity<T>> shiftRangesInOneFile(
     204             :       List<PositionedEntity<T>> sameFileEntities, Set<RangeMapping> sameFileRangeMappings) {
     205           3 :     ImmutableList<PositionedEntity<T>> sortedEntities = sortByStartEnd(sameFileEntities);
     206           3 :     ImmutableList<RangeMapping> sortedMappings = sortByOldStartEnd(sameFileRangeMappings);
     207             : 
     208           3 :     int shiftedAmount = 0;
     209           3 :     int mappingIndex = 0;
     210           3 :     int entityIndex = 0;
     211           3 :     ImmutableList.Builder<PositionedEntity<T>> resultingEntities =
     212           3 :         ImmutableList.builderWithExpectedSize(sortedEntities.size());
     213           3 :     while (entityIndex < sortedEntities.size() && mappingIndex < sortedMappings.size()) {
     214           3 :       PositionedEntity<T> entity = sortedEntities.get(entityIndex);
     215           3 :       if (entity.position().lineRange().isPresent()) {
     216           3 :         Range range = entity.position().lineRange().get();
     217           3 :         RangeMapping mapping = sortedMappings.get(mappingIndex);
     218           3 :         if (mapping.oldLineRange().end() <= range.start()) {
     219           3 :           shiftedAmount = mapping.newLineRange().end() - mapping.oldLineRange().end();
     220           3 :           mappingIndex++;
     221           3 :         } else if (range.end() <= mapping.oldLineRange().start()) {
     222           3 :           resultingEntities.add(entity.shiftPositionBy(shiftedAmount));
     223           3 :           entityIndex++;
     224             :         } else {
     225           3 :           positionConflictStrategy
     226           3 :               .getOnRangeConflict(entity.position())
     227           3 :               .map(entity::withPosition)
     228           3 :               .ifPresent(resultingEntities::add);
     229           3 :           entityIndex++;
     230             :         }
     231           3 :       } else {
     232             :         // No range -> no need to shift.
     233           1 :         resultingEntities.add(entity);
     234           1 :         entityIndex++;
     235             :       }
     236           3 :     }
     237           3 :     for (int i = entityIndex; i < sortedEntities.size(); i++) {
     238           3 :       resultingEntities.add(sortedEntities.get(i).shiftPositionBy(shiftedAmount));
     239             :     }
     240           3 :     return resultingEntities.build();
     241             :   }
     242             : 
     243             :   private static <T> ImmutableList<PositionedEntity<T>> sortByStartEnd(
     244             :       List<PositionedEntity<T>> entities) {
     245           3 :     return entities.stream()
     246           3 :         .sorted(
     247           3 :             comparing(
     248           3 :                 entity -> entity.position().lineRange(),
     249           3 :                 emptiesFirst(comparing(Range::start).thenComparing(Range::end))))
     250           3 :         .collect(toImmutableList());
     251             :   }
     252             : 
     253             :   private static ImmutableList<RangeMapping> sortByOldStartEnd(Set<RangeMapping> mappings) {
     254           3 :     return mappings.stream()
     255           3 :         .sorted(
     256           3 :             comparing(
     257           3 :                 RangeMapping::oldLineRange, comparing(Range::start).thenComparing(Range::end)))
     258           3 :         .collect(toImmutableList());
     259             :   }
     260             : 
     261             :   /**
     262             :    * A mapping from a {@link Position} in one Git commit/tree (source) to a {@link Position} in
     263             :    * another Git commit/tree (target).
     264             :    */
     265             :   @AutoValue
     266           4 :   public abstract static class Mapping {
     267             : 
     268             :     /** A mapping describing how the attributes of one file are mapped from source to target. */
     269             :     public abstract FileMapping file();
     270             : 
     271             :     /**
     272             :      * Mappings describing how line ranges within the file indicated by {@link #file()} are mapped
     273             :      * from source to target.
     274             :      */
     275             :     public abstract ImmutableSet<RangeMapping> ranges();
     276             : 
     277             :     public static Mapping create(FileMapping fileMapping, Iterable<RangeMapping> rangeMappings) {
     278           4 :       return new AutoValue_GitPositionTransformer_Mapping(
     279           4 :           fileMapping, ImmutableSet.copyOf(rangeMappings));
     280             :     }
     281             :   }
     282             : 
     283             :   /**
     284             :    * A mapping of attributes from a file in one Git tree (source) to a file in another Git tree
     285             :    * (target).
     286             :    *
     287             :    * <p>At the moment, only the file path is considered. Other attributes like file mode would be
     288             :    * imaginable too but are currently not supported.
     289             :    */
     290             :   @AutoValue
     291           4 :   public abstract static class FileMapping {
     292             : 
     293             :     /** File path in the source tree. For file additions, this is an empty {@link Optional}. */
     294             :     public abstract Optional<String> oldPath();
     295             : 
     296             :     /**
     297             :      * File path in the target tree. Can be the same as {@link #oldPath()} if unchanged. For file
     298             :      * deletions, this is an empty {@link Optional}.
     299             :      */
     300             :     public abstract Optional<String> newPath();
     301             : 
     302             :     /**
     303             :      * Creates a {@link FileMapping} for a file addition.
     304             :      *
     305             :      * <p>In the context of {@link GitPositionTransformer}, file additions are irrelevant as no
     306             :      * given position in the source tree can refer to such a new file in the target tree. We still
     307             :      * provide this factory method so that code outside of {@link GitPositionTransformer} doesn't
     308             :      * have to care about such details and can simply create {@link FileMapping}s for any
     309             :      * modifications between the trees.
     310             :      */
     311             :     public static FileMapping forAddedFile(String filePath) {
     312           0 :       return new AutoValue_GitPositionTransformer_FileMapping(
     313           0 :           Optional.empty(), Optional.of(filePath));
     314             :     }
     315             : 
     316             :     /** Creates a {@link FileMapping} for a file deletion. */
     317             :     public static FileMapping forDeletedFile(String filePath) {
     318           2 :       return new AutoValue_GitPositionTransformer_FileMapping(
     319           2 :           Optional.of(filePath), Optional.empty());
     320             :     }
     321             : 
     322             :     /** Creates a {@link FileMapping} for a file modification. */
     323             :     public static FileMapping forModifiedFile(String filePath) {
     324           0 :       return new AutoValue_GitPositionTransformer_FileMapping(
     325           0 :           Optional.of(filePath), Optional.of(filePath));
     326             :     }
     327             : 
     328             :     /** Creates a {@link FileMapping} for a file renaming. */
     329             :     public static FileMapping forRenamedFile(String oldPath, String newPath) {
     330           0 :       return new AutoValue_GitPositionTransformer_FileMapping(
     331           0 :           Optional.of(oldPath), Optional.of(newPath));
     332             :     }
     333             : 
     334             :     /** Creates a {@link FileMapping} using the old and new paths. */
     335             :     public static FileMapping forFile(Optional<String> oldPath, Optional<String> newPath) {
     336           3 :       return new AutoValue_GitPositionTransformer_FileMapping(oldPath, newPath);
     337             :     }
     338             :   }
     339             : 
     340             :   /**
     341             :    * A mapping of a line range in one Git tree (source) to the corresponding line range in another
     342             :    * Git tree (target).
     343             :    */
     344             :   @AutoValue
     345           3 :   public abstract static class RangeMapping {
     346             : 
     347             :     /** Range in the source tree. */
     348             :     public abstract Range oldLineRange();
     349             : 
     350             :     /** Range in the target tree. */
     351             :     public abstract Range newLineRange();
     352             : 
     353             :     /**
     354             :      * Creates a new {@code RangeMapping}.
     355             :      *
     356             :      * @param oldRange see {@link #oldLineRange()}
     357             :      * @param newRange see {@link #newLineRange()}
     358             :      */
     359             :     public static RangeMapping create(Range oldRange, Range newRange) {
     360           3 :       return new AutoValue_GitPositionTransformer_RangeMapping(oldRange, newRange);
     361             :     }
     362             :   }
     363             : 
     364             :   /**
     365             :    * A position within the tree of a Git commit.
     366             :    *
     367             :    * <p>The term 'position' is our own invention. The underlying idea is that a Gerrit comment is at
     368             :    * a specific position within the commit of a patchset. That position is defined by the attributes
     369             :    * defined in this class.
     370             :    *
     371             :    * <p>The same thinking can be applied to diff hunks (= JGit edits). Each diff hunk maps a
     372             :    * position in one commit (e.g. in the parent of the patchset) to a position in another commit
     373             :    * (e.g. in the commit of the patchset).
     374             :    *
     375             :    * <p>We only refer to lines and not character offsets within the lines here as Git only works
     376             :    * with line precision. In theory, we could do better in Gerrit as we also have intraline diffs.
     377             :    * Incorporating those requires careful considerations, though.
     378             :    */
     379             :   @AutoValue
     380           4 :   public abstract static class Position {
     381             : 
     382             :     /** Absolute file path. */
     383             :     public abstract Optional<String> filePath();
     384             : 
     385             :     /**
     386             :      * Affected lines. An empty {@link Optional} indicates that this position does not refer to any
     387             :      * specific lines (e.g. used for a file comment).
     388             :      */
     389             :     public abstract Optional<Range> lineRange();
     390             : 
     391             :     /**
     392             :      * Creates a copy of this {@code Position} whose range is shifted by the indicated amount.
     393             :      *
     394             :      * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance.
     395             :      *
     396             :      * @param amount number of lines to shift. Negative values mean moving the range up, positive
     397             :      *     values mean moving the range down.
     398             :      * @return a {@code Position} instance with the updated range
     399             :      */
     400             :     public Position shiftBy(int amount) {
     401           3 :       return lineRange()
     402           3 :           .map(range -> toBuilder().lineRange(range.shiftBy(amount)).build())
     403           3 :           .orElse(this);
     404             :     }
     405             : 
     406             :     /**
     407             :      * Creates a copy of this {@code Position} which doesn't refer to any specific lines.
     408             :      *
     409             :      * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance.
     410             :      *
     411             :      * @return a {@code Position} instance without a line range
     412             :      */
     413             :     public Position withoutLineRange() {
     414           1 :       return toBuilder().lineRange(Optional.empty()).build();
     415             :     }
     416             : 
     417             :     /**
     418             :      * Creates a copy of this {@code Position} whose file path is adjusted to the indicated value.
     419             :      *
     420             :      * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance.
     421             :      *
     422             :      * @param filePath the new file path to use
     423             :      * @return a {@code Position} instance with the indicated file path
     424             :      */
     425             :     public Position withFilePath(String filePath) {
     426           3 :       return toBuilder().filePath(filePath).build();
     427             :     }
     428             : 
     429             :     abstract Builder toBuilder();
     430             : 
     431             :     public static Builder builder() {
     432           4 :       return new AutoValue_GitPositionTransformer_Position.Builder();
     433             :     }
     434             : 
     435             :     /** Builder of a {@link Position}. */
     436             :     @AutoValue.Builder
     437           4 :     public abstract static class Builder {
     438             : 
     439             :       /** See {@link #filePath()}. */
     440             :       public abstract Builder filePath(String filePath);
     441             : 
     442             :       /** See {@link #lineRange()}. */
     443             :       public abstract Builder lineRange(Range lineRange);
     444             : 
     445             :       /** See {@link #lineRange()}. */
     446             :       public abstract Builder lineRange(Optional<Range> lineRange);
     447             : 
     448             :       public abstract Position build();
     449             :     }
     450             :   }
     451             : 
     452             :   /** A range. In the context of {@link GitPositionTransformer}, this is a line range. */
     453             :   @AutoValue
     454           3 :   public abstract static class Range {
     455             : 
     456             :     /** Start of the range. (inclusive) */
     457             :     public abstract int start();
     458             : 
     459             :     /** End of the range. (exclusive) */
     460             :     public abstract int end();
     461             : 
     462             :     /**
     463             :      * Creates a copy of this {@code Range} which is shifted by the indicated amount. A shift
     464             :      * equally applies to both {@link #start()} end {@link #end()}.
     465             :      *
     466             :      * <p><strong>Note:</strong> There's no guarantee that this method returns a new instance.
     467             :      *
     468             :      * @param amount amount to shift. Negative values mean moving the range up, positive values mean
     469             :      *     moving the range down.
     470             :      * @return a {@code Range} instance with updated start/end
     471             :      */
     472             :     public Range shiftBy(int amount) {
     473           3 :       return create(start() + amount, end() + amount);
     474             :     }
     475             : 
     476             :     public static Range create(int start, int end) {
     477           3 :       return new AutoValue_GitPositionTransformer_Range(start, end);
     478             :     }
     479             :   }
     480             : 
     481             :   /**
     482             :    * Wrapper around an instance of {@code T} which annotates it with a {@link Position}. Methods
     483             :    * such as {@link #shiftPositionBy(int)} and {@link #withFilePath(String)} allow to update the
     484             :    * associated {@link Position}. Afterwards, use {@link #getEntityAtUpdatedPosition()} to get an
     485             :    * updated version of the {@code T} instance.
     486             :    *
     487             :    * @param <T> an object/entity type which has a {@link Position}
     488             :    */
     489             :   public static class PositionedEntity<T> {
     490             : 
     491             :     private final T entity;
     492             :     private final Position position;
     493             :     private final BiFunction<T, Position, T> updatedEntityCreator;
     494             : 
     495             :     /**
     496             :      * Creates a new {@code PositionedEntity}.
     497             :      *
     498             :      * @param entity an instance which should be annotated with a {@link Position}
     499             :      * @param positionExtractor a function describing how a {@link Position} can be derived from the
     500             :      *     given entity
     501             :      * @param updatedEntityCreator a function to create a new entity of type {@code T} from an
     502             :      *     existing entity and a given {@link Position}. This must return a new instance of type
     503             :      *     {@code T}! The existing instance must not be modified!
     504             :      * @param <T> an object/entity type which has a {@link Position}
     505             :      */
     506             :     public static <T> PositionedEntity<T> create(
     507             :         T entity,
     508             :         Function<T, Position> positionExtractor,
     509             :         BiFunction<T, Position, T> updatedEntityCreator) {
     510           4 :       Position position = positionExtractor.apply(entity);
     511           4 :       return new PositionedEntity<>(entity, position, updatedEntityCreator);
     512             :     }
     513             : 
     514             :     private PositionedEntity(
     515           4 :         T entity, Position position, BiFunction<T, Position, T> updatedEntityCreator) {
     516           4 :       this.entity = entity;
     517           4 :       this.position = position;
     518           4 :       this.updatedEntityCreator = updatedEntityCreator;
     519           4 :     }
     520             : 
     521             :     /**
     522             :      * Returns the original underlying entity.
     523             :      *
     524             :      * @return the original instance of {@code T}
     525             :      */
     526             :     public T getEntity() {
     527           2 :       return entity;
     528             :     }
     529             : 
     530             :     /**
     531             :      * Returns an updated version of the entity to which the internally stored {@link Position} was
     532             :      * written back to.
     533             :      *
     534             :      * @return an updated instance of {@code T}
     535             :      */
     536             :     public T getEntityAtUpdatedPosition() {
     537           4 :       return updatedEntityCreator.apply(entity, position);
     538             :     }
     539             : 
     540             :     Position position() {
     541           4 :       return position;
     542             :     }
     543             : 
     544             :     /**
     545             :      * Shifts the tracked {@link Position} by the specified amount.
     546             :      *
     547             :      * @param amount number of lines to shift. Negative values mean moving the range up, positive
     548             :      *     values mean moving the range down.
     549             :      * @return a {@code PositionedEntity} with updated {@link Position}
     550             :      */
     551             :     public PositionedEntity<T> shiftPositionBy(int amount) {
     552           3 :       return new PositionedEntity<>(entity, position.shiftBy(amount), updatedEntityCreator);
     553             :     }
     554             : 
     555             :     /**
     556             :      * Updates the file path of the tracked {@link Position}.
     557             :      *
     558             :      * @param filePath the new file path to use
     559             :      * @return a {@code PositionedEntity} with updated {@link Position}
     560             :      */
     561             :     public PositionedEntity<T> withFilePath(String filePath) {
     562           3 :       return new PositionedEntity<>(entity, position.withFilePath(filePath), updatedEntityCreator);
     563             :     }
     564             : 
     565             :     /**
     566             :      * Updates the tracked {@link Position}.
     567             :      *
     568             :      * @return a {@code PositionedEntity} with updated {@link Position}
     569             :      */
     570             :     public PositionedEntity<T> withPosition(Position newPosition) {
     571           2 :       return new PositionedEntity<>(entity, newPosition, updatedEntityCreator);
     572             :     }
     573             :   }
     574             : 
     575             :   /**
     576             :    * Strategy indicating how to handle {@link Position}s for which mapping conflicts exist. A
     577             :    * mapping conflict means that a {@link Position} can't be transformed such that it still refers
     578             :    * to exactly the same commit content afterwards.
     579             :    *
     580             :    * <p>Example: A {@link Position} refers to file foo.txt and lines 5-6 which contain the text
     581             :    * "Line 5\nLine 6". One of the {@link Mapping}s given to {@link #transform(Collection, Set)}
     582             :    * indicates that line 5 of foo.txt was modified to "Line five\nLine 5.1\n". We could derive a
     583             :    * transformed {@link Position} (foo.txt, lines 5-7) but that {@link Position} would then refer to
     584             :    * the content "Line five\nLine 5.1\nLine 6". If the modification started already in line 4, we
     585             :    * could even only guess what the transformed {@link Position} would be.
     586             :    */
     587             :   public interface PositionConflictStrategy {
     588             :     /**
     589             :      * Determines an alternate {@link Position} when the range of the position can't be mapped
     590             :      * without a conflict.
     591             :      *
     592             :      * @param oldPosition position in the source tree
     593             :      * @return the new {@link Position} or an empty {@link Optional} if the position should be
     594             :      *     dropped
     595             :      */
     596             :     Optional<Position> getOnRangeConflict(Position oldPosition);
     597             : 
     598             :     /**
     599             :      * Determines an alternate {@link Position} when there is no file for the position (= file
     600             :      * deletion) in the target tree.
     601             :      *
     602             :      * @param oldPosition position in the source tree
     603             :      * @return the new {@link Position} or an empty {@link Optional} if the position should be *
     604             :      *     dropped
     605             :      */
     606             :     Optional<Position> getOnFileConflict(Position oldPosition);
     607             :   }
     608             : 
     609             :   /**
     610             :    * A strategy which drops any {@link Position}s on a conflicting mapping. Such a strategy is
     611             :    * useful if it's important that any mapped {@link Position} still refers to exactly the same
     612             :    * commit content as before. See more details at {@link PositionConflictStrategy}.
     613             :    *
     614             :    * <p>We need this strategy for computing edits due to rebase.
     615             :    */
     616           3 :   public enum OmitPositionOnConflict implements PositionConflictStrategy {
     617           3 :     INSTANCE;
     618             : 
     619             :     @Override
     620             :     public Optional<Position> getOnRangeConflict(Position oldPosition) {
     621           2 :       return Optional.empty();
     622             :     }
     623             : 
     624             :     @Override
     625             :     public Optional<Position> getOnFileConflict(Position oldPosition) {
     626           0 :       return Optional.empty();
     627             :     }
     628             :   }
     629             : 
     630             :   /**
     631             :    * A strategy which tries to select the next suitable {@link Position} on a conflicting mapping.
     632             :    * At the moment, this strategy is very basic and only defers to the next higher level (e.g. range
     633             :    * unclear -> drop range but keep file reference). This could be improved in the future.
     634             :    *
     635             :    * <p>We need this strategy for ported comments.
     636             :    *
     637             :    * <p><strong>Warning:</strong> With this strategy, mapped {@link Position}s are not guaranteed to
     638             :    * refer to exactly the same commit content as before. See more details at {@link
     639             :    * PositionConflictStrategy}.
     640             :    *
     641             :    * <p>Contract: This strategy will never drop any {@link Position}.
     642             :    */
     643         145 :   public enum BestPositionOnConflict implements PositionConflictStrategy {
     644         145 :     INSTANCE;
     645             : 
     646             :     @Override
     647             :     public Optional<Position> getOnRangeConflict(Position oldPosition) {
     648           1 :       return Optional.of(oldPosition.withoutLineRange());
     649             :     }
     650             : 
     651             :     @Override
     652             :     public Optional<Position> getOnFileConflict(Position oldPosition) {
     653             :       // If there isn't a target file, we can also drop any ranges.
     654           2 :       return Optional.of(Position.builder().build());
     655             :     }
     656             :   }
     657             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750