Java tutorial
// 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.change; import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS; import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES; import static com.google.gerrit.common.changes.ListChangesOption.ALL_REVISIONS; import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_COMMIT; import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_FILES; import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISION; import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_ACCOUNTS; import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_LABELS; import static com.google.gerrit.common.changes.ListChangesOption.LABELS; import static com.google.gerrit.common.changes.ListChangesOption.MESSAGES; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.collect.HashBasedTable; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.collect.Table; import com.google.gerrit.common.changes.ListChangesOption; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.LabelValue; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.extensions.restapi.Url; 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.ChangeMessage; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.PatchSetInfo; import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo; import com.google.gerrit.reviewdb.client.UserIdentity; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountInfo; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.LabelNormalizer; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.ssh.SshInfo; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.jcraft.jsch.HostKey; import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Timestamp; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; public class ChangeJson { private static final Logger log = LoggerFactory.getLogger(ChangeJson.class); @Singleton static class Urls { final String git; final String http; @Inject Urls(@GerritServerConfig Config cfg) { this.git = ensureSlash(cfg.getString("gerrit", null, "canonicalGitUrl")); this.http = ensureSlash(cfg.getString("gerrit", null, "gitHttpUrl")); } private static String ensureSlash(String in) { if (in != null && !in.endsWith("/")) { return in + "/"; } return in; } } private final Provider<ReviewDb> db; private final LabelNormalizer labelNormalizer; private final CurrentUser user; private final AnonymousUser anonymous; private final IdentifiedUser.GenericFactory userFactory; private final ChangeControl.GenericFactory changeControlGenericFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final FileInfoJson fileInfoJson; private final AccountInfo.Loader.Factory accountLoaderFactory; private final Provider<String> urlProvider; private final Urls urls; private ChangeControl.Factory changeControlUserFactory; private SshInfo sshInfo; private EnumSet<ListChangesOption> options; private AccountInfo.Loader accountLoader; private ChangeControl lastControl; @Inject ChangeJson(Provider<ReviewDb> db, LabelNormalizer ln, CurrentUser u, AnonymousUser au, IdentifiedUser.GenericFactory uf, ChangeControl.GenericFactory ccf, PatchSetInfoFactory psi, FileInfoJson fileInfoJson, AccountInfo.Loader.Factory ailf, @CanonicalWebUrl Provider<String> curl, Urls urls) { this.db = db; this.labelNormalizer = ln; this.user = u; this.anonymous = au; this.userFactory = uf; this.changeControlGenericFactory = ccf; this.patchSetInfoFactory = psi; this.fileInfoJson = fileInfoJson; this.accountLoaderFactory = ailf; this.urlProvider = curl; this.urls = urls; options = EnumSet.noneOf(ListChangesOption.class); } public ChangeJson addOption(ListChangesOption o) { options.add(o); return this; } public ChangeJson addOptions(Collection<ListChangesOption> o) { options.addAll(o); return this; } public ChangeJson setSshInfo(SshInfo info) { sshInfo = info; return this; } public ChangeJson setChangeControlFactory(ChangeControl.Factory cf) { changeControlUserFactory = cf; return this; } public ChangeInfo format(ChangeResource rsrc) throws OrmException { return format(new ChangeData(rsrc.getControl())); } public ChangeInfo format(Change change) throws OrmException { return format(new ChangeData(change)); } public ChangeInfo format(Change.Id id) throws OrmException { return format(new ChangeData(id)); } public ChangeInfo format(ChangeData cd) throws OrmException { List<ChangeData> tmp = ImmutableList.of(cd); return formatList2(ImmutableList.of(tmp)).get(0).get(0); } public ChangeInfo format(RevisionResource rsrc) throws OrmException { ChangeData cd = new ChangeData(rsrc.getControl()); cd.limitToPatchSets(ImmutableList.of(rsrc.getPatchSet().getId())); return format(cd); } public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in) throws OrmException { accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size()); for (List<ChangeData> changes : in) { ChangeData.ensureChangeLoaded(db, changes); if (has(ALL_REVISIONS)) { ChangeData.ensureAllPatchSetsLoaded(db, changes); } else { ChangeData.ensureCurrentPatchSetLoaded(db, changes); } ChangeData.ensureCurrentApprovalsLoaded(db, changes); res.add(toChangeInfo(changes)); } accountLoader.fill(); return res; } private boolean has(ListChangesOption option) { return options.contains(option); } private List<ChangeInfo> toChangeInfo(List<ChangeData> changes) throws OrmException { List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size()); for (ChangeData cd : changes) { info.add(toChangeInfo(cd)); } return info; } 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.changeId = in.getKey().get(); out.mergeable = in.getStatus() != Change.Status.MERGED ? in.isMergeable() : null; out.subject = in.getSubject(); out.status = in.getStatus(); out.owner = accountLoader.get(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, has(LABELS), has(DETAILED_LABELS)); Collection<PatchSet.Id> limited = cd.getLimitedPatchSets(); if (out.labels != null && has(DETAILED_LABELS)) { // If limited to specific patch sets but not the current patch set, don't // list permitted labels, since users can't vote on those patch sets. if (limited == null || limited.contains(in.currentPatchSetId())) { out.permitted_labels = permittedLabels(cd); } out.removable_reviewers = removableReviewers(cd, out.labels.values()); } if (options.contains(MESSAGES)) { out.messages = messages(cd); } out.finish(); if (has(ALL_REVISIONS) || has(CURRENT_REVISION) || limited != null) { out.revisions = revisions(cd); if (out.revisions != null) { for (String commit : out.revisions.keySet()) { if (out.revisions.get(commit).isCurrent) { out.current_revision = commit; break; } } } } lastControl = null; return out; } private ChangeControl control(ChangeData cd) throws OrmException { ChangeControl ctrl = cd.changeControl(); if (ctrl != null && ctrl.getCurrentUser() == user) { return ctrl; } else if (lastControl != null && cd.getId().equals(lastControl.getChange().getId())) { return lastControl; } try { if (changeControlUserFactory != null) { ctrl = changeControlUserFactory.controlFor(cd.change(db)); } else { ctrl = changeControlGenericFactory.controlFor(cd.change(db), user); } } catch (NoSuchChangeException e) { return null; } lastControl = ctrl; return ctrl; } private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException { if (cd.getSubmitRecords() != null) { return cd.getSubmitRecords(); } ChangeControl ctl = control(cd); if (ctl == null) { return ImmutableList.of(); } PatchSet ps = cd.currentPatchSet(db); if (ps == null) { return ImmutableList.of(); } cd.setSubmitRecords(ctl.canSubmit(db.get(), ps, cd, true, false, true)); return cd.getSubmitRecords(); } private Map<String, LabelInfo> labelsFor(ChangeData cd, boolean standard, boolean detailed) throws OrmException { if (!standard && !detailed) { return null; } ChangeControl ctl = control(cd); if (ctl == null) { return null; } PatchSet ps = cd.currentPatchSet(db); if (ps == null) { return null; } LabelTypes labelTypes = ctl.getLabelTypes(); if (cd.getChange().getStatus().isOpen()) { return labelsForOpenChange(cd, labelTypes, standard, detailed); } else { return labelsForClosedChange(cd, labelTypes, standard, detailed); } } private Map<String, LabelInfo> labelsForOpenChange(ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed) throws OrmException { Map<String, LabelInfo> labels = initLabels(cd, labelTypes, standard); if (detailed) { setAllApprovals(cd, labels); } for (Map.Entry<String, LabelInfo> e : labels.entrySet()) { LabelType type = labelTypes.byLabel(e.getKey()); if (type == null) { continue; } if (standard) { setRecommendedAndDisliked(cd, type, e.getValue()); } if (detailed) { setLabelValues(type, e.getValue()); } } return labels; } private Map<String, LabelInfo> initLabels(ChangeData cd, LabelTypes labelTypes, boolean standard) throws OrmException { // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167. Map<String, LabelInfo> labels = new TreeMap<String, LabelInfo>(labelTypes.nameComparator()); for (SubmitRecord rec : submitRecords(cd)) { 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; if (standard) { switch (r.status) { case OK: n.approved = accountLoader.get(r.appliedBy); break; case REJECT: n.rejected = accountLoader.get(r.appliedBy); break; default: break; } } n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null; labels.put(r.label, n); } } } return labels; } private void setRecommendedAndDisliked(ChangeData cd, LabelType type, LabelInfo label) throws OrmException { if (label.approved != null || label.rejected != null) { return; } if (type.getMin() == null || type.getMax() == null) { // Unknown or misconfigured type can't have intermediate scores. return; } 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. return; } for (PatchSetApproval psa : cd.currentApprovals(db)) { short val = psa.getValue(); if (val != 0 && min < val && val < max && type.matches(psa)) { if (0 < val) { label.recommended = accountLoader.get(psa.getAccountId()); label.value = val != 1 ? val : null; } else { label.disliked = accountLoader.get(psa.getAccountId()); label.value = val != -1 ? val : null; } } } return; } private void setAllApprovals(ChangeData cd, Map<String, LabelInfo> labels) throws OrmException { ChangeControl baseCtrl = control(cd); if (baseCtrl == null) { return; } // All users ever added, even if they can't vote on one or all labels. Set<Account.Id> allUsers = Sets.newHashSet(); ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals = cd.allApprovalsMap(db); for (PatchSetApproval psa : allApprovals.values()) { allUsers.add(psa.getAccountId()); } List<PatchSetApproval> currentList = labelNormalizer.normalize(baseCtrl, allApprovals.get(baseCtrl.getChange().currentPatchSetId())); // Most recent, normalized vote on each label for the current patch set by // each user (may be 0). Table<Account.Id, String, PatchSetApproval> current = HashBasedTable.create(allUsers.size(), baseCtrl.getLabelTypes().getLabelTypes().size()); for (PatchSetApproval psa : currentList) { current.put(psa.getAccountId(), psa.getLabel(), psa); } for (Account.Id accountId : allUsers) { IdentifiedUser user = userFactory.create(accountId); ChangeControl ctl = baseCtrl.forUser(user); for (Map.Entry<String, LabelInfo> e : labels.entrySet()) { LabelType lt = ctl.getLabelTypes().byLabel(e.getKey()); if (lt == null) { // Ignore submit record for undefined label; likely the submit rule // author didn't intend for the label to show up in the table. continue; } Integer value; Timestamp date = null; PatchSetApproval psa = current.get(accountId, lt.getName()); if (psa != null) { value = Integer.valueOf(psa.getValue()); date = psa.getGranted(); } else { // Either the user cannot vote on this label, or there just wasn't a // dummy approval for this label. Explicitly check whether the user // can vote on this label. value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null; } e.getValue().addApproval(approvalInfo(accountId, value, date)); } } } private Map<String, LabelInfo> labelsForClosedChange(ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed) throws OrmException { Set<Account.Id> allUsers = Sets.newHashSet(); for (PatchSetApproval psa : cd.allApprovals(db)) { allUsers.add(psa.getAccountId()); } Set<String> labelNames = Sets.newHashSet(); Multimap<Account.Id, PatchSetApproval> current = HashMultimap.create(); for (PatchSetApproval a : cd.currentApprovals(db)) { LabelType type = labelTypes.byLabel(a.getLabelId()); if (type != null && a.getValue() != 0) { labelNames.add(type.getName()); current.put(a.getAccountId(), a); } } // We can only approximately reconstruct what the submit rule evaluator // would have done. These should really come from a stored submit record. // // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167. Map<String, LabelInfo> labels = new TreeMap<String, LabelInfo>(labelTypes.nameComparator()); for (String name : labelNames) { LabelType type = labelTypes.byLabel(name); LabelInfo li = new LabelInfo(); if (detailed) { setLabelValues(type, li); } labels.put(type.getName(), li); } for (Account.Id accountId : allUsers) { Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size()); if (detailed) { for (String name : labels.keySet()) { ApprovalInfo ai = approvalInfo(accountId, 0, null); byLabel.put(name, ai); labels.get(name).addApproval(ai); } } for (PatchSetApproval psa : current.get(accountId)) { LabelType type = labelTypes.byLabel(psa.getLabelId()); if (type == null) { continue; } short val = psa.getValue(); ApprovalInfo info = byLabel.get(type.getName()); if (info != null) { info.value = Integer.valueOf(val); info.date = psa.getGranted(); } LabelInfo li = labels.get(type.getName()); if (!standard || li.approved != null || li.rejected != null) { continue; } if (val == type.getMax().getValue()) { li.approved = accountLoader.get(accountId); } else if (val == type.getMin().getValue() // A merged change can't have been rejected. && cd.getChange().getStatus() != Status.MERGED) { li.rejected = accountLoader.get(accountId); } else if (val > 0) { li.recommended = accountLoader.get(accountId); li.value = val; } else if (val < 0) { li.disliked = accountLoader.get(accountId); li.value = val; } } } return labels; } private ApprovalInfo approvalInfo(Account.Id id, Integer value, Timestamp date) { ApprovalInfo ai = new ApprovalInfo(id); ai.value = value; ai.date = date; accountLoader.put(ai); return ai; } private static boolean isOnlyZero(Collection<String> values) { return values.isEmpty() || (values.size() == 1 && values.contains(" 0")); } private void setLabelValues(LabelType type, LabelInfo label) { label.values = Maps.newLinkedHashMap(); for (LabelValue v : type.getValues()) { label.values.put(v.formatValue(), v.getText()); } if (isOnlyZero(label.values.keySet())) { label.values = null; } } private Map<String, Collection<String>> permittedLabels(ChangeData cd) throws OrmException { ChangeControl ctl = control(cd); if (ctl == null) { return null; } LabelTypes labelTypes = ctl.getLabelTypes(); ListMultimap<String, String> permitted = LinkedListMultimap.create(); for (SubmitRecord rec : submitRecords(cd)) { if (rec.labels == null) { continue; } for (SubmitRecord.Label r : rec.labels) { LabelType type = labelTypes.byLabel(r.label); if (type == null) { continue; } PermissionRange range = ctl.getRange(Permission.forLabel(r.label)); for (LabelValue v : type.getValues()) { if (range.contains(v.getValue())) { permitted.put(r.label, v.formatValue()); } } } } List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size()); for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) { if (isOnlyZero(e.getValue())) { toClear.add(e.getKey()); } } for (String label : toClear) { permitted.removeAll(label); } return permitted.asMap(); } private Collection<ChangeMessageInfo> messages(ChangeData cd) throws OrmException { List<ChangeMessage> messages = db.get().changeMessages().byChange(cd.getId()).toList(); if (messages.isEmpty()) { return Collections.emptyList(); } // chronological order Collections.sort(messages, new Comparator<ChangeMessage>() { @Override public int compare(ChangeMessage a, ChangeMessage b) { return a.getWrittenOn().compareTo(b.getWrittenOn()); } }); List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size()); for (ChangeMessage message : messages) { PatchSet.Id patchNum = message.getPatchSetId(); ChangeMessageInfo cmi = new ChangeMessageInfo(); cmi.id = message.getKey().get(); cmi.author = accountLoader.get(message.getAuthor()); cmi.date = message.getWrittenOn(); cmi.message = message.getMessage(); cmi._revisionNumber = patchNum != null ? patchNum.get() : null; result.add(cmi); } return result; } private Collection<AccountInfo> removableReviewers(ChangeData cd, Collection<LabelInfo> labels) throws OrmException { ChangeControl ctl = control(cd); if (ctl == null) { return null; } Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size()); Set<Account.Id> removable = Sets.newHashSetWithExpectedSize(labels.size()); for (LabelInfo label : labels) { if (label.all == null) { continue; } for (ApprovalInfo ai : label.all) { if (ctl.canRemoveReviewer(ai._id, Objects.firstNonNull(ai.value, 0))) { removable.add(ai._id); } else { fixed.add(ai._id); } } } removable.removeAll(fixed); List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size()); for (Account.Id id : removable) { result.add(accountLoader.get(id)); } return result; } 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; } private Map<String, RevisionInfo> revisions(ChangeData cd) throws OrmException { ChangeControl ctl = control(cd); if (ctl == null) { return null; } Collection<PatchSet> src; if (cd.getLimitedPatchSets() != null || has(ALL_REVISIONS)) { src = cd.patches(db); } else { src = Collections.singletonList(cd.currentPatchSet(db)); } Map<String, RevisionInfo> res = Maps.newLinkedHashMap(); for (PatchSet in : src) { if (ctl.isPatchVisible(in, db.get())) { res.put(in.getRevision().get(), toRevisionInfo(cd, in)); } } return res; } private RevisionInfo toRevisionInfo(ChangeData cd, PatchSet in) throws OrmException { RevisionInfo out = new RevisionInfo(); out.isCurrent = in.getId().equals(cd.change(db).currentPatchSetId()); out._number = in.getId().get(); out.draft = in.isDraft() ? true : null; out.fetch = makeFetchMap(cd, in); if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) { try { PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId()); out.commit = new CommitInfo(); out.commit.parents = Lists.newArrayListWithCapacity(info.getParents().size()); out.commit.author = toGitPerson(info.getAuthor()); out.commit.committer = toGitPerson(info.getCommitter()); out.commit.subject = info.getSubject(); out.commit.message = info.getMessage(); for (ParentInfo parent : info.getParents()) { CommitInfo i = new CommitInfo(); i.commit = parent.id.get(); i.subject = parent.shortMessage; out.commit.parents.add(i); } } catch (PatchSetInfoNotAvailableException e) { log.warn("Cannot load PatchSetInfo " + in.getId(), e); } } if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) { try { out.files = fileInfoJson.toFileInfoMap(cd.change(db), in); out.files.remove(Patch.COMMIT_MSG); } catch (PatchListNotAvailableException e) { log.warn("Cannot load PatchList " + in.getId(), e); } } return out; } private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in) throws OrmException { Map<String, FetchInfo> r = Maps.newLinkedHashMap(); String refName = in.getRefName(); ChangeControl ctl = control(cd); if (ctl != null && ctl.forUser(anonymous).isPatchVisible(in, db.get())) { if (urls.git != null) { r.put("git", new FetchInfo(urls.git + cd.change(db).getProject().get(), refName)); } } if (urls.http != null) { r.put("http", new FetchInfo(urls.http + cd.change(db).getProject().get(), refName)); } else { String http = urlProvider.get(); if (!Strings.isNullOrEmpty(http)) { r.put("http", new FetchInfo(http + cd.change(db).getProject().get(), refName)); } } if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) { HostKey host = sshInfo.getHostKeys().get(0); r.put("ssh", new FetchInfo( String.format("ssh://%s/%s", host.getHost(), cd.change(db).getProject().get()), refName)); } return r; } private static GitPerson toGitPerson(UserIdentity committer) { GitPerson p = new GitPerson(); p.name = committer.getName(); p.email = committer.getEmail(); p.date = committer.getDate(); p.tz = committer.getTimeZone(); return p; } public static class ChangeInfo { final String kind = "gerritcodereview#change"; String id; String project; String branch; String topic; public String changeId; public String subject; Change.Status status; Timestamp created; Timestamp updated; Boolean starred; Boolean reviewed; Boolean mergeable; String _sortkey; int _number; AccountInfo owner; Map<String, LabelInfo> labels; Map<String, Collection<String>> permitted_labels; Collection<AccountInfo> removable_reviewers; Collection<ChangeMessageInfo> messages; String current_revision; Map<String, RevisionInfo> revisions; public Boolean _moreChanges; void finish() { id = Joiner.on('~').join(Url.encode(project), Url.encode(branch), Url.encode(changeId)); } } static class RevisionInfo { private transient boolean isCurrent; Boolean draft; int _number; Map<String, FetchInfo> fetch; CommitInfo commit; Map<String, FileInfoJson.FileInfo> files; } static class FetchInfo { String url; String ref; FetchInfo(String url, String ref) { this.url = url; this.ref = ref; } } static class GitPerson { String name; String email; Timestamp date; int tz; } static class CommitInfo { String commit; List<CommitInfo> parents; GitPerson author; GitPerson committer; String subject; String message; } static class LabelInfo { transient SubmitRecord.Label.Status _status; AccountInfo approved; AccountInfo rejected; AccountInfo recommended; AccountInfo disliked; List<ApprovalInfo> all; Map<String, String> values; Short value; Boolean optional; void addApproval(ApprovalInfo ai) { if (all == null) { all = Lists.newArrayList(); } all.add(ai); } } static class ApprovalInfo extends AccountInfo { Integer value; Timestamp date; ApprovalInfo(Account.Id id) { super(id); } } static class ChangeMessageInfo { String id; AccountInfo author; Timestamp date; String message; Integer _revisionNumber; } }