Do not use HTTP requests, add integration tests, etc.
These changes are required to match Google Java coding style
and run on Google Gerrit production servers.
* Replace old calls to Gerrit REST API and Json processing
with calls to Gerrit Java APIs.
* Remove mocked classes in unit tests; replace them with integration test.
* Use Google Java Coding Style and google-java-format,
with line length limit of 100 characters.
* Replace Servlet with RestReadView<ChangeResource>
* Delegate GetOwners.apply to Action.apply.
* Update rest-api.md; move path2owners and owner2paths
into dbgmsgs of REST API result.
* Remove the "server" url in returned JSON debug info.
* Fix "owner_revision" bug in returned JSON.
* Make the Cache configurable through maxCacheAge and maxCacheSize,
with default no Cache and com.google.common.cache.Cache.
* Throw exceptions instead of returning error code;
use BadRequestException.
* Remove TRACE_SERVER_MSG, reduce some debug/trace messages.
* Decouple the Parser and OwnersDb classes.
* Do without String2*, StringSet, Owner2Weights classes.
* Replace Action.Input with Action.Parameters.
* Read config parameters from find-owners.config.
* Remove Server, move methods into Action, Checker, OwnersDb.
* Use AccountCache.
Change-Id: I502e29018247c389fa9ffffd3e5fce1815a4cd08
diff --git a/BUILD b/BUILD
index 7244080..c567c29 100644
--- a/BUILD
+++ b/BUILD
@@ -24,7 +24,6 @@
manifest_entries = [
'Gerrit-PluginName: find-owners',
'Gerrit-ReloadMode: restart',
- 'Gerrit-HttpModule: com.googlesource.gerrit.plugins.findowners.Servlet',
'Gerrit-Module: com.googlesource.gerrit.plugins.findowners.Module',
'Implementation-Title: Find-Owners plugin',
'Implementation-URL: https://gerrit.googlesource.com/plugins/find-owners',
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
index 48fd7ca..f5972d6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
@@ -14,75 +14,83 @@
package com.googlesource.gerrit.plugins.findowners;
-import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
-import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.googlesource.gerrit.plugins.findowners.Util.Owner2Weights;
-import com.googlesource.gerrit.plugins.findowners.Util.String2String;
-import com.googlesource.gerrit.plugins.findowners.Util.String2StringSet;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Create and return OWNERS info when "Find Owners" button is clicked. */
-class Action implements UiAction<RevisionResource>,
- RestModifyView<RevisionResource, Action.Input> {
+class Action implements RestReadView<RevisionResource>, UiAction<RevisionResource> {
private static final Logger log = LoggerFactory.getLogger(Action.class);
- private Provider<CurrentUser> user; // is null if from HTTP request
- private String url; // from REST request or client action call
- private Server server;
+ private AccountCache accountCache;
+ private ChangeData.Factory changeDataFactory;
+ private GitRepositoryManager repoManager;
+ private Provider<CurrentUser> userProvider;
+ private SchemaFactory<ReviewDb> reviewDbProvider;
- static class Input {
- // Only the change number is required.
- int change; // Revision change number
- String debug; // REST API parameter, 1, true/false, yes
- String patchset; // REST API parameter, patchset number
-
- Input(int change, String2String params) {
- this.change = change;
- debug = params.get("debug");
- patchset = params.get("patchset");
- // other keys in params are ignored
- }
+ static class Parameters {
+ Boolean debug; // REST API "debug" parameter, or null
+ Integer patchset; // REST API "patchset" parameter, or null
}
@Inject
- Action(@PluginCanonicalWebUrl String url, Provider<CurrentUser> user) {
- this.url = Util.normalizeURL(url); // replace "http:///" with "http://"
- int n = this.url.indexOf("/plugins/");
- if (n > 0) { // remove suffix "plugins/find-owners/...."
- this.url = this.url.substring(0, n + 1);
- }
- this.user = user;
- server = new Server();
+ Action(
+ Provider<CurrentUser> userProvider,
+ SchemaFactory<ReviewDb> reviewDbProvider,
+ ChangeData.Factory changeDataFactory,
+ AccountCache accountCache,
+ GitRepositoryManager repoManager) {
+ this.userProvider = userProvider;
+ this.reviewDbProvider = reviewDbProvider;
+ this.changeDataFactory = changeDataFactory;
+ this.accountCache = accountCache;
+ this.repoManager = repoManager;
}
- /** Used by unit tests to set up mocked Server. */
- void setServer(Server s) {
- server = s;
- }
+ @VisibleForTesting
+ Action() {}
private String getUserName() {
- return (null != user) ? user.get().getUserName() : "?";
+ if (userProvider != null && userProvider.get().getUserName() != null) {
+ return userProvider.get().getUserName();
+ }
+ return "?";
}
- private JsonArray getOwners(OwnersDb db, Collection<String> files) {
- Owner2Weights weights = new Owner2Weights();
- String2StringSet file2Owners = db.findOwners(files, weights);
- JsonArray result = new JsonArray();
- StringSet emails = new StringSet();
+ private List<String> getOwners(OwnersDb db, Collection<String> files) {
+ Map<String, OwnerWeights> weights = new HashMap<>();
+ db.findOwners(files, weights);
+ List<String> result = new ArrayList<>();
+ Set<String> emails = new HashSet<>();
for (String key : OwnerWeights.sortKeys(weights)) {
if (!emails.contains(key)) {
result.add(key + " " + weights.get(key).encodeLevelCounts());
@@ -92,83 +100,118 @@
return result;
}
- private void addNamedMap(JsonObject obj, String name,
- Map<String, StringSet> map) {
- JsonObject jsonMap = new JsonObject();
- for (String key : Util.sort(map.keySet())) {
- jsonMap.addProperty(key, String.join(" ", Util.sort(map.get(key))));
+ @Override
+ public Response<RestResult> apply(RevisionResource rev)
+ throws IOException, OrmException, BadRequestException {
+ return apply(rev.getChangeResource(), new Parameters());
+ }
+
+ // Used by both Action.apply and GetOwners.apply.
+ public Response<RestResult> apply(ChangeResource rsrc, Parameters params)
+ throws IOException, OrmException, BadRequestException {
+ try (ReviewDb reviewDb = reviewDbProvider.open()) {
+ return apply(reviewDb, rsrc, params);
}
- obj.add(name, jsonMap);
+ }
+
+ // Used by integration tests, because they do not have ReviewDb Provider.
+ public Response<RestResult> apply(ReviewDb reviewDb, ChangeResource rsrc, Parameters params)
+ throws IOException, OrmException, BadRequestException {
+ Change c = rsrc.getChange();
+ try (Repository repo = repoManager.openRepository(c.getProject())) {
+ ChangeData changeData = changeDataFactory.create(reviewDb, c);
+ return getChangeData(repo, params, changeData);
+ }
+ }
+
+ /** Returns reviewer emails got from ChangeData. */
+ static List<String> getReviewers(ChangeData changeData, AccountCache accountCache) {
+ List<String> result = new ArrayList<>();
+ try {
+ for (Account.Id id : changeData.reviewers().all()) {
+ Account account = accountCache.get(id).getAccount();
+ result.add(account.getPreferredEmail() + " []");
+ }
+ } catch (OrmException e) {
+ log.error("Exception", e);
+ result = new ArrayList<>();
+ }
+ return result;
+ }
+
+ /** Returns the current patchset number or the given patchsetNum if it is valid. */
+ static int getValidPatchsetNum(ChangeData changeData, Integer patchsetNum)
+ throws OrmException, BadRequestException {
+ int patchset = changeData.currentPatchSet().getId().get();
+ if (patchsetNum != null) {
+ if (patchsetNum < 1 || patchsetNum > patchset) {
+ throw new BadRequestException(
+ "Invalid patchset parameter: "
+ + patchsetNum
+ + "; must be 1"
+ + ((1 != patchset) ? (" to " + patchset) : ""));
+ }
+ return patchsetNum;
+ }
+ return patchset;
}
/** REST API to return owners info of a change. */
- public JsonObject getChangeData(int change, String2String params) {
- return apply(null, new Input(change, params));
- }
+ public Response<RestResult> getChangeData(
+ Repository repository, Parameters params, ChangeData changeData)
+ throws OrmException, BadRequestException {
+ int patchset = getValidPatchsetNum(changeData, params.patchset);
+ OwnersDb db = Cache.getInstance().get(repository, changeData, patchset);
+ Collection<String> changedFiles = changeData.currentFilePaths();
+ Map<String, Set<String>> file2Owners = db.findOwners(changedFiles);
- /** Called by the client "Find Owners" button. */
- @Override
- public JsonObject apply(RevisionResource rev, Input input) {
- server.setChangeId(url, input.change);
- String error = (null != server.error)
- ? server.error : server.setPatchId(input.patchset);
- if (null != error) {
- JsonObject obj = new JsonObject();
- obj.addProperty("error", error);
- return obj;
- }
- OwnersDb db = server.getCachedOwnersDb();
- Collection<String> changedFiles = server.getChangedFiles();
- String2StringSet file2Owners = db.findOwners(changedFiles);
-
- JsonObject obj = new JsonObject();
- obj.addProperty(Config.MIN_OWNER_VOTE_LEVEL, server.getMinOwnerVoteLevel());
- boolean addDebugMsg = (null != input.debug)
- ? Util.parseBoolean(input.debug) : server.getAddDebugMsg();
- obj.addProperty(Config.ADD_DEBUG_MSG, addDebugMsg);
- obj.addProperty("change", input.change);
- obj.addProperty("patchset", server.patchset);
- obj.addProperty("owner_revision", db.revision);
-
+ Boolean addDebugMsg = (params.debug != null) ? params.debug : Config.getAddDebugMsg();
+ RestResult obj = new RestResult(Config.getMinOwnerVoteLevel(changeData), addDebugMsg);
+ obj.change = changeData.getId().get();
+ obj.patchset = patchset;
+ obj.ownerRevision = db.revision;
if (addDebugMsg) {
- JsonObject dbgMsgObj = new JsonObject();
- dbgMsgObj.addProperty("user", getUserName());
- dbgMsgObj.addProperty("project", server.project);
- dbgMsgObj.addProperty("branch", server.branch);
- dbgMsgObj.addProperty("server", url);
- obj.add("dbgmsgs", dbgMsgObj);
- addNamedMap(obj, "path2owners", db.path2Owners);
- addNamedMap(obj, "owner2paths", db.owner2Paths);
+ obj.dbgmsgs.user = getUserName();
+ obj.dbgmsgs.project = changeData.change().getProject().get();
+ obj.dbgmsgs.branch = changeData.change().getDest().get();
+ obj.dbgmsgs.path2owners = Util.makeSortedMap(db.path2Owners);
+ obj.dbgmsgs.owner2paths = Util.makeSortedMap(db.owner2Paths);
}
- addNamedMap(obj, "file2owners", file2Owners);
- obj.add("reviewers", server.getReviewers());
- obj.add("owners", getOwners(db, changedFiles));
- obj.add("files", Util.newJsonArrayFromStrings(changedFiles));
- return obj;
+ obj.file2owners = Util.makeSortedMap(file2Owners);
+ obj.reviewers = getReviewers(changeData, accountCache);
+ obj.owners = getOwners(db, changedFiles);
+ obj.files = new ArrayList<>(changedFiles);
+ return Response.ok(obj);
}
@Override
public Description getDescription(RevisionResource resource) {
- int change = resource.getChange().getId().get();
- server.setChangeId(url, change);
- if (null == server.branch) {
- log.error("Cannot get branch of change: " + change);
- return null; // no "Find Owners" button
+ Change change = resource.getChangeResource().getChange();
+ try (ReviewDb reviewDb = reviewDbProvider.open();
+ Repository repo = repoManager.openRepository(change.getProject())) {
+ ChangeData changeData = changeDataFactory.create(reviewDb, change);
+ if (changeData.change().getDest().get() == null) {
+ log.error("Cannot get branch of change: " + changeData.getId().get());
+ return null; // no "Find Owners" button
+ }
+ OwnersDb db = Cache.getInstance().get(repo, changeData);
+ log.trace("getDescription db key = " + db.key);
+ Status status = resource.getChange().getStatus();
+ // Commit message is not used to enable/disable "Find Owners".
+ boolean needFindOwners =
+ userProvider != null
+ && userProvider.get() instanceof IdentifiedUser
+ && (db.getNumOwners() > 0)
+ && status != Status.ABANDONED
+ && status != Status.MERGED;
+ return new Description()
+ .setLabel("Find Owners")
+ .setTitle("Find owners to add to Reviewers list")
+ .setVisible(needFindOwners);
+ } catch (IOException | OrmException e) {
+ log.error("Exception", e);
+ throw new IllegalStateException(e);
}
- OwnersDb db = server.getCachedOwnersDb();
- if (server.traceServerMsg()) {
- log.info(server.genDebugMsg(db));
- }
- Status status = server.getStatus(resource);
- // Commit message is not used to enable/disable "Find Owners".
- boolean needFindOwners =
- (null != user && user.get() instanceof IdentifiedUser)
- && (db.getNumOwners() > 0)
- && (status != Status.ABANDONED && status != Status.MERGED);
- return new Description()
- .setLabel("Find Owners")
- .setTitle("Find owners to add to Reviewers list")
- .setVisible(needFindOwners);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
index b92c790..7d4aa0c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
@@ -14,15 +14,22 @@
package com.googlesource.gerrit.plugins.findowners;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
import java.util.Collection;
-import java.util.Date;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.LinkedList;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Save OwnersDb in a cache for multiple calls to submit_filter. */
class Cache {
+ private static final Logger log = LoggerFactory.getLogger(Cache.class);
+
// The OwnersDb is created from OWNERS files in directories that
// contain changed files of a patch set, which belongs to a project
// and branch. OwnersDb can be cached if the head of a project branch
@@ -37,90 +44,92 @@
// Each submit_filter has one Prolog engine.
// It would not be enough to keep the cache in a Prolog engine environment
// or a StoredValues.
- // We keep the cache in a Java class static object for all HTTP requests.
- // OwnersDb is cached for up to 10 seconds.
- private static final int CACHE_LIFE_MSEC = 10000;
+ private static Cache instance = null; // a singleton
// When branch is "refs/heads/xyz" use only "xyz",
// to share cached OwnersDb between these two branch names.
private static final String REFS_HEADS = "refs/heads/";
- static class CachedObj {
- long time; // system time in milliseconds, when db is created
- String key; // (changeId, patchSetId, branchName)
- OwnersDb db;
- CachedObj(String k, OwnersDb x) {
- time = new Date().getTime();
- key = k;
- db = x;
+ // Until we have a way to inject Java singleton into Prolog PRED_* constructors,
+ // we keep the cache in a static object for all HTTP requests.
+ private com.google.common.cache.Cache<String, OwnersDb> dbCache;
+
+ private Cache() {
+ init(Config.getMaxCacheAge(), Config.getMaxCacheSize());
+ }
+
+ long size() {
+ return (dbCache == null) ? 0 : dbCache.size();
+ }
+
+ Cache init(int maxSeconds, int maxSize) {
+ // This should be called once in normal configuration,
+ // but could be called multiple times in unit or integration tests.
+ if (dbCache != null) {
+ dbCache.invalidateAll(); // release all cached objects
+ }
+ if (maxSeconds > 0) {
+ log.info("Initialize Cache with maxSeconds=" + maxSeconds + " maxSize=" + maxSize);
+ dbCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(maxSize)
+ .expireAfterWrite(maxSeconds, SECONDS)
+ .build();
+ } else {
+ log.info("Cache disabled.");
+ dbCache = null;
+ }
+ return this;
+ }
+
+ /** Returns a cached or new OwnersDb, for the current patchset. */
+ OwnersDb get(Repository repo, ChangeData changeData) throws OrmException {
+ return get(repo, changeData, changeData.currentPatchSet().getId().get());
+ }
+
+ /** Returns a cached or new OwnersDb, for the specified patchset. */
+ OwnersDb get(Repository repository, ChangeData changeData, int patchset) throws OrmException {
+ String branch = changeData.change().getDest().get();
+ String dbKey = Cache.makeKey(changeData.getId().get(), patchset, branch);
+ // TODO: get changed files of the given patchset?
+ return get(dbKey, repository, branch, changeData.currentFilePaths());
+ }
+
+ /** Returns a cached or new OwnersDb, for the specified branch and changed files. */
+ OwnersDb get(String key, Repository repository, String branch, Collection<String> files) {
+ if (dbCache == null) { // Do not cache OwnersDb
+ log.trace("Create new OwnersDb, key=" + key);
+ return new OwnersDb(key, repository, branch, files);
+ }
+ try {
+ log.trace("Get from cash " + dbCache + ", key=" + key + ", cache size=" + dbCache.size());
+ return dbCache.get(
+ key,
+ new Callable<OwnersDb>() {
+ @Override
+ public OwnersDb call() {
+ log.trace("Create new OwnersDb, key=" + key);
+ return new OwnersDb(key, repository, branch, files);
+ }
+ });
+ } catch (ExecutionException e) {
+ log.error("Cache.get has exception: " + e);
+ return new OwnersDb(key, repository, branch, files);
}
}
- // Before a new CachedObj is added to the tail of dbQueue,
- // old and obsolete CachedObj are removed from the head.
- private static final Deque<CachedObj> dbQueue = new LinkedList<CachedObj>();
-
- // A HashMap provides quick lookup with a key.
- private static final HashMap<String, CachedObj> dbCache =
- new HashMap<String, CachedObj>();
-
- private static long minCachedObjectTime() {
- // Cached objects must be used within CACHE_LIFE_MSEC.
- return new Date().getTime() - CACHE_LIFE_MSEC;
- }
-
- static String makeKey(int change, int patch, String branch) {
+ public static String makeKey(int change, int patch, String branch) {
if (branch.indexOf(REFS_HEADS) == 0) {
branch = branch.substring(REFS_HEADS.length());
}
return change + ":" + patch + ":" + branch;
}
- private static void saveCachedDb(String key, OwnersDb db) {
- CachedObj obj = new CachedObj(key, db);
- long minTime = minCachedObjectTime();
- synchronized (dbCache) {
- // Remove cached objects older than minTime.
- while (dbQueue.size() > 0 && dbQueue.peek().time < minTime) {
- dbCache.remove(dbQueue.peek().key);
- dbQueue.removeFirst();
- }
- // Add the new one to the tail.
- dbCache.put(key, obj);
- dbQueue.addLast(obj);
+ public static Cache getInstance() {
+ if (instance == null) {
+ instance = new Cache();
}
- }
-
- static OwnersDb get(Server server, String key, String url, String project,
- String branch, Collection<String> files) {
- return get(server, key, null, url, project, branch, files);
- }
-
- static OwnersDb get(Server server, String key, Repository repository,
- String branch, Collection<String> files) {
- return get(server, key, repository, null, null, branch, files);
- }
-
- private static OwnersDb get(
- Server server, String key, Repository repository, String url,
- String project, String branch, Collection<String> files) {
- OwnersDb db = null;
- long minTime = minCachedObjectTime();
- synchronized (dbCache) {
- if (dbCache.containsKey(key)) {
- CachedObj obj = dbCache.get(key);
- if (obj.time >= minTime) {
- db = obj.db;
- }
- }
- }
- if (null == db) {
- db = (null != repository)
- ? new OwnersDb(server, key, repository, branch, files)
- : new OwnersDb(server, key, url, project, branch, files);
- saveCachedDb(key, db);
- }
- return db;
+ return instance;
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
index 4477e17..8cc92ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
@@ -14,10 +14,20 @@
package com.googlesource.gerrit.plugins.findowners;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.AccountAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlesource.gerrit.plugins.findowners.Util.String2Integer;
-import com.googlesource.gerrit.plugins.findowners.Util.String2StringSet;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,16 +35,40 @@
public class Checker {
private static final Logger log = LoggerFactory.getLogger(Checker.class);
- private Server server;
+ // Accept both "Exempt-" and "Exempted-".
+ private static final String EXEMPT_MESSAGE1 = "Exempt-From-Owner-Approval:";
+ private static final String EXEMPT_MESSAGE2 = "Exempted-From-Owner-Approval:";
+
+ private Repository repository;
+ private ChangeData changeData;
private int minVoteLevel;
- Checker(Server s, int v) {
+ Checker(Repository repository, ChangeData changeData, int v) {
+ this.repository = repository;
+ this.changeData = changeData;
minVoteLevel = v;
- server = s; // could be a mocked server
+ }
+
+ /** Returns a map from reviewer email to vote value. */
+ static Map<String, Integer> getVotes(ChangeData changeData) throws OrmException {
+ ReviewDb db = changeData.db();
+ Map<String, Integer> map = new HashMap<>();
+ AccountAccess ac = db.accounts();
+ for (PatchSetApproval p : changeData.currentApprovals()) {
+ if (p.getValue() != 0) {
+ Account.Id id = p.getAccountId();
+ try {
+ map.put(ac.get(id).getPreferredEmail(), Integer.valueOf(p.getValue()));
+ } catch (OrmException e) {
+ log.error("Cannot get email address of account id: " + id.get(), e);
+ }
+ }
+ }
+ return map;
}
/** Returns true if some owner in owners is "*" or in votes */
- boolean findOwnersInVotes(StringSet owners, String2Integer votes) {
+ boolean findOwnersInVotes(Set<String> owners, Map<String, Integer> votes) {
boolean foundVeto = false;
boolean foundApproval = false;
for (String owner : owners) {
@@ -45,20 +79,20 @@
foundApproval |= (v >= minVoteLevel);
foundVeto |= (v < 0); // an owner's -1 vote is a veto
} else if (owner.equals("*")) {
- foundApproval = true; // no specific owner
+ foundApproval = true; // no specific owner
}
}
return foundApproval && !foundVeto;
}
/** Returns 1 if owner approval is found, -1 if missing, 0 if unneeded. */
- int findApproval(OwnersDb db) {
- String2StringSet file2Owners = db.findOwners(server.getChangedFiles());
- if (file2Owners.size() == 0) { // do not need owner approval
+ int findApproval(OwnersDb db) throws OrmException {
+ Map<String, Set<String>> file2Owners = db.findOwners(changeData.currentFilePaths());
+ if (file2Owners.size() == 0) { // do not need owner approval
return 0;
}
- String2Integer votes = server.getVotes();
- for (StringSet owners : file2Owners.values()) {
+ Map<String, Integer> votes = getVotes(changeData);
+ for (Set<String> owners : file2Owners.values()) {
if (!findOwnersInVotes(owners, votes)) {
return -1;
}
@@ -68,25 +102,46 @@
/** Returns 1 if owner approval is found, -1 if missing, 0 if unneeded. */
public static int findApproval(Prolog engine, int minVoteLevel) {
- return new Checker(new Server(engine), minVoteLevel).findApproval();
+ try {
+ ChangeData changeData = StoredValues.CHANGE_DATA.get(engine);
+ Repository repository = StoredValues.REPOSITORY.get(engine);
+ return new Checker(repository, changeData, minVoteLevel).findApproval();
+ } catch (OrmException e) {
+ log.error("Exception", e);
+ return 0; // owner approval may or may not be required.
+ }
}
- int findApproval() {
- if (server.isExemptFromOwnerApproval()) {
+ /** Returns true if exempt from owner approval. */
+ static boolean isExemptFromOwnerApproval(ChangeData changeData) throws OrmException {
+ try {
+ String message = changeData.commitMessage();
+ if (message.contains(EXEMPT_MESSAGE1) || message.contains(EXEMPT_MESSAGE2)) {
+ return true;
+ }
+ } catch (IOException | OrmException e) {
+ log.error("Cannot get commit message", e);
+ return true; // exempt from owner approval due to lack of data
+ }
+ // Abandoned and merged changes do not need approval again.
+ Status status = changeData.change().getStatus();
+ return (status == Status.ABANDONED || status == Status.MERGED);
+ }
+
+ int findApproval() throws OrmException {
+ if (isExemptFromOwnerApproval(changeData)) {
return 0;
}
// One update to a Gerrit change can call submit_rule or submit_filter
// many times. So this function should use cached values.
- OwnersDb db = server.getCachedOwnersDb();
+ OwnersDb db = Cache.getInstance().get(repository, changeData);
if (db.getNumOwners() <= 0) {
return 0;
}
if (minVoteLevel <= 0) {
- minVoteLevel = server.getMinOwnerVoteLevel();
+ minVoteLevel = Config.getMinOwnerVoteLevel(changeData);
}
- if (server.traceServerMsg()) {
- log.info(server.genDebugMsg(db));
- }
+ log.trace("findApproval db key = " + db.key);
return findApproval(db);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
index f6bb5bf..66c394c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
@@ -14,64 +14,82 @@
package com.googlesource.gerrit.plugins.findowners;
+import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** find-owners configuration parameters */
class Config {
// Name of config parameters and plugin.
+ static final String SECTION = "findowners"; // used in Plugin config file
static final String ADD_DEBUG_MSG = "addDebugMsg";
static final String MIN_OWNER_VOTE_LEVEL = "minOwnerVoteLevel";
+ static final String MAX_CACHE_AGE = "maxCacheAge"; // seconds to stay in cache
+ static final String MAX_CACHE_SIZE = "maxCacheSize"; // number of OwnersDb in cache
static final String REPORT_SYNTAX_ERROR = "reportSyntaxError";
static final String PLUGIN_NAME = "find-owners";
static final String PROLOG_NAMESPACE = "find_owners";
- // Enable TRACE_SERVER_MSG only for dev/test builds.
- private static final boolean TRACE_SERVER_MSG = false;
-
// Global/plugin config parameters.
private static PluginConfigFactory config = null;
private static boolean addDebugMsg = false;
private static int minOwnerVoteLevel = 1;
+ private static int maxCacheAge = 0;
+ private static int maxCacheSize = 1000;
private static boolean reportSyntaxError = false;
private static final Logger log = LoggerFactory.getLogger(Config.class);
- static void setVariables(PluginConfigFactory conf,
- boolean dbgMsg, int voteLevel, boolean reportError) {
- if (TRACE_SERVER_MSG) {
- log.info("Set config parameters "
- + ADD_DEBUG_MSG + "=" + dbgMsg
- + ", " + MIN_OWNER_VOTE_LEVEL + "=" + voteLevel
- + ", " + REPORT_SYNTAX_ERROR + "=" + reportError);
- }
+ static void setVariables(
+ PluginConfigFactory conf, PluginConfig gc, org.eclipse.jgit.lib.Config pc) {
+ // Get config variables from pc, or from gc.
config = conf;
- addDebugMsg = dbgMsg;
- minOwnerVoteLevel = voteLevel;
- reportSyntaxError = reportError;
- }
-
- static boolean traceServerMsg() {
- return TRACE_SERVER_MSG && addDebugMsg;
+ addDebugMsg =
+ pc.getBoolean(SECTION, null, ADD_DEBUG_MSG, gc.getBoolean(Config.ADD_DEBUG_MSG, false));
+ reportSyntaxError =
+ pc.getBoolean(
+ SECTION, null, REPORT_SYNTAX_ERROR, gc.getBoolean(REPORT_SYNTAX_ERROR, false));
+ minOwnerVoteLevel =
+ pc.getInt(SECTION, null, MIN_OWNER_VOTE_LEVEL, gc.getInt(MIN_OWNER_VOTE_LEVEL, 1));
+ maxCacheAge = pc.getInt(SECTION, null, MAX_CACHE_AGE, gc.getInt(MAX_CACHE_AGE, 0));
+ maxCacheSize = pc.getInt(SECTION, null, MAX_CACHE_SIZE, gc.getInt(MAX_CACHE_SIZE, 1000));
}
static boolean getAddDebugMsg() {
return addDebugMsg; // defined globally, not per-project
}
+ static int getMaxCacheAge() {
+ return maxCacheAge;
+ }
+
+ static int getMaxCacheSize() {
+ return maxCacheSize;
+ }
+
static boolean getReportSyntaxError() {
return reportSyntaxError;
}
- static int getMinOwnerVoteLevel(Project.NameKey project) {
+ @VisibleForTesting
+ static void setReportSyntaxError(boolean value) {
+ reportSyntaxError = value;
+ }
+
+ static int getMinOwnerVoteLevel(ChangeData changeData) throws OrmException {
+ Project.NameKey project = changeData.change().getProject();
try {
- return (null == config || null == project) ? minOwnerVoteLevel
- : config.getFromProjectConfigWithInheritance(
- project, PLUGIN_NAME).getInt(MIN_OWNER_VOTE_LEVEL,
- minOwnerVoteLevel);
+ return (config == null || project == null)
+ ? minOwnerVoteLevel
+ : config
+ .getFromProjectConfigWithInheritance(project, PLUGIN_NAME)
+ .getInt(MIN_OWNER_VOTE_LEVEL, minOwnerVoteLevel);
} catch (NoSuchProjectException e) {
log.error("Cannot find project: " + project);
return minOwnerVoteLevel;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/GetChange.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/GetChange.java
deleted file mode 100644
index 244fd22..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/GetChange.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import com.google.gerrit.extensions.annotations.Export;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonObject;
-import com.google.inject.Singleton;
-import com.googlesource.gerrit.plugins.findowners.Util.String2String;
-import java.io.IOException;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/** Serves the HTTP GET /change/id request. */
-@Export("/change/*")
-@Singleton
-class GetChange extends HttpServlet {
- private static final Logger log = LoggerFactory.getLogger(GetChange.class);
-
- private static final String MAGIC_PREFIX = ")]}'"; // Gerrit REST specific
-
- private int getChangeId(String url) {
- // Expect url=".../plugins/find-owners/change/<digits>"
- final Pattern patChangeId =
- Pattern.compile("^.*/plugins/" + Config.PLUGIN_NAME
- + "/change/([0-9]+)/?$");
- Matcher m = patChangeId.matcher(url);
- return m.find() ? Integer.parseInt(m.group(1)) : 0;
- }
-
- private String2String parseParameters(HttpServletRequest req) {
- Map<String, String[]> map = req.getParameterMap();
- String2String params = new String2String();
- for (Map.Entry<String, String[]> entry : map.entrySet()) {
- String[] value = entry.getValue();
- // Use only the last definition, if there are multiple.
- params.put(entry.getKey(), value[value.length - 1]);
- }
- return params;
- }
-
- protected void doGet(HttpServletRequest req, HttpServletResponse res)
- throws IOException {
- String reqURL = req.getRequestURL().toString();
- // e.g. http://localhost:8082/plugins/find-owners/change/36
- String localAddress = req.getLocalAddr(); // e.g. 100.108.228.206
- int localPort = req.getLocalPort(); // e.g. 8080, not client port number
- String2String params = parseParameters(req);
- String url = "http://" + localAddress + ":" + localPort + "/";
- // TODO: recognize pp=0 parameter and Accept HTTP request header
- // to output compact JSON.
- int changeId = getChangeId(reqURL);
- if (Config.traceServerMsg()) {
- String paramsDump = "";
- for (Map.Entry<String, String> entry : params.entrySet()) {
- paramsDump += " " + entry.getKey() + "=" + entry.getValue();
- }
- log.info("goGet reqURL=" + reqURL
- + ", address=" + localAddress + ", port=" + localPort
- + ", changeId=" + changeId + ", params:" + paramsDump);
- }
- createResponse(url, params, changeId, res);
- }
-
- private void createErrorResponse(HttpServletResponse res,
- String msg) throws IOException {
- res.setContentType("text/plain");
- res.getWriter().println("Error: " + msg);
- }
-
- private void createResponse(
- String url, String2String params, int changeId,
- HttpServletResponse res) throws IOException {
- res.setCharacterEncoding("UTF-8");
- if (changeId > 0) {
- Action finder = new Action(url, null);
- JsonObject obj = finder.getChangeData(changeId, params);
- if (null != obj.get("error")) {
- createErrorResponse(res, obj.get("error").getAsString());
- } else {
- // Current prototype always returns pretty-printed JSON.
- // TODO: recognize HTTP Accept-Encoding request header "gzip",
- // to gzip compress the response.
- Gson gs = new GsonBuilder()
- .setPrettyPrinting().disableHtmlEscaping().create();
- res.setContentType("application/json");
- res.getWriter().println(MAGIC_PREFIX);
- res.getWriter().println(gs.toJson(obj));
- }
- } else {
- createErrorResponse(res,
- "Missing change number.\n"
- + "Usage: <baseURL>/plugins/"
- + Config.PLUGIN_NAME + "/change/<changeId>");
- }
- }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
new file mode 100644
index 0000000..bbf5619
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
+
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** REST API to get owners of a change. */
+public class GetOwners implements RestReadView<ChangeResource> {
+ private static final Logger log = LoggerFactory.getLogger(GetOwners.class);
+
+ private Action action;
+
+ // "debug" could be true/yes/1 or false/no/0,
+ // when not specified configuration variable "addDebugMsg" is used.
+ @Option(name = "--debug", usage = "get extra debug info")
+ private String debug;
+
+ @Option(name = "--patchset", usage = "select change patchset number")
+ private Integer patchset;
+
+ @Inject
+ GetOwners(
+ Provider<CurrentUser> userProvider,
+ SchemaFactory<ReviewDb> reviewDbProvider,
+ ChangeData.Factory dataFactory,
+ AccountCache accountCache,
+ GitRepositoryManager repoManager) {
+ this.action =
+ new Action(userProvider, reviewDbProvider, dataFactory, accountCache, repoManager);
+ }
+
+ @Override
+ public Response<RestResult> apply(ChangeResource rsrc) throws IOException, OrmException {
+ Action.Parameters params = new Action.Parameters();
+ params.patchset = patchset;
+ params.debug = (debug != null) ? Util.parseBoolean(debug) : null;
+ try {
+ return this.action.apply(rsrc, params);
+ } catch (BadRequestException e) {
+ // Catch this exception to avoid too many call stack dumps
+ // from bad wrong client requests.
+ log.error("Exception: " + e);
+ return Response.none();
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java
index 151ccba..13a3b5d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java
@@ -14,25 +14,23 @@
package com.googlesource.gerrit.plugins.findowners;
+import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.extensions.annotations.Listen;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.extensions.webui.JavaScriptPlugin;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.rules.PredicateProvider;
-import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
/** find-owners plugin module */
public class Module extends AbstractModule {
- /** Prolog Predicate Provider. */
- @Listen
+ /** Prolog Predicate Provider. */
static class FindOwnersProvider implements PredicateProvider {
@Override
public ImmutableSet<String> getPackages() {
@@ -42,26 +40,25 @@
@Inject
public Module(@PluginName String pluginName, PluginConfigFactory config) {
- // The 'true' parameter does not seem to reread gerrit.config on restart.
- PluginConfig c = config.getFromGerritConfig(pluginName, true);
- // TODO: get config parameters from plugin config file.
- Config.setVariables(config,
- c.getBoolean(Config.ADD_DEBUG_MSG, false),
- c.getInt(Config.MIN_OWNER_VOTE_LEVEL, 1),
- c.getBoolean(Config.REPORT_SYNTAX_ERROR, false));
+ Config.setVariables(
+ config,
+ config.getFromGerritConfig(pluginName, true),
+ config.getGlobalPluginConfig(pluginName));
+ Cache.getInstance(); // Create a single Cache.
}
@Override
protected void configure() {
- install(new RestApiModule() {
- @Override
- protected void configure() {
- post(REVISION_KIND, Config.PLUGIN_NAME).to(Action.class);
- }
- });
+ install(
+ new RestApiModule() {
+ @Override
+ protected void configure() {
+ get(CHANGE_KIND, "owners").to(GetOwners.class);
+ get(REVISION_KIND, Config.PLUGIN_NAME).to(Action.class);
+ }
+ });
DynamicSet.bind(binder(), WebUiPlugin.class)
.toInstance(new JavaScriptPlugin(Config.PLUGIN_NAME + ".js"));
- DynamicSet.bind(binder(), PredicateProvider.class)
- .to(FindOwnersProvider.class);
+ DynamicSet.bind(binder(), PredicateProvider.class).to(FindOwnersProvider.class);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnerWeights.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnerWeights.java
index 5664349..d57304b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnerWeights.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnerWeights.java
@@ -14,33 +14,35 @@
package com.googlesource.gerrit.plugins.findowners;
-import com.googlesource.gerrit.plugins.findowners.Util.Owner2Weights;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Keep owned files and count number of files at control level 1, 2, 3, etc.
*
- * <p>A source file can be owned by multiple OWNERS file in its directory or
- * parent directories. The owners listed in the lowest OWNERS file has
- * level 1 control of that source file. The 2nd lowest OWNERS file has
- * level 2 control, etc.
+ * <p>A source file can be owned by multiple OWNERS file in its directory or parent directories. The
+ * owners listed in the lowest OWNERS file has level 1 control of that source file. The 2nd lowest
+ * OWNERS file has level 2 control, etc.
+ *
* <p>An owner can own multiple source files at different control level.
- * <p>Each owner has an OwnerWeights object to keep
- * (0) the set of owned files,
- * (1) number of owned files with level 1 control,
- * (2) number of owned files with level 2 control,
- * (3) number of owned files with level 3 or higher control,
+ *
+ * <p>Each owner has an OwnerWeights object to keep (0) the set of owned files, (1) number of owned
+ * files with level 1 control, (2) number of owned files with level 2 control, (3) number of owned
+ * files with level 3 or higher control,
*/
class OwnerWeights {
static class WeightComparator implements Comparator<String> {
- private Owner2Weights map;
- WeightComparator(Owner2Weights weights) {
+ private Map<String, OwnerWeights> map;
+
+ WeightComparator(Map<String, OwnerWeights> weights) {
map = weights;
}
+
@Override
public int compare(String k1, String k2) {
OwnerWeights w1 = map.get(k1);
@@ -52,31 +54,21 @@
}
}
- StringSet files; /** paths of owned files */
- int countL1; /** number of files with control level 1 */
- int countL2; /** number of files with control level 2 */
- int countL3; /** number of files with control level 3 or more */
+ Set<String> files = new HashSet<>(); // paths of owned files
+ int countL1; // number of files with control level 1
+ int countL2; // number of files with control level 2
+ int countL3; // number of files with control level 3 or more
/** Return file counters as a compact string. */
String encodeLevelCounts() {
return "[" + countL1 + "+" + countL2 + "+" + countL3 + "]";
}
- private void init() {
- files = new StringSet();
- countL1 = 0;
- countL2 = 0;
- countL3 = 0;
- }
-
OwnerWeights(String file, int level) {
- init();
addFile(file, level);
}
- OwnerWeights() {
- init();
- }
+ OwnerWeights() {}
void addFile(String path, int level) {
// If a file is added multiple times,
@@ -94,8 +86,8 @@
}
/** Sort keys in weights map by control levels, and return keys. */
- static List<String> sortKeys(Owner2Weights weights) {
- ArrayList<String> keys = new ArrayList(weights.keySet());
+ static List<String> sortKeys(Map<String, OwnerWeights> weights) {
+ List<String> keys = new ArrayList<>(weights.keySet());
Collections.sort(keys, new WeightComparator(weights));
return keys;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
index 503da41..04a087a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
@@ -14,16 +14,24 @@
package com.googlesource.gerrit.plugins.findowners;
-import com.googlesource.gerrit.plugins.findowners.Util.Owner2Weights;
-import com.googlesource.gerrit.plugins.findowners.Util.String2StringSet;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,40 +39,45 @@
class OwnersDb {
private static final Logger log = LoggerFactory.getLogger(OwnersDb.class);
- private Server server; // could be set to a mocked server in unit tests
+ private int numOwners = -1; // # of owners of all given files.
- private int numOwners; // # of owners of all given files.
+ String key = ""; // key to find this OwnersDb in a cache.
+ String revision = ""; // tip of branch revision, where OWENRS were found.
+ Map<String, Set<String>> dir2Globs = new HashMap<>(); // directory to file globs in the directory
+ Map<String, Set<String>> owner2Paths = new HashMap<>(); // owner email to owned dirs or file globs
+ Map<String, Set<String>> path2Owners = new HashMap<>(); // dir or file glob to owner emails
+ Set<String> readDirs = new HashSet<>(); // directories in which we have checked OWNERS
+ Set<String> stopLooking = new HashSet<>(); // directories where OWNERS has "set noparent"
- String revision; // tip of branch revision, where OWENRS were found.
- String2StringSet dir2Globs; // (directory) => file globs in the directory
- String2StringSet owner2Paths; // (owner email) => owned dirs or file globs
- String2StringSet path2Owners; // (directory or file glob) => owner emails
- StringSet readDirs; // directories in which we have checked OWNERS
- StringSet stopLooking; // directories where OWNERS has "set noparent"
+ OwnersDb() {}
- private void init(Server s) {
- numOwners = -1;
- revision = "";
- dir2Globs = new String2StringSet();
- owner2Paths = new String2StringSet();
- path2Owners = new String2StringSet();
- readDirs = new StringSet();
- stopLooking = new StringSet();
- server = (null != s) ? s : new Server();
- }
-
- OwnersDb(Server s) {
- init(s);
- }
-
- OwnersDb(Server s, String key, Repository repository,
- String branch, Collection<String> files) {
- init(s, key, repository, null, null, branch, files);
- }
-
- OwnersDb(Server s, String key, String url, String project,
- String branch, Collection<String> files) {
- init(s, key, null, url, project, branch, files);
+ OwnersDb(String key, Repository repository, String branch, Collection<String> files) {
+ this.key = key;
+ for (String fileName : files) {
+ // Find OWNERS in fileName's directory and parent directories.
+ // Stop looking for a parent directory if OWNERS has "set noparent".
+ fileName = Util.normalizedFilePath(fileName);
+ String dir = Util.normalizedDirPath(fileName); // e.g. dir = ./d1/d2
+ while (!readDirs.contains(dir)) {
+ readDirs.add(dir);
+ String filePath = (dir + "/OWNERS").substring(2); // remove "./"
+ String content = getRepositoryFile(repository, branch, filePath);
+ if (content != null && !content.equals("")) {
+ addFile(dir + "/", dir + "/OWNERS", content.split("\\R+"));
+ }
+ if (stopLooking.contains(dir + "/") || !dir.contains("/")) {
+ break; // stop looking through parent directory
+ }
+ dir = Util.getDirName(dir); // go up one level
+ }
+ }
+ countNumOwners(files);
+ try {
+ revision = repository.getRef(branch).getObjectId().getName();
+ } catch (IOException e) {
+ log.error("Fail to get branch revision", e);
+ revision = "";
+ }
}
int getNumOwners() {
@@ -72,57 +85,54 @@
}
private void countNumOwners(Collection<String> files) {
- String2StringSet file2Owners = findOwners(files, null);
- if (null != file2Owners) {
- StringSet emails = new StringSet();
- for (String key : file2Owners.keySet()) {
- for (String owner : file2Owners.get(key)) {
- emails.add(owner);
- }
- }
+ Map<String, Set<String>> file2Owners = findOwners(files, null);
+ if (file2Owners != null) {
+ Set<String> emails = new HashSet<>();
+ file2Owners.values().forEach(emails::addAll);
numOwners = emails.size();
} else {
numOwners = owner2Paths.keySet().size();
}
}
- private static void addToMap(String2StringSet map,
- String key, String value) {
- if (null == map.get(key)) {
- map.put(key, new StringSet());
- }
- map.get(key).add(value);
- }
-
void addOwnerPathPair(String owner, String path) {
- addToMap(owner2Paths, owner, path);
- addToMap(path2Owners, path, owner);
+ Util.addToMap(owner2Paths, owner, path);
+ Util.addToMap(path2Owners, path, owner);
if (path.length() > 0 && path.charAt(path.length() - 1) != '/') {
- addToMap(dir2Globs, Util.getDirName(path) + "/", path); // A file glob.
+ Util.addToMap(dir2Globs, Util.getDirName(path) + "/", path); // A file glob.
}
}
- void addFile(String path, String file, String[] lines) {
- int n = 0;
- for (String line : lines) {
- String error = Parser.parseLine(this, path, file, line, ++n);
- if (null != error && server.getReportSyntaxError()) {
- log.warn(error);
+ void addFile(String dirPath, String filePath, String[] lines) {
+ Parser.Result result = Parser.parseFile(dirPath, filePath, lines);
+ if (result.stopLooking) {
+ stopLooking.add(dirPath);
+ }
+ for (String owner : result.owner2paths.keySet()) {
+ for (String path : result.owner2paths.get(owner)) {
+ addOwnerPathPair(owner, path);
}
}
+ if (Config.getReportSyntaxError()) {
+ result.warnings.forEach(w -> log.warn(w));
+ result.errors.forEach(w -> log.error(w));
+ }
}
private void addOwnerWeights(
- ArrayList<String> paths, ArrayList<Integer> distances,
- String file, String2StringSet file2Owners, Owner2Weights map) {
+ ArrayList<String> paths,
+ ArrayList<Integer> distances,
+ String file,
+ Map<String, Set<String>> file2Owners,
+ Map<String, OwnerWeights> map) {
for (int i = 0; i < paths.size(); i++) {
- StringSet owners = path2Owners.get(paths.get(i));
- if (null == owners) {
+ Set<String> owners = path2Owners.get(paths.get(i));
+ if (owners == null) {
continue;
}
for (String name : owners) {
- addToMap(file2Owners, file, name);
- if (null == map) {
+ Util.addToMap(file2Owners, file, name);
+ if (map == null) {
continue;
}
if (map.containsKey(name)) {
@@ -135,24 +145,23 @@
}
/** Quick method to find owner emails of every file. */
- String2StringSet findOwners(Collection<String> files) {
+ Map<String, Set<String>> findOwners(Collection<String> files) {
return findOwners(files, null);
}
/** Returns owner emails of every file and set up ownerWeights. */
- String2StringSet findOwners(Collection<String> files,
- Owner2Weights ownerWeights) {
+ Map<String, Set<String>> findOwners(
+ Collection<String> files, Map<String, OwnerWeights> ownerWeights) {
return findOwners(files.toArray(new String[0]), ownerWeights);
}
/** Returns true if path has '*' owner. */
- private boolean findStarOwner(String path, int distance,
- ArrayList<String> paths,
- ArrayList<Integer> distances) {
- StringSet owners = path2Owners.get(path);
- if (null != owners) {
+ private boolean findStarOwner(
+ String path, int distance, ArrayList<String> paths, ArrayList<Integer> distances) {
+ Set<String> owners = path2Owners.get(path);
+ if (owners != null) {
paths.add(path);
- distances.add(new Integer(distance));
+ distances.add(distance);
if (owners.contains("*")) {
return true;
}
@@ -161,12 +170,12 @@
}
/** Returns owner emails of every file and set up ownerWeights. */
- String2StringSet findOwners(String[] files, Owner2Weights ownerWeights) {
+ Map<String, Set<String>> findOwners(String[] files, Map<String, OwnerWeights> ownerWeights) {
// Returns a map of file to set of owner emails.
// If ownerWeights is not null, add to it owner to distance-from-dir;
// a distance of 1 is the lowest/closest possible distance
// (which makes the subsequent math easier).
- String2StringSet file2Owners = new String2StringSet();
+ Map<String, Set<String>> file2Owners = new HashMap<>();
for (String fileName : files) {
fileName = Util.normalizedFilePath(fileName);
String dirPath = Util.normalizedDirPath(fileName);
@@ -175,13 +184,13 @@
FileSystem fileSystem = FileSystems.getDefault();
// Collect all matched (path, distance) in all OWNERS files for
// fileName. Add them only if there is no special "*" owner.
- ArrayList<String> paths = new ArrayList<String>();
- ArrayList<Integer> distances = new ArrayList<Integer>();
+ ArrayList<String> paths = new ArrayList<>();
+ ArrayList<Integer> distances = new ArrayList<>();
boolean foundStar = false;
while (true) {
int savedSizeOfPaths = paths.size();
if (dir2Globs.containsKey(dirPath + "/")) {
- StringSet patterns = dir2Globs.get(dirPath + "/");
+ Set<String> patterns = dir2Globs.get(dirPath + "/");
for (String pat : patterns) {
PathMatcher matcher = fileSystem.getPathMatcher("glob:" + pat);
if (matcher.matches(Paths.get(dirPath + "/" + baseName))) {
@@ -201,45 +210,33 @@
}
if (foundStar // This file can be approved by anyone, no owner.
|| stopLooking.contains(dirPath + "/") // stop looking parent
- || !dirPath.contains("/") /* root */ ) {
+ || !dirPath.contains("/") /* root */) {
break;
}
if (paths.size() != savedSizeOfPaths) {
- distance++; // increase distance for each found OWNERS
+ distance++; // increase distance for each found OWNERS
}
dirPath = Util.getDirName(dirPath); // go up one level
}
if (!foundStar) {
- addOwnerWeights(paths, distances, fileName,
- file2Owners, ownerWeights);
+ addOwnerWeights(paths, distances, fileName, file2Owners, ownerWeights);
}
}
return file2Owners;
}
- private void init(
- Server s, String key, Repository repository, String url,
- String project, String branch, Collection<String> files) {
- init(s);
- for (String fileName : files) {
- // Find OWNERS in fileName's directory and parent directories.
- // Stop looking for a parent directory if OWNERS has "set noparent".
- fileName = Util.normalizedFilePath(fileName);
- String dir = Util.normalizedDirPath(fileName);
- while (!readDirs.contains(dir)) {
- readDirs.add(dir);
- String content = server.getOWNERS(dir, repository, url,
- project, branch);
- if (null != content && !content.equals("")) {
- addFile(dir + "/", dir + "/OWNERS", content.split("\\R+"));
- }
- if (stopLooking.contains(dir + "/") || !dir.contains("/")) {
- break; // stop looking through parent directory
- }
- dir = Util.getDirName(dir); // go up one level
+ /** Returns file content or empty string; uses Repository. */
+ static String getRepositoryFile(Repository repo, String branch, String file) {
+ try (RevWalk revWalk = new RevWalk(repo)) {
+ RevTree tree = revWalk.parseCommit(repo.resolve(branch)).getTree();
+ ObjectReader reader = revWalk.getObjectReader();
+ TreeWalk treeWalk = TreeWalk.forPath(reader, file, tree);
+ if (treeWalk != null) {
+ return new String(reader.open(treeWalk.getObjectId(0)).getBytes(), UTF_8);
}
+ } catch (IOException e) {
+ log.error("get file " + file, e);
}
- countNumOwners(files);
- revision = server.getBranchRevision(repository, url, project, branch);
+ return "";
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
index e1dfa8a..57f137d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
@@ -14,15 +14,20 @@
package com.googlesource.gerrit.plugins.findowners;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Parse lines in an OWNERS file and put them into an OwnersDb.
*
- * <p>OWNERS file syntax:<pre>
+ * <p>OWNERS file syntax:
+ *
+ * <pre>
* lines := (\s* line? \s* "\n")*
* line := "set noparent"
* | "per-file" \s+ glob \s* "=" \s* directive
@@ -34,67 +39,83 @@
* glob := [a-zA-Z0-9_-*?]+
* comment := "#" [^"\n"]*
* </pre>
- * <p> The "file:" directive is not implemented yet.
- * <p> "per-file glob = directive" applies directive only to files
- * matching glob. glob does not contain directory path.
+ *
+ * <p>The "file:" directive is not implemented yet.
+ *
+ * <p>"per-file glob = directive" applies directive only to files matching glob. glob does not
+ * contain directory path.
*/
class Parser {
- private static final Logger log = LoggerFactory.getLogger(Parser.class);
-
- static final Pattern PatComment = Pattern.compile("^ *(#.*)?$");
+ static final Pattern patComment = Pattern.compile("^ *(#.*)?$");
// TODO: have a more precise email address pattern.
- static final Pattern PatEmail = // email address or a "*"
+ static final Pattern patEmail = // email address or a "*"
Pattern.compile("^ *([^ <>@]+@[^ <>@#]+|\\*) *(#.*)?$");
- static final Pattern PatFile = Pattern.compile("^ *file:.*$");
- static final Pattern PatNoParent =
- Pattern.compile("^ *set +noparent(( |#).*)?$");
- static final Pattern PatPerFile =
- Pattern.compile("^ *per-file +([^= ]+) *= *([^ #]+).*$");
+ static final Pattern patFile = Pattern.compile("^ *file:.*$");
+ static final Pattern patNoParent = Pattern.compile("^ *set +noparent(( |#).*)?$");
+ static final Pattern patPerFile = Pattern.compile("^ *per-file +([^= ]+) *= *([^#]+).*$");
+
+ static class Result {
+ boolean stopLooking; // if this file contains set noparent
+ List<String> warnings; // warning messages
+ List<String> errors; // error messages
+ Map<String, Set<String>> owner2paths; // maps from owner email to pathGlobs
+
+ Result() {
+ stopLooking = false;
+ warnings = new ArrayList<>();
+ errors = new ArrayList<>();
+ owner2paths = new HashMap<>();
+ }
+ }
+
+ static Result parseFile(String dir, String file, String[] lines) {
+ Result result = new Result();
+ int n = 0;
+ for (String line : lines) {
+ Parser.parseLine(result, dir, file, line, ++n);
+ }
+ return result;
+ }
/**
- * Parse a line in OWNERS file and add info to OwnersDb.
+ * Parse a line in OWNERS file and add info to OwnersDb.
*
- * @param db an OwnersDb to keep parsed info.
- * @param path the path of OWNERS file.
- * @param file the OWNERS file path.
- * @param line the source line.
- * @param num the line number.
- * @return error message string or null.
+ * @param result a Result object to keep parsed info.
+ * @param dir the path to OWNERS file directory.
+ * @param file the OWNERS file path.
+ * @param line the source line.
+ * @param num the line number.
*/
- static String parseLine(OwnersDb db, String path,
- String file, String line, int num) {
+ static void parseLine(Result result, String dir, String file, String line, int num) {
// comment and file: directive are parsed but ignored.
- if (PatNoParent.matcher(line).find()) {
- db.stopLooking.add(path);
- return null;
- }
- if (PatPerFile.matcher(line).find()) {
- Matcher m = PatPerFile.matcher(line);
+ if (patNoParent.matcher(line).find()) {
+ result.stopLooking = true;
+ } else if (patPerFile.matcher(line).find()) {
+ Matcher m = patPerFile.matcher(line);
m.find();
- return parseDirective(db, path + m.group(1), file, m.group(2), num);
+ parseDirective(result, dir + m.group(1), file, m.group(2).trim(), num);
+ } else if (patFile.matcher(line).find()) {
+ result.warnings.add(warningMsg(file, num, "ignored", line));
+ } else if (patComment.matcher(line).find()) {
+ // ignore comment and empty lines.
+ } else {
+ parseDirective(result, dir, file, line, num);
}
- if (PatFile.matcher(line).find()) {
- return warningMsg(file, num, "ignored", line);
- }
- // ignore comment and empty lines.
- return (PatComment.matcher(line).find())
- ? null : parseDirective(db, path, file, line, num);
}
- private static String parseDirective(OwnersDb db, String pathGlob,
- String file, String line, int num) {
+ private static void parseDirective(
+ Result result, String pathGlob, String file, String line, int num) {
// A directive is an email address or "*".
- if (PatEmail.matcher(line).find()) {
- Matcher m = PatEmail.matcher(line);
+ if (patEmail.matcher(line).find()) {
+ Matcher m = patEmail.matcher(line);
m.find();
- db.addOwnerPathPair(m.group(1), pathGlob);
- return null;
+ Util.addToMap(result.owner2paths, m.group(1), pathGlob);
+ } else {
+ result.errors.add(errorMsg(file, num, "ignored unknown line", line));
}
- return errorMsg(file, num, "ignored unknown line", line);
}
- private static String createMsgLine(
- String prefix, String file, int n, String msg, String line) {
+ private static String createMsgLine(String prefix, String file, int n, String msg, String line) {
return prefix + file + ":" + n + ": " + msg + ": [" + line + "]";
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java
new file mode 100644
index 0000000..b0b227c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/** REST API return data Java class. */
+public class RestResult {
+ @SerializedName("minOwnerVoteLevel")
+ int minOwnerVoteLevel;
+
+ @SerializedName("addDebugMsg")
+ boolean addDebugMsg;
+
+ int change;
+ int patchset;
+
+ @SerializedName("owner_revision")
+ String ownerRevision;
+
+ DebugMessages dbgmsgs;
+ SortedMap<String, String> file2owners = new TreeMap<>();
+ List<String> reviewers = new ArrayList<>();
+ List<String> owners = new ArrayList<>();
+ List<String> files = new ArrayList<>();
+
+ RestResult(int voteLevel, boolean addDebugMsg) {
+ minOwnerVoteLevel = voteLevel;
+ this.addDebugMsg = addDebugMsg;
+ if (addDebugMsg) {
+ dbgmsgs = new DebugMessages();
+ dbgmsgs.path2owners = new TreeMap<>();
+ dbgmsgs.owner2paths = new TreeMap<>();
+ }
+ }
+
+ static class DebugMessages {
+ String user;
+ String project;
+ String branch;
+ SortedMap<String, String> path2owners;
+ SortedMap<String, String> owner2paths;
+ };
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Server.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Server.java
deleted file mode 100644
index 98a75aa..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Server.java
+++ /dev/null
@@ -1,304 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.AccountAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gwtorm.server.OrmException;
-import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlesource.gerrit.plugins.findowners.Util.String2Integer;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
-import java.io.IOException;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Map;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.filter.PathFilter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/** Wrapper of Gerrit server functions. */
-class Server {
- // Usage: construct one instance and init for each change id.
- private static final Logger log = LoggerFactory.getLogger(Server.class);
-
- // Accept both "Exempt-" and "Exempted-".
- private static final String EXEMPT_MESSAGE1 = "Exempt-From-Owner-Approval:";
- private static final String EXEMPT_MESSAGE2 = "Exempted-From-Owner-Approval:";
-
- private Prolog engine; // Gerrit Prolog engine
- String url; // Gerrit server URL, could be changed in mocked server
-
- int change;
- int patchset; // patchset number
- String project;
- String branch;
- String error; // default init to null
- Collection<PatchSetApproval> approvals; // only used with Prolog engine
-
- Server() {}
-
- Server(Prolog p) {
- engine = p;
- ChangeData data = StoredValues.CHANGE_DATA.get(engine);
- change = data.getId().get();
- try {
- patchset = data.currentPatchSet().getId().get();
- } catch (OrmException e) {
- log.error("Cannot get patchset: " + e);
- patchset = 1;
- }
- Change c = StoredValues.getChange(engine);
- project = c.getProject().get();
- // NOTE: repository.getBranch() returns incorrect "master"
- branch = c.getDest().get(); // e.g. "refs/heads/BetaBranch"
- try {
- approvals = data.currentApprovals();
- } catch (OrmException e) {
- log.error("Cannot get approvals: " + e);
- approvals = new ArrayList<PatchSetApproval>();
- }
- }
-
- int getMinOwnerVoteLevel() {
- return Config.getMinOwnerVoteLevel(new Project.NameKey(project));
- }
-
- boolean getAddDebugMsg() {
- return Config.getAddDebugMsg();
- }
-
- boolean traceServerMsg() {
- return Config.traceServerMsg();
- }
-
- boolean getReportSyntaxError() {
- return Config.getReportSyntaxError();
- }
-
- /** Returns a revision's change review status. */
- Status getStatus(RevisionResource resource) {
- return resource.getChange().getStatus();
- }
-
- /** Sets change number; retrieves other parameters from REST API. */
- void setChangeId(String url, int change) {
- this.url = url;
- this.change = change;
- String request = url + "changes/?q=" + change + "&o=CURRENT_REVISION";
- JsonArray arr = Util.getHTTPJsonArray(request);
- if (null == arr || arr.size() != 1) {
- error = "Failed request: " + request;
- return;
- }
- JsonObject obj = arr.get(0).getAsJsonObject();
- project = obj.get("project").getAsString();
- branch = obj.get("branch").getAsString();
- String revisionString = obj.get("current_revision").getAsString();
- JsonObject revisions = obj.get("revisions").getAsJsonObject();
- JsonObject revInfo = revisions.get(revisionString).getAsJsonObject();
- patchset = revInfo.get("_number").getAsInt();
- }
-
- /** Returns error message if patchsetNum has invalid value. */
- String setPatchId(String patchsetNum) {
- if (null != patchsetNum) {
- int n = Integer.parseInt(patchsetNum);
- if (n < 1 || n > patchset) {
- return "Invalid patchset parameter: " + patchsetNum + "; must be 1"
- + ((1 != patchset) ? (" to " + patchset) : "");
- }
- patchset = n;
- }
- return null;
- }
-
- /** Returns a map from reviewer email to vote value; uses Prolog engine. */
- String2Integer getVotes() {
- ChangeData data = StoredValues.CHANGE_DATA.get(engine);
- ReviewDb db = StoredValues.REVIEW_DB.get(engine);
- String2Integer map = new String2Integer();
- AccountAccess ac = db.accounts();
- for (PatchSetApproval p : approvals) {
- if (p.getValue() != 0) {
- int id = p.getAccountId().get();
- try {
- Account a = ac.get(new Account.Id(id));
- String email = a.getPreferredEmail();
- map.put(email, new Integer(p.getValue()));
- } catch (OrmException e) {
- log.error("Cannot get email address of account id: " + id + " " + e);
- }
- }
- }
- return map;
- }
-
- /** Returns changed files, uses Prolog engine or url REST API. */
- Collection<String> getChangedFiles() {
- if (null != engine) { // Get changed files faster from StoredValues.
- try {
- return StoredValues.CHANGE_DATA.get(engine).currentFilePaths();
- } catch (OrmException e) {
- log.error("OrmException in getChangedFiles: " + e);
- return new StringSet();
- }
- }
- String request =
- url + "changes/" + change + "/revisions/" + patchset + "/files";
- JsonObject map = Util.getHTTPJsonObject(request, false);
- StringSet result = new StringSet();
- for (Map.Entry<String, JsonElement> entry : map.entrySet()) {
- String key = entry.getKey();
- if (!key.equals("/COMMIT_MSG")) { // ignore commit message
- result.add(key);
- // If a file was moved then we need approvals for old directory.
- JsonObject attr = entry.getValue().getAsJsonObject();
- if (null != attr && null != attr.get("old_path")) {
- result.add(attr.get("old_path").getAsString());
- }
- }
- }
- return result;
- }
-
- /** Returns reviewer emails got from url REST API. */
- JsonArray getReviewers() {
- String request = url + "changes/" + change + "/reviewers";
- JsonArray reviewers = Util.getHTTPJsonArray(request);
- JsonArray result = new JsonArray();
- int numReviewers = reviewers.size();
- for (int i = 0; i < numReviewers; i++) {
- JsonObject map = reviewers.get(i).getAsJsonObject();
- result.add(map.get("email").getAsString() + " []");
- }
- return result;
- }
-
- /** Returns file content or empty string; uses Repository. */
- String getRepositoryFile(Repository repo, String branch, String file) {
- try (RevWalk revWalk = new RevWalk(repo)) {
- RevTree tree = revWalk.parseCommit(repo.resolve(branch)).getTree();
- try (TreeWalk treeWalk = new TreeWalk(repo)) {
- try (ObjectReader reader = repo.newObjectReader()) {
- treeWalk.addTree(tree);
- treeWalk.setRecursive(true);
- treeWalk.setFilter(PathFilter.create(file));
- if (treeWalk.next()) {
- return new String(reader.open(treeWalk.getObjectId(0)).getBytes());
- }
- }
- }
- } catch (IOException e) {
- log.error("get file " + file + ": " + e);
- }
- return "";
- }
-
- /** Returns OWNERS file content; uses Repository or url REST API. */
- String getOWNERS(String dir, Repository repository, String url,
- String project, String branch) {
- // e.g. dir = ./d1/d2
- String filePath = (dir + "/OWNERS").substring(2); // remove "./"
- if (null != repository) {
- return getRepositoryFile(repository, branch, filePath);
- } else {
- String requestUrl = url + "projects/" + project
- + "/branches/" + branch + "/files/"
- + URLEncoder.encode(filePath) + "/content";
- return Util.getHTTPBase64Content(requestUrl);
- }
- }
-
- /** Returns the revision string of the tip of target branch. */
- String getBranchRevision(Repository repository, String url,
- String project, String branch) {
- if (null != repository) {
- try {
- return repository.getRef(
- repository.getBranch()).getObjectId().getName();
- } catch (IOException e) {
- log.error("Fail to get branch revision: " + e);
- }
- } else {
- JsonObject obj = Util.getHTTPJsonObject(
- url + "projects/" + project + "/branches/" + branch, true);
- // cannot get revision of branch "refs/meta/config".
- if (null != obj && null != obj.get("revision")) {
- return obj.get("revision").getAsString();
- }
- }
- return "";
- }
-
- /** Returns true if exempt from owner approval; uses Prolog engine. */
- boolean isExemptFromOwnerApproval() {
- if (null == engine) {
- return true;
- }
- try {
- ChangeData data = StoredValues.CHANGE_DATA.get(engine);
- String message = data.commitMessage();
- if (message.contains(EXEMPT_MESSAGE1)
- || message.contains(EXEMPT_MESSAGE2)) {
- return true;
- }
- } catch (IOException | OrmException e) {
- log.error("Cannot get commit message: " + e);
- return true; // exempt from owner approval due to lack of data
- }
- // Abandoned and merged changes do not need approval again.
- Status status = StoredValues.getChange(engine).getStatus();
- return (status == Status.ABANDONED || status == Status.MERGED);
- }
-
- /** Returns a cached or new OwnersDb. */
- OwnersDb getCachedOwnersDb() {
- if (null != engine) { // Get changed files faster from StoredValues.
- Repository repository = StoredValues.REPOSITORY.get(engine);
- String dbKey = Cache.makeKey(change, patchset, branch);
- return Cache.get(this, dbKey, repository, branch, getChangedFiles());
- }
- String key = Cache.makeKey(change, patchset, branch);
- return Cache.get(this, key, url, project, branch, getChangedFiles());
- }
-
- /** Returns a debug message string, for server side logging. */
- String genDebugMsg(OwnersDb db) {
- return (null == url ? "" : ("\n## url=" + url))
- + "\n## change=" + change + ", patchset=" + patchset
- + ", project=" + project + ", branch=" + branch
- + "\n## changedFiles=" + getChangedFiles()
- + "\nnumOwners=" + db.getNumOwners()
- + ", minVoteLevel=" + getMinOwnerVoteLevel()
- + ", approvals=" + getVotes();
- }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Servlet.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Servlet.java
deleted file mode 100644
index 590290a..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Servlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import com.google.inject.servlet.ServletModule;
-
-/** Plugin HTTP Servlet module */
-public class Servlet extends ServletModule {
- protected void configureServlets() {
- serve("/change/*").with(GetChange.class);
- }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Util.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Util.java
index e0ca853..76a0e53 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Util.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Util.java
@@ -14,89 +14,22 @@
package com.googlesource.gerrit.plugins.findowners;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.googlesource.gerrit.plugins.findowners.Util.Owner2Weights;
-import com.googlesource.gerrit.plugins.findowners.Util.String2StringSet;
+import com.google.common.collect.Ordering;
import java.io.File;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.List;
-import java.util.Scanner;
+import java.util.Map;
import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.SortedMap;
+import java.util.TreeMap;
/** Utility classes and functions. */
class Util {
- private static final Logger log = LoggerFactory.getLogger(Util.class);
-
- static class Owner2Weights extends HashMap<String, OwnerWeights> {}
- static class String2Integer extends HashMap<String, Integer> {}
- static class String2String extends HashMap<String, String> {}
- static class String2StringSet extends HashMap<String, StringSet> {}
- static class StringSet extends HashSet<String> {}
-
- /** Removes extra "/" in url. */
- static String normalizeURL(String url) {
- return url.replace(":///", "://"); // Assuming only one ":///" in url.
- }
-
/** Strips REST magic prefix line. */
static String stripMagicPrefix(String data) {
final String magic = ")]}'\n";
return data.startsWith(magic) ? data.substring(magic.length()) : data;
}
- /** Issues Gerrit REST API GET command. */
- private static String getHTTP(String urlString, boolean ignoreIOException) {
- urlString = normalizeURL(urlString);
- try {
- URLConnection conn = new URL(urlString).openConnection();
- Scanner scanner = new Scanner(conn.getInputStream());
- String responseBody = scanner.useDelimiter("\\A").next();
- return stripMagicPrefix(responseBody);
- } catch (MalformedURLException e) {
- log.error("Malformed URL: " + urlString);
- } catch (IOException e) {
- // Not an error if looking for an OWNERS file
- // or revision info in the "refs/meta/config" branch.
- if (!ignoreIOException) {
- log.error("IOException URL: " + urlString);
- }
- }
- return null;
- }
-
- /** Issues Gerrit REST API GET; converts result to JsonObject. */
- static JsonObject getHTTPJsonObject(String url, boolean ignoreIOException) {
- String data = getHTTP(url, ignoreIOException);
- return (null == data) ? new JsonObject()
- : new JsonParser().parse(data).getAsJsonObject();
- }
-
- /** Issues Gerrit REST API GET; converts result to JsonArray. */
- static JsonArray getHTTPJsonArray(String url) {
- String data = getHTTP(url, false);
- return (null == data) ? new JsonArray()
- : new JsonParser().parse(data).getAsJsonArray();
- }
-
- /** Issues Gerrit REST API GET; decodes base64 content. */
- static String getHTTPBase64Content(String url) {
- String data = getHTTP(url, true);
- return (null == data) ? "" : new String(Base64.getDecoder().decode(data));
- }
-
static String getDirName(String path) {
return new File(path).getParent();
}
@@ -110,23 +43,21 @@
}
static boolean parseBoolean(String s) {
- return (null != s) && (s.equals("1")
- || s.equalsIgnoreCase("yes") || Boolean.parseBoolean(s));
+ return (s != null) && (s.equals("1") || s.equalsIgnoreCase("yes") || Boolean.parseBoolean(s));
}
- static List<String> sort(Set<String> names) {
- List<String> list = new ArrayList<String>(names);
- Collections.sort(list);
- return list;
- }
-
- static JsonArray newJsonArrayFromStrings(Collection<String> names) {
- JsonArray result = new JsonArray();
- List<String> list = new ArrayList<String>(names);
- Collections.sort(list);
- for (String name : list) {
- result.add(name);
+ static SortedMap<String, String> makeSortedMap(Map<String, Set<String>> map) {
+ SortedMap<String, String> result = new TreeMap<>();
+ for (String key : Ordering.natural().sortedCopy(map.keySet())) {
+ result.put(key, String.join(" ", Ordering.natural().sortedCopy(map.get(key))));
}
return result;
}
+
+ static void addToMap(Map<String, Set<String>> map, String key, String value) {
+ if (map.get(key) == null) {
+ map.put(key, new HashSet<>());
+ }
+ map.get(key).add(value);
+ }
}
diff --git a/src/main/java/find_owners/PRED_check_owner_approval_2.java b/src/main/java/find_owners/PRED_check_owner_approval_2.java
index 52c9338..082b50e 100644
--- a/src/main/java/find_owners/PRED_check_owner_approval_2.java
+++ b/src/main/java/find_owners/PRED_check_owner_approval_2.java
@@ -21,18 +21,13 @@
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.Term;
import com.googlesource.gerrit.plugins.findowners.Checker;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
- * 'check_owner_approval'(+N, R) sets R to -1, 0, or 1,
- * if owner approval is missing, unneeded, or satisfied.
+ * 'check_owner_approval'(+N, R) sets R to -1, 0, or 1, if owner approval is missing, unneeded, or
+ * satisfied.
*/
public class PRED_check_owner_approval_2 extends Predicate.P2 {
- private static final Logger log =
- LoggerFactory.getLogger(PRED_check_owner_approval_2.class);
-
public PRED_check_owner_approval_2(Term a1, Term a2, Operation n) {
arg1 = a1;
arg2 = a2;
diff --git a/src/main/resources/Documentation/rest-api.md b/src/main/resources/Documentation/rest-api.md
index 2e792b6..34152d3 100644
--- a/src/main/resources/Documentation/rest-api.md
+++ b/src/main/resources/Documentation/rest-api.md
@@ -2,13 +2,13 @@
===========================
Any Gerrit UI clients or tools can use the
-`plugins/find-owners/change/<id>` API to get
+`/changes/<id>/owners` API to get
OWNERS information of a change.
### Request
```bash
-GET /plugins/find-owners/change/<id> HTTP/1.0
+GET /changes/<id>/owners HTTP/1.0
```
The `<id>` is a Gerrit change ID. This API can have two parameters:
@@ -22,7 +22,7 @@
For example,
```bash
-http://.../plugins/find-owners/change/29?debug=true&patchset=3
+http://<gerrit_server>/changes/29/owners?debug=true&patchset=3
```
### Response
@@ -46,18 +46,23 @@
Due to caching this revision might be behind recent branches changes.
* **dbgmsgs**: returned only when addDebugMsg is true,
- a set of debugging messages including project name, branch name,
- server address, etc.
+ a JSON object with the following members:
-* **path2owners**: returned only when addDebugMsg is true,
- a map from directory path or file glob to a string of owner emails
- separated by space. Note that `*` is a special owner email address.
- It means that there is no owner and anyone can be the owner.
- Included directories are those affected by the change revision.
+ * **user**: the change's creator.
-* **owner2paths**: returned only when addDebugMsg is true,
- a map from owner email to directory path or file glob.
- This is opposite to the path2owners map.
+ * **project**: the change's project name.
+
+ * **branch**: the change's destination brach name.
+
+ * **path2owners**:
+ a map from directory path or file glob to a string of owner emails
+ separated by space. Note that `*` is a special owner email address.
+ It means that there is no owner and anyone can be the owner.
+ Included directories are those affected by the change revision.
+
+ * **owner2paths**:
+ a map from owner email to directory path or file glob.
+ This is opposite to the path2owners map.
* **file2owners**: a map from each file in the change patchset to
the file owner emails, separated by space.
diff --git a/src/main/resources/static/find-owners.js b/src/main/resources/static/find-owners.js
index 2e2c17c..c38471f 100644
--- a/src/main/resources/static/find-owners.js
+++ b/src/main/resources/static/find-owners.js
@@ -343,11 +343,9 @@
getReviewers(changeId, popupWindow);
}
function callServer(callBack) {
- // Use either the revision post API or plugin get API.
- // Only pass changeId, let server get current patch set,
- // project and branch info.
- c.call({change: changeId}, showFindOwnersResults);
- // self.get('change/' + changeId, showFindOwnersResults);
+ // Use the plugin REST API; pass only changeId;
+ // let server get current patch set, project and branch info.
+ Gerrit.get('changes/' + changeId + '/owners', showFindOwnersResults);
}
callServer(showFindOwnersResults);
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/ActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/ActionTest.java
deleted file mode 100644
index 373e48a..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/ActionTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import static com.google.common.truth.Truth.assertThat;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonObject;
-import com.googlesource.gerrit.plugins.findowners.Util.String2String;
-import org.junit.Before;
-import org.junit.Test;
-
-/** Test Action class */
-public class ActionTest {
- private MockedServer server;
- private Action finder;
- private Gson gs;
-
- @Before
- public void setUp() {
- finder = new Action("http://mocked:8888/", null);
- server = new MockedServer();
- finder.setServer(server);
- gs = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
- }
-
- @Test
- public void getChangeDataTest() {
- JsonObject obj = finder.getChangeData(23, new String2String());
- // Write expected output as a more readable string literal,
- // remove all ' ', then use '\'' for '\"' and ' ' for '\n'.
- String expected = "{ 'minOwnerVoteLevel':1, 'addDebugMsg':true, "
- + "'change':23, 'patchset':3, 'owner_revision':'', "
- + "'dbgmsgs':{ 'user':'?', 'project':'projectA', "
- + "'branch':'master', 'server':'http://mocked:8888/' }, "
- + "'path2owners':{}, 'owner2paths':{}, 'file2owners':{}, "
- + "'reviewers':[], 'owners':[], 'files':[ './README', "
- + "'./d1/test.c', './d2/t.txt' ] }";
- String result = gs.toJson(obj).replaceAll(" ", "");
- expected = expected.replaceAll(" ", "\n");
- expected = expected.replaceAll("'", "\"");
- assertThat(result).isEqualTo(expected);
- }
-
- // TODO: test getChangeData with non-trivial parameters
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/CheckerTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/CheckerTest.java
deleted file mode 100644
index bd87a0e..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/CheckerTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import static com.google.common.truth.Truth.assertThat;
-import com.googlesource.gerrit.plugins.findowners.Util.String2Integer;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
-import org.junit.Before;
-import org.junit.Test;
-
-/** Test Checker class */
-public class CheckerTest {
- private MockedServer server;
-
- @Before
- public void setUp() {
- server = new MockedServer();
- }
-
- @Test
- public void findOwnersInVotesTest() {
- Checker c = new Checker(server, 2);
- StringSet owners = new StringSet();
- String2Integer votes = new String2Integer();
- // no owner, default is false.
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(false);
- // has owner, no vote, default is false.
- owners.add("xyz@google.com");
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(false);
- // has "*" owner, no owner vote is needed
- owners.add("*");
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(true);
- // again, no owner means no
- owners = new StringSet();
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(false);
- // two owners, but only +1
- owners.add("abc@google.com");
- owners.add("xyz@google.com");
- votes.put("xyz@google.com", 1);
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(false);
- // one owner +2 vote is enough
- votes.put("xyz@google.com", 2);
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(true);
- // two +1 votes is not the same as one +2
- votes.put("abc@google.com", 1);
- votes.put("xyz@google.com", 1);
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(false);
- votes.put("xyz@google.com", 2);
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(true);
- // one owner -1 is a veto.
- votes.put("abc@google.com", -1);
- assertThat(c.findOwnersInVotes(owners, votes)).isEqualTo(false);
- }
-
- void addOwnersInfo(MockedOwnersDb db) {
- String[] emails = {"abc@google.com", "xyz@google.com"};
- StringSet dirs = new StringSet();
- StringSet owners = new StringSet();
- dirs.add("./d1");
- for (String e : emails) {
- db.owner2Paths.put(e, dirs);
- owners.add(e);
- }
- db.mockedFile2Owners.put("./d1/f1.c", owners);
- }
-
- @Test
- public void findApprovalOwnersDbTest() {
- Checker c = new Checker(server, 2);
- MockedOwnersDb db = new MockedOwnersDb();
- assertThat(c.findApproval(db)).isEqualTo(0); // no owners info
- addOwnersInfo(db); // add one file and two owners
- assertThat(db.getNumOwners()).isEqualTo(2);
- assertThat(db.findOwners(null).size()).isEqualTo(1);
- assertThat(db.findOwners(null).get("f1")).isEqualTo(null);
- assertThat(db.findOwners(null).get("./d1/f1.c").size()).isEqualTo(2);
- assertThat(c.findApproval(db)).isEqualTo(-1);
- server.votes.put("abc@google.com", 1);
- // sever has minOwnerVoteLevel 1, but checker requires 2.
- assertThat(server.getMinOwnerVoteLevel()).isEqualTo(1);
- assertThat(c.findApproval(db)).isEqualTo(-1); // vote 1 is not enough
- c = new Checker(server, 1);
- assertThat(c.findApproval(db)).isEqualTo(1);
- server.votes.put("xyz@google.com", -1);
- assertThat(c.findApproval(db)).isEqualTo(-1); // an owner's veto
- }
-
- @Test
- public void findApprovalTest() {
- Checker c = new Checker(server, 1);
- // default mocked, not exempted from owner approval
- assertThat(server.isExemptFromOwnerApproval()).isEqualTo(false);
- // not exempted, but no owner in mocked OwnersDb
- assertThat(c.findApproval()).isEqualTo(0);
- server.exemptFromOwnerApproval = true;
- assertThat(server.isExemptFromOwnerApproval()).isEqualTo(true);
- // exempted, no owner, should be 0, not 1 or -1.
- assertThat(c.findApproval()).isEqualTo(0);
- server.exemptFromOwnerApproval = false;
- // add mocked owners, no vote, should be -1, not approved.
- addOwnersInfo(server.ownersDb);
- assertThat(c.findApproval()).isEqualTo(-1);
- // add vote, should be approved now.
- server.votes.put("abc@google.com", 1);
- assertThat(c.findApproval()).isEqualTo(1);
- }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
new file mode 100644
index 0000000..75e1e16
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
@@ -0,0 +1,270 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.change.ChangeResource;
+import org.junit.Test;
+
+/** Test find-owners plugin API. */
+@TestPlugin(name = "find-owners", sysModule = "com.googlesource.gerrit.plugins.findowners.Module")
+public class FindOwnersIT extends LightweightPluginDaemonTest {
+
+ @Test
+ public void getOwnersTest() throws Exception {
+ ChangeInfo info1 = newChangeInfo("test1 GetOwners");
+ ChangeInfo info2 = newChangeInfo("test2 GetOwners");
+ assertThat(info2._number).isEqualTo(info1._number + 1);
+ String expected =
+ ")]}' { minOwnerVoteLevel:1, addDebugMsg:false, change:"
+ + info1._number
+ + ", patchset:1, file2owners:{}, reviewers:[], owners:[], files:[] }";
+ Cache cache = Cache.getInstance().init(0, 10); // reset, no Cache
+ assertThat(cache.size()).isEqualTo(0L);
+ // GetOwners GET API
+ assertThat(getOwnersResponse(info1)).isEqualTo(expected);
+ assertThat(cache.size()).isEqualTo(0L);
+ // find-owners GET API
+ assertThat(getFindOwnersResponse(info1)).isEqualTo(expected);
+ cache.init(10, 5); // create the Cache
+ assertThat(cache.size()).isEqualTo(0L);
+ assertThat(getOwnersResponse(info1)).isEqualTo(expected);
+ assertThat(getFindOwnersResponse(info1)).isEqualTo(expected);
+ assertThat(cache.size()).isEqualTo(1L);
+ }
+
+ @Test
+ public void ownersFile1Test() throws Exception {
+ // Create 1st OWNERS file, this change does not have owner.
+ PushOneCommit.Result c1 = createChange("add OWNERS", "OWNERS", "x@x\na@a\n");
+ assertThat(getOwnersResponse(c1)).contains("owners:[], files:[ OWNERS ]");
+ // Create another "t.c" file, which has no owner because c1 is not submitted yet.
+ PushOneCommit.Result c2 = createChange("add t.c", "t.c", "##");
+ assertThat(getOwnersResponse(c2)).contains("owners:[], files:[ t.c ]");
+ // Change of "t.c" file has owner after c1 is submitted.
+ approveSubmit(c1);
+ assertThat(getOwnersResponse(c2)).contains("owners:[ a@a[1+0+0], x@x[1+0+0] ], files:[ t.c ]");
+ // A submitted change gets owners info from current repository.
+ assertThat(getOwnersResponse(c1))
+ .contains("owners:[ a@a[1+0+0], x@x[1+0+0] ], files:[ OWNERS ]");
+ // Check all fields in response.
+ String expectedTail =
+ "path2owners:{ ./:a@ax@x }, owner2paths:{ a@a:./, x@x:./ } }"
+ + ", file2owners:{ ./t.c:a@ax@x }, reviewers:[], owners:[ "
+ + "a@a[1+0+0], x@x[1+0+0] ], files:[ t.c ] }";
+ assertThat(getOwnersDebugResponse(c2)).contains(expectedTail);
+ }
+
+ @Test
+ public void ownersFile2Test() throws Exception {
+ // Add OWNERS file, this test does not inherit files created in ownersFile1Test.
+ addFile("add OWNERS", "OWNERS", "per-file *.c=x@x\na@a\nc@c\nb@b\n");
+ // Add "t.c" file, which has per-file owner x@x, not a@a, b@b, c@c.
+ PushOneCommit.Result c2 = createChange("add t.c", "t.c", "Hello!");
+ assertThat(getOwnersResponse(c2)).contains("owners:[ x@x[1+0+0] ], files:[ t.c ]");
+ // Add "t.txt" file, which has new owners.
+ PushOneCommit.Result c3 = createChange("add t.txt", "t.txt", "Test!");
+ assertThat(getOwnersResponse(c3))
+ .contains("owners:[ a@a[1+0+0], b@b[1+0+0], c@c[1+0+0] ], files:[ t.txt ]");
+ }
+
+ @Test
+ public void subOwnersFileTest() throws Exception {
+ // Add OWNERS file in root and subdirectories.
+ addFile("add OWNERS", "OWNERS", "x@x\n");
+ addFile("add d1/OWNERS", "d1/OWNERS", "a@a\n");
+ addFile("add d2/OWNERS", "d2/OWNERS", "y@y\n");
+ addFile("add d3/OWNERS", "d3/OWNERS", "b@b\nset noparent\n");
+ // Add "t.c" file, which is not owned by subdirectory owners.
+ PushOneCommit.Result c2 = createChange("add t.c", "t.c", "Hello!");
+ assertThat(getOwnersResponse(c2)).contains("owners:[ x@x[1+0+0] ], files:[ t.c ]");
+ // Add "d1/t.c" file, which is owned by ./d1 and root owners.
+ PushOneCommit.Result c3 = createChange("add d1/t.c", "d1/t.c", "Hello!");
+ assertThat(getOwnersResponse(c3))
+ .contains("owners:[ a@a[1+0+0], x@x[0+1+0] ], files:[ d1/t.c ]");
+ // Add "d2/t.c" file, which is owned by ./d2 and root owners.
+ PushOneCommit.Result c4 = createChange("add d2/t.c", "d2/t.c", "Hello!");
+ assertThat(getOwnersResponse(c4))
+ .contains("owners:[ y@y[1+0+0], x@x[0+1+0] ], files:[ d2/t.c ]");
+ // Add "d2/d1/t.c" file, which is owned by ./d2 and root owners.
+ PushOneCommit.Result c5 = createChange("add d2/d1/t.c", "d2/d1/t.c", "Hello!");
+ assertThat(getOwnersResponse(c5))
+ .contains("owners:[ y@y[1+0+0], x@x[0+1+0] ], files:[ d2/d1/t.c ]");
+ // Add "d3/t.c" file, which is owned only by ./d3 owners due to "set noparent".
+ PushOneCommit.Result c6 = createChange("add d3/t.c", "d3/t.c", "Hello!");
+ assertThat(getOwnersResponse(c6)).contains("owners:[ b@b[1+0+0] ], files:[ d3/t.c ]");
+ // Add "d3/d1/t.c" file, which is owned only by ./d3 owners due to "set noparent".
+ PushOneCommit.Result c7 = createChange("add d3/d1/t.c", "d3/d1/t.c", "Hello!");
+ assertThat(getOwnersResponse(c7)).contains("owners:[ b@b[1+0+0] ], files:[ d3/d1/t.c ]");
+ }
+
+ @Test
+ public void requestErrorTest() throws Exception {
+ PushOneCommit.Result c1 = createChange("add t.c", "t.c", "##");
+ assertThat(getOwnersResponse(c1)).contains("owners:[], files:[ t.c ]");
+ int id = c1.getChange().getId().get();
+ // Correct change id.
+ String result = userRestSession.get("/changes/" + id + "/owners").getEntityContent();
+ assertThat(filteredJson(result)).contains("owners:[], files:[ t.c ]");
+ // Wrong change number, 404 not found.
+ RestResponse response = userRestSession.get("/changes/" + (id + 1) + "/owners");
+ assertThat(response.getStatusCode()).isEqualTo(404);
+ assertThat(response.getEntityContent()).isEqualTo("Not found: " + (id + 1));
+ // Wrong request parameter, 400 not a valid option
+ response = userRestSession.get("/changes/" + id + "/owners?xyz=3");
+ assertThat(response.getStatusCode()).isEqualTo(400);
+ assertThat(response.getEntityContent()).isEqualTo("\"--xyz\" is not a valid option");
+ // Wrong patchset parameter, no content
+ response = userRestSession.get("/changes/" + id + "/owners?patchset=2");
+ assertThat(response.getStatusCode()).isEqualTo(204);
+ assertThat(response.hasContent()).isFalse();
+ }
+
+ @Test
+ public void accountTest() throws Exception {
+ String[] myTestEmails = {"abc@g.com", "abc+xyz@g.com", "xyz@g.com"};
+ for (String email : myTestEmails) {
+ Account.Id id = accounts.create("User" + email, email, "FullName" + email).getId();
+ // Action.getReviewers uses accountCache to get email address.
+ assertThat(accountCache.get(id).getAccount().getPreferredEmail()).isEqualTo(email);
+ // Checker.getVotes uses AccountAccess to get email address.
+ assertThat(db.accounts().get(id).getPreferredEmail()).isEqualTo(email);
+ }
+ }
+
+ @Test
+ public void projectTest() throws Exception {
+ RestResponse response = adminRestSession.get("/projects/?d");
+ String content = response.getEntityContent();
+ // Predefined projects: "All-Projects", "All-Users",
+ // "com.googlesource.gerrit.plugins.findowners.FindOwnersIT_projectTest_project"
+ assertThat(content).contains("\"id\": \"All-Projects\",");
+ assertThat(content).contains("\"id\": \"All-Users\",");
+ assertThat(content).contains(idProject("projectTest", "project"));
+ assertThat(content).doesNotContain(idProject("projectTest", "ProjectA"));
+ createProject("ProjectA");
+ response = adminRestSession.get("/projects/?d");
+ assertThat(response.getEntityContent()).contains(idProject("projectTest", "ProjectA"));
+ }
+
+ @Test
+ public void actionApplyTest() throws Exception {
+ Cache cache = Cache.getInstance().init(0, 10);
+ assertThat(cache.size()).isEqualTo(0);
+ // TODO: create ChangeInput in a new project.
+ ChangeInfo changeInfo = newChangeInfo("test Action.apply");
+ ChangeResource cr = parseChangeResource(changeInfo.changeId);
+ Action.Parameters param = new Action.Parameters();
+ Action action = new Action(null, null, changeDataFactory, accountCache, repoManager);
+ Response<RestResult> response = action.apply(db, cr, param);
+ RestResult result = response.value();
+ verifyRestResult(result, 1, 1, changeInfo._number, false);
+ param.debug = true;
+ response = action.apply(db, cr, param);
+ result = response.value();
+ verifyRestResult(result, 1, 1, changeInfo._number, true);
+ assertThat(result.dbgmsgs.user).isEqualTo("?");
+ assertThat(result.dbgmsgs.project).isEqualTo(changeInfo.project);
+ // changeInfo.branch is "master" but result.dbgmsgs.branch is "refs/heads/master".
+ assertThat(result.dbgmsgs.branch).contains(changeInfo.branch);
+ assertThat(result.dbgmsgs.path2owners).isEmpty();
+ assertThat(result.dbgmsgs.owner2paths).isEmpty();
+ assertThat(result.file2owners).isEmpty();
+ assertThat(result.reviewers).isEmpty();
+ assertThat(result.owners).isEmpty();
+ assertThat(result.files).isEmpty();
+ // TODO: find expected value of ownerRevision.
+ assertThat(cache.size()).isEqualTo(0);
+ }
+
+ private ChangeInfo newChangeInfo(String subject) throws Exception {
+ // should be called with different subject
+ ChangeInput in = new ChangeInput();
+ in.project = project.get();
+ in.branch = "master";
+ in.subject = subject;
+ in.topic = "test empty change";
+ in.status = ChangeStatus.NEW;
+ return gApi.changes().create(in).get();
+ }
+
+ private String getFindOwnersResponse(ChangeInfo info) throws Exception {
+ return filteredJson(
+ userRestSession.get("/changes/" + info._number + "/revisions/1/find-owners"));
+ }
+
+ private String getOwnersResponse(ChangeInfo info) throws Exception {
+ return filteredJson(userRestSession.get("/changes/" + info._number + "/owners"));
+ }
+
+ private String getOwnersResponse(PushOneCommit.Result change) throws Exception {
+ return filteredJson(userRestSession.get("/changes/" + change.getChangeId() + "/owners"));
+ }
+
+ private String getOwnersDebugResponse(PushOneCommit.Result change) throws Exception {
+ return filteredJson(
+ userRestSession.get("/changes/" + change.getChangeId() + "/owners?debug=1"));
+ }
+
+ private void approveSubmit(PushOneCommit.Result change) throws Exception {
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).current().submit(new SubmitInput());
+ }
+
+ private void addFile(String subject, String file, String content) throws Exception {
+ approveSubmit(createChange(subject, file, content));
+ }
+
+ // Remove '"' and space; replace '\n' with ' '; ignore unpredictable "owner_revision".
+ private static String filteredJson(String json) {
+ return json.replaceAll("[\" ]*", "").replace('\n', ' ').replaceAll("owner_revision:[^ ]* ", "");
+ }
+
+ private static String filteredJson(RestResponse response) throws Exception {
+ return filteredJson(response.getEntityContent());
+ }
+
+ private static String idProject(String test, String project) {
+ // Expected string of "id": "project_name",
+ return String.format(
+ "\"id\": \"com.googlesource.gerrit.plugins.findowners.FindOwnersIT_" + "%s_%s\",",
+ test, project);
+ }
+
+ private static void verifyRestResult(
+ RestResult result, int voteLevel, int patchset, int changeNumber, boolean addDebugMsg)
+ throws Exception {
+ assertThat(result.minOwnerVoteLevel).isEqualTo(voteLevel);
+ assertThat(result.patchset).isEqualTo(patchset);
+ assertThat(result.change).isEqualTo(changeNumber);
+ assertThat(result.addDebugMsg).isEqualTo(addDebugMsg);
+ if (addDebugMsg) {
+ assertThat(result.dbgmsgs).isNotNull();
+ } else {
+ assertThat(result.dbgmsgs).isNull();
+ }
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/MockedOwnersDb.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/MockedOwnersDb.java
deleted file mode 100644
index fd24a9c..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/MockedOwnersDb.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import static com.google.common.truth.Truth.assertThat;
-import com.googlesource.gerrit.plugins.findowners.Util.String2StringSet;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
-import java.util.Collection;
-import org.junit.Test;
-
-/** Mocked OwnersDb to test Parser class */
-public class MockedOwnersDb extends OwnersDb {
- private String savedData;
- String2StringSet mockedFile2Owners;
-
- public MockedOwnersDb() {
- super(null);
- resetData();
- mockedFile2Owners = new String2StringSet();
- }
-
- void resetData() {
- savedData = "";
- stopLooking = new StringSet();
- }
-
- void appendSavedData(String s) {
- savedData += s;
- }
-
- String getSavedData() {
- return savedData;
- }
-
- StringSet getStopLooking() {
- return stopLooking;
- }
-
- @Override
- void addOwnerPathPair(String s1, String s2) {
- savedData += "s1:" + s1 + "\ns2:" + s2 + "\n";
- }
-
- @Override
- String2StringSet findOwners(Collection<String> files) {
- return mockedFile2Owners;
- }
-
- @Test
- public void defaultTest() {
- // Trivial test of default OwnersDb members.
- assertThat(revision).isEqualTo("");
- assertThat(dir2Globs.size()).isEqualTo(0);
- assertThat(owner2Paths.size()).isEqualTo(0);
- assertThat(path2Owners.size()).isEqualTo(0);
- assertThat(readDirs.size()).isEqualTo(0);
- assertThat(stopLooking.size()).isEqualTo(0);
- }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/MockedServer.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/MockedServer.java
deleted file mode 100644
index da5df7e..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/MockedServer.java
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import static com.google.common.truth.Truth.assertThat;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gson.JsonArray;
-import com.googlesource.gerrit.plugins.findowners.Util.String2Integer;
-import com.googlesource.gerrit.plugins.findowners.Util.String2String;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
-import java.util.Collection;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.Test;
-
-/** Mocked server to test Action, Checker, OwnersDb classes. */
-public class MockedServer extends Server {
-
- int minOwnerVoteLevel;
- boolean addDebugMsg;
- boolean traceServer;
- boolean reportSyntaxError;
- boolean exemptFromOwnerApproval;
- StringSet changedFiles;
- String2Integer votes;
- JsonArray reviewers;
- MockedOwnersDb ownersDb;
- Status status;
- String branchRevision;
- String2String dir2owners; // map from a directory path to OWNERS content
-
- public MockedServer() {
- change = 12;
- patchset = 3;
- project = "projectA";
- branch = "master";
- error = null;
- minOwnerVoteLevel = 1;
- addDebugMsg = true;
- traceServer = true;
- reportSyntaxError = true;
- exemptFromOwnerApproval = false;
- changedFiles = new StringSet();
- String[] sampleFiles = {"./README", "./d1/test.c", "./d2/t.txt"};
- for (String file : sampleFiles) {
- changedFiles.add(file);
- }
- votes = new String2Integer();
- reviewers = new JsonArray();
- ownersDb = new MockedOwnersDb();
- status = Status.NEW;
- branchRevision = "13579abcdef";
- dir2owners = new String2String();
- }
-
- @Override
- int getMinOwnerVoteLevel() {
- return minOwnerVoteLevel;
- }
-
- @Override
- boolean getAddDebugMsg() {
- return addDebugMsg;
- }
-
- @Override
- boolean traceServerMsg() {
- return traceServer;
- }
-
- @Override
- boolean getReportSyntaxError() {
- return reportSyntaxError;
- }
-
- @Override
- boolean isExemptFromOwnerApproval() {
- return exemptFromOwnerApproval;
- }
-
- @Override
- void setChangeId(String url, int change) {}
-
- @Override
- String setPatchId(String patchsetNum) {
- return null;
- }
-
- @Override
- Collection<String> getChangedFiles() {
- return changedFiles;
- }
-
- @Override
- String2Integer getVotes() {
- return votes;
- }
-
- @Override
- JsonArray getReviewers() {
- return reviewers;
- }
-
- @Override
- Status getStatus(RevisionResource resource) {
- return status;
- }
-
- @Override
- String getOWNERS(String dir, Repository repository, String url,
- String project, String branch) {
- String content = dir2owners.get(dir);
- return (null == content ? "" : content);
- }
-
- @Override
- String getBranchRevision(Repository repository, String url,
- String project, String branch) {
- return branchRevision;
- }
-
- @Override
- OwnersDb getCachedOwnersDb() {
- return ownersDb;
- }
-
- @Test
- public void genDebugMsgTest() {
- // Important because real server.traceServerMsg() is usually false.
- String expected =
- "\n## change=12, patchset=3, project=projectA, branch=master"
- + "\n## changedFiles=" + changedFiles
- + "\nnumOwners=0, minVoteLevel=1"
- + ", approvals=" + getVotes();
- assertThat(genDebugMsg(ownersDb)).isEqualTo(expected);
- url = "http://localhost:8081/";
- String expected2 = "\n## url=" + url + expected;
- assertThat(genDebugMsg(ownersDb)).isEqualTo(expected2);
- }
-
- // TODO: use a mocked Repository to test getRepositoryFile
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnerWeightsTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnerWeightsTest.java
index ef28a89..183a222 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnerWeightsTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnerWeightsTest.java
@@ -15,15 +15,20 @@
package com.googlesource.gerrit.plugins.findowners;
import static com.google.common.truth.Truth.assertThat;
+
import com.googlesource.gerrit.plugins.findowners.OwnerWeights.WeightComparator;
-import com.googlesource.gerrit.plugins.findowners.Util.Owner2Weights;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/** Test OwnerWeights class */
+@RunWith(JUnit4.class)
public class OwnerWeightsTest {
- private OwnerWeights createOwnerWeights(int[] counts) {
+ private static OwnerWeights createOwnerWeights(int[] counts) {
OwnerWeights obj = new OwnerWeights();
for (int i = 0; i < counts.length; i++) {
for (int j = 0; j < counts[i]; j++) {
@@ -65,7 +70,7 @@
assertThat(objX2.encodeLevelCounts()).isEqualTo("[0+2+1]");
assertThat(objX3.encodeLevelCounts()).isEqualTo("[0+2+3]");
assertThat(objX4.encodeLevelCounts()).isEqualTo("[1+1+1]");
- Owner2Weights map = new Owner2Weights();
+ Map<String, OwnerWeights> map = new HashMap<>();
map.put("objX1", objX1);
map.put("objX2", objX2);
map.put("objX3", objX3);
@@ -80,11 +85,11 @@
WeightComparator comp = new WeightComparator(map);
// comp.compare(A,B) < 0, if A has order before B
// comp.compare(A,B) > 0, if A has order after B
- assertThat(comp.compare("objX1", "objX2") > 0).isEqualTo(true);
- assertThat(comp.compare("objX3", "objX2") < 0).isEqualTo(true);
- assertThat(comp.compare("objX3", "objX4") > 0).isEqualTo(true);
+ assertThat(comp.compare("objX1", "objX2")).isGreaterThan(0);
+ assertThat(comp.compare("objX3", "objX2")).isLessThan(0);
+ assertThat(comp.compare("objX3", "objX4")).isGreaterThan(0);
assertThat(comp.compare("objX3", "objX3")).isEqualTo(0);
- assertThat(comp.compare("objX4", "objX0") > 0).isEqualTo(true);
- assertThat(comp.compare("objX0", "objX4") < 0).isEqualTo(true);
+ assertThat(comp.compare("objX4", "objX0")).isGreaterThan(0);
+ assertThat(comp.compare("objX0", "objX4")).isLessThan(0);
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersDbTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersDbTest.java
deleted file mode 100644
index df3c7fd..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersDbTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
-
-import static com.google.common.truth.Truth.assertThat;
-import org.junit.Before;
-import org.junit.Test;
-
-/** Test OwnersDb class */
-public class OwnersDbTest {
- private MockedServer server;
-
- @Before
- public void setUp() {
- server = new MockedServer();
- }
-
- @Test
- public void ctorTest() {
- // TODO: test getNumOwners, etc.
- }
-
- @Test
- public void addOwnerPathPairTest() {
- // TODO: test addOwnerPathPair
- }
-
- @Test
- public void addFileTest() {
- // TODO: test addFile
- assertThat(1 + 1).isEqualTo(2);
- }
-
- @Test
- public void findOwnersTest() {
- // TODO: test findOwners
- }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
index 4488512..6f0ca72 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
@@ -15,56 +15,58 @@
package com.googlesource.gerrit.plugins.findowners;
import static com.google.common.truth.Truth.assertThat;
+
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/** Test Parser class */
+@RunWith(JUnit4.class)
public class ParserTest {
- private MockedOwnersDb db;
-
@Before
public void setUp() {
- db = new MockedOwnersDb();
+ Config.setReportSyntaxError(true);
}
- private String mockedTestDir() {
+ private static String mockedTestDir() {
return "./d1/d2/";
}
- private void testLine(String line) {
- db.resetData();
- String result = Parser.parseLine(db, mockedTestDir(), "OWNERS", line, 3);
- db.appendSavedData((result != null) ? (result + line) : line);
+ private static Parser.Result testLine(String line) {
+ Parser.Result result = new Parser.Result();
+ Parser.parseLine(result, mockedTestDir(), "OWNERS", line, 3);
+ return result;
}
- private String testLineOwnerPath(String line, String s1) {
- return testLineOwnerPath(line, s1, mockedTestDir());
- }
-
- private String testLineOwnerPath(String line, String s1, String s2) {
- // expected db.savedData created by testLine(line)
- // followed by call to addOwnerPathPair(s1, s2)
- return "s1:" + s1 + "\ns2:" + s2 + "\n" + line;
- }
-
- private String testLineWarningMsg(String line) {
+ private static String testLineWarningMsg(String line) {
// expected warning message created by testLine(line)
return Parser.warningMsg("OWNERS", 3, "ignored", line);
}
- private String testLineErrorMsg(String line) {
+ private static String testLineErrorMsg(String line) {
// expected error message created by testLine(line)
return Parser.errorMsg("OWNERS", 3, "ignored unknown line", line);
}
@Test
+ public void emptyParserResult() {
+ Parser.Result result = new Parser.Result();
+ assertThat(result.stopLooking).isFalse();
+ assertThat(result.warnings).isEmpty();
+ assertThat(result.errors).isEmpty();
+ assertThat(result.owner2paths).isEmpty();
+ }
+
+ @Test
public void badLineTest() {
- String[] lines = {"actor", "a@b@c", "**", "per-files *.gyp",
- "a@b.com@c.com #..."};
+ String[] lines = {"actor", "a@b@c", "**", "per-files *.gyp", "a@b.com@c.com #..."};
for (String s : lines) {
- testLine(s);
- String expected = testLineErrorMsg(s) + s;
- assertThat(db.getSavedData()).isEqualTo(expected);
+ Parser.Result result = testLine(s);
+ assertThat(result.warnings).isEmpty();
+ assertThat(result.errors).hasSize(1);
+ String expected = testLineErrorMsg(s);
+ assertThat(result.errors.get(0)).isEqualTo(expected);
}
}
@@ -72,20 +74,24 @@
public void commentLineTest() {
String[] lines = {"", " ", "# comment #data", "#any", " # comment"};
for (String s : lines) {
- testLine(s);
- assertThat(db.getSavedData()).isEqualTo(s);
+ Parser.Result result = testLine(s);
+ assertThat(result.stopLooking).isFalse();
+ assertThat(result.warnings).isEmpty();
+ assertThat(result.errors).isEmpty();
+ assertThat(result.owner2paths).isEmpty();
}
}
@Test
public void emailLineTest() {
- String[] lines = {"a_b-c3@google.com", " x.y.z@gmail.com # comment",
- "*", " * # any user"};
+ String[] lines = {"a_b-c3@google.com", " x.y.z@gmail.com # comment", "*", " * # any user"};
String[] emails = {"a_b-c3@google.com", "x.y.z@gmail.com", "*", "*"};
for (int i = 0; i < lines.length; i++) {
- testLine(lines[i]);
- String expected = testLineOwnerPath(lines[i], emails[i]);
- assertThat(db.getSavedData()).isEqualTo(expected);
+ Parser.Result result = testLine(lines[i]);
+ assertThat(result.owner2paths).hasSize(1);
+ String[] paths = result.owner2paths.get(emails[i]).toArray(new String[1]);
+ assertThat(paths.length).isEqualTo(1);
+ assertThat(paths[0]).isEqualTo(mockedTestDir());
}
}
@@ -94,53 +100,49 @@
// file: directive is not implemented yet.
String[] lines = {"file://owners", " file: //d1/owner", "file:owner #"};
for (String s : lines) {
- testLine(s);
- String expected = testLineWarningMsg(s) + s;
- assertThat(db.getSavedData()).isEqualTo(expected);
+ Parser.Result result = testLine(s);
+ String expected = testLineWarningMsg(s);
+ assertThat(result.warnings).hasSize(1);
+ assertThat(result.warnings.get(0)).isEqualTo(expected);
}
}
@Test
public void noParentLineTest() {
- String[] lines = {"set noparent", " set noparent",
- "set noparent # comment"};
+ String[] lines = {"set noparent", " set noparent", "set noparent # comment"};
for (String line : lines) {
- db.resetData();
- assertThat(db.stopLooking.size()).isEqualTo(0);
+ Parser.Result result = testLine(line);
+ assertThat(result.stopLooking).isTrue();
testLine(line);
- assertThat(db.stopLooking.size()).isEqualTo(1);
- assertThat(db.stopLooking.contains(mockedTestDir())).isEqualTo(true);
- assertThat(db.getSavedData()).isEqualTo(line);
}
}
@Test
public void perFileGoodDirectiveTest() {
- String[] directives = {"abc@google.com#comment",
- " *# comment",
- " xyz@gmail.com # comment"};
+ String[] directives = {"abc@google.com#comment", " *# comment", " xyz@gmail.com # comment"};
String[] emails = {"abc@google.com", "*", "xyz@gmail.com"};
for (int i = 0; i < directives.length; i++) {
String line = "per-file *test*.java=" + directives[i];
- testLine(line);
- String expected =
- testLineOwnerPath(line, emails[i], mockedTestDir() + "*test*.java");
- assertThat(db.getSavedData()).isEqualTo(expected);
+ Parser.Result result = testLine(line);
+ String[] paths = result.owner2paths.get(emails[i]).toArray(new String[1]);
+ assertThat(paths.length).isEqualTo(1);
+ assertThat(paths[0]).isEqualTo(mockedTestDir() + "*test*.java");
}
}
@Test
public void perFileBadDirectiveTest() {
- // TODO: test "set noparent" after perf-file.
- String[] directives =
- {"file://OWNERS", " ** ", "a b@c .co", "a@b@c #com", "a.<b>@zc#"};
- String[] errors =
- {"file://OWNERS", "**", "a", "a@b@c", "a.<b>@zc"};
+ String[] directives = {
+ "file://OWNERS", " ** ", "a b@c .co", "a@b@c #com", "a.<b>@zc#", " set noparent "
+ };
+ String[] errors = {"file://OWNERS", "**", "a b@c .co", "a@b@c", "a.<b>@zc", "set noparent"};
for (int i = 0; i < directives.length; i++) {
String line = "per-file *test*.c=" + directives[i];
- testLine(line);
- String expected = testLineErrorMsg(errors[i]) + line;
- assertThat(db.getSavedData()).isEqualTo(expected);
+ Parser.Result result = testLine(line);
+ String expected = testLineErrorMsg(errors[i]);
+ assertThat(result.warnings).isEmpty();
+ assertThat(result.errors).hasSize(1);
+ assertThat(result.errors.get(0)).isEqualTo(expected);
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/UtilTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/UtilTest.java
index 96b29a1..bf7c8a7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/UtilTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/UtilTest.java
@@ -15,106 +15,93 @@
package com.googlesource.gerrit.plugins.findowners;
import static com.google.common.truth.Truth.assertThat;
-import com.googlesource.gerrit.plugins.findowners.Util.Owner2Weights;
-import com.googlesource.gerrit.plugins.findowners.Util.String2Integer;
-import com.googlesource.gerrit.plugins.findowners.Util.String2String;
-import com.googlesource.gerrit.plugins.findowners.Util.String2StringSet;
-import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/** Test Util class */
+@RunWith(JUnit4.class)
public class UtilTest {
@Test
public void getOwner2WeightsTest() {
- Owner2Weights m = new Owner2Weights();
- assertThat(m.size()).isEqualTo(0);
- assertThat(m.get("s1")).isEqualTo(null);
+ Map<String, OwnerWeights> m = new HashMap<>();
+ assertThat(m).isEmpty();
+ assertThat(m.get("s1")).isNull();
OwnerWeights v1 = new OwnerWeights();
OwnerWeights v2 = new OwnerWeights();
m.put("s1", v1);
assertThat(m.get("s1")).isEqualTo(v1);
// compare OwnerWeights by reference
assertThat(m.get("s1")).isNotEqualTo(v2);
- assertThat(m.get("s2")).isEqualTo(null);
- assertThat(m.size()).isEqualTo(1);
+ assertThat(m.get("s2")).isNull();
+ assertThat(m).hasSize(1);
}
@Test
public void getString2IntegerTest() {
- String2Integer m = new String2Integer();
- assertThat(m.size()).isEqualTo(0);
- assertThat(m.get("s1")).isEqualTo(null);
+ Map<String, Integer> m = new HashMap<>();
+ assertThat(m).isEmpty();
+ assertThat(m.get("s1")).isNull();
Integer v1 = 3;
Integer v2 = 3;
m.put("s1", v1);
assertThat(m.get("s1")).isEqualTo(v1);
// compare Integer by value
assertThat(m.get("s1")).isEqualTo(v2);
- assertThat(m.get("s2")).isEqualTo(null);
- assertThat(m.size()).isEqualTo(1);
+ assertThat(m.get("s2")).isNull();
+ assertThat(m).hasSize(1);
}
@Test
public void getString2StringTest() {
- String2String m = new String2String();
- assertThat(m.size()).isEqualTo(0);
- assertThat(m.get("s1")).isEqualTo(null);
+ Map<String, String> m = new HashMap<>();
+ assertThat(m).isEmpty();
+ assertThat(m.get("s1")).isNull();
String v1 = "x";
String v2 = "x";
m.put("s1", v1);
assertThat(m.get("s1")).isEqualTo(v1);
// compare String by value
assertThat(m.get("s1")).isEqualTo(v2);
- assertThat(m.get("s2")).isEqualTo(null);
- assertThat(m.size()).isEqualTo(1);
+ assertThat(m.get("s2")).isNull();
+ assertThat(m).hasSize(1);
}
@Test
public void getString2StringSetTest() {
- String2StringSet m = new String2StringSet();
- assertThat(m.size()).isEqualTo(0);
- assertThat(m.get("s1")).isEqualTo(null);
- StringSet v1 = new StringSet();
- StringSet v2 = new StringSet();
- assertThat(v1.size()).isEqualTo(0);
+ Map<String, Set<String>> m = new HashMap<>();
+ assertThat(m).isEmpty();
+ assertThat(m.get("s1")).isNull();
+ Set<String> v1 = new HashSet<>();
+ Set<String> v2 = new HashSet<>();
+ assertThat(v1).isEmpty();
v1.add("x");
v1.add("y");
v2.add("y");
v2.add("x");
- assertThat(v1.size()).isEqualTo(2);
+ assertThat(v1).hasSize(2);
m.put("s1", v1);
assertThat(m.get("s1")).isEqualTo(v1);
- // compare StringSet by value
+ // compare Set<String> by value
assertThat(m.get("s1")).isEqualTo(v2);
- assertThat(m.get("s2")).isEqualTo(null);
- assertThat(m.size()).isEqualTo(1);
+ assertThat(m.get("s2")).isNull();
+ assertThat(m).hasSize(1);
}
@Test
public void addStringSetTest() {
- StringSet s = new StringSet();
- assertThat(s.size()).isEqualTo(0);
+ Set<String> s = new HashSet<>();
+ assertThat(s).isEmpty();
s.add("s1");
- assertThat(s.contains("s1")).isEqualTo(true);
- assertThat(s.contains("s2")).isEqualTo(false);
- assertThat(s.size()).isEqualTo(1);
- }
-
- @Test
- public void normalizeURLTest() {
- String otherURL = "other:///something///else";
- String normalOtherURL = "other://something///else";
- String normalURL = "http://www.google.com:8080/plugins";
- String badURL = "http:///www.google.com:8080/plugins";
- String localURL = "http://localhost:8080/plugins";
- String badLocalURL = "http:///localhost:8080/plugins";
- // Allow other URL protocols, although we might need only http for now.
- assertThat(Util.normalizeURL(otherURL)).isEqualTo(normalOtherURL);
- assertThat(Util.normalizeURL(normalURL)).isEqualTo(normalURL);
- assertThat(Util.normalizeURL(badURL)).isEqualTo(normalURL);
- assertThat(Util.normalizeURL(localURL)).isEqualTo(localURL);
- assertThat(Util.normalizeURL(badLocalURL)).isEqualTo(localURL);
+ assertThat(s.contains("s1")).isTrue();
+ assertThat(s.contains("s2")).isFalse();
+ assertThat(s).hasSize(1);
}
@Test
@@ -125,33 +112,30 @@
assertThat(Util.stripMagicPrefix(")]}'\nxyz")).isEqualTo("xyz");
}
- // TODO: use a mocked HTTP server to test:
- // getHTTP getHTTPJsonObject getHTTPJsonArray getHTTPBase64Content
-
@Test
public void getDirNameTest() {
- String[] files = {"", "./d1/", "d1/d2/f1.c", "./d2/f2.c", "./d1" };
+ String[] files = {"", "./d1/", "d1/d2/f1.c", "./d2/f2.c", "./d1"};
String[] dirs = {null, ".", "d1/d2", "./d2", "."};
for (int i = 0; i < files.length; i++) {
- assertThat(Util.getDirName(files[i])).isEqualTo(dirs[i]);
+ assertThat(Util.getDirName(files[i])).isEqualTo(dirs[i]);
}
}
@Test
public void normalizeFilePathTest() {
- String[] files = {"", "./d1/", "d1/d2/f1.c", "d2/f2/", "d1" };
+ String[] files = {"", "./d1/", "d1/d2/f1.c", "d2/f2/", "d1"};
String[] results = {"./", "./d1/", "./d1/d2/f1.c", "./d2/f2/", "./d1"};
for (int i = 0; i < files.length; i++) {
- assertThat(Util.normalizedFilePath(files[i])).isEqualTo(results[i]);
+ assertThat(Util.normalizedFilePath(files[i])).isEqualTo(results[i]);
}
}
@Test
public void normalizedDirPathTest() {
- String[] files = {"", "./d1/", "d1/d2/f1.c", "./d2/f2.c", "./d1" };
+ String[] files = {"", "./d1/", "d1/d2/f1.c", "./d2/f2.c", "./d1"};
String[] dirs = {null, ".", "./d1/d2", "./d2", "."};
for (int i = 0; i < files.length; i++) {
- assertThat(Util.normalizedDirPath(files[i])).isEqualTo(dirs[i]);
+ assertThat(Util.normalizedDirPath(files[i])).isEqualTo(dirs[i]);
}
}
@@ -160,20 +144,15 @@
String[] yesStrs = {"True", "1", "true", "TRUE", "yes"};
String[] noStrs = {"", "False", "0", "false", "FALSE", "no", "other"};
for (String s : yesStrs) {
- assertThat(Util.parseBoolean(s)).isEqualTo(true);
+ assertThat(Util.parseBoolean(s)).isTrue();
}
for (String s : noStrs) {
- assertThat(Util.parseBoolean(s)).isEqualTo(false);
+ assertThat(Util.parseBoolean(s)).isFalse();
}
}
@Test
- public void sortTest() {
- // TODO: test Util.sort
- }
-
- @Test
- public void newJsonArrayFromStringSetTest() {
- // TODO: test Uril.newJsonArrayFromStringSet
+ public void makeSortedMapTest() {
+ // TODO: test Util.makeSortedMap
}
}