// Copyright (C) 2008 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.Dispatcher; import com.google.gerrit.client.FormatUtil; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.account.AccountInfo; import com.google.gerrit.client.change.RelatedChanges; import com.google.gerrit.client.change.RelatedChanges.ChangeAndCommit; import com.google.gerrit.client.changes.ChangeInfo.CommitInfo; import com.google.gerrit.client.diff.DiffApi; import com.google.gerrit.client.diff.FileInfo; import com.google.gerrit.client.projects.ConfigInfoCache; import com.google.gerrit.client.rpc.CallbackGroup; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.NativeMap; import com.google.gerrit.client.rpc.NativeString; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.rpc.RestApi; import com.google.gerrit.client.ui.CommentLinkProcessor; import com.google.gerrit.client.ui.CommentPanel; import com.google.gerrit.client.ui.ComplexDisclosurePanel; import com.google.gerrit.client.ui.ExpandAllCommand; import com.google.gerrit.client.ui.LinkMenuBar; import com.google.gerrit.client.ui.NeedsSignInKeyCommand; import com.google.gerrit.client.ui.Screen; import com.google.gerrit.common.data.AccountInfoCache; import com.google.gerrit.common.data.ChangeDetail; import com.google.gerrit.common.data.ChangeInfo; import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.extensions.common.ListChangesOption; import com.google.gerrit.extensions.common.SubmitType; import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change.Status; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Patch.ChangeType; import com.google.gerrit.reviewdb.client.Patch.PatchType; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.DisclosurePanel; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.Panel; import com.google.gwtexpui.globalkey.client.GlobalKey; import com.google.gwtexpui.globalkey.client.KeyCommand; import com.google.gwtexpui.globalkey.client.KeyCommandSet; import java.sql.Timestamp; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class ChangeScreen extends Screen implements ValueChangeHandler<ChangeDetail> { private final Change.Id changeId; private final PatchSet.Id openPatchSetId; private ChangeDetailCache detailCache; private com.google.gerrit.client.changes.ChangeInfo changeInfo; private ChangeDescriptionBlock descriptionBlock; private ApprovalTable approvals; private IncludedInTable includedInTable; private DisclosurePanel includedInPanel; private ComplexDisclosurePanel dependenciesPanel; private ChangeTable dependencies; private ChangeTable.Section dependsOn; private ChangeTable.Section neededBy; private PatchSetsBlock patchSetsBlock; private Panel comments; private CommentLinkProcessor commentLinkProcessor; private KeyCommandSet keysNavigation; private KeyCommandSet keysAction; private HandlerRegistration regNavigation; private HandlerRegistration regAction; private HandlerRegistration regDetailCache; private Grid patchesGrid; private ListBox patchesList; /** * The change id for which the old version history is valid. */ private static Change.Id currentChangeId; /** * Which patch set id is the diff base. */ private static PatchSet.Id diffBaseId; public ChangeScreen(final Change.Id toShow) { changeId = toShow; openPatchSetId = null; } public ChangeScreen(final PatchSet.Id toShow) { changeId = toShow.getParentKey(); openPatchSetId = toShow; } public ChangeScreen(final ChangeInfo c) { this(c.getId()); } @Override protected void onLoad() { super.onLoad(); detailCache.refresh(); } @Override protected void onUnload() { if (regNavigation != null) { regNavigation.removeHandler(); regNavigation = null; } if (regAction != null) { regAction.removeHandler(); regAction = null; } if (regDetailCache != null) { regDetailCache.removeHandler(); regDetailCache = null; } super.onUnload(); } @Override public void registerKeys() { super.registerKeys(); regNavigation = GlobalKey.add(this, keysNavigation); regAction = GlobalKey.add(this, keysAction); if (openPatchSetId != null) { patchSetsBlock.activate(openPatchSetId); } } @Override protected void onInitUI() { super.onInitUI(); ChangeCache cache = ChangeCache.get(changeId); detailCache = cache.getChangeDetailCache(); regDetailCache = detailCache.addValueChangeHandler(this); addStyleName(Gerrit.RESOURCES.css().changeScreen()); addStyleName(Gerrit.RESOURCES.css().screenNoHeader()); keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation()); keysAction = new KeyCommandSet(Gerrit.C.sectionActions()); keysNavigation.add(new UpToListKeyCommand(0, 'u', Util.C.upToChangeList())); keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies())); if (Gerrit.isSignedIn()) { keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C .keyPublishComments())); } descriptionBlock = new ChangeDescriptionBlock(keysAction); add(descriptionBlock); 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("auto"); } }; dependsOn = new ChangeTable.Section(Util.C.changeScreenDependsOn()); dependsOn.setChangeRowFormatter(new ChangeTable.ChangeRowFormatter() { @Override public String getRowStyle(ChangeInfo c) { if (! c.isLatest() || Change.Status.ABANDONED.equals(c.getStatus())) { return Gerrit.RESOURCES.css().outdated(); } return null; } @Override public String getDisplayText(final ChangeInfo c, final String displayText) { if (! c.isLatest()) { return displayText + " [OUTDATED]"; } return displayText; } }); neededBy = new ChangeTable.Section(Util.C.changeScreenNeededBy()); dependencies.addSection(dependsOn); dependencies.addSection(neededBy); dependenciesPanel = new ComplexDisclosurePanel( Util.C.changeScreenDependencies(), false); dependenciesPanel.setContent(dependencies); add(dependenciesPanel); patchesList = new ListBox(); patchesList.addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { final int index = patchesList.getSelectedIndex(); final String selectedPatchSet = patchesList.getValue(index); if (index == 0) { diffBaseId = null; } else { diffBaseId = PatchSet.Id.parse(selectedPatchSet); } if (patchSetsBlock != null) { patchSetsBlock.refresh(diffBaseId); } } }); patchesGrid = new Grid(1, 2); patchesGrid.setStyleName(Gerrit.RESOURCES.css().selectPatchSetOldVersion()); patchesGrid.setText(0, 0, Util.C.referenceVersion()); patchesGrid.setWidget(0, 1, patchesList); add(patchesGrid); patchSetsBlock = new PatchSetsBlock(); add(patchSetsBlock); comments = new FlowPanel(); comments.setStyleName(Gerrit.RESOURCES.css().changeComments()); add(comments); } private void displayTitle(final Change.Key changeId, final String subject) { final StringBuilder titleBuf = new StringBuilder(); if (LocaleInfo.getCurrentLocale().isRTL()) { if (subject != null) { titleBuf.append(subject); titleBuf.append(" :"); } titleBuf.append(Util.M.changeScreenTitleId(changeId.abbreviate())); } else { titleBuf.append(Util.M.changeScreenTitleId(changeId.abbreviate())); if (subject != null) { titleBuf.append(": "); titleBuf.append(subject); } } setPageTitle(titleBuf.toString()); setHeaderVisible(false); } @Override public void onValueChange(final ValueChangeEvent<ChangeDetail> event) { if (isAttached() && isLastValueChangeHandler()) { // Until this screen is fully migrated to the new API, these calls must // happen sequentially after the ChangeDetail lookup, because we can't // start an async get at the source of every call that might trigger a // value change. CallbackGroup cbs1 = new CallbackGroup(); final CallbackGroup cbs2 = new CallbackGroup(); final PatchSet.Id psId = event.getValue().getCurrentPatchSet().getId(); final Map<String, Patch> patches = new HashMap<>(); String revId = event.getValue().getCurrentPatchSetDetail().getInfo().getRevId(); if (event.getValue().getChange().getStatus().isOpen()) { ChangeApi.revision(changeId.get(), "current") .view("submit_type") .get(cbs1.add(new GerritCallback<NativeString>() { @Override public void onSuccess(NativeString result) { event.getValue().setSubmitTypeRecord(SubmitTypeRecord.OK( SubmitType.valueOf(result.asString()))); } public void onFailure(Throwable caught) {} })); } if (Gerrit.isSignedIn()) { ChangeApi.revision(changeId.get(), "" + psId.get()) .view("related") .get(cbs1.add(new AsyncCallback<RelatedChanges.RelatedInfo>() { @Override public void onSuccess(RelatedChanges.RelatedInfo info) { if (info.changes() != null) { dependsOn(info); neededBy(info); } } private void dependsOn(RelatedChanges.RelatedInfo info) { ChangeAndCommit self = null; Map<String, ChangeAndCommit> m = new HashMap<>(); for (int i = 0; i < info.changes().length(); i++) { ChangeAndCommit c = info.changes().get(i); if (changeId.equals(c.legacy_id())) { self = c; } if (c.commit() != null && c.commit().commit() != null) { m.put(c.commit().commit(), c); } } if (self != null && self.commit() != null && self.commit().parents() != null) { List<ChangeInfo> d = new ArrayList<>(); for (CommitInfo p : Natives.asList(self.commit().parents())) { ChangeAndCommit pc = m.get(p.commit()); if (pc != null && pc.has_change_number()) { ChangeInfo i = new ChangeInfo(); load(pc, i); d.add(i); } } event.getValue().setDependsOn(d); } } private void neededBy(RelatedChanges.RelatedInfo info) { Set<String> mine = new HashSet<>(); for (PatchSet ps : event.getValue().getPatchSets()) { mine.add(ps.getRevision().get()); } List<ChangeInfo> n = new ArrayList<>(); for (int i = 0; i < info.changes().length(); i++) { ChangeAndCommit c = info.changes().get(i); if (c.has_change_number() && c.commit() != null && c.commit().parents() != null) { for (int j = 0; j < c.commit().parents().length(); j++) { CommitInfo p = c.commit().parents().get(j); if (mine.contains(p.commit())) { ChangeInfo u = new ChangeInfo(); load(c, u); n.add(u); break; } } } } event.getValue().setNeededBy(n); } private void load(final ChangeAndCommit pc, final ChangeInfo i) { RestApi call = ChangeApi.change(pc.legacy_id().get()); ChangeList.addOptions(call, EnumSet.of( ListChangesOption.DETAILED_ACCOUNTS, ListChangesOption.CURRENT_REVISION)); call.get(cbs2.add(new AsyncCallback< com.google.gerrit.client.changes.ChangeInfo>() { public void onFailure(Throwable caught) {} public void onSuccess( com.google.gerrit.client.changes.ChangeInfo result) { i.set(ChangeDetailCache.toChange(result), pc.patch_set_id()); i.setStarred(result.starred()); event.getValue().getAccounts() .merge(ChangeDetailCache.users(result)); }})); } public void onFailure(Throwable caught) {} })); ChangeApi.revision(changeId.get(), revId) .view("files") .addParameterTrue("reviewed") .get(cbs1.add(new AsyncCallback<JsArrayString>() { @Override public void onSuccess(JsArrayString result) { for(int i = 0; i < result.length(); i++) { String path = result.get(i); Patch p = patches.get(path); if (p == null) { p = new Patch(new Patch.Key(psId, path)); patches.put(path, p); } p.setReviewedByCurrentUser(true); } } public void onFailure(Throwable caught) {} })); final Set<PatchSet.Id> withDrafts = new HashSet<>(); event.getValue().setPatchSetsWithDraftComments(withDrafts); for (PatchSet ps : event.getValue().getPatchSets()) { if (!ps.getId().equals(psId)) { final PatchSet.Id id = ps.getId(); ChangeApi.revision(changeId.get(), "" + id.get()) .view("drafts") .get(cbs1.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() { @Override public void onSuccess(NativeMap<JsArray<CommentInfo>> result) { if (!result.isEmpty()) { withDrafts.add(id); } } public void onFailure(Throwable caught) {} })); } } ChangeApi.revision(changeId.get(), "" + psId.get()) .view("drafts") .get(cbs1.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() { @Override public void onSuccess(NativeMap<JsArray<CommentInfo>> result) { for (String path : result.keySet()) { Patch p = patches.get(path); if (p == null) { p = new Patch(new Patch.Key(psId, path)); patches.put(path, p); } p.setDraftCount(result.get(path).length()); } if (!result.isEmpty()) { withDrafts.add(psId); } } public void onFailure(Throwable caught) {} })); } ChangeApi.revision(changeId.get(), revId) .view("comments") .get(cbs1.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() { @Override public void onSuccess(NativeMap<JsArray<CommentInfo>> result) { for (String path : result.keySet()) { Patch p = patches.get(path); if (p == null) { p = new Patch(new Patch.Key(psId, path)); patches.put(path, p); } p.setCommentCount(result.get(path).length()); } } public void onFailure(Throwable caught) {} })); DiffApi.list(changeId.get(), null, revId, new AsyncCallback<NativeMap<FileInfo>>() { @Override public void onSuccess(NativeMap<FileInfo> result) { JsArray<FileInfo> fileInfos = result.values(); FileInfo.sortFileInfoByPath(fileInfos); List<Patch> list = new ArrayList<>(fileInfos.length()); for (FileInfo f : Natives.asList(fileInfos)) { Patch p = patches.get(f.path()); if (p == null) { p = new Patch(new Patch.Key(psId, f.path())); patches.put(f.path(), p); } p.setInsertions(f.lines_inserted()); p.setDeletions(f.lines_deleted()); p.setPatchType(f.binary() ? PatchType.BINARY : PatchType.UNIFIED); if (f.status() == null) { p.setChangeType(ChangeType.MODIFIED); } else { p.setChangeType(ChangeType.forCode(f.status().charAt(0))); } list.add(p); } event.getValue().getCurrentPatchSetDetail().setPatches(list); } public void onFailure(Throwable caught) {} }); ConfigInfoCache.get( event.getValue().getChange().getProject(), cbs1.add(new GerritCallback<ConfigInfoCache.Entry>() { @Override public void onSuccess(ConfigInfoCache.Entry result) { commentLinkProcessor = result.getCommentLinkProcessor(); setTheme(result.getTheme()); } @Override public void onFailure(Throwable caught) { // Handled by last callback's onFailure. } })); ChangeApi.detail(changeId.get(), cbs1.addFinal( new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() { @Override public void onSuccess( com.google.gerrit.client.changes.ChangeInfo result) { changeInfo = result; cbs2.addFinal(new AsyncCallback<Void>() { @Override public void onSuccess(Void result) { display(event.getValue()); } public void onFailure(Throwable caught) {} }).onSuccess(null); } })); } } // Find the last attached screen. // When DialogBox is used (i. e. CommentedActionDialog) then the original // ChangeScreen is still in attached state. // Use here the fact, that the handlers (ChangeScreen) are sorted. private boolean isLastValueChangeHandler() { int count = detailCache.getHandlerCount(); return count > 0 && detailCache.getHandler(count - 1) == this; } private void display(final ChangeDetail detail) { displayTitle(detail.getChange().getKey(), detail.getChange().getSubject()); discardDiffBaseIfNotApplicable(detail.getChange().getId()); if (Status.MERGED == detail.getChange().getStatus()) { includedInPanel.setVisible(true); includedInPanel.addOpenHandler(includedInTable); } else { includedInPanel.setVisible(false); } dependencies.setAccountInfoCache(detail.getAccounts()); descriptionBlock.display(detail, detail.isStarred(), detail.canEditCommitMessage(), detail.getCurrentPatchSetDetail().getInfo(), detail.getAccounts(), detail.getSubmitTypeRecord(), commentLinkProcessor); dependsOn.display(detail.getDependsOn()); neededBy.display(detail.getNeededBy()); approvals.display(changeInfo); patchesList.clear(); if (detail.getCurrentPatchSetDetail().getInfo().getParents().size() > 1) { patchesList.addItem(Util.C.autoMerge()); } else { patchesList.addItem(Util.C.baseDiffItem()); } for (PatchSet pId : detail.getPatchSets()) { patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId .getId().toString()); } if (diffBaseId != null) { patchesList.setSelectedIndex(diffBaseId.get()); } patchSetsBlock.display(detail, diffBaseId); addComments(detail); // If any dependency change is still open, or is outdated, // or the change is needed by a change that is new or submitted, // show our dependency list. // boolean depsOpen = false; int outdated = 0; if (!detail.getChange().getStatus().isClosed()) { final List<ChangeInfo> dependsOn = detail.getDependsOn(); if (dependsOn != null) { for (final ChangeInfo ci : dependsOn) { if (!ci.isLatest()) { depsOpen = true; outdated++; } else if (ci.getStatus() != Change.Status.MERGED) { depsOpen = true; } } } } final List<ChangeInfo> neededBy = detail.getNeededBy(); if (neededBy != null) { for (final ChangeInfo ci : neededBy) { if ((ci.getStatus() == Change.Status.NEW) || (ci.getStatus() == Change.Status.SUBMITTED) || (ci.getStatus() == Change.Status.DRAFT)) { depsOpen = true; } } } dependenciesPanel.setOpen(depsOpen); dependenciesPanel.getHeader().clear(); if (outdated > 0) { dependenciesPanel.getHeader().add(new InlineLabel( Util.M.outdatedHeader(outdated))); } if (!isCurrentView()) { display(); } patchSetsBlock.setRegisterKeys(true); } private static void discardDiffBaseIfNotApplicable(final Change.Id toShow) { if (currentChangeId != null && !currentChangeId.equals(toShow)) { diffBaseId = null; } currentChangeId = toShow; } private void addComments(final ChangeDetail detail) { comments.clear(); final AccountInfoCache accts = detail.getAccounts(); final List<ChangeMessage> msgList = detail.getMessages(); HorizontalPanel title = new HorizontalPanel(); title.setWidth("100%"); title.add(new Label(Util.C.changeScreenComments())); if (msgList.size() > 1) { title.add(messagesMenuBar()); } title.setStyleName(Gerrit.RESOURCES.css().blockHeader()); comments.add(title); final long AGE = 7 * 24 * 60 * 60 * 1000L; final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE); CommentVisibilityStrategy commentVisibilityStrategy = CommentVisibilityStrategy.EXPAND_RECENT; if (Gerrit.isSignedIn()) { commentVisibilityStrategy = Gerrit.getUserAccount() .getGeneralPreferences().getCommentVisibilityStrategy(); } for (int i = 0; i < msgList.size(); i++) { final ChangeMessage msg = msgList.get(i); AccountInfo author; if (msg.getAuthor() != null) { author = FormatUtil.asInfo(accts.get(msg.getAuthor())); } else { author = AccountInfo.create(0, Util.C.messageNoAuthor(), null); } boolean isRecent; if (i == msgList.size() - 1) { isRecent = true; } else { // TODO Instead of opening messages by strict age, do it by "unread"? isRecent = msg.getWrittenOn().after(aged); } final CommentPanel cp = new CommentPanel(author, msg.getWrittenOn(), msg.getMessage(), commentLinkProcessor); cp.setRecent(isRecent); cp.addStyleName(Gerrit.RESOURCES.css().commentPanelBorder()); if (i == msgList.size() - 1) { cp.addStyleName(Gerrit.RESOURCES.css().commentPanelLast()); } boolean isOpen = false; switch (commentVisibilityStrategy) { case COLLAPSE_ALL: break; case EXPAND_ALL: isOpen = true; break; case EXPAND_MOST_RECENT: isOpen = i == msgList.size() - 1; break; case EXPAND_RECENT: default: isOpen = isRecent; break; } cp.setOpen(isOpen); comments.add(cp); } final Button b = new Button(Util.C.changeScreenAddComment()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId(); Gerrit.display(Dispatcher.toPublish(currentPatchSetId)); } }); comments.add(b); comments.setVisible(msgList.size() > 0); } private LinkMenuBar messagesMenuBar() { final Panel c = comments; final LinkMenuBar menuBar = new LinkMenuBar(); menuBar.addItem(Util.C.messageExpandRecent(), new ExpandAllCommand(c, true) { @Override protected void expand(final CommentPanel w) { w.setOpen(w.isRecent()); } }); menuBar.addItem(Util.C.messageExpandAll(), new ExpandAllCommand(c, true)); menuBar.addItem(Util.C.messageCollapseAll(), new ExpandAllCommand(c, false)); menuBar.addStyleName(Gerrit.RESOURCES.css().commentPanelMenuBar()); return menuBar; } public class UpToListKeyCommand extends KeyCommand { public UpToListKeyCommand(int mask, char key, String help) { super(mask, key, help); } @Override public void onKeyPress(final KeyPressEvent event) { Gerrit.displayLastChangeList(); } } public class ExpandCollapseDependencySectionKeyCommand extends KeyCommand { public ExpandCollapseDependencySectionKeyCommand(int mask, char key, String help) { super(mask, key, help); } @Override public void onKeyPress(KeyPressEvent event) { dependenciesPanel.setOpen(!dependenciesPanel.isOpen()); } } public class PublishCommentsKeyCommand extends NeedsSignInKeyCommand { public PublishCommentsKeyCommand(int mask, char key, String help) { super(mask, key, help); } @Override public void onKeyPress(final KeyPressEvent event) { PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId(); Gerrit.display(Dispatcher.toPublish(currentPatchSetId)); } } }