1 /* 2 * Copyright (c) 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.openjdk.convert; 24 25 import org.openjdk.skara.vcs.*; 26 import org.openjdk.skara.vcs.openjdk.*; 27 28 import java.io.*; 29 import java.net.URI; 30 import java.nio.charset.StandardCharsets; 31 import java.nio.file.*; 32 import java.time.ZonedDateTime; 33 import java.time.format.DateTimeFormatter; 34 import java.util.*; 35 import java.util.concurrent.TimeUnit; 36 import java.util.function.Function; 37 import java.util.logging.Logger; 38 import java.util.stream.Collectors; 39 40 import static java.lang.Integer.parseInt; 41 42 public class HgToGitConverter implements Converter { 43 private static class ProcessInfo implements AutoCloseable { 44 private final java.lang.Process process; 45 private final List<String> command; 46 private final Path stdout; 47 private final Path stderr; 48 private final CloseAction closeAction; 49 50 @FunctionalInterface 51 interface CloseAction { 52 void close() throws IOException; 53 } 54 55 ProcessInfo(java.lang.Process process, List<String> command, Path stdout, Path stderr, CloseAction closeAction) { 56 this.process = process; 57 this.command = command; 58 this.stdout = stdout; 59 this.stderr = stderr; 60 this.closeAction = closeAction; 61 } 62 63 ProcessInfo(java.lang.Process process, List<String> command, Path stdout, Path stderr) { 64 this(process, command, stdout, stderr, () -> {}); 65 } 66 67 java.lang.Process process() { 68 return process; 69 } 70 71 List<String> command() { 72 return command; 73 } 74 75 Path stdout() { 76 return stdout; 77 } 78 79 Path stderr() { 80 return stderr; 81 } 82 83 int waitForProcess() throws InterruptedException { 84 var finished = process.waitFor(12, TimeUnit.HOURS); 85 if (!finished) { 86 process.destroyForcibly().waitFor(); 87 throw new RuntimeException("Command '" + String.join(" ", command) + "' did not finish in 12 hours"); 88 } 89 return process.exitValue(); 90 } 91 92 @Override 93 public void close() throws IOException { 94 if (stdout != null) { 95 Files.delete(stdout); 96 } 97 if (stderr != null) { 98 Files.delete(stderr); 99 } 100 closeAction.close(); 101 } 102 } 103 104 private final byte[] fileBuffer = new byte[2048]; 105 private final Logger log = Logger.getLogger("org.openjdk.skara.vcs.openjdk.convert"); 106 107 private final Map<Hash, List<String>> replacements; 108 private final Map<Hash, Map<String, String>> corrections; 109 private final Set<Hash> lowercase; 110 private final Set<Hash> punctuated; 111 112 private final Map<String, String> authorMap; 113 private final Map<String, String> contributorMap; 114 private final Map<String, List<String>> sponsorMap; 115 116 private final CommitMessageParser parser = new ConverterCommitMessageParser(); 117 private int currentMark = 0; 118 private final Map<Hash, Integer> hgHashesToMarks = new HashMap<Hash, Integer>(); 119 private final Map<Integer, Hash> marksToHgHashes = new HashMap<Integer, Hash>(); 120 121 public HgToGitConverter(Map<Hash, List<String>> replacements, 122 Map<Hash, Map<String, String>> corrections, 123 Set<Hash> lowercase, 124 Set<Hash> punctuated, 125 Map<String, String> authorMap, 126 Map<String, String> contributorMap, 127 Map<String, List<String>> sponsorMap) { 128 this.replacements = replacements; 129 this.corrections = corrections; 130 this.lowercase = lowercase; 131 this.punctuated = punctuated; 132 133 this.authorMap = authorMap; 134 this.contributorMap = contributorMap; 135 this.sponsorMap = sponsorMap; 136 } 137 138 private static Branch convertBranch(Branch branch) { 139 if (branch.name().equals("default")) { 140 return new Branch("master"); 141 } 142 143 return branch; 144 } 145 146 private static String convertFlags(String flags) { 147 if (flags.contains("x")) { 148 return "100755"; 149 } 150 151 if (flags.contains("l")) { 152 return "120000"; 153 } 154 155 return "100644"; 156 } 157 158 private static String capitalize(String s) { 159 return s.substring(0, 1).toUpperCase() + s.substring(1); 160 } 161 162 private static String removePunctuation(String s) { 163 return s.endsWith(".") ? s.substring(0, s.length() - 1) : s; 164 } 165 166 private int nextMark(Hash hgHash) { 167 currentMark++; 168 var next = currentMark; 169 hgHashesToMarks.put(hgHash, next); 170 marksToHgHashes.put(next, hgHash); 171 return next; 172 } 173 174 private Author convertAuthor(Author from) { 175 var author = authorMap.get(from.name()); 176 if (author == null) { 177 throw new RuntimeException("Failed to find author mapping for: " + from.name()); 178 } 179 return Author.fromString(author); 180 } 181 182 private Attribution attribute(List<Author> contributorsFromCommit, Author hgAuthor) { 183 var isSponsored = false; 184 var contributors = new ArrayList<Author>(contributorsFromCommit); 185 if (contributors.size() == 1) { 186 isSponsored = true; 187 } else if (contributors.size() > 1) { 188 // The author has sponsored at least one commit, see if this commit was sponsored. 189 // The commit is sponsored if the author is *not* listed on the "Contributed-by" line. 190 191 var emails = sponsorMap.get(hgAuthor.name()); 192 if (emails == null) { 193 throw new RuntimeException("Failed to find sponsor mapping for: " + hgAuthor.name()); 194 } 195 Author authorAsContributor = null; 196 for (var email : emails) { 197 for (var contributor : contributors) { 198 if (contributor.email().equals(email)) { 199 authorAsContributor = contributor; 200 break; 201 } 202 } 203 } 204 if (authorAsContributor != null) { 205 contributors.remove(authorAsContributor); 206 } else { 207 isSponsored = true; 208 } 209 } 210 211 var originalAuthor = convertAuthor(hgAuthor); 212 213 Author author = null; 214 if (isSponsored) { 215 author = new Author(contributors.get(0).name(), contributors.get(0).email()); 216 contributors.remove(0); 217 } else { 218 author = originalAuthor; 219 } 220 var committer = isSponsored ? originalAuthor : author; 221 222 return new Attribution(author, committer, contributors); 223 } 224 225 private List<Author> addContributorNames(List<Author> contributors) { 226 final Function<Author, String> lookup = (Author a) -> { 227 var author = contributorMap.get(a.email()); 228 if (author == null) { 229 throw new RuntimeException("Failed to find contributor mapping for: " + a.email()); 230 } 231 return author; 232 }; 233 return contributors.stream() 234 .map(a -> a.name().isEmpty() ? Author.fromString(lookup.apply(a)) : a) 235 .collect(Collectors.toList()); 236 } 237 238 private static List<String> cleanup(List<String> original, Map<String, String> corrections) { 239 if (corrections == null) { 240 return original; 241 } 242 243 return original.stream().map(l -> corrections.getOrDefault(l, l)).collect(Collectors.toList()); 244 } 245 246 private String toGitCommitMessage(Hash hash, List<Issue> issues, List<String> summaries, List<Author> contributors, List<String> reviewers, List<String> others) { 247 List<String> body = new ArrayList<String>(); 248 body.addAll(summaries.stream().map(HgToGitConverter::capitalize).collect(Collectors.toList())); 249 body.addAll(others); 250 251 var subject = issues.stream().map(Issue::toString).collect(Collectors.toList()); 252 if (subject.size() == 0) { 253 subject = body.subList(0, 1); 254 body = body.subList(1, body.size()); 255 } 256 257 var firstNonNewlineIndex = 0; 258 while (firstNonNewlineIndex < body.size() && body.get(firstNonNewlineIndex).equals("")) { 259 firstNonNewlineIndex++; 260 } 261 body = body.subList(firstNonNewlineIndex, body.size()); 262 263 var sb = new StringBuilder(); 264 for (var line : subject) { 265 line = lowercase.contains(hash) ? line : capitalize(line); 266 line = punctuated.contains(hash) ? line : removePunctuation(line); 267 if (line.startsWith("JMC-")) { 268 line = line.substring(4); 269 } 270 sb.append(line); 271 sb.append("\n"); 272 } 273 if ((body.size() + contributors.size() + reviewers.size()) > 0) { 274 sb.append("\n"); 275 } 276 277 var hasPrintedNonNewline = false; 278 for (var line : body) { 279 // Remove any number of initial empty lines 280 if (!hasPrintedNonNewline && line.equals("")) { 281 continue; 282 } 283 sb.append(line); 284 sb.append("\n"); 285 hasPrintedNonNewline = true; 286 } 287 if (body.size() > 0) { 288 sb.append("\n"); 289 } 290 291 for (var contributor : contributors) { 292 sb.append("Co-authored-by: "); 293 sb.append(contributor.toString()); 294 sb.append("\n"); 295 } 296 297 if (reviewers.size() > 0) { 298 sb.append("Reviewed-by: "); 299 sb.append(String.join(", ", reviewers)); 300 sb.append("\n"); 301 } 302 303 return sb.toString(); 304 } 305 306 private GitCommitMetadata convertMetadata(Hash hgHash, 307 Branch hgBranch, 308 Author hgAuthor, 309 List<Hash> hgParentHashes, 310 ZonedDateTime hgDate, 311 List<String> hgCommitMessage) { 312 var shortHash = new Hash(hgHash.hex().substring(0, 12)); 313 314 hgCommitMessage = replacements.getOrDefault(shortHash, hgCommitMessage); 315 hgCommitMessage = cleanup(hgCommitMessage, corrections.get(shortHash)); 316 317 var commitMessage = parser.parse(hgCommitMessage); 318 var hgContributors = addContributorNames(commitMessage.contributors()); 319 320 var attribution = attribute(hgContributors, hgAuthor); 321 var gitAuthor = attribution.author(); 322 var gitCommitter = attribution.committer(); 323 var gitMessage = toGitCommitMessage(shortHash, 324 commitMessage.issues(), 325 commitMessage.summaries(), 326 attribution.contributors(), 327 commitMessage.reviewers(), 328 commitMessage.additional()); 329 330 var gitMark = nextMark(hgHash); 331 var gitParentMarks = hgParentHashes.stream().map(hgHashesToMarks::get).collect(Collectors.toList()); 332 333 var gitBranch = convertBranch(hgBranch); 334 var gitDate = hgDate; // no conversion needed 335 336 return new GitCommitMetadata(gitMark, gitParentMarks, gitAuthor, gitCommitter, gitBranch, gitDate, gitMessage); 337 } 338 339 private List<Hash> convertCommits(Pipe pipe) throws IOException { 340 var tagCommits = new ArrayList<Hash>(); 341 while (pipe.read() != -1) { 342 pipe.readln(); // skip delimiter 343 var hash = new Hash(pipe.readln()); 344 log.fine("Converting: " + hash.hex()); 345 pipe.readln(); // skip revision number 346 var branch = new Branch(pipe.readln()); 347 log.finer("Branch: " + branch.name()); 348 349 var parents = pipe.readln(); 350 log.finer("Parents: " + parents); 351 var parentHashes = Arrays.asList(parents.split(" ")) 352 .stream() 353 .map(Hash::new) 354 .collect(Collectors.toList()); 355 if (parentHashes.size() == 1 && parentHashes.get(0).equals(new Hash("0".repeat(40)))) { 356 parentHashes = new ArrayList<Hash>(); 357 } 358 pipe.readln(); // skip parent revisions 359 360 var author = Author.fromString(pipe.readln()); 361 log.finer("Author: " + author.toString()); 362 363 var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd H:m:sZ"); 364 var date = ZonedDateTime.parse(pipe.readln(), formatter); 365 log.finer("Date: " + date.toString()); 366 367 var messageSize = parseInt(pipe.readln()); 368 var messageBuffer = pipe.read(messageSize); 369 var hgMessage = new String(messageBuffer, 0, messageSize, StandardCharsets.UTF_8); 370 log.finest("Message: " + hgMessage); 371 372 var metadata = convertMetadata(hash, 373 branch, 374 author, 375 parentHashes, 376 date, 377 Arrays.asList(hgMessage.split("\n"))); 378 379 pipe.print("commit refs/heads/"); 380 pipe.println(metadata.branch().name()); 381 382 pipe.print("mark :"); 383 pipe.println(metadata.mark()); 384 385 var epoch = metadata.date().toEpochSecond(); 386 var offset = metadata.date().format(DateTimeFormatter.ofPattern("xx")); 387 388 pipe.print("author "); 389 pipe.print(metadata.author().name()); 390 pipe.print(" <"); 391 pipe.print(metadata.author().email()); 392 pipe.print("> "); 393 pipe.print(epoch); 394 pipe.print(" "); 395 pipe.println(offset); 396 397 pipe.print("committer "); 398 pipe.print(metadata.committer().name()); 399 pipe.print(" <"); 400 pipe.print(metadata.committer().email()); 401 pipe.print("> "); 402 pipe.print(epoch); 403 pipe.print(" "); 404 pipe.println(offset); 405 406 pipe.print("data "); 407 408 var gitMessage = metadata.message().getBytes(StandardCharsets.UTF_8); 409 pipe.println(gitMessage.length); 410 pipe.print(gitMessage); 411 412 if (metadata.parents().size() > 0) { 413 pipe.print("from :"); 414 pipe.println(metadata.parents().get(0)); 415 } 416 if (metadata.parents().size() > 1) { 417 pipe.print("merge :"); 418 pipe.println(metadata.parents().get(1)); 419 } 420 421 // Stream the file content 422 var numModified = parseInt(pipe.readln()); 423 var numAdded = parseInt(pipe.readln()); 424 var numRemoved = parseInt(pipe.readln()); 425 426 for (var i = 0; i < (numAdded + numModified); i++) { 427 var filename = pipe.readln(); 428 var flags = pipe.readln(); 429 430 if (filename.equals(".hgtags") && parentHashes.size() == 1) { 431 tagCommits.add(hash); 432 } 433 434 log.finest("M " + filename); 435 pipe.print("M "); 436 pipe.print(convertFlags(flags)); 437 pipe.print(" inline "); 438 pipe.println(filename); 439 440 var numBytes = parseInt(pipe.readln()); 441 pipe.print("data "); 442 pipe.println(numBytes); 443 444 var leftToRead = numBytes; 445 while (leftToRead != 0) { 446 var numRead = pipe.read(fileBuffer, 0, Math.min(fileBuffer.length, leftToRead)); 447 if (numRead == -1) { 448 throw new IOException("Unexpected end of input"); 449 } 450 pipe.print(fileBuffer, 0, numRead); 451 leftToRead -= numRead; 452 } 453 } 454 455 for (var i = 0; i < numRemoved; i++) { 456 var filename = pipe.readln(); 457 log.finest("D " + filename); 458 pipe.print("D "); 459 pipe.println(filename); 460 } 461 } 462 463 464 return tagCommits; 465 } 466 467 private void convertTags(Pipe pipe, List<Hash> tagCommits, ReadOnlyRepository hgRepo) throws IOException { 468 var tags = new HashMap<String, Commit>(); 469 for (var tagHash : tagCommits) { 470 log.fine("Inspecting tag commit " + tagHash.toString()); 471 var commit = hgRepo.lookup(tagHash).orElseThrow(() -> new IOException("Could not find commit " + tagHash)); 472 var diff = commit.parentDiffs().get(0); // convert never returns merge commits 473 for (var patch : diff.patches()) { 474 var target = patch.target().path(); 475 if (target.isPresent() && target.get().equals(Path.of(".hgtags"))) { 476 for (var hunk : patch.asTextualPatch().hunks()) { 477 for (var line : hunk.target().lines()) { 478 if (line.isEmpty()) { 479 continue; 480 } 481 var parts = line.split(" "); 482 var hash = parts[0]; 483 var tag = parts[1]; 484 if (hash.equals("0".repeat(40))) { 485 tags.remove(tag); 486 } else { 487 tags.put(tag, commit); 488 } 489 } 490 } 491 } 492 } 493 } 494 495 for (var tag : hgRepo.tags()) { 496 if (tags.containsKey(tag.name())) { 497 var commit = tags.get(tag.name()); 498 499 log.fine("Converting tag " + tag.name()); 500 pipe.print("tag "); 501 pipe.println(tag.name()); 502 pipe.print("from :"); 503 pipe.println(hgHashesToMarks.get(hgRepo.lookup(tag).get().hash())); 504 505 pipe.print("tagger "); 506 var author = convertAuthor(commit.author()); 507 pipe.print(author.name()); 508 pipe.print(" <"); 509 pipe.print(author.email()); 510 pipe.print("> "); 511 var epoch = commit.date().toEpochSecond(); 512 var offset = commit.date().format(DateTimeFormatter.ofPattern("xx")); 513 pipe.print(epoch); 514 pipe.print(" "); 515 pipe.println(offset); 516 517 pipe.print("data "); 518 var message = String.join("\n", commit.message()); 519 var bytes = message.getBytes(StandardCharsets.UTF_8); 520 pipe.println(bytes.length); 521 pipe.print(bytes); 522 } 523 } 524 } 525 526 private List<Mark> readMarks(Path p) throws IOException { 527 var marks = new ArrayList<Mark>(); 528 try (var reader = Files.newBufferedReader(p)) { 529 for (var line = reader.readLine(); line != null; line = reader.readLine()) { 530 var parts = line.split(" "); 531 var mark = parseInt(parts[0].substring(1)); 532 var gitHash = new Hash(parts[1]); 533 var hgHash = marksToHgHashes.get(mark); 534 log.finest("parsed mark " + mark + ", hg: " + hgHash.hex() + ", git: " + gitHash.hex()); 535 marks.add(new Mark(mark, hgHash, gitHash)); 536 } 537 } 538 return marks; 539 } 540 541 private Path writeMarks(List<Mark> marks) throws IOException { 542 var gitMarks = Files.createTempFile("git", ".marks.txt"); 543 try (var writer = Files.newBufferedWriter(gitMarks)) { 544 for (var mark : marks) { 545 writer.write(":"); 546 writer.write(Integer.toString(mark.key())); 547 writer.write(" "); 548 writer.write(mark.git().hex()); 549 writer.newLine(); 550 551 marksToHgHashes.put(mark.key(), mark.hg()); 552 hgHashesToMarks.put(mark.hg(), mark.key()); 553 } 554 } 555 return gitMarks; 556 } 557 558 private ProcessInfo dump(ReadOnlyRepository repo) throws IOException { 559 var ext = Files.createTempFile("ext", ".py"); 560 Files.copy(this.getClass().getResourceAsStream("/ext.py"), ext, StandardCopyOption.REPLACE_EXISTING); 561 562 var command = List.of("hg", "--config", "extensions.dump=" + ext.toAbsolutePath().toString(), "dump"); 563 var pb = new ProcessBuilder(command); 564 pb.environment().put("HGRCPATH", ""); 565 pb.environment().put("HGPLAIN", ""); 566 pb.directory(repo.root().toFile()); 567 568 var stderr = Files.createTempFile("dump", ".stderr.txt"); 569 pb.redirectError(stderr.toFile()); 570 log.finer("hg dump stderr: " + stderr.toString()); 571 572 log.finer("Starting '" + String.join(" ", command) + "'"); 573 return new ProcessInfo(pb.start(), command, null, stderr, () -> Files.delete(ext)); 574 } 575 576 private ProcessInfo pull(Repository repo, URI source) throws IOException { 577 var ext = Files.createTempFile("ext", ".py"); 578 var extStream = getClass().getResourceAsStream("/ext.py"); 579 if (extStream == null) { 580 // Used when running outside of the module path (such as from an IDE) 581 var classPath = Path.of(getClass().getProtectionDomain().getCodeSource().getLocation().getPath()); 582 var extPath = classPath.getParent().resolve("resources").resolve("ext.py"); 583 extStream = new FileInputStream(extPath.toFile()); 584 } 585 Files.copy(extStream, ext, StandardCopyOption.REPLACE_EXISTING); 586 587 var hook = "hooks.pretxnclose=python:" + ext.toAbsolutePath().toString() + ":pretxnclose"; 588 var command = List.of("hg", "--config", hook, "pull", "--quiet", source.toString()); 589 var pb = new ProcessBuilder(command); 590 pb.environment().put("HGRCPATH", ""); 591 pb.environment().put("HGPLAIN", ""); 592 pb.directory(repo.root().toFile()); 593 594 var stderr = Files.createTempFile("pull", ".stderr.txt"); 595 pb.redirectError(stderr.toFile()); 596 597 log.finer("Starting '" + String.join(" ", command) + "'"); 598 return new ProcessInfo(pb.start(), command, null, stderr, () -> Files.delete(ext)); 599 } 600 601 private ProcessInfo fastImport(ReadOnlyRepository repo) throws IOException { 602 var command = List.of("git", "fast-import"); 603 var pb = new ProcessBuilder(command); 604 pb.directory(repo.root().toFile()); 605 606 var stdout = Files.createTempFile("fast-import", ".stdout.txt"); 607 pb.redirectOutput(stdout.toFile()); 608 609 var stderr = Files.createTempFile("fast-import", ".stderr.txt"); 610 pb.redirectError(stderr.toFile()); 611 612 log.finer("Starting '" + String.join(" ", command) + "'"); 613 return new ProcessInfo(pb.start(), command, stdout, stderr); 614 } 615 616 private void await(ProcessInfo p) throws IOException { 617 try { 618 int res = p.waitForProcess(); 619 if (res != 0) { 620 var msg = String.join(" ", p.command()) + " exited with status " + res; 621 log.severe(msg); 622 throw new IOException(msg); 623 } 624 } catch (InterruptedException e) { 625 throw new IOException(e); 626 } 627 } 628 629 private void convert(ProcessInfo hg, ProcessInfo git, ReadOnlyRepository hgRepo, Path marks) throws IOException { 630 var pipe = new Pipe(hg.process().getInputStream(), git.process().getOutputStream(), 512); 631 632 pipe.println("feature done"); 633 pipe.println("feature import-marks-if-exists=" + marks.toAbsolutePath().toString()); 634 pipe.println("feature export-marks=" + marks.toAbsolutePath().toString()); 635 636 var tagCommits = convertCommits(pipe); 637 convertTags(pipe, tagCommits, hgRepo); 638 639 pipe.println("done"); 640 } 641 642 private void log(ProcessInfo hg, ProcessInfo git, Path gitRoot) throws IOException { 643 if (Files.exists(hg.stderr())) { 644 var content = Files.readString(hg.stderr()); 645 if (!content.isEmpty()) { 646 log.warning("'" + String.join(" ", hg.command()) + "' [stderr]: " + content); 647 } 648 } 649 650 if (Files.exists(git.stdout())) { 651 var content = Files.readString(git.stdout()); 652 if (!content.isEmpty()) { 653 log.warning("'" + String.join(" ", git.command()) + "' [stdout]: " + content); 654 } 655 } 656 if (Files.exists(git.stderr())) { 657 var content = Files.readString(git.stderr()); 658 if (!content.isEmpty()) { 659 log.warning("'" + String.join(" ", git.command()) + "' [stderr]: " + content); 660 } 661 } 662 663 if (Files.isDirectory(gitRoot)) { 664 for (var path : Files.walk(gitRoot).collect(Collectors.toList())) { 665 if (path.getFileName().toString().startsWith("fast_import_crash")) { 666 log.warning(Files.readString(path)); 667 } 668 } 669 } 670 } 671 672 public List<Mark> convert(ReadOnlyRepository hgRepo, Repository gitRepo) throws IOException { 673 try (var hg = dump(hgRepo); 674 var git = fastImport(gitRepo)) { 675 try { 676 var gitMarks = Files.createTempFile("git", ".marks.txt"); 677 convert(hg, git, hgRepo, gitMarks); 678 679 await(git); 680 await(hg); 681 682 var ret = readMarks(gitMarks); 683 Files.delete(gitMarks); 684 return ret; 685 } catch (IOException e) { 686 log(hg, git, gitRepo.root()); 687 throw e; 688 } 689 } 690 } 691 692 public List<Mark> pull(Repository hgRepo, URI source, Repository gitRepo, List<Mark> marks) throws IOException { 693 try (var hg = pull(hgRepo, source); 694 var git = fastImport(gitRepo)) { 695 try { 696 for (var mark : marks) { 697 hgHashesToMarks.put(mark.hg(), mark.key()); 698 marksToHgHashes.put(mark.key(), mark.hg()); 699 currentMark = Math.max(mark.key(), currentMark); 700 } 701 var gitMarks = writeMarks(marks); 702 convert(hg, git, hgRepo, gitMarks); 703 704 await(git); 705 await(hg); 706 707 var ret = readMarks(gitMarks); 708 Files.delete(gitMarks); 709 return ret; 710 } catch (IOException e) { 711 log(hg, git, gitRepo.root()); 712 throw e; 713 } 714 } 715 } 716 }