LCOV - code coverage report
Current view: top level - server/notedb - NoteDbUpdateManager.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 189 196 96.4 %
Date: 2022-11-19 15:00:39 Functions: 29 30 96.7 %

          Line data    Source code
       1             : // Copyright (C) 2016 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.notedb;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.base.Preconditions.checkArgument;
      19             : import static com.google.common.base.Preconditions.checkNotNull;
      20             : import static com.google.common.base.Preconditions.checkState;
      21             : import static com.google.common.collect.ImmutableListMultimap.flatteningToImmutableListMultimap;
      22             : import static com.google.gerrit.server.logging.TraceContext.newTimer;
      23             : 
      24             : import com.google.common.collect.ImmutableList;
      25             : import com.google.common.collect.ImmutableListMultimap;
      26             : import com.google.common.collect.ListMultimap;
      27             : import com.google.common.collect.MultimapBuilder;
      28             : import com.google.gerrit.common.Nullable;
      29             : import com.google.gerrit.entities.AttentionSetUpdate;
      30             : import com.google.gerrit.entities.Change;
      31             : import com.google.gerrit.entities.Project;
      32             : import com.google.gerrit.entities.ProjectChangeKey;
      33             : import com.google.gerrit.entities.RefNames;
      34             : import com.google.gerrit.exceptions.StorageException;
      35             : import com.google.gerrit.git.RefUpdateUtil;
      36             : import com.google.gerrit.metrics.Timer0;
      37             : import com.google.gerrit.server.GerritPersonIdent;
      38             : import com.google.gerrit.server.cancellation.RequestStateContext;
      39             : import com.google.gerrit.server.cancellation.RequestStateContext.NonCancellableOperationContext;
      40             : import com.google.gerrit.server.config.AllUsersName;
      41             : import com.google.gerrit.server.config.GerritServerConfig;
      42             : import com.google.gerrit.server.git.GitRepositoryManager;
      43             : import com.google.gerrit.server.logging.Metadata;
      44             : import com.google.gerrit.server.logging.TraceContext;
      45             : import com.google.gerrit.server.update.BatchUpdateListener;
      46             : import com.google.gerrit.server.update.ChainedReceiveCommands;
      47             : import com.google.inject.Inject;
      48             : import com.google.inject.Provider;
      49             : import com.google.inject.assistedinject.Assisted;
      50             : import java.io.IOException;
      51             : import java.util.Collection;
      52             : import java.util.HashSet;
      53             : import java.util.Map;
      54             : import java.util.Optional;
      55             : import java.util.Set;
      56             : import org.eclipse.jgit.errors.ConfigInvalidException;
      57             : import org.eclipse.jgit.lib.BatchRefUpdate;
      58             : import org.eclipse.jgit.lib.Config;
      59             : import org.eclipse.jgit.lib.ObjectId;
      60             : import org.eclipse.jgit.lib.ObjectInserter;
      61             : import org.eclipse.jgit.lib.PersonIdent;
      62             : import org.eclipse.jgit.lib.Ref;
      63             : import org.eclipse.jgit.lib.Repository;
      64             : import org.eclipse.jgit.revwalk.RevWalk;
      65             : import org.eclipse.jgit.transport.PushCertificate;
      66             : import org.eclipse.jgit.transport.ReceiveCommand;
      67             : 
      68             : /**
      69             :  * Object to manage a single sequence of updates to NoteDb.
      70             :  *
      71             :  * <p>Instances are one-time-use. Handles updating both the change repo and the All-Users repo for
      72             :  * any affected changes, with proper ordering.
      73             :  *
      74             :  * <p>To see the state that would be applied prior to executing the full sequence of updates, use
      75             :  * {@link #stage()}.
      76             :  */
      77             : public class NoteDbUpdateManager implements AutoCloseable {
      78             :   private static final int MAX_UPDATES_DEFAULT = 1000;
      79             :   /** Limits the number of patch sets that can be created. Can be overridden in the config. */
      80             :   private static final int MAX_PATCH_SETS_DEFAULT = 1000;
      81             : 
      82             :   public interface Factory {
      83             :     NoteDbUpdateManager create(Project.NameKey projectName);
      84             :   }
      85             : 
      86             :   private final Provider<PersonIdent> serverIdent;
      87             :   private final GitRepositoryManager repoManager;
      88             :   private final AllUsersName allUsersName;
      89             :   private final NoteDbMetrics metrics;
      90             :   private final Project.NameKey projectName;
      91             :   private final int maxUpdates;
      92             :   private final int maxPatchSets;
      93             :   private final ListMultimap<String, ChangeUpdate> changeUpdates;
      94             :   private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
      95             :   private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
      96             :   private final ListMultimap<String, NoteDbRewriter> rewriters;
      97             :   private final Set<Change.Id> changesToDelete;
      98             : 
      99             :   private OpenRepo changeRepo;
     100             :   private OpenRepo allUsersRepo;
     101             :   private AllUsersAsyncUpdate updateAllUsersAsync;
     102             :   private boolean executed;
     103             :   private String refLogMessage;
     104             :   private PersonIdent refLogIdent;
     105             :   private PushCertificate pushCert;
     106             :   private ImmutableList<BatchUpdateListener> batchUpdateListeners;
     107             : 
     108             :   @Inject
     109             :   NoteDbUpdateManager(
     110             :       @GerritServerConfig Config cfg,
     111             :       @GerritPersonIdent Provider<PersonIdent> serverIdent,
     112             :       GitRepositoryManager repoManager,
     113             :       AllUsersName allUsersName,
     114             :       NoteDbMetrics metrics,
     115             :       AllUsersAsyncUpdate updateAllUsersAsync,
     116         110 :       @Assisted Project.NameKey projectName) {
     117         110 :     this.serverIdent = serverIdent;
     118         110 :     this.repoManager = repoManager;
     119         110 :     this.allUsersName = allUsersName;
     120         110 :     this.metrics = metrics;
     121         110 :     this.updateAllUsersAsync = updateAllUsersAsync;
     122         110 :     this.projectName = projectName;
     123         110 :     maxUpdates = cfg.getInt("change", null, "maxUpdates", MAX_UPDATES_DEFAULT);
     124         110 :     maxPatchSets = cfg.getInt("change", null, "maxPatchSets", MAX_PATCH_SETS_DEFAULT);
     125         110 :     changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     126         110 :     draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     127         110 :     robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     128         110 :     rewriters = MultimapBuilder.hashKeys().arrayListValues().build();
     129         110 :     changesToDelete = new HashSet<>();
     130         110 :     batchUpdateListeners = ImmutableList.of();
     131         110 :   }
     132             : 
     133             :   @Override
     134             :   public void close() {
     135             :     try {
     136         110 :       if (allUsersRepo != null) {
     137          32 :         OpenRepo r = allUsersRepo;
     138          32 :         allUsersRepo = null;
     139          32 :         r.close();
     140             :       }
     141             :     } finally {
     142         110 :       if (changeRepo != null) {
     143         110 :         OpenRepo r = changeRepo;
     144         110 :         changeRepo = null;
     145         110 :         r.close();
     146             :       }
     147             :     }
     148         110 :   }
     149             : 
     150             :   public NoteDbUpdateManager setChangeRepo(
     151             :       Repository repo, RevWalk rw, @Nullable ObjectInserter ins, ChainedReceiveCommands cmds) {
     152         110 :     checkState(changeRepo == null, "change repo already initialized");
     153         110 :     changeRepo = new OpenRepo(repo, rw, ins, cmds, false);
     154         110 :     return this;
     155             :   }
     156             : 
     157             :   public NoteDbUpdateManager setRefLogMessage(String message) {
     158         110 :     this.refLogMessage = message;
     159         110 :     return this;
     160             :   }
     161             : 
     162             :   public NoteDbUpdateManager setRefLogIdent(PersonIdent ident) {
     163         110 :     this.refLogIdent = ident;
     164         110 :     return this;
     165             :   }
     166             : 
     167             :   /**
     168             :    * Set a push certificate for the push that originally triggered this NoteDb update.
     169             :    *
     170             :    * <p>The pusher will not necessarily have specified any of the NoteDb refs explicitly, such as
     171             :    * when processing a push to {@code refs/for/master}. That's fine; this is just passed to the
     172             :    * underlying {@link BatchRefUpdate}, and the implementation decides what to do with it.
     173             :    *
     174             :    * <p>The cert should be associated with the main repo. There is currently no way of associating a
     175             :    * push cert with the {@code All-Users} repo, since it is not currently possible to update draft
     176             :    * changes via push.
     177             :    *
     178             :    * @param pushCert push certificate; may be null.
     179             :    * @return this
     180             :    */
     181             :   public NoteDbUpdateManager setPushCertificate(PushCertificate pushCert) {
     182         110 :     this.pushCert = pushCert;
     183         110 :     return this;
     184             :   }
     185             : 
     186             :   public NoteDbUpdateManager setBatchUpdateListeners(
     187             :       ImmutableList<BatchUpdateListener> batchUpdateListeners) {
     188         110 :     checkNotNull(batchUpdateListeners);
     189         110 :     this.batchUpdateListeners = batchUpdateListeners;
     190         110 :     return this;
     191             :   }
     192             : 
     193             :   public boolean isExecuted() {
     194         110 :     return executed;
     195             :   }
     196             : 
     197             :   private void initChangeRepo() throws IOException {
     198         109 :     if (changeRepo == null) {
     199           2 :       changeRepo = OpenRepo.open(repoManager, projectName);
     200             :     }
     201         109 :   }
     202             : 
     203             :   private void initAllUsersRepo() throws IOException {
     204          32 :     if (allUsersRepo == null) {
     205          32 :       allUsersRepo = OpenRepo.open(repoManager, allUsersName);
     206             :     }
     207          32 :   }
     208             : 
     209             :   private boolean isEmpty() {
     210         110 :     return changeUpdates.isEmpty()
     211          60 :         && draftUpdates.isEmpty()
     212          59 :         && robotCommentUpdates.isEmpty()
     213          59 :         && rewriters.isEmpty()
     214          59 :         && changesToDelete.isEmpty()
     215          57 :         && !hasCommands(changeRepo)
     216          55 :         && !hasCommands(allUsersRepo)
     217         110 :         && updateAllUsersAsync.isEmpty();
     218             :   }
     219             : 
     220             :   private static boolean hasCommands(@Nullable OpenRepo or) {
     221          57 :     return or != null && !or.cmds.isEmpty();
     222             :   }
     223             : 
     224             :   /**
     225             :    * Add an update to the list of updates to execute.
     226             :    *
     227             :    * <p>Updates should only be added to the manager after all mutations have been made, as this
     228             :    * method may eagerly access the update.
     229             :    *
     230             :    * @param update the update to add.
     231             :    */
     232             :   public void add(ChangeUpdate update) {
     233         103 :     checkNotExecuted();
     234         103 :     checkArgument(
     235         103 :         update.getProjectName().equals(projectName),
     236             :         "update for project %s cannot be added to manager for project %s",
     237         103 :         update.getProjectName(),
     238             :         projectName);
     239         103 :     checkArgument(
     240         103 :         !rewriters.containsKey(update.getRefName()),
     241             :         "cannot update & rewrite ref %s in one BatchUpdate",
     242         103 :         update.getRefName());
     243             : 
     244         103 :     ChangeDraftUpdate du = update.getDraftUpdate();
     245         103 :     if (du != null) {
     246          29 :       draftUpdates.put(du.getRefName(), du);
     247             :     }
     248         103 :     RobotCommentUpdate rcu = update.getRobotCommentUpdate();
     249         103 :     if (rcu != null) {
     250           9 :       robotCommentUpdates.put(rcu.getRefName(), rcu);
     251             :     }
     252         103 :     DeleteCommentRewriter deleteCommentRewriter = update.getDeleteCommentRewriter();
     253         103 :     if (deleteCommentRewriter != null) {
     254             :       // Checks whether there is any ChangeUpdate or rewriter added earlier for the same ref.
     255           3 :       checkArgument(
     256           3 :           !changeUpdates.containsKey(deleteCommentRewriter.getRefName()),
     257             :           "cannot update & rewrite ref %s in one BatchUpdate",
     258           3 :           deleteCommentRewriter.getRefName());
     259           3 :       checkArgument(
     260           3 :           !rewriters.containsKey(deleteCommentRewriter.getRefName()),
     261             :           "cannot rewrite the same ref %s in one BatchUpdate",
     262           3 :           deleteCommentRewriter.getRefName());
     263           3 :       rewriters.put(deleteCommentRewriter.getRefName(), deleteCommentRewriter);
     264             :     }
     265             : 
     266         103 :     DeleteChangeMessageRewriter deleteChangeMessageRewriter =
     267         103 :         update.getDeleteChangeMessageRewriter();
     268         103 :     if (deleteChangeMessageRewriter != null) {
     269             :       // Checks whether there is any ChangeUpdate or rewriter added earlier for the same ref.
     270           1 :       checkArgument(
     271           1 :           !changeUpdates.containsKey(deleteChangeMessageRewriter.getRefName()),
     272             :           "cannot update & rewrite ref %s in one BatchUpdate",
     273           1 :           deleteChangeMessageRewriter.getRefName());
     274           1 :       checkArgument(
     275           1 :           !rewriters.containsKey(deleteChangeMessageRewriter.getRefName()),
     276             :           "cannot rewrite the same ref %s in one BatchUpdate",
     277           1 :           deleteChangeMessageRewriter.getRefName());
     278           1 :       rewriters.put(deleteChangeMessageRewriter.getRefName(), deleteChangeMessageRewriter);
     279             :     }
     280             : 
     281         103 :     changeUpdates.put(update.getRefName(), update);
     282         103 :   }
     283             : 
     284             :   public void add(ChangeDraftUpdate draftUpdate) {
     285           1 :     checkNotExecuted();
     286           1 :     draftUpdates.put(draftUpdate.getRefName(), draftUpdate);
     287           1 :   }
     288             : 
     289             :   public void deleteChange(Change.Id id) {
     290          11 :     checkNotExecuted();
     291          11 :     changesToDelete.add(id);
     292          11 :   }
     293             : 
     294             :   /**
     295             :    * Stage updates in the manager's internal list of commands.
     296             :    *
     297             :    * @throws IOException if a storage layer error occurs.
     298             :    */
     299             :   private void stage() throws IOException {
     300         109 :     try (Timer0.Context timer = metrics.stageUpdateLatency.start()) {
     301         109 :       if (isEmpty()) {
     302           0 :         return;
     303             :       }
     304             : 
     305         109 :       initChangeRepo();
     306         109 :       if (!draftUpdates.isEmpty() || !changesToDelete.isEmpty()) {
     307          32 :         initAllUsersRepo();
     308             :       }
     309         109 :       addCommands();
     310           0 :     }
     311         109 :   }
     312             : 
     313             :   @Nullable
     314             :   public BatchRefUpdate execute() throws IOException {
     315           2 :     return execute(false);
     316             :   }
     317             : 
     318             :   @Nullable
     319             :   public BatchRefUpdate execute(boolean dryrun) throws IOException {
     320         110 :     checkNotExecuted();
     321         110 :     if (isEmpty()) {
     322          55 :       executed = true;
     323          55 :       return null;
     324             :     }
     325         109 :     try (Timer0.Context timer = metrics.updateLatency.start();
     326             :         NonCancellableOperationContext nonCancellableOperationContext =
     327         109 :             RequestStateContext.startNonCancellableOperation()) {
     328         109 :       stage();
     329             :       // ChangeUpdates must execute before ChangeDraftUpdates.
     330             :       //
     331             :       // ChangeUpdate will automatically delete draft comments for any published
     332             :       // comments, but the updates to the two repos don't happen atomically.
     333             :       // Thus if the change meta update succeeds and the All-Users update fails,
     334             :       // we may have stale draft comments. Doing it in this order allows stale
     335             :       // comments to be filtered out by ChangeNotes, reflecting the fact that
     336             :       // comments can only go from DRAFT to PUBLISHED, not vice versa.
     337             :       BatchRefUpdate result;
     338         109 :       try (TraceContext.TraceTimer ignored =
     339         109 :           newTimer("NoteDbUpdateManager#updateRepo", Metadata.empty())) {
     340         109 :         result = execute(changeRepo, dryrun, pushCert);
     341             :       }
     342         109 :       try (TraceContext.TraceTimer ignored =
     343         109 :           newTimer("NoteDbUpdateManager#updateAllUsersSync", Metadata.empty())) {
     344         109 :         execute(allUsersRepo, dryrun, null);
     345             :       }
     346         109 :       if (!dryrun) {
     347             :         // Only execute the asynchronous operation if we are not in dry-run mode: The dry run would
     348             :         // have to run synchronous to be of any value at all. For the removal of draft comments from
     349             :         // All-Users we don't care much of the operation succeeds, so we are skipping the dry run
     350             :         // altogether.
     351         109 :         updateAllUsersAsync.execute(refLogIdent, refLogMessage, pushCert);
     352             :       }
     353         109 :       executed = true;
     354         109 :       return result;
     355             :     } finally {
     356         109 :       close();
     357             :     }
     358             :   }
     359             : 
     360             :   public ImmutableListMultimap<ProjectChangeKey, AttentionSetUpdate> attentionSetUpdates() {
     361         110 :     return this.changeUpdates.values().stream()
     362         110 :         .collect(
     363         110 :             flatteningToImmutableListMultimap(
     364         103 :                 cu -> ProjectChangeKey.create(cu.getProjectName(), cu.getId()),
     365         103 :                 cu -> cu.getAttentionSetUpdates().stream()));
     366             :   }
     367             : 
     368             :   @Nullable
     369             :   private BatchRefUpdate execute(OpenRepo or, boolean dryrun, @Nullable PushCertificate pushCert)
     370             :       throws IOException {
     371         109 :     if (or == null || or.cmds.isEmpty()) {
     372         109 :       return null;
     373             :     }
     374         109 :     if (!dryrun) {
     375         109 :       or.flush();
     376             :     } else {
     377             :       // OpenRepo buffers objects separately; caller may assume that objects are available in the
     378             :       // inserter it previously passed via setChangeRepo.
     379           0 :       or.flushToFinalInserter();
     380             :     }
     381             : 
     382         109 :     BatchRefUpdate bru = or.repo.getRefDatabase().newBatchUpdate();
     383         109 :     bru.setPushCertificate(pushCert);
     384         109 :     if (refLogMessage != null) {
     385         100 :       bru.setRefLogMessage(refLogMessage, false);
     386             :     } else {
     387          91 :       bru.setRefLogMessage(
     388          91 :           firstNonNull(NoteDbUtil.guessRestApiHandler(), "Update NoteDb refs"), false);
     389             :     }
     390         109 :     bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get());
     391         109 :     bru.setAtomic(true);
     392         109 :     or.cmds.addTo(bru);
     393         109 :     bru.setAllowNonFastForwards(allowNonFastForwards(or.cmds));
     394         109 :     for (BatchUpdateListener listener : batchUpdateListeners) {
     395          53 :       bru = listener.beforeUpdateRefs(bru);
     396          53 :     }
     397             : 
     398         109 :     if (!dryrun) {
     399         109 :       RefUpdateUtil.executeChecked(bru, or.rw);
     400             :     }
     401         109 :     return bru;
     402             :   }
     403             : 
     404             :   private void addCommands() throws IOException {
     405         109 :     changeRepo.addUpdates(changeUpdates, Optional.of(maxUpdates), Optional.of(maxPatchSets));
     406         109 :     if (!draftUpdates.isEmpty()) {
     407          29 :       boolean publishOnly = draftUpdates.values().stream().allMatch(ChangeDraftUpdate::canRunAsync);
     408          29 :       if (publishOnly) {
     409          26 :         updateAllUsersAsync.setDraftUpdates(draftUpdates);
     410             :       } else {
     411          21 :         allUsersRepo.addUpdatesNoLimits(draftUpdates);
     412             :       }
     413             :     }
     414         109 :     if (!robotCommentUpdates.isEmpty()) {
     415           9 :       changeRepo.addUpdatesNoLimits(robotCommentUpdates);
     416             :     }
     417         109 :     if (!rewriters.isEmpty()) {
     418           4 :       addRewrites(rewriters, changeRepo);
     419             :     }
     420             : 
     421         109 :     for (Change.Id id : changesToDelete) {
     422          11 :       doDelete(id);
     423          11 :     }
     424         109 :   }
     425             : 
     426             :   private void doDelete(Change.Id id) throws IOException {
     427          11 :     String metaRef = RefNames.changeMetaRef(id);
     428          11 :     Optional<ObjectId> old = changeRepo.cmds.get(metaRef);
     429          11 :     old.ifPresent(
     430           0 :         objectId -> changeRepo.cmds.add(new ReceiveCommand(objectId, ObjectId.zeroId(), metaRef)));
     431             : 
     432             :     // Just scan repo for ref names, but get "old" values from cmds.
     433             :     for (Ref r :
     434          11 :         allUsersRepo.repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(id))) {
     435           1 :       old = allUsersRepo.cmds.get(r.getName());
     436           1 :       old.ifPresent(
     437             :           objectId ->
     438           1 :               allUsersRepo.cmds.add(new ReceiveCommand(objectId, ObjectId.zeroId(), r.getName())));
     439           1 :     }
     440          11 :   }
     441             : 
     442             :   private void checkNotExecuted() {
     443         110 :     checkState(!executed, "update has already been executed");
     444         110 :   }
     445             : 
     446             :   private static void addRewrites(ListMultimap<String, NoteDbRewriter> rewriters, OpenRepo openRepo)
     447             :       throws IOException {
     448           4 :     for (Map.Entry<String, Collection<NoteDbRewriter>> entry : rewriters.asMap().entrySet()) {
     449           4 :       String refName = entry.getKey();
     450           4 :       ObjectId oldTip = openRepo.cmds.get(refName).orElse(ObjectId.zeroId());
     451             : 
     452           4 :       if (oldTip.equals(ObjectId.zeroId())) {
     453           0 :         throw new StorageException(String.format("Ref %s is empty", refName));
     454             :       }
     455             : 
     456           4 :       ObjectId currTip = oldTip;
     457             :       try {
     458           4 :         for (NoteDbRewriter noteDbRewriter : entry.getValue()) {
     459           4 :           ObjectId nextTip =
     460           4 :               noteDbRewriter.rewriteCommitHistory(openRepo.rw, openRepo.tempIns, currTip);
     461           4 :           if (nextTip != null) {
     462           4 :             currTip = nextTip;
     463             :           }
     464           4 :         }
     465           0 :       } catch (ConfigInvalidException e) {
     466           0 :         throw new StorageException("Cannot rewrite commit history", e);
     467           4 :       }
     468             : 
     469           4 :       if (!oldTip.equals(currTip)) {
     470           4 :         openRepo.cmds.add(new ReceiveCommand(oldTip, currTip, refName));
     471             :       }
     472           4 :     }
     473           4 :   }
     474             : 
     475             :   /**
     476             :    * Returns true if we should allow non-fast-forwards while performing the batch ref update. Non-ff
     477             :    * updates are necessary in some specific cases:
     478             :    *
     479             :    * <p>1. Draft ref updates are non fast-forward, since the ref always points to a single commit
     480             :    * that has no parents.
     481             :    *
     482             :    * <p>2. NoteDb rewriters.
     483             :    *
     484             :    * <p>3. If any of the receive commands is of type {@link
     485             :    * org.eclipse.jgit.transport.ReceiveCommand.Type#UPDATE_NONFASTFORWARD} (for example due to a
     486             :    * force push).
     487             :    *
     488             :    * <p>Note that we don't need to explicitly allow non fast-forward updates for DELETE commands
     489             :    * since JGit forces the update implicitly in this case.
     490             :    */
     491             :   private boolean allowNonFastForwards(ChainedReceiveCommands receiveCommands) {
     492         109 :     return !draftUpdates.isEmpty()
     493         109 :         || !rewriters.isEmpty()
     494         109 :         || receiveCommands.getCommands().values().stream()
     495         109 :             .anyMatch(cmd -> cmd.getType().equals(ReceiveCommand.Type.UPDATE_NONFASTFORWARD));
     496             :   }
     497             : }

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