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