diff --git a/bots/topological/build.gradle b/bots/topological/build.gradle new file mode 100644 index 000000000..acb19c00f --- /dev/null +++ b/bots/topological/build.gradle @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +module { + name = 'org.openjdk.skara.bots.topological' + test { + requires 'org.junit.jupiter.api' + requires 'org.openjdk.skara.test' + opens 'org.openjdk.skara.bots.topological' to 'org.junit.platform.commons' + } +} + +dependencies { + implementation project(':host') + implementation project(':bot') + implementation project(':census') + implementation project(':json') + implementation project(':vcs') + + testImplementation project(':test') +} diff --git a/bots/topological/src/main/java/module-info.java b/bots/topological/src/main/java/module-info.java new file mode 100644 index 000000000..ae3f7a992 --- /dev/null +++ b/bots/topological/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +module org.openjdk.skara.bots.topological { + requires org.openjdk.skara.bot; + requires org.openjdk.skara.vcs; + requires java.logging; + + provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.topological.TopologicalBotFactory; +} diff --git a/bots/topological/src/main/java/org/openjdk/skara/bots/topological/Edge.java b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/Edge.java new file mode 100644 index 000000000..720e80640 --- /dev/null +++ b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/Edge.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.topological; + +import org.openjdk.skara.vcs.Branch; + +import java.util.Objects; + +class Edge { + final Branch from; + final Branch to; + + Edge(Branch from, Branch to) { + this.from = from; + this.to = to; + } + + @Override + public String toString() { + return "Edge{" + + "from='" + from + '\'' + + ", to='" + to + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Edge edge = (Edge) o; + return Objects.equals(from, edge.from) && + Objects.equals(to, edge.to); + } + + @Override + public int hashCode() { + return Objects.hash(from, to); + } +} diff --git a/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBot.java b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBot.java new file mode 100644 index 000000000..520352ce7 --- /dev/null +++ b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBot.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.topological; + +import org.openjdk.skara.bot.*; +import org.openjdk.skara.host.*; +import org.openjdk.skara.vcs.*; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Files; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Bot that automatically merges any changes from a dependency branch into a target branch + */ +class TopologicalBot implements Bot, WorkItem { + private final Logger log = Logger.getLogger("org.openjdk.skara.bots"); + private final Path storage; + private final HostedRepository hostedRepo; + private final List branches; + private final String depsFileName; + + TopologicalBot(Path storage, HostedRepository repo, List branches, String depsFileName) { + this.storage = storage; + this.hostedRepo = repo; + this.branches = branches; + this.depsFileName = depsFileName; + } + + @Override + public boolean concurrentWith(WorkItem other) { + if (!(other instanceof TopologicalBot)) { + return true; + } + var otherBot = (TopologicalBot) other; + return !hostedRepo.getName().equals(otherBot.hostedRepo.getName()); + } + + @Override + public void run(Path scratchPath) { + log.info("Starting topobot run"); + try { + var sanitizedUrl = URLEncoder.encode(hostedRepo.getWebUrl().toString(), StandardCharsets.UTF_8); + var dir = storage.resolve(sanitizedUrl); + Repository repo; + if (!Files.exists(dir)) { + log.info("Cloning " + hostedRepo.getName()); + Files.createDirectories(dir); + repo = Repository.clone(hostedRepo.getUrl(), dir); + } else { + log.info("Found existing scratch directory for " + hostedRepo.getName()); + repo = Repository.get(dir) + .orElseThrow(() -> new RuntimeException("Repository in " + dir + " has vanished")); + } + + repo.fetchAll(); + var depsFile = repo.root().resolve(depsFileName); + + var orderedBranches = orderedBranches(repo, depsFile); + log.info("Merge order " + orderedBranches); + for (var branch : orderedBranches) { + log.info("Processing branch " + branch + "..."); + repo.checkout(branch); + var parents = dependencies(repo, repo.head(), depsFile).collect(Collectors.toSet()); + List failedMerges = new ArrayList<>(); + boolean progress; + boolean failed; + do { + // We need to attempt merge parents in any order that works. Keep merging + // and pushing, until no further progress can be made. + progress = false; + failed = false; + for (var parentsIt = parents.iterator(); parentsIt.hasNext();) { + var parent = parentsIt.next(); + try { + mergeIfAhead(repo, branch, parent); + progress = true; + parentsIt.remove(); // avoid doing pointless merges + } catch(IOException e) { + log.severe("Merge with " + parent + " failed. Reverting..."); + repo.abortMerge(); + failedMerges.add(branch + " <- " + parent); + failed = true; + } + } + } while(progress && failed); + + if (!failedMerges.isEmpty()) { + throw new IOException("There were failed merges:\n" + failedMerges); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + log.info("Ending topobot run"); + } + + private static Stream dependencies(Repository repo, Hash hash, Path depsFile) throws IOException { + return repo.lines(depsFile, hash).map(l -> { + var lines = l.stream().filter(s -> !s.isEmpty()).collect(Collectors.toList()); + if (lines.size() > 1) { + throw new IllegalStateException("Multiple non-empty lines in " + depsFile.toString() + ": " + + String.join("\n", lines)); + } + return Stream.of(lines.get(0).split(" ")).map(Branch::new); + }) + .orElse(Stream.of(repo.defaultBranch())); + } + + private List orderedBranches(Repository repo, Path depsFile) throws IOException { + List deps = new ArrayList<>(); + for (var branch : branches) { + dependencies(repo, repo.resolve("origin/" + branch.name()).orElseThrow(), depsFile) + .forEach(dep -> deps.add(new Edge(dep, branch))); + } + var defaultBranch = repo.defaultBranch(); + return TopologicalSort.sort(deps).stream() + .filter(branch -> !branch.equals(defaultBranch)) + .collect(Collectors.toList()); + } + + private void mergeIfAhead(Repository repo, Branch branch, Branch parent) throws IOException { + var fromHash = repo.resolve(parent.name()).orElseThrow(); + var oldHead = repo.head(); + if (!repo.contains(branch, fromHash)) { + var isFastForward = repo.isAncestor(oldHead, fromHash); + repo.merge(fromHash); + if (!isFastForward) { + log.info("Merged " + parent + " into " + branch); + repo.commit("Automatic merge with " + parent, "duke", "duke@openjdk.org"); + } else { + log.info("Fast forwarded " + branch + " to " + parent); + } + try (var commits = repo.commits("origin/" + branch.name() + ".." + branch.name()).stream()) { + log.info("merge with " + parent + " succeeded. The following commits will be pushed:\n" + + commits + .map(Commit::toString) + .collect(Collectors.joining("\n", "\n", "\n"))); + } + try { + repo.push(repo.head(), hostedRepo.getUrl(), branch.name()); + } catch (IOException e) { + log.severe("Pushing failed! Aborting..."); + repo.reset(oldHead, true); + throw e; + } + } + } + + @Override + public String toString() { + return "TopoBot@(" + hostedRepo + ")"; + } + + @Override + public List getPeriodicItems() { + return List.of(this); + } +} diff --git a/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBotFactory.java b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBotFactory.java new file mode 100644 index 000000000..eec99cdb8 --- /dev/null +++ b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBotFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.topological; + +import org.openjdk.skara.bot.*; +import org.openjdk.skara.json.JSONValue; +import org.openjdk.skara.vcs.Branch; + +import java.io.*; +import java.nio.file.Files; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class TopologicalBotFactory implements BotFactory { + private final Logger log = Logger.getLogger("org.openjdk.skara.bots"); + + @Override + public String name() { + return "topological"; + } + + @Override + public List create(BotConfiguration configuration) { + var storage = configuration.storageFolder(); + try { + Files.createDirectories(storage); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + var specific = configuration.specific(); + + var repoName = specific.get("repo").asString(); + var repo = configuration.repository(repoName); + + var branches = specific.get("branches").asArray().stream() + .map(JSONValue::asString) + .map(Branch::new) + .collect(Collectors.toList()); + + var depsFile = specific.get("depsFile").asString(); + + log.info("Setting up topological merging in: " + repoName); + return List.of(new TopologicalBot(storage, repo, branches, depsFile)); + } +} diff --git a/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalSort.java b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalSort.java new file mode 100644 index 000000000..63ea6f59c --- /dev/null +++ b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalSort.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.topological; + +import org.openjdk.skara.vcs.Branch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +class TopologicalSort { + static List sort(List edges) { + List eCopy = new ArrayList<>(edges); + List result = new ArrayList<>(); + while (!eCopy.isEmpty()) { + Set orphans = eCopy.stream() + .map(e -> e.from) + .filter(f -> eCopy.stream().map(e -> e.to).noneMatch(f::equals)) + .collect(Collectors.toSet()); + if (orphans.isEmpty()) { + throw new IllegalStateException("Detected a cycle! " + edges); + } + orphans.forEach(o -> { + result.add(o); + eCopy.removeIf(e -> o.equals(e.from)); + }); + } + + // add all leaves + edges.stream() + .map(e -> e.to) + .filter(f -> edges.stream().map(e -> e.from).noneMatch(f::equals)) + .forEach(result::add); + + return result; + } +} diff --git a/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalBotTests.java b/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalBotTests.java new file mode 100644 index 000000000..53170cc89 --- /dev/null +++ b/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalBotTests.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.topological; + +import org.openjdk.skara.host.*; +import org.openjdk.skara.test.*; +import org.openjdk.skara.vcs.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.stream.Collectors; + +import static java.nio.file.StandardOpenOption.APPEND; +import static org.junit.jupiter.api.Assertions.*; + +class TopologicalBotTests { + + @Test + void testTopoMerge() throws IOException { + try (var temp = new TemporaryDirectory()) { + var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke"))); + + var fromDir = temp.path().resolve("from.git"); + var repo = Repository.init(fromDir, VCS.GIT); + var gitConfig = repo.root().resolve(".git").resolve("config"); + Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var hostedRepo = new TestHostedRepository(host, "test", repo); + + // make non bare + var readme = fromDir.resolve("README.txt"); + Files.writeString(readme, "Hello world\n"); + repo.add(readme); + repo.commit("An initial commit", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + var aBranch = repo.branch(repo.head(), "A"); + // no deps -> depends on master + + var depsFileName = "deps.txt"; + + var bBranch = repo.branch(repo.head(), "B"); + repo.checkout(bBranch); + var bDeps = fromDir.resolve(depsFileName); + Files.writeString(bDeps, "A"); + repo.add(bDeps); + repo.commit("Adding deps file to B", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + var cBranch = repo.branch(repo.head(), "C"); + repo.checkout(cBranch); + var cDeps = fromDir.resolve(depsFileName); + Files.writeString(cDeps, "B A"); + repo.add(cDeps); + repo.commit("Adding deps file to C", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + repo.checkout(new Branch("master")); + var newFile = fromDir.resolve("NewFile.txt"); + Files.writeString(newFile, "Hello world\n"); + repo.add(newFile); + var preHash = repo.commit("An additional commit", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + var preCommits = repo.commits().asList(); + assertEquals(4, preCommits.size()); + assertEquals(preHash, repo.head()); + + var branches = List.of("C", "A", "B").stream().map(Branch::new).collect(Collectors.toList()); + var storage = temp.path().resolve("storage"); + var bot = new TopologicalBot(storage, hostedRepo, branches, depsFileName); + TestBotRunner.runPeriodicItems(bot); + + var postCommits = repo.commits().asList(); + assertEquals(7, postCommits.size()); + + repo.checkout(aBranch); + assertEquals(preHash, repo.head()); + + repo.checkout(bBranch); + assertNotEquals(preHash, repo.head()); // merge commit + + repo.checkout(cBranch); + assertNotEquals(preHash, repo.head()); // merge commit + } + } + + @Test + void testTopoMergeFailure() throws IOException { + try (var temp = new TemporaryDirectory()) { + var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke"))); + + var fromDir = temp.path().resolve("from.git"); + var repo = Repository.init(fromDir, VCS.GIT); + var gitConfig = repo.root().resolve(".git").resolve("config"); + Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), APPEND); + var hostedRepo = new TestHostedRepository(host, "test", repo); + + // make non bare + var readme = fromDir.resolve("README.txt"); + Files.writeString(readme, "Hello world\n"); + repo.add(readme); + repo.commit("An initial commit", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + var aBranch = repo.branch(repo.head(), "A"); + repo.checkout(aBranch); + Files.writeString(readme, "A conflicting line\n", APPEND); + repo.add(readme); + var aStartHash = repo.commit("A conflicting commit", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + var depsFileName = "deps.txt"; + + var bBranch = repo.branch(repo.head(), "B"); + repo.checkout(bBranch); + var bDeps = fromDir.resolve(depsFileName); + Files.writeString(bDeps, "A"); + repo.add(bDeps); + var bDepsHash = repo.commit("Adding deps file to B", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + var cBranch = repo.branch(repo.head(), "C"); + repo.checkout(cBranch); + var cDeps = fromDir.resolve(depsFileName); + Files.writeString(cDeps, "B"); + repo.add(cDeps); + var cDepsHash = repo.commit("Adding deps file to C", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + repo.checkout(new Branch("master")); + Files.writeString(readme, "Goodbye world!\n", APPEND); + repo.add(readme); + var preHash = repo.commit("An additional commit", "duke", "duke@openjdk.org"); + repo.pushAll(hostedRepo.getUrl()); + + var preCommits = repo.commits().asList(); + assertEquals(5, preCommits.size()); + assertEquals(preHash, repo.head()); + + var branches = List.of("C", "A", "B").stream().map(Branch::new).collect(Collectors.toList()); + var storage = temp.path().resolve("storage"); + var bot = new TopologicalBot(storage, hostedRepo, branches, depsFileName); + assertThrows(UncheckedIOException.class, () -> TestBotRunner.runPeriodicItems(bot)); + + var postCommits = repo.commits().asList(); + assertEquals(5, postCommits.size()); + + repo.checkout(aBranch); + assertEquals(aStartHash, repo.head()); + + repo.checkout(bBranch); + assertEquals(bDepsHash, repo.head()); + + repo.checkout(cBranch); + assertEquals(cDepsHash, repo.head()); + } + } +} diff --git a/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalSortTest.java b/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalSortTest.java new file mode 100644 index 000000000..11a79f2df --- /dev/null +++ b/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalSortTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.topological; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.openjdk.skara.vcs.Branch; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TopologicalSortTest { + + private static Edge edge(String from, String to) { + return new Edge(new Branch(from), new Branch(to)); + } + + private static List brancheList(String... names) { + return Arrays.stream(names).map(Branch::new).collect(Collectors.toList()); + } + + @Test + void testEmpty() { + var branches = TopologicalSort.sort(List.of()); + assertEquals(brancheList(), branches); + } + + @Test + void testTrivial() { + var branches = TopologicalSort.sort(List.of(edge("A", "B"))); + assertEquals(brancheList("A", "B"), branches); + } + + @Test() + void testCycleTrivial() { + assertThrows(IllegalStateException.class, () -> TopologicalSort.sort(List.of(edge("A", "A")))); + } + + @Test() + void testCycle() { + assertThrows(IllegalStateException.class, () -> + TopologicalSort.sort(List.of(edge("B", "C"), edge("A", "B"), edge("C", "A")))); + } + + @ParameterizedTest + @ArgumentsSource(EdgeProvider.class) + void testSort(List edges) { + var branches = TopologicalSort.sort(edges); + assertEquals(brancheList("A", "B", "C", "D", "E"), branches); + } + + private static class EdgeProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + List edges = List.of(edge("A", "B"), edge("B", "C"), edge("C", "D"), edge("B", "D"), edge("D", "E")); + List> permutations = new ArrayList<>(); + permutations(edges, List.of(), permutations); + return permutations.stream().map(Arguments::arguments); + } + + static void permutations(List source, List perm, List> result) { + if (source.size() == perm.size()) { + result.add(perm); + return; + } + for (var edge : source) { + if (!perm.contains(edge)) { + List newPerm = new ArrayList<>(perm); + newPerm.add(edge); + permutations(source, newPerm, result); + } + } + } + } +} diff --git a/settings.gradle b/settings.gradle index d6b56c1cd..b772ed6da 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,3 +50,4 @@ include 'bots:mlbridge' include 'bots:notify' include 'bots:pr' include 'bots:submit' +include 'bots:topological' diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java b/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java index f0113b492..91cdec7f5 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java @@ -48,6 +48,7 @@ default void checkout(Branch b) throws IOException { void push(Hash hash, URI uri, String ref, boolean force) throws IOException; void push(Branch branch, String remote, boolean setUpstream) throws IOException; void clean() throws IOException; + void reset(Hash target, boolean hard) throws IOException; void revert(Hash parent) throws IOException; Repository reinitialize() throws IOException; void squash(Hash h) throws IOException; @@ -172,7 +173,7 @@ static Repository materialize(Path p, URI remote, String ref, boolean checkout) } static Repository clone(URI from) throws IOException { - var to = Path.of(from.getPath()).getFileName(); + var to = Path.of(from).getFileName(); if (to.toString().endsWith(".git")) { to = Path.of(to.toString().replace(".git", "")); } diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java b/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java index 6e244c1fc..2d8f0e17b 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java @@ -295,6 +295,20 @@ public void clean() throws IOException { } } + @Override + public void reset(Hash target, boolean hard) throws IOException { + var cmd = new ArrayList<>(List.of("git", "reset")); + if (hard) { + cmd.add("--hard"); + } + cmd.add(target.hex()); + + try (var p = capture(cmd)) { + await(p); + } + } + + @Override public void revert(Hash h) throws IOException { try (var p = capture("git", "checkout", h.hex(), "--", ".")) { diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java b/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java index d7ebe492c..5b24eeb28 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java @@ -318,6 +318,11 @@ public void clean() throws IOException { } } + @Override + public void reset(Hash target, boolean hard) throws IOException { + throw new RuntimeException("Not implemented yet"); + } + @Override public Repository reinitialize() throws IOException { Files.walk(dir) diff --git a/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java b/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java index a1c74d13d..8c24388d2 100644 --- a/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java +++ b/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java @@ -22,6 +22,7 @@ */ package org.openjdk.skara.vcs; +import org.junit.jupiter.api.Assumptions; import org.openjdk.skara.test.TemporaryDirectory; import org.junit.jupiter.api.Test; @@ -37,6 +38,7 @@ import static java.nio.file.StandardOpenOption.*; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; public class RepositoryTests { @@ -1787,4 +1789,32 @@ void testAbortMerge(VCS vcs) throws IOException { assertTrue(r.isClean()); } } + + @ParameterizedTest + @EnumSource(VCS.class) + void testReset(VCS vcs) throws IOException { + assumeTrue(vcs == VCS.GIT); // FIXME reset is not yet implemented for HG + + try (var dir = new TemporaryDirectory()) { + var repo = Repository.init(dir.path(), vcs); + assertTrue(repo.isClean()); + + var f = dir.path().resolve("README"); + Files.writeString(f, "Hello\n"); + repo.add(f); + var initial = repo.commit("Initial commit", "duke", "duke@openjdk.org"); + + Files.writeString(f, "Hello again\n"); + repo.add(f); + var second = repo.commit("Second commit", "duke", "duke@openjdk.org"); + + assertEquals(second, repo.head()); + assertEquals(2, repo.commits().asList().size()); + + repo.reset(initial, true); + + assertEquals(initial, repo.head()); + assertEquals(1, repo.commits().asList().size()); + } + } }