ChangeScreen2: Display a list of related commits

Instead of just showing the immediate parent(s) and immediate first
child of the current revision, display the entire topic lineage back
to the merge base with the branch, and all changes that build on this
change.  Display in a scroll pane to the right of the commit info.

This makes navigation within a topic much easier, at a glance the
reviewer can see where this change fits in and what changes come
before and after it in the history.

I am not sure about the result structure returned by /related.  It is
sufficient for this use case but may be too verbose and subject to
changes as this feature is iterated on in the web UI.  Documentation
for /related is omitted until the project has settled on and can
commit to an output format.

Change-Id: I9b9e63a02af1c762fcad32cfef541af0b1fa9114
This commit is contained in:
Shawn Pearce
2013-07-22 10:18:24 -07:00
parent ba08a67a73
commit 0cf04a7f65
11 changed files with 717 additions and 2 deletions

View File

@@ -127,6 +127,7 @@ public class ChangeScreen2 extends Screen {
@UiField ListBox revisionList;
@UiField Labels labels;
@UiField CommitBox commit;
@UiField RelatedChanges related;
@UiField FileTable files;
@UiField FlowPanel history;
@@ -210,12 +211,17 @@ public class ChangeScreen2 extends Screen {
keys.add(GlobalKey.add(this, keysNavigation));
keys.add(GlobalKey.add(this, keysAction));
files.registerKeys();
related.registerKeys();
}
@Override
public void onShowView() {
super.onShowView();
related.setMaxHeight(commit.getElement()
.getParentElement()
.getOffsetHeight());
String prior = Gerrit.getPriorView();
if (prior != null && prior.startsWith("/c/")) {
scrollToPath(prior.substring(3));
@@ -427,6 +433,7 @@ public class ChangeScreen2 extends Screen {
reload.set(info);
topic.set(info);
commit.set(commentLinkProcessor, info, revision);
related.set(info, revision);
quickApprove.set(info, revision);
if (Gerrit.isSignedIn()) {

View File

@@ -131,10 +131,13 @@ limitations under the License.
color: red;
}
.commitColumn {
.commitColumn, .related {
padding: 0;
vertical-align: top;
}
.commitColumn {
width: 600px;
}
.labels {
border-spacing: 0;
@@ -272,6 +275,10 @@ limitations under the License.
<td class='{style.commitColumn}'>
<c:CommitBox ui:field='commit'/>
</td>
<td class='{style.related}'>
<c:RelatedChanges ui:field='related'/>
</td>
</tr>
</table>

View File

@@ -0,0 +1,21 @@
// Copyright (C) 2013 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.client.change;
interface Constants extends com.google.gwt.i18n.client.Constants {
String previousChange();
String nextChange();
String openChange();
}

View File

@@ -0,0 +1,3 @@
previousChange = Previous related change
nextChange = Next related change
openChange = Open related change

View File

@@ -55,7 +55,7 @@ import java.util.Collections;
import java.util.Comparator;
class FileTable extends FlowPanel {
private static final FileTableResources R = GWT
static final FileTableResources R = GWT
.create(FileTableResources.class);
interface FileTableResources extends ClientBundle {

View File

@@ -0,0 +1,338 @@
// Copyright (C) 2013 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.client.change;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
import com.google.gerrit.client.ui.NavigationTable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
import com.google.gwtexpui.progress.client.ProgressBar;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
class RelatedChanges extends Composite {
interface Binder extends UiBinder<HTMLPanel, RelatedChanges> {}
private static Binder uiBinder = GWT.create(Binder.class);
private static final String OPEN;
private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
static {
OPEN = DOM.createUniqueId().replace('-', '_');
init(OPEN);
}
private static final native void init(String o) /*-{
$wnd[o] = $entry(function(e,i) {
return @com.google.gerrit.client.change.RelatedChanges::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
});
}-*/;
private static boolean onOpen(NativeEvent e, int idx) {
if (link.handleAsClick(e.<Event> cast())) {
MyTable t = getMyTable(e);
if (t != null) {
t.onOpenRow(idx);
e.preventDefault();
return false;
}
}
return true;
}
private static MyTable getMyTable(NativeEvent event) {
com.google.gwt.user.client.Element e = event.getEventTarget().cast();
for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
EventListener l = DOM.getEventListener(e);
if (l instanceof MyTable) {
return (MyTable) l;
}
}
return null;
}
interface Style extends CssResource {
String subject();
}
private String project;
private MyTable table;
private boolean register;
@UiField Style style;
@UiField Element header;
@UiField ScrollPanel scroll;
@UiField ProgressBar progress;
@UiField Element error;
RelatedChanges() {
initWidget(uiBinder.createAndBindUi(this));
}
void set(ChangeInfo info, final String revision) {
if (info.status().isClosed()) {
setVisible(false);
return;
}
project = info.project();
ChangeApi.revision(info.legacy_id().get(), revision)
.view("related")
.get(new AsyncCallback<RelatedInfo>() {
@Override
public void onSuccess(RelatedInfo result) {
render(revision, result.changes());
}
@Override
public void onFailure(Throwable err) {
progress.setVisible(false);
scroll.setVisible(false);
UIObject.setVisible(error, true);
error.setInnerText(err.getMessage());
}
});
}
void setMaxHeight(int height) {
int h = height - header.getOffsetHeight();
scroll.setHeight(h + "px");
}
void registerKeys() {
register = true;
if (table != null) {
table.setRegisterKeys(true);
}
}
private void render(String revision, JsArray<ChangeAndCommit> graph) {
DisplayCommand cmd = new DisplayCommand(revision, graph);
if (cmd.execute()) {
Scheduler.get().scheduleIncremental(cmd);
}
}
private void setTable(MyTable t) {
progress.setVisible(false);
scroll.clear();
scroll.add(t);
scroll.setVisible(true);
table = t;
if (register) {
table.setRegisterKeys(true);
}
}
private String url(ChangeAndCommit c) {
if (c.has_change_number() && c.has_revision_number()) {
PatchSet.Id id = c.patch_set_id();
return "#" + PageLinks.toChange2(
id.getParentKey(),
String.valueOf(id.get()));
}
GitwebLink gw = Gerrit.getGitwebLink();
if (gw != null) {
return gw.toRevision(project, c.commit().commit());
}
return null;
}
private class MyTable extends NavigationTable<ChangeAndCommit> {
private final JsArray<ChangeAndCommit> list;
MyTable(JsArray<ChangeAndCommit> list) {
this.list = list;
table.setWidth("");
keysNavigation.setName(Gerrit.C.sectionNavigation());
keysNavigation.add(new PrevKeyCommand(0, 'K',
Resources.C.previousChange()));
keysNavigation.add(new NextKeyCommand(0, 'J', Resources.C.nextChange()));
keysNavigation.add(new OpenKeyCommand(0, 'O', Resources.C.openChange()));
}
@Override
protected Object getRowItemKey(ChangeAndCommit item) {
return item.id();
}
@Override
protected ChangeAndCommit getRowItem(int row) {
if (0 <= row && row <= list.length()) {
return list.get(row);
}
return null;
}
@Override
protected void onOpenRow(int row) {
if (0 <= row && row <= list.length()) {
ChangeAndCommit c = list.get(row);
String url = url(c);
if (url != null && url.startsWith("#")) {
Gerrit.display(url.substring(1));
} else if (url != null) {
Window.Location.assign(url);
}
}
}
void selectRow(int select) {
movePointerTo(select, true);
}
}
private final class DisplayCommand implements RepeatingCommand {
private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
private final MyTable table;
private final String revision;
private final JsArray<ChangeAndCommit> list;
private boolean attached;
private int row;
private int select;
private double start;
private DisplayCommand(String revision, JsArray<ChangeAndCommit> list) {
this.table = new MyTable(list);
this.revision = revision;
this.list = list;
}
public boolean execute() {
boolean attachedNow = isAttached();
if (!attached && attachedNow) {
// Remember that we have been attached at least once. If
// later we find we aren't attached we should stop running.
attached = true;
} else if (attached && !attachedNow) {
// If the user navigated away, we aren't in the DOM anymore.
// Don't continue to render.
return false;
}
start = System.currentTimeMillis();
while (row < list.length()) {
ChangeAndCommit info = list.get(row);
if (revision.equals(info.commit().commit())) {
select = row;
}
render(sb, row, info);
if ((++row % 10) == 0 && longRunning()) {
updateMeter();
return true;
}
}
table.resetHtml(sb);
setTable(table);
table.selectRow(select);
return false;
}
private void render(SafeHtmlBuilder sb, int row, ChangeAndCommit info) {
sb.openTr();
sb.openTd().setStyleName(FileTable.R.css().pointer()).closeTd();
sb.openTd().addStyleName(style.subject());
String url = url(info);
if (url != null) {
sb.openAnchor().setAttribute("href", url);
if (url.startsWith("#")) {
sb.setAttribute("onclick", OPEN + "(event," + row + ")");
}
sb.append(info.commit().subject());
sb.closeAnchor();
} else {
sb.append(info.commit().subject());
}
sb.closeTd();
sb.closeTr();
}
private void updateMeter() {
progress.setValue((100 * row) / list.length());
}
private boolean longRunning() {
return System.currentTimeMillis() - start > 200;
}
}
private static class RelatedInfo extends JavaScriptObject {
final native JsArray<ChangeAndCommit> changes() /*-{ return this.changes }-*/;
protected RelatedInfo() {
}
}
private static class ChangeAndCommit extends JavaScriptObject {
final native String id() /*-{ return this.change_id }-*/;
final native CommitInfo commit() /*-{ return this.commit }-*/;
final Change.Id legacy_id() {
return has_change_number() ? new Change.Id(_change_number()) : null;
}
final PatchSet.Id patch_set_id() {
return has_change_number() && has_revision_number()
? new PatchSet.Id(legacy_id(), _revision_number())
: null;
}
private final native boolean has_change_number()
/*-{ return this.hasOwnProperty('_change_number') }-*/;
private final native boolean has_revision_number()
/*-{ return this.hasOwnProperty('_revision_number') }-*/;
private final native int _change_number()
/*-{ return this._change_number }-*/;
private final native int _revision_number()
/*-{ return this._revision_number }-*/;
protected ChangeAndCommit() {
}
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2013 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.
-->
<ui:UiBinder
xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'
xmlns:x='urn:import:com.google.gwtexpui.progress.client'>
<ui:style type='com.google.gerrit.client.change.RelatedChanges.Style'>
.subject {
width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</ui:style>
<g:HTMLPanel>
<div ui:field='header'>
<div title='Same branch changes connected by Git history'>
<ui:attribute name='title'/>
<ui:msg>Related Changes</ui:msg>
</div>
</div>
<g:ScrollPanel ui:field='scroll' visible='false'/>
<x:ProgressBar ui:field='progress'/>
<div ui:field='error' aria-hidden='true' style='display: NONE'/>
</g:HTMLPanel>
</ui:UiBinder>

View File

@@ -21,6 +21,7 @@ import com.google.gwt.resources.client.ImageResource;
public interface Resources extends ClientBundle {
public static final Resources I = GWT.create(Resources.class);
static final Constants C = GWT.create(Constants.class);
@Source("star_open.png") ImageResource star_open();
@Source("star_filled.png") ImageResource star_filled();

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Change.Id;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.client.RevId;
@@ -31,6 +32,9 @@ public interface PatchSetAncestorAccess extends
@Query("WHERE key.patchSetId = ? ORDER BY key.position")
ResultSet<PatchSetAncestor> ancestorsOf(PatchSet.Id id) throws OrmException;
@Query("WHERE key.patchSetId.changeId = ?")
ResultSet<PatchSetAncestor> byChange(Id id) throws OrmException;
@Query("WHERE key.patchSetId = ?")
ResultSet<PatchSetAncestor> byPatchSet(PatchSet.Id id) throws OrmException;

View File

@@ -0,0 +1,292 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.change;
import com.google.common.collect.ArrayListMultimap;
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.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.ChangeJson.CommitInfo;
import com.google.gerrit.server.change.ChangeJson.GitPerson;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
public class GetRelated implements RestReadView<RevisionResource> {
private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
private final GitRepositoryManager gitMgr;
private final Provider<ReviewDb> dbProvider;
@Inject
GetRelated(GitRepositoryManager gitMgr, Provider<ReviewDb> db) {
this.gitMgr = gitMgr;
this.dbProvider = db;
}
@Override
public Object apply(RevisionResource rsrc)
throws RepositoryNotFoundException, IOException, OrmException {
Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
try {
Ref ref = git.getRef(rsrc.getChange().getDest().get());
RevWalk rw = new RevWalk(git);
try {
RelatedInfo info = new RelatedInfo();
info.changes = walk(rsrc, rw, ref);
return info;
} finally {
rw.release();
}
} finally {
git.close();
}
}
private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
throws OrmException, IOException {
Map<Change.Id, Change> changes = allOpenChanges(rsrc);
Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(changes.keySet());
List<ChangeAndCommit> graph = children(rsrc, rw, changes, patchSets);
Map<String, PatchSet> commits = Maps.newHashMap();
for (PatchSet p : patchSets.values()) {
commits.put(p.getRevision().get(), p);
}
RevCommit rev = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
rw.sort(RevSort.TOPO);
rw.markStart(rev);
if (ref != null && ref.getObjectId() != null) {
try {
rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
} catch (IncorrectObjectTypeException notCommit) {
// Ignore and treat as new branch.
}
}
for (RevCommit c; (c = rw.next()) != null;) {
PatchSet p = commits.get(c.name());
Change g = p != null ? changes.get(p.getId().getParentKey()) : null;
graph.add(new ChangeAndCommit(g, p, c));
}
return graph;
}
private Map<Change.Id, Change> allOpenChanges(RevisionResource rsrc)
throws OrmException {
ReviewDb db = dbProvider.get();
return db.changes().toMap(
db.changes().byBranchOpenAll(rsrc.getChange().getDest()));
}
private Map<PatchSet.Id, PatchSet> allPatchSets(Collection<Change.Id> ids)
throws OrmException {
int n = ids.size();
ReviewDb db = dbProvider.get();
List<ResultSet<PatchSet>> t = Lists.newArrayListWithCapacity(n);
for (Change.Id id : ids) {
t.add(db.patchSets().byChange(id));
}
Map<PatchSet.Id, PatchSet> r = Maps.newHashMapWithExpectedSize(n * 2);
for (ResultSet<PatchSet> rs : t) {
for (PatchSet p : rs) {
r.put(p.getId(), p);
}
}
return r;
}
private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
Map<Change.Id, Change> changes, Map<PatchSet.Id, PatchSet> patchSets)
throws OrmException, IOException {
// children is a map of parent commit name to PatchSet built on it.
Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
RevFlag seenCommit = rw.newFlag("seenCommit");
LinkedList<String> q = Lists.newLinkedList();
seedQueue(rsrc, rw, seenCommit, patchSets, q);
ProjectControl projectCtl = rsrc.getControl().getProjectControl();
Set<Change.Id> seenChange = Sets.newHashSet();
List<ChangeAndCommit> graph = Lists.newArrayList();
while (!q.isEmpty()) {
String id = q.remove();
// For every matching change find the most recent patch set.
Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
for (PatchSet.Id psId : children.get(id)) {
PatchSet.Id e = matches.get(psId.getParentKey());
if ((e == null || e.get() < psId.get())
&& isVisible(projectCtl, changes, patchSets, e)) {
matches.put(psId.getParentKey(), psId);
}
}
for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
Change change = changes.get(e.getKey());
PatchSet ps = patchSets.get(e.getValue());
if (change == null || ps == null || !seenChange.add(e.getKey())) {
continue;
}
RevCommit c = rw.parseCommit(ObjectId.fromString(
ps.getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.addFirst(ps.getRevision().get());
graph.add(new ChangeAndCommit(change, ps, c));
}
}
}
Collections.reverse(graph);
return graph;
}
private boolean isVisible(ProjectControl projectCtl,
Map<Change.Id, Change> changes,
Map<PatchSet.Id, PatchSet> patchSets,
PatchSet.Id psId) throws OrmException {
Change c = changes.get(psId.getParentKey());
PatchSet ps = patchSets.get(psId);
if (c != null && ps != null) {
ChangeControl ctl = projectCtl.controlFor(c);
return ctl.isVisible(dbProvider.get())
&& ctl.isPatchVisible(ps, dbProvider.get());
}
return false;
}
private void seedQueue(RevisionResource rsrc, RevWalk rw,
RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
LinkedList<String> q) throws IOException {
RevCommit tip = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
tip.add(seenCommit);
q.add(tip.name());
Change.Id cId = rsrc.getChange().getId();
for (PatchSet p : patchSets.values()) {
if (cId.equals(p.getId().getParentKey())) {
try {
RevCommit c = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.add(c.name());
}
} catch (IOException e) {
log.warn(String.format(
"Cannot read patch set %d of %d",
p.getPatchSetId(), cId.get()), e);
}
}
}
}
private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
throws OrmException {
ReviewDb db = dbProvider.get();
List<ResultSet<PatchSetAncestor>> t =
Lists.newArrayListWithCapacity(ids.size());
for (Change.Id id : ids) {
t.add(db.patchSetAncestors().byChange(id));
}
Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
for (ResultSet<PatchSetAncestor> rs : t) {
for (PatchSetAncestor a : rs) {
r.put(a.getAncestorRevision().get(), a.getPatchSet());
}
}
return r;
}
private static GitPerson toGitPerson(PersonIdent id) {
GitPerson p = new GitPerson();
p.name = id.getName();
p.email = id.getEmailAddress();
p.date = new Timestamp(id.getWhen().getTime());
p.tz = id.getTimeZoneOffset();
return p;
}
static class RelatedInfo {
List<ChangeAndCommit> changes;
}
static class ChangeAndCommit {
String changeId;
CommitInfo commit;
Integer _changeNumber;
Integer _revisionNumber;
ChangeAndCommit(@Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
if (change != null) {
changeId = change.getKey().get();
_changeNumber = change.getChangeId();
_revisionNumber = ps != null ? ps.getPatchSetId() : null;
}
commit = new CommitInfo();
commit.commit = c.name();
commit.parents = Lists.newArrayListWithCapacity(c.getParentCount());
for (int i = 0; i < c.getParentCount(); i++) {
CommitInfo p = new CommitInfo();
p.commit = c.getParent(i).name();
commit.parents.add(p);
}
commit.author = toGitPerson(c.getAuthorIdent());
commit.subject = c.getShortMessage();
}
}
}

View File

@@ -64,6 +64,7 @@ public class Module extends RestApiModule {
post(REVISION_KIND, "cherrypick").to(CherryPick.class);
get(REVISION_KIND, "commit").to(GetCommit.class);
get(REVISION_KIND, "mergeable").to(Mergeable.class);
get(REVISION_KIND, "related").to(GetRelated.class);
get(REVISION_KIND, "review").to(GetReview.class);
post(REVISION_KIND, "review").to(PostReview.class);
post(REVISION_KIND, "submit").to(Submit.class);