# HG changeset patch
# User Dan Villiom Podlaski Christiansen <danchr@gmail.com>
# Date 1624281308 -7200
#      Mon Jun 21 15:15:08 2021 +0200
# Node ID 6bc30f991fc52d7d2559547fab83132fc792ee63
# Parent  744f9d32a694b7e26252fc5bd2144e1374de7350
pull: use a transaction between map saves

By using a transaction, we can avoid unnecessary I/O and let Mercurial
do whatever it does to keep things fast. This is a significant
optimisation, as cloning Dulwich from Github goes from ~1m40s to ~52s
on a Mac, and ~49s to ~32s on Linux.

diff --git a/README.rst b/README.rst
--- a/README.rst
+++ b/README.rst
@@ -333,12 +333,15 @@
 ``hggit.mapsavefrequency``
 --------------------------
 
-Controls how often the mapping between Git and Mercurial commit hashes
-gets saved when importing or exporting changesets. Set this to a number
-greater than 0 to save the mapping after converting that many commits.
-This can help when the conversion encounters an error partway through a
-large batch of changes. Defaults to 0, so that the mapping is saved once
-at the end.
+By default, hg-git only saves the results of a conversion at the end.
+Use this option to enable resuming long-running pulls and pushes. Set
+this to a number greater than 0 to allow resuming after converting
+that many commits. This can help when the conversion encounters an
+error partway through a large batch of changes. Otherwise, an error or
+interruption will roll back the transaction, similar to regular
+Mercurial.
+
+Defaults to 0, so that the mapping is saved once at the end.
 
 Please note that this is meaningless for an initial clone, as any
 error or interruption will delete the destination. So instead of
diff --git a/hggit/git_handler.py b/hggit/git_handler.py
--- a/hggit/git_handler.py
+++ b/hggit/git_handler.py
@@ -824,6 +824,18 @@
     def get_git_incoming(self, refs):
         return git2hg.find_incoming(self.git.object_store, self._map_git, refs)
 
+    def get_transaction(self, desc=b"hg-git"):
+        """obtain a transaction specific for the repository
+
+        this ensures that we only save the map on close
+
+        """
+        tr = self.repo.transaction(desc)
+
+        tr.addfinalize(b'hg-git-save', lambda tr: self.save_map(self.map_file))
+
+        return tr
+
     def import_git_objects(self, remote_name, refs):
         result = self.get_git_incoming(refs)
         commits = result.commits
@@ -836,13 +848,18 @@
             self.ui.status(_(b"no changes found\n"))
 
         mapsavefreq = self.ui.configint(b'hggit', b'mapsavefrequency')
-        with self.ui.makeprogress(b'importing', unit=b'commits', total=total) as progress:
-            for i, csha in enumerate(commits, 1):
-                progress.increment()
-                commit = commit_cache[csha]
-                self.import_git_commit(commit)
-                if mapsavefreq and i % mapsavefreq == 0:
-                    self.save_map(self.map_file)
+        chunksize = max(mapsavefreq or total, 1)
+        progress = self.ui.makeprogress(b'importing', unit=b'commits', total=total)
+
+        self.ui.note(b"processing commits in batches of %d\n" % chunksize)
+
+        with progress, self.repo.lock():
+            for offset in range(0, total, chunksize):
+                # speed up conversion by batching commits in a transaction
+                with self.get_transaction(b"gimport"):
+                    for csha in commits[offset:offset + chunksize]:
+                        progress.increment()
+                        self.import_git_commit(commit_cache[csha])
 
         # TODO if the tags cache is used, remove any dangling tag references
         return total
diff --git a/tests/test-transactions.t b/tests/test-transactions.t
--- a/tests/test-transactions.t
+++ b/tests/test-transactions.t
@@ -68,44 +68,32 @@
   $ ABORT_AFTER=99 hg pull
   pulling from $TESTTMP/gitrepo
   importing git objects into hg
+  transaction abort!
+  rollback completed
   abort: aborted after 99 commits!
   [255]
   $ hg log -l 10 -T '{rev} {gitnode}\n'
-  99 
-  98 
-  97 
-  96 
-  95 
-  94 
-  93 
-  92 
-  91 
-  90 
-
-Reset the repository
-
-  $ hg strip --no-backup 'all()'
-  $ hg gclear
-  clearing out the git cache data
 
 How does map save interval work?
 
   $ EXIT_AFTER=15 hg --config hggit.mapsavefrequency=10 pull
   pulling from $TESTTMP/gitrepo
   importing git objects into hg
+  transaction abort!
+  rollback completed
   interrupted!
   [255]
   $ hg log -l 10 -T '{rev} {gitnode}\n'
-  15 
-  14 
-  13 
-  12 
-  11 
-  10 
   9 1d6b9d3de3098d28bb786d18849f5790a08a9a08
   8 42da70ed92bbecf9f348ba59c93646be723d0bf2
   7 17e841146e5744b81af9959634d82c20a5d7df52
   6 c31065bf97bf014815e37cdfbdef2c32c687f314
+  5 fcf21b8e0520ec1cced1d7593d13f9ee54721269
+  4 46acd02d0352e4b92bd6a099bb0490305d847a18
+  3 61eeda444b37b8aa3892d5f04c66c5441d21dd66
+  2 e55db11bb0472791c7af3fc636772174cdea4a36
+  1 17a2672b3c24c02d568f99d8d55ccae2bf362d5c
+  0 4e195b4c6e77604b70a8ad3b01306adbb9b1c7e7
 
 Reset the repository
 
@@ -118,19 +106,21 @@
   $ EXIT_AFTER=15 hg --config hggit.mapsavefrequency=10 pull
   pulling from $TESTTMP/gitrepo
   importing git objects into hg
+  transaction abort!
+  rollback completed
   interrupted!
   [255]
   $ hg log -l 10 -T '{rev} {gitnode}\n'
-  15 
-  14 
-  13 
-  12 
-  11 
-  10 
   9 1d6b9d3de3098d28bb786d18849f5790a08a9a08
   8 42da70ed92bbecf9f348ba59c93646be723d0bf2
   7 17e841146e5744b81af9959634d82c20a5d7df52
   6 c31065bf97bf014815e37cdfbdef2c32c687f314
+  5 fcf21b8e0520ec1cced1d7593d13f9ee54721269
+  4 46acd02d0352e4b92bd6a099bb0490305d847a18
+  3 61eeda444b37b8aa3892d5f04c66c5441d21dd66
+  2 e55db11bb0472791c7af3fc636772174cdea4a36
+  1 17a2672b3c24c02d568f99d8d55ccae2bf362d5c
+  0 4e195b4c6e77604b70a8ad3b01306adbb9b1c7e7
   $ cd ..
   $ rm -rf hgrepo
 
@@ -142,6 +132,8 @@
   > --config extensions.breakage=$TESTDIR/testlib/ext-break-git-import.py \
   > clone gitrepo hgrepo
   importing git objects into hg
+  transaction abort!
+  rollback completed
   interrupted!
   [255]
   $ hg id hgrepo