| // 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; |
| } |
| } |
| } |