Line data Source code
1 : // Copyright (C) 2017 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.filediff;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 : import static com.google.common.collect.Multimaps.toMultimap;
20 :
21 : import com.google.auto.value.AutoValue;
22 : import com.google.common.collect.ArrayListMultimap;
23 : import com.google.common.collect.ImmutableList;
24 : import com.google.common.collect.ImmutableSet;
25 : import com.google.common.collect.Multimap;
26 : import com.google.common.flogger.FluentLogger;
27 : import com.google.gerrit.entities.Patch;
28 : import com.google.gerrit.server.patch.DiffMappings;
29 : import com.google.gerrit.server.patch.GitPositionTransformer;
30 : import com.google.gerrit.server.patch.GitPositionTransformer.Mapping;
31 : import com.google.gerrit.server.patch.GitPositionTransformer.OmitPositionOnConflict;
32 : import com.google.gerrit.server.patch.GitPositionTransformer.Position;
33 : import com.google.gerrit.server.patch.GitPositionTransformer.PositionedEntity;
34 : import com.google.gerrit.server.patch.GitPositionTransformer.Range;
35 : import java.util.List;
36 : import java.util.Objects;
37 : import java.util.Optional;
38 : import java.util.function.Function;
39 : import java.util.stream.Stream;
40 :
41 : /**
42 : * Transformer of edits regarding their base trees. An edit describes a difference between {@code
43 : * treeA} and {@code treeB}. This class allows to describe the edit as a difference between {@code
44 : * treeA'} and {@code treeB'} given the transformation of {@code treeA} to {@code treeA'} and {@code
45 : * treeB} to {@code treeB'}. Edits which can't be transformed due to conflicts with the
46 : * transformation are omitted.
47 : */
48 : class EditTransformer {
49 3 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
50 :
51 3 : private final GitPositionTransformer positionTransformer =
52 : new GitPositionTransformer(OmitPositionOnConflict.INSTANCE);
53 : private List<ContextAwareEdit> edits;
54 :
55 : /**
56 : * Creates a new {@code EditTransformer} for the edits contained in the specified {@code
57 : * FileEdits}s.
58 : *
59 : * @param fileEdits a list of {@code FileEdits}s containing the edits
60 : */
61 3 : public EditTransformer(List<FileEdits> fileEdits) {
62 : // TODO(ghareeb): Can we replace FileEdits with another entity from the new refactored
63 : // diff cache implementation? e.g. one of the GitFileDiffCache entities
64 3 : edits = fileEdits.stream().flatMap(EditTransformer::toEdits).collect(toImmutableList());
65 3 : }
66 :
67 : /**
68 : * Transforms the references of side A of the edits. If the edits describe differences between
69 : * {@code treeA} and {@code treeB} and the specified {@code FileEdits}s define a transformation
70 : * from {@code treeA} to {@code treeA'}, the resulting edits will be defined as differences
71 : * between {@code treeA'} and {@code treeB}. Edits which can't be transformed due to conflicts
72 : * with the transformation are omitted.
73 : *
74 : * @param transformingEntries a list of {@code FileEdits}s defining the transformation of {@code
75 : * treeA} to {@code treeA'}
76 : */
77 : public void transformReferencesOfSideA(ImmutableList<FileEdits> transformingEntries) {
78 3 : transformEdits(transformingEntries, SideAStrategy.INSTANCE);
79 3 : }
80 :
81 : /**
82 : * Transforms the references of side B of the edits. If the edits describe differences between
83 : * {@code treeA} and {@code treeB} and the specified {@code FileEdits}s define a transformation
84 : * from {@code treeB} to {@code treeB'}, the resulting edits will be defined as differences
85 : * between {@code treeA} and {@code treeB'}. Edits which can't be transformed due to conflicts
86 : * with the transformation are omitted.
87 : *
88 : * @param transformingEntries a list of {@code PatchListEntry}s defining the transformation of
89 : * {@code treeB} to {@code treeB'}
90 : */
91 : public void transformReferencesOfSideB(ImmutableList<FileEdits> transformingEntries) {
92 3 : transformEdits(transformingEntries, SideBStrategy.INSTANCE);
93 3 : }
94 :
95 : /**
96 : * Returns the transformed edits per file path they modify in {@code treeB'}.
97 : *
98 : * @return the transformed edits per file path
99 : */
100 : public Multimap<String, ContextAwareEdit> getEditsPerFilePath() {
101 3 : return edits.stream()
102 3 : .collect(
103 3 : toMultimap(
104 : c -> {
105 : String path =
106 3 : c.getNewFilePath().isPresent()
107 3 : ? c.getNewFilePath().get()
108 3 : : c.getOldFilePath().get();
109 3 : return path;
110 : },
111 3 : Function.identity(),
112 : ArrayListMultimap::create));
113 : }
114 :
115 : public static Stream<ContextAwareEdit> toEdits(FileEdits in) {
116 3 : List<Edit> edits = in.edits();
117 3 : if (edits.isEmpty()) {
118 0 : return Stream.of(ContextAwareEdit.createForNoContentEdit(in.oldPath(), in.newPath()));
119 : }
120 :
121 3 : return edits.stream().map(edit -> ContextAwareEdit.create(in.oldPath(), in.newPath(), edit));
122 : }
123 :
124 : private void transformEdits(List<FileEdits> inputs, SideStrategy sideStrategy) {
125 3 : ImmutableList<PositionedEntity<ContextAwareEdit>> positionedEdits =
126 3 : edits.stream()
127 3 : .map(edit -> toPositionedEntity(edit, sideStrategy))
128 3 : .collect(toImmutableList());
129 3 : ImmutableSet<Mapping> mappings =
130 3 : inputs.stream().map(DiffMappings::toMapping).collect(toImmutableSet());
131 :
132 3 : edits =
133 3 : positionTransformer.transform(positionedEdits, mappings).stream()
134 3 : .map(PositionedEntity::getEntityAtUpdatedPosition)
135 3 : .collect(toImmutableList());
136 3 : }
137 :
138 : private static PositionedEntity<ContextAwareEdit> toPositionedEntity(
139 : ContextAwareEdit edit, SideStrategy sideStrategy) {
140 3 : return PositionedEntity.create(
141 3 : edit, sideStrategy::extractPosition, sideStrategy::createEditAtNewPosition);
142 : }
143 :
144 : @AutoValue
145 3 : abstract static class ContextAwareEdit {
146 : static ContextAwareEdit create(Optional<String> oldPath, Optional<String> newPath, Edit edit) {
147 : // TODO(ghareeb): Look if the new FileEdits class is capable of representing renames/copies
148 : // and in this case we can get rid of the ContextAwareEdit class.
149 3 : return create(
150 3 : oldPath, newPath, edit.beginA(), edit.endA(), edit.beginB(), edit.endB(), false);
151 : }
152 :
153 : static ContextAwareEdit createForNoContentEdit(
154 : Optional<String> oldPath, Optional<String> newPath) {
155 : // Remove the warning in createEditAtNewPosition() if we switch to an empty range instead of
156 : // (-1:-1, -1:-1) in the future.
157 0 : return create(oldPath, newPath, -1, -1, -1, -1, false);
158 : }
159 :
160 : static ContextAwareEdit create(
161 : Optional<String> oldFilePath,
162 : Optional<String> newFilePath,
163 : int beginA,
164 : int endA,
165 : int beginB,
166 : int endB,
167 : boolean filePathAdjusted) {
168 3 : Optional<String> adjustedFilePath = oldFilePath.isPresent() ? oldFilePath : newFilePath;
169 3 : boolean implicitRename =
170 3 : newFilePath.isPresent()
171 3 : && oldFilePath.isPresent()
172 3 : && !Objects.equals(oldFilePath.get(), newFilePath.get())
173 : && filePathAdjusted;
174 3 : return new AutoValue_EditTransformer_ContextAwareEdit(
175 : adjustedFilePath, newFilePath, beginA, endA, beginB, endB, implicitRename);
176 : }
177 :
178 : public abstract Optional<String> getOldFilePath();
179 :
180 : public abstract Optional<String> getNewFilePath();
181 :
182 : public abstract int getBeginA();
183 :
184 : public abstract int getEndA();
185 :
186 : public abstract int getBeginB();
187 :
188 : public abstract int getEndB();
189 :
190 : // Used for equals(), for which this value is important.
191 : public abstract boolean isImplicitRename();
192 :
193 : public Optional<org.eclipse.jgit.diff.Edit> toEdit() {
194 3 : if (getBeginA() < 0) {
195 0 : return Optional.empty();
196 : }
197 :
198 3 : return Optional.of(
199 3 : new org.eclipse.jgit.diff.Edit(getBeginA(), getEndA(), getBeginB(), getEndB()));
200 : }
201 : }
202 :
203 : private interface SideStrategy {
204 : Position extractPosition(ContextAwareEdit edit);
205 :
206 : ContextAwareEdit createEditAtNewPosition(ContextAwareEdit edit, Position newPosition);
207 : }
208 :
209 3 : private enum SideAStrategy implements SideStrategy {
210 3 : INSTANCE;
211 :
212 : @Override
213 : public Position extractPosition(ContextAwareEdit edit) {
214 : String filePath =
215 3 : edit.getOldFilePath().isPresent()
216 3 : ? edit.getOldFilePath().get()
217 3 : : edit.getNewFilePath().get();
218 3 : return Position.builder()
219 3 : .filePath(filePath)
220 3 : .lineRange(Range.create(edit.getBeginA(), edit.getEndA()))
221 3 : .build();
222 : }
223 :
224 : @Override
225 : public ContextAwareEdit createEditAtNewPosition(ContextAwareEdit edit, Position newPosition) {
226 : // Use an empty range at Gerrit "file level" if no target range is available. Such an empty
227 : // range should not occur right now but this should be a safe fallback if something changes
228 : // in the future.
229 3 : Range updatedRange = newPosition.lineRange().orElseGet(() -> Range.create(-1, -1));
230 3 : if (!newPosition.lineRange().isPresent()) {
231 0 : logger.atWarning().log(
232 : "Position %s has an empty range which is unexpected for the edits-due-to-rebase"
233 : + " computation. This is likely a regression!",
234 : newPosition);
235 : }
236 : // Same as for the range above. PATCHSET_LEVEL is a safe fallback.
237 3 : String updatedFilePath = newPosition.filePath().orElse(Patch.PATCHSET_LEVEL);
238 3 : if (!newPosition.filePath().isPresent()) {
239 0 : logger.atWarning().log(
240 : "Position %s has an empty file path which is unexpected for the edits-due-to-rebase"
241 : + " computation. This is likely a regression!",
242 : newPosition);
243 : }
244 3 : return ContextAwareEdit.create(
245 3 : Optional.of(updatedFilePath),
246 3 : edit.getNewFilePath(),
247 3 : updatedRange.start(),
248 3 : updatedRange.end(),
249 3 : edit.getBeginB(),
250 3 : edit.getEndB(),
251 3 : !Objects.equals(edit.getOldFilePath(), Optional.of(updatedFilePath)));
252 : }
253 : }
254 :
255 3 : private enum SideBStrategy implements SideStrategy {
256 3 : INSTANCE;
257 :
258 : @Override
259 : public Position extractPosition(ContextAwareEdit edit) {
260 : String filePath =
261 3 : edit.getNewFilePath().isPresent()
262 3 : ? edit.getNewFilePath().get()
263 3 : : edit.getOldFilePath().get();
264 3 : return Position.builder()
265 3 : .filePath(filePath)
266 3 : .lineRange(Range.create(edit.getBeginB(), edit.getEndB()))
267 3 : .build();
268 : }
269 :
270 : @Override
271 : public ContextAwareEdit createEditAtNewPosition(ContextAwareEdit edit, Position newPosition) {
272 : // Use an empty range at Gerrit "file level" if no target range is available. Such an empty
273 : // range should not occur right now but this should be a safe fallback if something changes
274 : // in the future.
275 3 : Range updatedRange = newPosition.lineRange().orElseGet(() -> Range.create(-1, -1));
276 : // Same as far the range above. PATCHSET_LEVEL is a safe fallback.
277 3 : Optional<String> updatedFilePath =
278 3 : Optional.of(newPosition.filePath().orElse(Patch.PATCHSET_LEVEL));
279 3 : return ContextAwareEdit.create(
280 3 : edit.getOldFilePath(),
281 : updatedFilePath,
282 3 : edit.getBeginA(),
283 3 : edit.getEndA(),
284 3 : updatedRange.start(),
285 3 : updatedRange.end(),
286 3 : !Objects.equals(edit.getNewFilePath(), updatedFilePath));
287 : }
288 : }
289 : }
|