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
This commit is contained in:
committed by
David Pursehouse
parent
2d8580003b
commit
fd508cab05
@@ -1134,6 +1134,52 @@ Adds or updates the change in the secondary index.
|
|||||||
HTTP/1.1 204 No Content
|
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]]
|
[[edit-endpoints]]
|
||||||
== Change Edit Endpoints
|
== Change Edit Endpoints
|
||||||
|
|
||||||
@@ -3861,6 +3907,24 @@ path within change edit.
|
|||||||
|`restore_path`|optional|Path to file to restore.
|
|`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
|
GERRIT
|
||||||
------
|
------
|
||||||
Part of link:index.html[Gerrit Code Review]
|
Part of link:index.html[Gerrit Code Review]
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ java_test(
|
|||||||
'//lib:guava',
|
'//lib:guava',
|
||||||
'//lib:gwtorm',
|
'//lib:gwtorm',
|
||||||
'//lib:junit',
|
'//lib:junit',
|
||||||
|
'//lib:truth',
|
||||||
'//lib/guice:guice',
|
'//lib/guice:guice',
|
||||||
'//lib/guice:guice-assistedinject',
|
'//lib/guice:guice-assistedinject',
|
||||||
'//lib/jgit:jgit',
|
'//lib/jgit:jgit',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ public class Module extends RestApiModule {
|
|||||||
get(CHANGE_KIND, "topic").to(GetTopic.class);
|
get(CHANGE_KIND, "topic").to(GetTopic.class);
|
||||||
get(CHANGE_KIND, "in").to(IncludedIn.class);
|
get(CHANGE_KIND, "in").to(IncludedIn.class);
|
||||||
get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
|
get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
|
||||||
|
get(CHANGE_KIND, "check").to(Check.class);
|
||||||
put(CHANGE_KIND, "topic").to(PutTopic.class);
|
put(CHANGE_KIND, "topic").to(PutTopic.class);
|
||||||
delete(CHANGE_KIND, "topic").to(PutTopic.class);
|
delete(CHANGE_KIND, "topic").to(PutTopic.class);
|
||||||
delete(CHANGE_KIND).to(DeleteDraftChange.class);
|
delete(CHANGE_KIND).to(DeleteDraftChange.class);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import com.google.gerrit.reviewdb.client.Change;
|
|||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||||
import com.google.gerrit.reviewdb.client.PatchSetInfo;
|
import com.google.gerrit.reviewdb.client.PatchSetInfo;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
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.ChangeUtil;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.config.AllUsersName;
|
import com.google.gerrit.server.config.AllUsersName;
|
||||||
@@ -39,6 +40,7 @@ import com.google.gwtorm.server.OrmException;
|
|||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
|
|
||||||
import org.easymock.EasyMock;
|
import org.easymock.EasyMock;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@@ -66,6 +68,15 @@ public class TestChanges {
|
|||||||
return c;
|
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,
|
public static ChangeUpdate newUpdate(Injector injector,
|
||||||
GitRepositoryManager repoManager, NotesMigration migration, Change c,
|
GitRepositoryManager repoManager, NotesMigration migration, Change c,
|
||||||
final AllUsersNameProvider allUsers, final IdentifiedUser user)
|
final AllUsersNameProvider allUsers, final IdentifiedUser user)
|
||||||
|
|||||||
Reference in New Issue
Block a user