Java tutorial
// 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.gerrit.server.ChangeUtil.PS_ID_ORDER; import static com.google.gerrit.server.ChangeUtil.TO_PS_ID; import com.google.auto.value.AutoValue; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; 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.common.ProblemInfo; import com.google.gerrit.extensions.common.ProblemInfo.Status; 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.client.RevId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.AtomicUpdate; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; 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.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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; /** * 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 Logger log = LoggerFactory.getLogger(ConsistencyChecker.class); @AutoValue public abstract static class Result { private static Result create(Change.Id id, List<ProblemInfo> problems) { return new AutoValue_ConsistencyChecker_Result(id, null, problems); } private static Result create(Change c, List<ProblemInfo> problems) { return new AutoValue_ConsistencyChecker_Result(c.getId(), c, problems); } public abstract Change.Id id(); @Nullable public abstract Change change(); public abstract List<ProblemInfo> problems(); } private final Provider<ReviewDb> db; private final GitRepositoryManager repoManager; private final Provider<CurrentUser> user; private final Provider<PersonIdent> serverIdent; private final ChangeControl.GenericFactory changeControlFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetInserter.Factory patchSetInserterFactory; private FixInput fix; private Change change; private Repository repo; private RevWalk rw; private RevCommit tip; private Multimap<ObjectId, PatchSet> patchSetsBySha; private PatchSet currPs; private RevCommit currPsCommit; private List<ProblemInfo> problems; @Inject ConsistencyChecker(Provider<ReviewDb> db, GitRepositoryManager repoManager, Provider<CurrentUser> user, @GerritPersonIdent Provider<PersonIdent> serverIdent, ChangeControl.GenericFactory changeControlFactory, PatchSetInfoFactory patchSetInfoFactory, PatchSetInserter.Factory patchSetInserterFactory) { this.db = db; this.repoManager = repoManager; this.user = user; this.serverIdent = serverIdent; this.changeControlFactory = changeControlFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.patchSetInserterFactory = patchSetInserterFactory; reset(); } private void reset() { change = null; repo = null; rw = null; problems = new ArrayList<>(); } public Result check(ChangeData cd) { return check(cd, null); } public Result check(ChangeData cd, @Nullable FixInput f) { reset(); try { return check(cd.change(), f); } catch (OrmException e) { error("Error looking up change", e); return Result.create(cd.getId(), problems); } } public Result check(Change c) { return check(c, null); } public Result check(Change c, @Nullable FixInput f) { reset(); fix = f; change = c; try { checkImpl(); return Result.create(c, problems); } finally { if (rw != null) { rw.close(); } if (repo != null) { repo.close(); } } } private void checkImpl() { checkOwner(); checkCurrentPatchSetEntity(); // All checks that require the repo. if (!openRepo()) { return; } if (!checkPatchSets()) { return; } checkMerged(); } private void checkOwner() { try { if (db.get().accounts().get(change.getOwner()) == null) { problem("Missing change owner: " + change.getOwner()); } } catch (OrmException e) { error("Failed to look up owner", e); } } private void checkCurrentPatchSetEntity() { try { PatchSet.Id psId = change.currentPatchSetId(); currPs = db.get().patchSets().get(psId); if (currPs == null) { problem(String.format("Current patch set %d not found", psId.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); rw = new RevWalk(repo); 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 { all = Lists.newArrayList(db.get().patchSets().byChange(change.getId())); } catch (OrmException e) { return error("Failed to look up patch sets", e); } // Iterate in descending order so deletePatchSet can assume the latest patch // set exists. Collections.sort(all, PS_ID_ORDER.reverse()); patchSetsBySha = MultimapBuilder.hashKeys(all.size()).treeSetValues(PS_ID_ORDER).build(); Map<String, Ref> refs; try { refs = repo.getRefDatabase().exactRef(Lists.transform(all, new Function<PatchSet, String>() { @Override public String apply(PatchSet ps) { return ps.getId().toRefName(); } }).toArray(new String[all.size()])); } catch (IOException e) { error("error reading refs", e); refs = Collections.emptyMap(); } 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) { deletePatchSet(lastProblem(), ps.getId()); } continue; } else if (refProblem != null && fix != null) { fixPatchSetRef(refProblem, ps); } if (ps.getId().equals(change.currentPatchSetId())) { currPsCommit = psCommit; } } // 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(), TO_PS_ID))); } } 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, currPsCommit, merged); } } private void checkMergedBitMatchesStatus(PatchSet ps, RevCommit commit, boolean merged) { String refName = change.getDest().get(); if (merged && change.getStatus() != Change.Status.MERGED) { ProblemInfo p = problem(String.format( "Patch set %d (%s) is merged into destination ref %s (%s), but change" + " status is %s", ps.getId().get(), commit.name(), refName, tip.name(), change.getStatus())); 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; } if (Objects.equals(commit, currPsCommit)) { // Caller gave us latest patch set SHA-1; verified in checkPatchSets. 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; } RevId revId = new RevId(commit.name()); List<PatchSet> patchSets = FluentIterable.from(db.get().patchSets().byRevision(revId)) .filter(new Predicate<PatchSet>() { @Override public boolean apply(PatchSet ps) { try { Change c = db.get().changes().get(ps.getId().getParentKey()); return c != null && c.getDest().equals(change.getDest()); } catch (OrmException e) { warn(e); return true; // Should cause an error below, that's good. } } }).toSortedList(ChangeUtil.PS_ID_ORDER); switch (patchSets.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; } PatchSet ps = insertPatchSet(commit); if (ps != null) { checkMergedBitMatchesStatus(ps, commit, true); } break; case 1: // Existing patch set of this commit; check that it is the current // patch set. // TODO(dborowitz): This could be fixed if it's an older patch set of // the current change. PatchSet.Id id = patchSets.get(0).getId(); if (!id.equals(change.currentPatchSetId())) { problem(String.format( "Expected merged commit %s corresponds to" + " patch set %s, which is not the current patch set %s", commit.name(), id, change.currentPatchSetId())); } break; default: problem(String.format("Multiple patch sets for expected merged commit %s: %s", commit.name(), patchSets)); break; } } catch (OrmException | IOException e) { error("Error looking up expected merged commit " + fix.expectMergedAs, e); } } private PatchSet insertPatchSet(RevCommit commit) { ProblemInfo p = problem("No patch set found for merged commit " + commit.name()); if (!user.get().isIdentifiedUser()) { p.status = Status.FIX_FAILED; p.outcome = "Must be called by an identified user to insert new patch set"; return null; } try { ChangeControl ctl = changeControlFactory.controlFor(change, user.get()); PatchSetInserter inserter = patchSetInserterFactory.create(repo, rw, ctl, commit); change = inserter.setValidatePolicy(ValidatePolicy.NONE).setRunHooks(false).setSendMail(false) .setAllowClosed(true).setUploader(((IdentifiedUser) user.get()).getAccountId()) // TODO: fix setMessage to work without init() .setMessage("Patch set for merged commit inserted by consistency checker").insert(); p.status = Status.FIXED; p.outcome = "Inserted as patch set " + change.currentPatchSetId().get(); return inserter.getPatchSet(); } catch (InvalidChangeOperationException | OrmException | IOException | NoSuchChangeException e) { warn(e); p.status = Status.FIX_FAILED; p.outcome = "Error inserting new patch set"; return null; } } private void fixMerged(ProblemInfo p) { try { change = db.get().changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change c) { c.setStatus(Change.Status.MERGED); return c; } }); p.status = Status.FIXED; p.outcome = "Marked change as merged"; } catch (OrmException e) { log.warn("Error marking " + change.getId() + "as merged", e); p.status = Status.FIX_FAILED; p.outcome = "Error updating status to merged"; } } 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; 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"; log.warn(msg + ' ' + ps.getId().toRefName(), e); p.status = Status.FIX_FAILED; p.outcome = msg; } } private void deletePatchSet(ProblemInfo p, PatchSet.Id psId) { ReviewDb db = this.db.get(); Change.Id cid = psId.getParentKey(); try { db.changes().beginTransaction(cid); try { Change c = db.changes().get(cid); if (c == null) { throw new OrmException("Change missing: " + cid); } if (psId.equals(c.currentPatchSetId())) { List<PatchSet> all = Lists.newArrayList(db.patchSets().byChange(cid)); if (all.size() == 1 && all.get(0).getId().equals(psId)) { p.status = Status.FIX_FAILED; p.outcome = "Cannot delete patch set; no patch sets would remain"; return; } // If there were multiple missing patch sets, assumes deletePatchSet // has been called in decreasing order, so the max remaining PatchSet // is the effective current patch set. Collections.sort(all, PS_ID_ORDER.reverse()); PatchSet.Id latest = null; for (PatchSet ps : all) { latest = ps.getId(); if (!ps.getId().equals(psId)) { break; } } c.setCurrentPatchSet(patchSetInfoFactory.get(db, latest)); db.changes().update(Collections.singleton(c)); } // Delete dangling primary key references. Don't delete ChangeMessages, // which don't use patch sets as a primary key, and may provide useful // historical information. db.accountPatchReviews().delete(db.accountPatchReviews().byPatchSet(psId)); db.patchSetAncestors().delete(db.patchSetAncestors().byPatchSet(psId)); db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId)); db.patchComments().delete(db.patchComments().byPatchSet(psId)); db.patchSets().deleteKeys(Collections.singleton(psId)); db.commit(); p.status = Status.FIXED; p.outcome = "Deleted patch set"; } finally { db.rollback(); } } catch (PatchSetInfoNotAvailableException | OrmException e) { String msg = "Error deleting patch set"; log.warn(msg + ' ' + psId, e); p.status = Status.FIX_FAILED; p.outcome = msg; } } private PersonIdent newRefLogIdent() { CurrentUser u = user.get(); if (u.isIdentifiedUser()) { return ((IdentifiedUser) u).newRefLogIdent(); } else { 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 = 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) { log.warn("Error in consistency check of change " + change.getId(), t); } }