Line data Source code
1 : // Copyright (C) 2009 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.ImmutableList.toImmutableList;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 :
20 : import com.google.common.collect.ImmutableList;
21 : import com.google.common.collect.ImmutableSet;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.common.data.PatchScript;
24 : import com.google.gerrit.common.data.PatchScript.DisplayMethod;
25 : import com.google.gerrit.entities.FixReplacement;
26 : import com.google.gerrit.entities.Patch;
27 : import com.google.gerrit.entities.Patch.ChangeType;
28 : import com.google.gerrit.entities.Patch.PatchType;
29 : import com.google.gerrit.extensions.client.DiffPreferencesInfo;
30 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
31 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
32 : import com.google.gerrit.server.fixes.FixCalculator;
33 : import com.google.gerrit.server.mime.FileTypeRegistry;
34 : import com.google.gerrit.server.patch.DiffContentCalculator.DiffCalculatorResult;
35 : import com.google.gerrit.server.patch.DiffContentCalculator.TextSource;
36 : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
37 : import com.google.gerrit.server.patch.filediff.TaggedEdit;
38 : import com.google.inject.Inject;
39 : import eu.medsea.mimeutil.MimeType;
40 : import eu.medsea.mimeutil.MimeUtil2;
41 : import java.io.IOException;
42 : import java.util.List;
43 : import java.util.Objects;
44 : import java.util.Optional;
45 : import java.util.Set;
46 : import org.eclipse.jgit.diff.Edit;
47 : import org.eclipse.jgit.lib.Constants;
48 : import org.eclipse.jgit.lib.FileMode;
49 : import org.eclipse.jgit.lib.ObjectId;
50 : import org.eclipse.jgit.lib.ObjectReader;
51 : import org.eclipse.jgit.lib.Repository;
52 : import org.eclipse.jgit.revwalk.RevTree;
53 : import org.eclipse.jgit.revwalk.RevWalk;
54 : import org.eclipse.jgit.treewalk.TreeWalk;
55 :
56 : class PatchScriptBuilder {
57 :
58 : private DiffPreferencesInfo diffPrefs;
59 : private final FileTypeRegistry registry;
60 : private IntraLineDiffCalculator intralineDiffCalculator;
61 :
62 : @Inject
63 14 : PatchScriptBuilder(FileTypeRegistry ftr) {
64 14 : registry = ftr;
65 14 : }
66 :
67 : void setDiffPrefs(DiffPreferencesInfo dp) {
68 14 : diffPrefs = dp;
69 14 : }
70 :
71 : void setIntraLineDiffCalculator(IntraLineDiffCalculator calculator) {
72 6 : intralineDiffCalculator = calculator;
73 6 : }
74 :
75 : /** Convert into {@link PatchScript} using the new diff cache output. */
76 : PatchScript toPatchScript(Repository git, FileDiffOutput content) throws IOException {
77 12 : PatchFileChange change =
78 : new PatchFileChange(
79 12 : content.edits().stream().map(TaggedEdit::jgitEdit).collect(toImmutableList()),
80 12 : content.edits().stream()
81 12 : .filter(TaggedEdit::dueToRebase)
82 12 : .map(TaggedEdit::jgitEdit)
83 12 : .collect(toImmutableSet()),
84 12 : content.headerLines(),
85 12 : FilePathAdapter.getOldPath(content.oldPath(), content.changeType()),
86 12 : FilePathAdapter.getNewPath(content.oldPath(), content.newPath(), content.changeType()),
87 12 : content.changeType(),
88 12 : content.patchType().orElse(null));
89 12 : SidesResolver sidesResolver = new SidesResolver(git, content.comparisonType());
90 12 : ResolvedSides sides =
91 12 : resolveSides(
92 : git,
93 : sidesResolver,
94 12 : oldName(change),
95 12 : newName(change),
96 12 : content.oldCommitId(),
97 12 : content.newCommitId());
98 12 : return build(sides.a, sides.b, change);
99 : }
100 :
101 : private ResolvedSides resolveSides(
102 : Repository git,
103 : SidesResolver sidesResolver,
104 : String oldName,
105 : String newName,
106 : ObjectId aId,
107 : ObjectId bId)
108 : throws IOException {
109 12 : try (ObjectReader reader = git.newObjectReader()) {
110 12 : PatchSide a = sidesResolver.resolve(registry, reader, oldName, null, aId, true);
111 12 : PatchSide b =
112 12 : sidesResolver.resolve(registry, reader, newName, a, bId, Objects.equals(aId, bId));
113 12 : return new ResolvedSides(a, b);
114 : }
115 : }
116 :
117 : PatchScript toPatchScript(
118 : Repository git, ObjectId baseId, String fileName, List<FixReplacement> fixReplacements)
119 : throws IOException, ResourceConflictException, ResourceNotFoundException {
120 2 : SidesResolver sidesResolver = new SidesResolver(git, ComparisonType.againstOtherPatchSet());
121 2 : PatchSide a = resolveSideA(git, sidesResolver, fileName, baseId);
122 2 : if (a.mode == FileMode.MISSING) {
123 2 : throw new ResourceNotFoundException(String.format("File %s not found", fileName));
124 : }
125 2 : FixCalculator.FixResult fixResult = FixCalculator.calculateFix(a.src, fixReplacements);
126 2 : PatchSide b =
127 : new PatchSide(
128 : null,
129 : fileName,
130 2 : ObjectId.zeroId(),
131 : a.mode,
132 2 : fixResult.text.getContent(),
133 : fixResult.text,
134 : a.mimeType,
135 : a.displayMethod,
136 : a.fileMode);
137 :
138 2 : PatchFileChange change =
139 : new PatchFileChange(
140 : fixResult.edits,
141 2 : ImmutableSet.of(),
142 2 : ImmutableList.of(),
143 : fileName,
144 : fileName,
145 : ChangeType.MODIFIED,
146 : PatchType.UNIFIED);
147 :
148 2 : return build(a, b, change);
149 : }
150 :
151 : private PatchSide resolveSideA(
152 : Repository git, SidesResolver sidesResolver, String path, ObjectId baseId)
153 : throws IOException {
154 2 : try (ObjectReader reader = git.newObjectReader()) {
155 2 : return sidesResolver.resolve(registry, reader, path, null, baseId, true);
156 : }
157 : }
158 :
159 : private PatchScript build(PatchSide a, PatchSide b, PatchFileChange content) {
160 14 : ImmutableList<Edit> contentEdits = content.getEdits();
161 14 : ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase();
162 :
163 14 : IntraLineDiffCalculatorResult intralineResult = IntraLineDiffCalculatorResult.NO_RESULT;
164 :
165 14 : if (isModify(content) && intralineDiffCalculator != null && isIntralineModeAllowed(b)) {
166 5 : intralineResult =
167 5 : intralineDiffCalculator.calculateIntraLineDiff(
168 : contentEdits, editsDueToRebase, a.id, b.id, a.src, b.src, b.treeId, b.path);
169 : }
170 14 : ImmutableList<Edit> finalEdits = intralineResult.edits.orElse(contentEdits);
171 14 : DiffContentCalculator calculator = new DiffContentCalculator(diffPrefs);
172 14 : DiffCalculatorResult diffCalculatorResult =
173 14 : calculator.calculateDiffContent(new TextSource(a.src), new TextSource(b.src), finalEdits);
174 :
175 14 : return new PatchScript(
176 14 : content.getChangeType(),
177 14 : content.getOldName(),
178 14 : content.getNewName(),
179 : a.fileMode,
180 : b.fileMode,
181 14 : content.getHeaderLines(),
182 : diffPrefs,
183 : diffCalculatorResult.diffContent.a,
184 : diffCalculatorResult.diffContent.b,
185 : diffCalculatorResult.edits,
186 : editsDueToRebase,
187 : a.displayMethod,
188 : b.displayMethod,
189 : a.mimeType,
190 : b.mimeType,
191 : intralineResult.failure,
192 : intralineResult.timeout,
193 14 : content.getPatchType() == Patch.PatchType.BINARY,
194 14 : a.treeId == null ? null : a.treeId.getName(),
195 14 : b.treeId == null ? null : b.treeId.getName());
196 : }
197 :
198 : private static boolean isModify(PatchFileChange content) {
199 14 : switch (content.getChangeType()) {
200 : case MODIFIED:
201 : case COPIED:
202 : case RENAMED:
203 : case REWRITE:
204 10 : return true;
205 :
206 : case ADDED:
207 : case DELETED:
208 : default:
209 8 : return false;
210 : }
211 : }
212 :
213 : @Nullable
214 : private static String oldName(PatchFileChange entry) {
215 12 : switch (entry.getChangeType()) {
216 : case ADDED:
217 8 : return null;
218 : case DELETED:
219 : case MODIFIED:
220 7 : return entry.getNewName();
221 : case COPIED:
222 : case RENAMED:
223 : case REWRITE:
224 : default:
225 4 : return entry.getOldName();
226 : }
227 : }
228 :
229 : @Nullable
230 : private static String newName(PatchFileChange entry) {
231 12 : switch (entry.getChangeType()) {
232 : case DELETED:
233 4 : return null;
234 : case ADDED:
235 : case MODIFIED:
236 : case COPIED:
237 : case RENAMED:
238 : case REWRITE:
239 : default:
240 12 : return entry.getNewName();
241 : }
242 : }
243 :
244 : private static boolean isIntralineModeAllowed(PatchSide side) {
245 : // The intraline diff cache keys are the same for these cases. It's better to not show
246 : // intraline results than showing completely wrong diffs or to run into a server error.
247 5 : return !Patch.isMagic(side.path) && !isSubmoduleCommit(side.mode);
248 : }
249 :
250 : private static boolean isSubmoduleCommit(FileMode mode) {
251 5 : return mode.getObjectType() == Constants.OBJ_COMMIT;
252 : }
253 :
254 : private static class PatchSide {
255 : final ObjectId treeId;
256 : final String path;
257 : final ObjectId id;
258 : final FileMode mode;
259 : final byte[] srcContent;
260 : final Text src;
261 : final String mimeType;
262 : final DisplayMethod displayMethod;
263 : final PatchScript.FileMode fileMode;
264 :
265 : private PatchSide(
266 : ObjectId treeId,
267 : String path,
268 : ObjectId id,
269 : FileMode mode,
270 : byte[] srcContent,
271 : Text src,
272 : String mimeType,
273 : DisplayMethod displayMethod,
274 14 : PatchScript.FileMode fileMode) {
275 14 : this.treeId = treeId;
276 14 : this.path = path;
277 14 : this.id = id;
278 14 : this.mode = mode;
279 14 : this.srcContent = srcContent;
280 14 : this.src = src;
281 14 : this.mimeType = mimeType;
282 14 : this.displayMethod = displayMethod;
283 14 : this.fileMode = fileMode;
284 14 : }
285 : }
286 :
287 : private static class ResolvedSides {
288 : // Not an @AutoValue because PatchSide can't be AutoValue
289 : public final PatchSide a;
290 : public final PatchSide b;
291 :
292 12 : ResolvedSides(PatchSide a, PatchSide b) {
293 12 : this.a = a;
294 12 : this.b = b;
295 12 : }
296 : }
297 :
298 : static class SidesResolver {
299 :
300 : private final Repository db;
301 : private final ComparisonType comparisonType;
302 :
303 14 : SidesResolver(Repository db, ComparisonType comparisonType) {
304 14 : this.db = db;
305 14 : this.comparisonType = comparisonType;
306 14 : }
307 :
308 : PatchSide resolve(
309 : final FileTypeRegistry registry,
310 : final ObjectReader reader,
311 : final String path,
312 : final PatchSide other,
313 : final ObjectId within,
314 : final boolean isWithinEqualsA)
315 : throws IOException {
316 : try {
317 14 : boolean isCommitMsg = Patch.COMMIT_MSG.equals(path);
318 14 : boolean isMergeList = Patch.MERGE_LIST.equals(path);
319 14 : if (isCommitMsg || isMergeList) {
320 5 : if (comparisonType.isAgainstParentOrAutoMerge() && isWithinEqualsA) {
321 0 : return createSide(
322 : within,
323 : path,
324 0 : ObjectId.zeroId(),
325 : FileMode.MISSING,
326 : Text.NO_BYTES,
327 : Text.EMPTY,
328 0 : MimeUtil2.UNKNOWN_MIME_TYPE.toString(),
329 : DisplayMethod.NONE,
330 : false);
331 : }
332 : Text src =
333 5 : isCommitMsg
334 4 : ? Text.forCommit(reader, within)
335 5 : : Text.forMergeList(comparisonType, reader, within);
336 5 : byte[] srcContent = src.getContent();
337 : DisplayMethod displayMethod;
338 : FileMode mode;
339 5 : if (src == Text.EMPTY) {
340 0 : mode = FileMode.MISSING;
341 0 : displayMethod = DisplayMethod.NONE;
342 : } else {
343 5 : mode = FileMode.REGULAR_FILE;
344 5 : displayMethod = DisplayMethod.DIFF;
345 : }
346 5 : return createSide(
347 : within,
348 : path,
349 : within,
350 : mode,
351 : srcContent,
352 : src,
353 5 : MimeUtil2.UNKNOWN_MIME_TYPE.toString(),
354 : displayMethod,
355 : false);
356 : }
357 14 : final TreeWalk tw = find(reader, path, within);
358 14 : ObjectId id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
359 14 : FileMode mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
360 14 : boolean reuse =
361 : other != null
362 11 : && other.id.equals(id)
363 14 : && (other.mode == mode || isBothFile(other.mode, mode));
364 14 : Text src = null;
365 : byte[] srcContent;
366 14 : if (reuse) {
367 4 : srcContent = other.srcContent;
368 : } else {
369 14 : srcContent = SrcContentResolver.getSourceContent(db, id, mode);
370 : }
371 14 : String mimeType = MimeUtil2.UNKNOWN_MIME_TYPE.toString();
372 14 : DisplayMethod displayMethod = DisplayMethod.DIFF;
373 14 : if (reuse) {
374 4 : mimeType = other.mimeType;
375 4 : displayMethod = other.displayMethod;
376 4 : src = other.src;
377 :
378 14 : } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) {
379 13 : MimeType registryMimeType = registry.getMimeType(path, srcContent);
380 13 : if ("image".equals(registryMimeType.getMediaType())
381 0 : && registry.isSafeInline(registryMimeType)) {
382 0 : displayMethod = DisplayMethod.IMG;
383 : }
384 13 : mimeType = registryMimeType.toString();
385 : }
386 14 : return createSide(within, path, id, mode, srcContent, src, mimeType, displayMethod, reuse);
387 :
388 0 : } catch (IOException err) {
389 0 : throw new IOException("Cannot read " + within.name() + ":" + path, err);
390 : }
391 : }
392 :
393 : private PatchSide createSide(
394 : ObjectId treeId,
395 : String path,
396 : ObjectId id,
397 : FileMode mode,
398 : byte[] srcContent,
399 : Text src,
400 : String mimeType,
401 : DisplayMethod displayMethod,
402 : boolean reuse) {
403 14 : if (!reuse) {
404 14 : if (srcContent == Text.NO_BYTES) {
405 10 : src = Text.EMPTY;
406 : } else {
407 14 : src = new Text(srcContent);
408 : }
409 : }
410 14 : if (mode == FileMode.MISSING) {
411 10 : displayMethod = DisplayMethod.NONE;
412 : }
413 14 : PatchScript.FileMode fileMode = PatchScript.FileMode.fromJgitFileMode(mode);
414 14 : return new PatchSide(
415 : treeId, path, id, mode, srcContent, src, mimeType, displayMethod, fileMode);
416 : }
417 :
418 : @Nullable
419 : private TreeWalk find(ObjectReader reader, String path, ObjectId within) throws IOException {
420 14 : if (path == null || within == null) {
421 8 : return null;
422 : }
423 13 : try (RevWalk rw = new RevWalk(reader)) {
424 13 : final RevTree tree = rw.parseTree(within);
425 13 : return TreeWalk.forPath(reader, path, tree);
426 : }
427 : }
428 : }
429 :
430 : private static boolean isBothFile(FileMode a, FileMode b) {
431 0 : return (a.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE
432 0 : && (b.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE;
433 : }
434 :
435 : static class IntraLineDiffCalculatorResult {
436 : // Not an @AutoValue because Edit is mutable
437 : final boolean failure;
438 : final boolean timeout;
439 : private final Optional<ImmutableList<Edit>> edits;
440 :
441 : private IntraLineDiffCalculatorResult(
442 14 : Optional<ImmutableList<Edit>> edits, boolean failure, boolean timeout) {
443 14 : this.failure = failure;
444 14 : this.timeout = timeout;
445 14 : this.edits = edits;
446 14 : }
447 :
448 14 : static final IntraLineDiffCalculatorResult NO_RESULT =
449 14 : new IntraLineDiffCalculatorResult(Optional.empty(), false, false);
450 14 : static final IntraLineDiffCalculatorResult FAILURE =
451 14 : new IntraLineDiffCalculatorResult(Optional.empty(), true, false);
452 14 : static final IntraLineDiffCalculatorResult TIMEOUT =
453 14 : new IntraLineDiffCalculatorResult(Optional.empty(), false, true);
454 :
455 : static IntraLineDiffCalculatorResult success(ImmutableList<Edit> edits) {
456 5 : return new IntraLineDiffCalculatorResult(Optional.of(edits), false, false);
457 : }
458 : }
459 :
460 : interface IntraLineDiffCalculator {
461 :
462 : IntraLineDiffCalculatorResult calculateIntraLineDiff(
463 : ImmutableList<Edit> edits,
464 : Set<Edit> editsDueToRebase,
465 : ObjectId aId,
466 : ObjectId bId,
467 : Text aSrc,
468 : Text bSrc,
469 : ObjectId bTreeId,
470 : String bPath);
471 : }
472 :
473 : static class PatchFileChange {
474 : private final ImmutableList<Edit> edits;
475 : private final ImmutableSet<Edit> editsDueToRebase;
476 : private final ImmutableList<String> headerLines;
477 : private final String oldName;
478 : private final String newName;
479 : private final ChangeType changeType;
480 : private final Patch.PatchType patchType;
481 :
482 : public PatchFileChange(
483 : ImmutableList<Edit> edits,
484 : ImmutableSet<Edit> editsDueToRebase,
485 : ImmutableList<String> headerLines,
486 : String oldName,
487 : String newName,
488 : ChangeType changeType,
489 14 : Patch.PatchType patchType) {
490 14 : this.edits = edits;
491 14 : this.editsDueToRebase = editsDueToRebase;
492 14 : this.headerLines = headerLines;
493 14 : this.oldName = oldName;
494 14 : this.newName = newName;
495 14 : this.changeType = changeType;
496 14 : this.patchType = patchType;
497 14 : }
498 :
499 : ImmutableList<Edit> getEdits() {
500 14 : return edits;
501 : }
502 :
503 : ImmutableSet<Edit> getEditsDueToRebase() {
504 14 : return editsDueToRebase;
505 : }
506 :
507 : ImmutableList<String> getHeaderLines() {
508 14 : return headerLines;
509 : }
510 :
511 : String getNewName() {
512 14 : return newName;
513 : }
514 :
515 : String getOldName() {
516 14 : return oldName;
517 : }
518 :
519 : ChangeType getChangeType() {
520 14 : return changeType;
521 : }
522 :
523 : Patch.PatchType getPatchType() {
524 14 : return patchType;
525 : }
526 : }
527 : }
|