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.hg; 24 25 import org.openjdk.skara.process.Process; 26 import org.openjdk.skara.process.Execution; 27 import org.openjdk.skara.vcs.*; 28 import org.openjdk.skara.vcs.tools.*; 29 30 import java.io.*; 31 import java.nio.file.*; 32 import java.time.*; 33 import java.time.format.DateTimeFormatter; 34 import java.util.*; 35 import java.util.logging.Logger; 36 import java.util.stream.*; 37 import java.net.URI; 38 39 public class HgRepository implements Repository { 40 private static final String EXT_PY = "ext.py"; 41 private final Path dir; 42 private final Logger log = Logger.getLogger("org.openjdk.skara.vcs.hg"); 43 44 private void copyResource(String name, Path p) throws IOException { 45 Files.copy(this.getClass().getResourceAsStream("/" + name), p, StandardCopyOption.REPLACE_EXISTING); 46 } 47 48 private java.lang.Process start(String... cmd) throws IOException { 49 return start(Arrays.asList(cmd)); 50 } 51 52 private java.lang.Process start(List<String> cmd) throws IOException { 53 log.fine("Executing " + String.join(" ", cmd)); 54 var pb = new ProcessBuilder(cmd); 55 pb.directory(dir.toFile()); 56 pb.redirectError(ProcessBuilder.Redirect.DISCARD); 57 pb.environment().put("HGRCPATH", ""); 58 pb.environment().put("HGPLAIN", ""); 59 return pb.start(); 60 } 61 62 private static void stop(java.lang.Process p) throws IOException { 63 if (p != null && p.isAlive()) { 64 var stream = p.getInputStream(); 65 var read = 0; 66 var buf = new byte[128]; 67 while (read != -1) { 68 read = stream.read(buf); 69 } 70 try { 71 p.waitFor(); 72 } catch (InterruptedException e) { 73 throw new IOException(e); 74 } 75 } 76 } 77 78 private Execution capture(List<String> cmd) { 79 return capture(cmd.toArray(new String[0])); 80 } 81 82 private Execution capture(String... cmd) { 83 return capture(dir, cmd); 84 } 85 86 private static Execution capture(Path cwd, List<String> cmd) { 87 return capture(cwd, cmd.toArray(new String[0])); 88 } 89 private static Execution capture(Path cwd, String... cmd) { 90 return Process.capture(cmd) 91 .environ("HGRCPATH", "") 92 .environ("HGPLAIN", "") 93 .workdir(cwd) 94 .execute(); 95 } 96 97 private static Execution.Result await(Execution e) throws IOException { 98 var result = e.await(); 99 if (result.status() != 0) { 100 if (result.exception().isPresent()) { 101 throw new IOException("Unexpected exit code\n" + result, result.exception().get()); 102 } else { 103 throw new IOException("Unexpected exit code\n" + result); 104 } 105 } 106 return result; 107 } 108 109 private static void await(java.lang.Process p) throws IOException { 110 try { 111 var res = p.waitFor(); 112 if (res != 0) { 113 throw new IOException("Unexpected exit code: " + res); 114 } 115 } catch (InterruptedException e) { 116 throw new IOException(e); 117 } 118 } 119 120 public HgRepository(Path dir) { 121 this.dir = dir.toAbsolutePath(); 122 } 123 124 @Override 125 public List<Branch> branches() throws IOException { 126 try (var p = capture("hg", "branches")) { 127 return await(p).stdout() 128 .stream() 129 .map(line -> line.split("\\s")[0]) 130 .map(Branch::new) 131 .collect(Collectors.toList()); 132 } 133 } 134 135 @Override 136 public List<Tag> tags() throws IOException { 137 try (var p = capture("hg", "tags")) { 138 return await(p).stdout() 139 .stream() 140 .map(line -> line.split("\\s")[0]) 141 .map(Tag::new) 142 .collect(Collectors.toList()); 143 } 144 } 145 146 @Override 147 public Path root() throws IOException { 148 try (var p = capture("hg", "root")) { 149 var res = await(p); 150 if (res.stdout().size() != 1) { 151 throw new IOException("Unexpected output\n" + res); 152 } 153 return Paths.get(res.stdout().get(0)); 154 } 155 } 156 157 private void checkout(String ref, boolean force) throws IOException { 158 var cmd = new ArrayList<String>(); 159 cmd.addAll(List.of("hg", "update")); 160 if (!force) { 161 cmd.add("--check"); 162 } 163 cmd.add(ref); 164 try (var p = capture(cmd)) { 165 await(p); 166 } 167 } 168 169 @Override 170 public void checkout(Hash h, boolean force) throws IOException { 171 checkout(h.hex(), force); 172 } 173 174 @Override 175 public void checkout(Branch b, boolean force) throws IOException { 176 checkout(b.name(), force); 177 } 178 179 @Override 180 public Optional<Hash> resolve(String ref) throws IOException { 181 try (var p = capture("hg", "log", "--rev=" + ref, "--template={node}\n")) { 182 var res = p.await(); 183 if (res.status() == 0 && res.stdout().size() == 1) { 184 return Optional.of(new Hash(res.stdout().get(0))); 185 } 186 return Optional.empty(); 187 } 188 } 189 190 @Override 191 public Commits commits() throws IOException { 192 return commits(null, -1, false); 193 } 194 195 @Override 196 public Commits commits(boolean reverse) throws IOException { 197 return commits(null, -1, reverse); 198 } 199 200 @Override 201 public Commits commits(int n) throws IOException { 202 return commits(null, n, false); 203 } 204 205 @Override 206 public Commits commits(int n, boolean reverse) throws IOException { 207 return commits(null, n, reverse); 208 } 209 210 @Override 211 public Commits commits(String range) throws IOException { 212 return commits(range, -1, false); 213 } 214 215 @Override 216 public Commits commits(String range, int n) throws IOException { 217 return commits(range, n, false); 218 } 219 220 @Override 221 public Commits commits(String range, boolean reverse) throws IOException { 222 return commits(range, -1, reverse); 223 } 224 225 @Override 226 public Commits commits(String range, int n, boolean reverse) throws IOException { 227 var ext = Files.createTempFile("ext", ".py"); 228 copyResource(EXT_PY, ext); 229 return new HgCommits(dir, range, ext, reverse, n); 230 } 231 232 @Override 233 public Optional<Commit> lookup(Hash h) throws IOException { 234 var commits = commits(h.hex()).asList(); 235 if (commits.size() != 1) { 236 return Optional.empty(); 237 } 238 return Optional.of(commits.get(0)); 239 } 240 241 @Override 242 public Optional<Commit> lookup(Branch b) throws IOException { 243 var hash = resolve(b.name()).orElseThrow(() -> new IOException("Branch " + b.name() + " not found")); 244 return lookup(hash); 245 } 246 247 @Override 248 public Optional<Commit> lookup(Tag t) throws IOException { 249 var hash = resolve(t.name()).orElseThrow(() -> new IOException("Tag " + t.name() + " not found")); 250 return lookup(hash); 251 } 252 253 @Override 254 public List<CommitMetadata> commitMetadata() throws IOException { 255 var ext = Files.createTempFile("ext", ".py"); 256 copyResource(EXT_PY, ext); 257 258 var p = start("hg", "--config", "extensions.dump=" + ext.toAbsolutePath().toString(), "metadata"); 259 var reader = new UnixStreamReader(p.getInputStream()); 260 var result = new ArrayList<CommitMetadata>(); 261 262 var line = reader.readLine(); 263 while (line != null) { 264 result.add(HgCommitMetadata.read(reader)); 265 line = reader.readLine(); 266 } 267 268 await(p); 269 return result; 270 } 271 272 @Override 273 public boolean isEmpty() throws IOException { 274 var numBranches = branches().size(); 275 var numTags = tags().size(); 276 277 if (numBranches > 0 || numTags > 1) { 278 return false; 279 } 280 281 var tip = resolve("tip"); 282 return tip.isEmpty() || tip.get().hex().equals("0".repeat(40)); 283 } 284 285 @Override 286 public boolean isHealthy() throws IOException { 287 var root = root().toString(); 288 return !(Files.exists(Path.of(root, ".hg", "wlock")) || 289 Files.exists(Path.of(root, ".hg", "store", "lock"))); 290 } 291 292 @Override 293 public void clean() throws IOException { 294 try (var p = capture("hg", "merge", "--abort")) { 295 p.await(); 296 } 297 298 try (var p = capture("hg", "recover")) { 299 p.await(); 300 } 301 302 try (var p = capture("hg", "status", "--ignored", "--no-status")) { 303 var root = root().toString(); 304 for (var filename : await(p).stdout()) { 305 Files.delete(Path.of(root, filename)); 306 } 307 } 308 309 try (var p = capture("hg", "status", "--unknown", "--no-status")) { 310 var root = root().toString(); 311 for (var filename : await(p).stdout()) { 312 Files.delete(Path.of(root, filename)); 313 } 314 } 315 316 try (var p = capture("hg", "revert", "--no-backup", "--all")) { 317 await(p); 318 } 319 } 320 321 @Override 322 public void reset(Hash target, boolean hard) throws IOException { 323 throw new RuntimeException("Not implemented yet"); 324 } 325 326 @Override 327 public Repository reinitialize() throws IOException { 328 Files.walk(dir) 329 .map(Path::toFile) 330 .sorted(Comparator.reverseOrder()) 331 .forEach(File::delete); 332 333 return init(); 334 } 335 336 @Override 337 public Hash fetch(URI uri, String refspec) throws IOException { 338 var oldHeads = new HashSet<Hash>(heads()); 339 340 var cmd = new ArrayList<String>(); 341 cmd.add("hg"); 342 cmd.add("pull"); 343 if (refspec != null) { 344 cmd.add("--rev"); 345 cmd.add(refspec); 346 } 347 if (uri != null) { 348 cmd.add(uri.toString()); 349 } 350 try (var p = capture(cmd)) { 351 await(p); 352 } 353 354 var newHeads = new HashSet<Hash>(heads()); 355 newHeads.removeAll(oldHeads); 356 357 if (newHeads.size() > 1) { 358 throw new IllegalStateException("fetching multiple heads is not supported"); 359 } else if (newHeads.size() == 0) { 360 // no new head was fetched, return current head 361 return head(); 362 } 363 return newHeads.iterator().next(); 364 } 365 366 @Override 367 public void fetchAll() throws IOException { 368 var pullPaths = new ArrayList<URI>(); 369 try (var p = capture("hg", "paths")) { 370 var res = await(p); 371 for (var line : res.stdout()) { 372 var parts = line.split("="); 373 var name = parts[0].trim(); 374 var uri = parts[1].trim(); 375 if (!name.endsWith("-push")) { 376 pullPaths.add(URI.create(uri)); 377 } 378 } 379 } 380 381 for (var uri : pullPaths) { 382 fetch(uri, null); 383 } 384 } 385 386 @Override 387 public void delete(Branch b) throws IOException { 388 throw new RuntimeException("Branches cannot be deleted in Mercurial"); 389 } 390 391 @Override 392 public Repository init() throws IOException { 393 if (!Files.exists(dir)) { 394 Files.createDirectories(dir); 395 } 396 397 try (var p = capture("hg", "init")) { 398 await(p); 399 return this; 400 } 401 } 402 403 @Override 404 public void pushAll(URI uri) throws IOException { 405 try (var p = capture("hg", "push", "--new-branch", uri.toString())) { 406 await(p); 407 } 408 } 409 410 @Override 411 public void push(Hash hash, URI uri, String ref, boolean force) throws IOException { 412 var cmd = new ArrayList<>(List.of("hg", "push", "--rev=" + hash.hex())); 413 if (force) { 414 cmd.add("--force"); 415 } 416 cmd.add(uri.toString() + "#" + ref); 417 try (var p = capture(cmd)) { 418 await(p); 419 } 420 } 421 422 @Override 423 public void push(Branch branch, String remote, boolean setUpstream) throws IOException { 424 // ignore setUpstream, no such concept in Mercurial 425 try (var p = capture("hg", "push", "--branch", branch.name(), remote)) { 426 await(p); 427 } 428 } 429 430 @Override 431 public boolean isClean() throws IOException { 432 try (var p = capture("hg", "status")) { 433 var output = await(p); 434 return output.stdout().size() == 0; 435 } 436 } 437 438 @Override 439 public boolean exists() throws IOException { 440 if (!Files.exists(dir)) { 441 return false; 442 } 443 444 try { 445 root(); 446 return true; 447 } catch (IOException e) { 448 return false; 449 } 450 } 451 452 private void export(String revset, Path to) throws IOException { 453 var cmd = List.of("hg", "export", "--git", "--rev", revset); 454 log.fine("Executing " + String.join(" ", cmd)); 455 var pb = new ProcessBuilder(cmd); 456 pb.directory(dir.toFile()); 457 pb.redirectError(ProcessBuilder.Redirect.DISCARD); 458 pb.redirectOutput(to.toFile()); 459 pb.environment().put("HGRCPATH", ""); 460 pb.environment().put("HGPLAIN", ""); 461 var p = pb.start(); 462 try { 463 await(p); 464 } catch (Throwable t) { 465 if (p.isAlive()) { 466 try { 467 p.waitFor(); 468 } catch (InterruptedException e) { 469 throw new IOException(e); 470 } 471 } 472 473 throw new IOException(t); 474 } 475 } 476 477 @Override 478 public void squash(Hash h) throws IOException { 479 var revset = ".:" + h.hex() + " and not ."; 480 var patch = Files.createTempFile("squash", ".patch"); 481 export(revset, patch); 482 483 try (var p = capture("hg", "--config", "extensions.mq=", "strip", "--rev", revset)) { 484 await(p); 485 } 486 487 try (var p = capture("hg", "import", "--no-commit", patch.toString())) { 488 await(p); 489 } 490 } 491 492 493 @Override 494 public Hash commit(String message, String authorName, String authorEmail) throws IOException { 495 return commit(message, authorName, authorEmail, null); 496 } 497 498 @Override 499 public Hash commit(String message, String authorName, String authorEmail, ZonedDateTime authorDate) throws IOException { 500 var user = authorEmail == null ? authorName : authorName + " <" + authorEmail + ">"; 501 var cmd = new ArrayList<String>(); 502 cmd.addAll(List.of("hg", "commit", "--message=" + message, "--user=" + user)); 503 if (authorDate != null) { 504 var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; 505 cmd.add("--date=" + authorDate.format(formatter)); 506 } 507 try (var p = capture(cmd)) { 508 await(p); 509 } 510 return resolve("tip").orElseThrow(() -> new IOException("Could not resolve 'tip'")); 511 } 512 513 @Override 514 public Hash commit(String message, 515 String authorName, 516 String authorEmail, 517 String committerName, 518 String committerEmail) throws IOException { 519 return commit(message, authorName, authorEmail, null, committerName, committerEmail, null); 520 } 521 522 @Override 523 public Hash commit(String message, 524 String authorName, 525 String authorEmail, 526 ZonedDateTime authorDate, 527 String committerName, 528 String committerEmail, 529 ZonedDateTime committerDate) throws IOException { 530 if (!Objects.equals(authorName, committerName) || 531 !Objects.equals(authorEmail, committerEmail) || 532 !Objects.equals(authorDate, committerDate)) { 533 throw new IllegalArgumentException("hg does not support different author and committer data"); 534 } 535 536 return commit(message, authorName, authorEmail, authorDate); 537 } 538 539 @Override 540 public Hash amend(String message, String authorName, String authorEmail) throws IOException { 541 var user = authorEmail == null ? authorName : authorName + " <" + authorEmail + ">"; 542 try (var p = capture("hg", "commit", "--amend", "--message=" + message, "--user=" + user)) { 543 await(p); 544 } 545 return resolve("tip").orElseThrow(() -> new IOException("Could not resolve 'tip'")); 546 } 547 548 @Override 549 public Hash amend(String message, String authorName, String authorEmail, String committerName, String committerEmail) throws IOException { 550 if (!Objects.equals(authorName, committerName) || 551 !Objects.equals(authorEmail, committerEmail)) { 552 throw new IllegalArgumentException("hg does not support different author and committer data"); 553 } 554 555 return amend(message, authorName, authorEmail); 556 } 557 558 @Override 559 public Tag tag(Hash hash, String name, String message, String authorName, String authorEmail) throws IOException { 560 var user = authorName + " <" + authorEmail + ">"; 561 try (var p = capture("hg", "tag", 562 "--message", message, 563 "--user", user, 564 "--rev", hash.hex(), 565 name)) { 566 await(p); 567 } 568 569 return new Tag(name); 570 } 571 572 @Override 573 public Branch branch(Hash hash, String name) throws IOException { 574 // Model a lightweight branch with a bookmark. Not ideal but the 575 // closest to git branches. 576 try (var p = capture("hg", "bookmark", "--rev", hash.hex(), name)) { 577 await(p); 578 } 579 580 return new Branch(name); 581 } 582 583 @Override 584 public Hash mergeBase(Hash first, Hash second) throws IOException { 585 var revset = "ancestor(" + first.hex() + ", " + second.hex() + ")"; 586 try (var p = capture("hg", "log", "--rev=" + revset, "--template={node}\n")) { 587 var res = await(p); 588 if (res.stdout().size() != 1) { 589 throw new IOException("Unexpected output\n" + res); 590 } 591 return new Hash(res.stdout().get(0)); 592 } 593 } 594 595 @Override 596 public boolean isAncestor(Hash ancestor, Hash descendant) throws IOException { 597 throw new RuntimeException("Not implemented yet"); 598 } 599 600 @Override 601 public void rebase(Hash hash, String committerName, String committerEmail) throws IOException { 602 var current = currentBranch().name(); 603 try (var p = capture("hg", "--config", "extensions.rebase=", 604 "rebase", "--dest", hash.hex(), "--base", current)) { 605 await(p); 606 } 607 } 608 609 @Override 610 public Branch currentBranch() throws IOException { 611 try (var p = capture("hg", "branch")) { 612 var res = await(p); 613 if (res.stdout().size() != 1) { 614 throw new IllegalStateException("No current branch\n" + res); 615 } 616 return new Branch(res.stdout().get(0)); 617 } 618 } 619 620 @Override 621 public Optional<Bookmark> currentBookmark() throws IOException { 622 try (var p = capture("hg", "log", "-r", ".", "--template", "{activebookmark}\n")) { 623 var res = await(p); 624 if (res.stdout().size() == 1) { 625 return Optional.of(new Bookmark(res.stdout().get(0))); 626 } 627 return Optional.empty(); 628 } 629 } 630 631 @Override 632 public Branch defaultBranch() throws IOException { 633 return new Branch("default"); 634 } 635 636 @Override 637 public Optional<Tag> defaultTag() throws IOException { 638 return Optional.of(new Tag("tip")); 639 } 640 641 @Override 642 public Optional<byte[]> show(Path path, Hash hash) throws IOException { 643 var output = Files.createTempFile("hg-cat-rev-" + hash.abbreviate(), ".bin"); 644 try (var p = capture("hg", "cat", "--output=" + output, "--rev=" + hash.hex(), path.toString())) { 645 var res = p.await(); 646 if (res.status() == 0 && Files.exists(output)) { 647 var bytes = Files.readAllBytes(output); 648 Files.delete(output); 649 return Optional.of(bytes); 650 } 651 652 if (Files.exists(output)) { 653 Files.delete(output); 654 } 655 return Optional.empty(); 656 } 657 } 658 659 private List<FileEntry> allFiles(Hash hash, List<Path> paths) throws IOException { 660 var ext = Files.createTempFile("ext", ".py"); 661 copyResource(EXT_PY, ext); 662 663 var include = new HashSet<>(paths); 664 665 try (var p = capture("hg", "--config", "extensions.ls-tree=" + ext, "ls-tree", hash.hex())) { 666 var res = await(p); 667 var entries = new ArrayList<FileEntry>(); 668 for (var line : res.stdout()) { 669 var parts = line.split("\t"); 670 var metadata = parts[0].split(" "); 671 var path = Path.of(parts[1]); 672 if (include.isEmpty() || include.contains(path)) { 673 var entry = new FileEntry(hash, 674 FileType.fromOctal(metadata[0]), 675 new Hash(metadata[2]), 676 path); 677 entries.add(entry); 678 } 679 } 680 return entries; 681 } 682 } 683 684 @Override 685 public List<FileEntry> files(Hash hash, List<Path> paths) throws IOException { 686 if (paths.isEmpty()) { 687 return allFiles(hash, paths); 688 } 689 690 var entries = new ArrayList<FileEntry>(); 691 var batchSize = 64; 692 var start = 0; 693 while (start < paths.size()) { 694 var end = start + batchSize; 695 if (end > paths.size()) { 696 end = paths.size(); 697 } 698 entries.addAll(allFiles(hash, paths.subList(start, end))); 699 start = end; 700 } 701 return entries; 702 } 703 704 @Override 705 public List<StatusEntry> status(Hash from, Hash to) throws IOException { 706 var ext = Files.createTempFile("ext", ".py"); 707 copyResource(EXT_PY, ext); 708 709 try (var p = capture("hg", "--config", "extensions.diff-git-raw=" + ext.toAbsolutePath().toString(), 710 "diff-git-raw", from.hex(), to.hex())) { 711 var res = await(p); 712 var entries = new ArrayList<StatusEntry>(); 713 for (var line : res.stdout()) { 714 entries.add(StatusEntry.fromRawLine(line)); 715 } 716 return entries; 717 } 718 } 719 720 @Override 721 public void dump(FileEntry entry, Path to) throws IOException { 722 var output = to.toAbsolutePath(); 723 try (var p = capture("hg", "cat", "--output=" + output.toString(), 724 "--rev=" + entry.commit(), 725 entry.path().toString())) { 726 await(p); 727 } 728 } 729 730 @Override 731 public void revert(Hash parent) throws IOException { 732 try (var p = capture("hg", "revert", "--no-backup", "--all", "--rev", parent.hex())) { 733 await(p); 734 } 735 } 736 737 @Override 738 public Diff diff(Hash from) throws IOException { 739 return diff(from, null); 740 } 741 742 @Override 743 public Diff diff(Hash from, Hash to) throws IOException { 744 var ext = Files.createTempFile("ext", ".py"); 745 copyResource(EXT_PY, ext); 746 747 var cmd = new ArrayList<>(List.of("hg", "--config", "extensions.diff-git-raw=" + ext.toAbsolutePath(), 748 "diff-git-raw", "--patch", from.hex())); 749 if (to != null) { 750 cmd.add(to.hex()); 751 } 752 753 var p = start(cmd); 754 try { 755 var patches = UnifiedDiffParser.parseGitRaw(p.getInputStream()); 756 await(p); 757 return new Diff(from, to, patches); 758 } catch (Throwable t) { 759 throw new IOException(t); 760 } 761 } 762 763 @Override 764 public Optional<String> username() throws IOException { 765 var lines = config("ui.username"); 766 return lines.size() == 1 ? Optional.of(lines.get(0)) : Optional.empty(); 767 } 768 769 @Override 770 public Hash head() throws IOException { 771 return resolve(".").orElseThrow(() -> new IOException(". not available")); 772 } 773 774 private List<Hash> heads() throws IOException { 775 var heads = new ArrayList<Hash>(); 776 try (var p = capture("hg", "heads", "--template={node}\n")) { 777 var res = p.await(); 778 if (res.status() == 0) { 779 for (var hash : res.stdout()) { 780 heads.add(new Hash(hash)); 781 } 782 } 783 } 784 return heads; 785 } 786 787 @Override 788 public List<String> config(String key) throws IOException { 789 // Do not use HgRepository.capture() here, want to run *with* 790 // hg configuration. 791 try (var p = Process.capture("hg", "showconfig", key) 792 .workdir(dir) 793 .execute()) { 794 var res = p.await(); 795 if (res.status() == 1) { 796 return List.of(); 797 } 798 return res.stdout(); 799 } 800 } 801 802 public static Optional<Repository> get(Path p) throws IOException { 803 if (!Files.exists(p)) { 804 return Optional.empty(); 805 } 806 807 var r = new HgRepository(p); 808 return r.exists() ? Optional.of(new HgRepository(r.root())) : Optional.empty(); 809 } 810 811 @Override 812 public Repository copyTo(Path destination) throws IOException { 813 var from = root().toAbsolutePath().toString(); 814 var to = destination.toAbsolutePath().toString(); 815 try (var p = capture("hg", "clone", from, to)) { 816 await(p); 817 } 818 819 return new HgRepository(destination.toAbsolutePath()); 820 } 821 822 @Override 823 public void merge(Hash h) throws IOException { 824 merge(h, null); 825 } 826 827 @Override 828 public void merge(Hash h, String stragegy) throws IOException { 829 var cmd = new ArrayList<String>(); 830 cmd.addAll(List.of("hg", "merge", "--rev=" + h.hex())); 831 if (stragegy != null) { 832 cmd.add("--tool=" + stragegy); 833 } 834 try (var p = capture(cmd)) { 835 await(p); 836 } 837 } 838 839 @Override 840 public void abortMerge() throws IOException { 841 try (var p = capture("hg", "merge", "--abort")) { 842 await(p); 843 } 844 845 try (var p = capture("hg", "status", "--unknown", "--no-status")) { 846 var res = await(p); 847 for (var path : res.stdout()) { 848 if (path.toString().endsWith(".orig")) { 849 Files.delete(root().resolve(path)); 850 } 851 } 852 } 853 } 854 855 @Override 856 public void addRemote(String name, String path) throws IOException { 857 setPaths(name, path, path); 858 } 859 860 @Override 861 public void setPaths(String remote, String pullPath, String pushPath) throws IOException { 862 var hgrc = Path.of(root().toString(), ".hg", "hgrc"); 863 if (!Files.exists(hgrc)) { 864 Files.createFile(hgrc); 865 } 866 867 var lines = Files.readAllLines(hgrc); 868 var newLines = new ArrayList<String>(); 869 870 var isInPathsSection = false; 871 var hasPathsSection = false; 872 for (var line : lines) { 873 var isSectionHeader = line.startsWith("[") && line.endsWith("]"); 874 if (isSectionHeader && !isInPathsSection) { 875 isInPathsSection = line.equals("[paths]"); 876 if (isInPathsSection) { 877 newLines.add(line); 878 newLines.add(remote + " = " + (pullPath == null ? "" : pullPath)); 879 newLines.add(remote + "-push = " + (pushPath == null ? "" : pushPath)); 880 hasPathsSection = true; 881 continue; 882 } 883 } 884 885 if (isInPathsSection && line.startsWith(remote)) { 886 if (line.startsWith(remote + "-push")) { 887 // skip 888 } else if (line.startsWith(remote + ":pushurl")) { 889 // skip 890 } else if (line.startsWith(remote + " ") || line.startsWith(remote + "=")) { 891 // skip 892 } else { 893 newLines.add(line); 894 } 895 } else { 896 newLines.add(line); 897 } 898 } 899 900 Files.write(hgrc, newLines, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); 901 if (!hasPathsSection) { 902 var section = List.of("[paths]", 903 remote + " = " + (pullPath == null ? "" : pullPath), 904 remote + "-push = " + (pushPath == null ? "" : pushPath)); 905 Files.write(hgrc, section, StandardOpenOption.WRITE, StandardOpenOption.APPEND); 906 } 907 } 908 909 @Override 910 public String pullPath(String remote) throws IOException { 911 var lines = config("paths." + remote); 912 if (lines.size() != 1) { 913 throw new IOException("Pull path not found for remote: " + remote); 914 } 915 return lines.get(0); 916 } 917 918 @Override 919 public String pushPath(String remote) throws IOException { 920 var lines = config("paths." + remote + "-push"); 921 if (lines.size() != 1) { 922 lines = config("paths." + remote + "@push"); 923 } 924 if (lines.size() != 1) { 925 return pullPath(remote); 926 } 927 return lines.get(0); 928 } 929 930 @Override 931 public boolean isValidRevisionRange(String expression) throws IOException { 932 try (var p = capture("hg", "log", "--template", " ", "--rev", expression)) { 933 return p.await().status() == 0; 934 } 935 } 936 937 private void setPermissions(Patch.Info target) throws IOException { 938 if (target.path().isPresent() && target.type().isPresent()) { 939 var perms = target.type().get().permissions(); 940 if (perms.isPresent()) { 941 Files.setPosixFilePermissions(target.path().get(), perms.get()); 942 } 943 } 944 } 945 946 @Override 947 public void apply(Diff diff, boolean force) throws IOException { 948 var patchFile = Files.createTempFile("import", ".patch"); 949 diff.toFile(patchFile); 950 apply(patchFile, force); 951 Files.delete(patchFile); 952 } 953 954 @Override 955 public void apply(Path patchFile, boolean force) throws IOException { 956 var cmd = new ArrayList<String>(); 957 cmd.addAll(List.of("hg", "import", "--no-commit")); 958 if (force) { 959 cmd.add("--force"); 960 } 961 cmd.add(patchFile.toAbsolutePath().toString()); 962 try (var p = capture(cmd)) { 963 await(p); 964 } 965 } 966 967 @Override 968 public void copy(Path from, Path to) throws IOException { 969 try (var p = capture("hg", "copy", from.toString(), to.toString())) { 970 await(p); 971 } 972 } 973 974 @Override 975 public void move(Path from, Path to) throws IOException { 976 try (var p = capture("hg", "move", from.toString(), to.toString())) { 977 await(p); 978 } 979 } 980 981 @FunctionalInterface 982 private static interface Operation { 983 void execute(List<Path> args) throws IOException; 984 } 985 986 private void batch(Operation op, List<Path> args) throws IOException { 987 var batchSize = 64; 988 var start = 0; 989 while (start < args.size()) { 990 var end = start + batchSize; 991 if (end > args.size()) { 992 end = args.size(); 993 } 994 op.execute(args.subList(start, end)); 995 start = end; 996 } 997 } 998 999 private void addAll(List<Path> paths) throws IOException { 1000 var cmd = new ArrayList<>(List.of("hg", "add")); 1001 for (var path : paths) { 1002 cmd.add(path.toString()); 1003 } 1004 try (var p = capture(cmd)) { 1005 await(p); 1006 } 1007 } 1008 1009 private void removeAll(List<Path> paths) throws IOException { 1010 var cmd = new ArrayList<>(List.of("hg", "rm")); 1011 for (var path : paths) { 1012 cmd.add(path.toString()); 1013 } 1014 try (var p = capture(cmd)) { 1015 await(p); 1016 } 1017 } 1018 1019 1020 @Override 1021 public void remove(List<Path> paths) throws IOException { 1022 batch(this::removeAll, paths); 1023 } 1024 1025 @Override 1026 public void add(List<Path> paths) throws IOException { 1027 batch(this::addAll, paths); 1028 } 1029 1030 @Override 1031 public void addremove() throws IOException { 1032 try (var p = capture("hg", "addremove")) { 1033 await(p); 1034 } 1035 } 1036 1037 @Override 1038 public Optional<String> upstreamFor(Branch b) throws IOException { 1039 // Mercurial doesn't have the concept of remotes like git, 1040 // a local branch must have the same name (if present) on the remote 1041 return Optional.of(b.name()); 1042 } 1043 1044 public static Repository clone(URI from, Path to, boolean isBare) throws IOException { 1045 var cmd = new ArrayList<String>(); 1046 cmd.addAll(List.of("hg", "clone")); 1047 if (isBare) { 1048 cmd.add("--noupdate"); 1049 } 1050 cmd.addAll(List.of(from.toString(), to.toString())); 1051 1052 try (var p = capture(Path.of("").toAbsolutePath(), cmd)) { 1053 await(p); 1054 } 1055 return new HgRepository(to); 1056 } 1057 1058 @Override 1059 public void pull() throws IOException { 1060 pull(null, null); 1061 } 1062 1063 @Override 1064 public void pull(String remote) throws IOException { 1065 pull(remote, null); 1066 } 1067 1068 @Override 1069 public void pull(String remote, String refspec) throws IOException { 1070 var cmd = new ArrayList<String>(); 1071 cmd.addAll(List.of("hg", "pull", "--update")); 1072 if (refspec != null) { 1073 cmd.add("--branch"); 1074 cmd.add(refspec); 1075 } 1076 if (remote != null) { 1077 cmd.add(remote); 1078 } 1079 try (var p = capture(cmd)) { 1080 await(p); 1081 } 1082 } 1083 1084 @Override 1085 public boolean contains(Branch b, Hash h) throws IOException { 1086 try (var p = capture("hg", "log", "--template", "{branch}", "-r", h.hex())) { 1087 var res = await(p); 1088 if (res.stdout().size() != 1) { 1089 throw new IOException("Unexpected output: " + String.join("\n", res.stdout())); 1090 } 1091 var line = res.stdout().get(0); 1092 return line.equals(b.name()); 1093 } 1094 } 1095 }