ConsistencyChecker check and fix for missing patch set refs
Change-Id: Ic485aa6e35fd0b0127362b5213e7f392312b727e
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index 6f5618b..ef28ed8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server;
+import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.account.CapabilityControl;
@@ -39,8 +40,9 @@
InternalUser create();
}
+ @VisibleForTesting
@Inject
- protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
+ public InternalUser(CapabilityControl.Factory capabilityControlFactory) {
super(capabilityControlFactory);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 7197269..0469342 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -28,6 +28,9 @@
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.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.AtomicUpdate;
@@ -39,7 +42,9 @@
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;
@@ -82,6 +87,8 @@
private final Provider<ReviewDb> db;
private final GitRepositoryManager repoManager;
+ private final Provider<CurrentUser> user;
+ private final Provider<PersonIdent> serverIdent;
private FixInput fix;
private Change change;
@@ -95,9 +102,13 @@
@Inject
ConsistencyChecker(Provider<ReviewDb> db,
- GitRepositoryManager repoManager) {
+ GitRepositoryManager repoManager,
+ Provider<CurrentUser> user,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent) {
this.db = db;
this.repoManager = repoManager;
+ this.user = user;
+ this.serverIdent = serverIdent;
reset();
}
@@ -209,9 +220,11 @@
.treeSetValues(Ordering.natural().onResultOf(toPsId))
.build();
for (PatchSet ps : all) {
+ // Check revision format.
ObjectId objId;
String rev = ps.getRevision().get();
int psNum = ps.getId().get();
+ String refName = ps.getId().toRefName();
try {
objId = ObjectId.fromString(rev);
} catch (IllegalArgumentException e) {
@@ -221,16 +234,39 @@
}
bySha.put(objId, ps);
+ // Check ref existence.
+ ProblemInfo refProblem = null;
+ try {
+ Ref ref = repo.getRef(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));
+ }
+ } catch (IOException e) {
+ error("Error reading ref: " + refName, e);
+ refProblem = lastProblem();
+ }
+
+ // Check object existence.
RevCommit psCommit = parseCommit(
objId, String.format("patch set %d", psNum));
if (psCommit == null) {
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
: bySha.asMap().entrySet()) {
if (e.getValue().size() > 1) {
@@ -305,6 +341,44 @@
}
}
+ 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 PersonIdent newRefLogIdent() {
+ CurrentUser u = user.get();
+ if (u.isIdentifiedUser()) {
+ return ((IdentifiedUser) u).newRefLogIdent();
+ } else {
+ return serverIdent.get();
+ }
+ }
+
private RevCommit parseCommit(ObjectId objId, String desc) {
try {
return rw.parseCommit(objId);
@@ -325,6 +399,10 @@
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.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
index 20d24a3..9972656 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
@@ -30,6 +30,8 @@
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.server.CurrentUser;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.testutil.InMemoryDatabase;
import com.google.gerrit.testutil.InMemoryRepositoryManager;
import com.google.gerrit.testutil.TestChanges;
@@ -38,6 +40,7 @@
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.After;
@@ -63,7 +66,11 @@
schemaFactory.create();
db = schemaFactory.open();
repoManager = new InMemoryRepositoryManager();
- checker = new ConsistencyChecker(Providers.<ReviewDb> of(db), repoManager);
+ checker = new ConsistencyChecker(
+ Providers.<ReviewDb> of(db),
+ repoManager,
+ Providers.<CurrentUser> of(new InternalUser(null)),
+ Providers.of(new PersonIdent("server", "noreply@example.com")));
project = new Project.NameKey("repo");
repo = new TestRepository<>(repoManager.createRepository(project));
userId = new Account.Id(1);
@@ -141,18 +148,65 @@
+ " fooooooooooooooooooooooooooooooooooooooo");
}
+ // No test for ref existing but object missing; InMemoryRepository won't let
+ // us do such a thing.
+
@Test
- public void patchSetObjectMissing() throws Exception {
+ public void patchSetObjectAndRefMissing() throws Exception {
Change c = insertChange();
PatchSet ps = newPatchSet(c.currentPatchSetId(),
ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
db.patchSets().insert(singleton(ps));
assertProblems(c,
+ "Ref missing: " + ps.getId().toRefName(),
"Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
}
@Test
+ public void patchSetObjectAndRefMissingWithFix() throws Exception {
+ Change c = insertChange();
+ PatchSet ps = newPatchSet(c.currentPatchSetId(),
+ ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+ db.patchSets().insert(singleton(ps));
+
+ String refName = ps.getId().toRefName();
+ List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+ ProblemInfo p = problems.get(0);
+ assertThat(p.message).isEqualTo("Ref missing: " + refName);
+ assertThat(p.status).isNull();
+ }
+
+ @Test
+ public void patchSetRefMissing() throws Exception {
+ Change c = insertChange();
+ PatchSet ps = insertPatchSet(c);
+ String refName = ps.getId().toRefName();
+ repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+ deleteRef(refName);
+
+ assertProblems(c, "Ref missing: " + refName);
+ }
+
+ @Test
+ public void patchSetRefMissingWithFix() throws Exception {
+ Change c = insertChange();
+ PatchSet ps = insertPatchSet(c);
+ String refName = ps.getId().toRefName();
+ repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+ deleteRef(refName);
+
+ List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+ ProblemInfo p = problems.get(0);
+ assertThat(p.message).isEqualTo("Ref missing: " + refName);
+ assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+ assertThat(p.outcome).isEqualTo("Repaired patch set ref");
+
+ assertThat(repo.getRepository().getRef(refName).getObjectId().name())
+ .isEqualTo(ps.getRevision().get());
+ }
+
+ @Test
public void currentPatchSetMissing() throws Exception {
Change c = insertChange();
assertProblems(c, "Current patch set 1 not found");
@@ -164,7 +218,8 @@
PatchSet ps1 = insertPatchSet(c);
String rev = ps1.getRevision().get();
incrementPatchSet(c);
- insertMissingPatchSet(c, rev);
+ PatchSet ps2 = insertMissingPatchSet(c, rev);
+ updatePatchSetRef(ps2);
assertProblems(c,
"Multiple patch sets pointing to " + rev + ": [1, 2]");
@@ -178,6 +233,7 @@
Change c = insertChange();
RevCommit commit = repo.commit().create();
PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+ updatePatchSetRef(ps);
db.patchSets().insert(singleton(ps));
assertProblems(c, "Destination ref not found (may be new branch): master");
@@ -248,6 +304,7 @@
RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
.parent(tip).create();
PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+ updatePatchSetRef(ps);
db.patchSets().insert(singleton(ps));
return ps;
}
@@ -259,6 +316,17 @@
return ps;
}
+ private void updatePatchSetRef(PatchSet ps) throws Exception {
+ repo.update(ps.getId().toRefName(),
+ ObjectId.fromString(ps.getRevision().get()));
+ }
+
+ private void deleteRef(String refName) throws Exception {
+ RefUpdate ru = repo.getRepository().updateRef(refName, true);
+ ru.setForceUpdate(true);
+ assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+
private void assertProblems(Change c, String... expected) {
assertThat(Lists.transform(checker.check(c).problems(),
new Function<ProblemInfo, String>() {