From 01c758991ce454c0e1efd320351538cf37f48fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20Tu=CC=88rkoglu?= Date: Wed, 9 May 2012 12:43:02 -0700 Subject: [PATCH] SSH-command : set-project Change a project's settings such as merge-type-strategy, require- change-id, use-content-merge, project-state from CLI. [verse] 'ssh' -p 'gerrit set-project' [--description | -d ] [--submit-type | -t ] [--use|no-contributor-agreements | --ca|nca] [--use|no-signed-off-by | --so|nso] [--use|no-content-merge] [--require|no-change-id | --id|nid] [--project-state | --ps] Change-Id: I8352ffeda0d3ca7af61d45c44c93b62035e20676 --- Documentation/cmd-index.txt | 3 + Documentation/cmd-set-project.txt | 111 +++++++++++ .../server/args4j/ProjectControlHandler.java | 2 +- .../com/google/gerrit/sshd/BaseCommand.java | 8 + .../sshd/commands/MasterCommandModule.java | 1 + .../sshd/commands/SetProjectCommand.java | 173 ++++++++++++++++++ 6 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 Documentation/cmd-set-project.txt create mode 100644 gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt index 936729e125..4c0560e512 100644 --- a/Documentation/cmd-index.txt +++ b/Documentation/cmd-index.txt @@ -105,6 +105,9 @@ link:cmd-create-group.html[gerrit create-group]:: link:cmd-create-project.html[gerrit create-project]:: Create a new project and associated Git repository. +link:cmd-set-project.html[gerrit set-project]:: + Change a project's settings. + link:cmd-flush-caches.html[gerrit flush-caches]:: Flush some/all server caches from memory. diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt new file mode 100644 index 0000000000..c4b9b4f960 --- /dev/null +++ b/Documentation/cmd-set-project.txt @@ -0,0 +1,111 @@ +gerrit set-project +================== + +NAME +---- +gerrit set-project - Change a project's settings. + +SYNOPSIS +-------- +[verse] +'ssh' -p 'gerrit set-project' + [--description | -d ] + [--submit-type | -t ] + [--use|no-contributor-agreements | --ca|nca] + [--use|no-signed-off-by | --so|nso] + [--use|no-content-merge] + [--require|no-change-id | --id|nid] + [--project-state | --ps] + + +DESCRIPTION +----------- +Modifies a given project's settings. This command can be useful to +batch change projects. + +The command is argument-safe, that is, if no argument is given the +previous settings are kept intact. + +ACCESS +------ +Caller must be a member of the privileged 'Administrators' group. + +SCRIPTING +--------- +This command is intended to be used in scripts. + +OPTIONS +------- +:: + Required; name of the project to edit. If name ends + with `.git` the suffix will be automatically removed. + +--description:: +-d:: + New description of the project. If not specified, + the old description is kept. ++ +Description values containing spaces should be quoted in single quotes +('). This most likely requires double quoting the value, for example +`--description "'A description string'"`. + +--submit-type:: +-t:: + Action used by Gerrit to submit an approved change to its + destination branch. Supported options are: ++ +* FAST_FORWARD_ONLY: produces a strictly linear history. +* MERGE_IF_NECESSARY: create a merge commit when required. +* MERGE_ALWAYS: always create a merge commit. +* CHERRY_PICK: always cherry-pick the commit. + ++ +For more details see +link:project-setup.html#submit_type[Change Submit Actions]. + +--use|no-content-merge:: + If enabled, Gerrit will try to perform a 3-way merge of text + file content when a file has been modified by both the + destination branch and the change being submitted. This + option only takes effect if submit type is not + FAST_FORWARD_ONLY. + +--use|no-contributor-agreements:: +--ca|nca:: + If enabled, authors must complete a contributor agreement + on the site before pushing any commits or changes to this + project. + +--use|no-signed-off-by:: +--so|nso: + If enabled, each change must contain a Signed-off-by line + from either the author or the uploader in the commit message. + +--require|no-change-id:: +--id|nid:: + Require a valid link:user-changeid.html[Change-Id] footer + in any commit uploaded for review. This does not apply to + commits pushed directly to a branch or tag. + +--project-state:: +--ps:: + Set project's visibility. ++ +* ACTIVE: project is regular and is the default value. +* READ_ONLY: users can see the project if read permission +is granted, but all modification operations are disabled. +* HIDDEN: the project is not visible for those who are not owners + +EXAMPLES +-------- +Change project `example` to be hidden, require change id, don't use content merge +and use 'merge if necessary' as merge strategy: + +==== + $ ssh -p 29418 review.example.com gerrit set-project example --submit-type MERGE_IF_NECESSARY\ + --require-change-id --no-content-merge --project-state HIDDEN +==== + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] \ No newline at end of file diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java index c1c9c50cc0..da033e76b2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java @@ -71,7 +71,7 @@ public class ProjectControlHandler extends OptionHandler { final ProjectControl control; try { Project.NameKey nameKey = new Project.NameKey(projectName); - control = projectControlFactory.validateFor(nameKey); + control = projectControlFactory.validateFor(nameKey, ProjectControl.OWNER | ProjectControl.VISIBLE); } catch (NoSuchProjectException e) { throw new CmdLineException(owner, "'" + token + "': not a Gerrit project"); } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java index 2b4543b47a..9e04f05edd 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java @@ -371,6 +371,14 @@ public abstract class BaseCommand implements Command { return new UnloggedFailure(1, "fatal: " + why.getMessage(), why); } + public void checkExclusivity(final Object arg1, final String arg1name, + final Object arg2, final String arg2name) throws UnloggedFailure { + if (arg1 != null && arg2 != null) { + throw new UnloggedFailure(String.format( + "%s and %s options are mutually exclusive.", arg1name, arg2name)); + } + } + private final class TaskThunk implements CancelableRunnable, ProjectRunnable { private final CommandRunnable thunk; private final Context context; diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java index c5cdbd53f1..db4e985d01 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java @@ -36,5 +36,6 @@ public class MasterCommandModule extends CommandModule { command(gerrit, "set-project-parent").to(AdminSetParent.class); command(gerrit, "review").to(ReviewCommand.class); command(gerrit, "set-account").to(SetAccountCommand.class); + command(gerrit, "set-project").to(SetProjectCommand.class); } } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java new file mode 100644 index 0000000000..0b3fbbded2 --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java @@ -0,0 +1,173 @@ +// Copyright (C) 2012 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.sshd.commands; + +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.Project.State; +import com.google.gerrit.reviewdb.client.Project.SubmitType; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.sshd.SshCommand; +import com.google.inject.Inject; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER) +final class SetProjectCommand extends SshCommand { + private static final Logger log = LoggerFactory + .getLogger(SetProjectCommand.class); + + @Argument(index = 0, required = true, metaVar = "NAME", usage = "name of the project") + private ProjectControl projectControl; + + @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of project") + private String projectDescription; + + @Option(name = "--submit-type", aliases = {"-t"}, usage = "project submit type\n" + + "(default: MERGE_IF_NECESSARY)") + private SubmitType submitType; + + @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required") + private Boolean contributorAgreements; + + @Option(name = "--no-contributor-agreements", aliases = {"--nca"}, usage = "if contributor agreement is not required") + private Boolean noContributorAgreements; + + @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required") + private Boolean signedOffBy; + + @Option(name = "--no-signed-off-by", aliases = {"--nso"}, usage = "if signed-off-by is not required") + private Boolean noSignedOffBy; + + @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files") + private Boolean contentMerge; + + @Option(name = "--no-content-merge", usage = "don't allow automatic conflict resolving within files") + private Boolean noContentMerge; + + @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required") + private Boolean requireChangeID; + + @Option(name = "--no-change-id", aliases = {"--nid"}, usage = "if change-id is not required") + private Boolean noRequireChangeID; + + @Option(name = "--project-state", aliases = {"--ps"}, usage = "project's visibility state") + private State state; + + @Inject + private MetaDataUpdate.User metaDataUpdateFactory; + + @Inject + private ProjectCache projectCache; + + @Override + protected void run() throws Failure { + validate(); + Project ctlProject = projectControl.getProject(); + Project.NameKey nameKey = ctlProject.getNameKey(); + String name = ctlProject.getName(); + final StringBuilder err = new StringBuilder(); + + try { + MetaDataUpdate md = metaDataUpdateFactory.create(nameKey); + try { + ProjectConfig config = ProjectConfig.read(md); + Project project = config.getProject(); + + project.setRequireChangeID(requireChangeID != null ? requireChangeID + : project.isRequireChangeID()); + + project.setRequireChangeID(noRequireChangeID != null + ? !noRequireChangeID : project.isRequireChangeID()); + + project.setSubmitType(submitType != null ? submitType : project + .getSubmitType()); + + project.setUseContentMerge(contentMerge != null ? contentMerge + : project.isUseContentMerge()); + + project.setUseContentMerge(noContentMerge != null ? !noContentMerge + : project.isUseContentMerge()); + + project.setUseContributorAgreements(contributorAgreements != null + ? contributorAgreements : project.isUseContributorAgreements()); + + project.setUseContributorAgreements(noContributorAgreements != null + ? !noContributorAgreements : project.isUseContributorAgreements()); + + project.setUseSignedOffBy(signedOffBy != null ? signedOffBy : project + .isUseSignedOffBy()); + + project.setUseContentMerge(noSignedOffBy != null ? !noSignedOffBy + : project.isUseContentMerge()); + + project.setDescription(projectDescription != null ? projectDescription + : project.getDescription()); + + project.setState(state != null ? state : project.getState()); + + md.setMessage("Project settings updated"); + if (!config.commit(md)) { + err.append("error: Could not update project " + name + "\n"); + } + } finally { + md.close(); + } + } catch (RepositoryNotFoundException notFound) { + err.append("error: Project " + name + " not found\n"); + } catch (IOException e) { + final String msg = "Cannot update project " + name; + log.error(msg, e); + err.append("error: " + msg + "\n"); + } catch (ConfigInvalidException e) { + final String msg = "Cannot update project " + name; + log.error(msg, e); + err.append("error: " + msg + "\n"); + } + projectCache.evict(ctlProject); + + if (err.length() > 0) { + while (err.charAt(err.length() - 1) == '\n') { + err.setLength(err.length() - 1); + } + throw new UnloggedFailure(1, err.toString()); + } + } + + private void validate() throws UnloggedFailure { + checkExclusivity(contentMerge, "--use-content-merge", + noContentMerge, "--no-content-merge"); + + checkExclusivity(contributorAgreements, "--use-contributor-agreements", + noContributorAgreements, "--no-contributor-agreements"); + + checkExclusivity(signedOffBy, "--use-signed-off-by", + noSignedOffBy, "--no-signed-off-by"); + + checkExclusivity(requireChangeID, "--require-change-id", + noRequireChangeID, "--no-change-id"); + } +}