diff options
Diffstat (limited to 'java/com/google/gerrit/server/change/ConsistencyChecker.java')
-rw-r--r-- | java/com/google/gerrit/server/change/ConsistencyChecker.java | 786 |
1 files changed, 786 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java new file mode 100644 index 0000000000..128297f16c --- /dev/null +++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java @@ -0,0 +1,786 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES; +import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering; +import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER; +import static java.util.Objects.requireNonNull; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.SetMultimap; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.FooterConstants; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.changes.FixInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.common.ProblemInfo; +import com.google.gerrit.extensions.common.ProblemInfo.Status; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.account.Accounts; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.PatchSetState; +import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.plugincontext.PluginItemContext; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.BatchUpdateReviewDb; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RepoContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.UpdateException; +import com.google.gerrit.server.util.time.TimeUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Checks changes for various kinds of inconsistency and corruption. + * + * <p>A single instance may be reused for checking multiple changes, but not concurrently. + */ +public class ConsistencyChecker { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + @AutoValue + public abstract static class Result { + private static Result create(ChangeNotes notes, List<ProblemInfo> problems) { + return new AutoValue_ConsistencyChecker_Result( + notes.getChangeId(), notes.getChange(), problems); + } + + public abstract Change.Id id(); + + @Nullable + public abstract Change change(); + + public abstract List<ProblemInfo> problems(); + } + + private final ChangeNotes.Factory notesFactory; + private final Accounts accounts; + private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore; + private final GitRepositoryManager repoManager; + private final PatchSetInfoFactory patchSetInfoFactory; + private final PatchSetInserter.Factory patchSetInserterFactory; + private final PatchSetUtil psUtil; + private final Provider<CurrentUser> user; + private final Provider<PersonIdent> serverIdent; + private final Provider<ReviewDb> db; + private final RetryHelper retryHelper; + + private BatchUpdate.Factory updateFactory; + private FixInput fix; + private ChangeNotes notes; + private Repository repo; + private RevWalk rw; + private ObjectInserter oi; + + private RevCommit tip; + private SetMultimap<ObjectId, PatchSet> patchSetsBySha; + private PatchSet currPs; + private RevCommit currPsCommit; + + private List<ProblemInfo> problems; + + @Inject + ConsistencyChecker( + @GerritPersonIdent Provider<PersonIdent> serverIdent, + ChangeNotes.Factory notesFactory, + Accounts accounts, + PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore, + GitRepositoryManager repoManager, + PatchSetInfoFactory patchSetInfoFactory, + PatchSetInserter.Factory patchSetInserterFactory, + PatchSetUtil psUtil, + Provider<CurrentUser> user, + Provider<ReviewDb> db, + RetryHelper retryHelper) { + this.accounts = accounts; + this.accountPatchReviewStore = accountPatchReviewStore; + this.db = db; + this.notesFactory = notesFactory; + this.patchSetInfoFactory = patchSetInfoFactory; + this.patchSetInserterFactory = patchSetInserterFactory; + this.psUtil = psUtil; + this.repoManager = repoManager; + this.retryHelper = retryHelper; + this.serverIdent = serverIdent; + this.user = user; + reset(); + } + + private void reset() { + updateFactory = null; + notes = null; + repo = null; + rw = null; + problems = new ArrayList<>(); + } + + private Change change() { + return notes.getChange(); + } + + public Result check(ChangeNotes notes, @Nullable FixInput f) { + requireNonNull(notes); + try { + return retryHelper.execute( + buf -> { + try { + reset(); + this.updateFactory = buf; + this.notes = notes; + fix = f; + checkImpl(); + return result(); + } finally { + if (rw != null) { + rw.getObjectReader().close(); + rw.close(); + oi.close(); + } + if (repo != null) { + repo.close(); + } + } + }); + } catch (RestApiException e) { + return logAndReturnOneProblem(e, notes, "Error checking change: " + e.getMessage()); + } catch (UpdateException e) { + return logAndReturnOneProblem(e, notes, "Error checking change"); + } + } + + private Result logAndReturnOneProblem(Exception e, ChangeNotes notes, String problem) { + logger.atWarning().withCause(e).log("Error checking change %s", notes.getChangeId()); + return Result.create(notes, ImmutableList.of(problem(problem))); + } + + private void checkImpl() { + checkOwner(); + checkCurrentPatchSetEntity(); + + // All checks that require the repo. + if (!openRepo()) { + return; + } + if (!checkPatchSets()) { + return; + } + checkMerged(); + } + + private void checkOwner() { + try { + if (!accounts.get(change().getOwner()).isPresent()) { + problem("Missing change owner: " + change().getOwner()); + } + } catch (IOException | ConfigInvalidException e) { + error("Failed to look up owner", e); + } + } + + private void checkCurrentPatchSetEntity() { + try { + currPs = psUtil.current(db.get(), notes); + if (currPs == null) { + problem( + String.format("Current patch set %d not found", change().currentPatchSetId().get())); + } + } catch (OrmException e) { + error("Failed to look up current patch set", e); + } + } + + private boolean openRepo() { + Project.NameKey project = change().getDest().getParentKey(); + try { + repo = repoManager.openRepository(project); + oi = repo.newObjectInserter(); + rw = new RevWalk(oi.newReader()); + return true; + } catch (RepositoryNotFoundException e) { + return error("Destination repository not found: " + project, e); + } catch (IOException e) { + return error("Failed to open repository: " + project, e); + } + } + + private boolean checkPatchSets() { + List<PatchSet> all; + try { + // Iterate in descending order. + all = PS_ID_ORDER.sortedCopy(psUtil.byChange(db.get(), notes)); + } catch (OrmException e) { + return error("Failed to look up patch sets", e); + } + patchSetsBySha = MultimapBuilder.hashKeys(all.size()).treeSetValues(PS_ID_ORDER).build(); + + Map<String, Ref> refs; + try { + refs = + repo.getRefDatabase() + .exactRef(all.stream().map(ps -> ps.getId().toRefName()).toArray(String[]::new)); + } catch (IOException e) { + error("error reading refs", e); + refs = Collections.emptyMap(); + } + + List<DeletePatchSetFromDbOp> deletePatchSetOps = new ArrayList<>(); + for (PatchSet ps : all) { + // Check revision format. + int psNum = ps.getId().get(); + String refName = ps.getId().toRefName(); + ObjectId objId = parseObjectId(ps.getRevision().get(), "patch set " + psNum); + if (objId == null) { + continue; + } + patchSetsBySha.put(objId, ps); + + // Check ref existence. + ProblemInfo refProblem = null; + Ref ref = refs.get(refName); + if (ref == null) { + refProblem = problem("Ref missing: " + refName); + } else if (!objId.equals(ref.getObjectId())) { + String actual = ref.getObjectId() != null ? ref.getObjectId().name() : "null"; + refProblem = + problem( + String.format( + "Expected %s to point to %s, found %s", ref.getName(), objId.name(), actual)); + } + + // Check object existence. + RevCommit psCommit = parseCommit(objId, String.format("patch set %d", psNum)); + if (psCommit == null) { + if (fix != null && fix.deletePatchSetIfCommitMissing) { + deletePatchSetOps.add(new DeletePatchSetFromDbOp(lastProblem(), ps.getId())); + } + continue; + } else if (refProblem != null && fix != null) { + fixPatchSetRef(refProblem, ps); + } + if (ps.getId().equals(change().currentPatchSetId())) { + currPsCommit = psCommit; + } + } + + // Delete any bad patch sets found above, in a single update. + deletePatchSets(deletePatchSetOps); + + // Check for duplicates. + for (Map.Entry<ObjectId, Collection<PatchSet>> e : patchSetsBySha.asMap().entrySet()) { + if (e.getValue().size() > 1) { + problem( + String.format( + "Multiple patch sets pointing to %s: %s", + e.getKey().name(), Collections2.transform(e.getValue(), PatchSet::getPatchSetId))); + } + } + + return currPs != null && currPsCommit != null; + } + + private void checkMerged() { + String refName = change().getDest().get(); + Ref dest; + try { + dest = repo.getRefDatabase().exactRef(refName); + } catch (IOException e) { + problem("Failed to look up destination ref: " + refName); + return; + } + if (dest == null) { + problem("Destination ref not found (may be new branch): " + refName); + return; + } + tip = parseCommit(dest.getObjectId(), "destination ref " + refName); + if (tip == null) { + return; + } + + if (fix != null && fix.expectMergedAs != null) { + checkExpectMergedAs(); + } else { + boolean merged; + try { + merged = rw.isMergedInto(currPsCommit, tip); + } catch (IOException e) { + problem("Error checking whether patch set " + currPs.getId().get() + " is merged"); + return; + } + checkMergedBitMatchesStatus(currPs.getId(), currPsCommit, merged); + } + } + + private ProblemInfo wrongChangeStatus(PatchSet.Id psId, RevCommit commit) { + String refName = change().getDest().get(); + return problem( + String.format( + "Patch set %d (%s) is merged into destination ref %s (%s), but change" + + " status is %s", + psId.get(), commit.name(), refName, tip.name(), change().getStatus())); + } + + private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit, boolean merged) { + String refName = change().getDest().get(); + if (merged && change().getStatus() != Change.Status.MERGED) { + ProblemInfo p = wrongChangeStatus(psId, commit); + if (fix != null) { + fixMerged(p); + } + } else if (!merged && change().getStatus() == Change.Status.MERGED) { + problem( + String.format( + "Patch set %d (%s) is not merged into" + + " destination ref %s (%s), but change status is %s", + currPs.getId().get(), commit.name(), refName, tip.name(), change().getStatus())); + } + } + + private void checkExpectMergedAs() { + ObjectId objId = parseObjectId(fix.expectMergedAs, "expected merged commit"); + RevCommit commit = parseCommit(objId, "expected merged commit"); + if (commit == null) { + return; + } + + try { + if (!rw.isMergedInto(commit, tip)) { + problem( + String.format( + "Expected merged commit %s is not merged into destination ref %s (%s)", + commit.name(), change().getDest().get(), tip.name())); + return; + } + + List<PatchSet.Id> thisCommitPsIds = new ArrayList<>(); + for (Ref ref : repo.getRefDatabase().getRefsByPrefix(REFS_CHANGES)) { + if (!ref.getObjectId().equals(commit)) { + continue; + } + PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); + if (psId == null) { + continue; + } + try { + Change c = + notesFactory + .createChecked(db.get(), change().getProject(), psId.getParentKey()) + .getChange(); + if (!c.getDest().equals(change().getDest())) { + continue; + } + } catch (OrmException e) { + warn(e); + // Include this patch set; should cause an error below, which is good. + } + thisCommitPsIds.add(psId); + } + switch (thisCommitPsIds.size()) { + case 0: + // No patch set for this commit; insert one. + rw.parseBody(commit); + String changeId = + Iterables.getFirst(commit.getFooterLines(FooterConstants.CHANGE_ID), null); + // Missing Change-Id footer is ok, but mismatched is not. + if (changeId != null && !changeId.equals(change().getKey().get())) { + problem( + String.format( + "Expected merged commit %s has Change-Id: %s, but expected %s", + commit.name(), changeId, change().getKey().get())); + return; + } + insertMergedPatchSet(commit, null, false); + break; + + case 1: + // Existing patch set ref pointing to this commit. + PatchSet.Id id = thisCommitPsIds.get(0); + if (id.equals(change().currentPatchSetId())) { + // If it's the current patch set, we can just fix the status. + fixMerged(wrongChangeStatus(id, commit)); + } else if (id.get() > change().currentPatchSetId().get()) { + // If it's newer than the current patch set, reuse this patch set + // ID when inserting a new merged patch set. + insertMergedPatchSet(commit, id, true); + } else { + // If it's older than the current patch set, just delete the old + // ref, and use a new ID when inserting a new merged patch set. + insertMergedPatchSet(commit, id, false); + } + break; + + default: + problem( + String.format( + "Multiple patch sets for expected merged commit %s: %s", + commit.name(), intKeyOrdering().sortedCopy(thisCommitPsIds))); + break; + } + } catch (IOException e) { + error("Error looking up expected merged commit " + fix.expectMergedAs, e); + } + } + + private void insertMergedPatchSet( + final RevCommit commit, @Nullable PatchSet.Id psIdToDelete, boolean reuseOldPsId) { + ProblemInfo notFound = problem("No patch set found for merged commit " + commit.name()); + if (!user.get().isIdentifiedUser()) { + notFound.status = Status.FIX_FAILED; + notFound.outcome = "Must be called by an identified user to insert new patch set"; + return; + } + ProblemInfo insertPatchSetProblem; + ProblemInfo deleteOldPatchSetProblem; + + if (psIdToDelete == null) { + insertPatchSetProblem = + problem( + String.format( + "Expected merged commit %s has no associated patch set", commit.name())); + deleteOldPatchSetProblem = null; + } else { + String msg = + String.format( + "Expected merge commit %s corresponds to patch set %s," + + " not the current patch set %s", + commit.name(), psIdToDelete.get(), change().currentPatchSetId().get()); + // Maybe an identical problem, but different fix. + deleteOldPatchSetProblem = reuseOldPsId ? null : problem(msg); + insertPatchSetProblem = problem(msg); + } + + List<ProblemInfo> currProblems = new ArrayList<>(3); + currProblems.add(notFound); + if (deleteOldPatchSetProblem != null) { + currProblems.add(deleteOldPatchSetProblem); + } + currProblems.add(insertPatchSetProblem); + + try { + PatchSet.Id psId = + (psIdToDelete != null && reuseOldPsId) + ? psIdToDelete + : ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId()); + PatchSetInserter inserter = patchSetInserterFactory.create(notes, psId, commit); + try (BatchUpdate bu = newBatchUpdate()) { + bu.setRepository(repo, rw, oi); + + if (psIdToDelete != null) { + // Delete the given patch set ref. If reuseOldPsId is true, + // PatchSetInserter will reinsert the same ref, making it a no-op. + bu.addOp( + notes.getChangeId(), + new BatchUpdateOp() { + @Override + public void updateRepo(RepoContext ctx) throws IOException { + ctx.addRefUpdate(commit, ObjectId.zeroId(), psIdToDelete.toRefName()); + } + }); + if (!reuseOldPsId) { + bu.addOp( + notes.getChangeId(), + new DeletePatchSetFromDbOp(requireNonNull(deleteOldPatchSetProblem), psIdToDelete)); + } + } + + bu.addOp( + notes.getChangeId(), + inserter + .setValidate(false) + .setFireRevisionCreated(false) + .setNotify(NotifyHandling.NONE) + .setAllowClosed(true) + .setMessage("Patch set for merged commit inserted by consistency checker")); + bu.addOp(notes.getChangeId(), new FixMergedOp(notFound)); + bu.execute(); + } + notes = notesFactory.createChecked(db.get(), inserter.getChange()); + insertPatchSetProblem.status = Status.FIXED; + insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get(); + } catch (OrmException | IOException | UpdateException | RestApiException e) { + warn(e); + for (ProblemInfo pi : currProblems) { + pi.status = Status.FIX_FAILED; + pi.outcome = "Error inserting merged patch set"; + } + return; + } + } + + private static class FixMergedOp implements BatchUpdateOp { + private final ProblemInfo p; + + private FixMergedOp(ProblemInfo p) { + this.p = p; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws OrmException { + ctx.getChange().setStatus(Change.Status.MERGED); + ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED); + p.status = Status.FIXED; + p.outcome = "Marked change as merged"; + return true; + } + } + + private void fixMerged(ProblemInfo p) { + try (BatchUpdate bu = newBatchUpdate()) { + bu.setRepository(repo, rw, oi); + bu.addOp(notes.getChangeId(), new FixMergedOp(p)); + bu.execute(); + } catch (UpdateException | RestApiException e) { + logger.atWarning().withCause(e).log("Error marking %s as merged", notes.getChangeId()); + p.status = Status.FIX_FAILED; + p.outcome = "Error updating status to merged"; + } + } + + private BatchUpdate newBatchUpdate() { + return updateFactory.create(db.get(), change().getProject(), user.get(), TimeUtil.nowTs()); + } + + private void fixPatchSetRef(ProblemInfo p, PatchSet ps) { + try { + RefUpdate ru = repo.updateRef(ps.getId().toRefName()); + ru.setForceUpdate(true); + ru.setNewObjectId(ObjectId.fromString(ps.getRevision().get())); + ru.setRefLogIdent(newRefLogIdent()); + ru.setRefLogMessage("Repair patch set ref", true); + RefUpdate.Result result = ru.update(); + switch (result) { + case NEW: + case FORCED: + case FAST_FORWARD: + case NO_CHANGE: + p.status = Status.FIXED; + p.outcome = "Repaired patch set ref"; + return; + case IO_FAILURE: + case LOCK_FAILURE: + case NOT_ATTEMPTED: + case REJECTED: + case REJECTED_CURRENT_BRANCH: + case RENAMED: + case REJECTED_MISSING_OBJECT: + case REJECTED_OTHER_REASON: + default: + p.status = Status.FIX_FAILED; + p.outcome = "Failed to update patch set ref: " + result; + return; + } + } catch (IOException e) { + String msg = "Error fixing patch set ref"; + logger.atWarning().withCause(e).log("%s %s", msg, ps.getId().toRefName()); + p.status = Status.FIX_FAILED; + p.outcome = msg; + } + } + + private void deletePatchSets(List<DeletePatchSetFromDbOp> ops) { + try (BatchUpdate bu = newBatchUpdate()) { + bu.setRepository(repo, rw, oi); + for (DeletePatchSetFromDbOp op : ops) { + checkArgument(op.psId.getParentKey().equals(notes.getChangeId())); + bu.addOp(notes.getChangeId(), op); + } + bu.addOp(notes.getChangeId(), new UpdateCurrentPatchSetOp(ops)); + bu.execute(); + } catch (NoPatchSetsWouldRemainException e) { + for (DeletePatchSetFromDbOp op : ops) { + op.p.status = Status.FIX_FAILED; + op.p.outcome = e.getMessage(); + } + } catch (UpdateException | RestApiException e) { + String msg = "Error deleting patch set"; + logger.atWarning().withCause(e).log("%s of change %s", msg, ops.get(0).psId.getParentKey()); + for (DeletePatchSetFromDbOp op : ops) { + // Overwrite existing statuses that were set before the transaction was + // rolled back. + op.p.status = Status.FIX_FAILED; + op.p.outcome = msg; + } + } + } + + private class DeletePatchSetFromDbOp implements BatchUpdateOp { + private final ProblemInfo p; + private final PatchSet.Id psId; + + private DeletePatchSetFromDbOp(ProblemInfo p, PatchSet.Id psId) { + this.p = p; + this.psId = psId; + } + + @Override + public boolean updateChange(ChangeContext ctx) + throws OrmException, PatchSetInfoNotAvailableException { + // Delete dangling key references. + ReviewDb db = BatchUpdateReviewDb.unwrap(ctx.getDb()); + accountPatchReviewStore.run(s -> s.clearReviewed(psId), OrmException.class); + db.changeMessages().delete(db.changeMessages().byChange(psId.getParentKey())); + db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId)); + db.patchComments().delete(db.patchComments().byPatchSet(psId)); + db.patchSets().deleteKeys(Collections.singleton(psId)); + + // NoteDb requires no additional fiddling; setting the state to deleted is + // sufficient to filter everything else out. + ctx.getUpdate(psId).setPatchSetState(PatchSetState.DELETED); + + p.status = Status.FIXED; + p.outcome = "Deleted patch set"; + return true; + } + } + + private static class NoPatchSetsWouldRemainException extends RestApiException { + private static final long serialVersionUID = 1L; + + private NoPatchSetsWouldRemainException() { + super("Cannot delete patch set; no patch sets would remain"); + } + } + + private class UpdateCurrentPatchSetOp implements BatchUpdateOp { + private final Set<PatchSet.Id> toDelete; + + private UpdateCurrentPatchSetOp(List<DeletePatchSetFromDbOp> deleteOps) { + toDelete = new HashSet<>(); + for (DeletePatchSetFromDbOp op : deleteOps) { + toDelete.add(op.psId); + } + } + + @Override + public boolean updateChange(ChangeContext ctx) + throws OrmException, PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException { + if (!toDelete.contains(ctx.getChange().currentPatchSetId())) { + return false; + } + Set<PatchSet.Id> all = new HashSet<>(); + // Doesn't make any assumptions about the order in which deletes happen + // and whether they are seen by this op; we are already given the full set + // of patch sets that will eventually be deleted in this update. + for (PatchSet ps : psUtil.byChange(ctx.getDb(), ctx.getNotes())) { + if (!toDelete.contains(ps.getId())) { + all.add(ps.getId()); + } + } + if (all.isEmpty()) { + throw new NoPatchSetsWouldRemainException(); + } + PatchSet.Id latest = ReviewDbUtil.intKeyOrdering().max(all); + ctx.getChange() + .setCurrentPatchSet(patchSetInfoFactory.get(ctx.getDb(), ctx.getNotes(), latest)); + return true; + } + } + + private PersonIdent newRefLogIdent() { + CurrentUser u = user.get(); + if (u.isIdentifiedUser()) { + return u.asIdentifiedUser().newRefLogIdent(); + } + return serverIdent.get(); + } + + private ObjectId parseObjectId(String objIdStr, String desc) { + try { + return ObjectId.fromString(objIdStr); + } catch (IllegalArgumentException e) { + problem(String.format("Invalid revision on %s: %s", desc, objIdStr)); + return null; + } + } + + private RevCommit parseCommit(ObjectId objId, String desc) { + try { + return rw.parseCommit(objId); + } catch (MissingObjectException e) { + problem(String.format("Object missing: %s: %s", desc, objId.name())); + } catch (IncorrectObjectTypeException e) { + problem(String.format("Not a commit: %s: %s", desc, objId.name())); + } catch (IOException e) { + problem(String.format("Failed to look up: %s: %s", desc, objId.name())); + } + return null; + } + + private ProblemInfo problem(String msg) { + ProblemInfo p = new ProblemInfo(); + p.message = requireNonNull(msg); + problems.add(p); + return p; + } + + private ProblemInfo lastProblem() { + return problems.get(problems.size() - 1); + } + + private boolean error(String msg, Throwable t) { + problem(msg); + // TODO(dborowitz): Expose stack trace to administrators. + warn(t); + return false; + } + + private void warn(Throwable t) { + logger.atWarning().withCause(t).log( + "Error in consistency check of change %s", notes.getChangeId()); + } + + private Result result() { + return Result.create(notes, problems); + } +} |