From be31ec0d9673eaf040e78b80faeef3c942fde9fa Mon Sep 17 00:00:00 2001 From: Jukka Jokiniva Date: Thu, 3 Jan 2019 10:04:04 +0200 Subject: Add UnStage functionality Add UnStage button to UI and corresponding REST API. Fixes: QTBI-1403 Change-Id: Ibab2f37a16b907a3977edbd07e9e685373e7c9fe Reviewed-by: Frederik Gladhorn --- qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html | 4 + .../gerrit/plugins/qtcodereview/QtModule.java | 1 + .../gerrit/plugins/qtcodereview/QtUnStage.java | 206 ++++++++++++ .../gerrit/plugins/qtcodereview/QtUtil.java | 184 +++++++++++ .../plugins/qtcodereview/QtCodeReviewIT.java | 17 + .../gerrit/plugins/qtcodereview/QtUnStageIT.java | 364 +++++++++++++++++++++ 6 files changed, 776 insertions(+) create mode 100644 src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStage.java create mode 100644 src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStageIT.java diff --git a/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html index 66c6e49..9dc76e8 100644 --- a/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html +++ b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html @@ -24,6 +24,10 @@ 'gerrit-plugin-qt-workflow~stage' : { header: 'Submit to staging?', action_name: 'stage' + }, + 'gerrit-plugin-qt-workflow~unstage' : { + header: 'Unstage the change?', + action_name: 'unstage' } }; 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 c6aa706..a3bf187 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java @@ -34,6 +34,7 @@ public class QtModule extends FactoryModule { post(CHANGE_KIND, "defer").to(QtDefer.class); post(CHANGE_KIND, "reopen").to(QtReOpen.class); post(REVISION_KIND, "stage").to(QtStage.class); + post(REVISION_KIND, "unstage").to(QtUnStage.class); } } ); diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStage.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStage.java new file mode 100644 index 0000000..0b5d167 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStage.java @@ -0,0 +1,206 @@ +// +// 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.SubmitInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.ProjectUtil; +import com.google.gerrit.server.account.AccountResolver; +import com.google.gerrit.server.change.RevisionResource; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.RefPermission; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.update.BatchUpdate; +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 java.io.IOException; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.ObjectId; + + +class QtUnStage implements RestModifyView, UiAction { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static class Output { + transient Change change; + + private Output(Change c) { + change = c; + } + } + + private final Provider dbProvider; + private final GitRepositoryManager repoManager; + private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; + private final AccountResolver accountResolver; + private final ProjectCache projectCache; + + private final QtUtil qtUtil; + private final QtChangeUpdateOp.Factory qtUpdateFactory; + + private Change change; + private Project.NameKey projectKey; + private Branch.NameKey destBranchKey; + private Branch.NameKey stagingBranchKey; + + @Inject + QtUnStage( + Provider dbProvider, + GitRepositoryManager repoManager, + PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, + AccountResolver accountResolver, + ProjectCache projectCache, + QtUtil qtUtil, + QtChangeUpdateOp.Factory qtUpdateFactory) { + this.dbProvider = dbProvider; + this.repoManager = repoManager; + this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; + this.accountResolver = accountResolver; + this.projectCache = projectCache; + this.qtUtil = qtUtil; + this.qtUpdateFactory = qtUpdateFactory; + } + + @Override + public Output apply(RevisionResource rsrc, SubmitInput input) + throws RestApiException, IOException, OrmException, UpdateException, + PermissionBackendException, ConfigInvalidException { + + logger.atInfo().log("qtcodereview: unstage %s", rsrc.getChange().toString()); + + IdentifiedUser submitter = rsrc.getUser().asIdentifiedUser(); + + change = rsrc.getChange(); + projectKey = rsrc.getProject(); + destBranchKey = change.getDest(); + stagingBranchKey = QtUtil.getStagingBranch(destBranchKey); + + rsrc.permissions().check(ChangePermission.QT_STAGE); + + projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite(); + + return new Output(removeChangeFromStaging(rsrc, submitter)); + } + + private Change removeChangeFromStaging(RevisionResource rsrc, IdentifiedUser submitter) + throws IOException, ResourceConflictException, RestApiException, UpdateException { + + Repository git = null; + final Project.NameKey projectKey = rsrc.getProject(); + PatchSet patchSet = rsrc.getPatchSet(); + + logger.atInfo().log("qtcodereview: unstage start for %s", change); + + if (change.getStatus() != Change.Status.STAGED) { + logger.atSevere().log("qtcodereview: unstage: change %s status wrong %s", change, change.getStatus()); + throw new ResourceConflictException("change is " + change.getStatus()); + } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) { + logger.atSevere().log("qtcodereview: unstage: change %s destination branch \"%s\" not found", change, change.getDest().get()); + throw new ResourceConflictException(String.format("destination branch \"%s\" not found.", change.getDest().get())); + } else if (!rsrc.getPatchSet().getId().equals(change.currentPatchSetId())) { + logger.atSevere().log("qtcodereview: unstage: change %s revision %s is not current revision", change, rsrc.getPatchSet().getRevision().get()); + throw new ResourceConflictException(String.format("revision %s is not current revision", rsrc.getPatchSet().getRevision().get())); + } + + final Branch.NameKey destBranchShortKey = QtUtil.getNameKeyShort(projectKey.get(), QtUtil.R_STAGING, stagingBranchKey.get()); + + try { + git = repoManager.openRepository(projectKey); + + ObjectId srcId = git.resolve(patchSet.getRevision().get()); + if (srcId == null) { + logger.atSevere().log("qtcodereview: unstage merge: change %s has invalid revision %s", change, patchSet); + throw new ResourceConflictException("Invalid Revision: " + patchSet); + } + + QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.NEW, "Unstaged", null, null, null); + BatchUpdate u = updateFactory.create(dbProvider.get(), projectKey, submitter, TimeUtil.nowTs()); + u.addOp(rsrc.getChange().getId(), op).execute(); + + qtUtil.rebuildStagingBranch(git, submitter, projectKey, stagingBranchKey, destBranchShortKey); + + change = op.getChange(); + logger.atInfo().log("qtcodereview: unstaged %s from %s", change, stagingBranchKey); + + } catch (ResourceConflictException e) { + logger.atSevere().log("qtcodereview: unstage resource conflict error %s", e); + throw new ResourceConflictException(e.toString()); + } catch (QtUtil.MergeConflictException e) { + logger.atSevere().log("qtcodereview: unstage merge conflict error %s", e); + throw new IOException(e); + } catch (IOException e) { + logger.atSevere().log("qtcodereview: unstage IOException %s", e); + throw new IOException(e); + } finally { + if (git != null) { + git.close(); + } + } + + return change; // this doesn't return data to client, if needed use ChangeJson to convert it + } + + @Override + public UiAction.Description getDescription(RevisionResource rsrc) { + UiAction.Description description = new UiAction.Description() + .setLabel("Unstage") + .setTitle("Unstage the change") + .setVisible(false); + + Change change = rsrc.getChange(); + if (change.getStatus() != Change.Status.STAGED) { + return description; + } + + try { + change = rsrc.getChange(); + projectKey = rsrc.getProject(); + destBranchKey = change.getDest(); + stagingBranchKey = QtUtil.getStagingBranch(destBranchKey); + rsrc.permissions().check(ChangePermission.QT_STAGE); + } catch (AuthException | PermissionBackendException e) { + 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; + } + + return description.setVisible(true); + } + +} diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java index fe35a3c..bf14223 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java +++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java @@ -16,12 +16,27 @@ package com.googlesource.gerrit.plugins.qtcodereview; +import com.google.gerrit.common.FooterConstants; +import com.google.common.collect.Lists; import com.google.common.flogger.FluentLogger; import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.project.NoSuchRefException; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.submit.IntegrationException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; @@ -35,6 +50,8 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** @@ -48,6 +65,19 @@ public class QtUtil { public static final String R_HEADS = "refs/heads/"; public static final String R_STAGING = "refs/staging/"; + private final Provider queryProvider; + private final GitReferenceUpdated referenceUpdated; + private final QtCherryPickPatch qtCherryPickPatch; + + @Inject + QtUtil(Provider queryProvider, + GitReferenceUpdated referenceUpdated, + QtCherryPickPatch qtCherryPickPatch) { + this.queryProvider = queryProvider; + this.referenceUpdated = referenceUpdated; + this.qtCherryPickPatch = qtCherryPickPatch; + } + public static class MergeConflictException extends Exception { private static final long serialVersionUID = 1L; public MergeConflictException(final String message) { @@ -55,6 +85,32 @@ public class QtUtil { } } + public static Project.NameKey getProjectKey(final String project) { + String projectName = project; + if (project.endsWith(Constants.DOT_GIT_EXT)) { + projectName = project.substring(0, project.length() - Constants.DOT_GIT_EXT.length()); + } + return new Project.NameKey(projectName); + } + + /** + * Creates a branch key without any prefix. + * @param project Project for the branch key. + * @param prefix Prefix to remove. + * @param branch Branch name with or without prefix. + * @return Branch name key without prefix. + */ + public static Branch.NameKey getNameKeyShort(final String project, + final String prefix, + final String branch) { + final Project.NameKey projectKey = getProjectKey(project); + if (branch.startsWith(prefix)) { + return new Branch.NameKey(projectKey, branch.substring(prefix.length())); + } else { + return new Branch.NameKey(projectKey, branch); + } + } + /** * Gets a staging branch for a branch. * @param branch Branch under refs/heads. E.g. refs/heads/master. Can be short @@ -121,6 +177,134 @@ public class QtUtil { return result; } + private String getChangeId(RevCommit commit) { + List changeIds = commit.getFooterLines(FooterConstants.CHANGE_ID); + String changeId = null; + if (!changeIds.isEmpty()) changeId = changeIds.get(0); + return changeId; + } + + private ChangeData findChangeFromList(String changeId, List changes) + throws OrmException { + for (ChangeData item : changes) { + if (item.change().getKey().get().equals(changeId)) return item; + } + return null; + } + + private List arrangeOrderLikeInRef(Repository git, + ObjectId refObj, + ObjectId tipObj, + List changeList) + throws MissingObjectException, OrmException, + IOException { + List results = new ArrayList(); + if (refObj.equals(tipObj)) return results; + + RevWalk revWalk = new RevWalk(git); + RevCommit commit = revWalk.parseCommit(refObj); + int count = 0; + do { + count++; + String changeId = getChangeId(commit); + + if (commit.getParentCount() == 0) { + commit = null; // something is going wrong, just exit + } else { + if (changeId == null && commit.getParentCount() > 1) { + changeId = getChangeId(revWalk.parseCommit(commit.getParent(1))); + } + ChangeData change = findChangeFromList(changeId, changeList); + if (change != null) results.add(0, change); + + commit = revWalk.parseCommit(commit.getParent(0)); + } + } while (commit != null && !commit.equals(tipObj) && count < 100); + + if (count == 100) return null; + return results; + } + + private ObjectId pickChangestoStagingRef(Repository git, + final Project.NameKey projectKey, + List changes, + ObjectId tipObj) + throws OrmException, IOException, IntegrationException { + ObjectId newId = tipObj; + for (ChangeData item : changes) { + Change change = item.change(); + logger.atInfo().log("qtcodereview: rebuilding add %s", change); + PatchSet p = item.currentPatchSet(); + ObjectId srcId = git.resolve(p.getRevision().get()); + newId = qtCherryPickPatch.cherryPickPatch(item, + projectKey, + srcId, + newId, + true, // allowFastForward + null, // newStatus + null, // defaultMessage + null, // inputMessage + null // tag + ).toObjectId(); + } + return newId; + } + + public void rebuildStagingBranch(Repository git, + IdentifiedUser user, + final Project.NameKey projectKey, + final Branch.NameKey stagingBranchKey, + final Branch.NameKey destBranchShortKey) + throws MergeConflictException { + try { + ObjectId oldStageRefObjId = git.resolve(stagingBranchKey.get()); + ObjectId branchObjId = git.resolve(destBranchShortKey.get()); + + InternalChangeQuery query = queryProvider.get(); + List changes_integrating = query.byBranchStatus(destBranchShortKey, Change.Status.INTEGRATING); + + query = queryProvider.get(); + List changes_staged = query.byBranchStatus(destBranchShortKey, Change.Status.STAGED); + + List changes_allowed = new ArrayList(); + changes_allowed.addAll(changes_integrating); + changes_allowed.addAll(changes_staged); + List changes = arrangeOrderLikeInRef(git, oldStageRefObjId, branchObjId, changes_allowed); + + logger.atInfo().log("qtcodereview: rebuild reset %s back to %s", stagingBranchKey, destBranchShortKey); + Result result = QtUtil.createStagingBranch(git, destBranchShortKey); + if (result == null) throw new NoSuchRefException("Cannot create staging ref: " + stagingBranchKey.get()); + logger.atInfo().log("qtcodereview: rebuild reset result %s",result); + + ObjectId newId = pickChangestoStagingRef(git, + projectKey, + changes, + git.resolve(stagingBranchKey.get())); + + RefUpdate refUpdate = git.updateRef(stagingBranchKey.get()); + refUpdate.setNewObjectId(newId); + refUpdate.update(); + + // send ref updated event only if it changed + if (!newId.equals(oldStageRefObjId)) { + referenceUpdated.fire(projectKey, stagingBranchKey.get(), oldStageRefObjId, newId, user.state()); + } + + } catch (OrmException e) { + logger.atSevere().log("qtcodereview: rebuild %s failed. Failed to access database %s", stagingBranchKey, e); + throw new MergeConflictException("fatal: Failed to access database"); + } catch (IOException e) { + logger.atSevere().log("qtcodereview: rebuild %s failed. IOException %s", stagingBranchKey, e); + throw new MergeConflictException("fatal: IOException"); + } catch (NoSuchRefException e) { + logger.atSevere().log("qtcodereview: rebuild %s failed. No such ref %s", stagingBranchKey, e); + throw new MergeConflictException("fatal: NoSuchRefException"); + } catch(IntegrationException e) { + logger.atSevere().log("qtcodereview: rebuild %s failed. IntegrationException %s", stagingBranchKey, e); + throw new MergeConflictException("fatal: IntegrationException"); + } + } + public static RevCommit merge(PersonIdent committerIdent, Repository git, ObjectInserter objInserter, 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 168f2e1..28a9d73 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java @@ -103,6 +103,19 @@ public class QtCodeReviewIT extends LightweightPluginDaemonTest { return response; } + protected void QtUnStage(PushOneCommit.Result c) throws Exception { + RestResponse response = call_REST_API_UnStage(c.getChangeId(), getCurrentPatchId(c)); + response.assertOK(); + Change change = c.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.NEW); + } + + protected RestResponse call_REST_API_UnStage(String changeId, String revisionId) throws Exception { + String url = "/changes/" + changeId + "/revisions/" + revisionId + "/gerrit-plugin-qt-workflow~unstage"; + RestResponse response = userRestSession.post(url); + return response; + } + protected PushOneCommit.Result pushCommit(String branch, String message, String file, @@ -192,4 +205,8 @@ public class QtCodeReviewIT extends LightweightPluginDaemonTest { } } + protected String getCurrentPatchId(PushOneCommit.Result c) throws Exception { + return String.valueOf(c.getChange().currentPatchSet().getPatchSetId()); + } + } diff --git a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStageIT.java b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStageIT.java new file mode 100644 index 0000000..633515b --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStageIT.java @@ -0,0 +1,364 @@ +// 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.common.collect.ImmutableList; +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.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; + +import org.apache.http.HttpStatus; + +import org.eclipse.jgit.revwalk.RevCommit; + +import java.util.ArrayList; + +import org.junit.Before; +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 QtUnStageIT extends QtCodeReviewIT { + + private final String UNSTAGED_MSG = "Unstaged"; + + @Before + public void SetDefaultPermissions() throws Exception { + createBranch(new Branch.NameKey(project, "feature")); + grant(project, "refs/heads/master", Permission.QT_STAGE, false, REGISTERED_USERS); + } + + @Test + public void singleChange_UnStage() throws Exception { + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + approve(c.getChangeId()); + QtStage(c); + + RevCommit stagingHead = qtUnStageExpectCommit(c, initialHead); + assertApproval(c.getChangeId(), admin); + } + + @Test + public void multiChange_UnStage_First() throws Exception { + // Push 3 independent commits + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result c1 = pushCommit("master", "commitmsg1", "file1", "content1"); + testRepo.reset(initialHead); + PushOneCommit.Result c2 = pushCommit("master", "commitmsg2", "file2", "content2"); + testRepo.reset(initialHead); + PushOneCommit.Result c3 = pushCommit("master", "commitmsg3", "file3", "content3"); + + approve(c1.getChangeId()); + QtStage(c1); + approve(c2.getChangeId()); + QtStage(c2); + approve(c3.getChangeId()); + QtStage(c3); + + RevCommit stagingHead = qtUnStageExpectCherryPick(c1, c3); + Change change = c2.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED); + change = c3.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED); + } + + @Test + public void multiChange_UnStage_Middle() throws Exception { + // Push 3 independent commits + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result c1 = pushCommit("master", "commitmsg1", "file1", "content1"); + testRepo.reset(initialHead); + PushOneCommit.Result c2 = pushCommit("master", "commitmsg2", "file2", "content2"); + testRepo.reset(initialHead); + PushOneCommit.Result c3 = pushCommit("master", "commitmsg3", "file3", "content3"); + + approve(c1.getChangeId()); + QtStage(c1); + approve(c2.getChangeId()); + QtStage(c2); + approve(c3.getChangeId()); + QtStage(c3); + + RevCommit stagingHead = qtUnStageExpectCherryPick(c2, c3); + Change change = c1.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED); + change = c3.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED); + } + + @Test + public void multiChange_UnStage_Last() throws Exception { + // Push 3 independent commits + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result c1 = pushCommit("master", "commitmsg1", "file1", "content1"); + testRepo.reset(initialHead); + PushOneCommit.Result c2 = pushCommit("master", "commitmsg2", "file2", "content2"); + testRepo.reset(initialHead); + PushOneCommit.Result c3 = pushCommit("master", "commitmsg3", "file3", "content3"); + + approve(c1.getChangeId()); + QtStage(c1); + approve(c2.getChangeId()); + QtStage(c2); + RevCommit expectedFastForward = getRemoteHead(project, R_STAGING + "master"); + approve(c3.getChangeId()); + QtStage(c3); + + RevCommit stagingHead = qtUnStageExpectCommit(c3, expectedFastForward); + Change change = c1.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED); + change = c2.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED); + } + + @Test + public void multiChange_UnStage_Merge_When_First() throws Exception { + RevCommit initialHead = getRemoteHead(); + + // make changes on feature branch + PushOneCommit.Result f1 = pushCommit("feature", "commitmsg1", "file1", "content1"); + PushOneCommit.Result f2 = pushCommit("feature", "commitmsg2", "file2", "content2"); + approve(f1.getChangeId()); + gApi.changes().id(f1.getChangeId()).current().submit(); + approve(f2.getChangeId()); + gApi.changes().id(f2.getChangeId()).current().submit(); + + // make a change on master branch + testRepo.reset(initialHead); + PushOneCommit.Result c1 = pushCommit("master", "commitmsg3", "file3", "content3"); + approve(c1.getChangeId()); + gApi.changes().id(c1.getChangeId()).current().submit(); + + // merge feature branch into master + PushOneCommit mm = pushFactory.create(db, admin.getIdent(), testRepo); + mm.setParents(ImmutableList.of(c1.getCommit(), f2.getCommit())); + PushOneCommit.Result m = mm.to("refs/for/master"); + m.assertOkStatus(); + approve(m.getChangeId()); + QtStage(m); + + // Stage another change + testRepo.reset(initialHead); + PushOneCommit.Result c2 = pushCommit("master", "commitmsg3", "file3", "content3"); + approve(c2.getChangeId()); + QtStage(c2); + + RevCommit stagingHead = qtUnStageExpectCherryPick(m, c2); + String gitLog = getRemoteLog("refs/staging/master").toString(); + assertThat(gitLog).contains(initialHead.getId().name()); + assertThat(gitLog).contains(c1.getCommit().getId().name()); + assertThat(gitLog).contains(stagingHead.getId().name()); + assertThat(gitLog).doesNotContain(m.getCommit().getId().name()); + assertThat(gitLog).doesNotContain(f1.getCommit().getId().name()); + assertThat(gitLog).doesNotContain(f2.getCommit().getId().name()); + } + + @Test + public void multiChange_UnStage_Before_MergeCommit() throws Exception { + RevCommit initialHead = getRemoteHead(); + + // make changes on feature branch + PushOneCommit.Result f1 = pushCommit("feature", "commitmsg1", "file1", "content1"); + PushOneCommit.Result f2 = pushCommit("feature", "commitmsg2", "file2", "content2"); + approve(f1.getChangeId()); + gApi.changes().id(f1.getChangeId()).current().submit(); + approve(f2.getChangeId()); + gApi.changes().id(f2.getChangeId()).current().submit(); + + // make a change on master branch + testRepo.reset(initialHead); + PushOneCommit.Result c1 = pushCommit("master", "commitmsg3", "file3", "content3"); + approve(c1.getChangeId()); + gApi.changes().id(c1.getChangeId()).current().submit(); + + // Stage a change + testRepo.reset(initialHead); + PushOneCommit.Result c2 = pushCommit("master", "commitmsg4", "file4", "content4"); + approve(c2.getChangeId()); + QtStage(c2); + + // merge feature branch and stage it on top + PushOneCommit mm = pushFactory.create(db, admin.getIdent(), testRepo); + mm.setParents(ImmutableList.of(c1.getCommit(), f2.getCommit())); + PushOneCommit.Result m = mm.to("refs/for/master"); + m.assertOkStatus(); + approve(m.getChangeId()); + QtStage(m); + + RevCommit stagingHead = qtUnStageExpectMerge(c2, m); + String gitLog = getRemoteLog("refs/staging/master").toString(); + assertThat(gitLog).contains(initialHead.getId().name()); + assertThat(gitLog).contains(c1.getCommit().getId().name()); + assertThat(gitLog).contains(stagingHead.getId().name()); + assertThat(gitLog).contains(f1.getCommit().getId().name()); + assertThat(gitLog).contains(f2.getCommit().getId().name()); + assertThat(gitLog).contains(m.getCommit().getId().name()); + assertThat(gitLog).doesNotContain(c2.getCommit().getId().name()); + } + + @Test + public void errorUnStage_No_Permission() throws Exception { + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + approve(c.getChangeId()); + QtStage(c); + + deny(project, "refs/heads/master", Permission.QT_STAGE, REGISTERED_USERS); + + RestResponse response = qtUnStageExpectFail(c, HttpStatus.SC_FORBIDDEN); + assertThat(response.getEntityContent()).contains("not permitted"); + + grant(project, "refs/heads/master", Permission.QT_STAGE, false, REGISTERED_USERS); + } + + @Test + public void errorUnStage_Wrong_Status() throws Exception { + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + approve(c.getChangeId()); + + RestResponse response = qtUnStageExpectFail(c, HttpStatus.SC_CONFLICT); + assertThat(response.getEntityContent()).isEqualTo("change is NEW"); + } + + @Test + public void errorUnStage_Invalid_ChangeId() throws Exception { + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + approve(c.getChangeId()); + QtStage(c); + + RestResponse response = call_REST_API_UnStage("thischangeidnotfound", c.getCommit().getName()); + response.assertStatus(HttpStatus.SC_NOT_FOUND); + assertThat(response.getEntityContent()).contains("Not found: thischangeidnotfound"); + } + + @Test + public void errorUnStage_Invalid_RevisionId() throws Exception { + PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1"); + approve(c.getChangeId()); + QtStage(c); + + RestResponse response = call_REST_API_UnStage(c.getChangeId(), "thisrevisionidnotfound"); + response.assertStatus(HttpStatus.SC_NOT_FOUND); + assertThat(response.getEntityContent()).contains("Not found: thisrevisionidnotfound"); + } + + @Test + public void errorUnStage_Revision_Not_Current() throws Exception { + + PushOneCommit.Result c1 = pushCommit("master", "commitmsg1", "file1", "content1"); + PushOneCommit.Result c2 = amendCommit(c1.getChangeId()); + approve(c2.getChangeId()); + QtStage(c2); + + RestResponse response = call_REST_API_UnStage(c1.getChangeId(), c1.getCommit().getName()); + response.assertStatus(HttpStatus.SC_CONFLICT); + assertThat(response.getEntityContent()).contains("is not current revision"); + } + + private RevCommit qtUnStageExpectCherryPick(PushOneCommit.Result c, + PushOneCommit.Result expectedContent) + throws Exception { + return qtUnStage(c, null, expectedContent, false); + } + + private RevCommit qtUnStageExpectCommit(PushOneCommit.Result c, + RevCommit expectedStagingHead) + throws Exception { + return qtUnStage(c, expectedStagingHead, null, false); + } + + private RevCommit qtUnStageExpectMerge(PushOneCommit.Result c, + PushOneCommit.Result expectedContent) + throws Exception { + return qtUnStage(c, null, expectedContent, true); + } + + private RevCommit qtUnStage(PushOneCommit.Result c, + RevCommit expectedStagingHead, + PushOneCommit.Result expectedContent, + boolean merge) + throws Exception { + String branch = getBranchNameFromRef(c.getChange().change().getDest().get()); + String stagingRef = R_STAGING + branch; + String branchRef = R_HEADS + branch; + RevCommit originalCommit = c.getCommit(); + String changeId = c.getChangeId(); + RevCommit initialHead = getRemoteHead(project, branchRef); + RevCommit oldStagingHead = getRemoteHead(project, stagingRef); + + RestResponse response = call_REST_API_UnStage(changeId, getCurrentPatchId(c)); + response.assertOK(); + + RevCommit masterHead = getRemoteHead(project, branchRef); + assertThat(masterHead.getId()).isEqualTo(initialHead.getId()); // master is not updated + + RevCommit stagingHead = getRemoteHead(project, stagingRef); + assertThat(stagingHead).isNotEqualTo(oldStagingHead); + + if (merge) { + assertThat(stagingHead.getParentCount()).isEqualTo(2); + assertThat(stagingHead.getParent(1)).isEqualTo(expectedContent.getCommit()); + expectedStagingHead = stagingHead; + } else if (expectedStagingHead == null && expectedContent != null) { + assertCherryPick(stagingHead, expectedContent.getCommit(), getCurrentPatchSHA(expectedContent)); + expectedStagingHead = stagingHead; + } else { + assertThat(stagingHead).isEqualTo(expectedStagingHead); // staging is updated + } + + if (expectedStagingHead.equals(oldStagingHead)) { + assertRefUpdatedEvents(stagingRef); // no events + } else { + assertRefUpdatedEvents(stagingRef, oldStagingHead, expectedStagingHead); + resetEvents(); + } + + Change change = c.getChange().change(); + assertThat(change.getStatus()).isEqualTo(Change.Status.NEW); + + ArrayList messages = new ArrayList(c.getChange().messages()); + assertThat(messages.get(messages.size() - 1).getMessage()).isEqualTo(UNSTAGED_MSG); + + return stagingHead; + } + + private RestResponse qtUnStageExpectFail(PushOneCommit.Result c, + int expectedStatus) + throws Exception { + String branch = getBranchNameFromRef(c.getChange().change().getDest().get()); + String stagingRef = R_STAGING + branch; + String branchRef = R_HEADS + branch; + + RevCommit initialHead = getRemoteHead(project, branchRef); + RevCommit oldStagingHead = getRemoteHead(project, stagingRef); + + RestResponse response = call_REST_API_UnStage(c.getChangeId(), getCurrentPatchId(c)); + response.assertStatus(expectedStatus); + + RevCommit masterHead = getRemoteHead(project, branchRef); + assertThat(masterHead.getId()).isEqualTo(initialHead.getId()); // master is not updated + + RevCommit stagingHead = getRemoteHead(project, stagingRef); + if (stagingHead != null) assertThat(stagingHead.getId()).isEqualTo(oldStagingHead.getId()); // staging is not updated + + assertRefUpdatedEvents(branchRef); // no events + assertRefUpdatedEvents(stagingRef); // no events + + return response; + } + +} -- cgit v1.2.3