| // 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.common.collect.ImmutableList.toImmutableList; |
| import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES; |
| import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER; |
| import static java.util.Comparator.comparing; |
| 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.exceptions.StorageException; |
| import com.google.gerrit.extensions.api.changes.FixInput; |
| 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.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.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.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.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| 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 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, |
| RetryHelper retryHelper) { |
| this.accounts = accounts; |
| this.accountPatchReviewStore = accountPatchReviewStore; |
| 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(notes); |
| if (currPs == null) { |
| problem( |
| String.format("Current patch set %d not found", change().currentPatchSetId().get())); |
| } |
| } catch (StorageException 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(notes)); |
| } catch (StorageException 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( |
| formatProblemMessage( |
| "Patch set %d (%s) is merged into destination ref %s (%s), but change" |
| + " status is %s", |
| psId.get(), commit.name(), refName, tip.name())); |
| } |
| |
| private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit, boolean merged) { |
| String refName = change().getDest().get(); |
| if (merged && !change().isMerged()) { |
| ProblemInfo p = wrongChangeStatus(psId, commit); |
| if (fix != null) { |
| fixMerged(p); |
| } |
| } else if (!merged && change().isMerged()) { |
| problem( |
| formatProblemMessage( |
| "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())); |
| } |
| } |
| |
| private String formatProblemMessage( |
| String message, int psId, String commitName, String refName, String tipName) { |
| return String.format( |
| message, |
| psId, |
| commitName, |
| refName, |
| tipName, |
| ChangeUtil.status(change()).toUpperCase(Locale.US)); |
| } |
| |
| 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(change().getProject(), psId.getParentKey()).getChange(); |
| if (!c.getDest().equals(change().getDest())) { |
| continue; |
| } |
| } catch (StorageException 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(), |
| thisCommitPsIds.stream() |
| .sorted(comparing(PatchSet.Id::get)) |
| .collect(toImmutableList()))); |
| 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.setNotify(NotifyResolver.Result.none()); |
| bu.addOp( |
| notes.getChangeId(), |
| inserter |
| .setValidate(false) |
| .setFireRevisionCreated(false) |
| .setAllowClosed(true) |
| .setMessage("Patch set for merged commit inserted by consistency checker")); |
| bu.addOp(notes.getChangeId(), new FixMergedOp(notFound)); |
| bu.execute(); |
| } |
| notes = notesFactory.createChecked(inserter.getChange()); |
| insertPatchSetProblem.status = Status.FIXED; |
| insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get(); |
| } catch (StorageException | 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) { |
| 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(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 PatchSetInfoNotAvailableException { |
| // Delete dangling key references. |
| accountPatchReviewStore.run(s -> s.clearReviewed(psId)); |
| |
| // For NoteDb setting the state to deleted is sufficient to filter everything 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 PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException { |
| if (!toDelete.contains(ctx.getChange().currentPatchSetId())) { |
| return false; |
| } |
| TreeSet<PatchSet.Id> all = new TreeSet<>(comparing(PatchSet.Id::get)); |
| // 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.getNotes())) { |
| if (!toDelete.contains(ps.getId())) { |
| all.add(ps.getId()); |
| } |
| } |
| if (all.isEmpty()) { |
| throw new NoPatchSetsWouldRemainException(); |
| } |
| ctx.getChange().setCurrentPatchSet(patchSetInfoFactory.get(ctx.getNotes(), all.last())); |
| 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); |
| } |
| } |