All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.restapi.change.Submit Maven / Gradle / Ivy

The newest version!
// 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.restapi.change;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.stream.Collectors.joining;

import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.BranchUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
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.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.submit.ChangeSet;
import com.google.gerrit.server.submit.MergeOp;
import com.google.gerrit.server.submit.MergeSuperSet;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;

/**
 * REST API handler for triggering submit of the specific revision of the change.
 *
 * 

See /Documentation/rest-api-changes.html#submit-revision for more information. * *

Even though the endpoint is defined for url including a revision, only revision corresponding * to the latest patch set is allowed. */ @Singleton public class Submit implements RestModifyView, UiAction { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final String DEFAULT_TOOLTIP = "Submit patch set ${patchSet} into ${branch}"; private static final String DEFAULT_TOOLTIP_ANCESTORS = "Submit patch set ${patchSet} and ancestors (${submitSize} changes " + "altogether) into ${branch}"; private static final String DEFAULT_TOPIC_TOOLTIP = "Submit all ${topicSize} changes of the same topic " + "(${submitSize} changes including ancestors and other " + "changes related by topic)"; private static final String CLICK_FAILURE_TOOLTIP = "Clicking the button would fail"; private static final String CHANGE_UNMERGEABLE = "Problems with integrating this change"; private final GitRepositoryManager repoManager; private final PermissionBackend permissionBackend; private final Provider mergeOpProvider; private final Provider mergeSuperSet; private final AccountResolver accountResolver; private final String label; private final String labelWithParents; private final ParameterizedString titlePattern; private final ParameterizedString titlePatternWithAncestors; private final String submitTopicLabel; private final ParameterizedString submitTopicTooltip; private final boolean submitWholeTopic; private final Provider queryProvider; private final PatchSetUtil psUtil; private final ProjectCache projectCache; private final ChangeJson.Factory json; private final boolean useMergeabilityCheck; @Inject Submit( GitRepositoryManager repoManager, PermissionBackend permissionBackend, Provider mergeOpProvider, Provider mergeSuperSet, AccountResolver accountResolver, @GerritServerConfig Config cfg, Provider queryProvider, PatchSetUtil psUtil, ProjectCache projectCache, ChangeJson.Factory json) { this.repoManager = repoManager; this.permissionBackend = permissionBackend; this.mergeOpProvider = mergeOpProvider; this.mergeSuperSet = mergeSuperSet; this.accountResolver = accountResolver; this.label = MoreObjects.firstNonNull( Strings.emptyToNull(cfg.getString("change", null, "submitLabel")), "Submit"); this.labelWithParents = MoreObjects.firstNonNull( Strings.emptyToNull(cfg.getString("change", null, "submitLabelWithParents")), "Submit including parents"); this.titlePattern = new ParameterizedString( MoreObjects.firstNonNull( cfg.getString("change", null, "submitTooltip"), DEFAULT_TOOLTIP)); this.titlePatternWithAncestors = new ParameterizedString( MoreObjects.firstNonNull( cfg.getString("change", null, "submitTooltipAncestors"), DEFAULT_TOOLTIP_ANCESTORS)); submitWholeTopic = MergeSuperSet.wholeTopicEnabled(cfg); this.submitTopicLabel = MoreObjects.firstNonNull( Strings.emptyToNull(cfg.getString("change", null, "submitTopicLabel")), "Submit whole topic"); this.submitTopicTooltip = new ParameterizedString( MoreObjects.firstNonNull( cfg.getString("change", null, "submitTopicTooltip"), DEFAULT_TOPIC_TOOLTIP)); this.queryProvider = queryProvider; this.psUtil = psUtil; this.projectCache = projectCache; this.json = json; this.useMergeabilityCheck = MergeabilityComputationBehavior.fromConfig(cfg).includeInApi(); } @Override public Response apply(RevisionResource rsrc, @Nullable SubmitInput input) throws RestApiException, RepositoryNotFoundException, IOException, PermissionBackendException, UpdateException, ConfigInvalidException { if (input == null) { input = new SubmitInput(); } input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf); IdentifiedUser submitter = rsrc.getUser().asIdentifiedUser(); // It's possible that the user does not have permission to submit all changes in the superset, // but we check the current change for an early exit. rsrc.permissions().check(ChangePermission.SUBMIT); if (input.onBehalfOf != null) { submitter = onBehalfOf(rsrc, input); } projectCache .get(rsrc.getProject()) .orElseThrow(illegalState(rsrc.getProject())) .checkStatePermitsWrite(); return Response.ok(json.noOptions().format(mergeChange(rsrc, submitter, input))); } @UsedAt(UsedAt.Project.GOOGLE) public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input) throws RestApiException, IOException, UpdateException, ConfigInvalidException, PermissionBackendException { Change change = rsrc.getChange(); if (!change.isNew()) { throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } else if (!BranchUtil.branchExists(repoManager, change.getDest())) { throw new ResourceConflictException( String.format("destination branch \"%s\" not found.", change.getDest().branch())); } else if (!rsrc.getPatchSet().id().equals(change.currentPatchSetId())) { // TODO Allow submitting non-current revision by changing the current. throw new ResourceConflictException( String.format( "revision %s is not current revision", rsrc.getPatchSet().commitId().name())); } try (MergeOp op = mergeOpProvider.get()) { Change updatedChange = op.merge(change, submitter, true, input, false); if (updatedChange.isMerged()) { return updatedChange; } throw new IllegalStateException( String.format( "change %s of project %s unexpectedly had status %s after submit attempt", updatedChange.getId(), updatedChange.getProject(), updatedChange.getStatus())); } } /** * Returns a message describing what prevents the current change from being submitted - or null. * This method only considers parent changes, and changes in the same topic. The caller is * responsible for making sure the current change to be submitted can indeed be submitted * (permissions, submit rules, is not a WIP...) * * @param cd the change the user is currently looking at * @param cs set of changes to be submitted at once * @param user the user who is checking to submit * @return a reason why any of the changes is not submittable or null */ @Nullable private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs, CurrentUser user) { Optional reason = MergeOp.checkCommonSubmitProblems(cd.change(), cs, false, permissionBackend, user).stream() .findFirst() .map(MergeOp.ChangeProblem::getProblem); if (reason.isPresent()) { return reason.get(); } try { if (!useMergeabilityCheck) { return null; } Collection unmergeable = getUnmergeableChanges(cs); if (unmergeable == null) { return CLICK_FAILURE_TOOLTIP; } else if (!unmergeable.isEmpty()) { for (ChangeData c : unmergeable) { if (c.change().getKey().equals(cd.change().getKey())) { return CHANGE_UNMERGEABLE; } } return "Problems with change(s): " + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", ")); } } catch (IOException e) { logger.atSevere().withCause(e).log("Error checking if change is submittable"); throw new StorageException("Could not determine problems for the change", e); } return null; } @Nullable @Override public UiAction.Description getDescription(RevisionResource resource) throws IOException, PermissionBackendException { Change change = resource.getChange(); if (!change.isNew() || !resource.isCurrent()) { return null; // submit not visible } if (!projectCache .get(resource.getProject()) .map(ProjectState::statePermitsWrite) .orElse(false)) { return null; // submit not visible } ChangeData cd = resource.getChangeResource().getChangeData(); try { MergeOp.checkSubmitRequirements(cd); } catch (ResourceConflictException e) { return null; // submit not visible } ChangeSet cs = mergeSuperSet .get() .completeChangeSet(cd.change(), resource.getUser(), /* includingTopicClosure= */ false); // Replace potentially stale ChangeData for the current change with the fresher one. cs = new ChangeSet( cs.changes().stream() .map(csChange -> csChange.getId().equals(cd.getId()) ? cd : csChange) .collect(toImmutableList()), cs.nonVisibleChanges()); String submitProblems = problemsForSubmittingChangeset(cd, cs, resource.getUser()); String topic = change.getTopic(); int topicSize = 0; if (!Strings.isNullOrEmpty(topic)) { topicSize = queryProvider.get().noFields().byTopicOpen(topic).size(); } boolean treatWithTopic = submitWholeTopic && !Strings.isNullOrEmpty(topic) && topicSize > 1; if (submitProblems != null) { return new UiAction.Description() .setLabel(treatWithTopic ? submitTopicLabel : (cs.size() > 1) ? labelWithParents : label) .setTitle(submitProblems) .setVisible(true) .setEnabled(false); } // Recheck mergeability rather than using value stored in the index, which may be stale. // TODO(dborowitz): This is ugly; consider providing a way to not read stored fields from the // index in the first place. // cd.setMergeable(null); // That was done in unmergeableChanges which was called by problemsForSubmittingChangeset, so // now it is safe to read from the cache, as it yields the same result. Boolean enabled = useMergeabilityCheck ? cd.isMergeable() : true; if (treatWithTopic) { ImmutableMap params = ImmutableMap.of( "topicSize", String.valueOf(topicSize), "submitSize", String.valueOf(cs.size())); return new UiAction.Description() .setLabel(submitTopicLabel) .setTitle(Strings.emptyToNull(submitTopicTooltip.replace(params))) .setVisible(true) .setEnabled(Boolean.TRUE.equals(enabled)); } ImmutableMap params = ImmutableMap.of( "patchSet", String.valueOf(resource.getPatchSet().number()), "branch", change.getDest().shortName(), "commit", abbreviateName(resource.getPatchSet().commitId()), "submitSize", String.valueOf(cs.size())); ParameterizedString tp = cs.size() > 1 ? titlePatternWithAncestors : titlePattern; return new UiAction.Description() .setLabel(cs.size() > 1 ? labelWithParents : label) .setTitle(Strings.emptyToNull(tp.replace(params))) .setVisible(true) .setEnabled(Boolean.TRUE.equals(enabled)); } @Nullable public Collection getUnmergeableChanges(ChangeSet cs) throws IOException { Set unmergeableChanges = new HashSet<>(); Set outDatedPatchSets = new HashSet<>(); for (ChangeData change : cs.changes()) { unmergeableChanges.add(change); addAllOutdatedPatchSets(outDatedPatchSets, change); } ListMultimap cbb = cs.changesByBranch(); for (BranchNameKey branch : cbb.keySet()) { List targetBranch = cbb.get(branch); HashMap commits = mapToCommits(targetBranch, branch.project()); Set allParents = commits.values().stream() .flatMap(c -> Arrays.stream(c.getParents())) .map(RevObject::getId) .collect(Collectors.toSet()); for (ChangeData change : targetBranch) { RevCommit commit = commits.get(change.getId()); boolean isMergeCommit = commit.getParentCount() > 1; boolean isLastInChain = !allParents.contains(commit.getId()); if (Arrays.stream(commit.getParents()).anyMatch(c -> outDatedPatchSets.contains(c.getId())) && !isCherryPickSubmit(change)) { // Found a parent that depends on an outdated patchset and the submit strategy is not // cherry-pick. continue; } // Recheck mergeability rather than using value stored in the index, // which may be stale. // TODO(dborowitz): This is ugly; consider providing a way to not read // stored fields from the index in the first place. change.setMergeable(null); Boolean mergeable = change.isMergeable(); if (mergeable == null) { // Skip whole check, cannot determine if mergeable return null; } if (mergeable) { unmergeableChanges.remove(change); } if (isLastInChain && isMergeCommit && mergeable) { targetBranch.stream().forEach(unmergeableChanges::remove); break; } } } return unmergeableChanges; } /** * Add all outdated patch-sets (non-last patch-sets) to the output set {@code outdatedPatchSets}. */ private static void addAllOutdatedPatchSets(Set outdatedPatchSets, ChangeData cd) { outdatedPatchSets.addAll( cd.notes().getPatchSets().values().stream() .map(p -> p.commitId()) .collect(Collectors.toSet())); outdatedPatchSets.remove(cd.currentPatchSet().commitId()); } private boolean isCherryPickSubmit(ChangeData changeData) { SubmitTypeRecord submitTypeRecord = changeData.submitTypeRecord(); return submitTypeRecord.isOk() && submitTypeRecord.type == SubmitType.CHERRY_PICK; } /** Map input {@code changes} to the commit SHA-1 of their latest patch-set. */ private HashMap mapToCommits( Collection changes, Project.NameKey project) throws IOException { HashMap commits = new HashMap<>(); try (Repository repo = repoManager.openRepository(project); RevWalk walk = new RevWalk(repo)) { for (ChangeData change : changes) { RevCommit commit = walk.parseCommit(psUtil.current(change.notes()).commitId()); commits.put(change.getId(), commit); } } return commits; } private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in) throws AuthException, UnprocessableEntityException, PermissionBackendException, IOException, ConfigInvalidException { PermissionBackend.ForChange perm = rsrc.permissions(); // It's possible that the current user or on-behalf-of user does not have permission for all // changes in the superset, but we check the current change for an early exit. perm.check(ChangePermission.SUBMIT_AS); CurrentUser caller = rsrc.getUser(); IdentifiedUser submitter = accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller); try { permissionBackend.user(submitter).change(rsrc.getNotes()).check(ChangePermission.READ); } catch (AuthException e) { throw new UnprocessableEntityException( String.format("on_behalf_of account %s cannot see change", submitter.getAccountId()), e); } logger.atFine().log( "Change %d is being submitted by %s on behalf of %s", rsrc.getChange().getChangeId(), rsrc.getUser().getUserName(), submitter.getUserName()); return submitter; } /** * Change-level REST API endpoint that calls submit for the latest revision on a change. * *

See /Documentation/rest-api-changes.html#submit-change for more information. */ public static class CurrentRevision implements RestModifyView { private final Submit submit; private final PatchSetUtil psUtil; @Inject CurrentRevision(Submit submit, PatchSetUtil psUtil) { this.submit = submit; this.psUtil = psUtil; } @Override public Response apply(ChangeResource rsrc, SubmitInput input) throws Exception { PatchSet ps = psUtil.current(rsrc.getNotes()); if (ps == null) { throw new ResourceConflictException("current revision is missing"); } return submit.apply(new RevisionResource(rsrc, ps), input); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy