diff --git a/mercurial/copies.py b/mercurial/copies.py
index 25f8e6fc3f97a94ddb36dc7bb63cb15df6932c6c_bWVyY3VyaWFsL2NvcGllcy5weQ==..2717d29877b0149b25ea566e7f4fdd27c1d461f8_bWVyY3VyaWFsL2NvcGllcy5weQ== 100644
--- a/mercurial/copies.py
+++ b/mercurial/copies.py
@@ -259,7 +259,7 @@
     children = {}
 
     cl = repo.changelog
-    isancestor = cached_is_ancestor(cl.isancestorrev)
+    isancestor = cl.isancestorrev
     missingrevs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
     mrset = set(missingrevs)
     roots = set()
@@ -321,6 +321,8 @@
             list(revs), children, targetrev, revinfo, isancestor
         )
 
+    isancestor = cached_is_ancestor(isancestor)
+
     all_copies = {}
     for r in revs:
         copies = all_copies.pop(r, None)
diff --git a/rust/hg-core/src/copy_tracing.rs b/rust/hg-core/src/copy_tracing.rs
index 25f8e6fc3f97a94ddb36dc7bb63cb15df6932c6c_cnVzdC9oZy1jb3JlL3NyYy9jb3B5X3RyYWNpbmcucnM=..2717d29877b0149b25ea566e7f4fdd27c1d461f8_cnVzdC9oZy1jb3JlL3NyYy9jb3B5X3RyYWNpbmcucnM= 100644
--- a/rust/hg-core/src/copy_tracing.rs
+++ b/rust/hg-core/src/copy_tracing.rs
@@ -64,6 +64,42 @@
     }
 }
 
+/// A struct responsible for answering "is X ancestors of Y" quickly
+///
+/// The structure will delegate ancestors call to a callback, and cache the
+/// result.
+#[derive(Debug)]
+struct AncestorOracle<'a, A: Fn(Revision, Revision) -> bool> {
+    inner: &'a A,
+    pairs: HashMap<(Revision, Revision), bool>,
+}
+
+impl<'a, A: Fn(Revision, Revision) -> bool> AncestorOracle<'a, A> {
+    fn new(func: &'a A) -> Self {
+        Self {
+            inner: func,
+            pairs: HashMap::default(),
+        }
+    }
+
+    /// returns `true` if `anc` is an ancestors of `desc`, `false` otherwise
+    fn is_ancestor(&mut self, anc: Revision, desc: Revision) -> bool {
+        if anc > desc {
+            false
+        } else if anc == desc {
+            true
+        } else {
+            if let Some(b) = self.pairs.get(&(anc, desc)) {
+                *b
+            } else {
+                let b = (self.inner)(anc, desc);
+                self.pairs.insert((anc, desc), b);
+                b
+            }
+        }
+    }
+}
+
 /// Same as mercurial.copies._combine_changeset_copies, but in Rust.
 ///
 /// Arguments are:
@@ -77,8 +113,8 @@
 ///   * ChangedFiles
 /// isancestors(low_rev, high_rev): callback to check if a revision is an
 ///                                 ancestor of another
-pub fn combine_changeset_copies(
+pub fn combine_changeset_copies<A: Fn(Revision, Revision) -> bool>(
     revs: Vec<Revision>,
     children: HashMap<Revision, Vec<Revision>>,
     target_rev: Revision,
     rev_info: &impl Fn(Revision) -> RevInfo,
@@ -81,7 +117,7 @@
     revs: Vec<Revision>,
     children: HashMap<Revision, Vec<Revision>>,
     target_rev: Revision,
     rev_info: &impl Fn(Revision) -> RevInfo,
-    is_ancestor: &impl Fn(Revision, Revision) -> bool,
+    is_ancestor: &A,
 ) -> PathCopies {
     let mut all_copies = HashMap::new();
@@ -86,5 +122,6 @@
 ) -> PathCopies {
     let mut all_copies = HashMap::new();
+    let mut oracle = AncestorOracle::new(is_ancestor);
 
     for rev in revs {
         // Retrieve data computed in a previous iteration
@@ -168,7 +205,7 @@
                         _ => unreachable!(),
                     };
                     let merged_copies =
-                        merge_copies_dict(minor, major, &changes, is_ancestor);
+                        merge_copies_dict(minor, major, &changes, &mut oracle);
                     all_copies.insert(child, merged_copies);
                 }
             };
@@ -194,7 +231,7 @@
 /// In case of conflict, value from "major" will be picked, unless in some
 /// cases. See inline documentation for details.
 #[allow(clippy::if_same_then_else)]
-fn merge_copies_dict(
+fn merge_copies_dict<A: Fn(Revision, Revision) -> bool>(
     minor: TimeStampedPathCopies,
     major: TimeStampedPathCopies,
     changes: &ChangedFiles,
@@ -198,7 +235,7 @@
     minor: TimeStampedPathCopies,
     major: TimeStampedPathCopies,
     changes: &ChangedFiles,
-    is_ancestor: &impl Fn(Revision, Revision) -> bool,
+    oracle: &mut AncestorOracle<A>,
 ) -> TimeStampedPathCopies {
     if minor.is_empty() {
         return major;
@@ -244,7 +281,8 @@
                         // If the two entry are identical, no need to do
                         // anything (but diff should not have yield them)
                         unreachable!();
-                    } else if is_ancestor(src_major.rev, src_minor.rev) {
+                    } else if oracle.is_ancestor(src_major.rev, src_minor.rev)
+                    {
                         pick_minor();
                     } else {
                         pick_major();
@@ -271,7 +309,7 @@
                     // each side might conflict.  The major side will win such
                     // conflict.
                     pick_major();
-                } else if is_ancestor(src_major.rev, src_minor.rev) {
+                } else if oracle.is_ancestor(src_major.rev, src_minor.rev) {
                     // If the minor side is strictly newer than the major side,
                     // it should be kept.
                     pick_minor();
@@ -279,7 +317,7 @@
                     // without any special case, the "major" value win other
                     // the "minor" one.
                     pick_major();
-                } else if is_ancestor(src_minor.rev, src_major.rev) {
+                } else if oracle.is_ancestor(src_minor.rev, src_major.rev) {
                     // the "major" rev is a direct ancestors of "minor", any
                     // different value should overwrite
                     pick_major();