// Copyright (C) 2008 The Android Open Source Project // Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). // // 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.GitwebLink; import com.google.gerrit.client.download.DownloadPanel; import com.google.gerrit.client.patches.PatchUtil; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.ui.AccountLinkPanel; import com.google.gerrit.client.ui.CommentedActionDialog; import com.google.gerrit.client.ui.ComplexDisclosurePanel; import com.google.gerrit.client.ui.ListenableAccountDiffPreference; import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.data.ChangeDetail; import com.google.gerrit.common.data.PatchSetDetail; import com.google.gerrit.reviewdb.client.AccountDiffPreference; import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand; import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetInfo; import com.google.gerrit.reviewdb.client.UserIdentity; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.logical.shared.OpenEvent; import com.google.gwt.event.logical.shared.OpenHandler; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.DialogBox; import com.google.gwt.user.client.ui.DisclosurePanel; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.HasAlignment; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwtjsonrpc.common.VoidResult; import java.util.HashSet; import java.util.List; import java.util.Set; class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements OpenHandler { private static final int R_AUTHOR = 0; private static final int R_COMMITTER = 1; private static final int R_PARENTS = 2; private static final int R_DOWNLOAD = 3; private static final int R_CNT = 4; private final ChangeDetailCache detailCache; private final ChangeDetail changeDetail; private final PatchSet patchSet; private final FlowPanel body; private Grid infoTable; private Panel actionsPanel; private PatchTable patchTable; private final Set registeredClickHandler = new HashSet(); private PatchSet.Id diffBaseId; /** * Creates a closed complex disclosure panel for a patch set. * The patch set details are loaded when the complex disclosure panel is opened. */ public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen, boolean hasDraftComments) { super(Util.M.patchSetHeader(ps.getPatchSetId()), isOpen); detailCache = ChangeCache.get(ps.getId().getParentKey()).getChangeDetailCache(); changeDetail = detailCache.get(); patchSet = ps; body = new FlowPanel(); setContent(body); if (hasDraftComments) { final Image draftComments = new Image(Gerrit.RESOURCES.draftComments()); draftComments.setTitle(Util.C.patchSetWithDraftCommentsToolTip()); getHeader().add(draftComments); } final GitwebLink gw = Gerrit.getGitwebLink(); final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " "); revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision()); getHeader().add(revtxt); if (gw != null && gw.canLink(ps)) { final Anchor revlink = new Anchor(gw.getLinkName(), false, gw.toRevision(changeDetail.getChange() .getProject(), ps)); revlink.addStyleName(Gerrit.RESOURCES.css().patchSetLink()); getHeader().add(revlink); } if (ps.isDraft()) { final InlineLabel draftLabel = new InlineLabel(Util.C.draftPatchSetLabel()); draftLabel.addStyleName(Gerrit.RESOURCES.css().patchSetRevision()); getHeader().add(draftLabel); } if (isOpen) { ensureLoaded(changeDetail.getCurrentPatchSetDetail()); } else { addOpenHandler(this); } } public void setDiffBaseId(PatchSet.Id diffBaseId) { this.diffBaseId = diffBaseId; } /** * Display the table showing the Author, Committer and Download links, * followed by the action buttons. */ public void ensureLoaded(final PatchSetDetail detail) { loadInfoTable(detail); loadActionPanel(detail); loadPatchTable(detail); } public void loadInfoTable(final PatchSetDetail detail) { infoTable = new Grid(R_CNT, 2); infoTable.setStyleName(Gerrit.RESOURCES.css().infoBlock()); infoTable.addStyleName(Gerrit.RESOURCES.css().patchSetInfoBlock()); initRow(R_AUTHOR, Util.C.patchSetInfoAuthor()); initRow(R_COMMITTER, Util.C.patchSetInfoCommitter()); initRow(R_PARENTS, Util.C.patchSetInfoParents()); initRow(R_DOWNLOAD, Util.C.patchSetInfoDownload()); final CellFormatter itfmt = infoTable.getCellFormatter(); itfmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost()); itfmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost()); itfmt.addStyleName(R_CNT - 1, 0, Gerrit.RESOURCES.css().bottomheader()); itfmt.addStyleName(R_AUTHOR, 1, Gerrit.RESOURCES.css().useridentity()); itfmt.addStyleName(R_COMMITTER, 1, Gerrit.RESOURCES.css().useridentity()); itfmt.addStyleName(R_DOWNLOAD, 1, Gerrit.RESOURCES.css() .downloadLinkListCell()); final PatchSetInfo info = detail.getInfo(); displayUserIdentity(R_AUTHOR, info.getAuthor()); displayUserIdentity(R_COMMITTER, info.getCommitter()); displayParents(info.getParents()); displayDownload(); body.add(infoTable); } public void loadActionPanel(final PatchSetDetail detail) { if (!patchSet.getId().equals(diffBaseId)) { actionsPanel = new FlowPanel(); actionsPanel.setStyleName(Gerrit.RESOURCES.css().patchSetActions()); actionsPanel.setVisible(true); if (Gerrit.isSignedIn()) { if (changeDetail.canEdit()) { populateReviewAction(); if (changeDetail.isCurrentPatchSet(detail)) { populateActions(detail); } } if (detail.getPatchSet().isDraft()) { if (changeDetail.canPublish()) { populatePublishAction(); } if (changeDetail.canDeleteDraft() && changeDetail.getPatchSets().size() > 1) { populateDeleteDraftPatchSetAction(); } } } body.add(actionsPanel); } } public void loadPatchTable(final PatchSetDetail detail) { if (!patchSet.getId().equals(diffBaseId)) { patchTable = new PatchTable(); patchTable.setSavePointerId("PatchTable " + patchSet.getId()); patchTable.display(diffBaseId, detail); for (ClickHandler clickHandler : registeredClickHandler) { patchTable.addClickHandler(clickHandler); } patchTable.setRegisterKeys(true); setActive(true); body.add(patchTable); } } public class ChangeDownloadPanel extends DownloadPanel { public ChangeDownloadPanel(String project, String ref, boolean allowAnonymous) { super(project, ref, allowAnonymous); } @Override public void populateDownloadCommandLinks() { // This site prefers usage of the 'repo' tool, so suggest // that for easy fetch. // if (allowedSchemes.contains(DownloadScheme.REPO_DOWNLOAD)) { commands.add(cmdLinkfactory.new RepoCommandLink(projectName, changeDetail.getChange().getChangeId() + "/" + patchSet.getPatchSetId())); } if (!urls.isEmpty()) { if (allowedCommands.contains(DownloadCommand.CHECKOUT) || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) { commands.add(cmdLinkfactory.new CheckoutCommandLink()); } if (allowedCommands.contains(DownloadCommand.PULL) || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) { commands.add(cmdLinkfactory.new PullCommandLink()); } if (allowedCommands.contains(DownloadCommand.CHERRY_PICK) || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) { commands.add(cmdLinkfactory.new CherryPickCommandLink()); } if (allowedCommands.contains(DownloadCommand.FORMAT_PATCH) || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) { commands.add(cmdLinkfactory.new FormatPatchCommandLink()); } } } } private void displayDownload() { ChangeDownloadPanel dp = new ChangeDownloadPanel( changeDetail.getChange().getProject().get(), patchSet.getRefName(), changeDetail.isAllowsAnonymous()); infoTable.setWidget(R_DOWNLOAD, 1, dp); } private void displayUserIdentity(final int row, final UserIdentity who) { if (who == null) { infoTable.clearCell(row, 1); return; } final FlowPanel fp = new FlowPanel(); fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity()); if (who.getName() != null) { if (who.getAccount() != null) { fp.add(new AccountLinkPanel(who)); } else { final InlineLabel lbl = new InlineLabel(who.getName()); lbl.setStyleName(Gerrit.RESOURCES.css().accountName()); fp.add(lbl); } } if (who.getEmail() != null) { fp.add(new InlineLabel("<" + who.getEmail() + ">")); } if (who.getDate() != null) { fp.add(new InlineLabel(FormatUtil.mediumFormat(who.getDate()))); } infoTable.setWidget(row, 1, fp); } private void displayParents(final List parents) { if (parents.size() == 0) { infoTable.setWidget(R_PARENTS, 1, new InlineLabel(Util.C.initialCommit())); return; } final Grid parentsTable = new Grid(parents.size(), 2); parentsTable.setStyleName(Gerrit.RESOURCES.css().parentsTable()); parentsTable.addStyleName(Gerrit.RESOURCES.css().noborder()); final CellFormatter ptfmt = parentsTable.getCellFormatter(); int row = 0; for (PatchSetInfo.ParentInfo parent : parents) { parentsTable.setWidget(row, 0, new InlineLabel(parent.id.get())); ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().noborder()); ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().monospace()); parentsTable.setWidget(row, 1, new InlineLabel(Util.cropSubject(parent.shortMessage))); ptfmt.addStyleName(row, 1, Gerrit.RESOURCES.css().noborder()); row++; } infoTable.setWidget(R_PARENTS, 1, parentsTable); } private void populateActions(final PatchSetDetail detail) { final boolean isOpen = changeDetail.getChange().getStatus().isOpen(); final boolean isNew = changeDetail.getChange().getStatus() == Change.Status.NEW; // Staging is allowed only for NEW changes. User is required to have // STAGING approval category. if (isNew && changeDetail.canStage()) { // Create new button for staging and add click handler for it. final Button b = new Button(Util.M.mergeToStagingPatchSet(detail.getPatchSet().getPatchSetId())); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { // Disable the button until this click is handled. b.setEnabled(false); ChangeApi.stage( patchSet.getId().getParentKey().get(), patchSet.getRevision().get(), new GerritCallback() { public void onSuccess(SubmitInfo result) { redisplay(); } public void onFailure(Throwable err) { if (StageFailureDialog.isConflict(err)) { // Stage can fail if someone else has already changed // the status from NEW to something else. Like abandoned, // deferred or staged the change. // In this case error is shown and change reloaded. new StageFailureDialog(err.getMessage()).center(); } else { b.setEnabled(true); super.onFailure(err); } redisplay(); } private void redisplay() { Gerrit.display( PageLinks.toChange(patchSet.getId().getParentKey()), new ChangeScreen(patchSet.getId().getParentKey())); } }); } }); actionsPanel.add(b); } if ((changeDetail.getChange().getStatus() == Change.Status.STAGED || changeDetail.getChange().getStatus() == Change.Status.STAGING) && changeDetail.canUnstage()) { final Button b = new Button(Util.C.buttonUnstagingChange()); b.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { b.setEnabled(false); ChangeApi.unstage( patchSet.getId().getParentKey().get(), patchSet.getRevision().get(), new GerritCallback() { public void onSuccess(SubmitInfo result) { redisplay(); } public void onFailure(Throwable err) { if (UnstageFailureDialog.isConflict(err)) { // Unstage can fail if someone else has already changed // the status from STAGED to something else. Like already // done unstage or integrating has started. // In this case error is shown and change reloaded. new UnstageFailureDialog(err.getMessage()).center(); } else { b.setEnabled(true); super.onFailure(err); } redisplay(); } private void redisplay() { Gerrit.display( PageLinks.toChange(patchSet.getId().getParentKey()), new ChangeScreen(patchSet.getId().getParentKey())); } }); } }); actionsPanel.add(b); } if (isOpen && changeDetail.canSubmit()) { final Button b = new Button(Util.M .submitPatchSet(detail.getPatchSet().getPatchSetId())); if (Gerrit.getConfig().testChangeMerge()) { b.setEnabled(changeDetail.getChange().isMergeable()); } b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); ChangeApi.submit( patchSet.getId().getParentKey().get(), patchSet.getRevision().get(), new GerritCallback() { public void onSuccess(SubmitInfo result) { redisplay(); } public void onFailure(Throwable err) { if (SubmitFailureDialog.isConflict(err)) { new SubmitFailureDialog(err.getMessage()).center(); redisplay(); } else { b.setEnabled(true); super.onFailure(err); } } private void redisplay() { Gerrit.display( PageLinks.toChange(patchSet.getId().getParentKey()), new ChangeScreen(patchSet.getId().getParentKey())); } }); } }); actionsPanel.add(b); } if (changeDetail.canRevert()) { final Button b = new Button(Util.C.buttonRevertChangeBegin()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); new ActionDialog(b, true, Util.C.revertChangeTitle(), Util.C.headingRevertMessage()) { { sendButton.setText(Util.C.buttonRevertChangeSend()); message.setText(Util.M.revertChangeDefaultMessage( detail.getInfo().getSubject(), detail.getPatchSet().getRevision().get()) ); } @Override public void onSend() { ChangeApi.revert(changeDetail.getChange().getChangeId(), getMessageText(), new GerritCallback() { @Override public void onSuccess(ChangeInfo result) { sent = true; Gerrit.display(PageLinks.toChange(new Change.Id(result ._number()))); hide(); } @Override public void onFailure(Throwable caught) { enableButtons(true); super.onFailure(caught); } }); } }.center(); } }); actionsPanel.add(b); } if (changeDetail.canDefer()) { final Button b = new Button(Util.C.buttonDeferChangeBegin()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); new ActionDialog(b, false, Util.C.deferChangeTitle(), Util.C.headingDeferMessage()) { { sendButton.setText(Util.C.buttonDeferChangeSend()); } @Override public void onSend() { // TODO: once the other users of ActionDialog have converted to // REST APIs, we can use createCallback() rather than providing // them directly. ChangeApi.defer(changeDetail.getChange().getChangeId(), getMessageText(), new GerritCallback() { @Override public void onSuccess(ChangeInfo result) { sent = true; Gerrit.display(PageLinks.toChange(new Change.Id(result ._number()))); hide(); } @Override public void onFailure(Throwable caught) { enableButtons(true); super.onFailure(caught); } }); } }.center(); } }); actionsPanel.add(b); } if (changeDetail.canAbandon()) { final Button b = new Button(Util.C.buttonAbandonChangeBegin()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); new ActionDialog(b, false, Util.C.abandonChangeTitle(), Util.C.headingAbandonMessage()) { { sendButton.setText(Util.C.buttonAbandonChangeSend()); } @Override public void onSend() { // TODO: once the other users of ActionDialog have converted to // REST APIs, we can use createCallback() rather than providing // them directly. ChangeApi.abandon(changeDetail.getChange().getChangeId(), getMessageText(), new GerritCallback() { @Override public void onSuccess(ChangeInfo result) { sent = true; Gerrit.display(PageLinks.toChange(new Change.Id(result ._number()))); hide(); } @Override public void onFailure(Throwable caught) { enableButtons(true); super.onFailure(caught); } }); } }.center(); } }); actionsPanel.add(b); } if (changeDetail.getChange().getStatus() == Change.Status.DRAFT && changeDetail.canDeleteDraft()) { final Button b = new Button(Util.C.buttonDeleteDraftChange()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); Util.MANAGE_SVC.deleteDraftChange(patchSet.getId(), new GerritCallback() { public void onSuccess(VoidResult result) { Gerrit.display(PageLinks.MINE); } @Override public void onFailure(Throwable caught) { b.setEnabled(true); super.onFailure(caught); } }); } }); actionsPanel.add(b); } if (changeDetail.canRestore()) { final Button b = new Button(Util.C.buttonRestoreChangeBegin()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); new ActionDialog(b, false, Util.C.restoreChangeTitle(), Util.C.headingRestoreMessage()) { { sendButton.setText(Util.C.buttonRestoreChangeSend()); } @Override public void onSend() { ChangeApi.restore(changeDetail.getChange().getChangeId(), getMessageText(), new GerritCallback() { @Override public void onSuccess(ChangeInfo result) { sent = true; Gerrit.display(PageLinks.toChange(new Change.Id(result ._number()))); hide(); } @Override public void onFailure(Throwable caught) { enableButtons(true); super.onFailure(caught); } }); } }.center(); } }); actionsPanel.add(b); } if (!Gerrit.getConfig().isHideRebase() && changeDetail.canRebase()) { final Button b = new Button(Util.C.buttonRebaseChange()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); Util.MANAGE_SVC.rebaseChange(patchSet.getId(), new ChangeDetailCache.GerritWidgetCallback(b)); } }); actionsPanel.add(b); } } private void populateReviewAction() { final Button b = new Button(Util.C.buttonReview()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { Gerrit.display(Dispatcher.toPublish(patchSet.getId())); } }); actionsPanel.add(b); } private void populatePublishAction() { final Button b = new Button(Util.C.buttonPublishPatchSet()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); Util.MANAGE_SVC.publish(patchSet.getId(), new ChangeDetailCache.GerritWidgetCallback(b)); } }); actionsPanel.add(b); } private void populateDeleteDraftPatchSetAction() { final Button b = new Button(Util.C.buttonDeleteDraftPatchSet()); b.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { b.setEnabled(false); PatchUtil.DETAIL_SVC.deleteDraftPatchSet(patchSet.getId(), new ChangeDetailCache.GerritWidgetCallback(b) { public void onSuccess(final ChangeDetail result) { if (result != null) { detailCache.set(result); } else { Gerrit.display(PageLinks.MINE); } } }); } }); actionsPanel.add(b); } public void refresh() { if (patchSet.getId().equals(diffBaseId)) { if (patchTable != null) { patchTable.setVisible(false); } if (actionsPanel != null) { actionsPanel.setVisible(false); } } else { if (patchTable != null) { if (patchTable.getBase() == null && diffBaseId == null || patchTable.getBase() != null && patchTable.getBase().equals(diffBaseId)) { actionsPanel.setVisible(true); patchTable.setVisible(true); return; } } AccountDiffPreference diffPrefs; if (patchTable == null) { diffPrefs = new ListenableAccountDiffPreference().get(); } else { diffPrefs = patchTable.getPreferences().get(); patchTable.setVisible(false); } Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs, new GerritCallback() { @Override public void onSuccess(PatchSetDetail result) { if (actionsPanel != null) { actionsPanel.setVisible(true); } else { loadActionPanel(result); } loadPatchTable(result); } }); } } @Override public void onOpen(final OpenEvent event) { if (infoTable == null) { AccountDiffPreference diffPrefs; if (diffBaseId == null) { diffPrefs = null; } else { diffPrefs = new ListenableAccountDiffPreference().get(); } Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs, new GerritCallback() { public void onSuccess(final PatchSetDetail result) { loadInfoTable(result); loadActionPanel(result); } }); } } private void initRow(final int row, final String name) { infoTable.setText(row, 0, name); infoTable.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header()); } public PatchSet getPatchSet() { return patchSet; } /** * Adds a click handler to the patch table. * If the patch table is not yet initialized it is guaranteed that the click handler * is added to the patch table after initialization. */ public void addClickHandler(final ClickHandler clickHandler) { registeredClickHandler.add(clickHandler); if (patchTable != null) { patchTable.addClickHandler(clickHandler); } } /** Activates / Deactivates the key navigation and the highlighting of the current row for the patch table */ public void setActive(boolean active) { if (patchTable != null) { patchTable.setActive(active); } } protected static DialogBox alertMessageBox(final String header, final String message) { final DialogBox box = new DialogBox(); final VerticalPanel panel = new VerticalPanel(); panel.add(new Label(message)); final Label empty = new Label(""); empty.setSize("auto", "16px"); panel.add(empty); panel.add(empty); final Button close = new Button(Util.C.buttonClose(), new ClickHandler() { public void onClick(final ClickEvent event) { box.hide(); } }); panel.add(close); panel.setSpacing(16); panel.setCellHorizontalAlignment(close, HasAlignment.ALIGN_RIGHT); box.setText(header); box.add(panel); box.setGlassEnabled(true); // Gray out background return box; } private abstract class ActionDialog extends CommentedActionDialog { public ActionDialog(final FocusWidget enableOnFailure, final boolean redirect, String dialogTitle, String dialogHeading) { super(dialogTitle, dialogHeading, new ChangeDetailCache.IgnoreErrorCallback() { @Override public void onSuccess(ChangeDetail result) { if (redirect) { Gerrit.display(PageLinks.toChange(result.getChange().getId())); } else { super.onSuccess(result); } } @Override public void onFailure(Throwable caught) { enableOnFailure.setEnabled(true); } }); } } }