/* Copyright (c) 2012-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.api.porcelain;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Platform;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RevTree;
import org.locationtech.geogig.api.hooks.Hookables;
import org.locationtech.geogig.api.plumbing.RefParse;
import org.locationtech.geogig.api.plumbing.ResolveGeogigDir;
import org.locationtech.geogig.api.plumbing.UpdateRef;
import org.locationtech.geogig.api.plumbing.UpdateSymRef;
import org.locationtech.geogig.di.CanRunDuringConflict;
import org.locationtech.geogig.di.PluginDefaults;
import org.locationtech.geogig.di.VersionedFormat;
import org.locationtech.geogig.repository.Repository;
import org.locationtech.geogig.repository.RepositoryConnectionException;
import org.locationtech.geogig.storage.ConfigDatabase;
import org.locationtech.geogig.storage.ObjectDatabase;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.google.inject.Inject;
/**
* Creates or "initializes" a repository in the {@link Platform#pwd() working directory}.
* <p>
* This command tries to find an existing {@code .geogig} repository directory in the current
* directory's hierarchy. It is safe to call it from inside a directory that's a child of a
* repository.
* <p>
* If no repository directory is found, then a new one is created on the current directory.
*
* @see ResolveGeogigDir
* @see RefParse
* @see UpdateRef
* @see UpdateSymRef
*/
@CanRunDuringConflict
public class InitOp extends AbstractGeoGigOp<Repository> {
private Map<String, String> config;
private PluginDefaults defaults;
private String filterFile;
@Nullable
private File targetDir;
/**
* Constructs a new {@code InitOp} with the specified parameters.
*
* @param platform where to get the current directory from
* @param context where to get the repository from (with auto-wired dependencies) once ensured
* the {@code .geogig} repository directory is found or created.
*/
@Inject
public InitOp(PluginDefaults defaults) {
checkNotNull(defaults);
this.defaults = defaults;
this.config = Maps.newTreeMap();
}
public InitOp setConfig(Map<String, String> suppliedConfiguration) {
this.config = ImmutableMap.copyOf(suppliedConfiguration);
return this;
}
public InitOp setTarget(File targetRepoDirectory) {
this.targetDir = targetRepoDirectory;
return this;
}
public InitOp setFilterFile(String filterFile) {
this.filterFile = filterFile;
return this;
}
/**
* Executes the Init operation.
*
* @return the initialized repository
* @throws IllegalStateException if a repository cannot be created on the current directory or
* re-initialized in the current dir or one if its parents as determined by
* {@link ResolveGeogigDir}
*/
@Override
protected Repository _call() {
final Platform platform = platform();
final File workingDirectory = platform.pwd();
checkState(workingDirectory != null, "working directory is null");
final File targetDir = this.targetDir == null ? workingDirectory : this.targetDir;
if (!targetDir.exists() && !targetDir.mkdirs()) {
throw new IllegalArgumentException("Can't create directory "
+ targetDir.getAbsolutePath());
}
Repository repository;
try {
platform.setWorkingDir(targetDir);
repository = callInternal();
} finally {
// restore current directory
platform.setWorkingDir(workingDirectory);
}
return repository;
}
private Repository callInternal() {
final Platform platform = platform();
final File workingDirectory = platform.pwd();
final Optional<URL> repoUrl = new ResolveGeogigDir(platform).call();
final boolean repoExisted = repoUrl.isPresent();
final File envHome;
if (repoExisted) {
// we're at either the repo working dir or a subdirectory of it
try {
envHome = new File(repoUrl.get().toURI());
} catch (URISyntaxException e) {
throw Throwables.propagate(e);
}
} else {
envHome = new File(workingDirectory, ".geogig");
if (!envHome.mkdirs()) {
throw new RuntimeException("Unable to create geogig environment at '"
+ envHome.getAbsolutePath() + "'");
}
}
Map<String, String> effectiveConfigBuilder = Maps.newTreeMap();
addDefaults(defaults, effectiveConfigBuilder);
if (config != null) {
effectiveConfigBuilder.putAll(config);
}
if (filterFile != null) {
try {
final String FILTER_FILE = "filter.ini";
File oldFilterFile = new File(filterFile);
if (!oldFilterFile.exists()) {
throw new FileNotFoundException("No filter file found at " + filterFile + ".");
}
Optional<URL> envHomeURL = new ResolveGeogigDir(platform).call();
Preconditions.checkState(envHomeURL.isPresent(), "Not inside a geogig directory");
final URL url = envHomeURL.get();
if (!"file".equals(url.getProtocol())) {
throw new UnsupportedOperationException(
"Sparse clone works only against file system repositories. "
+ "Repository location: " + url.toExternalForm());
}
File repoDir;
try {
repoDir = new File(url.toURI());
} catch (URISyntaxException e) {
throw new IllegalStateException("Unable to access directory "
+ url.toExternalForm(), e);
}
File newFilterFile = new File(repoDir, FILTER_FILE);
Files.copy(oldFilterFile, newFilterFile);
effectiveConfigBuilder.put("sparse.filter", FILTER_FILE);
} catch (Exception e) {
throw new IllegalStateException("Unable to copy filter file at path " + filterFile
+ " to the new repository.", e);
}
}
try {
Preconditions.checkState(envHome.toURI().toURL()
.equals(new ResolveGeogigDir(platform).call().get()));
} catch (MalformedURLException e) {
Throwables.propagate(e);
}
Repository repository;
try {
if (!repoExisted) {
ConfigDatabase configDB = context.configDatabase();
try {
for (Entry<String, String> pair : effectiveConfigBuilder.entrySet()) {
String key = pair.getKey();
String value = pair.getValue();
configDB.put(key, value);
}
repository = repository();
repository.configure();
} catch (RepositoryConnectionException e) {
throw new IllegalStateException(
"Unable to initialize repository for the first time: " + e.getMessage(),
e);
}
} else {
repository = repository();
}
try {
repository.open();
// make sure the repo has the empty tree
ObjectDatabase objectDatabase = repository.objectDatabase();
objectDatabase.put(RevTree.EMPTY);
} catch (RepositoryConnectionException e) {
throw new IllegalStateException("Error opening repository databases: "
+ e.getMessage(), e);
}
createSampleHooks(envHome);
} catch (ConfigException e) {
throw e;
} catch (RuntimeException e) {
Throwables.propagateIfInstanceOf(e, IllegalStateException.class);
throw new IllegalStateException("Can't access repository at '"
+ envHome.getAbsolutePath() + "'", e);
}
if (!repoExisted) {
try {
createDefaultRefs();
} catch (IllegalStateException e) {
Throwables.propagate(e);
}
}
return repository;
}
private void createSampleHooks(File envHome) {
File hooks = new File(envHome, "hooks");
hooks.mkdirs();
if (!hooks.exists()) {
throw new RuntimeException();
}
try {
copyHookFile(hooks.getAbsolutePath(), "pre_commit.js.sample");
// TODO: add other example hooks
} catch (IOException e) {
throw new RuntimeException();
}
}
private void copyHookFile(String folder, String file) throws IOException {
URL url = Resources.getResource(Hookables.class, file);
OutputStream os = new FileOutputStream(new File(folder, file).getAbsolutePath());
Resources.copy(url, os);
os.close();
}
private void addDefaults(PluginDefaults defaults, Map<String, String> configProps) {
Optional<VersionedFormat> refs = defaults.getRefs();
Optional<VersionedFormat> objects = defaults.getObjects();
Optional<VersionedFormat> staging = defaults.getStaging();
Optional<VersionedFormat> graph = defaults.getGraph();
if (refs.isPresent()) {
configProps.put("storage.refs", refs.get().getFormat());
configProps.put(refs.get().getFormat() + ".version", refs.get().getVersion());
}
if (objects.isPresent()) {
configProps.put("storage.objects", objects.get().getFormat());
configProps.put(objects.get().getFormat() + ".version", objects.get().getVersion());
}
if (staging.isPresent()) {
configProps.put("storage.staging", staging.get().getFormat());
configProps.put(staging.get().getFormat() + ".version", staging.get().getVersion());
}
if (graph.isPresent()) {
configProps.put("storage.graph", graph.get().getFormat());
configProps.put(graph.get().getFormat() + ".version", graph.get().getVersion());
}
}
private void createDefaultRefs() {
Optional<Ref> master = command(RefParse.class).setName(Ref.MASTER).call();
Preconditions.checkState(!master.isPresent(), Ref.MASTER + " was already initialized.");
command(UpdateRef.class).setName(Ref.MASTER).setNewValue(ObjectId.NULL)
.setReason("Repository initialization").call();
Optional<Ref> head = command(RefParse.class).setName(Ref.HEAD).call();
Preconditions.checkState(!head.isPresent(), Ref.HEAD + " was already initialized.");
command(UpdateSymRef.class).setName(Ref.HEAD).setNewValue(Ref.MASTER)
.setReason("Repository initialization").call();
Optional<Ref> workhead = command(RefParse.class).setName(Ref.WORK_HEAD).call();
Preconditions
.checkState(!workhead.isPresent(), Ref.WORK_HEAD + " was already initialized.");
command(UpdateRef.class).setName(Ref.WORK_HEAD).setNewValue(RevTree.EMPTY.getId())
.setReason("Repository initialization").call();
Optional<Ref> stagehead = command(RefParse.class).setName(Ref.STAGE_HEAD).call();
Preconditions.checkState(!stagehead.isPresent(), Ref.STAGE_HEAD
+ " was already initialized.");
command(UpdateRef.class).setName(Ref.STAGE_HEAD).setNewValue(RevTree.EMPTY.getId())
.setReason("Repository initialization").call();
}
}