diff --git a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java index 0b5a949d4..57f342114 100644 --- a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java +++ b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java @@ -49,19 +49,21 @@ class MirrorBot implements Bot, WorkItem { private final List<Pattern> branchPatterns; private final boolean includeTags; private final boolean onlyTags; + private final List<String> refspecs; MirrorBot(Path storage, HostedRepository from, HostedRepository to) { - this(storage, from, to, List.of(), true, false); + this(storage, from, to, List.of(), true, false, List.of()); } MirrorBot(Path storage, HostedRepository from, HostedRepository to, List<Pattern> branchPatterns, - boolean includeTags, boolean onlyTags) { + boolean includeTags, boolean onlyTags, List<String> refspecs) { this.storage = storage; this.from = from; this.to = to; this.branchPatterns = branchPatterns; this.includeTags = includeTags; this.onlyTags = onlyTags; + this.refspecs = refspecs; } @Override @@ -103,14 +105,14 @@ public Collection<WorkItem> run(Path scratchPath) { } log.info("Pulling " + from.name()); - repo.fetchAll(from.authenticatedUrl(), includeTags || onlyTags); + repo.fetchAll(from.authenticatedUrl(), includeTags || onlyTags || !refspecs.isEmpty()); if (onlyTags) { log.info("Pushing tags to " + to.name()); repo.pushTags(to.authenticatedUrl(), true); } else if (branchPatterns.isEmpty() && includeTags) { log.info("Pushing tags and branches to " + to.name()); repo.pushAll(to.authenticatedUrl(), true); - } else { + } else if (!branchPatterns.isEmpty()) { for (var branch : repo.branches()) { if (branchPatterns.stream().anyMatch(p -> p.matcher(branch.name()).matches())) { var hash = repo.resolve(branch); @@ -123,6 +125,11 @@ public Collection<WorkItem> run(Path scratchPath) { } } } + } else if (!refspecs.isEmpty()) { + for (var refspec : refspecs) { + log.info("Pushing using refspec " + refspec + " to " + to.name()); + repo.push(refspec, to.authenticatedUrl()); + } } } catch (IOException e) { throw new UncheckedIOException(e); @@ -133,22 +140,26 @@ public Collection<WorkItem> run(Path scratchPath) { @Override public String toString() { var name = "MirrorBot@" + from.name() + "->" + to.name(); - if (branchPatterns.isEmpty()) { + if (!refspecs.isEmpty()) { + name += " (" + String.join(",", refspecs) + ")"; + } else { + if (branchPatterns.isEmpty()) { + if (onlyTags) { + name += " ()"; + } else { + name += " (*)"; + } + } else { + var branchPatterns = this.branchPatterns.stream().map(Pattern::toString).collect(Collectors.toList()); + name += " (" + String.join(",", branchPatterns) + ")"; + } if (onlyTags) { - name += " ()"; + name += " [tags only]"; + } else if (includeTags) { + name += " [tags included]"; } else { - name += " (*)"; + name += " [tags excluded]"; } - } else { - var branchPatterns = this.branchPatterns.stream().map(Pattern::toString).collect(Collectors.toList()); - name += " (" + String.join(",", branchPatterns) + ")"; - } - if (onlyTags) { - name += " [tags only]"; - } else if (includeTags) { - name += " [tags included]"; - } else { - name += " [tags excluded]"; } return name; } @@ -184,4 +195,8 @@ public boolean isIncludeTags() { public boolean isOnlyTags() { return onlyTags; } + + public List<String> getRefspecs() { + return refspecs; + } } diff --git a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java index 99eddd6e9..7e1c94e24 100644 --- a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java +++ b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java @@ -60,8 +60,25 @@ public List<Bot> create(BotConfiguration configuration) { var toName = repo.get("to").asString(); var toRepo = configuration.repository(toName); + List<String> refspecs; + if (repo.contains("refspecs")) { + var refspecsElement = repo.get("refspecs"); + if (refspecsElement.isArray()) { + refspecs = refspecsElement.asArray().stream() + .map(JSONValue::asString) + .toList(); + } else { + refspecs = List.of(refspecsElement.asString()); + } + } else { + refspecs = List.of(); + } + List<Pattern> branchPatterns; if (repo.contains("branches")) { + if (!refspecs.isEmpty()) { + throw new IllegalStateException("Cannot combine refspecs and branches"); + } // Accept both an array of regex patterns as well as a single comma separated // string for backwards compatibility var branchesElement = repo.get("branches"); @@ -79,7 +96,7 @@ public List<Bot> create(BotConfiguration configuration) { branchPatterns = List.of(); } - var includeTags = branchPatterns.isEmpty(); + var includeTags = branchPatterns.isEmpty() && refspecs.isEmpty(); var onlyTags = false; if (repo.contains("tags")) { var tags = repo.get("tags").asString().toLowerCase().strip(); @@ -96,9 +113,12 @@ public List<Bot> create(BotConfiguration configuration) { if (onlyTags && !branchPatterns.isEmpty()) { throw new IllegalStateException("Branches cannot be mirrored when only tags are mirrored"); } + if ((onlyTags || includeTags) && !refspecs.isEmpty()) { + throw new IllegalStateException("Cannot combine refspecs and tags"); + } log.info("Setting up mirroring from " + fromRepo.name() + " to " + toRepo.name()); - bots.add(new MirrorBot(storage, fromRepo, toRepo, branchPatterns, includeTags, onlyTags)); + bots.add(new MirrorBot(storage, fromRepo, toRepo, branchPatterns, includeTags, onlyTags, refspecs)); } return bots; } diff --git a/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotFactoryTest.java b/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotFactoryTest.java index 8b0e07c8f..707ec0ac5 100644 --- a/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotFactoryTest.java +++ b/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotFactoryTest.java @@ -252,4 +252,109 @@ public void testCreateWithTags() { mirrorBot6.getBranchPatterns().stream().map(Pattern::toString).toList()); } } + + @Test + public void testThrowsWithRefspecsAndTags() { + try (var tempFolder = new TemporaryDirectory()) { + String jsonString = """ + { + "repositories": [ + { + "from": "from1", + "to": "to1", + "refspecs": "refs/foo", + "tags": "only" + } + ] + } + """; + var jsonConfig = JWCC.parse(jsonString).asObject(); + + var testBotFactory = TestBotFactory.newBuilder() + .addHostedRepository("from1", new TestHostedRepository("from1")) + .addHostedRepository("to1", new TestHostedRepository("to1")) + .storagePath(tempFolder.path().resolve("storage")) + .build(); + + assertThrows(IllegalStateException.class, () -> testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig)); + } + } + + @Test + public void testThrowsWithRefspecsAndBranches() { + try (var tempFolder = new TemporaryDirectory()) { + String jsonString = """ + { + "repositories": [ + { + "from": "from1", + "to": "to1", + "refspecs": "refs/foo", + "branches": "master" + } + ] + } + """; + var jsonConfig = JWCC.parse(jsonString).asObject(); + + var testBotFactory = TestBotFactory.newBuilder() + .addHostedRepository("from1", new TestHostedRepository("from1")) + .addHostedRepository("to1", new TestHostedRepository("to1")) + .storagePath(tempFolder.path().resolve("storage")) + .build(); + + assertThrows(IllegalStateException.class, () -> testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig)); + } + } + + @Test + public void testCreateWithRefspecs() { + try (var tempFolder = new TemporaryDirectory()) { + String jsonString = """ + { + "repositories": [ + { + "from": "from1", + "to": "to1", + "refspecs": "refs/foo", + }, + { + "from": "from2", + "to": "to2", + "refspecs": [ + "refs/foo", + "refs/bar" + ] + } + ] + } + """; + var jsonConfig = JWCC.parse(jsonString).asObject(); + + var testBotFactory = TestBotFactory.newBuilder() + .addHostedRepository("from1", new TestHostedRepository("from1")) + .addHostedRepository("from2", new TestHostedRepository("from2")) + .addHostedRepository("to1", new TestHostedRepository("to1")) + .addHostedRepository("to2", new TestHostedRepository("to2")) + .storagePath(tempFolder.path().resolve("storage")) + .build(); + + var bots = testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig); + assertEquals(2, bots.size()); + + MirrorBot mirrorBot1 = (MirrorBot) bots.get(0); + assertEquals("MirrorBot@from1->to1 (refs/foo)", mirrorBot1.toString()); + assertFalse(mirrorBot1.isIncludeTags()); + assertFalse(mirrorBot1.isOnlyTags()); + assertEquals(List.of(), mirrorBot1.getBranchPatterns()); + assertEquals(List.of("refs/foo"), mirrorBot1.getRefspecs()); + + MirrorBot mirrorBot2 = (MirrorBot) bots.get(1); + assertEquals("MirrorBot@from2->to2 (refs/foo,refs/bar)", mirrorBot2.toString()); + assertFalse(mirrorBot2.isIncludeTags()); + assertFalse(mirrorBot2.isOnlyTags()); + assertEquals(List.of(), mirrorBot2.getBranchPatterns()); + assertEquals(List.of("refs/foo", "refs/bar"), mirrorBot2.getRefspecs()); + } + } } diff --git a/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java b/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java index a0c396a98..b6248504f 100644 --- a/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java +++ b/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java @@ -216,7 +216,7 @@ void mirrorSingleBranchAndTags(TestInfo testInfo) throws IOException { assertEquals(0, toLocalRepo.tags().size()); var storage = temp.path().resolve("storage"); - var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), true, false); + var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), true, false, List.of()); TestBotRunner.runPeriodicItems(bot); toCommits = toLocalRepo.commits().asList(); @@ -284,7 +284,7 @@ void mirrorSingleBranchNoTags(TestInfo testInfo) throws IOException { assertEquals(0, toLocalRepo.tags().size()); var storage = temp.path().resolve("storage"); - var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), false, false); + var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), false, false, List.of()); TestBotRunner.runPeriodicItems(bot); toCommits = toLocalRepo.commits().asList(); @@ -393,7 +393,7 @@ void mirrorSelectedBranches(TestInfo testInfo) throws IOException { assertEquals(0, toCommits.size()); var storage = temp.path().resolve("storage"); - var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), false, false); + var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), false, false, List.of()); TestBotRunner.runPeriodicItems(bot); toCommits = toLocalRepo.commits().asList(); @@ -441,7 +441,7 @@ void mirrorSelectedBranchPattern(TestInfo testInfo) throws IOException { assertEquals(0, toCommits.size()); var storage = temp.path().resolve("storage"); - var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("f.*")), false, false); + var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("f.*")), false, false, List.of()); TestBotRunner.runPeriodicItems(bot); toCommits = toLocalRepo.commits().asList(); @@ -528,7 +528,7 @@ void mirrorOnlyTags(TestInfo testInfo) throws IOException { assertEquals(0, toLocalRepo.branches().size()); var storage = temp.path().resolve("storage"); - var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(), true, true); + var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(), true, true, List.of()); TestBotRunner.runPeriodicItems(bot); toCommits = toLocalRepo.commits().asList(); @@ -568,4 +568,95 @@ void mirrorOnlyTags(TestInfo testInfo) throws IOException { assertEquals(fromLocalRepo.annotate(firstTag), toLocalRepo.annotate(firstTag), "First tag not correctly mirrored"); } } + + @Test + void mirrorRefspecs() throws IOException { + try (var temp = new TemporaryDirectory()) { + var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke"))); + + var fromDir = temp.path().resolve("from.git"); + var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT); + var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo); + + var toDir = temp.path().resolve("to.git"); + var toLocalRepo = TestableRepository.init(toDir, VCS.GIT); + var gitConfig = toDir.resolve(".git").resolve("config"); + Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo); + + var newFile = fromDir.resolve("this-file-cannot-exist.txt"); + Files.writeString(newFile, "Hello world\n"); + fromLocalRepo.add(newFile); + var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org"); + var fromCommits = fromLocalRepo.commits().asList(); + assertEquals(1, fromCommits.size()); + assertEquals(newHash, fromCommits.get(0).hash()); + + var toCommits = toLocalRepo.commits().asList(); + assertEquals(0, toCommits.size()); + + var storage = temp.path().resolve("storage"); + var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(), false, false, + List.of("refs/heads/master:refs/heads/master")); + TestBotRunner.runPeriodicItems(bot); + + toCommits = toLocalRepo.commits().asList(); + assertEquals(1, toCommits.size()); + assertEquals(newHash, toCommits.get(0).hash()); + } + } + + @Test + void mirrorMultipleRefspecs() throws IOException { + try (var temp = new TemporaryDirectory()) { + var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke"))); + + var fromDir = temp.path().resolve("from.git"); + var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT); + var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo); + + var toDir = temp.path().resolve("to.git"); + var toLocalRepo = TestableRepository.init(toDir, VCS.GIT); + var gitConfig = toDir.resolve(".git").resolve("config"); + Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo); + + var newFile = fromDir.resolve("this-file-cannot-exist.txt"); + Files.writeString(newFile, "Hello world\n"); + fromLocalRepo.add(newFile); + var first = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org"); + var featureBranch = fromLocalRepo.branch(first, "feature"); + fromLocalRepo.checkout(featureBranch, false); + assertEquals(Optional.of(featureBranch), fromLocalRepo.currentBranch()); + + Files.writeString(newFile, "Hello again\n", StandardOpenOption.APPEND); + fromLocalRepo.add(newFile); + var second = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org"); + + assertEquals(Optional.of(first), fromLocalRepo.resolve("master")); + assertEquals(Optional.of(second), fromLocalRepo.resolve("feature")); + + fromLocalRepo.tag(first, "firstTag", "add first tag", "duke", "duk@openjdk.org"); + fromLocalRepo.tag(second, "secondTag", "add second tag", "duke", "duk@openjdk.org"); + + var fromCommits = fromLocalRepo.commits().asList(); + assertEquals(2, fromCommits.size()); + + var toCommits = toLocalRepo.commits().asList(); + assertEquals(0, toCommits.size()); + + var storage = temp.path().resolve("storage"); + var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(), false, false, + List.of("refs/heads/m*:refs/heads/m*", "refs/tags/s*:refs/tags/s*")); + TestBotRunner.runPeriodicItems(bot); + + toCommits = toLocalRepo.commits().asList(); + assertEquals(2, toCommits.size()); + assertEquals(second, toCommits.get(0).hash()); + assertEquals(List.of(new Branch("master")), toLocalRepo.branches()); + assertEquals(List.of(new Tag("secondTag")), toLocalRepo.tags()); + } + } } 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 d926cf45f..ca4d965b7 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java @@ -71,6 +71,7 @@ default void push(Hash hash, URI uri, String ref, boolean force) throws IOExcept void push(Hash hash, URI uri, String ref, boolean force, boolean includeTags) throws IOException; void push(Branch branch, String remote, boolean setUpstream) throws IOException; void push(Tag tag, URI uri, boolean force) throws IOException; + void push(String refspec, URI uri) throws IOException; void clean() throws IOException; void reset(Hash target, boolean hard) throws IOException; void revert(Hash parent) throws IOException; 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 f0d9bcf08..b187e3cde 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 @@ -649,6 +649,13 @@ public void push(Tag tag, URI uri, boolean force) throws IOException { } } + @Override + public void push(String refspec, URI uri) throws IOException { + try (var p = capture("git", "push", uri.toString(), refspec)) { + await(p); + } + } + @Override public void push(Branch branch, String remote, boolean setUpstream) throws IOException { var cmd = new ArrayList<String>(); 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 f61a2331a..d5a98a9a7 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 @@ -603,6 +603,11 @@ public void push(Tag tag, URI uri, boolean force) throws IOException { } } + @Override + public void push(String refspec, URI uri) throws IOException { + throw new RuntimeException("Refspec not supported with Mercurial"); + } + @Override public boolean isClean() throws IOException { try (var p = capture("hg", "status")) {