LCOV - code coverage report
Current view: top level - server/update - BatchUpdate.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 316 323 97.8 %
Date: 2022-11-19 15:00:39 Functions: 73 75 97.3 %

          Line data    Source code
       1             : // Copyright (C) 2017 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.update;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : import static com.google.common.base.Preconditions.checkState;
      19             : import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
      20             : import static com.google.common.flogger.LazyArgs.lazy;
      21             : import static java.util.Comparator.comparing;
      22             : import static java.util.Objects.requireNonNull;
      23             : import static java.util.stream.Collectors.toMap;
      24             : import static java.util.stream.Collectors.toSet;
      25             : 
      26             : import com.google.common.base.Throwables;
      27             : import com.google.common.collect.ArrayListMultimap;
      28             : import com.google.common.collect.ImmutableList;
      29             : import com.google.common.collect.ImmutableListMultimap;
      30             : import com.google.common.collect.ImmutableMap;
      31             : import com.google.common.collect.ListMultimap;
      32             : import com.google.common.collect.MultimapBuilder;
      33             : import com.google.common.collect.Multiset;
      34             : import com.google.common.flogger.FluentLogger;
      35             : import com.google.common.util.concurrent.Futures;
      36             : import com.google.common.util.concurrent.ListenableFuture;
      37             : import com.google.gerrit.common.Nullable;
      38             : import com.google.gerrit.entities.AttentionSetUpdate;
      39             : import com.google.gerrit.entities.BranchNameKey;
      40             : import com.google.gerrit.entities.Change;
      41             : import com.google.gerrit.entities.PatchSet;
      42             : import com.google.gerrit.entities.Project;
      43             : import com.google.gerrit.entities.ProjectChangeKey;
      44             : import com.google.gerrit.entities.RefNames;
      45             : import com.google.gerrit.extensions.api.changes.NotifyHandling;
      46             : import com.google.gerrit.extensions.config.FactoryModule;
      47             : import com.google.gerrit.extensions.restapi.BadRequestException;
      48             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      49             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      50             : import com.google.gerrit.extensions.restapi.RestApiException;
      51             : import com.google.gerrit.server.CurrentUser;
      52             : import com.google.gerrit.server.GerritPersonIdent;
      53             : import com.google.gerrit.server.account.AccountState;
      54             : import com.google.gerrit.server.change.NotifyResolver;
      55             : import com.google.gerrit.server.extensions.events.AttentionSetObserver;
      56             : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
      57             : import com.google.gerrit.server.git.GitRepositoryManager;
      58             : import com.google.gerrit.server.git.validators.OnSubmitValidators;
      59             : import com.google.gerrit.server.index.change.ChangeIndexer;
      60             : import com.google.gerrit.server.logging.Metadata;
      61             : import com.google.gerrit.server.logging.RequestId;
      62             : import com.google.gerrit.server.logging.TraceContext;
      63             : import com.google.gerrit.server.notedb.ChangeNotes;
      64             : import com.google.gerrit.server.notedb.ChangeUpdate;
      65             : import com.google.gerrit.server.notedb.LimitExceededException;
      66             : import com.google.gerrit.server.notedb.NoteDbUpdateManager;
      67             : import com.google.gerrit.server.project.InvalidChangeOperationException;
      68             : import com.google.gerrit.server.project.NoSuchChangeException;
      69             : import com.google.gerrit.server.project.NoSuchProjectException;
      70             : import com.google.gerrit.server.project.NoSuchRefException;
      71             : import com.google.gerrit.server.query.change.ChangeData;
      72             : import com.google.inject.Inject;
      73             : import com.google.inject.Module;
      74             : import com.google.inject.assistedinject.Assisted;
      75             : import java.io.IOException;
      76             : import java.time.Instant;
      77             : import java.time.ZoneId;
      78             : import java.util.ArrayList;
      79             : import java.util.Collection;
      80             : import java.util.HashMap;
      81             : import java.util.List;
      82             : import java.util.Map;
      83             : import java.util.Objects;
      84             : import java.util.Optional;
      85             : import java.util.TreeMap;
      86             : import java.util.function.Function;
      87             : import org.eclipse.jgit.lib.BatchRefUpdate;
      88             : import org.eclipse.jgit.lib.ObjectInserter;
      89             : import org.eclipse.jgit.lib.PersonIdent;
      90             : import org.eclipse.jgit.lib.Repository;
      91             : import org.eclipse.jgit.revwalk.RevWalk;
      92             : import org.eclipse.jgit.transport.PushCertificate;
      93             : import org.eclipse.jgit.transport.ReceiveCommand;
      94             : import org.eclipse.jgit.transport.ReceiveCommand.Result;
      95             : 
      96             : /**
      97             :  * Helper for a set of change updates that should be applied to the NoteDb database.
      98             :  *
      99             :  * <p>An update operation can be divided into three phases:
     100             :  *
     101             :  * <ol>
     102             :  *   <li>Git reference updates
     103             :  *   <li>Review metadata updates
     104             :  *   <li>Post-update steps
     105             :  *   <li>
     106             :  * </ol>
     107             :  *
     108             :  * A single conceptual operation, such as a REST API call or a merge operation, may make multiple
     109             :  * changes at each step, which all need to be serialized relative to each other. Moreover, for
     110             :  * consistency, the git ref updates must be visible to the review metadata updates, since for
     111             :  * example the metadata might refer to newly-created patch set refs. In NoteDb, this is accomplished
     112             :  * by combining these two phases into a single {@link BatchRefUpdate}.
     113             :  *
     114             :  * <p>Similarly, all post-update steps, such as sending email, must run only after all storage
     115             :  * mutations have completed.
     116             :  */
     117             : public class BatchUpdate implements AutoCloseable {
     118         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     119             : 
     120             :   public static Module module() {
     121         152 :     return new FactoryModule() {
     122             :       @Override
     123             :       public void configure() {
     124         152 :         factory(BatchUpdate.Factory.class);
     125         152 :       }
     126             :     };
     127             :   }
     128             : 
     129             :   public interface Factory {
     130             :     BatchUpdate create(Project.NameKey project, CurrentUser user, Instant when);
     131             :   }
     132             : 
     133             :   public static void execute(
     134             :       Collection<BatchUpdate> updates, ImmutableList<BatchUpdateListener> listeners, boolean dryrun)
     135             :       throws UpdateException, RestApiException {
     136         110 :     requireNonNull(listeners);
     137         110 :     if (updates.isEmpty()) {
     138          68 :       return;
     139             :     }
     140             : 
     141         110 :     checkDifferentProject(updates);
     142             : 
     143             :     try {
     144         110 :       List<ListenableFuture<ChangeData>> indexFutures = new ArrayList<>();
     145         110 :       List<ChangesHandle> changesHandles = new ArrayList<>(updates.size());
     146             :       try {
     147         110 :         for (BatchUpdate u : updates) {
     148         110 :           u.executeUpdateRepo();
     149         110 :         }
     150         110 :         notifyAfterUpdateRepo(listeners);
     151         110 :         for (BatchUpdate u : updates) {
     152         110 :           changesHandles.add(u.executeChangeOps(listeners, dryrun));
     153         110 :         }
     154         110 :         for (ChangesHandle h : changesHandles) {
     155         110 :           h.execute();
     156         110 :           if (h.requiresReindex()) {
     157         109 :             indexFutures.addAll(h.startIndexFutures());
     158             :           }
     159         110 :         }
     160         110 :         notifyAfterUpdateRefs(listeners);
     161         110 :         notifyAfterUpdateChanges(listeners);
     162             :       } finally {
     163         110 :         for (ChangesHandle h : changesHandles) {
     164         110 :           h.close();
     165         110 :         }
     166             :       }
     167             : 
     168         110 :       Map<Change.Id, ChangeData> changeDatas =
     169         110 :           Futures.allAsList(indexFutures).get().stream()
     170             :               // filter out null values that were returned for change deletions
     171         110 :               .filter(Objects::nonNull)
     172         110 :               .collect(toMap(cd -> cd.change().getId(), Function.identity()));
     173             : 
     174             :       // Fire ref update events only after all mutations are finished, since callers may assume a
     175             :       // patch set ref being created means the change was created, or a branch advancing meaning
     176             :       // some changes were closed.
     177         110 :       updates.forEach(BatchUpdate::fireRefChangeEvent);
     178             : 
     179         110 :       if (!dryrun) {
     180         110 :         for (BatchUpdate u : updates) {
     181         110 :           u.executePostOps(changeDatas);
     182         110 :         }
     183             :       }
     184          26 :     } catch (Exception e) {
     185           0 :       wrapAndThrowException(e);
     186         110 :     }
     187         110 :   }
     188             : 
     189             :   private static void notifyAfterUpdateRepo(ImmutableList<BatchUpdateListener> listeners)
     190             :       throws Exception {
     191         110 :     for (BatchUpdateListener listener : listeners) {
     192          53 :       listener.afterUpdateRepos();
     193          53 :     }
     194         110 :   }
     195             : 
     196             :   private static void notifyAfterUpdateRefs(ImmutableList<BatchUpdateListener> listeners)
     197             :       throws Exception {
     198         110 :     for (BatchUpdateListener listener : listeners) {
     199          53 :       listener.afterUpdateRefs();
     200          53 :     }
     201         110 :   }
     202             : 
     203             :   private static void notifyAfterUpdateChanges(ImmutableList<BatchUpdateListener> listeners)
     204             :       throws Exception {
     205         110 :     for (BatchUpdateListener listener : listeners) {
     206          53 :       listener.afterUpdateChanges();
     207          53 :     }
     208         110 :   }
     209             : 
     210             :   private static void checkDifferentProject(Collection<BatchUpdate> updates) {
     211         110 :     Multiset<Project.NameKey> projectCounts =
     212         110 :         updates.stream().map(u -> u.project).collect(toImmutableMultiset());
     213         110 :     checkArgument(
     214         110 :         projectCounts.entrySet().size() == updates.size(),
     215             :         "updates must all be for different projects, got: %s",
     216             :         projectCounts);
     217         110 :   }
     218             : 
     219             :   private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
     220             :     // Convert common non-REST exception types with user-visible messages to corresponding REST
     221             :     // exception types.
     222          26 :     if (e instanceof InvalidChangeOperationException || e instanceof LimitExceededException) {
     223           1 :       throw new ResourceConflictException(e.getMessage(), e);
     224          26 :     } else if (e instanceof NoSuchChangeException
     225             :         || e instanceof NoSuchRefException
     226             :         || e instanceof NoSuchProjectException) {
     227           0 :       throw new ResourceNotFoundException(e.getMessage(), e);
     228          26 :     } else if (e instanceof CommentsRejectedException) {
     229             :       // SC_BAD_REQUEST is not ideal because it's not a syntactic error, but there is no better
     230             :       // status code and it's isolated in monitoring.
     231           3 :       throw new BadRequestException(e.getMessage(), e);
     232             :     }
     233             : 
     234          24 :     Throwables.throwIfUnchecked(e);
     235             : 
     236             :     // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
     237             :     // ResourceConflictException to indicate an atomic update failure.
     238          24 :     Throwables.throwIfInstanceOf(e, UpdateException.class);
     239          12 :     Throwables.throwIfInstanceOf(e, RestApiException.class);
     240             : 
     241             :     // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
     242          12 :     throw new UpdateException(e);
     243             :   }
     244             : 
     245         110 :   class ContextImpl implements Context {
     246             :     @Override
     247             :     public RepoView getRepoView() throws IOException {
     248         109 :       return BatchUpdate.this.getRepoView();
     249             :     }
     250             : 
     251             :     @Override
     252             :     public RevWalk getRevWalk() throws IOException {
     253         103 :       return getRepoView().getRevWalk();
     254             :     }
     255             : 
     256             :     @Override
     257             :     public Project.NameKey getProject() {
     258         103 :       return project;
     259             :     }
     260             : 
     261             :     @Override
     262             :     public Instant getWhen() {
     263         103 :       return when;
     264             :     }
     265             : 
     266             :     @Override
     267             :     public ZoneId getZoneId() {
     268          17 :       return zoneId;
     269             :     }
     270             : 
     271             :     @Override
     272             :     public CurrentUser getUser() {
     273         103 :       return user;
     274             :     }
     275             : 
     276             :     @Override
     277             :     public NotifyResolver.Result getNotify(Change.Id changeId) {
     278         103 :       NotifyHandling notifyHandling = perChangeNotifyHandling.get(changeId);
     279         103 :       return notifyHandling != null ? notify.withHandling(notifyHandling) : notify;
     280             :     }
     281             :   }
     282             : 
     283         110 :   private class RepoContextImpl extends ContextImpl implements RepoContext {
     284             :     @Override
     285             :     public ObjectInserter getInserter() throws IOException {
     286         103 :       return getRepoView().getInserterWrapper();
     287             :     }
     288             : 
     289             :     @Override
     290             :     public void addRefUpdate(ReceiveCommand cmd) throws IOException {
     291         109 :       getRepoView().getCommands().add(cmd);
     292         109 :     }
     293             :   }
     294             : 
     295             :   private class ChangeContextImpl extends ContextImpl implements ChangeContext {
     296             :     private final ChangeNotes notes;
     297             : 
     298             :     /**
     299             :      * Updates where the caller instructed us to create one NoteDb commit per update. Keyed by
     300             :      * PatchSet.Id only for convenience.
     301             :      */
     302             :     private final Map<PatchSet.Id, ChangeUpdate> defaultUpdates;
     303             : 
     304             :     /**
     305             :      * Updates where the caller allowed us to combine potentially multiple adjustments into a single
     306             :      * commit in NoteDb by re-using the same ChangeUpdate instance. Will still be one commit per
     307             :      * patch set.
     308             :      */
     309             :     private final ListMultimap<PatchSet.Id, ChangeUpdate> distinctUpdates;
     310             : 
     311             :     private boolean deleted;
     312             : 
     313         103 :     ChangeContextImpl(ChangeNotes notes) {
     314         103 :       this.notes = requireNonNull(notes);
     315         103 :       defaultUpdates = new TreeMap<>(comparing(PatchSet.Id::get));
     316         103 :       distinctUpdates = ArrayListMultimap.create();
     317         103 :     }
     318             : 
     319             :     @Override
     320             :     public ChangeUpdate getUpdate(PatchSet.Id psId) {
     321         103 :       ChangeUpdate u = defaultUpdates.get(psId);
     322         103 :       if (u == null) {
     323         103 :         u = getNewChangeUpdate(psId);
     324         103 :         defaultUpdates.put(psId, u);
     325             :       }
     326         103 :       return u;
     327             :     }
     328             : 
     329             :     @Override
     330             :     public ChangeUpdate getDistinctUpdate(PatchSet.Id psId) {
     331           4 :       ChangeUpdate u = getNewChangeUpdate(psId);
     332           4 :       distinctUpdates.put(psId, u);
     333           4 :       return u;
     334             :     }
     335             : 
     336             :     private ChangeUpdate getNewChangeUpdate(PatchSet.Id psId) {
     337         103 :       ChangeUpdate u = changeUpdateFactory.create(notes, user, when);
     338         103 :       if (newChanges.containsKey(notes.getChangeId())) {
     339         103 :         u.setAllowWriteToNewRef(true);
     340             :       }
     341         103 :       u.setPatchSetId(psId);
     342         103 :       return u;
     343             :     }
     344             : 
     345             :     @Override
     346             :     public ChangeNotes getNotes() {
     347         103 :       return notes;
     348             :     }
     349             : 
     350             :     @Override
     351             :     public void deleteChange() {
     352          11 :       deleted = true;
     353          11 :     }
     354             :   }
     355             : 
     356             :   private class PostUpdateContextImpl extends ContextImpl implements PostUpdateContext {
     357             :     private final Map<Change.Id, ChangeData> changeDatas;
     358             : 
     359         110 :     PostUpdateContextImpl(Map<Change.Id, ChangeData> changeDatas) {
     360         110 :       this.changeDatas = changeDatas;
     361         110 :     }
     362             : 
     363             :     @Override
     364             :     public ChangeData getChangeData(Project.NameKey projectName, Change.Id changeId) {
     365          83 :       return changeDatas.computeIfAbsent(
     366           0 :           changeId, id -> changeDataFactory.create(projectName, changeId));
     367             :     }
     368             : 
     369             :     @Override
     370             :     public ChangeData getChangeData(Change change) {
     371         103 :       return changeDatas.computeIfAbsent(change.getId(), id -> changeDataFactory.create(change));
     372             :     }
     373             :   }
     374             : 
     375             :   /** Per-change result status from {@link #executeChangeOps}. */
     376         103 :   private enum ChangeResult {
     377         103 :     SKIPPED,
     378         103 :     UPSERTED,
     379         103 :     DELETED
     380             :   }
     381             : 
     382             :   private final GitRepositoryManager repoManager;
     383             :   private final ChangeData.Factory changeDataFactory;
     384             :   private final ChangeNotes.Factory changeNotesFactory;
     385             :   private final ChangeUpdate.Factory changeUpdateFactory;
     386             :   private final NoteDbUpdateManager.Factory updateManagerFactory;
     387             :   private final ChangeIndexer indexer;
     388             :   private final GitReferenceUpdated gitRefUpdated;
     389             : 
     390             :   private final Project.NameKey project;
     391             :   private final CurrentUser user;
     392             :   private final Instant when;
     393             :   private final ZoneId zoneId;
     394             : 
     395         110 :   private final ListMultimap<Change.Id, BatchUpdateOp> ops =
     396         110 :       MultimapBuilder.linkedHashKeys().arrayListValues().build();
     397         110 :   private final Map<Change.Id, Change> newChanges = new HashMap<>();
     398         110 :   private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
     399         110 :   private final Map<Change.Id, NotifyHandling> perChangeNotifyHandling = new HashMap<>();
     400             : 
     401             :   private RepoView repoView;
     402             :   private BatchRefUpdate batchRefUpdate;
     403             :   private ImmutableListMultimap<ProjectChangeKey, AttentionSetUpdate> attentionSetUpdates;
     404             : 
     405             :   private boolean executed;
     406             :   private OnSubmitValidators onSubmitValidators;
     407             :   private PushCertificate pushCert;
     408             :   private String refLogMessage;
     409         110 :   private NotifyResolver.Result notify = NotifyResolver.Result.all();
     410             :   // Batch operations doesn't need observer
     411             :   private AttentionSetObserver attentionSetObserver;
     412             : 
     413             :   @Inject
     414             :   BatchUpdate(
     415             :       GitRepositoryManager repoManager,
     416             :       @GerritPersonIdent PersonIdent serverIdent,
     417             :       ChangeData.Factory changeDataFactory,
     418             :       ChangeNotes.Factory changeNotesFactory,
     419             :       ChangeUpdate.Factory changeUpdateFactory,
     420             :       NoteDbUpdateManager.Factory updateManagerFactory,
     421             :       ChangeIndexer indexer,
     422             :       GitReferenceUpdated gitRefUpdated,
     423             :       AttentionSetObserver attentionSetObserver,
     424             :       @Assisted Project.NameKey project,
     425             :       @Assisted CurrentUser user,
     426         110 :       @Assisted Instant when) {
     427         110 :     this.repoManager = repoManager;
     428         110 :     this.changeDataFactory = changeDataFactory;
     429         110 :     this.changeNotesFactory = changeNotesFactory;
     430         110 :     this.changeUpdateFactory = changeUpdateFactory;
     431         110 :     this.updateManagerFactory = updateManagerFactory;
     432         110 :     this.indexer = indexer;
     433         110 :     this.gitRefUpdated = gitRefUpdated;
     434         110 :     this.project = project;
     435         110 :     this.user = user;
     436         110 :     this.when = when;
     437         110 :     this.attentionSetObserver = attentionSetObserver;
     438         110 :     zoneId = serverIdent.getZoneId();
     439         110 :   }
     440             : 
     441             :   @Override
     442             :   public void close() {
     443         110 :     if (repoView != null) {
     444         110 :       repoView.close();
     445             :     }
     446         110 :   }
     447             : 
     448             :   public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
     449           1 :     execute(ImmutableList.of(this), ImmutableList.of(listener), false);
     450           1 :   }
     451             : 
     452             :   public void execute() throws UpdateException, RestApiException {
     453         108 :     execute(ImmutableList.of(this), ImmutableList.of(), false);
     454         108 :   }
     455             : 
     456             :   public boolean isExecuted() {
     457          53 :     return executed;
     458             :   }
     459             : 
     460             :   public BatchUpdate setRepository(Repository repo, RevWalk revWalk, ObjectInserter inserter) {
     461         109 :     checkState(this.repoView == null, "repo already set");
     462         109 :     repoView = new RepoView(repo, revWalk, inserter);
     463         109 :     return this;
     464             :   }
     465             : 
     466             :   public BatchUpdate setPushCertificate(@Nullable PushCertificate pushCert) {
     467           0 :     this.pushCert = pushCert;
     468           0 :     return this;
     469             :   }
     470             : 
     471             :   public BatchUpdate setRefLogMessage(@Nullable String refLogMessage) {
     472         101 :     this.refLogMessage = refLogMessage;
     473         101 :     return this;
     474             :   }
     475             : 
     476             :   /**
     477             :    * Set the default notification settings for all changes in the batch.
     478             :    *
     479             :    * @param notify notification settings.
     480             :    * @return this.
     481             :    */
     482             :   public BatchUpdate setNotify(NotifyResolver.Result notify) {
     483         102 :     this.notify = requireNonNull(notify);
     484         102 :     return this;
     485             :   }
     486             : 
     487             :   /**
     488             :    * Override the {@link NotifyHandling} on a per-change basis.
     489             :    *
     490             :    * <p>Only the handling enum can be overridden; all changes share the same value for {@link
     491             :    * com.google.gerrit.server.change.NotifyResolver.Result#accounts()}.
     492             :    *
     493             :    * @param changeId change ID.
     494             :    * @param notifyHandling notify handling.
     495             :    * @return this.
     496             :    */
     497             :   public BatchUpdate setNotifyHandling(Change.Id changeId, NotifyHandling notifyHandling) {
     498          37 :     this.perChangeNotifyHandling.put(changeId, requireNonNull(notifyHandling));
     499          37 :     return this;
     500             :   }
     501             : 
     502             :   /**
     503             :    * Add a validation step for intended ref operations, which will be performed at the end of {@link
     504             :    * RepoOnlyOp#updateRepo(RepoContext)} step.
     505             :    */
     506             :   public BatchUpdate setOnSubmitValidators(OnSubmitValidators onSubmitValidators) {
     507          53 :     this.onSubmitValidators = onSubmitValidators;
     508          53 :     return this;
     509             :   }
     510             : 
     511             :   public Project.NameKey getProject() {
     512          53 :     return project;
     513             :   }
     514             : 
     515             :   private void initRepository() throws IOException {
     516         110 :     if (repoView == null) {
     517          76 :       repoView = new RepoView(repoManager, project);
     518             :     }
     519         110 :   }
     520             : 
     521             :   private RepoView getRepoView() throws IOException {
     522         109 :     initRepository();
     523         109 :     return repoView;
     524             :   }
     525             : 
     526             :   private Optional<AccountState> getAccount() {
     527         109 :     return user.isIdentifiedUser()
     528         109 :         ? Optional.of(user.asIdentifiedUser().state())
     529           1 :         : Optional.empty();
     530             :   }
     531             : 
     532             :   public Map<String, ReceiveCommand> getRefUpdates() {
     533          69 :     return repoView != null ? repoView.getCommands().getCommands() : ImmutableMap.of();
     534             :   }
     535             : 
     536             :   /**
     537             :    * Return the references successfully updated by this BatchUpdate with their command. In dryrun,
     538             :    * we assume all updates were successful.
     539             :    */
     540             :   public Map<BranchNameKey, ReceiveCommand> getSuccessfullyUpdatedBranches(boolean dryrun) {
     541          69 :     return getRefUpdates().entrySet().stream()
     542          69 :         .filter(entry -> dryrun || entry.getValue().getResult() == Result.OK)
     543          69 :         .collect(
     544          69 :             toMap(entry -> BranchNameKey.create(project, entry.getKey()), Map.Entry::getValue));
     545             :   }
     546             : 
     547             :   public BatchUpdate addOp(Change.Id id, BatchUpdateOp op) {
     548          98 :     checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
     549          98 :     requireNonNull(op);
     550          98 :     ops.put(id, op);
     551          98 :     return this;
     552             :   }
     553             : 
     554             :   public BatchUpdate addRepoOnlyOp(RepoOnlyOp op) {
     555          66 :     checkArgument(!(op instanceof BatchUpdateOp), "use addOp()");
     556          66 :     repoOnlyOps.add(op);
     557          66 :     return this;
     558             :   }
     559             : 
     560             :   public BatchUpdate insertChange(InsertChangeOp op) throws IOException {
     561         103 :     Context ctx = new ContextImpl();
     562         103 :     Change c = op.createChange(ctx);
     563         103 :     checkArgument(
     564         103 :         !newChanges.containsKey(c.getId()), "only one op allowed to create change %s", c.getId());
     565         103 :     newChanges.put(c.getId(), c);
     566         103 :     ops.get(c.getId()).add(0, op);
     567         103 :     return this;
     568             :   }
     569             : 
     570             :   private void executeUpdateRepo() throws UpdateException, RestApiException {
     571             :     try {
     572         110 :       logDebug("Executing updateRepo on %d ops", ops.size());
     573         110 :       RepoContextImpl ctx = new RepoContextImpl();
     574         110 :       for (Map.Entry<Change.Id, BatchUpdateOp> op : ops.entries()) {
     575         103 :         try (TraceContext.TraceTimer ignored =
     576         103 :             TraceContext.newTimer(
     577         103 :                 op.getClass().getSimpleName() + "#updateRepo",
     578         103 :                 Metadata.builder()
     579         103 :                     .projectName(project.get())
     580         103 :                     .changeId(op.getKey().get())
     581         103 :                     .build())) {
     582         103 :           op.getValue().updateRepo(ctx);
     583             :         }
     584         103 :       }
     585             : 
     586         110 :       logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
     587         110 :       for (RepoOnlyOp op : repoOnlyOps) {
     588          66 :         op.updateRepo(ctx);
     589          66 :       }
     590             : 
     591         110 :       if (onSubmitValidators != null && !getRefUpdates().isEmpty()) {
     592             :         // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
     593             :         // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
     594             :         // first update's executeRefUpdates has finished, hence after first repo's refs have been
     595             :         // updated, which is too late.
     596          53 :         onSubmitValidators.validate(
     597          53 :             project, ctx.getRevWalk().getObjectReader(), repoView.getCommands());
     598             :       }
     599          10 :     } catch (Exception e) {
     600           1 :       Throwables.throwIfInstanceOf(e, RestApiException.class);
     601           1 :       throw new UpdateException(e);
     602         110 :     }
     603         110 :   }
     604             : 
     605             :   private void fireRefChangeEvent() {
     606         110 :     if (batchRefUpdate != null) {
     607         109 :       gitRefUpdated.fire(project, batchRefUpdate, getAccount().orElse(null));
     608             :     }
     609         110 :   }
     610             : 
     611             :   private void fireAttentionSetUpdateEvents(PostUpdateContext ctx) {
     612         110 :     for (ProjectChangeKey key : attentionSetUpdates.keySet()) {
     613          51 :       ChangeData change = ctx.getChangeData(key.projectName(), key.changeId());
     614          51 :       AccountState account = ctx.getAccount();
     615          51 :       for (AttentionSetUpdate update : attentionSetUpdates.get(key)) {
     616          51 :         attentionSetObserver.fire(change, account, update, ctx.getWhen());
     617          51 :       }
     618          51 :     }
     619         110 :   }
     620             : 
     621             :   private class ChangesHandle implements AutoCloseable {
     622             :     private final NoteDbUpdateManager manager;
     623             :     private final boolean dryrun;
     624             :     private final Map<Change.Id, ChangeResult> results;
     625             : 
     626         110 :     ChangesHandle(NoteDbUpdateManager manager, boolean dryrun) {
     627         110 :       this.manager = manager;
     628         110 :       this.dryrun = dryrun;
     629         110 :       results = new HashMap<>();
     630         110 :     }
     631             : 
     632             :     @Override
     633             :     public void close() {
     634         110 :       manager.close();
     635         110 :     }
     636             : 
     637             :     void setResult(Change.Id id, ChangeResult result) {
     638         103 :       ChangeResult old = results.putIfAbsent(id, result);
     639         103 :       checkArgument(old == null, "result for change %s already set: %s", id, old);
     640         103 :     }
     641             : 
     642             :     void execute() throws IOException {
     643         110 :       BatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
     644         110 :       BatchUpdate.this.executed = manager.isExecuted();
     645         110 :       BatchUpdate.this.attentionSetUpdates = manager.attentionSetUpdates();
     646         110 :     }
     647             : 
     648             :     boolean requiresReindex() {
     649             :       // We do not need to reindex changes if there are no ref updates, or if updated refs
     650             :       // are all draft comment refs (since draft fields are not stored in the change index).
     651         110 :       BatchRefUpdate bru = BatchUpdate.this.batchRefUpdate;
     652         110 :       return !(bru == null
     653         109 :           || bru.getCommands().isEmpty()
     654         109 :           || bru.getCommands().stream()
     655         110 :               .allMatch(cmd -> RefNames.isRefsDraftsComments(cmd.getRefName())));
     656             :     }
     657             : 
     658             :     ImmutableList<ListenableFuture<ChangeData>> startIndexFutures() {
     659         109 :       if (dryrun) {
     660           0 :         return ImmutableList.of();
     661             :       }
     662         109 :       logDebug("Reindexing %d changes", results.size());
     663         109 :       ImmutableList.Builder<ListenableFuture<ChangeData>> indexFutures =
     664         109 :           ImmutableList.builderWithExpectedSize(results.size());
     665         109 :       for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
     666         103 :         Change.Id id = e.getKey();
     667         103 :         switch (e.getValue()) {
     668             :           case UPSERTED:
     669         103 :             indexFutures.add(indexer.indexAsync(project, id));
     670         103 :             break;
     671             :           case DELETED:
     672          11 :             indexFutures.add(indexer.deleteAsync(id));
     673          11 :             break;
     674             :           case SKIPPED:
     675          45 :             break;
     676             :           default:
     677           0 :             throw new IllegalStateException("unexpected result: " + e.getValue());
     678             :         }
     679         103 :       }
     680         109 :       return indexFutures.build();
     681             :     }
     682             :   }
     683             : 
     684             :   private ChangesHandle executeChangeOps(
     685             :       ImmutableList<BatchUpdateListener> batchUpdateListeners, boolean dryrun) throws Exception {
     686         110 :     logDebug("Executing change ops");
     687         110 :     initRepository();
     688         110 :     Repository repo = repoView.getRepository();
     689         110 :     checkState(
     690         110 :         repo.getRefDatabase().performsAtomicTransactions(),
     691             :         "cannot use NoteDb with a repository that does not support atomic batch ref updates: %s",
     692             :         repo);
     693             : 
     694         110 :     ChangesHandle handle =
     695             :         new ChangesHandle(
     696             :             updateManagerFactory
     697         110 :                 .create(project)
     698         110 :                 .setBatchUpdateListeners(batchUpdateListeners)
     699         110 :                 .setChangeRepo(
     700         110 :                     repo, repoView.getRevWalk(), repoView.getInserter(), repoView.getCommands()),
     701             :             dryrun);
     702         110 :     if (user.isIdentifiedUser()) {
     703         110 :       handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, zoneId));
     704             :     }
     705         110 :     handle.manager.setRefLogMessage(refLogMessage);
     706         110 :     handle.manager.setPushCertificate(pushCert);
     707         110 :     for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
     708         103 :       Change.Id id = e.getKey();
     709         103 :       ChangeContextImpl ctx = newChangeContext(id);
     710         103 :       boolean dirty = false;
     711         103 :       logDebug(
     712             :           "Applying %d ops for change %s: %s",
     713         103 :           e.getValue().size(),
     714             :           id,
     715         103 :           lazy(() -> e.getValue().stream().map(op -> op.getClass().getName()).collect(toSet())));
     716         103 :       for (BatchUpdateOp op : e.getValue()) {
     717         103 :         try (TraceContext.TraceTimer ignored =
     718         103 :             TraceContext.newTimer(
     719         103 :                 op.getClass().getSimpleName() + "#updateChange",
     720         103 :                 Metadata.builder().projectName(project.get()).changeId(id.get()).build())) {
     721         103 :           dirty |= op.updateChange(ctx);
     722             :         }
     723         103 :       }
     724         103 :       if (!dirty) {
     725          52 :         logDebug("No ops reported dirty, short-circuiting");
     726          52 :         handle.setResult(id, ChangeResult.SKIPPED);
     727          52 :         continue;
     728             :       }
     729         103 :       ctx.defaultUpdates.values().forEach(handle.manager::add);
     730         103 :       ctx.distinctUpdates.values().forEach(handle.manager::add);
     731         103 :       if (ctx.deleted) {
     732          11 :         logDebug("Change %s was deleted", id);
     733          11 :         handle.manager.deleteChange(id);
     734          11 :         handle.setResult(id, ChangeResult.DELETED);
     735             :       } else {
     736         103 :         handle.setResult(id, ChangeResult.UPSERTED);
     737             :       }
     738         103 :     }
     739         110 :     return handle;
     740             :   }
     741             : 
     742             :   private ChangeContextImpl newChangeContext(Change.Id id) {
     743         103 :     logDebug("Opening change %s for update", id);
     744         103 :     Change c = newChanges.get(id);
     745         103 :     boolean isNew = c != null;
     746         103 :     if (!isNew) {
     747             :       // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for
     748             :       // existence and populating columns from the parsed notes state.
     749             :       // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way.
     750          95 :       c = ChangeNotes.Factory.newChange(project, id);
     751             :     } else {
     752         103 :       logDebug("Change %s is new", id);
     753             :     }
     754         103 :     ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
     755         103 :     return new ChangeContextImpl(notes);
     756             :   }
     757             : 
     758             :   private void executePostOps(Map<Change.Id, ChangeData> changeDatas) throws Exception {
     759         110 :     PostUpdateContextImpl ctx = new PostUpdateContextImpl(changeDatas);
     760         110 :     for (BatchUpdateOp op : ops.values()) {
     761         103 :       try (TraceContext.TraceTimer ignored =
     762         103 :           TraceContext.newTimer(op.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
     763         103 :         op.postUpdate(ctx);
     764             :       }
     765         103 :     }
     766             : 
     767         110 :     for (RepoOnlyOp op : repoOnlyOps) {
     768          66 :       try (TraceContext.TraceTimer ignored =
     769          66 :           TraceContext.newTimer(op.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
     770          66 :         op.postUpdate(ctx);
     771             :       }
     772          66 :     }
     773         110 :     try (TraceContext.TraceTimer ignored =
     774         110 :         TraceContext.newTimer("fireAttentionSetUpdates#postUpdate", Metadata.empty())) {
     775         110 :       fireAttentionSetUpdateEvents(ctx);
     776             :     }
     777         110 :   }
     778             : 
     779             :   private static void logDebug(String msg) {
     780             :     // Only log if there is a requestId assigned, since those are the
     781             :     // expensive/complicated requests like MergeOp. Doing it every time would be
     782             :     // noisy.
     783         110 :     if (RequestId.isSet()) {
     784         101 :       logger.atFine().log("%s", msg);
     785             :     }
     786         110 :   }
     787             : 
     788             :   private static void logDebug(String msg, @Nullable Object arg) {
     789             :     // Only log if there is a requestId assigned, since those are the
     790             :     // expensive/complicated requests like MergeOp. Doing it every time would be
     791             :     // noisy.
     792         110 :     if (RequestId.isSet()) {
     793         101 :       logger.atFine().log(msg, arg);
     794             :     }
     795         110 :   }
     796             : 
     797             :   private static void logDebug(
     798             :       String msg, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
     799             :     // Only log if there is a requestId assigned, since those are the
     800             :     // expensive/complicated requests like MergeOp. Doing it every time would be
     801             :     // noisy.
     802         103 :     if (RequestId.isSet()) {
     803          93 :       logger.atFine().log(msg, arg1, arg2, arg3);
     804             :     }
     805         103 :   }
     806             : }

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