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.filediff;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 : import static java.nio.charset.StandardCharsets.UTF_8;
19 :
20 : import com.google.common.cache.CacheLoader;
21 : import com.google.common.cache.LoadingCache;
22 : import com.google.common.collect.ImmutableList;
23 : import com.google.common.collect.ImmutableMap;
24 : import com.google.common.collect.Iterables;
25 : import com.google.common.collect.Multimap;
26 : import com.google.common.collect.Streams;
27 : import com.google.common.flogger.FluentLogger;
28 : import com.google.gerrit.common.Nullable;
29 : import com.google.gerrit.entities.Patch;
30 : import com.google.gerrit.entities.Project;
31 : import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
32 : import com.google.gerrit.server.cache.CacheModule;
33 : import com.google.gerrit.server.git.GitRepositoryManager;
34 : import com.google.gerrit.server.logging.Metadata;
35 : import com.google.gerrit.server.logging.TraceContext;
36 : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
37 : import com.google.gerrit.server.patch.AutoMerger;
38 : import com.google.gerrit.server.patch.ComparisonType;
39 : import com.google.gerrit.server.patch.DiffNotAvailableException;
40 : import com.google.gerrit.server.patch.DiffUtil;
41 : import com.google.gerrit.server.patch.Text;
42 : import com.google.gerrit.server.patch.filediff.EditTransformer.ContextAwareEdit;
43 : import com.google.gerrit.server.patch.gitfilediff.FileHeaderUtil;
44 : import com.google.gerrit.server.patch.gitfilediff.GitFileDiff;
45 : import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl;
46 : import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl.DiffAlgorithmFactory;
47 : import com.google.inject.Inject;
48 : import com.google.inject.Module;
49 : import com.google.inject.Singleton;
50 : import com.google.inject.name.Named;
51 : import java.io.IOException;
52 : import java.util.ArrayList;
53 : import java.util.Collection;
54 : import java.util.HashMap;
55 : import java.util.HashSet;
56 : import java.util.List;
57 : import java.util.Map;
58 : import java.util.Optional;
59 : import java.util.Set;
60 : import java.util.concurrent.ExecutionException;
61 : import java.util.stream.Collectors;
62 : import org.eclipse.jgit.diff.EditList;
63 : import org.eclipse.jgit.diff.RawText;
64 : import org.eclipse.jgit.diff.RawTextComparator;
65 : import org.eclipse.jgit.lib.ObjectId;
66 : import org.eclipse.jgit.lib.ObjectReader;
67 : import org.eclipse.jgit.lib.Repository;
68 : import org.eclipse.jgit.patch.FileHeader;
69 : import org.eclipse.jgit.patch.FileHeader.PatchType;
70 : import org.eclipse.jgit.revwalk.RevCommit;
71 : import org.eclipse.jgit.revwalk.RevTree;
72 : import org.eclipse.jgit.revwalk.RevWalk;
73 :
74 : /**
75 : * Cache for the single file diff between two commits for a single file path. This cache adds extra
76 : * Gerrit logic such as identifying edits due to rebase.
77 : *
78 : * <p>If the {@link FileDiffCacheKey#oldCommit()} is equal to {@link ObjectId#zeroId()}, the git
79 : * diff will be evaluated against the empty tree.
80 : */
81 : @Singleton
82 : public class FileDiffCacheImpl implements FileDiffCache {
83 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
84 :
85 : private static final String DIFF = "gerrit_file_diff";
86 :
87 : private final LoadingCache<FileDiffCacheKey, FileDiffOutput> cache;
88 :
89 : public static Module module() {
90 152 : return new CacheModule() {
91 : @Override
92 : protected void configure() {
93 152 : bind(FileDiffCache.class).to(FileDiffCacheImpl.class);
94 :
95 152 : factory(AllDiffsEvaluator.Factory.class);
96 :
97 152 : persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
98 152 : .maximumWeight(10 << 20)
99 152 : .weigher(FileDiffWeigher.class)
100 152 : .version(9)
101 152 : .keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
102 152 : .valueSerializer(FileDiffOutput.Serializer.INSTANCE)
103 152 : .loader(FileDiffLoader.class);
104 152 : }
105 : };
106 : }
107 :
108 104 : private enum MagicPath {
109 104 : COMMIT,
110 104 : MERGE_LIST
111 : }
112 :
113 : @Inject
114 152 : public FileDiffCacheImpl(@Named(DIFF) LoadingCache<FileDiffCacheKey, FileDiffOutput> cache) {
115 152 : this.cache = cache;
116 152 : }
117 :
118 : @Override
119 : public FileDiffOutput get(FileDiffCacheKey key) throws DiffNotAvailableException {
120 : try {
121 0 : return cache.get(key);
122 0 : } catch (ExecutionException e) {
123 0 : throw new DiffNotAvailableException(e);
124 : }
125 : }
126 :
127 : @Override
128 : public ImmutableMap<FileDiffCacheKey, FileDiffOutput> getAll(Iterable<FileDiffCacheKey> keys)
129 : throws DiffNotAvailableException {
130 : try {
131 104 : ImmutableMap<FileDiffCacheKey, FileDiffOutput> result = cache.getAll(keys);
132 104 : if (result.size() != Iterables.size(keys)) {
133 0 : throw new DiffNotAvailableException(
134 0 : String.format(
135 : "Failed to load the value for all %d keys. Returned "
136 : + "map contains only %d values",
137 0 : Iterables.size(keys), result.size()));
138 : }
139 104 : return result;
140 0 : } catch (ExecutionException e) {
141 0 : throw new DiffNotAvailableException(e);
142 : }
143 : }
144 :
145 : static class FileDiffLoader extends CacheLoader<FileDiffCacheKey, FileDiffOutput> {
146 : private final GitRepositoryManager repoManager;
147 : private final AllDiffsEvaluator.Factory allDiffsEvaluatorFactory;
148 :
149 : @Inject
150 : FileDiffLoader(
151 152 : AllDiffsEvaluator.Factory allDiffsEvaluatorFactory, GitRepositoryManager manager) {
152 152 : this.allDiffsEvaluatorFactory = allDiffsEvaluatorFactory;
153 152 : this.repoManager = manager;
154 152 : }
155 :
156 : @Override
157 : public FileDiffOutput load(FileDiffCacheKey key) throws IOException, DiffNotAvailableException {
158 0 : try (TraceTimer timer =
159 0 : TraceContext.newTimer(
160 : "Loading a single key from file diff cache",
161 0 : Metadata.builder().filePath(key.newFilePath()).build())) {
162 0 : return loadAll(ImmutableList.of(key)).get(key);
163 : }
164 : }
165 :
166 : @Override
167 : public Map<FileDiffCacheKey, FileDiffOutput> loadAll(Iterable<? extends FileDiffCacheKey> keys)
168 : throws DiffNotAvailableException {
169 104 : try (TraceTimer timer = TraceContext.newTimer("Loading multiple keys from file diff cache")) {
170 104 : ImmutableMap.Builder<FileDiffCacheKey, FileDiffOutput> result = ImmutableMap.builder();
171 :
172 104 : Map<Project.NameKey, List<FileDiffCacheKey>> keysByProject =
173 104 : Streams.stream(keys)
174 104 : .distinct()
175 104 : .collect(Collectors.groupingBy(FileDiffCacheKey::project));
176 :
177 104 : for (Project.NameKey project : keysByProject.keySet()) {
178 104 : List<FileDiffCacheKey> fileKeys = new ArrayList<>();
179 :
180 104 : try (Repository repo = repoManager.openRepository(project);
181 104 : ObjectReader reader = repo.newObjectReader();
182 104 : RevWalk rw = new RevWalk(reader)) {
183 :
184 104 : for (FileDiffCacheKey key : keysByProject.get(project)) {
185 104 : if (key.newFilePath().equals(Patch.COMMIT_MSG)) {
186 104 : result.put(key, createMagicPathEntry(key, reader, rw, MagicPath.COMMIT));
187 93 : } else if (key.newFilePath().equals(Patch.MERGE_LIST)) {
188 31 : result.put(key, createMagicPathEntry(key, reader, rw, MagicPath.MERGE_LIST));
189 : } else {
190 93 : fileKeys.add(key);
191 : }
192 104 : }
193 104 : result.putAll(createFileEntries(reader, fileKeys, rw));
194 0 : } catch (IOException e) {
195 0 : logger.atWarning().log("Failed to open the repository %s: %s", project, e.getMessage());
196 104 : }
197 104 : }
198 104 : return result.build();
199 : }
200 : }
201 :
202 : private ComparisonType getComparisonType(
203 : RevWalk rw, ObjectReader reader, ObjectId oldCommitId, ObjectId newCommitId)
204 : throws IOException {
205 104 : if (oldCommitId.equals(ObjectId.zeroId())) {
206 32 : return ComparisonType.againstRoot();
207 : }
208 100 : RevCommit oldCommit = DiffUtil.getRevCommit(rw, oldCommitId);
209 100 : RevCommit newCommit = DiffUtil.getRevCommit(rw, newCommitId);
210 100 : for (int i = 0; i < newCommit.getParentCount(); i++) {
211 100 : if (newCommit.getParent(i).equals(oldCommit)) {
212 100 : return ComparisonType.againstParent(i + 1);
213 : }
214 : }
215 : // TODO(ghareeb): it's not trivial to distinguish if diff with old commit is against another
216 : // patchset or auto-merge. Looking at the commit message of old commit gives a strong
217 : // signal that we are diffing against auto-merge, though not 100% accurate (e.g. if old commit
218 : // has the auto-merge prefix in the commit message). A better resolution would be to move the
219 : // COMMIT_MSG and MERGE_LIST evaluations outside of the diff cache. For more details, see
220 : // discussion in
221 : // https://gerrit-review.googlesource.com/c/gerrit/+/280519/6..18/java/com/google/gerrit/server/patch/FileDiffCache.java#b540
222 37 : String oldCommitMsgTxt = new String(Text.forCommit(reader, oldCommit).getContent(), UTF_8);
223 37 : if (oldCommitMsgTxt.contains(AutoMerger.AUTO_MERGE_MSG_PREFIX)) {
224 31 : return ComparisonType.againstAutoMerge();
225 : }
226 11 : return ComparisonType.againstOtherPatchSet();
227 : }
228 :
229 : /**
230 : * Creates a {@link FileDiffOutput} entry for the "Commit message" or "Merge list" magic paths.
231 : */
232 : private FileDiffOutput createMagicPathEntry(
233 : FileDiffCacheKey key, ObjectReader reader, RevWalk rw, MagicPath magicPath) {
234 : try {
235 104 : RawTextComparator cmp = comparatorFor(key.whitespace());
236 104 : ComparisonType comparisonType =
237 104 : getComparisonType(rw, reader, key.oldCommit(), key.newCommit());
238 : RevCommit aCommit =
239 104 : key.oldCommit().equals(ObjectId.zeroId())
240 32 : ? null
241 104 : : DiffUtil.getRevCommit(rw, key.oldCommit());
242 104 : RevCommit bCommit = DiffUtil.getRevCommit(rw, key.newCommit());
243 104 : return magicPath == MagicPath.COMMIT
244 104 : ? createCommitEntry(reader, aCommit, bCommit, comparisonType, cmp, key.diffAlgorithm())
245 31 : : createMergeListEntry(
246 31 : reader, aCommit, bCommit, comparisonType, cmp, key.diffAlgorithm());
247 0 : } catch (IOException e) {
248 0 : logger.atWarning().log("Failed to compute commit entry for key %s", key);
249 : }
250 0 : return FileDiffOutput.empty(key.newFilePath(), key.oldCommit(), key.newCommit());
251 : }
252 :
253 : private static RawTextComparator comparatorFor(Whitespace ws) {
254 104 : switch (ws) {
255 : case IGNORE_ALL:
256 0 : return RawTextComparator.WS_IGNORE_ALL;
257 :
258 : case IGNORE_TRAILING:
259 0 : return RawTextComparator.WS_IGNORE_TRAILING;
260 :
261 : case IGNORE_LEADING_AND_TRAILING:
262 3 : return RawTextComparator.WS_IGNORE_CHANGE;
263 :
264 : case IGNORE_NONE:
265 : default:
266 104 : return RawTextComparator.DEFAULT;
267 : }
268 : }
269 :
270 : /**
271 : * Creates a commit entry. {@code oldCommit} is null if the comparison is against a root commit.
272 : */
273 : private FileDiffOutput createCommitEntry(
274 : ObjectReader reader,
275 : @Nullable RevCommit oldCommit,
276 : RevCommit newCommit,
277 : ComparisonType comparisonType,
278 : RawTextComparator rawTextComparator,
279 : GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
280 : throws IOException {
281 : Text aText =
282 104 : oldCommit == null || comparisonType.isAgainstParentOrAutoMerge()
283 104 : ? Text.EMPTY
284 104 : : Text.forCommit(reader, oldCommit);
285 104 : Text bText = Text.forCommit(reader, newCommit);
286 104 : return createMagicFileDiffOutput(
287 : oldCommit,
288 : newCommit,
289 : comparisonType,
290 : rawTextComparator,
291 : aText,
292 : bText,
293 : Patch.COMMIT_MSG,
294 : diffAlgorithm);
295 : }
296 :
297 : /**
298 : * Creates a merge list entry. {@code oldCommit} is null if the comparison is against a root
299 : * commit.
300 : */
301 : private FileDiffOutput createMergeListEntry(
302 : ObjectReader reader,
303 : @Nullable RevCommit oldCommit,
304 : RevCommit newCommit,
305 : ComparisonType comparisonType,
306 : RawTextComparator rawTextComparator,
307 : GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
308 : throws IOException {
309 : Text aText =
310 31 : oldCommit == null || comparisonType.isAgainstParentOrAutoMerge()
311 31 : ? Text.EMPTY
312 31 : : Text.forMergeList(comparisonType, reader, oldCommit);
313 31 : Text bText = Text.forMergeList(comparisonType, reader, newCommit);
314 31 : return createMagicFileDiffOutput(
315 : oldCommit,
316 : newCommit,
317 : comparisonType,
318 : rawTextComparator,
319 : aText,
320 : bText,
321 : Patch.MERGE_LIST,
322 : diffAlgorithm);
323 : }
324 :
325 : private static FileDiffOutput createMagicFileDiffOutput(
326 : @Nullable ObjectId oldCommit,
327 : ObjectId newCommit,
328 : ComparisonType comparisonType,
329 : RawTextComparator rawTextComparator,
330 : Text aText,
331 : Text bText,
332 : String fileName,
333 : GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm) {
334 104 : byte[] rawHdr = getRawHeader(!comparisonType.isAgainstParentOrAutoMerge(), fileName);
335 104 : byte[] aContent = aText.getContent();
336 104 : byte[] bContent = bText.getContent();
337 104 : long size = bContent.length;
338 104 : long sizeDelta = size - aContent.length;
339 104 : RawText aRawText = new RawText(aContent);
340 104 : RawText bRawText = new RawText(bContent);
341 104 : EditList edits =
342 104 : DiffAlgorithmFactory.create(diffAlgorithm).diff(rawTextComparator, aRawText, bRawText);
343 104 : FileHeader fileHeader = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
344 104 : Patch.ChangeType changeType = FileHeaderUtil.getChangeType(fileHeader);
345 104 : return FileDiffOutput.builder()
346 104 : .oldCommitId(oldCommit == null ? ObjectId.zeroId() : oldCommit)
347 104 : .newCommitId(newCommit)
348 104 : .comparisonType(comparisonType)
349 104 : .oldPath(FileHeaderUtil.getOldPath(fileHeader))
350 104 : .newPath(FileHeaderUtil.getNewPath(fileHeader))
351 104 : .changeType(changeType)
352 104 : .patchType(Optional.of(FileHeaderUtil.getPatchType(fileHeader)))
353 104 : .headerLines(FileHeaderUtil.getHeaderLines(fileHeader))
354 104 : .edits(
355 104 : asTaggedEdits(
356 104 : edits.stream().map(Edit::fromJGitEdit).collect(Collectors.toList()),
357 104 : ImmutableList.of()))
358 104 : .size(size)
359 104 : .sizeDelta(sizeDelta)
360 104 : .build();
361 : }
362 :
363 : private static byte[] getRawHeader(boolean hasA, String fileName) {
364 104 : StringBuilder hdr = new StringBuilder();
365 104 : hdr.append("diff --git");
366 104 : if (hasA) {
367 10 : hdr.append(" a/").append(fileName);
368 : } else {
369 104 : hdr.append(" ").append(FileHeader.DEV_NULL);
370 : }
371 104 : hdr.append(" b/").append(fileName);
372 104 : hdr.append("\n");
373 :
374 104 : if (hasA) {
375 10 : hdr.append("--- a/").append(fileName).append("\n");
376 : } else {
377 104 : hdr.append("--- ").append(FileHeader.DEV_NULL).append("\n");
378 : }
379 104 : hdr.append("+++ b/").append(fileName).append("\n");
380 104 : return hdr.toString().getBytes(UTF_8);
381 : }
382 :
383 : private Map<FileDiffCacheKey, FileDiffOutput> createFileEntries(
384 : ObjectReader reader, List<FileDiffCacheKey> keys, RevWalk rw)
385 : throws DiffNotAvailableException, IOException {
386 104 : Map<AugmentedFileDiffCacheKey, AllFileGitDiffs> allFileDiffs =
387 104 : allDiffsEvaluatorFactory.create(rw).execute(wrapKeys(keys, rw));
388 :
389 104 : Map<FileDiffCacheKey, FileDiffOutput> result = new HashMap<>();
390 :
391 104 : for (AugmentedFileDiffCacheKey augmentedKey : allFileDiffs.keySet()) {
392 93 : AllFileGitDiffs allDiffs = allFileDiffs.get(augmentedKey);
393 93 : GitFileDiff mainGitDiff = allDiffs.mainDiff().gitDiff();
394 :
395 93 : if (mainGitDiff.isNegative()) {
396 : // If the result of the git diff computation was negative, i.e. due to timeout, cache a
397 : // negative result.
398 0 : result.put(
399 0 : augmentedKey.key(),
400 0 : FileDiffOutput.createNegative(
401 0 : mainGitDiff.newPath().orElse(""),
402 0 : augmentedKey.key().oldCommit(),
403 0 : augmentedKey.key().newCommit()));
404 0 : continue;
405 : }
406 :
407 93 : FileEdits rebaseFileEdits = FileEdits.empty();
408 93 : if (!augmentedKey.ignoreRebase()) {
409 4 : rebaseFileEdits = computeRebaseEdits(allDiffs);
410 : }
411 93 : List<Edit> rebaseEdits = rebaseFileEdits.edits();
412 :
413 93 : ObjectId oldTreeId = allDiffs.mainDiff().gitKey().oldTree();
414 :
415 93 : RevTree aTree = oldTreeId.equals(ObjectId.zeroId()) ? null : rw.parseTree(oldTreeId);
416 93 : RevTree bTree = rw.parseTree(allDiffs.mainDiff().gitKey().newTree());
417 :
418 : Long oldSize =
419 93 : aTree != null && mainGitDiff.oldMode().isPresent() && mainGitDiff.oldPath().isPresent()
420 : ? new FileSizeEvaluator(reader, aTree)
421 54 : .compute(
422 54 : mainGitDiff.oldId(),
423 54 : mainGitDiff.oldMode().get(),
424 54 : mainGitDiff.oldPath().get())
425 90 : : 0;
426 : Long newSize =
427 93 : mainGitDiff.newMode().isPresent() && mainGitDiff.newPath().isPresent()
428 : ? new FileSizeEvaluator(reader, bTree)
429 93 : .compute(
430 93 : mainGitDiff.newId(),
431 93 : mainGitDiff.newMode().get(),
432 93 : mainGitDiff.newPath().get())
433 23 : : 0;
434 :
435 93 : ObjectId oldCommit = augmentedKey.key().oldCommit();
436 93 : ObjectId newCommit = augmentedKey.key().newCommit();
437 : FileDiffOutput fileDiff =
438 93 : FileDiffOutput.builder()
439 93 : .oldCommitId(oldCommit)
440 93 : .newCommitId(newCommit)
441 93 : .comparisonType(getComparisonType(rw, reader, oldCommit, newCommit))
442 93 : .changeType(mainGitDiff.changeType())
443 93 : .patchType(mainGitDiff.patchType())
444 93 : .oldPath(mainGitDiff.oldPath())
445 93 : .newPath(mainGitDiff.newPath())
446 93 : .oldMode(mainGitDiff.oldMode())
447 93 : .newMode(mainGitDiff.newMode())
448 93 : .headerLines(FileHeaderUtil.getHeaderLines(mainGitDiff.fileHeader()))
449 93 : .edits(asTaggedEdits(mainGitDiff.edits(), rebaseEdits))
450 93 : .size(newSize)
451 93 : .sizeDelta(newSize - oldSize)
452 93 : .build();
453 :
454 93 : result.put(augmentedKey.key(), fileDiff);
455 93 : }
456 :
457 104 : return result;
458 : }
459 :
460 : /**
461 : * Convert the list of input keys {@link FileDiffCacheKey} to a list of {@link
462 : * AugmentedFileDiffCacheKey} that also include the old and new parent commit IDs, and a boolean
463 : * that indicates whether we should include the rebase edits for each key.
464 : *
465 : * <p>The output list is expected to have the same size of the input list, i.e. we map all keys.
466 : */
467 : private List<AugmentedFileDiffCacheKey> wrapKeys(List<FileDiffCacheKey> keys, RevWalk rw) {
468 104 : List<AugmentedFileDiffCacheKey> result = new ArrayList<>();
469 104 : for (FileDiffCacheKey key : keys) {
470 93 : if (key.oldCommit().equals(ObjectId.zeroId())) {
471 24 : result.add(AugmentedFileDiffCacheKey.builder().key(key).ignoreRebase(true).build());
472 24 : continue;
473 : }
474 : try {
475 91 : RevCommit oldRevCommit = DiffUtil.getRevCommit(rw, key.oldCommit());
476 91 : RevCommit newRevCommit = DiffUtil.getRevCommit(rw, key.newCommit());
477 91 : if (!DiffUtil.areRelated(oldRevCommit, newRevCommit)) {
478 4 : result.add(
479 4 : AugmentedFileDiffCacheKey.builder()
480 4 : .key(key)
481 4 : .oldParentId(Optional.of(oldRevCommit.getParent(0).getId()))
482 4 : .newParentId(Optional.of(newRevCommit.getParent(0).getId()))
483 4 : .ignoreRebase(false)
484 4 : .build());
485 : } else {
486 91 : result.add(AugmentedFileDiffCacheKey.builder().key(key).ignoreRebase(true).build());
487 : }
488 0 : } catch (IOException e) {
489 0 : logger.atWarning().log(
490 : "Failed to evaluate commits relation for key "
491 : + key
492 : + ". Skipping this key: "
493 0 : + e.getMessage(),
494 : e);
495 0 : result.add(AugmentedFileDiffCacheKey.builder().key(key).ignoreRebase(true).build());
496 91 : }
497 91 : }
498 104 : return result;
499 : }
500 :
501 : private static ImmutableList<TaggedEdit> asTaggedEdits(
502 : List<Edit> normalEdits, List<Edit> rebaseEdits) {
503 104 : Set<Edit> rebaseEditsSet = new HashSet<>(rebaseEdits);
504 104 : ImmutableList.Builder<TaggedEdit> result =
505 104 : ImmutableList.builderWithExpectedSize(normalEdits.size());
506 104 : for (Edit e : normalEdits) {
507 104 : result.add(TaggedEdit.create(e, rebaseEditsSet.contains(e)));
508 104 : }
509 104 : return result.build();
510 : }
511 :
512 : /**
513 : * Computes the subset of edits that are due to rebase between 2 commits.
514 : *
515 : * <p>The input parameter {@link AllFileGitDiffs#mainDiff} contains all the edits in
516 : * consideration. Of those, we identify the edits due to rebase as a function of:
517 : *
518 : * <ol>
519 : * <li>The edits between the old commit and its parent {@link
520 : * AllFileGitDiffs#oldVsParentDiff}.
521 : * <li>The edits between the new commit and its parent {@link
522 : * AllFileGitDiffs#newVsParentDiff}.
523 : * <li>The edits between the parents of the old commit and new commits {@link
524 : * AllFileGitDiffs#parentVsParentDiff}.
525 : * </ol>
526 : *
527 : * @param diffs an entity containing 4 sets of edits: those between the old and new commit,
528 : * between the old and new commits vs. their parents, and between the old and new parents.
529 : * @return the list of edits that are due to rebase.
530 : */
531 : private FileEdits computeRebaseEdits(AllFileGitDiffs diffs) {
532 4 : if (!diffs.parentVsParentDiff().isPresent()) {
533 3 : return FileEdits.empty();
534 : }
535 :
536 3 : GitFileDiff parentVsParentDiff = diffs.parentVsParentDiff().get().gitDiff();
537 :
538 3 : EditTransformer editTransformer =
539 : new EditTransformer(
540 3 : ImmutableList.of(
541 3 : FileEdits.create(
542 3 : parentVsParentDiff.edits().stream().collect(toImmutableList()),
543 3 : parentVsParentDiff.oldPath(),
544 3 : parentVsParentDiff.newPath())));
545 :
546 3 : if (diffs.oldVsParentDiff().isPresent()) {
547 3 : GitFileDiff oldVsParDiff = diffs.oldVsParentDiff().get().gitDiff();
548 3 : editTransformer.transformReferencesOfSideA(
549 3 : ImmutableList.of(
550 3 : FileEdits.create(
551 3 : oldVsParDiff.edits().stream().collect(toImmutableList()),
552 3 : oldVsParDiff.oldPath(),
553 3 : oldVsParDiff.newPath())));
554 : }
555 :
556 3 : if (diffs.newVsParentDiff().isPresent()) {
557 3 : GitFileDiff newVsParDiff = diffs.newVsParentDiff().get().gitDiff();
558 3 : editTransformer.transformReferencesOfSideB(
559 3 : ImmutableList.of(
560 3 : FileEdits.create(
561 3 : newVsParDiff.edits().stream().collect(toImmutableList()),
562 3 : newVsParDiff.oldPath(),
563 3 : newVsParDiff.newPath())));
564 : }
565 :
566 3 : Multimap<String, ContextAwareEdit> editsPerFilePath = editTransformer.getEditsPerFilePath();
567 :
568 3 : if (editsPerFilePath.isEmpty()) {
569 2 : return FileEdits.empty();
570 : }
571 :
572 : // editsPerFilePath is expected to have a single item representing the file
573 3 : String filePath = editsPerFilePath.keys().iterator().next();
574 3 : Collection<ContextAwareEdit> edits = editsPerFilePath.get(filePath);
575 3 : return FileEdits.create(
576 3 : edits.stream()
577 3 : .map(ContextAwareEdit::toEdit)
578 3 : .filter(Optional::isPresent)
579 3 : .map(Optional::get)
580 3 : .map(Edit::fromJGitEdit)
581 3 : .collect(toImmutableList()),
582 3 : edits.iterator().next().getOldFilePath(),
583 3 : edits.iterator().next().getNewFilePath());
584 : }
585 : }
586 : }
|