diff --git a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java index 8794181af..c8f56f7d3 100644 --- a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java +++ b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java @@ -43,14 +43,16 @@ class MergeBot implements Bot, WorkItem { private final Branch fromBranch; private final HostedRepository to; private final Branch toBranch; + private final HostedRepository toFork; MergeBot(Path storage, HostedRepository from, Branch fromBranch, - HostedRepository to, Branch toBranch) { + HostedRepository to, Branch toBranch, HostedRepository toFork) { this.storage = storage; this.from = from; this.fromBranch = fromBranch; this.to = to; this.toBranch = toBranch; + this.toFork = toFork; } @Override @@ -72,7 +74,7 @@ public void run(Path scratchPath) { if (!Files.exists(dir)) { log.info("Cloning " + to.name()); Files.createDirectories(dir); - repo = Repository.clone(to.url(), dir); + repo = Repository.clone(toFork.url(), dir); } else { log.info("Found existing scratch directory for " + to.name()); repo = Repository.get(dir).orElseThrow(() -> { @@ -80,10 +82,18 @@ public void run(Path scratchPath) { }); } - repo.fetchAll(); - var originToBranch = new Branch("origin/" + toBranch.name()); + // Sync personal fork + var remoteBranches = repo.remoteBranches(to.url().toString()); + for (var branch : remoteBranches) { + var fetchHead = repo.fetch(to.url(), branch.hash().hex()); + repo.push(fetchHead, toFork.url(), branch.name()); + } + + // Checkout the branch to merge into + repo.pull(toFork.url().toString(), toBranch.name()); + repo.checkout(toBranch, false); - // Check if pull request already created + // Check if merge conflict pull request is present var title = "Cannot automatically merge " + from.name() + ":" + fromBranch.name(); var marker = ""; for (var pr : to.pullRequests()) { @@ -92,7 +102,7 @@ public void run(Path scratchPath) { to.forge().currentUser().equals(pr.author())) { var lines = pr.body().split("\n"); var head = new Hash(lines[1].substring(5, 45)); - if (repo.contains(originToBranch, head)) { + if (repo.contains(toBranch, head)) { log.info("Closing resolved merge conflict PR " + pr.id()); pr.addComment("Merge conflicts have been resolved, closing this PR"); pr.setState(PullRequest.State.CLOSED); @@ -108,7 +118,7 @@ public void run(Path scratchPath) { var head = repo.resolve(toBranch.name()).orElseThrow(() -> new IOException("Could not resolve branch " + toBranch.name()) ); - if (repo.contains(originToBranch, fetchHead)) { + if (repo.contains(toBranch, fetchHead)) { log.info("Nothing to merge"); return; } @@ -116,7 +126,6 @@ public void run(Path scratchPath) { var isAncestor = repo.isAncestor(head, fetchHead); log.info("Trying to merge into " + toBranch.name()); - repo.checkout(toBranch, false); IOException error = null; try { repo.merge(fetchHead); @@ -135,6 +144,10 @@ public void run(Path scratchPath) { log.info("Aborting unsuccesful merge"); repo.abortMerge(); + var fromRepoName = Path.of(from.webUrl().getPath()).getFileName(); + var fromBranchDesc = fromRepoName + "/" + fromBranch.name(); + repo.push(fetchHead, toFork.url(), fromBranchDesc, true); + log.info("Creating pull request to alert"); var mergeBase = repo.mergeBase(fetchHead, head); var commits = repo.commits(mergeBase.hex() + ".." + fetchHead.hex(), true).asList(); @@ -164,15 +177,16 @@ public void run(Path scratchPath) { message.add("$ git commit -m 'Merge'"); message.add("```"); message.add(""); - message.add("Push the resulting merge conflict to your personal fork and " + - "create a pull request towards this repository. This pull request " + - "will be closed automatically once the pull request with the resolved " + - "conflicts has been integrated."); - var pr = from.createPullRequest(to, - toBranch.name(), - fromBranch.name(), - title, - message); + message.add("Push the resolved merge conflict to your personal fork and " + + "create a pull request towards this repository."); + message.add(""); + message.add("This pull request will be closed automatically by a bot once " + + "the merge conflicts have been resolved."); + var pr = toFork.createPullRequest(to, + toBranch.name(), + fromBranchDesc, + title, + message); } } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java index 8310c5642..879bd9f6e 100644 --- a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java +++ b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java @@ -55,10 +55,11 @@ public List create(BotConfiguration configuration) { var toRepo = configuration.repository(repo.get("to").asString()); var toBranch = new Branch(configuration.repositoryRef(repo.get("to").asString())); + var toFork = configuration.repository(repo.get("fork").asString()); log.info("Setting up merging from " + fromRepo.name() + ":" + fromBranch.name() + " to " + toRepo.name() + ":" + toBranch.name()); - bots.add(new MergeBot(storage, fromRepo, fromBranch, toRepo, toBranch)); + bots.add(new MergeBot(storage, fromRepo, fromBranch, toRepo, toBranch, toFork)); } return bots; } diff --git a/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java b/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java index edd6ccd60..4f7e9fa59 100644 --- a/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java +++ b/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java @@ -41,7 +41,7 @@ class MergeBotTests { @Test void mergeMasterBranch(TestInfo testInfo) throws IOException { - try (var temp = new TemporaryDirectory()) { + try (var temp = new TemporaryDirectory(false)) { var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke"))); var fromDir = temp.path().resolve("from.git"); @@ -50,11 +50,18 @@ void mergeMasterBranch(TestInfo testInfo) throws IOException { var toDir = temp.path().resolve("to.git"); var toLocalRepo = Repository.init(toDir, VCS.GIT); - var gitConfig = toDir.resolve(".git").resolve("config"); - Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + var toGitConfig = toDir.resolve(".git").resolve("config"); + Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), StandardOpenOption.APPEND); var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo); + var forkDir = temp.path().resolve("fork.git"); + var forkLocalRepo = Repository.init(forkDir, VCS.GIT); + var forkGitConfig = forkDir.resolve(".git").resolve("config"); + Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo); + var now = ZonedDateTime.now(); var fromFileA = fromDir.resolve("a.txt"); Files.writeString(fromFileA, "Hello A\n"); @@ -85,7 +92,7 @@ void mergeMasterBranch(TestInfo testInfo) throws IOException { var storage = temp.path().resolve("storage"); var master = new Branch("master"); - var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master); + var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master, toFork); TestBotRunner.runPeriodicItems(bot); toCommits = toLocalRepo.commits().asList(); @@ -108,7 +115,7 @@ void mergeMasterBranch(TestInfo testInfo) throws IOException { @Test void failingMergeTest(TestInfo testInfo) throws IOException { - try (var temp = new TemporaryDirectory()) { + try (var temp = new TemporaryDirectory(false)) { var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke"))); var fromDir = temp.path().resolve("from.git"); @@ -117,11 +124,18 @@ void failingMergeTest(TestInfo testInfo) throws IOException { var toDir = temp.path().resolve("to.git"); var toLocalRepo = Repository.init(toDir, VCS.GIT); - var gitConfig = toDir.resolve(".git").resolve("config"); - Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + var toGitConfig = toDir.resolve(".git").resolve("config"); + Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), StandardOpenOption.APPEND); var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo); + var forkDir = temp.path().resolve("fork.git"); + var forkLocalRepo = Repository.init(forkDir, VCS.GIT); + var forkGitConfig = forkDir.resolve(".git").resolve("config"); + Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo); + var now = ZonedDateTime.now(); var fromFileA = fromDir.resolve("a.txt"); Files.writeString(fromFileA, "Hello A\n"); @@ -152,7 +166,7 @@ void failingMergeTest(TestInfo testInfo) throws IOException { var storage = temp.path().resolve("storage"); var master = new Branch("master"); - var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master); + var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master, toFork); TestBotRunner.runPeriodicItems(bot); toCommits = toLocalRepo.commits().asList(); @@ -170,7 +184,7 @@ void failingMergeTest(TestInfo testInfo) throws IOException { @Test void failingMergeShouldResultInOnlyOnePR(TestInfo testInfo) throws IOException { - try (var temp = new TemporaryDirectory()) { + try (var temp = new TemporaryDirectory(false)) { var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke"))); var fromDir = temp.path().resolve("from.git"); @@ -179,11 +193,18 @@ void failingMergeShouldResultInOnlyOnePR(TestInfo testInfo) throws IOException { var toDir = temp.path().resolve("to.git"); var toLocalRepo = Repository.init(toDir, VCS.GIT); - var gitConfig = toDir.resolve(".git").resolve("config"); - Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + var toGitConfig = toDir.resolve(".git").resolve("config"); + Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), StandardOpenOption.APPEND); var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo); + var forkDir = temp.path().resolve("fork.git"); + var forkLocalRepo = Repository.init(forkDir, VCS.GIT); + var forkGitConfig = forkDir.resolve(".git").resolve("config"); + Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo); + var now = ZonedDateTime.now(); var fromFileA = fromDir.resolve("a.txt"); Files.writeString(fromFileA, "Hello A\n"); @@ -214,7 +235,7 @@ void failingMergeShouldResultInOnlyOnePR(TestInfo testInfo) throws IOException { var storage = temp.path().resolve("storage"); var master = new Branch("master"); - var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master); + var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master, toFork); TestBotRunner.runPeriodicItems(bot); TestBotRunner.runPeriodicItems(bot); @@ -242,11 +263,18 @@ void resolvedMergeConflictShouldResultInClosedPR(TestInfo testInfo) throws IOExc var toDir = temp.path().resolve("to.git"); var toLocalRepo = Repository.init(toDir, VCS.GIT); - var gitConfig = toDir.resolve(".git").resolve("config"); - Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + var toGitConfig = toDir.resolve(".git").resolve("config"); + Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), StandardOpenOption.APPEND); var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo); + var forkDir = temp.path().resolve("fork.git"); + var forkLocalRepo = Repository.init(forkDir, VCS.GIT); + var forkGitConfig = forkDir.resolve(".git").resolve("config"); + Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo); + var now = ZonedDateTime.now(); var fromFileA = fromDir.resolve("a.txt"); Files.writeString(fromFileA, "Hello A\n"); @@ -277,7 +305,7 @@ void resolvedMergeConflictShouldResultInClosedPR(TestInfo testInfo) throws IOExc var storage = temp.path().resolve("storage"); var master = new Branch("master"); - var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master); + var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master, toFork); TestBotRunner.runPeriodicItems(bot); TestBotRunner.runPeriodicItems(bot); @@ -316,11 +344,18 @@ void resolvedMergeConflictAndThenNewConflict(TestInfo testInfo) throws IOExcepti var toDir = temp.path().resolve("to.git"); var toLocalRepo = Repository.init(toDir, VCS.GIT); - var gitConfig = toDir.resolve(".git").resolve("config"); - Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + var toGitConfig = toDir.resolve(".git").resolve("config"); + Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), StandardOpenOption.APPEND); var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo); + var forkDir = temp.path().resolve("fork.git"); + var forkLocalRepo = Repository.init(forkDir, VCS.GIT); + var forkGitConfig = forkDir.resolve(".git").resolve("config"); + Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), + StandardOpenOption.APPEND); + var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo); + var now = ZonedDateTime.now(); var fromFileA = fromDir.resolve("a.txt"); Files.writeString(fromFileA, "Hello A\n"); @@ -351,7 +386,7 @@ void resolvedMergeConflictAndThenNewConflict(TestInfo testInfo) throws IOExcepti var storage = temp.path().resolve("storage"); var master = new Branch("master"); - var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master); + var bot = new MergeBot(storage, fromHostedRepo, master, toHostedRepo, master, toFork); TestBotRunner.runPeriodicItems(bot); TestBotRunner.runPeriodicItems(bot); diff --git a/test/src/main/java/org/openjdk/skara/test/TestHost.java b/test/src/main/java/org/openjdk/skara/test/TestHost.java index 97d659205..e53b76e3b 100644 --- a/test/src/main/java/org/openjdk/skara/test/TestHost.java +++ b/test/src/main/java/org/openjdk/skara/test/TestHost.java @@ -143,9 +143,9 @@ void close() { } } - TestPullRequest createPullRequest(TestHostedRepository repository, String targetRef, String sourceRef, String title, List body, boolean draft) { + TestPullRequest createPullRequest(TestHostedRepository targetRepository, TestHostedRepository sourceRepository, String targetRef, String sourceRef, String title, List body, boolean draft) { var id = String.valueOf(data.pullRequests.size() + 1); - var pr = TestPullRequest.createNew(repository, id, targetRef, sourceRef, title, body, draft); + var pr = TestPullRequest.createNew(targetRepository, sourceRepository, id, targetRef, sourceRef, title, body, draft); data.pullRequests.put(id, pr); return pr; } diff --git a/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java b/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java index d70d4f251..d7ff58b5e 100644 --- a/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java +++ b/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java @@ -59,7 +59,7 @@ public Optional parent() { @Override public PullRequest createPullRequest(HostedRepository target, String targetRef, String sourceRef, String title, List body, boolean draft) { - return host.createPullRequest(this, targetRef, sourceRef, title, body, draft); + return host.createPullRequest((TestHostedRepository) target, this, targetRef, sourceRef, title, body, draft); } @Override diff --git a/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java b/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java index a1b33efb9..538f81e8d 100644 --- a/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java +++ b/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java @@ -36,22 +36,24 @@ import java.util.stream.Collectors; public class TestPullRequest extends TestIssue implements PullRequest { - private final TestHostedRepository repository; + private final TestHostedRepository targetRepository; + private final TestHostedRepository sourceRepository; private final String targetRef; private final String sourceRef; private final PullRequestData data; - private TestPullRequest(TestHostedRepository repository, String id, HostUser author, HostUser user, String targetRef, String sourceRef, PullRequestData data) { - super(repository, id, author, user, data); - this.repository = repository; + private TestPullRequest(TestHostedRepository targetRepository, TestHostedRepository sourceRepository, String id, HostUser author, HostUser user, String targetRef, String sourceRef, PullRequestData data) { + super(targetRepository, id, author, user, data); + this.targetRepository = targetRepository; + this.sourceRepository = sourceRepository; this.targetRef = targetRef; this.sourceRef = sourceRef; this.data = data; try { - var headHash = repository.localRepository().resolve(sourceRef).orElseThrow(); - if (!headHash.equals(data.headHash)) { - data.headHash = headHash; + var headHash = sourceRepository.localRepository().resolve(sourceRef); + if (headHash.isPresent() && !headHash.get().equals(data.headHash)) { + data.headHash = headHash.get(); data.lastUpdate = ZonedDateTime.now(); } } catch (IOException e) { @@ -59,23 +61,23 @@ private TestPullRequest(TestHostedRepository repository, String id, HostUser aut } } - static TestPullRequest createNew(TestHostedRepository repository, String id, String targetRef, String sourceRef, String title, List body, boolean draft) { + static TestPullRequest createNew(TestHostedRepository targetRepository, TestHostedRepository sourceRepository, String id, String targetRef, String sourceRef, String title, List body, boolean draft) { var data = new PullRequestData(); data.title = title; data.body = String.join("\n", body); data.draft = draft; - var pr = new TestPullRequest(repository, id, repository.forge().currentUser(), repository.forge().currentUser(), targetRef, sourceRef, data); + var pr = new TestPullRequest(targetRepository, sourceRepository, id, targetRepository.forge().currentUser(), targetRepository.forge().currentUser(), targetRef, sourceRef, data); return pr; } static TestPullRequest createFrom(TestHostedRepository repository, TestPullRequest other) { - var pr = new TestPullRequest(repository, other.id, other.author, repository.forge().currentUser(), other.targetRef, other.sourceRef, other.data); + var pr = new TestPullRequest(repository, other.sourceRepository, other.id, other.author, repository.forge().currentUser(), other.targetRef, other.sourceRef, other.data); return pr; } @Override public HostedRepository repository() { - return repository; + return targetRepository; } @Override @@ -91,8 +93,8 @@ public List reviews() { @Override public void addReview(Review.Verdict verdict, String body) { try { - var review = new Review(ZonedDateTime.now(), repository.forge().currentUser(), - verdict, repository.localRepository().resolve(sourceRef).orElseThrow(), + var review = new Review(ZonedDateTime.now(), targetRepository.forge().currentUser(), + verdict, targetRepository.localRepository().resolve(sourceRef).orElseThrow(), data.reviews.size(), body); @@ -145,7 +147,7 @@ public String sourceRef() { @Override public HostedRepository sourceRepository() { - return repository; + return sourceRepository; } @Override @@ -155,7 +157,7 @@ public String targetRef() { @Override public Hash targetHash() { - return repository.branchHash(targetRef); + return targetRepository.branchHash(targetRef); } @Override @@ -205,7 +207,7 @@ public boolean isDraft() { @Override public URI webUrl() { try { - return new URI(repository.url().toString() + "/pr/" + id()); + return new URI(targetRepository.url().toString() + "/pr/" + id()); } catch (URISyntaxException e) { throw new RuntimeException(e); } 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 6e84f82d9..ed20809d9 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java @@ -98,6 +98,7 @@ Hash amend(String message, String committerEmail) throws IOException; Tag tag(Hash hash, String tagName, String message, String authorName, String authorEmail) throws IOException; Branch branch(Hash hash, String branchName) throws IOException; + void prune(Branch branch, String remote) throws IOException; void delete(Branch b) throws IOException; void rebase(Hash hash, String committerName, String committerEmail) throws IOException; void merge(Hash hash) 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 c8eba9bd8..bda7edc0a 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 @@ -626,6 +626,16 @@ public Branch branch(Hash hash, String name) throws IOException { return new Branch(name); } + @Override + public void prune(Branch branch, String remote) throws IOException { + try (var p = capture("git", "push", "--delete", remote, branch.name())) { + await(p); + } + try (var p = capture("git", "branch", "--delete", "--force", branch.name())) { + await(p); + } + } + @Override public Hash mergeBase(Hash first, Hash second) throws IOException { try (var p = capture("git", "merge-base", first.hex(), second.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 2bf94a80c..f7b0bc1d5 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 @@ -589,6 +589,16 @@ public Branch branch(Hash hash, String name) throws IOException { return new Branch(name); } + @Override + public void prune(Branch branch, String remote) throws IOException { + try (var p = capture("hg", "bookmark", "--delete", branch.name())) { + await(p); + } + try (var p = capture("hg", "push", "--bookmark", branch.name(), remote)) { + await(p); + } + } + @Override public Hash mergeBase(Hash first, Hash second) throws IOException { var revset = "ancestor(" + first.hex() + ", " + second.hex() + ")"; 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 ee256b1c9..89c82c79c 100644 --- a/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java +++ b/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java @@ -2028,4 +2028,52 @@ void testFetchRemote(VCS vcs) throws IOException { } } } + + @ParameterizedTest + @EnumSource(VCS.class) + void testPrune(VCS vcs) throws IOException { + assumeTrue(vcs == VCS.GIT); // FIXME hard to test with hg due to bookmarks and branches + try (var dir = new TemporaryDirectory(false)) { + var upstreamDir = dir.path().resolve("upstream" + (vcs == VCS.GIT ? ".git" : ".hg")); + var upstream = Repository.init(upstreamDir, vcs); + + Files.write(upstream.root().resolve(".git").resolve("config"), + List.of("[receive]", "denyCurrentBranch=ignore"), + WRITE, APPEND); + + var readme = upstreamDir.resolve("README"); + Files.write(readme, List.of("Hello, readme!")); + + upstream.add(readme); + var head = upstream.commit("Add README", "duke", "duke@openjdk.java.net"); + var branch = upstream.branch(head, "foo"); + var upstreamBranches = upstream.branches(); + assertEquals(2, upstreamBranches.size()); + assertTrue(upstreamBranches.contains(branch)); + + var upstreamURI = URI.create("file:///" + upstreamDir.toString().replace('\\', '/')); + var downstreamDir = dir.path().resolve("downstream"); + var downstream = Repository.clone(upstreamURI, downstreamDir); + + // Ensure that 'foo' branch is materialized downstream + downstream.checkout(branch); + downstream.checkout(downstream.defaultBranch()); + + var remotes = downstream.remotes(); + assertEquals(1, remotes.size()); + var downstreamBranches = downstream.branches(); + assertEquals(2, downstreamBranches.size()); + assertEquals(downstreamBranches, upstreamBranches); + + downstream.prune(branch, remotes.get(0)); + + downstreamBranches = downstream.branches(); + assertEquals(1, downstreamBranches.size()); + assertEquals(List.of(downstream.defaultBranch()), downstreamBranches); + + upstreamBranches = upstream.branches(); + assertEquals(1, upstreamBranches.size()); + assertEquals(List.of(upstream.defaultBranch()), upstreamBranches); + } + } }