// 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 static com.google.gerrit.common.PageLinks.op; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.changes.ChangeApi; import com.google.gerrit.client.changes.ChangeList; import com.google.gerrit.client.info.ChangeInfo; import com.google.gerrit.client.info.ChangeInfo.CommitInfo; import com.google.gerrit.client.info.ChangeInfo.RevisionInfo; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.extensions.client.ListChangesOption; 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.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.TabBar; import com.google.gwt.user.client.ui.TabPanel; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; public class RelatedChanges extends TabPanel { static final RelatedChangesResources R = GWT.create(RelatedChangesResources.class); interface RelatedChangesResources extends ClientBundle { @Source("related_changes.css") RelatedChangesCss css(); } interface RelatedChangesCss extends CssResource { String activeRow(); String current(); String gitweb(); String indirect(); String notCurrent(); String pointer(); String row(); String subject(); String strikedSubject(); String submittable(); String tabPanel(); } enum Tab { RELATED_CHANGES(Resources.C.relatedChanges(), Resources.C.relatedChangesTooltip()) { @Override String getTitle(int count) { return Resources.M.relatedChanges(count); } @Override String getTitle(String count) { return Resources.M.relatedChanges(count); } }, SUBMITTED_TOGETHER(Resources.C.submittedTogether(), Resources.C.submittedTogether()) { @Override String getTitle(int count) { return Resources.M.submittedTogether(count); } @Override String getTitle(String count) { return Resources.M.submittedTogether(count); } }, SAME_TOPIC(Resources.C.sameTopic(), Resources.C.sameTopicTooltip()) { @Override String getTitle(int count) { return Resources.M.sameTopic(count); } @Override String getTitle(String count) { return Resources.M.sameTopic(count); } }, CONFLICTING_CHANGES(Resources.C.conflictingChanges(), Resources.C.conflictingChangesTooltip()) { @Override String getTitle(int count) { return Resources.M.conflictingChanges(count); } @Override String getTitle(String count) { return Resources.M.conflictingChanges(count); } }, CHERRY_PICKS(Resources.C.cherryPicks(), Resources.C.cherryPicksTooltip()) { @Override String getTitle(int count) { return Resources.M.cherryPicks(count); } @Override String getTitle(String count) { return Resources.M.cherryPicks(count); } }; final String defaultTitle; final String tooltip; abstract String getTitle(int count); abstract String getTitle(String count); Tab(String defaultTitle, String tooltip) { this.defaultTitle = defaultTitle; this.tooltip = tooltip; } } private static Tab savedTab; private final List<RelatedChangesTab> tabs; private int maxHeightWithHeader; private int selectedTab; private int outstandingCallbacks; RelatedChanges() { tabs = new ArrayList<>(Tab.values().length); selectedTab = -1; setVisible(false); addStyleName(R.css().tabPanel()); initTabBar(); } private void initTabBar() { TabBar tabBar = getTabBar(); tabBar.addSelectionHandler( new SelectionHandler<Integer>() { @Override public void onSelection(SelectionEvent<Integer> event) { if (selectedTab >= 0) { tabs.get(selectedTab).registerKeys(false); } selectedTab = event.getSelectedItem(); tabs.get(selectedTab).registerKeys(true); } }); for (Tab tabInfo : Tab.values()) { RelatedChangesTab panel = new RelatedChangesTab(tabInfo); add(panel, tabInfo.defaultTitle); tabs.add(panel); TabBar.Tab tab = tabBar.getTab(tabInfo.ordinal()); tab.setWordWrap(false); ((Composite) tab).setTitle(tabInfo.tooltip); setTabEnabled(tabInfo, false); } getTab(Tab.RELATED_CHANGES).setShowIndirectAncestors(true); getTab(Tab.CHERRY_PICKS).setShowBranches(true); getTab(Tab.SAME_TOPIC).setShowBranches(true); getTab(Tab.SAME_TOPIC).setShowProjects(true); getTab(Tab.SAME_TOPIC).setShowSubmittable(true); getTab(Tab.SUBMITTED_TOGETHER).setShowBranches(true); getTab(Tab.SUBMITTED_TOGETHER).setShowProjects(true); getTab(Tab.SUBMITTED_TOGETHER).setShowSubmittable(true); } void set(final ChangeInfo info, final String revision) { if (info.status().isOpen()) { setForOpenChange(info, revision); } ChangeApi.revision(info.legacyId().get(), revision) .view("related") .get( new TabCallback<RelatedInfo>(Tab.RELATED_CHANGES, info.project(), revision) { @Override public JsArray<ChangeAndCommit> convert(RelatedInfo result) { return result.changes(); } }); StringBuilder cherryPicksQuery = new StringBuilder(); cherryPicksQuery.append(op("project", info.project())); cherryPicksQuery.append(" ").append(op("change", info.changeId())); cherryPicksQuery.append(" ").append(op("-change", info.legacyId().get())); cherryPicksQuery.append(" -is:abandoned"); ChangeList.query( cherryPicksQuery.toString(), EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT), new TabChangeListCallback(Tab.CHERRY_PICKS, info.project(), revision)); if (info.currentRevision() != null && info.currentRevision().equals(revision)) { ChangeApi.change(info.legacyId().get()) .view("submitted_together") .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER, info.project(), revision)); } if (!Gerrit.info().change().isSubmitWholeTopicEnabled() && info.topic() != null && !"".equals(info.topic())) { StringBuilder topicQuery = new StringBuilder(); topicQuery.append("status:open"); topicQuery.append(" ").append(op("topic", info.topic())); ChangeList.query( topicQuery.toString(), EnumSet.of( ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT, ListChangesOption.DETAILED_LABELS, ListChangesOption.LABELS), new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision)); } } private void setForOpenChange(final ChangeInfo info, final String revision) { if (info.mergeable()) { StringBuilder conflictsQuery = new StringBuilder(); conflictsQuery.append("status:open"); conflictsQuery.append(" is:mergeable"); conflictsQuery.append(" ").append(op("conflicts", info.legacyId().get())); ChangeList.query( conflictsQuery.toString(), EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT), new TabChangeListCallback(Tab.CONFLICTING_CHANGES, info.project(), revision)); } } @Override protected void onLoad() { super.onLoad(); R.css().ensureInjected(); } static void setSavedTab(Tab subject) { savedTab = subject; } private RelatedChangesTab getTab(Tab tabInfo) { return tabs.get(tabInfo.ordinal()); } private void setTabTitle(Tab tabInfo, String title) { getTabBar().setTabText(tabInfo.ordinal(), title); } private void setTabEnabled(Tab tabInfo, boolean enabled) { getTabBar().setTabEnabled(tabInfo.ordinal(), enabled); } void setMaxHeight(int height) { maxHeightWithHeader = height; if (isVisible()) { applyMaxHeight(); } } private void applyMaxHeight() { int header = getTabBar().getOffsetHeight() + 2 /* padding */; for (int i = 0; i < getTabBar().getTabCount(); i++) { tabs.get(i).setMaxHeight(maxHeightWithHeader - header); } } private abstract class TabCallback<T> implements AsyncCallback<T> { private final Tab tabInfo; private final String project; private final String revision; TabCallback(Tab tabInfo, String project, String revision) { this.tabInfo = tabInfo; this.project = project; this.revision = revision; outstandingCallbacks++; } protected abstract JsArray<ChangeAndCommit> convert(T result); @Override public void onSuccess(T result) { if (isAttached()) { JsArray<ChangeAndCommit> changes = convert(result); if (changes.length() > 0) { setTabTitle(tabInfo, tabInfo.getTitle(changes.length())); getTab(tabInfo).setChanges(project, revision, changes); } onDone(changes.length() > 0); } } @Override public void onFailure(Throwable err) { if (isAttached()) { setTabTitle(tabInfo, tabInfo.getTitle(Resources.C.notAvailable())); getTab(tabInfo).setError(err.getMessage()); onDone(true); } } private void onDone(boolean enabled) { setTabEnabled(tabInfo, enabled); outstandingCallbacks--; if (outstandingCallbacks == 0 || (enabled && tabInfo == Tab.RELATED_CHANGES)) { outstandingCallbacks = 0; // Only execute this block once for (int i = 0; i < getTabBar().getTabCount(); i++) { if (getTabBar().isTabEnabled(i)) { selectTab(i); setVisible(true); applyMaxHeight(); break; } } } if (tabInfo == savedTab && enabled) { selectTab(savedTab.ordinal()); } } } private class TabChangeListCallback extends TabCallback<ChangeList> { TabChangeListCallback(Tab tabInfo, String project, String revision) { super(tabInfo, project, revision); } @Override protected JsArray<ChangeAndCommit> convert(ChangeList l) { JsArray<ChangeAndCommit> arr = JavaScriptObject.createArray().cast(); for (ChangeInfo i : Natives.asList(l)) { if (i.currentRevision() != null && i.revisions().containsKey(i.currentRevision())) { RevisionInfo currentRevision = i.revision(i.currentRevision()); ChangeAndCommit c = ChangeAndCommit.create(); c.setId(i.id()); c.setCommit(currentRevision.commit()); c.setChangeNumber(i.legacyId().get()); c.setRevisionNumber(currentRevision._number()); c.setBranch(i.branch()); c.setProject(i.project()); c.setSubmittable(i.submittable() && i.mergeable()); c.setStatus(i.status().asChangeStatus().toString()); arr.push(c); } } return arr; } } public static class RelatedInfo extends JavaScriptObject { public final native JsArray<ChangeAndCommit> changes() /*-{ return this.changes }-*/; protected RelatedInfo() {} } public static class ChangeAndCommit extends JavaScriptObject { static ChangeAndCommit create() { return (ChangeAndCommit) createObject(); } public final native String id() /*-{ return this.change_id }-*/; public final native CommitInfo commit() /*-{ return this.commit }-*/; final native String branch() /*-{ return this.branch }-*/; final native String project() /*-{ return this.project }-*/; final native boolean submittable() /*-{ return this._submittable ? true : false; }-*/; final Change.Status status() { String s = statusRaw(); return s != null ? Change.Status.valueOf(s) : null; } private native String statusRaw() /*-{ return this.status; }-*/; final native void setId(String i) /*-{ if(i)this.change_id=i; }-*/; final native void setCommit(CommitInfo c) /*-{ if(c)this.commit=c; }-*/; final native void setBranch(String b) /*-{ if(b)this.branch=b; }-*/; final native void setProject(String b) /*-{ if(b)this.project=b; }-*/; public final Change.Id legacyId() { return hasChangeNumber() ? new Change.Id(_changeNumber()) : null; } public final PatchSet.Id patchSetId() { return hasChangeNumber() && hasRevisionNumber() ? new PatchSet.Id(legacyId(), _revisionNumber()) : null; } public final native boolean hasChangeNumber() /*-{ return this.hasOwnProperty('_change_number') }-*/ ; final native boolean hasRevisionNumber() /*-{ return this.hasOwnProperty('_revision_number') }-*/ ; final native boolean hasCurrentRevisionNumber() /*-{ return this.hasOwnProperty('_current_revision_number') }-*/ ; final native int _changeNumber() /*-{ return this._change_number }-*/; final native int _revisionNumber() /*-{ return this._revision_number }-*/; final native int _currentRevisionNumber() /*-{ return this._current_revision_number }-*/; final native void setChangeNumber(int n) /*-{ this._change_number=n; }-*/; final native void setRevisionNumber(int n) /*-{ this._revision_number=n; }-*/; final native void setCurrentRevisionNumber(int n) /*-{ this._current_revision_number=n; }-*/; final native void setSubmittable(boolean s) /*-{ this._submittable=s; }-*/; final native void setStatus(String s) /*-{ if(s)this.status=s; }-*/; protected ChangeAndCommit() {} } }