com.google.gerrit.server.change.RevisionJson Maven / Gradle / Ivy
The newest version!
// Copyright (C) 2018 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.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.DETAILED_ACCOUNTS;
import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
import static com.google.gerrit.extensions.client.ListChangesOption.PARENTS;
import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
import static com.google.gerrit.server.CommonConverters.toGitPerson;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ParentCommitData;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.FetchInfo;
import com.google.gerrit.extensions.common.PushCertificateInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.RevisionInfo.ParentInfo;
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.registration.Extension;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtilFactory;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
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.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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;
/** Produces {@link RevisionInfo} and {@link CommitInfo} which are serialized to JSON afterwards. */
public class RevisionJson {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public interface Factory {
RevisionJson create(Iterable options);
}
@Singleton
private static class Metrics {
private final Timer0 parentDataLatency;
@Inject
Metrics(MetricMaker metricMaker) {
parentDataLatency =
metricMaker.newTimer(
"http/server/rest_api/change_json/to_change_info_latency/parent_data_computation",
new Description(
"Latency for computing parent data information in toRevisionInfo"
+ " invocations in RevisionJson")
.setCumulative()
.setUnit(Units.MILLISECONDS));
}
}
private final MergeUtilFactory mergeUtilFactory;
private final IdentifiedUser.GenericFactory userFactory;
private final FileInfoJson fileInfoJson;
private final GpgApiAdapter gpgApi;
private final ChangeResource.Factory changeResourceFactory;
private final ChangeKindCache changeKindCache;
private final ActionJson actionJson;
private final DynamicMap downloadSchemes;
private final DynamicMap downloadCommands;
private final WebLinks webLinks;
private final Provider userProvider;
private final ProjectCache projectCache;
private final ImmutableSet options;
private final AccountLoader.Factory accountLoaderFactory;
private final AnonymousUser anonymous;
private final GitRepositoryManager repoManager;
private final PermissionBackend permissionBackend;
private final ParentDataProvider parentDataProvider;
private final Metrics metrics;
@Inject
RevisionJson(
Provider userProvider,
AnonymousUser anonymous,
ProjectCache projectCache,
IdentifiedUser.GenericFactory userFactory,
MergeUtilFactory mergeUtilFactory,
FileInfoJson fileInfoJson,
AccountLoader.Factory accountLoaderFactory,
DynamicMap downloadSchemes,
DynamicMap downloadCommands,
WebLinks webLinks,
ActionJson actionJson,
GpgApiAdapter gpgApi,
ChangeResource.Factory changeResourceFactory,
ChangeKindCache changeKindCache,
GitRepositoryManager repoManager,
PermissionBackend permissionBackend,
ParentDataProvider parentDataProvider,
Metrics metrics,
@Assisted Iterable options) {
this.userProvider = userProvider;
this.anonymous = anonymous;
this.projectCache = projectCache;
this.userFactory = userFactory;
this.mergeUtilFactory = mergeUtilFactory;
this.fileInfoJson = fileInfoJson;
this.accountLoaderFactory = accountLoaderFactory;
this.downloadSchemes = downloadSchemes;
this.downloadCommands = downloadCommands;
this.webLinks = webLinks;
this.actionJson = actionJson;
this.gpgApi = gpgApi;
this.changeResourceFactory = changeResourceFactory;
this.changeKindCache = changeKindCache;
this.permissionBackend = permissionBackend;
this.repoManager = repoManager;
this.parentDataProvider = parentDataProvider;
this.metrics = metrics;
this.options = ImmutableSet.copyOf(options);
}
/**
* Returns a {@link RevisionInfo} based on a change and patch set. Reads from the repository
* depending on the options provided when constructing this instance.
*/
public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
AccountLoader accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
try (Repository repo = openRepoIfNecessary(cd.project());
RevWalk rw = newRevWalk(repo)) {
RevisionInfo rev = toRevisionInfo(accountLoader, cd, in, repo, rw, true, null);
accountLoader.fill();
return rev;
}
}
/**
* Returns a {@link CommitInfo} based on a commit and formatting options. Uses the provided
* RevWalk and assumes it is backed by an open repository.
*/
public CommitInfo getCommitInfo(
Project.NameKey project,
RevWalk rw,
RevCommit commit,
boolean addLinks,
boolean fillCommit,
String branchName,
String changeKey,
int numericChangeId)
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) {
ImmutableList patchSetLinks =
webLinks.getPatchSetLinks(
project,
commit.name(),
commit.getFullMessage(),
branchName,
changeKey,
numericChangeId);
info.webLinks = patchSetLinks.isEmpty() ? null : patchSetLinks;
ImmutableList resolveConflictsLinks =
webLinks.getResolveConflictsLinks(
project, commit.name(), commit.getFullMessage(), branchName);
info.resolveConflictsWebLinks =
resolveConflictsLinks.isEmpty() ? null : resolveConflictsLinks;
}
for (RevCommit parent : commit.getParents()) {
rw.parseBody(parent);
CommitInfo i = new CommitInfo();
i.commit = parent.name();
i.subject = parent.getShortMessage();
if (addLinks) {
ImmutableList parentLinks =
webLinks.getParentLinks(project, parent.name(), parent.getFullMessage(), branchName);
i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
}
info.parents.add(i);
}
return info;
}
/**
* Returns multiple {@link RevisionInfo}s for a single change. Uses the provided {@link
* AccountLoader} to lazily populate accounts. Callers have to call {@link AccountLoader#fill()}
* afterwards to populate all accounts in the returned {@link RevisionInfo}s.
*/
Map getRevisions(
AccountLoader accountLoader,
ChangeData cd,
Map map,
Optional limitToPsId,
ChangeInfo changeInfo)
throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
try (TraceTimer timer =
TraceContext.newTimer(
"Get revisions", Metadata.builder().changeId(cd.change().getId().get()).build())) {
Map res = new LinkedHashMap<>();
try (Repository repo = openRepoIfNecessary(cd.project());
RevWalk rw = newRevWalk(repo)) {
for (PatchSet in : map.values()) {
PatchSet.Id id = in.id();
boolean want;
if (has(ALL_REVISIONS)) {
want = true;
} else if (limitToPsId.isPresent()) {
want = id.equals(limitToPsId.get());
} else {
want = id.equals(cd.change().currentPatchSetId());
}
if (want) {
res.put(
in.commitId().name(),
toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
}
}
return res;
}
}
}
private Map makeFetchMap(ChangeData cd, PatchSet in)
throws PermissionBackendException {
Map r = new LinkedHashMap<>();
for (Extension e : downloadSchemes) {
String schemeName = e.getExportName();
DownloadScheme scheme = e.getProvider().get();
if (!scheme.isEnabled()
|| scheme.isHidden()
|| (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
continue;
}
if (!scheme.isAuthSupported() && !isWorldReadable(cd)) {
continue;
}
String projectName = cd.project().get();
String url = scheme.getUrl(projectName);
String refName = in.refName();
FetchInfo fetchInfo = new FetchInfo(url, refName);
r.put(schemeName, fetchInfo);
if (has(DOWNLOAD_COMMANDS)) {
DownloadCommandsJson.populateFetchMap(
scheme, downloadCommands, projectName, refName, fetchInfo);
}
}
return r;
}
private RevisionInfo toRevisionInfo(
AccountLoader accountLoader,
ChangeData cd,
PatchSet in,
@Nullable Repository repo,
@Nullable RevWalk rw,
boolean fillCommit,
@Nullable ChangeInfo changeInfo)
throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
Change c = cd.change();
RevisionInfo out = new RevisionInfo();
out.isCurrent = in.id().equals(c.currentPatchSetId());
out._number = in.id().get();
out.ref = in.refName();
out.setCreated(in.createdOn());
if (in.branch().isPresent()) {
// set the per-patch-set branch if it exists
out.branch = in.branch().get();
} else if (in.number() == cd.patchSets().size()) {
// only set the per-change branch on this patch-set if this is the last patch-set
out.branch = cd.change().getDest().branch();
}
out.uploader = accountLoader.get(in.uploader());
if (!in.uploader().equals(in.realUploader())) {
out.realUploader = accountLoader.get(in.realUploader());
}
out.fetch = makeFetchMap(cd, in);
out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
out.description = in.description().orElse(null);
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.commitId().name();
RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
rw.parseBody(commit);
String branchName = out.branch;
if (setCommit) {
out.commit =
getCommitInfo(
project,
rw,
commit,
has(WEB_LINKS),
fillCommit,
branchName,
c.getKey().get(),
c.getId().get());
}
if (has(PARENTS)) {
try (Timer0.Context ignored = metrics.parentDataLatency.start()) {
String targetBranch =
in.branch().isPresent() ? in.branch().get() : cd.change().getDest().branch();
List parentData = new ArrayList<>();
for (RevCommit parent : commit.getParents()) {
ParentCommitData p =
parentDataProvider.get(project, repo, parent.getId(), targetBranch);
parentData.add(p);
}
out.parentsData = getParentInfo(parentData);
}
}
if (addFooters) {
Ref ref = repo.exactRef(branchName);
RevCommit mergeTip = null;
if (ref != null) {
mergeTip = rw.parseCommit(ref.getObjectId());
rw.parseBody(mergeTip);
}
out.commitWithFooters =
mergeUtilFactory
.create(projectCache.get(project).orElseThrow(illegalState(project)))
.createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.id());
}
}
if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
try {
out.files = fileInfoJson.getFileInfoMap(c, in);
out.files.remove(Patch.COMMIT_MSG);
out.files.remove(Patch.MERGE_LIST);
} catch (ResourceConflictException e) {
logger.atWarning().withCause(e).log("creating file list failed");
}
}
if (out.isCurrent && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
actionJson.addRevisionActions(
changeInfo,
out,
new RevisionResource(changeResourceFactory.create(cd, userProvider.get()), in));
}
if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
if (in.pushCertificate().isPresent()) {
out.pushCertificate =
gpgApi.checkPushCertificate(
in.pushCertificate().get(), userFactory.create(in.uploader()));
} else {
out.pushCertificate = new PushCertificateInfo();
}
}
return out;
}
private boolean has(ListChangesOption option) {
return options.contains(option);
}
private boolean isWorldReadable(ChangeData cd) throws PermissionBackendException {
if (!permissionBackend.user(anonymous).change(cd).test(ChangePermission.READ)) {
return false;
}
ProjectState projectState =
projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
return projectState.statePermitsRead();
}
@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 static List getParentInfo(List parentsData) {
List result = new ArrayList<>();
for (ParentCommitData parentData : parentsData) {
ParentInfo parentInfo = new ParentInfo();
if (parentData.branchName().isPresent()) {
parentInfo.branchName = parentData.branchName().get();
}
if (parentData.commitId().isPresent()) {
parentInfo.commitId = parentData.commitId().get().name();
}
if (parentData.changeKey().isPresent()) {
parentInfo.changeId = parentData.changeKey().get().get();
}
if (parentData.changeNumber().isPresent()) {
parentInfo.changeNumber = parentData.changeNumber().get();
}
if (parentData.patchSetNumber().isPresent()) {
parentInfo.patchSetNumber = parentData.patchSetNumber().get();
}
if (parentData.changeStatus().isPresent()) {
parentInfo.changeStatus = parentData.changeStatus().get().name();
}
parentInfo.isMergedInTargetBranch = parentData.isMergedInTargetBranch();
result.add(parentInfo);
}
return result;
}
}