LCOV - code coverage report
Current view: top level - server/restapi/change - Submit.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 196 221 88.7 %
Date: 2022-11-19 15:00:39 Functions: 15 15 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2012 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.restapi.change;
      16             : 
      17             : import static com.google.gerrit.git.ObjectIds.abbreviateName;
      18             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      19             : import static java.util.stream.Collectors.joining;
      20             : 
      21             : import com.google.common.base.MoreObjects;
      22             : import com.google.common.base.Strings;
      23             : import com.google.common.collect.ImmutableMap;
      24             : import com.google.common.collect.ListMultimap;
      25             : import com.google.common.collect.Sets;
      26             : import com.google.common.flogger.FluentLogger;
      27             : import com.google.gerrit.common.Nullable;
      28             : import com.google.gerrit.common.UsedAt;
      29             : import com.google.gerrit.common.data.ParameterizedString;
      30             : import com.google.gerrit.entities.BranchNameKey;
      31             : import com.google.gerrit.entities.Change;
      32             : import com.google.gerrit.entities.PatchSet;
      33             : import com.google.gerrit.entities.Project;
      34             : import com.google.gerrit.entities.SubmitTypeRecord;
      35             : import com.google.gerrit.exceptions.StorageException;
      36             : import com.google.gerrit.extensions.api.changes.SubmitInput;
      37             : import com.google.gerrit.extensions.client.SubmitType;
      38             : import com.google.gerrit.extensions.common.ChangeInfo;
      39             : import com.google.gerrit.extensions.restapi.AuthException;
      40             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      41             : import com.google.gerrit.extensions.restapi.Response;
      42             : import com.google.gerrit.extensions.restapi.RestApiException;
      43             : import com.google.gerrit.extensions.restapi.RestModifyView;
      44             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      45             : import com.google.gerrit.extensions.webui.UiAction;
      46             : import com.google.gerrit.server.ChangeUtil;
      47             : import com.google.gerrit.server.CurrentUser;
      48             : import com.google.gerrit.server.IdentifiedUser;
      49             : import com.google.gerrit.server.PatchSetUtil;
      50             : import com.google.gerrit.server.ProjectUtil;
      51             : import com.google.gerrit.server.account.AccountResolver;
      52             : import com.google.gerrit.server.change.ChangeJson;
      53             : import com.google.gerrit.server.change.ChangeResource;
      54             : import com.google.gerrit.server.change.RevisionResource;
      55             : import com.google.gerrit.server.config.GerritServerConfig;
      56             : import com.google.gerrit.server.git.GitRepositoryManager;
      57             : import com.google.gerrit.server.permissions.ChangePermission;
      58             : import com.google.gerrit.server.permissions.PermissionBackend;
      59             : import com.google.gerrit.server.permissions.PermissionBackendException;
      60             : import com.google.gerrit.server.project.ProjectCache;
      61             : import com.google.gerrit.server.project.ProjectState;
      62             : import com.google.gerrit.server.query.change.ChangeData;
      63             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      64             : import com.google.gerrit.server.submit.ChangeSet;
      65             : import com.google.gerrit.server.submit.MergeOp;
      66             : import com.google.gerrit.server.submit.MergeSuperSet;
      67             : import com.google.gerrit.server.update.UpdateException;
      68             : import com.google.inject.Inject;
      69             : import com.google.inject.Provider;
      70             : import com.google.inject.Singleton;
      71             : import java.io.IOException;
      72             : import java.util.Arrays;
      73             : import java.util.Collection;
      74             : import java.util.EnumSet;
      75             : import java.util.HashMap;
      76             : import java.util.HashSet;
      77             : import java.util.Map;
      78             : import java.util.Set;
      79             : import java.util.stream.Collectors;
      80             : import org.eclipse.jgit.errors.ConfigInvalidException;
      81             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      82             : import org.eclipse.jgit.lib.Config;
      83             : import org.eclipse.jgit.lib.ObjectId;
      84             : import org.eclipse.jgit.lib.Repository;
      85             : import org.eclipse.jgit.revwalk.RevCommit;
      86             : import org.eclipse.jgit.revwalk.RevWalk;
      87             : 
      88             : @Singleton
      89             : public class Submit
      90             :     implements RestModifyView<RevisionResource, SubmitInput>, UiAction<RevisionResource> {
      91         145 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      92             : 
      93             :   private static final String DEFAULT_TOOLTIP = "Submit patch set ${patchSet} into ${branch}";
      94             :   private static final String DEFAULT_TOOLTIP_ANCESTORS =
      95             :       "Submit patch set ${patchSet} and ancestors (${submitSize} changes "
      96             :           + "altogether) into ${branch}";
      97             :   private static final String DEFAULT_TOPIC_TOOLTIP =
      98             :       "Submit all ${topicSize} changes of the same topic "
      99             :           + "(${submitSize} changes including ancestors and other "
     100             :           + "changes related by topic)";
     101             :   private static final String BLOCKED_HIDDEN_SUBMIT_TOOLTIP =
     102             :       "This change depends on other hidden changes which are not ready";
     103             :   private static final String CLICK_FAILURE_TOOLTIP = "Clicking the button would fail";
     104             :   private static final String CHANGE_UNMERGEABLE = "Problems with integrating this change";
     105             : 
     106             :   private final GitRepositoryManager repoManager;
     107             :   private final PermissionBackend permissionBackend;
     108             :   private final Provider<MergeOp> mergeOpProvider;
     109             :   private final Provider<MergeSuperSet> mergeSuperSet;
     110             :   private final AccountResolver accountResolver;
     111             :   private final String label;
     112             :   private final String labelWithParents;
     113             :   private final ParameterizedString titlePattern;
     114             :   private final ParameterizedString titlePatternWithAncestors;
     115             :   private final String submitTopicLabel;
     116             :   private final ParameterizedString submitTopicTooltip;
     117             :   private final boolean submitWholeTopic;
     118             :   private final Provider<InternalChangeQuery> queryProvider;
     119             :   private final PatchSetUtil psUtil;
     120             :   private final ProjectCache projectCache;
     121             :   private final ChangeJson.Factory json;
     122             : 
     123             :   @Inject
     124             :   Submit(
     125             :       GitRepositoryManager repoManager,
     126             :       PermissionBackend permissionBackend,
     127             :       Provider<MergeOp> mergeOpProvider,
     128             :       Provider<MergeSuperSet> mergeSuperSet,
     129             :       AccountResolver accountResolver,
     130             :       @GerritServerConfig Config cfg,
     131             :       Provider<InternalChangeQuery> queryProvider,
     132             :       PatchSetUtil psUtil,
     133             :       ProjectCache projectCache,
     134         145 :       ChangeJson.Factory json) {
     135         145 :     this.repoManager = repoManager;
     136         145 :     this.permissionBackend = permissionBackend;
     137         145 :     this.mergeOpProvider = mergeOpProvider;
     138         145 :     this.mergeSuperSet = mergeSuperSet;
     139         145 :     this.accountResolver = accountResolver;
     140         145 :     this.label =
     141         145 :         MoreObjects.firstNonNull(
     142         145 :             Strings.emptyToNull(cfg.getString("change", null, "submitLabel")), "Submit");
     143         145 :     this.labelWithParents =
     144         145 :         MoreObjects.firstNonNull(
     145         145 :             Strings.emptyToNull(cfg.getString("change", null, "submitLabelWithParents")),
     146             :             "Submit including parents");
     147         145 :     this.titlePattern =
     148             :         new ParameterizedString(
     149         145 :             MoreObjects.firstNonNull(
     150         145 :                 cfg.getString("change", null, "submitTooltip"), DEFAULT_TOOLTIP));
     151         145 :     this.titlePatternWithAncestors =
     152             :         new ParameterizedString(
     153         145 :             MoreObjects.firstNonNull(
     154         145 :                 cfg.getString("change", null, "submitTooltipAncestors"),
     155             :                 DEFAULT_TOOLTIP_ANCESTORS));
     156         145 :     submitWholeTopic = MergeSuperSet.wholeTopicEnabled(cfg);
     157         145 :     this.submitTopicLabel =
     158         145 :         MoreObjects.firstNonNull(
     159         145 :             Strings.emptyToNull(cfg.getString("change", null, "submitTopicLabel")),
     160             :             "Submit whole topic");
     161         145 :     this.submitTopicTooltip =
     162             :         new ParameterizedString(
     163         145 :             MoreObjects.firstNonNull(
     164         145 :                 cfg.getString("change", null, "submitTopicTooltip"), DEFAULT_TOPIC_TOOLTIP));
     165         145 :     this.queryProvider = queryProvider;
     166         145 :     this.psUtil = psUtil;
     167         145 :     this.projectCache = projectCache;
     168         145 :     this.json = json;
     169         145 :   }
     170             : 
     171             :   @Override
     172             :   public Response<ChangeInfo> apply(RevisionResource rsrc, @Nullable SubmitInput input)
     173             :       throws RestApiException, RepositoryNotFoundException, IOException, PermissionBackendException,
     174             :           UpdateException, ConfigInvalidException {
     175          46 :     if (input == null) {
     176           0 :       input = new SubmitInput();
     177             :     }
     178          46 :     input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
     179             :     IdentifiedUser submitter;
     180          46 :     if (input.onBehalfOf != null) {
     181           2 :       submitter = onBehalfOf(rsrc, input);
     182             :     } else {
     183          45 :       rsrc.permissions().check(ChangePermission.SUBMIT);
     184          45 :       submitter = rsrc.getUser().asIdentifiedUser();
     185             :     }
     186          46 :     projectCache
     187          46 :         .get(rsrc.getProject())
     188          46 :         .orElseThrow(illegalState(rsrc.getProject()))
     189          46 :         .checkStatePermitsWrite();
     190             : 
     191          46 :     return Response.ok(json.noOptions().format(mergeChange(rsrc, submitter, input)));
     192             :   }
     193             : 
     194             :   @UsedAt(UsedAt.Project.GOOGLE)
     195             :   public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
     196             :       throws RestApiException, IOException, UpdateException, ConfigInvalidException,
     197             :           PermissionBackendException {
     198          46 :     Change change = rsrc.getChange();
     199          46 :     if (!change.isNew()) {
     200           0 :       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     201          46 :     } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
     202           0 :       throw new ResourceConflictException(
     203           0 :           String.format("destination branch \"%s\" not found.", change.getDest().branch()));
     204          46 :     } else if (!rsrc.getPatchSet().id().equals(change.currentPatchSetId())) {
     205             :       // TODO Allow submitting non-current revision by changing the current.
     206           0 :       throw new ResourceConflictException(
     207           0 :           String.format(
     208           0 :               "revision %s is not current revision", rsrc.getPatchSet().commitId().name()));
     209             :     }
     210             : 
     211          46 :     try (MergeOp op = mergeOpProvider.get()) {
     212             :       Change updatedChange;
     213             : 
     214          46 :       updatedChange = op.merge(change, submitter, true, input, false);
     215          46 :       if (updatedChange.isMerged()) {
     216          46 :         return updatedChange;
     217             :       }
     218             : 
     219           0 :       throw new IllegalStateException(
     220           0 :           String.format(
     221             :               "change %s of project %s unexpectedly had status %s after submit attempt",
     222           0 :               updatedChange.getId(), updatedChange.getProject(), updatedChange.getStatus()));
     223             :     }
     224             :   }
     225             : 
     226             :   /**
     227             :    * Returns a message describing what prevents the current change from being submitted - or null.
     228             :    * This method only considers parent changes, and changes in the same topic. The caller is
     229             :    * responsible for making sure the current change to be submitted can indeed be submitted
     230             :    * (permissions, submit rules, is not a WIP...)
     231             :    *
     232             :    * @param cd the change the user is currently looking at
     233             :    * @param cs set of changes to be submitted at once
     234             :    * @param user the user who is checking to submit
     235             :    * @return a reason why any of the changes is not submittable or null
     236             :    */
     237             :   @Nullable
     238             :   private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs, CurrentUser user) {
     239             :     try {
     240          18 :       if (cs.furtherHiddenChanges()) {
     241           0 :         logger.atFine().log(
     242             :             "Change %d cannot be submitted by user %s because it depends on hidden changes: %s",
     243           0 :             cd.getId().get(), user.getLoggableName(), cs.nonVisibleChanges());
     244           0 :         return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
     245             :       }
     246          18 :       for (ChangeData c : cs.changes()) {
     247          18 :         Set<ChangePermission> can =
     248             :             permissionBackend
     249          18 :                 .user(user)
     250          18 :                 .change(c)
     251          18 :                 .test(EnumSet.of(ChangePermission.READ, ChangePermission.SUBMIT));
     252          18 :         if (!can.contains(ChangePermission.READ)) {
     253           0 :           logger.atFine().log(
     254             :               "Change %d cannot be submitted by user %s because it depends on change %d which the user cannot read",
     255           0 :               cd.getId().get(), user.getLoggableName(), c.getId().get());
     256           0 :           return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
     257             :         }
     258          18 :         if (!can.contains(ChangePermission.SUBMIT)) {
     259           2 :           return "You don't have permission to submit change " + c.getId();
     260             :         }
     261          18 :         if (c.change().isWorkInProgress()) {
     262           0 :           return "Change " + c.getId() + " is marked work in progress";
     263             :         }
     264             :         try {
     265             :           // The data in the change index may be stale (e.g. if submit requirements have been
     266             :           // changed). For that one change for which the submit action is computed, use the
     267             :           // freshly loaded ChangeData instance 'cd' instead of the potentially stale ChangeData
     268             :           // instance 'c' that was loaded from the index. This makes a difference if the ChangeSet
     269             :           // 'cs' only contains this one single change. If the ChangeSet contains further changes
     270             :           // those may still be stale.
     271          18 :           MergeOp.checkSubmitRequirements(cd.getId().equals(c.getId()) ? cd : c);
     272           1 :         } catch (ResourceConflictException e) {
     273           1 :           return (c.getId() == cd.getId())
     274           0 :               ? String.format("Change %s is not ready: %s", cd.getId(), e.getMessage())
     275           1 :               : String.format(
     276             :                   "Change %s must be submitted with change %s but %s is not ready: %s",
     277           1 :                   cd.getId(), c.getId(), c.getId(), e.getMessage());
     278          18 :         }
     279          18 :       }
     280             : 
     281          18 :       Collection<ChangeData> unmergeable = unmergeableChanges(cs);
     282          18 :       if (unmergeable == null) {
     283           0 :         return CLICK_FAILURE_TOOLTIP;
     284          18 :       } else if (!unmergeable.isEmpty()) {
     285           6 :         for (ChangeData c : unmergeable) {
     286           6 :           if (c.change().getKey().equals(cd.change().getKey())) {
     287           5 :             return CHANGE_UNMERGEABLE;
     288             :           }
     289           1 :         }
     290             : 
     291           1 :         return "Problems with change(s): "
     292           1 :             + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", "));
     293             :       }
     294           0 :     } catch (PermissionBackendException | IOException e) {
     295           0 :       logger.atSevere().withCause(e).log("Error checking if change is submittable");
     296           0 :       throw new StorageException("Could not determine problems for the change", e);
     297          18 :     }
     298          18 :     return null;
     299             :   }
     300             : 
     301             :   @Nullable
     302             :   @Override
     303             :   public UiAction.Description getDescription(RevisionResource resource)
     304             :       throws IOException, PermissionBackendException {
     305          57 :     Change change = resource.getChange();
     306          57 :     if (!change.isNew() || !resource.isCurrent()) {
     307          24 :       return null; // submit not visible
     308             :     }
     309          52 :     if (!projectCache
     310          52 :         .get(resource.getProject())
     311          52 :         .map(ProjectState::statePermitsWrite)
     312          52 :         .orElse(false)) {
     313           0 :       return null; // submit not visible
     314             :     }
     315             : 
     316          52 :     ChangeData cd = resource.getChangeResource().getChangeData();
     317             :     try {
     318          18 :       MergeOp.checkSubmitRequirements(cd);
     319          52 :     } catch (ResourceConflictException e) {
     320          52 :       return null; // submit not visible
     321          18 :     }
     322             : 
     323          18 :     ChangeSet cs =
     324             :         mergeSuperSet
     325          18 :             .get()
     326          18 :             .completeChangeSet(cd.change(), resource.getUser(), /*includingTopicClosure= */ false);
     327          18 :     String topic = change.getTopic();
     328          18 :     int topicSize = 0;
     329          18 :     if (!Strings.isNullOrEmpty(topic)) {
     330           9 :       topicSize = queryProvider.get().noFields().byTopicOpen(topic).size();
     331             :     }
     332          18 :     boolean treatWithTopic = submitWholeTopic && !Strings.isNullOrEmpty(topic) && topicSize > 1;
     333             : 
     334          18 :     String submitProblems = problemsForSubmittingChangeset(cd, cs, resource.getUser());
     335             : 
     336          18 :     if (submitProblems != null) {
     337           7 :       return new UiAction.Description()
     338           7 :           .setLabel(treatWithTopic ? submitTopicLabel : (cs.size() > 1) ? labelWithParents : label)
     339           7 :           .setTitle(submitProblems)
     340           7 :           .setVisible(true)
     341           7 :           .setEnabled(false);
     342             :     }
     343             : 
     344             :     // Recheck mergeability rather than using value stored in the index, which may be stale.
     345             :     // TODO(dborowitz): This is ugly; consider providing a way to not read stored fields from the
     346             :     // index in the first place.
     347             :     // cd.setMergeable(null);
     348             :     // That was done in unmergeableChanges which was called by problemsForSubmittingChangeset, so
     349             :     // now it is safe to read from the cache, as it yields the same result.
     350          18 :     Boolean enabled = cd.isMergeable();
     351             : 
     352          18 :     if (treatWithTopic) {
     353           7 :       Map<String, String> params =
     354           7 :           ImmutableMap.of(
     355           7 :               "topicSize", String.valueOf(topicSize),
     356           7 :               "submitSize", String.valueOf(cs.size()));
     357           7 :       return new UiAction.Description()
     358           7 :           .setLabel(submitTopicLabel)
     359           7 :           .setTitle(Strings.emptyToNull(submitTopicTooltip.replace(params)))
     360           7 :           .setVisible(true)
     361           7 :           .setEnabled(Boolean.TRUE.equals(enabled));
     362             :     }
     363          18 :     Map<String, String> params =
     364          18 :         ImmutableMap.of(
     365          18 :             "patchSet", String.valueOf(resource.getPatchSet().number()),
     366          18 :             "branch", change.getDest().shortName(),
     367          18 :             "commit", abbreviateName(resource.getPatchSet().commitId()),
     368          18 :             "submitSize", String.valueOf(cs.size()));
     369          18 :     ParameterizedString tp = cs.size() > 1 ? titlePatternWithAncestors : titlePattern;
     370          18 :     return new UiAction.Description()
     371          18 :         .setLabel(cs.size() > 1 ? labelWithParents : label)
     372          18 :         .setTitle(Strings.emptyToNull(tp.replace(params)))
     373          18 :         .setVisible(true)
     374          18 :         .setEnabled(Boolean.TRUE.equals(enabled));
     375             :   }
     376             : 
     377             :   @Nullable
     378             :   public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws IOException {
     379          19 :     Set<ChangeData> mergeabilityMap = new HashSet<>();
     380          19 :     Set<ObjectId> outDatedPatchsets = new HashSet<>();
     381          19 :     for (ChangeData change : cs.changes()) {
     382          19 :       mergeabilityMap.add(change);
     383             :       // Add all the patchsets commit ids except the current patchset.
     384          19 :       outDatedPatchsets.addAll(
     385          19 :           change.notes().getPatchSets().values().stream()
     386          19 :               .map(p -> p.commitId())
     387          19 :               .collect(Collectors.toSet()));
     388          19 :       outDatedPatchsets.remove(change.currentPatchSet().commitId());
     389          19 :     }
     390             : 
     391          19 :     ListMultimap<BranchNameKey, ChangeData> cbb = cs.changesByBranch();
     392          19 :     for (BranchNameKey branch : cbb.keySet()) {
     393          19 :       Collection<ChangeData> targetBranch = cbb.get(branch);
     394          19 :       HashMap<Change.Id, RevCommit> commits = findCommits(targetBranch, branch.project());
     395             : 
     396          19 :       Set<ObjectId> allParents = Sets.newHashSetWithExpectedSize(cs.size());
     397          19 :       for (RevCommit commit : commits.values()) {
     398          19 :         for (RevCommit parent : commit.getParents()) {
     399          15 :           allParents.add(parent.getId());
     400             :         }
     401          19 :       }
     402          19 :       for (ChangeData change : targetBranch) {
     403             : 
     404          19 :         RevCommit commit = commits.get(change.getId());
     405          19 :         boolean isMergeCommit = commit.getParentCount() > 1;
     406          19 :         boolean isLastInChain = !allParents.contains(commit.getId());
     407          19 :         if (Arrays.stream(commit.getParents()).anyMatch(c -> outDatedPatchsets.contains(c.getId()))
     408           5 :             && !isCherryPickSubmit(change)) {
     409             :           // Found a parent that depends on an outdated patchset and the submit strategy is not
     410             :           // cherry-pick.
     411           5 :           continue;
     412             :         }
     413             :         // Recheck mergeability rather than using value stored in the index,
     414             :         // which may be stale.
     415             :         // TODO(dborowitz): This is ugly; consider providing a way to not read
     416             :         // stored fields from the index in the first place.
     417          19 :         change.setMergeable(null);
     418          19 :         Boolean mergeable = change.isMergeable();
     419          19 :         if (mergeable == null) {
     420             :           // Skip whole check, cannot determine if mergeable
     421           0 :           return null;
     422             :         }
     423          19 :         if (mergeable) {
     424          19 :           mergeabilityMap.remove(change);
     425             :         }
     426             : 
     427          19 :         if (isLastInChain && isMergeCommit && mergeable) {
     428           7 :           for (ChangeData c : targetBranch) {
     429           7 :             mergeabilityMap.remove(c);
     430           7 :           }
     431           7 :           break;
     432             :         }
     433          19 :       }
     434          19 :     }
     435          19 :     return mergeabilityMap;
     436             :   }
     437             : 
     438             :   private boolean isCherryPickSubmit(ChangeData changeData) {
     439           5 :     SubmitTypeRecord submitTypeRecord = changeData.submitTypeRecord();
     440           5 :     return submitTypeRecord.isOk() && submitTypeRecord.type == SubmitType.CHERRY_PICK;
     441             :   }
     442             : 
     443             :   private HashMap<Change.Id, RevCommit> findCommits(
     444             :       Collection<ChangeData> changes, Project.NameKey project) throws IOException {
     445          19 :     HashMap<Change.Id, RevCommit> commits = new HashMap<>();
     446          19 :     try (Repository repo = repoManager.openRepository(project);
     447          19 :         RevWalk walk = new RevWalk(repo)) {
     448          19 :       for (ChangeData change : changes) {
     449          19 :         RevCommit commit = walk.parseCommit(psUtil.current(change.notes()).commitId());
     450          19 :         commits.put(change.getId(), commit);
     451          19 :       }
     452             :     }
     453          19 :     return commits;
     454             :   }
     455             : 
     456             :   private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in)
     457             :       throws AuthException, UnprocessableEntityException, PermissionBackendException, IOException,
     458             :           ConfigInvalidException {
     459           2 :     PermissionBackend.ForChange perm = rsrc.permissions();
     460           2 :     perm.check(ChangePermission.SUBMIT);
     461           2 :     perm.check(ChangePermission.SUBMIT_AS);
     462             : 
     463           2 :     CurrentUser caller = rsrc.getUser();
     464           2 :     IdentifiedUser submitter =
     465           2 :         accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
     466             :     try {
     467           2 :       permissionBackend.user(submitter).change(rsrc.getNotes()).check(ChangePermission.READ);
     468           1 :     } catch (AuthException e) {
     469           1 :       throw new UnprocessableEntityException(
     470           1 :           String.format("on_behalf_of account %s cannot see change", submitter.getAccountId()), e);
     471           2 :     }
     472           2 :     return submitter;
     473             :   }
     474             : 
     475             :   public static class CurrentRevision implements RestModifyView<ChangeResource, SubmitInput> {
     476             :     private final Submit submit;
     477             :     private final PatchSetUtil psUtil;
     478             : 
     479             :     @Inject
     480          57 :     CurrentRevision(Submit submit, PatchSetUtil psUtil) {
     481          57 :       this.submit = submit;
     482          57 :       this.psUtil = psUtil;
     483          57 :     }
     484             : 
     485             :     @Override
     486             :     public Response<ChangeInfo> apply(ChangeResource rsrc, SubmitInput input) throws Exception {
     487           1 :       PatchSet ps = psUtil.current(rsrc.getNotes());
     488           1 :       if (ps == null) {
     489           0 :         throw new ResourceConflictException("current revision is missing");
     490             :       }
     491             : 
     492           1 :       return submit.apply(new RevisionResource(rsrc, ps), input);
     493             :     }
     494             :   }
     495             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750