Merge change 11109

* changes:
  Adding new ssh create-project command.
This commit is contained in:
Android Code Review
2009-08-12 07:05:35 -07:00
6 changed files with 387 additions and 3 deletions

View File

@@ -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<ReplicationConfig> configIter = configs.iterator();
while (configIter.hasNext()) {
ReplicationConfig rp = configIter.next();
List<URIish> uriList = rp.getURIs(projectName, "*");
Iterator<URIish> 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);
}
}
}
}

View File

@@ -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.
* <p>
* 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);
}

View File

@@ -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

View File

@@ -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();
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);