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
review. See link:user-upload.html#push_create[Creating Changes].
link:cmd-approve.html[gerrit approve]::
Approve a patch set from the command line.
link:cmd-review.html[gerrit approve]::
Alias for 'gerrit review'.
link:cmd-ls-projects.html[gerrit ls-projects]::
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]::
Monitor events occuring in real time.

View File

@@ -1,19 +1,21 @@
gerrit approve
gerrit review
==============
NAME
----
gerrit approve - Approve one or more patch sets
gerrit review - Verify, approve and/or submit one or more patch sets
SYNOPSIS
--------
[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
-----------
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
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
your site administrator for further details.
\--submit::
-s::
Submit the specified patch set(s) for merging.
ACCESS
------
Any user who has configured an SSH key.
@@ -65,14 +71,16 @@ EXAMPLES
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 \
--code-review=+2 \
--submit \
--project=this/project \
$(git rev-list origin/master..HEAD)
====

View File

@@ -14,15 +14,23 @@
package com.google.gerrit.server;
import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
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.server.git.MergeQueue;
import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmConcurrencyException;
import com.google.gwtorm.client.OrmException;
import org.eclipse.jgit.util.Base64;
import org.eclipse.jgit.util.NB;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ChangeUtil {
private static int uuidPrefix;
@@ -67,6 +75,49 @@ public class ChangeUtil {
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) {
// The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
// 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;
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.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
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.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.List;
/** Access control management for a user accessing a single change. */
public class ChangeControl {
public static class Factory {
@@ -173,4 +183,54 @@ public class ChangeControl {
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 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. */
public boolean canUpdate() {
return canPerform(PUSH_HEAD, PUSH_HEAD_UPDATE);

View File

@@ -25,12 +25,13 @@ public class MasterCommandModule extends CommandModule {
protected void configure() {
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-project").to(CreateProject.class);
command(gerrit, "gsql").to(AdminQueryShell.class);
command(gerrit, "receive-pack").to(Receive.class);
command(gerrit, "replicate").to(AdminReplicate.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.server.ChangeUtil;
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.EmailException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
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.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectControl;
@@ -56,9 +58,9 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
public class ApproveCommand extends BaseCommand {
public class ReviewCommand extends BaseCommand {
private static final Logger log =
LoggerFactory.getLogger(ApproveCommand.class);
LoggerFactory.getLogger(ReviewCommand.class);
@Override
protected final CmdLineParser newCmdLineParser() {
@@ -71,7 +73,7 @@ public class ApproveCommand extends BaseCommand {
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) {
try {
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")
private String changeComment;
@Option(name = "--submit", aliases = "-s", usage = "submit the patch set")
private boolean submitChange;
@Inject
private ReviewDb db;
@Inject
private IdentifiedUser currentUser;
@Inject
private MergeQueue merger;
@Inject
private CommentSender.Factory commentSenderFactory;
@@ -209,6 +217,17 @@ public class ApproveCommand extends BaseCommand {
hooks.doCommentAddedHook(change, currentUser.getAccount(), db.patchSets()
.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)

View File

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