From 6a2bd080169b6576ec442b9c85390c2a16945011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrik=20Sj=C3=B6lin?= Date: Tue, 11 Aug 2009 05:40:29 +0900 Subject: [PATCH] Adding new ssh create-project command. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When invoked this command will (as it may sound) create a new empty gerrit project. The command will also contact all the replication sites specfied in replication.config that can be connected to via ssh, and create the project there too. Signed-off-by: Ulrik Sjölin --- .../google/gerrit/git/PushReplication.java | 113 +++++++++- .../google/gerrit/git/ReplicationQueue.java | 14 +- .../google/gerrit/server/GerritServer.java | 33 +++ .../gerrit/server/account/GroupCache.java | 21 ++ .../ssh/commands/AdminCreateProject.java | 208 ++++++++++++++++++ .../ssh/commands/DefaultCommandModule.java | 1 + 6 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java diff --git a/src/main/java/com/google/gerrit/git/PushReplication.java b/src/main/java/com/google/gerrit/git/PushReplication.java index d25d97aa5a..f0e42a5d0d 100644 --- a/src/main/java/com/google/gerrit/git/PushReplication.java +++ b/src/main/java/com/google/gerrit/git/PushReplication.java @@ -22,6 +22,9 @@ import com.google.inject.Injector; import com.google.inject.Singleton; import com.google.inject.assistedinject.FactoryProvider; +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import org.slf4j.Logger; @@ -35,13 +38,16 @@ import org.spearce.jgit.transport.RemoteConfig; import org.spearce.jgit.transport.SshConfigSessionFactory; import org.spearce.jgit.transport.SshSessionFactory; import org.spearce.jgit.transport.URIish; +import org.spearce.jgit.util.QuotedString; import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -168,6 +174,111 @@ public class PushReplication implements ReplicationQueue { return result; } + public void replicateNewProject(Project.NameKey projectName) { + if (!isEnabled()) { + return; + } + + Iterator configIter = configs.iterator(); + + while (configIter.hasNext()) { + ReplicationConfig rp = configIter.next(); + List uriList = rp.getURIs(projectName, "*"); + + Iterator uriIter = uriList.iterator(); + + while (uriIter.hasNext()) { + replicateProject(uriIter.next()); + } + } + } + + + private void replicateProject(final URIish replicateURI) { + SshSessionFactory sshFactory = SshSessionFactory.getInstance(); + Session sshSession; + String projectPath = QuotedString.BOURNE.quote(replicateURI.getPath()); + + if(!usingSSH(replicateURI)) { + log.warn("Cannot create new project on remote site since the connection " + + "method is not SSH: " + replicateURI.toString()); + return; + } + + OutputStream errStream = createErrStream(); + String cmd = "mkdir " + projectPath + + "&& cd " + projectPath + + "&& git init --bare"; + + try { + sshSession = sshFactory.getSession(replicateURI.getUser(), + replicateURI.getPass(), replicateURI.getHost(), replicateURI.getPort()); + sshSession.connect(); + + Channel channel = sshSession.openChannel("exec"); + ((ChannelExec) channel).setCommand(cmd); + + channel.setInputStream(null); + + ((ChannelExec) channel).setErrStream(errStream); + + channel.connect(); + + while (!channel.isClosed()) { + try { + final int delay = 50; + Thread.sleep(delay); + } catch (InterruptedException e) { } + } + channel.disconnect(); + sshSession.disconnect(); + } catch(JSchException e) { + log.error("Communication error when trying to replicate to: " + + replicateURI.toString() + "\n" + + "Error reported: " + e.getMessage() + "\n" + + "Error in communication: " + errStream.toString()); + } + } + + private OutputStream createErrStream() { + return new OutputStream() { + private StringBuilder all = new StringBuilder(); + private StringBuilder sb = new StringBuilder(); + + public String toString() { + String r = all.toString(); + while (r.endsWith("\n")) + r = r.substring(0, r.length() - 1); + return r; + } + + @Override + public void write(final int b) throws IOException { + if (b == '\r') { + return; + } + + sb.append((char) b); + + if (b == '\n') { + all.append(sb); + sb.setLength(0); + } + } + }; + } + + private boolean usingSSH(final URIish uri) { + final String scheme = uri.getScheme(); + if (!uri.isRemote()) + return false; + if (scheme != null && scheme.toLowerCase().contains("ssh")) + return true; + if (scheme == null && uri.getHost() != null && uri.getPath() != null) + return true; + return false; + } + static class ReplicationConfig { private final RemoteConfig remote; private final int delay; @@ -247,4 +358,4 @@ public class PushReplication implements ReplicationQueue { return uri.toString().contains(urlMatch); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/google/gerrit/git/ReplicationQueue.java b/src/main/java/com/google/gerrit/git/ReplicationQueue.java index fb5f4743d2..b9abd8ad3a 100644 --- a/src/main/java/com/google/gerrit/git/ReplicationQueue.java +++ b/src/main/java/com/google/gerrit/git/ReplicationQueue.java @@ -28,7 +28,7 @@ public interface ReplicationQueue { * local project state. If not, they are updated by pushing new refs, updating * existing ones which don't match, and deleting stale refs which have been * removed from the local repository. - * + * * @param project identity of the project to replicate. * @param urlMatch substring that must appear in a URI to support replication. */ @@ -40,9 +40,19 @@ public interface ReplicationQueue { * This method automatically tries to batch together multiple requests in the * same project, to take advantage of Git's native ability to update multiple * refs during a single push operation. - * + * * @param project identity of the project to replicate. * @param ref unique name of the ref; must start with {@code refs/}. */ void scheduleUpdate(Project.NameKey project, String ref); + + /** + * Create new empty project at the remote sites. + *

+ * When a new project has been created locally call this method to make sure + * that the project will be created at the remote sites as well. + * + * @param project of the project to be created. + */ + void replicateNewProject(Project.NameKey project); } diff --git a/src/main/java/com/google/gerrit/server/GerritServer.java b/src/main/java/com/google/gerrit/server/GerritServer.java index 4b9ec6abc6..d5fd936ef8 100644 --- a/src/main/java/com/google/gerrit/server/GerritServer.java +++ b/src/main/java/com/google/gerrit/server/GerritServer.java @@ -91,6 +91,39 @@ public class GerritServer { } } + /** + * Create (and open) a repository by name. + * + * @param name the repository name, relative to the base directory. + * @return the cached Repository instance. Caller must call {@code close()} + * when done to decrement the resource handle. + * @throws RepositoryNotFoundException the name does not denote an existing + * repository, or the name cannot be read as a repository. + */ + public Repository createRepository(String name) + throws RepositoryNotFoundException { + if (basepath == null) { + throw new RepositoryNotFoundException("No gerrit.basepath configured"); + } + + if (isUnreasonableName(name)) { + throw new RepositoryNotFoundException("Invalid name: " + name); + } + + try { + if (!name.endsWith(".git")) { + name = name + ".git"; + } + final FileKey loc = FileKey.exact(new File(basepath, name)); + return RepositoryCache.open(loc, false); + } catch (IOException e1) { + final RepositoryNotFoundException e2; + e2 = new RepositoryNotFoundException("Cannot open repository " + name); + e2.initCause(e1); + throw e2; + } + } + private boolean isUnreasonableName(final String name) { if (name.length() == 0) return true; // no empty paths diff --git a/src/main/java/com/google/gerrit/server/account/GroupCache.java b/src/main/java/com/google/gerrit/server/account/GroupCache.java index 8065f27a20..7c7f6e2ad9 100644 --- a/src/main/java/com/google/gerrit/server/account/GroupCache.java +++ b/src/main/java/com/google/gerrit/server/account/GroupCache.java @@ -58,6 +58,10 @@ public class GroupCache { mgr.replaceCacheWithDecoratedCache(dc, self); } + public final AccountGroup.Id getAdministrators() { + return administrators; + } + private AccountGroup lookup(final AccountGroup.Id groupId) throws OrmException { final ReviewDb db = schema.open(); @@ -110,4 +114,21 @@ public class GroupCache { self.remove(groupId); } } + + public AccountGroup lookup(final String groupName) throws OrmException { + final ReviewDb db = schema.open(); + try { + final AccountGroup.NameKey nameKey = + new AccountGroup.NameKey(groupName); + + final AccountGroup group = db.accountGroups().get(nameKey); + if (group != null) { + return group; + } else { + return null; + } + } finally { + db.close(); + } + } } diff --git a/src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java b/src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java new file mode 100644 index 0000000000..b96c40d0c2 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java @@ -0,0 +1,208 @@ +// 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.AccountGroup; +import com.google.gerrit.client.reviewdb.ApprovalCategory; +import com.google.gerrit.client.reviewdb.Branch; +import com.google.gerrit.client.reviewdb.Project; +import com.google.gerrit.client.reviewdb.ProjectRight; +import com.google.gerrit.client.reviewdb.ReviewDb; +import com.google.gerrit.client.reviewdb.Project.SubmitType; +import com.google.gerrit.client.rpc.NoSuchEntityException; +import com.google.gerrit.git.ReplicationQueue; +import com.google.gerrit.server.GerritServer; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.ssh.AdminCommand; +import com.google.gerrit.server.ssh.BaseCommand; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.Transaction; +import com.google.inject.Inject; + +import org.kohsuke.args4j.Option; +import org.spearce.jgit.lib.Repository; + +import java.io.PrintWriter; +import java.util.Collections; + +/** Create a new project. **/ +@AdminCommand +final class AdminCreateProject extends BaseCommand { + @Option(name = "--name", required = true, aliases = { "-n" }, usage = "name of project to be created") + private String projectName; + + @Option(name = "--owner", aliases = { "-o" }, usage = "name of group that will own the project (defaults to: Administrators)") + private String ownerName; + + @Option(name = "--description", aliases = { "-d" }, usage = "description of the project") + private String projectDescription; + + @Option(name = "--submit-type", aliases = { "-t" }, usage = "project submit type (F)ast forward only, (M)erge if necessary, merge (A)lways or (C)herry pick (defaults to: F)") + private String submitTypeStr; + + @Option(name = "--use-contributor-agreements", aliases = { "--ca" }, usage = "set this to true if project should make the user sign a contributor agreement (defaults to: N)") + private String useContributorAgreements; + + @Option(name = "--use-signed-off-by", aliases = { "--so" }, usage = "set this to true if the project should mandate signed-off-by (defaults to: N)") + private String useSignedOffBy; + + @Inject + private ReviewDb db; + + @Inject + private GerritServer gs; + + @Inject + private GroupCache groupCache; + + @Inject + private ReplicationQueue rq; + + private AccountGroup.Id ownerId = null; + private boolean contributorAgreements = false; + private boolean signedOffBy = false; + private SubmitType submitType = null; + + @Override + public void start() { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + PrintWriter p = toPrintWriter(out); + + parseCommandLine(); + + try { + validateParameters(); + + Transaction txn = db.beginTransaction(); + + createProject(txn); + + Repository repo = gs.createRepository(projectName); + repo.create(true); + + txn.commit(); + + rq.replicateNewProject(new Project.NameKey(projectName)); + } catch (Exception e) { + p.print("Error when trying to create project: " + + e.getMessage() + "\n"); + p.flush(); + } + + } + }); + } + + + private void createProject(Transaction txn) throws OrmException, + NoSuchEntityException { + final Project.NameKey newProjectNameKey = + new Project.NameKey(projectName); + + final Project newProject = + new Project(newProjectNameKey, + new Project.Id(db.nextProjectId())); + + newProject.setDescription(projectDescription); + newProject.setSubmitType(submitType); + newProject.setUseContributorAgreements(contributorAgreements); + newProject.setUseSignedOffBy(signedOffBy); + + db.projects().insert(Collections.singleton(newProject), txn); + + final ProjectRight.Key prk = + new ProjectRight.Key(newProjectNameKey, + ApprovalCategory.OWN, ownerId); + final ProjectRight pr = new ProjectRight(prk); + pr.setMaxValue((short) 1); + pr.setMinValue((short) 1); + db.projectRights().insert(Collections.singleton(pr), txn); + + final Branch newBranch = + new Branch( + new Branch.NameKey(newProjectNameKey, Branch.R_HEADS + "master")); + + db.branches().insert(Collections.singleton(newBranch), txn); + } + + private boolean stringToBoolean(final String boolStr, + final boolean defaultValue) throws Failure { + if (boolStr == null) { + return defaultValue; + } + + if (boolStr.equalsIgnoreCase("FALSE") + || boolStr.equalsIgnoreCase("F") + || boolStr.equalsIgnoreCase("NO") + || boolStr.equalsIgnoreCase("N")) { + return false; + } + + if (boolStr.equalsIgnoreCase("TRUE") + || boolStr.equalsIgnoreCase("T") + || boolStr.equalsIgnoreCase("YES") + || boolStr.equalsIgnoreCase("Y")) { + return true; + } + + throw new Failure(1, "Parameter must have boolean value (true, false)"); + } + + private void validateParameters() throws Failure, OrmException { + if (projectName.endsWith(".git")) { + projectName = projectName.substring(0, + projectName.length() - ".git".length()); + } + + if (ownerName == null) { + ownerId = groupCache.getAdministrators(); + } else { + AccountGroup ownerGroup = groupCache.lookup(ownerName); + if (ownerGroup == null) { + throw new Failure(1, "Specified group does not exist"); + } + ownerId = ownerGroup.getId(); + } + + if (projectDescription == null) { + projectDescription = ""; + } + + contributorAgreements = stringToBoolean(useContributorAgreements, false); + signedOffBy = stringToBoolean(useSignedOffBy, false); + + if (submitTypeStr == null) { + submitTypeStr = "fast-forward-only"; + } + + if (submitTypeStr.toLowerCase().equalsIgnoreCase("fast-forward-only")) { + submitType = SubmitType.FAST_FORWARD_ONLY; + } else if (submitTypeStr.toLowerCase().equalsIgnoreCase("merge-if-necessary")) { + submitType = SubmitType.MERGE_IF_NECESSARY; + } else if (submitTypeStr.toLowerCase().equalsIgnoreCase("merge-always")) { + submitType = SubmitType.MERGE_ALWAYS; + } else if (submitTypeStr.toLowerCase().equalsIgnoreCase("cherry-pick")) { + submitType = SubmitType.CHERRY_PICK; + } + + if (submitType == null) { + throw new Failure(1, "Submit type must be either: fast-forward-only, " + + "merge-if-necessary, merge-always or cherry-pick"); + } + } +} + 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 aa28ce886a..a1b6d51dfa 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, "create-project").to(AdminCreateProject.class); command(gerrit, "flush-caches").to(AdminFlushCaches.class); command(gerrit, "ls-projects").to(ListProjects.class); command(gerrit, "receive-pack").to(Receive.class);