Add a basic consistency checker for changes
In the past, Gerrit bugs, lack of transactions, and unreliable NoSQL
backends have at various times produced a bewildering variety of
corrupt states. Similarly, we are not immune from bugs being
introduced in the future.
Add a tool to detect and explain some of these possible states.
Change-Id: Ia91b35b140bf05254877f413003d12cf779b775c
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 584c661..bb9bb3a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1134,6 +1134,52 @@
HTTP/1.1 204 No Content
----
+[[check-change]]
+=== Check change
+--
+'GET /changes/link:#change-id[\{change-id\}]/check'
+--
+
+Performs consistency checks on the change, and returns a
+link:#check-result[CheckResult] entity.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "change": {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "project": "myProject",
+ "branch": "master",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "subject": "Implementing Feature X",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": true,
+ "insertions": 34,
+ "deletions": 101,
+ "_sortkey": "0023412400000f7d",
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ },
+ "messages": [
+ "Current patch set 1 not found"
+ ]
+ }
+----
+
[[edit-endpoints]]
== Change Edit Endpoints
@@ -3861,6 +3907,24 @@
|`restore_path`|optional|Path to file to restore.
|===========================
+[[check-result]]
+=== CheckResult
+The `CheckResult` entity contains the results of a consistency check on
+a change.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name|Description
+|`change`|
+link:#change-info[ChangeInfo] entity containing information about the change,
+as in link:#get-change[Get Change] with no options. Some fields not marked
+optional may be missing if a consistency check failed, but at least
+`id`, `project`, `branch`, and `_number` will be present.
+|`messages`|
+List of messages describing potential problems with the change. May be
+empty if no problems were found.
+|===========================
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 11f054c..b9ca3e3 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -204,6 +204,7 @@
'//lib:guava',
'//lib:gwtorm',
'//lib:junit',
+ '//lib:truth',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',
'//lib/jgit:jgit',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
new file mode 100644
index 0000000..8d613fb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -0,0 +1,76 @@
+// 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 com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class Check implements RestReadView<ChangeResource> {
+ private static final Logger log = LoggerFactory.getLogger(Check.class);
+
+ private final Provider<ConsistencyChecker> checkerProvider;
+ private final ChangeJson json;
+
+ @Inject
+ Check(Provider<ConsistencyChecker> checkerProvider,
+ ChangeJson json) {
+ this.checkerProvider = checkerProvider;
+ this.json = json;
+ }
+
+ @Override
+ public CheckResult apply(ChangeResource rsrc) {
+ CheckResult result = new CheckResult();
+ result.messages = checkerProvider.get().check(rsrc.getChange());
+ try {
+ result.change = json.format(rsrc);
+ } catch (OrmException e) {
+ // Even with no options there are a surprising number of dependencies in
+ // ChangeJson. Fall back to a very basic implementation with no
+ // dependencies if this fails.
+ String msg = "Error rendering final ChangeInfo";
+ log.warn(msg, e);
+ result.messages.add(msg);
+ result.change = basicChangeInfo(rsrc.getChange());
+ }
+ return result;
+ }
+
+ private static ChangeInfo basicChangeInfo(Change c) {
+ ChangeInfo info = new ChangeInfo();
+ info.project = c.getProject().get();
+ info.branch = c.getDest().getShortName();
+ info.topic = c.getTopic();
+ info.changeId = c.getKey().get();
+ info.subject = c.getSubject();
+ info.status = c.getStatus();
+ info.owner = new AccountInfo(c.getOwner());
+ info.created = c.getCreatedOn();
+ info.updated = c.getLastUpdatedOn();
+ info._number = c.getId().get();
+ info.finish();
+ return info;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java
new file mode 100644
index 0000000..673d010
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java
@@ -0,0 +1,24 @@
+// 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 com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+
+import java.util.List;
+
+public class CheckResult {
+ public ChangeInfo change;
+ public List<String> messages;
+}
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
new file mode 100644
index 0000000..e5c2ad8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -0,0 +1,260 @@
+// 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 com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Ordering;
+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.server.git.GitRepositoryManager;
+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.Ref;
+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.List;
+import java.util.Map;
+
+/**
+ * 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);
+
+ private final Provider<ReviewDb> db;
+ private final GitRepositoryManager repoManager;
+
+ private Change change;
+ private Repository repo;
+ private RevWalk rw;
+
+ private PatchSet currPs;
+ private RevCommit currPsCommit;
+
+ private List<String> messages;
+
+ @Inject
+ ConsistencyChecker(Provider<ReviewDb> db,
+ GitRepositoryManager repoManager) {
+ this.db = db;
+ this.repoManager = repoManager;
+ reset();
+ }
+
+ private void reset() {
+ change = null;
+ repo = null;
+ rw = null;
+ messages = new ArrayList<>();
+ }
+
+ public List<String> check(Change c) {
+ reset();
+ change = c;
+ try {
+ checkImpl();
+ return messages;
+ } finally {
+ if (rw != null) {
+ rw.release();
+ }
+ 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) {
+ messages.add("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) {
+ messages.add(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 = db.get().patchSets().byChange(change.getId()).toList();
+ } catch (OrmException e) {
+ return error("Failed to look up patch sets", e);
+ }
+ Function<PatchSet, Integer> toPsId = new Function<PatchSet, Integer>() {
+ @Override
+ public Integer apply(PatchSet in) {
+ return in.getId().get();
+ }
+ };
+ Multimap<ObjectId, PatchSet> bySha = MultimapBuilder.hashKeys(all.size())
+ .treeSetValues(Ordering.natural().onResultOf(toPsId))
+ .build();
+ for (PatchSet ps : all) {
+ ObjectId objId;
+ String rev = ps.getRevision().get();
+ int psNum = ps.getId().get();
+ try {
+ objId = ObjectId.fromString(rev);
+ } catch (IllegalArgumentException e) {
+ error(String.format("Invalid revision on patch set %d: %s", psNum, rev),
+ e);
+ continue;
+ }
+ bySha.put(objId, ps);
+
+ RevCommit psCommit = parseCommit(
+ objId, String.format("patch set %d", psNum));
+ if (psCommit == null) {
+ continue;
+ }
+ if (ps.getId().equals(change.currentPatchSetId())) {
+ currPsCommit = psCommit;
+ }
+ }
+
+ for (Map.Entry<ObjectId, Collection<PatchSet>> e
+ : bySha.asMap().entrySet()) {
+ if (e.getValue().size() > 1) {
+ messages.add(String.format("Multiple patch sets pointing to %s: %s",
+ e.getKey().name(),
+ Collections2.transform(e.getValue(), toPsId)));
+ }
+ }
+
+ return currPs != null && currPsCommit != null;
+ }
+
+ private void checkMerged() {
+ String refName = change.getDest().get();
+ Ref dest;
+ try {
+ dest = repo.getRef(refName);
+ } catch (IOException e) {
+ messages.add("Failed to look up destination ref: " + refName);
+ return;
+ }
+ if (dest == null) {
+ messages.add("Destination ref not found (may be new branch): "
+ + change.getDest().get());
+ return;
+ }
+ RevCommit tip = parseCommit(dest.getObjectId(),
+ "destination ref " + refName);
+ if (tip == null) {
+ return;
+ }
+ boolean merged;
+ try {
+ merged = rw.isMergedInto(currPsCommit, tip);
+ } catch (IOException e) {
+ messages.add("Error checking whether patch set " + currPs.getId().get()
+ + " is merged");
+ return;
+ }
+ if (merged && change.getStatus() != Change.Status.MERGED) {
+ messages.add(String.format("Patch set %d (%s) is merged into destination"
+ + " ref %s (%s), but change status is %s", currPs.getId().get(),
+ currPsCommit.name(), refName, tip.name(), change.getStatus()));
+ // TODO(dborowitz): Just fix it.
+ } else if (!merged && change.getStatus() == Change.Status.MERGED) {
+ messages.add(String.format("Patch set %d (%s) is not merged into"
+ + " destination ref %s (%s), but change status is %s",
+ currPs.getId().get(), currPsCommit.name(), refName, tip.name(),
+ change.getStatus()));
+ }
+ }
+
+ private RevCommit parseCommit(ObjectId objId, String desc) {
+ try {
+ return rw.parseCommit(objId);
+ } catch (MissingObjectException e) {
+ messages.add(String.format("Object missing: %s: %s", desc, objId.name()));
+ } catch (IncorrectObjectTypeException e) {
+ messages.add(String.format("Not a commit: %s: %s", desc, objId.name()));
+ } catch (IOException e) {
+ messages.add(String.format("Failed to look up: %s: %s", desc, objId.name()));
+ }
+ return null;
+ }
+
+ private boolean error(String msg, Throwable t) {
+ messages.add(msg);
+ // TODO(dborowitz): Expose stack trace to administrators.
+ log.warn("Error in consistency check of change " + change.getId(), t);
+ return false;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index fc13786..634e19d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -52,6 +52,7 @@
get(CHANGE_KIND, "topic").to(GetTopic.class);
get(CHANGE_KIND, "in").to(IncludedIn.class);
get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
+ get(CHANGE_KIND, "check").to(Check.class);
put(CHANGE_KIND, "topic").to(PutTopic.class);
delete(CHANGE_KIND, "topic").to(PutTopic.class);
delete(CHANGE_KIND).to(DeleteDraftChange.class);
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
new file mode 100644
index 0000000..3104b69
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
@@ -0,0 +1,242 @@
+// 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.truth.Truth.assertThat;
+import static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
+import static com.google.gerrit.testutil.TestChanges.newChange;
+import static com.google.gerrit.testutil.TestChanges.newPatchSet;
+import static java.util.Collections.singleton;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+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.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.inject.util.Providers;
+
+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.RefUpdate;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ConsistencyCheckerTest {
+ private InMemoryDatabase schemaFactory;
+ private ReviewDb db;
+ private InMemoryRepositoryManager repoManager;
+ private ConsistencyChecker checker;
+
+ private TestRepository<InMemoryRepository> repo;
+ private Project.NameKey project;
+ private Account.Id userId;
+ private RevCommit tip;
+
+ @Before
+ public void setUp() throws Exception {
+ schemaFactory = InMemoryDatabase.newDatabase();
+ schemaFactory.create();
+ db = schemaFactory.open();
+ repoManager = new InMemoryRepositoryManager();
+ checker = new ConsistencyChecker(Providers.<ReviewDb> of(db), repoManager);
+ project = new Project.NameKey("repo");
+ repo = new TestRepository<>(repoManager.createRepository(project));
+ userId = new Account.Id(1);
+ db.accounts().insert(singleton(new Account(userId, TimeUtil.nowTs())));
+ tip = repo.branch("master").commit().create();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (db != null) {
+ db.close();
+ }
+ if (schemaFactory != null) {
+ InMemoryDatabase.drop(schemaFactory);
+ }
+ }
+
+ @Test
+ public void validNewChange() throws Exception {
+ Change c = newChange(project, userId);
+ db.changes().insert(singleton(c));
+ RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+ db.patchSets().insert(singleton(ps1));
+
+ incrementPatchSet(c);
+ RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
+ db.patchSets().insert(singleton(ps2));
+
+ assertThat(checker.check(c)).isEmpty();
+ }
+
+ @Test
+ public void validMergedChange() throws Exception {
+ Change c = newChange(project, userId);
+ c.setStatus(Change.Status.MERGED);
+ db.changes().insert(singleton(c));
+ RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+ db.patchSets().insert(singleton(ps1));
+
+ incrementPatchSet(c);
+ RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
+ db.patchSets().insert(singleton(ps2));
+
+ repo.branch(c.getDest().get()).update(commit2);
+ assertThat(checker.check(c)).isEmpty();
+ }
+
+ @Test
+ public void missingOwner() throws Exception {
+ Change c = newChange(project, new Account.Id(2));
+ db.changes().insert(singleton(c));
+ RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+ db.patchSets().insert(singleton(ps));
+
+ assertThat(checker.check(c)).containsExactly("Missing change owner: 2");
+ }
+
+ @Test
+ public void missingRepo() throws Exception {
+ Change c = newChange(new Project.NameKey("otherproject"), userId);
+ db.changes().insert(singleton(c));
+ PatchSet ps = newPatchSet(c.currentPatchSetId(),
+ ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+ db.patchSets().insert(singleton(ps));
+ assertThat(checker.check(c))
+ .containsExactly("Destination repository not found: otherproject");
+ }
+
+ @Test
+ public void invalidRevision() throws Exception {
+ Change c = newChange(project, userId);
+ db.changes().insert(singleton(c));
+
+ PatchSet ps = new PatchSet(c.currentPatchSetId());
+ ps.setRevision(new RevId("fooooooooooooooooooooooooooooooooooooooo"));
+ ps.setUploader(userId);
+ ps.setCreatedOn(TimeUtil.nowTs());
+ db.patchSets().insert(singleton(ps));
+
+ incrementPatchSet(c);
+ RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
+ db.patchSets().insert(singleton(ps2));
+
+ assertThat(checker.check(c)).containsExactly(
+ "Invalid revision on patch set 1:"
+ + " fooooooooooooooooooooooooooooooooooooooo");
+ }
+
+ @Test
+ public void patchSetObjectMissing() throws Exception {
+ Change c = newChange(project, userId);
+ db.changes().insert(singleton(c));
+ PatchSet ps = newPatchSet(c.currentPatchSetId(),
+ ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+ db.patchSets().insert(singleton(ps));
+
+ assertThat(checker.check(c)).containsExactly(
+ "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ }
+
+ @Test
+ public void currentPatchSetMissing() throws Exception {
+ Change c = newChange(project, userId);
+ db.changes().insert(singleton(c));
+ assertThat(checker.check(c))
+ .containsExactly("Current patch set 1 not found");
+ }
+
+ @Test
+ public void duplicatePatchSetRevisions() throws Exception {
+ Change c = newChange(project, userId);
+ db.changes().insert(singleton(c));
+ RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+ db.patchSets().insert(singleton(ps1));
+
+ incrementPatchSet(c);
+ PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+ db.patchSets().insert(singleton(ps2));
+
+ assertThat(checker.check(c)).containsExactly("Multiple patch sets pointing to "
+ + commit1.name() + ": [1, 2]");
+ }
+
+ @Test
+ public void missingDestRef() throws Exception {
+ RefUpdate ru = repo.getRepository().updateRef("refs/heads/master");
+ ru.setForceUpdate(true);
+ assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ Change c = newChange(project, userId);
+ db.changes().insert(singleton(c));
+ RevCommit commit = repo.commit().create();
+ PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+ db.patchSets().insert(singleton(ps));
+
+ assertThat(checker.check(c)).containsExactly(
+ "Destination ref not found (may be new branch): master");
+ }
+
+ @Test
+ public void mergedChangeIsNotMerged() throws Exception {
+ Change c = newChange(project, userId);
+ c.setStatus(Change.Status.MERGED);
+ db.changes().insert(singleton(c));
+ RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+ db.patchSets().insert(singleton(ps));
+
+ assertThat(checker.check(c)).containsExactly(
+ "Patch set 1 (" + commit.name() + ") is not merged into destination ref"
+ + " master (" + tip.name() + "), but change status is MERGED");
+ }
+
+ @Test
+ public void newChangeIsMerged() throws Exception {
+ Change c = newChange(project, userId);
+ db.changes().insert(singleton(c));
+ RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+ .parent(tip).create();
+ PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+ db.patchSets().insert(singleton(ps));
+ repo.branch(c.getDest().get()).update(commit);
+
+ assertThat(checker.check(c)).containsExactly(
+ "Patch set 1 (" + commit.name() + ") is merged into destination ref"
+ + " master (" + commit.name() + "), but change status is NEW");
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index cfcbc7a..fa7bc259 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -24,6 +24,7 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
@@ -39,6 +40,7 @@
import com.google.inject.Injector;
import org.easymock.EasyMock;
+import org.eclipse.jgit.lib.ObjectId;
import java.util.concurrent.atomic.AtomicInteger;
@@ -66,6 +68,15 @@
return c;
}
+ public static PatchSet newPatchSet(PatchSet.Id id, ObjectId revision,
+ Account.Id userId) {
+ PatchSet ps = new PatchSet(id);
+ ps.setRevision(new RevId(revision.name()));
+ ps.setUploader(userId);
+ ps.setCreatedOn(TimeUtil.nowTs());
+ return ps;
+ }
+
public static ChangeUpdate newUpdate(Injector injector,
GitRepositoryManager repoManager, NotesMigration migration, Change c,
final AllUsersNameProvider allUsers, final IdentifiedUser user)