| // Copyright (C) 2012 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.change; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES; |
| import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CHECK; |
| import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; |
| import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.LABELS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES; |
| import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES; |
| import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED; |
| import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES; |
| import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE; |
| import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS; |
| import static com.google.gerrit.server.CommonConverters.toGitPerson; |
| import static java.util.stream.Collectors.toList; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.FluentIterable; |
| import com.google.common.collect.HashBasedTable; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.LinkedHashMultimap; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.MultimapBuilder; |
| import com.google.common.collect.SetMultimap; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.Table; |
| import com.google.common.primitives.Ints; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.data.LabelType; |
| import com.google.gerrit.common.data.LabelTypes; |
| import com.google.gerrit.common.data.LabelValue; |
| import com.google.gerrit.common.data.SubmitRecord; |
| import com.google.gerrit.common.data.SubmitTypeRecord; |
| import com.google.gerrit.extensions.api.changes.FixInput; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.client.ReviewerState; |
| import com.google.gerrit.extensions.common.AccountInfo; |
| import com.google.gerrit.extensions.common.ApprovalInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.ChangeMessageInfo; |
| import com.google.gerrit.extensions.common.CommitInfo; |
| import com.google.gerrit.extensions.common.FetchInfo; |
| import com.google.gerrit.extensions.common.LabelInfo; |
| import com.google.gerrit.extensions.common.ProblemInfo; |
| import com.google.gerrit.extensions.common.PushCertificateInfo; |
| import com.google.gerrit.extensions.common.ReviewerUpdateInfo; |
| import com.google.gerrit.extensions.common.RevisionInfo; |
| import com.google.gerrit.extensions.common.TrackingIdInfo; |
| import com.google.gerrit.extensions.common.VotingRangeInfo; |
| import com.google.gerrit.extensions.common.WebLinkInfo; |
| import com.google.gerrit.extensions.config.DownloadCommand; |
| import com.google.gerrit.extensions.config.DownloadScheme; |
| import com.google.gerrit.extensions.registration.DynamicMap; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.Url; |
| import com.google.gerrit.index.query.QueryResult; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.ChangeMessage; |
| import com.google.gerrit.reviewdb.client.Patch; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.PatchSetApproval; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.AnonymousUser; |
| import com.google.gerrit.server.ApprovalsUtil; |
| import com.google.gerrit.server.ChangeMessagesUtil; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.GpgException; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.ReviewerByEmailSet; |
| import com.google.gerrit.server.ReviewerSet; |
| import com.google.gerrit.server.ReviewerStatusUpdate; |
| import com.google.gerrit.server.StarredChangesUtil; |
| import com.google.gerrit.server.WebLinks; |
| import com.google.gerrit.server.account.AccountInfoComparator; |
| import com.google.gerrit.server.account.AccountLoader; |
| import com.google.gerrit.server.account.GpgApiAdapter; |
| import com.google.gerrit.server.config.TrackingFooters; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.MergeUtil; |
| import com.google.gerrit.server.index.change.ChangeField; |
| import com.google.gerrit.server.index.change.ChangeIndexCollection; |
| import com.google.gerrit.server.mail.Address; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.notedb.ReviewerStateInternal; |
| import com.google.gerrit.server.patch.PatchListNotAvailableException; |
| import com.google.gerrit.server.permissions.ChangePermission; |
| import com.google.gerrit.server.permissions.LabelPermission; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.RemoveReviewerControl; |
| import com.google.gerrit.server.project.SubmitRuleOptions; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.ChangeData.ChangedLines; |
| import com.google.gerrit.server.query.change.PluginDefinedAttributesFactory; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| public class ChangeJson { |
| private static final Logger log = LoggerFactory.getLogger(ChangeJson.class); |
| |
| public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT = |
| ChangeField.SUBMIT_RULE_OPTIONS_LENIENT.toBuilder().build(); |
| |
| public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT = |
| ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build(); |
| |
| public static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD = |
| ImmutableSet.of( |
| ALL_COMMITS, |
| ALL_REVISIONS, |
| CHANGE_ACTIONS, |
| CHECK, |
| COMMIT_FOOTERS, |
| CURRENT_ACTIONS, |
| CURRENT_COMMIT, |
| MESSAGES); |
| |
| @Singleton |
| public static class Factory { |
| private final AssistedFactory factory; |
| |
| @Inject |
| Factory(AssistedFactory factory) { |
| this.factory = factory; |
| } |
| |
| public ChangeJson noOptions() { |
| return create(ImmutableSet.of()); |
| } |
| |
| public ChangeJson create(Iterable<ListChangesOption> options) { |
| return factory.create(options); |
| } |
| |
| public ChangeJson create(ListChangesOption first, ListChangesOption... rest) { |
| return create(Sets.immutableEnumSet(first, rest)); |
| } |
| } |
| |
| public interface AssistedFactory { |
| ChangeJson create(Iterable<ListChangesOption> options); |
| } |
| |
| private final Provider<ReviewDb> db; |
| private final Provider<CurrentUser> userProvider; |
| private final AnonymousUser anonymous; |
| private final PermissionBackend permissionBackend; |
| private final GitRepositoryManager repoManager; |
| private final ProjectCache projectCache; |
| private final MergeUtil.Factory mergeUtilFactory; |
| private final IdentifiedUser.GenericFactory userFactory; |
| private final ChangeData.Factory changeDataFactory; |
| private final FileInfoJson fileInfoJson; |
| private final AccountLoader.Factory accountLoaderFactory; |
| private final DynamicMap<DownloadScheme> downloadSchemes; |
| private final DynamicMap<DownloadCommand> downloadCommands; |
| private final WebLinks webLinks; |
| private final ImmutableSet<ListChangesOption> options; |
| private final ChangeMessagesUtil cmUtil; |
| private final Provider<ConsistencyChecker> checkerProvider; |
| private final ActionJson actionJson; |
| private final GpgApiAdapter gpgApi; |
| private final ChangeNotes.Factory notesFactory; |
| private final ChangeResource.Factory changeResourceFactory; |
| private final ChangeKindCache changeKindCache; |
| private final ChangeIndexCollection indexes; |
| private final ApprovalsUtil approvalsUtil; |
| private final RemoveReviewerControl removeReviewerControl; |
| private final TrackingFooters trackingFooters; |
| private boolean lazyLoad = true; |
| private AccountLoader accountLoader; |
| private FixInput fix; |
| private PluginDefinedAttributesFactory pluginDefinedAttributesFactory; |
| |
| @Inject |
| ChangeJson( |
| Provider<ReviewDb> db, |
| Provider<CurrentUser> user, |
| AnonymousUser au, |
| PermissionBackend permissionBackend, |
| GitRepositoryManager repoManager, |
| ProjectCache projectCache, |
| MergeUtil.Factory mergeUtilFactory, |
| IdentifiedUser.GenericFactory uf, |
| ChangeData.Factory cdf, |
| FileInfoJson fileInfoJson, |
| AccountLoader.Factory ailf, |
| DynamicMap<DownloadScheme> downloadSchemes, |
| DynamicMap<DownloadCommand> downloadCommands, |
| WebLinks webLinks, |
| ChangeMessagesUtil cmUtil, |
| Provider<ConsistencyChecker> checkerProvider, |
| ActionJson actionJson, |
| GpgApiAdapter gpgApi, |
| ChangeNotes.Factory notesFactory, |
| ChangeResource.Factory changeResourceFactory, |
| ChangeKindCache changeKindCache, |
| ChangeIndexCollection indexes, |
| ApprovalsUtil approvalsUtil, |
| RemoveReviewerControl removeReviewerControl, |
| TrackingFooters trackingFooters, |
| @Assisted Iterable<ListChangesOption> options) { |
| this.db = db; |
| this.userProvider = user; |
| this.anonymous = au; |
| this.changeDataFactory = cdf; |
| this.permissionBackend = permissionBackend; |
| this.repoManager = repoManager; |
| this.userFactory = uf; |
| this.projectCache = projectCache; |
| this.mergeUtilFactory = mergeUtilFactory; |
| this.fileInfoJson = fileInfoJson; |
| this.accountLoaderFactory = ailf; |
| this.downloadSchemes = downloadSchemes; |
| this.downloadCommands = downloadCommands; |
| this.webLinks = webLinks; |
| this.cmUtil = cmUtil; |
| this.checkerProvider = checkerProvider; |
| this.actionJson = actionJson; |
| this.gpgApi = gpgApi; |
| this.notesFactory = notesFactory; |
| this.changeResourceFactory = changeResourceFactory; |
| this.changeKindCache = changeKindCache; |
| this.indexes = indexes; |
| this.approvalsUtil = approvalsUtil; |
| this.removeReviewerControl = removeReviewerControl; |
| this.options = Sets.immutableEnumSet(options); |
| this.trackingFooters = trackingFooters; |
| } |
| |
| public ChangeJson lazyLoad(boolean load) { |
| lazyLoad = load; |
| return this; |
| } |
| |
| public ChangeJson fix(FixInput fix) { |
| this.fix = fix; |
| return this; |
| } |
| |
| public void setPluginDefinedAttributesFactory(PluginDefinedAttributesFactory pluginsFactory) { |
| this.pluginDefinedAttributesFactory = pluginsFactory; |
| } |
| |
| public ChangeInfo format(ChangeResource rsrc) throws OrmException { |
| return format(changeDataFactory.create(db.get(), rsrc.getNotes())); |
| } |
| |
| public ChangeInfo format(Change change) throws OrmException { |
| return format(changeDataFactory.create(db.get(), change)); |
| } |
| |
| public ChangeInfo format(Project.NameKey project, Change.Id id) throws OrmException { |
| ChangeNotes notes; |
| try { |
| notes = notesFactory.createChecked(db.get(), project, id); |
| } catch (OrmException e) { |
| if (!has(CHECK)) { |
| throw e; |
| } |
| return checkOnly(changeDataFactory.create(db.get(), project, id)); |
| } |
| return format(changeDataFactory.create(db.get(), notes)); |
| } |
| |
| public ChangeInfo format(ChangeData cd) throws OrmException { |
| return format(cd, Optional.empty(), true); |
| } |
| |
| private ChangeInfo format( |
| ChangeData cd, Optional<PatchSet.Id> limitToPsId, boolean fillAccountLoader) |
| throws OrmException { |
| try { |
| if (fillAccountLoader) { |
| accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); |
| ChangeInfo res = toChangeInfo(cd, limitToPsId); |
| accountLoader.fill(); |
| return res; |
| } |
| return toChangeInfo(cd, limitToPsId); |
| } catch (PatchListNotAvailableException |
| | GpgException |
| | OrmException |
| | IOException |
| | PermissionBackendException |
| | NoSuchProjectException |
| | RuntimeException e) { |
| if (!has(CHECK)) { |
| Throwables.throwIfInstanceOf(e, OrmException.class); |
| throw new OrmException(e); |
| } |
| return checkOnly(cd); |
| } |
| } |
| |
| public ChangeInfo format(RevisionResource rsrc) throws OrmException { |
| ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes()); |
| return format(cd, Optional.of(rsrc.getPatchSet().getId()), true); |
| } |
| |
| public List<List<ChangeInfo>> formatQueryResults(List<QueryResult<ChangeData>> in) |
| throws OrmException { |
| accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); |
| ensureLoaded(FluentIterable.from(in).transformAndConcat(QueryResult::entities)); |
| |
| List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size()); |
| Map<Change.Id, ChangeInfo> out = new HashMap<>(); |
| for (QueryResult<ChangeData> r : in) { |
| List<ChangeInfo> infos = toChangeInfo(out, r.entities()); |
| if (!infos.isEmpty() && r.more()) { |
| infos.get(infos.size() - 1)._moreChanges = true; |
| } |
| res.add(infos); |
| } |
| accountLoader.fill(); |
| return res; |
| } |
| |
| public List<ChangeInfo> formatChangeDatas(Collection<ChangeData> in) throws OrmException { |
| accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); |
| ensureLoaded(in); |
| List<ChangeInfo> out = new ArrayList<>(in.size()); |
| for (ChangeData cd : in) { |
| out.add(format(cd)); |
| } |
| accountLoader.fill(); |
| return out; |
| } |
| |
| private void ensureLoaded(Iterable<ChangeData> all) throws OrmException { |
| if (lazyLoad) { |
| ChangeData.ensureChangeLoaded(all); |
| if (has(ALL_REVISIONS)) { |
| ChangeData.ensureAllPatchSetsLoaded(all); |
| } else if (has(CURRENT_REVISION) || has(MESSAGES)) { |
| ChangeData.ensureCurrentPatchSetLoaded(all); |
| } |
| if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) { |
| ChangeData.ensureReviewedByLoadedForOpenChanges(all); |
| } |
| ChangeData.ensureCurrentApprovalsLoaded(all); |
| } else { |
| for (ChangeData cd : all) { |
| cd.setLazyLoad(false); |
| } |
| } |
| } |
| |
| private boolean has(ListChangesOption option) { |
| return options.contains(option); |
| } |
| |
| private List<ChangeInfo> toChangeInfo(Map<Change.Id, ChangeInfo> out, List<ChangeData> changes) { |
| List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size()); |
| for (ChangeData cd : changes) { |
| ChangeInfo i = out.get(cd.getId()); |
| if (i == null) { |
| try { |
| i = toChangeInfo(cd, Optional.empty()); |
| } catch (PatchListNotAvailableException |
| | GpgException |
| | OrmException |
| | IOException |
| | PermissionBackendException |
| | NoSuchProjectException |
| | RuntimeException e) { |
| if (has(CHECK)) { |
| i = checkOnly(cd); |
| } else if (e instanceof NoSuchChangeException) { |
| log.info( |
| "NoSuchChangeException: Omitting corrupt change " |
| + cd.getId() |
| + " from results. Seems to be stale in the index."); |
| continue; |
| } else { |
| log.warn("Omitting corrupt change " + cd.getId() + " from results", e); |
| continue; |
| } |
| } |
| out.put(cd.getId(), i); |
| } |
| info.add(i); |
| } |
| return info; |
| } |
| |
| private ChangeInfo checkOnly(ChangeData cd) { |
| ChangeNotes notes; |
| try { |
| notes = cd.notes(); |
| } catch (OrmException e) { |
| String msg = "Error loading change"; |
| log.warn(msg + " " + cd.getId(), e); |
| ChangeInfo info = new ChangeInfo(); |
| info._number = cd.getId().get(); |
| ProblemInfo p = new ProblemInfo(); |
| p.message = msg; |
| info.problems = Lists.newArrayList(p); |
| return info; |
| } |
| |
| ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix); |
| ChangeInfo info; |
| Change c = result.change(); |
| if (c != null) { |
| info = new ChangeInfo(); |
| info.project = c.getProject().get(); |
| info.branch = c.getDest().getShortName(); |
| info.topic = c.getTopic(); |
| info.changeId = c.getKey().get(); |
| info.subject = c.getSubject(); |
| info.status = c.getStatus().asChangeStatus(); |
| info.owner = new AccountInfo(c.getOwner().get()); |
| info.created = c.getCreatedOn(); |
| info.updated = c.getLastUpdatedOn(); |
| info._number = c.getId().get(); |
| info.problems = result.problems(); |
| info.isPrivate = c.isPrivate() ? true : null; |
| info.workInProgress = c.isWorkInProgress() ? true : null; |
| info.hasReviewStarted = c.hasReviewStarted(); |
| finish(info); |
| } else { |
| info = new ChangeInfo(); |
| info._number = result.id().get(); |
| info.problems = result.problems(); |
| } |
| return info; |
| } |
| |
| private ChangeInfo toChangeInfo(ChangeData cd, Optional<PatchSet.Id> limitToPsId) |
| throws PatchListNotAvailableException, GpgException, OrmException, IOException, |
| PermissionBackendException, NoSuchProjectException { |
| ChangeInfo out = new ChangeInfo(); |
| CurrentUser user = userProvider.get(); |
| |
| if (has(CHECK)) { |
| out.problems = checkerProvider.get().check(cd.notes(), fix).problems(); |
| // If any problems were fixed, the ChangeData needs to be reloaded. |
| for (ProblemInfo p : out.problems) { |
| if (p.status == ProblemInfo.Status.FIXED) { |
| cd = changeDataFactory.create(cd.db(), cd.project(), cd.getId()); |
| break; |
| } |
| } |
| } |
| |
| PermissionBackend.ForChange perm = permissionBackendForChange(user, cd); |
| Change in = cd.change(); |
| out.project = in.getProject().get(); |
| out.branch = in.getDest().getShortName(); |
| out.topic = in.getTopic(); |
| if (indexes.getSearchIndex().getSchema().hasField(ChangeField.ASSIGNEE)) { |
| if (in.getAssignee() != null) { |
| out.assignee = accountLoader.get(in.getAssignee()); |
| } |
| } |
| out.hashtags = cd.hashtags(); |
| out.changeId = in.getKey().get(); |
| if (in.getStatus().isOpen()) { |
| SubmitTypeRecord str = cd.submitTypeRecord(); |
| if (str.isOk()) { |
| out.submitType = str.type; |
| } |
| out.mergeable = cd.isMergeable(); |
| if (has(SUBMITTABLE)) { |
| out.submittable = submittable(cd); |
| } |
| } |
| Optional<ChangedLines> changedLines = cd.changedLines(); |
| if (changedLines.isPresent()) { |
| out.insertions = changedLines.get().insertions; |
| out.deletions = changedLines.get().deletions; |
| } |
| out.isPrivate = in.isPrivate() ? true : null; |
| out.workInProgress = in.isWorkInProgress() ? true : null; |
| out.hasReviewStarted = in.hasReviewStarted(); |
| out.subject = in.getSubject(); |
| out.status = in.getStatus().asChangeStatus(); |
| out.owner = accountLoader.get(in.getOwner()); |
| out.created = in.getCreatedOn(); |
| out.updated = in.getLastUpdatedOn(); |
| out._number = in.getId().get(); |
| out.unresolvedCommentCount = cd.unresolvedCommentCount(); |
| |
| if (user.isIdentifiedUser()) { |
| Collection<String> stars = cd.stars(user.getAccountId()); |
| out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null; |
| if (!stars.isEmpty()) { |
| out.stars = stars; |
| } |
| } |
| |
| if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) { |
| out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null; |
| } |
| |
| out.labels = labelsFor(perm, cd, has(LABELS), has(DETAILED_LABELS)); |
| |
| if (out.labels != null && has(DETAILED_LABELS)) { |
| // If limited to specific patch sets but not the current patch set, don't |
| // list permitted labels, since users can't vote on those patch sets. |
| if (user.isIdentifiedUser() |
| && (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) { |
| out.permittedLabels = |
| cd.change().getStatus() != Change.Status.ABANDONED |
| ? permittedLabels(perm, cd) |
| : ImmutableMap.of(); |
| } |
| |
| out.reviewers = reviewerMap(cd.reviewers(), cd.reviewersByEmail(), false); |
| out.pendingReviewers = reviewerMap(cd.pendingReviewers(), cd.pendingReviewersByEmail(), true); |
| out.removableReviewers = removableReviewers(cd, out); |
| } |
| |
| setSubmitter(cd, out); |
| out.plugins = |
| pluginDefinedAttributesFactory != null ? pluginDefinedAttributesFactory.create(cd) : null; |
| out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null; |
| |
| if (has(REVIEWER_UPDATES)) { |
| out.reviewerUpdates = reviewerUpdates(cd); |
| } |
| |
| boolean needMessages = has(MESSAGES); |
| boolean needRevisions = has(ALL_REVISIONS) || has(CURRENT_REVISION) || limitToPsId.isPresent(); |
| Map<PatchSet.Id, PatchSet> src; |
| if (needMessages || needRevisions) { |
| src = loadPatchSets(cd, limitToPsId); |
| } else { |
| src = null; |
| } |
| |
| if (needMessages) { |
| out.messages = messages(cd); |
| } |
| finish(out); |
| |
| // This block must come after the ChangeInfo is mostly populated, since |
| // it will be passed to ActionVisitors as-is. |
| if (needRevisions) { |
| out.revisions = revisions(cd, src, limitToPsId, out); |
| if (out.revisions != null) { |
| for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) { |
| if (entry.getValue().isCurrent) { |
| out.currentRevision = entry.getKey(); |
| break; |
| } |
| } |
| } |
| } |
| |
| if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) { |
| actionJson.addChangeActions(out, cd.notes()); |
| } |
| |
| if (has(TRACKING_IDS)) { |
| ListMultimap<String, String> set = trackingFooters.extract(cd.commitFooters()); |
| out.trackingIds = |
| set.entries() |
| .stream() |
| .map(e -> new TrackingIdInfo(e.getKey(), e.getValue())) |
| .collect(toList()); |
| } |
| |
| return out; |
| } |
| |
| private Map<ReviewerState, Collection<AccountInfo>> reviewerMap( |
| ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, boolean includeRemoved) { |
| Map<ReviewerState, Collection<AccountInfo>> reviewerMap = new HashMap<>(); |
| for (ReviewerStateInternal state : ReviewerStateInternal.values()) { |
| if (!includeRemoved && state == ReviewerStateInternal.REMOVED) { |
| continue; |
| } |
| Collection<AccountInfo> reviewersByState = toAccountInfo(reviewers.byState(state)); |
| reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state))); |
| if (!reviewersByState.isEmpty()) { |
| reviewerMap.put(state.asReviewerState(), reviewersByState); |
| } |
| } |
| return reviewerMap; |
| } |
| |
| private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) throws OrmException { |
| List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates(); |
| List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size()); |
| for (ReviewerStatusUpdate c : reviewerUpdates) { |
| ReviewerUpdateInfo change = new ReviewerUpdateInfo(); |
| change.updated = c.date(); |
| change.state = c.state().asReviewerState(); |
| change.updatedBy = accountLoader.get(c.updatedBy()); |
| change.reviewer = accountLoader.get(c.reviewer()); |
| result.add(change); |
| } |
| return result; |
| } |
| |
| private boolean submittable(ChangeData cd) throws OrmException { |
| return SubmitRecord.findOkRecord(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)).isPresent(); |
| } |
| |
| private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException { |
| return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT); |
| } |
| |
| private Map<String, LabelInfo> labelsFor( |
| PermissionBackend.ForChange perm, ChangeData cd, boolean standard, boolean detailed) |
| throws OrmException, PermissionBackendException { |
| if (!standard && !detailed) { |
| return null; |
| } |
| |
| LabelTypes labelTypes = cd.getLabelTypes(); |
| Map<String, LabelWithStatus> withStatus = |
| cd.change().getStatus() == Change.Status.MERGED |
| ? labelsForSubmittedChange(perm, cd, labelTypes, standard, detailed) |
| : labelsForUnsubmittedChange(perm, cd, labelTypes, standard, detailed); |
| return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label)); |
| } |
| |
| private Map<String, LabelWithStatus> labelsForUnsubmittedChange( |
| PermissionBackend.ForChange perm, |
| ChangeData cd, |
| LabelTypes labelTypes, |
| boolean standard, |
| boolean detailed) |
| throws OrmException, PermissionBackendException { |
| Map<String, LabelWithStatus> labels = initLabels(cd, labelTypes, standard); |
| if (detailed) { |
| setAllApprovals(perm, cd, labels); |
| } |
| for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) { |
| LabelType type = labelTypes.byLabel(e.getKey()); |
| if (type == null) { |
| continue; |
| } |
| if (standard) { |
| for (PatchSetApproval psa : cd.currentApprovals()) { |
| if (type.matches(psa)) { |
| short val = psa.getValue(); |
| Account.Id accountId = psa.getAccountId(); |
| setLabelScores(type, e.getValue(), val, accountId); |
| } |
| } |
| } |
| if (detailed) { |
| setLabelValues(type, e.getValue()); |
| } |
| } |
| return labels; |
| } |
| |
| private Map<String, LabelWithStatus> initLabels( |
| ChangeData cd, LabelTypes labelTypes, boolean standard) throws OrmException { |
| Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator()); |
| for (SubmitRecord rec : submitRecords(cd)) { |
| if (rec.labels == null) { |
| continue; |
| } |
| for (SubmitRecord.Label r : rec.labels) { |
| LabelWithStatus p = labels.get(r.label); |
| if (p == null || p.status().compareTo(r.status) < 0) { |
| LabelInfo n = new LabelInfo(); |
| if (standard) { |
| switch (r.status) { |
| case OK: |
| n.approved = accountLoader.get(r.appliedBy); |
| break; |
| case REJECT: |
| n.rejected = accountLoader.get(r.appliedBy); |
| n.blocking = true; |
| break; |
| case IMPOSSIBLE: |
| case MAY: |
| case NEED: |
| default: |
| break; |
| } |
| } |
| |
| n.optional = r.status == SubmitRecord.Label.Status.MAY ? true : null; |
| labels.put(r.label, LabelWithStatus.create(n, r.status)); |
| } |
| } |
| } |
| return labels; |
| } |
| |
| private void setLabelScores( |
| LabelType type, LabelWithStatus l, short score, Account.Id accountId) { |
| if (l.label().approved != null || l.label().rejected != null) { |
| return; |
| } |
| |
| if (type.getMin() == null || type.getMax() == null) { |
| // Can't set score for unknown or misconfigured type. |
| return; |
| } |
| |
| if (score != 0) { |
| if (score == type.getMin().getValue()) { |
| l.label().rejected = accountLoader.get(accountId); |
| } else if (score == type.getMax().getValue()) { |
| l.label().approved = accountLoader.get(accountId); |
| } else if (score < 0) { |
| l.label().disliked = accountLoader.get(accountId); |
| l.label().value = score; |
| } else if (score > 0 && l.label().disliked == null) { |
| l.label().recommended = accountLoader.get(accountId); |
| l.label().value = score; |
| } |
| } |
| } |
| |
| private void setAllApprovals( |
| PermissionBackend.ForChange basePerm, ChangeData cd, Map<String, LabelWithStatus> labels) |
| throws OrmException, PermissionBackendException { |
| Change.Status status = cd.change().getStatus(); |
| checkState( |
| status != Change.Status.MERGED, "should not call setAllApprovals on %s change", status); |
| |
| // Include a user in the output for this label if either: |
| // - They are an explicit reviewer. |
| // - They ever voted on this change. |
| Set<Account.Id> allUsers = new HashSet<>(); |
| allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER)); |
| for (PatchSetApproval psa : cd.approvals().values()) { |
| allUsers.add(psa.getAccountId()); |
| } |
| |
| Table<Account.Id, String, PatchSetApproval> current = |
| HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size()); |
| for (PatchSetApproval psa : cd.currentApprovals()) { |
| current.put(psa.getAccountId(), psa.getLabel(), psa); |
| } |
| |
| LabelTypes labelTypes = cd.getLabelTypes(); |
| for (Account.Id accountId : allUsers) { |
| PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId)); |
| Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(perm, cd)); |
| for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) { |
| LabelType lt = labelTypes.byLabel(e.getKey()); |
| if (lt == null) { |
| // Ignore submit record for undefined label; likely the submit rule |
| // author didn't intend for the label to show up in the table. |
| continue; |
| } |
| Integer value; |
| VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.getName(), null); |
| String tag = null; |
| Timestamp date = null; |
| PatchSetApproval psa = current.get(accountId, lt.getName()); |
| if (psa != null) { |
| value = Integer.valueOf(psa.getValue()); |
| if (value == 0) { |
| // This may be a dummy approval that was inserted when the reviewer |
| // was added. Explicitly check whether the user can vote on this |
| // label. |
| value = perm.test(new LabelPermission(lt)) ? 0 : null; |
| } |
| tag = psa.getTag(); |
| date = psa.getGranted(); |
| if (psa.isPostSubmit()) { |
| log.warn("unexpected post-submit approval on open change: {}", psa); |
| } |
| } else { |
| // Either the user cannot vote on this label, or they were added as a |
| // reviewer but have not responded yet. Explicitly check whether the |
| // user can vote on this label. |
| value = perm.test(new LabelPermission(lt)) ? 0 : null; |
| } |
| addApproval( |
| e.getValue().label(), approvalInfo(accountId, value, permittedVotingRange, tag, date)); |
| } |
| } |
| } |
| |
| private Map<String, VotingRangeInfo> getPermittedVotingRanges( |
| Map<String, Collection<String>> permittedLabels) { |
| Map<String, VotingRangeInfo> permittedVotingRanges = |
| Maps.newHashMapWithExpectedSize(permittedLabels.size()); |
| for (String label : permittedLabels.keySet()) { |
| List<Integer> permittedVotingRange = |
| permittedLabels |
| .get(label) |
| .stream() |
| .map(this::parseRangeValue) |
| .filter(java.util.Objects::nonNull) |
| .sorted() |
| .collect(toList()); |
| |
| if (permittedVotingRange.isEmpty()) { |
| permittedVotingRanges.put(label, null); |
| } else { |
| int minPermittedValue = permittedVotingRange.get(0); |
| int maxPermittedValue = Iterables.getLast(permittedVotingRange); |
| permittedVotingRanges.put(label, new VotingRangeInfo(minPermittedValue, maxPermittedValue)); |
| } |
| } |
| return permittedVotingRanges; |
| } |
| |
| private Integer parseRangeValue(String value) { |
| if (value.startsWith("+")) { |
| value = value.substring(1); |
| } else if (value.startsWith(" ")) { |
| value = value.trim(); |
| } |
| return Ints.tryParse(value); |
| } |
| |
| private void setSubmitter(ChangeData cd, ChangeInfo out) throws OrmException { |
| Optional<PatchSetApproval> s = cd.getSubmitApproval(); |
| if (!s.isPresent()) { |
| return; |
| } |
| out.submitted = s.get().getGranted(); |
| out.submitter = accountLoader.get(s.get().getAccountId()); |
| } |
| |
| private Map<String, LabelWithStatus> labelsForSubmittedChange( |
| PermissionBackend.ForChange basePerm, |
| ChangeData cd, |
| LabelTypes labelTypes, |
| boolean standard, |
| boolean detailed) |
| throws OrmException, PermissionBackendException { |
| Set<Account.Id> allUsers = new HashSet<>(); |
| if (detailed) { |
| // Users expect to see all reviewers on closed changes, even if they |
| // didn't vote on the latest patch set. If we don't need detailed labels, |
| // we aren't including 0 votes for all users below, so we can just look at |
| // the latest patch set (in the next loop). |
| for (PatchSetApproval psa : cd.approvals().values()) { |
| allUsers.add(psa.getAccountId()); |
| } |
| } |
| |
| Set<String> labelNames = new HashSet<>(); |
| SetMultimap<Account.Id, PatchSetApproval> current = |
| MultimapBuilder.hashKeys().hashSetValues().build(); |
| for (PatchSetApproval a : cd.currentApprovals()) { |
| allUsers.add(a.getAccountId()); |
| LabelType type = labelTypes.byLabel(a.getLabelId()); |
| if (type != null) { |
| labelNames.add(type.getName()); |
| // Not worth the effort to distinguish between votable/non-votable for 0 |
| // values on closed changes, since they can't vote anyway. |
| current.put(a.getAccountId(), a); |
| } |
| } |
| |
| // Since voting on merged changes is allowed all labels which apply to |
| // the change must be returned. All applying labels can be retrieved from |
| // the submit records, which is what initLabels does. |
| // It's not possible to only compute the labels based on the approvals |
| // since merged changes may not have approvals for all labels (e.g. if not |
| // all labels are required for submit or if the change was auto-closed due |
| // to direct push or if new labels were defined after the change was |
| // merged). |
| Map<String, LabelWithStatus> labels; |
| labels = initLabels(cd, labelTypes, standard); |
| |
| // Also include all labels for which approvals exists. E.g. there can be |
| // approvals for labels that are ignored by a Prolog submit rule and hence |
| // it wouldn't be included in the submit records. |
| for (String name : labelNames) { |
| if (!labels.containsKey(name)) { |
| labels.put(name, LabelWithStatus.create(new LabelInfo(), null)); |
| } |
| } |
| |
| if (detailed) { |
| labels |
| .entrySet() |
| .stream() |
| .filter(e -> labelTypes.byLabel(e.getKey()) != null) |
| .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()), e.getValue())); |
| } |
| |
| for (Account.Id accountId : allUsers) { |
| Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size()); |
| Map<String, VotingRangeInfo> pvr = Collections.emptyMap(); |
| if (detailed) { |
| PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId)); |
| pvr = getPermittedVotingRanges(permittedLabels(perm, cd)); |
| for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) { |
| ApprovalInfo ai = approvalInfo(accountId, 0, null, null, null); |
| byLabel.put(entry.getKey(), ai); |
| addApproval(entry.getValue().label(), ai); |
| } |
| } |
| for (PatchSetApproval psa : current.get(accountId)) { |
| LabelType type = labelTypes.byLabel(psa.getLabelId()); |
| if (type == null) { |
| continue; |
| } |
| |
| short val = psa.getValue(); |
| ApprovalInfo info = byLabel.get(type.getName()); |
| if (info != null) { |
| info.value = Integer.valueOf(val); |
| info.permittedVotingRange = pvr.getOrDefault(type.getName(), null); |
| info.date = psa.getGranted(); |
| info.tag = psa.getTag(); |
| if (psa.isPostSubmit()) { |
| info.postSubmit = true; |
| } |
| } |
| if (!standard) { |
| continue; |
| } |
| |
| setLabelScores(type, labels.get(type.getName()), val, accountId); |
| } |
| } |
| return labels; |
| } |
| |
| private ApprovalInfo approvalInfo( |
| Account.Id id, |
| Integer value, |
| VotingRangeInfo permittedVotingRange, |
| String tag, |
| Timestamp date) { |
| ApprovalInfo ai = getApprovalInfo(id, value, permittedVotingRange, tag, date); |
| accountLoader.put(ai); |
| return ai; |
| } |
| |
| public static ApprovalInfo getApprovalInfo( |
| Account.Id id, |
| Integer value, |
| VotingRangeInfo permittedVotingRange, |
| String tag, |
| Timestamp date) { |
| ApprovalInfo ai = new ApprovalInfo(id.get()); |
| ai.value = value; |
| ai.permittedVotingRange = permittedVotingRange; |
| ai.date = date; |
| ai.tag = tag; |
| return ai; |
| } |
| |
| private static boolean isOnlyZero(Collection<String> values) { |
| return values.isEmpty() || (values.size() == 1 && values.contains(" 0")); |
| } |
| |
| private void setLabelValues(LabelType type, LabelWithStatus l) { |
| l.label().defaultValue = type.getDefaultValue(); |
| l.label().values = new LinkedHashMap<>(); |
| for (LabelValue v : type.getValues()) { |
| l.label().values.put(v.formatValue(), v.getText()); |
| } |
| if (isOnlyZero(l.label().values.keySet())) { |
| l.label().values = null; |
| } |
| } |
| |
| private Map<String, Collection<String>> permittedLabels( |
| PermissionBackend.ForChange perm, ChangeData cd) |
| throws OrmException, PermissionBackendException { |
| boolean isMerged = cd.change().getStatus() == Change.Status.MERGED; |
| LabelTypes labelTypes = cd.getLabelTypes(); |
| Map<String, LabelType> toCheck = new HashMap<>(); |
| for (SubmitRecord rec : submitRecords(cd)) { |
| if (rec.labels != null) { |
| for (SubmitRecord.Label r : rec.labels) { |
| LabelType type = labelTypes.byLabel(r.label); |
| if (type != null && (!isMerged || type.allowPostSubmit())) { |
| toCheck.put(type.getName(), type); |
| } |
| } |
| } |
| } |
| |
| Map<String, Short> labels = null; |
| Set<LabelPermission.WithValue> can = perm.testLabels(toCheck.values()); |
| SetMultimap<String, String> permitted = LinkedHashMultimap.create(); |
| for (SubmitRecord rec : submitRecords(cd)) { |
| if (rec.labels == null) { |
| continue; |
| } |
| for (SubmitRecord.Label r : rec.labels) { |
| LabelType type = labelTypes.byLabel(r.label); |
| if (type == null || (isMerged && !type.allowPostSubmit())) { |
| continue; |
| } |
| |
| for (LabelValue v : type.getValues()) { |
| boolean ok = can.contains(new LabelPermission.WithValue(type, v)); |
| if (isMerged) { |
| if (labels == null) { |
| labels = currentLabels(perm, cd); |
| } |
| short prev = labels.getOrDefault(type.getName(), (short) 0); |
| ok &= v.getValue() >= prev; |
| } |
| if (ok) { |
| permitted.put(r.label, v.formatValue()); |
| } |
| } |
| } |
| } |
| |
| List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size()); |
| for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) { |
| if (isOnlyZero(e.getValue())) { |
| toClear.add(e.getKey()); |
| } |
| } |
| for (String label : toClear) { |
| permitted.removeAll(label); |
| } |
| return permitted.asMap(); |
| } |
| |
| private Map<String, Short> currentLabels(PermissionBackend.ForChange perm, ChangeData cd) |
| throws OrmException { |
| IdentifiedUser user = perm.user().asIdentifiedUser(); |
| Map<String, Short> result = new HashMap<>(); |
| for (PatchSetApproval psa : |
| approvalsUtil.byPatchSetUser( |
| db.get(), |
| lazyLoad ? cd.notes() : notesFactory.createFromIndexedChange(cd.change()), |
| user, |
| cd.change().currentPatchSetId(), |
| user.getAccountId(), |
| null, |
| null)) { |
| result.put(psa.getLabel(), psa.getValue()); |
| } |
| return result; |
| } |
| |
| private Collection<ChangeMessageInfo> messages(ChangeData cd) throws OrmException { |
| List<ChangeMessage> messages = cmUtil.byChange(db.get(), cd.notes()); |
| if (messages.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| |
| List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size()); |
| for (ChangeMessage message : messages) { |
| PatchSet.Id patchNum = message.getPatchSetId(); |
| ChangeMessageInfo cmi = new ChangeMessageInfo(); |
| cmi.id = message.getKey().get(); |
| cmi.author = accountLoader.get(message.getAuthor()); |
| cmi.date = message.getWrittenOn(); |
| cmi.message = message.getMessage(); |
| cmi.tag = message.getTag(); |
| cmi._revisionNumber = patchNum != null ? patchNum.get() : null; |
| Account.Id realAuthor = message.getRealAuthor(); |
| if (realAuthor != null) { |
| cmi.realAuthor = accountLoader.get(realAuthor); |
| } |
| result.add(cmi); |
| } |
| return result; |
| } |
| |
| private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out) |
| throws PermissionBackendException, NoSuchProjectException, OrmException, IOException { |
| // Although this is called removableReviewers, this method also determines |
| // which CCs are removable. |
| // |
| // For reviewers, we need to look at each approval, because the reviewer |
| // should only be considered removable if *all* of their approvals can be |
| // removed. First, add all reviewers with *any* removable approval to the |
| // "removable" set. Along the way, if we encounter a non-removable approval, |
| // add the reviewer to the "fixed" set. Before we return, remove all members |
| // of "fixed" from "removable", because not all of their approvals can be |
| // removed. |
| Collection<LabelInfo> labels = out.labels.values(); |
| Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size()); |
| Set<Account.Id> removable = Sets.newHashSetWithExpectedSize(labels.size()); |
| for (LabelInfo label : labels) { |
| if (label.all == null) { |
| continue; |
| } |
| for (ApprovalInfo ai : label.all) { |
| Account.Id id = new Account.Id(ai._accountId); |
| |
| if (removeReviewerControl.testRemoveReviewer( |
| cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) { |
| removable.add(id); |
| } else { |
| fixed.add(id); |
| } |
| } |
| } |
| |
| // CCs are simpler than reviewers. They are removable if the ChangeControl |
| // would permit a non-negative approval by that account to be removed, in |
| // which case add them to removable. We don't need to add unremovable CCs to |
| // "fixed" because we only visit each CC once here. |
| Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC); |
| if (ccs != null) { |
| for (AccountInfo ai : ccs) { |
| if (ai._accountId != null) { |
| Account.Id id = new Account.Id(ai._accountId); |
| if (removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) { |
| removable.add(id); |
| } |
| } |
| } |
| } |
| |
| // Subtract any reviewers with non-removable approvals from the "removable" |
| // set. This also subtracts any CCs that for some reason also hold |
| // unremovable approvals. |
| removable.removeAll(fixed); |
| |
| List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size()); |
| for (Account.Id id : removable) { |
| result.add(accountLoader.get(id)); |
| } |
| // Reviewers added by email are always removable |
| for (Collection<AccountInfo> infos : out.reviewers.values()) { |
| for (AccountInfo info : infos) { |
| if (info._accountId == null) { |
| result.add(info); |
| } |
| } |
| } |
| return result; |
| } |
| |
| private Collection<AccountInfo> toAccountInfo(Collection<Account.Id> accounts) { |
| return accounts |
| .stream() |
| .map(accountLoader::get) |
| .sorted(AccountInfoComparator.ORDER_NULLS_FIRST) |
| .collect(toList()); |
| } |
| |
| private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) { |
| return addresses |
| .stream() |
| .map(a -> new AccountInfo(a.getName(), a.getEmail())) |
| .sorted(AccountInfoComparator.ORDER_NULLS_FIRST) |
| .collect(toList()); |
| } |
| |
| @Nullable |
| private Repository openRepoIfNecessary(Project.NameKey project) throws IOException { |
| if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) { |
| return repoManager.openRepository(project); |
| } |
| return null; |
| } |
| |
| @Nullable |
| private RevWalk newRevWalk(@Nullable Repository repo) { |
| return repo != null ? new RevWalk(repo) : null; |
| } |
| |
| private Map<String, RevisionInfo> revisions( |
| ChangeData cd, |
| Map<PatchSet.Id, PatchSet> map, |
| Optional<PatchSet.Id> limitToPsId, |
| ChangeInfo changeInfo) |
| throws PatchListNotAvailableException, GpgException, OrmException, IOException, |
| PermissionBackendException { |
| Map<String, RevisionInfo> res = new LinkedHashMap<>(); |
| Boolean isWorldReadable = null; |
| try (Repository repo = openRepoIfNecessary(cd.project()); |
| RevWalk rw = newRevWalk(repo)) { |
| for (PatchSet in : map.values()) { |
| PatchSet.Id id = in.getId(); |
| boolean want = false; |
| if (has(ALL_REVISIONS)) { |
| want = true; |
| } else if (limitToPsId.isPresent()) { |
| want = id.equals(limitToPsId.get()); |
| } else { |
| want = id.equals(cd.change().currentPatchSetId()); |
| } |
| if (want) { |
| if (isWorldReadable == null) { |
| isWorldReadable = isWorldReadable(cd); |
| } |
| res.put( |
| in.getRevision().get(), |
| toRevisionInfo(cd, in, repo, rw, false, changeInfo, isWorldReadable)); |
| } |
| } |
| return res; |
| } |
| } |
| |
| private Map<PatchSet.Id, PatchSet> loadPatchSets(ChangeData cd, Optional<PatchSet.Id> limitToPsId) |
| throws OrmException { |
| Collection<PatchSet> src; |
| if (has(ALL_REVISIONS) || has(MESSAGES)) { |
| src = cd.patchSets(); |
| } else { |
| PatchSet ps; |
| if (limitToPsId.isPresent()) { |
| ps = cd.patchSet(limitToPsId.get()); |
| if (ps == null) { |
| throw new OrmException("missing patch set " + limitToPsId.get()); |
| } |
| } else { |
| ps = cd.currentPatchSet(); |
| if (ps == null) { |
| throw new OrmException("missing current patch set for change " + cd.getId()); |
| } |
| } |
| src = Collections.singletonList(ps); |
| } |
| Map<PatchSet.Id, PatchSet> map = Maps.newHashMapWithExpectedSize(src.size()); |
| for (PatchSet patchSet : src) { |
| map.put(patchSet.getId(), patchSet); |
| } |
| return map; |
| } |
| |
| public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in) |
| throws PatchListNotAvailableException, GpgException, OrmException, IOException, |
| PermissionBackendException { |
| accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); |
| try (Repository repo = openRepoIfNecessary(cd.project()); |
| RevWalk rw = newRevWalk(repo)) { |
| RevisionInfo rev = toRevisionInfo(cd, in, repo, rw, true, null, isWorldReadable(cd)); |
| accountLoader.fill(); |
| return rev; |
| } |
| } |
| |
| private RevisionInfo toRevisionInfo( |
| ChangeData cd, |
| PatchSet in, |
| @Nullable Repository repo, |
| @Nullable RevWalk rw, |
| boolean fillCommit, |
| @Nullable ChangeInfo changeInfo, |
| boolean isWorldReadable) |
| throws PatchListNotAvailableException, GpgException, OrmException, IOException { |
| Change c = cd.change(); |
| RevisionInfo out = new RevisionInfo(); |
| out.isCurrent = in.getId().equals(c.currentPatchSetId()); |
| out._number = in.getId().get(); |
| out.ref = in.getRefName(); |
| out.created = in.getCreatedOn(); |
| out.uploader = accountLoader.get(in.getUploader()); |
| out.fetch = makeFetchMap(cd, in, isWorldReadable); |
| out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in); |
| out.description = in.getDescription(); |
| |
| boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT)); |
| boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS); |
| if (setCommit || addFooters) { |
| checkState(rw != null); |
| checkState(repo != null); |
| Project.NameKey project = c.getProject(); |
| String rev = in.getRevision().get(); |
| RevCommit commit = rw.parseCommit(ObjectId.fromString(rev)); |
| rw.parseBody(commit); |
| if (setCommit) { |
| out.commit = toCommit(project, rw, commit, has(WEB_LINKS), fillCommit); |
| } |
| if (addFooters) { |
| Ref ref = repo.exactRef(cd.change().getDest().get()); |
| RevCommit mergeTip = null; |
| if (ref != null) { |
| mergeTip = rw.parseCommit(ref.getObjectId()); |
| rw.parseBody(mergeTip); |
| } |
| out.commitWithFooters = |
| mergeUtilFactory |
| .create(projectCache.get(project)) |
| .createCommitMessageOnSubmit( |
| commit, mergeTip, cd.notes(), userProvider.get(), in.getId()); |
| } |
| } |
| |
| if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) { |
| out.files = fileInfoJson.toFileInfoMap(c, in); |
| out.files.remove(Patch.COMMIT_MSG); |
| out.files.remove(Patch.MERGE_LIST); |
| } |
| |
| if (out.isCurrent && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) { |
| |
| actionJson.addRevisionActions( |
| changeInfo, |
| out, |
| new RevisionResource(changeResourceFactory.create(cd.notes(), userProvider.get()), in)); |
| } |
| |
| if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) { |
| if (in.getPushCertificate() != null) { |
| out.pushCertificate = |
| gpgApi.checkPushCertificate( |
| in.getPushCertificate(), userFactory.create(in.getUploader())); |
| } else { |
| out.pushCertificate = new PushCertificateInfo(); |
| } |
| } |
| |
| return out; |
| } |
| |
| public CommitInfo toCommit( |
| Project.NameKey project, RevWalk rw, RevCommit commit, boolean addLinks, boolean fillCommit) |
| throws IOException { |
| CommitInfo info = new CommitInfo(); |
| if (fillCommit) { |
| info.commit = commit.name(); |
| } |
| info.parents = new ArrayList<>(commit.getParentCount()); |
| info.author = toGitPerson(commit.getAuthorIdent()); |
| info.committer = toGitPerson(commit.getCommitterIdent()); |
| info.subject = commit.getShortMessage(); |
| info.message = commit.getFullMessage(); |
| |
| if (addLinks) { |
| List<WebLinkInfo> links = webLinks.getPatchSetLinks(project, commit.name()); |
| info.webLinks = links.isEmpty() ? null : links; |
| } |
| |
| for (RevCommit parent : commit.getParents()) { |
| rw.parseBody(parent); |
| CommitInfo i = new CommitInfo(); |
| i.commit = parent.name(); |
| i.subject = parent.getShortMessage(); |
| if (addLinks) { |
| List<WebLinkInfo> parentLinks = webLinks.getParentLinks(project, parent.name()); |
| i.webLinks = parentLinks.isEmpty() ? null : parentLinks; |
| } |
| info.parents.add(i); |
| } |
| return info; |
| } |
| |
| private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in, boolean isWorldReadable) { |
| Map<String, FetchInfo> r = new LinkedHashMap<>(); |
| for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) { |
| String schemeName = e.getExportName(); |
| DownloadScheme scheme = e.getProvider().get(); |
| if (!scheme.isEnabled() |
| || (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) { |
| continue; |
| } |
| if (!scheme.isAuthSupported() && !isWorldReadable) { |
| continue; |
| } |
| |
| String projectName = cd.project().get(); |
| String url = scheme.getUrl(projectName); |
| String refName = in.getRefName(); |
| FetchInfo fetchInfo = new FetchInfo(url, refName); |
| r.put(schemeName, fetchInfo); |
| |
| if (has(DOWNLOAD_COMMANDS)) { |
| populateFetchMap(scheme, downloadCommands, projectName, refName, fetchInfo); |
| } |
| } |
| |
| return r; |
| } |
| |
| public static void populateFetchMap( |
| DownloadScheme scheme, |
| DynamicMap<DownloadCommand> commands, |
| String projectName, |
| String refName, |
| FetchInfo fetchInfo) { |
| for (DynamicMap.Entry<DownloadCommand> e2 : commands) { |
| String commandName = e2.getExportName(); |
| DownloadCommand command = e2.getProvider().get(); |
| String c = command.getCommand(scheme, projectName, refName); |
| if (c != null) { |
| addCommand(fetchInfo, commandName, c); |
| } |
| } |
| } |
| |
| private static void addCommand(FetchInfo fetchInfo, String commandName, String c) { |
| if (fetchInfo.commands == null) { |
| fetchInfo.commands = new TreeMap<>(); |
| } |
| fetchInfo.commands.put(commandName, c); |
| } |
| |
| static void finish(ChangeInfo info) { |
| info.id = |
| Joiner.on('~') |
| .join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId)); |
| } |
| |
| private static void addApproval(LabelInfo label, ApprovalInfo approval) { |
| if (label.all == null) { |
| label.all = new ArrayList<>(); |
| } |
| label.all.add(approval); |
| } |
| |
| /** |
| * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed |
| * from either an index-backed or a database-backed {@link ChangeData} depending on {@code |
| * lazyload}. |
| */ |
| private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd) |
| throws OrmException { |
| PermissionBackend.WithUser withUser = permissionBackend.user(user).database(db); |
| return lazyLoad |
| ? withUser.change(cd) |
| : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change())); |
| } |
| |
| private boolean isWorldReadable(ChangeData cd) throws OrmException, PermissionBackendException { |
| try { |
| permissionBackendForChange(anonymous, cd).check(ChangePermission.READ); |
| return true; |
| } catch (AuthException ae) { |
| return false; |
| } |
| } |
| |
| @AutoValue |
| abstract static class LabelWithStatus { |
| private static LabelWithStatus create(LabelInfo label, SubmitRecord.Label.Status status) { |
| return new AutoValue_ChangeJson_LabelWithStatus(label, status); |
| } |
| |
| abstract LabelInfo label(); |
| |
| @Nullable |
| abstract SubmitRecord.Label.Status status(); |
| } |
| } |