aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/googlesource/gerrit
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/googlesource/gerrit')
-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
8 files changed, 752 insertions, 8 deletions
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));
+ }
+
+}