Line data Source code
1 : // Copyright (C) 2018 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.change;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
19 : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES;
20 : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
21 : import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
22 : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
23 : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
24 : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
25 : import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
26 : import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
27 : import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
28 : import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
29 : import static com.google.gerrit.server.CommonConverters.toGitPerson;
30 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
31 :
32 : import com.google.common.collect.ImmutableList;
33 : import com.google.common.collect.ImmutableSet;
34 : import com.google.common.flogger.FluentLogger;
35 : import com.google.gerrit.common.Nullable;
36 : import com.google.gerrit.entities.Change;
37 : import com.google.gerrit.entities.Patch;
38 : import com.google.gerrit.entities.PatchSet;
39 : import com.google.gerrit.entities.Project;
40 : import com.google.gerrit.extensions.client.ListChangesOption;
41 : import com.google.gerrit.extensions.common.ChangeInfo;
42 : import com.google.gerrit.extensions.common.CommitInfo;
43 : import com.google.gerrit.extensions.common.FetchInfo;
44 : import com.google.gerrit.extensions.common.PushCertificateInfo;
45 : import com.google.gerrit.extensions.common.RevisionInfo;
46 : import com.google.gerrit.extensions.common.WebLinkInfo;
47 : import com.google.gerrit.extensions.config.DownloadCommand;
48 : import com.google.gerrit.extensions.config.DownloadScheme;
49 : import com.google.gerrit.extensions.registration.DynamicMap;
50 : import com.google.gerrit.extensions.registration.Extension;
51 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
52 : import com.google.gerrit.server.AnonymousUser;
53 : import com.google.gerrit.server.CurrentUser;
54 : import com.google.gerrit.server.GpgException;
55 : import com.google.gerrit.server.IdentifiedUser;
56 : import com.google.gerrit.server.WebLinks;
57 : import com.google.gerrit.server.account.AccountLoader;
58 : import com.google.gerrit.server.account.GpgApiAdapter;
59 : import com.google.gerrit.server.git.GitRepositoryManager;
60 : import com.google.gerrit.server.git.MergeUtilFactory;
61 : import com.google.gerrit.server.patch.PatchListNotAvailableException;
62 : import com.google.gerrit.server.permissions.ChangePermission;
63 : import com.google.gerrit.server.permissions.PermissionBackend;
64 : import com.google.gerrit.server.permissions.PermissionBackendException;
65 : import com.google.gerrit.server.project.ProjectCache;
66 : import com.google.gerrit.server.project.ProjectState;
67 : import com.google.gerrit.server.query.change.ChangeData;
68 : import com.google.inject.Inject;
69 : import com.google.inject.Provider;
70 : import com.google.inject.assistedinject.Assisted;
71 : import java.io.IOException;
72 : import java.util.ArrayList;
73 : import java.util.LinkedHashMap;
74 : import java.util.Map;
75 : import java.util.Optional;
76 : import org.eclipse.jgit.lib.ObjectId;
77 : import org.eclipse.jgit.lib.Ref;
78 : import org.eclipse.jgit.lib.Repository;
79 : import org.eclipse.jgit.revwalk.RevCommit;
80 : import org.eclipse.jgit.revwalk.RevWalk;
81 :
82 : /** Produces {@link RevisionInfo} and {@link CommitInfo} which are serialized to JSON afterwards. */
83 : public class RevisionJson {
84 103 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
85 :
86 : public interface Factory {
87 : RevisionJson create(Iterable<ListChangesOption> options);
88 : }
89 :
90 : private final MergeUtilFactory mergeUtilFactory;
91 : private final IdentifiedUser.GenericFactory userFactory;
92 : private final FileInfoJson fileInfoJson;
93 : private final GpgApiAdapter gpgApi;
94 : private final ChangeResource.Factory changeResourceFactory;
95 : private final ChangeKindCache changeKindCache;
96 : private final ActionJson actionJson;
97 : private final DynamicMap<DownloadScheme> downloadSchemes;
98 : private final DynamicMap<DownloadCommand> downloadCommands;
99 : private final WebLinks webLinks;
100 : private final Provider<CurrentUser> userProvider;
101 : private final ProjectCache projectCache;
102 : private final ImmutableSet<ListChangesOption> options;
103 : private final AccountLoader.Factory accountLoaderFactory;
104 : private final AnonymousUser anonymous;
105 : private final GitRepositoryManager repoManager;
106 : private final PermissionBackend permissionBackend;
107 :
108 : @Inject
109 : RevisionJson(
110 : Provider<CurrentUser> userProvider,
111 : AnonymousUser anonymous,
112 : ProjectCache projectCache,
113 : IdentifiedUser.GenericFactory userFactory,
114 : MergeUtilFactory mergeUtilFactory,
115 : FileInfoJson fileInfoJson,
116 : AccountLoader.Factory accountLoaderFactory,
117 : DynamicMap<DownloadScheme> downloadSchemes,
118 : DynamicMap<DownloadCommand> downloadCommands,
119 : WebLinks webLinks,
120 : ActionJson actionJson,
121 : GpgApiAdapter gpgApi,
122 : ChangeResource.Factory changeResourceFactory,
123 : ChangeKindCache changeKindCache,
124 : GitRepositoryManager repoManager,
125 : PermissionBackend permissionBackend,
126 103 : @Assisted Iterable<ListChangesOption> options) {
127 103 : this.userProvider = userProvider;
128 103 : this.anonymous = anonymous;
129 103 : this.projectCache = projectCache;
130 103 : this.userFactory = userFactory;
131 103 : this.mergeUtilFactory = mergeUtilFactory;
132 103 : this.fileInfoJson = fileInfoJson;
133 103 : this.accountLoaderFactory = accountLoaderFactory;
134 103 : this.downloadSchemes = downloadSchemes;
135 103 : this.downloadCommands = downloadCommands;
136 103 : this.webLinks = webLinks;
137 103 : this.actionJson = actionJson;
138 103 : this.gpgApi = gpgApi;
139 103 : this.changeResourceFactory = changeResourceFactory;
140 103 : this.changeKindCache = changeKindCache;
141 103 : this.permissionBackend = permissionBackend;
142 103 : this.repoManager = repoManager;
143 103 : this.options = ImmutableSet.copyOf(options);
144 103 : }
145 :
146 : /**
147 : * Returns a {@link RevisionInfo} based on a change and patch set. Reads from the repository
148 : * depending on the options provided when constructing this instance.
149 : */
150 : public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
151 : throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
152 99 : AccountLoader accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
153 99 : try (Repository repo = openRepoIfNecessary(cd.project());
154 99 : RevWalk rw = newRevWalk(repo)) {
155 99 : RevisionInfo rev = toRevisionInfo(accountLoader, cd, in, repo, rw, true, null);
156 99 : accountLoader.fill();
157 99 : return rev;
158 : }
159 : }
160 :
161 : /**
162 : * Returns a {@link CommitInfo} based on a commit and formatting options. Uses the provided
163 : * RevWalk and assumes it is backed by an open repository.
164 : */
165 : public CommitInfo getCommitInfo(
166 : Project.NameKey project,
167 : RevWalk rw,
168 : RevCommit commit,
169 : boolean addLinks,
170 : boolean fillCommit,
171 : String branchName,
172 : String changeKey)
173 : throws IOException {
174 103 : CommitInfo info = new CommitInfo();
175 103 : if (fillCommit) {
176 99 : info.commit = commit.name();
177 : }
178 103 : info.parents = new ArrayList<>(commit.getParentCount());
179 103 : info.author = toGitPerson(commit.getAuthorIdent());
180 103 : info.committer = toGitPerson(commit.getCommitterIdent());
181 103 : info.subject = commit.getShortMessage();
182 103 : info.message = commit.getFullMessage();
183 :
184 103 : if (addLinks) {
185 103 : ImmutableList<WebLinkInfo> patchSetLinks =
186 103 : webLinks.getPatchSetLinks(
187 103 : project, commit.name(), commit.getFullMessage(), branchName, changeKey);
188 103 : info.webLinks = patchSetLinks.isEmpty() ? null : patchSetLinks;
189 103 : ImmutableList<WebLinkInfo> resolveConflictsLinks =
190 103 : webLinks.getResolveConflictsLinks(
191 103 : project, commit.name(), commit.getFullMessage(), branchName);
192 103 : info.resolveConflictsWebLinks =
193 103 : resolveConflictsLinks.isEmpty() ? null : resolveConflictsLinks;
194 : }
195 :
196 103 : for (RevCommit parent : commit.getParents()) {
197 98 : rw.parseBody(parent);
198 98 : CommitInfo i = new CommitInfo();
199 98 : i.commit = parent.name();
200 98 : i.subject = parent.getShortMessage();
201 98 : if (addLinks) {
202 98 : ImmutableList<WebLinkInfo> parentLinks =
203 98 : webLinks.getParentLinks(project, parent.name(), parent.getFullMessage(), branchName);
204 98 : i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
205 : }
206 98 : info.parents.add(i);
207 : }
208 103 : return info;
209 : }
210 :
211 : /**
212 : * Returns multiple {@link RevisionInfo}s for a single change. Uses the provided {@link
213 : * AccountLoader} to lazily populate accounts. Callers have to call {@link AccountLoader#fill()}
214 : * afterwards to populate all accounts in the returned {@link RevisionInfo}s.
215 : */
216 : Map<String, RevisionInfo> getRevisions(
217 : AccountLoader accountLoader,
218 : ChangeData cd,
219 : Map<PatchSet.Id, PatchSet> map,
220 : Optional<PatchSet.Id> limitToPsId,
221 : ChangeInfo changeInfo)
222 : throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
223 103 : Map<String, RevisionInfo> res = new LinkedHashMap<>();
224 103 : try (Repository repo = openRepoIfNecessary(cd.project());
225 103 : RevWalk rw = newRevWalk(repo)) {
226 103 : for (PatchSet in : map.values()) {
227 103 : PatchSet.Id id = in.id();
228 : boolean want;
229 103 : if (has(ALL_REVISIONS)) {
230 103 : want = true;
231 28 : } else if (limitToPsId.isPresent()) {
232 3 : want = id.equals(limitToPsId.get());
233 : } else {
234 26 : want = id.equals(cd.change().currentPatchSetId());
235 : }
236 103 : if (want) {
237 103 : res.put(
238 103 : in.commitId().name(),
239 103 : toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
240 : }
241 103 : }
242 103 : return res;
243 : }
244 : }
245 :
246 : private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
247 : throws PermissionBackendException {
248 103 : Map<String, FetchInfo> r = new LinkedHashMap<>();
249 103 : for (Extension<DownloadScheme> e : downloadSchemes) {
250 0 : String schemeName = e.getExportName();
251 0 : DownloadScheme scheme = e.getProvider().get();
252 0 : if (!scheme.isEnabled()
253 0 : || (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
254 0 : continue;
255 : }
256 0 : if (!scheme.isAuthSupported() && !isWorldReadable(cd)) {
257 0 : continue;
258 : }
259 :
260 0 : String projectName = cd.project().get();
261 0 : String url = scheme.getUrl(projectName);
262 0 : String refName = in.refName();
263 0 : FetchInfo fetchInfo = new FetchInfo(url, refName);
264 0 : r.put(schemeName, fetchInfo);
265 :
266 0 : if (has(DOWNLOAD_COMMANDS)) {
267 0 : DownloadCommandsJson.populateFetchMap(
268 : scheme, downloadCommands, projectName, refName, fetchInfo);
269 : }
270 0 : }
271 :
272 103 : return r;
273 : }
274 :
275 : private RevisionInfo toRevisionInfo(
276 : AccountLoader accountLoader,
277 : ChangeData cd,
278 : PatchSet in,
279 : @Nullable Repository repo,
280 : @Nullable RevWalk rw,
281 : boolean fillCommit,
282 : @Nullable ChangeInfo changeInfo)
283 : throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
284 103 : Change c = cd.change();
285 103 : RevisionInfo out = new RevisionInfo();
286 103 : out.isCurrent = in.id().equals(c.currentPatchSetId());
287 103 : out._number = in.id().get();
288 103 : out.ref = in.refName();
289 103 : out.setCreated(in.createdOn());
290 103 : out.uploader = accountLoader.get(in.uploader());
291 103 : out.fetch = makeFetchMap(cd, in);
292 103 : out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
293 103 : out.description = in.description().orElse(null);
294 :
295 103 : boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT));
296 103 : boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
297 103 : if (setCommit || addFooters) {
298 103 : checkState(rw != null);
299 103 : checkState(repo != null);
300 103 : Project.NameKey project = c.getProject();
301 103 : String rev = in.commitId().name();
302 103 : RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
303 103 : rw.parseBody(commit);
304 103 : String branchName = cd.change().getDest().branch();
305 103 : if (setCommit) {
306 103 : out.commit =
307 103 : getCommitInfo(
308 103 : project, rw, commit, has(WEB_LINKS), fillCommit, branchName, c.getKey().get());
309 : }
310 103 : if (addFooters) {
311 103 : Ref ref = repo.exactRef(branchName);
312 103 : RevCommit mergeTip = null;
313 103 : if (ref != null) {
314 98 : mergeTip = rw.parseCommit(ref.getObjectId());
315 98 : rw.parseBody(mergeTip);
316 : }
317 103 : out.commitWithFooters =
318 : mergeUtilFactory
319 103 : .create(projectCache.get(project).orElseThrow(illegalState(project)))
320 103 : .createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.id());
321 : }
322 : }
323 :
324 103 : if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
325 : try {
326 103 : out.files = fileInfoJson.getFileInfoMap(c, in);
327 103 : out.files.remove(Patch.COMMIT_MSG);
328 103 : out.files.remove(Patch.MERGE_LIST);
329 0 : } catch (ResourceConflictException e) {
330 0 : logger.atWarning().withCause(e).log("creating file list failed");
331 103 : }
332 : }
333 :
334 103 : if (out.isCurrent && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
335 57 : actionJson.addRevisionActions(
336 : changeInfo,
337 : out,
338 57 : new RevisionResource(changeResourceFactory.create(cd, userProvider.get()), in));
339 : }
340 :
341 103 : if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
342 3 : if (in.pushCertificate().isPresent()) {
343 0 : out.pushCertificate =
344 0 : gpgApi.checkPushCertificate(
345 0 : in.pushCertificate().get(), userFactory.create(in.uploader()));
346 : } else {
347 3 : out.pushCertificate = new PushCertificateInfo();
348 : }
349 : }
350 :
351 103 : return out;
352 : }
353 :
354 : private boolean has(ListChangesOption option) {
355 103 : return options.contains(option);
356 : }
357 :
358 : private boolean isWorldReadable(ChangeData cd) throws PermissionBackendException {
359 0 : if (!permissionBackend.user(anonymous).change(cd).test(ChangePermission.READ)) {
360 0 : return false;
361 : }
362 0 : ProjectState projectState =
363 0 : projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
364 0 : return projectState.statePermitsRead();
365 : }
366 :
367 : @Nullable
368 : private Repository openRepoIfNecessary(Project.NameKey project) throws IOException {
369 103 : if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
370 103 : return repoManager.openRepository(project);
371 : }
372 25 : return null;
373 : }
374 :
375 : @Nullable
376 : private RevWalk newRevWalk(@Nullable Repository repo) {
377 103 : return repo != null ? new RevWalk(repo) : null;
378 : }
379 : }
|