// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.query.change;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.events.AccountAttribute;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gson.reflect.TypeToken;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.io.Writer;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
public class ListChanges {
private final QueryProcessor imp;
private final Provider<ReviewDb> db;
private final ApprovalTypes approvalTypes;
private final CurrentUser user;
private final ChangeControl.Factory changeControlFactory;
private boolean reverse;
private Map<Account.Id, AccountAttribute> accounts;
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
private OutputFormat format = OutputFormat.TEXT;
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
private List<String> queries;
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
void setLimit(int limit) {
imp.setLimit(limit);
}
@Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
void setSortKeyAfter(String key) {
// Querying for the prior page of changes requires sortkey_after predicate.
// Changes are shown most recent->least recent. The previous page of
// results contains changes that were updated after the given key.
imp.setSortkeyAfter(key);
reverse = true;
}
@Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
void setSortKeyBefore(String key) {
// Querying for the next page of changes requires sortkey_before predicate.
// Changes are shown most recent->least recent. The next page contains
// changes that were updated before the given key.
imp.setSortkeyBefore(key);
}
@Inject
ListChanges(QueryProcessor qp,
Provider<ReviewDb> db,
ApprovalTypes at,
CurrentUser u,
ChangeControl.Factory cf) {
this.imp = qp;
this.db = db;
this.approvalTypes = at;
this.user = u;
this.changeControlFactory = cf;
accounts = Maps.newHashMap();
}
public OutputFormat getFormat() {
return format;
}
public ListChanges setFormat(OutputFormat fmt) {
this.format = fmt;
return this;
}
public void query(Writer out)
throws OrmException, QueryParseException, IOException {
if (imp.isDisabled()) {
throw new QueryParseException("query disabled");
}
if (queries == null || queries.isEmpty()) {
queries = Collections.singletonList("status:open");
} else if (queries.size() > 10) {
// Hard-code a default maximum number of queries to prevent
// users from submitting too much to the server in a single call.
throw new QueryParseException("limit of 10 queries");
}
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
for (String query : queries) {
List<ChangeData> changes = imp.queryChanges(query);
boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
if (moreChanges) {
if (reverse) {
changes = changes.subList(1, changes.size());
} else {
changes = changes.subList(0, imp.getLimit());
}
}
ChangeData.ensureChangeLoaded(db, changes);
ChangeData.ensureCurrentPatchSetLoaded(db, changes);
ChangeData.ensureCurrentApprovalsLoaded(db, changes);
List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
for (ChangeData cd : changes) {
info.add(toChangeInfo(cd));
}
if (moreChanges && !info.isEmpty()) {
if (reverse) {
info.get(0)._moreChanges = true;
} else {
info.get(info.size() - 1)._moreChanges = true;
}
}
res.add(info);
}
if (!accounts.isEmpty()) {
for (Account account : db.get().accounts().get(accounts.keySet())) {
AccountAttribute a = accounts.get(account.getId());
a.name = Strings.emptyToNull(account.getFullName());
}
}
if (format.isJson()) {
format.newGson().toJson(
res.size() == 1 ? res.get(0) : res,
new TypeToken<List<ChangeInfo>>() {}.getType(),
out);
out.write('\n');
} else {
boolean firstQuery = true;
for (List<ChangeInfo> info : res) {
if (firstQuery) {
firstQuery = false;
} else {
out.write('\n');
}
for (ChangeInfo c : info) {
String id = new Change.Key(c.id).abbreviate();
String subject = c.subject;
if (subject.length() + id.length() > 80) {
subject = subject.substring(0, 80 - id.length());
}
out.write(id);
out.write(' ');
out.write(subject.replace('\n', ' '));
out.write('\n');
}
}
}
}
private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
ChangeInfo out = new ChangeInfo();
Change in = cd.change(db);
out.project = in.getProject().get();
out.branch = in.getDest().getShortName();
out.topic = in.getTopic();
out.id = in.getKey().get();
out.subject = in.getSubject();
out.status = in.getStatus();
out.owner = asAccountAttribute(in.getOwner());
out.created = in.getCreatedOn();
out.updated = in.getLastUpdatedOn();
out._number = in.getId().get();
out._sortkey = in.getSortKey();
out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
out.labels = labelsFor(cd);
return out;
}
private AccountAttribute asAccountAttribute(Account.Id user) {
if (user == null) {
return null;
}
AccountAttribute a = accounts.get(user);
if (a == null) {
a = new AccountAttribute();
accounts.put(user, a);
}
return a;
}
private Map<String, LabelInfo> labelsFor(ChangeData cd) throws OrmException {
Change in = cd.change(db);
ChangeControl ctl = cd.changeControl();
if (ctl == null || ctl.getCurrentUser() != user) {
try {
ctl = changeControlFactory.controlFor(in);
} catch (NoSuchChangeException e) {
return null;
}
}
PatchSet ps = cd.currentPatchSet(db);
Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true, false)) {
if (rec.labels == null) {
continue;
}
for (SubmitRecord.Label r : rec.labels) {
LabelInfo p = labels.get(r.label);
if (p == null || p._status.compareTo(r.status) < 0) {
LabelInfo n = new LabelInfo();
n._status = r.status;
switch (r.status) {
case OK:
n.approved = asAccountAttribute(r.appliedBy);
break;
case REJECT:
n.rejected = asAccountAttribute(r.appliedBy);
break;
}
n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
labels.put(r.label, n);
}
}
}
Collection<PatchSetApproval> approvals = null;
for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
if (e.getValue().approved != null || e.getValue().rejected != null) {
continue;
}
ApprovalType type = approvalTypes.byLabel(e.getKey());
if (type == null || type.getMin() == null || type.getMax() == null) {
// Unknown or misconfigured type can't have intermediate scores.
continue;
}
short min = type.getMin().getValue();
short max = type.getMax().getValue();
if (-1 <= min && max <= 1) {
// Types with a range of -1..+1 can't have intermediate scores.
continue;
}
if (approvals == null) {
approvals = cd.currentApprovals(db);
}
for (PatchSetApproval psa : approvals) {
short val = psa.getValue();
if (val != 0 && min < val && val < max
&& psa.getCategoryId().equals(type.getCategory().getId())) {
if (0 < val) {
e.getValue().recommended = asAccountAttribute(psa.getAccountId());
e.getValue().value = val != 1 ? val : null;
} else {
e.getValue().disliked = asAccountAttribute(psa.getAccountId());
e.getValue().value = val != -1 ? val : null;
}
}
}
}
return labels;
}
private boolean isChangeReviewed(ChangeData cd) throws OrmException {
if (user instanceof IdentifiedUser) {
PatchSet currentPatchSet = cd.currentPatchSet(db);
if (currentPatchSet == null) {
return false;
}
List<ChangeMessage> messages =
db.get().changeMessages().byPatchSet(currentPatchSet.getId()).toList();
if (messages.isEmpty()) {
return false;
}
// Sort messages to let the most recent ones at the beginning.
Collections.sort(messages, new Comparator<ChangeMessage>() {
@Override
public int compare(ChangeMessage a, ChangeMessage b) {
return b.getWrittenOn().compareTo(a.getWrittenOn());
}
});
Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
Account.Id changeOwnerId = cd.change(db).getOwner();
for (ChangeMessage cm : messages) {
if (currentUserId.equals(cm.getAuthor())) {
return true;
} else if (changeOwnerId.equals(cm.getAuthor())) {
return false;
}
}
}
return false;
}
static class ChangeInfo {
String project;
String branch;
String topic;
String id;
String subject;
Change.Status status;
Timestamp created;
Timestamp updated;
Boolean starred;
Boolean reviewed;
String _sortkey;
int _number;
AccountAttribute owner;
Map<String, LabelInfo> labels;
Boolean _moreChanges;
}
static class LabelInfo {
transient SubmitRecord.Label.Status _status;
AccountAttribute approved;
AccountAttribute rejected;
AccountAttribute recommended;
AccountAttribute disliked;
Short value;
Boolean optional;
}
}