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 : }
|