diff options
author | Jukka Jokiniva <jukka.jokiniva@qt.io> | 2018-12-28 10:37:40 +0200 |
---|---|---|
committer | Jukka Jokiniva <jukka.jokiniva@qt.io> | 2019-01-18 08:29:03 +0000 |
commit | 3019b115f8fbee9d82e9e76132ef8b4c35944e14 (patch) | |
tree | af67f058ab99760bd9d7f2b9f5f8160036d4af80 | |
parent | b793cd0db4164d0ea390eae68e1e4af1d42e53c5 (diff) |
Add ReOpen for deferred changes
Adds ReOpen button to UI and corresponding REST API.
Change-Id: I4e79b4d8dc65993bf7491a09ffa85d7448176f27
Reviewed-by: Jukka Jokiniva <jukka.jokiniva@qt.io>
Reviewed-by: Kari Oikarinen <kari.oikarinen@qt.io>
4 files changed, 259 insertions, 0 deletions
diff --git a/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html index f077159..03480b0 100644 --- a/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html +++ b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html @@ -16,6 +16,10 @@ 'gerrit-plugin-qt-workflow~defer' : { header: 'Defer the change?', action_name: 'defer' + }, + 'gerrit-plugin-qt-workflow~reopen' : { + header: 'Reopen the change?', + action_name: 'reopen' } }; 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 4b478b5..46ae162 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java @@ -31,6 +31,7 @@ public class QtModule extends FactoryModule { protected void configure() { post(CHANGE_KIND, "abandon").to(QtAbandon.class); post(CHANGE_KIND, "defer").to(QtDefer.class); + post(CHANGE_KIND, "reopen").to(QtReOpen.class); } } ); diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpen.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpen.java new file mode 100644 index 0000000..778d3dc --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpen.java @@ -0,0 +1,131 @@ +// +// Copyright (C) 2019 The Qt Company +// + + +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.RestoreInput; +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.project.ProjectCache; +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 QtReOpen extends RetryingRestModifyView<ChangeResource, RestoreInput, 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 ProjectCache projectCache; + private final QtChangeUpdateOp.Factory qtUpdateFactory; + + @Inject + QtReOpen(Provider<ReviewDb> dbProvider, + ChangeJson.Factory json, + PatchSetUtil psUtil, + RetryHelper retryHelper, + ProjectCache projectCache, + QtChangeUpdateOp.Factory qtUpdateFactory) { + super(retryHelper); + this.dbProvider = dbProvider; + this.json = json; + this.psUtil = psUtil; + this.projectCache = projectCache; + this.qtUpdateFactory = qtUpdateFactory; + } + + + @Override + protected ChangeInfo applyImpl(BatchUpdate.Factory updateFactory, + ChangeResource rsrc, + RestoreInput input) + throws RestApiException, UpdateException, + OrmException, PermissionBackendException, + IOException { + Change change = rsrc.getChange(); + logger.atInfo().log("qtcodereview: reopen %s", change); + + // Not allowed to restore if the current patch set is locked. + psUtil.checkPatchSetNotLocked(rsrc.getNotes()); + + // Use same permission as Restore. Note that Abandon permission grants the + // Restore if the user also has push permission on the change’s destination ref. + rsrc.permissions().database(dbProvider).check(ChangePermission.RESTORE); + projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite(); + + if (change.getStatus() != Change.Status.DEFERRED) { + logger.atSevere().log("qtcodereview: reopen %s status wrong %s", change, change.getStatus()); + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); + } + + QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.NEW, "Reopened", 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: reopened %s", change); + return json.noOptions().format(change); + } + + @Override + public UiAction.Description getDescription(ChangeResource rsrc) { + UiAction.Description description = new UiAction.Description() + .setLabel("Reopen") + .setTitle("Reopen the change") + .setVisible(false); + + Change change = rsrc.getChange(); + if (change.getStatus() != Change.Status.DEFERRED) { + return description; + } + + try { + if (!projectCache.checkedGet(rsrc.getProject()).statePermitsWrite()) { + return description; + } + } catch (IOException e) { + logger.atSevere().withCause(e).log("Failed to check if project state permits write: %s", rsrc.getProject()); + 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; + } + + boolean visible = rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.RESTORE); + return description.setVisible(visible); + } + +} diff --git a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpenIT.java b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpenIT.java new file mode 100644 index 0000000..6ed9091 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpenIT.java @@ -0,0 +1,123 @@ +// Copyright (C) 2019 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.RestoreInput; +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 QtReOpenIT extends QtCodeReviewIT { + + @Before + public void SetDefaultPermissions() throws Exception { + grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS); + } + + @Test + public void singleChange_New_Defer_ReOpen() throws Exception { + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + + QtDefer(c); + RevCommit updatedHead = qtReOpen(c, initialHead); + } + + @Test + public void singleChange_Defer_ReOpen_With_Input_Messsage() throws Exception { + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + approve(c.getChangeId()); + String changeId = c.getChangeId(); + + QtDefer(c); + + RestoreInput restoreInput = new RestoreInput(); + restoreInput.message = "myrestorenote"; + + String url = "/changes/" + changeId + "/gerrit-plugin-qt-workflow~reopen"; + RestResponse response = adminRestSession.post(url, restoreInput); + response.assertOK(); + Change change = c.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.NEW); + } + + @Test + public void errorReOpen_No_Permission() throws Exception { + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + QtDefer(c); + + deny(project, "refs/heads/master", Permission.ABANDON, REGISTERED_USERS); + RestResponse response = qtReOpenExpectFail(c, HttpStatus.SC_FORBIDDEN); + assertThat(response.getEntityContent()).isEqualTo("restore not permitted"); + grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS); + } + + @Test + public void errorReOpen_Wrong_Status() throws Exception { + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + RestResponse response = qtReOpenExpectFail(c, HttpStatus.SC_CONFLICT); + assertThat(response.getEntityContent()).isEqualTo("change is new"); + } + + private RevCommit qtReOpen(PushOneCommit.Result c, + RevCommit initialHead) + throws Exception { + String masterRef = R_HEADS + "master"; + String stagingRef = R_STAGING + "master"; + + RestResponse response = call_REST_API_ReOpen(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.NEW); + + return masterHead; + } + + private RestResponse qtReOpenExpectFail(PushOneCommit.Result c, + int expectedStatus) + throws Exception { + RestResponse response = call_REST_API_ReOpen(c.getChangeId()); + response.assertStatus(expectedStatus); + return response; + } + + private RestResponse call_REST_API_ReOpen(String changeId) throws Exception { + String url = "/changes/" + changeId + "/gerrit-plugin-qt-workflow~reopen"; + RestResponse response = userRestSession.post(url); + return response; + } + +} |