Merge "ChangeScreen2: Display a list of related commits"
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 208621a..83215d9 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
@@ -139,6 +139,7 @@
@UiField ListBox revisionList;
@UiField Labels labels;
@UiField CommitBox commit;
+ @UiField RelatedChanges related;
@UiField FileTable files;
@UiField FlowPanel history;
@@ -243,12 +244,17 @@
handlers.add(GlobalKey.add(this, keysNavigation));
handlers.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));
@@ -545,6 +551,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 6a0b717..d50acc9 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
@@ -121,10 +121,13 @@
color: red;
}
- .commitColumn {
+ .commitColumn, .related {
padding: 0;
vertical-align: top;
}
+ .commitColumn {
+ width: 600px;
+ }
.labels {
border-spacing: 0;
@@ -279,6 +282,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 2cf1caf..a637fbe 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
@@ -57,7 +57,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);