Add ssh command line feature to submit change set

Rename the ssh gerrit approve command to review and enhance
it with the ability to perform submits via --submit.  Create
an alias for 'review' as 'approve' to support legacy approve.
Update docs accordingly.

Bug: issue 310
Change-Id: I5b88c8818b227de7dfc9c8a879eb5e7c7821e3b5
This commit is contained in:
Martin Fick
2010-06-10 10:32:00 -06:00
committed by Shawn O. Pearce
parent a78a37cf45
commit 8c84ba35e5
9 changed files with 205 additions and 14 deletions

View File

@@ -60,12 +60,15 @@ link:cmd-receive-pack.html[git receive-pack]::
Also implements the magic associated with uploading commits for Also implements the magic associated with uploading commits for
review. See link:user-upload.html#push_create[Creating Changes]. review. See link:user-upload.html#push_create[Creating Changes].
link:cmd-approve.html[gerrit approve]:: link:cmd-review.html[gerrit approve]::
Approve a patch set from the command line. Alias for 'gerrit review'.
link:cmd-ls-projects.html[gerrit ls-projects]:: link:cmd-ls-projects.html[gerrit ls-projects]::
List projects visible to the caller. List projects visible to the caller.
link:cmd-review.html[gerrit review]::
Verify, approve and/or submit a patch set from the command line.
link:cmd-stream-events.html[gerrit stream-events]:: link:cmd-stream-events.html[gerrit stream-events]::
Monitor events occuring in real time. Monitor events occuring in real time.

View File

@@ -1,19 +1,21 @@
gerrit approve gerrit review
============== ==============
NAME NAME
---- ----
gerrit approve - Approve one or more patch sets gerrit review - Verify, approve and/or submit one or more patch sets
SYNOPSIS SYNOPSIS
-------- --------
[verse] [verse]
'ssh' -p <port> <host> 'gerrit approve' [\--project <PROJECT>] [\--message <MESSAGE>] {COMMIT | CHANGEID,PATCHSET}... 'ssh' -p <port> <host> 'gerrit approve' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--submit] {COMMIT | CHANGEID,PATCHSET}...
'ssh' -p <port> <host> 'gerrit review' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--submit] {COMMIT | CHANGEID,PATCHSET}...
DESCRIPTION DESCRIPTION
----------- -----------
Updates the current user's approval status of the specified patch Updates the current user's approval status of the specified patch
sets, sending out email notifications and updating the database. sets and/or submits them for merging, sending out email
notifications and updating the database.
Patch sets should be specified as complete or abbreviated commit Patch sets should be specified as complete or abbreviated commit
SHA-1s. If the same commit is available in multiple projects the SHA-1s. If the same commit is available in multiple projects the
@@ -52,6 +54,10 @@ OPTIONS
differs per site, check the output of \--help, or contact differs per site, check the output of \--help, or contact
your site administrator for further details. your site administrator for further details.
\--submit::
-s::
Submit the specified patch set(s) for merging.
ACCESS ACCESS
------ ------
Any user who has configured an SSH key. Any user who has configured an SSH key.
@@ -65,14 +71,16 @@ EXAMPLES
Approve the change with commit c0ff33 as "Verified +1" Approve the change with commit c0ff33 as "Verified +1"
===== =====
$ ssh -p 29418 review.example.com gerrit approve --verified=+1 c0ff33 $ ssh -p 29418 review.example.com gerrit review --verified=+1 c0ff33
===== =====
Mark the unmerged commits both "Verified +1" and "Code Review +2": Mark the unmerged commits both "Verified +1" and "Code Review +2" and
submit them for merging:
==== ====
$ ssh -p 29418 review.example.com gerrit approve \ $ ssh -p 29418 review.example.com gerrit review \
--verified=+1 \ --verified=+1 \
--code-review=+2 \ --code-review=+2 \
--submit \
--project=this/project \ --project=this/project \
$(git rev-list origin/master..HEAD) $(git rev-list origin/master..HEAD)
==== ====

View File

@@ -14,15 +14,23 @@
package com.google.gerrit.server; package com.google.gerrit.server;
import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmConcurrencyException; import com.google.gwtorm.client.OrmConcurrencyException;
import com.google.gwtorm.client.OrmException; import com.google.gwtorm.client.OrmException;
import org.eclipse.jgit.util.Base64; import org.eclipse.jgit.util.Base64;
import org.eclipse.jgit.util.NB; import org.eclipse.jgit.util.NB;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
public class ChangeUtil { public class ChangeUtil {
private static int uuidPrefix; private static int uuidPrefix;
@@ -67,6 +75,49 @@ public class ChangeUtil {
computeSortKey(c); computeSortKey(c);
} }
public static void submit(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db, MergeQueue merger)
throws OrmException {
final Change.Id changeId = patchSetId.getParentKey();
final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db);
db.patchSetApprovals().upsert(Collections.singleton(approval));
final Change change = db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus() == Change.Status.NEW) {
change.setStatus(Change.Status.SUBMITTED);
ChangeUtil.updated(change);
}
return change;
}
});
if (change.getStatus() == Change.Status.SUBMITTED) {
merger.merge(change.getDest());
}
}
public static PatchSetApproval createSubmitApproval(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db)
throws OrmException {
final List<PatchSetApproval> allApprovals =
new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
patchSetId).toList());
final PatchSetApproval.Key akey =
new PatchSetApproval.Key(patchSetId, user.getAccountId(), SUBMIT);
for (final PatchSetApproval approval : allApprovals) {
if (akey.equals(approval.getKey())) {
approval.setValue((short) 1);
approval.setGranted();
return approval;
}
}
return new PatchSetApproval(akey, (short) 1);
}
public static void computeSortKey(final Change c) { public static void computeSortKey(final Change c) {
// The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC. // The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
// We overrun approximately 4,085 years later, so ~6093. // We overrun approximately 4,085 years later, so ~6093.

View File

@@ -0,0 +1,43 @@
// Copyright (C) 2010 The Android Open Source Project
//
// 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.google.gerrit.server.project;
/**
* Result from {@code ChangeControl.canSubmit()}.
*
* @see ChangeControl#canSubmit(com.google.gerrit.reviewdb.PatchSet.Id,
* com.google.gerrit.reviewdb.ReviewDb,
* com.google.gerrit.common.data.ApprovalTypes,
* com.google.gerrit.server.workflow.FunctionState.Factory)
*/
public class CanSubmitResult {
/** Magic constant meaning submitting is possible. */
public static final CanSubmitResult OK = new CanSubmitResult("OK");
private final String errorMessage;
CanSubmitResult(String error) {
this.errorMessage = error;
}
public String getMessage() {
return errorMessage;
}
@Override
public String toString() {
return "CanSubmitResult[" + getMessage() + "]";
}
}

View File

@@ -14,17 +14,27 @@
package com.google.gerrit.server.project; package com.google.gerrit.server.project;
import com.google.gerrit.reviewdb.Account; import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval; import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.workflow.CategoryFunction;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.gwtorm.client.OrmException; import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.List;
/** Access control management for a user accessing a single change. */ /** Access control management for a user accessing a single change. */
public class ChangeControl { public class ChangeControl {
public static class Factory { public static class Factory {
@@ -173,4 +183,54 @@ public class ChangeControl {
return false; return false;
} }
/** @return {@link CanSubmitResult#OK}, or a result with an error message. */
public CanSubmitResult canSubmit(final PatchSet.Id patchSetId, final ReviewDb db,
final ApprovalTypes approvalTypes,
FunctionState.Factory functionStateFactory)
throws OrmException {
if (change.getStatus().isClosed()) {
return new CanSubmitResult("Change " + change.getId() + " is closed");
}
if (!patchSetId.equals(change.currentPatchSetId())) {
return new CanSubmitResult("Patch set " + patchSetId + " is not current");
}
if (!getRefControl().canSubmit()) {
return new CanSubmitResult("User does not have permission to submit");
}
if (!(getCurrentUser() instanceof IdentifiedUser)) {
return new CanSubmitResult("User is not signed-in");
}
final List<PatchSetApproval> allApprovals =
new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
patchSetId).toList());
final PatchSetApproval myAction =
ChangeUtil.createSubmitApproval(patchSetId,
(IdentifiedUser) getCurrentUser(), db);
final ApprovalType actionType =
approvalTypes.getApprovalType(myAction.getCategoryId());
if (actionType == null || !actionType.getCategory().isAction()) {
return new CanSubmitResult("Invalid action " + myAction.getCategoryId());
}
final FunctionState fs =
functionStateFactory.create(change, patchSetId, allApprovals);
for (ApprovalType c : approvalTypes.getApprovalTypes()) {
CategoryFunction.forCategory(c.getCategory()).run(c, fs);
}
if (!CategoryFunction.forCategory(actionType.getCategory()).isValid(
getCurrentUser(), actionType, fs)) {
return new CanSubmitResult(actionType.getCategory().getName()
+ " not permitted");
}
fs.normalize(actionType, myAction);
if (myAction.getValue() <= 0) {
return new CanSubmitResult(actionType.getCategory().getName()
+ " not permitted");
}
return CanSubmitResult.OK;
}
} }

View File

@@ -118,6 +118,11 @@ public class RefControl {
return canPerform(READ, (short) 2); return canPerform(READ, (short) 2);
} }
/** @return true if this user can submit patch sets to this ref */
public boolean canSubmit() {
return canPerform(ApprovalCategory.SUBMIT, (short) 1);
}
/** @return true if the user can update the reference as a fast-forward. */ /** @return true if the user can update the reference as a fast-forward. */
public boolean canUpdate() { public boolean canUpdate() {
return canPerform(PUSH_HEAD, PUSH_HEAD_UPDATE); return canPerform(PUSH_HEAD, PUSH_HEAD_UPDATE);

View File

@@ -25,12 +25,13 @@ public class MasterCommandModule extends CommandModule {
protected void configure() { protected void configure() {
final CommandName gerrit = Commands.named("gerrit"); final CommandName gerrit = Commands.named("gerrit");
command(gerrit, "approve").to(ApproveCommand.class); command(gerrit, "approve").to(ReviewCommand.class);
command(gerrit, "create-account").to(AdminCreateAccount.class); command(gerrit, "create-account").to(AdminCreateAccount.class);
command(gerrit, "create-project").to(CreateProject.class); command(gerrit, "create-project").to(CreateProject.class);
command(gerrit, "gsql").to(AdminQueryShell.class); command(gerrit, "gsql").to(AdminQueryShell.class);
command(gerrit, "receive-pack").to(Receive.class); command(gerrit, "receive-pack").to(Receive.class);
command(gerrit, "replicate").to(AdminReplicate.class); command(gerrit, "replicate").to(AdminReplicate.class);
command(gerrit, "set-project-parent").to(AdminSetParent.class); command(gerrit, "set-project-parent").to(AdminSetParent.class);
command(gerrit, "review").to(ReviewCommand.class);
} }
} }

View File

@@ -27,10 +27,12 @@ import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.mail.CommentSender; import com.google.gerrit.server.mail.CommentSender;
import com.google.gerrit.server.mail.EmailException; import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.CanSubmitResult;
import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectControl;
@@ -56,9 +58,9 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
public class ApproveCommand extends BaseCommand { public class ReviewCommand extends BaseCommand {
private static final Logger log = private static final Logger log =
LoggerFactory.getLogger(ApproveCommand.class); LoggerFactory.getLogger(ReviewCommand.class);
@Override @Override
protected final CmdLineParser newCmdLineParser() { protected final CmdLineParser newCmdLineParser() {
@@ -71,7 +73,7 @@ public class ApproveCommand extends BaseCommand {
private final Set<PatchSet.Id> patchSetIds = new HashSet<PatchSet.Id>(); private final Set<PatchSet.Id> patchSetIds = new HashSet<PatchSet.Id>();
@Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "patch to approve") @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "patch to review")
void addPatchSetId(final String token) { void addPatchSetId(final String token) {
try { try {
patchSetIds.addAll(parsePatchSetId(token)); patchSetIds.addAll(parsePatchSetId(token));
@@ -88,12 +90,18 @@ public class ApproveCommand extends BaseCommand {
@Option(name = "--message", aliases = "-m", usage = "cover message to publish on change", metaVar = "MESSAGE") @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change", metaVar = "MESSAGE")
private String changeComment; private String changeComment;
@Option(name = "--submit", aliases = "-s", usage = "submit the patch set")
private boolean submitChange;
@Inject @Inject
private ReviewDb db; private ReviewDb db;
@Inject @Inject
private IdentifiedUser currentUser; private IdentifiedUser currentUser;
@Inject
private MergeQueue merger;
@Inject @Inject
private CommentSender.Factory commentSenderFactory; private CommentSender.Factory commentSenderFactory;
@@ -209,6 +217,17 @@ public class ApproveCommand extends BaseCommand {
hooks.doCommentAddedHook(change, currentUser.getAccount(), db.patchSets() hooks.doCommentAddedHook(change, currentUser.getAccount(), db.patchSets()
.get(patchSetId), changeComment, approvalsMap); .get(patchSetId), changeComment, approvalsMap);
if (submitChange) {
CanSubmitResult result =
changeControl.canSubmit(patchSetId, db, approvalTypes,
functionStateFactory);
if (result == CanSubmitResult.OK) {
ChangeUtil.submit(patchSetId, currentUser, db, merger);
} else {
throw error(result.getMessage());
}
}
} }
private Set<PatchSet.Id> parsePatchSetId(final String patchIdentity) private Set<PatchSet.Id> parsePatchSetId(final String patchIdentity)

View File

@@ -31,6 +31,7 @@ public class SlaveCommandModule extends CommandModule {
command(gerrit, "gsql").to(ErrorSlaveMode.class); command(gerrit, "gsql").to(ErrorSlaveMode.class);
command(gerrit, "receive-pack").to(ErrorSlaveMode.class); command(gerrit, "receive-pack").to(ErrorSlaveMode.class);
command(gerrit, "replicate").to(ErrorSlaveMode.class); command(gerrit, "replicate").to(ErrorSlaveMode.class);
command(gerrit, "review").to(ErrorSlaveMode.class);
command(gerrit, "set-project-parent").to(ErrorSlaveMode.class); command(gerrit, "set-project-parent").to(ErrorSlaveMode.class);
} }
} }