Visualize in which revisions a merged change is included

Introduced a new "Included in" panel on merged changes. The panel
will produce sorted lists all branches and tags which contains the
merged change.

Provides a simple way for users to determine in which revisions of
the project the change is included.

Change-Id: If94b2604607f53a2e45330b56d55cc5de8288054
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
index 1f63adf..4d362fe 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
@@ -26,6 +26,8 @@
 public interface ChangeDetailService extends RemoteJsonService {
   void changeDetail(Change.Id id, AsyncCallback<ChangeDetail> callback);
 
+  void includedInDetail(Change.Id id, AsyncCallback<IncludedInDetail> callback);
+
   void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback);
 
   @SignInRequired
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java
new file mode 100644
index 0000000..9365db8
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2010 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.common.data;
+
+import java.util.Collections;
+import java.util.List;
+
+public class IncludedInDetail {
+  private List<String> branches;
+  private List<String> tags;
+
+  public IncludedInDetail() {
+  }
+
+  public void setBranches(final List<String> b) {
+    Collections.sort(b);
+    branches = b;
+  }
+
+  public List<String> getBranches() {
+    return branches;
+  }
+
+  public void setTags(final List<String> t) {
+    Collections.sort(t);
+    tags = t;
+  }
+
+  public List<String> getTags() {
+    return tags;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index fed0927..4e34d25 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -63,6 +63,7 @@
   String prevPatchLinkIcon();
   String nextPatchLinkIcon();
 
+  String changeScreenIncludedIn();
   String changeScreenDependencies();
   String changeScreenDependsOn();
   String changeScreenNeededBy();
@@ -79,6 +80,9 @@
   String changeInfoBlockStatus();
   String changePermalink();
 
+  String includedInTableBranch();
+  String includedInTableTag();
+
   String messageNoAuthor();
   String messageExpandRecent();
   String messageExpandAll();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 155d657..7211c94 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -40,6 +40,7 @@
 patchTableNext = Next file
 patchTableOpen = Open file
 
+changeScreenIncludedIn =  Included in
 changeScreenDependencies =  Dependencies
 changeScreenDependsOn = Depends On
 changeScreenNeededBy = Needed By
@@ -56,6 +57,9 @@
 changeInfoBlockStatus = Status
 changePermalink = Permalink
 
+includedInTableBranch = Branch Name
+includedInTableTag = Tag Name
+
 messageNoAuthor = Gerrit Code Review
 messageExpandRecent = Expand Recent
 messageExpandAll = Expand All
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 9288788..a700dd8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.Change.Status;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -64,6 +65,8 @@
   private ChangeDescriptionBlock descriptionBlock;
   private ApprovalTable approvals;
 
+  private IncludedInTable includedInTable;
+  private DisclosurePanel includedInPanel;
   private DisclosurePanel dependenciesPanel;
   private ChangeTable dependencies;
   private ChangeTable.Section dependsOn;
@@ -171,6 +174,12 @@
     approvals = new ApprovalTable();
     add(approvals);
 
+    includedInPanel = new DisclosurePanel(Util.C.changeScreenIncludedIn());
+    includedInTable = new IncludedInTable(changeId);
+
+    includedInPanel.setContent(includedInTable);
+    add(includedInPanel);
+
     dependencies = new ChangeTable() {
       {
         table.setWidth("98%");
@@ -219,6 +228,13 @@
       setStarred(detail.isStarred());
     }
 
+    if (Status.MERGED == detail.getChange().getStatus()) {
+      includedInPanel.setVisible(true);
+      includedInPanel.addOpenHandler(includedInTable);
+    } else {
+      includedInPanel.setVisible(false);
+    }
+
     dependencies.setAccountInfoCache(detail.getAccounts());
     approvals.setAccountInfoCache(detail.getAccounts());
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java
new file mode 100644
index 0000000..533999d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2010 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.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.data.IncludedInDetail;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gwt.event.logical.shared.OpenEvent;
+import com.google.gwt.event.logical.shared.OpenHandler;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DisclosurePanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+
+
+/** Displays a table of Branches and Tags containing the change record. */
+public class IncludedInTable extends Composite implements
+    OpenHandler<DisclosurePanel> {
+  private final Grid table;
+  private final Change.Id changeId;
+  private boolean loaded = false;
+
+  public IncludedInTable(final Change.Id chId) {
+    changeId = chId;
+    table = new Grid(1, 1);
+    initWidget(table);
+  }
+
+  public void loadTable(final IncludedInDetail detail) {
+    int row = 0;
+    table.resizeRows(detail.getBranches().size() + 1);
+    table.addStyleName(Gerrit.RESOURCES.css().changeTable());
+    final CellFormatter fmt = table.getCellFormatter();
+    fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().dataHeader());
+    table.setText(row, 0, Util.C.includedInTableBranch());
+
+    for (final String branch : detail.getBranches()) {
+      fmt.addStyleName(++row, 0, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().leftMostCell());
+      table.setText(row, 0, branch);
+    }
+
+    if (!detail.getTags().isEmpty()) {
+      table.resizeRows(table.getRowCount() + 2 + detail.getTags().size());
+      row++;
+      fmt.addStyleName(++row, 0, Gerrit.RESOURCES.css().dataHeader());
+      table.setText(row, 0, Util.C.includedInTableTag());
+
+      for (final String tag : detail.getTags()) {
+        fmt.addStyleName(++row, 0, Gerrit.RESOURCES.css().dataCell());
+        fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().leftMostCell());
+        table.setText(row, 0, tag);
+      }
+    }
+
+    table.setVisible(true);
+    loaded = true;
+  }
+
+  @Override
+  public void onOpen(OpenEvent<DisclosurePanel> event) {
+    if (!loaded) {
+      Util.DETAIL_SVC.includedInDetail(changeId,
+          new GerritCallback<IncludedInDetail>() {
+            @Override
+            public void onSuccess(final IncludedInDetail result) {
+              loadTable(result);
+            }
+          });
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
index 12fc41d..ebb4502 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeDetailService;
+import com.google.gerrit.common.data.IncludedInDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
 import com.google.gerrit.reviewdb.Change;
@@ -25,14 +26,17 @@
 
 class ChangeDetailServiceImpl implements ChangeDetailService {
   private final ChangeDetailFactory.Factory changeDetail;
+  private final IncludedInDetailFactory.Factory includedInDetail;
   private final PatchSetDetailFactory.Factory patchSetDetail;
   private final PatchSetPublishDetailFactory.Factory patchSetPublishDetail;
 
   @Inject
   ChangeDetailServiceImpl(final ChangeDetailFactory.Factory changeDetail,
+      final IncludedInDetailFactory.Factory includedInDetail,
       final PatchSetDetailFactory.Factory patchSetDetail,
       final PatchSetPublishDetailFactory.Factory patchSetPublishDetail) {
     this.changeDetail = changeDetail;
+    this.includedInDetail = includedInDetail;
     this.patchSetDetail = patchSetDetail;
     this.patchSetPublishDetail = patchSetPublishDetail;
   }
@@ -42,6 +46,11 @@
     changeDetail.create(id).to(callback);
   }
 
+  public void includedInDetail(final Change.Id id,
+      final AsyncCallback<IncludedInDetail> callback) {
+    includedInDetail.create(id).to(callback);
+  }
+
   public void patchSetDetail(final PatchSet.Id id,
       final AsyncCallback<PatchSetDetail> callback) {
     patchSetDetail.create(id).to(callback);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
index 211f505..8605de3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
@@ -30,6 +30,7 @@
       protected void configure() {
         factory(AbandonChange.Factory.class);
         factory(ChangeDetailFactory.Factory.class);
+        factory(IncludedInDetailFactory.Factory.class);
         factory(PatchSetDetailFactory.Factory.class);
         factory(PatchSetPublishDetailFactory.Factory.class);
         factory(SubmitAction.Factory.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
new file mode 100644
index 0000000..b49a078
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2010 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.httpd.rpc.changedetail;
+
+import com.google.gerrit.common.data.IncludedInDetail;
+import com.google.gerrit.common.errors.InvalidRevisionException;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Creates a {@link IncludedInDetail} of a {@link Change}. */
+class IncludedInDetailFactory extends Handler<IncludedInDetail> {
+  interface Factory {
+    IncludedInDetailFactory create(Change.Id id);
+  }
+
+  private final ReviewDb db;
+  private final ChangeControl.Factory changeControlFactory;
+  private final GitRepositoryManager repoManager;
+  private final Change.Id changeId;
+
+  private IncludedInDetail detail;
+  private ChangeControl control;
+
+  @Inject
+  IncludedInDetailFactory(final ReviewDb db,
+      final ChangeControl.Factory changeControlFactory,
+      final GitRepositoryManager repoManager, @Assisted final Change.Id changeId) {
+    this.changeControlFactory = changeControlFactory;
+    this.repoManager = repoManager;
+    this.changeId = changeId;
+    this.db = db;
+  }
+
+  @Override
+  public IncludedInDetail call() throws OrmException, NoSuchChangeException,
+      NoSuchEntityException, IOException, InvalidRevisionException {
+    control = changeControlFactory.validateFor(changeId);
+    final PatchSet patch =
+        db.patchSets().get(control.getChange().currentPatchSetId());
+    final Repository repo =
+        repoManager.openRepository(control.getProject().getName());
+    final Map<String, Ref> refsHeads =
+        repo.getRefDatabase().getRefs(Constants.R_HEADS);
+    final Map<String, Ref> refsTags =
+        repo.getRefDatabase().getRefs(Constants.R_TAGS);
+    RevWalk rw = new RevWalk(repo);
+
+    try {
+      final RevCommit rev =
+          rw.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+      final List<String> branches = new ArrayList<String>();
+      for (final String branch : refsHeads.keySet()) {
+        if (rw.isMergedInto(rev, rw.parseCommit(refsHeads.get(branch)
+            .getObjectId()))) {
+          branches.add(branch);
+        }
+      }
+      final List<String> tags = new ArrayList<String>();
+      for (final String tag : refsTags.keySet()) {
+        if (rw.isMergedInto(rev, rw
+            .parseCommit(refsTags.get(tag).getObjectId()))) {
+          tags.add(tag);
+        }
+      }
+      detail = new IncludedInDetail();
+      detail.setBranches(branches);
+      detail.setTags(tags);
+
+      return detail;
+    } catch (IncorrectObjectTypeException err) {
+      throw new InvalidRevisionException();
+    } catch (MissingObjectException err) {
+      throw new InvalidRevisionException();
+    }
+  }
+}