blob: ec7bc4a100903e750fd2d41ceb871e7de8f3699f [file] [log] [blame]
// 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.QueryScreen;
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.Nullable;
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.gerrit.reviewdb.client.Project;
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;
@Nullable private Project.NameKey project;
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 Element wipText;
@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 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(
@Nullable Project.NameKey project,
Change.Id changeId,
DiffObject base,
String revision,
boolean openReplyBox,
FileTable.Mode mode) {
this.project = project;
this.changeId = changeId;
this.base = base;
this.revision = normalize(revision);
this.openReplyBox = openReplyBox;
this.fileTableMode = mode;
this.lc = new LocalComments(project, changeId);
add(uiBinder.createAndBindUi(this));
}
public Project.NameKey getProject() {
return project;
}
PatchSet.Id getPatchSetId() {
return new PatchSet.Id(changeInfo.legacyId(), changeInfo.revisions().get(revision)._number());
}
@Override
protected void onLoad() {
super.onLoad();
loadChangeScreen();
}
private void loadChangeScreen() {
if (project == null) {
// Load the project if it is not already present. This is the case when the user used a URL
// that doesn't include the project. Setting it here will rewrite the URL token to include the
// project (visible to the user) and all future API calls made from the change screen will use
// project/+/changeId to identify the change.
String query = "change:" + changeId.get();
ChangeList.query(
query,
Collections.emptySet(),
new AsyncCallback<ChangeList>() {
@Override
public void onSuccess(ChangeList result) {
if (result.length() == 0) {
Gerrit.display(getToken(), new NotFoundScreen());
} else if (result.length() > 1) {
Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
} else {
// Initialize current screen with newly obtained project
project = result.get(0).projectNameKey();
loadChangeScreen();
}
}
@Override
public void onFailure(Throwable caught) {
GerritCallback.showFailure(caught);
}
});
return;
}
CallbackGroup group = new CallbackGroup();
if (Gerrit.isSignedIn()) {
ChangeList.query(
"change:" + changeId.get() + " has:draft",
Collections.emptySet(),
group.add(
new AsyncCallback<ChangeList>() {
@Override
public void onSuccess(ChangeList result) {
hasDraftComments = result.length() > 0;
}
@Override
public void onFailure(Throwable caught) {}
}));
ChangeApi.editWithFiles(
Project.NameKey.asStringOrNull(project),
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(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(Project.NameKey.asStringOrNull(project), 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(getProject().get(), 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(
project,
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.projectNameKey(), 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) {
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.projectNameKey(), info.legacyId(), revision, edit, style, headerLine, patchSets);
}
private void initDownloadAction(ChangeInfo info, String revision) {
downloadAction = new DownloadAction(info, revision, style, headerLine, download);
}
private void initProjectLinks(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(
info.projectNameKey(), changeId, info.revision(revision), style, addFile, files);
deleteFileAction =
new DeleteFileAction(
info.projectNameKey(), changeId, info.revision(revision), style, addFile);
renameFileAction =
new RenameFileAction(
info.projectNameKey(), 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(getProject(), changeId, publishEdit, rebaseEdit, deleteEdit);
}
@UiHandler("rebaseEdit")
void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) {
EditActions.rebaseEdit(getProject(), changeId, publishEdit, rebaseEdit, deleteEdit);
}
@UiHandler("deleteEdit")
void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) {
if (Window.confirm(Resources.C.deleteChangeEdit())) {
EditActions.deleteEdit(getProject(), changeId, publishEdit, rebaseEdit, deleteEdit);
}
}
@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(project, 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) {
ProjectChangeId cId;
try {
cId = ProjectChangeId.create(token);
} catch (IllegalArgumentException e) {
// Scrolling is best-effort.
return;
}
if (!changeId.equals(cId.getChangeId())) {
return; // Unrelated URL, do not scroll.
}
// Extract the start of a file path. The patch set is always contained in the URL and separated
// by from the changeId by a forward slash. Example: /c/project/+/123/1/folder/file.txt
int s = token.indexOf('/', cId.identifierLength() + 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(project, 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(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(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(PageLinks.toChangeId(info.projectNameKey(), info.legacyId()))
.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(getProject().get(), 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(
getProject().get(),
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()),
getProject(),
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(getProject().get(), 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(getProject().get(), 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(RevisionInfo rev, CallbackGroup group) {
if (rev.isEdit() || rev.commit() != null) {
return;
}
ChangeApi.commitWithLinks(
getProject().get(),
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 (status == Change.Status.NEW && !changeInfo.isWorkInProgress()) {
if (canSubmit) {
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());
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);
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 {
statusText.setInnerText(Util.toLongString(info.status()));
}
if (info.isPrivate()) {
privateText.setInnerText(Util.C.isPrivate());
}
if (info.isWorkInProgress()) {
wipText.setInnerText(Util.C.isWorkInProgress());
}
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);
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 (om.length() == nm.length()) {
return;
}
if (updateAvailable == null) {
updateAvailable =
new UpdateAvailableBar() {
@Override
void onShow() {
Gerrit.display(PageLinks.toChange(project, 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;
}
}
}