ChangeScreen2: allow edit commit message

Add new "Edit Message" button in change header and bind it to "e"
shortcut.  Add a new UiAction as REST API endpoint to create a new
revision with updated commit message.  Reload the screen on success.

Change-Id: Ic555f16efba4783eca9684340b27f9d58b1482bb
This commit is contained in:
David Ostrovsky
2013-08-18 16:25:40 -07:00
committed by Shawn Pearce
parent 4139c3a41d
commit 60400e2bc1
14 changed files with 417 additions and 9 deletions

View File

@@ -35,7 +35,7 @@ import java.util.TreeSet;
class Actions extends Composite {
private static final String[] CORE = {
"abandon", "restore", "revert", "topic",
"cherrypick", "submit", "rebase"};
"cherrypick", "submit", "rebase", "message"};
interface Binder extends UiBinder<FlowPanel, Actions> {}
private static Binder uiBinder = GWT.create(Binder.class);

View File

@@ -19,6 +19,7 @@ import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.ActionInfo;
import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
@@ -148,8 +149,10 @@ public class ChangeScreen2 extends Screen {
@UiField Button reply;
@UiField Button expandAll;
@UiField Button collapseAll;
@UiField Button editMessage;
@UiField QuickApprove quickApprove;
private ReplyAction replyAction;
private EditMessageAction editMessageAction;
public ChangeScreen2(Change.Id changeId, String revision, boolean openReplyBox) {
this.changeId = changeId;
@@ -241,6 +244,26 @@ public class ChangeScreen2 extends Screen {
}
}
private void initEditMessageAction() {
NativeMap<ActionInfo> actions = changeInfo.revision(revision).actions();
if (actions != null && actions.containsKey("message")) {
editMessage.setVisible(true);
editMessageAction = new EditMessageAction(
changeInfo.legacy_id(),
revision,
changeInfo.revision(revision).commit().message(),
style,
editMessage,
reply);
keysAction.add(new KeyCommand(0, 'e', Util.C.keyEditMessage()) {
@Override
public void onKeyPress(KeyPressEvent event) {
editMessageAction.onEdit();
}
});
}
}
@Override
public void registerKeys() {
super.registerKeys();
@@ -325,6 +348,11 @@ public class ChangeScreen2 extends Screen {
}
}
@UiHandler("editMessage")
void onEditMessage(ClickEvent e) {
editMessageAction.onEdit();
}
@UiHandler("expandAll")
void onExpandAll(ClickEvent e) {
int n = history.getWidgetCount();
@@ -566,6 +594,7 @@ public class ChangeScreen2 extends Screen {
quickApprove.set(info, revision);
if (Gerrit.isSignedIn()) {
initEditMessageAction();
replyAction = new ReplyAction(info, revision, style, reply);
if (topic.canEdit()) {
keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {

View File

@@ -222,6 +222,13 @@ limitations under the License.
title='Apply score with one click'>
<ui:attribute name='title'/>
</c:QuickApprove>
<g:Button ui:field='editMessage'
styleName=''
visible='false'
title='Edit commit message (Shortcut: e)'>
<ui:attribute name='title'/>
<div><ui:msg>Edit Message</ui:msg></div>
</g:Button>
</div>
<div class='{style.revisionList}' ui:field='revisionParent'>
<ui:msg>Revision <g:ListBox ui:field='revisionList'/></ui:msg>

View File

@@ -0,0 +1,80 @@
// 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.reviewdb.client.Change;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.user.client.PluginSafePopupPanel;
class EditMessageAction {
private final Change.Id changeId;
private final String revision;
private final String originalMessage;
private final ChangeScreen2.Style style;
private final Widget editMessageButton;
private final Widget replyButton;
private EditMessageBox editBox;
private PopupPanel popup;
EditMessageAction(
Change.Id changeId,
String revision,
String originalMessage,
ChangeScreen2.Style style,
Widget editButton,
Widget replyButton) {
this.changeId = changeId;
this.revision = revision;
this.originalMessage = originalMessage;
this.style = style;
this.editMessageButton = editButton;
this.replyButton = replyButton;
}
void onEdit() {
if (popup != null) {
popup.hide();
return;
}
if (editBox == null) {
editBox = new EditMessageBox(
changeId,
revision,
originalMessage);
}
final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
p.setStyleName(style.replyBox());
p.addAutoHidePartner(editMessageButton.getElement());
p.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
if (popup == p) {
popup = null;
}
}
});
p.add(editBox);
p.showRelativeTo(replyButton);
GlobalKey.dialog(p);
popup = p;
}
}

View File

@@ -0,0 +1,102 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.TextBoxChangeListener;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.ClickEvent;
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.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextArea;
class EditMessageBox extends Composite {
interface Binder extends UiBinder<HTMLPanel, EditMessageBox> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private final Change.Id changeId;
private final String revision;
private String originalMessage;
@UiField NpTextArea message;
@UiField Button save;
@UiField Button cancel;
EditMessageBox(
Change.Id changeId,
String revision,
String msg) {
this.changeId = changeId;
this.revision = revision;
this.originalMessage = msg.trim();
initWidget(uiBinder.createAndBindUi(this));
new TextBoxChangeListener(message) {
public void onTextChanged(String newText) {
save.setEnabled(!newText.trim()
.equals(originalMessage));
}
};
}
@Override
protected void onLoad() {
message.setText(originalMessage);
save.setEnabled(false);
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
message.setFocus(true);
}});
}
@UiHandler("save")
void onSave(ClickEvent e) {
ChangeApi.message(changeId.get(), revision, message.getText().trim(),
new GerritCallback<JavaScriptObject>() {
@Override
public void onSuccess(JavaScriptObject msg) {
Gerrit.display(PageLinks.toChange2(changeId));
hide();
};
});
}
@UiHandler("cancel")
void onCancel(ClickEvent e) {
hide();
}
private void hide() {
for (Widget w = getParent(); w != null; w = w.getParent()) {
if (w instanceof PopupPanel) {
((PopupPanel) w).hide();
break;
}
}
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<ui:UiBinder
xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'
xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
<ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
<ui:style>
.commitMessage {
background-color: white;
font-family: monospace;
}
.cancel { float: right; }
</ui:style>
<g:HTMLPanel>
<div class='{res.style.section}'>
<c:NpTextArea
visibleLines='30'
characterWidth='72'
styleName='{style.commitMessage}'
ui:field='message'/>
</div>
<div class='{res.style.section}'>
<g:Button ui:field='save'
title='Create new patch set with updated commit message'
styleName='{res.style.button}'>
<ui:attribute name='title'/>
<div><ui:msg>Save</ui:msg></div>
</g:Button>
<g:Button ui:field='cancel'
styleName='{res.style.button}'
addStyleNames='{style.cancel}'>
<div>Cancel</div>
</g:Button>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@@ -26,11 +26,6 @@ limitations under the License.
max-height: 260px;
}
.section {
padding: 5px 5px;
border-bottom: 1px solid #b8b8b8;
}
.label_name {
font-weight: bold;
text-align: left;
@@ -46,16 +41,16 @@ limitations under the License.
}
</ui:style>
<g:HTMLPanel styleName='{style.replyBox}'>
<div class='{style.section}'>
<div class='{res.style.section}'>
<c:NpTextArea
visibleLines='5'
characterWidth='70'
ui:field='message'/>
</div>
<div class='{style.section}' ui:field='labelsParent'>
<div class='{res.style.section}' ui:field='labelsParent'>
<g:Grid ui:field='labelsTable'/>
</div>
<div class='{style.section}'>
<div class='{res.style.section}'>
<g:Button ui:field='send'
title='Send reply (Shortcut: Ctrl-Enter)'
styleName='{res.style.button}'>

View File

@@ -33,5 +33,6 @@ public interface Resources extends ClientBundle {
String button();
String popup();
String popupContent();
String section();
}
}

View File

@@ -49,3 +49,8 @@
height: 10px;
line-height: 10px;
}
.section {
padding: 5px 5px;
border-bottom: 1px solid #b8b8b8;
}

View File

@@ -102,6 +102,14 @@ public class ChangeApi {
call(id, commit, "cherrypick").post(cherryPickInput, cb);
}
/** Edit commit message for specific revision of a change. */
public static void message(int id, String commit, String message,
AsyncCallback<JavaScriptObject> cb) {
CherryPickInput input = CherryPickInput.create();
input.setMessage(message);
call(id, commit, "message").post(input, cb);
}
/** Submit a specific revision of a change. */
public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
SubmitInput in = SubmitInput.create();

View File

@@ -58,6 +58,7 @@ public interface ChangeConstants extends Constants {
String keyReload();
String keyPublishComments();
String keyEditTopic();
String keyEditMessage();
String patchTableColumnName();
String patchTableColumnComments();

View File

@@ -38,6 +38,8 @@ nextPatchSet = Next patch set
keyReload = Reload change
keyPublishComments = Review and publish comments
keyEditTopic = Edit change topic
keyEditMessage = Edit commit message
patchTableColumnName = File Path
patchTableColumnComments = Comments

View File

@@ -0,0 +1,126 @@
// 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.server.change;
import com.google.common.base.Strings;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
import com.google.gerrit.server.change.EditMessage.Input;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mail.CommitMessageEditedSender;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
class EditMessage implements RestModifyView<RevisionResource, Input>,
UiAction<RevisionResource> {
private final Provider<ReviewDb> dbProvider;
private final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory;
private final GitRepositoryManager gitManager;
private final PersonIdent myIdent;
private final PatchSetInserter.Factory patchSetInserterFactory;
private final ChangeJson json;
static class Input {
@DefaultInput
String message;
}
@Inject
EditMessage(final Provider<ReviewDb> dbProvider,
final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
final GitRepositoryManager gitManager,
final PatchSetInserter.Factory patchSetInserterFactory,
@GerritPersonIdent final PersonIdent myIdent,
ChangeJson json) {
this.dbProvider = dbProvider;
this.commitMessageEditedSenderFactory = commitMessageEditedSenderFactory;
this.gitManager = gitManager;
this.myIdent = myIdent;
this.patchSetInserterFactory = patchSetInserterFactory;
this.json = json;
}
@Override
public ChangeInfo apply(RevisionResource rsrc, Input input)
throws BadRequestException, ResourceConflictException, EmailException,
OrmException, ResourceNotFoundException, IOException {
if (Strings.isNullOrEmpty(input.message)) {
throw new BadRequestException("message must be non-empty");
}
final Repository git;
try {
git = gitManager.openRepository(rsrc.getChange().getProject());
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(e.getMessage());
}
try {
return json.format(ChangeUtil.editCommitMessage(
rsrc.getPatchSet().getId(),
rsrc.getControl().getRefControl(),
(IdentifiedUser) rsrc.getControl().getCurrentUser(),
input.message, dbProvider.get(),
commitMessageEditedSenderFactory, git, myIdent,
patchSetInserterFactory));
} catch (InvalidChangeOperationException e) {
throw new BadRequestException(e.getMessage());
} catch (MissingObjectException e) {
throw new ResourceConflictException(e.getMessage());
} catch (IncorrectObjectTypeException e) {
throw new ResourceConflictException(e.getMessage());
} catch (PatchSetInfoNotAvailableException e) {
throw new ResourceConflictException(e.getMessage());
} catch (NoSuchChangeException e) {
throw new ResourceNotFoundException();
} finally {
git.close();
}
}
@Override
public UiAction.Description getDescription(RevisionResource resource) {
PatchSet.Id current = resource.getChange().currentPatchSetId();
return new UiAction.Description()
.setLabel("Edit commit message")
.setVisible(resource.getChange().getStatus().isOpen()
&& resource.getPatchSet().getId().equals(current)
&& resource.getControl().canAddPatchSet());
}
}

View File

@@ -69,6 +69,7 @@ public class Module extends RestApiModule {
post(REVISION_KIND, "review").to(PostReview.class);
post(REVISION_KIND, "submit").to(Submit.class);
post(REVISION_KIND, "rebase").to(Rebase.class);
post(REVISION_KIND, "message").to(EditMessage.class);
get(REVISION_KIND, "patch").to(GetPatch.class);
get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);