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.comment;
16 :
17 : import static com.google.gerrit.entities.Patch.COMMIT_MSG;
18 : import static com.google.gerrit.entities.Patch.MERGE_LIST;
19 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
20 : import static java.util.stream.Collectors.groupingBy;
21 :
22 : import com.google.auto.value.AutoValue;
23 : import com.google.common.collect.ImmutableMap;
24 : import com.google.common.collect.Iterables;
25 : import com.google.common.flogger.FluentLogger;
26 : import com.google.gerrit.common.Nullable;
27 : import com.google.gerrit.common.data.PatchScript;
28 : import com.google.gerrit.entities.Comment;
29 : import com.google.gerrit.entities.CommentContext;
30 : import com.google.gerrit.entities.Project;
31 : import com.google.gerrit.extensions.common.ContextLineInfo;
32 : import com.google.gerrit.server.change.FileContentUtil;
33 : import com.google.gerrit.server.git.GitRepositoryManager;
34 : import com.google.gerrit.server.mime.FileTypeRegistry;
35 : import com.google.gerrit.server.patch.ComparisonType;
36 : import com.google.gerrit.server.patch.SrcContentResolver;
37 : import com.google.gerrit.server.patch.Text;
38 : import com.google.gerrit.server.project.ProjectCache;
39 : import com.google.gerrit.server.project.ProjectState;
40 : import com.google.inject.Inject;
41 : import com.google.inject.assistedinject.Assisted;
42 : import eu.medsea.mimeutil.MimeType;
43 : import eu.medsea.mimeutil.MimeUtil2;
44 : import java.io.IOException;
45 : import java.util.Collection;
46 : import java.util.List;
47 : import java.util.Map;
48 : import java.util.Optional;
49 : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
50 : import org.eclipse.jgit.errors.MissingObjectException;
51 : import org.eclipse.jgit.lib.ObjectId;
52 : import org.eclipse.jgit.lib.ObjectReader;
53 : import org.eclipse.jgit.lib.Repository;
54 : import org.eclipse.jgit.revwalk.RevCommit;
55 : import org.eclipse.jgit.revwalk.RevWalk;
56 : import org.eclipse.jgit.treewalk.TreeWalk;
57 :
58 : /**
59 : * Computes the list of {@link ContextLineInfo} for a given comment, that is, the lines of the
60 : * source file surrounding and including the area where the comment was written.
61 : */
62 : public class CommentContextLoader {
63 1 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
64 :
65 : private final FileTypeRegistry registry;
66 : private final GitRepositoryManager repoManager;
67 : private final Project.NameKey project;
68 : private final ProjectState projectState;
69 :
70 : public interface Factory {
71 : CommentContextLoader create(Project.NameKey project);
72 : }
73 :
74 : @Inject
75 : CommentContextLoader(
76 : FileTypeRegistry registry,
77 : GitRepositoryManager repoManager,
78 : ProjectCache projectCache,
79 1 : @Assisted Project.NameKey project) {
80 1 : this.registry = registry;
81 1 : this.repoManager = repoManager;
82 1 : this.project = project;
83 1 : projectState = projectCache.get(project).orElseThrow(illegalState(project));
84 1 : }
85 :
86 : /**
87 : * Load the comment context for multiple contextInputs at once. This method will open the
88 : * repository and read the source files for all necessary contextInputs' file paths.
89 : *
90 : * @param contextInputs a list of contextInputs.
91 : * @return a Map where all entries consist of the input contextInputs and the values are their
92 : * corresponding {@link CommentContext}.
93 : */
94 : public Map<ContextInput, CommentContext> getContext(Collection<ContextInput> contextInputs)
95 : throws IOException {
96 1 : ImmutableMap.Builder<ContextInput, CommentContext> result =
97 1 : ImmutableMap.builderWithExpectedSize(Iterables.size(contextInputs));
98 :
99 : // Group contextInputs by commit ID so that each commit is parsed only once
100 1 : Map<ObjectId, List<ContextInput>> commentsByCommitId =
101 1 : contextInputs.stream().collect(groupingBy(ContextInput::commitId));
102 :
103 1 : try (Repository repo = repoManager.openRepository(project);
104 1 : RevWalk rw = new RevWalk(repo)) {
105 1 : for (ObjectId commitId : commentsByCommitId.keySet()) {
106 : RevCommit commit;
107 : try {
108 1 : commit = rw.parseCommit(commitId);
109 1 : } catch (IncorrectObjectTypeException | MissingObjectException e) {
110 1 : logger.atWarning().log("Commit %s is missing or has an incorrect object type", commitId);
111 1 : commentsByCommitId
112 1 : .get(commitId)
113 1 : .forEach(contextInput -> result.put(contextInput, CommentContext.empty()));
114 1 : continue;
115 1 : }
116 1 : for (ContextInput contextInput : commentsByCommitId.get(commitId)) {
117 1 : Optional<Range> range = getStartAndEndLines(contextInput);
118 1 : if (!range.isPresent()) {
119 1 : result.put(contextInput, CommentContext.empty());
120 1 : continue;
121 : }
122 1 : String filePath = contextInput.filePath();
123 1 : switch (filePath) {
124 : case COMMIT_MSG:
125 1 : result.put(
126 : contextInput,
127 1 : getContextForCommitMessage(
128 1 : rw.getObjectReader(), commit, range.get(), contextInput.contextPadding()));
129 1 : break;
130 : case MERGE_LIST:
131 1 : result.put(
132 : contextInput,
133 1 : getContextForMergeList(
134 1 : rw.getObjectReader(), commit, range.get(), contextInput.contextPadding()));
135 1 : break;
136 : default:
137 1 : result.put(
138 : contextInput,
139 1 : getContextForFilePath(
140 1 : repo, rw, commit, filePath, range.get(), contextInput.contextPadding()));
141 : }
142 1 : }
143 1 : }
144 1 : return result.build();
145 : }
146 : }
147 :
148 : private CommentContext getContextForCommitMessage(
149 : ObjectReader reader, RevCommit commit, Range commentRange, int contextPadding)
150 : throws IOException {
151 1 : Text text = Text.forCommit(reader, commit);
152 1 : return createContext(
153 : text, commentRange, contextPadding, FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE);
154 : }
155 :
156 : private CommentContext getContextForMergeList(
157 : ObjectReader reader, RevCommit commit, Range commentRange, int contextPadding)
158 : throws IOException {
159 1 : ComparisonType cmp = ComparisonType.againstParent(1);
160 1 : Text text = Text.forMergeList(cmp, reader, commit);
161 1 : return createContext(
162 : text, commentRange, contextPadding, FileContentUtil.TEXT_X_GERRIT_MERGE_LIST);
163 : }
164 :
165 : private CommentContext getContextForFilePath(
166 : Repository repo,
167 : RevWalk rw,
168 : RevCommit commit,
169 : String filePath,
170 : Range commentRange,
171 : int contextPadding)
172 : throws IOException {
173 : // TODO(ghareeb): We can further group the comments by file paths to avoid opening
174 : // the same file multiple times.
175 1 : try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), filePath, commit.getTree())) {
176 1 : if (tw == null) {
177 0 : logger.atWarning().log(
178 0 : "Could not find path %s in the git tree of ID %s.", filePath, commit.getTree().getId());
179 0 : return CommentContext.empty();
180 : }
181 1 : ObjectId id = tw.getObjectId(0);
182 1 : byte[] sourceContent = SrcContentResolver.getSourceContent(repo, id, tw.getFileMode(0));
183 1 : Text textSrc = new Text(sourceContent);
184 1 : String contentType = getContentType(tw, filePath, textSrc);
185 1 : return createContext(textSrc, commentRange, contextPadding, contentType);
186 0 : }
187 : }
188 :
189 : private String getContentType(TreeWalk tw, String filePath, Text src) {
190 1 : PatchScript.FileMode fileMode = PatchScript.FileMode.fromJgitFileMode(tw.getFileMode(0));
191 1 : String mimeType = MimeUtil2.UNKNOWN_MIME_TYPE.toString();
192 1 : if (src.size() > 0 && PatchScript.FileMode.SYMLINK != fileMode) {
193 1 : MimeType registryMimeType = registry.getMimeType(filePath, src.getContent());
194 1 : mimeType = registryMimeType.toString();
195 : }
196 1 : return FileContentUtil.resolveContentType(projectState, filePath, fileMode, mimeType);
197 : }
198 :
199 : private static CommentContext createContext(
200 : Text src, Range commentRange, int contextPadding, String contentType) {
201 1 : if (commentRange.start() < 1 || commentRange.end() - 1 > src.size()) {
202 : // TODO(ghareeb): We should throw an exception in this case. See
203 : // https://bugs.chromium.org/p/gerrit/issues/detail?id=14102 which is an example where the
204 : // diff contains an extra line not in the original file.
205 1 : return CommentContext.empty();
206 : }
207 1 : commentRange = adjustRange(commentRange, contextPadding, src.size());
208 1 : ImmutableMap.Builder<Integer, String> context =
209 1 : ImmutableMap.builderWithExpectedSize(commentRange.end() - commentRange.start());
210 1 : for (int i = commentRange.start(); i < commentRange.end(); i++) {
211 1 : context.put(i, src.getString(i - 1));
212 : }
213 1 : return CommentContext.create(context.build(), contentType);
214 : }
215 :
216 : /**
217 : * Adjust the {@code commentRange} parameter by adding {@code contextPadding} lines before and
218 : * after the comment range.
219 : */
220 : private static Range adjustRange(Range commentRange, int contextPadding, int fileLines) {
221 1 : int newStartLine = commentRange.start() - contextPadding;
222 1 : int newEndLine = commentRange.end() + contextPadding;
223 1 : return Range.create(Math.max(1, newStartLine), Math.min(fileLines + 1, newEndLine));
224 : }
225 :
226 : private static Optional<Range> getStartAndEndLines(ContextInput comment) {
227 1 : if (comment.range() != null) {
228 1 : return Optional.of(Range.create(comment.range().startLine, comment.range().endLine + 1));
229 1 : } else if (comment.lineNumber() > 0) {
230 1 : return Optional.of(Range.create(comment.lineNumber(), comment.lineNumber() + 1));
231 : }
232 1 : return Optional.empty();
233 : }
234 :
235 : @AutoValue
236 1 : abstract static class Range {
237 : static Range create(int start, int end) {
238 1 : return new AutoValue_CommentContextLoader_Range(start, end);
239 : }
240 :
241 : /** Start line of the comment (inclusive). */
242 : abstract int start();
243 :
244 : /** End line of the comment (exclusive). */
245 : abstract int end();
246 :
247 : /** Number of lines covered by this range. */
248 : int size() {
249 0 : return end() - start();
250 : }
251 : }
252 :
253 : /** This entity only contains comment fields needed to load the comment context. */
254 : @AutoValue
255 1 : abstract static class ContextInput {
256 : static ContextInput fromComment(Comment comment, int contextPadding) {
257 1 : return new AutoValue_CommentContextLoader_ContextInput.Builder()
258 1 : .commitId(comment.getCommitId())
259 1 : .filePath(comment.key.filename)
260 1 : .range(comment.range)
261 1 : .lineNumber(comment.lineNbr)
262 1 : .contextPadding(contextPadding)
263 1 : .build();
264 : }
265 :
266 : /** 20 bytes SHA-1 of the patchset commit containing the file where the comment is written. */
267 : abstract ObjectId commitId();
268 :
269 : /** File path where the comment is written. */
270 : abstract String filePath();
271 :
272 : /**
273 : * Position of the comment in the file (start line, start char, end line, end char). This field
274 : * can be null if the range is not available for this comment.
275 : */
276 : @Nullable
277 : abstract Comment.Range range();
278 :
279 : /**
280 : * The 1-based line number where the comment is written. A value 0 means that the line number is
281 : * not available for this comment.
282 : */
283 : abstract Integer lineNumber();
284 :
285 : /** Number of extra lines of context that should be added before and after the comment range. */
286 : abstract Integer contextPadding();
287 :
288 : @AutoValue.Builder
289 1 : public abstract static class Builder {
290 :
291 : public abstract Builder commitId(ObjectId commitId);
292 :
293 : public abstract Builder filePath(String filePath);
294 :
295 : public abstract Builder range(@Nullable Comment.Range range);
296 :
297 : public abstract Builder lineNumber(Integer lineNumber);
298 :
299 : public abstract Builder contextPadding(Integer contextPadding);
300 :
301 : public abstract ContextInput build();
302 : }
303 : }
304 : }
|