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>
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 00246bb..069ec59 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -9,8 +9,8 @@
[[programs]]Programs
--------------------
-CreateSchema::
- Initialize a new database schema.
+link:pgm-init.html[init]::
+ Initialize a new Gerrit server installation
link:pgm-daemon.html[daemon]::
Gerrit HTTP, SSH network server.
@@ -18,6 +18,9 @@
version::
Display the release version of Gerrit Code Review.
+CreateSchema::
+ Initialize a new database schema.
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
new file mode 100644
index 0000000..b1e2ec8
--- /dev/null
+++ b/Documentation/pgm-init.txt
@@ -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]
diff --git a/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java b/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java
index c86bd08..c55c1e5 100644
--- a/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java
+++ b/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java
@@ -54,6 +54,7 @@
System.err.println("usage: java -jar " + jar + " command [ARG ...]");
System.err.println();
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(" version Display the build version number");
System.err.println();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/AbstractProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/AbstractProgram.java
index 4a304a9..ca0bc29 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/AbstractProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/AbstractProgram.java
@@ -72,7 +72,12 @@
ProxyUtil.configureHttpProxy();
return run();
} 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;
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ConsoleUI.java
new file mode 100644
index 0000000..212f00b
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ConsoleUI.java
@@ -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) {
+ }
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ErrorLogFile.java
index 6d2802b..4c19a47 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ErrorLogFile.java
@@ -17,6 +17,7 @@
import com.google.gerrit.lifecycle.LifecycleListener;
import org.apache.log4j.Appender;
+import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.DailyRollingFileAppender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
@@ -29,6 +30,22 @@
import java.io.File;
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) {
final File logdir = new File(sitePath, "logs");
if (!logdir.exists() && !logdir.mkdirs()) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
new file mode 100644
index 0000000..7716654
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -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);
+ }
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LibraryDownloader.java
new file mode 100644
index 0000000..d72c948
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LibraryDownloader.java
@@ -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);
+ }
+ }
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SiteProgram.java
index 7124a6a..8387eeb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SiteProgram.java
@@ -31,19 +31,22 @@
import org.kohsuke.args4j.Option;
import java.io.File;
+import java.io.FileFilter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import javax.sql.DataSource;
public abstract class SiteProgram extends AbstractProgram {
- private boolean siteLibLoaded;
-
@Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data")
private File sitePath = new File(".");
@@ -58,30 +61,38 @@
/** Load extra JARs from {@code lib/} subdirectory of {@link #getSitePath()} */
protected void loadSiteLib() {
- if (!siteLibLoaded) {
- final File libdir = new File(getSitePath(), "lib");
- final File[] list = libdir.listFiles();
- if (list != null) {
- final List<File> toLoad = new ArrayList<File>();
- for (final File u : list) {
- if (u.isFile() && (u.getName().endsWith(".jar") //
- || u.getName().endsWith(".zip"))) {
- toLoad.add(u);
- }
+ final File libdir = new File(getSitePath(), "lib");
+ final File[] list = libdir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File path) {
+ if (!path.isFile()) {
+ return false;
}
- addToClassLoader(toLoad);
+ return path.getName().endsWith(".jar") //
+ || path.getName().endsWith(".zip");
}
-
- siteLibLoaded = true;
+ });
+ 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());
+ }
+ });
+ addToClassLoader(list);
}
}
- private void addToClassLoader(final List<File> additionalLocations) {
+ private void addToClassLoader(final File[] additionalLocations) {
final ClassLoader cl = getClass().getClassLoader();
if (!(cl instanceof URLClassLoader)) {
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;
try {
m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
@@ -92,17 +103,20 @@
throw noAddURL("Method addURL not available", e);
}
- for (final File u : additionalLocations) {
+ for (final File path : additionalLocations) {
try {
- m.invoke(cl, u.toURI().toURL());
+ final URL url = path.toURI().toURL();
+ if (have.add(url)) {
+ m.invoke(cl, url);
+ }
} catch (MalformedURLException e) {
- throw noAddURL("addURL " + u + " failed", e);
+ throw noAddURL("addURL " + path + " failed", e);
} catch (IllegalArgumentException e) {
- throw noAddURL("addURL " + u + " failed", e);
+ throw noAddURL("addURL " + path + " failed", e);
} catch (IllegalAccessException e) {
- throw noAddURL("addURL " + u + " failed", e);
+ throw noAddURL("addURL " + path + " failed", e);
} catch (InvocationTargetException e) {
- throw noAddURL("addURL " + u + " failed", e.getCause());
+ throw noAddURL("addURL " + path + " failed", e.getCause());
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index 33a1f58..2d48568 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -29,10 +29,13 @@
import org.eclipse.jgit.lib.WindowCache;
import org.eclipse.jgit.lib.WindowCacheConfig;
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.LoggerFactory;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
/** Class managing Git repositories. */
@@ -40,6 +43,9 @@
public class GitRepositoryManager {
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 {
private final Config cfg;
@@ -148,9 +154,45 @@
}
/**
+ * 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.
* <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.
*
* @param name the repository name, relative to the base directory.