com.google.gerrit.client.change.ChangeScreen.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.client.change.ChangeScreen.java

Source

// 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.AvatarImage;
import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GerritUiExtensionPoint;
import com.google.gerrit.client.NotFoundScreen;
import com.google.gerrit.client.api.ChangeGlue;
import com.google.gerrit.client.api.ExtensionPanel;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeList;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.changes.RevisionInfoCache;
import com.google.gerrit.client.changes.StarredChanges;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.diff.DiffApi;
import com.google.gerrit.client.info.AccountInfo;
import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
import com.google.gerrit.client.info.ActionInfo;
import com.google.gerrit.client.info.ChangeInfo;
import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
import com.google.gerrit.client.info.ChangeInfo.EditInfo;
import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.FileInfo;
import com.google.gerrit.client.info.GpgKeyInfo;
import com.google.gerrit.client.info.PushCertificateInfo;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
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.Natives;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.client.ui.UserActivityMonitor;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.SelectElement;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.event.dom.client.ChangeEvent;
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.shared.HandlerRegistration;
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.uibinder.client.UiHandler;
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.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtorm.client.KeyUtil;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.codemirror.lib.CodeMirror;

public class ChangeScreen extends Screen {
    private static final Logger logger = Logger.getLogger(ChangeScreen.class.getName());

    interface Binder extends UiBinder<HTMLPanel, ChangeScreen> {
    }

    private static final Binder uiBinder = GWT.create(Binder.class);

    interface Style extends CssResource {
        String avatar();

        String hashtagName();

        String hashtagIcon();

        String highlight();

        String labelName();

        String label_may();

        String label_need();

        String label_ok();

        String label_reject();

        String label_user();

        String pushCertStatus();

        String replyBox();

        String selected();

        String notCurrentPatchSet();
    }

    static ChangeScreen get(NativeEvent in) {
        Element e = in.getEventTarget().cast();
        for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
            EventListener l = DOM.getEventListener(e);
            if (l instanceof ChangeScreen) {
                return (ChangeScreen) l;
            }
        }
        return null;
    }

    private final Change.Id changeId;
    private DiffObject base;
    private String revision;
    private ChangeInfo changeInfo;
    private boolean hasDraftComments;
    private CommentLinkProcessor commentLinkProcessor;
    private EditInfo edit;
    private LocalComments lc;

    private List<HandlerRegistration> handlers = new ArrayList<>(4);
    private UpdateCheckTimer updateCheck;
    private Timestamp lastDisplayedUpdate;
    private UpdateAvailableBar updateAvailable;
    private boolean openReplyBox;
    private boolean loaded;
    private FileTable.Mode fileTableMode;

    @UiField
    HTMLPanel headerLine;
    @UiField
    SimplePanel headerExtension;
    @UiField
    SimplePanel headerExtensionMiddle;
    @UiField
    SimplePanel headerExtensionRight;
    @UiField
    Style style;
    @UiField
    ToggleButton star;
    @UiField
    Anchor permalink;

    @UiField
    Assignee assignee;
    @UiField
    Element assigneeRow;
    @UiField
    Element ccText;
    @UiField
    Reviewers reviewers;
    @UiField
    Hashtags hashtags;
    @UiField
    Element hashtagTableRow;

    @UiField
    FlowPanel ownerPanel;
    @UiField
    InlineHyperlink ownerLink;

    @UiField
    Element uploaderRow;
    @UiField
    FlowPanel uploaderPanel;
    @UiField
    InlineLabel uploaderName;

    @UiField
    Element statusText;
    @UiField
    Element privateText;
    @UiField
    Image projectSettings;
    @UiField
    AnchorElement projectSettingsLink;
    @UiField
    InlineHyperlink projectDashboard;
    @UiField
    InlineHyperlink branchLink;
    @UiField
    Element strategy;
    @UiField
    Element submitActionText;
    @UiField
    Element notMergeable;
    @UiField
    Topic topic;
    @UiField
    Element actionText;
    @UiField
    Element actionDate;
    @UiField
    SimplePanel changeExtension;
    @UiField
    SimplePanel relatedExtension;
    @UiField
    SimplePanel commitExtension;

    @UiField
    Actions actions;
    @UiField
    Labels labels;
    @UiField
    CommitBox commit;
    @UiField
    RelatedChanges related;
    @UiField
    FileTable files;
    @UiField
    ListBox diffBase;
    @UiField
    History history;
    @UiField
    SimplePanel historyExtensionRight;

    @UiField
    Button includedIn;
    @UiField
    Button patchSets;
    @UiField
    Element patchSetsText;
    @UiField
    Button download;
    @UiField
    Button reply;
    @UiField
    Button publishEdit;
    @UiField
    Button rebaseEdit;
    @UiField
    Button deleteEdit;
    @UiField
    Button publish;
    @UiField
    Button deleteRevision;
    @UiField
    Button openAll;
    @UiField
    Button editMode;
    @UiField
    Button reviewMode;
    @UiField
    Button addFile;
    @UiField
    Button deleteFile;
    @UiField
    Button renameFile;
    @UiField
    Button expandAll;
    @UiField
    Button collapseAll;
    @UiField
    Button hideTaggedComments;
    @UiField
    Button showTaggedComments;
    @UiField
    QuickApprove quickApprove;

    private ReplyAction replyAction;
    private IncludedInAction includedInAction;
    private PatchSetsAction patchSetsAction;
    private DownloadAction downloadAction;
    private AddFileAction addFileAction;
    private DeleteFileAction deleteFileAction;
    private RenameFileAction renameFileAction;

    public ChangeScreen(Change.Id changeId, DiffObject base, String revision, boolean openReplyBox,
            FileTable.Mode mode) {
        this.changeId = changeId;
        this.base = base;
        this.revision = normalize(revision);
        this.openReplyBox = openReplyBox;
        this.fileTableMode = mode;
        this.lc = new LocalComments(changeId);
        add(uiBinder.createAndBindUi(this));
    }

    PatchSet.Id getPatchSetId() {
        return new PatchSet.Id(changeInfo.legacyId(), changeInfo.revisions().get(revision)._number());
    }

    @Override
    protected void onLoad() {
        super.onLoad();
        CallbackGroup group = new CallbackGroup();
        if (Gerrit.isSignedIn()) {
            ChangeList.query("change:" + changeId.get() + " has:draft", Collections.<ListChangesOption>emptySet(),
                    group.add(new AsyncCallback<ChangeList>() {
                        @Override
                        public void onSuccess(ChangeList result) {
                            hasDraftComments = result.length() > 0;
                        }

                        @Override
                        public void onFailure(Throwable caught) {
                        }
                    }));
            ChangeApi.editWithFiles(changeId.get(), group.add(new AsyncCallback<EditInfo>() {
                @Override
                public void onSuccess(EditInfo result) {
                    edit = result;
                }

                @Override
                public void onFailure(Throwable caught) {
                }
            }));
        }
        loadChangeInfo(true, group.addFinal(new GerritCallback<ChangeInfo>() {
            @Override
            public void onSuccess(final ChangeInfo info) {
                info.init();
                initCurrentRevision(info);
                final RevisionInfo rev = info.revision(revision);
                CallbackGroup group = new CallbackGroup();
                loadCommit(rev, group);

                group.addListener(new GerritCallback<Void>() {
                    @Override
                    public void onSuccess(Void result) {
                        if (base.isBase() && rev.isMerge()) {
                            base = DiffObject.parse(info.legacyId(),
                                    Gerrit.getUserPreferences().defaultBaseForMerges().getBase());
                        }
                        loadConfigInfo(info, base);
                        JsArray<MessageInfo> mAr = info.messages();
                        for (int i = 0; i < mAr.length(); i++) {
                            if (mAr.get(i).tag() != null) {
                                hideTaggedComments.setVisible(true);
                                break;
                            }
                        }
                    }
                });
                group.done();
            }
        }));
    }

    private RevisionInfo initCurrentRevision(ChangeInfo info) {
        info.revisions().copyKeysIntoChildren("name");
        if (edit != null) {
            edit.setName(edit.commit().commit());
            info.setEdit(edit);
            if (edit.hasFiles()) {
                edit.files().copyKeysIntoChildren("path");
            }
            info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
            JsArray<RevisionInfo> list = info.revisions().values();

            // Edit is converted to a regular revision (with number = 0) and
            // added to the list of revisions. Additionally under certain
            // circumstances change edit is assigned to be the current revision
            // and is selected to be shown on the change screen.
            // We have two different strategies to assign edit to the current ps:
            // 1. revision == null: no revision is selected, so use the edit only
            //    if it is based on the latest patch set
            // 2. edit was selected explicitly from ps drop down:
            //    use the edit regardless of which patch set it is based on
            if (revision == null) {
                RevisionInfo.sortRevisionInfoByNumber(list);
                RevisionInfo rev = list.get(list.length() - 1);
                if (rev.isEdit()) {
                    info.setCurrentRevision(rev.name());
                }
            } else if (revision.equals("edit") || revision.equals("0")) {
                for (int i = 0; i < list.length(); i++) {
                    RevisionInfo r = list.get(i);
                    if (r.isEdit()) {
                        info.setCurrentRevision(r.name());
                        break;
                    }
                }
            }
        }
        return resolveRevisionToDisplay(info);
    }

    private void addExtensionPoints(ChangeInfo change, RevisionInfo rev, Entry result) {
        addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER, headerExtension, change, rev);
        addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS, headerExtensionMiddle,
                change, rev);
        addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS, headerExtensionRight,
                change, rev);
        addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK, changeExtension, change,
                rev, result.getExtensionPanelNames(
                        GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK.toString()));
        addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK, relatedExtension, change,
                rev);
        addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK, commitExtension, change,
                rev);
        addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HISTORY_RIGHT_OF_BUTTONS, historyExtensionRight,
                change, rev);
    }

    private void addExtensionPoint(GerritUiExtensionPoint extensionPoint, Panel p, ChangeInfo change,
            RevisionInfo rev, List<String> panelNames) {
        ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint, panelNames);
        extensionPanel.putObject(GerritUiExtensionPoint.Key.CHANGE_INFO, change);
        extensionPanel.putObject(GerritUiExtensionPoint.Key.REVISION_INFO, rev);
        p.add(extensionPanel);
    }

    private void addExtensionPoint(GerritUiExtensionPoint extensionPoint, Panel p, ChangeInfo change,
            RevisionInfo rev) {
        addExtensionPoint(extensionPoint, p, change, rev, Collections.emptyList());
    }

    private boolean enableSignedPush() {
        return Gerrit.info().receive().enableSignedPush();
    }

    void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
        RestApi call = ChangeApi.detail(changeId.get());
        EnumSet<ListChangesOption> opts = EnumSet.of(ListChangesOption.ALL_REVISIONS,
                ListChangesOption.CHANGE_ACTIONS);
        if (enableSignedPush()) {
            opts.add(ListChangesOption.PUSH_CERTIFICATES);
        }
        ChangeList.addOptions(call, opts);
        if (!fg) {
            call.background();
        }
        call.get(cb);
    }

    void loadRevisionInfo() {
        RestApi call = ChangeApi.actions(changeId.get(), revision);
        call.background();
        call.get(new GerritCallback<NativeMap<ActionInfo>>() {
            @Override
            public void onSuccess(NativeMap<ActionInfo> actionMap) {
                actionMap.copyKeysIntoChildren("id");
                renderRevisionInfo(changeInfo, actionMap);
            }
        });
    }

    @Override
    protected void onUnload() {
        if (replyAction != null) {
            replyAction.hide();
        }
        if (updateCheck != null) {
            updateCheck.cancel();
            updateCheck = null;
        }
        for (HandlerRegistration h : handlers) {
            h.removeHandler();
        }
        handlers.clear();
        super.onUnload();
    }

    @Override
    protected void onInitUI() {
        super.onInitUI();
        setHeaderVisible(false);
        Resources.I.style().ensureInjected();
        star.setVisible(Gerrit.isSignedIn());
        labels.init(style);
        reviewers.init(style, ccText);
        hashtags.init(style);
    }

    private void initReplyButton(ChangeInfo info, String revision) {
        if (!info.revision(revision).isEdit()) {
            reply.setTitle(Gerrit.info().change().replyLabel());
            reply.setHTML(new SafeHtmlBuilder().openDiv().append(Gerrit.info().change().replyLabel()).closeDiv());
            if (hasDraftComments || lc.hasReplyComment()) {
                reply.setStyleName(style.highlight());
            }
            reply.setVisible(true);
        }
    }

    private void gotoSibling(int offset) {
        if (offset > 0 && changeInfo.currentRevision() != null && changeInfo.currentRevision().equals(revision)) {
            return;
        }

        if (offset < 0 && changeInfo.revision(revision)._number() == 1) {
            return;
        }

        JsArray<RevisionInfo> revisions = changeInfo.revisions().values();
        RevisionInfo.sortRevisionInfoByNumber(revisions);
        for (int i = 0; i < revisions.length(); i++) {
            if (revision.equals(revisions.get(i).name())) {
                if (0 <= i + offset && i + offset < revisions.length()) {
                    Gerrit.display(PageLinks
                            .toChange(new PatchSet.Id(changeInfo.legacyId(), revisions.get(i + offset)._number())));
                    return;
                }
                return;
            }
        }
    }

    private void initIncludedInAction(ChangeInfo info) {
        if (info.status() == Status.MERGED) {
            includedInAction = new IncludedInAction(info.legacyId(), style, headerLine, includedIn);
            includedIn.setVisible(true);
        }
    }

    private void updatePatchSetsTextStyle(boolean isPatchSetCurrent) {
        if (isPatchSetCurrent) {
            patchSetsText.removeClassName(style.notCurrentPatchSet());
        } else {
            patchSetsText.addClassName(style.notCurrentPatchSet());
        }
    }

    private void initRevisionsAction(ChangeInfo info, String revision, NativeMap<ActionInfo> actions) {
        int currentPatchSet;
        if (info.currentRevision() != null && info.revisions().containsKey(info.currentRevision())) {
            currentPatchSet = info.revision(info.currentRevision())._number();
        } else {
            JsArray<RevisionInfo> revList = info.revisions().values();
            RevisionInfo.sortRevisionInfoByNumber(revList);
            currentPatchSet = revList.get(revList.length() - 1)._number();
        }

        String currentlyViewedPatchSet;
        boolean isPatchSetCurrent = true;
        String revisionId = info.revision(revision).id();
        if (revisionId.equals("edit")) {
            currentlyViewedPatchSet = Resources.M
                    .editPatchSet(RevisionInfo.findEditParent(info.revisions().values()));
            currentPatchSet = info.revisions().values().length() - 1;
        } else {
            currentlyViewedPatchSet = revisionId;
            if (!currentlyViewedPatchSet.equals(Integer.toString(currentPatchSet))) {
                isPatchSetCurrent = false;
            }
        }
        patchSetsText.setInnerText(Resources.M.patchSets(currentlyViewedPatchSet, currentPatchSet));
        updatePatchSetsTextStyle(isPatchSetCurrent);
        patchSetsAction = new PatchSetsAction(info.legacyId(), revision, edit, style, headerLine, patchSets);

        RevisionInfo revInfo = info.revision(revision);
        if (revInfo.draft()) {
            if (actions.containsKey("publish")) {
                publish.setVisible(true);
                publish.setTitle(actions.get("publish").title());
            }
            if (actions.containsKey("/")) {
                deleteRevision.setVisible(true);
                deleteRevision.setTitle(actions.get("/").title());
            }
        }
    }

    private void initDownloadAction(ChangeInfo info, String revision) {
        downloadAction = new DownloadAction(info, revision, style, headerLine, download);
    }

    private void initProjectLinks(final ChangeInfo info) {
        projectSettingsLink.setHref("#" + PageLinks.toProject(info.projectNameKey()));
        projectSettings.addDomHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                if (Hyperlink.impl.handleAsClick((Event) event.getNativeEvent())) {
                    event.stopPropagation();
                    event.preventDefault();
                    Gerrit.display(PageLinks.toProject(info.projectNameKey()));
                }
            }
        }, ClickEvent.getType());
        projectDashboard.setText(info.project());
        projectDashboard.setTargetHistoryToken(PageLinks.toProjectDefaultDashboard(info.projectNameKey()));
    }

    private void initBranchLink(ChangeInfo info) {
        branchLink.setText(info.branch());
        branchLink.setTargetHistoryToken(PageLinks
                .toChangeQuery(BranchLink.query(info.projectNameKey(), info.status(), info.branch(), null)));
    }

    private void initEditMode(ChangeInfo info, String revision) {
        if (Gerrit.isSignedIn()) {
            RevisionInfo rev = info.revision(revision);
            if (info.status().isOpen()) {
                if (isEditModeEnabled(info, rev)) {
                    editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
                    addFile.setVisible(!editMode.isVisible());
                    deleteFile.setVisible(!editMode.isVisible());
                    renameFile.setVisible(!editMode.isVisible());
                    reviewMode.setVisible(!editMode.isVisible());
                    addFileAction = new AddFileAction(changeId, info.revision(revision), style, addFile, files);
                    deleteFileAction = new DeleteFileAction(changeId, info.revision(revision), style, addFile);
                    renameFileAction = new RenameFileAction(changeId, info.revision(revision), style, addFile);
                } else {
                    editMode.setVisible(false);
                    addFile.setVisible(false);
                    reviewMode.setVisible(false);
                }

                if (rev.isEdit()) {
                    if (info.hasEditBasedOnCurrentPatchSet()) {
                        publishEdit.setVisible(true);
                    } else {
                        rebaseEdit.setVisible(true);
                    }
                    deleteEdit.setVisible(true);
                }
            } else if (rev.isEdit()) {
                deleteEdit.setStyleName(style.highlight());
                deleteEdit.setVisible(true);
            }
        }
    }

    private boolean isEditModeEnabled(ChangeInfo info, RevisionInfo rev) {
        if (rev.isEdit()) {
            return true;
        }
        if (edit == null) {
            return revision.equals(info.currentRevision());
        }
        return rev._number() == RevisionInfo.findEditParent(info.revisions().values());
    }

    @UiHandler("publishEdit")
    void onPublishEdit(@SuppressWarnings("unused") ClickEvent e) {
        EditActions.publishEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
    }

    @UiHandler("rebaseEdit")
    void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) {
        EditActions.rebaseEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
    }

    @UiHandler("deleteEdit")
    void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) {
        if (Window.confirm(Resources.C.deleteChangeEdit())) {
            EditActions.deleteEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
        }
    }

    @UiHandler("publish")
    void onPublish(@SuppressWarnings("unused") ClickEvent e) {
        ChangeActions.publish(changeId, revision, publish, deleteRevision);
    }

    @UiHandler("deleteRevision")
    void onDeleteRevision(@SuppressWarnings("unused") ClickEvent e) {
        if (Window.confirm(Resources.C.deleteDraftRevision())) {
            ChangeActions.delete(changeId, revision, publish, deleteRevision);
        }
    }

    @Override
    public void registerKeys() {
        super.registerKeys();

        KeyCommandSet keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
        keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                Gerrit.displayLastChangeList();
            }
        });
        keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                Gerrit.display(PageLinks.toChange(changeId));
            }
        });
        keysNavigation.add(new KeyCommand(0, 'n', Util.C.keyNextPatchSet()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                gotoSibling(1);
            }
        }, new KeyCommand(0, 'p', Util.C.keyPreviousPatchSet()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                gotoSibling(-1);
            }
        });
        handlers.add(GlobalKey.add(this, keysNavigation));

        KeyCommandSet keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
        keysAction.add(new KeyCommand(0, 'a', Util.C.keyPublishComments()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                if (Gerrit.isSignedIn()) {
                    onReply(null);
                } else {
                    Gerrit.doSignIn(getToken());
                }
            }
        });
        keysAction.add(new KeyCommand(0, 'x', Util.C.keyExpandAllMessages()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                onExpandAll(null);
            }
        });
        keysAction.add(new KeyCommand(0, 'z', Util.C.keyCollapseAllMessages()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                onCollapseAll(null);
            }
        });
        keysAction.add(new KeyCommand(0, 's', Util.C.changeTableStar()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                if (Gerrit.isSignedIn()) {
                    star.setValue(!star.getValue(), true);
                } else {
                    Gerrit.doSignIn(getToken());
                }
            }
        });
        keysAction.add(new KeyCommand(0, 'c', Util.C.keyAddReviewers()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                if (Gerrit.isSignedIn()) {
                    reviewers.onOpenForm();
                } else {
                    Gerrit.doSignIn(getToken());
                }
            }
        });
        keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                if (Gerrit.isSignedIn()) {
                    // In Firefox this event is mistakenly called when F5 is pressed so
                    // differentiate F5 from 't' by checking the charCode(F5=0, t=116).
                    if (event.getNativeEvent().getCharCode() == 0) {
                        Window.Location.reload();
                        return;
                    }
                    if (topic.canEdit()) {
                        topic.onEdit();
                    }
                } else {
                    Gerrit.doSignIn(getToken());
                }
            }
        });
        handlers.add(GlobalKey.add(this, keysAction));
        files.registerKeys();
    }

    @Override
    public void onShowView() {
        super.onShowView();
        commit.onShowView();
        related.setMaxHeight(commit.getElement().getParentElement().getOffsetHeight());

        if (openReplyBox) {
            onReply();
        } else {
            String prior = Gerrit.getPriorView();
            if (prior != null && prior.startsWith("/c/")) {
                scrollToPath(prior.substring(3));
            }
        }

        ChangeGlue.fireShowChange(changeInfo, changeInfo.revision(revision));
        CodeMirror.preload();
        startPoller();
    }

    private void scrollToPath(String token) {
        int s = token.indexOf('/');
        try {
            String c = token.substring(0, s);
            int editIndex = c.indexOf(",edit");
            if (editIndex > 0) {
                c = c.substring(0, editIndex);
            }
            if (s < 0 || !changeId.equals(Change.Id.parse(c))) {
                return; // Unrelated URL, do not scroll.
            }
        } catch (IllegalArgumentException e) {
            return;
        }

        s = token.indexOf('/', s + 1);
        if (s < 0) {
            return; // URL does not name a file.
        }

        int c = token.lastIndexOf(',');
        if (0 <= c) {
            token = token.substring(s + 1, c);
        } else {
            token = token.substring(s + 1);
        }

        if (!token.isEmpty()) {
            files.scrollToPath(KeyUtil.decode(token));
        }
    }

    @UiHandler("star")
    void onToggleStar(ValueChangeEvent<Boolean> e) {
        StarredChanges.toggleStar(changeId, e.getValue());
    }

    @UiHandler("includedIn")
    void onIncludedIn(@SuppressWarnings("unused") ClickEvent e) {
        includedInAction.show();
    }

    @UiHandler("download")
    void onDownload(@SuppressWarnings("unused") ClickEvent e) {
        downloadAction.show();
    }

    @UiHandler("patchSets")
    void onPatchSets(@SuppressWarnings("unused") ClickEvent e) {
        patchSetsAction.show();
    }

    @UiHandler("reply")
    void onReply(@SuppressWarnings("unused") ClickEvent e) {
        onReply();
    }

    @UiHandler("permalink")
    void onReload(ClickEvent e) {
        e.preventDefault();
        Gerrit.display(PageLinks.toChange(changeId));
    }

    private void onReply() {
        if (Gerrit.isSignedIn()) {
            replyAction.onReply(null);
        } else {
            Gerrit.doSignIn(getToken());
        }
    }

    @UiHandler("openAll")
    void onOpenAll(@SuppressWarnings("unused") ClickEvent e) {
        files.openAll();
    }

    @UiHandler("editMode")
    void onEditMode(@SuppressWarnings("unused") ClickEvent e) {
        fileTableMode = FileTable.Mode.EDIT;
        refreshFileTable();
        editMode.setVisible(false);
        addFile.setVisible(true);
        deleteFile.setVisible(true);
        renameFile.setVisible(true);
        reviewMode.setVisible(true);
    }

    @UiHandler("reviewMode")
    void onReviewMode(@SuppressWarnings("unused") ClickEvent e) {
        fileTableMode = FileTable.Mode.REVIEW;
        refreshFileTable();
        editMode.setVisible(true);
        addFile.setVisible(false);
        deleteFile.setVisible(false);
        renameFile.setVisible(false);
        reviewMode.setVisible(false);
    }

    @UiHandler("addFile")
    void onAddFile(@SuppressWarnings("unused") ClickEvent e) {
        addFileAction.onEdit();
    }

    @UiHandler("deleteFile")
    void onDeleteFile(@SuppressWarnings("unused") ClickEvent e) {
        deleteFileAction.onDelete();
    }

    @UiHandler("renameFile")
    void onRenameFile(@SuppressWarnings("unused") ClickEvent e) {
        renameFileAction.onRename();
    }

    private void refreshFileTable() {
        int idx = diffBase.getSelectedIndex();
        if (0 <= idx) {
            String n = diffBase.getValue(idx);
            loadConfigInfo(changeInfo, DiffObject.parse(changeInfo.legacyId(), n));
        }
    }

    @UiHandler("showTaggedComments")
    void onShowTaggedComments(@SuppressWarnings("unused") ClickEvent e) {
        showTaggedComments.setVisible(false);
        hideTaggedComments.setVisible(true);
        int n = history.getWidgetCount();
        for (int i = 0; i < n; i++) {
            Message m = ((Message) history.getWidget(i));
            m.setVisible(true);
        }
    }

    @UiHandler("hideTaggedComments")
    void onHideTaggedComments(@SuppressWarnings("unused") ClickEvent e) {
        hideTaggedComments.setVisible(false);
        showTaggedComments.setVisible(true);
        int n = history.getWidgetCount();
        for (int i = 0; i < n; i++) {
            Message m = ((Message) history.getWidget(i));
            if (m.getMessageInfo().tag() != null) {
                m.setVisible(false);
            }
        }
    }

    @UiHandler("expandAll")
    void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
        int n = history.getWidgetCount();
        for (int i = 0; i < n; i++) {
            ((Message) history.getWidget(i)).setOpen(true);
        }
        expandAll.setVisible(false);
        collapseAll.setVisible(true);
    }

    @UiHandler("collapseAll")
    void onCollapseAll(@SuppressWarnings("unused") ClickEvent e) {
        int n = history.getWidgetCount();
        for (int i = 0; i < n; i++) {
            ((Message) history.getWidget(i)).setOpen(false);
        }
        expandAll.setVisible(true);
        collapseAll.setVisible(false);
    }

    @UiHandler("diffBase")
    void onChangeRevision(@SuppressWarnings("unused") ChangeEvent e) {
        int idx = diffBase.getSelectedIndex();
        if (0 <= idx) {
            String n = diffBase.getValue(idx);
            loadConfigInfo(changeInfo, DiffObject.parse(changeInfo.legacyId(), n));
        }
    }

    private void loadConfigInfo(final ChangeInfo info, DiffObject base) {
        final RevisionInfo rev = info.revision(revision);
        if (base.isAutoMerge() && !initCurrentRevision(info).isMerge()) {
            Gerrit.display(getToken(), new NotFoundScreen());
        }

        updateToken(info, base, rev);

        RevisionInfo baseRev = resolveRevisionOrPatchSetId(info, base.asString(), null);

        CallbackGroup group = new CallbackGroup();
        Timestamp lastReply = myLastReply(info);
        if (rev.isEdit()) {
            // Comments are filtered for the current revision. Use parent
            // patch set for edits, as edits themself can never have comments.
            RevisionInfo p = RevisionInfo.findEditParentRevision(info.revisions().values());
            List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(p, group);
            loadFileList(base, baseRev, rev, lastReply, group, comments, null);
        } else {
            loadDiff(base, baseRev, rev, lastReply, group);
        }
        group.addListener(new AsyncCallback<Void>() {
            @Override
            public void onSuccess(Void result) {
                loadConfigInfo(info, rev);
            }

            @Override
            public void onFailure(Throwable caught) {
                logger.log(Level.SEVERE, "Loading file list and inline comments failed: " + caught.getMessage());
                loadConfigInfo(info, rev);
            }
        });
        group.done();
    }

    private void loadConfigInfo(final ChangeInfo info, RevisionInfo rev) {
        if (loaded) {
            return;
        }

        RevisionInfoCache.add(changeId, rev);
        ConfigInfoCache.add(info);
        ConfigInfoCache.get(info.projectNameKey(), new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
            @Override
            protected void preDisplay(Entry result) {
                loaded = true;
                commentLinkProcessor = result.getCommentLinkProcessor();
                setTheme(result.getTheme());
                renderChangeInfo(info);
                loadRevisionInfo();
            }
        });
        ConfigInfoCache.get(info.projectNameKey(), new GerritCallback<Entry>() {
            @Override
            public void onSuccess(Entry entry) {
                addExtensionPoints(info, rev, entry);
            }
        });
    }

    private void updateToken(ChangeInfo info, DiffObject base, RevisionInfo rev) {
        StringBuilder token = new StringBuilder("/c/").append(info._number()).append("/");
        if (base.asString() != null) {
            token.append(base.asString()).append("..");
        }
        if (base.asString() != null || !rev.name().equals(info.currentRevision())) {
            token.append(rev._number());
        }
        setToken(token.toString());
    }

    static Timestamp myLastReply(ChangeInfo info) {
        if (Gerrit.isSignedIn() && info.messages() != null) {
            int self = Gerrit.getUserAccount()._accountId();
            for (int i = info.messages().length() - 1; i >= 0; i--) {
                MessageInfo m = info.messages().get(i);
                if (m.author() != null && m.author()._accountId() == self) {
                    return m.date();
                }
            }
        }
        return null;
    }

    private void loadDiff(DiffObject base, RevisionInfo baseRev, RevisionInfo rev, Timestamp myLastReply,
            CallbackGroup group) {
        List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
        List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
        loadFileList(base, baseRev, rev, myLastReply, group, comments, drafts);

        if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
            ChangeApi.revision(changeId.get(), rev.name()).view("files").addParameterTrue("reviewed")
                    .get(group.add(new AsyncCallback<JsArrayString>() {
                        @Override
                        public void onSuccess(JsArrayString result) {
                            files.markReviewed(result);
                        }

                        @Override
                        public void onFailure(Throwable caught) {
                        }
                    }));
        }
    }

    private void loadFileList(final DiffObject base, final RevisionInfo baseRev, final RevisionInfo rev,
            final Timestamp myLastReply, CallbackGroup group, final List<NativeMap<JsArray<CommentInfo>>> comments,
            final List<NativeMap<JsArray<CommentInfo>>> drafts) {
        DiffApi.list(changeId.get(), rev.name(), baseRev, group.add(new AsyncCallback<NativeMap<FileInfo>>() {
            @Override
            public void onSuccess(NativeMap<FileInfo> m) {
                files.set(base, new PatchSet.Id(changeId, rev._number()), style, reply, fileTableMode,
                        edit != null);
                files.setValue(m, myLastReply, comments != null ? comments.get(0) : null,
                        drafts != null ? drafts.get(0) : null);
            }

            @Override
            public void onFailure(Throwable caught) {
                files.showError(caught);
            }
        }));
    }

    private List<NativeMap<JsArray<CommentInfo>>> loadComments(final RevisionInfo rev, CallbackGroup group) {
        final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
        // TODO(dborowitz): Could eliminate this call by adding an option to include
        // inline comments in the change detail.
        ChangeApi.comments(changeId.get()).get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
            @Override
            public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
                // Return value is used for populating the file table, so only count
                // comments for the current revision. Still include all comments in
                // the history table.
                r.add(filterForRevision(result, rev._number()));
                history.addComments(result);
            }

            @Override
            public void onFailure(Throwable caught) {
            }
        }));
        return r;
    }

    private static NativeMap<JsArray<CommentInfo>> filterForRevision(NativeMap<JsArray<CommentInfo>> comments,
            int id) {
        NativeMap<JsArray<CommentInfo>> filtered = NativeMap.create();
        for (String k : comments.keySet()) {
            JsArray<CommentInfo> allRevisions = comments.get(k);
            JsArray<CommentInfo> thisRevision = JsArray.createArray().cast();
            for (int i = 0; i < allRevisions.length(); i++) {
                CommentInfo c = allRevisions.get(i);
                if (c.patchSet() == id) {
                    thisRevision.push(c);
                }
            }
            filtered.put(k, thisRevision);
        }
        return filtered;
    }

    private List<NativeMap<JsArray<CommentInfo>>> loadDrafts(RevisionInfo rev, CallbackGroup group) {
        final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
        if (Gerrit.isSignedIn()) {
            ChangeApi.revision(changeId.get(), rev.name()).view("drafts")
                    .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
                        @Override
                        public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
                            r.add(result);
                        }

                        @Override
                        public void onFailure(Throwable caught) {
                        }
                    }));
        } else {
            r.add(NativeMap.<JsArray<CommentInfo>>create());
        }
        return r;
    }

    private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
        if (rev.isEdit() || rev.commit() != null) {
            return;
        }

        ChangeApi.commitWithLinks(changeId.get(), rev.name(), group.add(new AsyncCallback<CommitInfo>() {
            @Override
            public void onSuccess(CommitInfo info) {
                rev.setCommit(info);
            }

            @Override
            public void onFailure(Throwable caught) {
            }
        }));
    }

    private void renderSubmitType(Change.Status status, boolean canSubmit, SubmitType submitType) {
        if (canSubmit && status == Change.Status.NEW) {
            statusText.setInnerText(changeInfo.mergeable() ? Util.C.readyToSubmit() : Util.C.mergeConflict());
        }
        setVisible(notMergeable, !changeInfo.mergeable());
        submitActionText.setInnerText(com.google.gerrit.client.admin.Util.toLongString(submitType));
    }

    private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
        RevisionInfo rev = resolveRevisionOrPatchSetId(info, revision, info.currentRevision());
        if (rev != null) {
            revision = rev.name();
            return rev;
        }

        // the revision is not visible to the calling user (maybe it is a draft?)
        // or the change is corrupt, take the last revision that was returned,
        // if no revision was returned display an error
        JsArray<RevisionInfo> revisions = info.revisions().values();
        if (revisions.length() > 0) {
            RevisionInfo.sortRevisionInfoByNumber(revisions);
            rev = revisions.get(revisions.length() - 1);
            revision = rev.name();
            return rev;
        }
        new ErrorDialog(Resources.M.changeWithNoRevisions(info.legacyId().get())).center();
        throw new IllegalStateException("no revision, cannot proceed");
    }

    /**
     * Resolve a revision or patch set id string to RevisionInfo. When this view is created from the
     * changes table, revision is passed as a real revision. When this view is created from side by
     * side (by closing it with 'u') patch set id is passed.
     *
     * @param info change info
     * @param revOrId revision or patch set id
     * @param defaultValue value returned when revOrId is null
     * @return resolved revision or default value
     */
    private RevisionInfo resolveRevisionOrPatchSetId(ChangeInfo info, String revOrId, String defaultValue) {
        int parentNum;
        if (revOrId == null) {
            revOrId = defaultValue;
        } else if ((parentNum = toParentNum(revOrId)) > 0) {
            CommitInfo commitInfo = info.revision(revision).commit();
            JsArray<CommitInfo> parents = commitInfo.parents();
            if (parents.length() >= parentNum) {
                return RevisionInfo.forParent(-parentNum, parents.get(parentNum - 1));
            }
        } else if (!info.revisions().containsKey(revOrId)) {
            JsArray<RevisionInfo> list = info.revisions().values();
            for (int i = 0; i < list.length(); i++) {
                RevisionInfo r = list.get(i);
                if (revOrId.equals(String.valueOf(r._number()))) {
                    revOrId = r.name();
                    break;
                }
            }
        }
        return revOrId != null ? info.revision(revOrId) : null;
    }

    private boolean isSubmittable(ChangeInfo info) {
        boolean canSubmit = info.status().isOpen() && revision.equals(info.currentRevision())
                && !info.revision(revision).draft();
        if (canSubmit && info.status() == Change.Status.NEW) {
            for (String name : info.labels()) {
                LabelInfo label = info.label(name);
                switch (label.status()) {
                case NEED:
                    statusText.setInnerText(Util.M.needs(name));
                    canSubmit = false;
                    break;
                case REJECT:
                case IMPOSSIBLE:
                    if (label.blocking()) {
                        statusText.setInnerText(Util.M.blockedOn(name));
                        canSubmit = false;
                    }
                    break;
                case MAY:
                case OK:
                default:
                    break;
                }
            }
        }
        return canSubmit;
    }

    private void renderChangeInfo(ChangeInfo info) {
        RevisionInfo revisionInfo = info.revision(revision);
        changeInfo = info;
        lastDisplayedUpdate = info.updated();

        labels.set(info);

        renderOwner(info);
        renderUploader(info, revisionInfo);
        renderActionTextDate(info);
        renderDiffBaseListBox(info);
        initReplyButton(info, revision);
        initIncludedInAction(info);
        initDownloadAction(info, revision);
        initProjectLinks(info);
        initBranchLink(info);
        initEditMode(info, revision);
        actions.display(info, revision);

        star.setValue(info.starred());
        permalink.setHref(ChangeLink.permalink(changeId));
        permalink.setText(String.valueOf(info.legacyId()));
        topic.set(info, revision);
        commit.set(commentLinkProcessor, info, revision);
        related.set(info, revision);
        reviewers.set(info);
        assignee.set(info);
        if (Gerrit.isNoteDbEnabled()) {
            hashtags.set(info, revision);
        } else {
            setVisible(hashtagTableRow, false);
        }

        StringBuilder sb = new StringBuilder();
        sb.append(Util.M.changeScreenTitleId(info.idAbbreviated()));
        if (info.subject() != null) {
            sb.append(": ");
            sb.append(info.subject());
        }
        setWindowTitle(sb.toString());

        // Although this is related to the revision, we can process it early to
        // render it faster.
        if (!info.status().isOpen() || !revision.equals(info.currentRevision()) || revisionInfo.isEdit()) {
            setVisible(strategy, false);
        }

        // Properly render revision actions initially while waiting for
        // the callback to populate them correctly.
        NativeMap<ActionInfo> emptyMap = NativeMap.<ActionInfo>create();
        initRevisionsAction(info, revision, emptyMap);
        quickApprove.setVisible(false);
        actions.reloadRevisionActions(emptyMap);

        boolean current = revision.equals(info.currentRevision()) && !revisionInfo.isEdit();

        if (revisionInfo.isEdit()) {
            statusText.setInnerText(Util.C.changeEdit());
        } else if (!current) {
            statusText.setInnerText(Util.C.notCurrent());
            labels.setVisible(false);
        } else {
            Status s = info.revision(revision).draft() ? Status.DRAFT : info.status();
            statusText.setInnerText(Util.toLongString(s));
        }

        if (info.isPrivate()) {
            privateText.setInnerText(Util.C.isPrivate());
        }

        if (Gerrit.isSignedIn()) {
            replyAction = new ReplyAction(info, revision, hasDraftComments, style, commentLinkProcessor, reply,
                    quickApprove);
        }
        history.set(commentLinkProcessor, replyAction, changeId, info);

        if (current && info.status().isOpen()) {
            quickApprove.set(info, revision, replyAction);
            renderSubmitType(info.status(), isSubmittable(info), info.submitType());
        } else {
            quickApprove.setVisible(false);
        }
    }

    private void renderRevisionInfo(ChangeInfo info, NativeMap<ActionInfo> actionMap) {
        initRevisionsAction(info, revision, actionMap);
        commit.setParentNotCurrent(actionMap.containsKey("rebase") && actionMap.get("rebase").enabled());
        actions.reloadRevisionActions(actionMap);
    }

    private void renderOwner(ChangeInfo info) {
        // TODO info card hover
        String name = name(info.owner());
        if (info.owner().avatar(AvatarInfo.DEFAULT_SIZE) != null) {
            ownerPanel.insert(new AvatarImage(info.owner()), 0);
        }
        ownerLink.setText(name);
        ownerLink.setTitle(email(info.owner(), name));
        ownerLink
                .setTargetHistoryToken(
                        PageLinks
                                .toAccountQuery(
                                        info.owner().name() != null ? info.owner().name()
                                                : info.owner().email() != null ? info.owner().email()
                                                        : String.valueOf(info.owner()._accountId()),
                                        Change.Status.NEW));
    }

    private void renderUploader(ChangeInfo changeInfo, RevisionInfo revInfo) {
        AccountInfo uploader = revInfo.uploader();
        boolean isOwner = uploader == null || uploader._accountId() == changeInfo.owner()._accountId();
        renderPushCertificate(revInfo, isOwner ? ownerPanel : uploaderPanel);
        if (isOwner) {
            uploaderRow.getStyle().setDisplay(Display.NONE);
            return;
        }
        uploaderRow.getStyle().setDisplay(Display.TABLE_ROW);

        if (uploader.avatar(AvatarInfo.DEFAULT_SIZE) != null) {
            uploaderPanel.insert(new AvatarImage(uploader), 0);
        }
        String name = name(uploader);
        uploaderName.setText(name);
        uploaderName.setTitle(email(uploader, name));
    }

    private void renderPushCertificate(RevisionInfo revInfo, FlowPanel panel) {
        if (!enableSignedPush()) {
            return;
        }
        Image status = new Image();
        panel.add(status);
        status.setStyleName(style.pushCertStatus());
        if (!revInfo.hasPushCertificate() || revInfo.pushCertificate().key() == null) {
            status.setResource(Gerrit.RESOURCES.question());
            status.setTitle(Util.C.pushCertMissing());
            return;
        }
        PushCertificateInfo certInfo = revInfo.pushCertificate();
        GpgKeyInfo.Status s = certInfo.key().status();
        switch (s) {
        case BAD:
            status.setResource(Gerrit.RESOURCES.redNot());
            status.setTitle(problems(Util.C.pushCertBad(), certInfo));
            break;
        case OK:
            status.setResource(Gerrit.RESOURCES.warning());
            status.setTitle(problems(Util.C.pushCertOk(), certInfo));
            break;
        case TRUSTED:
            status.setResource(Gerrit.RESOURCES.greenCheck());
            status.setTitle(Util.C.pushCertTrusted());
            break;
        }
    }

    private static String name(AccountInfo info) {
        return info.name() != null ? info.name() : Gerrit.info().user().anonymousCowardName();
    }

    private static String email(AccountInfo info, String name) {
        return info.email() != null ? info.email() : name;
    }

    private static String problems(String msg, PushCertificateInfo info) {
        if (info.key() == null || !info.key().hasProblems() || info.key().problems().length() == 0) {
            return msg;
        }

        StringBuilder sb = new StringBuilder();
        sb.append(msg).append(':');
        for (String problem : Natives.asList(info.key().problems())) {
            sb.append('\n').append(problem);
        }
        return sb.toString();
    }

    private void renderActionTextDate(ChangeInfo info) {
        String action;
        if (info.created().equals(info.updated())) {
            action = Util.C.changeInfoBlockUploaded();
        } else {
            action = Util.C.changeInfoBlockUpdated();
        }
        actionText.setInnerText(action);
        actionDate.setInnerText(FormatUtil.relativeFormat(info.updated()));
    }

    private void renderDiffBaseListBox(ChangeInfo info) {
        JsArray<RevisionInfo> list = info.revisions().values();
        RevisionInfo.sortRevisionInfoByNumber(list);
        int selectedIdx = list.length();
        for (int i = list.length() - 1; i >= 0; i--) {
            RevisionInfo r = list.get(i);
            diffBase.addItem(r.id() + ": " + r.name().substring(0, 6), r.id());
            if (r.name().equals(revision)) {
                SelectElement.as(diffBase.getElement()).getOptions().getItem(diffBase.getItemCount() - 1)
                        .setDisabled(true);
            }
            if (base.isPatchSet() && base.asPatchSetId().get() == r._number()) {
                selectedIdx = diffBase.getItemCount() - 1;
            }
        }

        RevisionInfo rev = info.revisions().get(revision);
        JsArray<CommitInfo> parents = rev.commit().parents();
        if (parents.length() > 1) {
            diffBase.addItem(Util.C.autoMerge(), DiffObject.AUTO_MERGE);
            for (int i = 0; i < parents.length(); i++) {
                int parentNum = i + 1;
                diffBase.addItem(Util.M.diffBaseParent(parentNum), String.valueOf(-parentNum));
            }

            if (base.isParent()) {
                selectedIdx = list.length() + base.getParentNum();
            }
        } else {
            diffBase.addItem(Util.C.baseDiffItem(), "");
        }

        diffBase.setSelectedIndex(selectedIdx);
    }

    void showUpdates(ChangeInfo newInfo) {
        if (!isAttached() || newInfo.updated().equals(lastDisplayedUpdate)) {
            return;
        }

        JsArray<MessageInfo> om = changeInfo.messages();
        JsArray<MessageInfo> nm = newInfo.messages();

        if (om == null) {
            om = JsArray.createArray().cast();
        }
        if (nm == null) {
            nm = JsArray.createArray().cast();
        }

        if (updateAvailable == null) {
            updateAvailable = new UpdateAvailableBar() {
                @Override
                void onShow() {
                    Gerrit.display(PageLinks.toChange(changeId));
                }

                @Override
                void onIgnore(Timestamp newTime) {
                    lastDisplayedUpdate = newTime;
                }
            };
        }
        updateAvailable.set(Natives.asList(nm).subList(om.length(), nm.length()), newInfo.updated());
        if (!updateAvailable.isAttached()) {
            add(updateAvailable);
        }
    }

    private void startPoller() {
        if (Gerrit.isSignedIn() && 0 < Gerrit.info().change().updateDelay()) {
            updateCheck = new UpdateCheckTimer(this);
            updateCheck.schedule();
            handlers.add(UserActivityMonitor.addValueChangeHandler(updateCheck));
        }
    }

    private static String normalize(String r) {
        return r != null && !r.isEmpty() ? r : null;
    }

    /**
     * @param parentToken
     * @return 1-based parentNum if parentToken is a String which can be parsed as a negative integer
     *     i.e. "-1", "-2", etc. If parentToken cannot be parsed as a negative integer, return zero.
     */
    private static int toParentNum(String parentToken) {
        try {
            int n = Integer.parseInt(parentToken);
            if (n < 0) {
                return -n;
            }
            return 0;
        } catch (NumberFormatException e) {
            return 0;
        }
    }
}