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