Line data Source code
1 : // Copyright (C) 2019 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 com.google.common.collect.ImmutableList;
18 : import com.google.gerrit.extensions.client.DiffPreferencesInfo;
19 : import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
20 : import com.google.gerrit.jgit.diff.ReplaceEdit;
21 : import com.google.gerrit.prettify.common.EditHunk;
22 : import com.google.gerrit.prettify.common.SparseFileContent;
23 : import com.google.gerrit.prettify.common.SparseFileContentBuilder;
24 : import java.util.List;
25 : import java.util.Optional;
26 : import org.eclipse.jgit.diff.Edit;
27 :
28 : /** Collects all lines and their content to be displayed in diff view. */
29 : class DiffContentCalculator {
30 : private final DiffPreferencesInfo diffPrefs;
31 :
32 14 : DiffContentCalculator(DiffPreferencesInfo diffPrefs) {
33 14 : this.diffPrefs = diffPrefs;
34 14 : }
35 :
36 : /**
37 : * Gather information necessary to display line-by-line difference between 2 texts.
38 : *
39 : * <p>The method returns instance of {@link DiffCalculatorResult} with the following data:
40 : *
41 : * <ul>
42 : * <li>All changed lines
43 : * <li>Additional lines to be displayed above and below the changed lines
44 : * <li>All changed and unchanged lines with comments
45 : * <li>Additional lines to be displayed above and below lines with commentsEdits with special
46 : * "fake" edits for unchanged lines with comments
47 : * </ul>
48 : *
49 : * <p>More details can be found in {@link DiffCalculatorResult}.
50 : *
51 : * @param srcA Original text content
52 : * @param srcB New text content
53 : * @param edits List of edits which was applied to srcA to produce srcB
54 : * @return an instance of {@link DiffCalculatorResult}.
55 : */
56 : DiffCalculatorResult calculateDiffContent(
57 : TextSource srcA, TextSource srcB, ImmutableList<Edit> edits) {
58 14 : if (srcA.src == srcB.src && edits.isEmpty()) {
59 : // Odd special case; the files are identical (100% rename or copy)
60 : // and the user has asked for context that is larger than the file.
61 : // Send them the entire file, with an empty edit after the last line.
62 : //
63 4 : SparseFileContentBuilder diffA = new SparseFileContentBuilder(srcA.size());
64 4 : for (int i = 0; i < srcA.size(); i++) {
65 4 : srcA.copyLineTo(diffA, i);
66 : }
67 4 : DiffContent diffContent =
68 4 : new DiffContent(diffA.build(), SparseFileContent.create(ImmutableList.of(), srcB.size()));
69 4 : Edit emptyEdit = new Edit(srcA.size(), srcA.size());
70 4 : return new DiffCalculatorResult(diffContent, ImmutableList.of(emptyEdit));
71 : }
72 14 : ImmutableList<Edit> sortedEdits = correctForDifferencesInNewlineAtEnd(srcA, srcB, edits);
73 :
74 14 : DiffContent diffContent =
75 14 : packContent(srcA, srcB, diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE, sortedEdits);
76 14 : return new DiffCalculatorResult(diffContent, sortedEdits);
77 : }
78 :
79 : private ImmutableList<Edit> correctForDifferencesInNewlineAtEnd(
80 : TextSource a, TextSource b, ImmutableList<Edit> edits) {
81 : // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it.
82 14 : int aSize = a.src.size();
83 14 : int bSize = b.src.size();
84 :
85 14 : if (edits.isEmpty() && (aSize == 0 || bSize == 0)) {
86 : // The diff was requested for a file which was either added or deleted but which JGit doesn't
87 : // consider a file addition/deletion (e.g. requesting a diff for the old file name of a
88 : // renamed file looks like a deletion).
89 2 : return edits;
90 : }
91 :
92 14 : if (edits.isEmpty() && (aSize != bSize)) {
93 : // Only edits due to rebase were present. If we now added the edits for the newlines, the
94 : // code which later assembles the file contents would fail.
95 2 : return edits;
96 : }
97 :
98 14 : Optional<Edit> lastEdit = getLast(edits);
99 14 : if (isNewlineAtEndDeleted(a, b)) {
100 3 : Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
101 :
102 3 : if (lastLineEdit.isPresent()) {
103 3 : Edit edit = lastLineEdit.get();
104 : Edit updatedLastLineEdit =
105 3 : edit instanceof ReplaceEdit
106 1 : ? new ReplaceEdit(
107 1 : edit.getBeginA(),
108 1 : edit.getEndA() + 1,
109 1 : edit.getBeginB(),
110 1 : edit.getEndB(),
111 1 : ((ReplaceEdit) edit).getInternalEdits())
112 3 : : new Edit(edit.getBeginA(), edit.getEndA() + 1, edit.getBeginB(), edit.getEndB());
113 :
114 3 : ImmutableList.Builder<Edit> newEditsBuilder =
115 3 : ImmutableList.builderWithExpectedSize(edits.size());
116 3 : return newEditsBuilder
117 3 : .addAll(edits.subList(0, edits.size() - 1))
118 3 : .add(updatedLastLineEdit)
119 3 : .build();
120 : }
121 2 : ImmutableList.Builder<Edit> newEditsBuilder =
122 2 : ImmutableList.builderWithExpectedSize(edits.size() + 1);
123 2 : Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize);
124 2 : return newEditsBuilder.addAll(edits).add(newlineEdit).build();
125 :
126 14 : } else if (isNewlineAtEndAdded(a, b)) {
127 7 : Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize);
128 7 : if (lastLineEdit.isPresent()) {
129 7 : Edit edit = lastLineEdit.get();
130 : Edit updatedLastLineEdit =
131 7 : edit instanceof ReplaceEdit
132 3 : ? new ReplaceEdit(
133 3 : edit.getBeginA(),
134 3 : edit.getEndA(),
135 3 : edit.getBeginB(),
136 3 : edit.getEndB() + 1,
137 3 : ((ReplaceEdit) edit).getInternalEdits())
138 7 : : new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB() + 1);
139 :
140 7 : ImmutableList.Builder<Edit> newEditsBuilder =
141 7 : ImmutableList.builderWithExpectedSize(edits.size());
142 7 : return newEditsBuilder
143 7 : .addAll(edits.subList(0, edits.size() - 1))
144 7 : .add(updatedLastLineEdit)
145 7 : .build();
146 : }
147 2 : ImmutableList.Builder<Edit> newEditsBuilder =
148 2 : ImmutableList.builderWithExpectedSize(edits.size() + 1);
149 2 : Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1);
150 2 : return newEditsBuilder.addAll(edits).add(newlineEdit).build();
151 : }
152 12 : return edits;
153 : }
154 :
155 : private static <T> Optional<T> getLast(List<T> list) {
156 14 : return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1));
157 : }
158 :
159 : private boolean isNewlineAtEndDeleted(TextSource a, TextSource b) {
160 14 : return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd();
161 : }
162 :
163 : private boolean isNewlineAtEndAdded(TextSource a, TextSource b) {
164 14 : return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd();
165 : }
166 :
167 : private DiffContent packContent(
168 : TextSource a, TextSource b, boolean ignoredWhitespace, ImmutableList<Edit> edits) {
169 14 : SparseFileContentBuilder diffA = new SparseFileContentBuilder(a.size());
170 14 : SparseFileContentBuilder diffB = new SparseFileContentBuilder(b.size());
171 14 : if (!edits.isEmpty()) {
172 14 : EditHunk hunk = new EditHunk(edits, a.size(), b.size());
173 14 : while (hunk.next()) {
174 14 : if (hunk.isUnmodifiedLine()) {
175 6 : String lineA = a.getSourceLine(hunk.getCurA());
176 6 : diffA.addLine(hunk.getCurA(), lineA);
177 :
178 6 : if (ignoredWhitespace) {
179 : // If we ignored whitespace in some form, also get the line
180 : // from b when it does not exactly match the line from a.
181 : //
182 3 : String lineB = b.getSourceLine(hunk.getCurB());
183 3 : if (!lineA.equals(lineB)) {
184 0 : diffB.addLine(hunk.getCurB(), lineB);
185 : }
186 : }
187 6 : hunk.incBoth();
188 6 : continue;
189 : }
190 :
191 14 : if (hunk.isDeletedA()) {
192 10 : a.copyLineTo(diffA, hunk.getCurA());
193 10 : hunk.incA();
194 : }
195 :
196 14 : if (hunk.isInsertedB()) {
197 14 : b.copyLineTo(diffB, hunk.getCurB());
198 14 : hunk.incB();
199 : }
200 : }
201 : }
202 14 : return new DiffContent(diffA.build(), diffB.build());
203 : }
204 :
205 : /** Contains information to be displayed in line-by-line diff view. */
206 : static class DiffCalculatorResult {
207 : // This class is not @AutoValue, because Edit is mutable
208 :
209 : /** Lines to be displayed */
210 : final DiffContent diffContent;
211 : /** List of edits including "fake" edits for unchanged lines with comments. */
212 : final ImmutableList<Edit> edits;
213 :
214 14 : DiffCalculatorResult(DiffContent diffContent, ImmutableList<Edit> edits) {
215 14 : this.diffContent = diffContent;
216 14 : this.edits = edits;
217 14 : }
218 : }
219 :
220 : /** Lines to be displayed in line-by-line diff view. */
221 : static class DiffContent {
222 : /* All lines from the original text (i.e. srcA) to be displayed. */
223 : final SparseFileContent a;
224 : /**
225 : * All lines from the new text (i.e. srcB) which are different than in original text. Lines are:
226 : * a) All changed lines (i.e. if the content of the line was replaced with the new line) b) All
227 : * inserted lines Note, that deleted lines are added to the a and are not added to b
228 : */
229 : final SparseFileContent b;
230 :
231 14 : DiffContent(SparseFileContent a, SparseFileContent b) {
232 14 : this.a = a;
233 14 : this.b = b;
234 14 : }
235 : }
236 :
237 : static class TextSource {
238 : final Text src;
239 :
240 14 : TextSource(Text src) {
241 14 : this.src = src;
242 14 : }
243 :
244 : int size() {
245 14 : if (src == null) {
246 0 : return 0;
247 : }
248 14 : if (src.isMissingNewlineAtEnd()) {
249 14 : return src.size();
250 : }
251 8 : return src.size() + 1;
252 : }
253 :
254 : void copyLineTo(SparseFileContentBuilder target, int lineNumber) {
255 14 : target.addLine(lineNumber, getSourceLine(lineNumber));
256 14 : }
257 :
258 : private String getSourceLine(int lineNumber) {
259 14 : return lineNumber >= src.size() ? "" : src.getString(lineNumber);
260 : }
261 : }
262 : }
|