diff --git a/src/main/java/com/google/gerrit/server/ssh/BaseCommand.java b/src/main/java/com/google/gerrit/server/ssh/BaseCommand.java index 21a6f39e26..363dc88130 100644 --- a/src/main/java/com/google/gerrit/server/ssh/BaseCommand.java +++ b/src/main/java/com/google/gerrit/server/ssh/BaseCommand.java @@ -156,7 +156,7 @@ public abstract class BaseCommand implements Command { list.add(r.toString()); } - final CmdLineParser clp = new CmdLineParser(this); + final CmdLineParser clp = newCmdLineParserInstance(this); try { clp.parseArgument(list.toArray(new String[list.size()])); } catch (CmdLineException err) { @@ -178,6 +178,10 @@ public abstract class BaseCommand implements Command { } } + protected CmdLineParser newCmdLineParserInstance(Object bean) { + return new CmdLineParser(bean); + } + /** * Spawn a function into its own thread. *

diff --git a/src/main/java/com/google/gerrit/server/ssh/commands/ApproveCommand.java b/src/main/java/com/google/gerrit/server/ssh/commands/ApproveCommand.java new file mode 100644 index 0000000000..c985c89600 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/ssh/commands/ApproveCommand.java @@ -0,0 +1,230 @@ +// Copyright (C) 2009 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.ssh.commands; + +import com.google.gerrit.client.data.ApprovalType; +import com.google.gerrit.client.data.ApprovalTypes; +import com.google.gerrit.client.reviewdb.ApprovalCategory; +import com.google.gerrit.client.reviewdb.ApprovalCategoryValue; +import com.google.gerrit.client.reviewdb.Change; +import com.google.gerrit.client.reviewdb.ChangeMessage; +import com.google.gerrit.client.reviewdb.PatchSet; +import com.google.gerrit.client.reviewdb.PatchSetApproval; +import com.google.gerrit.client.reviewdb.ReviewDb; +import com.google.gerrit.pgm.CmdLineParser; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.mail.CommentSender; +import com.google.gerrit.server.mail.EmailException; +import com.google.gerrit.server.mail.CommentSender.Factory; +import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.ssh.BaseCommand; +import com.google.gerrit.server.workflow.FunctionState; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.Transaction; +import com.google.inject.Inject; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ApproveCommand extends BaseCommand { + static { + CmdLineParser.registerHandler(PatchSet.Id.class, PatchSetIdHandler.class); + } + + protected final CmdLineParser newCmdLineParserInstance(final Object bean) { + CmdLineParser parser = new CmdLineParser(bean); + + for (CmdOption c : optionList) { + parser.addOption(c, c); + } + + return parser; + } + + private static final int CMD_ERR = 3; + + @Argument(index = 0, required = true, usage = "Patch set to approve") + private PatchSet.Id patchSetId; + @Option(name = "--message", aliases = "-m", usage = "Message to put on change/patchset", metaVar = "MESSAGE") + private String changeComment; + @Inject + private ReviewDb db; + @Inject + private IdentifiedUser currentUser; + @Inject + private Factory commentSenderFactory; + @Inject + private PatchSetInfoFactory patchSetInfoFactory; + @Inject + private ApprovalTypes approvalTypes; + @Inject + private ChangeControl.Factory changeControlFactory; + @Inject + private FunctionState.Factory functionStateFactory; + + + private List optionList; + + @Override + public final void start() throws IOException { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + getApprovalNames(); + parseCommandLine(); + + final Transaction txn = db.beginTransaction(); + + final PatchSet ps = db.patchSets().get(patchSetId); + + if (ps == null) { + throw new UnloggedFailure(CMD_ERR, "Invalid patchset id"); + } + + final Change.Id cid = ps.getId().getParentKey(); + final ChangeControl control = changeControlFactory.validateFor(cid); + final Change c = control.getChange(); + + if (c.getStatus().isClosed()) { + throw new UnloggedFailure(CMD_ERR, "Change is closed."); + } + + StringBuffer sb = new StringBuffer(); + sb.append("Patch Set "); + sb.append(patchSetId.get()); + sb.append(": "); + + for (CmdOption co : optionList) { + ApprovalCategory.Id category = + new ApprovalCategory.Id(co.approvalKey()); + PatchSetApproval.Key psaKey = + new PatchSetApproval.Key(patchSetId, currentUser + .getAccountId(), category); + PatchSetApproval psa = db.patchSetApprovals().get(psaKey); + + Short score = co.value(); + + if (score != null) { + addApproval(psaKey, score, c, co, txn); + } else { + if (psa == null) { + score = 0; + addApproval(psaKey, score, c, co, txn); + } else { + score = psa.getValue(); + } + } + + String message = + db.approvalCategoryValues().get( + new ApprovalCategoryValue.Id(category, score)).getName(); + sb.append(" " + message + ";"); + } + + sb.deleteCharAt(sb.length() - 1); + sb.append("\n\n"); + + if (changeComment != null) { + sb.append(changeComment); + } + + String uuid = ChangeUtil.messageUUID(db); + ChangeMessage cm = + new ChangeMessage(new ChangeMessage.Key(cid, uuid), currentUser + .getAccountId()); + cm.setMessage(sb.toString()); + db.changeMessages().insert(Collections.singleton(cm), txn); + ChangeUtil.updated(c); + db.changes().update(Collections.singleton(c), txn); + txn.commit(); + sendMail(c, c.currentPatchSetId(), cm); + } + }); + } + + private void sendMail(final Change c, final PatchSet.Id psid, + final ChangeMessage message) throws PatchSetInfoNotAvailableException, + EmailException, OrmException { + PatchSet ps = db.patchSets().get(psid); + final CommentSender cm; + cm = commentSenderFactory.create(c); + cm.setFrom(currentUser.getAccountId()); + cm.setPatchSet(ps, patchSetInfoFactory.get(psid)); + cm.setChangeMessage(message); + cm.setReviewDb(db); + cm.send(); + } + + private void addApproval(final PatchSetApproval.Key psaKey, + final Short score, final Change c, final CmdOption co, + final Transaction txn) throws OrmException, + UnloggedFailure { + PatchSetApproval psa = db.patchSetApprovals().get(psaKey); + boolean insert = false; + + if (psa == null) { + insert = true; + psa = new PatchSetApproval(psaKey, score); + } + + final List approvals = Collections.emptyList(); + final FunctionState fs = + functionStateFactory.create(c, patchSetId, approvals); + psa.setValue(score); + fs.normalize( + approvalTypes.getApprovalType(psa.getCategoryId()), psa); + if (score != psa.getValue()) { + throw new UnloggedFailure(CMD_ERR, co.name() + "=" + co.value() + + " not permitted"); + } + + psa.setGranted(); + + if (insert) { + db.patchSetApprovals().insert(Collections.singleton(psa), txn); + } else { + db.patchSetApprovals().update(Collections.singleton(psa), txn); + } + } + + private void getApprovalNames() throws OrmException { + optionList = new ArrayList(); + + for (ApprovalType type : approvalTypes.getApprovalTypes()) { + String usage = ""; + final ApprovalCategory category = type.getCategory(); + usage = "Score for " + category.getName() + "\n"; + + for (ApprovalCategoryValue v : type.getValues()) { + usage += + String.format("%4d", v.getValue()) + " - " + v.getName() + "\n"; + } + + optionList.add( + new CmdOption( + "--" + category.getName().toLowerCase().replace(' ', '-'), usage, + category.getId().get(), type.getMin().getValue(), + type.getMax().getValue(), category.getName())); + } + } +} diff --git a/src/main/java/com/google/gerrit/server/ssh/commands/CmdOption.java b/src/main/java/com/google/gerrit/server/ssh/commands/CmdOption.java new file mode 100644 index 0000000000..1d037b48c7 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/ssh/commands/CmdOption.java @@ -0,0 +1,134 @@ +// Copyright (C) 2009 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.ssh.commands; + +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Setter; + +import java.lang.annotation.Annotation; + +class CmdOption implements Option, Setter { + private String metaVar; + private boolean multiValued; + private String name; + private boolean required; + private String usage; + + private String approvalKey; + private Short approvalMax; + private Short approvalMin; + private String descrName; + + private Short value; + + public CmdOption(final String name, final String usage, final String key, + final Short min, final Short max, final String descrName) { + this.name = name; + this.usage = usage; + + this.metaVar = ""; + this.multiValued = false; + this.required = false; + this.value = null; + + this.approvalKey = key; + this.approvalMax = max; + this.approvalMin = min; + this.descrName = descrName; + } + + @Override + public final String[] aliases() { + return new String[0]; + } + + @Override + public final Class handler() { + return OptionHandler.class; + } + + @Override + public final String metaVar() { + return metaVar; + } + + @Override + public final boolean multiValued() { + return multiValued; + } + + @Override + public final String name() { + return name; + } + + @Override + public final boolean required() { + return required; + } + + @Override + public final String usage() { + return usage; + } + + public final Short value() { + return value; + } + + public final String approvalKey() { + return approvalKey; + } + + public final Short approvalMax() { + return approvalMax; + } + + public final Short approvalMin() { + return approvalMin; + } + + public final String descrName() { + return descrName; + } + + @Override + public Class annotationType() { + return null; + } + + @Override + public void addValue(final Object value) throws CmdLineException { + Short val = (Short) value; + if (val < approvalMin || val > approvalMax) { + throw new CmdLineException(name() + " valid values are " + + approvalMin.toString() + ".." + approvalMax.toString()); + } + + this.value = (Short) value; + } + + @Override + public Class getType() { + return Short.class; + } + + @Override + public boolean isMultiValued() { + return false; + } +} diff --git a/src/main/java/com/google/gerrit/server/ssh/commands/DefaultCommandModule.java b/src/main/java/com/google/gerrit/server/ssh/commands/DefaultCommandModule.java index a1b6d51dfa..488a241fda 100644 --- a/src/main/java/com/google/gerrit/server/ssh/commands/DefaultCommandModule.java +++ b/src/main/java/com/google/gerrit/server/ssh/commands/DefaultCommandModule.java @@ -28,6 +28,7 @@ public class DefaultCommandModule extends CommandModule { final CommandName gerrit = Commands.named("gerrit"); command(gerrit).toProvider(new DispatchCommandProvider(gerrit)); + command(gerrit, "approve").to(ApproveCommand.class); command(gerrit, "create-project").to(AdminCreateProject.class); command(gerrit, "flush-caches").to(AdminFlushCaches.class); command(gerrit, "ls-projects").to(ListProjects.class); diff --git a/src/main/java/com/google/gerrit/server/ssh/commands/PatchSetIdHandler.java b/src/main/java/com/google/gerrit/server/ssh/commands/PatchSetIdHandler.java new file mode 100644 index 0000000000..a8abc78611 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/ssh/commands/PatchSetIdHandler.java @@ -0,0 +1,51 @@ +// Copyright (C) 2009 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.ssh.commands; + +import com.google.gerrit.client.reviewdb.PatchSet; + +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Parameters; +import org.kohsuke.args4j.spi.Setter; + +public class PatchSetIdHandler extends OptionHandler { + public PatchSetIdHandler(final CmdLineParser parser, final OptionDef option, + final Setter setter) { + super(parser, option, setter); + } + + @Override + public final int parseArguments(final Parameters params) + throws CmdLineException { + final String idString = params.getParameter(0); + final PatchSet.Id id; + try { + id = PatchSet.Id.parse(idString); + } catch (IllegalArgumentException e) { + throw new CmdLineException("Invalid patch set: " + idString); + } + + setter.addValue(id); + return 1; + } + + @Override + public final String getDefaultMetaVariable() { + return "CHANGE,PATCHSET"; + } +}