aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJukka Jokiniva <jukka.jokiniva@qt.io>2018-12-27 13:20:24 +0200
committerJukka Jokiniva <jukka.jokiniva@qt.io>2019-01-10 09:52:33 +0000
commitb6c336cc2dcc75553d860a18682d893c0505bfd2 (patch)
tree3ef407c3977b9c609f9fbae9744d42043db91d7a
parent0e85bdd51a072bacd1a5b3a9e4e52b21491dafd6 (diff)
Add Defer functionality
Html plugin adds Defer button and dropdown search to UI. Defer REST API added to java plugin. Change-Id: I49f70c1bdd1cb8609e4f2a206e937eb4a1a66778 Reviewed-by: Paul Wicking <paul.wicking@qt.io> Reviewed-by: Frederik Gladhorn <frederik.gladhorn@qt.io>
-rw-r--r--qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html190
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java92
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java128
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java5
-rw-r--r--src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java52
-rw-r--r--src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtDeferIT.java124
6 files changed, 589 insertions, 2 deletions
diff --git a/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html
new file mode 100644
index 0000000..2d42702
--- /dev/null
+++ b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html
@@ -0,0 +1,190 @@
+//
+// Copyright (C) 2018 The Qt Company
+//
+// This plugin provides UI customization for codereview.qt-project.org
+//
+
+<dom-module id="qt-gerrit-ui-plugin">
+ <script>
+ 'use strict';
+
+ var BUTTONS = {
+ 'gerrit-plugin-qt-workflow~defer' : {
+ header: 'Defer the change?',
+ action_name: 'defer'
+ }
+ };
+
+ Gerrit.install(plugin => {
+
+ plugin.custom_popup = null;
+ plugin.custom_popup_promise = null;
+ plugin.buttons = null;
+
+ function htmlToElement(html) {
+ var template = document.createElement('template');
+ html = html.trim(); // No white space
+ template.innerHTML = html;
+ return template.content.firstChild;
+ }
+
+ // Customize header changes dropdown menu
+ plugin.hook('header-dropdown-Changes').onAttached(element => {
+ // this is ugly, but there is no API for this
+ var ul_elem = element.content.children[1].children[0].children[0].children[0];
+ var li_elem;
+ var link_elem;
+
+ li_elem = htmlToElement(ul_elem.children[1].outerHTML);
+ link_elem = li_elem.children[0].children[1];
+ link_elem.text = 'Deferred';
+ link_elem.href = '/q/status:deferred';
+ ul_elem.insertBefore(li_elem, ul_elem.children[3]);
+ });
+
+ // Customize change view
+ plugin.on('showchange', function(changeInfo, revisionInfo) {
+ plugin.ca = plugin.changeActions();
+
+ // Remove any existing buttons
+ if (plugin.buttons) {
+ for (var key in BUTTONS) {
+ if (BUTTONS.hasOwnProperty(key)) {
+ if(typeof plugin.buttons[key] !== 'undefined' && plugin.buttons[key] !== null) {
+ plugin.ca.removeTapListener(plugin.buttons[key], (param) => {} );
+ plugin.ca.remove(plugin.buttons[key]);
+ plugin.buttons[key] = null;
+ }
+ }
+ }
+ } else plugin.buttons = [];
+
+ // Add buttons based on server response
+ for (var key in BUTTONS) {
+ if (BUTTONS.hasOwnProperty(key)) {
+ var action = plugin.ca.getActionDetails(key);
+ if (action) {
+ // hide dropdown action
+ plugin.ca.setActionHidden(action.__type, action.__key, true);
+
+ // add button
+ plugin.buttons[key] = plugin.ca.add(action.__type, action.label);
+ plugin.ca.setTitle(plugin.buttons[key], action.title);
+ plugin.ca.addTapListener(plugin.buttons[key], buttonEventCallback);
+ }
+ }
+ }
+
+ function buttonEventCallback(event) {
+ var button_key = event.type.substring(0, event.type.indexOf('-tap'));
+ var button_action = null;
+ var button_index;
+ for (var k in plugin.buttons) {
+ if (plugin.buttons[k] === button_key) {
+ button_action = plugin.ca.getActionDetails(k);
+ button_index = k;
+ break;
+ }
+ }
+
+ if (button_action) {
+ plugin.popup('qt-gerrit-ui-confirm-dialog').then( (param) => {
+ plugin.custom_popup_promise = param;
+ plugin.custom_popup.set('header', BUTTONS[button_index].header);
+ plugin.custom_popup.set('subject', changeInfo.subject);
+ plugin.custom_popup.set('action_name', BUTTONS[button_index].action_name);
+ plugin.custom_popup.set('api_url', button_action.__url);
+ }).catch( (param) => { console.log('unexpected error: promise failed');});
+ } else console.log('unexpected error: no action');
+ }
+ });
+ });
+ </script>
+</dom-module>
+
+<dom-module id="qt-gerrit-ui-confirm-dialog">
+ <template>
+ <style include="shared-styles">
+ #dialog {
+ min-width: 40em;
+ }
+ p {
+ margin-bottom: 1em;
+ }
+ @media screen and (max-width: 50em) {
+ #dialog {
+ min-width: inherit;
+ width: 100%;
+ }
+ }
+ </style>
+ <gr-dialog
+ id="dialog"
+ confirm-label="Continue"
+ confirm-on-enter
+ on-cancel="_handleCancelTap"
+ on-confirm="_handleConfirmTap">
+ <div class="header" slot="header">[[header]]</div>
+ <div class="main" slot="main">
+ <p>Ready to [[action_name]] &ldquo;<strong>[[subject]]</strong>&rdquo;?</p>
+ <p class="main" style="color: red;font-weight: bold;">[[errorMessage]]</p>
+ </div>
+ </gr-dialog>
+ </template>
+ <script>
+ 'use strict';
+
+ var qtgerrituiconfirmdialog = Polymer({
+ is: 'qt-gerrit-ui-confirm-dialog',
+
+ properties: {
+ header: {
+ type: String,
+ value: '...wait...'
+ },
+ action_name: {
+ type: String,
+ value: ''
+ },
+ api_url: {
+ type: String,
+ value: ''
+ },
+ subject: {
+ type: String,
+ value: ''
+ },
+ errorMessage: {
+ type: String,
+ value: ''
+ }
+ },
+
+ attached: function() {
+ this.plugin.custom_popup = this;
+ },
+
+ resetFocus(e) {
+ this.$.dialog.resetFocus();
+ },
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ this.plugin.restApi().post(this.get('api_url'), {})
+ .then((ok_resp) => {
+ this.plugin.custom_popup_promise.close();
+ this.plugin.custom_popup_promise = null;
+ window.location.reload(true);
+ }).catch((failed_resp) => {
+ this.set('errorMessage', 'FAILED: ' + failed_resp);
+ });
+ },
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ this.plugin.custom_popup_promise.close();
+ this.plugin.custom_popup_promise = null;
+ },
+ });
+ </script>
+</dom-module>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java
new file mode 100644
index 0000000..491e896
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java
@@ -0,0 +1,92 @@
+//
+// Copyright (C) 2018 The Qt Company
+//
+
+package com.googlesource.gerrit.plugins.qtcodereview;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+
+public class QtChangeUpdateOp implements BatchUpdateOp {
+
+ public interface Factory {
+ QtChangeUpdateOp create(Change.Status newStatus,
+ @Assisted("defaultMessage") String defaultMessage,
+ @Assisted("inputMessage") String inputMessage,
+ @Assisted("tag") String tag);
+ }
+
+ private final Change.Status newStatus;
+ private final String defaultMessage;
+ private final String inputMessage;
+ private final String tag;
+
+ private Change change;
+
+ private final ChangeMessagesUtil cmUtil;
+
+
+ @Inject
+ QtChangeUpdateOp(ChangeMessagesUtil cmUtil,
+ @Nullable @Assisted Change.Status newStatus,
+ @Nullable @Assisted("defaultMessage") String defaultMessage,
+ @Nullable @Assisted("inputMessage") String inputMessage,
+ @Nullable @Assisted("tag") String tag) {
+ this.cmUtil = cmUtil;
+ this.newStatus = newStatus;
+ this.defaultMessage = defaultMessage;
+ this.inputMessage = inputMessage;
+ this.tag = tag;
+ }
+
+ public Change getChange() {
+ return change;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws OrmException, IOException, ResourceConflictException {
+ boolean updated = false;
+ change = ctx.getChange();
+ PatchSet.Id psId = change.currentPatchSetId();
+ ChangeUpdate update = ctx.getUpdate(psId);
+
+ if (newStatus != null) {
+ change.setStatus(newStatus);
+ updated = true;
+ }
+
+ if (updated == true) {
+ change.setLastUpdatedOn(ctx.getWhen());
+ }
+
+ if (defaultMessage != null) {
+ cmUtil.addChangeMessage(ctx.getDb(), update, newMessage(ctx));
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private ChangeMessage newMessage(ChangeContext ctx) {
+ StringBuilder msg = new StringBuilder();
+ msg.append(defaultMessage);
+ if (!Strings.nullToEmpty(inputMessage).trim().isEmpty()) {
+ msg.append("\n\n");
+ msg.append(inputMessage.trim());
+ }
+ return ChangeMessagesUtil.newMessage(ctx, msg.toString(), tag);
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java
new file mode 100644
index 0000000..30b218f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java
@@ -0,0 +1,128 @@
+//
+// Copyright (C) 2019 The Qt Company
+// Modified from https://gerrit.googlesource.com/gerrit/+/refs/heads/stable-2.16/java/com/google/gerrit/server/restapi/change/Abandon.java
+//
+// Copyright (C) 2012 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.googlesource.gerrit.plugins.qtcodereview;
+
+import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.changes.AbandonInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+
+@Singleton
+class QtDefer extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo>
+ implements UiAction<ChangeResource> {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Provider<ReviewDb> dbProvider;
+ private final ChangeJson.Factory json;
+ private final PatchSetUtil psUtil;
+ private final QtChangeUpdateOp.Factory qtUpdateFactory;
+
+ @Inject
+ QtDefer(Provider<ReviewDb> dbProvider,
+ ChangeJson.Factory json,
+ RetryHelper retryHelper,
+ PatchSetUtil psUtil,
+ QtChangeUpdateOp.Factory qtUpdateFactory) {
+ super(retryHelper);
+ this.dbProvider = dbProvider;
+ this.json = json;
+ this.psUtil = psUtil;
+ this.qtUpdateFactory = qtUpdateFactory;
+ }
+
+ @Override
+ protected ChangeInfo applyImpl(BatchUpdate.Factory updateFactory,
+ ChangeResource rsrc,
+ AbandonInput input)
+ throws RestApiException, UpdateException,
+ OrmException, PermissionBackendException,
+ IOException {
+ Change change = rsrc.getChange();
+ logger.atInfo().log("qtcodereview: defer %s", rsrc.getChange().toString());
+
+ // Not allowed to defer if the current patch set is locked.
+ psUtil.checkPatchSetNotLocked(rsrc.getNotes());
+
+ // Defer uses same permission as abandon
+ rsrc.permissions().database(dbProvider).check(ChangePermission.ABANDON);
+
+ if (change.getStatus() != Change.Status.NEW && change.getStatus() != Change.Status.ABANDONED) {
+ logger.atSevere().log("qtcodereview: defer: change %s status wrong %s", change, change.getStatus());
+ throw new ResourceConflictException("change is " + ChangeUtil.status(change));
+ }
+
+ QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.DEFERRED, "Deferred", input.message, null);
+ try (BatchUpdate u = updateFactory.create(dbProvider.get(), change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ u.addOp(rsrc.getId(), op).execute();
+ }
+
+ change = op.getChange();
+ logger.atInfo().log("qtcodereview: deferred %s", change);
+
+ return json.noOptions().format(change);
+ }
+
+ @Override
+ public UiAction.Description getDescription(ChangeResource rsrc) {
+ UiAction.Description description = new UiAction.Description()
+ .setLabel("Defer")
+ .setTitle("Defer the change")
+ .setVisible(false);
+
+ Change change = rsrc.getChange();
+ if (change.getStatus() != Change.Status.NEW && change.getStatus() != Change.Status.ABANDONED) {
+ return description;
+ }
+
+ try {
+ if (psUtil.isPatchSetLocked(rsrc.getNotes())) {
+ return description;
+ }
+ } catch (OrmException | IOException e) {
+ logger.atSevere().withCause(e).log("Failed to check if the current patch set of change %s is locked", change.getId());
+ return description;
+ }
+
+ return description.setVisible(rsrc.permissions().testOrFalse(ChangePermission.ABANDON));
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java
index 5cfba4a..0753356 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java
@@ -4,6 +4,8 @@
package com.googlesource.gerrit.plugins.qtcodereview;
+import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
+
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -18,10 +20,13 @@ public class QtModule extends FactoryModule {
@Override
protected void configure() {
+ factory(QtChangeUpdateOp.Factory.class);
+
install(
new RestApiModule() {
@Override
protected void configure() {
+ post(CHANGE_KIND, "defer").to(QtDefer.class);
}
}
);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java
index 2aa798a..fd0d42a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java
@@ -2,24 +2,72 @@
package com.googlesource.gerrit.plugins.qtcodereview;
+import static com.google.common.truth.Truth.assertThat;
+
import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestPlugin;
-import com.google.gerrit.acceptance.UseSsh;
+
+import com.google.gerrit.reviewdb.client.Change;
+
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
+
@TestPlugin(
name = "gerrit-plugin-qt-workflow",
sysModule = "com.googlesource.gerrit.plugins.qtcodereview.QtModule",
sshModule = "com.googlesource.gerrit.plugins.qtcodereview.QtSshModule"
)
-@UseSsh
public class QtCodeReviewIT extends LightweightPluginDaemonTest {
+ protected static final String R_HEADS = "refs/heads/";
+ protected static final String R_STAGING = "refs/staging/";
+ protected static final String R_PUSH = "refs/for/";
+
@Test
public void dummyTest() {
}
+
+// Helper functions
+
+ protected void QtDefer(PushOneCommit.Result c) throws Exception {
+ RestResponse response = call_REST_API_Defer(c.getChangeId());
+ response.assertOK();
+ }
+
+ protected RestResponse call_REST_API_Defer(String changeId) throws Exception {
+ String url = "/changes/"+changeId+"/gerrit-plugin-qt-workflow~defer";
+ RestResponse response = userRestSession.post(url);
+ return response;
+ }
+
+ protected PushOneCommit.Result pushCommit(String branch,
+ String message,
+ String file,
+ String content)
+ throws Exception {
+ String pushRef = R_PUSH + branch;
+ PushOneCommit.Result c = createUserChange(pushRef, message, file, content);
+ Change change = c.getChange().change();
+ assertThat(change.getStatus()).isEqualTo(Change.Status.NEW);
+ return c;
+ }
+
+ protected PushOneCommit.Result createUserChange(String ref, String message, String file, String content) throws Exception {
+ PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo, message, file, content);
+ PushOneCommit.Result result = push.to(ref);
+ result.assertOkStatus();
+ return result;
+ }
+
+ protected void assertRefUpdatedEvents(String refName, RevCommit ... expected) throws Exception {
+ eventRecorder.assertRefUpdatedEvents(project.get(), refName, expected);
+ }
+
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtDeferIT.java b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtDeferIT.java
new file mode 100644
index 0000000..b2b46b8
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtDeferIT.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2018 The Qt Company
+
+package com.googlesource.gerrit.plugins.qtcodereview;
+
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseSsh;
+
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.AbandonInput;
+import com.google.gerrit.reviewdb.client.Change;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.http.HttpStatus;
+
+@TestPlugin(
+ name = "gerrit-plugin-qt-workflow",
+ sysModule = "com.googlesource.gerrit.plugins.qtcodereview.QtModule",
+ sshModule = "com.googlesource.gerrit.plugins.qtcodereview.QtSshModule"
+)
+
+@UseSsh
+public class QtDeferIT extends QtCodeReviewIT {
+
+ @Before
+ public void SetDefaultPermissions() throws Exception {
+ grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
+ }
+
+ @Test
+ public void singleChange_Defer() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1","file1", "content1");
+
+ RevCommit updatedHead = qtDefer(c, initialHead);
+ }
+
+ @Test
+ public void singleChange_Defer_With_Input_Message() throws Exception {
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1","file1", "content1");
+ approve(c.getChangeId());
+ String changeId = c.getChangeId();
+
+ AbandonInput abandonInput = new AbandonInput();
+ abandonInput.message = "myabandonednote";
+
+ String url = "/changes/"+changeId+"/gerrit-plugin-qt-workflow~defer";
+ RestResponse response = userRestSession.post(url, abandonInput);
+ response.assertOK();
+ Change change = c.getChange().change();
+ assertThat(change.getStatus()).isEqualTo(Change.Status.DEFERRED);
+ }
+
+ @Test
+ public void errorDefer_No_Permission() throws Exception {
+ deny(project, "refs/heads/master", Permission.ABANDON, REGISTERED_USERS);
+
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+ RestResponse response = qtDeferExpectFail(c, HttpStatus.SC_FORBIDDEN);
+ assertThat(response.getEntityContent()).isEqualTo("abandon not permitted");
+
+ grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
+ }
+
+ @Test
+ public void errorDefer_Wrong_Status() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+ merge(c);
+
+ RestResponse response = qtDeferExpectFail(c, HttpStatus.SC_CONFLICT);
+ assertThat(response.getEntityContent()).isEqualTo("change is merged");
+ }
+
+
+ private RevCommit qtDefer(PushOneCommit.Result c,
+ RevCommit initialHead)
+ throws Exception {
+ String masterRef = R_HEADS + "master";
+ String stagingRef = R_STAGING + "master";
+
+ RestResponse response = call_REST_API_Defer(c.getChangeId());
+ response.assertOK();
+
+ RevCommit masterHead = getRemoteHead(project, masterRef);
+ assertThat(masterHead.getId()).isEqualTo(initialHead.getId()); // master is not updated
+
+ RevCommit stagingHead = getRemoteHead(project, stagingRef);
+ if (stagingHead != null) assertThat(stagingHead.getId()).isEqualTo(initialHead.getId()); // staging is not updated
+
+ assertRefUpdatedEvents(masterRef); // no events
+ assertRefUpdatedEvents(stagingRef); // no events
+
+ Change change = c.getChange().change();
+ assertThat(change.getStatus()).isEqualTo(Change.Status.DEFERRED);
+
+ return masterHead;
+ }
+
+ private RestResponse qtDeferExpectFail(PushOneCommit.Result c,
+ int expectedStatus)
+ throws Exception {
+ RestResponse response = call_REST_API_Defer(c.getChangeId());
+ response.assertStatus(expectedStatus);
+
+ Change change = c.getChange().change();
+ assertThat(change.getStatus()).isNotEqualTo(Change.Status.DEFERRED);
+
+ return response;
+ }
+
+
+
+}