aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJukka Jokiniva <jukka.jokiniva@qt.io>2019-01-02 15:08:21 +0200
committerFrederik Gladhorn <frederik.gladhorn@qt.io>2019-04-05 07:27:51 +0000
commit765d16cf31f6068bd83c5c98e7932232b84f8616 (patch)
treeef5f1c6c4f6be88cdcd0b06d0ba1c4af6087ae1e
parent3019b115f8fbee9d82e9e76132ef8b4c35944e14 (diff)
Add Stage functionality
Stage button and search added to UI. Also corresponding REST API added. Fixes: QTBI-1402 Change-Id: I49e6999813aa48d142b885a1713f54b8d559286c Reviewed-by: Frederik Gladhorn <frederik.gladhorn@qt.io>
-rw-r--r--qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html13
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtAbandon.java5
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java114
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtCherryPickPatch.java198
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java2
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java2
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpen.java2
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtStage.java269
-rw-r--r--src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java168
-rw-r--r--src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java130
-rw-r--r--src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtStageIT.java294
11 files changed, 1183 insertions, 14 deletions
diff --git a/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html
index 03480b0..66c6e49 100644
--- a/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html
+++ b/qt-gerrit-ui-plugin/qt-gerrit-ui-plugin.html
@@ -20,6 +20,10 @@
'gerrit-plugin-qt-workflow~reopen' : {
header: 'Reopen the change?',
action_name: 'reopen'
+ },
+ 'gerrit-plugin-qt-workflow~stage' : {
+ header: 'Submit to staging?',
+ action_name: 'stage'
}
};
@@ -74,9 +78,16 @@
li_elem = htmlToElement(ul_elem.children[1].outerHTML);
link_elem = li_elem.children[0].children[1];
+ link_elem.text = 'Staged';
+ link_elem.href = '/q/status:staged';
+ ul_elem.insertBefore(li_elem, ul_elem.children[2]);
+
+
+ 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]);
+ ul_elem.insertBefore(li_elem, ul_elem.children[4]);
});
// Customize change view
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtAbandon.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtAbandon.java
index b0dafe6..0d37198 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtAbandon.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtAbandon.java
@@ -1,5 +1,5 @@
//
-// Copyright (C) 2018 The Qt Company
+// Copyright (C) 2019 The Qt Company
//
package com.googlesource.gerrit.plugins.qtcodereview;
@@ -81,7 +81,8 @@ public class QtAbandon extends RetryingRestModifyView<ChangeResource, AbandonInp
QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.ABANDONED,
"Abandoned",
input.message,
- ChangeMessagesUtil.TAG_ABANDON);
+ ChangeMessagesUtil.TAG_ABANDON,
+ null);
try (BatchUpdate u = updateFactory.create(dbProvider.get(), change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
u.addOp(rsrc.getId(), op).execute();
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java
index 491e896..6db1074 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtChangeUpdateOp.java
@@ -1,16 +1,24 @@
//
-// Copyright (C) 2018 The Qt Company
+// Copyright (C) 2019 The Qt Company
//
package com.googlesource.gerrit.plugins.qtcodereview;
import com.google.common.base.Strings;
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
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.LabelId;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -18,6 +26,8 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
public class QtChangeUpdateOp implements BatchUpdateOp {
@@ -25,30 +35,42 @@ public class QtChangeUpdateOp implements BatchUpdateOp {
QtChangeUpdateOp create(Change.Status newStatus,
@Assisted("defaultMessage") String defaultMessage,
@Assisted("inputMessage") String inputMessage,
- @Assisted("tag") String tag);
+ @Assisted("tag") String tag,
+ CodeReviewCommit copyApprovalsFrom);
}
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final Change.Status newStatus;
private final String defaultMessage;
private final String inputMessage;
private final String tag;
+ private CodeReviewCommit copyApprovalsFrom;
private Change change;
+ private PatchSetApproval submitter;
private final ChangeMessagesUtil cmUtil;
-
+ private final ApprovalsUtil approvalsUtil;
+ private final LabelNormalizer labelNormalizer;
@Inject
QtChangeUpdateOp(ChangeMessagesUtil cmUtil,
+ ApprovalsUtil approvalsUtil,
+ LabelNormalizer labelNormalizer,
@Nullable @Assisted Change.Status newStatus,
@Nullable @Assisted("defaultMessage") String defaultMessage,
@Nullable @Assisted("inputMessage") String inputMessage,
- @Nullable @Assisted("tag") String tag) {
+ @Nullable @Assisted("tag") String tag,
+ @Nullable @Assisted CodeReviewCommit copyApprovalsFrom) {
this.cmUtil = cmUtil;
+ this.approvalsUtil = approvalsUtil;
+ this.labelNormalizer = labelNormalizer;
this.newStatus = newStatus;
this.defaultMessage = defaultMessage;
this.inputMessage = inputMessage;
this.tag = tag;
+ this.copyApprovalsFrom = copyApprovalsFrom;
}
public Change getChange() {
@@ -64,6 +86,22 @@ public class QtChangeUpdateOp implements BatchUpdateOp {
if (newStatus != null) {
change.setStatus(newStatus);
+ update.fixStatus(newStatus);
+ updated = true;
+ }
+
+ if (copyApprovalsFrom != null) {
+ Change.Id id = ctx.getChange().getId();
+ PatchSet.Id oldPsId = copyApprovalsFrom.getPatchsetId();
+
+ logger.atFine().log("Copy approval for %s oldps=%s newps=%s", id, oldPsId, psId);
+ ChangeUpdate origPsUpdate = ctx.getUpdate(oldPsId);
+ LabelNormalizer.Result normalized = approve(ctx, origPsUpdate);
+
+ ChangeUpdate newPsUpdate = ctx.getUpdate(psId);
+
+ saveApprovals(normalized, ctx, newPsUpdate, true);
+ submitter = convertPatchSet(psId).apply(submitter);
updated = true;
}
@@ -89,4 +127,72 @@ public class QtChangeUpdateOp implements BatchUpdateOp {
return ChangeMessagesUtil.newMessage(ctx, msg.toString(), tag);
}
+ private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
+ throws OrmException, IOException {
+ PatchSet.Id psId = update.getPatchSetId();
+ Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
+ for (PatchSetApproval psa : approvalsUtil.byPatchSet(ctx.getDb(),
+ ctx.getNotes(),
+ psId,
+ ctx.getRevWalk(),
+ ctx.getRepoView().getConfig())) {
+ byKey.put(psa.getKey(), psa);
+ }
+
+ submitter = ApprovalsUtil.newApproval(psId, ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen());
+ byKey.put(submitter.getKey(), submitter);
+
+ LabelNormalizer.Result normalized = labelNormalizer.normalize(ctx.getNotes(), byKey.values());
+ update.putApproval(submitter.getLabel(), submitter.getValue());
+ saveApprovals(normalized, ctx, update, false);
+ return normalized;
+ }
+
+ private void saveApprovals(LabelNormalizer.Result normalized,
+ ChangeContext ctx,
+ ChangeUpdate update,
+ boolean includeUnchanged)
+ throws OrmException {
+ PatchSet.Id psId = update.getPatchSetId();
+ ctx.getDb().patchSetApprovals().upsert(convertPatchSet(normalized.getNormalized(), psId));
+ ctx.getDb().patchSetApprovals().upsert(zero(convertPatchSet(normalized.deleted(), psId)));
+ for (PatchSetApproval psa : normalized.updated()) {
+ update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
+ }
+ for (PatchSetApproval psa : normalized.deleted()) {
+ update.removeApprovalFor(psa.getAccountId(), psa.getLabel());
+ }
+
+ for (PatchSetApproval psa : normalized.unchanged()) {
+ if (includeUnchanged || psa.isLegacySubmit()) {
+ logger.atFine().log("Adding submit label %s", psa);
+ update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
+ }
+ }
+ }
+
+ private Function<PatchSetApproval, PatchSetApproval> convertPatchSet(final PatchSet.Id psId) {
+ return psa -> {
+ if (psa.getPatchSetId().equals(psId)) {
+ return psa;
+ }
+ return new PatchSetApproval(psId, psa);
+ };
+ }
+
+ private Iterable<PatchSetApproval> convertPatchSet(Iterable<PatchSetApproval> approvals, PatchSet.Id psId) {
+ return Iterables.transform(approvals, convertPatchSet(psId));
+ }
+
+ private Iterable<PatchSetApproval> zero(Iterable<PatchSetApproval> approvals) {
+ return Iterables.transform(
+ approvals,
+ a -> {
+ PatchSetApproval copy = new PatchSetApproval(a.getPatchSetId(), a);
+ copy.setValue((short) 0);
+ return copy;
+ }
+ );
+ }
+
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtCherryPickPatch.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtCherryPickPatch.java
new file mode 100644
index 0000000..73eb7ce
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtCherryPickPatch.java
@@ -0,0 +1,198 @@
+//
+// Copyright (C) 2019 The Qt Company
+//
+
+package com.googlesource.gerrit.plugins.qtcodereview;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+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.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.submit.IntegrationException;
+import com.google.gerrit.server.submit.MergeIdenticalTreeException;
+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 com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+@Singleton
+public class QtCherryPickPatch {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Provider<ReviewDb> dbProvider;
+ private final BatchUpdate.Factory batchUpdateFactory;
+ private final GitRepositoryManager gitManager;
+ private final Provider<IdentifiedUser> user;
+ private final PatchSetInserter.Factory patchSetInserterFactory;
+ private final MergeUtil.Factory mergeUtilFactory;
+ private final ProjectCache projectCache;
+ private final QtChangeUpdateOp.Factory qtUpdateFactory;
+
+ @Inject
+ QtCherryPickPatch(Provider<ReviewDb> dbProvider,
+ BatchUpdate.Factory batchUpdateFactory,
+ GitRepositoryManager gitManager,
+ Provider<IdentifiedUser> user,
+ PatchSetInserter.Factory patchSetInserterFactory,
+ MergeUtil.Factory mergeUtilFactory,
+ ProjectCache projectCache,
+ QtChangeUpdateOp.Factory qtUpdateFactory) {
+ this.dbProvider = dbProvider;
+ this.batchUpdateFactory = batchUpdateFactory;
+ this.gitManager = gitManager;
+ this.user = user;
+ this.patchSetInserterFactory = patchSetInserterFactory;
+ this.mergeUtilFactory = mergeUtilFactory;
+ this.projectCache = projectCache;
+ this.qtUpdateFactory = qtUpdateFactory;
+ }
+
+ public CodeReviewCommit cherryPickPatch(ChangeData changeData,
+ Project.NameKey project,
+ ObjectId sourceId,
+ ObjectId destId,
+ boolean allowFastForward,
+ Change.Status newStatus,
+ String defaultMessage,
+ String inputMessage,
+ String tag)
+ throws IntegrationException {
+
+ IdentifiedUser identifiedUser = user.get();
+ try (Repository git = gitManager.openRepository(project);
+ ObjectInserter oi = git.newObjectInserter();
+ ObjectReader reader = oi.newReader();
+ CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
+
+ if (!git.hasObject(sourceId)) throw new NoSuchRefException("Invalid source objectId: " + sourceId);
+ if (!git.hasObject(destId)) throw new NoSuchRefException("Invalid destination objectid: " + destId);
+
+ RevCommit baseCommit = revWalk.parseCommit(destId);
+ CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceId);
+
+ List parents = Arrays.asList(commitToCherryPick.getParents());
+ if (allowFastForward == true && parents.contains(baseCommit) && commitToCherryPick.getParentCount() < 2) {
+ logger.atInfo().log("qtcodereview: cherrypick fast forward %s on top of %s", sourceId, destId);
+ return commitToCherryPick;
+ }
+
+ Timestamp now = TimeUtil.nowTs();
+ PersonIdent committerIdent = commitToCherryPick.getCommitterIdent();
+
+ commitToCherryPick.setPatchsetId(changeData.currentPatchSet().getId());
+ commitToCherryPick.setNotes(changeData.notes());
+
+ CodeReviewCommit cherryPickCommit;
+ boolean mergeCommit = false;
+
+ ProjectState projectState = projectCache.checkedGet(project);
+ if (projectState == null) throw new NoSuchProjectException(project);
+
+ MergeUtil mergeUtil = mergeUtilFactory.create(projectState, true);
+ if (commitToCherryPick.getParentCount() > 1) {
+ // Merge commit cannot be cherrypicked
+ logger.atInfo().log("qtcodereview: merge commit detected %s", commitToCherryPick);
+ mergeCommit = true;
+ RevCommit commit = QtUtil.merge(committerIdent,
+ git, oi,
+ revWalk,
+ commitToCherryPick,
+ baseCommit,
+ true /* merge always */);
+ cherryPickCommit = revWalk.parseCommit(commit);
+ } else {
+ String commitMessage = mergeUtil.createCommitMessageOnSubmit(commitToCherryPick, baseCommit);
+ commitMessage += " "; // This ensures unique SHA1 is generated, otherwise old is reused
+ cherryPickCommit = mergeUtil.createCherryPickFromCommit(oi,
+ git.getConfig(),
+ baseCommit,
+ commitToCherryPick,
+ committerIdent,
+ commitMessage,
+ revWalk,
+ 0,
+ true, // ignoreIdenticalTree
+ false); // allowConflicts
+ }
+
+ boolean patchSetNotChanged = cherryPickCommit.equals(commitToCherryPick);
+ if (!patchSetNotChanged) {
+ logger.atInfo().log("qtcodereview: new patch %s -> %s", commitToCherryPick, cherryPickCommit);
+ oi.flush();
+ }
+ BatchUpdate bu = batchUpdateFactory.create(dbProvider.get(), project, identifiedUser, now);
+ bu.setRepository(git, revWalk, oi);
+ if (!patchSetNotChanged && !mergeCommit) {
+ Change.Id changeId = insertPatchSet(bu, git, changeData.notes(), cherryPickCommit);
+ bu.addOp(changeData.getId(), qtUpdateFactory.create(newStatus,
+ defaultMessage,
+ inputMessage,
+ tag,
+ commitToCherryPick));
+ logger.atInfo().log("qtcodereview: cherrypick new patch %s for %s", cherryPickCommit.toObjectId(), changeId);
+ } else {
+ bu.addOp(changeData.getId(), qtUpdateFactory.create(newStatus,
+ defaultMessage,
+ inputMessage,
+ tag,
+ null));
+ }
+
+ bu.execute();
+ logger.atInfo().log("qtcodereview: cherrypick done %s", changeData.getId());
+ return cherryPickCommit;
+ } catch (Exception e) {
+ throw new IntegrationException("Cherry pick failed: " + e.getMessage());
+ }
+ }
+
+ private Change.Id insertPatchSet(BatchUpdate bu,
+ Repository git,
+ ChangeNotes destNotes,
+ CodeReviewCommit cherryPickCommit)
+ throws IOException, OrmException, BadRequestException, ConfigInvalidException {
+ Change destChange = destNotes.getChange();
+ PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
+ PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
+ inserter.setNotify(NotifyHandling.NONE)
+ .setAllowClosed(true);
+ // .setCopyApprovals(true) doesn't work, so copying done in QtChangeUpdateOp
+ bu.addOp(destChange.getId(), inserter);
+ return destChange.getId();
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java
index 30b218f..afd1771 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtDefer.java
@@ -90,7 +90,7 @@ class QtDefer extends RetryingRestModifyView<ChangeResource, AbandonInput, Chang
throw new ResourceConflictException("change is " + ChangeUtil.status(change));
}
- QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.DEFERRED, "Deferred", input.message, null);
+ QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.DEFERRED, "Deferred", input.message, null, null);
try (BatchUpdate u = updateFactory.create(dbProvider.get(), change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
u.addOp(rsrc.getId(), op).execute();
}
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 46ae162..c6aa706 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtModule.java
@@ -5,6 +5,7 @@
package com.googlesource.gerrit.plugins.qtcodereview;
import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
+import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.config.FactoryModule;
@@ -32,6 +33,7 @@ public class QtModule extends FactoryModule {
post(CHANGE_KIND, "abandon").to(QtAbandon.class);
post(CHANGE_KIND, "defer").to(QtDefer.class);
post(CHANGE_KIND, "reopen").to(QtReOpen.class);
+ post(REVISION_KIND, "stage").to(QtStage.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
index 778d3dc..fcc1e38 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtReOpen.java
@@ -84,7 +84,7 @@ class QtReOpen extends RetryingRestModifyView<ChangeResource, RestoreInput, Chan
throw new ResourceConflictException("change is " + ChangeUtil.status(change));
}
- QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.NEW, "Reopened", input.message, null);
+ QtChangeUpdateOp op = qtUpdateFactory.create(Change.Status.NEW, "Reopened", input.message, null, null);
try (BatchUpdate u = updateFactory.create(dbProvider.get(), change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
u.addOp(rsrc.getId(), op).execute();
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtStage.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtStage.java
new file mode 100644
index 0000000..4244686
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtStage.java
@@ -0,0 +1,269 @@
+//
+// Copyright (C) 2019 The Qt Company
+//
+
+package com.googlesource.gerrit.plugins.qtcodereview;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.ParameterizedString;
+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.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+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.config.GerritServerConfig;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.CodeReviewCommit;
+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.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.submit.IntegrationException;
+import com.google.gerrit.server.submit.MergeOp;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+
+@Singleton
+public class QtStage implements RestModifyView<RevisionResource, SubmitInput>,
+ UiAction<RevisionResource> {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final String DEFAULT_TOOLTIP = "Stage patch set ${patchSet} into ${branch}";
+
+ 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 ChangeData.Factory changeDataFactory;
+ private final ProjectCache projectCache;
+ private final GitReferenceUpdated referenceUpdated;
+ private final QtCherryPickPatch qtCherryPickPatch;
+ private final QtUtil qtUtil;
+
+ private final AccountResolver accountResolver;
+ private final String label;
+ private final ParameterizedString titlePattern;
+
+ private Change change;
+ private Project.NameKey projectKey;
+ private Branch.NameKey destBranchKey;
+ private Branch.NameKey stagingBranchKey;
+
+ @Inject
+ QtStage(Provider<ReviewDb> dbProvider,
+ GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
+ ChangeData.Factory changeDataFactory,
+ AccountResolver accountResolver,
+ @GerritServerConfig Config cfg,
+ ProjectCache projectCache,
+ GitReferenceUpdated referenceUpdated,
+ QtCherryPickPatch qtCherryPickPatch,
+ QtUtil qtUtil) {
+
+ this.dbProvider = dbProvider;
+ this.repoManager = repoManager;
+ this.permissionBackend = permissionBackend;
+ this.changeDataFactory = changeDataFactory;
+ this.accountResolver = accountResolver;
+ this.label = "Stage";
+ this.titlePattern =
+ new ParameterizedString(
+ MoreObjects.firstNonNull(
+ cfg.getString("change", null, "stageTooltip"), DEFAULT_TOOLTIP));
+ this.projectCache = projectCache;
+ this.referenceUpdated = referenceUpdated;
+ this.qtCherryPickPatch = qtCherryPickPatch;
+ this.qtUtil = qtUtil;
+ }
+
+ @Override
+ public Output apply(RevisionResource rsrc, SubmitInput input)
+ throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
+ PermissionBackendException, UpdateException, ConfigInvalidException {
+
+ logger.atInfo().log("qtcodereview: submit %s to staging", 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(changeToStaging(rsrc, submitter, input));
+ }
+
+ private Change changeToStaging(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
+ throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException,
+ PermissionBackendException {
+ logger.atInfo().log("qtcodereview: changeToStaging starts");
+
+ if (change.getStatus() != Change.Status.NEW) {
+ logger.atSevere().log("qtcodereview: stage: 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: stage: 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: stage: 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()));
+ }
+
+ Repository git = null;
+ ObjectId destId = null;
+ ObjectId sourceId = null;
+ ChangeData changeData;
+
+ try {
+ git = repoManager.openRepository(projectKey);
+ // Check if staging branch exists. Create the staging branch if it does not exist.
+ if (!ProjectUtil.branchExists(repoManager, stagingBranchKey)) {
+ Result result = QtUtil.createStagingBranch(git, destBranchKey);
+ if (result == null) throw new NoSuchRefException("Cannot create staging ref: " + stagingBranchKey.get());
+ }
+ destId = git.resolve(stagingBranchKey.get());
+ if (destId == null) throw new NoSuchRefException("Invalid Revision: " + stagingBranchKey.get());
+
+ sourceId = git.resolve(rsrc.getPatchSet().getRevision().get());
+ if (sourceId == null) throw new NoSuchRefException("Invalid Revision: " + rsrc.getPatchSet().getRevision().get());
+
+ changeData = changeDataFactory.create(dbProvider.get(), change);
+ MergeOp.checkSubmitRule(changeData, false);
+
+ CodeReviewCommit commit = qtCherryPickPatch.cherryPickPatch(changeData,
+ projectKey,
+ sourceId,
+ destId,
+ false, // allowFastForward
+ Change.Status.STAGED,
+ "Staged for CI", // defaultMessage
+ null, // inputMessage
+ null // tag
+ );
+ Result result = qtUtil.updateRef(git, stagingBranchKey.get(), commit.toObjectId(), false);
+ referenceUpdated.fire(projectKey, stagingBranchKey.get(), destId, commit.toObjectId(), submitter.state());
+
+ } catch (IntegrationException e) {
+ logger.atInfo().log("qtcodereview: stage merge error %s", e);
+ throw new ResourceConflictException("merge error " + e);
+ } catch (NoSuchRefException e) {
+ logger.atSevere().log("qtcodereview: stage error %s", e);
+ throw new ResourceConflictException("resource error " + e);
+ } finally {
+ if (git != null) {
+ git.close();
+ }
+ }
+
+ change = changeData.reloadChange();
+ switch (change.getStatus()) {
+ case STAGED:
+ logger.atInfo().log("qtcodereview: changeToStaging %s added to %s", change, stagingBranchKey);
+ return change; // this doesn't return data to client, if needed use ChangeJson to convert it
+ case NEW:
+ throw new RestApiException("change unexpectedly had status " + change.getStatus() + " after submit attempt");
+ case ABANDONED:
+ default:
+ throw new ResourceConflictException("change is " + change.getStatus());
+ }
+
+ }
+
+ @Override
+ public UiAction.Description getDescription(RevisionResource resource) {
+ Change change = resource.getChange();
+ if (!change.getStatus().isOpen()
+ || change.isWorkInProgress()
+ || !resource.isCurrent()
+ || !resource.permissions().testOrFalse(ChangePermission.QT_STAGE)) {
+ return null; // submit not visible
+ }
+ try {
+ if (!projectCache.checkedGet(resource.getProject()).statePermitsWrite()) {
+ return null; // stage not visible
+ }
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log("Error checking if change is submittable");
+ throw new OrmRuntimeException("Could not determine problems for the change", e);
+ }
+
+ ReviewDb db = dbProvider.get();
+ ChangeData cd = changeDataFactory.create(db, resource.getNotes());
+ try {
+ MergeOp.checkSubmitRule(cd, false);
+ } catch (ResourceConflictException e) {
+ return null; // stage not visible
+ } catch (OrmException e) {
+ logger.atSevere().withCause(e).log("Error checking if change is submittable");
+ throw new OrmRuntimeException("Could not determine problems for the change", e);
+ }
+
+ Boolean enabled;
+ try {
+ enabled = cd.isMergeable();
+ } catch (OrmException e) {
+ throw new OrmRuntimeException("Could not determine mergeability", e);
+ }
+
+ RevId revId = resource.getPatchSet().getRevision();
+ Map<String, String> params =
+ ImmutableMap.of(
+ "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()),
+ "branch", change.getDest().getShortName(),
+ "commit", ObjectId.fromString(revId.get()).abbreviate(7).name() );
+ return new UiAction.Description()
+ .setLabel( label)
+ .setTitle(Strings.emptyToNull(titlePattern.replace(params)))
+ .setVisible(true)
+ .setEnabled(Boolean.TRUE.equals(enabled));
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java
new file mode 100644
index 0000000..fe35a3c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java
@@ -0,0 +1,168 @@
+// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
+// Copyright (C) 2019 The Qt Company
+//
+// 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.flogger.FluentLogger;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+
+/**
+ * Utility methods for working with git and database.
+ */
+@Singleton
+public class QtUtil {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public static final String R_HEADS = "refs/heads/";
+ public static final String R_STAGING = "refs/staging/";
+
+ public static class MergeConflictException extends Exception {
+ private static final long serialVersionUID = 1L;
+ public MergeConflictException(final String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Gets a staging branch for a branch.
+ * @param branch Branch under refs/heads. E.g. refs/heads/master. Can be short
+ * name.
+ * @return Matching staging branch. E.g. refs/staging/master
+ */
+ public static Branch.NameKey getStagingBranch(final Branch.NameKey branch) {
+ return getBranchWithNewPrefix(branch, R_HEADS, R_STAGING);
+ }
+
+ private static Branch.NameKey getBranchWithNewPrefix(final Branch.NameKey branch,
+ final String oldPrefix,
+ final String newPrefix) {
+ final String ref = branch.get();
+
+ if (ref.startsWith(oldPrefix)) {
+ // Create new ref replacing the old prefix with new.
+ return new Branch.NameKey(branch.getParentKey(), newPrefix + ref.substring(oldPrefix.length()));
+ }
+ // Treat the ref as short name.
+ return new Branch.NameKey(branch.getParentKey(), newPrefix + ref);
+ }
+
+ public static Result createStagingBranch(Repository git,
+ final Branch.NameKey sourceBranch) {
+ try {
+ final String sourceBranchName;
+ if (sourceBranch.get().startsWith(R_HEADS)) {
+ sourceBranchName = sourceBranch.get();
+ } else {
+ sourceBranchName = R_HEADS + sourceBranch.get();
+ }
+
+ final String stagingBranch = R_STAGING + sourceBranch.getShortName();
+
+ return updateRef(git, stagingBranch, sourceBranchName, true);
+ } catch (NoSuchRefException | IOException e ) {
+ return null;
+ }
+ }
+
+ private static Result updateRef(Repository git,
+ final String ref,
+ final String newValue,
+ final boolean force)
+ throws IOException, NoSuchRefException {
+ Ref sourceRef = git.getRefDatabase().getRef(newValue);
+ if (sourceRef == null) {
+ throw new NoSuchRefException(newValue);
+ }
+
+ return updateRef(git, ref, sourceRef.getObjectId(), force);
+ }
+
+ public static Result updateRef(Repository git,
+ final String ref,
+ final ObjectId id,
+ final boolean force)
+ throws IOException, NoSuchRefException {
+ RefUpdate refUpdate = git.updateRef(ref);
+ refUpdate.setNewObjectId(id);
+ refUpdate.setForceUpdate(force);
+ RefUpdate.Result result = refUpdate.update();
+ return result;
+ }
+
+ public static RevCommit merge(PersonIdent committerIdent,
+ Repository git,
+ ObjectInserter objInserter,
+ RevWalk revWalk,
+ RevCommit toMerge,
+ RevCommit mergeTip,
+ boolean mergeAlways)
+ throws NoSuchRefException, IOException, MergeConflictException {
+
+ if (revWalk.isMergedInto(toMerge, mergeTip)) {
+ logger.atWarning().log("qtcodereview: commit %s already in %s", toMerge, mergeTip);
+ return mergeTip; // already up to date
+ }
+
+ ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(git, true);
+ if (!merger.merge(mergeTip, toMerge)) {
+ logger.atWarning().log("qtcodereview: merge conflict %s on top of %s", toMerge, mergeTip);
+ throw new MergeConflictException("Merge conflict");
+ }
+
+ if (!mergeAlways && merger.getResultTreeId().equals(toMerge.getTree().toObjectId())) {
+ // Just fast forward, note that this will bring in all dependencies from source
+ logger.atInfo().log("qtcodereview: merge fast forward %s on top of %s", toMerge, mergeTip);
+ return toMerge;
+ }
+
+ String message;
+ try {
+ message = revWalk.parseCommit(toMerge).getShortMessage();
+ } catch (Exception e) {
+ message = toMerge.toString();
+ }
+ message = "Merge \"" + message + "\"";
+
+ final CommitBuilder mergeCommit = new CommitBuilder();
+ mergeCommit.setTreeId(merger.getResultTreeId());
+ mergeCommit.setParentIds(mergeTip, toMerge);
+ mergeCommit.setAuthor(toMerge.getAuthorIdent());
+ mergeCommit.setCommitter(committerIdent);
+ mergeCommit.setMessage(message);
+
+ return revWalk.parseCommit(objInserter.insert(mergeCommit));
+ }
+
+}
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 965f6d2..168f2e1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java
@@ -1,18 +1,36 @@
-// Copyright (C) 2018 The Qt Company
+// Copyright (C) 2019 The Qt Company
package com.googlesource.gerrit.plugins.qtcodereview;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.reviewdb.client.Change;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.PatternLayout;
+
+import java.util.List;
+
+import org.junit.Before;
import org.junit.Test;
@@ -29,6 +47,26 @@ public class QtCodeReviewIT extends LightweightPluginDaemonTest {
protected static final String R_STAGING = "refs/staging/";
protected static final String R_PUSH = "refs/for/";
+ protected static final String CONTENT_DATA = "hereisjustsomecontentforthecommits";
+
+ @Before
+ public void ReduceLogging() throws Exception {
+ LogManager.resetConfiguration();
+
+ final PatternLayout layout = new PatternLayout();
+ layout.setConversionPattern("%-5p %c %x: %m%n");
+
+ final ConsoleAppender dst = new ConsoleAppender();
+ dst.setLayout(layout);
+ dst.setTarget("System.err");
+ dst.setThreshold(Level.INFO);
+ dst.activateOptions();
+
+ final Logger root = LogManager.getRootLogger();
+ root.removeAllAppenders();
+ root.addAppender(dst);
+ }
+
@Test
public void pingSSHTest() throws Exception {
assertThat(adminSshSession.exec("gerrit-plugin-qt-workflow ping")).contains("Pong");
@@ -41,6 +79,8 @@ public class QtCodeReviewIT extends LightweightPluginDaemonTest {
protected void QtDefer(PushOneCommit.Result c) throws Exception {
RestResponse response = call_REST_API_Defer(c.getChangeId());
response.assertOK();
+ Change change = c.getChange().change();
+ assertThat(change.getStatus()).isEqualTo(Change.Status.DEFERRED);
}
protected RestResponse call_REST_API_Defer(String changeId) throws Exception {
@@ -49,11 +89,25 @@ public class QtCodeReviewIT extends LightweightPluginDaemonTest {
return response;
}
+ protected void QtStage(PushOneCommit.Result c) throws Exception {
+ RestResponse response = call_REST_API_Stage(c.getChangeId(), c.getCommit().getName());
+ response.assertOK();
+ Change change = c.getChange().change();
+ assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED);
+ resetEvents();
+ }
+
+ protected RestResponse call_REST_API_Stage(String changeId, String revisionId) throws Exception {
+ String url = "/changes/" + changeId + "/revisions/" + revisionId + "/gerrit-plugin-qt-workflow~stage";
+ RestResponse response = userRestSession.post(url);
+ return response;
+ }
+
protected PushOneCommit.Result pushCommit(String branch,
- String message,
- String file,
- String content)
- throws Exception {
+ 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();
@@ -61,6 +115,19 @@ public class QtCodeReviewIT extends LightweightPluginDaemonTest {
return c;
}
+ protected PushOneCommit.Result amendCommit(String changeId) throws Exception {
+ String branch = "master";
+ String pushRef = R_PUSH + branch;
+ PushOneCommit push = pushFactory.create(db,
+ user.getIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ PushOneCommit.FILE_NAME,
+ CONTENT_DATA,
+ changeId);
+ return push.to(pushRef);
+ }
+
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);
@@ -68,8 +135,61 @@ public class QtCodeReviewIT extends LightweightPluginDaemonTest {
return result;
}
+ protected void assertCherryPick(RevCommit head, RevCommit source, String cherrypickSHA) {
+ assertThat(head.getName()).isEqualTo(cherrypickSHA);
+ assertThat(head).isNotEqualTo(source);
+ assertThat(cherrypickSHA).isNotEqualTo(source.getName());
+ assertThat(head.getShortMessage()).isEqualTo(source.getShortMessage());
+ assertThat(head.getFooterLines("Change-Id")).isEqualTo(source.getFooterLines("Change-Id"));
+ assertThat(head.getParentCount()).isEqualTo(1);
+ }
+
+ protected void assertApproval(String changeId, TestAccount user) throws Exception {
+ ChangeInfo c = gApi.changes().id(changeId).get(DETAILED_LABELS, CURRENT_REVISION, CURRENT_COMMIT);
+
+ String label = "Code-Review";
+ int expectedVote = 2;
+ Integer vote = 0;
+ if (c.labels.get(label) != null && c.labels.get(label).all != null) {
+ for (ApprovalInfo approval : c.labels.get(label).all) {
+ if (approval._accountId == user.id.get()) {
+ vote = approval.value;
+ break;
+ }
+ }
+ }
+
+ String name = "label = " + label;
+ assertThat(vote).named(name).isEqualTo(expectedVote);
+ }
+
protected void assertRefUpdatedEvents(String refName, RevCommit ... expected) throws Exception {
eventRecorder.assertRefUpdatedEvents(project.get(), refName, expected);
}
+ protected void resetEvents() {
+ closeEventRecorder();
+ startEventRecorder();
+ }
+
+ protected String getBranchNameFromRef(String refStr) {
+ if (refStr.startsWith(R_HEADS)) {
+ return refStr.substring(R_HEADS.length());
+ } else {
+ return refStr;
+ }
+ }
+
+ protected String getCurrentPatchSHA(PushOneCommit.Result c) throws Exception {
+ return c.getChange().currentPatchSet().getRevision().get();
+ }
+
+ protected List<RevCommit> getRemoteLog(String ref) throws Exception {
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk rw = new RevWalk(repo)) {
+ rw.markStart(rw.parseCommit(repo.exactRef(ref).getObjectId()));
+ return Lists.newArrayList(rw);
+ }
+ }
+
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtStageIT.java b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtStageIT.java
new file mode 100644
index 0000000..60be674
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtStageIT.java
@@ -0,0 +1,294 @@
+// 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.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.SubmitInput;
+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 QtStageIT extends QtCodeReviewIT {
+
+ private final String STAGED_MSG = "Staged for CI";
+
+ @Before
+ public void SetDefaultPermissions() throws Exception {
+ createBranch(new Branch.NameKey(project, "feature"));
+
+ grant(project, "refs/heads/master", Permission.QT_STAGE, false, REGISTERED_USERS);
+ grant(project, "refs/heads/feature", Permission.QT_STAGE, false, REGISTERED_USERS);
+ }
+
+ @Test
+ public void singleChange_Stage() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+ approve(c.getChangeId());
+ RevCommit stagingHead = qtStage(c);
+ assertApproval(c.getChangeId(), admin);
+ }
+
+ @Test
+ public void multiChange_Stage() throws Exception {
+ RevCommit initialHead;
+ RevCommit stagingHead;
+
+ // Push 3 independent commits
+ 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());
+ approve(c2.getChangeId());
+ approve(c3.getChangeId());
+
+ stagingHead = qtStage(c1);
+ stagingHead = qtStage(c2);
+ stagingHead = qtStage(c3);
+ }
+
+ @Test
+ public void mergeCommit_Stage() 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());
+ RevCommit stagingHead = qtStageExpectMerge(m);
+
+ // check that all commits are in staging ref
+ String gitLog = getRemoteLog("refs/staging/master").toString();
+ assertThat(gitLog).contains(initialHead.getId().name());
+ assertThat(gitLog).contains(c1.getCommit().getId().name());
+ assertThat(gitLog).contains(f1.getCommit().getId().name());
+ assertThat(gitLog).contains(f2.getCommit().getId().name());
+ assertThat(gitLog).contains(m.getCommit().getId().name());
+ }
+
+ @Test
+ public void emptyChange_Stage() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c = pushCommit("master", "1st commit", "afile", "");
+ approve(c.getChangeId());
+ RevCommit stagingHead = qtStage(c);
+ assertApproval(c.getChangeId(), admin);
+
+ // no changes in this commit
+ c = pushCommit("master", "no content", "afile", "");
+ approve(c.getChangeId());
+ stagingHead = qtStage(c);
+ assertApproval(c.getChangeId(), admin);
+ }
+
+ @Test
+ public void errorStage_No_Permission() throws Exception {
+ deny(project, "refs/heads/master", Permission.QT_STAGE, REGISTERED_USERS);
+
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+ approve(c.getChangeId());
+
+ RestResponse response = qtStageExpectFail(c, initialHead, initialHead, HttpStatus.SC_FORBIDDEN);
+ assertThat(response.getEntityContent()).contains("not permitted");
+
+ grant(project, "refs/heads/master", Permission.QT_STAGE, false, REGISTERED_USERS);
+ }
+
+ @Test
+ public void errorStage_Wrong_Status() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+ approve(c.getChangeId());
+
+ grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
+ QtDefer(c);
+ deny(project, "refs/heads/master", Permission.ABANDON, REGISTERED_USERS);
+
+ RestResponse response = qtStageExpectFail(c, initialHead, initialHead, HttpStatus.SC_CONFLICT);
+ assertThat(response.getEntityContent()).contains("change is DEFERRED");
+ }
+
+ @Test
+ public void errorStage_Invalid_ChangeId() throws Exception {
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+
+ RestResponse response = call_REST_API_Stage("thischangeidnotfound", c.getCommit().getName());
+ response.assertStatus(HttpStatus.SC_NOT_FOUND);
+ assertThat(response.getEntityContent()).contains("Not found: thischangeidnotfound");
+ }
+
+ @Test
+ public void errorStage_Invalid_RevisionId() throws Exception {
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+
+ RestResponse response = call_REST_API_Stage(c.getChangeId(), "thisrevisionidnotfound");
+ response.assertStatus(HttpStatus.SC_NOT_FOUND);
+ assertThat(response.getEntityContent()).contains("Not found: thisrevisionidnotfound");
+ }
+
+ @Test
+ public void errorStage_Revision_Not_Current() throws Exception {
+ PushOneCommit.Result c1 = pushCommit("master", "commitmsg1", "file1", "content1");
+ PushOneCommit.Result c2 = amendCommit(c1.getChangeId());
+
+ RestResponse response = call_REST_API_Stage(c1.getChangeId(), c1.getCommit().getName());
+ response.assertStatus(HttpStatus.SC_CONFLICT);
+ assertThat(response.getEntityContent()).contains("is not current revision");
+ }
+
+ @Test
+ public void errorStage_Not_Reviewed() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c = pushCommit("master", "commitmsg1", "file1", "content1");
+
+ RestResponse response = qtStageExpectFail(c, initialHead, initialHead, HttpStatus.SC_CONFLICT);
+ assertThat(response.getEntityContent()).contains("needs Code-Review");
+ }
+
+ @Test
+ public void errorAmend_Status_Staged() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c1 = pushCommit("master", "commitmsg1", "file1", "content1");
+ approve(c1.getChangeId());
+ RevCommit stagingHead = qtStage(c1);
+
+ PushOneCommit.Result c2 = amendCommit(c1.getChangeId());
+ c2.assertErrorStatus(" closed");
+
+ RevCommit updatedHead = getRemoteHead(project, "refs/staging/master");
+ assertThat(updatedHead.getId()).isEqualTo(stagingHead.getId()); // not updated
+ }
+
+ @Test
+ public void errorStage_Merge_Conlict() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result c1 = pushCommit("master", "commitmsg1", "thesamefile", "content");
+ approve(c1.getChangeId());
+ RevCommit stagingHead1 = qtStage(c1);
+
+ testRepo.reset(initialHead);
+ PushOneCommit.Result c2 = pushCommit("master", "commitmsg2", "thesamefile", "conficting content");
+ approve(c2.getChangeId());
+ RestResponse response = qtStageExpectFail(c2, initialHead, stagingHead1, HttpStatus.SC_CONFLICT);
+ assertThat(response.getEntityContent()).contains("merge conflict");
+
+ Change change = c2.getChange().change();
+ assertThat(change.getStatus()).isEqualTo(Change.Status.NEW);
+ }
+
+ private RevCommit qtStage(PushOneCommit.Result c) throws Exception {
+ return qtStage(c, false);
+ }
+
+ private RevCommit qtStageExpectMerge(PushOneCommit.Result c) throws Exception {
+ return qtStage(c, true);
+ }
+
+ private RevCommit qtStage(PushOneCommit.Result c, 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();
+ RevCommit initialHead = getRemoteHead(project, branchRef);
+ RevCommit oldStagingHead = getRemoteHead(project, stagingRef);
+ if (oldStagingHead==null) oldStagingHead = initialHead;
+
+ RestResponse response = call_REST_API_Stage(c.getChangeId(), originalCommit.getName());
+ response.assertOK();
+
+ RevCommit branchHead = getRemoteHead(project, branchRef);
+ assertThat(branchHead.getId()).isEqualTo(initialHead.getId()); // master is not updated
+
+ RevCommit stagingHead = getRemoteHead(project, stagingRef);
+
+ if (merge) {
+ assertThat(stagingHead.getParentCount()).isEqualTo(2);
+ assertThat(stagingHead.getParent(1)).isEqualTo(originalCommit);
+ } else {
+ assertCherryPick(stagingHead, originalCommit, getCurrentPatchSHA(c));
+ }
+ assertThat(stagingHead.getParent(0)).isEqualTo(oldStagingHead);
+ assertRefUpdatedEvents(stagingRef, oldStagingHead, stagingHead);
+ resetEvents();
+
+ Change change = c.getChange().change();
+ assertThat(change.getStatus()).isEqualTo(Change.Status.STAGED);
+
+ ArrayList<ChangeMessage> messages = new ArrayList(c.getChange().messages());
+ assertThat(messages.get(messages.size()-1).getMessage()).isEqualTo(STAGED_MSG); // check last message
+
+ return stagingHead;
+ }
+
+ private RestResponse qtStageExpectFail(PushOneCommit.Result c,
+ RevCommit initialHead,
+ RevCommit oldStagingHead,
+ int expectedStatus)
+ throws Exception {
+ String branch = getBranchNameFromRef(c.getChange().change().getDest().get());
+ String stagingRef = R_STAGING + branch;
+ String branchRef = R_HEADS + branch;
+
+ RestResponse response = call_REST_API_Stage(c.getChangeId(), c.getCommit().getName());
+ response.assertStatus(expectedStatus);
+
+ RevCommit branchHead = getRemoteHead(project, branchRef);
+ assertThat(branchHead.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;
+ }
+
+}