1 /*
   2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package org.openjdk.skara.vcs;
  24 
  25 import org.junit.jupiter.api.Assumptions;
  26 import org.openjdk.skara.test.TemporaryDirectory;
  27 
  28 import org.junit.jupiter.api.Test;
  29 import org.junit.jupiter.params.ParameterizedTest;
  30 import org.junit.jupiter.params.provider.EnumSource;
  31 
  32 import java.io.IOException;
  33 import java.net.URI;
  34 import java.nio.file.*;
  35 import java.nio.file.attribute.*;
  36 import java.util.*;
  37 import java.util.stream.Collectors;
  38 
  39 import static java.nio.file.StandardOpenOption.*;
  40 import static org.junit.jupiter.api.Assertions.*;
  41 import static org.junit.jupiter.api.Assumptions.assumeTrue;
  42 
  43 public class RepositoryTests {
  44 
  45     @ParameterizedTest
  46     @EnumSource(VCS.class)
  47     void testExistsOnMissingDirectory(VCS vcs) throws IOException {
  48         var d = Paths.get("/", "this", "path", "does", "not", "exist");
  49         var r = Repository.get(d);
  50         assertTrue(r.isEmpty());
  51     }
  52 
  53     @ParameterizedTest
  54     @EnumSource(VCS.class)
  55     void testExistsOnEmptyDirectory(VCS vcs) throws IOException {
  56         try (var dir = new TemporaryDirectory()) {
  57             var r = Repository.get(dir.path());
  58             assertTrue(r.isEmpty());
  59         }
  60     }
  61 
  62     @ParameterizedTest
  63     @EnumSource(VCS.class)
  64     void testExistsOnInitializedRepository(VCS vcs) throws IOException {
  65         try (var dir = new TemporaryDirectory()) {
  66             var r = Repository.init(dir.path(), vcs);
  67             assertTrue(r.exists());
  68         }
  69     }
  70 
  71     @ParameterizedTest
  72     @EnumSource(VCS.class)
  73     void testExistsOnSubdir() throws IOException {
  74         try (var dir = new TemporaryDirectory()) {
  75             var r = Repository.init(dir.path(), VCS.GIT);
  76             assertTrue(r.exists());
  77 
  78             var subdir = Paths.get(dir.toString(), "test");
  79             Files.createDirectories(subdir);
  80             var r2 = Repository.get(subdir);
  81             assertTrue(r2.get().exists());
  82         }
  83     }
  84 
  85     @ParameterizedTest
  86     @EnumSource(VCS.class)
  87     void testRootOnTopLevel() throws IOException {
  88         try (var dir = new TemporaryDirectory()) {
  89             var r = Repository.init(dir.path(), VCS.GIT);
  90             assertEquals(dir.toString(), r.root().toString());
  91         }
  92     }
  93 
  94     @ParameterizedTest
  95     @EnumSource(VCS.class)
  96     void testRootOnSubdirectory(VCS vcs) throws IOException {
  97         try (var dir = new TemporaryDirectory()) {
  98             var r = Repository.init(dir.path(), vcs);
  99             assertEquals(dir.toString(), r.root().toString());
 100 
 101             var subdir = Paths.get(dir.toString(), "sub");
 102             Files.createDirectories(subdir);
 103 
 104             var r2 = Repository.get(subdir);
 105             assertEquals(dir.toString(), r2.get().root().toString());
 106         }
 107     }
 108 
 109     @ParameterizedTest
 110     @EnumSource(VCS.class)
 111     void testResolveOnEmptyRepository(VCS vcs) throws IOException {
 112         try (var dir = new TemporaryDirectory()) {
 113             var r = Repository.init(dir.path(), vcs);
 114             assertTrue(r.resolve("HEAD").isEmpty());
 115         }
 116     }
 117 
 118     @ParameterizedTest
 119     @EnumSource(VCS.class)
 120     void testResolveWithHEAD(VCS vcs) throws IOException {
 121         try (var dir = new TemporaryDirectory()) {
 122             var r = Repository.init(dir.path(), vcs);
 123 
 124             var readme = dir.path().resolve("README");
 125             Files.write(readme, List.of("Hello, readme!"));
 126 
 127             r.add(readme);
 128             var head = r.commit("Add README", "duke", "duke@openjdk.java.net");
 129             assertEquals(head, r.head());
 130         }
 131     }
 132 
 133     @ParameterizedTest
 134     @EnumSource(VCS.class)
 135     void testConfig(VCS vcs) throws IOException {
 136         try (var dir = new TemporaryDirectory()) {
 137             var r = Repository.init(dir.path(), vcs);
 138 
 139             if (vcs == VCS.GIT) {
 140                 var config = dir.path().resolve(".git").resolve("config");
 141                 Files.write(config, List.of("[user]", "name = duke"), WRITE, APPEND);
 142                 assertEquals(List.of("duke"), r.config("user.name"));
 143             } else if (vcs == VCS.HG) {
 144                 var config = dir.path().resolve(".hg").resolve("hgrc");
 145                 Files.write(config, List.of("[ui]", "username = duke"), WRITE, CREATE);
 146                 assertEquals(List.of("duke"), r.config("ui.username"));
 147             }
 148 
 149             assertEquals("duke", r.username().get());
 150         }
 151     }
 152 
 153     @ParameterizedTest
 154     @EnumSource(VCS.class)
 155     void testCurrentBranchOnEmptyRepository(VCS vcs) throws IOException {
 156         try (var dir = new TemporaryDirectory()) {
 157             var r = Repository.init(dir.path(), vcs);
 158             assertEquals(r.defaultBranch(), r.currentBranch());
 159         }
 160     }
 161 
 162     @ParameterizedTest
 163     @EnumSource(VCS.class)
 164     void testCheckout(VCS vcs) throws IOException {
 165         try (var dir = new TemporaryDirectory()) {
 166             var r = Repository.init(dir.path(), vcs);
 167 
 168             var readme = dir.path().resolve("README");
 169             Files.write(readme, List.of("Hello, readme!"));
 170             r.add(readme);
 171 
 172             var head1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 173             assertEquals(head1, r.head());
 174 
 175             Files.write(readme, List.of("Another line"), WRITE, APPEND);
 176             r.add(readme);
 177 
 178             var head2 = r.commit("Add one more line", "duke", "duke@openjdk.java.net");
 179             assertEquals(head2, r.head());
 180 
 181             r.checkout(head1, false);
 182             assertEquals(head1, r.head());
 183 
 184             r.checkout(head2, false);
 185             assertEquals(head2, r.head());
 186         }
 187     }
 188 
 189     @ParameterizedTest
 190     @EnumSource(VCS.class)
 191     void testLines(VCS vcs) throws IOException {
 192         try (var dir = new TemporaryDirectory()) {
 193             var r = Repository.init(dir.path(), vcs);
 194 
 195             var readme = dir.path().resolve("README");
 196             Files.write(readme, List.of("Hello, readme!"));
 197             r.add(readme);
 198 
 199             var head1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 200             assertEquals(List.of("Hello, readme!"),
 201                          r.lines(readme, head1).orElseThrow());
 202 
 203             Files.write(readme, List.of("Another line"), WRITE, APPEND);
 204             r.add(readme);
 205 
 206             var head2 = r.commit("Add one more line", "duke", "duke@openjdk.java.net");
 207             assertEquals(List.of("Hello, readme!", "Another line"),
 208                          r.lines(readme, head2).orElseThrow());
 209         }
 210     }
 211 
 212     @ParameterizedTest
 213     @EnumSource(VCS.class)
 214     void testLinesInSubdir(VCS vcs) throws IOException {
 215         try (var dir = new TemporaryDirectory()) {
 216             Repository.init(dir.path(), vcs);
 217 
 218             var subdir = dir.path().resolve("sub");
 219             Files.createDirectories(subdir);
 220             var r = Repository.get(subdir).get();
 221 
 222             var readme = subdir.getParent().resolve("README");
 223             Files.write(readme, List.of("Hello, readme!"));
 224             r.add(readme);
 225 
 226             var head = r.commit("Add README", "duke", "duke@openjdk.java.net");
 227             assertEquals(List.of("Hello, readme!"),
 228                          r.lines(readme, head).orElseThrow());
 229 
 230             var example = subdir.resolve("EXAMPLE");
 231             Files.write(example, List.of("An example"));
 232             r.add(example);
 233 
 234             var head2 = r.commit("Add EXAMPLE", "duke", "duke@openjdk.java.net");
 235             assertEquals(List.of("An example"),
 236                          r.lines(example, head2).orElseThrow());
 237         }
 238     }
 239 
 240     @ParameterizedTest
 241     @EnumSource(VCS.class)
 242     void testCommitListingOnEmptyRepo(VCS vcs) throws IOException {
 243         try (var dir = new TemporaryDirectory()) {
 244             var r = Repository.init(dir.path(), vcs);
 245             assertTrue(r.commits().asList().isEmpty());
 246         }
 247     }
 248 
 249     @ParameterizedTest
 250     @EnumSource(VCS.class)
 251     void testCommitListingWithSingleCommit(VCS vcs) throws IOException {
 252         try (var dir = new TemporaryDirectory()) {
 253             var r = Repository.init(dir.path(), vcs);
 254 
 255             var readme = dir.path().resolve("README");
 256             Files.write(readme, List.of("Hello, readme!"));
 257 
 258             r.add(readme);
 259 
 260             var committerName = vcs == VCS.GIT ? "bot" : "duke";
 261             var committerEmail = vcs == VCS.GIT ? "bot@openjdk.java.net" : "duke@openjdk.java.net";
 262             var hash = r.commit("Add README", "duke", "duke@openjdk.java.net", committerName, committerEmail);
 263 
 264             var commits = r.commits().asList();
 265             assertEquals(1, commits.size());
 266 
 267             var commit = commits.get(0);
 268             assertEquals("duke", commit.author().name());
 269             assertEquals("duke@openjdk.java.net", commit.author().email());
 270             assertEquals(committerName, commit.committer().name());
 271             assertEquals(committerEmail, commit.committer().email());
 272 
 273             assertEquals(List.of("Add README"), commit.message());
 274 
 275             assertEquals(1, commit.numParents());
 276             assertEquals(1, commit.parents().size());
 277 
 278             var nullHash = "0".repeat(40);
 279             var parent = commit.parents().get(0);
 280             assertEquals(nullHash, parent.hex());
 281 
 282             assertTrue(commit.isInitialCommit());
 283             assertFalse(commit.isMerge());
 284             assertEquals(hash, commit.hash());
 285 
 286             var diffs = commit.parentDiffs();
 287             assertEquals(1, diffs.size());
 288 
 289             var diff = diffs.get(0);
 290             assertEquals(nullHash, diff.from().hex());
 291             assertEquals(hash, diff.to());
 292 
 293             assertEquals(0, diff.removed());
 294             assertEquals(0, diff.modified());
 295             assertEquals(1, diff.added());
 296 
 297             var patches = diff.patches();
 298             assertEquals(1, patches.size());
 299 
 300             var patch = patches.get(0).asTextualPatch();
 301             assertTrue(patch.status().isAdded());
 302 
 303             assertTrue(patch.source().path().isEmpty());
 304             assertTrue(patch.source().type().isEmpty());
 305 
 306             assertEquals(Path.of("README"), patch.target().path().get());
 307             assertTrue(patch.target().type().get().isRegularNonExecutable());
 308 
 309             var hunks = patch.hunks();
 310             assertEquals(1, hunks.size());
 311 
 312             var hunk = hunks.get(0);
 313             assertEquals(new Range(0, 0), hunk.source().range());
 314             assertEquals(new Range(1, 1), hunk.target().range());
 315 
 316             assertLinesEquals(List.of(), hunk.source().lines());
 317             assertLinesEquals(List.of("Hello, readme!"), hunk.target().lines());
 318         }
 319     }
 320 
 321     static String stripTrailingCR(String line) {
 322         return line.endsWith("\r") ? line.substring(0, line.length() - 1) : line;
 323     }
 324 
 325     static void assertLinesEquals(List<String> expected, List<String> actual) {
 326         assertEquals(expected, actual.stream().map(RepositoryTests::stripTrailingCR).collect(Collectors.toList()));
 327     }
 328 
 329     @ParameterizedTest
 330     @EnumSource(VCS.class)
 331     void testCommitListingWithMultipleCommits(VCS vcs) throws IOException {
 332         try (var dir = new TemporaryDirectory()) {
 333             var r = Repository.init(dir.path(), vcs);
 334 
 335             var readme = dir.path().resolve("README");
 336             Files.write(readme, List.of("Hello, readme!"));
 337 
 338             r.add(readme);
 339             var hash1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 340 
 341             Files.write(readme, List.of("Another line"), WRITE, APPEND);
 342             r.add(readme);
 343             var hash2 = r.commit("Modify README", "duke", "duke@openjdk.java.net");
 344 
 345             var commits = r.commits().asList();
 346             assertEquals(2, commits.size());
 347 
 348             var commit = commits.get(0);
 349             assertEquals("duke", commit.author().name());
 350             assertEquals("duke@openjdk.java.net", commit.author().email());
 351 
 352             assertEquals(List.of("Modify README"), commit.message());
 353 
 354             assertEquals(1, commit.numParents());
 355             assertEquals(1, commit.parents().size());
 356 
 357             var parent = commit.parents().get(0);
 358             assertEquals(hash1, parent);
 359 
 360             assertFalse(commit.isInitialCommit());
 361             assertFalse(commit.isMerge());
 362             assertEquals(hash2, commit.hash());
 363 
 364             var diffs = commit.parentDiffs();
 365             assertEquals(1, diffs.size());
 366 
 367             var diff = diffs.get(0);
 368             assertEquals(hash1, diff.from());
 369             assertEquals(hash2, diff.to());
 370 
 371             assertEquals(0, diff.removed());
 372             assertEquals(0, diff.modified());
 373             assertEquals(1, diff.added());
 374 
 375             var patches = diff.patches();
 376             assertEquals(1, patches.size());
 377 
 378             var patch = patches.get(0).asTextualPatch();
 379             assertTrue(patch.status().isModified());
 380             assertEquals(Path.of("README"), patch.source().path().get());
 381             assertTrue(patch.source().type().get().isRegularNonExecutable());
 382             assertEquals(Path.of("README"), patch.target().path().get());
 383             assertTrue(patch.target().type().get().isRegularNonExecutable());
 384 
 385             var hunks = patch.hunks();
 386             assertEquals(1, hunks.size());
 387 
 388             var hunk = hunks.get(0);
 389             assertEquals(new Range(2, 0), hunk.source().range());
 390             assertEquals(new Range(2, 1), hunk.target().range());
 391 
 392             assertLinesEquals(List.of(), hunk.source().lines());
 393             assertLinesEquals(List.of("Another line"), hunk.target().lines());
 394         }
 395     }
 396 
 397     @ParameterizedTest
 398     @EnumSource(VCS.class)
 399     void testSquashDeletes(VCS vcs) throws IOException {
 400         try (var dir = new TemporaryDirectory()) {
 401             var r = Repository.init(dir.path(), vcs);
 402 
 403             var file1 = dir.path().resolve("file1.txt");
 404             Files.write(file1, List.of("Hello, file 1!"));
 405             var file2 = dir.path().resolve("file2.txt");
 406             Files.write(file2, List.of("Hello, file 2!"));
 407             var file3 = dir.path().resolve("file3.txt");
 408             Files.write(file3, List.of("Hello, file 3!"));
 409 
 410             r.add(file1, file2, file3);
 411             var hash1 = r.commit("Add files", "duke", "duke@openjdk.java.net");
 412 
 413             Files.delete(file2);
 414             r.remove(file2);
 415             var hash2 = r.commit("Remove file 2", "duke", "duke@openjdk.java.net");
 416 
 417             Files.delete(file3);
 418             r.remove(file3);
 419             var hash3 = r.commit("Remove file 3", "duke", "duke@openjdk.java.net");
 420 
 421             var refspec = vcs == VCS.GIT ? r.head().hex() : r.head().hex() + ":0";
 422             assertEquals(3, r.commits(refspec).asList().size());
 423 
 424             r.checkout(hash1, false);
 425             r.squash(hash3);
 426             r.commit("Squashed remove of file 2 and 3", "duke", "duke@openjdk.java.net");
 427 
 428             refspec = vcs == VCS.GIT ? r.head().hex() : r.head().hex() + ":0";
 429             var commits = r.commits(refspec).asList();
 430             assertEquals(2, commits.size());
 431 
 432             assertEquals(hash1, commits.get(1).hash());
 433 
 434             var head = commits.get(0);
 435             assertNotEquals(hash2, head);
 436             assertNotEquals(hash3, head);
 437 
 438             assertEquals(hash1, head.parents().get(0));
 439             assertFalse(head.isInitialCommit());
 440             assertFalse(head.isMerge());
 441 
 442             var diffs = head.parentDiffs();
 443             assertEquals(1, diffs.size());
 444 
 445             var diff = diffs.get(0);
 446             assertEquals(hash1, diff.from());
 447             assertEquals(head.hash(), diff.to());
 448 
 449             assertEquals(2, diff.removed());
 450             assertEquals(0, diff.modified());
 451             assertEquals(0, diff.added());
 452         }
 453     }
 454 
 455     @ParameterizedTest
 456     @EnumSource(VCS.class)
 457     void testSquash(VCS vcs) throws IOException {
 458         try (var dir = new TemporaryDirectory()) {
 459             var r = Repository.init(dir.path(), vcs);
 460 
 461             var readme = dir.path().resolve("README");
 462             Files.write(readme, List.of("Hello, readme!"));
 463 
 464             r.add(readme);
 465             var hash1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 466 
 467             Files.write(readme, List.of("Another line"), WRITE, APPEND);
 468             r.add(readme);
 469             var hash2 = r.commit("Modify README", "duke", "duke@openjdk.java.net");
 470 
 471             Files.write(readme, List.of("A final line"), WRITE, APPEND);
 472             r.add(readme);
 473             var hash3 = r.commit("Modify README again", "duke", "duke@openjdk.java.net");
 474 
 475             var refspec = vcs == VCS.GIT ? r.head().hex() : r.head().hex() + ":0";
 476             assertEquals(3, r.commits(refspec).asList().size());
 477 
 478             r.checkout(hash1, false);
 479             r.squash(hash3);
 480             r.commit("Squashed commits 2 and 3", "duke", "duke@openjdk.java.net");
 481 
 482             refspec = vcs == VCS.GIT ? r.head().hex() : r.head().hex() + ":0";
 483             var commits = r.commits(refspec).asList();
 484             assertEquals(2, commits.size());
 485 
 486             assertEquals(hash1, commits.get(1).hash());
 487 
 488             var head = commits.get(0);
 489             assertNotEquals(hash2, head);
 490             assertNotEquals(hash3, head);
 491 
 492             assertEquals(hash1, head.parents().get(0));
 493             assertFalse(head.isInitialCommit());
 494             assertFalse(head.isMerge());
 495 
 496             var diffs = head.parentDiffs();
 497             assertEquals(1, diffs.size());
 498 
 499             var diff = diffs.get(0);
 500             assertEquals(hash1, diff.from());
 501             assertEquals(head.hash(), diff.to());
 502 
 503             assertEquals(0, diff.removed());
 504             assertEquals(0, diff.modified());
 505             assertEquals(2, diff.added());
 506 
 507             var patches = diff.patches();
 508             assertEquals(1, patches.size());
 509 
 510             var patch = patches.get(0).asTextualPatch();
 511             assertTrue(patch.status().isModified());
 512             assertEquals(Path.of("README"), patch.source().path().get());
 513             assertTrue(patch.source().type().get().isRegularNonExecutable());
 514             assertEquals(Path.of("README"), patch.target().path().get());
 515             assertTrue(patch.target().type().get().isRegularNonExecutable());
 516 
 517             var hunks = patch.hunks();
 518             assertEquals(1, hunks.size());
 519 
 520             var hunk = hunks.get(0);
 521             assertEquals(new Range(2, 0), hunk.source().range());
 522             assertEquals(new Range(2, 2), hunk.target().range());
 523 
 524             assertLinesEquals(List.of(), hunk.source().lines());
 525             assertLinesEquals(List.of("Another line", "A final line"), hunk.target().lines());
 526         }
 527     }
 528 
 529     @ParameterizedTest
 530     @EnumSource(VCS.class)
 531     void testMergeBase(VCS vcs) throws IOException {
 532         try (var dir = new TemporaryDirectory()) {
 533             var r = Repository.init(dir.path(), vcs);
 534 
 535             var readme = dir.path().resolve("README");
 536             Files.write(readme, List.of("Hello, readme!"));
 537 
 538             r.add(readme);
 539             var hash1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 540 
 541             Files.write(readme, List.of("Another line"), WRITE, APPEND);
 542             r.add(readme);
 543             var hash2 = r.commit("Modify README", "duke", "duke@openjdk.java.net");
 544 
 545             r.checkout(hash1, false);
 546             Files.write(readme, List.of("A conflicting line"), WRITE, APPEND);
 547             r.add(readme);
 548             var hash3 = r.commit("Branching README modification", "duke", "duke@openjdk.java.net");
 549 
 550             assertEquals(hash1, r.mergeBase(hash2, hash3));
 551         }
 552     }
 553 
 554     @ParameterizedTest
 555     @EnumSource(VCS.class)
 556     void testRebase(VCS vcs) throws IOException {
 557         try (var dir = new TemporaryDirectory()) {
 558             var r = Repository.init(dir.path(), vcs);
 559 
 560             var readme = dir.path().resolve("README");
 561             Files.write(readme, List.of("Hello, readme!"));
 562 
 563             r.add(readme);
 564             var hash1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 565 
 566             Files.write(readme, List.of("Another line"), WRITE, APPEND);
 567             r.add(readme);
 568             var hash2 = r.commit("Modify README", "duke", "duke@openjdk.java.net");
 569 
 570             r.checkout(hash1, false);
 571 
 572             var contributing = dir.path().resolve("CONTRIBUTING");
 573             Files.write(contributing, List.of("Keep the patches coming"));
 574             r.add(contributing);
 575             var hash3 = r.commit("Add independent change", "duke", "duke@openjdk.java.net");
 576 
 577             var committerName = vcs == VCS.GIT ? "bot" : "duke";
 578             var committerEmail = vcs == VCS.GIT ? "bot@openjdk.java.net" : "duke@openjdk.java.net";
 579             r.rebase(hash2, committerName, committerEmail);
 580 
 581             var refspec = vcs == VCS.GIT ? r.head().hex() : r.head().hex() + ":0";
 582             var commits = r.commits(refspec).asList();
 583             assertEquals(3, commits.size());
 584             assertEquals(hash2, commits.get(1).hash());
 585             assertEquals(hash1, commits.get(2).hash());
 586 
 587             assertEquals("duke", commits.get(0).author().name());
 588             assertEquals("duke@openjdk.java.net", commits.get(0).author().email());
 589             assertEquals(committerName, commits.get(0).committer().name());
 590             assertEquals(committerEmail, commits.get(0).committer().email());
 591 
 592             assertEquals("duke", commits.get(1).author().name());
 593             assertEquals("duke@openjdk.java.net", commits.get(1).author().email());
 594             assertEquals("duke", commits.get(1).committer().name());
 595             assertEquals("duke@openjdk.java.net", commits.get(1).committer().email());
 596 
 597             assertEquals("duke", commits.get(2).author().name());
 598             assertEquals("duke@openjdk.java.net", commits.get(2).author().email());
 599             assertEquals("duke", commits.get(2).committer().name());
 600             assertEquals("duke@openjdk.java.net", commits.get(2).committer().email());
 601 
 602             var head = commits.get(0);
 603             assertEquals(hash2, head.parents().get(0));
 604             assertEquals(List.of("Add independent change"), head.message());
 605 
 606             var diffs = head.parentDiffs();
 607             assertEquals(1, diffs.size());
 608             var diff = diffs.get(0);
 609 
 610             assertEquals(0, diff.removed());
 611             assertEquals(0, diff.modified());
 612             assertEquals(1, diff.added());
 613 
 614             var patches = diff.patches();
 615             assertEquals(1, patches.size());
 616             var patch = patches.get(0).asTextualPatch();
 617             assertEquals(Path.of("CONTRIBUTING"), patch.target().path().get());
 618 
 619             var hunks = patch.hunks();
 620             assertEquals(1, hunks.size());
 621             var hunk = hunks.get(0);
 622             assertLinesEquals(List.of("Keep the patches coming"), hunk.target().lines());
 623         }
 624     }
 625 
 626     @ParameterizedTest
 627     @EnumSource(VCS.class)
 628     void testInitializedRepositoryIsEmpty(VCS vcs) throws IOException {
 629         try (var dir = new TemporaryDirectory()) {
 630             var r = Repository.init(dir.path(), vcs);
 631             assertTrue(r.isEmpty());
 632         }
 633     }
 634 
 635     @ParameterizedTest
 636     @EnumSource(VCS.class)
 637     void testRepositoryWithCommitIsNonEmpty(VCS vcs) throws IOException {
 638         try (var dir = new TemporaryDirectory()) {
 639             var r = Repository.init(dir.path(), vcs);
 640 
 641             var readme = dir.path().resolve("README");
 642             Files.write(readme, List.of("Hello, readme!"));
 643 
 644             r.add(readme);
 645             r.commit("Add README", "duke", "duke@openjdk.java.net");
 646 
 647             assertFalse(r.isEmpty());
 648         }
 649     }
 650 
 651     @ParameterizedTest
 652     @EnumSource(VCS.class)
 653     void testEmptyRepositoryIsHealthy(VCS vcs) throws IOException {
 654         try (var dir = new TemporaryDirectory()) {
 655             var r = Repository.init(dir.path(), vcs);
 656             assertTrue(r.isHealthy());
 657         }
 658     }
 659 
 660     @ParameterizedTest
 661     @EnumSource(VCS.class)
 662     void testNonEmptyRepositoryIsHealthy(VCS vcs) throws IOException {
 663         try (var dir = new TemporaryDirectory()) {
 664             var r = Repository.init(dir.path(), vcs);
 665 
 666             var readme = dir.path().resolve("README");
 667             Files.write(readme, List.of("Hello, readme!"));
 668 
 669             r.add(readme);
 670             r.commit("Add README", "duke", "duke@openjdk.java.net");
 671 
 672             assertTrue(r.isHealthy());
 673         }
 674     }
 675 
 676     @ParameterizedTest
 677     @EnumSource(VCS.class)
 678     void testNonCheckedOutRepositoryIsHealthy(VCS vcs) throws IOException {
 679         try (var dir1 = new TemporaryDirectory();
 680              var dir2 = new TemporaryDirectory()) {
 681             var r1 = Repository.init(dir1.path(), vcs);
 682 
 683             var readme = dir1.path().resolve("README");
 684             Files.write(readme, List.of("Hello, readme!"));
 685 
 686             r1.add(readme);
 687             var hash = r1.commit("Add README", "duke", "duke@openjdk.java.net");
 688             r1.tag(hash, "tag", "tagging", "duke", "duke@openjdk.java.net");
 689 
 690             var r2 = Repository.init(dir2.path(), vcs);
 691             r2.fetch(r1.root().toUri(), r1.defaultBranch().name());
 692 
 693             assertTrue(r2.isHealthy());
 694         }
 695     }
 696 
 697     @ParameterizedTest
 698     @EnumSource(VCS.class)
 699     void testBranchesOnEmptyRepository(VCS vcs) throws IOException {
 700         try (var dir = new TemporaryDirectory()) {
 701             var r = Repository.init(dir.path(), vcs);
 702             var expected = vcs == VCS.GIT ? List.of() : List.of(new Branch("default"));
 703             assertEquals(List.of(), r.branches());
 704         }
 705     }
 706 
 707     @ParameterizedTest
 708     @EnumSource(VCS.class)
 709     void testBranchesOnNonEmptyRepository(VCS vcs) throws IOException {
 710         try (var dir = new TemporaryDirectory()) {
 711             var r = Repository.init(dir.path(), vcs);
 712 
 713             var readme = dir.path().resolve("README");
 714             Files.write(readme, List.of("Hello, readme!"));
 715 
 716             r.add(readme);
 717             r.commit("Add README", "duke", "duke@openjdk.java.net");
 718 
 719             assertEquals(List.of(r.defaultBranch()), r.branches());
 720         }
 721     }
 722 
 723     @ParameterizedTest
 724     @EnumSource(VCS.class)
 725     void testTagsOnEmptyRepository(VCS vcs) throws IOException {
 726         try (var dir = new TemporaryDirectory()) {
 727             var r = Repository.init(dir.path(), vcs);
 728             var expected = vcs == VCS.GIT ? List.of() : List.of(new Tag("tip"));
 729             assertEquals(expected, r.tags());
 730         }
 731     }
 732 
 733     @ParameterizedTest
 734     @EnumSource(VCS.class)
 735     void testTagsOnNonEmptyRepository(VCS vcs) throws IOException {
 736         try (var dir = new TemporaryDirectory()) {
 737             var r = Repository.init(dir.path(), vcs);
 738 
 739             var readme = dir.path().resolve("README");
 740             Files.write(readme, List.of("Hello, readme!"));
 741 
 742             r.add(readme);
 743             r.commit("Add README", "duke", "duke@openjdk.java.net");
 744 
 745             var expected = vcs == VCS.GIT ? List.of() : List.of(new Tag("tip"));
 746             assertEquals(expected, r.tags());
 747         }
 748     }
 749 
 750     @ParameterizedTest
 751     @EnumSource(VCS.class)
 752     void testFetchAndPush(VCS vcs) throws IOException {
 753         try (var dir = new TemporaryDirectory()) {
 754             var upstream = Repository.init(dir.path(), vcs);
 755 
 756             if (vcs == VCS.GIT) {
 757                 Files.write(upstream.root().resolve(".git").resolve("config"),
 758                             List.of("[receive]", "denyCurrentBranch=ignore"),
 759                             WRITE, APPEND);
 760             }
 761 
 762             var readme = dir.path().resolve("README");
 763             Files.write(readme, List.of("Hello, readme!"));
 764 
 765             upstream.add(readme);
 766             upstream.commit("Add README", "duke", "duke@openjdk.java.net");
 767 
 768             try (var dir2 = new TemporaryDirectory()) {
 769                 var downstream = Repository.init(dir2.path(), vcs);
 770 
 771                  // note: forcing unix path separators for URI
 772                 var upstreamURI = URI.create("file:///" + dir.toString().replace('\\', '/'));
 773 
 774                 var fetchHead = downstream.fetch(upstreamURI, downstream.defaultBranch().name());
 775                 downstream.checkout(fetchHead, false);
 776 
 777                 var downstreamReadme = dir2.path().resolve("README");
 778                 Files.write(downstreamReadme, List.of("Downstream change"), WRITE, APPEND);
 779 
 780                 downstream.add(downstreamReadme);
 781                 var head = downstream.commit("Modify README", "duke", "duke@openjdk.java.net");
 782 
 783                 downstream.push(head, upstreamURI, downstream.defaultBranch().name());
 784             }
 785 
 786             upstream.checkout(upstream.resolve(upstream.defaultBranch().name()).get(), false);
 787 
 788             var commits = upstream.commits().asList();
 789             assertEquals(2, commits.size());
 790         }
 791     }
 792 
 793     @ParameterizedTest
 794     @EnumSource(VCS.class)
 795     void testClean(VCS vcs) throws IOException {
 796         try (var dir = new TemporaryDirectory()) {
 797             var r = Repository.init(dir.path(), vcs);
 798             r.clean();
 799 
 800             var readme = dir.path().resolve("README");
 801             Files.write(readme, List.of("Hello, readme!"));
 802 
 803             r.add(readme);
 804             var hash1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 805 
 806             r.clean();
 807 
 808             assertEquals(hash1, r.head());
 809 
 810             Files.write(readme, List.of("A random change"), WRITE, APPEND);
 811 
 812             r.clean();
 813 
 814             assertEquals(List.of("Hello, readme!"), Files.readAllLines(readme));
 815 
 816             var untracked = dir.path().resolve("UNTRACKED");
 817             Files.write(untracked, List.of("Random text"));
 818 
 819             r.clean();
 820 
 821             assertFalse(Files.exists(untracked));
 822 
 823             // Mercurial cannot currently deal with this situation
 824             if (vcs != VCS.HG) {
 825                 var subRepo = Repository.init(dir.path().resolve("submodule"), vcs);
 826                 var subRepoFile = subRepo.root().resolve("file.txt");
 827                 Files.write(subRepoFile, List.of("Looks like a file in a submodule"));
 828 
 829                 r.clean();
 830 
 831                 assertFalse(Files.exists(subRepoFile));
 832             }
 833         }
 834     }
 835 
 836     @ParameterizedTest
 837     @EnumSource(VCS.class)
 838     void testCleanIgnored(VCS vcs) throws IOException {
 839         try (var dir = new TemporaryDirectory()) {
 840             var r = Repository.init(dir.path(), vcs);
 841             r.clean();
 842 
 843             var readme = dir.path().resolve("README");
 844             Files.write(readme, List.of("Hello, readme!"));
 845             Files.write(dir.path().resolve(".gitignore"), List.of("*.txt"));
 846             Files.write(dir.path().resolve(".hgignore"), List.of(".*txt"));
 847 
 848             r.add(readme);
 849             var hash1 = r.commit("Add README", "duke", "duke@openjdk.java.net");
 850 
 851             var ignored = dir.path().resolve("ignored.txt");
 852             Files.write(ignored, List.of("Random text"));
 853 
 854             r.clean();
 855 
 856             assertFalse(Files.exists(ignored));
 857         }
 858     }
 859 
 860     @ParameterizedTest
 861     @EnumSource(VCS.class)
 862     void testDiffBetweenCommits(VCS vcs) throws IOException {
 863         try (var dir = new TemporaryDirectory()) {
 864             var r = Repository.init(dir.path(), vcs);
 865 
 866             var readme = dir.path().resolve("README");
 867             Files.write(readme, List.of("Hello, readme!"));
 868 
 869             r.add(readme);
 870             var first = r.commit("Add README", "duke", "duke@openjdk.java.net");
 871 
 872             Files.write(readme, List.of("One more line"), WRITE, APPEND);
 873             r.add(readme);
 874             var second = r.commit("Add one more line", "duke", "duke@openjdk.java.net");
 875 
 876             var diff = r.diff(first, second);
 877             assertEquals(first, diff.from());
 878             assertEquals(second, diff.to());
 879 
 880             var patches = diff.patches();
 881             assertEquals(1, patches.size());
 882 
 883             var patch = patches.get(0).asTextualPatch();
 884             assertEquals(Path.of("README"), patch.source().path().get());
 885             assertEquals(Path.of("README"), patch.target().path().get());
 886             assertTrue(patch.source().type().get().isRegularNonExecutable());
 887             assertTrue(patch.target().type().get().isRegularNonExecutable());
 888             assertTrue(patch.status().isModified());
 889 
 890             var hunks = patch.hunks();
 891             assertEquals(1, hunks.size());
 892 
 893             var hunk = hunks.get(0);
 894             assertEquals(2, hunk.source().range().start());
 895             assertEquals(0, hunk.source().range().count());
 896             assertEquals(0, hunk.source().lines().size());
 897 
 898             assertEquals(2, hunk.target().range().start());
 899             assertEquals(1, hunk.target().range().count());
 900             assertLinesEquals(List.of("One more line"), hunk.target().lines());
 901 
 902             assertEquals(1, hunk.added());
 903             assertEquals(0, hunk.removed());
 904             assertEquals(0, hunk.modified());
 905         }
 906     }
 907 
 908     @ParameterizedTest
 909     @EnumSource(VCS.class)
 910     void testDiffBetweenCommitsWithMultiplePatches(VCS vcs) throws IOException {
 911         try (var dir = new TemporaryDirectory()) {
 912             var r = Repository.init(dir.path(), vcs);
 913 
 914             var readme = dir.path().resolve("README");
 915             Files.write(readme, List.of("Hello, readme!"));
 916 
 917             var building = dir.path().resolve("BUILDING");
 918             Files.write(building, List.of("make"));
 919 
 920             r.add(readme);
 921             r.add(building);
 922             var first = r.commit("Add README and BUILDING", "duke", "duke@openjdk.java.net");
 923 
 924             Files.write(readme, List.of("Hello, Skara!"), WRITE, TRUNCATE_EXISTING);
 925             Files.write(building, List.of("make images"), WRITE, TRUNCATE_EXISTING);
 926             r.add(readme);
 927             r.add(building);
 928             var second = r.commit("Modify README and BUILDING", "duke", "duke@openjdk.java.net");
 929 
 930             var diff = r.diff(first, second);
 931             assertEquals(first, diff.from());
 932             assertEquals(second, diff.to());
 933 
 934             var patches = diff.patches();
 935             assertEquals(2, patches.size());
 936 
 937             var patch1 = patches.get(0).asTextualPatch();
 938             assertEquals(Path.of("BUILDING"), patch1.source().path().get());
 939             assertEquals(Path.of("BUILDING"), patch1.target().path().get());
 940             assertTrue(patch1.source().type().get().isRegularNonExecutable());
 941             assertTrue(patch1.target().type().get().isRegularNonExecutable());
 942             assertTrue(patch1.status().isModified());
 943 
 944             var hunks1 = patch1.hunks();
 945             assertEquals(1, hunks1.size());
 946 
 947             var hunk1 = hunks1.get(0);
 948             assertEquals(1, hunk1.source().range().start());
 949             assertEquals(1, hunk1.source().range().count());
 950             assertLinesEquals(List.of("make"), hunk1.source().lines());
 951 
 952             assertEquals(1, hunk1.target().range().start());
 953             assertEquals(1, hunk1.target().range().count());
 954             assertLinesEquals(List.of("make images"), hunk1.target().lines());
 955 
 956             var patch2 = patches.get(1).asTextualPatch();
 957             assertEquals(Path.of("README"), patch2.source().path().get());
 958             assertEquals(Path.of("README"), patch2.target().path().get());
 959             assertTrue(patch2.source().type().get().isRegularNonExecutable());
 960             assertTrue(patch2.target().type().get().isRegularNonExecutable());
 961             assertTrue(patch2.status().isModified());
 962 
 963             var hunks2 = patch2.hunks();
 964             assertEquals(1, hunks2.size());
 965 
 966             var hunk2 = hunks2.get(0);
 967             assertEquals(1, hunk2.source().range().start());
 968             assertEquals(1, hunk2.source().range().count());
 969             assertLinesEquals(List.of("Hello, readme!"), hunk2.source().lines());
 970 
 971             assertEquals(1, hunk2.target().range().start());
 972             assertEquals(1, hunk2.target().range().count());
 973             assertLinesEquals(List.of("Hello, Skara!"), hunk2.target().lines());
 974         }
 975     }
 976 
 977     @ParameterizedTest
 978     @EnumSource(VCS.class)
 979     void testDiffBetweenCommitsWithMultipleHunks(VCS vcs) throws IOException {
 980         try (var dir = new TemporaryDirectory()) {
 981             var r = Repository.init(dir.path(), vcs);
 982 
 983             var abc = dir.path().resolve("abc.txt");
 984             Files.write(abc, List.of("A", "B", "C"));
 985 
 986             r.add(abc);
 987             var first = r.commit("Added ABC", "duke", "duke@openjdk.java.net");
 988 
 989             Files.write(abc, List.of("1", "2", "B", "3"), WRITE, TRUNCATE_EXISTING);
 990             r.add(abc);
 991             var second = r.commit("Modify A and C", "duke", "duke@openjdk.java.net");
 992 
 993             var diff = r.diff(first, second);
 994             assertEquals(first, diff.from());
 995             assertEquals(second, diff.to());
 996 
 997             var patches = diff.patches();
 998             assertEquals(1, patches.size());
 999 
1000             var patch = patches.get(0).asTextualPatch();
1001             assertEquals(Path.of("abc.txt"), patch.source().path().get());
1002             assertEquals(Path.of("abc.txt"), patch.target().path().get());
1003             assertTrue(patch.source().type().get().isRegularNonExecutable());
1004             assertTrue(patch.target().type().get().isRegularNonExecutable());
1005             assertTrue(patch.status().isModified());
1006 
1007             var hunks = patch.hunks();
1008             assertEquals(2, hunks.size());
1009 
1010             var hunk1 = hunks.get(0);
1011             assertEquals(1, hunk1.source().range().start());
1012             assertEquals(1, hunk1.source().range().count());
1013             assertLinesEquals(List.of("A"), hunk1.source().lines());
1014 
1015             assertEquals(1, hunk1.target().range().start());
1016             assertEquals(2, hunk1.target().range().count());
1017             assertLinesEquals(List.of("1", "2"), hunk1.target().lines());
1018 
1019             assertEquals(1, hunk1.added());
1020             assertEquals(0, hunk1.removed());
1021             assertEquals(1, hunk1.modified());
1022 
1023             var hunk2 = hunks.get(1);
1024             assertEquals(3, hunk2.source().range().start());
1025             assertEquals(1, hunk2.source().range().count());
1026             assertLinesEquals(List.of("C"), hunk2.source().lines());
1027 
1028             assertEquals(4, hunk2.target().range().start());
1029             assertEquals(1, hunk2.target().range().count());
1030             assertLinesEquals(List.of("3"), hunk2.target().lines());
1031 
1032             assertEquals(0, hunk2.added());
1033             assertEquals(0, hunk2.removed());
1034             assertEquals(1, hunk2.modified());
1035         }
1036     }
1037 
1038     @ParameterizedTest
1039     @EnumSource(VCS.class)
1040     void testDiffWithRemoval(VCS vcs) throws IOException {
1041         try (var dir = new TemporaryDirectory()) {
1042             var r = Repository.init(dir.path(), vcs);
1043 
1044             var readme = dir.path().resolve("README");
1045             Files.write(readme, List.of("Hello, world!"));
1046 
1047             r.add(readme);
1048             var first = r.commit("Added README", "duke", "duke@openjdk.java.net");
1049 
1050             Files.delete(readme);
1051             r.remove(readme);
1052             var second = r.commit("Removed README", "duke", "duke@openjdk.java.net");
1053 
1054             var diff = r.diff(first, second);
1055             assertEquals(first, diff.from());
1056             assertEquals(second, diff.to());
1057 
1058             var patches = diff.patches();
1059             assertEquals(1, patches.size());
1060 
1061             var patch = patches.get(0).asTextualPatch();
1062             assertEquals(Path.of("README"), patch.source().path().get());
1063             assertTrue(patch.target().path().isEmpty());
1064             assertTrue(patch.source().type().get().isRegularNonExecutable());
1065             assertTrue(patch.target().type().isEmpty());
1066             assertTrue(patch.status().isDeleted());
1067 
1068             var hunks = patch.hunks();
1069             assertEquals(1, hunks.size());
1070 
1071             var hunk = hunks.get(0);
1072             assertEquals(1, hunk.source().range().start());
1073             assertEquals(1, hunk.source().range().count());
1074             assertLinesEquals(List.of("Hello, world!"), hunk.source().lines());
1075 
1076             assertEquals(0, hunk.target().range().start());
1077             assertEquals(0, hunk.target().range().count());
1078             assertLinesEquals(List.of(), hunk.target().lines());
1079 
1080             assertEquals(0, hunk.added());
1081             assertEquals(1, hunk.removed());
1082             assertEquals(0, hunk.modified());
1083         }
1084     }
1085 
1086     @ParameterizedTest
1087     @EnumSource(VCS.class)
1088     void testDiffWithAddition(VCS vcs) throws IOException {
1089         try (var dir = new TemporaryDirectory()) {
1090             var r = Repository.init(dir.path(), vcs);
1091 
1092             var readme = dir.path().resolve("README");
1093             Files.write(readme, List.of("Hello, world!"));
1094 
1095             r.add(readme);
1096             var first = r.commit("Added README", "duke", "duke@openjdk.java.net");
1097 
1098             var building = dir.path().resolve("BUILDING");
1099             Files.write(building, List.of("make"));
1100             r.add(building);
1101             var second = r.commit("Added BUILDING", "duke", "duke@openjdk.java.net");
1102 
1103             var diff = r.diff(first, second);
1104             assertEquals(first, diff.from());
1105             assertEquals(second, diff.to());
1106 
1107             var patches = diff.patches();
1108             assertEquals(1, patches.size());
1109 
1110             var patch = patches.get(0).asTextualPatch();
1111             assertTrue(patch.source().path().isEmpty());
1112             assertEquals(Path.of("BUILDING"), patch.target().path().get());
1113             assertTrue(patch.source().type().isEmpty());
1114             assertTrue(patch.target().type().get().isRegularNonExecutable());
1115             assertTrue(patch.status().isAdded());
1116 
1117             var hunks = patch.hunks();
1118             assertEquals(1, hunks.size());
1119 
1120             var hunk = hunks.get(0);
1121             assertEquals(0, hunk.source().range().start());
1122             assertEquals(0, hunk.source().range().count());
1123             assertLinesEquals(List.of(), hunk.source().lines());
1124 
1125             assertEquals(1, hunk.target().range().start());
1126             assertEquals(1, hunk.target().range().count());
1127             assertLinesEquals(List.of("make"), hunk.target().lines());
1128 
1129             assertEquals(1, hunk.added());
1130             assertEquals(0, hunk.removed());
1131             assertEquals(0, hunk.modified());
1132         }
1133     }
1134 
1135     @ParameterizedTest
1136     @EnumSource(VCS.class)
1137     void testDiffWithWorkingDir(VCS vcs) throws IOException {
1138         try (var dir = new TemporaryDirectory()) {
1139             var r = Repository.init(dir.path(), vcs);
1140 
1141             var readme = dir.path().resolve("README");
1142             Files.write(readme, List.of("Hello, world!"));
1143 
1144             r.add(readme);
1145             var first = r.commit("Added README", "duke", "duke@openjdk.java.net");
1146 
1147             Files.write(readme, List.of("One more line"), WRITE, APPEND);
1148             var diff = r.diff(first);
1149 
1150             assertEquals(first, diff.from());
1151             assertNull(diff.to());
1152 
1153             var patches = diff.patches();
1154             assertEquals(1, patches.size());
1155 
1156             var patch = patches.get(0).asTextualPatch();
1157             assertEquals(Path.of("README"), patch.source().path().get());
1158             assertEquals(Path.of("README"), patch.target().path().get());
1159             assertTrue(patch.source().type().get().isRegularNonExecutable());
1160             assertTrue(patch.target().type().get().isRegularNonExecutable());
1161             assertTrue(patch.status().isModified());
1162 
1163             var hunks = patch.hunks();
1164             assertEquals(1, hunks.size());
1165 
1166             var hunk = hunks.get(0);
1167             assertEquals(2, hunk.source().range().start());
1168             assertEquals(0, hunk.source().range().count());
1169             assertLinesEquals(List.of(), hunk.source().lines());
1170 
1171             assertEquals(2, hunk.target().range().start());
1172             assertEquals(1, hunk.target().range().count());
1173             assertLinesEquals(List.of("One more line"), hunk.target().lines());
1174 
1175             assertEquals(1, hunk.added());
1176             assertEquals(0, hunk.removed());
1177             assertEquals(0, hunk.modified());
1178         }
1179     }
1180 
1181     @ParameterizedTest
1182     @EnumSource(VCS.class)
1183     void testCommitMetadata(VCS vcs) throws IOException {
1184         try (var dir = new TemporaryDirectory()) {
1185             var r = Repository.init(dir.path(), vcs);
1186 
1187             var readme = dir.path().resolve("README");
1188             Files.write(readme, List.of("Hello, world!"));
1189             r.add(readme);
1190             var first = r.commit("Added README", "duke", "duke@openjdk.java.net");
1191 
1192             Files.write(readme, List.of("One more line"), WRITE, APPEND);
1193             r.add(readme);
1194             var second = r.commit("Modified README", "duke", "duke@openjdk.java.net");
1195 
1196             var metadata = r.commitMetadata();
1197             assertEquals(2, metadata.size());
1198 
1199             assertEquals(first, metadata.get(0).hash());
1200             assertEquals(List.of("Added README"), metadata.get(0).message());
1201 
1202             assertEquals(second, metadata.get(1).hash());
1203             assertEquals(List.of("Modified README"), metadata.get(1).message());
1204         }
1205     }
1206 
1207     @ParameterizedTest
1208     @EnumSource(VCS.class)
1209     void testTrivialMerge(VCS vcs) throws IOException {
1210         try (var dir = new TemporaryDirectory()) {
1211             var r = Repository.init(dir.path(), vcs);
1212 
1213             var readme = dir.path().resolve("README");
1214             Files.write(readme, List.of("Hello, world!"));
1215             r.add(readme);
1216             var first = r.commit("Added README", "duke", "duke@openjdk.java.net");
1217 
1218             Files.write(readme, List.of("One more line"), WRITE, APPEND);
1219             r.add(readme);
1220             var second = r.commit("Modified README", "duke", "duke@openjdk.java.net");
1221 
1222             r.checkout(first, false);
1223 
1224             var contributing = dir.path().resolve("CONTRIBUTING");
1225             Files.write(contributing, List.of("Send those patches!"));
1226             r.add(contributing);
1227             var third = r.commit("Added contributing", "duke", "duke@openjdk.java.net");
1228 
1229             r.merge(second);
1230             r.commit("Merge", "duke", "duke@openjdk.java.net");
1231 
1232             var refspec = vcs == VCS.GIT ? r.head().hex() : r.head().hex() + ":0";
1233             var commits = r.commits(refspec).asList();
1234 
1235             assertEquals(4, commits.size());
1236 
1237             var merge = commits.get(0);
1238             assertEquals(List.of("Merge"), merge.message());
1239 
1240             var parents = new HashSet<>(merge.parents());
1241             assertEquals(2, parents.size());
1242             assertTrue(parents.contains(second));
1243             assertTrue(parents.contains(third));
1244 
1245             var diffs = merge.parentDiffs();
1246             assertEquals(2, diffs.size());
1247 
1248             var diff1 = diffs.get(0);
1249             assertEquals(merge.hash(), diff1.to());
1250             assertEquals(0, diff1.patches().size());
1251             assertTrue(parents.contains(diff1.from()));
1252 
1253             var diff2 = diffs.get(1);
1254             assertEquals(merge.hash(), diff2.to());
1255             assertEquals(0, diff2.patches().size());
1256             assertTrue(parents.contains(diff2.from()));
1257         }
1258     }
1259 
1260     @ParameterizedTest
1261     @EnumSource(VCS.class)
1262     void testMergeWithEdit(VCS vcs) throws IOException {
1263         try (var dir = new TemporaryDirectory()) {
1264             var r = Repository.init(dir.path(), vcs);
1265 
1266             var readme = dir.path().resolve("README");
1267             Files.write(readme, List.of("Hello, world!"));
1268             r.add(readme);
1269             var first = r.commit("Added README", "duke", "duke@openjdk.java.net");
1270 
1271             Files.write(readme, List.of("One more line"), WRITE, APPEND);
1272             r.add(readme);
1273             var second = r.commit("Modified README", "duke", "duke@openjdk.java.net");
1274 
1275             r.checkout(first, false);
1276 
1277             var contributing = dir.path().resolve("CONTRIBUTING");
1278             Files.write(contributing, List.of("Send those patches!"));
1279             r.add(contributing);
1280             var third = r.commit("Added contributing", "duke", "duke@openjdk.java.net");
1281 
1282             r.merge(second);
1283 
1284             Files.write(readme, List.of("One last line"), WRITE, APPEND);
1285             r.add(readme);
1286             r.commit("Merge", "duke", "duke@openjdk.java.net");
1287 
1288             var refspec = vcs == VCS.GIT ? r.head().hex() : r.head().hex() + ":0";
1289             var commits = r.commits(refspec).asList();
1290 
1291             assertEquals(4, commits.size());
1292 
1293             var merge = commits.get(0);
1294             assertEquals(List.of("Merge"), merge.message());
1295 
1296             var parents = new HashSet<>(merge.parents());
1297             assertEquals(2, parents.size());
1298             assertTrue(parents.contains(second));
1299             assertTrue(parents.contains(third));
1300 
1301             var diffs = merge.parentDiffs();
1302             assertEquals(2, diffs.size());
1303 
1304             var secondDiff = diffs.stream().filter(d -> d.from().equals(second)).findFirst().get();
1305             assertEquals(merge.hash(), secondDiff.to());
1306             assertEquals(1, secondDiff.patches().size());
1307             var secondPatch = secondDiff.patches().get(0).asTextualPatch();
1308 
1309             assertEquals(Path.of("README"), secondPatch.source().path().get());
1310             assertEquals(Path.of("README"), secondPatch.target().path().get());
1311             assertTrue(secondPatch.status().isModified());
1312             assertEquals(1, secondPatch.hunks().size());
1313 
1314             var secondHunk = secondPatch.hunks().get(0);
1315             assertLinesEquals(List.of(), secondHunk.source().lines());
1316             assertLinesEquals(List.of("One last line"), secondHunk.target().lines());
1317 
1318             assertEquals(3, secondHunk.source().range().start());
1319             assertEquals(0, secondHunk.source().range().count());
1320             assertEquals(3, secondHunk.target().range().start());
1321             assertEquals(1, secondHunk.target().range().count());
1322 
1323             var thirdDiff = diffs.stream().filter(d -> d.from().equals(third)).findFirst().get();
1324             assertEquals(merge.hash(), thirdDiff.to());
1325             assertEquals(1, thirdDiff.patches().size());
1326             var thirdPatch = thirdDiff.patches().get(0).asTextualPatch();
1327 
1328             assertEquals(Path.of("README"), thirdPatch.source().path().get());
1329             assertEquals(Path.of("README"), thirdPatch.target().path().get());
1330             assertTrue(thirdPatch.status().isModified());
1331             assertEquals(1, thirdPatch.hunks().size());
1332 
1333             var thirdHunk = thirdPatch.hunks().get(0);
1334             assertLinesEquals(List.of(), thirdHunk.source().lines());
1335             assertLinesEquals(List.of("One more line", "One last line"), thirdHunk.target().lines());
1336 
1337             assertEquals(2, thirdHunk.source().range().start());
1338             assertEquals(0, thirdHunk.source().range().count());
1339             assertEquals(2, thirdHunk.target().range().start());
1340             assertEquals(2, thirdHunk.target().range().count());
1341         }
1342     }
1343 
1344     @ParameterizedTest
1345     @EnumSource(VCS.class)
1346     void testDefaultBranch(VCS vcs) throws IOException {
1347         try (var dir = new TemporaryDirectory()) {
1348             var r = Repository.init(dir.path(), vcs);
1349             var expected = vcs == VCS.GIT ? "master" : "default";
1350             assertEquals(expected, r.defaultBranch().name());
1351         }
1352     }
1353 
1354     @ParameterizedTest
1355     @EnumSource(VCS.class)
1356     void testPaths(VCS vcs) throws IOException {
1357         try (var dir = new TemporaryDirectory()) {
1358             var r = Repository.init(dir.path(), vcs);
1359             var remote = vcs == VCS.GIT ? "origin" : "default";
1360             r.setPaths(remote, "http://pull", "http://push");
1361             assertEquals("http://pull", r.pullPath(remote));
1362             assertEquals("http://push", r.pushPath(remote));
1363         }
1364     }
1365 
1366     @ParameterizedTest
1367     @EnumSource(VCS.class)
1368     void testIsValidRevisionRange(VCS vcs) throws IOException {
1369         try (var dir = new TemporaryDirectory()) {
1370             var r = Repository.init(dir.path(), vcs);
1371             assertFalse(r.isValidRevisionRange("foo"));
1372 
1373             var readme = dir.path().resolve("README");
1374             Files.write(readme, List.of("Hello, world!"));
1375             r.add(readme);
1376             r.commit("Added README", "duke", "duke@openjdk.java.net");
1377 
1378             assertTrue(r.isValidRevisionRange(r.defaultBranch().toString()));
1379         }
1380     }
1381 
1382     @ParameterizedTest
1383     @EnumSource(VCS.class)
1384     void testDefaultTag(VCS vcs) throws IOException {
1385         try (var dir = new TemporaryDirectory()) {
1386             var r = Repository.init(dir.path(), vcs);
1387             var expected = vcs == VCS.GIT ? Optional.empty() : Optional.of(new Tag("tip"));
1388             assertEquals(expected, r.defaultTag());
1389         }
1390     }
1391 
1392     @ParameterizedTest
1393     @EnumSource(VCS.class)
1394     void testTag(VCS vcs) throws IOException {
1395         try (var dir = new TemporaryDirectory()) {
1396             var r = Repository.init(dir.path(), vcs);
1397 
1398             var readme = dir.path().resolve("README");
1399             Files.write(readme, List.of("Hello, world!"));
1400             r.add(readme);
1401             var first = r.commit("Added README", "duke", "duke@openjdk.java.net");
1402 
1403             r.tag(first, "test", "Tagging test", "duke", "duke@openjdk.java.net");
1404             var defaultTag = r.defaultTag().orElse(null);
1405             var nonDefaultTags = r.tags().stream()
1406                                   .filter(tag -> !tag.equals(defaultTag))
1407                                   .map(Tag::toString)
1408                                   .collect(Collectors.toList());
1409             assertEquals(List.of("test"), nonDefaultTags);
1410         }
1411     }
1412 
1413     @ParameterizedTest
1414     @EnumSource(VCS.class)
1415     void testIsClean(VCS vcs) throws IOException {
1416         try (var dir = new TemporaryDirectory()) {
1417             var r = Repository.init(dir.path(), vcs);
1418             assertTrue(r.isClean());
1419 
1420             var readme = dir.path().resolve("README");
1421             Files.write(readme, List.of("Hello, world!"));
1422             assertFalse(r.isClean());
1423 
1424             r.add(readme);
1425             assertFalse(r.isClean());
1426 
1427             r.commit("Added README", "duke", "duke@openjdk.java.net");
1428             assertTrue(r.isClean());
1429 
1430             Files.delete(readme);
1431             assertFalse(r.isClean());
1432 
1433             Files.write(readme, List.of("Hello, world!"));
1434             assertTrue(r.isClean());
1435         }
1436     }
1437 
1438     @ParameterizedTest
1439     @EnumSource(VCS.class)
1440     void testShowOnExecutableFiles(VCS vcs) throws IOException {
1441         try (var dir = new TemporaryDirectory()) {
1442             var r = Repository.init(dir.path(), vcs);
1443             assertTrue(r.isClean());
1444 
1445             var readOnlyExecutableFile = dir.path().resolve("hello.sh");
1446             Files.write(readOnlyExecutableFile, List.of("echo 'hello'"));
1447             if (readOnlyExecutableFile.getFileSystem().supportedFileAttributeViews().contains("posix")) {
1448                 var permissions = PosixFilePermissions.fromString("r-xr-xr-x");
1449                 Files.setPosixFilePermissions(readOnlyExecutableFile, permissions);
1450             }
1451             r.add(readOnlyExecutableFile);
1452             var hash = r.commit("Added read only executable file", "duke", "duke@openjdk.java.net");
1453             assertEquals(Optional.of(List.of("echo 'hello'")), r.lines(readOnlyExecutableFile, hash));
1454 
1455             var readWriteExecutableFile = dir.path().resolve("goodbye.sh");
1456             Files.write(readWriteExecutableFile, List.of("echo 'goodbye'"));
1457             if (readOnlyExecutableFile.getFileSystem().supportedFileAttributeViews().contains("posix")) {
1458                 var permissions = PosixFilePermissions.fromString("rwxrwxrwx");
1459                 Files.setPosixFilePermissions(readWriteExecutableFile, permissions);
1460             }
1461             r.add(readWriteExecutableFile);
1462             var hash2 = r.commit("Added read-write executable file", "duke", "duke@openjdk.java.net");
1463             assertEquals(Optional.of(List.of("echo 'goodbye'")), r.lines(readWriteExecutableFile, hash2));
1464         }
1465     }
1466 
1467     @Test
1468     void testGetAndExistsOnNonExistingDirectory() throws IOException {
1469         var nonExistingDirectory = Path.of("this", "does", "not", "exist");
1470         assertEquals(Optional.empty(), Repository.get(nonExistingDirectory));
1471         assertEquals(false, Repository.exists(nonExistingDirectory));
1472     }
1473 
1474     @ParameterizedTest
1475     @EnumSource(VCS.class)
1476     void testDiffOnFilenamesWithSpace(VCS vcs) throws IOException {
1477         try (var dir = new TemporaryDirectory()) {
1478             var r = Repository.init(dir.path(), vcs);
1479             assertTrue(r.isClean());
1480 
1481             var fileWithSpaceInName = dir.path().resolve("hello world.txt");
1482             Files.writeString(fileWithSpaceInName, "Hello world\n");
1483             r.add(fileWithSpaceInName);
1484             var hash1 = r.commit("Added file with space in name", "duke", "duke@openjdk.java.net");
1485             Files.writeString(fileWithSpaceInName, "Goodbye world\n");
1486             r.add(fileWithSpaceInName);
1487             var hash2 = r.commit("Modified file with space in name", "duke", "duke@openjdk.java.net");
1488             var diff = r.diff(hash1, hash2);
1489             var patches = diff.patches();
1490             assertEquals(1, patches.size());
1491             var patch = patches.get(0);
1492             assertTrue(patch.target().path().isPresent());
1493             var path = patch.target().path().get();
1494             assertEquals(Path.of("hello world.txt"), path);
1495         }
1496     }
1497 
1498     @Test
1499     void testSingleEmptyCommit() throws IOException, InterruptedException {
1500         try (var dir = new TemporaryDirectory()) {
1501             var r = Repository.init(dir.path(), VCS.GIT);
1502             assertTrue(r.isClean());
1503 
1504             // must ust git directly to be able to pass --allow-empty
1505             var pb = new ProcessBuilder("git", "commit", "--message", "An empty commit", "--allow-empty");
1506             pb.environment().put("GIT_AUTHOR_NAME", "duke");
1507             pb.environment().put("GIT_AUTHOR_EMAIL", "duke@openjdk.org");
1508             pb.environment().put("GIT_COMMITTER_NAME", "duke");
1509             pb.environment().put("GIT_COMMITTER_EMAIL", "duke@openjdk.org");
1510             pb.directory(dir.path().toFile());
1511 
1512             var res = pb.start().waitFor();
1513             assertEquals(0, res);
1514 
1515             var commits = r.commits().asList();
1516             assertEquals(1, commits.size());
1517             var commit = commits.get(0);
1518             assertEquals("duke", commit.author().name());
1519             assertEquals("duke@openjdk.org", commit.author().email());
1520             assertEquals("duke", commit.committer().name());
1521             assertEquals("duke@openjdk.org", commit.committer().email());
1522             assertEquals(List.of("An empty commit"), commit.message());
1523         }
1524     }
1525 
1526     @Test
1527     void testEmptyCommitWithParent() throws IOException, InterruptedException {
1528         try (var dir = new TemporaryDirectory()) {
1529             var r = Repository.init(dir.path(), VCS.GIT);
1530             assertTrue(r.isClean());
1531 
1532             var f = Files.createFile(dir.path().resolve("hello.txt"));
1533             Files.writeString(f, "Hello world\n");
1534             r.add(f);
1535             r.commit("Initial commit", "duke", "duke@openjdk.org");
1536 
1537             // must ust git directly to be able to pass --allow-empty
1538             var pb = new ProcessBuilder("git", "commit", "--message", "An empty commit", "--allow-empty");
1539             pb.environment().put("GIT_AUTHOR_NAME", "duke");
1540             pb.environment().put("GIT_AUTHOR_EMAIL", "duke@openjdk.org");
1541             pb.environment().put("GIT_COMMITTER_NAME", "duke");
1542             pb.environment().put("GIT_COMMITTER_EMAIL", "duke@openjdk.org");
1543             pb.directory(dir.path().toFile());
1544 
1545             var res = pb.start().waitFor();
1546             assertEquals(0, res);
1547 
1548             var commits = r.commits().asList();
1549             assertEquals(2, commits.size());
1550             var commit = commits.get(0);
1551             assertEquals("duke", commit.author().name());
1552             assertEquals("duke@openjdk.org", commit.author().email());
1553             assertEquals("duke", commit.committer().name());
1554             assertEquals("duke@openjdk.org", commit.committer().email());
1555             assertEquals(List.of("An empty commit"), commit.message());
1556         }
1557     }
1558 
1559     @ParameterizedTest
1560     @EnumSource(VCS.class)
1561     void testAmend(VCS vcs) throws IOException {
1562         try (var dir = new TemporaryDirectory()) {
1563             var r = Repository.init(dir.path(), vcs);
1564             assertTrue(r.isClean());
1565 
1566             var f = dir.path().resolve("README");
1567             Files.writeString(f, "Hello\n");
1568             r.add(f);
1569             r.commit("Initial commit", "duke", "duke@openjdk.org");
1570 
1571             Files.writeString(f, "Hello, world\n");
1572             r.add(f);
1573             r.amend("Initial commit corrected", "duke", "duke@openjdk.java.net");
1574             var commits = r.commits().asList();
1575             assertEquals(1, commits.size());
1576             var commit = commits.get(0);
1577             assertEquals(List.of("Initial commit corrected"), commit.message());
1578         }
1579     }
1580 
1581     @ParameterizedTest
1582     @EnumSource(VCS.class)
1583     void testRevert(VCS vcs) throws IOException {
1584         try (var dir = new TemporaryDirectory()) {
1585             var r = Repository.init(dir.path(), vcs);
1586             assertTrue(r.isClean());
1587 
1588             var f = dir.path().resolve("README");
1589             Files.writeString(f, "Hello\n");
1590             r.add(f);
1591             var initial = r.commit("Initial commit", "duke", "duke@openjdk.org");
1592 
1593             Files.writeString(f, "Hello, world\n");
1594             r.revert(initial);
1595             Files.writeString(f, "Goodbye, world\n");
1596             r.add(f);
1597             var hash = r.commit("Second commit", "duke", "duke@openjdk.org");
1598             var commit = r.lookup(hash).orElseThrow();
1599             var patches = commit.parentDiffs().get(0).patches();
1600             assertEquals(1, patches.size());
1601             var patch = patches.get(0).asTextualPatch();
1602             assertEquals(1, patch.hunks().size());
1603             var hunk = patch.hunks().get(0);
1604             assertEquals(List.of("Goodbye, world"), hunk.target().lines());
1605         }
1606     }
1607 
1608     @ParameterizedTest
1609     @EnumSource(VCS.class)
1610     void testFiles(VCS vcs) throws IOException {
1611         try (var dir = new TemporaryDirectory()) {
1612             var r = Repository.init(dir.path(), vcs);
1613             assertTrue(r.isClean());
1614 
1615             var f = dir.path().resolve("README");
1616             Files.writeString(f, "Hello\n");
1617             r.add(f);
1618             var initial = r.commit("Initial commit", "duke", "duke@openjdk.org");
1619 
1620             var entries = r.files(initial);
1621             assertEquals(1, entries.size());
1622             var entry = entries.get(0);
1623             assertEquals(Path.of("README"), entry.path());
1624             assertTrue(entry.type().isRegularNonExecutable());
1625 
1626             var f2 = dir.path().resolve("CONTRIBUTING");
1627             Files.writeString(f2, "Hello\n");
1628             r.add(f2);
1629             var second = r.commit("Second commit", "duke", "duke@openjdk.org");
1630 
1631             entries = r.files(second);
1632             assertEquals(2, entries.size());
1633             assertTrue(entries.stream().allMatch(e -> e.type().isRegularNonExecutable()));
1634             var paths = entries.stream().map(FileEntry::path).collect(Collectors.toSet());
1635             assertTrue(paths.contains(Path.of("README")));
1636             assertTrue(paths.contains(Path.of("CONTRIBUTING")));
1637 
1638             entries = r.files(second, Path.of("README"));
1639             assertEquals(1, entries.size());
1640             entry = entries.get(0);
1641             assertEquals(Path.of("README"), entry.path());
1642             assertTrue(entry.type().isRegularNonExecutable());
1643         }
1644     }
1645 
1646     @ParameterizedTest
1647     @EnumSource(VCS.class)
1648     void testDump(VCS vcs) throws IOException {
1649         try (var dir = new TemporaryDirectory()) {
1650             var r = Repository.init(dir.path(), vcs);
1651             assertTrue(r.isClean());
1652 
1653             var f = dir.path().resolve("README");
1654             Files.writeString(f, "Hello\n");
1655             r.add(f);
1656             var initial = r.commit("Initial commit", "duke", "duke@openjdk.org");
1657 
1658             var readme = r.files(initial).get(0);
1659 
1660             var tmp = Files.createTempFile("README", "txt");
1661             r.dump(readme, tmp);
1662             assertEquals("Hello\n", Files.readString(tmp));
1663             Files.delete(tmp);
1664         }
1665     }
1666 
1667     @ParameterizedTest
1668     @EnumSource(VCS.class)
1669     void testStatus(VCS vcs) throws IOException {
1670         try (var dir = new TemporaryDirectory()) {
1671             var r = Repository.init(dir.path(), vcs);
1672             assertTrue(r.isClean());
1673 
1674             var f = dir.path().resolve("README");
1675             Files.writeString(f, "Hello\n");
1676             r.add(f);
1677             var initial = r.commit("Initial commit", "duke", "duke@openjdk.org");
1678 
1679             var f2 = dir.path().resolve("CONTRIBUTING");
1680             Files.writeString(f2, "Goodbye\n");
1681             r.add(f2);
1682             var second = r.commit("Second commit", "duke", "duke@openjdk.org");
1683 
1684             var entries = r.status(initial, second);
1685             assertEquals(1, entries.size());
1686             var entry = entries.get(0);
1687             assertTrue(entry.status().isAdded());
1688             assertTrue(entry.source().path().isEmpty());
1689             assertTrue(entry.source().type().isEmpty());
1690 
1691             assertTrue(entry.target().path().isPresent());
1692             assertEquals(Path.of("CONTRIBUTING"), entry.target().path().get());
1693             assertTrue(entry.target().type().get().isRegular());
1694         }
1695     }
1696 
1697     @ParameterizedTest
1698     @EnumSource(VCS.class)
1699     void testTrackLineEndings(VCS vcs) throws IOException, InterruptedException {
1700         try (var dir = new TemporaryDirectory()) {
1701             var r = Repository.init(dir.path(), vcs);
1702             if (vcs == VCS.GIT) { // turn of git's meddling
1703                 int exitCode = new ProcessBuilder()
1704                         .command("git", "config", "--local", "core.autocrlf", "false")
1705                         .directory(dir.path().toFile())
1706                         .start()
1707                         .waitFor();
1708                 assertEquals(0, exitCode);
1709             }
1710 
1711             var readme = dir.path().resolve("README");
1712             Files.writeString(readme, "Line with Unix line ending\n");
1713             Files.writeString(readme, "Line with Windows line ending\r\n", APPEND);
1714 
1715             r.add(readme);
1716             r.commit("Add README", "duke", "duke@openjdk.java.net");
1717 
1718             var commits = r.commits().asList();
1719             assertEquals(1, commits.size());
1720 
1721             var commit = commits.get(0);
1722             var diffs = commit.parentDiffs();
1723             var diff = diffs.get(0);
1724             assertEquals(2, diff.added());
1725 
1726             var patches = diff.patches();
1727             assertEquals(1, patches.size());
1728 
1729             var patch = patches.get(0).asTextualPatch();
1730             var hunks = patch.hunks();
1731             assertEquals(1, hunks.size());
1732 
1733             var hunk = hunks.get(0);
1734             assertEquals(new Range(0, 0), hunk.source().range());
1735             assertEquals(new Range(1, 2), hunk.target().range());
1736 
1737             assertEquals(
1738                     List.of("Line with Unix line ending", "Line with Windows line ending\r"),
1739                     hunk.target().lines());
1740         }
1741     }
1742 
1743     @ParameterizedTest
1744     @EnumSource(VCS.class)
1745     void testContains(VCS vcs) throws IOException {
1746         try (var dir = new TemporaryDirectory()) {
1747             var r = Repository.init(dir.path(), vcs);
1748             assertTrue(r.isClean());
1749 
1750             var f = dir.path().resolve("README");
1751             Files.writeString(f, "Hello\n");
1752             r.add(f);
1753             var initial = r.commit("Initial commit", "duke", "duke@openjdk.org");
1754 
1755             assertTrue(r.contains(r.defaultBranch(), initial));
1756 
1757             Files.writeString(f, "Hello again\n");
1758             r.add(f);
1759             var second = r.commit("Second commit", "duke", "duke@openjdk.org");
1760 
1761             assertTrue(r.contains(r.defaultBranch(), initial));
1762         }
1763     }
1764 
1765     @ParameterizedTest
1766     @EnumSource(VCS.class)
1767     void testAbortMerge(VCS vcs) throws IOException {
1768         try (var dir = new TemporaryDirectory(false)) {
1769             var r = Repository.init(dir.path(), vcs);
1770             assertTrue(r.isClean());
1771 
1772             var f = dir.path().resolve("README");
1773             Files.writeString(f, "Hello\n");
1774             r.add(f);
1775             var initial = r.commit("Initial commit", "duke", "duke@openjdk.org");
1776 
1777             Files.writeString(f, "Hello again\n");
1778             r.add(f);
1779             var second = r.commit("Second commit", "duke", "duke@openjdk.org");
1780 
1781             r.checkout(initial);
1782             Files.writeString(f, "Conflicting hello\n");
1783             r.add(f);
1784             var third = r.commit("Third commit", "duke", "duke@openjdk.org");
1785 
1786             assertThrows(IOException.class, () -> { r.merge(second); });
1787 
1788             r.abortMerge();
1789             assertTrue(r.isClean());
1790         }
1791     }
1792 
1793     @ParameterizedTest
1794     @EnumSource(VCS.class)
1795     void testReset(VCS vcs) throws IOException {
1796         assumeTrue(vcs == VCS.GIT); // FIXME reset is not yet implemented for HG
1797 
1798         try (var dir = new TemporaryDirectory()) {
1799             var repo = Repository.init(dir.path(), vcs);
1800             assertTrue(repo.isClean());
1801 
1802             var f = dir.path().resolve("README");
1803             Files.writeString(f, "Hello\n");
1804             repo.add(f);
1805             var initial = repo.commit("Initial commit", "duke", "duke@openjdk.org");
1806 
1807             Files.writeString(f, "Hello again\n");
1808             repo.add(f);
1809             var second = repo.commit("Second commit", "duke", "duke@openjdk.org");
1810 
1811             assertEquals(second, repo.head());
1812             assertEquals(2, repo.commits().asList().size());
1813 
1814             repo.reset(initial, true);
1815 
1816             assertEquals(initial, repo.head());
1817             assertEquals(1, repo.commits().asList().size());
1818         }
1819     }
1820 }