io.github.oliviercailloux.git.factory.FactoGit Maven / Gradle / Ivy
package io.github.oliviercailloux.git.factory;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import com.google.common.base.Functions;
import com.google.common.base.Verify;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.Graphs;
import com.google.common.graph.ImmutableGraph;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import io.github.oliviercailloux.git.common.IdStamp;
import io.github.oliviercailloux.jaris.graphs.GraphUtils;
import io.github.oliviercailloux.jaris.throwing.TOptional;
import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectDatabase;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TreeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Graphs used here use the convention that the successor relation represents the child-of relation:
* the successors of a node are its children; a pair (a, b) in the graph represents a parent commit
* a and its child commit b. (See also gitjfs.)
*
* TODO should be two, the simplest one not dealing with constant dags or closing dags, and which
* also returns a mapping path ⇔ commits (is this useful?); and a wrapper one that admits constant
* dags.
*/
public class FactoGit {
@SuppressWarnings("unused")
private static final Logger LOGGER = LoggerFactory.getLogger(FactoGit.class);
@SuppressWarnings("unused")
private static ImmutableSet toLineOwn(Graph lineGraph) {
if (lineGraph.nodes().isEmpty()) {
return ImmutableSet.of();
}
final ImmutableSet starters = lineGraph.nodes().stream()
.filter(n -> lineGraph.predecessors(n).isEmpty()).collect(ImmutableSet.toImmutableSet());
if (starters.isEmpty()) {
verify(Graphs.hasCycle(lineGraph));
throw new IllegalArgumentException("The given (supposedly 'line') graph has a cycle.");
}
if (starters.size() >= 2) {
throw new IllegalArgumentException(
"The given (supposedly 'line') graph has more than one starting point"
+ " (node with no parent).");
}
final Path starter = Iterables.getOnlyElement(starters);
final ImmutableSet.Builder builder = ImmutableSet.builder();
Path current = starter;
do {
builder.add(current);
final Set nexts = lineGraph.successors(current);
if (nexts.isEmpty()) {
break;
}
if (nexts.size() >= 2) {
throw new IllegalArgumentException(
"The given (supposedly 'line') graph branches (some node has multiple children).");
}
current = Iterables.getOnlyElement(nexts);
} while (true);
final ImmutableSet line = builder.build();
if (line.size() != lineGraph.nodes().size()) {
/*
* Only one starter but we did not exhaust the graph by following it; so some other component
* of it cycles.
*/
verify(Graphs.hasCycle(lineGraph));
throw new IllegalArgumentException("The given (supposedly 'line') graph has a cycle.");
}
return line;
}
private static ImmutableSet toLine(Graph lineGraph) {
checkArgument(lineGraph.nodes().stream().filter(n -> lineGraph.inDegree(n) == 0).count() == 1L);
checkArgument(lineGraph.nodes().stream().allMatch(n -> lineGraph.inDegree(n) <= 1));
checkArgument(
lineGraph.nodes().stream().filter(n -> lineGraph.outDegree(n) == 0).count() == 1L);
checkArgument(lineGraph.nodes().stream().allMatch(n -> lineGraph.outDegree(n) <= 1));
verify(!Graphs.hasCycle(lineGraph));
return GraphUtils.topologicallySortedNodes(lineGraph);
}
private static enum ConstantDag {
BASIC, SUB, LINKED
}
private static class CloseableDagOfPaths implements Closeable {
public static CloseableDagOfPaths given(Graph dag) {
return new CloseableDagOfPaths(dag);
}
public static CloseableDagOfPaths dag(ConstantDag sort) throws IOException {
switch (sort) {
case BASIC:
return dagBasic();
case SUB:
return dagSub();
case LINKED:
return dagLinked();
default:
throw new IllegalStateException();
}
}
public static CloseableDagOfPaths dagBasic() throws IOException {
final FileSystem jimFs = Jimfs.newFileSystem(Configuration.unix());
return dagBasic(jimFs);
}
private static CloseableDagOfPaths dagBasic(FileSystem fs) throws IOException {
final Path workDir = fs.getPath("");
Files.writeString(workDir.resolve("file1.txt"), "Hello, world");
Files.writeString(workDir.resolve("file2.txt"), "Hello again");
final ImmutableGraph graph =
GraphBuilder.directed().immutable().addNode(workDir).build();
return new CloseableDagOfPaths(fs, graph);
}
public static CloseableDagOfPaths dagSub() throws IOException {
final FileSystem jimFs = Jimfs.newFileSystem(Configuration.unix());
return dagSub(jimFs);
}
private static CloseableDagOfPaths dagSub(FileSystem fs) throws IOException {
final ImmutableList.Builder builder = ImmutableList.builder();
{
final Path workDir = fs.getPath("1");
Files.createDirectories(workDir);
Files.writeString(workDir.resolve("file1.txt"), "Hello, world");
builder.add(workDir);
}
{
final Path workDir = fs.getPath("2");
Files.createDirectories(workDir);
Files.writeString(workDir.resolve("file1.txt"), "Hello, world");
Files.writeString(workDir.resolve("file2.txt"), "Hello again");
builder.add(workDir);
}
{
final Path workDir = fs.getPath("3");
Files.createDirectories(workDir);
Files.writeString(workDir.resolve("file1.txt"), "Hello, world");
Files.writeString(workDir.resolve("file2.txt"), "I insist");
final Path subDirectory = workDir.resolve("dir");
Files.createDirectory(subDirectory);
Files.writeString(subDirectory.resolve("file.txt"), "Hello from sub dir");
builder.add(workDir);
}
final ImmutableGraph graph = ImmutableGraph.copyOf(GraphUtils.asGraph(builder.build()));
return new CloseableDagOfPaths(fs, graph);
}
public static CloseableDagOfPaths dagLinked() throws IOException {
final FileSystem jimFs = Jimfs.newFileSystem(Configuration.unix());
return dagLinked(jimFs);
}
private static CloseableDagOfPaths dagLinked(FileSystem fs) throws IOException {
final ImmutableList.Builder builder = ImmutableList.builder();
{
final Path workDir = fs.getPath("1");
Files.createDirectories(workDir);
final Path file1 = workDir.resolve("file1.txt");
Files.writeString(file1, "Hello, world");
final Path link = workDir.resolve("link.txt");
Files.createSymbolicLink(link, file1);
final Path absoluteLink = workDir.resolve("absolute link");
Files.createSymbolicLink(absoluteLink, workDir.resolve("/absolute"));
verify(Files.isSymbolicLink(link));
builder.add(workDir);
}
{
final Path workDir = fs.getPath("2");
Files.createDirectories(workDir);
final Path file1 = workDir.resolve("file1.txt");
Files.writeString(file1, "Hello instead");
final Path link = workDir.resolve("link.txt");
Files.createSymbolicLink(link, file1);
final Path absoluteLink = workDir.resolve("absolute link");
Files.createSymbolicLink(absoluteLink, workDir.resolve("/absolute"));
verify(Files.isSymbolicLink(link));
builder.add(workDir);
}
{
final Path workDir = fs.getPath("3");
Files.createDirectories(workDir);
final Path file1 = workDir.resolve("file1.txt");
Files.writeString(file1, "Hello instead");
final Path link = workDir.resolve("link.txt");
Files.createSymbolicLink(link, file1);
final Path absoluteLink = workDir.resolve("absolute link");
Files.createSymbolicLink(absoluteLink, workDir.resolve("/absolute"));
verify(Files.isSymbolicLink(link));
final Path subDirectory = workDir.resolve("dir");
Files.createDirectory(subDirectory);
Files.createSymbolicLink(subDirectory.resolve("link"), fs.getPath("../link.txt"));
Files.createSymbolicLink(subDirectory.resolve("linkToParent"), fs.getPath(".."));
Files.createSymbolicLink(subDirectory.resolve("cyclingLink"),
fs.getPath("../dir/cyclingLink"));
builder.add(workDir);
}
{
final Path workDir = fs.getPath("4");
Files.createDirectories(workDir);
final Path file1 = workDir.resolve("file1.txt");
Files.writeString(file1, "Hello instead");
final Path link = workDir.resolve("link.txt");
Files.createSymbolicLink(link, file1);
final Path absoluteLink = workDir.resolve("absolute link");
Files.createSymbolicLink(absoluteLink, workDir.resolve("/absolute"));
verify(Files.isSymbolicLink(link));
final Path subDirectory = workDir.resolve("dir");
Files.createDirectory(subDirectory);
Files.createSymbolicLink(subDirectory.resolve("link"), fs.getPath("../link.txt"));
Files.createSymbolicLink(subDirectory.resolve("linkToParent"), fs.getPath(".."));
Files.createSymbolicLink(subDirectory.resolve("cyclingLink"),
fs.getPath("../dir/cyclingLink"));
Files.delete(file1);
builder.add(workDir);
}
final ImmutableGraph graph = ImmutableGraph.copyOf(GraphUtils.asGraph(builder.build()));
return new CloseableDagOfPaths(fs, graph);
}
private final TOptional fs;
private final ImmutableGraph dag;
private CloseableDagOfPaths(FileSystem fs, Graph dag) {
this.fs = TOptional.of(fs);
this.dag = ImmutableGraph.copyOf(dag);
verify(!Graphs.hasCycle(dag));
}
private CloseableDagOfPaths(Graph dag) {
this.fs = TOptional.empty();
this.dag = ImmutableGraph.copyOf(dag);
verify(!Graphs.hasCycle(dag));
}
@SuppressWarnings("OverloadMethodsDeclarationOrder")
public ImmutableGraph dag() {
return dag;
}
@Override
public void close() throws IOException {
fs.ifPresent(f -> f.close());
}
}
public static FactoGit empty() {
return new FactoGit();
}
private static Function identsFunction(ImmutableGraph ourDag,
IdStamp identStartThenIncrease) {
final Function ourIdent;
final ImmutableSet line = toLine(ourDag);
final ImmutableMap.Builder builder = ImmutableMap.builder();
IdStamp current = identStartThenIncrease;
for (Path path : line) {
builder.put(path, current);
current = new IdStamp(current.name(), current.email(),
current.timestamp().plus(1, ChronoUnit.HOURS));
}
final ImmutableMap idents = builder.build();
ourIdent = Functions.forMap(idents);
return ourIdent;
}
private static Function messagesFunction(ImmutableGraph ourDag) {
final ImmutableSet line = toLine(ourDag);
final ImmutableMap.Builder builder = ImmutableMap.builder();
int current = 1;
for (Path path : line) {
builder.put(path, "Commit number " + current);
++current;
}
final ImmutableMap idents = builder.build();
return Functions.forMap(idents);
}
private static PersonIdent personIdent(IdStamp ident) {
return new PersonIdent(ident.name(), ident.email(), ident.timestamp().toInstant(),
ident.timestamp().getZone());
}
/**
* Does not return null
. Exactly one of ident and identStartThenIncrease is null.
*/
private Function ident;
/**
* Exactly one of ident and identStartThenIncrease is null.
*/
private IdStamp identStartThenIncrease;
/**
* No cycle. At least one of dag and constantDag is {@code null}.
*/
private ImmutableGraph dag;
/**
* At least one of dag and constantDag is {@code null}.
*/
private ConstantDag constantDag;
private Path links;
private FactoGit() {
ident = p -> new IdStamp("", "", Instant.now().atZone(ZoneOffset.UTC));
dag = null;
constantDag = null;
links = null;
identStartThenIncrease = null;
}
public void setIdentConstant(IdStamp ident) {
checkNotNull(ident);
this.ident = p -> ident;
identStartThenIncrease = null;
}
public void setIdentIncreasing(IdStamp start) {
ident = null;
identStartThenIncrease = checkNotNull(start);
}
/**
* @param identF use Functions.forMap(idents) if you have a map.
*/
public void setIdentFunction(Function identF) {
this.ident = identF.andThen(i -> {
if (i == null) {
throw new IllegalArgumentException();
}
return i;
});
identStartThenIncrease = null;
}
public void setDag(Graph dag) {
checkArgument(!Graphs.hasCycle(dag));
this.dag = ImmutableGraph.copyOf(dag);
constantDag = null;
}
public void setSingletonDag(Path root) {
dag = GraphBuilder.directed().immutable().addNode(root).build();
constantDag = null;
}
public void setBasicDag() {
dag = null;
constantDag = ConstantDag.BASIC;
}
public void setSubDag() {
dag = null;
constantDag = ConstantDag.SUB;
}
public void setLinkedDag() {
dag = null;
constantDag = ConstantDag.LINKED;
}
public void setLinks(Path links) {
this.links = links;
}
public InMemoryRepository build() throws IOException {
verify(dag == null || constantDag == null);
verify(Objects.isNull(ident) != Objects.isNull(identStartThenIncrease));
checkState(dag != null || constantDag != null);
final InMemoryRepository repository = new InMemoryRepository(new DfsRepositoryDescription());
repository.create(true);
{
final ImmutableList refs = ImmutableList.copyOf(repository.getRefDatabase().getRefs());
verify(refs.size() == 0, refs.toString());
}
{
final ImmutableList refs =
ImmutableList.copyOf(repository.getRefDatabase().getAdditionalRefs());
verify(refs.size() == 0, refs.toString());
}
final ObjectDatabase objectDatabase = repository.getObjectDatabase();
try (CloseableDagOfPaths clDag = TOptional.ofNullable(constantDag)
.map(c -> CloseableDagOfPaths.dag(c)).orElseGet(() -> CloseableDagOfPaths.given(dag))) {
final ImmutableGraph ourDag = clDag.dag();
final Function ourIdent = Optional.ofNullable(ident)
.orElseGet(() -> identsFunction(ourDag, identStartThenIncrease));
final Function messagesFunction = messagesFunction(ourDag);
final ImmutableSet starters = ourDag.nodes().stream()
.filter(p -> ourDag.predecessors(p).size() == 0).collect(ImmutableSet.toImmutableSet());
LOGGER.debug("Visiting from {}.", starters);
final ImmutableSet sources = GraphUtils.topologicallySortedNodes(ourDag);
final BiMap commitsBuilder = HashBiMap.create(ourDag.nodes().size());
try (ObjectInserter inserter = objectDatabase.newInserter()) {
for (Path source : sources) {
LOGGER.debug("Visiting {}.", source);
final Set parentPaths = ourDag.predecessors(source);
final ImmutableList parents = parentPaths.stream()
.map(p -> commitsBuilder.get(p)).collect(ImmutableSet.toImmutableSet()).asList();
final ObjectId oId = insertCommit(inserter, personIdent(ourIdent.apply(source)), source,
parents, messagesFunction.apply(source));
commitsBuilder.put(source, oId);
}
}
final ImmutableBiMap commits = ImmutableBiMap.copyOf(commitsBuilder);
final ImmutableSet ends = ourDag.nodes().stream().filter(n -> ourDag.outDegree(n) == 0)
.collect(ImmutableSet.toImmutableSet());
if (ends.size() == 1) {
final Path lastVisited = sources.asList().get(sources.size() - 1);
verify(ourDag.outDegree(lastVisited) == 0);
final ObjectId lastCommit = commits.get(lastVisited);
setMainAndHead(repository, lastCommit);
LOGGER.debug("Set main at {}.", lastCommit);
} else {
LOGGER.debug("Set no main.");
}
final ImmutableSet allLinks;
if (links != null) {
allLinks = Files.find(links, 50, (p, a) -> true).filter(p -> !Files.isDirectory(p))
.collect(ImmutableSet.toImmutableSet());
} else {
allLinks = ImmutableSet.of();
}
for (Path link : allLinks) {
final Path targetPath = Files.readSymbolicLink(link);
final ObjectId targetId = commits.get(targetPath);
final Path relativeLinkName = links.relativize(link);
LOGGER.debug("Linking {} to {}.", relativeLinkName, targetPath);
final RefUpdate update =
repository.getRefDatabase().newUpdate(relativeLinkName.toString(), false);
update.setNewObjectId(targetId);
update.setExpectedOldObjectId(ObjectId.zeroId());
final Result result = update.update();
checkState(result.equals(Result.NEW));
// Git.wrap(repository).branchCreate().setName("origin/" +
// link.getFileName().toString())
// .setStartPoint(targetId.getName()).call();
}
}
return repository;
}
public static InMemoryRepository createBasicRepo() throws IOException {
final FactoGit f = FactoGit.empty();
f.setBasicDag();
return f.build();
}
public static InMemoryRepository createRepository(IdStamp ident, Graph baseDirs, Path links)
throws IOException {
final FactoGit f = FactoGit.empty();
f.setIdentConstant(ident);
f.setDag(baseDirs);
f.setLinks(links);
return f.build();
}
public static InMemoryRepository createRepository(IdStamp ident, String path, String content)
throws IOException {
try (FileSystem jimFs = Jimfs.newFileSystem(Configuration.unix())) {
final Path workDir = jimFs.getPath("");
final Path target = workDir.resolve(path);
Files.createDirectories(target.getParent());
Files.writeString(target, content);
final FactoGit f = FactoGit.empty();
f.setIdentConstant(ident);
f.setSingletonDag(target);
return f.build();
}
}
private static ObjectId insertCommit(ObjectInserter inserter, PersonIdent personIdent,
Path directory, List parents, String commitMessage) throws IOException {
final ObjectId treeId = insertTree(inserter, directory);
return insertCommit(inserter, personIdent, treeId, parents, commitMessage);
}
private static ObjectId insertCommit(ObjectInserter inserter, PersonIdent personIdent,
ObjectId treeId, List parents, String commitMessage) throws IOException {
final CommitBuilder commitBuilder = new CommitBuilder();
commitBuilder.setMessage(commitMessage);
commitBuilder.setAuthor(personIdent);
commitBuilder.setCommitter(personIdent);
commitBuilder.setTreeId(treeId);
for (ObjectId parent : parents) {
commitBuilder.addParentId(parent);
}
final ObjectId commitId = inserter.insert(commitBuilder);
inserter.flush();
LOGGER.debug("Created commit: {}.", commitId);
return commitId;
}
/**
* Inserts a tree containing the content of the given directory.
*
* Does not flush the inserter.
*/
private static ObjectId insertTree(ObjectInserter inserter, Path directory) throws IOException {
checkArgument(Files.isDirectory(directory));
/*
* TODO TreeFormatter says that the entries must come in the right order; what’s that?
*/
final TreeFormatter treeFormatter = new TreeFormatter();
/* See TreeFormatter: “This formatter does not process subtrees”. */
try (Stream content = Files.list(directory);) {
for (Path relEntry : (Iterable) content::iterator) {
final String entryName = relEntry.getFileName().toString();
/* Work around Jimfs bug, see https://github.com/google/jimfs/issues/105 . */
final Path entry = relEntry.toAbsolutePath();
if (Files.isRegularFile(entry, LinkOption.NOFOLLOW_LINKS)) {
LOGGER.debug("Creating regular: {}.", entry);
final String fileContent = Files.readString(entry);
final ObjectId fileOid =
inserter.insert(Constants.OBJ_BLOB, fileContent.getBytes(StandardCharsets.UTF_8));
treeFormatter.append(entryName, FileMode.REGULAR_FILE, fileOid);
} else if (Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS)) {
final ObjectId tree = insertTree(inserter, entry);
treeFormatter.append(entryName, FileMode.TREE, tree);
} else if (Files.isSymbolicLink(entry)) {
LOGGER.debug("Creating link: {}.", entry);
final String destSlashSeparated;
{
final Path dest = Files.readSymbolicLink(entry);
final String separator = dest.getFileSystem().getSeparator();
if (dest.getFileSystem().provider().getScheme().equals("file")
&& separator.equals("\\")) {
destSlashSeparated = dest.toString().replace("\\", "/");
} else {
checkArgument(separator.equals("/"));
destSlashSeparated = dest.toString();
}
}
final byte[] destAsBytes = destSlashSeparated.getBytes(StandardCharsets.UTF_8);
final ObjectId fileObjId = inserter.insert(Constants.OBJ_BLOB, destAsBytes);
treeFormatter.append(entryName, FileMode.SYMLINK, fileObjId);
} else {
throw new IllegalArgumentException("Unknown entry: " + entry);
}
}
}
final ObjectId inserted = inserter.insert(treeFormatter);
return inserted;
}
private static void setMainAndHead(Repository repository, ObjectId newId) throws IOException {
{
final RefUpdate updateRef = repository.updateRef("refs/heads/main");
updateRef.setNewObjectId(newId);
final Result updateResult = updateRef.update();
Verify.verify(updateResult == Result.NEW, updateResult.toString());
}
{
final ImmutableList refs = ImmutableList.copyOf(repository.getRefDatabase().getRefs());
verify(refs.size() == 1, refs.toString());
}
{
final RefUpdate updateRef = repository.updateRef(Constants.HEAD);
final Result updateResult = updateRef.link("refs/heads/main");
verify(updateResult == Result.FORCED, updateResult.toString());
{
final ImmutableList refs = ImmutableList.copyOf(repository.getRefDatabase().getRefs());
verify(refs.size() == 2, refs.toString());
}
}
}
}