aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJukka Jokiniva <jukka.jokiniva@qt.io>2019-01-03 10:04:04 +0200
committerFrederik Gladhorn <frederik.gladhorn@qt.io>2019-04-09 12:30:09 +0000
commitbe31ec0d9673eaf040e78b80faeef3c942fde9fa (patch)
tree9eb3213fc5adb51c204955c77d473800f5f05a95
parent765d16cf31f6068bd83c5c98e7932232b84f8616 (diff)
Add UnStage functionality
Add UnStage button to UI and corresponding REST API. Fixes: QTBI-1403 Change-Id: Ibab2f37a16b907a3977edbd07e9e685373e7c9fe Reviewed-by: Frederik Gladhorn <frederik.gladhorn@qt.io>
-rw-r--r--qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html4
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java1
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStage.java206
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java184
-rw-r--r--src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java17
-rw-r--r--src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtUnStageIT.java364
6 files changed, 776 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 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<RevisionResource, SubmitInput>, UiAction<RevisionResource> {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public static class Output {
+ transient Change change;
+
+ private Output(Change c) {
+ change = c;
+ }
+ }
+
+ private final Provider<ReviewDb> 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<ReviewDb> 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<InternalChangeQuery> queryProvider;
+ private final GitReferenceUpdated referenceUpdated;
+ private final QtCherryPickPatch qtCherryPickPatch;
+
+ @Inject
+ QtUtil(Provider<InternalChangeQuery> 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<String> changeIds = commit.getFooterLines(FooterConstants.CHANGE_ID);
+ String changeId = null;
+ if (!changeIds.isEmpty()) changeId = changeIds.get(0);
+ return changeId;
+ }
+
+ private ChangeData findChangeFromList(String changeId, List<ChangeData> changes)
+ throws OrmException {
+ for (ChangeData item : changes) {
+ if (item.change().getKey().get().equals(changeId)) return item;
+ }
+ return null;
+ }
+
+ private List<ChangeData> arrangeOrderLikeInRef(Repository git,
+ ObjectId refObj,
+ ObjectId tipObj,
+ List<ChangeData> changeList)
+ throws MissingObjectException, OrmException,
+ IOException {
+ List<ChangeData> results = new ArrayList<ChangeData>();
+ 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<ChangeData> 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<ChangeData> changes_integrating = query.byBranchStatus(destBranchShortKey, Change.Status.INTEGRATING);
+
+ query = queryProvider.get();
+ List<ChangeData> changes_staged = query.byBranchStatus(destBranchShortKey, Change.Status.STAGED);
+
+ List<ChangeData> changes_allowed = new ArrayList<ChangeData>();
+ changes_allowed.addAll(changes_integrating);
+ changes_allowed.addAll(changes_staged);
+ List<ChangeData> 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<ChangeMessage> 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;
+ }
+
+}