mercurial/merge.py
changeset 52060 8b7123c8947b
parent 52059 b332ae615714
child 52061 43e15277498e
--- a/mercurial/merge.py	Wed Oct 16 19:14:30 2024 +0200
+++ b/mercurial/merge.py	Tue Oct 01 13:49:11 2024 +0200
@@ -8,6 +8,7 @@
 from __future__ import annotations
 
 import collections
+import os
 import struct
 import typing
 from typing import Dict, Optional, Tuple
@@ -34,12 +35,15 @@
     pathutil,
     policy,
     pycompat,
+    requirements,
     scmutil,
     subrepoutil,
     util,
     worker,
 )
 
+rust_update_mod = policy.importrust("update")
+
 _pack = struct.pack
 _unpack = struct.unpack
 
@@ -147,6 +151,8 @@
     dircache = dict()
     dirstate = repo.dirstate
     wvfs = repo.wvfs
+    # wouldn't it be easier to loop over unknown files (and dirs)?
+
     if not force:
 
         def collectconflicts(conflicts, config):
@@ -1835,6 +1841,12 @@
 UPDATECHECK_LINEAR = b'linear'
 UPDATECHECK_NO_CONFLICT = b'noconflict'
 
+# Let extensions turn off any Rust code in the update code if that interferes
+# will their patching.
+# This being `True` does not mean that you have Rust extensions installed or
+# that the Rust path will be taken for any given invocation.
+MAYBE_USE_RUST_UPDATE = True
+
 
 def _update(
     repo,
@@ -2008,6 +2020,60 @@
         if not branchmerge and not wc.dirty(missing=True):
             followcopies = False
 
+        update_from_null = False
+        update_from_null_fallback = False
+        if (
+            MAYBE_USE_RUST_UPDATE
+            and rust_update_mod is not None
+            and p1.rev() == nullrev
+            and not branchmerge
+            # TODO it's probably not too hard to pass down the transaction and
+            # respect the write patterns from Rust. But since it doesn't affect
+            # a simple update from null, then it doesn't matter yet.
+            and repo.currenttransaction() is None
+            and matcher is None
+            and not wc.mergestate().active()
+            and b'.hgsubstate' not in p2
+        ):
+            working_dir_iter = os.scandir(repo.root)
+            maybe_hg_folder = next(working_dir_iter)
+            assert maybe_hg_folder is not None
+            if maybe_hg_folder.name == b".hg":
+                try:
+                    next(working_dir_iter)
+                except StopIteration:
+                    update_from_null = True
+
+        if update_from_null:
+            # Check the narrowspec and sparsespec here to display warnings
+            # more easily.
+            # TODO figure out of a way of bubbling up warnings to Python
+            # while not polluting the Rust code (probably a channel)
+            repo.narrowmatch()
+            sparse.matcher(repo, [nullrev, p2.rev()])
+            repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
+            # note that we're in the middle of an update
+            repo.vfs.write(b'updatestate', p2.hex())
+            try:
+                updated_count = rust_update_mod.update_from_null(
+                    repo.root, p2.rev()
+                )
+            except rust_update_mod.FallbackError:
+                update_from_null_fallback = True
+            else:
+                # We've changed the dirstate from Rust, we need to tell Python
+                repo.dirstate.invalidate()
+                # This includes setting the parents, since they are not read
+                # again on invalidation
+                with repo.dirstate.changing_parents(repo):
+                    repo.dirstate.setparents(fp2)
+                repo.dirstate.setbranch(p2.branch(), repo.currenttransaction())
+                sparse.prunetemporaryincludes(repo)
+                repo.hook(b'update', parent1=xp1, parent2=xp2, error=0)
+                # update completed, clear state
+                util.unlink(repo.vfs.join(b'updatestate'))
+                return updateresult(updated_count, 0, 0, 0)
+
         ### calculate phase
         mresult = calculateupdates(
             repo,
@@ -2131,11 +2197,13 @@
         # the dirstate.
         always = matcher is None or matcher.always()
         updatedirstate = updatedirstate and always and not wc.isinmemory()
-        if updatedirstate:
+        # If we're in the fallback case, we've already done this
+        if updatedirstate and not update_from_null_fallback:
             repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
             # note that we're in the middle of an update
             repo.vfs.write(b'updatestate', p2.hex())
 
+        # TODO don't run if Rust is available
         _advertisefsmonitor(
             repo, mresult.len((mergestatemod.ACTION_GET,)), p1.node()
         )
@@ -2172,14 +2240,14 @@
                 mergestatemod.recordupdates(
                     repo, mresult.actionsdict, branchmerge, getfiledata
                 )
-                # update completed, clear state
-                util.unlink(repo.vfs.join(b'updatestate'))
-
                 if not branchmerge:
                     repo.dirstate.setbranch(
                         p2.branch(), repo.currenttransaction()
                     )
 
+                # update completed, clear state
+                util.unlink(repo.vfs.join(b'updatestate'))
+
                 # If we're updating to a location, clean up any stale temporary includes
                 # (ex: this happens during hg rebase --abort).
                 if not branchmerge: