LCOV - code coverage report
Current view: top level - server/git/receive - ReceiveCommits.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 1572 1814 86.7 %
Date: 2022-11-19 15:00:39 Functions: 163 170 95.9 %

          Line data    Source code
       1             : // Copyright (C) 2008 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.git.receive;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.base.Preconditions.checkState;
      19             : import static com.google.common.collect.ImmutableList.toImmutableList;
      20             : import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
      21             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      22             : import static com.google.common.flogger.LazyArgs.lazy;
      23             : import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
      24             : import static com.google.gerrit.entities.RefNames.isConfigRef;
      25             : import static com.google.gerrit.entities.RefNames.isRefsUsersSelf;
      26             : import static com.google.gerrit.git.ObjectIds.abbreviateName;
      27             : import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
      28             : import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
      29             : import static com.google.gerrit.server.git.receive.ReceiveConstants.COMMAND_REJECTION_MESSAGE_FOOTER;
      30             : import static com.google.gerrit.server.git.receive.ReceiveConstants.ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP;
      31             : import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
      32             : import static com.google.gerrit.server.git.receive.ReceiveConstants.SAME_CHANGE_ID_IN_MULTIPLE_CHANGES;
      33             : import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
      34             : import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
      35             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      36             : import static java.nio.charset.StandardCharsets.UTF_8;
      37             : import static java.util.Objects.requireNonNull;
      38             : import static java.util.stream.Collectors.joining;
      39             : import static java.util.stream.Collectors.toList;
      40             : import static org.eclipse.jgit.lib.Constants.R_HEADS;
      41             : import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
      42             : import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
      43             : import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
      44             : import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
      45             : 
      46             : import com.google.common.base.Joiner;
      47             : import com.google.common.base.Splitter;
      48             : import com.google.common.base.Strings;
      49             : import com.google.common.base.Throwables;
      50             : import com.google.common.collect.BiMap;
      51             : import com.google.common.collect.HashBiMap;
      52             : import com.google.common.collect.ImmutableList;
      53             : import com.google.common.collect.ImmutableListMultimap;
      54             : import com.google.common.collect.ImmutableMap;
      55             : import com.google.common.collect.ImmutableSet;
      56             : import com.google.common.collect.ImmutableSetMultimap;
      57             : import com.google.common.collect.Iterables;
      58             : import com.google.common.collect.LinkedListMultimap;
      59             : import com.google.common.collect.ListMultimap;
      60             : import com.google.common.collect.Lists;
      61             : import com.google.common.collect.Maps;
      62             : import com.google.common.collect.MultimapBuilder;
      63             : import com.google.common.collect.Sets;
      64             : import com.google.common.collect.SortedSetMultimap;
      65             : import com.google.common.collect.Streams;
      66             : import com.google.common.flogger.FluentLogger;
      67             : import com.google.gerrit.common.Nullable;
      68             : import com.google.gerrit.common.UsedAt;
      69             : import com.google.gerrit.entities.Account;
      70             : import com.google.gerrit.entities.BooleanProjectConfig;
      71             : import com.google.gerrit.entities.BranchNameKey;
      72             : import com.google.gerrit.entities.Change;
      73             : import com.google.gerrit.entities.HumanComment;
      74             : import com.google.gerrit.entities.LabelType;
      75             : import com.google.gerrit.entities.LabelTypes;
      76             : import com.google.gerrit.entities.PatchSet;
      77             : import com.google.gerrit.entities.PatchSetInfo;
      78             : import com.google.gerrit.entities.Project;
      79             : import com.google.gerrit.entities.RefNames;
      80             : import com.google.gerrit.entities.SubmissionId;
      81             : import com.google.gerrit.exceptions.StorageException;
      82             : import com.google.gerrit.extensions.api.changes.HashtagsInput;
      83             : import com.google.gerrit.extensions.api.changes.NotifyHandling;
      84             : import com.google.gerrit.extensions.api.changes.NotifyInfo;
      85             : import com.google.gerrit.extensions.api.changes.RecipientType;
      86             : import com.google.gerrit.extensions.api.changes.SubmitInput;
      87             : import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
      88             : import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
      89             : import com.google.gerrit.extensions.registration.DynamicItem;
      90             : import com.google.gerrit.extensions.registration.DynamicMap;
      91             : import com.google.gerrit.extensions.registration.DynamicSet;
      92             : import com.google.gerrit.extensions.registration.Extension;
      93             : import com.google.gerrit.extensions.restapi.AuthException;
      94             : import com.google.gerrit.extensions.restapi.BadRequestException;
      95             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      96             : import com.google.gerrit.extensions.restapi.RestApiException;
      97             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      98             : import com.google.gerrit.extensions.validators.CommentForValidation;
      99             : import com.google.gerrit.extensions.validators.CommentForValidation.CommentSource;
     100             : import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
     101             : import com.google.gerrit.extensions.validators.CommentValidationContext;
     102             : import com.google.gerrit.extensions.validators.CommentValidationFailure;
     103             : import com.google.gerrit.extensions.validators.CommentValidator;
     104             : import com.google.gerrit.metrics.Counter0;
     105             : import com.google.gerrit.metrics.Counter3;
     106             : import com.google.gerrit.metrics.Description;
     107             : import com.google.gerrit.metrics.Field;
     108             : import com.google.gerrit.metrics.MetricMaker;
     109             : import com.google.gerrit.server.CancellationMetrics;
     110             : import com.google.gerrit.server.ChangeUtil;
     111             : import com.google.gerrit.server.CommentsUtil;
     112             : import com.google.gerrit.server.CreateGroupPermissionSyncer;
     113             : import com.google.gerrit.server.DeadlineChecker;
     114             : import com.google.gerrit.server.IdentifiedUser;
     115             : import com.google.gerrit.server.InvalidDeadlineException;
     116             : import com.google.gerrit.server.PatchSetUtil;
     117             : import com.google.gerrit.server.PublishCommentUtil;
     118             : import com.google.gerrit.server.PublishCommentsOp;
     119             : import com.google.gerrit.server.RequestInfo;
     120             : import com.google.gerrit.server.RequestListener;
     121             : import com.google.gerrit.server.account.AccountResolver;
     122             : import com.google.gerrit.server.approval.ApprovalsUtil;
     123             : import com.google.gerrit.server.cancellation.RequestCancelledException;
     124             : import com.google.gerrit.server.cancellation.RequestStateContext;
     125             : import com.google.gerrit.server.change.AttentionSetUnchangedOp;
     126             : import com.google.gerrit.server.change.ChangeInserter;
     127             : import com.google.gerrit.server.change.NotifyResolver;
     128             : import com.google.gerrit.server.change.SetHashtagsOp;
     129             : import com.google.gerrit.server.change.SetPrivateOp;
     130             : import com.google.gerrit.server.change.SetTopicOp;
     131             : import com.google.gerrit.server.config.AllProjectsName;
     132             : import com.google.gerrit.server.config.GerritServerConfig;
     133             : import com.google.gerrit.server.config.PluginConfig;
     134             : import com.google.gerrit.server.config.ProjectConfigEntry;
     135             : import com.google.gerrit.server.config.UrlFormatter;
     136             : import com.google.gerrit.server.edit.ChangeEdit;
     137             : import com.google.gerrit.server.edit.ChangeEditUtil;
     138             : import com.google.gerrit.server.git.BanCommit;
     139             : import com.google.gerrit.server.git.ChangeReportFormatter;
     140             : import com.google.gerrit.server.git.GroupCollector;
     141             : import com.google.gerrit.server.git.MergedByPushOp;
     142             : import com.google.gerrit.server.git.MultiProgressMonitor;
     143             : import com.google.gerrit.server.git.MultiProgressMonitor.Task;
     144             : import com.google.gerrit.server.git.ReceivePackInitializer;
     145             : import com.google.gerrit.server.git.TagCache;
     146             : import com.google.gerrit.server.git.ValidationError;
     147             : import com.google.gerrit.server.git.validators.CommentCountValidator;
     148             : import com.google.gerrit.server.git.validators.CommentSizeValidator;
     149             : import com.google.gerrit.server.git.validators.CommitValidationMessage;
     150             : import com.google.gerrit.server.git.validators.RefOperationValidationException;
     151             : import com.google.gerrit.server.git.validators.RefOperationValidators;
     152             : import com.google.gerrit.server.git.validators.ValidationMessage;
     153             : import com.google.gerrit.server.index.change.ChangeIndexer;
     154             : import com.google.gerrit.server.logging.Metadata;
     155             : import com.google.gerrit.server.logging.PerformanceLogContext;
     156             : import com.google.gerrit.server.logging.PerformanceLogger;
     157             : import com.google.gerrit.server.logging.RequestId;
     158             : import com.google.gerrit.server.logging.TraceContext;
     159             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
     160             : import com.google.gerrit.server.mail.MailUtil.MailRecipients;
     161             : import com.google.gerrit.server.notedb.ChangeNotes;
     162             : import com.google.gerrit.server.notedb.Sequences;
     163             : import com.google.gerrit.server.patch.AutoMerger;
     164             : import com.google.gerrit.server.patch.PatchSetInfoFactory;
     165             : import com.google.gerrit.server.permissions.ChangePermission;
     166             : import com.google.gerrit.server.permissions.GlobalPermission;
     167             : import com.google.gerrit.server.permissions.PermissionBackend;
     168             : import com.google.gerrit.server.permissions.PermissionBackendException;
     169             : import com.google.gerrit.server.permissions.PermissionDeniedException;
     170             : import com.google.gerrit.server.permissions.ProjectPermission;
     171             : import com.google.gerrit.server.permissions.RefPermission;
     172             : import com.google.gerrit.server.plugincontext.PluginSetContext;
     173             : import com.google.gerrit.server.project.CreateRefControl;
     174             : import com.google.gerrit.server.project.NoSuchChangeException;
     175             : import com.google.gerrit.server.project.NoSuchProjectException;
     176             : import com.google.gerrit.server.project.ProjectCache;
     177             : import com.google.gerrit.server.project.ProjectConfig;
     178             : import com.google.gerrit.server.project.ProjectState;
     179             : import com.google.gerrit.server.query.change.ChangeData;
     180             : import com.google.gerrit.server.query.change.InternalChangeQuery;
     181             : import com.google.gerrit.server.restapi.change.ReplyAttentionSetUpdates;
     182             : import com.google.gerrit.server.submit.MergeOp;
     183             : import com.google.gerrit.server.submit.MergeOpRepoManager;
     184             : import com.google.gerrit.server.update.BatchUpdate;
     185             : import com.google.gerrit.server.update.BatchUpdateOp;
     186             : import com.google.gerrit.server.update.ChangeContext;
     187             : import com.google.gerrit.server.update.PostUpdateContext;
     188             : import com.google.gerrit.server.update.RepoContext;
     189             : import com.google.gerrit.server.update.RepoOnlyOp;
     190             : import com.google.gerrit.server.update.RetryHelper;
     191             : import com.google.gerrit.server.update.SubmissionExecutor;
     192             : import com.google.gerrit.server.update.SubmissionListener;
     193             : import com.google.gerrit.server.update.SuperprojectUpdateOnSubmission;
     194             : import com.google.gerrit.server.update.UpdateException;
     195             : import com.google.gerrit.server.util.LabelVote;
     196             : import com.google.gerrit.server.util.MagicBranch;
     197             : import com.google.gerrit.server.util.RequestScopePropagator;
     198             : import com.google.gerrit.server.util.time.TimeUtil;
     199             : import com.google.gerrit.util.cli.CmdLineParser;
     200             : import com.google.inject.Inject;
     201             : import com.google.inject.Provider;
     202             : import com.google.inject.Singleton;
     203             : import com.google.inject.assistedinject.Assisted;
     204             : import com.google.inject.util.Providers;
     205             : import java.io.IOException;
     206             : import java.io.StringWriter;
     207             : import java.io.UnsupportedEncodingException;
     208             : import java.net.URLDecoder;
     209             : import java.util.ArrayList;
     210             : import java.util.Arrays;
     211             : import java.util.Collection;
     212             : import java.util.Collections;
     213             : import java.util.HashMap;
     214             : import java.util.HashSet;
     215             : import java.util.Iterator;
     216             : import java.util.LinkedHashMap;
     217             : import java.util.LinkedHashSet;
     218             : import java.util.List;
     219             : import java.util.Map;
     220             : import java.util.Objects;
     221             : import java.util.Optional;
     222             : import java.util.Queue;
     223             : import java.util.Set;
     224             : import java.util.TreeSet;
     225             : import java.util.concurrent.ConcurrentLinkedQueue;
     226             : import java.util.concurrent.ExecutionException;
     227             : import java.util.concurrent.Future;
     228             : import java.util.stream.Collectors;
     229             : import java.util.stream.Stream;
     230             : import java.util.stream.StreamSupport;
     231             : import org.eclipse.jgit.errors.ConfigInvalidException;
     232             : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
     233             : import org.eclipse.jgit.errors.MissingObjectException;
     234             : import org.eclipse.jgit.lib.Config;
     235             : import org.eclipse.jgit.lib.Constants;
     236             : import org.eclipse.jgit.lib.ObjectId;
     237             : import org.eclipse.jgit.lib.ObjectInserter;
     238             : import org.eclipse.jgit.lib.ObjectReader;
     239             : import org.eclipse.jgit.lib.PersonIdent;
     240             : import org.eclipse.jgit.lib.Ref;
     241             : import org.eclipse.jgit.lib.Repository;
     242             : import org.eclipse.jgit.notes.NoteMap;
     243             : import org.eclipse.jgit.revwalk.FooterLine;
     244             : import org.eclipse.jgit.revwalk.RevCommit;
     245             : import org.eclipse.jgit.revwalk.RevObject;
     246             : import org.eclipse.jgit.revwalk.RevSort;
     247             : import org.eclipse.jgit.revwalk.RevWalk;
     248             : import org.eclipse.jgit.revwalk.filter.RevFilter;
     249             : import org.eclipse.jgit.transport.ReceiveCommand;
     250             : import org.eclipse.jgit.transport.ReceivePack;
     251             : import org.kohsuke.args4j.CmdLineException;
     252             : import org.kohsuke.args4j.Option;
     253             : 
     254             : /**
     255             :  * Receives change upload using the Git receive-pack protocol.
     256             :  *
     257             :  * <p>Conceptually, most use of Gerrit is a push of some commits to refs/for/BRANCH. However, the
     258             :  * receive-pack protocol that this is based on allows multiple ref updates to be processed at once.
     259             :  * So we have to be prepared to also handle normal pushes (refs/heads/BRANCH), and legacy pushes
     260             :  * (refs/changes/CHANGE). It is hard to split this class up further, because normal pushes can also
     261             :  * result in updates to reviews, through the autoclose mechanism.
     262             :  */
     263             : class ReceiveCommits {
     264          97 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     265             : 
     266             :   private static final String CANNOT_DELETE_CHANGES = "Cannot delete from '" + REFS_CHANGES + "'";
     267             :   private static final String CANNOT_DELETE_CONFIG =
     268             :       "Cannot delete project configuration from '" + RefNames.REFS_CONFIG + "'";
     269             :   private static final String INTERNAL_SERVER_ERROR = "internal server error";
     270             : 
     271             :   interface Factory {
     272             :     ReceiveCommits create(
     273             :         ProjectState projectState,
     274             :         IdentifiedUser user,
     275             :         ReceivePack receivePack,
     276             :         Repository repository,
     277             :         AllRefsWatcher allRefsWatcher,
     278             :         MessageSender messageSender);
     279             :   }
     280             : 
     281          97 :   private class ReceivePackMessageSender implements MessageSender {
     282             :     @Override
     283             :     public void sendMessage(String what) {
     284          90 :       receivePack.sendMessage(what);
     285          90 :     }
     286             : 
     287             :     @Override
     288             :     public void sendError(String what) {
     289           0 :       receivePack.sendError(what);
     290           0 :     }
     291             : 
     292             :     @Override
     293             :     public void sendBytes(byte[] what) {
     294          96 :       sendBytes(what, 0, what.length);
     295          96 :     }
     296             : 
     297             :     @Override
     298             :     public void sendBytes(byte[] what, int off, int len) {
     299             :       try {
     300          96 :         receivePack.getMessageOutputStream().write(what, off, len);
     301           0 :       } catch (IOException e) {
     302             :         // Ignore write failures (matching JGit behavior).
     303          96 :       }
     304          96 :     }
     305             : 
     306             :     @Override
     307             :     public void flush() {
     308             :       try {
     309          96 :         receivePack.getMessageOutputStream().flush();
     310           0 :       } catch (IOException e) {
     311             :         // Ignore write failures (matching JGit behavior).
     312          96 :       }
     313          96 :     }
     314             :   }
     315             : 
     316             :   private static RestApiException asRestApiException(Exception e) {
     317           3 :     if (e instanceof RestApiException) {
     318           0 :       return (RestApiException) e;
     319           3 :     } else if ((e instanceof ExecutionException) && (e.getCause() instanceof RestApiException)) {
     320           0 :       return (RestApiException) e.getCause();
     321             :     }
     322           3 :     return RestApiException.wrap("Error inserting change/patchset", e);
     323             :   }
     324             : 
     325             :   @Singleton
     326             :   private static class Metrics {
     327             :     private final Counter0 psRevisionMissing;
     328             :     private final Counter3<String, String, String> pushCount;
     329             : 
     330             :     @Inject
     331          97 :     Metrics(MetricMaker metricMaker) {
     332          97 :       psRevisionMissing =
     333          97 :           metricMaker.newCounter(
     334             :               "receivecommits/ps_revision_missing",
     335             :               new Description("errors due to patch set revision missing"));
     336          97 :       pushCount =
     337          97 :           metricMaker.newCounter(
     338             :               "receivecommits/push_count",
     339             :               new Description("number of pushes"),
     340          97 :               Field.ofString("kind", (metadataBuilder, fieldValue) -> {})
     341          97 :                   .description("The push kind (direct vs. magic).")
     342          97 :                   .build(),
     343          97 :               Field.ofString(
     344             :                       "project",
     345           0 :                       (metadataBuilder, fieldValue) -> metadataBuilder.projectName(fieldValue))
     346          97 :                   .description("The name of the project for which the push is done.")
     347          97 :                   .build(),
     348          97 :               Field.ofString("type", (metadataBuilder, fieldValue) -> {})
     349          97 :                   .description(
     350             :                       "The type of the update (CREATE, UPDATE, CREATE/UPDATE,"
     351             :                           + " UPDATE_NONFASTFORWARD, DELETE).")
     352          97 :                   .build());
     353          97 :     }
     354             :   }
     355             : 
     356             :   // ReceiveCommits has a lot of fields, sorry. Here and in the constructor they are split up
     357             :   // somewhat, and kept sorted lexicographically within sections, except where later assignments
     358             :   // depend on previous ones.
     359             : 
     360             :   // Injected fields.
     361             :   private final AccountResolver accountResolver;
     362             :   private final AllProjectsName allProjectsName;
     363             :   private final BatchUpdate.Factory batchUpdateFactory;
     364             :   private final CancellationMetrics cancellationMetrics;
     365             :   private final ChangeEditUtil editUtil;
     366             :   private final ChangeIndexer indexer;
     367             :   private final ChangeInserter.Factory changeInserterFactory;
     368             :   private final ChangeNotes.Factory notesFactory;
     369             :   private final ChangeReportFormatter changeFormatter;
     370             :   private final CmdLineParser.Factory optionParserFactory;
     371             :   private final CommentsUtil commentsUtil;
     372             :   private final PluginSetContext<CommentValidator> commentValidators;
     373             :   private final BranchCommitValidator.Factory commitValidatorFactory;
     374             :   private final Config config;
     375             :   private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
     376             :   private final CreateRefControl createRefControl;
     377             :   private final DeadlineChecker.Factory deadlineCheckerFactory;
     378             :   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
     379             :   private final DynamicSet<PluginPushOption> pluginPushOptions;
     380             :   private final PluginSetContext<ReceivePackInitializer> initializers;
     381             :   private final MergedByPushOp.Factory mergedByPushOpFactory;
     382             :   private final Metrics metrics;
     383             :   private final PatchSetInfoFactory patchSetInfoFactory;
     384             :   private final PatchSetUtil psUtil;
     385             :   private final DynamicSet<PerformanceLogger> performanceLoggers;
     386             :   private final PermissionBackend permissionBackend;
     387             :   private final ProjectCache projectCache;
     388             :   private final Provider<InternalChangeQuery> queryProvider;
     389             :   private final Provider<MergeOp> mergeOpProvider;
     390             :   private final Provider<MergeOpRepoManager> ormProvider;
     391             :   private final ReceiveConfig receiveConfig;
     392             :   private final RefOperationValidators.Factory refValidatorsFactory;
     393             :   private final ReplaceOp.Factory replaceOpFactory;
     394             :   private final PluginSetContext<RequestListener> requestListeners;
     395             :   private final PublishCommentsOp.Factory publishCommentsOp;
     396             :   private final RetryHelper retryHelper;
     397             :   private final RequestScopePropagator requestScopePropagator;
     398             :   private final Sequences seq;
     399             :   private final SetHashtagsOp.Factory hashtagsFactory;
     400             :   private final SetTopicOp.Factory setTopicFactory;
     401             :   private final ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners;
     402             :   private final TagCache tagCache;
     403             :   private final ProjectConfig.Factory projectConfigFactory;
     404             :   private final SetPrivateOp.Factory setPrivateOpFactory;
     405             :   private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
     406             :   private final DynamicItem<UrlFormatter> urlFormatter;
     407             :   private final AutoMerger autoMerger;
     408             : 
     409             :   // Assisted injected fields.
     410             :   private final ProjectState projectState;
     411             :   private final IdentifiedUser user;
     412             :   private final ReceivePack receivePack;
     413             : 
     414             :   // Immutable fields derived from constructor arguments.
     415             :   private final boolean allowProjectOwnersToChangeParent;
     416             :   private final LabelTypes labelTypes;
     417             :   private final NoteMap rejectCommits;
     418             :   private final PermissionBackend.ForProject permissions;
     419             :   private final Project project;
     420             :   private final Repository repo;
     421             : 
     422             :   // Collections populated during processing.
     423             :   private final List<UpdateGroupsRequest> updateGroups;
     424             :   private final Queue<ValidationMessage> messages;
     425             :   /** Multimap of error text to refnames that produced that error. */
     426             :   private final ListMultimap<String, String> errors;
     427             : 
     428             :   private final ListMultimap<String, String> pushOptions;
     429             :   private final ReceivePackRefCache receivePackRefCache;
     430             :   private final Map<Change.Id, ReplaceRequest> replaceByChange;
     431             : 
     432             :   // Other settings populated during processing.
     433             :   private MagicBranchInput magicBranch;
     434             :   private boolean newChangeForAllNotInTarget;
     435             :   private boolean setChangeAsPrivate;
     436             :   private Optional<NoteDbPushOption> noteDbPushOption;
     437             :   private Optional<String> tracePushOption;
     438             : 
     439             :   private MessageSender messageSender;
     440             :   private ReceiveCommitsResult.Builder result;
     441             :   private ImmutableMap<String, String> loggingTags;
     442             :   private ImmutableList<String> transitionalPluginOptions;
     443             : 
     444             :   /** This object is for single use only. */
     445             :   private boolean used;
     446             : 
     447             :   @Inject
     448             :   ReceiveCommits(
     449             :       AccountResolver accountResolver,
     450             :       AllProjectsName allProjectsName,
     451             :       BatchUpdate.Factory batchUpdateFactory,
     452             :       CancellationMetrics cancellationMetrics,
     453             :       ProjectConfig.Factory projectConfigFactory,
     454             :       @GerritServerConfig Config config,
     455             :       ChangeEditUtil editUtil,
     456             :       ChangeIndexer indexer,
     457             :       ChangeInserter.Factory changeInserterFactory,
     458             :       ChangeNotes.Factory notesFactory,
     459             :       DynamicItem<ChangeReportFormatter> changeFormatterProvider,
     460             :       CmdLineParser.Factory optionParserFactory,
     461             :       CommentsUtil commentsUtil,
     462             :       BranchCommitValidator.Factory commitValidatorFactory,
     463             :       CreateGroupPermissionSyncer createGroupPermissionSyncer,
     464             :       CreateRefControl createRefControl,
     465             :       DeadlineChecker.Factory deadlineCheckerFactory,
     466             :       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
     467             :       DynamicSet<PluginPushOption> pluginPushOptions,
     468             :       PluginSetContext<ReceivePackInitializer> initializers,
     469             :       PluginSetContext<CommentValidator> commentValidators,
     470             :       MergedByPushOp.Factory mergedByPushOpFactory,
     471             :       Metrics metrics,
     472             :       PatchSetInfoFactory patchSetInfoFactory,
     473             :       PatchSetUtil psUtil,
     474             :       DynamicSet<PerformanceLogger> performanceLoggers,
     475             :       PermissionBackend permissionBackend,
     476             :       ProjectCache projectCache,
     477             :       Provider<InternalChangeQuery> queryProvider,
     478             :       Provider<MergeOp> mergeOpProvider,
     479             :       Provider<MergeOpRepoManager> ormProvider,
     480             :       PublishCommentsOp.Factory publishCommentsOp,
     481             :       ReceiveConfig receiveConfig,
     482             :       RefOperationValidators.Factory refValidatorsFactory,
     483             :       ReplaceOp.Factory replaceOpFactory,
     484             :       PluginSetContext<RequestListener> requestListeners,
     485             :       RetryHelper retryHelper,
     486             :       RequestScopePropagator requestScopePropagator,
     487             :       Sequences seq,
     488             :       SetHashtagsOp.Factory hashtagsFactory,
     489             :       SetTopicOp.Factory setTopicFactory,
     490             :       @SuperprojectUpdateOnSubmission
     491             :           ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners,
     492             :       TagCache tagCache,
     493             :       SetPrivateOp.Factory setPrivateOpFactory,
     494             :       ReplyAttentionSetUpdates replyAttentionSetUpdates,
     495             :       DynamicItem<UrlFormatter> urlFormatter,
     496             :       AutoMerger autoMerger,
     497             :       @Assisted ProjectState projectState,
     498             :       @Assisted IdentifiedUser user,
     499             :       @Assisted ReceivePack rp,
     500             :       @Assisted Repository repository,
     501             :       @Assisted AllRefsWatcher allRefsWatcher,
     502             :       @Nullable @Assisted MessageSender messageSender)
     503          97 :       throws IOException {
     504             :     // Injected fields.
     505          97 :     this.accountResolver = accountResolver;
     506          97 :     this.allProjectsName = allProjectsName;
     507          97 :     this.batchUpdateFactory = batchUpdateFactory;
     508          97 :     this.cancellationMetrics = cancellationMetrics;
     509          97 :     this.changeFormatter = changeFormatterProvider.get();
     510          97 :     this.changeInserterFactory = changeInserterFactory;
     511          97 :     this.commentsUtil = commentsUtil;
     512          97 :     this.commentValidators = commentValidators;
     513          97 :     this.commitValidatorFactory = commitValidatorFactory;
     514          97 :     this.config = config;
     515          97 :     this.createRefControl = createRefControl;
     516          97 :     this.createGroupPermissionSyncer = createGroupPermissionSyncer;
     517          97 :     this.deadlineCheckerFactory = deadlineCheckerFactory;
     518          97 :     this.editUtil = editUtil;
     519          97 :     this.hashtagsFactory = hashtagsFactory;
     520          97 :     this.setTopicFactory = setTopicFactory;
     521          97 :     this.indexer = indexer;
     522          97 :     this.initializers = initializers;
     523          97 :     this.mergeOpProvider = mergeOpProvider;
     524          97 :     this.mergedByPushOpFactory = mergedByPushOpFactory;
     525          97 :     this.notesFactory = notesFactory;
     526          97 :     this.optionParserFactory = optionParserFactory;
     527          97 :     this.ormProvider = ormProvider;
     528          97 :     this.metrics = metrics;
     529          97 :     this.patchSetInfoFactory = patchSetInfoFactory;
     530          97 :     this.permissionBackend = permissionBackend;
     531          97 :     this.pluginConfigEntries = pluginConfigEntries;
     532          97 :     this.pluginPushOptions = pluginPushOptions;
     533          97 :     this.projectCache = projectCache;
     534          97 :     this.psUtil = psUtil;
     535          97 :     this.performanceLoggers = performanceLoggers;
     536          97 :     this.publishCommentsOp = publishCommentsOp;
     537          97 :     this.queryProvider = queryProvider;
     538          97 :     this.receiveConfig = receiveConfig;
     539          97 :     this.refValidatorsFactory = refValidatorsFactory;
     540          97 :     this.replaceOpFactory = replaceOpFactory;
     541          97 :     this.requestListeners = requestListeners;
     542          97 :     this.retryHelper = retryHelper;
     543          97 :     this.requestScopePropagator = requestScopePropagator;
     544          97 :     this.seq = seq;
     545          97 :     this.superprojectUpdateSubmissionListeners = superprojectUpdateSubmissionListeners;
     546          97 :     this.tagCache = tagCache;
     547          97 :     this.projectConfigFactory = projectConfigFactory;
     548          97 :     this.setPrivateOpFactory = setPrivateOpFactory;
     549          97 :     this.replyAttentionSetUpdates = replyAttentionSetUpdates;
     550          97 :     this.urlFormatter = urlFormatter;
     551          97 :     this.autoMerger = autoMerger;
     552             : 
     553             :     // Assisted injected fields.
     554          97 :     this.projectState = projectState;
     555          97 :     this.user = user;
     556          97 :     this.receivePack = rp;
     557             :     // This repository instance in unwrapped, while the repository instance in
     558             :     // receivePack.getRepo() is wrapped in PermissionAwareRepository instance.
     559          97 :     this.repo = repository;
     560             : 
     561             :     // Immutable fields derived from constructor arguments.
     562          97 :     project = projectState.getProject();
     563          97 :     labelTypes = projectState.getLabelTypes();
     564          97 :     permissions = permissionBackend.user(user).project(project.getNameKey());
     565          97 :     rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
     566             : 
     567             :     // Collections populated during processing.
     568          97 :     errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
     569          97 :     messages = new ConcurrentLinkedQueue<>();
     570          97 :     pushOptions = LinkedListMultimap.create();
     571          97 :     replaceByChange = new LinkedHashMap<>();
     572          97 :     updateGroups = new ArrayList<>();
     573             : 
     574          97 :     used = false;
     575             : 
     576          97 :     this.allowProjectOwnersToChangeParent =
     577          97 :         config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
     578             : 
     579             :     // Other settings populated during processing.
     580          97 :     newChangeForAllNotInTarget =
     581          97 :         projectState.is(BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET);
     582             : 
     583             :     // Handles for outputting back over the wire to the end user.
     584          97 :     this.messageSender = messageSender != null ? messageSender : new ReceivePackMessageSender();
     585          97 :     this.result = ReceiveCommitsResult.builder();
     586          97 :     this.loggingTags = ImmutableMap.of();
     587             : 
     588             :     // TODO(hiesel): Make this decision implicit once vetted
     589          97 :     boolean useRefCache = config.getBoolean("receive", "enableInMemoryRefCache", true);
     590          97 :     receivePackRefCache =
     591          97 :         useRefCache
     592          97 :             ? ReceivePackRefCache.withAdvertisedRefs(() -> allRefsWatcher.getAllRefs())
     593          97 :             : ReceivePackRefCache.noCache(receivePack.getRepository().getRefDatabase());
     594          97 :     this.transitionalPluginOptions =
     595          97 :         ImmutableList.copyOf(config.getStringList("plugins", null, "transitionalPushOptions"));
     596          97 :   }
     597             : 
     598             :   void init() {
     599          97 :     initializers.runEach(i -> i.init(projectState.getNameKey(), receivePack));
     600          97 :   }
     601             : 
     602             :   MessageSender getMessageSender() {
     603          96 :     return messageSender;
     604             :   }
     605             : 
     606             :   Project getProject() {
     607          96 :     return project;
     608             :   }
     609             : 
     610             :   private void addMessage(String message, ValidationMessage.Type type) {
     611           6 :     messages.add(new CommitValidationMessage(message, type));
     612           6 :   }
     613             : 
     614             :   private void addMessage(String message) {
     615          88 :     messages.add(new CommitValidationMessage(message, ValidationMessage.Type.OTHER));
     616          88 :   }
     617             : 
     618             :   private void addError(String error) {
     619           5 :     addMessage(error, ValidationMessage.Type.ERROR);
     620           5 :   }
     621             : 
     622             :   /**
     623             :    * Sends all messages which have been collected while processing the push to the client.
     624             :    *
     625             :    * <p><strong>Attention:</strong>{@link AsyncReceiveCommits} may call this method while {@link
     626             :    * #processCommands(Collection, MultiProgressMonitor)} is still running (if the execution of
     627             :    * processCommands takes too long and AsyncReceiveCommits gets a timeout). This means that local
     628             :    * variables that are accessed in this method must be thread-safe (otherwise we may hit a {@link
     629             :    * java.util.ConcurrentModificationException} if we read a variable here that at the same time is
     630             :    * updated by the background thread that still executes processCommands).
     631             :    */
     632             :   void sendMessages() {
     633          96 :     try (TraceContext traceContext =
     634          96 :         TraceContext.newTrace(
     635          96 :             loggingTags.containsKey(RequestId.Type.TRACE_ID.name()),
     636          96 :             loggingTags.get(RequestId.Type.TRACE_ID.name()),
     637           4 :             (tagName, traceId) -> {})) {
     638          96 :       loggingTags.forEach((tagName, tagValue) -> traceContext.addTag(tagName, tagValue));
     639             : 
     640          96 :       for (ValidationMessage m : messages) {
     641          90 :         String msg = m.getType().getPrefix() + m.getMessage();
     642          90 :         logger.atFine().log("Sending message: %s", msg);
     643             : 
     644             :         // Avoid calling sendError which will add its own error: prefix.
     645          90 :         messageSender.sendMessage(msg);
     646          90 :       }
     647             :     }
     648          96 :   }
     649             : 
     650             :   ReceiveCommitsResult processCommands(
     651             :       Collection<ReceiveCommand> commands, MultiProgressMonitor progress) throws StorageException {
     652          96 :     checkState(!used, "Tried to re-use a ReceiveCommits objects that is single-use only");
     653          96 :     long start = TimeUtil.nowNanos();
     654          96 :     parsePushOptions();
     655          96 :     String clientProvidedDeadlineValue =
     656          96 :         Iterables.getLast(pushOptions.get("deadline"), /* defaultValue= */ null);
     657          96 :     int commandCount = commands.size();
     658          96 :     try (TraceContext traceContext =
     659          96 :             TraceContext.newTrace(
     660          96 :                 tracePushOption.isPresent(),
     661          96 :                 tracePushOption.orElse(null),
     662           4 :                 (tagName, traceId) -> addMessage(tagName + ": " + traceId));
     663          96 :         PerformanceLogContext performanceLogContext =
     664             :             new PerformanceLogContext(config, performanceLoggers);
     665          96 :         TraceTimer traceTimer =
     666          96 :             newTimer("processCommands", Metadata.builder().resourceCount(commandCount))) {
     667          96 :       RequestInfo requestInfo =
     668          96 :           RequestInfo.builder(RequestInfo.RequestType.GIT_RECEIVE, user, traceContext)
     669          96 :               .project(project.getNameKey())
     670          96 :               .build();
     671          96 :       requestListeners.runEach(l -> l.onRequest(requestInfo));
     672          96 :       traceContext.addTag(RequestId.Type.RECEIVE_ID, new RequestId(project.getNameKey().get()));
     673             : 
     674             :       // Log the push options here, rather than in parsePushOptions(), so that they are included
     675             :       // into the trace if tracing is enabled.
     676          96 :       logger.atFine().log("push options: %s", receivePack.getPushOptions());
     677             : 
     678          96 :       Task commandProgress = progress.beginSubTask("refs", UNKNOWN);
     679          96 :       commands =
     680          96 :           commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
     681             : 
     682             :       try (RequestStateContext requestStateContext =
     683          96 :           RequestStateContext.open()
     684          96 :               .addRequestStateProvider(progress)
     685          96 :               .addRequestStateProvider(
     686          96 :                   deadlineCheckerFactory.create(start, requestInfo, clientProvidedDeadlineValue))) {
     687          96 :         processCommandsUnsafe(commands, progress);
     688          96 :         rejectRemaining(commands, INTERNAL_SERVER_ERROR);
     689           1 :       } catch (InvalidDeadlineException e) {
     690           1 :         rejectRemaining(commands, e.getMessage());
     691           4 :       } catch (RuntimeException e) {
     692           4 :         Optional<RequestCancelledException> requestCancelledException =
     693           4 :             RequestCancelledException.getFromCausalChain(e);
     694           4 :         if (!requestCancelledException.isPresent()) {
     695           0 :           Throwables.throwIfUnchecked(e);
     696             :         }
     697           1 :         cancellationMetrics.countCancelledRequest(
     698           1 :             requestInfo, requestCancelledException.get().getCancellationReason());
     699           1 :         StringBuilder msg =
     700           1 :             new StringBuilder(requestCancelledException.get().formatCancellationReason());
     701           1 :         if (requestCancelledException.get().getCancellationMessage().isPresent()) {
     702           1 :           msg.append(
     703           1 :               String.format(
     704           1 :                   " (%s)", requestCancelledException.get().getCancellationMessage().get()));
     705             :         }
     706           1 :         rejectRemaining(commands, msg.toString());
     707          96 :       }
     708             : 
     709             :       // This sends error messages before the 'done' string of the progress monitor is sent.
     710             :       // Currently, the test framework relies on this ordering to understand if pushes completed
     711             :       // successfully.
     712          96 :       sendErrorMessages();
     713             : 
     714          96 :       commandProgress.end();
     715          96 :       loggingTags = traceContext.getTags();
     716          96 :       logger.atFine().log("Processing commands done.");
     717             :     }
     718          96 :     progress.end();
     719          96 :     return result.build();
     720             :   }
     721             : 
     722             :   // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
     723             :   private void processCommandsUnsafe(
     724             :       Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
     725          96 :     logger.atFine().log("Calling user: %s, commands: %d", user.getLoggableName(), commands.size());
     726             : 
     727             :     // If the list of groups is large, the log entry may get dropped, so separate out.
     728          96 :     logger.atFine().log("Groups: %s", lazy(() -> user.getEffectiveGroups().getKnownGroups()));
     729             : 
     730          96 :     if (!projectState.getProject().getState().permitsWrite()) {
     731           1 :       for (ReceiveCommand cmd : commands) {
     732           1 :         reject(cmd, "prohibited by Gerrit: project state does not permit write");
     733           1 :       }
     734           1 :       return;
     735             :     }
     736             : 
     737          96 :     List<ReceiveCommand> magicCommands = new ArrayList<>();
     738          96 :     List<ReceiveCommand> regularCommands = new ArrayList<>();
     739             : 
     740          96 :     for (ReceiveCommand cmd : commands) {
     741          96 :       if (MagicBranch.isMagicBranch(cmd.getRefName())) {
     742          89 :         magicCommands.add(cmd);
     743             :       } else {
     744          49 :         regularCommands.add(cmd);
     745             :       }
     746          96 :     }
     747             : 
     748          96 :     if (!magicCommands.isEmpty() && !regularCommands.isEmpty()) {
     749           1 :       rejectRemaining(commands, "cannot combine normal pushes and magic pushes");
     750           1 :       return;
     751             :     }
     752             : 
     753             :     try {
     754          96 :       if (!magicCommands.isEmpty()) {
     755          89 :         parseMagicBranch(Iterables.getLast(magicCommands));
     756             :         // Using the submit option submits the created change(s) immediately without checking labels
     757             :         // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
     758             :         // code review is being done.
     759          89 :         String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
     760          89 :         metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
     761             :       }
     762          96 :       if (!regularCommands.isEmpty()) {
     763          49 :         metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
     764             :       }
     765             : 
     766          96 :       if (!regularCommands.isEmpty()) {
     767          49 :         handleRegularCommands(regularCommands, progress);
     768          49 :         return;
     769             :       }
     770             : 
     771          89 :       boolean first = true;
     772          89 :       for (ReceiveCommand cmd : magicCommands) {
     773          89 :         if (first) {
     774          89 :           first = false;
     775             :         } else {
     776           0 :           reject(cmd, "duplicate request");
     777             :         }
     778          89 :       }
     779           0 :     } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
     780           0 :       logger.atSevere().withCause(err).log("Failed to process refs in %s", project.getName());
     781           0 :       return;
     782          89 :     }
     783             : 
     784          89 :     Task newProgress = progress.beginSubTask("new", UNKNOWN);
     785          89 :     Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
     786             : 
     787          89 :     ImmutableList<CreateRequest> newChanges = ImmutableList.of();
     788             :     try {
     789          89 :       if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
     790             :         try {
     791          89 :           newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
     792           0 :         } catch (IOException e) {
     793           0 :           throw new StorageException("Failed to select new changes in " + project.getName(), e);
     794          89 :         }
     795             :       }
     796             : 
     797             :       // Commit validation has already happened, so any changes without Change-Id are for the
     798             :       // deprecated feature.
     799          89 :       warnAboutMissingChangeId(newChanges);
     800          89 :       preparePatchSetsForReplace(newChanges);
     801          89 :       insertChangesAndPatchSets(newChanges, replaceProgress);
     802             :     } finally {
     803          89 :       newProgress.end();
     804          89 :       replaceProgress.end();
     805             :     }
     806             : 
     807          89 :     queueSuccessMessages(newChanges);
     808             : 
     809          89 :     logger.atFine().log(
     810             :         "Command results: %s",
     811          89 :         lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
     812          89 :   }
     813             : 
     814             :   private String getUpdateType(List<ReceiveCommand> commands) {
     815          96 :     return commands.stream()
     816          96 :         .map(ReceiveCommand::getType)
     817          96 :         .map(ReceiveCommand.Type::name)
     818          96 :         .distinct()
     819          96 :         .sorted()
     820          96 :         .collect(joining("/"));
     821             :   }
     822             : 
     823             :   private void sendErrorMessages() {
     824          96 :     if (!errors.isEmpty()) {
     825          12 :       logger.atFine().log("Handling error conditions: %s", errors.keySet());
     826          12 :       for (String error : errors.keySet()) {
     827          12 :         receivePack.sendMessage("error: " + buildError(error, errors.get(error)));
     828          12 :       }
     829          12 :       receivePack.sendMessage(String.format("User: %s", user.getLoggableName()));
     830          12 :       receivePack.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
     831             :     }
     832          96 :   }
     833             : 
     834             :   private void handleRegularCommands(List<ReceiveCommand> cmds, MultiProgressMonitor progress)
     835             :       throws PermissionBackendException, IOException, NoSuchProjectException {
     836          49 :     try (TraceTimer traceTimer =
     837          49 :         newTimer("handleRegularCommands", Metadata.builder().resourceCount(cmds.size()))) {
     838          49 :       result.magicPush(false);
     839          49 :       for (ReceiveCommand cmd : cmds) {
     840          49 :         parseRegularCommand(cmd);
     841          49 :       }
     842             : 
     843             :       Map<BranchNameKey, ReceiveCommand> branches;
     844          49 :       try (BatchUpdate bu =
     845          49 :               batchUpdateFactory.create(
     846          49 :                   project.getNameKey(), user.materializedCopy(), TimeUtil.now());
     847          49 :           ObjectInserter ins = repo.newObjectInserter();
     848          49 :           ObjectReader reader = ins.newReader();
     849          49 :           RevWalk rw = new RevWalk(reader);
     850          49 :           MergeOpRepoManager orm = ormProvider.get()) {
     851          49 :         bu.setRepository(repo, rw, ins);
     852          49 :         bu.setRefLogMessage("push");
     853             : 
     854          49 :         int added = 0;
     855          49 :         for (ReceiveCommand cmd : cmds) {
     856          49 :           if (cmd.getResult() == NOT_ATTEMPTED) {
     857          47 :             bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
     858          47 :             added++;
     859             :           }
     860          49 :         }
     861          49 :         logger.atFine().log("Added %d additional ref updates", added);
     862             : 
     863          49 :         SubmissionExecutor submissionExecutor =
     864             :             new SubmissionExecutor(false, superprojectUpdateSubmissionListeners);
     865             : 
     866          49 :         submissionExecutor.execute(ImmutableList.of(bu));
     867             : 
     868          49 :         orm.setContext(TimeUtil.now(), user, NotifyResolver.Result.none());
     869          49 :         submissionExecutor.afterExecutions(orm);
     870             : 
     871          49 :         branches = bu.getSuccessfullyUpdatedBranches(false);
     872           0 :       } catch (UpdateException | RestApiException e) {
     873           0 :         throw new StorageException(e);
     874          49 :       }
     875             : 
     876             :       // This could be moved into a SubmissionListener
     877          49 :       branches.values().stream()
     878          49 :           .filter(c -> isHead(c) || isConfig(c))
     879          49 :           .forEach(
     880             :               c -> {
     881             :                 // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps
     882             :                 // that should happen in this loops are things that can't happen within one
     883             :                 // BatchUpdate because they involve kicking off an additional BatchUpdate.
     884          38 :                 switch (c.getType()) {
     885             :                   case CREATE:
     886             :                   case UPDATE:
     887             :                   case UPDATE_NONFASTFORWARD:
     888          38 :                     Task closeProgress = progress.beginSubTask("closed", UNKNOWN);
     889          38 :                     autoCloseChanges(c, closeProgress);
     890          38 :                     closeProgress.end();
     891          38 :                     break;
     892             : 
     893             :                   case DELETE:
     894             :                     break;
     895             :                 }
     896          38 :               });
     897             :     }
     898          49 :   }
     899             : 
     900             :   /** Appends messages for successful change creation/updates. */
     901             :   private void queueSuccessMessages(List<CreateRequest> newChanges) {
     902             :     // adjacency list for commit => parent
     903          89 :     Map<String, String> adjList = new HashMap<>();
     904          89 :     for (CreateRequest cr : newChanges) {
     905          88 :       String parent = cr.commit.getParentCount() == 0 ? null : cr.commit.getParent(0).name();
     906          88 :       adjList.put(cr.commit.name(), parent);
     907          88 :     }
     908          89 :     for (ReplaceRequest rr : replaceByChange.values()) {
     909          38 :       String parent = null;
     910          38 :       if (rr.revCommit != null) {
     911          38 :         parent = rr.revCommit.getParentCount() == 0 ? null : rr.revCommit.getParent(0).name();
     912             :       }
     913          38 :       adjList.put(rr.newCommitId.name(), parent);
     914          38 :     }
     915             : 
     916             :     // get commits that are not parents
     917          89 :     Set<String> leafs = new TreeSet<>(adjList.keySet());
     918          89 :     leafs.removeAll(adjList.values());
     919             :     // go backwards from the last commit to its parent(s)
     920          89 :     Set<String> ordered = new LinkedHashSet<>();
     921          89 :     for (String leaf : leafs) {
     922          88 :       if (ordered.contains(leaf)) {
     923           0 :         continue;
     924             :       }
     925          88 :       while (leaf != null) {
     926          88 :         if (!ordered.contains(leaf)) {
     927          88 :           ordered.add(leaf);
     928             :         }
     929          88 :         leaf = adjList.get(leaf);
     930             :       }
     931          88 :     }
     932             :     // reverse the order to start with earliest commit
     933          89 :     List<String> orderedCommits = new ArrayList<>(ordered);
     934          89 :     Collections.reverse(orderedCommits);
     935             : 
     936          89 :     Map<String, CreateRequest> created =
     937          89 :         newChanges.stream()
     938          89 :             .filter(r -> r.change != null)
     939          89 :             .collect(Collectors.toMap(r -> r.commit.name(), r -> r));
     940          89 :     Map<String, ReplaceRequest> updated =
     941          89 :         replaceByChange.values().stream()
     942          89 :             .filter(r -> r.inputCommand.getResult() == OK)
     943          89 :             .collect(Collectors.toMap(r -> r.newCommitId.name(), r -> r));
     944             : 
     945          89 :     if (created.isEmpty() && updated.isEmpty()) {
     946          14 :       return;
     947             :     }
     948             : 
     949          88 :     addMessage("");
     950          88 :     addMessage("SUCCESS");
     951          88 :     addMessage("");
     952             : 
     953          88 :     boolean edit = false;
     954          88 :     Boolean isPrivate = null;
     955          88 :     Boolean wip = null;
     956          88 :     if (!updated.isEmpty()) {
     957          37 :       edit = magicBranch != null && magicBranch.edit;
     958          37 :       if (magicBranch != null) {
     959          37 :         if (magicBranch.isPrivate) {
     960           1 :           isPrivate = true;
     961          37 :         } else if (magicBranch.removePrivate) {
     962           3 :           isPrivate = false;
     963             :         }
     964          37 :         if (magicBranch.workInProgress) {
     965           5 :           wip = true;
     966          37 :         } else if (magicBranch.ready) {
     967           4 :           wip = false;
     968             :         }
     969             :       }
     970             :     }
     971             : 
     972          88 :     for (String commit : orderedCommits) {
     973          88 :       if (created.get(commit) != null) {
     974          88 :         addCreatedMessage(created.get(commit));
     975          88 :       } else if (updated.get(commit) != null) {
     976          37 :         addReplacedMessage(updated.get(commit), edit, isPrivate, wip);
     977             :       }
     978          88 :     }
     979          88 :     addMessage("");
     980          88 :   }
     981             : 
     982             :   private void addCreatedMessage(CreateRequest c) {
     983          88 :     addMessage(
     984          88 :         changeFormatter.newChange(
     985          88 :             ChangeReportFormatter.Input.builder().setChange(c.change).build()));
     986          88 :   }
     987             : 
     988             :   private void addReplacedMessage(ReplaceRequest u, boolean edit, Boolean isPrivate, Boolean wip) {
     989             :     String subject;
     990          37 :     if (edit) {
     991             :       subject =
     992           3 :           u.revCommit == null ? u.notes.getChange().getSubject() : u.revCommit.getShortMessage();
     993             :     } else {
     994          37 :       subject = u.info.getSubject();
     995             :     }
     996             : 
     997          37 :     if (isPrivate == null) {
     998          37 :       isPrivate = u.notes.getChange().isPrivate();
     999             :     }
    1000          37 :     if (wip == null) {
    1001          37 :       wip = u.notes.getChange().isWorkInProgress();
    1002             :     }
    1003             : 
    1004             :     ChangeReportFormatter.Input input =
    1005          37 :         ChangeReportFormatter.Input.builder()
    1006          37 :             .setChange(u.notes.getChange())
    1007          37 :             .setSubject(subject)
    1008          37 :             .setIsEdit(edit)
    1009          37 :             .setIsPrivate(isPrivate)
    1010          37 :             .setIsWorkInProgress(wip)
    1011          37 :             .build();
    1012          37 :     addMessage(changeFormatter.changeUpdated(input));
    1013          37 :     u.getOutdatedApprovalsMessage().map(msg -> "\n" + msg + "\n").ifPresent(this::addMessage);
    1014          37 :   }
    1015             : 
    1016             :   private void insertChangesAndPatchSets(
    1017             :       ImmutableList<CreateRequest> newChanges, Task replaceProgress) {
    1018          89 :     try (TraceTimer traceTimer =
    1019          89 :         newTimer(
    1020          89 :             "insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
    1021          89 :       ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
    1022          89 :       if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
    1023           9 :         logger.atWarning().log(
    1024             :             "Skipping change updates on %s because ref update failed: %s %s",
    1025           9 :             project.getName(),
    1026           9 :             magicBranchCmd.getResult(),
    1027           9 :             Strings.nullToEmpty(magicBranchCmd.getMessage()));
    1028           9 :         return;
    1029             :       }
    1030             : 
    1031          88 :       try (BatchUpdate bu =
    1032          88 :               batchUpdateFactory.create(
    1033          88 :                   project.getNameKey(), user.materializedCopy(), TimeUtil.now());
    1034          88 :           ObjectInserter ins = repo.newObjectInserter();
    1035          88 :           ObjectReader reader = ins.newReader();
    1036          88 :           RevWalk rw = new RevWalk(reader)) {
    1037          88 :         bu.setRepository(repo, rw, ins);
    1038          88 :         bu.setRefLogMessage("push");
    1039          88 :         if (magicBranch != null) {
    1040          88 :           bu.setNotify(magicBranch.getNotifyForNewChange());
    1041             :         }
    1042             : 
    1043          88 :         logger.atFine().log("Adding %d replace requests", newChanges.size());
    1044          88 :         for (ReplaceRequest replace : replaceByChange.values()) {
    1045          37 :           replace.addOps(bu, replaceProgress);
    1046          37 :           if (magicBranch != null) {
    1047          37 :             bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
    1048          37 :             if (magicBranch.shouldPublishComments()) {
    1049           4 :               bu.addOp(
    1050           4 :                   replace.notes.getChangeId(),
    1051           4 :                   publishCommentsOp.create(replace.psId, project.getNameKey()));
    1052           4 :               Optional<ChangeNotes> changeNotes = getChangeNotes(replace.notes.getChangeId());
    1053           4 :               if (!changeNotes.isPresent()) {
    1054             :                 // If not present, no need to update attention set here since this is a new change.
    1055           0 :                 continue;
    1056             :               }
    1057           4 :               List<HumanComment> drafts =
    1058           4 :                   commentsUtil.draftByChangeAuthor(changeNotes.get(), user.getAccountId());
    1059           4 :               if (drafts.isEmpty()) {
    1060             :                 // If no comments, attention set shouldn't update since the user didn't reply.
    1061           1 :                 continue;
    1062             :               }
    1063           4 :               replyAttentionSetUpdates.processAutomaticAttentionSetRulesOnReply(
    1064           4 :                   bu, changeNotes.get(), isReadyForReview(changeNotes.get()), user, drafts);
    1065             :             }
    1066             :           }
    1067          37 :         }
    1068             : 
    1069          88 :         logger.atFine().log("Adding %d create requests", newChanges.size());
    1070          88 :         for (CreateRequest create : newChanges) {
    1071          88 :           create.addOps(bu);
    1072          88 :         }
    1073             : 
    1074          88 :         logger.atFine().log("Adding %d group update requests", newChanges.size());
    1075          88 :         updateGroups.forEach(r -> r.addOps(bu));
    1076             : 
    1077          88 :         logger.atFine().log("Executing batch");
    1078             :         try {
    1079          88 :           bu.execute();
    1080           3 :         } catch (UpdateException e) {
    1081           3 :           throw asRestApiException(e);
    1082          88 :         }
    1083             : 
    1084          88 :         replaceByChange.values().stream()
    1085          88 :             .forEach(
    1086             :                 req ->
    1087          37 :                     result.addChange(ReceiveCommitsResult.ChangeStatus.REPLACED, req.ontoChange));
    1088          88 :         newChanges.stream()
    1089          88 :             .forEach(
    1090          88 :                 req -> result.addChange(ReceiveCommitsResult.ChangeStatus.CREATED, req.changeId));
    1091             : 
    1092          88 :         if (magicBranchCmd != null) {
    1093          88 :           magicBranchCmd.setResult(OK);
    1094             :         }
    1095          88 :         for (ReplaceRequest replace : replaceByChange.values()) {
    1096          37 :           String rejectMessage = replace.getRejectMessage();
    1097          37 :           if (rejectMessage == null) {
    1098          37 :             if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
    1099             :               // Not necessarily the magic branch, so need to set OK on the original value.
    1100           0 :               replace.inputCommand.setResult(OK);
    1101             :             }
    1102             :           } else {
    1103           0 :             logger.atFine().log("Rejecting due to message from ReplaceOp");
    1104           0 :             reject(replace.inputCommand, rejectMessage);
    1105             :           }
    1106          37 :         }
    1107             : 
    1108           0 :       } catch (ResourceConflictException e) {
    1109           0 :         addError(e.getMessage());
    1110           0 :         reject(magicBranchCmd, "conflict");
    1111           3 :       } catch (BadRequestException | UnprocessableEntityException | AuthException e) {
    1112           3 :         logger.atFine().withCause(e).log("Rejecting due to client error");
    1113           3 :         reject(magicBranchCmd, e.getMessage());
    1114           3 :       } catch (RestApiException | IOException e) {
    1115           3 :         throw new StorageException("Can't insert change/patch set for " + project.getName(), e);
    1116          88 :       }
    1117             : 
    1118          88 :       if (magicBranch != null && magicBranch.submit) {
    1119             :         try {
    1120           8 :           submit(newChanges, replaceByChange.values());
    1121           2 :         } catch (ResourceConflictException e) {
    1122           2 :           addError(e.getMessage());
    1123           2 :           reject(magicBranchCmd, "conflict");
    1124           0 :         } catch (RestApiException
    1125             :             | StorageException
    1126             :             | UpdateException
    1127             :             | IOException
    1128             :             | ConfigInvalidException
    1129             :             | PermissionBackendException e) {
    1130           0 :           logger.atSevere().withCause(e).log("Error submitting changes to %s", project.getName());
    1131           0 :           reject(magicBranchCmd, "error during submit");
    1132           8 :         }
    1133             :       }
    1134           9 :     }
    1135          88 :   }
    1136             : 
    1137             :   private boolean isReadyForReview(ChangeNotes changeNotes) {
    1138           4 :     return (!changeNotes.getChange().isWorkInProgress() && !magicBranch.workInProgress)
    1139             :         || magicBranch.ready;
    1140             :   }
    1141             : 
    1142             :   private String buildError(String error, List<String> branches) {
    1143          12 :     StringBuilder sb = new StringBuilder();
    1144          12 :     if (branches.size() == 1) {
    1145          12 :       String branch = branches.get(0);
    1146          12 :       sb.append("branch ").append(branch).append(":\n");
    1147             :       // As of 2020, there are still many git-review <1.27 installations in the wild.
    1148             :       // These users will see failures as their old git-review assumes that
    1149             :       // `refs/publish/...` is still magic, which it isn't. As Gerrit's default error messages are
    1150             :       // misleading for these users, we hint them at upgrading their git-review.
    1151          12 :       if (branch.startsWith("refs/publish/")) {
    1152           0 :         sb.append("If you are using git-review, update to at least git-review 1.27. Otherwise:\n");
    1153             :       }
    1154          12 :       sb.append(error);
    1155          12 :       return sb.toString();
    1156             :     }
    1157           1 :     sb.append("branches ").append(Joiner.on(", ").join(branches));
    1158           1 :     return sb.append(":\n").append(error).toString();
    1159             :   }
    1160             : 
    1161             :   /** Parses push options specified as "git push -o OPTION" */
    1162             :   private void parsePushOptions() {
    1163          96 :     List<String> optionList = receivePack.getPushOptions();
    1164          96 :     if (optionList != null) {
    1165           7 :       for (String option : optionList) {
    1166           6 :         int e = option.indexOf('=');
    1167           6 :         if (e > 0) {
    1168           6 :           pushOptions.put(option.substring(0, e), option.substring(e + 1));
    1169             :         } else {
    1170           5 :           pushOptions.put(option, "");
    1171             :         }
    1172           6 :       }
    1173             :     }
    1174             : 
    1175          96 :     List<String> noteDbValues = pushOptions.get("notedb");
    1176          96 :     if (!noteDbValues.isEmpty()) {
    1177             :       // These semantics for duplicates/errors are somewhat arbitrary and may not match e.g. the
    1178             :       // CmdLineParser behavior used by MagicBranchInput.
    1179           4 :       String value = Iterables.getLast(noteDbValues);
    1180           4 :       noteDbPushOption = NoteDbPushOption.parse(value);
    1181           4 :       if (!noteDbPushOption.isPresent()) {
    1182           3 :         addError("Invalid value in -o " + NoteDbPushOption.OPTION_NAME + "=" + value);
    1183             :       }
    1184           4 :     } else {
    1185          96 :       noteDbPushOption = Optional.of(NoteDbPushOption.DISALLOW);
    1186             :     }
    1187             : 
    1188          96 :     List<String> traceValues = pushOptions.get("trace");
    1189          96 :     if (!traceValues.isEmpty()) {
    1190           4 :       tracePushOption = Optional.of(Iterables.getLast(traceValues));
    1191             :     } else {
    1192          96 :       tracePushOption = Optional.empty();
    1193             :     }
    1194          96 :   }
    1195             : 
    1196             :   // Wrap ReceiveCommand so the progress counter works automatically.
    1197             :   private ReceiveCommand wrapReceiveCommand(ReceiveCommand cmd, Task progress) {
    1198          96 :     String refname = cmd.getRefName();
    1199             : 
    1200          96 :     if (isRefsUsersSelf(cmd.getRefName(), projectState.isAllUsers())) {
    1201           2 :       refname = RefNames.refsUsers(user.getAccountId());
    1202           2 :       logger.atFine().log("Swapping out command for %s to %s", RefNames.REFS_USERS_SELF, refname);
    1203             :     }
    1204             : 
    1205             :     // We must also update the original, because callers may inspect it afterwards to decide if
    1206             :     // the command went through or not.
    1207          96 :     return new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), refname, cmd.getType()) {
    1208             :       @Override
    1209             :       public void setResult(Result s, String m) {
    1210          96 :         if (getResult() == NOT_ATTEMPTED) { // Only report the progress update once.
    1211          96 :           progress.update(1);
    1212             :         }
    1213             :         // Counter intuitively, we don't check that results == NOT_ATTEMPTED here.
    1214             :         // This is so submit-on-push can still reject the update if the change is created
    1215             :         // successfully
    1216             :         // (status OK) but the submit failed (merge failed: REJECTED_OTHER_REASON).
    1217          96 :         super.setResult(s, m);
    1218          96 :         cmd.setResult(s, m);
    1219          96 :       }
    1220             :     };
    1221             :   }
    1222             : 
    1223             :   /*
    1224             :    * Interpret a normal push.
    1225             :    */
    1226             :   private void parseRegularCommand(ReceiveCommand cmd)
    1227             :       throws PermissionBackendException, NoSuchProjectException, IOException {
    1228          49 :     try (TraceTimer traceTimer = newTimer("parseRegularCommand")) {
    1229          49 :       if (cmd.getResult() != NOT_ATTEMPTED) {
    1230             :         // Already rejected by the core receive process.
    1231           0 :         logger.atFine().log("Already processed by core: %s %s", cmd.getResult(), cmd);
    1232           0 :         return;
    1233             :       }
    1234             : 
    1235          49 :       if (!Repository.isValidRefName(cmd.getRefName()) || cmd.getRefName().contains("//")) {
    1236           0 :         reject(cmd, "not valid ref");
    1237           0 :         return;
    1238             :       }
    1239          49 :       if (RefNames.isNoteDbMetaRef(cmd.getRefName())) {
    1240             :         // Reject pushes to NoteDb refs without a special option and permission. Note that this
    1241             :         // prohibition doesn't depend on NoteDb being enabled in any way, since all sites will
    1242             :         // migrate to NoteDb eventually, and we don't want garbage data waiting there when the
    1243             :         // migration finishes.
    1244           4 :         logger.atFine().log(
    1245             :             "%s NoteDb ref %s with %s=%s",
    1246           4 :             cmd.getType(), cmd.getRefName(), NoteDbPushOption.OPTION_NAME, noteDbPushOption);
    1247           4 :         if (!Optional.of(NoteDbPushOption.ALLOW).equals(noteDbPushOption)) {
    1248             :           // Only reject this command, not the whole push. This supports the use case of "git clone
    1249             :           // --mirror" followed by "git push --mirror", when the user doesn't really intend to clone
    1250             :           // or mirror the NoteDb data; there is no single refspec that describes all refs *except*
    1251             :           // NoteDb refs.
    1252           3 :           reject(
    1253             :               cmd,
    1254             :               "NoteDb update requires -o "
    1255             :                   + NoteDbPushOption.OPTION_NAME
    1256             :                   + "="
    1257           3 :                   + NoteDbPushOption.ALLOW.value());
    1258           3 :           return;
    1259             :         }
    1260           4 :         if (!permissionBackend.user(user).test(GlobalPermission.ACCESS_DATABASE)) {
    1261           4 :           reject(cmd, "NoteDb update requires access database permission");
    1262           4 :           return;
    1263             :         }
    1264             :       }
    1265             : 
    1266          49 :       switch (cmd.getType()) {
    1267             :         case CREATE:
    1268          29 :           parseCreate(cmd);
    1269          29 :           break;
    1270             : 
    1271             :         case UPDATE:
    1272          44 :           parseUpdate(cmd);
    1273          44 :           break;
    1274             : 
    1275             :         case DELETE:
    1276          11 :           parseDelete(cmd);
    1277          11 :           break;
    1278             : 
    1279             :         case UPDATE_NONFASTFORWARD:
    1280          13 :           parseRewind(cmd);
    1281          13 :           break;
    1282             : 
    1283             :         default:
    1284           0 :           reject(cmd, "prohibited by Gerrit: unknown command type " + cmd.getType());
    1285           0 :           return;
    1286             :       }
    1287             : 
    1288          49 :       if (cmd.getResult() != NOT_ATTEMPTED) {
    1289          22 :         return;
    1290             :       }
    1291             : 
    1292          47 :       if (isConfig(cmd)) {
    1293           5 :         validateConfigPush(cmd);
    1294             :       }
    1295          22 :     }
    1296          47 :   }
    1297             : 
    1298             :   /** Validates a push to refs/meta/config, and reject the command if it fails. */
    1299             :   private void validateConfigPush(ReceiveCommand cmd) throws PermissionBackendException {
    1300           5 :     try (TraceTimer traceTimer = newTimer("validateConfigPush")) {
    1301           5 :       logger.atFine().log("Processing %s command", cmd.getRefName());
    1302           5 :       if (!permissions.test(ProjectPermission.WRITE_CONFIG)) {
    1303           0 :         reject(
    1304             :             cmd,
    1305           0 :             String.format(
    1306             :                 "must be either project owner or have %s permission",
    1307           0 :                 ProjectPermission.WRITE_CONFIG.describeForException()));
    1308           0 :         return;
    1309             :       }
    1310             : 
    1311           5 :       switch (cmd.getType()) {
    1312             :         case CREATE:
    1313             :         case UPDATE:
    1314             :         case UPDATE_NONFASTFORWARD:
    1315             :           try {
    1316           5 :             ProjectConfig cfg = projectConfigFactory.create(project.getNameKey());
    1317           5 :             cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
    1318           5 :             if (!cfg.getValidationErrors().isEmpty()) {
    1319           0 :               addError("Invalid project configuration:");
    1320           0 :               for (ValidationError err : cfg.getValidationErrors()) {
    1321           0 :                 addError("  " + err.getMessage());
    1322           0 :               }
    1323           0 :               reject(cmd, "invalid project configuration");
    1324           0 :               logger.atSevere().log(
    1325             :                   "User %s tried to push invalid project configuration %s for %s",
    1326           0 :                   user.getLoggableName(), cmd.getNewId().name(), project.getName());
    1327           0 :               return;
    1328             :             }
    1329           5 :             Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
    1330           5 :             Project.NameKey oldParent = project.getParent(allProjectsName);
    1331           5 :             if (oldParent == null) {
    1332             :               // update of the 'All-Projects' project
    1333           1 :               if (newParent != null) {
    1334           0 :                 reject(cmd, "invalid project configuration: root project cannot have parent");
    1335           0 :                 return;
    1336             :               }
    1337             :             } else {
    1338           5 :               if (!oldParent.equals(newParent)) {
    1339           1 :                 if (allowProjectOwnersToChangeParent) {
    1340           0 :                   if (!permissionBackend
    1341           0 :                       .user(user)
    1342           0 :                       .project(project.getNameKey())
    1343           0 :                       .test(ProjectPermission.WRITE_CONFIG)) {
    1344           0 :                     reject(
    1345             :                         cmd, "invalid project configuration: only project owners can set parent");
    1346           0 :                     return;
    1347             :                   }
    1348             :                 } else {
    1349           1 :                   if (!permissionBackend.user(user).test(GlobalPermission.ADMINISTRATE_SERVER)) {
    1350           1 :                     reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
    1351           1 :                     return;
    1352             :                   }
    1353             :                 }
    1354             :               }
    1355             : 
    1356           5 :               if (!projectCache.get(newParent).isPresent()) {
    1357           0 :                 reject(cmd, "invalid project configuration: parent does not exist");
    1358           0 :                 return;
    1359             :               }
    1360             :             }
    1361           5 :             validatePluginConfig(cmd, cfg);
    1362           0 :           } catch (Exception e) {
    1363           0 :             reject(cmd, "invalid project configuration");
    1364           0 :             logger.atSevere().withCause(e).log(
    1365             :                 "User %s tried to push invalid project configuration %s for %s",
    1366           0 :                 user.getLoggableName(), cmd.getNewId().name(), project.getName());
    1367           0 :             return;
    1368           5 :           }
    1369             :           break;
    1370             : 
    1371             :         case DELETE:
    1372           0 :           break;
    1373             : 
    1374             :         default:
    1375           0 :           reject(
    1376             :               cmd,
    1377             :               "prohibited by Gerrit: don't know how to handle config update of type "
    1378           0 :                   + cmd.getType());
    1379             :       }
    1380           1 :     }
    1381           5 :   }
    1382             : 
    1383             :   /**
    1384             :    * validates a push to refs/meta/config for plugin configuration, and rejects the push if it
    1385             :    * fails.
    1386             :    */
    1387             :   private void validatePluginConfig(ReceiveCommand cmd, ProjectConfig cfg) {
    1388           5 :     for (Extension<ProjectConfigEntry> e : pluginConfigEntries) {
    1389           0 :       PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName());
    1390           0 :       ProjectConfigEntry configEntry = e.getProvider().get();
    1391           0 :       String value = pluginCfg.getString(e.getExportName());
    1392           0 :       String oldValue =
    1393           0 :           projectState.getPluginConfig(e.getPluginName()).getString(e.getExportName());
    1394           0 :       if (configEntry.getType() == ProjectConfigEntryType.ARRAY) {
    1395           0 :         oldValue =
    1396           0 :             Arrays.stream(
    1397             :                     projectState
    1398           0 :                         .getPluginConfig(e.getPluginName())
    1399           0 :                         .getStringList(e.getExportName()))
    1400           0 :                 .collect(joining("\n"));
    1401             :       }
    1402             : 
    1403           0 :       if ((value == null ? oldValue != null : !value.equals(oldValue))
    1404           0 :           && !configEntry.isEditable(projectState)) {
    1405           0 :         reject(
    1406             :             cmd,
    1407           0 :             String.format(
    1408             :                 "invalid project configuration: Not allowed to set parameter"
    1409             :                     + " '%s' of plugin '%s' on project '%s'.",
    1410           0 :                 e.getExportName(), e.getPluginName(), project.getName()));
    1411           0 :         continue;
    1412             :       }
    1413             : 
    1414           0 :       if (ProjectConfigEntryType.LIST.equals(configEntry.getType())
    1415             :           && value != null
    1416           0 :           && !configEntry.getPermittedValues().contains(value)) {
    1417           0 :         reject(
    1418             :             cmd,
    1419           0 :             String.format(
    1420             :                 "invalid project configuration: The value '%s' is "
    1421             :                     + "not permitted for parameter '%s' of plugin '%s'.",
    1422           0 :                 value, e.getExportName(), e.getPluginName()));
    1423             :       }
    1424           0 :     }
    1425           5 :   }
    1426             : 
    1427             :   private void parseCreate(ReceiveCommand cmd)
    1428             :       throws PermissionBackendException, NoSuchProjectException, IOException {
    1429          29 :     try (TraceTimer traceTimer = newTimer("parseCreate")) {
    1430          29 :       if (repo.resolve(cmd.getRefName()) != null) {
    1431           8 :         reject(
    1432             :             cmd,
    1433           8 :             String.format("Cannot create ref '%s' because it already exists.", cmd.getRefName()));
    1434           8 :         return;
    1435             :       }
    1436             :       RevObject obj;
    1437             :       try {
    1438          29 :         obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
    1439           0 :       } catch (IOException e) {
    1440           0 :         throw new StorageException(
    1441           0 :             String.format(
    1442           0 :                 "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
    1443             :             e);
    1444          29 :       }
    1445          29 :       logger.atFine().log("Creating %s", cmd);
    1446             : 
    1447          29 :       if (isHead(cmd) && !isCommit(cmd)) {
    1448           0 :         return;
    1449             :       }
    1450             : 
    1451          29 :       BranchNameKey branch = BranchNameKey.create(project.getName(), cmd.getRefName());
    1452             :       try {
    1453             :         // Must pass explicit user instead of injecting a provider into CreateRefControl, since
    1454             :         // Provider<CurrentUser> within ReceiveCommits will always return anonymous.
    1455          29 :         createRefControl.checkCreateRef(
    1456          29 :             Providers.of(user), receivePack.getRepository(), branch, obj, /* forPush= */ true);
    1457           8 :       } catch (AuthException denied) {
    1458           8 :         rejectProhibited(cmd, denied);
    1459           8 :         return;
    1460           0 :       } catch (ResourceConflictException denied) {
    1461           0 :         reject(cmd, "prohibited by Gerrit: " + denied.getMessage());
    1462           0 :         return;
    1463          29 :       }
    1464             : 
    1465          29 :       if (validRefOperation(cmd)) {
    1466          29 :         validateRegularPushCommits(
    1467          29 :             BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
    1468             :       }
    1469           9 :     }
    1470          29 :   }
    1471             : 
    1472             :   private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
    1473          44 :     try (TraceTimer traceTimer = TraceContext.newTimer("parseUpdate")) {
    1474          44 :       logger.atFine().log("Updating %s", cmd);
    1475          44 :       Optional<AuthException> err = checkRefPermission(cmd, RefPermission.UPDATE);
    1476          44 :       if (!err.isPresent()) {
    1477          44 :         if (isHead(cmd) && !isCommit(cmd)) {
    1478           0 :           reject(cmd, "head must point to commit");
    1479           0 :           return;
    1480             :         }
    1481          44 :         if (validRefOperation(cmd)) {
    1482          44 :           validateRegularPushCommits(
    1483          44 :               BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
    1484             :         }
    1485             :       } else {
    1486           3 :         rejectProhibited(cmd, err.get());
    1487             :       }
    1488           0 :     }
    1489          44 :   }
    1490             : 
    1491             :   private boolean isCommit(ReceiveCommand cmd) {
    1492             :     RevObject obj;
    1493             :     try {
    1494          36 :       obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
    1495           0 :     } catch (IOException e) {
    1496           0 :       throw new StorageException(
    1497           0 :           String.format(
    1498           0 :               "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
    1499             :           e);
    1500          36 :     }
    1501             : 
    1502          36 :     if (obj instanceof RevCommit) {
    1503          36 :       return true;
    1504             :     }
    1505           0 :     reject(cmd, "not a commit");
    1506           0 :     return false;
    1507             :   }
    1508             : 
    1509             :   private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException {
    1510          11 :     try (TraceTimer traceTimer = newTimer("parseDelete")) {
    1511          11 :       logger.atFine().log("Deleting %s", cmd);
    1512          11 :       if (cmd.getRefName().startsWith(REFS_CHANGES)) {
    1513           0 :         errors.put(CANNOT_DELETE_CHANGES, cmd.getRefName());
    1514           0 :         reject(cmd, "cannot delete changes");
    1515          11 :       } else if (isConfigRef(cmd.getRefName())) {
    1516           0 :         errors.put(CANNOT_DELETE_CONFIG, cmd.getRefName());
    1517           0 :         reject(cmd, "cannot delete project configuration");
    1518             :       }
    1519             : 
    1520          11 :       Optional<AuthException> err = checkRefPermission(cmd, RefPermission.DELETE);
    1521          11 :       if (!err.isPresent()) {
    1522          10 :         validRefOperation(cmd);
    1523             :       } else {
    1524           7 :         rejectProhibited(cmd, err.get());
    1525             :       }
    1526             :     }
    1527          11 :   }
    1528             : 
    1529             :   private void parseRewind(ReceiveCommand cmd) throws PermissionBackendException {
    1530          13 :     try (TraceTimer traceTimer = newTimer("parseRewind")) {
    1531             :       try {
    1532          13 :         receivePack.getRevWalk().parseCommit(cmd.getNewId());
    1533           0 :       } catch (IOException e) {
    1534           0 :         throw new StorageException(
    1535           0 :             String.format(
    1536           0 :                 "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
    1537             :             e);
    1538          13 :       }
    1539          13 :       logger.atFine().log("Rewinding %s", cmd);
    1540             : 
    1541          13 :       if (!validRefOperation(cmd)) {
    1542           1 :         return;
    1543             :       }
    1544          12 :       validateRegularPushCommits(BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
    1545          12 :       if (cmd.getResult() != NOT_ATTEMPTED) {
    1546           0 :         return;
    1547             :       }
    1548             : 
    1549          12 :       Optional<AuthException> err = checkRefPermission(cmd, RefPermission.FORCE_UPDATE);
    1550          12 :       if (err.isPresent()) {
    1551           7 :         rejectProhibited(cmd, err.get());
    1552             :       }
    1553           1 :     }
    1554          12 :   }
    1555             : 
    1556             :   private Optional<AuthException> checkRefPermission(ReceiveCommand cmd, RefPermission perm)
    1557             :       throws PermissionBackendException {
    1558          49 :     return checkRefPermission(permissions.ref(cmd.getRefName()), perm);
    1559             :   }
    1560             : 
    1561             :   private Optional<AuthException> checkRefPermission(
    1562             :       PermissionBackend.ForRef forRef, RefPermission perm) throws PermissionBackendException {
    1563             :     try {
    1564          96 :       forRef.check(perm);
    1565          96 :       return Optional.empty();
    1566          12 :     } catch (AuthException e) {
    1567          12 :       return Optional.of(e);
    1568             :     }
    1569             :   }
    1570             : 
    1571             :   private void rejectProhibited(ReceiveCommand cmd, AuthException err) {
    1572          12 :     err.getAdvice().ifPresent(a -> errors.put(a, cmd.getRefName()));
    1573          12 :     reject(cmd, prohibited(err, cmd.getRefName()));
    1574          12 :   }
    1575             : 
    1576             :   private static String prohibited(AuthException e, String alreadyDisplayedResource) {
    1577          12 :     String msg = e.getMessage();
    1578          12 :     if (e instanceof PermissionDeniedException) {
    1579          12 :       PermissionDeniedException pde = (PermissionDeniedException) e;
    1580          12 :       if (pde.getResource().isPresent()
    1581          12 :           && pde.getResource().get().equals(alreadyDisplayedResource)) {
    1582             :         // Avoid repeating resource name if exactly the given name was already displayed by the
    1583             :         // generic git push machinery.
    1584          10 :         msg = PermissionDeniedException.MESSAGE_PREFIX + pde.describePermission();
    1585             :       }
    1586             :     }
    1587          12 :     return "prohibited by Gerrit: " + msg;
    1588             :   }
    1589             : 
    1590             :   static class MagicBranchInput {
    1591         152 :     private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings();
    1592             : 
    1593             :     private final IdentifiedUser user;
    1594             :     private final ProjectState projectState;
    1595             :     private final boolean defaultPublishComments;
    1596             : 
    1597             :     final ReceiveCommand cmd;
    1598             :     final LabelTypes labelTypes;
    1599             :     /**
    1600             :      * Draft comments are published with the commit iff {@code --publish-comments} is set. All
    1601             :      * drafts are withheld (overriding the option) if at least one of the following conditions are
    1602             :      * met:
    1603             :      *
    1604             :      * <ul>
    1605             :      *   <li>Installed {@link CommentValidator} plugins reject one or more draft comments.
    1606             :      *   <li>One or more comments exceed the maximum comment size (see {@link
    1607             :      *       CommentSizeValidator}).
    1608             :      *   <li>The maximum number of comments would be exceeded (see {@link CommentCountValidator}).
    1609             :      * </ul>
    1610             :      */
    1611          89 :     private boolean withholdComments = false;
    1612             : 
    1613             :     BranchNameKey dest;
    1614             :     PermissionBackend.ForRef perm;
    1615          89 :     Set<String> reviewer = Sets.newLinkedHashSet();
    1616          89 :     Set<String> cc = Sets.newLinkedHashSet();
    1617          89 :     Map<String, Short> labels = new HashMap<>();
    1618             :     String message;
    1619             :     List<RevCommit> baseCommit;
    1620             :     CmdLineParser cmdLineParser;
    1621          89 :     Set<String> hashtags = new HashSet<>();
    1622             : 
    1623             :     @Option(name = "--trace", metaVar = "NAME", usage = "enable tracing")
    1624             :     String trace;
    1625             : 
    1626             :     @Option(
    1627             :         name = "--deadline",
    1628             :         metaVar = "NAME",
    1629             :         usage = "deadline after which the push should be aborted")
    1630             :     String deadline;
    1631             : 
    1632             :     @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
    1633             :     List<ObjectId> base;
    1634             : 
    1635             :     @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
    1636             :     String topic;
    1637             : 
    1638             :     @Option(name = "--private", usage = "mark new/updated change as private")
    1639             :     boolean isPrivate;
    1640             : 
    1641             :     @Option(name = "--remove-private", usage = "remove privacy flag from updated change")
    1642             :     boolean removePrivate;
    1643             : 
    1644             :     /**
    1645             :      * The skip-validation option is defined to allow parsing it using the {@link #cmdLineParser}.
    1646             :      * However we do not allow this option for pushes to magic branches. This option is used to fail
    1647             :      * with a proper error message.
    1648             :      */
    1649             :     @Option(name = "--skip-validation", usage = "skips commit validation")
    1650             :     boolean skipValidation;
    1651             : 
    1652             :     @Option(
    1653             :         name = "--wip",
    1654             :         aliases = {"-work-in-progress"},
    1655             :         usage = "mark change as work in progress")
    1656             :     boolean workInProgress;
    1657             : 
    1658             :     @Option(name = "--ready", usage = "mark change as ready")
    1659             :     boolean ready;
    1660             : 
    1661             :     @Option(
    1662             :         name = "--edit",
    1663             :         aliases = {"-e"},
    1664             :         usage = "upload as change edit")
    1665             :     boolean edit;
    1666             : 
    1667             :     @Option(name = "--submit", usage = "immediately submit the change")
    1668             :     boolean submit;
    1669             : 
    1670             :     @Option(name = "--merged", usage = "create single change for a merged commit")
    1671             :     boolean merged;
    1672             : 
    1673             :     @Option(name = "--publish-comments", usage = "publish all draft comments on updated changes")
    1674             :     private boolean publishComments;
    1675             : 
    1676             :     @Option(
    1677             :         name = "--no-publish-comments",
    1678             :         aliases = {"--np"},
    1679             :         usage = "do not publish draft comments")
    1680             :     private boolean noPublishComments;
    1681             : 
    1682             :     @Option(
    1683             :         name = "--notify",
    1684             :         usage =
    1685             :             "Notify handling that defines to whom email notifications "
    1686             :                 + "should be sent. Allowed values are NONE, OWNER, "
    1687             :                 + "OWNER_REVIEWERS, ALL. If not set, the default is ALL.")
    1688             :     private NotifyHandling notifyHandling;
    1689             : 
    1690          89 :     @Option(
    1691             :         name = "--notify-to",
    1692             :         metaVar = "USER",
    1693             :         usage = "user that should be notified one time by email")
    1694             :     List<Account.Id> notifyTo = new ArrayList<>();
    1695             : 
    1696          89 :     @Option(
    1697             :         name = "--notify-cc",
    1698             :         metaVar = "USER",
    1699             :         usage = "user that should be CC'd one time by email")
    1700             :     List<Account.Id> notifyCc = new ArrayList<>();
    1701             : 
    1702          89 :     @Option(
    1703             :         name = "--notify-bcc",
    1704             :         metaVar = "USER",
    1705             :         usage = "user that should be BCC'd one time by email")
    1706             :     List<Account.Id> notifyBcc = new ArrayList<>();
    1707             : 
    1708             :     @Option(
    1709             :         name = "--reviewer",
    1710             :         aliases = {"-r"},
    1711             :         metaVar = "REVIEWER",
    1712             :         usage = "add reviewer to changes")
    1713             :     void reviewer(String str) {
    1714           6 :       reviewer.add(str);
    1715           6 :     }
    1716             : 
    1717             :     @Option(name = "--cc", metaVar = "CC", usage = "add CC to changes")
    1718             :     void cc(String str) {
    1719           6 :       cc.add(str);
    1720           6 :     }
    1721             : 
    1722             :     @Option(
    1723             :         name = "--label",
    1724             :         aliases = {"-l"},
    1725             :         metaVar = "LABEL+VALUE",
    1726             :         usage = "label(s) to assign (defaults to +1 if no value provided)")
    1727             :     void addLabel(String token) throws CmdLineException {
    1728           4 :       LabelVote v = LabelVote.parse(token);
    1729             :       try {
    1730           4 :         LabelType.checkName(v.label());
    1731           4 :         ApprovalsUtil.checkLabel(labelTypes, v.label(), v.value());
    1732           3 :       } catch (BadRequestException e) {
    1733           3 :         throw cmdLineParser.reject(e.getMessage());
    1734           4 :       }
    1735           4 :       labels.put(v.label(), v.value());
    1736           4 :     }
    1737             : 
    1738             :     @Option(
    1739             :         name = "--message",
    1740             :         aliases = {"-m"},
    1741             :         metaVar = "MESSAGE",
    1742             :         usage = "Comment message to apply to the review")
    1743             :     void addMessage(String token) {
    1744             :       // Many characters have special meaning in the context of a git ref.
    1745             :       //
    1746             :       // Clients can use underscores to represent spaces.
    1747           4 :       message = token.replace("_", " ");
    1748             :       try {
    1749             :         // Other characters can be represented using percent-encoding.
    1750           4 :         message = URLDecoder.decode(message, UTF_8.name());
    1751           3 :       } catch (IllegalArgumentException e) {
    1752             :         // Ignore decoding errors; leave message as percent-encoded.
    1753           0 :       } catch (UnsupportedEncodingException e) {
    1754             :         // This shouldn't happen; surely URLDecoder recognizes UTF-8.
    1755           0 :         throw new IllegalStateException(e);
    1756           4 :       }
    1757           4 :     }
    1758             : 
    1759             :     @Option(
    1760             :         name = "--hashtag",
    1761             :         aliases = {"-t"},
    1762             :         metaVar = "HASHTAG",
    1763             :         usage = "add hashtag to changes")
    1764             :     void addHashtag(String token) {
    1765           3 :       String hashtag = cleanupHashtag(token);
    1766           3 :       if (!hashtag.isEmpty()) {
    1767           3 :         hashtags.add(hashtag);
    1768             :       }
    1769           3 :     }
    1770             : 
    1771             :     @UsedAt(UsedAt.Project.GOOGLE)
    1772             :     @SuppressWarnings("unused") // unused in upstream, but used at Google
    1773             :     @Option(name = "--create-cod-token", usage = "create a token for consistency-on-demand")
    1774             :     private boolean createCodToken;
    1775             : 
    1776             :     @Option(
    1777             :         name = "--ignore-automatic-attention-set-rules",
    1778             :         aliases = {"-ias", "-ignore-attention-set"},
    1779             :         usage = "do not change the attention set on this push")
    1780             :     boolean ignoreAttentionSet;
    1781             : 
    1782             :     MagicBranchInput(
    1783          89 :         IdentifiedUser user, ProjectState projectState, ReceiveCommand cmd, LabelTypes labelTypes) {
    1784          89 :       this.user = user;
    1785          89 :       this.projectState = projectState;
    1786          89 :       this.cmd = cmd;
    1787          89 :       this.labelTypes = labelTypes;
    1788          89 :       GeneralPreferencesInfo prefs = user.state().generalPreferences();
    1789          89 :       this.defaultPublishComments =
    1790          89 :           prefs != null
    1791          89 :               ? firstNonNull(user.state().generalPreferences().publishCommentsOnPush, false)
    1792          89 :               : false;
    1793          89 :     }
    1794             : 
    1795             :     /**
    1796             :      * Get reviewer strings from magic branch options, combined with additional recipients computed
    1797             :      * from some other place.
    1798             :      *
    1799             :      * <p>The set of reviewers on a change includes strings passed explicitly via options as well as
    1800             :      * account IDs computed from the commit message itself.
    1801             :      *
    1802             :      * @param additionalRecipients recipients parsed from the commit.
    1803             :      * @return set of reviewer strings to pass to {@code ReviewerModifier}.
    1804             :      */
    1805             :     ImmutableSet<String> getCombinedReviewers(MailRecipients additionalRecipients) {
    1806          88 :       return getCombinedReviewers(reviewer, additionalRecipients.getReviewers());
    1807             :     }
    1808             : 
    1809             :     /**
    1810             :      * Get CC strings from magic branch options, combined with additional recipients computed from
    1811             :      * some other place.
    1812             :      *
    1813             :      * <p>The set of CCs on a change includes strings passed explicitly via options as well as
    1814             :      * account IDs computed from the commit message itself.
    1815             :      *
    1816             :      * @param additionalRecipients recipients parsed from the commit.
    1817             :      * @return set of CC strings to pass to {@code ReviewerModifier}.
    1818             :      */
    1819             :     ImmutableSet<String> getCombinedCcs(MailRecipients additionalRecipients) {
    1820          88 :       return getCombinedReviewers(cc, additionalRecipients.getCcOnly());
    1821             :     }
    1822             : 
    1823             :     private static ImmutableSet<String> getCombinedReviewers(
    1824             :         Set<String> strings, Set<Account.Id> ids) {
    1825          88 :       return Streams.concat(strings.stream(), ids.stream().map(Account.Id::toString))
    1826          88 :           .collect(toImmutableSet());
    1827             :     }
    1828             : 
    1829             :     void setWithholdComments(boolean withholdComments) {
    1830           4 :       this.withholdComments = withholdComments;
    1831           4 :     }
    1832             : 
    1833             :     boolean shouldPublishComments() {
    1834          38 :       if (withholdComments) {
    1835             :         // Validation messages of type WARNING have already been added, now withhold the comments.
    1836           1 :         return false;
    1837             :       }
    1838          38 :       if (publishComments) {
    1839           4 :         return true;
    1840             :       }
    1841          37 :       if (noPublishComments) {
    1842           3 :         return false;
    1843             :       }
    1844          37 :       return defaultPublishComments;
    1845             :     }
    1846             : 
    1847             :     /**
    1848             :      * returns the destination ref of the magic branch, and populates options in the cmdLineParser.
    1849             :      */
    1850             :     String parse(ListMultimap<String, String> pushOptions) throws CmdLineException {
    1851          89 :       String ref = RefNames.fullName(MagicBranch.getDestBranchName(cmd.getRefName()));
    1852             : 
    1853          89 :       ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
    1854             : 
    1855             :       // Process and lop off the "%OPTION" suffix.
    1856          89 :       int optionStart = ref.indexOf('%');
    1857          89 :       if (0 < optionStart) {
    1858          35 :         for (String s : COMMAS.split(ref.substring(optionStart + 1))) {
    1859          35 :           int e = s.indexOf('=');
    1860          35 :           if (0 < e) {
    1861          26 :             options.put(s.substring(0, e), s.substring(e + 1));
    1862             :           } else {
    1863          25 :             options.put(s, "");
    1864             :           }
    1865          35 :         }
    1866          35 :         ref = ref.substring(0, optionStart);
    1867             :       }
    1868             : 
    1869          89 :       if (!options.isEmpty()) {
    1870          35 :         cmdLineParser.parseOptionMap(options);
    1871             :       }
    1872          89 :       return ref;
    1873             :     }
    1874             : 
    1875             :     public boolean shouldSetWorkInProgressOnNewChanges() {
    1876             :       // When wip or ready explicitly provided, leave it as is.
    1877          88 :       if (workInProgress) {
    1878          13 :         return true;
    1879             :       }
    1880          88 :       if (ready) {
    1881           5 :         return false;
    1882             :       }
    1883             : 
    1884          88 :       return projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
    1885          88 :           || firstNonNull(user.state().generalPreferences().workInProgressByDefault, false);
    1886             :     }
    1887             : 
    1888             :     NotifyResolver.Result getNotifyForNewChange() {
    1889          88 :       return NotifyResolver.Result.create(
    1890          88 :           firstNonNull(
    1891             :               notifyHandling,
    1892          88 :               shouldSetWorkInProgressOnNewChanges() ? NotifyHandling.OWNER : NotifyHandling.ALL),
    1893          88 :           ImmutableSetMultimap.<RecipientType, Account.Id>builder()
    1894          88 :               .putAll(RecipientType.TO, notifyTo)
    1895          88 :               .putAll(RecipientType.CC, notifyCc)
    1896          88 :               .putAll(RecipientType.BCC, notifyBcc)
    1897          88 :               .build());
    1898             :     }
    1899             : 
    1900             :     NotifyHandling getNotifyHandling(ChangeNotes notes) {
    1901          37 :       requireNonNull(notes);
    1902          37 :       if (notifyHandling != null) {
    1903           1 :         return notifyHandling;
    1904             :       }
    1905          37 :       if (workInProgress || (!ready && notes.getChange().isWorkInProgress())) {
    1906           5 :         return NotifyHandling.OWNER;
    1907             :       }
    1908          37 :       return NotifyHandling.ALL;
    1909             :     }
    1910             :   }
    1911             : 
    1912             :   /**
    1913             :    * Parse the magic branch data (refs/for/BRANCH/OPTIONALTOPIC%OPTIONS) into the magicBranch
    1914             :    * member.
    1915             :    *
    1916             :    * <p>Assumes we are handling a magic branch here.
    1917             :    */
    1918             :   private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException, IOException {
    1919          89 :     try (TraceTimer traceTimer = newTimer("parseMagicBranch")) {
    1920          89 :       logger.atFine().log("Found magic branch %s", cmd.getRefName());
    1921          89 :       MagicBranchInput magicBranch = new MagicBranchInput(user, projectState, cmd, labelTypes);
    1922             : 
    1923             :       String ref;
    1924          89 :       magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
    1925             : 
    1926             :       // Filter out plugin push options, as the parser would reject them as unknown.
    1927          89 :       ImmutableListMultimap<String, String> pushOptionsToParse =
    1928          89 :           pushOptions.entries().stream()
    1929          89 :               .filter(e -> !isPluginPushOption(e.getKey()))
    1930          89 :               .collect(toImmutableListMultimap(e -> e.getKey(), e -> e.getValue()));
    1931             :       try {
    1932          89 :         ref = magicBranch.parse(pushOptionsToParse);
    1933           3 :       } catch (CmdLineException e) {
    1934           3 :         if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
    1935           3 :           logger.atFine().log("Invalid branch syntax");
    1936           3 :           reject(cmd, e.getMessage());
    1937           3 :           return;
    1938             :         }
    1939           0 :         ref = null; // never happens
    1940          89 :       }
    1941             : 
    1942          89 :       if (magicBranch.skipValidation) {
    1943           3 :         reject(
    1944             :             cmd,
    1945           3 :             String.format(
    1946             :                 "\"--%s\" option is only supported for direct push", PUSH_OPTION_SKIP_VALIDATION));
    1947           3 :         return;
    1948             :       }
    1949             : 
    1950          89 :       if (magicBranch.topic != null && magicBranch.topic.length() > ChangeUtil.TOPIC_MAX_LENGTH) {
    1951           3 :         reject(
    1952           3 :             cmd, String.format("topic length exceeds the limit (%d)", ChangeUtil.TOPIC_MAX_LENGTH));
    1953             :       }
    1954             : 
    1955          89 :       if (magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
    1956           3 :         StringWriter w = new StringWriter();
    1957           3 :         w.write("\nHelp for refs/for/branch:\n\n");
    1958           3 :         magicBranch.cmdLineParser.printUsage(w, null);
    1959             : 
    1960           3 :         String pluginPushOptionsHelp =
    1961           3 :             StreamSupport.stream(pluginPushOptions.entries().spliterator(), /* parallel= */ false)
    1962           3 :                 .map(
    1963             :                     e ->
    1964           3 :                         String.format(
    1965             :                             "-o %s~%s: %s",
    1966           3 :                             e.getPluginName(), e.get().getName(), e.get().getDescription()))
    1967           3 :                 .sorted()
    1968           3 :                 .collect(joining("\n"));
    1969           3 :         if (!pluginPushOptionsHelp.isEmpty()) {
    1970           3 :           w.write("\nPlugin push options:\n" + pluginPushOptionsHelp);
    1971             :         }
    1972             : 
    1973           3 :         addMessage(w.toString());
    1974           3 :         reject(cmd, "see help");
    1975           3 :         return;
    1976             :       }
    1977          89 :       if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
    1978           2 :         logger.atFine().log("Handling %s", RefNames.REFS_USERS_SELF);
    1979           2 :         ref = RefNames.refsUsers(user.getAccountId());
    1980             :       }
    1981             :       // Pushing changes for review usually requires that the target branch exists, but there is an
    1982             :       // exception for the branch to which HEAD points to and for refs/meta/config. Pushing for
    1983             :       // review to these branches is allowed even if the branch does not exist yet. This allows to
    1984             :       // push initial code for review to an empty repository and to review an initial project
    1985             :       // configuration.
    1986          89 :       if (receivePackRefCache.exactRef(ref) == null
    1987          19 :           && !ref.equals(readHEAD(repo))
    1988           5 :           && !ref.equals(RefNames.REFS_CONFIG)) {
    1989           5 :         logger.atFine().log("Ref %s not found", ref);
    1990           5 :         if (ref.startsWith(Constants.R_HEADS)) {
    1991           5 :           String n = ref.substring(Constants.R_HEADS.length());
    1992           5 :           reject(cmd, "branch " + n + " not found");
    1993           5 :         } else {
    1994           0 :           reject(cmd, ref + " not found");
    1995             :         }
    1996           5 :         return;
    1997             :       }
    1998             : 
    1999          89 :       magicBranch.dest = BranchNameKey.create(project.getNameKey(), ref);
    2000          89 :       magicBranch.perm = permissions.ref(ref);
    2001             : 
    2002          89 :       Optional<AuthException> err =
    2003          89 :           checkRefPermission(magicBranch.perm, RefPermission.READ)
    2004          89 :               .map(Optional::of)
    2005          89 :               .orElse(checkRefPermission(magicBranch.perm, RefPermission.CREATE_CHANGE));
    2006          89 :       if (err.isPresent()) {
    2007           1 :         rejectProhibited(cmd, err.get());
    2008           1 :         return;
    2009             :       }
    2010             : 
    2011          89 :       if (magicBranch.isPrivate && magicBranch.removePrivate) {
    2012           3 :         reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive");
    2013           3 :         return;
    2014             :       }
    2015             : 
    2016          89 :       boolean privateByDefault =
    2017             :           projectCache
    2018          89 :               .get(project.getNameKey())
    2019          89 :               .orElseThrow(illegalState(project.getNameKey()))
    2020          89 :               .is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
    2021          89 :       setChangeAsPrivate =
    2022             :           magicBranch.isPrivate || (privateByDefault && !magicBranch.removePrivate);
    2023             : 
    2024          89 :       if (receiveConfig.disablePrivateChanges && setChangeAsPrivate) {
    2025           2 :         reject(cmd, "private changes are disabled");
    2026           2 :         return;
    2027             :       }
    2028             : 
    2029          89 :       if (magicBranch.workInProgress && magicBranch.ready) {
    2030           3 :         reject(cmd, "the options 'wip' and 'ready' are mutually exclusive");
    2031           3 :         return;
    2032             :       }
    2033          89 :       if (magicBranch.publishComments && magicBranch.noPublishComments) {
    2034           0 :         reject(
    2035             :             cmd, "the options 'publish-comments' and 'no-publish-comments' are mutually exclusive");
    2036           0 :         return;
    2037             :       }
    2038             : 
    2039          89 :       if (magicBranch.submit) {
    2040           9 :         err = checkRefPermission(magicBranch.perm, RefPermission.UPDATE_BY_SUBMIT);
    2041           9 :         if (err.isPresent()) {
    2042           3 :           rejectProhibited(cmd, err.get());
    2043           3 :           return;
    2044             :         }
    2045             :       }
    2046             : 
    2047          89 :       RevWalk walk = receivePack.getRevWalk();
    2048             :       RevCommit tip;
    2049             :       try {
    2050          89 :         tip = walk.parseCommit(magicBranch.cmd.getNewId());
    2051          89 :         logger.atFine().log("Tip of push: %s", tip.name());
    2052           0 :       } catch (IOException ex) {
    2053           0 :         magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
    2054           0 :         logger.atSevere().withCause(ex).log(
    2055             :             "Invalid pack upload; one or more objects weren't sent");
    2056           0 :         return;
    2057          89 :       }
    2058             : 
    2059          89 :       String destBranch = magicBranch.dest.branch();
    2060             :       try {
    2061          89 :         if (magicBranch.merged) {
    2062           3 :           if (magicBranch.base != null) {
    2063           0 :             reject(cmd, "cannot use merged with base");
    2064           0 :             return;
    2065             :           }
    2066           3 :           Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch());
    2067           3 :           if (refTip == null) {
    2068           0 :             reject(cmd, magicBranch.dest.branch() + " not found");
    2069           0 :             return;
    2070             :           }
    2071           3 :           RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
    2072           3 :           if (!walk.isMergedInto(tip, branchTip)) {
    2073           3 :             reject(cmd, "not merged into branch");
    2074           3 :             return;
    2075             :           }
    2076             :         }
    2077             : 
    2078             :         // If tip is a merge commit, or the root commit or
    2079             :         // if %base or %merged was specified, ignore newChangeForAllNotInTarget.
    2080          89 :         if (tip.getParentCount() > 1
    2081             :             || magicBranch.base != null
    2082             :             || magicBranch.merged
    2083          89 :             || tip.getParentCount() == 0) {
    2084          28 :           logger.atFine().log("Forcing newChangeForAllNotInTarget = false");
    2085          28 :           newChangeForAllNotInTarget = false;
    2086             :         }
    2087             : 
    2088          89 :         if (magicBranch.base != null) {
    2089           3 :           logger.atFine().log("Handling %%base: %s", magicBranch.base);
    2090           3 :           magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size());
    2091           3 :           for (ObjectId id : magicBranch.base) {
    2092             :             try {
    2093           3 :               magicBranch.baseCommit.add(walk.parseCommit(id));
    2094           0 :             } catch (IncorrectObjectTypeException notCommit) {
    2095           0 :               reject(cmd, "base must be a commit");
    2096           0 :               return;
    2097           0 :             } catch (MissingObjectException e) {
    2098           0 :               reject(cmd, "base not found");
    2099           0 :               return;
    2100           0 :             } catch (IOException e) {
    2101           0 :               throw new StorageException(
    2102           0 :                   String.format("Project %s cannot read %s", project.getName(), id.name()), e);
    2103           3 :             }
    2104           3 :           }
    2105          89 :         } else if (newChangeForAllNotInTarget) {
    2106          11 :           Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch());
    2107          11 :           if (refTip != null) {
    2108          11 :             RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
    2109          11 :             magicBranch.baseCommit = Collections.singletonList(branchTip);
    2110          11 :             logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
    2111          11 :           } else {
    2112             :             // The target branch does not exist. Usually pushing changes for review requires that
    2113             :             // the
    2114             :             // target branch exists, but there is an exception for the branch to which HEAD points
    2115             :             // to
    2116             :             // and for refs/meta/config. Pushing for review to these branches is allowed even if the
    2117             :             // branch does not exist yet. This allows to push initial code for review to an empty
    2118             :             // repository and to review an initial project configuration.
    2119           3 :             if (!ref.equals(readHEAD(repo)) && !ref.equals(RefNames.REFS_CONFIG)) {
    2120           0 :               reject(cmd, magicBranch.dest.branch() + " not found");
    2121           0 :               return;
    2122             :             }
    2123             :           }
    2124             :         }
    2125           0 :       } catch (IOException e) {
    2126           0 :         throw new StorageException(
    2127           0 :             String.format("Error walking to %s in project %s", destBranch, project.getName()), e);
    2128          89 :       }
    2129             : 
    2130          89 :       if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
    2131          89 :         this.magicBranch = magicBranch;
    2132          89 :         this.result.magicPush(true);
    2133             :       }
    2134           8 :     }
    2135          89 :   }
    2136             : 
    2137             :   private boolean isPluginPushOption(String pushOptionName) {
    2138           4 :     if (transitionalPluginOptions.contains(pushOptionName)) {
    2139           3 :       return true;
    2140             :     }
    2141           4 :     return StreamSupport.stream(pluginPushOptions.entries().spliterator(), /* parallel= */ false)
    2142           4 :         .anyMatch(e -> pushOptionName.equals(e.getPluginName() + "~" + e.get().getName()));
    2143             :   }
    2144             : 
    2145             :   // Validate that the new commits are connected with the target
    2146             :   // branch.  If they aren't, we want to abort. We do this check by
    2147             :   // looking to see if we can compute a merge base between the new
    2148             :   // commits and the target branch head.
    2149             :   private boolean validateConnected(ReceiveCommand cmd, BranchNameKey dest, RevCommit tip) {
    2150          89 :     try (TraceTimer traceTimer =
    2151          89 :         newTimer("validateConnected", Metadata.builder().branchName(dest.branch()))) {
    2152          89 :       RevWalk walk = receivePack.getRevWalk();
    2153             :       try {
    2154          89 :         Ref targetRef = receivePackRefCache.exactRef(dest.branch());
    2155          89 :         if (targetRef == null || targetRef.getObjectId() == null) {
    2156             :           // The destination branch does not yet exist. Assume the
    2157             :           // history being sent for review will start it and thus
    2158             :           // is "connected" to the branch.
    2159          16 :           logger.atFine().log("Branch is unborn");
    2160             : 
    2161             :           // This is not an error condition.
    2162          16 :           return true;
    2163             :         }
    2164             : 
    2165          89 :         RevCommit h = walk.parseCommit(targetRef.getObjectId());
    2166          89 :         logger.atFine().log("Current branch tip: %s", h.name());
    2167          89 :         RevFilter oldRevFilter = walk.getRevFilter();
    2168             :         try {
    2169          89 :           walk.reset();
    2170          89 :           walk.setRevFilter(RevFilter.MERGE_BASE);
    2171          89 :           walk.markStart(tip);
    2172          89 :           walk.markStart(h);
    2173          89 :           if (walk.next() == null) {
    2174           4 :             reject(cmd, "no common ancestry");
    2175           4 :             return false;
    2176             :           }
    2177             :         } finally {
    2178          89 :           walk.reset();
    2179          89 :           walk.setRevFilter(oldRevFilter);
    2180             :         }
    2181           0 :       } catch (IOException e) {
    2182           0 :         cmd.setResult(REJECTED_MISSING_OBJECT);
    2183           0 :         logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
    2184           0 :         return false;
    2185          89 :       }
    2186          89 :       return true;
    2187          17 :     }
    2188             :   }
    2189             : 
    2190             :   private static String readHEAD(Repository repo) {
    2191             :     try {
    2192          19 :       String head = repo.getFullBranch();
    2193          19 :       logger.atFine().log("HEAD = %s", head);
    2194          19 :       return head;
    2195           0 :     } catch (IOException e) {
    2196           0 :       throw new StorageException("Cannot read HEAD symref", e);
    2197             :     }
    2198             :   }
    2199             : 
    2200             :   /**
    2201             :    * Update an existing change. If draft comments are to be published, these are validated and may
    2202             :    * be withheld.
    2203             :    *
    2204             :    * @return True if the command succeeded, false if it was rejected.
    2205             :    */
    2206             :   private boolean requestReplaceAndValidateComments(
    2207             :       ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit)
    2208             :       throws IOException {
    2209          38 :     try (TraceTimer traceTimer = newTimer("requestReplaceAndValidateComments")) {
    2210          38 :       if (change.isClosed()) {
    2211           3 :         reject(
    2212             :             cmd,
    2213           3 :             changeFormatter.changeClosed(
    2214           3 :                 ChangeReportFormatter.Input.builder().setChange(change).build()));
    2215           3 :         return false;
    2216             :       }
    2217             : 
    2218          38 :       ReplaceRequest req = new ReplaceRequest(change.getId(), newCommit, cmd, checkMergedInto);
    2219          38 :       if (replaceByChange.containsKey(req.ontoChange)) {
    2220           0 :         reject(cmd, "duplicate request");
    2221           0 :         return false;
    2222             :       }
    2223             : 
    2224          38 :       if (magicBranch != null && magicBranch.shouldPublishComments()) {
    2225           4 :         List<HumanComment> drafts =
    2226           4 :             commentsUtil.draftByChangeAuthor(
    2227           4 :                 notesFactory.createChecked(change), user.getAccountId());
    2228           4 :         ImmutableList<CommentForValidation> draftsForValidation =
    2229           4 :             drafts.stream()
    2230           4 :                 .map(
    2231             :                     comment ->
    2232           4 :                         CommentForValidation.create(
    2233             :                             CommentSource.HUMAN,
    2234           4 :                             comment.lineNbr > 0
    2235           4 :                                 ? CommentType.INLINE_COMMENT
    2236           4 :                                 : CommentType.FILE_COMMENT,
    2237             :                             comment.message,
    2238           4 :                             comment.message.length()))
    2239           4 :                 .collect(toImmutableList());
    2240           4 :         CommentValidationContext ctx =
    2241           4 :             CommentValidationContext.create(
    2242           4 :                 change.getChangeId(), change.getProject().get(), change.getDest().branch());
    2243           4 :         ImmutableList<CommentValidationFailure> commentValidationFailures =
    2244           4 :             PublishCommentUtil.findInvalidComments(ctx, commentValidators, draftsForValidation);
    2245           4 :         magicBranch.setWithholdComments(!commentValidationFailures.isEmpty());
    2246           4 :         commentValidationFailures.forEach(
    2247             :             failure ->
    2248           1 :                 addMessage(
    2249           1 :                     "Comment validation failure: " + failure.getMessage(),
    2250             :                     ValidationMessage.Type.WARNING));
    2251             :       }
    2252             : 
    2253          38 :       replaceByChange.put(req.ontoChange, req);
    2254          38 :       return true;
    2255           3 :     }
    2256             :   }
    2257             : 
    2258             :   private void warnAboutMissingChangeId(ImmutableList<CreateRequest> newChanges) {
    2259          89 :     for (CreateRequest create : newChanges) {
    2260             :       try {
    2261          88 :         receivePack.getRevWalk().parseBody(create.commit);
    2262           0 :       } catch (IOException e) {
    2263           0 :         throw new StorageException("Can't parse commit", e);
    2264          88 :       }
    2265          88 :       List<String> idList = ChangeUtil.getChangeIdsFromFooter(create.commit, urlFormatter.get());
    2266             : 
    2267          88 :       if (idList.isEmpty()) {
    2268           3 :         messages.add(
    2269             :             new ValidationMessage("warning: pushing without Change-Id is deprecated", false));
    2270           3 :         break;
    2271             :       }
    2272          88 :     }
    2273          89 :   }
    2274             : 
    2275             :   private ImmutableList<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress)
    2276             :       throws IOException {
    2277          89 :     try (TraceTimer traceTimer = newTimer("selectNewAndReplacedChangesFromMagicBranch")) {
    2278          89 :       logger.atFine().log("Finding new and replaced changes");
    2279          89 :       List<CreateRequest> newChanges = new ArrayList<>();
    2280             : 
    2281          89 :       GroupCollector groupCollector =
    2282          89 :           GroupCollector.create(receivePackRefCache, psUtil, notesFactory, project.getNameKey());
    2283             : 
    2284          89 :       BranchCommitValidator validator =
    2285          89 :           commitValidatorFactory.create(projectState, magicBranch.dest, user);
    2286             : 
    2287             :       try {
    2288          89 :         RevCommit start = setUpWalkForSelectingChanges();
    2289          89 :         if (start == null) {
    2290           0 :           return ImmutableList.of();
    2291             :         }
    2292             : 
    2293          89 :         LinkedHashMap<RevCommit, ChangeLookup> pending = new LinkedHashMap<>();
    2294          89 :         Set<Change.Key> newChangeIds = new HashSet<>();
    2295          89 :         int maxBatchChanges = receiveConfig.getEffectiveMaxBatchChangesLimit(user);
    2296          89 :         int total = 0;
    2297          89 :         int alreadyTracked = 0;
    2298          89 :         boolean rejectImplicitMerges =
    2299          89 :             start.getParentCount() == 1
    2300             :                 && projectCache
    2301          89 :                     .get(project.getNameKey())
    2302          89 :                     .orElseThrow(illegalState(project.getNameKey()))
    2303          89 :                     .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES)
    2304             :                 // Don't worry about implicit merges when creating changes for
    2305             :                 // already-merged commits; they're already in history, so it's too
    2306             :                 // late.
    2307             :                 && !magicBranch.merged;
    2308             :         Set<RevCommit> mergedParents;
    2309          89 :         if (rejectImplicitMerges) {
    2310           1 :           mergedParents = new HashSet<>();
    2311             :         } else {
    2312          89 :           mergedParents = null;
    2313             :         }
    2314             : 
    2315             :         for (; ; ) {
    2316          89 :           RevCommit c = receivePack.getRevWalk().next();
    2317          89 :           if (c == null) {
    2318          88 :             break;
    2319             :           }
    2320          89 :           total++;
    2321          89 :           receivePack.getRevWalk().parseBody(c);
    2322          89 :           String name = c.name();
    2323          89 :           groupCollector.visit(c);
    2324          89 :           Collection<PatchSet.Id> existingPatchSets =
    2325          89 :               receivePackRefCache.patchSetIdsFromObjectId(c);
    2326             : 
    2327          89 :           if (rejectImplicitMerges) {
    2328           1 :             Collections.addAll(mergedParents, c.getParents());
    2329           1 :             mergedParents.remove(c);
    2330             :           }
    2331             : 
    2332          89 :           boolean commitAlreadyTracked = !existingPatchSets.isEmpty();
    2333          89 :           if (commitAlreadyTracked) {
    2334          46 :             alreadyTracked++;
    2335             :             // Corner cases where an existing commit might need a new group:
    2336             :             // A) Existing commit has a null group; wasn't assigned during schema
    2337             :             //    upgrade, or schema upgrade is performed on a running server.
    2338             :             // B) Let A<-B<-C, then:
    2339             :             //      1. Push A to refs/heads/master
    2340             :             //      2. Push B to refs/for/master
    2341             :             //      3. Force push A~ to refs/heads/master
    2342             :             //      4. Push C to refs/for/master.
    2343             :             //      B will be in existing so we aren't replacing the patch set. It
    2344             :             //      used to have its own group, but now needs to to be changed to
    2345             :             //      A's group.
    2346             :             // C) Commit is a PatchSet of a pre-existing change uploaded with a
    2347             :             //    different target branch.
    2348          46 :             existingPatchSets.stream()
    2349          46 :                 .forEach(i -> updateGroups.add(new UpdateGroupsRequest(i, c)));
    2350          46 :             if (!(newChangeForAllNotInTarget || magicBranch.base != null)) {
    2351          46 :               continue;
    2352             :             }
    2353             :           }
    2354             : 
    2355          89 :           List<String> idList = ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get());
    2356          89 :           if (!idList.isEmpty()) {
    2357          89 :             pending.put(c, lookupByChangeKey(c, Change.key(idList.get(idList.size() - 1).trim())));
    2358             :           } else {
    2359           3 :             pending.put(c, lookupByCommit(c));
    2360             :           }
    2361             : 
    2362          89 :           int n = pending.size() + newChanges.size();
    2363          89 :           if (maxBatchChanges != 0 && n > maxBatchChanges) {
    2364           0 :             logger.atFine().log("%d changes exceeds limit of %d", n, maxBatchChanges);
    2365           0 :             reject(
    2366             :                 magicBranch.cmd,
    2367             :                 "the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges);
    2368           0 :             return ImmutableList.of();
    2369             :           }
    2370             : 
    2371          89 :           if (commitAlreadyTracked) {
    2372           5 :             boolean changeExistsOnDestBranch = false;
    2373           5 :             for (ChangeData cd : pending.get(c).destChanges) {
    2374           3 :               if (cd.change().getDest().equals(magicBranch.dest)) {
    2375           3 :                 changeExistsOnDestBranch = true;
    2376           3 :                 break;
    2377             :               }
    2378           0 :             }
    2379           5 :             if (changeExistsOnDestBranch) {
    2380           3 :               continue;
    2381             :             }
    2382             : 
    2383           5 :             logger.atFine().log(
    2384             :                 "Creating new change for %s even though it is already tracked", name);
    2385             :           }
    2386             : 
    2387          89 :           BranchCommitValidator.Result validationResult =
    2388          89 :               validator.validateCommit(
    2389             :                   repo,
    2390          89 :                   receivePack.getRevWalk().getObjectReader(),
    2391             :                   magicBranch.cmd,
    2392             :                   c,
    2393          89 :                   ImmutableListMultimap.copyOf(pushOptions),
    2394             :                   magicBranch.merged,
    2395             :                   rejectCommits,
    2396             :                   null);
    2397          89 :           messages.addAll(validationResult.messages());
    2398          89 :           if (!validationResult.isValid()) {
    2399             :             // Not a change the user can propose? Abort as early as possible.
    2400           6 :             logger.atFine().log("Aborting early due to invalid commit");
    2401           6 :             return ImmutableList.of();
    2402             :           }
    2403             : 
    2404             :           // Don't allow merges to be uploaded in commit chain via all-not-in-target
    2405          88 :           if (newChangeForAllNotInTarget && c.getParentCount() > 1) {
    2406           0 :             reject(
    2407             :                 magicBranch.cmd,
    2408             :                 "Pushing merges in commit chains with 'all not in target' is not allowed,\n"
    2409             :                     + "to override please set the base manually");
    2410           0 :             logger.atFine().log("Rejecting merge commit %s with newChangeForAllNotInTarget", name);
    2411             :             // TODO(dborowitz): Should we early return here?
    2412             :           }
    2413             : 
    2414          88 :           if (idList.isEmpty()) {
    2415           3 :             newChanges.add(new CreateRequest(c, magicBranch.dest.branch(), newProgress));
    2416           3 :             continue;
    2417             :           }
    2418          88 :         }
    2419          88 :         logger.atFine().log(
    2420             :             "Finished initial RevWalk with %d commits total: %d already"
    2421             :                 + " tracked, %d new changes with no Change-Id, and %d deferred"
    2422             :                 + " lookups",
    2423          88 :             total, alreadyTracked, newChanges.size(), pending.size());
    2424             : 
    2425          88 :         if (rejectImplicitMerges) {
    2426           1 :           rejectImplicitMerges(mergedParents);
    2427             :         }
    2428             : 
    2429          88 :         for (Iterator<ChangeLookup> itr = pending.values().iterator(); itr.hasNext(); ) {
    2430          88 :           ChangeLookup p = itr.next();
    2431          88 :           if (p.changeKey == null) {
    2432           3 :             continue;
    2433             :           }
    2434             : 
    2435          88 :           if (newChangeIds.contains(p.changeKey)) {
    2436           3 :             logger.atFine().log("Multiple commits with Change-Id %s", p.changeKey);
    2437           3 :             reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
    2438           3 :             return ImmutableList.of();
    2439             :           }
    2440             : 
    2441          88 :           List<ChangeData> changes = p.destChanges;
    2442          88 :           if (changes.size() > 1) {
    2443           0 :             logger.atFine().log(
    2444             :                 "Multiple changes in branch %s with Change-Id %s: %s",
    2445             :                 magicBranch.dest,
    2446             :                 p.changeKey,
    2447           0 :                 changes.stream().map(cd -> cd.getId().toString()).collect(joining()));
    2448             :             // WTF, multiple changes in this branch have the same key?
    2449             :             // Since the commit is new, the user should recreate it with
    2450             :             // a different Change-Id. In practice, we should never see
    2451             :             // this error message as Change-Id should be unique per branch.
    2452             :             //
    2453           0 :             reject(magicBranch.cmd, p.changeKey.get() + " has duplicates");
    2454           0 :             return ImmutableList.of();
    2455             :           }
    2456             : 
    2457          88 :           if (changes.size() == 1) {
    2458             :             // Schedule as a replacement to this one matching change.
    2459             :             //
    2460             : 
    2461          38 :             ObjectId currentPs = changes.get(0).currentPatchSet().commitId();
    2462             :             // If Commit is already current PatchSet of target Change.
    2463          38 :             if (p.commit.equals(currentPs)) {
    2464           3 :               if (pending.size() == 1) {
    2465             :                 // There are no commits left to check, all commits in pending were already
    2466             :                 // current PatchSet of the corresponding target changes.
    2467           3 :                 reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
    2468             :               } else {
    2469             :                 // Commit is already current PatchSet.
    2470             :                 // Remove from pending and try next commit.
    2471           3 :                 itr.remove();
    2472           3 :                 continue;
    2473             :               }
    2474             :             }
    2475          38 :             if (requestReplaceAndValidateComments(
    2476          38 :                 magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
    2477          38 :               continue;
    2478             :             }
    2479           3 :             return ImmutableList.of();
    2480             :           }
    2481             : 
    2482          88 :           if (changes.isEmpty()) {
    2483          88 :             if (!isValidChangeId(p.changeKey.get())) {
    2484           1 :               reject(magicBranch.cmd, "invalid Change-Id");
    2485           1 :               return ImmutableList.of();
    2486             :             }
    2487             : 
    2488             :             // In case the change look up from the index failed,
    2489             :             // double check against the existing refs
    2490          88 :             if (foundInExistingPatchSets(receivePackRefCache.patchSetIdsFromObjectId(p.commit))) {
    2491           3 :               if (pending.size() == 1) {
    2492           3 :                 reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
    2493           3 :                 return ImmutableList.of();
    2494             :               }
    2495           0 :               itr.remove();
    2496           0 :               continue;
    2497             :             }
    2498          88 :             newChangeIds.add(p.changeKey);
    2499             :           }
    2500          88 :           newChanges.add(new CreateRequest(p.commit, magicBranch.dest.branch(), newProgress));
    2501          88 :         }
    2502          88 :         logger.atFine().log(
    2503             :             "Finished deferred lookups with %d updates and %d new changes",
    2504          88 :             replaceByChange.size(), newChanges.size());
    2505           0 :       } catch (IOException e) {
    2506             :         // Should never happen, the core receive process would have
    2507             :         // identified the missing object earlier before we got control.
    2508           0 :         throw new StorageException("Invalid pack upload; one or more objects weren't sent", e);
    2509          88 :       }
    2510             : 
    2511          88 :       if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
    2512           3 :         reject(magicBranch.cmd, "no new changes");
    2513           3 :         return ImmutableList.of();
    2514             :       }
    2515          88 :       if (!newChanges.isEmpty() && magicBranch.edit) {
    2516           0 :         reject(magicBranch.cmd, "edit is not supported for new changes");
    2517           0 :         return ImmutableList.copyOf(newChanges);
    2518             :       }
    2519             : 
    2520          88 :       SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
    2521          88 :       List<Integer> newIds = seq.nextChangeIds(newChanges.size());
    2522          88 :       for (int i = 0; i < newChanges.size(); i++) {
    2523          88 :         CreateRequest create = newChanges.get(i);
    2524          88 :         create.setChangeId(newIds.get(i));
    2525          88 :         create.groups = ImmutableList.copyOf(groups.get(create.commit));
    2526             :       }
    2527          88 :       for (ReplaceRequest replace : replaceByChange.values()) {
    2528          38 :         replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
    2529          38 :       }
    2530          88 :       for (UpdateGroupsRequest update : updateGroups) {
    2531          45 :         update.groups = ImmutableList.copyOf(groups.get(update.commit));
    2532          45 :       }
    2533          88 :       logger.atFine().log("Finished updating groups from GroupCollector");
    2534          88 :       return ImmutableList.copyOf(newChanges);
    2535           6 :     }
    2536             :   }
    2537             : 
    2538             :   private boolean foundInExistingPatchSets(Collection<PatchSet.Id> existingPatchSets) {
    2539          88 :     try (TraceTimer traceTimer = newTimer("foundInExistingPatchSet")) {
    2540          88 :       for (PatchSet.Id psId : existingPatchSets) {
    2541           5 :         ChangeNotes notes = notesFactory.create(project.getNameKey(), psId.changeId());
    2542           5 :         Change change = notes.getChange();
    2543           5 :         if (change.getDest().equals(magicBranch.dest)) {
    2544           3 :           logger.atFine().log("Found change %s from existing refs.", change.getKey());
    2545             :           // Reindex the change asynchronously, ignoring errors.
    2546             :           @SuppressWarnings("unused")
    2547           3 :           Future<?> possiblyIgnoredError = indexer.indexAsync(project.getNameKey(), change.getId());
    2548           3 :           return true;
    2549             :         }
    2550           5 :       }
    2551          88 :       return false;
    2552           3 :     }
    2553             :   }
    2554             : 
    2555             :   private RevCommit setUpWalkForSelectingChanges() throws IOException {
    2556          89 :     try (TraceTimer traceTimer = newTimer("setUpWalkForSelectingChanges")) {
    2557          89 :       RevWalk rw = receivePack.getRevWalk();
    2558          89 :       RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId());
    2559             : 
    2560          89 :       rw.reset();
    2561          89 :       rw.sort(RevSort.TOPO);
    2562          89 :       rw.sort(RevSort.REVERSE, true);
    2563          89 :       receivePack.getRevWalk().markStart(start);
    2564          89 :       if (magicBranch.baseCommit != null) {
    2565          11 :         markExplicitBasesUninteresting();
    2566          89 :       } else if (magicBranch.merged) {
    2567           3 :         logger.atFine().log("Marking parents of merged commit %s uninteresting", start.name());
    2568           3 :         for (RevCommit c : start.getParents()) {
    2569           3 :           rw.markUninteresting(c);
    2570             :         }
    2571             :       } else {
    2572          89 :         markHeadsAsUninteresting(rw, magicBranch.dest != null ? magicBranch.dest.branch() : null);
    2573             :       }
    2574          89 :       return start;
    2575             :     }
    2576             :   }
    2577             : 
    2578             :   private void markExplicitBasesUninteresting() throws IOException {
    2579          11 :     try (TraceTimer traceTimer = newTimer("markExplicitBasesUninteresting")) {
    2580          11 :       logger.atFine().log("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
    2581          11 :       for (RevCommit c : magicBranch.baseCommit) {
    2582          11 :         receivePack.getRevWalk().markUninteresting(c);
    2583          11 :       }
    2584          11 :       Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
    2585          11 :       if (targetRef != null) {
    2586          11 :         logger.atFine().log(
    2587             :             "Marking target ref %s (%s) uninteresting",
    2588          11 :             magicBranch.dest.branch(), targetRef.getObjectId().name());
    2589          11 :         receivePack
    2590          11 :             .getRevWalk()
    2591          11 :             .markUninteresting(receivePack.getRevWalk().parseCommit(targetRef.getObjectId()));
    2592             :       }
    2593             :     }
    2594          11 :   }
    2595             : 
    2596             :   private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException {
    2597           1 :     try (TraceTimer traceTimer = newTimer("rejectImplicitMerges")) {
    2598           1 :       if (!mergedParents.isEmpty()) {
    2599           1 :         Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
    2600           1 :         if (targetRef != null) {
    2601           1 :           RevWalk rw = receivePack.getRevWalk();
    2602           1 :           RevCommit tip = rw.parseCommit(targetRef.getObjectId());
    2603           1 :           boolean containsImplicitMerges = true;
    2604           1 :           for (RevCommit p : mergedParents) {
    2605           1 :             containsImplicitMerges &= !rw.isMergedInto(p, tip);
    2606           1 :           }
    2607             : 
    2608           1 :           if (containsImplicitMerges) {
    2609           1 :             rw.reset();
    2610           1 :             for (RevCommit p : mergedParents) {
    2611           1 :               rw.markStart(p);
    2612           1 :             }
    2613           1 :             rw.markUninteresting(tip);
    2614             :             RevCommit c;
    2615           1 :             while ((c = rw.next()) != null) {
    2616           1 :               rw.parseBody(c);
    2617           1 :               messages.add(
    2618             :                   new CommitValidationMessage(
    2619             :                       "Implicit Merge of "
    2620           1 :                           + abbreviateName(c, rw.getObjectReader())
    2621             :                           + " "
    2622           1 :                           + c.getShortMessage(),
    2623             :                       ValidationMessage.Type.ERROR));
    2624             :             }
    2625           1 :             reject(magicBranch.cmd, "implicit merges detected");
    2626             :           }
    2627             :         }
    2628             :       }
    2629             :     }
    2630           1 :   }
    2631             : 
    2632             :   // Mark all branch tips as uninteresting in the given revwalk,
    2633             :   // so we get only the new commits when walking rw.
    2634             :   private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) throws IOException {
    2635          96 :     try (TraceTimer traceTimer =
    2636          96 :         newTimer("markHeadsAsUninteresting", Metadata.builder().branchName(forRef))) {
    2637          96 :       int i = 0;
    2638             :       for (Ref ref :
    2639          96 :           Iterables.concat(
    2640          96 :               receivePackRefCache.byPrefix(R_HEADS),
    2641          96 :               Collections.singletonList(receivePackRefCache.exactRef(forRef)))) {
    2642          96 :         if (ref != null && ref.getObjectId() != null) {
    2643             :           try {
    2644          96 :             rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
    2645          96 :             i++;
    2646           1 :           } catch (IOException e) {
    2647           1 :             logger.atWarning().withCause(e).log(
    2648           1 :                 "Invalid ref %s in %s", ref.getName(), project.getName());
    2649          96 :           }
    2650             :         }
    2651          96 :       }
    2652          96 :       logger.atFine().log("Marked %d heads as uninteresting", i);
    2653             :     }
    2654          96 :   }
    2655             : 
    2656             :   private static boolean isValidChangeId(String idStr) {
    2657          88 :     return idStr.matches("^I[0-9a-fA-F]{40}$") && !idStr.matches("^I00*$");
    2658             :   }
    2659             : 
    2660             :   private static class ChangeLookup {
    2661             :     final RevCommit commit;
    2662             : 
    2663             :     @Nullable final Change.Key changeKey;
    2664             :     final List<ChangeData> destChanges;
    2665             : 
    2666          89 :     ChangeLookup(RevCommit c, @Nullable Change.Key key, final List<ChangeData> destChanges) {
    2667          89 :       this.commit = c;
    2668          89 :       this.changeKey = key;
    2669          89 :       this.destChanges = destChanges;
    2670          89 :     }
    2671             :   }
    2672             : 
    2673             :   private ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) {
    2674          89 :     try (TraceTimer traceTimer = newTimer("lookupByChangeKey")) {
    2675          89 :       List<ChangeData> byBranchKeyExactMatch =
    2676          89 :           queryProvider.get().byBranchKey(magicBranch.dest, key).stream()
    2677          89 :               .filter(cd -> cd.change().getKey().equals(key))
    2678          89 :               .collect(toList());
    2679          89 :       return new ChangeLookup(c, key, byBranchKeyExactMatch);
    2680             :     }
    2681             :   }
    2682             : 
    2683             :   private ChangeLookup lookupByCommit(RevCommit c) {
    2684           3 :     try (TraceTimer traceTimer = newTimer("lookupByCommit")) {
    2685           3 :       return new ChangeLookup(
    2686           3 :           c, null, queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()));
    2687             :     }
    2688             :   }
    2689             : 
    2690             :   /** Represents a commit for which a Change should be created. */
    2691             :   private class CreateRequest {
    2692             :     final RevCommit commit;
    2693             :     final Task progress;
    2694             :     final String refName;
    2695             : 
    2696             :     Change.Id changeId;
    2697             :     ReceiveCommand cmd;
    2698             :     ChangeInserter ins;
    2699          88 :     List<String> groups = ImmutableList.of();
    2700             : 
    2701             :     Change change;
    2702             : 
    2703          88 :     CreateRequest(RevCommit commit, String refName, Task progress) {
    2704          88 :       this.commit = commit;
    2705          88 :       this.refName = refName;
    2706          88 :       this.progress = progress;
    2707          88 :     }
    2708             : 
    2709             :     private void setChangeId(int id) {
    2710          88 :       try (TraceTimer traceTimer = newTimer(CreateRequest.class, "setChangeId")) {
    2711          88 :         changeId = Change.id(id);
    2712          88 :         ins =
    2713             :             changeInserterFactory
    2714          88 :                 .create(changeId, commit, refName)
    2715          88 :                 .setTopic(magicBranch.topic)
    2716          88 :                 .setPrivate(setChangeAsPrivate)
    2717          88 :                 .setWorkInProgress(magicBranch.shouldSetWorkInProgressOnNewChanges())
    2718             :                 // Changes already validated in validateNewCommits.
    2719          88 :                 .setValidate(false);
    2720             : 
    2721          88 :         if (magicBranch.merged) {
    2722           3 :           ins.setStatus(Change.Status.MERGED);
    2723             :         }
    2724          88 :         cmd = new ReceiveCommand(ObjectId.zeroId(), commit, ins.getPatchSetId().toRefName());
    2725          88 :         if (receivePack.getPushCertificate() != null) {
    2726           0 :           ins.setPushCertificate(receivePack.getPushCertificate().toTextWithSignature());
    2727             :         }
    2728             :       }
    2729          88 :     }
    2730             : 
    2731             :     private void addOps(BatchUpdate bu) throws RestApiException {
    2732          88 :       try (TraceTimer traceTimer = newTimer(CreateRequest.class, "addOps")) {
    2733          88 :         checkState(changeId != null, "must call setChangeId before addOps");
    2734             :         try {
    2735          88 :           RevWalk rw = receivePack.getRevWalk();
    2736          88 :           rw.parseBody(commit);
    2737          88 :           final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
    2738          88 :           Account.Id me = user.getAccountId();
    2739          88 :           List<FooterLine> footerLines = commit.getFooterLines();
    2740          88 :           requireNonNull(magicBranch);
    2741             : 
    2742             :           // TODO(dborowitz): Support reviewers by email from footers? Maybe not: kernel developers
    2743             :           // with AOSP accounts already complain about these notifications, and that would make it
    2744             :           // worse. Might be better to get rid of the feature entirely:
    2745             :           // https://groups.google.com/d/topic/repo-discuss/tIFxY7L4DXk/discussion
    2746          88 :           MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, footerLines);
    2747          88 :           fromFooters.remove(me);
    2748             : 
    2749          88 :           Map<String, Short> approvals = magicBranch.labels;
    2750          88 :           StringBuilder msg =
    2751             :               new StringBuilder(
    2752          88 :                   ApprovalsUtil.renderMessageWithApprovals(
    2753          88 :                       psId.get(), approvals, Collections.emptyMap()));
    2754          88 :           msg.append('.');
    2755          88 :           if (!Strings.isNullOrEmpty(magicBranch.message)) {
    2756           3 :             msg.append("\n").append(magicBranch.message);
    2757             :           }
    2758             : 
    2759          88 :           bu.setNotify(magicBranch.getNotifyForNewChange());
    2760          88 :           bu.insertChange(
    2761          88 :               ins.setReviewersAndCcsAsStrings(
    2762          88 :                       magicBranch.getCombinedReviewers(fromFooters),
    2763          88 :                       magicBranch.getCombinedCcs(fromFooters))
    2764          88 :                   .setApprovals(approvals)
    2765          88 :                   .setMessage(msg.toString())
    2766          88 :                   .setRequestScopePropagator(requestScopePropagator)
    2767          88 :                   .setSendMail(true)
    2768          88 :                   .setPatchSetDescription(magicBranch.message));
    2769          88 :           if (!magicBranch.hashtags.isEmpty()) {
    2770             :             // Any change owner is allowed to add hashtags when creating a change.
    2771           3 :             bu.addOp(
    2772             :                 changeId,
    2773             :                 hashtagsFactory
    2774           3 :                     .create(new HashtagsInput(magicBranch.hashtags))
    2775           3 :                     .setFireEvent(false));
    2776             :           }
    2777          88 :           if (!Strings.isNullOrEmpty(magicBranch.topic)) {
    2778          22 :             bu.addOp(changeId, setTopicFactory.create(magicBranch.topic));
    2779             :           }
    2780          88 :           if (magicBranch.ignoreAttentionSet) {
    2781           3 :             bu.addOp(changeId, new AttentionSetUnchangedOp());
    2782             :           }
    2783          88 :           bu.addOp(
    2784             :               changeId,
    2785          88 :               new BatchUpdateOp() {
    2786             :                 @Override
    2787             :                 public boolean updateChange(ChangeContext ctx) {
    2788          88 :                   CreateRequest.this.change = ctx.getChange();
    2789          88 :                   return false;
    2790             :                 }
    2791             :               });
    2792          88 :           bu.addOp(changeId, new ChangeProgressOp(progress));
    2793           0 :         } catch (Exception e) {
    2794           0 :           throw asRestApiException(e);
    2795          88 :         }
    2796             :       }
    2797          88 :     }
    2798             :   }
    2799             : 
    2800             :   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
    2801             :       throws RestApiException, UpdateException, IOException, ConfigInvalidException,
    2802             :           PermissionBackendException {
    2803           8 :     try (TraceTimer traceTimer = newTimer("submit")) {
    2804           8 :       Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
    2805           8 :       for (CreateRequest r : create) {
    2806           7 :         requireNonNull(
    2807             :             r.change,
    2808           0 :             () -> String.format("cannot submit new change %s; op may not have run", r.changeId));
    2809           7 :         bySha.put(r.commit, r.change);
    2810           7 :       }
    2811           8 :       for (ReplaceRequest r : replace) {
    2812           3 :         bySha.put(r.newCommitId, r.notes.getChange());
    2813           3 :       }
    2814           8 :       Change tipChange = bySha.get(magicBranch.cmd.getNewId());
    2815           8 :       requireNonNull(
    2816             :           tipChange,
    2817             :           () ->
    2818           0 :               String.format(
    2819             :                   "tip of push does not correspond to a change; found these changes: %s", bySha));
    2820           8 :       logger.atFine().log(
    2821             :           "Processing submit with tip change %s (%s)",
    2822           8 :           tipChange.getId(), magicBranch.cmd.getNewId());
    2823           8 :       try (MergeOp op = mergeOpProvider.get()) {
    2824           8 :         SubmitInput submitInput = new SubmitInput();
    2825           8 :         submitInput.notify = magicBranch.notifyHandling;
    2826           8 :         submitInput.notifyDetails = new HashMap<>();
    2827           8 :         submitInput.notifyDetails.put(
    2828             :             RecipientType.TO,
    2829           8 :             new NotifyInfo(magicBranch.notifyTo.stream().map(Object::toString).collect(toList())));
    2830           8 :         submitInput.notifyDetails.put(
    2831             :             RecipientType.CC,
    2832           8 :             new NotifyInfo(magicBranch.notifyCc.stream().map(Object::toString).collect(toList())));
    2833           8 :         submitInput.notifyDetails.put(
    2834             :             RecipientType.BCC,
    2835           8 :             new NotifyInfo(magicBranch.notifyBcc.stream().map(Object::toString).collect(toList())));
    2836           8 :         op.merge(tipChange, user, false, submitInput, false);
    2837             :       }
    2838             :     }
    2839           8 :   }
    2840             : 
    2841             :   private void preparePatchSetsForReplace(ImmutableList<CreateRequest> newChanges) {
    2842          89 :     try (TraceTimer traceTimer =
    2843          89 :         newTimer(
    2844          89 :             "preparePatchSetsForReplace", Metadata.builder().resourceCount(newChanges.size()))) {
    2845             :       try {
    2846          89 :         readChangesForReplace();
    2847          89 :         for (ReplaceRequest req : replaceByChange.values()) {
    2848          38 :           if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
    2849          38 :             req.validateNewPatchSet();
    2850             :           }
    2851          38 :         }
    2852           0 :       } catch (IOException | PermissionBackendException e) {
    2853           0 :         throw new StorageException(
    2854           0 :             "Cannot read repository before replacement for project " + project.getName(), e);
    2855          89 :       }
    2856          89 :       logger.atFine().log("Read %d changes to replace", replaceByChange.size());
    2857             : 
    2858          89 :       if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
    2859             :         // Cancel creations tied to refs/for/ command.
    2860           9 :         for (ReplaceRequest req : replaceByChange.values()) {
    2861           5 :           if (req.inputCommand == magicBranch.cmd && req.cmd != null) {
    2862           0 :             req.cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, "aborted");
    2863             :           }
    2864           5 :         }
    2865           9 :         for (CreateRequest req : newChanges) {
    2866           1 :           req.cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, "aborted");
    2867           1 :         }
    2868             :       }
    2869             :     }
    2870          89 :   }
    2871             : 
    2872             :   private void readChangesForReplace() {
    2873          89 :     try (TraceTimer traceTimer = newTimer("readChangesForReplace")) {
    2874          89 :       replaceByChange.values().stream()
    2875          89 :           .map(r -> r.ontoChange)
    2876          89 :           .map(id -> notesFactory.create(repo, project.getNameKey(), id))
    2877          89 :           .forEach(notes -> replaceByChange.get(notes.getChangeId()).notes = notes);
    2878             :     }
    2879          89 :   }
    2880             : 
    2881             :   /** Represents a commit that should be stored in a new patchset of an existing change. */
    2882             :   private class ReplaceRequest {
    2883             :     final Change.Id ontoChange;
    2884             :     final ObjectId newCommitId;
    2885             :     final ReceiveCommand inputCommand;
    2886             :     final boolean checkMergedInto;
    2887             :     RevCommit revCommit;
    2888             :     ChangeNotes notes;
    2889             :     BiMap<RevCommit, PatchSet.Id> revisions;
    2890             :     PatchSet.Id psId;
    2891             :     ReceiveCommand prev;
    2892             :     ReceiveCommand cmd;
    2893             :     PatchSetInfo info;
    2894             :     PatchSet.Id priorPatchSet;
    2895          38 :     List<String> groups = ImmutableList.of();
    2896             :     ReplaceOp replaceOp;
    2897             : 
    2898             :     ReplaceRequest(
    2899             :         Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto)
    2900          38 :         throws IOException {
    2901          38 :       this.ontoChange = toChange;
    2902          38 :       this.newCommitId = newCommit.copy();
    2903          38 :       this.inputCommand = requireNonNull(cmd);
    2904          38 :       this.checkMergedInto = checkMergedInto;
    2905             : 
    2906             :       try {
    2907          38 :         revCommit = receivePack.getRevWalk().parseCommit(newCommitId);
    2908           0 :       } catch (IOException e) {
    2909           0 :         revCommit = null;
    2910          38 :       }
    2911          38 :       revisions = HashBiMap.create();
    2912          38 :       for (Ref ref : receivePackRefCache.byPrefix(RefNames.changeRefPrefix(toChange))) {
    2913             :         try {
    2914          38 :           PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
    2915          38 :           if (psId != null) {
    2916          38 :             revisions.forcePut(receivePack.getRevWalk().parseCommit(ref.getObjectId()), psId);
    2917             :           }
    2918           0 :         } catch (IOException err) {
    2919           0 :           logger.atWarning().withCause(err).log(
    2920           0 :               "Project %s contains invalid change ref %s", project.getName(), ref.getName());
    2921          38 :         }
    2922          38 :       }
    2923          38 :     }
    2924             : 
    2925             :     /**
    2926             :      * Validate the new patch set commit for this change.
    2927             :      *
    2928             :      * <p><strong>Side effects:</strong>
    2929             :      *
    2930             :      * <ul>
    2931             :      *   <li>May add error or warning messages to the progress monitor
    2932             :      *   <li>Will reject {@code cmd} prior to returning false
    2933             :      *   <li>May reset {@code receivePack.getRevWalk()}; do not call in the middle of a walk.
    2934             :      * </ul>
    2935             :      *
    2936             :      * @return whether the new commit is valid
    2937             :      */
    2938             :     boolean validateNewPatchSet() throws IOException, PermissionBackendException {
    2939          38 :       try (TraceTimer traceTimer = newTimer("validateNewPatchSet")) {
    2940          38 :         if (!validateNewPatchSetNoteDb()) {
    2941           5 :           return false;
    2942             :         }
    2943          37 :         sameTreeWarning();
    2944             : 
    2945          37 :         if (magicBranch != null) {
    2946          37 :           validateMagicBranchWipStatusChange();
    2947          37 :           if (inputCommand.getResult() != NOT_ATTEMPTED) {
    2948           3 :             return false;
    2949             :           }
    2950             : 
    2951          37 :           if (magicBranch.edit) {
    2952           3 :             return newEdit();
    2953             :           }
    2954             :         }
    2955             : 
    2956          37 :         newPatchSet();
    2957          37 :         return true;
    2958           5 :       }
    2959             :     }
    2960             : 
    2961             :     boolean validateNewPatchSetForAutoClose() throws IOException, PermissionBackendException {
    2962           6 :       if (!validateNewPatchSetNoteDb()) {
    2963           0 :         return false;
    2964             :       }
    2965             : 
    2966           6 :       newPatchSet();
    2967           6 :       return true;
    2968             :     }
    2969             : 
    2970             :     /** Validates the new PS against permissions and notedb status. */
    2971             :     private boolean validateNewPatchSetNoteDb() throws IOException, PermissionBackendException {
    2972          38 :       try (TraceTimer traceTimer = newTimer("validateNewPatchSetNoteDb")) {
    2973          38 :         if (notes == null) {
    2974           0 :           reject(inputCommand, "change " + ontoChange + " not found");
    2975           0 :           return false;
    2976             :         }
    2977             : 
    2978          38 :         Change change = notes.getChange();
    2979          38 :         priorPatchSet = change.currentPatchSetId();
    2980          38 :         if (!revisions.containsValue(priorPatchSet)) {
    2981           0 :           metrics.psRevisionMissing.increment();
    2982           0 :           logger.atWarning().log(
    2983             :               "Change %d is missing revision for patch set %s"
    2984             :                   + " (it has revisions for these patch sets: %s)",
    2985           0 :               change.getChangeId(),
    2986           0 :               priorPatchSet.getId(),
    2987           0 :               Iterables.toString(
    2988           0 :                   revisions.values().stream()
    2989           0 :                       .limit(100) // Enough for "normal" changes.
    2990           0 :                       .map(PatchSet.Id::getId)
    2991           0 :                       .collect(Collectors.toList())));
    2992           0 :           reject(inputCommand, "change " + ontoChange + " missing revisions");
    2993           0 :           return false;
    2994             :         }
    2995             : 
    2996          38 :         RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
    2997             : 
    2998             :         // Not allowed to create a new patch set if the current patch set is locked.
    2999          38 :         if (psUtil.isPatchSetLocked(notes)) {
    3000           3 :           reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
    3001           3 :           return false;
    3002             :         }
    3003             : 
    3004          38 :         if (!permissions.change(notes).test(ChangePermission.ADD_PATCH_SET)) {
    3005           2 :           reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
    3006           2 :           return false;
    3007             :         }
    3008             : 
    3009          37 :         if (change.isClosed()) {
    3010           0 :           reject(inputCommand, "change " + ontoChange + " closed");
    3011           0 :           return false;
    3012          37 :         } else if (revisions.containsKey(newCommit)) {
    3013           0 :           reject(inputCommand, "commit already exists (in the change)");
    3014           0 :           return false;
    3015             :         }
    3016             : 
    3017          37 :         List<PatchSet.Id> existingPatchSetsWithSameCommit =
    3018          37 :             receivePackRefCache.patchSetIdsFromObjectId(newCommit);
    3019          37 :         if (!existingPatchSetsWithSameCommit.isEmpty()) {
    3020             :           // TODO(hiesel, hanwen): Remove this check entirely when Gerrit requires change IDs
    3021             :           //  without the option to turn that off.
    3022           0 :           reject(
    3023             :               inputCommand,
    3024             :               "commit already exists (in the project): "
    3025           0 :                   + existingPatchSetsWithSameCommit.get(0).toRefName());
    3026           0 :           return false;
    3027             :         }
    3028             : 
    3029          37 :         try (TraceTimer traceTimer2 = newTimer("validateNewPatchSetNoteDb#isMergedInto")) {
    3030          37 :           for (RevCommit prior : revisions.keySet()) {
    3031             :             // Don't allow a change to directly depend upon itself. This is a
    3032             :             // very common error due to users making a new commit rather than
    3033             :             // amending when trying to address review comments.
    3034          37 :             if (receivePack.getRevWalk().isMergedInto(prior, newCommit)) {
    3035           3 :               reject(inputCommand, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
    3036           3 :               return false;
    3037             :             }
    3038          37 :           }
    3039           3 :         }
    3040             : 
    3041          37 :         return true;
    3042           5 :       }
    3043             :     }
    3044             : 
    3045             :     /** Validates whether the WIP change is allowed. Rejects inputCommand if not. */
    3046             :     private void validateMagicBranchWipStatusChange() throws PermissionBackendException {
    3047          37 :       Change change = notes.getChange();
    3048          37 :       if ((magicBranch.workInProgress || magicBranch.ready)
    3049           5 :           && magicBranch.workInProgress != change.isWorkInProgress()
    3050           5 :           && !user.getAccountId().equals(change.getOwner())) {
    3051           3 :         if (!permissions.test(ProjectPermission.WRITE_CONFIG)) {
    3052           3 :           if (!permissions.change(notes).test(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE)) {
    3053           3 :             reject(inputCommand, ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP);
    3054             :           }
    3055             :         }
    3056             :       }
    3057          37 :     }
    3058             : 
    3059             :     /** prints a warning if the new PS has the same tree as the previous commit. */
    3060             :     private void sameTreeWarning() throws IOException {
    3061          37 :       try (TraceTimer traceTimer = newTimer("sameTreeWarning")) {
    3062          37 :         RevWalk rw = receivePack.getRevWalk();
    3063          37 :         RevCommit newCommit = rw.parseCommit(newCommitId);
    3064          37 :         RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
    3065             : 
    3066          37 :         if (newCommit.getTree().equals(priorCommit.getTree())) {
    3067          11 :           rw.parseBody(newCommit);
    3068          11 :           rw.parseBody(priorCommit);
    3069          11 :           boolean messageEq =
    3070          11 :               Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
    3071          11 :           boolean parentsEq = parentsEqual(newCommit, priorCommit);
    3072          11 :           boolean authorEq = authorEqual(newCommit, priorCommit);
    3073          11 :           ObjectReader reader = receivePack.getRevWalk().getObjectReader();
    3074             : 
    3075          11 :           if (messageEq && parentsEq && authorEq) {
    3076           8 :             addMessage(
    3077           8 :                 String.format(
    3078             :                     "warning: no changes between prior commit %s and new commit %s",
    3079           8 :                     abbreviateName(priorCommit, reader), abbreviateName(newCommit, reader)));
    3080             :           } else {
    3081           7 :             StringBuilder msg = new StringBuilder();
    3082           7 :             msg.append("warning: ").append(abbreviateName(newCommit, reader));
    3083           7 :             msg.append(":");
    3084           7 :             msg.append(" no files changed");
    3085           7 :             if (!authorEq) {
    3086           3 :               msg.append(", author changed");
    3087             :             }
    3088           7 :             if (!messageEq) {
    3089           6 :               msg.append(", message updated");
    3090             :             }
    3091           7 :             if (!parentsEq) {
    3092           5 :               msg.append(", was rebased");
    3093             :             }
    3094           7 :             addMessage(msg.toString());
    3095             :           }
    3096             :         }
    3097             :       }
    3098          37 :     }
    3099             : 
    3100             :     /**
    3101             :      * Sets cmd and prev to the ReceiveCommands for change edits. Returns false if there was a
    3102             :      * failure.
    3103             :      */
    3104             :     private boolean newEdit() {
    3105           3 :       try (TraceTimer traceTimer = newTimer("newEdit")) {
    3106           3 :         psId = notes.getChange().currentPatchSetId();
    3107             :         Optional<ChangeEdit> edit;
    3108             : 
    3109             :         try {
    3110           3 :           edit = editUtil.byChange(notes, user);
    3111           0 :         } catch (AuthException | IOException e) {
    3112           0 :           logger.atSevere().withCause(e).log("Cannot retrieve edit");
    3113           0 :           return false;
    3114           3 :         }
    3115             : 
    3116           3 :         if (edit.isPresent()) {
    3117           0 :           if (edit.get().getBasePatchSet().id().equals(psId)) {
    3118             :             // replace edit
    3119           0 :             cmd =
    3120             :                 new ReceiveCommand(
    3121           0 :                     edit.get().getEditCommit(), newCommitId, edit.get().getRefName());
    3122             :           } else {
    3123             :             // delete old edit ref on rebase
    3124           0 :             prev =
    3125             :                 new ReceiveCommand(
    3126           0 :                     edit.get().getEditCommit(), ObjectId.zeroId(), edit.get().getRefName());
    3127           0 :             createEditCommand();
    3128             :           }
    3129             :         } else {
    3130           3 :           createEditCommand();
    3131             :         }
    3132             : 
    3133           3 :         return true;
    3134           0 :       }
    3135             :     }
    3136             : 
    3137             :     /** Creates a ReceiveCommand for a new edit. */
    3138             :     private void createEditCommand() {
    3139           3 :       cmd =
    3140             :           new ReceiveCommand(
    3141           3 :               ObjectId.zeroId(),
    3142             :               newCommitId,
    3143           3 :               RefNames.refsEdit(user.getAccountId(), notes.getChangeId(), psId));
    3144           3 :     }
    3145             : 
    3146             :     /** Updates 'this' to add a new patchset. */
    3147             :     private void newPatchSet() throws IOException {
    3148          37 :       try (TraceTimer traceTimer = newTimer("newPatchSet")) {
    3149          37 :         RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
    3150          37 :         psId = nextPatchSetId(notes.getChange().currentPatchSetId());
    3151          37 :         info = patchSetInfoFactory.get(receivePack.getRevWalk(), newCommit, psId);
    3152          37 :         cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName());
    3153             :       }
    3154          37 :     }
    3155             : 
    3156             :     private PatchSet.Id nextPatchSetId(PatchSet.Id psId) throws IOException {
    3157          37 :       PatchSet.Id next = ChangeUtil.nextPatchSetId(psId);
    3158          37 :       while (receivePackRefCache.exactRef(next.toRefName()) != null) {
    3159           0 :         next = ChangeUtil.nextPatchSetId(next);
    3160             :       }
    3161          37 :       return next;
    3162             :     }
    3163             : 
    3164             :     void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException {
    3165          37 :       try (TraceTimer traceTimer = newTimer("addOps")) {
    3166          37 :         if (magicBranch != null && magicBranch.edit) {
    3167           3 :           bu.addOp(notes.getChangeId(), new ReindexOnlyOp());
    3168           3 :           if (prev != null) {
    3169           0 :             bu.addRepoOnlyOp(new UpdateOneRefOp(prev));
    3170             :           }
    3171           3 :           bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
    3172           3 :           return;
    3173             :         }
    3174          37 :         RevWalk rw = receivePack.getRevWalk();
    3175             :         // TODO(dborowitz): Move to ReplaceOp#updateRepo.
    3176          37 :         RevCommit newCommit = rw.parseCommit(newCommitId);
    3177          37 :         rw.parseBody(newCommit);
    3178             : 
    3179          37 :         RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
    3180          37 :         replaceOp =
    3181          37 :             replaceOpFactory.create(
    3182             :                 projectState,
    3183          37 :                 notes.getChange(),
    3184             :                 checkMergedInto,
    3185          37 :                 checkMergedInto ? inputCommand.getNewId().name() : null,
    3186             :                 priorPatchSet,
    3187             :                 priorCommit,
    3188             :                 psId,
    3189             :                 newCommit,
    3190             :                 info,
    3191             :                 groups,
    3192             :                 magicBranch,
    3193          37 :                 receivePack.getPushCertificate(),
    3194             :                 requestScopePropagator);
    3195          37 :         bu.addOp(notes.getChangeId(), replaceOp);
    3196          37 :         if (progress != null) {
    3197          37 :           bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
    3198             :         }
    3199          37 :         bu.addRepoOnlyOp(
    3200          37 :             new RepoOnlyOp() {
    3201             :               @Override
    3202             :               public void updateRepo(RepoContext ctx) throws Exception {
    3203             :                 // Create auto merge ref if the new patch set is a merge commit. This is only
    3204             :                 // required for new patch sets on existing changes as these do not go through
    3205             :                 // PatchSetInserter. New changes pushed via git go through ChangeInserter and have
    3206             :                 // their auto merge commits created there.
    3207          37 :                 Optional<ReceiveCommand> autoMerge =
    3208          37 :                     autoMerger.createAutoMergeCommitIfNecessary(
    3209          37 :                         ctx.getRepoView(),
    3210          37 :                         ctx.getRevWalk(),
    3211          37 :                         ctx.getInserter(),
    3212          37 :                         ctx.getRevWalk().parseCommit(newCommitId));
    3213          37 :                 if (autoMerge.isPresent()) {
    3214           5 :                   ctx.addRefUpdate(autoMerge.get());
    3215             :                 }
    3216          37 :               }
    3217             :             });
    3218           3 :       }
    3219          37 :     }
    3220             : 
    3221             :     @Nullable
    3222             :     String getRejectMessage() {
    3223          37 :       return replaceOp != null ? replaceOp.getRejectMessage() : null;
    3224             :     }
    3225             : 
    3226             :     Optional<String> getOutdatedApprovalsMessage() {
    3227          37 :       return replaceOp != null ? replaceOp.getOutdatedApprovalsMessage() : Optional.empty();
    3228             :     }
    3229             :   }
    3230             : 
    3231             :   private class UpdateGroupsRequest {
    3232             :     final PatchSet.Id psId;
    3233             :     final RevCommit commit;
    3234          46 :     List<String> groups = ImmutableList.of();
    3235             : 
    3236          46 :     UpdateGroupsRequest(PatchSet.Id psId, RevCommit commit) {
    3237          46 :       this.psId = psId;
    3238          46 :       this.commit = commit;
    3239          46 :     }
    3240             : 
    3241             :     private void addOps(BatchUpdate bu) {
    3242          45 :       bu.addOp(
    3243          45 :           psId.changeId(),
    3244          45 :           new BatchUpdateOp() {
    3245             :             @Override
    3246             :             public boolean updateChange(ChangeContext ctx) {
    3247          45 :               PatchSet ps = psUtil.get(ctx.getNotes(), psId);
    3248          45 :               List<String> oldGroups = ps.groups();
    3249          45 :               if (oldGroups == null) {
    3250           0 :                 if (groups == null) {
    3251           0 :                   return false;
    3252             :                 }
    3253          45 :               } else if (sameGroups(oldGroups, groups)) {
    3254          45 :                 return false;
    3255             :               }
    3256           0 :               ctx.getUpdate(psId).setGroups(groups);
    3257           0 :               return true;
    3258             :             }
    3259             :           });
    3260          45 :     }
    3261             : 
    3262             :     private boolean sameGroups(List<String> a, List<String> b) {
    3263          45 :       return Sets.newHashSet(a).equals(Sets.newHashSet(b));
    3264             :     }
    3265             :   }
    3266             : 
    3267             :   private class UpdateOneRefOp implements RepoOnlyOp {
    3268             :     final ReceiveCommand cmd;
    3269             : 
    3270          47 :     private UpdateOneRefOp(ReceiveCommand cmd) {
    3271          47 :       this.cmd = requireNonNull(cmd);
    3272          47 :     }
    3273             : 
    3274             :     @Override
    3275             :     public void updateRepo(RepoContext ctx) throws IOException {
    3276          47 :       ctx.addRefUpdate(cmd);
    3277          47 :     }
    3278             : 
    3279             :     @Override
    3280             :     public void postUpdate(PostUpdateContext ctx) {
    3281          47 :       String refName = cmd.getRefName();
    3282          47 :       if (cmd.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
    3283          42 :         logger.atFine().log("Updating tag cache on fast-forward of %s", cmd.getRefName());
    3284          42 :         tagCache.updateFastForward(project.getNameKey(), refName, cmd.getOldId(), cmd.getNewId());
    3285             :       }
    3286          47 :       if (isConfig(cmd)) {
    3287           5 :         logger.atFine().log("Reloading project in cache");
    3288           5 :         projectCache.evictAndReindex(project);
    3289           5 :         ProjectState ps =
    3290           5 :             projectCache.get(project.getNameKey()).orElseThrow(illegalState(project.getNameKey()));
    3291             :         try {
    3292           5 :           logger.atFine().log("Updating project description");
    3293           5 :           repo.setGitwebDescription(ps.getProject().getDescription());
    3294           0 :         } catch (IOException e) {
    3295           0 :           throw new StorageException("cannot update description of " + project.getName(), e);
    3296           5 :         }
    3297           5 :         if (allProjectsName.equals(project.getNameKey())) {
    3298             :           try {
    3299           1 :             createGroupPermissionSyncer.syncIfNeeded();
    3300           0 :           } catch (IOException | ConfigInvalidException e) {
    3301           0 :             throw new StorageException("cannot update description of " + project.getName(), e);
    3302           1 :           }
    3303             :         }
    3304             :       }
    3305          47 :     }
    3306             :   }
    3307             : 
    3308             :   private static class ReindexOnlyOp implements BatchUpdateOp {
    3309             :     @Override
    3310             :     public boolean updateChange(ChangeContext ctx) {
    3311             :       // Trigger reindexing even though change isn't actually updated.
    3312           3 :       return true;
    3313             :     }
    3314             :   }
    3315             : 
    3316             :   private static boolean parentsEqual(RevCommit a, RevCommit b) {
    3317          11 :     if (a.getParentCount() != b.getParentCount()) {
    3318           4 :       return false;
    3319             :     }
    3320          10 :     for (int i = 0; i < a.getParentCount(); i++) {
    3321          10 :       if (!a.getParent(i).equals(b.getParent(i))) {
    3322           4 :         return false;
    3323             :       }
    3324             :     }
    3325          10 :     return true;
    3326             :   }
    3327             : 
    3328             :   private static boolean authorEqual(RevCommit a, RevCommit b) {
    3329          11 :     PersonIdent aAuthor = a.getAuthorIdent();
    3330          11 :     PersonIdent bAuthor = b.getAuthorIdent();
    3331             : 
    3332          11 :     if (aAuthor == null && bAuthor == null) {
    3333           0 :       return true;
    3334          11 :     } else if (aAuthor == null || bAuthor == null) {
    3335           0 :       return false;
    3336             :     }
    3337             : 
    3338          11 :     return Objects.equals(aAuthor.getName(), bAuthor.getName())
    3339          11 :         && Objects.equals(aAuthor.getEmailAddress(), bAuthor.getEmailAddress());
    3340             :   }
    3341             : 
    3342             :   // Run RefValidators on the command. If any validator fails, the command status is set to
    3343             :   // REJECTED, and the return value is 'false'
    3344             :   private boolean validRefOperation(ReceiveCommand cmd) {
    3345          49 :     try (TraceTimer traceTimer = newTimer("validRefOperation")) {
    3346          49 :       RefOperationValidators refValidators =
    3347          49 :           refValidatorsFactory.create(
    3348          49 :               getProject(), user, cmd, ImmutableListMultimap.copyOf(pushOptions));
    3349             : 
    3350             :       try {
    3351          49 :         messages.addAll(refValidators.validateForRefOperation());
    3352           5 :       } catch (RefOperationValidationException e) {
    3353           5 :         messages.addAll(e.getMessages());
    3354           5 :         reject(cmd, e.getMessage());
    3355           5 :         return false;
    3356          49 :       }
    3357             : 
    3358          49 :       return true;
    3359           5 :     }
    3360             :   }
    3361             : 
    3362             :   /**
    3363             :    * Validates the commits that a regular push brings in.
    3364             :    *
    3365             :    * <p>On validation failure, the command is rejected.
    3366             :    */
    3367             :   private void validateRegularPushCommits(BranchNameKey branch, ReceiveCommand cmd)
    3368             :       throws PermissionBackendException {
    3369          49 :     try (TraceTimer traceTimer =
    3370          49 :         newTimer("validateRegularPushCommits", Metadata.builder().branchName(branch.branch()))) {
    3371          49 :       boolean skipValidation =
    3372          49 :           !RefNames.REFS_CONFIG.equals(cmd.getRefName())
    3373          47 :               && !(MagicBranch.isMagicBranch(cmd.getRefName())
    3374          47 :                   || NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
    3375          49 :               && pushOptions.containsKey(PUSH_OPTION_SKIP_VALIDATION);
    3376          49 :       if (skipValidation) {
    3377           4 :         if (projectState.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
    3378           0 :           reject(cmd, "requireSignedOffBy prevents option " + PUSH_OPTION_SKIP_VALIDATION);
    3379           0 :           return;
    3380             :         }
    3381             : 
    3382           4 :         Optional<AuthException> err =
    3383           4 :             checkRefPermission(permissions.ref(branch.branch()), RefPermission.SKIP_VALIDATION);
    3384           4 :         if (err.isPresent()) {
    3385           4 :           rejectProhibited(cmd, err.get());
    3386           4 :           return;
    3387             :         }
    3388           3 :         if (!Iterables.isEmpty(rejectCommits)) {
    3389           0 :           reject(cmd, "reject-commits prevents " + PUSH_OPTION_SKIP_VALIDATION);
    3390             :         }
    3391             :       }
    3392             : 
    3393          49 :       BranchCommitValidator validator = commitValidatorFactory.create(projectState, branch, user);
    3394          49 :       RevWalk walk = receivePack.getRevWalk();
    3395          49 :       walk.reset();
    3396          49 :       walk.sort(RevSort.NONE);
    3397             :       try {
    3398          49 :         RevObject parsedObject = walk.parseAny(cmd.getNewId());
    3399          49 :         if (!(parsedObject instanceof RevCommit)) {
    3400           2 :           return;
    3401             :         }
    3402          47 :         walk.markStart((RevCommit) parsedObject);
    3403          47 :         markHeadsAsUninteresting(walk, cmd.getRefName());
    3404          47 :         int limit = receiveConfig.maxBatchCommits;
    3405          47 :         int n = 0;
    3406          47 :         for (RevCommit c; (c = walk.next()) != null; ) {
    3407             :           // Even if skipValidation is set, we still get here when at least one plugin
    3408             :           // commit validator requires to validate all commits. In this case, however,
    3409             :           // we don't need to check the commit limit.
    3410          47 :           if (++n > limit && !skipValidation) {
    3411           3 :             logger.atFine().log("Number of new commits exceeds limit of %d", limit);
    3412           3 :             reject(
    3413             :                 cmd,
    3414           3 :                 String.format(
    3415           3 :                     "more than %d commits, and %s not set", limit, PUSH_OPTION_SKIP_VALIDATION));
    3416           3 :             return;
    3417             :           }
    3418          47 :           if (!receivePackRefCache.patchSetIdsFromObjectId(c).isEmpty()) {
    3419          11 :             continue;
    3420             :           }
    3421             : 
    3422          47 :           BranchCommitValidator.Result validationResult =
    3423          47 :               validator.validateCommit(
    3424             :                   repo,
    3425          47 :                   walk.getObjectReader(),
    3426             :                   cmd,
    3427             :                   c,
    3428          47 :                   ImmutableListMultimap.copyOf(pushOptions),
    3429             :                   false,
    3430             :                   rejectCommits,
    3431             :                   null,
    3432             :                   skipValidation);
    3433          47 :           messages.addAll(validationResult.messages());
    3434          47 :           if (!validationResult.isValid()) {
    3435           8 :             break;
    3436             :           }
    3437          45 :         }
    3438          47 :         logger.atFine().log("Validated %d new commits", n);
    3439           0 :       } catch (IOException err) {
    3440           0 :         cmd.setResult(REJECTED_MISSING_OBJECT);
    3441           0 :         logger.atSevere().withCause(err).log(
    3442             :             "Invalid pack upload; one or more objects weren't sent");
    3443          47 :       }
    3444           6 :     }
    3445          47 :   }
    3446             : 
    3447             :   private void autoCloseChanges(ReceiveCommand cmd, Task progress) {
    3448          38 :     try (TraceTimer traceTimer = newTimer("autoCloseChanges")) {
    3449          38 :       logger.atFine().log("Starting auto-closing of changes");
    3450          38 :       String refName = cmd.getRefName();
    3451          38 :       Set<Change.Id> ids = new HashSet<>();
    3452             : 
    3453             :       // TODO(dborowitz): Combine this BatchUpdate with the main one in
    3454             :       // handleRegularCommands
    3455             :       try {
    3456          38 :         retryHelper
    3457          38 :             .changeUpdate(
    3458             :                 "autoCloseChanges",
    3459             :                 updateFactory -> {
    3460          38 :                   try (BatchUpdate bu =
    3461          38 :                           updateFactory.create(projectState.getNameKey(), user, TimeUtil.now());
    3462          38 :                       ObjectInserter ins = repo.newObjectInserter();
    3463          38 :                       ObjectReader reader = ins.newReader();
    3464          38 :                       RevWalk rw = new RevWalk(reader)) {
    3465          38 :                     if (ObjectId.zeroId().equals(cmd.getOldId())) {
    3466             :                       // The user is creating a new branch. The branch can't contain any changes, so
    3467             :                       // auto-closing doesn't apply. Exiting here early to spare any further,
    3468             :                       // potentially expensive computation that loop over all commits.
    3469          22 :                       return null;
    3470             :                     }
    3471             : 
    3472          38 :                     bu.setRepository(repo, rw, ins);
    3473             :                     // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
    3474             : 
    3475          38 :                     RevCommit newTip = rw.parseCommit(cmd.getNewId());
    3476          38 :                     BranchNameKey branch = BranchNameKey.create(project.getNameKey(), refName);
    3477             : 
    3478          38 :                     rw.reset();
    3479          38 :                     rw.sort(RevSort.REVERSE);
    3480          38 :                     rw.markStart(newTip);
    3481          38 :                     rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
    3482             : 
    3483          38 :                     Map<Change.Key, ChangeData> changeDataByKey = null;
    3484          38 :                     List<ReplaceRequest> replaceAndClose = new ArrayList<>();
    3485             : 
    3486          38 :                     int existingPatchSets = 0;
    3487          38 :                     int newPatchSets = 0;
    3488          38 :                     SubmissionId submissionId = null;
    3489             :                     COMMIT:
    3490          38 :                     for (RevCommit c; (c = rw.next()) != null; ) {
    3491          38 :                       rw.parseBody(c);
    3492             : 
    3493             :                       // Check if change refs point to this commit. Usually there are 0-1 change
    3494             :                       // refs pointing to this commit.
    3495             :                       for (PatchSet.Id psId :
    3496          38 :                           receivePackRefCache.patchSetIdsFromObjectId(c.copy())) {
    3497          10 :                         Optional<ChangeNotes> notes = getChangeNotes(psId.changeId());
    3498          10 :                         if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
    3499          10 :                           if (submissionId == null) {
    3500          10 :                             submissionId = new SubmissionId(notes.get().getChange());
    3501             :                           }
    3502          10 :                           existingPatchSets++;
    3503          10 :                           bu.addOp(
    3504          10 :                               notes.get().getChangeId(), setPrivateOpFactory.create(false, null));
    3505          10 :                           bu.addOp(
    3506          10 :                               psId.changeId(),
    3507          10 :                               mergedByPushOpFactory.create(
    3508             :                                   requestScopePropagator,
    3509             :                                   psId,
    3510             :                                   submissionId,
    3511             :                                   refName,
    3512          10 :                                   newTip.getId().getName()));
    3513          10 :                           continue COMMIT;
    3514             :                         }
    3515           1 :                       }
    3516             : 
    3517             :                       for (String changeId :
    3518          38 :                           ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get())) {
    3519          36 :                         if (changeDataByKey == null) {
    3520          36 :                           changeDataByKey =
    3521             :                               retryHelper
    3522          36 :                                   .changeIndexQuery(
    3523             :                                       "queryOpenChangesByKeyByBranch",
    3524          36 :                                       q -> openChangesByKeyByBranch(q, branch))
    3525          36 :                                   .call();
    3526             :                         }
    3527             : 
    3528          36 :                         ChangeData onto = changeDataByKey.get(Change.key(changeId.trim()));
    3529          36 :                         if (onto != null) {
    3530           6 :                           newPatchSets++;
    3531             :                           // Hold onto this until we're done with the walk, as the call to
    3532             :                           // req.validate below calls isMergedInto which resets the walk.
    3533           6 :                           ChangeNotes ontoNotes = onto.notes();
    3534           6 :                           ReplaceRequest req =
    3535           6 :                               new ReplaceRequest(ontoNotes.getChangeId(), c, cmd, false);
    3536           6 :                           req.notes = ontoNotes;
    3537           6 :                           replaceAndClose.add(req);
    3538           6 :                           continue COMMIT;
    3539             :                         }
    3540          38 :                       }
    3541             :                     }
    3542             : 
    3543          38 :                     for (ReplaceRequest req : replaceAndClose) {
    3544           6 :                       Change.Id id = req.notes.getChangeId();
    3545           6 :                       if (!req.validateNewPatchSetForAutoClose()) {
    3546           0 :                         logger.atFine().log("Not closing %s because validation failed", id);
    3547           0 :                         continue;
    3548             :                       }
    3549           6 :                       if (submissionId == null) {
    3550           6 :                         submissionId = new SubmissionId(req.notes.getChange());
    3551             :                       }
    3552           6 :                       req.addOps(bu, null);
    3553           6 :                       bu.addOp(id, setPrivateOpFactory.create(false, null));
    3554           6 :                       bu.addOp(
    3555             :                           id,
    3556             :                           mergedByPushOpFactory
    3557           6 :                               .create(
    3558             :                                   requestScopePropagator,
    3559             :                                   req.psId,
    3560             :                                   submissionId,
    3561             :                                   refName,
    3562           6 :                                   newTip.getId().getName())
    3563           6 :                               .setPatchSetProvider(req.replaceOp::getPatchSet));
    3564           6 :                       bu.addOp(id, new ChangeProgressOp(progress));
    3565           6 :                       ids.add(id);
    3566           6 :                     }
    3567             : 
    3568          38 :                     logger.atFine().log(
    3569             :                         "Auto-closing %d changes with existing patch sets and %d with new patch"
    3570             :                             + " sets",
    3571             :                         existingPatchSets, newPatchSets);
    3572          38 :                     bu.execute();
    3573          22 :                   } catch (IOException | StorageException | PermissionBackendException e) {
    3574           0 :                     throw new StorageException("Failed to auto-close changes", e);
    3575          38 :                   }
    3576             : 
    3577             :                   // If we are here, we didn't throw UpdateException. Record the result.
    3578             :                   // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id
    3579             :                   // doesn't
    3580             :                   // fit into TreeSet.
    3581          38 :                   ids.stream()
    3582          38 :                       .forEach(
    3583           6 :                           id -> result.addChange(ReceiveCommitsResult.ChangeStatus.AUTOCLOSED, id));
    3584             : 
    3585          38 :                   return null;
    3586             :                 })
    3587             :             // Use a multiple of the default timeout to account for inner retries that may otherwise
    3588             :             // eat up the whole timeout so that no time is left to retry this outer action.
    3589          38 :             .defaultTimeoutMultiplier(5)
    3590          38 :             .call();
    3591           0 :       } catch (RestApiException e) {
    3592           0 :         logger.atSevere().withCause(e).log("Can't insert patchset");
    3593           0 :       } catch (UpdateException e) {
    3594           0 :         logger.atSevere().withCause(e).log("Failed to auto-close changes");
    3595             :       } finally {
    3596          38 :         logger.atFine().log("Done auto-closing changes");
    3597             :       }
    3598             :     }
    3599          38 :   }
    3600             : 
    3601             :   private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) {
    3602             :     try {
    3603          11 :       return Optional.of(notesFactory.createChecked(project.getNameKey(), changeId));
    3604           0 :     } catch (NoSuchChangeException e) {
    3605           0 :       return Optional.empty();
    3606             :     }
    3607             :   }
    3608             : 
    3609             :   private Map<Change.Key, ChangeData> openChangesByKeyByBranch(
    3610             :       InternalChangeQuery internalChangeQuery, BranchNameKey branch) {
    3611          36 :     try (TraceTimer traceTimer =
    3612          36 :         newTimer("openChangesByKeyByBranch", Metadata.builder().branchName(branch.branch()))) {
    3613          36 :       Map<Change.Key, ChangeData> r = new HashMap<>();
    3614          36 :       for (ChangeData cd : internalChangeQuery.byBranchOpen(branch)) {
    3615             :         try {
    3616             :           // ChangeData is not materialised into a ChangeNotes for avoiding
    3617             :           // to load a potentially large number of changes meta-data into memory
    3618             :           // which would cause unnecessary disk I/O, CPU and heap utilisation.
    3619          12 :           r.put(cd.change().getKey(), cd);
    3620           0 :         } catch (NoSuchChangeException e) {
    3621             :           // Ignore deleted change
    3622          12 :         }
    3623          12 :       }
    3624          36 :       return r;
    3625             :     }
    3626             :   }
    3627             : 
    3628             :   private TraceTimer newTimer(String name) {
    3629          96 :     return newTimer(getClass(), name);
    3630             :   }
    3631             : 
    3632             :   private TraceTimer newTimer(Class<?> clazz, String name) {
    3633          96 :     return newTimer(clazz, name, Metadata.builder());
    3634             :   }
    3635             : 
    3636             :   private TraceTimer newTimer(String name, Metadata.Builder metadataBuilder) {
    3637          96 :     return newTimer(getClass(), name, metadataBuilder);
    3638             :   }
    3639             : 
    3640             :   private TraceTimer newTimer(Class<?> clazz, String name, Metadata.Builder metadataBuilder) {
    3641          96 :     metadataBuilder.projectName(project.getName());
    3642          96 :     return TraceContext.newTimer(clazz.getSimpleName() + "#" + name, metadataBuilder.build());
    3643             :   }
    3644             : 
    3645             :   private static void reject(ReceiveCommand cmd, String why) {
    3646          25 :     logger.atFine().log("Rejecting command '%s': %s", cmd, why);
    3647          25 :     cmd.setResult(REJECTED_OTHER_REASON, why);
    3648          25 :   }
    3649             : 
    3650             :   private static void rejectRemaining(Collection<ReceiveCommand> commands, String why) {
    3651          96 :     rejectRemaining(commands.stream(), why);
    3652          96 :   }
    3653             : 
    3654             :   private static void rejectRemaining(Stream<ReceiveCommand> commands, String why) {
    3655          96 :     commands.filter(cmd -> cmd.getResult() == NOT_ATTEMPTED).forEach(cmd -> reject(cmd, why));
    3656          96 :   }
    3657             : 
    3658             :   private static boolean isHead(ReceiveCommand cmd) {
    3659          49 :     return cmd.getRefName().startsWith(Constants.R_HEADS);
    3660             :   }
    3661             : 
    3662             :   private static boolean isConfig(ReceiveCommand cmd) {
    3663          47 :     return cmd.getRefName().equals(RefNames.REFS_CONFIG);
    3664             :   }
    3665             : 
    3666             :   private static String commandToString(ReceiveCommand cmd) {
    3667           5 :     StringBuilder b = new StringBuilder();
    3668           5 :     b.append(cmd);
    3669           5 :     b.append("  (").append(cmd.getResult());
    3670           5 :     if (cmd.getMessage() != null) {
    3671           0 :       b.append(": ").append(cmd.getMessage());
    3672             :     }
    3673           5 :     b.append(")\n");
    3674           5 :     return b.toString();
    3675             :   }
    3676             : }

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