Line data Source code
1 : // Copyright (C) 2013 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.restapi.change;
16 :
17 : import com.google.common.collect.Iterables;
18 : import com.google.common.collect.Lists;
19 : import com.google.common.flogger.FluentLogger;
20 : import com.google.common.hash.Hasher;
21 : import com.google.common.hash.Hashing;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.entities.Account;
24 : import com.google.gerrit.entities.Change;
25 : import com.google.gerrit.entities.PatchSet;
26 : import com.google.gerrit.entities.Project;
27 : import com.google.gerrit.extensions.api.GerritApi;
28 : import com.google.gerrit.extensions.common.FileInfo;
29 : import com.google.gerrit.extensions.registration.DynamicMap;
30 : import com.google.gerrit.extensions.restapi.AuthException;
31 : import com.google.gerrit.extensions.restapi.BadRequestException;
32 : import com.google.gerrit.extensions.restapi.CacheControl;
33 : import com.google.gerrit.extensions.restapi.ChildCollection;
34 : import com.google.gerrit.extensions.restapi.ETagView;
35 : import com.google.gerrit.extensions.restapi.IdString;
36 : import com.google.gerrit.extensions.restapi.Response;
37 : import com.google.gerrit.extensions.restapi.RestApiException;
38 : import com.google.gerrit.extensions.restapi.RestView;
39 : import com.google.gerrit.server.CurrentUser;
40 : import com.google.gerrit.server.PatchSetUtil;
41 : import com.google.gerrit.server.change.AccountPatchReviewStore;
42 : import com.google.gerrit.server.change.AccountPatchReviewStore.PatchSetWithReviewedFiles;
43 : import com.google.gerrit.server.change.FileInfoJson;
44 : import com.google.gerrit.server.change.FileResource;
45 : import com.google.gerrit.server.change.RevisionResource;
46 : import com.google.gerrit.server.git.GitRepositoryManager;
47 : import com.google.gerrit.server.patch.DiffNotAvailableException;
48 : import com.google.gerrit.server.patch.DiffOperations;
49 : import com.google.gerrit.server.patch.DiffOptions;
50 : import com.google.gerrit.server.patch.PatchListKey;
51 : import com.google.gerrit.server.patch.PatchListNotAvailableException;
52 : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
53 : import com.google.gerrit.server.permissions.PermissionBackendException;
54 : import com.google.gerrit.server.plugincontext.PluginItemContext;
55 : import com.google.inject.Inject;
56 : import com.google.inject.Provider;
57 : import com.google.inject.Singleton;
58 : import java.io.IOException;
59 : import java.util.ArrayList;
60 : import java.util.Collection;
61 : import java.util.Collections;
62 : import java.util.List;
63 : import java.util.Map;
64 : import java.util.Optional;
65 : import java.util.Set;
66 : import java.util.concurrent.TimeUnit;
67 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
68 : import org.eclipse.jgit.lib.ObjectId;
69 : import org.eclipse.jgit.lib.ObjectReader;
70 : import org.eclipse.jgit.lib.Repository;
71 : import org.eclipse.jgit.revwalk.RevCommit;
72 : import org.eclipse.jgit.revwalk.RevWalk;
73 : import org.eclipse.jgit.treewalk.TreeWalk;
74 : import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
75 : import org.kohsuke.args4j.Option;
76 :
77 : @Singleton
78 : public class Files implements ChildCollection<RevisionResource, FileResource> {
79 : private final DynamicMap<RestView<FileResource>> views;
80 : private final Provider<ListFiles> list;
81 :
82 : @Inject
83 145 : Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) {
84 145 : this.views = views;
85 145 : this.list = list;
86 145 : }
87 :
88 : @Override
89 : public DynamicMap<RestView<FileResource>> views() {
90 4 : return views;
91 : }
92 :
93 : @Override
94 : public RestView<RevisionResource> list() throws AuthException {
95 3 : return list.get();
96 : }
97 :
98 : @Override
99 : public FileResource parse(RevisionResource rev, IdString id) {
100 16 : return new FileResource(rev, id.get());
101 : }
102 :
103 : public static final class ListFiles implements ETagView<RevisionResource> {
104 79 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
105 :
106 : @Option(name = "--base", metaVar = "revision-id")
107 : String base;
108 :
109 : @Option(name = "--parent", metaVar = "parent-number")
110 : int parentNum;
111 :
112 : @Option(name = "--reviewed")
113 : boolean reviewed;
114 :
115 : @Option(name = "-q")
116 : String query;
117 :
118 : private final DiffOperations diffOperations;
119 : private final Provider<CurrentUser> self;
120 : private final FileInfoJson fileInfoJson;
121 : private final Revisions revisions;
122 : private final GitRepositoryManager gitManager;
123 : private final PatchSetUtil psUtil;
124 : private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
125 : private final GerritApi gApi;
126 :
127 : @Inject
128 : ListFiles(
129 : DiffOperations diffOperations,
130 : Provider<CurrentUser> self,
131 : FileInfoJson fileInfoJson,
132 : Revisions revisions,
133 : GitRepositoryManager gitManager,
134 : PatchSetUtil psUtil,
135 : PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
136 79 : GerritApi gApi) {
137 79 : this.diffOperations = diffOperations;
138 79 : this.self = self;
139 79 : this.fileInfoJson = fileInfoJson;
140 79 : this.revisions = revisions;
141 79 : this.gitManager = gitManager;
142 79 : this.psUtil = psUtil;
143 79 : this.accountPatchReviewStore = accountPatchReviewStore;
144 79 : this.gApi = gApi;
145 79 : }
146 :
147 : public ListFiles setReviewed(boolean r) {
148 1 : this.reviewed = r;
149 1 : return this;
150 : }
151 :
152 : @Override
153 : public Response<?> apply(RevisionResource resource)
154 : throws RestApiException, RepositoryNotFoundException, IOException,
155 : PatchListNotAvailableException, PermissionBackendException {
156 10 : checkOptions();
157 10 : if (reviewed) {
158 1 : return Response.ok(reviewed(resource));
159 10 : } else if (query != null) {
160 1 : return Response.ok(query(resource));
161 : }
162 :
163 : Response<Map<String, FileInfo>> r;
164 10 : if (base != null) {
165 3 : RevisionResource baseResource =
166 3 : revisions.parse(resource.getChangeResource(), IdString.fromDecoded(base));
167 3 : r =
168 3 : Response.ok(
169 3 : fileInfoJson.getFileInfoMap(
170 3 : resource.getChange(),
171 3 : resource.getPatchSet().commitId(),
172 3 : baseResource.getPatchSet()));
173 10 : } else if (parentNum != 0) {
174 5 : int parents =
175 5 : gApi.changes()
176 5 : .id(resource.getChange().getChangeId())
177 5 : .revision(resource.getPatchSet().id().get())
178 5 : .commit(false)
179 : .parents
180 5 : .size();
181 5 : if (parentNum < 0 || parentNum > parents) {
182 1 : throw new BadRequestException(String.format("invalid parent number: %d", parentNum));
183 : }
184 5 : r =
185 5 : Response.ok(
186 5 : fileInfoJson.getFileInfoMap(
187 5 : resource.getChange(), resource.getPatchSet().commitId(), parentNum));
188 5 : } else {
189 10 : r = Response.ok(fileInfoJson.getFileInfoMap(resource.getChange(), resource.getPatchSet()));
190 : }
191 :
192 10 : if (resource.isCacheable()) {
193 5 : r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
194 : }
195 10 : return r;
196 : }
197 :
198 : private void checkOptions() throws BadRequestException {
199 10 : int supplied = 0;
200 10 : if (base != null) {
201 3 : supplied++;
202 : }
203 10 : if (parentNum > 0) {
204 5 : supplied++;
205 : }
206 10 : if (reviewed) {
207 1 : supplied++;
208 : }
209 10 : if (query != null) {
210 1 : supplied++;
211 : }
212 10 : if (supplied > 1) {
213 0 : throw new BadRequestException("cannot combine base, parent, reviewed, query");
214 : }
215 10 : }
216 :
217 : private List<String> query(RevisionResource resource)
218 : throws RepositoryNotFoundException, IOException {
219 1 : Project.NameKey project = resource.getChange().getProject();
220 1 : try (Repository git = gitManager.openRepository(project);
221 1 : ObjectReader or = git.newObjectReader();
222 1 : RevWalk rw = new RevWalk(or);
223 1 : TreeWalk tw = new TreeWalk(or)) {
224 1 : RevCommit c = rw.parseCommit(resource.getPatchSet().commitId());
225 :
226 1 : tw.addTree(c.getTree());
227 1 : tw.setRecursive(true);
228 1 : List<String> paths = new ArrayList<>();
229 1 : while (tw.next() && paths.size() < 20) {
230 1 : String s = tw.getPathString();
231 1 : if (s.contains(query)) {
232 1 : paths.add(s);
233 : }
234 1 : }
235 1 : return paths;
236 : }
237 : }
238 :
239 : private Collection<String> reviewed(RevisionResource resource) throws AuthException {
240 1 : CurrentUser user = self.get();
241 1 : if (!user.isIdentifiedUser()) {
242 0 : throw new AuthException("Authentication required");
243 : }
244 :
245 1 : Account.Id userId = user.getAccountId();
246 1 : PatchSet patchSetId = resource.getPatchSet();
247 : Optional<PatchSetWithReviewedFiles> o;
248 1 : o = accountPatchReviewStore.call(s -> s.findReviewed(patchSetId.id(), userId));
249 :
250 1 : if (o.isPresent()) {
251 1 : PatchSetWithReviewedFiles res = o.get();
252 1 : if (res.patchSetId().equals(patchSetId.id())) {
253 1 : return res.files();
254 : }
255 :
256 : try {
257 1 : return copy(res.files(), res.patchSetId(), resource, userId);
258 0 : } catch (IOException | DiffNotAvailableException e) {
259 0 : logger.atWarning().withCause(e).log("Cannot copy patch review flags");
260 : }
261 : }
262 :
263 1 : return Collections.emptyList();
264 : }
265 :
266 : private List<String> copy(
267 : Set<String> paths, PatchSet.Id old, RevisionResource resource, Account.Id userId)
268 : throws IOException, DiffNotAvailableException {
269 1 : Project.NameKey project = resource.getChange().getProject();
270 1 : try (Repository git = gitManager.openRepository(project);
271 1 : ObjectReader reader = git.newObjectReader();
272 1 : RevWalk rw = new RevWalk(reader);
273 1 : TreeWalk tw = new TreeWalk(reader)) {
274 1 : Change change = resource.getChange();
275 1 : PatchSet patchSet = psUtil.get(resource.getNotes(), old);
276 1 : if (patchSet == null) {
277 0 : throw new DiffNotAvailableException(
278 0 : String.format(
279 0 : "patch set %s of change %s not found", old.get(), change.getId().get()));
280 : }
281 :
282 1 : Map<String, FileDiffOutput> oldList =
283 1 : diffOperations.listModifiedFilesAgainstParent(
284 1 : project, patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
285 :
286 1 : Map<String, FileDiffOutput> curList =
287 1 : diffOperations.listModifiedFilesAgainstParent(
288 : project,
289 1 : resource.getPatchSet().commitId(),
290 : /* parentNum= */ 0,
291 : DiffOptions.DEFAULTS);
292 :
293 1 : int sz = paths.size();
294 1 : List<String> pathList = Lists.newArrayListWithCapacity(sz);
295 :
296 1 : tw.setFilter(PathFilterGroup.createFromStrings(paths));
297 1 : tw.setRecursive(true);
298 1 : int o = tw.addTree(rw.parseCommit(getNewId(oldList)).getTree());
299 1 : int c = tw.addTree(rw.parseCommit(getNewId(curList)).getTree());
300 :
301 1 : int op = -1;
302 1 : if (getOldId(oldList) != null) {
303 1 : op = tw.addTree(rw.parseTree(getOldId(oldList)));
304 : }
305 :
306 1 : int cp = -1;
307 1 : if (getOldId(curList) != null) {
308 1 : cp = tw.addTree(rw.parseTree(getOldId(curList)));
309 : }
310 :
311 1 : while (tw.next()) {
312 1 : String path = tw.getPathString();
313 1 : if (tw.getRawMode(o) != 0
314 1 : && tw.getRawMode(c) != 0
315 1 : && tw.idEqual(o, c)
316 0 : && paths.contains(path)) {
317 : // File exists in previously reviewed oldList and in curList.
318 : // File content is identical.
319 0 : pathList.add(path);
320 1 : } else if (op >= 0
321 : && cp >= 0
322 1 : && tw.getRawMode(o) == 0
323 0 : && tw.getRawMode(c) == 0
324 0 : && tw.getRawMode(op) != 0
325 0 : && tw.getRawMode(cp) != 0
326 0 : && tw.idEqual(op, cp)
327 0 : && paths.contains(path)) {
328 : // File was deleted in previously reviewed oldList and curList.
329 : // File exists in ancestor of oldList and curList.
330 : // File content is identical in ancestors.
331 0 : pathList.add(path);
332 : }
333 1 : }
334 :
335 1 : accountPatchReviewStore.run(
336 1 : s -> s.markReviewed(resource.getPatchSet().id(), userId, pathList));
337 1 : return pathList;
338 : }
339 : }
340 :
341 : public ListFiles setQuery(String query) {
342 1 : this.query = query;
343 1 : return this;
344 : }
345 :
346 : public ListFiles setBase(@Nullable String base) {
347 7 : this.base = base;
348 7 : return this;
349 : }
350 :
351 : public ListFiles setParent(int parentNum) {
352 4 : this.parentNum = parentNum;
353 4 : return this;
354 : }
355 :
356 : @Override
357 : public String getETag(RevisionResource resource) {
358 2 : Hasher h = Hashing.murmur3_128().newHasher();
359 2 : resource.prepareETag(h, resource.getUser());
360 : // File list comes from the PatchListCache, so any change to the key or value should
361 : // invalidate ETag.
362 2 : h.putLong(PatchListKey.serialVersionUID);
363 2 : return h.hash().toString();
364 : }
365 :
366 : @Nullable
367 : private ObjectId getOldId(Map<String, FileDiffOutput> fileDiffList) {
368 1 : return fileDiffList.isEmpty()
369 0 : ? null
370 1 : : Iterables.getFirst(fileDiffList.values(), null).oldCommitId();
371 : }
372 :
373 : @Nullable
374 : private ObjectId getNewId(Map<String, FileDiffOutput> fileDiffList) {
375 1 : return fileDiffList.isEmpty()
376 0 : ? null
377 1 : : Iterables.getFirst(fileDiffList.values(), null).newCommitId();
378 : }
379 : }
380 : }
|