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
 | 
			
		||||
----
 | 
			
		||||
 | 
			
		||||
[[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 @@ path within change edit.
 | 
			
		||||
|`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]
 | 
			
		||||
 
 | 
			
		||||
@@ -204,6 +204,7 @@ java_test(
 | 
			
		||||
    '//lib:guava',
 | 
			
		||||
    '//lib:gwtorm',
 | 
			
		||||
    '//lib:junit',
 | 
			
		||||
    '//lib:truth',
 | 
			
		||||
    '//lib/guice:guice',
 | 
			
		||||
    '//lib/guice:guice-assistedinject',
 | 
			
		||||
    '//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, "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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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.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.gwtorm.server.OrmException;
 | 
			
		||||
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 @@ public class TestChanges {
 | 
			
		||||
    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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user