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
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
index fb5d0f5..21a236b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -127,6 +127,7 @@
@UiField ListBox revisionList;
@UiField Labels labels;
@UiField CommitBox commit;
+ @UiField RelatedChanges related;
@UiField FileTable files;
@UiField FlowPanel history;
@@ -210,12 +211,17 @@
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 @@
reload.set(info);
topic.set(info);
commit.set(commentLinkProcessor, info, revision);
+ related.set(info, revision);
quickApprove.set(info, revision);
if (Gerrit.isSignedIn()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
index e0c56bb..0df9ff5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
@@ -131,10 +131,13 @@
color: red;
}
- .commitColumn {
+ .commitColumn, .related {
padding: 0;
vertical-align: top;
}
+ .commitColumn {
+ width: 600px;
+ }
.labels {
border-spacing: 0;
@@ -272,6 +275,10 @@
<td class='{style.commitColumn}'>
<c:CommitBox ui:field='commit'/>
</td>
+
+ <td class='{style.related}'>
+ <c:RelatedChanges ui:field='related'/>
+ </td>
</tr>
</table>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
new file mode 100644
index 0000000..e7b5d47
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
@@ -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();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties
new file mode 100644
index 0000000..f5971b6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties
@@ -0,0 +1,3 @@
+previousChange = Previous related change
+nextChange = Next related change
+openChange = Open related change
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index c5005ef..f2955c4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -55,7 +55,7 @@
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 {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
new file mode 100644
index 0000000..c4c9e09
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -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() {
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml
new file mode 100644
index 0000000..1fbaff8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml
@@ -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>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
index 4b1681e..3f6cfe6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
@@ -21,6 +21,7 @@
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();
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index f2b1cb7..ac2c849 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -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 @@
@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;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
new file mode 100644
index 0000000..f0a1738
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -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();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index a9e3604..814adde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -64,6 +64,7 @@
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);