com.google.gerrit.server.change.ChangeJson Maven / Gradle / Ivy
// 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.AccountLoader;
import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gerrit.server.api.accounts.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);
// Submit rule options in this class should always use fastEvalLabels for
// efficiency reasons. Callers that care about submittability after taking
// vote squashing into account should be looking at the submit action.
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT.toBuilder().fastEvalLabels(true).build();
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().fastEvalLabels(true).build();
public static final ImmutableSet 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 options) {
return factory.create(options);
}
public ChangeJson create(ListChangesOption first, ListChangesOption... rest) {
return create(Sets.immutableEnumSet(first, rest));
}
}
public interface AssistedFactory {
ChangeJson create(Iterable options);
}
private final Provider db;
private final Provider 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 downloadSchemes;
private final DynamicMap downloadCommands;
private final WebLinks webLinks;
private final ImmutableSet options;
private final ChangeMessagesUtil cmUtil;
private final Provider 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 db,
Provider user,
AnonymousUser au,
PermissionBackend permissionBackend,
GitRepositoryManager repoManager,
ProjectCache projectCache,
MergeUtil.Factory mergeUtilFactory,
IdentifiedUser.GenericFactory uf,
ChangeData.Factory cdf,
FileInfoJson fileInfoJson,
AccountLoader.Factory ailf,
DynamicMap downloadSchemes,
DynamicMap downloadCommands,
WebLinks webLinks,
ChangeMessagesUtil cmUtil,
Provider checkerProvider,
ActionJson actionJson,
GpgApiAdapter gpgApi,
ChangeNotes.Factory notesFactory,
ChangeResource.Factory changeResourceFactory,
ChangeKindCache changeKindCache,
ChangeIndexCollection indexes,
ApprovalsUtil approvalsUtil,
RemoveReviewerControl removeReviewerControl,
TrackingFooters trackingFooters,
@Assisted Iterable 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 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> formatQueryResults(List> in)
throws OrmException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
ensureLoaded(FluentIterable.from(in).transformAndConcat(QueryResult::entities));
List> res = Lists.newArrayListWithCapacity(in.size());
Map out = new HashMap<>();
for (QueryResult r : in) {
List 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 formatChangeDatas(Collection in) throws OrmException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
ensureLoaded(in);
List out = new ArrayList<>(in.size());
for (ChangeData cd : in) {
out.add(format(cd));
}
accountLoader.fill();
return out;
}
private void ensureLoaded(Iterable 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 toChangeInfo(Map out, List changes) {
List 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 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 = 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 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 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 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 set = trackingFooters.extract(cd.commitFooters());
out.trackingIds =
set.entries().stream()
.map(e -> new TrackingIdInfo(e.getKey(), e.getValue()))
.collect(toList());
}
return out;
}
private Map> reviewerMap(
ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, boolean includeRemoved) {
Map> reviewerMap = new HashMap<>();
for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
if (!includeRemoved && state == ReviewerStateInternal.REMOVED) {
continue;
}
Collection reviewersByState = toAccountInfo(reviewers.byState(state));
reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state)));
if (!reviewersByState.isEmpty()) {
reviewerMap.put(state.asReviewerState(), reviewersByState);
}
}
return reviewerMap;
}
private Collection reviewerUpdates(ChangeData cd) throws OrmException {
List reviewerUpdates = cd.reviewerUpdates();
List 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 submitRecords(ChangeData cd) throws OrmException {
return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT);
}
private Map labelsFor(
PermissionBackend.ForChange perm, ChangeData cd, boolean standard, boolean detailed)
throws OrmException, PermissionBackendException {
if (!standard && !detailed) {
return null;
}
LabelTypes labelTypes = cd.getLabelTypes();
Map 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 labelsForUnsubmittedChange(
PermissionBackend.ForChange perm,
ChangeData cd,
LabelTypes labelTypes,
boolean standard,
boolean detailed)
throws OrmException, PermissionBackendException {
Map labels = initLabels(cd, labelTypes, standard);
if (detailed) {
setAllApprovals(perm, cd, labels);
}
for (Map.Entry 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 initLabels(
ChangeData cd, LabelTypes labelTypes, boolean standard) throws OrmException {
Map 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 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 allUsers = new HashSet<>();
allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
for (PatchSetApproval psa : cd.approvals().values()) {
allUsers.add(psa.getAccountId());
}
Table 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 pvr = getPermittedVotingRanges(permittedLabels(perm, cd));
for (Map.Entry 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 getPermittedVotingRanges(
Map> permittedLabels) {
Map permittedVotingRanges =
Maps.newHashMapWithExpectedSize(permittedLabels.size());
for (String label : permittedLabels.keySet()) {
List 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 s = cd.getSubmitApproval();
if (!s.isPresent()) {
return;
}
out.submitted = s.get().getGranted();
out.submitter = accountLoader.get(s.get().getAccountId());
}
private Map labelsForSubmittedChange(
PermissionBackend.ForChange basePerm,
ChangeData cd,
LabelTypes labelTypes,
boolean standard,
boolean detailed)
throws OrmException, PermissionBackendException {
Set 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 labelNames = new HashSet<>();
SetMultimap 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 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 byLabel = Maps.newHashMapWithExpectedSize(labels.size());
Map pvr = Collections.emptyMap();
if (detailed) {
PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId));
pvr = getPermittedVotingRanges(permittedLabels(perm, cd));
for (Map.Entry 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,
@Nullable VotingRangeInfo permittedVotingRange,
@Nullable 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 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> permittedLabels(
PermissionBackend.ForChange perm, ChangeData cd)
throws OrmException, PermissionBackendException {
boolean isMerged = cd.change().getStatus() == Change.Status.MERGED;
LabelTypes labelTypes = cd.getLabelTypes();
Map 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 labels = null;
Set can = perm.testLabels(toCheck.values());
SetMultimap 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 toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
for (Map.Entry> e : permitted.asMap().entrySet()) {
if (isOnlyZero(e.getValue())) {
toClear.add(e.getKey());
}
}
for (String label : toClear) {
permitted.removeAll(label);
}
return permitted.asMap();
}
private Map currentLabels(PermissionBackend.ForChange perm, ChangeData cd)
throws OrmException {
IdentifiedUser user = perm.user().asIdentifiedUser();
Map 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 messages(ChangeData cd) throws OrmException {
List messages = cmUtil.byChange(db.get(), cd.notes());
if (messages.isEmpty()) {
return Collections.emptyList();
}
List 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 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 labels = out.labels.values();
Set fixed = Sets.newHashSetWithExpectedSize(labels.size());
Set 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 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 result = Lists.newArrayListWithCapacity(removable.size());
for (Account.Id id : removable) {
result.add(accountLoader.get(id));
}
// Reviewers added by email are always removable
for (Collection infos : out.reviewers.values()) {
for (AccountInfo info : infos) {
if (info._accountId == null) {
result.add(info);
}
}
}
return result;
}
private Collection toAccountInfo(Collection accounts) {
return accounts.stream()
.map(accountLoader::get)
.sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
.collect(toList());
}
private Collection toAccountInfoByEmail(Collection 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 revisions(
ChangeData cd,
Map map,
Optional limitToPsId,
ChangeInfo changeInfo)
throws PatchListNotAvailableException, GpgException, OrmException, IOException,
PermissionBackendException {
Map 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 loadPatchSets(ChangeData cd, Optional limitToPsId)
throws OrmException {
Collection 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 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;
}
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 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 parentLinks = webLinks.getParentLinks(project, parent.name());
i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
}
info.parents.add(i);
}
return info;
}
private Map makeFetchMap(ChangeData cd, PatchSet in, boolean isWorldReadable) {
Map r = new LinkedHashMap<>();
for (DynamicMap.Entry 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 commands,
String projectName,
String refName,
FetchInfo fetchInfo) {
for (DynamicMap.Entry 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();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy