blob: 98a75aa2db00455926044010e61f66935b46d4b1 [file] [log] [blame]
// 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();
}
}