LCOV - code coverage report
Current view: top level - server/change - ConsistencyChecker.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 305 403 75.7 %
Date: 2022-11-19 15:00:39 Functions: 40 41 97.6 %

          Line data    Source code
       1             : // Copyright (C) 2014 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.change;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : import static com.google.common.collect.ImmutableList.toImmutableList;
      19             : import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
      20             : import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
      21             : import static java.util.Comparator.comparing;
      22             : import static java.util.Objects.requireNonNull;
      23             : 
      24             : import com.google.auto.value.AutoValue;
      25             : import com.google.common.collect.Collections2;
      26             : import com.google.common.collect.ImmutableList;
      27             : import com.google.common.collect.Iterables;
      28             : import com.google.common.collect.MultimapBuilder;
      29             : import com.google.common.collect.SetMultimap;
      30             : import com.google.common.flogger.FluentLogger;
      31             : import com.google.gerrit.common.Nullable;
      32             : import com.google.gerrit.entities.Change;
      33             : import com.google.gerrit.entities.PatchSet;
      34             : import com.google.gerrit.entities.Project;
      35             : import com.google.gerrit.entities.SubmissionId;
      36             : import com.google.gerrit.exceptions.StorageException;
      37             : import com.google.gerrit.extensions.api.changes.FixInput;
      38             : import com.google.gerrit.extensions.common.ProblemInfo;
      39             : import com.google.gerrit.extensions.common.ProblemInfo.Status;
      40             : import com.google.gerrit.extensions.registration.DynamicItem;
      41             : import com.google.gerrit.extensions.restapi.RestApiException;
      42             : import com.google.gerrit.server.ChangeUtil;
      43             : import com.google.gerrit.server.CurrentUser;
      44             : import com.google.gerrit.server.GerritPersonIdent;
      45             : import com.google.gerrit.server.PatchSetUtil;
      46             : import com.google.gerrit.server.account.Accounts;
      47             : import com.google.gerrit.server.config.UrlFormatter;
      48             : import com.google.gerrit.server.git.GitRepositoryManager;
      49             : import com.google.gerrit.server.notedb.ChangeNotes;
      50             : import com.google.gerrit.server.notedb.PatchSetState;
      51             : import com.google.gerrit.server.patch.PatchSetInfoFactory;
      52             : import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
      53             : import com.google.gerrit.server.plugincontext.PluginItemContext;
      54             : import com.google.gerrit.server.update.BatchUpdate;
      55             : import com.google.gerrit.server.update.BatchUpdateOp;
      56             : import com.google.gerrit.server.update.ChangeContext;
      57             : import com.google.gerrit.server.update.RepoContext;
      58             : import com.google.gerrit.server.update.RetryHelper;
      59             : import com.google.gerrit.server.update.UpdateException;
      60             : import com.google.gerrit.server.util.time.TimeUtil;
      61             : import com.google.inject.Inject;
      62             : import com.google.inject.Provider;
      63             : import java.io.IOException;
      64             : import java.util.ArrayList;
      65             : import java.util.Collection;
      66             : import java.util.Collections;
      67             : import java.util.HashSet;
      68             : import java.util.List;
      69             : import java.util.Locale;
      70             : import java.util.Map;
      71             : import java.util.Set;
      72             : import java.util.TreeSet;
      73             : import org.eclipse.jgit.errors.ConfigInvalidException;
      74             : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
      75             : import org.eclipse.jgit.errors.MissingObjectException;
      76             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      77             : import org.eclipse.jgit.lib.ObjectId;
      78             : import org.eclipse.jgit.lib.ObjectInserter;
      79             : import org.eclipse.jgit.lib.PersonIdent;
      80             : import org.eclipse.jgit.lib.Ref;
      81             : import org.eclipse.jgit.lib.RefUpdate;
      82             : import org.eclipse.jgit.lib.Repository;
      83             : import org.eclipse.jgit.revwalk.RevCommit;
      84             : import org.eclipse.jgit.revwalk.RevWalk;
      85             : 
      86             : /**
      87             :  * Checks changes for various kinds of inconsistency and corruption.
      88             :  *
      89             :  * <p>A single instance may be reused for checking multiple changes, but not concurrently.
      90             :  */
      91             : public class ConsistencyChecker {
      92           5 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      93             : 
      94             :   @AutoValue
      95           5 :   public abstract static class Result {
      96             :     private static Result create(ChangeNotes notes, List<ProblemInfo> problems) {
      97           5 :       return new AutoValue_ConsistencyChecker_Result(
      98           5 :           notes.getChangeId(), notes.getChange(), ImmutableList.copyOf(problems));
      99             :     }
     100             : 
     101             :     public abstract Change.Id id();
     102             : 
     103             :     @Nullable
     104             :     public abstract Change change();
     105             : 
     106             :     public abstract ImmutableList<ProblemInfo> problems();
     107             :   }
     108             : 
     109             :   private final ChangeNotes.Factory notesFactory;
     110             :   private final Accounts accounts;
     111             :   private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
     112             :   private final GitRepositoryManager repoManager;
     113             :   private final PatchSetInfoFactory patchSetInfoFactory;
     114             :   private final PatchSetInserter.Factory patchSetInserterFactory;
     115             :   private final PatchSetUtil psUtil;
     116             :   private final Provider<CurrentUser> user;
     117             :   private final Provider<PersonIdent> serverIdent;
     118             :   private final RetryHelper retryHelper;
     119             :   private final DynamicItem<UrlFormatter> urlFormatter;
     120             : 
     121             :   private BatchUpdate.Factory updateFactory;
     122             :   private FixInput fix;
     123             :   private ChangeNotes notes;
     124             :   private Repository repo;
     125             :   private RevWalk rw;
     126             :   private ObjectInserter oi;
     127             : 
     128             :   private RevCommit tip;
     129             :   private SetMultimap<ObjectId, PatchSet> patchSetsBySha;
     130             :   private PatchSet currPs;
     131             :   private RevCommit currPsCommit;
     132             : 
     133             :   private List<ProblemInfo> problems;
     134             : 
     135             :   @Inject
     136             :   ConsistencyChecker(
     137             :       @GerritPersonIdent Provider<PersonIdent> serverIdent,
     138             :       ChangeNotes.Factory notesFactory,
     139             :       Accounts accounts,
     140             :       PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
     141             :       GitRepositoryManager repoManager,
     142             :       PatchSetInfoFactory patchSetInfoFactory,
     143             :       PatchSetInserter.Factory patchSetInserterFactory,
     144             :       PatchSetUtil psUtil,
     145             :       Provider<CurrentUser> user,
     146             :       RetryHelper retryHelper,
     147           5 :       DynamicItem<UrlFormatter> urlFormatter) {
     148           5 :     this.accounts = accounts;
     149           5 :     this.accountPatchReviewStore = accountPatchReviewStore;
     150           5 :     this.notesFactory = notesFactory;
     151           5 :     this.patchSetInfoFactory = patchSetInfoFactory;
     152           5 :     this.patchSetInserterFactory = patchSetInserterFactory;
     153           5 :     this.psUtil = psUtil;
     154           5 :     this.repoManager = repoManager;
     155           5 :     this.retryHelper = retryHelper;
     156           5 :     this.serverIdent = serverIdent;
     157           5 :     this.user = user;
     158           5 :     this.urlFormatter = urlFormatter;
     159           5 :     reset();
     160           5 :   }
     161             : 
     162             :   private void reset() {
     163           5 :     updateFactory = null;
     164           5 :     notes = null;
     165           5 :     repo = null;
     166           5 :     rw = null;
     167           5 :     problems = new ArrayList<>();
     168           5 :   }
     169             : 
     170             :   private Change change() {
     171           5 :     return notes.getChange();
     172             :   }
     173             : 
     174             :   public Result check(ChangeNotes notes, @Nullable FixInput f) {
     175           5 :     requireNonNull(notes);
     176             :     try {
     177           5 :       return retryHelper
     178           5 :           .changeUpdate(
     179             :               "checkChangeConsistency",
     180             :               buf -> {
     181             :                 try {
     182           5 :                   reset();
     183           5 :                   this.updateFactory = buf;
     184           5 :                   this.notes = notes;
     185           5 :                   fix = f;
     186           5 :                   checkImpl();
     187           5 :                   return result();
     188             :                 } finally {
     189           5 :                   if (rw != null) {
     190           5 :                     rw.getObjectReader().close();
     191           5 :                     rw.close();
     192           5 :                     oi.close();
     193             :                   }
     194           5 :                   if (repo != null) {
     195           5 :                     repo.close();
     196             :                   }
     197             :                 }
     198             :               })
     199           5 :           .call();
     200           0 :     } catch (RestApiException e) {
     201           0 :       return logAndReturnOneProblem(e, notes, "Error checking change: " + e.getMessage());
     202           0 :     } catch (UpdateException e) {
     203           0 :       return logAndReturnOneProblem(e, notes, "Error checking change");
     204             :     }
     205             :   }
     206             : 
     207             :   private Result logAndReturnOneProblem(Exception e, ChangeNotes notes, String problem) {
     208           0 :     logger.atWarning().withCause(e).log("Error checking change %s", notes.getChangeId());
     209           0 :     return Result.create(notes, ImmutableList.of(problem(problem)));
     210             :   }
     211             : 
     212             :   private void checkImpl() {
     213           5 :     checkOwner();
     214           5 :     checkCurrentPatchSetEntity();
     215             : 
     216             :     // All checks that require the repo.
     217           5 :     if (!openRepo()) {
     218           0 :       return;
     219             :     }
     220           5 :     if (!checkPatchSets()) {
     221           1 :       return;
     222             :     }
     223           5 :     checkMerged();
     224           5 :   }
     225             : 
     226             :   private void checkOwner() {
     227             :     try {
     228           5 :       if (!accounts.get(change().getOwner()).isPresent()) {
     229           1 :         problem("Missing change owner: " + change().getOwner());
     230             :       }
     231           0 :     } catch (IOException | ConfigInvalidException e) {
     232           0 :       ProblemInfo problem = problem("Failed to look up owner");
     233           0 :       logger.atWarning().withCause(e).log(
     234           0 :           "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
     235           5 :     }
     236           5 :   }
     237             : 
     238             :   private void checkCurrentPatchSetEntity() {
     239             :     try {
     240           5 :       currPs = psUtil.current(notes);
     241           5 :       if (currPs == null) {
     242           0 :         problem(
     243           0 :             String.format("Current patch set %d not found", change().currentPatchSetId().get()));
     244             :       }
     245           0 :     } catch (StorageException e) {
     246           0 :       ProblemInfo problem = problem("Failed to look up current patch set");
     247           0 :       logger.atWarning().withCause(e).log(
     248           0 :           "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
     249           5 :     }
     250           5 :   }
     251             : 
     252             :   private boolean openRepo() {
     253           5 :     Project.NameKey project = change().getDest().project();
     254             :     try {
     255           5 :       repo = repoManager.openRepository(project);
     256           5 :       oi = repo.newObjectInserter();
     257           5 :       rw = new RevWalk(oi.newReader());
     258           5 :       return true;
     259           0 :     } catch (RepositoryNotFoundException e) {
     260           0 :       ProblemInfo problem = problem("Destination repository not found: " + project);
     261           0 :       logger.atWarning().withCause(e).log(
     262           0 :           "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
     263           0 :       return false;
     264           0 :     } catch (IOException e) {
     265           0 :       ProblemInfo problem = problem("Failed to open repository: " + project);
     266           0 :       logger.atWarning().withCause(e).log(
     267           0 :           "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
     268           0 :       return false;
     269             :     }
     270             :   }
     271             : 
     272             :   private boolean checkPatchSets() {
     273             :     List<PatchSet> all;
     274             :     try {
     275             :       // Iterate in descending order.
     276           5 :       all = PS_ID_ORDER.sortedCopy(psUtil.byChange(notes));
     277           0 :     } catch (StorageException e) {
     278           0 :       ProblemInfo problem = problem("Failed to look up patch sets");
     279           0 :       logger.atWarning().withCause(e).log(
     280           0 :           "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
     281           0 :       return false;
     282           5 :     }
     283           5 :     patchSetsBySha = MultimapBuilder.hashKeys(all.size()).treeSetValues(PS_ID_ORDER).build();
     284             : 
     285             :     Map<String, Ref> refs;
     286             :     try {
     287           5 :       refs =
     288           5 :           repo.getRefDatabase()
     289           5 :               .exactRef(all.stream().map(ps -> ps.id().toRefName()).toArray(String[]::new));
     290           0 :     } catch (IOException e) {
     291           0 :       ProblemInfo problem = problem("Error reading refs");
     292           0 :       logger.atWarning().withCause(e).log(
     293           0 :           "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
     294           0 :       refs = Collections.emptyMap();
     295           5 :     }
     296             : 
     297           5 :     List<DeletePatchSetFromDbOp> deletePatchSetOps = new ArrayList<>();
     298           5 :     for (PatchSet ps : all) {
     299             :       // Check revision format.
     300           5 :       int psNum = ps.id().get();
     301           5 :       String refName = ps.id().toRefName();
     302           5 :       ObjectId objId = ps.commitId();
     303           5 :       patchSetsBySha.put(objId, ps);
     304             : 
     305             :       // Check ref existence.
     306           5 :       ProblemInfo refProblem = null;
     307           5 :       Ref ref = refs.get(refName);
     308           5 :       if (ref == null) {
     309           1 :         refProblem = problem("Ref missing: " + refName);
     310           5 :       } else if (!objId.equals(ref.getObjectId())) {
     311           0 :         String actual = ref.getObjectId() != null ? ref.getObjectId().name() : "null";
     312           0 :         refProblem =
     313           0 :             problem(
     314           0 :                 String.format(
     315           0 :                     "Expected %s to point to %s, found %s", ref.getName(), objId.name(), actual));
     316             :       }
     317             : 
     318             :       // Check object existence.
     319           5 :       RevCommit psCommit = parseCommit(objId, String.format("patch set %d", psNum));
     320           5 :       if (psCommit == null) {
     321           1 :         if (fix != null && fix.deletePatchSetIfCommitMissing) {
     322           1 :           deletePatchSetOps.add(new DeletePatchSetFromDbOp(lastProblem(), ps.id()));
     323             :         }
     324             :         continue;
     325           5 :       } else if (refProblem != null && fix != null) {
     326           1 :         fixPatchSetRef(refProblem, ps);
     327             :       }
     328           5 :       if (ps.id().equals(change().currentPatchSetId())) {
     329           5 :         currPsCommit = psCommit;
     330             :       }
     331           5 :     }
     332             : 
     333             :     // Delete any bad patch sets found above, in a single update.
     334           5 :     deletePatchSets(deletePatchSetOps);
     335             : 
     336             :     // Check for duplicates.
     337           5 :     for (Map.Entry<ObjectId, Collection<PatchSet>> e : patchSetsBySha.asMap().entrySet()) {
     338           5 :       if (e.getValue().size() > 1) {
     339           1 :         problem(
     340           1 :             String.format(
     341             :                 "Multiple patch sets pointing to %s: %s",
     342           1 :                 e.getKey().name(), Collections2.transform(e.getValue(), PatchSet::number)));
     343             :       }
     344           5 :     }
     345             : 
     346           5 :     return currPs != null && currPsCommit != null;
     347             :   }
     348             : 
     349             :   private void checkMerged() {
     350           5 :     String refName = change().getDest().branch();
     351             :     Ref dest;
     352             :     try {
     353           5 :       dest = repo.getRefDatabase().exactRef(refName);
     354           0 :     } catch (IOException e) {
     355           0 :       problem("Failed to look up destination ref: " + refName);
     356           0 :       return;
     357           5 :     }
     358           5 :     if (dest == null) {
     359           1 :       problem("Destination ref not found (may be new branch): " + refName);
     360           1 :       return;
     361             :     }
     362           5 :     tip = parseCommit(dest.getObjectId(), "destination ref " + refName);
     363           5 :     if (tip == null) {
     364           0 :       return;
     365             :     }
     366             : 
     367           5 :     if (fix != null && fix.expectMergedAs != null) {
     368           2 :       checkExpectMergedAs();
     369             :     } else {
     370             :       boolean merged;
     371             :       try {
     372           5 :         merged = rw.isMergedInto(currPsCommit, tip);
     373           0 :       } catch (IOException e) {
     374           0 :         problem("Error checking whether patch set " + currPs.id().get() + " is merged");
     375           0 :         return;
     376           5 :       }
     377           5 :       checkMergedBitMatchesStatus(currPs.id(), currPsCommit, merged);
     378             :     }
     379           5 :   }
     380             : 
     381             :   private ProblemInfo wrongChangeStatus(PatchSet.Id psId, RevCommit commit) {
     382           2 :     String refName = change().getDest().branch();
     383           2 :     return problem(
     384           2 :         formatProblemMessage(
     385             :             "Patch set %d (%s) is merged into destination ref %s (%s), but change"
     386             :                 + " status is %s",
     387           2 :             psId.get(), commit.name(), refName, tip.name()));
     388             :   }
     389             : 
     390             :   private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit, boolean merged) {
     391           5 :     String refName = change().getDest().branch();
     392           5 :     if (merged && !change().isMerged()) {
     393           2 :       ProblemInfo p = wrongChangeStatus(psId, commit);
     394           2 :       if (fix != null) {
     395           1 :         fixMerged(p);
     396             :       }
     397           5 :     } else if (!merged && change().isMerged()) {
     398           1 :       problem(
     399           1 :           formatProblemMessage(
     400             :               "Patch set %d (%s) is not merged into"
     401             :                   + " destination ref %s (%s), but change status is %s",
     402           1 :               currPs.id().get(), commit.name(), refName, tip.name()));
     403             :     }
     404           5 :   }
     405             : 
     406             :   private String formatProblemMessage(
     407             :       String message, int psId, String commitName, String refName, String tipName) {
     408           2 :     return String.format(
     409             :         message,
     410           2 :         psId,
     411             :         commitName,
     412             :         refName,
     413             :         tipName,
     414           2 :         ChangeUtil.status(change()).toUpperCase(Locale.US));
     415             :   }
     416             : 
     417             :   private void checkExpectMergedAs() {
     418           2 :     if (!ObjectId.isId(fix.expectMergedAs)) {
     419           0 :       problem("Invalid revision on expected merged commit: " + fix.expectMergedAs);
     420           0 :       return;
     421             :     }
     422           2 :     ObjectId objId = ObjectId.fromString(fix.expectMergedAs);
     423           2 :     RevCommit commit = parseCommit(objId, "expected merged commit");
     424           2 :     if (commit == null) {
     425           0 :       return;
     426             :     }
     427             : 
     428             :     try {
     429           2 :       if (!rw.isMergedInto(commit, tip)) {
     430           1 :         problem(
     431           1 :             String.format(
     432             :                 "Expected merged commit %s is not merged into destination ref %s (%s)",
     433           1 :                 commit.name(), change().getDest().branch(), tip.name()));
     434           1 :         return;
     435             :       }
     436             : 
     437           2 :       List<PatchSet.Id> thisCommitPsIds = new ArrayList<>();
     438           2 :       for (Ref ref : repo.getRefDatabase().getRefsByPrefix(REFS_CHANGES)) {
     439           2 :         if (!ref.getObjectId().equals(commit)) {
     440           2 :           continue;
     441             :         }
     442           2 :         PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
     443           2 :         if (psId == null) {
     444           0 :           continue;
     445             :         }
     446             :         try {
     447           2 :           Change c = notesFactory.createChecked(change().getProject(), psId.changeId()).getChange();
     448           2 :           if (!c.getDest().equals(change().getDest())) {
     449           0 :             continue;
     450             :           }
     451           0 :         } catch (StorageException e) {
     452           0 :           logger.atWarning().withCause(e).log(
     453           0 :               "Error in consistency check of change %s", notes.getChangeId());
     454             :           // Include this patch set; should cause an error below, which is good.
     455           2 :         }
     456           2 :         thisCommitPsIds.add(psId);
     457           2 :       }
     458           2 :       switch (thisCommitPsIds.size()) {
     459             :         case 0:
     460             :           // No patch set for this commit; insert one.
     461           2 :           rw.parseBody(commit);
     462           2 :           String changeId =
     463           2 :               Iterables.getFirst(
     464           2 :                   ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get()), null);
     465             :           // Missing Change-Id footer is ok, but mismatched is not.
     466           2 :           if (changeId != null && !changeId.equals(change().getKey().get())) {
     467           1 :             problem(
     468           1 :                 String.format(
     469             :                     "Expected merged commit %s has Change-Id: %s, but expected %s",
     470           1 :                     commit.name(), changeId, change().getKey().get()));
     471           1 :             return;
     472             :           }
     473           2 :           insertMergedPatchSet(commit, null, false);
     474           2 :           break;
     475             : 
     476             :         case 1:
     477             :           // Existing patch set ref pointing to this commit.
     478           2 :           PatchSet.Id id = thisCommitPsIds.get(0);
     479           2 :           if (id.equals(change().currentPatchSetId())) {
     480             :             // If it's the current patch set, we can just fix the status.
     481           2 :             fixMerged(wrongChangeStatus(id, commit));
     482           1 :           } else if (id.get() > change().currentPatchSetId().get()) {
     483             :             // If it's newer than the current patch set, reuse this patch set
     484             :             // ID when inserting a new merged patch set.
     485           1 :             insertMergedPatchSet(commit, id, true);
     486             :           } else {
     487             :             // If it's older than the current patch set, just delete the old
     488             :             // ref, and use a new ID when inserting a new merged patch set.
     489           1 :             insertMergedPatchSet(commit, id, false);
     490             :           }
     491           1 :           break;
     492             : 
     493             :         default:
     494           1 :           problem(
     495           1 :               String.format(
     496             :                   "Multiple patch sets for expected merged commit %s: %s",
     497           1 :                   commit.name(),
     498           1 :                   thisCommitPsIds.stream()
     499           1 :                       .sorted(comparing(PatchSet.Id::get))
     500           1 :                       .collect(toImmutableList())));
     501             :           break;
     502             :       }
     503           0 :     } catch (IOException e) {
     504           0 :       ProblemInfo problem =
     505           0 :           problem("Error looking up expected merged commit " + fix.expectMergedAs);
     506           0 :       logger.atWarning().withCause(e).log(
     507           0 :           "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
     508           2 :     }
     509           2 :   }
     510             : 
     511             :   private void insertMergedPatchSet(
     512             :       final RevCommit commit, @Nullable PatchSet.Id psIdToDelete, boolean reuseOldPsId) {
     513           2 :     ProblemInfo notFound = problem("No patch set found for merged commit " + commit.name());
     514           2 :     if (!user.get().isIdentifiedUser()) {
     515           0 :       notFound.status = Status.FIX_FAILED;
     516           0 :       notFound.outcome = "Must be called by an identified user to insert new patch set";
     517           0 :       return;
     518             :     }
     519             :     ProblemInfo insertPatchSetProblem;
     520             :     ProblemInfo deleteOldPatchSetProblem;
     521             : 
     522           2 :     if (psIdToDelete == null) {
     523           2 :       insertPatchSetProblem =
     524           2 :           problem(
     525           2 :               String.format(
     526           2 :                   "Expected merged commit %s has no associated patch set", commit.name()));
     527           2 :       deleteOldPatchSetProblem = null;
     528             :     } else {
     529           1 :       String msg =
     530           1 :           String.format(
     531             :               "Expected merge commit %s corresponds to patch set %s,"
     532             :                   + " not the current patch set %s",
     533           1 :               commit.name(), psIdToDelete.get(), change().currentPatchSetId().get());
     534             :       // Maybe an identical problem, but different fix.
     535           1 :       deleteOldPatchSetProblem = reuseOldPsId ? null : problem(msg);
     536           1 :       insertPatchSetProblem = problem(msg);
     537             :     }
     538             : 
     539           2 :     List<ProblemInfo> currProblems = new ArrayList<>(3);
     540           2 :     currProblems.add(notFound);
     541           2 :     if (deleteOldPatchSetProblem != null) {
     542           1 :       currProblems.add(deleteOldPatchSetProblem);
     543             :     }
     544           2 :     currProblems.add(insertPatchSetProblem);
     545             : 
     546             :     try {
     547             :       PatchSet.Id psId =
     548           2 :           (psIdToDelete != null && reuseOldPsId)
     549           1 :               ? psIdToDelete
     550           2 :               : ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId());
     551           2 :       PatchSetInserter inserter = patchSetInserterFactory.create(notes, psId, commit);
     552           2 :       try (BatchUpdate bu = newBatchUpdate()) {
     553           2 :         bu.setRepository(repo, rw, oi);
     554             : 
     555           2 :         if (psIdToDelete != null) {
     556             :           // Delete the given patch set ref. If reuseOldPsId is true,
     557             :           // PatchSetInserter will reinsert the same ref, making it a no-op.
     558           1 :           bu.addOp(
     559           1 :               notes.getChangeId(),
     560           1 :               new BatchUpdateOp() {
     561             :                 @Override
     562             :                 public void updateRepo(RepoContext ctx) throws IOException {
     563           1 :                   ctx.addRefUpdate(commit, ObjectId.zeroId(), psIdToDelete.toRefName());
     564           1 :                 }
     565             :               });
     566           1 :           if (!reuseOldPsId) {
     567           1 :             bu.addOp(
     568           1 :                 notes.getChangeId(),
     569           1 :                 new DeletePatchSetFromDbOp(requireNonNull(deleteOldPatchSetProblem), psIdToDelete));
     570             :           }
     571             :         }
     572             : 
     573           2 :         bu.setNotify(NotifyResolver.Result.none());
     574           2 :         bu.addOp(
     575           2 :             notes.getChangeId(),
     576             :             inserter
     577           2 :                 .setValidate(false)
     578           2 :                 .setFireRevisionCreated(false)
     579           2 :                 .setAllowClosed(true)
     580           2 :                 .setMessage("Patch set for merged commit inserted by consistency checker"));
     581           2 :         bu.addOp(notes.getChangeId(), new FixMergedOp(notFound));
     582           2 :         bu.execute();
     583             :       }
     584           2 :       notes = notesFactory.createChecked(inserter.getChange());
     585           2 :       insertPatchSetProblem.status = Status.FIXED;
     586           2 :       insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get();
     587           0 :     } catch (StorageException | IOException | UpdateException | RestApiException e) {
     588           0 :       logger.atWarning().withCause(e).log(
     589           0 :           "Error in consistency check of change %s", notes.getChangeId());
     590           0 :       for (ProblemInfo pi : currProblems) {
     591           0 :         pi.status = Status.FIX_FAILED;
     592           0 :         pi.outcome = "Error inserting merged patch set";
     593           0 :       }
     594           0 :       return;
     595           2 :     }
     596           2 :   }
     597             : 
     598             :   private static class FixMergedOp implements BatchUpdateOp {
     599             :     private final ProblemInfo p;
     600             : 
     601           2 :     private FixMergedOp(ProblemInfo p) {
     602           2 :       this.p = p;
     603           2 :     }
     604             : 
     605             :     @Override
     606             :     public boolean updateChange(ChangeContext ctx) {
     607           2 :       ctx.getChange().setStatus(Change.Status.MERGED);
     608           2 :       ctx.getUpdate(ctx.getChange().currentPatchSetId())
     609           2 :           .fixStatusToMerged(new SubmissionId(ctx.getChange()));
     610           2 :       p.status = Status.FIXED;
     611           2 :       p.outcome = "Marked change as merged";
     612           2 :       return true;
     613             :     }
     614             :   }
     615             : 
     616             :   private void fixMerged(ProblemInfo p) {
     617           2 :     try (BatchUpdate bu = newBatchUpdate()) {
     618           2 :       bu.setRepository(repo, rw, oi);
     619           2 :       bu.addOp(notes.getChangeId(), new FixMergedOp(p));
     620           2 :       bu.execute();
     621           0 :     } catch (UpdateException | RestApiException e) {
     622           0 :       logger.atWarning().withCause(e).log("Error marking %s as merged", notes.getChangeId());
     623           0 :       p.status = Status.FIX_FAILED;
     624           0 :       p.outcome = "Error updating status to merged";
     625           2 :     }
     626           2 :   }
     627             : 
     628             :   private BatchUpdate newBatchUpdate() {
     629           5 :     return updateFactory.create(change().getProject(), user.get(), TimeUtil.now());
     630             :   }
     631             : 
     632             :   private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
     633             :     try {
     634           1 :       RefUpdate ru = repo.updateRef(ps.id().toRefName());
     635           1 :       ru.setForceUpdate(true);
     636           1 :       ru.setNewObjectId(ps.commitId());
     637           1 :       ru.setRefLogIdent(newRefLogIdent());
     638           1 :       ru.setRefLogMessage("Repair patch set ref", true);
     639           1 :       RefUpdate.Result result = ru.update();
     640           1 :       switch (result) {
     641             :         case NEW:
     642             :         case FORCED:
     643             :         case FAST_FORWARD:
     644             :         case NO_CHANGE:
     645           1 :           p.status = Status.FIXED;
     646           1 :           p.outcome = "Repaired patch set ref";
     647           1 :           return;
     648             :         case IO_FAILURE:
     649             :         case LOCK_FAILURE:
     650             :         case NOT_ATTEMPTED:
     651             :         case REJECTED:
     652             :         case REJECTED_CURRENT_BRANCH:
     653             :         case RENAMED:
     654             :         case REJECTED_MISSING_OBJECT:
     655             :         case REJECTED_OTHER_REASON:
     656             :         default:
     657           0 :           p.status = Status.FIX_FAILED;
     658           0 :           p.outcome = "Failed to update patch set ref: " + result;
     659           0 :           return;
     660             :       }
     661           0 :     } catch (IOException e) {
     662           0 :       String msg = "Error fixing patch set ref";
     663           0 :       logger.atWarning().withCause(e).log("%s %s", msg, ps.id().toRefName());
     664           0 :       p.status = Status.FIX_FAILED;
     665           0 :       p.outcome = msg;
     666             :     }
     667           0 :   }
     668             : 
     669             :   private void deletePatchSets(List<DeletePatchSetFromDbOp> ops) {
     670           5 :     try (BatchUpdate bu = newBatchUpdate()) {
     671           5 :       bu.setRepository(repo, rw, oi);
     672           5 :       for (DeletePatchSetFromDbOp op : ops) {
     673           1 :         checkArgument(op.psId.changeId().equals(notes.getChangeId()));
     674           1 :         bu.addOp(notes.getChangeId(), op);
     675           1 :       }
     676           5 :       bu.addOp(notes.getChangeId(), new UpdateCurrentPatchSetOp(ops));
     677           5 :       bu.execute();
     678           1 :     } catch (NoPatchSetsWouldRemainException e) {
     679           1 :       for (DeletePatchSetFromDbOp op : ops) {
     680           1 :         op.p.status = Status.FIX_FAILED;
     681           1 :         op.p.outcome = e.getMessage();
     682           1 :       }
     683           0 :     } catch (UpdateException | RestApiException e) {
     684           0 :       String msg = "Error deleting patch set";
     685           0 :       logger.atWarning().withCause(e).log("%s of change %s", msg, ops.get(0).psId.changeId());
     686           0 :       for (DeletePatchSetFromDbOp op : ops) {
     687             :         // Overwrite existing statuses that were set before the transaction was
     688             :         // rolled back.
     689           0 :         op.p.status = Status.FIX_FAILED;
     690           0 :         op.p.outcome = msg;
     691           0 :       }
     692           5 :     }
     693           5 :   }
     694             : 
     695             :   private class DeletePatchSetFromDbOp implements BatchUpdateOp {
     696             :     private final ProblemInfo p;
     697             :     private final PatchSet.Id psId;
     698             : 
     699           1 :     private DeletePatchSetFromDbOp(ProblemInfo p, PatchSet.Id psId) {
     700           1 :       this.p = p;
     701           1 :       this.psId = psId;
     702           1 :     }
     703             : 
     704             :     @Override
     705             :     public boolean updateChange(ChangeContext ctx) throws PatchSetInfoNotAvailableException {
     706             :       // Delete dangling key references.
     707           1 :       accountPatchReviewStore.run(s -> s.clearReviewed(psId));
     708             : 
     709             :       // For NoteDb setting the state to deleted is sufficient to filter everything out.
     710           1 :       ctx.getUpdate(psId).setPatchSetState(PatchSetState.DELETED);
     711             : 
     712           1 :       p.status = Status.FIXED;
     713           1 :       p.outcome = "Deleted patch set";
     714           1 :       return true;
     715             :     }
     716             :   }
     717             : 
     718             :   private static class NoPatchSetsWouldRemainException extends RestApiException {
     719             :     private static final long serialVersionUID = 1L;
     720             : 
     721             :     private NoPatchSetsWouldRemainException() {
     722           1 :       super("Cannot delete patch set; no patch sets would remain");
     723           1 :     }
     724             :   }
     725             : 
     726             :   private class UpdateCurrentPatchSetOp implements BatchUpdateOp {
     727             :     private final Set<PatchSet.Id> toDelete;
     728             : 
     729           5 :     private UpdateCurrentPatchSetOp(List<DeletePatchSetFromDbOp> deleteOps) {
     730           5 :       toDelete = new HashSet<>();
     731           5 :       for (DeletePatchSetFromDbOp op : deleteOps) {
     732           1 :         toDelete.add(op.psId);
     733           1 :       }
     734           5 :     }
     735             : 
     736             :     @Override
     737             :     public boolean updateChange(ChangeContext ctx)
     738             :         throws PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException {
     739           5 :       if (!toDelete.contains(ctx.getChange().currentPatchSetId())) {
     740           5 :         return false;
     741             :       }
     742           1 :       TreeSet<PatchSet.Id> all = new TreeSet<>(comparing(PatchSet.Id::get));
     743             :       // Doesn't make any assumptions about the order in which deletes happen
     744             :       // and whether they are seen by this op; we are already given the full set
     745             :       // of patch sets that will eventually be deleted in this update.
     746           1 :       for (PatchSet ps : psUtil.byChange(ctx.getNotes())) {
     747           1 :         if (!toDelete.contains(ps.id())) {
     748           1 :           all.add(ps.id());
     749             :         }
     750           1 :       }
     751           1 :       if (all.isEmpty()) {
     752           1 :         throw new NoPatchSetsWouldRemainException();
     753             :       }
     754           1 :       ctx.getChange().setCurrentPatchSet(patchSetInfoFactory.get(ctx.getNotes(), all.last()));
     755           1 :       return true;
     756             :     }
     757             :   }
     758             : 
     759             :   private PersonIdent newRefLogIdent() {
     760           1 :     CurrentUser u = user.get();
     761           1 :     if (u.isIdentifiedUser()) {
     762           1 :       return u.asIdentifiedUser().newRefLogIdent();
     763             :     }
     764           0 :     return serverIdent.get();
     765             :   }
     766             : 
     767             :   @Nullable
     768             :   private RevCommit parseCommit(ObjectId objId, String desc) {
     769             :     try {
     770           5 :       return rw.parseCommit(objId);
     771           1 :     } catch (MissingObjectException e) {
     772           1 :       problem(String.format("Object missing: %s: %s", desc, objId.name()));
     773           0 :     } catch (IncorrectObjectTypeException e) {
     774           0 :       problem(String.format("Not a commit: %s: %s", desc, objId.name()));
     775           0 :     } catch (IOException e) {
     776           0 :       problem(String.format("Failed to look up: %s: %s", desc, objId.name()));
     777           1 :     }
     778           1 :     return null;
     779             :   }
     780             : 
     781             :   private ProblemInfo problem(String msg) {
     782           2 :     ProblemInfo p = new ProblemInfo();
     783           2 :     p.message = requireNonNull(msg);
     784           2 :     problems.add(p);
     785           2 :     return p;
     786             :   }
     787             : 
     788             :   private ProblemInfo lastProblem() {
     789           1 :     return problems.get(problems.size() - 1);
     790             :   }
     791             : 
     792             :   private Result result() {
     793           5 :     return Result.create(notes, problems);
     794             :   }
     795             : }

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