diff options
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java')
-rw-r--r-- | gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java | 535 |
1 files changed, 0 insertions, 535 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java deleted file mode 100644 index 84ba88e071..0000000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java +++ /dev/null @@ -1,535 +0,0 @@ -// 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 java.util.stream.Collectors.joining; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Strings; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Sets; -import com.google.gerrit.common.data.ParameterizedString; -import com.google.gerrit.extensions.api.changes.SubmitInput; -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.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.reviewdb.client.Branch; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.ChangeMessage; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.client.RevId; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.ChangeMessagesUtil; -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.ProjectUtil; -import com.google.gerrit.server.account.AccountsCollection; -import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.git.ChangeSet; -import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.git.MergeOp; -import com.google.gerrit.server.git.MergeSuperSet; -import com.google.gerrit.server.notedb.ChangeNotes; -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.NoSuchChangeException; -import com.google.gerrit.server.query.change.ChangeData; -import com.google.gerrit.server.query.change.InternalChangeQuery; -import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.OrmRuntimeException; -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import java.io.IOException; -import java.util.Collection; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -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.RevWalk; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Singleton -public class Submit - implements RestModifyView<RevisionResource, SubmitInput>, UiAction<RevisionResource> { - private static final Logger log = LoggerFactory.getLogger(Submit.class); - - 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 BLOCKED_SUBMIT_TOOLTIP = - "This change depends on other changes which are not ready"; - private static final String BLOCKED_HIDDEN_SUBMIT_TOOLTIP = - "This change depends on other hidden changes which are not ready"; - private static final String BLOCKED_WORK_IN_PROGRESS = "This change is marked work in progress"; - private static final String CLICK_FAILURE_TOOLTIP = "Clicking the button would fail"; - private static final String CHANGE_UNMERGEABLE = "Problems with integrating this change"; - private static final String CHANGES_NOT_MERGEABLE = "Problems with change(s): "; - - public static class Output { - transient Change change; - - private Output(Change c) { - change = c; - } - } - - /** - * Subclass of {@link SubmitInput} with special bits that may be flipped for testing purposes - * only. - */ - @VisibleForTesting - public static class TestSubmitInput extends SubmitInput { - public boolean failAfterRefUpdates; - - /** - * For each change being submitted, an element is removed from this queue and, if the value is - * true, a bogus ref update is added to the batch, in order to generate a lock failure during - * execution. - */ - public Queue<Boolean> generateLockFailures; - } - - private final Provider<ReviewDb> dbProvider; - private final GitRepositoryManager repoManager; - private final PermissionBackend permissionBackend; - private final ChangeData.Factory changeDataFactory; - private final ChangeMessagesUtil cmUtil; - private final ChangeNotes.Factory changeNotesFactory; - private final Provider<MergeOp> mergeOpProvider; - private final Provider<MergeSuperSet> mergeSuperSet; - private final AccountsCollection accounts; - 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<InternalChangeQuery> queryProvider; - private final PatchSetUtil psUtil; - - @Inject - Submit( - Provider<ReviewDb> dbProvider, - GitRepositoryManager repoManager, - PermissionBackend permissionBackend, - ChangeData.Factory changeDataFactory, - ChangeMessagesUtil cmUtil, - ChangeNotes.Factory changeNotesFactory, - Provider<MergeOp> mergeOpProvider, - Provider<MergeSuperSet> mergeSuperSet, - AccountsCollection accounts, - @GerritServerConfig Config cfg, - Provider<InternalChangeQuery> queryProvider, - PatchSetUtil psUtil) { - this.dbProvider = dbProvider; - this.repoManager = repoManager; - this.permissionBackend = permissionBackend; - this.changeDataFactory = changeDataFactory; - this.cmUtil = cmUtil; - this.changeNotesFactory = changeNotesFactory; - this.mergeOpProvider = mergeOpProvider; - this.mergeSuperSet = mergeSuperSet; - this.accounts = accounts; - 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 = 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; - } - - @Override - public Output apply(RevisionResource rsrc, SubmitInput input) - throws RestApiException, RepositoryNotFoundException, IOException, OrmException, - PermissionBackendException, UpdateException, ConfigInvalidException { - input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf); - IdentifiedUser submitter; - if (input.onBehalfOf != null) { - submitter = onBehalfOf(rsrc, input); - } else { - rsrc.permissions().check(ChangePermission.SUBMIT); - submitter = rsrc.getUser().asIdentifiedUser(); - } - - return new Output(mergeChange(rsrc, submitter, input)); - } - - public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input) - throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException, - PermissionBackendException { - Change change = rsrc.getChange(); - if (!change.getStatus().isOpen()) { - throw new ResourceConflictException("change is " + ChangeUtil.status(change)); - } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) { - throw new ResourceConflictException( - String.format("destination branch \"%s\" not found.", change.getDest().get())); - } else if (!rsrc.getPatchSet().getId().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().getRevision().get())); - } - - try (MergeOp op = mergeOpProvider.get()) { - ReviewDb db = dbProvider.get(); - op.merge(db, change, submitter, true, input, false); - try { - change = - changeNotesFactory.createChecked(db, change.getProject(), change.getId()).getChange(); - } catch (NoSuchChangeException e) { - throw new ResourceConflictException("change is deleted"); - } - } - - switch (change.getStatus()) { - case MERGED: - return change; - case NEW: - ChangeMessage msg = getConflictMessage(rsrc); - if (msg != null) { - throw new ResourceConflictException(msg.getMessage()); - } - // $FALL-THROUGH$ - case ABANDONED: - default: - throw new ResourceConflictException("change is " + ChangeUtil.status(change)); - } - } - - /** - * @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 - */ - private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs, CurrentUser user) { - try { - if (cs.furtherHiddenChanges()) { - return BLOCKED_HIDDEN_SUBMIT_TOOLTIP; - } - for (ChangeData c : cs.changes()) { - Set<ChangePermission> can = - permissionBackend - .user(user) - .database(dbProvider) - .change(c) - .test(EnumSet.of(ChangePermission.READ, ChangePermission.SUBMIT)); - if (!can.contains(ChangePermission.READ)) { - return BLOCKED_HIDDEN_SUBMIT_TOOLTIP; - } - if (!can.contains(ChangePermission.SUBMIT)) { - return BLOCKED_SUBMIT_TOOLTIP; - } - if (c.change().isWorkInProgress()) { - return BLOCKED_WORK_IN_PROGRESS; - } - MergeOp.checkSubmitRule(c, false); - } - - Collection<ChangeData> unmergeable = unmergeableChanges(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 CHANGES_NOT_MERGEABLE - + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", ")); - } - } catch (ResourceConflictException e) { - return BLOCKED_SUBMIT_TOOLTIP; - } catch (PermissionBackendException | OrmException | IOException e) { - log.error("Error checking if change is submittable", e); - throw new OrmRuntimeException("Could not determine problems for the change", e); - } - return null; - } - - @Override - public UiAction.Description getDescription(RevisionResource resource) { - Change change = resource.getChange(); - if (!change.getStatus().isOpen() - || !resource.isCurrent() - || !resource.permissions().testOrFalse(ChangePermission.SUBMIT)) { - return null; // submit not visible - } - - ReviewDb db = dbProvider.get(); - ChangeData cd = changeDataFactory.create(db, resource.getNotes()); - try { - MergeOp.checkSubmitRule(cd, false); - } catch (ResourceConflictException e) { - return null; // submit not visible - } catch (OrmException e) { - log.error("Error checking if change is submittable", e); - throw new OrmRuntimeException("Could not determine problems for the change", e); - } - - ChangeSet cs; - try { - cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getUser()); - } catch (OrmException | IOException | PermissionBackendException e) { - throw new OrmRuntimeException( - "Could not determine complete set of changes to be submitted", e); - } - - String topic = change.getTopic(); - int topicSize = 0; - if (!Strings.isNullOrEmpty(topic)) { - topicSize = getChangesByTopic(topic).size(); - } - boolean treatWithTopic = submitWholeTopic && !Strings.isNullOrEmpty(topic) && topicSize > 1; - - String submitProblems = problemsForSubmittingChangeset(cd, cs, resource.getUser()); - - Boolean enabled; - try { - // 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. - enabled = cd.isMergeable(); - } catch (OrmException e) { - throw new OrmRuntimeException("Could not determine mergeability", e); - } - - if (submitProblems != null) { - return new UiAction.Description() - .setLabel(treatWithTopic ? submitTopicLabel : (cs.size() > 1) ? labelWithParents : label) - .setTitle(submitProblems) - .setVisible(true) - .setEnabled(false); - } - - if (treatWithTopic) { - Map<String, String> 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)); - } - RevId revId = resource.getPatchSet().getRevision(); - Map<String, String> params = - ImmutableMap.of( - "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()), - "branch", change.getDest().getShortName(), - "commit", ObjectId.fromString(revId.get()).abbreviate(7).name(), - "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)); - } - - /** - * If the merge was attempted and it failed the system usually writes a comment as a ChangeMessage - * and sets status to NEW. Find the relevant message and return it. - */ - public ChangeMessage getConflictMessage(RevisionResource rsrc) throws OrmException { - return FluentIterable.from( - cmUtil.byPatchSet(dbProvider.get(), rsrc.getNotes(), rsrc.getPatchSet().getId())) - .filter(cm -> cm.getAuthor() == null) - .last() - .orNull(); - } - - public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws OrmException, IOException { - Set<ChangeData> mergeabilityMap = new HashSet<>(); - for (ChangeData change : cs.changes()) { - mergeabilityMap.add(change); - } - - ListMultimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch(); - for (Branch.NameKey branch : cbb.keySet()) { - Collection<ChangeData> targetBranch = cbb.get(branch); - HashMap<Change.Id, RevCommit> commits = findCommits(targetBranch, branch.getParentKey()); - - Set<ObjectId> allParents = Sets.newHashSetWithExpectedSize(cs.size()); - for (RevCommit commit : commits.values()) { - for (RevCommit parent : commit.getParents()) { - allParents.add(parent.getId()); - } - } - - for (ChangeData change : targetBranch) { - RevCommit commit = commits.get(change.getId()); - boolean isMergeCommit = commit.getParentCount() > 1; - boolean isLastInChain = !allParents.contains(commit.getId()); - - // 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) { - mergeabilityMap.remove(change); - } - - if (isLastInChain && isMergeCommit && mergeable) { - for (ChangeData c : targetBranch) { - mergeabilityMap.remove(c); - } - break; - } - } - } - return mergeabilityMap; - } - - private HashMap<Change.Id, RevCommit> findCommits( - Collection<ChangeData> changes, Project.NameKey project) throws IOException, OrmException { - HashMap<Change.Id, RevCommit> commits = new HashMap<>(); - try (Repository repo = repoManager.openRepository(project); - RevWalk walk = new RevWalk(repo)) { - for (ChangeData change : changes) { - RevCommit commit = - walk.parseCommit( - ObjectId.fromString( - psUtil.current(dbProvider.get(), change.notes()).getRevision().get())); - commits.put(change.getId(), commit); - } - } - return commits; - } - - private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in) - throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException, - IOException, ConfigInvalidException { - PermissionBackend.ForChange perm = rsrc.permissions().database(dbProvider); - perm.check(ChangePermission.SUBMIT); - perm.check(ChangePermission.SUBMIT_AS); - - CurrentUser caller = rsrc.getUser(); - IdentifiedUser submitter = accounts.parseOnBehalfOf(caller, in.onBehalfOf); - try { - perm.user(submitter).check(ChangePermission.READ); - } catch (AuthException e) { - throw new UnprocessableEntityException( - String.format("on_behalf_of account %s cannot see change", submitter.getAccountId())); - } - return submitter; - } - - public static boolean wholeTopicEnabled(Config config) { - return config.getBoolean("change", null, "submitWholeTopic", false); - } - - private List<ChangeData> getChangesByTopic(String topic) { - try { - return queryProvider.get().byTopicOpen(topic); - } catch (OrmException e) { - throw new OrmRuntimeException(e); - } - } - - public static class CurrentRevision implements RestModifyView<ChangeResource, SubmitInput> { - private final Provider<ReviewDb> dbProvider; - private final Submit submit; - private final ChangeJson.Factory json; - private final PatchSetUtil psUtil; - - @Inject - CurrentRevision( - Provider<ReviewDb> dbProvider, - Submit submit, - ChangeJson.Factory json, - PatchSetUtil psUtil) { - this.dbProvider = dbProvider; - this.submit = submit; - this.json = json; - this.psUtil = psUtil; - } - - @Override - public ChangeInfo apply(ChangeResource rsrc, SubmitInput input) - throws RestApiException, RepositoryNotFoundException, IOException, OrmException, - PermissionBackendException, UpdateException, ConfigInvalidException { - PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes()); - if (ps == null) { - throw new ResourceConflictException("current revision is missing"); - } - - Output out = submit.apply(new RevisionResource(rsrc, ps), input); - return json.noOptions().format(out.change); - } - } -} |