init: Create a command to setup a new Gerrit installation
The init command uses an interactive prompting process to help the user get a basic website configured. The --import-projects option will automatically search for and import any Git repositories which are discovered within gerrit.basePath. Bug: issue 323 Bug: issue 330 Change-Id: I3d6e8f9f5fea8bfc78f6dfb1fc8f284bebfba670 Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
@@ -9,8 +9,8 @@ through the Java command line. For example:
|
|||||||
[[programs]]Programs
|
[[programs]]Programs
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
CreateSchema::
|
link:pgm-init.html[init]::
|
||||||
Initialize a new database schema.
|
Initialize a new Gerrit server installation
|
||||||
|
|
||||||
link:pgm-daemon.html[daemon]::
|
link:pgm-daemon.html[daemon]::
|
||||||
Gerrit HTTP, SSH network server.
|
Gerrit HTTP, SSH network server.
|
||||||
@@ -18,6 +18,9 @@ link:pgm-daemon.html[daemon]::
|
|||||||
version::
|
version::
|
||||||
Display the release version of Gerrit Code Review.
|
Display the release version of Gerrit Code Review.
|
||||||
|
|
||||||
|
CreateSchema::
|
||||||
|
Initialize a new database schema.
|
||||||
|
|
||||||
GERRIT
|
GERRIT
|
||||||
------
|
------
|
||||||
Part of link:index.html[Gerrit Code Review]
|
Part of link:index.html[Gerrit Code Review]
|
||||||
|
|||||||
51
Documentation/pgm-init.txt
Normal file
51
Documentation/pgm-init.txt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
init
|
||||||
|
====
|
||||||
|
|
||||||
|
NAME
|
||||||
|
----
|
||||||
|
init - Initialize a new Gerrit server installation
|
||||||
|
|
||||||
|
SYNOPSIS
|
||||||
|
--------
|
||||||
|
[verse]
|
||||||
|
'java' -jar gerrit.war 'init'
|
||||||
|
-d <SITE_PATH>
|
||||||
|
[\--batch]
|
||||||
|
[\--import-projects]
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
-----------
|
||||||
|
Creates a new Gerrit server installation, interactively prompting
|
||||||
|
for some basic setup prior to writing default configuration files
|
||||||
|
into a newly created `$site_path`.
|
||||||
|
|
||||||
|
If run an an existing `$site_path`, init will upgrade some resources
|
||||||
|
as necessary. This can be useful to import newly created projects.
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
-------
|
||||||
|
\--batch::
|
||||||
|
Run in batch mode, skipping interactive prompts. Reasonable
|
||||||
|
configuration defaults are chosen based on the whims of
|
||||||
|
the Gerrit developers.
|
||||||
|
|
||||||
|
\--import-projects::
|
||||||
|
Recursively search
|
||||||
|
link:config-gerrit.html#gerrit.basePath[gerrit.basePath]
|
||||||
|
for any Git repositories not yet registered as a project,
|
||||||
|
and initializes a new project for them.
|
||||||
|
|
||||||
|
-d::
|
||||||
|
\--site-path::
|
||||||
|
Location of the gerrit.config file, and all other per-site
|
||||||
|
configuration data, supporting libaries and log files.
|
||||||
|
|
||||||
|
CONTEXT
|
||||||
|
-------
|
||||||
|
This command can only be run on a server which has direct
|
||||||
|
connectivity to the metadata database, and local access to the
|
||||||
|
managed Git repositories.
|
||||||
|
|
||||||
|
GERRIT
|
||||||
|
------
|
||||||
|
Part of link:index.html[Gerrit Code Review]
|
||||||
@@ -54,6 +54,7 @@ public final class GerritLauncher {
|
|||||||
System.err.println("usage: java -jar " + jar + " command [ARG ...]");
|
System.err.println("usage: java -jar " + jar + " command [ARG ...]");
|
||||||
System.err.println();
|
System.err.println();
|
||||||
System.err.println("The most commonly used commands are:");
|
System.err.println("The most commonly used commands are:");
|
||||||
|
System.err.println(" init Initialize a Gerrit installation");
|
||||||
System.err.println(" daemon Run the Gerrit network daemons");
|
System.err.println(" daemon Run the Gerrit network daemons");
|
||||||
System.err.println(" version Display the build version number");
|
System.err.println(" version Display the build version number");
|
||||||
System.err.println();
|
System.err.println();
|
||||||
|
|||||||
@@ -72,7 +72,12 @@ public abstract class AbstractProgram {
|
|||||||
ProxyUtil.configureHttpProxy();
|
ProxyUtil.configureHttpProxy();
|
||||||
return run();
|
return run();
|
||||||
} catch (Die err) {
|
} catch (Die err) {
|
||||||
System.err.println("fatal: " + err.getMessage());
|
final Throwable cause = err.getCause();
|
||||||
|
final String diemsg = err.getMessage();
|
||||||
|
if (cause != null && !cause.getMessage().equals(diemsg)) {
|
||||||
|
System.err.println("fatal: " + cause.getMessage());
|
||||||
|
}
|
||||||
|
System.err.println("fatal: " + diemsg);
|
||||||
return 128;
|
return 128;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
238
gerrit-pgm/src/main/java/com/google/gerrit/pgm/ConsoleUI.java
Normal file
238
gerrit-pgm/src/main/java/com/google/gerrit/pgm/ConsoleUI.java
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// 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.pgm;
|
||||||
|
|
||||||
|
import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
|
||||||
|
|
||||||
|
import java.io.Console;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
/** Console based interaction with the invoking user. */
|
||||||
|
public abstract class ConsoleUI {
|
||||||
|
/** Get a UI instance, assuming interactive mode. */
|
||||||
|
public static ConsoleUI getInstance() {
|
||||||
|
return getInstance(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a UI instance, possibly forcing batch mode. */
|
||||||
|
public static ConsoleUI getInstance(final boolean batchMode) {
|
||||||
|
Console console = batchMode ? null : System.console();
|
||||||
|
return console != null ? new Interactive(console) : new Batch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constructs an exception indicating the user aborted the operation. */
|
||||||
|
protected static Die abort() {
|
||||||
|
return new Die("aborted by user");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Obtain all values from an enumeration. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected static <T extends Enum<?>> T[] all(final T value) {
|
||||||
|
try {
|
||||||
|
return (T[]) value.getClass().getMethod("values").invoke(null);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true if this is a batch UI that has no user interaction. */
|
||||||
|
public abstract boolean isBatch();
|
||||||
|
|
||||||
|
/** Display a header message before a series of prompts. */
|
||||||
|
public abstract void header(String fmt, Object... args);
|
||||||
|
|
||||||
|
/** Request the user to answer a yes/no question. */
|
||||||
|
public abstract boolean yesno(String fmt, Object... args);
|
||||||
|
|
||||||
|
/** Prints a message asking the user to let us know when its safe to continue. */
|
||||||
|
public abstract void waitForUser();
|
||||||
|
|
||||||
|
/** Prompt the user for a string, suggesting a default, and returning choice. */
|
||||||
|
public final String readString(String def, String fmt, Object... args) {
|
||||||
|
if (def != null && def.isEmpty()) {
|
||||||
|
def = null;
|
||||||
|
}
|
||||||
|
return readStringImpl(def, fmt, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prompt the user for a string, suggesting a default, and returning choice. */
|
||||||
|
protected abstract String readStringImpl(String def, String fmt,
|
||||||
|
Object... args);
|
||||||
|
|
||||||
|
/** Prompt the user for a password, returning the string; null if blank. */
|
||||||
|
public abstract String password(String fmt, Object... args);
|
||||||
|
|
||||||
|
/** Prompt the user to make a choice from an enumeration's values. */
|
||||||
|
public abstract <T extends Enum<?>> T readEnum(T def, String fmt,
|
||||||
|
Object... args);
|
||||||
|
|
||||||
|
|
||||||
|
private static class Interactive extends ConsoleUI {
|
||||||
|
private final Console console;
|
||||||
|
|
||||||
|
Interactive(final Console console) {
|
||||||
|
this.console = console;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBatch() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean yesno(String fmt, Object... args) {
|
||||||
|
final String prompt = String.format(fmt, args);
|
||||||
|
for (;;) {
|
||||||
|
final String yn = console.readLine("%-30s [y/n]? ", prompt);
|
||||||
|
if (yn == null) {
|
||||||
|
throw abort();
|
||||||
|
}
|
||||||
|
if (yn.equalsIgnoreCase("y") || yn.equalsIgnoreCase("yes")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (yn.equalsIgnoreCase("n") || yn.equalsIgnoreCase("no")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void waitForUser() {
|
||||||
|
if (console.readLine("Press enter to continue ") == null) {
|
||||||
|
throw abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String readStringImpl(String def, String fmt, Object... args) {
|
||||||
|
final String prompt = String.format(fmt, args);
|
||||||
|
String r;
|
||||||
|
if (def != null) {
|
||||||
|
r = console.readLine("%-30s [%s]: ", prompt, def);
|
||||||
|
} else {
|
||||||
|
r = console.readLine("%-30s : ", prompt);
|
||||||
|
}
|
||||||
|
if (r == null) {
|
||||||
|
throw abort();
|
||||||
|
}
|
||||||
|
r = r.trim();
|
||||||
|
if (r.isEmpty()) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String password(String fmt, Object... args) {
|
||||||
|
final String prompt = String.format(fmt, args);
|
||||||
|
for (;;) {
|
||||||
|
final char[] a1 = console.readPassword("%-30s : ", prompt);
|
||||||
|
if (a1 == null) {
|
||||||
|
throw abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
final char[] a2 = console.readPassword("%30s : ", "confirm password");
|
||||||
|
if (a2 == null) {
|
||||||
|
throw abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String s1 = new String(a1);
|
||||||
|
final String s2 = new String(a2);
|
||||||
|
if (!s1.equals(s2)) {
|
||||||
|
console.printf("error: Passwords did not match; try again\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return !s1.isEmpty() ? s1 : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Enum<?>> T readEnum(T def, String fmt, Object... args) {
|
||||||
|
final String prompt = String.format(fmt, args);
|
||||||
|
final T[] options = all(def);
|
||||||
|
for (;;) {
|
||||||
|
String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString());
|
||||||
|
if (r == null) {
|
||||||
|
throw abort();
|
||||||
|
}
|
||||||
|
r = r.trim();
|
||||||
|
if (r.isEmpty()) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
for (final T e : options) {
|
||||||
|
if (equalsIgnoreCase(e.toString(), r)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!"?".equals(r)) {
|
||||||
|
console.printf("error: '%s' is not a valid choice\n", r);
|
||||||
|
}
|
||||||
|
console.printf(" Supported options are:\n");
|
||||||
|
for (final T e : options) {
|
||||||
|
console.printf(" %s\n", e.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void header(String fmt, Object... args) {
|
||||||
|
fmt = fmt.replaceAll("\n", "\n*** ");
|
||||||
|
console.printf("\n*** " + fmt + "\n*** \n\n", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Batch extends ConsoleUI {
|
||||||
|
@Override
|
||||||
|
public boolean isBatch() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean yesno(String fmt, Object... args) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String readStringImpl(String def, String fmt, Object... args) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void waitForUser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String password(String fmt, Object... args) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Enum<?>> T readEnum(T def, String fmt, Object... args) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void header(String fmt, Object... args) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ package com.google.gerrit.pgm;
|
|||||||
import com.google.gerrit.lifecycle.LifecycleListener;
|
import com.google.gerrit.lifecycle.LifecycleListener;
|
||||||
|
|
||||||
import org.apache.log4j.Appender;
|
import org.apache.log4j.Appender;
|
||||||
|
import org.apache.log4j.ConsoleAppender;
|
||||||
import org.apache.log4j.DailyRollingFileAppender;
|
import org.apache.log4j.DailyRollingFileAppender;
|
||||||
import org.apache.log4j.Level;
|
import org.apache.log4j.Level;
|
||||||
import org.apache.log4j.LogManager;
|
import org.apache.log4j.LogManager;
|
||||||
@@ -29,6 +30,22 @@ import org.apache.log4j.spi.LoggingEvent;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
public class ErrorLogFile {
|
public class ErrorLogFile {
|
||||||
|
public static void errorOnlyConsole() {
|
||||||
|
LogManager.resetConfiguration();
|
||||||
|
|
||||||
|
final PatternLayout layout = new PatternLayout();
|
||||||
|
layout.setConversionPattern("%-5p %c %x: %m%n");
|
||||||
|
|
||||||
|
final ConsoleAppender dst = new ConsoleAppender();
|
||||||
|
dst.setLayout(layout);
|
||||||
|
dst.setTarget("System.err");
|
||||||
|
dst.setThreshold(Level.ERROR);
|
||||||
|
|
||||||
|
final Logger root = LogManager.getRootLogger();
|
||||||
|
root.removeAllAppenders();
|
||||||
|
root.addAppender(dst);
|
||||||
|
}
|
||||||
|
|
||||||
public static LifecycleListener start(final File sitePath) {
|
public static LifecycleListener start(final File sitePath) {
|
||||||
final File logdir = new File(sitePath, "logs");
|
final File logdir = new File(sitePath, "logs");
|
||||||
if (!logdir.exists() && !logdir.mkdirs()) {
|
if (!logdir.exists() && !logdir.mkdirs()) {
|
||||||
|
|||||||
659
gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
Normal file
659
gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
// 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.pgm;
|
||||||
|
|
||||||
|
import static com.google.gerrit.pgm.DataSourceProvider.Type.H2;
|
||||||
|
|
||||||
|
import com.google.gerrit.reviewdb.AuthType;
|
||||||
|
import com.google.gerrit.reviewdb.Project;
|
||||||
|
import com.google.gerrit.reviewdb.ReviewDb;
|
||||||
|
import com.google.gerrit.reviewdb.Project.SubmitType;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gerrit.server.mail.SmtpEmailSender.Encryption;
|
||||||
|
import com.google.gwtjsonrpc.server.SignedToken;
|
||||||
|
import com.google.gwtorm.client.OrmException;
|
||||||
|
import com.google.gwtorm.client.SchemaFactory;
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import com.google.inject.Module;
|
||||||
|
|
||||||
|
import org.apache.sshd.common.util.SecurityUtils;
|
||||||
|
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
import org.eclipse.jgit.lib.Constants;
|
||||||
|
import org.eclipse.jgit.lib.FileBasedConfig;
|
||||||
|
import org.eclipse.jgit.lib.LockFile;
|
||||||
|
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
|
||||||
|
import org.eclipse.jgit.util.SystemReader;
|
||||||
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Initialize a new Gerrit installation. */
|
||||||
|
public class Init extends SiteProgram {
|
||||||
|
@Option(name = "--batch", usage = "Batch mode; skip interactive prompting")
|
||||||
|
private boolean batchMode;
|
||||||
|
|
||||||
|
@Option(name = "--import-projects", usage = "Import git repositories as projects")
|
||||||
|
private boolean importProjects;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private GitRepositoryManager repositoryManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SchemaFactory<ReviewDb> schema;
|
||||||
|
|
||||||
|
private boolean deleteOnFailure;
|
||||||
|
private ConsoleUI ui;
|
||||||
|
private Injector dbInjector;
|
||||||
|
private Injector sysInjector;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int run() throws Exception {
|
||||||
|
ErrorLogFile.errorOnlyConsole();
|
||||||
|
ui = ConsoleUI.getInstance(batchMode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
initSitePath();
|
||||||
|
inject();
|
||||||
|
initGit();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
if (deleteOnFailure) {
|
||||||
|
recursiveDelete(getSitePath());
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
|
} catch (Error failure) {
|
||||||
|
if (deleteOnFailure) {
|
||||||
|
recursiveDelete(getSitePath());
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
System.err.println("Initialized " + getSitePath().getCanonicalPath());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSitePath() throws IOException, InterruptedException {
|
||||||
|
final File sitePath = getSitePath();
|
||||||
|
|
||||||
|
final File gerrit_config = new File(sitePath, "gerrit.config");
|
||||||
|
final File secure_config = new File(sitePath, "secure.config");
|
||||||
|
final File replication_config = new File(sitePath, "replication.config");
|
||||||
|
final File lib_dir = new File(sitePath, "lib");
|
||||||
|
final File logs_dir = new File(sitePath, "logs");
|
||||||
|
final File static_dir = new File(sitePath, "static");
|
||||||
|
final File cache_dir = new File(sitePath, "cache");
|
||||||
|
|
||||||
|
if (gerrit_config.exists()) {
|
||||||
|
if (!gerrit_config.exists()) {
|
||||||
|
throw die("'" + sitePath + "' is not a Gerrit server site");
|
||||||
|
}
|
||||||
|
} else if (!gerrit_config.exists()) {
|
||||||
|
ui.header("Gerrit Code Review %s", version());
|
||||||
|
if (!ui.yesno("Initialize '%s'", sitePath.getCanonicalPath())) {
|
||||||
|
throw die("aborted by user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sitePath.mkdirs()) {
|
||||||
|
throw die("Cannot make directory " + sitePath);
|
||||||
|
}
|
||||||
|
deleteOnFailure = true;
|
||||||
|
|
||||||
|
final FileBasedConfig cfg = new FileBasedConfig(gerrit_config);
|
||||||
|
final FileBasedConfig sec = new FileBasedConfig(secure_config);
|
||||||
|
init_gerrit_basepath(cfg);
|
||||||
|
init_database(cfg, sec);
|
||||||
|
init_auth(cfg, sec);
|
||||||
|
init_sendemail(cfg, sec);
|
||||||
|
init_sshd(cfg, sec);
|
||||||
|
init_httpd(cfg, sec);
|
||||||
|
|
||||||
|
cache_dir.mkdir();
|
||||||
|
set(cfg, "cache", "directory", cache_dir.getName());
|
||||||
|
|
||||||
|
cfg.save();
|
||||||
|
saveSecureConfig(sec);
|
||||||
|
|
||||||
|
if (ui != null) {
|
||||||
|
System.err.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secure_config.exists()) {
|
||||||
|
chmod600(secure_config);
|
||||||
|
}
|
||||||
|
if (!replication_config.exists()) {
|
||||||
|
replication_config.createNewFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
lib_dir.mkdir();
|
||||||
|
logs_dir.mkdir();
|
||||||
|
static_dir.mkdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initGit() throws OrmException, IOException {
|
||||||
|
final File root = repositoryManager.getBasePath();
|
||||||
|
if (root != null && importProjects) {
|
||||||
|
System.err.println("Scanning projects under " + root);
|
||||||
|
final ReviewDb db = schema.open();
|
||||||
|
try {
|
||||||
|
final HashSet<String> have = new HashSet<String>();
|
||||||
|
for (Project p : db.projects().all()) {
|
||||||
|
have.add(p.getName());
|
||||||
|
}
|
||||||
|
importProjects(root, "", db, have);
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importProjects(final File dir, final String prefix,
|
||||||
|
final ReviewDb db, final Set<String> have) throws OrmException,
|
||||||
|
IOException {
|
||||||
|
final File[] ls = dir.listFiles();
|
||||||
|
if (ls == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File f : ls) {
|
||||||
|
if (".".equals(f.getName()) || "..".equals(f.getName())) {
|
||||||
|
} else if (FileKey.isGitRepository(f)) {
|
||||||
|
String name = f.getName();
|
||||||
|
if (name.equals(".git")) {
|
||||||
|
name = prefix.substring(0, prefix.length() - 1);
|
||||||
|
} else if (name.endsWith(".git")) {
|
||||||
|
name = prefix + name.substring(0, name.length() - 4);
|
||||||
|
} else {
|
||||||
|
name = prefix + name;
|
||||||
|
System.err.println("Ignoring non-standard name '" + name + "'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (have.contains(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Project.NameKey nameKey = new Project.NameKey(name);
|
||||||
|
final Project.Id idKey = new Project.Id(db.nextProjectId());
|
||||||
|
final Project p = new Project(nameKey, idKey);
|
||||||
|
|
||||||
|
p.setDescription(repositoryManager.getProjectDescription(name));
|
||||||
|
p.setSubmitType(SubmitType.MERGE_IF_NECESSARY);
|
||||||
|
p.setUseContributorAgreements(false);
|
||||||
|
p.setUseSignedOffBy(false);
|
||||||
|
db.projects().insert(Collections.singleton(p));
|
||||||
|
|
||||||
|
} else if (f.isDirectory()) {
|
||||||
|
importProjects(f, prefix + f.getName() + "/", db, have);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveSecureConfig(final FileBasedConfig sec) throws IOException {
|
||||||
|
final byte[] out = Constants.encode(sec.toText());
|
||||||
|
final File path = sec.getFile();
|
||||||
|
final LockFile lf = new LockFile(path);
|
||||||
|
if (!lf.lock()) {
|
||||||
|
throw new IOException("Cannot lock " + path);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
chmod600(new File(path.getParentFile(), path.getName() + ".lock"));
|
||||||
|
lf.write(out);
|
||||||
|
if (!lf.commit()) {
|
||||||
|
throw new IOException("Cannot commit write to " + path);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lf.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void chmod600(final File path) throws IOException {
|
||||||
|
if (!path.exists() && !path.createNewFile()) {
|
||||||
|
throw new IOException("Cannot create " + path);
|
||||||
|
}
|
||||||
|
path.setWritable(false, false /* all */);
|
||||||
|
path.setReadable(false, false /* all */);
|
||||||
|
path.setExecutable(false, false /* all */);
|
||||||
|
|
||||||
|
path.setWritable(true, true /* owner only */);
|
||||||
|
path.setReadable(true, true /* owner only */);
|
||||||
|
if (path.isDirectory()) {
|
||||||
|
path.setExecutable(true, true /* owner only */);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init_gerrit_basepath(final Config cfg) {
|
||||||
|
ui.header("Git Repositories");
|
||||||
|
|
||||||
|
File d = new File(ui.readString("git", "Location of Git repositories"));
|
||||||
|
set(cfg, "gerrit", "basePath", d.getPath());
|
||||||
|
|
||||||
|
if (d.exists()) {
|
||||||
|
if (!importProjects && d.list() != null && d.list().length > 0) {
|
||||||
|
importProjects = ui.yesno("Import existing repositories");
|
||||||
|
}
|
||||||
|
} else if (!d.mkdirs()) {
|
||||||
|
throw die("Cannot create " + d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init_database(final Config cfg, final Config sec) {
|
||||||
|
ui.header("SQL Database");
|
||||||
|
|
||||||
|
DataSourceProvider.Type db_type = ui.readEnum(H2, "Database server type");
|
||||||
|
if (db_type == DataSourceProvider.Type.DEFAULT) {
|
||||||
|
db_type = H2;
|
||||||
|
}
|
||||||
|
set(cfg, "database", "type", db_type, null);
|
||||||
|
|
||||||
|
switch (db_type) {
|
||||||
|
case MYSQL:
|
||||||
|
createDownloader()
|
||||||
|
.setRequired(true)
|
||||||
|
.setName("MySQL Connector/J 5.1.10")
|
||||||
|
.setJarUrl(
|
||||||
|
"http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.10/mysql-connector-java-5.1.10.jar")
|
||||||
|
.setSHA1("b83574124f1a00d6f70d56ba64aa52b8e1588e6d").download();
|
||||||
|
loadSiteLib();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean userPassAuth;
|
||||||
|
switch (db_type) {
|
||||||
|
case H2:
|
||||||
|
userPassAuth = false;
|
||||||
|
new File(getSitePath(), "db").mkdirs();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JDBC: {
|
||||||
|
userPassAuth = true;
|
||||||
|
String driver = ui.readString("", "Driver class name");
|
||||||
|
String url = ui.readString("", "url");
|
||||||
|
|
||||||
|
set(cfg, "database", "driver", driver);
|
||||||
|
set(cfg, "database", "url", url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case POSTGRES:
|
||||||
|
case POSTGRESQL:
|
||||||
|
case MYSQL: {
|
||||||
|
userPassAuth = true;
|
||||||
|
String def_port = "(" + db_type.toString() + " default)";
|
||||||
|
String hostname = ui.readString("localhost", "Server hostname");
|
||||||
|
String port = ui.readString(def_port, "Server port");
|
||||||
|
String database = ui.readString("reviewdb", "Database name");
|
||||||
|
|
||||||
|
set(cfg, "database", "hostname", hostname);
|
||||||
|
set(cfg, "database", "port", port != def_port ? port : null);
|
||||||
|
set(cfg, "database", "database", database);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw die("internal bug, database " + db_type + " not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userPassAuth) {
|
||||||
|
String user = ui.readString(username(), "Database username");
|
||||||
|
String pass = user != null ? ui.password("%s's password", user) : null;
|
||||||
|
set(cfg, "database", "username", user);
|
||||||
|
set(sec, "database", "password", pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init_auth(final Config cfg, final Config sec) {
|
||||||
|
ui.header("User Authentication");
|
||||||
|
|
||||||
|
AuthType auth_type = ui.readEnum(AuthType.OPENID, "Authentication method");
|
||||||
|
set(cfg, "auth", "type", auth_type, null);
|
||||||
|
|
||||||
|
switch (auth_type) {
|
||||||
|
case HTTP:
|
||||||
|
case HTTP_LDAP: {
|
||||||
|
String def_hdr = "(HTTP Basic)";
|
||||||
|
String hdr = ui.readString(def_hdr, "Username HTTP header");
|
||||||
|
String logoutUrl = ui.readString("", "Single-sign-on logout URL");
|
||||||
|
|
||||||
|
set(cfg, "auth", "httpHeader", hdr != def_hdr ? hdr : null);
|
||||||
|
set(cfg, "auth", "logoutUrl", logoutUrl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (auth_type) {
|
||||||
|
case LDAP:
|
||||||
|
case HTTP_LDAP: {
|
||||||
|
String server = ui.readString("ldap://localhost", "LDAP server");
|
||||||
|
|
||||||
|
if (server != null && !server.startsWith("ldap://")
|
||||||
|
&& !server.startsWith("ldaps://")) {
|
||||||
|
if (ui.yesno("Use SSL")) {
|
||||||
|
server = "ldaps://" + server;
|
||||||
|
} else {
|
||||||
|
server = "ldap://" + server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String def_dn = dnOf(server);
|
||||||
|
String accountBase = ui.readString(def_dn, "Account BaseDN");
|
||||||
|
String groupBase = ui.readString(accountBase, "Group BaseDN");
|
||||||
|
|
||||||
|
String user = ui.readString(null, "LDAP username");
|
||||||
|
String pass = user != null ? ui.password("%s's password", user) : null;
|
||||||
|
|
||||||
|
set(cfg, "ldap", "server", server);
|
||||||
|
set(cfg, "ldap", "username", user);
|
||||||
|
set(sec, "ldap", "password", pass);
|
||||||
|
|
||||||
|
set(cfg, "ldap", "accountBase", accountBase);
|
||||||
|
set(cfg, "ldap", "groupBase", groupBase);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init_sendemail(final Config cfg, final Config sec) {
|
||||||
|
ui.header("Email Delivery");
|
||||||
|
|
||||||
|
String def_port = "(default)";
|
||||||
|
String smtpserver = ui.readString("localhost", "SMTP server hostname");
|
||||||
|
String port = ui.readString(def_port, "SMTP server port");
|
||||||
|
Encryption enc = ui.readEnum(Encryption.NONE, "SMTP encryption");
|
||||||
|
String username = null;
|
||||||
|
if (enc != Encryption.NONE || !isLocal(smtpserver)) {
|
||||||
|
username = username();
|
||||||
|
}
|
||||||
|
username = ui.readString(username, "SMTP username");
|
||||||
|
String password =
|
||||||
|
username != null ? ui.password("%s's password", username) : null;
|
||||||
|
|
||||||
|
set(cfg, "sendemail", "smtpServer", smtpserver);
|
||||||
|
set(cfg, "sendemail", "smtpServerPort", port != def_port ? port : null);
|
||||||
|
set(cfg, "sendemail", "smtpEncryption", enc, Encryption.NONE);
|
||||||
|
|
||||||
|
set(cfg, "sendemail", "smtpUser", username);
|
||||||
|
set(sec, "sendemail", "smtpPass", password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init_sshd(final Config cfg, final Config sec)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
ui.header("SSH Daemon");
|
||||||
|
|
||||||
|
String sshd_hostname = ui.readString("*", "Gerrit SSH listens on address");
|
||||||
|
String sshd_port = ui.readString("29418", "Gerrit SSH listens on port");
|
||||||
|
set(cfg, "sshd", "listenAddress", sshd_hostname + ":" + sshd_port);
|
||||||
|
|
||||||
|
// Download and install BouncyCastle if the user wants to use it.
|
||||||
|
//
|
||||||
|
createDownloader().setRequired(false).setName("Bouncy Castle Crypto v144")
|
||||||
|
.setJarUrl("http://www.bouncycastle.org/download/bcprov-jdk16-144.jar")
|
||||||
|
.setSHA1("6327a5f7a3dc45e0fd735adb5d08c5a74c05c20c").download();
|
||||||
|
loadSiteLib();
|
||||||
|
|
||||||
|
System.err.print("Generating SSH host key ...");
|
||||||
|
System.err.flush();
|
||||||
|
if (SecurityUtils.isBouncyCastleRegistered()) {
|
||||||
|
// Generate the SSH daemon host key using ssh-keygen.
|
||||||
|
//
|
||||||
|
final String comment = "gerrit-code-review@" + hostname();
|
||||||
|
final File rsa = new File(getSitePath(), "ssh_host_rsa_key");
|
||||||
|
final File dsa = new File(getSitePath(), "ssh_host_dsa_key");
|
||||||
|
|
||||||
|
System.err.print(" rsa...");
|
||||||
|
System.err.flush();
|
||||||
|
Runtime.getRuntime().exec(new String[] {"ssh-keygen", //
|
||||||
|
"-q" /* quiet */, //
|
||||||
|
"-t", "rsa", //
|
||||||
|
"-P", "", //
|
||||||
|
"-C", comment, //
|
||||||
|
"-f", rsa.getAbsolutePath() //
|
||||||
|
}).waitFor();
|
||||||
|
|
||||||
|
System.err.print(" dsa...");
|
||||||
|
System.err.flush();
|
||||||
|
Runtime.getRuntime().exec(new String[] {"ssh-keygen", //
|
||||||
|
"-q" /* quiet */, //
|
||||||
|
"-t", "dsa", //
|
||||||
|
"-P", "", //
|
||||||
|
"-C", comment, //
|
||||||
|
"-f", dsa.getAbsolutePath() //
|
||||||
|
}).waitFor();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Generate the SSH daemon host key ourselves. This is complex
|
||||||
|
// because SimpleGeneratorHostKeyProvider doesn't mark the data
|
||||||
|
// file as only readable by us, exposing the private key for a
|
||||||
|
// short period of time. We try to reduce that risk by creating
|
||||||
|
// the key within a temporary directory.
|
||||||
|
//
|
||||||
|
final File tmpdir = new File(getSitePath(), "tmp.sshkeygen");
|
||||||
|
if (!tmpdir.mkdir()) {
|
||||||
|
throw die("Cannot create directory " + tmpdir);
|
||||||
|
}
|
||||||
|
chmod600(tmpdir);
|
||||||
|
|
||||||
|
final String keyname = "ssh_host_key";
|
||||||
|
final File tmpkey = new File(tmpdir, keyname);
|
||||||
|
final SimpleGeneratorHostKeyProvider p;
|
||||||
|
|
||||||
|
System.err.print(" rsa(simple)...");
|
||||||
|
System.err.flush();
|
||||||
|
p = new SimpleGeneratorHostKeyProvider();
|
||||||
|
p.setPath(tmpkey.getAbsolutePath());
|
||||||
|
p.setAlgorithm("RSA");
|
||||||
|
p.loadKeys(); // forces the key to generate.
|
||||||
|
chmod600(tmpkey);
|
||||||
|
|
||||||
|
final File key = new File(getSitePath(), keyname);
|
||||||
|
if (!tmpkey.renameTo(key)) {
|
||||||
|
throw die("Cannot rename " + tmpkey + " to " + key);
|
||||||
|
}
|
||||||
|
if (!tmpdir.delete()) {
|
||||||
|
throw die("Cannot delete " + tmpdir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.err.println(" done");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init_httpd(final Config cfg, final Config sec)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
ui.header("HTTP Daemon");
|
||||||
|
|
||||||
|
final boolean reverseProxy =
|
||||||
|
ui.yesno("Behind reverse HTTP proxy (e.g. Apache mod_proxy)");
|
||||||
|
|
||||||
|
final boolean useSSL;
|
||||||
|
if (reverseProxy) {
|
||||||
|
useSSL = ui.yesno("Does the proxy server use https:// (SSL)");
|
||||||
|
} else {
|
||||||
|
useSSL = ui.yesno("Use https:// (SSL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String scheme = useSSL ? "https" : "http";
|
||||||
|
final String port_def = useSSL ? "8443" : "8080";
|
||||||
|
String httpd_hostname = ui.readString(reverseProxy ? "localhost" : "*", //
|
||||||
|
"Gerrit HTTP listens on address");
|
||||||
|
String httpd_port = ui.readString(reverseProxy ? "8081" : port_def, //
|
||||||
|
"Gerrit HTTP listens on port");
|
||||||
|
|
||||||
|
String context = "/";
|
||||||
|
if (reverseProxy) {
|
||||||
|
context = ui.readString("/", "Gerrit's subdirectory on proxy server");
|
||||||
|
if (!context.endsWith("/")) {
|
||||||
|
context += "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String httpd_url = (reverseProxy ? "proxy-" : "") //
|
||||||
|
+ scheme + "://" + httpd_hostname + ":" + httpd_port + context;
|
||||||
|
set(cfg, "httpd", "listenUrl", httpd_url);
|
||||||
|
|
||||||
|
if (useSSL && !reverseProxy
|
||||||
|
&& ui.yesno("Create self-signed SSL certificate")) {
|
||||||
|
final String certName =
|
||||||
|
ui.readString("*".equals(httpd_hostname) ? hostname()
|
||||||
|
: httpd_hostname, "Certificate server name");
|
||||||
|
final String validity =
|
||||||
|
ui.readString("365", "Certificate expires in (days)");
|
||||||
|
|
||||||
|
final String ssl_pass = SignedToken.generateRandomKey();
|
||||||
|
final String dname =
|
||||||
|
"CN=" + certName + ",OU=Gerrit Code Review,O=" + domainOf(certName);
|
||||||
|
|
||||||
|
final File tmpdir = new File(getSitePath(), "tmp.sslcertgen");
|
||||||
|
if (!tmpdir.mkdir()) {
|
||||||
|
throw die("Cannot create directory " + tmpdir);
|
||||||
|
}
|
||||||
|
chmod600(tmpdir);
|
||||||
|
|
||||||
|
final File tmpstore = new File(tmpdir, "keystore");
|
||||||
|
Runtime.getRuntime().exec(new String[] {"keytool", //
|
||||||
|
"-keystore", tmpstore.getAbsolutePath(), //
|
||||||
|
"-storepass", ssl_pass, //
|
||||||
|
"-genkeypair", //
|
||||||
|
"-alias", certName, //
|
||||||
|
"-keyalg", "RSA", //
|
||||||
|
"-validity", validity, //
|
||||||
|
"-dname", dname, //
|
||||||
|
"-keypass", ssl_pass, //
|
||||||
|
}).waitFor();
|
||||||
|
chmod600(tmpstore);
|
||||||
|
|
||||||
|
final File store = new File(getSitePath(), "keystore");
|
||||||
|
if (!tmpstore.renameTo(store)) {
|
||||||
|
throw die("Cannot rename " + tmpstore + " to " + store);
|
||||||
|
}
|
||||||
|
if (!tmpdir.delete()) {
|
||||||
|
throw die("Cannot delete " + tmpdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(sec, "httpd", "sslKeyPassword", ssl_pass);
|
||||||
|
set(cfg, "gerrit", "canonicalWebUrl", "https://" + certName + ":"
|
||||||
|
+ httpd_port + context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends Enum<?>> void set(Config cfg, String section, String name,
|
||||||
|
T value, T def) {
|
||||||
|
if (value != null && value != def) {
|
||||||
|
cfg.setString(section, null, name, value.toString());
|
||||||
|
} else {
|
||||||
|
cfg.unset(section, null, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void set(Config cfg, String section, String name, String value) {
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
cfg.setString(section, null, name, value);
|
||||||
|
} else {
|
||||||
|
cfg.unset(section, null, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inject() {
|
||||||
|
dbInjector = createDbInjector();
|
||||||
|
sysInjector = createSysInjector();
|
||||||
|
sysInjector.injectMembers(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Injector createSysInjector() {
|
||||||
|
final List<Module> modules = new ArrayList<Module>();
|
||||||
|
modules.add(new AbstractModule() {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(GitRepositoryManager.class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return dbInjector.createChildInjector(modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LibraryDownloader createDownloader() {
|
||||||
|
return new LibraryDownloader(ui, getSitePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String version() {
|
||||||
|
return com.google.gerrit.common.Version.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String username() {
|
||||||
|
return System.getProperty("user.name");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hostname() {
|
||||||
|
return SystemReader.getInstance().getHostname();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLocal(final String hostname) {
|
||||||
|
try {
|
||||||
|
return InetAddress.getByName(hostname).isLoopbackAddress();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String dnOf(String name) {
|
||||||
|
if (name != null) {
|
||||||
|
int p = name.indexOf("://");
|
||||||
|
if (0 < p) {
|
||||||
|
name = name.substring(p + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
p = name.indexOf(".");
|
||||||
|
if (0 < p) {
|
||||||
|
name = name.substring(p + 1);
|
||||||
|
name = "DC=" + name.replaceAll("\\.", ",DC=");
|
||||||
|
} else {
|
||||||
|
name = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String domainOf(String name) {
|
||||||
|
if (name != null) {
|
||||||
|
int p = name.indexOf("://");
|
||||||
|
if (0 < p) {
|
||||||
|
name = name.substring(p + 3);
|
||||||
|
}
|
||||||
|
p = name.indexOf(".");
|
||||||
|
if (0 < p) {
|
||||||
|
name = name.substring(p + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void recursiveDelete(File path) {
|
||||||
|
File[] entries = path.listFiles();
|
||||||
|
if (entries != null) {
|
||||||
|
for (File e : entries) {
|
||||||
|
recursiveDelete(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!path.delete() && path.exists()) {
|
||||||
|
System.err.println("warn: Cannot remove " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
// 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.pgm;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.util.HttpSupport;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.ProxySelector;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/** Get optional or required 3rd party library files into $site_path/lib. */
|
||||||
|
class LibraryDownloader {
|
||||||
|
private final ConsoleUI console;
|
||||||
|
private final File libDirectory;
|
||||||
|
private boolean required;
|
||||||
|
private String name;
|
||||||
|
private String jarUrl;
|
||||||
|
private String sha1;
|
||||||
|
private File dst;
|
||||||
|
|
||||||
|
LibraryDownloader(final ConsoleUI console, final File sitePath) {
|
||||||
|
this.console = console;
|
||||||
|
this.libDirectory = new File(sitePath, "lib");
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryDownloader setRequired(final boolean required) {
|
||||||
|
this.required = required;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryDownloader setName(final String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryDownloader setJarUrl(final String url) {
|
||||||
|
this.jarUrl = url;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryDownloader setSHA1(final String sha1) {
|
||||||
|
this.sha1 = sha1;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void download() {
|
||||||
|
if (jarUrl == null || !jarUrl.contains("/")) {
|
||||||
|
throw new IllegalStateException("Invalid JarUrl for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String jarName = jarUrl.substring(jarUrl.lastIndexOf('/') + 1);
|
||||||
|
if (jarName.contains("/") || jarName.contains("\\")) {
|
||||||
|
throw new IllegalStateException("Invalid JarUrl: " + jarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == null) {
|
||||||
|
name = jarName;
|
||||||
|
}
|
||||||
|
|
||||||
|
dst = new File(libDirectory, jarName);
|
||||||
|
if (!dst.exists() && shouldGet()) {
|
||||||
|
doGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldGet() {
|
||||||
|
if (console.isBatch()) {
|
||||||
|
return required;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
final StringBuilder msg = new StringBuilder();
|
||||||
|
msg.append("\n");
|
||||||
|
msg.append("Gerrit Code Review is not shipped with %s\n");
|
||||||
|
if (required) {
|
||||||
|
msg.append("** This library is required for your configuration. **\n");
|
||||||
|
} else {
|
||||||
|
msg.append(" If available, Gerrit can take advantage of features\n");
|
||||||
|
msg.append(" in the library, but will also function without it.\n");
|
||||||
|
}
|
||||||
|
msg.append("Download and install it now");
|
||||||
|
return console.yesno(msg.toString(), name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doGet() {
|
||||||
|
if (!libDirectory.exists() && !libDirectory.mkdirs()) {
|
||||||
|
throw new Die("Cannot create " + libDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
doGetByHttp();
|
||||||
|
verifyFileChecksum();
|
||||||
|
} catch (IOException err) {
|
||||||
|
dst.delete();
|
||||||
|
|
||||||
|
if (console.isBatch()) {
|
||||||
|
throw new Die("error: Cannot get " + jarUrl, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.err.println();
|
||||||
|
System.err.println();
|
||||||
|
System.err.println("error: " + err.getMessage());
|
||||||
|
System.err.println("Please download:");
|
||||||
|
System.err.println();
|
||||||
|
System.err.println(" " + jarUrl);
|
||||||
|
System.err.println();
|
||||||
|
System.err.println("and save as:");
|
||||||
|
System.err.println();
|
||||||
|
System.err.println(" " + dst.getAbsolutePath());
|
||||||
|
System.err.println();
|
||||||
|
System.err.flush();
|
||||||
|
|
||||||
|
console.waitForUser();
|
||||||
|
|
||||||
|
if (dst.exists()) {
|
||||||
|
verifyFileChecksum();
|
||||||
|
|
||||||
|
} else if (!console.yesno("Continue without this library")) {
|
||||||
|
throw new Die("aborted by user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doGetByHttp() throws IOException {
|
||||||
|
System.err.print("Downloading " + jarUrl + " ...");
|
||||||
|
System.err.flush();
|
||||||
|
try {
|
||||||
|
final ProxySelector proxySelector = ProxySelector.getDefault();
|
||||||
|
final URL url = new URL(jarUrl);
|
||||||
|
final Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
|
||||||
|
final HttpURLConnection c = (HttpURLConnection) url.openConnection(proxy);
|
||||||
|
final InputStream in;
|
||||||
|
|
||||||
|
switch (HttpSupport.response(c)) {
|
||||||
|
case HttpURLConnection.HTTP_OK:
|
||||||
|
in = c.getInputStream();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HttpURLConnection.HTTP_NOT_FOUND:
|
||||||
|
throw new FileNotFoundException(url.toString());
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new IOException(url.toString() + ": " + HttpSupport.response(c)
|
||||||
|
+ " " + c.getResponseMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final OutputStream out = new FileOutputStream(dst);
|
||||||
|
try {
|
||||||
|
final byte[] buf = new byte[8192];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(buf)) > 0) {
|
||||||
|
out.write(buf, 0, n);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
System.err.println(" OK");
|
||||||
|
System.err.flush();
|
||||||
|
} catch (IOException err) {
|
||||||
|
dst.delete();
|
||||||
|
System.err.println(" !! FAIL !!");
|
||||||
|
System.err.flush();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyFileChecksum() {
|
||||||
|
if (sha1 != null) {
|
||||||
|
try {
|
||||||
|
final MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
final FileInputStream in = new FileInputStream(dst);
|
||||||
|
try {
|
||||||
|
final byte[] buf = new byte[8192];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(buf)) > 0) {
|
||||||
|
md.update(buf, 0, n);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sha1.equals(ObjectId.fromRaw(md.digest()).name())) {
|
||||||
|
System.err.println("Checksum " + dst.getName() + " OK");
|
||||||
|
System.err.flush();
|
||||||
|
|
||||||
|
} else if (console.isBatch()) {
|
||||||
|
dst.delete();
|
||||||
|
throw new Die(dst + " SHA-1 checksum does not match");
|
||||||
|
|
||||||
|
} else if (!console.yesno("error: SHA-1 checksum does not match\n"
|
||||||
|
+ "Use %s anyway", dst.getName())) {
|
||||||
|
dst.delete();
|
||||||
|
throw new Die("aborted by user");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException checksumError) {
|
||||||
|
dst.delete();
|
||||||
|
throw new Die("cannot checksum " + dst, checksumError);
|
||||||
|
|
||||||
|
} catch (NoSuchAlgorithmException checksumError) {
|
||||||
|
dst.delete();
|
||||||
|
throw new Die("cannot checksum " + dst, checksumError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,19 +31,22 @@ import com.google.inject.name.Names;
|
|||||||
import org.kohsuke.args4j.Option;
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
public abstract class SiteProgram extends AbstractProgram {
|
public abstract class SiteProgram extends AbstractProgram {
|
||||||
private boolean siteLibLoaded;
|
|
||||||
|
|
||||||
@Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data")
|
@Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data")
|
||||||
private File sitePath = new File(".");
|
private File sitePath = new File(".");
|
||||||
|
|
||||||
@@ -58,30 +61,38 @@ public abstract class SiteProgram extends AbstractProgram {
|
|||||||
|
|
||||||
/** Load extra JARs from {@code lib/} subdirectory of {@link #getSitePath()} */
|
/** Load extra JARs from {@code lib/} subdirectory of {@link #getSitePath()} */
|
||||||
protected void loadSiteLib() {
|
protected void loadSiteLib() {
|
||||||
if (!siteLibLoaded) {
|
|
||||||
final File libdir = new File(getSitePath(), "lib");
|
final File libdir = new File(getSitePath(), "lib");
|
||||||
final File[] list = libdir.listFiles();
|
final File[] list = libdir.listFiles(new FileFilter() {
|
||||||
if (list != null) {
|
@Override
|
||||||
final List<File> toLoad = new ArrayList<File>();
|
public boolean accept(File path) {
|
||||||
for (final File u : list) {
|
if (!path.isFile()) {
|
||||||
if (u.isFile() && (u.getName().endsWith(".jar") //
|
return false;
|
||||||
|| u.getName().endsWith(".zip"))) {
|
|
||||||
toLoad.add(u);
|
|
||||||
}
|
}
|
||||||
|
return path.getName().endsWith(".jar") //
|
||||||
|
|| path.getName().endsWith(".zip");
|
||||||
}
|
}
|
||||||
addToClassLoader(toLoad);
|
});
|
||||||
|
if (list != null && 0 < list.length) {
|
||||||
|
Arrays.sort(list, new Comparator<File>() {
|
||||||
|
@Override
|
||||||
|
public int compare(File a, File b) {
|
||||||
|
return a.getName().compareTo(b.getName());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
siteLibLoaded = true;
|
addToClassLoader(list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addToClassLoader(final List<File> additionalLocations) {
|
private void addToClassLoader(final File[] additionalLocations) {
|
||||||
final ClassLoader cl = getClass().getClassLoader();
|
final ClassLoader cl = getClass().getClassLoader();
|
||||||
if (!(cl instanceof URLClassLoader)) {
|
if (!(cl instanceof URLClassLoader)) {
|
||||||
throw noAddURL("Not loaded by URLClassLoader", null);
|
throw noAddURL("Not loaded by URLClassLoader", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final URLClassLoader ucl = (URLClassLoader) cl;
|
||||||
|
final Set<URL> have = new HashSet<URL>();
|
||||||
|
have.addAll(Arrays.asList(ucl.getURLs()));
|
||||||
|
|
||||||
final Method m;
|
final Method m;
|
||||||
try {
|
try {
|
||||||
m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
|
m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
|
||||||
@@ -92,17 +103,20 @@ public abstract class SiteProgram extends AbstractProgram {
|
|||||||
throw noAddURL("Method addURL not available", e);
|
throw noAddURL("Method addURL not available", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final File u : additionalLocations) {
|
for (final File path : additionalLocations) {
|
||||||
try {
|
try {
|
||||||
m.invoke(cl, u.toURI().toURL());
|
final URL url = path.toURI().toURL();
|
||||||
|
if (have.add(url)) {
|
||||||
|
m.invoke(cl, url);
|
||||||
|
}
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw noAddURL("addURL " + u + " failed", e);
|
throw noAddURL("addURL " + path + " failed", e);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw noAddURL("addURL " + u + " failed", e);
|
throw noAddURL("addURL " + path + " failed", e);
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
throw noAddURL("addURL " + u + " failed", e);
|
throw noAddURL("addURL " + path + " failed", e);
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
throw noAddURL("addURL " + u + " failed", e.getCause());
|
throw noAddURL("addURL " + path + " failed", e.getCause());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ import org.eclipse.jgit.lib.RepositoryCache;
|
|||||||
import org.eclipse.jgit.lib.WindowCache;
|
import org.eclipse.jgit.lib.WindowCache;
|
||||||
import org.eclipse.jgit.lib.WindowCacheConfig;
|
import org.eclipse.jgit.lib.WindowCacheConfig;
|
||||||
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
|
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
|
||||||
|
import org.eclipse.jgit.util.IO;
|
||||||
|
import org.eclipse.jgit.util.RawParseUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/** Class managing Git repositories. */
|
/** Class managing Git repositories. */
|
||||||
@@ -40,6 +43,9 @@ import java.io.IOException;
|
|||||||
public class GitRepositoryManager {
|
public class GitRepositoryManager {
|
||||||
private static final Logger log = LoggerFactory.getLogger(GitRepositoryManager.class);
|
private static final Logger log = LoggerFactory.getLogger(GitRepositoryManager.class);
|
||||||
|
|
||||||
|
private static final String UNNAMED =
|
||||||
|
"Unnamed repository; edit this file to name it for gitweb.";
|
||||||
|
|
||||||
public static class Lifecycle implements LifecycleListener {
|
public static class Lifecycle implements LifecycleListener {
|
||||||
private final Config cfg;
|
private final Config cfg;
|
||||||
|
|
||||||
@@ -147,10 +153,46 @@ public class GitRepositoryManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the {@code GIT_DIR/description} file for gitweb.
|
||||||
|
* <p>
|
||||||
|
* NB: This code should really be in JGit, as a member of the Repository
|
||||||
|
* object. Until it moves there, its here.
|
||||||
|
*
|
||||||
|
* @param name the repository name, relative to the base directory.
|
||||||
|
* @return description text; null if no description has been configured.
|
||||||
|
* @throws RepositoryNotFoundException the named repository does not exist.
|
||||||
|
* @throws IOException the description file exists, but is not readable by
|
||||||
|
* this process.
|
||||||
|
*/
|
||||||
|
public String getProjectDescription(final String name)
|
||||||
|
throws RepositoryNotFoundException, IOException {
|
||||||
|
final Repository e = openRepository(name);
|
||||||
|
final File d = new File(e.getDirectory(), "description");
|
||||||
|
|
||||||
|
String description;
|
||||||
|
try {
|
||||||
|
description = RawParseUtils.decode(IO.readFully(d));
|
||||||
|
} catch (FileNotFoundException err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
description = description.trim();
|
||||||
|
if (description.isEmpty()) {
|
||||||
|
description = null;
|
||||||
|
}
|
||||||
|
if (UNNAMED.equals(description)) {
|
||||||
|
description = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the {@code GIT_DIR/description} file for gitweb.
|
* Set the {@code GIT_DIR/description} file for gitweb.
|
||||||
* <p>
|
* <p>
|
||||||
* NB: This code should really be in JGit, as a member of the Repostiory
|
* NB: This code should really be in JGit, as a member of the Repository
|
||||||
* object. Until it moves there, its here.
|
* object. Until it moves there, its here.
|
||||||
*
|
*
|
||||||
* @param name the repository name, relative to the base directory.
|
* @param name the repository name, relative to the base directory.
|
||||||
|
|||||||
Reference in New Issue
Block a user