# HG changeset patch
# User Arseniy Alekseyev <aalekseyev@janestreet.com>
# Date 1741102386 18000
#      Tue Mar 04 10:33:06 2025 -0500
# Node ID e5e02ba608909e81fe47a469e2e441732222c004
# Parent  0471bf04ddf1231456a848d0965b3d05bf00bf8f
# EXP-Topic copytracing-in-rust
rust: refactor StatusRevRev so it works on borrowed manifests

diff --git a/rust/hg-core/src/operations/status_rev_rev.rs b/rust/hg-core/src/operations/status_rev_rev.rs
--- a/rust/hg-core/src/operations/status_rev_rev.rs
+++ b/rust/hg-core/src/operations/status_rev_rev.rs
@@ -22,6 +22,7 @@
 }
 
 /// What copy/rename information to report.
+#[derive(Copy, Clone)]
 pub enum ListCopies {
     /// Report copies only for added files.
     Added,
@@ -30,7 +31,8 @@
 }
 
 /// Strategy for determining a file's copy source.
-enum CopyStrategy<'a> {
+#[derive(Clone)]
+pub(crate) enum CopyStrategy<'a> {
     /// Use the [`Repo`] to look up copy information in filelog metadata.
     /// Assumes we are producing the status for a single changeset.
     Change(&'a Repo),
@@ -38,10 +40,17 @@
 }
 
 pub struct StatusRevRev<'a> {
-    manifest1: Manifest,
-    manifest2: Manifest,
-    narrow_matcher: Box<dyn Matcher>,
-    copies: Option<(ListCopies, CopyStrategy<'a>)>,
+    pub(crate) manifest1: Manifest,
+    pub(crate) manifest2: Manifest,
+    pub(crate) narrow_matcher: Box<dyn Matcher>,
+    pub(crate) copies: Option<(ListCopies, CopyStrategy<'a>)>,
+}
+
+pub struct StatusRevRevRef<'a> {
+    pub(crate) manifest1: &'a Manifest,
+    pub(crate) manifest2: &'a Manifest,
+    pub(crate) narrow_matcher: &'a dyn Matcher,
+    pub(crate) copies: Option<(ListCopies, CopyStrategy<'a>)>,
 }
 
 fn manifest_for_rev(repo: &Repo, rev: Revision) -> Result<Manifest, HgError> {
@@ -85,17 +94,38 @@
 }
 
 impl StatusRevRev<'_> {
+    fn get_ref(&self) -> StatusRevRevRef<'_> {
+        StatusRevRevRef {
+            manifest1: &self.manifest1,
+            manifest2: &self.manifest2,
+            narrow_matcher: &*self.narrow_matcher,
+            copies: self.copies.clone(),
+        }
+    }
+
     pub fn iter(
         &self,
     ) -> impl Iterator<Item = Result<(StatusPath<'_>, DiffStatus), HgError>>
     {
+        self.get_ref().iter()
+    }
+}
+
+impl<'a> StatusRevRevRef<'a> {
+    pub fn iter(
+        self,
+    ) -> impl Iterator<Item = Result<(StatusPath<'a>, DiffStatus), HgError>>
+    {
         let iter1 = self.manifest1.iter();
         let iter2 = self.manifest2.iter();
+        let copies = self.copies.clone();
 
         let merged =
             merge_join_results_by(iter1, iter2, |i1, i2| i1.path.cmp(i2.path));
+        let narrow_matcher = self.narrow_matcher;
+        let manifest1 = self.manifest1;
 
-        filter_map_results(merged, |entry| {
+        filter_map_results(merged, move |entry| {
             let (path, status) = match &entry {
                 EitherOrBoth::Left(entry) => (entry.path, DiffStatus::Removed),
                 EitherOrBoth::Right(entry) => (entry.path, DiffStatus::Added),
@@ -110,28 +140,34 @@
                     }
                 }
             };
-            if !self.narrow_matcher.matches(path) {
+            if !narrow_matcher.matches(path) {
                 return Ok(None);
             }
             let path = StatusPath {
                 path: Cow::Borrowed(path),
-                copy_source: self
-                    .find_copy_source(path, status, entry.right().as_ref())?
-                    .map(Cow::Owned),
+                copy_source: Self::find_copy_source_impl(
+                    &copies,
+                    manifest1,
+                    path,
+                    status,
+                    entry.right().as_ref(),
+                )?
+                .map(Cow::Owned),
             };
             Ok(Some((path, status)))
         })
     }
 
     /// Returns the path that a file was copied from, if it should be reported.
-    fn find_copy_source(
-        &self,
+    fn find_copy_source_impl(
+        copies: &Option<(ListCopies, CopyStrategy<'a>)>,
+        manifest1: &Manifest,
         path: &HgPath,
         status: DiffStatus,
         entry: Option<&ManifestEntry>,
     ) -> Result<Option<HgPathBuf>, HgError> {
         let Some(entry) = entry else { return Ok(None) };
-        let Some((list, strategy)) = &self.copies else {
+        let Some((list, strategy)) = copies else {
             return Ok(None);
         };
         match (list, status) {
@@ -148,7 +184,7 @@
                     .filelog(path)?
                     .data_for_node(entry.node_id().unwrap())?;
                 if let Some(copy) = data.metadata()?.parse()?.copy {
-                    if self.manifest1.find_by_path(copy)?.is_some() {
+                    if manifest1.find_by_path(copy)?.is_some() {
                         return Ok(Some(copy.to_owned()));
                     }
                 }
# HG changeset patch
# User Arseniy Alekseyev <aalekseyev@janestreet.com>
# Date 1741106816 18000
#      Tue Mar 04 11:46:56 2025 -0500
# Node ID 2437d682a6c38fac4a77bef8e6a44b2158cc7eeb
# Parent  e5e02ba608909e81fe47a469e2e441732222c004
# EXP-Topic copytracing-in-rust
rust: implement copytracing in rhg

This introduces:

- `copytracing_v1` module that implements the actual copytracing algorithm,
transcribed from Python.

- `FilenodeGraphAccess` and `ManifestAccess` abstractions to make it
possible to use the copytracing algorithm with a data store alternative
to vanilla mercurial.

- module `status_rev_rev_nocopy` to compute the differences and commonalities
between two manifests in a maximally detailed form.

- adjusted the call site in `status.rs` to use the new copytracing, via the
new module in rhg `copytracing.rs` which instantiates all the abstractions the
correct way for rhg.

diff --git a/mercurial/configitems.toml b/mercurial/configitems.toml
--- a/mercurial/configitems.toml
+++ b/mercurial/configitems.toml
@@ -1982,6 +1982,11 @@
 
 [[items]]
 section = "rust"
+name = "copytracing.parallel"
+default = true
+
+[[items]]
+section = "rust"
 name = "update-from-null"
 default = true
 experimental = true
diff --git a/rust/hg-core/src/copy_tracing.rs b/rust/hg-core/src/copy_tracing.rs
--- a/rust/hg-core/src/copy_tracing.rs
+++ b/rust/hg-core/src/copy_tracing.rs
@@ -5,6 +5,10 @@
 #[cfg(test)]
 mod tests;
 
+pub mod copytracing_v1;
+pub mod filenode_graph_access;
+pub mod types;
+
 use crate::utils::hg_path::HgPath;
 use crate::utils::hg_path::HgPathBuf;
 use crate::Revision;
diff --git a/rust/hg-core/src/copy_tracing/copytracing_v1.rs b/rust/hg-core/src/copy_tracing/copytracing_v1.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/copy_tracing/copytracing_v1.rs
@@ -0,0 +1,514 @@
+use std::{
+    cmp::Ordering,
+    collections::{BTreeMap, BTreeSet, HashSet},
+    mem,
+    sync::Mutex,
+};
+
+use crate::{
+    errors::HgError,
+    matchers::{AlwaysMatcher, Matcher},
+    revlog::changelog::Changelog,
+    utils::hg_path::{HgPath, HgPathBuf},
+    Node, NULL_NODE,
+};
+use rayon::iter::{IntoParallelIterator, ParallelBridge, ParallelIterator};
+
+use super::{
+    filenode_graph_access::{
+        FileCtxAccessors, FilenodeGraphAccess, ManifestAccessForCopytracing,
+        ParentsCtx,
+    },
+    types::FileAndNode,
+};
+
+#[derive(PartialEq, Eq)]
+struct FilenodeAncestorsHeapKey {
+    approx_linkrev: i32,
+    // NOTE: path is missing from this key, which leads to
+    // surprising things on filenode collisions between different
+    // files. This is only done to mirror what Python hg is doing.
+    filenode: Node,
+}
+
+impl PartialOrd for FilenodeAncestorsHeapKey {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for FilenodeAncestorsHeapKey {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        let o = self.approx_linkrev.cmp(&other.approx_linkrev);
+        if !matches!(o, Ordering::Equal) {
+            return o;
+        }
+        (self.filenode.data).cmp(&(other.filenode.data))
+    }
+}
+
+/// Topological order iterator over the ancestors of a file node,
+/// taking renaming edges into account.
+/// For reference, see `basefilectx.ancestors` in `context.py`
+///
+/// TODO: support [followfirst] if needed?
+struct FilenodeAncestorsIterator<'a, G: FilenodeGraphAccess> {
+    // `visited` set is missing in Python.
+    // it's used to detect out-of-order linkrev causing weird traversal
+    // artifacts or loops
+    visited: HashSet<FileAndNode>,
+    heap: BTreeMap<FilenodeAncestorsHeapKey, G::FileCtx>,
+    graph: &'a G,
+}
+
+impl<'a, G: FilenodeGraphAccess> Iterator
+    for FilenodeAncestorsIterator<'a, G>
+{
+    type Item = Result<FileAndNode, HgError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self.next_impl() {
+            Ok(None) => None,
+            Ok(Some(v)) => Some(Ok(v.file_and_node())),
+            Err(e) => Some(Err(e)),
+        }
+    }
+}
+
+impl<'a, G: FilenodeGraphAccess> FilenodeAncestorsIterator<'a, G> {
+    fn new(graph: &'a G, initial: FileAndNode) -> Result<Self, HgError> {
+        let mut s = Self {
+            heap: BTreeMap::new(),
+            graph,
+            visited: HashSet::new(),
+        };
+        s.maybe_add(Some(graph.ctx(initial)?))?;
+        Ok(s)
+    }
+
+    fn maybe_add(
+        &mut self,
+        file_and_node: Option<G::FileCtx>,
+    ) -> Result<(), HgError> {
+        let Some(fctx) = file_and_node else {
+            return Ok(());
+        };
+        self.heap.insert(
+            FilenodeAncestorsHeapKey {
+                approx_linkrev: fctx.approx_linkrev(),
+                filenode: fctx.node(),
+            },
+            fctx,
+        );
+        Ok(())
+    }
+
+    fn next_impl(&mut self) -> Result<Option<G::FileCtx>, HgError> {
+        loop {
+            let next = self.heap.pop_last();
+            let Some(next) = next else {
+                return Ok(None);
+            };
+            let (
+                FilenodeAncestorsHeapKey {
+                    approx_linkrev,
+                    filenode: _,
+                },
+                fctx,
+            ) = next;
+            if self.visited.contains(&fctx.file_and_node()) {
+                eprintln!(
+                    "warning: filenode graph repeat visit {}:{:x}(linkrev {})",
+                    String::from_utf8_lossy(fctx.path().as_bytes()),
+                    fctx.node(),
+                    approx_linkrev,
+                );
+                continue;
+            }
+            self.visited.insert(fctx.file_and_node());
+            let ParentsCtx { p1, p2 } = self.graph.get_parents(&fctx)?;
+            self.maybe_add(p1)?;
+            self.maybe_add(p2)?;
+            return Ok(Some(fctx));
+        }
+    }
+}
+
+/// Ancestors iterator (see `basefilectx.ancestors` in `context.py`)
+fn _fctx_ancestors<G: FilenodeGraphAccess>(
+    filenode_graph_access: &G,
+    file_and_node: FileAndNode,
+) -> Result<FilenodeAncestorsIterator<'_, G>, HgError> {
+    FilenodeAncestorsIterator::new(filenode_graph_access, file_and_node)
+}
+
+/// Returns the path that the file `fileid` was known as in ancestor `am`
+/// or base manifest `basemf`
+///
+/// see `_tracefile` in `copies.py`
+#[inline(never)]
+fn _tracefile<M: ManifestAccessForCopytracing, G: FilenodeGraphAccess>(
+    manifest_access: &M,
+    filenode_graph_access: &G,
+    fileid: FileAndNode,
+    am: &M::ManifestDict,
+    basemf: &M::ManifestDict,
+) -> Result<Option<HgPathBuf>, HgError> {
+    for f in _fctx_ancestors(filenode_graph_access, fileid)?.skip(1) {
+        let f = f?;
+
+        let path = f.path;
+        if manifest_access.find(am, &path)?.map(|(_typ, node)| node)
+            == Some(f.node)
+        {
+            return Ok(Some(path));
+        }
+        if manifest_access
+            .find(basemf, &path)?
+            .map(|(_typ, node)| node)
+            == Some(f.node)
+        {
+            return Ok(Some(path));
+        }
+    }
+    Ok(None)
+}
+
+/// Computes which files are in b but not a.
+/// This is its own function so extensions can easily wrap this call to see
+/// what files _forwardcopies is about to process.
+///
+/// See `_computeforwardmissing` in `copies.py`
+#[inline(never)]
+fn _computeforwardmissing<M: ManifestAccessForCopytracing>(
+    manifest_access: &M,
+    matcher: &(impl Matcher + ?Sized),
+    a: &M::ManifestDict,
+    b: &M::ManifestDict,
+) -> Result<Vec<FileAndNode>, HgError> {
+    let diff_status = manifest_access.diff_status(b, a, false)?;
+    let mut missing = vec![];
+    for (path, before, after) in diff_status {
+        if let (Some(before), None) = (before, after) {
+            let path: HgPathBuf = path.into_owned();
+            if matcher.matches(&path) {
+                missing.push(FileAndNode {
+                    path,
+                    node: before.1,
+                })
+            }
+        }
+    }
+    Ok(missing)
+}
+
+/// find {dst@b: src@a} copy mapping where a is an ancestor of b
+///
+/// See `_committedforwardcopies` in `copies.py`
+#[inline(never)]
+fn _committedforwardcopies<
+    M: ManifestAccessForCopytracing<ManifestDict: Sync> + Sync,
+    G: FilenodeGraphAccess + Sync,
+>(
+    manifest_access: &M,
+    filenode_graph_access: &G,
+    parallel: bool,
+    matcher: &(impl Matcher + ?Sized),
+    a: &M::ManifestDict,
+    b: &M::ManifestDict,
+    base: &M::ManifestDict,
+) -> Result<Copies, HgError> {
+    // TODO: forwardmissingmatch optimization
+
+    let missing = _computeforwardmissing(manifest_access, matcher, a, b)?;
+
+    let cm = Mutex::new(BTreeMap::new());
+
+    let f = |f: &FileAndNode| -> Result<(), HgError> {
+        let opath = _tracefile(
+            manifest_access,
+            filenode_graph_access,
+            f.clone(),
+            a,
+            base,
+        )?;
+        if let Some(opath) = opath {
+            cm.lock().unwrap().insert(f.path.clone(), opath);
+        }
+        Ok(())
+    };
+
+    if parallel {
+        missing
+            .iter()
+            .par_bridge()
+            .into_par_iter()
+            .try_for_each(f)?;
+    } else {
+        missing.iter().try_for_each(f)?;
+    }
+    let mut lock = cm.lock().unwrap();
+    let res = mem::take(&mut *lock);
+    Ok(res)
+}
+
+fn _forwardcopies<
+    M: ManifestAccessForCopytracing + Sync,
+    G: FilenodeGraphAccess + Sync,
+>(
+    manifest_access: &M,
+    graph_access: &G,
+    parallel: bool,
+    matcher: &(impl Matcher + ?Sized),
+    a: &M::ManifestDict,
+    b: &M::ManifestDict,
+    base: Option<&M::ManifestDict>,
+) -> Result<Copies, HgError>
+where
+    M::ManifestDict: Sync,
+{
+    let base = base.unwrap_or(a);
+
+    _committedforwardcopies(
+        manifest_access,
+        graph_access,
+        parallel,
+        matcher,
+        a,
+        b,
+        base,
+    )
+}
+
+/// Finds a greatest common ancestor between two revisions.
+///
+/// Intended to correspond to `changectx.ancestor()`, but it's
+/// actually more similar to `revlog.ancestor`
+pub fn _ancestor(
+    changelog: &Changelog,
+    x: Node,
+    y: Node,
+) -> Result<Node, HgError> {
+    let x_rev = changelog.rev_from_node(x.into())?;
+    let y_rev = changelog.rev_from_node(y.into())?;
+    let index = &changelog.revlog.index();
+    let ancestors = index
+        .ancestors(&[x_rev, y_rev])
+        .map_err(|err| HgError::abort_simple(err.to_string()))?;
+    // TODO: respect merge.preferancestor when GCA is not unique
+    let mut ancestors: Vec<Node> = ancestors
+        .into_iter()
+        .map(|rev| *(changelog.node_from_rev(rev)))
+        .collect();
+    ancestors.sort_by_key(|node| node.data);
+    Ok(*ancestors.last().unwrap_or(&NULL_NODE))
+}
+
+/// given copies to context 'dst', finds renames from that context
+///
+/// See `_reverse_renames` in `copies.py`
+fn _reverse_renames<M: ManifestAccessForCopytracing>(
+    manifest_access: &M,
+    matcher: &(impl Matcher + ?Sized),
+    copies: Copies,
+    dst: &M::ManifestDict,
+) -> Result<Copies, HgError> {
+    /* Even though we're not taking copies into account, 1:n rename situations
+    can still exist (e.g. hg cp a b; hg mv a c). In those cases we
+    arbitrarily pick one of the renames.
+    */
+    let mut r: Copies = BTreeMap::new();
+    for (k, v) in copies.into_iter() {
+        if !matcher.matches(&v) {
+            continue;
+        }
+        if manifest_access.find(dst, &v)?.is_some() {
+            continue;
+        }
+        r.insert(v, k);
+    }
+    Ok(r)
+}
+
+/// find renames from a to b
+///
+/// See `_backwardrenames` in `copies.py`
+fn _backwardrenames<
+    M: ManifestAccessForCopytracing + Sync,
+    G: FilenodeGraphAccess + Sync,
+>(
+    manifest_access: &M,
+    filenode_graph_access: &G,
+    parallel: bool,
+    matcher: &(impl Matcher + ?Sized),
+    a: &M::ManifestDict,
+    b: &M::ManifestDict,
+) -> Result<Copies, HgError>
+where
+    M::ManifestDict: Sync,
+{
+    /* We don't want to pass in "matcher" here, since that would filter
+    the destination by it. Since we're reversing the copies, we want
+    to filter the source instead. */
+    let copies = _forwardcopies(
+        manifest_access,
+        filenode_graph_access,
+        parallel,
+        &AlwaysMatcher,
+        b,
+        a,
+        None,
+    )?;
+    _reverse_renames(manifest_access, matcher, copies, a)
+}
+
+pub type Copies = BTreeMap<HgPathBuf, HgPathBuf>;
+
+fn _is_in<M: ManifestAccessForCopytracing>(
+    manifest_access: &M,
+    m: &M::ManifestDict,
+    path: &HgPath,
+) -> Result<bool, HgError> {
+    Ok(manifest_access.find(m, path)?.is_some())
+}
+
+/// filters out invalid copies after chaining
+///
+/// See `_filter` in `copies.py`
+fn _filter<M: ManifestAccessForCopytracing>(
+    manifest_access: &M,
+    src: &M::ManifestDict,
+    dst: &M::ManifestDict,
+    mut t: Copies,
+) -> Result<Copies, HgError> {
+    /* When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
+    # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
+    # in the following table (not including trivial cases). For example, case 6
+    # is where a file existed in 'src' and remained under that name in 'mid' and
+    # then was renamed between 'mid' and 'dst'.
+    #
+    # case src mid dst result
+    #   1   x   y   -    -
+    #   2   x   y   y   x->y
+    #   3   x   y   x    -
+    #   4   x   y   z   x->z
+    #   5   -   x   y    -
+    #   6   x   x   y   x->y
+    #
+    # _chain() takes care of chaining the copies in 'a' and 'b', but it
+    # cannot tell the difference between cases 1 and 2, between 3 and 4, or
+    # between 5 and 6, so it includes all cases in its result.
+    # Cases 1, 3, and 5 are then removed by _filter().
+    */
+
+    for (k, v) in t.clone().into_iter() {
+        if k == v {
+            // case 3
+            t.remove(&k);
+        } else if !(_is_in(manifest_access, src, &v)?) {
+            // case 5
+            // remove copies from files that didn't exist
+            t.remove(&k);
+        } else if !(_is_in(manifest_access, dst, &k)?) {
+            // case 1
+            // remove copies to files that were then removed
+            t.remove(&k);
+        }
+    }
+    Ok(t)
+}
+
+/// chain two sets of copies 'prefix' and 'suffix'
+///
+/// See `_chain` in `copies.py`
+fn _chain(prefix: Copies, suffix: Copies) -> Copies {
+    let mut result = prefix.clone();
+    for (key, value) in suffix.into_iter() {
+        result.insert(key, prefix.get(&value).cloned().unwrap_or(value));
+    }
+    result
+}
+
+/// find {dst@y: src@x} copy mapping for directed compare
+///
+/// See `pathcopies` in `copies.py`
+pub fn pathcopies_gen<
+    M: ManifestAccessForCopytracing + Sync,
+    G: FilenodeGraphAccess + Sync,
+>(
+    changelog: &Changelog,
+    manifest_access: &M,
+    graph_access: &G,
+    parallel: bool,
+    matcher: &(impl Matcher + ?Sized),
+    x: (Node, &M::ManifestDict),
+    y: (Node, &M::ManifestDict),
+) -> Result<Copies, HgError>
+where
+    M::ManifestDict: Send + Sync,
+{
+    let (x, xm) = x;
+    let (y, ym) = y;
+    if x == y {
+        return Ok(BTreeMap::new());
+    };
+
+    let a = _ancestor(changelog, x, y)?;
+    let copies;
+    if a == x {
+        copies = _forwardcopies(
+            manifest_access,
+            graph_access,
+            parallel,
+            matcher,
+            xm,
+            ym,
+            None,
+        )?;
+    } else if a == y {
+        copies = _backwardrenames(
+            manifest_access,
+            graph_access,
+            parallel,
+            matcher,
+            xm,
+            ym,
+        )?;
+    } else {
+        let am = manifest_access.manifest_for_changelog_node(a)?;
+        let base = if a == NULL_NODE { None } else { Some(xm) };
+        let mut x_copies = _forwardcopies(
+            manifest_access,
+            graph_access,
+            parallel,
+            matcher,
+            &am,
+            xm,
+            None,
+        )?;
+        let mut y_copies = _forwardcopies(
+            manifest_access,
+            graph_access,
+            parallel,
+            matcher,
+            &am,
+            ym,
+            base,
+        )?;
+        let x_copies_keys: BTreeSet<HgPathBuf> =
+            x_copies.keys().cloned().collect();
+        let y_copies_keys: BTreeSet<HgPathBuf> =
+            x_copies.keys().cloned().collect();
+        let same_keys = x_copies_keys.intersection(&y_copies_keys);
+        for k in same_keys.into_iter() {
+            if x_copies.get(k) == y_copies.get(k) {
+                x_copies.remove(k);
+                y_copies.remove(k);
+            }
+        }
+        let x_backward_renames =
+            _reverse_renames(manifest_access, matcher, x_copies, xm)?;
+        copies = _chain(x_backward_renames, y_copies)
+    }
+    _filter(manifest_access, xm, ym, copies)
+}
diff --git a/rust/hg-core/src/copy_tracing/filenode_graph_access.rs b/rust/hg-core/src/copy_tracing/filenode_graph_access.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/copy_tracing/filenode_graph_access.rs
@@ -0,0 +1,336 @@
+use std::{borrow::Cow, num::NonZero};
+
+use crate::{
+    errors::HgError,
+    revlog::{filelog::Filelog, options::RevlogOpenOptions},
+    utils::hg_path::HgPath,
+    vfs::VfsImpl,
+    Node,
+};
+
+use super::types::FileAndNode;
+
+pub struct ParentsCtx<P> {
+    pub p1: Option<P>,
+    pub p2: Option<P>,
+}
+
+pub trait FileCtxAccessors {
+    fn path(&self) -> &HgPath;
+    fn node(&self) -> Node;
+    fn file_and_node(&self) -> FileAndNode;
+    fn approx_linkrev(&self) -> i32;
+}
+
+pub trait FilenodeGraphAccess {
+    /// FileCtx corresponds to a particular file version (file path and file
+    /// node). Ownership of `FileCtx` may additionally hold certain
+    /// file-relevant caches alive, such as holding open the corresponding
+    /// FileLog.
+    type FileCtx: FileCtxAccessors;
+    fn ctx(
+        &self,
+        file_and_node: FileAndNode,
+    ) -> Result<Self::FileCtx, HgError>;
+    fn get_parents(
+        &self,
+        key: &Self::FileCtx,
+    ) -> Result<ParentsCtx<Self::FileCtx>, HgError>;
+}
+
+type TypAndNode = (Option<NonZero<u8>>, Node);
+
+type DiffStatusEntry<'a> =
+    (Cow<'a, HgPath>, Option<TypAndNode>, Option<TypAndNode>);
+
+/// The manifest operations used by copytracing
+pub trait ManifestAccessForCopytracing {
+    type ManifestDict;
+
+    fn manifest_for_changelog_node(
+        &self,
+        node: Node,
+    ) -> Result<Self::ManifestDict, HgError>;
+
+    fn find(
+        &self,
+        manifest: &Self::ManifestDict,
+        path: &HgPath,
+    ) -> Result<Option<TypAndNode>, HgError>;
+
+    fn diff_status<'a>(
+        &'a self,
+        manifest1: &'a Self::ManifestDict,
+        manifest2: &'a Self::ManifestDict,
+        include_clean: bool,
+    ) -> Result<Vec<DiffStatusEntry>, HgError>;
+}
+
+pub struct FilelogOpener<'a> {
+    pub store_vfs: &'a VfsImpl,
+    pub open_options: RevlogOpenOptions,
+}
+
+impl<'a> FilelogOpener<'a> {
+    pub fn open(&self, path: &HgPath) -> Result<Filelog, HgError> {
+        Filelog::open_vfs(self.store_vfs, path, self.open_options)
+    }
+}
+
+pub mod vanilla {
+    use std::{borrow::Cow, cell::Ref, rc::Rc};
+
+    use crate::{
+        copy_tracing::types::{FileAndNode, Parents},
+        errors::HgError,
+        matchers::AlwaysMatcher,
+        operations::status_rev_rev_nocopy::StatusRevRevNocopies,
+        repo::Repo,
+        revlog::{
+            changelog::Changelog,
+            filelog::{Filelog, FilelogRevisionData},
+            manifest::{Manifest, Manifestlog},
+        },
+        utils::hg_path::HgPath,
+        Node, Revision, UncheckedRevision, NULL_NODE, NULL_REVISION,
+    };
+
+    use super::{
+        FileCtxAccessors, FilelogOpener, FilenodeGraphAccess,
+        ManifestAccessForCopytracing, ParentsCtx,
+    };
+
+    pub struct VanillaFileCtx {
+        file_and_node: FileAndNode,
+        revision: Revision,
+        approx_linkrev: UncheckedRevision,
+        filelog: Rc<Filelog>,
+    }
+
+    impl FileCtxAccessors for VanillaFileCtx {
+        fn path(&self) -> &HgPath {
+            &self.file_and_node.path
+        }
+
+        fn node(&self) -> Node {
+            self.file_and_node.node
+        }
+
+        fn file_and_node(&self) -> FileAndNode {
+            self.file_and_node.clone()
+        }
+
+        fn approx_linkrev(&self) -> i32 {
+            self.approx_linkrev.0
+        }
+    }
+
+    impl VanillaFileCtx {
+        fn new(
+            filelog: Rc<Filelog>,
+            file_and_node: FileAndNode,
+        ) -> Result<Self, HgError> {
+            let revision =
+                filelog.revlog.rev_from_node(file_and_node.node.into())?;
+            let approx_linkrev = filelog
+                .revlog
+                .index()
+                .get_entry(revision)
+                .unwrap()
+                .link_revision();
+            Ok(Self {
+                filelog,
+                file_and_node,
+                revision,
+                approx_linkrev,
+            })
+        }
+
+        fn child_ctx(
+            &self,
+            opener: &FilelogOpener,
+            child: Option<FileAndNode>,
+        ) -> Result<Option<VanillaFileCtx>, HgError> {
+            let Some(file_and_node) = child else {
+                return Ok(None);
+            };
+            let filelog = if file_and_node.path == self.file_and_node.path {
+                self.filelog.clone()
+            } else {
+                Rc::new(opener.open(&file_and_node.path)?)
+            };
+            Ok(Some(Self::new(filelog, file_and_node)?))
+        }
+    }
+
+    pub struct VanillaFilenodeGraphAccess<'a> {
+        pub filelog_opener: FilelogOpener<'a>,
+    }
+
+    impl<'a> FilenodeGraphAccess for VanillaFilenodeGraphAccess<'a> {
+        type FileCtx = VanillaFileCtx;
+
+        fn ctx(
+            &self,
+            file_and_node: FileAndNode,
+        ) -> Result<Self::FileCtx, HgError> {
+            VanillaFileCtx::new(
+                Rc::new(self.filelog_opener.open(&file_and_node.path)?),
+                file_and_node,
+            )
+        }
+
+        fn get_parents(
+            &self,
+            key: &Self::FileCtx,
+        ) -> Result<super::ParentsCtx<Self::FileCtx>, HgError> {
+            let filelog = &key.filelog;
+            let rev = key.revision;
+            let Parents {
+                p1,
+                p2,
+                approx_linkrev: _,
+            } = compute_one_entry(
+                filelog.as_ref(),
+                &key.file_and_node.path,
+                rev,
+            )?;
+            let p1 = key.child_ctx(&self.filelog_opener, p1)?;
+            let p2 = key.child_ctx(&self.filelog_opener, p2)?;
+            Ok(ParentsCtx { p1, p2 })
+        }
+    }
+
+    /// Compute the parents for a given file revision, taking copies
+    /// into account. Corresponds to the computation done in
+    /// Python that's spread across `basefilectx.parents` in `context.py`,
+    /// `filectx._copied`, and more.
+    pub fn compute_one_entry(
+        filelog: &Filelog,
+        path: &HgPath,
+        rev: Revision,
+    ) -> Result<Parents, HgError> {
+        let index_entry = filelog.revlog.index().get_entry(rev).unwrap();
+        let entry = filelog.entry_for_unchecked_rev(rev.into())?;
+        let p1_rev = entry.0.p1().unwrap_or(NULL_REVISION);
+        let p2_rev = entry.0.p2().unwrap_or(NULL_REVISION);
+        let p1_node = filelog.revlog.node_from_rev(p1_rev);
+        let p2_node = filelog.revlog.node_from_rev(p2_rev);
+        let data: FilelogRevisionData;
+        let p1 = if *p1_node != NULL_NODE {
+            Some((path, *p1_node))
+        } else {
+            data = filelog.data_for_unchecked_rev(rev.into())?;
+            let parsed = data.metadata()?.parse()?;
+            match (parsed.copy, parsed.copyrev) {
+                (Some(copy), Some(copyrev)) => Some((copy, copyrev)),
+                _ => None,
+            }
+        };
+        let p1 = p1.map(|(path, node)| FileAndNode {
+            path: path.to_owned(),
+            node,
+        });
+        let p2 = if *p2_node == NULL_NODE {
+            None
+        } else {
+            Some(FileAndNode {
+                path: path.to_owned(),
+                node: *p2_node,
+            })
+        };
+        let approx_linkrev = index_entry.link_revision().0;
+
+        Ok(Parents {
+            p1,
+            p2,
+            approx_linkrev,
+        })
+    }
+
+    pub struct VanillaManifestAccess<'a> {
+        pub(crate) changelog: Ref<'a, Changelog>,
+        pub(crate) manifestlog: Ref<'a, Manifestlog>,
+    }
+
+    impl<'a> VanillaManifestAccess<'a> {
+        pub fn new(repo: &'a Repo) -> Result<Self, HgError> {
+            let changelog = repo.changelog()?;
+            let manifestlog = repo.manifestlog()?;
+            Ok(Self {
+                changelog,
+                manifestlog,
+            })
+        }
+
+        pub fn borrow(&'a self) -> VanillaManifestAccessRef<'a> {
+            VanillaManifestAccessRef {
+                changelog: &self.changelog,
+                manifestlog: &self.manifestlog,
+            }
+        }
+    }
+
+    /// VanillaManifestAccess, but thread-safe (`Sync`)
+    /// (because apparently `Ref` is not `Sync`)
+    pub struct VanillaManifestAccessRef<'a> {
+        pub(crate) changelog: &'a Changelog,
+        pub(crate) manifestlog: &'a Manifestlog,
+    }
+
+    impl<'a> ManifestAccessForCopytracing for VanillaManifestAccessRef<'a> {
+        type ManifestDict = Manifest;
+
+        fn manifest_for_changelog_node(
+            &self,
+            node: Node,
+        ) -> Result<Self::ManifestDict, HgError> {
+            let rev = self.changelog.rev_from_node(node.into())?;
+            let entry = self.changelog.entry(rev)?;
+            let manifest_node = entry.data()?.manifest_node()?;
+            Ok(self.manifestlog.data_for_node(manifest_node.into())?)
+        }
+
+        fn find(
+            &self,
+            manifest: &Self::ManifestDict,
+            path: &HgPath,
+        ) -> Result<Option<super::TypAndNode>, HgError> {
+            let entry = manifest.find_by_path(path)?;
+            match entry {
+                None => Ok(None),
+                Some(entry) => Ok(Some((entry.flags, entry.node_id()?))),
+            }
+        }
+
+        fn diff_status<'b>(
+            &'b self,
+            manifest1: &'b Self::ManifestDict,
+            manifest2: &'b Self::ManifestDict,
+            include_clean: bool,
+        ) -> Result<
+            Vec<(
+                Cow<'b, HgPath>,
+                Option<super::TypAndNode>,
+                Option<super::TypAndNode>,
+            )>,
+            HgError,
+        > {
+            let iter = StatusRevRevNocopies {
+                manifest1,
+                manifest2,
+                narrow_matcher: &AlwaysMatcher,
+            };
+            let mut result = vec![];
+            for entry in iter.iter_detailed_nocopies() {
+                let (path, status) = entry?;
+                let (before, after) = status;
+                if !include_clean && (before == after) {
+                    continue;
+                }
+                result.push((path, before, after));
+            }
+            Ok(result)
+        }
+    }
+}
diff --git a/rust/hg-core/src/copy_tracing/types/mod.rs b/rust/hg-core/src/copy_tracing/types/mod.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/copy_tracing/types/mod.rs
@@ -0,0 +1,16 @@
+use crate::{utils::hg_path::HgPathBuf, Node};
+
+#[derive(PartialEq, Eq, Hash, Clone)]
+pub struct FileAndNode {
+    pub path: HgPathBuf,
+    pub node: Node,
+}
+
+#[derive(Clone)]
+pub struct Parents {
+    pub p1: Option<FileAndNode>,
+    pub p2: Option<FileAndNode>,
+    // [approx_linkrev] is used to walk the file nodes in
+    // topological order
+    pub approx_linkrev: i32,
+}
diff --git a/rust/hg-core/src/operations/mod.rs b/rust/hg-core/src/operations/mod.rs
--- a/rust/hg-core/src/operations/mod.rs
+++ b/rust/hg-core/src/operations/mod.rs
@@ -6,7 +6,8 @@
 mod cat;
 mod debugdata;
 mod list_tracked_files;
-mod status_rev_rev;
+pub(crate) mod status_rev_rev;
+pub(crate) mod status_rev_rev_nocopy;
 pub use annotate::{
     annotate, AnnotateOptions, AnnotateOutput, ChangesetAnnotation,
 };
@@ -17,6 +18,5 @@
     FilesForRev,
 };
 pub use status_rev_rev::{
-    status_change, status_rev_rev_no_copies, DiffStatus, ListCopies,
-    StatusRevRev,
+    status_change, status_rev_rev, DiffStatus, ListCopies, StatusRevRev,
 };
diff --git a/rust/hg-core/src/operations/status_rev_rev.rs b/rust/hg-core/src/operations/status_rev_rev.rs
--- a/rust/hg-core/src/operations/status_rev_rev.rs
+++ b/rust/hg-core/src/operations/status_rev_rev.rs
@@ -1,5 +1,6 @@
 use std::borrow::Cow;
 
+use crate::copy_tracing::copytracing_v1::Copies;
 use crate::dirstate::status::StatusPath;
 use crate::errors::HgError;
 use crate::matchers::Matcher;
@@ -35,22 +36,22 @@
 pub(crate) enum CopyStrategy<'a> {
     /// Use the [`Repo`] to look up copy information in filelog metadata.
     /// Assumes we are producing the status for a single changeset.
-    Change(&'a Repo),
-    // TODO: For --rev --rev --copies use a precomputed copy map
+    Change(ListCopies, &'a Repo),
+    Precomputed(Copies),
 }
 
 pub struct StatusRevRev<'a> {
     pub(crate) manifest1: Manifest,
     pub(crate) manifest2: Manifest,
     pub(crate) narrow_matcher: Box<dyn Matcher>,
-    pub(crate) copies: Option<(ListCopies, CopyStrategy<'a>)>,
+    pub(crate) copies: Option<CopyStrategy<'a>>,
 }
 
 pub struct StatusRevRevRef<'a> {
     pub(crate) manifest1: &'a Manifest,
     pub(crate) manifest2: &'a Manifest,
     pub(crate) narrow_matcher: &'a dyn Matcher,
-    pub(crate) copies: Option<(ListCopies, CopyStrategy<'a>)>,
+    pub(crate) copies: Option<CopyStrategy<'a>>,
 }
 
 fn manifest_for_rev(repo: &Repo, rev: Revision) -> Result<Manifest, HgError> {
@@ -62,17 +63,18 @@
     })
 }
 
-pub fn status_rev_rev_no_copies(
+pub fn status_rev_rev(
     repo: &Repo,
     rev1: Revision,
     rev2: Revision,
     narrow_matcher: Box<dyn Matcher>,
+    copies: Option<Copies>,
 ) -> Result<StatusRevRev, HgError> {
     Ok(StatusRevRev {
         manifest1: manifest_for_rev(repo, rev1)?,
         manifest2: manifest_for_rev(repo, rev2)?,
         narrow_matcher,
-        copies: None,
+        copies: copies.map(CopyStrategy::Precomputed),
     })
 }
 
@@ -89,7 +91,7 @@
         manifest1: manifest_for_rev(repo, parent)?,
         manifest2: manifest_for_rev(repo, rev)?,
         narrow_matcher,
-        copies: list_copies.map(|list| (list, CopyStrategy::Change(repo))),
+        copies: list_copies.map(|list| (CopyStrategy::Change(list, repo))),
     })
 }
 
@@ -160,26 +162,26 @@
 
     /// Returns the path that a file was copied from, if it should be reported.
     fn find_copy_source_impl(
-        copies: &Option<(ListCopies, CopyStrategy<'a>)>,
+        copies: &Option<CopyStrategy<'a>>,
         manifest1: &Manifest,
         path: &HgPath,
         status: DiffStatus,
         entry: Option<&ManifestEntry>,
     ) -> Result<Option<HgPathBuf>, HgError> {
         let Some(entry) = entry else { return Ok(None) };
-        let Some((list, strategy)) = copies else {
+        let Some(strategy) = copies else {
             return Ok(None);
         };
-        match (list, status) {
-            (ListCopies::Added, DiffStatus::Added) => {}
-            (
-                ListCopies::AddedOrModified,
-                DiffStatus::Added | DiffStatus::Modified,
-            ) => {}
-            _ => return Ok(None),
-        }
         match strategy {
-            CopyStrategy::Change(repo) => {
+            CopyStrategy::Change(list, repo) => {
+                match (list, status) {
+                    (ListCopies::Added, DiffStatus::Added) => {}
+                    (
+                        ListCopies::AddedOrModified,
+                        DiffStatus::Added | DiffStatus::Modified,
+                    ) => {}
+                    _ => return Ok(None),
+                }
                 let data = repo
                     .filelog(path)?
                     .data_for_node(entry.node_id().unwrap())?;
@@ -189,7 +191,11 @@
                     }
                 }
             }
+            CopyStrategy::Precomputed(copies) => {
+                return Ok(copies.get(path).map(|p| p.to_owned()))
+            }
         }
+
         Ok(None)
     }
 }
diff --git a/rust/hg-core/src/operations/status_rev_rev_nocopy.rs b/rust/hg-core/src/operations/status_rev_rev_nocopy.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/operations/status_rev_rev_nocopy.rs
@@ -0,0 +1,64 @@
+/// Even though this module lives in operations, it's more of an
+/// internal thing: it takes manifests and returns filenodes, it
+/// doesn't support copies, etc.
+use std::borrow::Cow;
+use std::num::NonZero;
+
+use crate::dirstate::status::HgPathCow;
+use crate::errors::HgError;
+use crate::matchers::Matcher;
+use crate::revlog::manifest::Manifest;
+use crate::utils::filter_map_results;
+use crate::utils::merge_join_results_by;
+
+use crate::Node;
+
+use itertools::EitherOrBoth;
+
+type TypAndNode = (Option<NonZero<u8>>, Node);
+
+pub type DiffStatusDetailed = (Option<TypAndNode>, Option<TypAndNode>);
+
+pub struct StatusRevRevNocopies<'a> {
+    pub(crate) manifest1: &'a Manifest,
+    pub(crate) manifest2: &'a Manifest,
+    pub(crate) narrow_matcher: &'a dyn Matcher,
+}
+
+type R<T> = Result<T, HgError>;
+
+impl<'a> StatusRevRevNocopies<'a> {
+    pub fn iter_detailed_nocopies(
+        self,
+    ) -> impl Iterator<Item = R<(HgPathCow<'a>, DiffStatusDetailed)>> {
+        let iter1 = self.manifest1.iter();
+        let iter2 = self.manifest2.iter();
+
+        let merged =
+            merge_join_results_by(iter1, iter2, |i1, i2| i1.path.cmp(i2.path));
+        let narrow_matcher = self.narrow_matcher;
+
+        filter_map_results(merged, move |entry| {
+            let (path, status) = match &entry {
+                EitherOrBoth::Left(entry) => {
+                    (entry.path, (Some((entry.flags, entry.node_id()?)), None))
+                }
+                EitherOrBoth::Right(entry) => {
+                    (entry.path, (None, Some((entry.flags, entry.node_id()?))))
+                }
+                EitherOrBoth::Both(entry1, entry2) => (
+                    entry1.path,
+                    (
+                        Some((entry1.flags, entry1.node_id()?)),
+                        Some((entry2.flags, entry2.node_id()?)),
+                    ),
+                ),
+            };
+            if !narrow_matcher.matches(path) {
+                return Ok(None);
+            }
+            let path = Cow::Borrowed(path);
+            Ok(Some((path, status)))
+        })
+    }
+}
diff --git a/rust/hg-core/src/revlog/filelog.rs b/rust/hg-core/src/revlog/filelog.rs
--- a/rust/hg-core/src/revlog/filelog.rs
+++ b/rust/hg-core/src/revlog/filelog.rs
@@ -259,7 +259,7 @@
 }
 
 /// The optional metadata header included in [`FilelogRevisionData`].
-pub struct FilelogRevisionMetadata<'a>(Option<&'a [u8]>);
+pub struct FilelogRevisionMetadata<'a>(pub Option<&'a [u8]>);
 
 /// Fields parsed from [`FilelogRevisionMetadata`].
 #[derive(Debug, PartialEq, Default)]
diff --git a/rust/hg-core/src/revlog/mod.rs b/rust/hg-core/src/revlog/mod.rs
--- a/rust/hg-core/src/revlog/mod.rs
+++ b/rust/hg-core/src/revlog/mod.rs
@@ -290,7 +290,7 @@
         Self::open_gen(store_vfs, index_path, data_path, options, None)
     }
 
-    fn index(&self) -> &Index {
+    pub(crate) fn index(&self) -> &Index {
         &self.inner.index
     }
 
diff --git a/rust/hg-core/src/revlog/node.rs b/rust/hg-core/src/revlog/node.rs
--- a/rust/hg-core/src/revlog/node.rs
+++ b/rust/hg-core/src/revlog/node.rs
@@ -56,10 +56,10 @@
 /// the size or return an error at runtime.
 ///
 /// [`nybbles_len`]: #method.nybbles_len
-#[derive(Copy, Clone, PartialEq, BytesCast, derive_more::From)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash, BytesCast, derive_more::From)]
 #[repr(transparent)]
 pub struct Node {
-    data: NodeData,
+    pub(crate) data: NodeData,
 }
 
 impl fmt::Debug for Node {
diff --git a/rust/rhg/src/commands/status.rs b/rust/rhg/src/commands/status.rs
--- a/rust/rhg/src/commands/status.rs
+++ b/rust/rhg/src/commands/status.rs
@@ -6,6 +6,7 @@
 // GNU General Public License version 2 or any later version.
 
 use crate::error::CommandError;
+use crate::renames::copytracing;
 use crate::ui::{
     format_pattern_file_warning, print_narrow_sparse_warnings, relative_paths,
     RelativePaths, Ui,
@@ -516,14 +517,16 @@
             None
         };
         let stat = if let Some((rev1, rev2)) = revpair {
-            if list_copies.is_some() {
-                return Err(CommandError::unsupported(
-                    "status --rev --rev with copy information is not implemented yet",
-                ));
-            }
-            hg::operations::status_rev_rev_no_copies(
-                repo, rev1, rev2, matcher,
-            )?
+            let copies = if list_copies.is_some() {
+                let node1 =
+                    repo.node(rev1.into()).expect("rev already resolved");
+                let node2 =
+                    repo.node(rev2.into()).expect("rev already resolved");
+                Some(copytracing::pathcopies(repo, &*matcher, node1, node2)?)
+            } else {
+                None
+            };
+            hg::operations::status_rev_rev(repo, rev1, rev2, matcher, copies)?
         } else if let Some(rev) = change {
             hg::operations::status_change(repo, rev, matcher, list_copies)?
         } else {
diff --git a/rust/rhg/src/main.rs b/rust/rhg/src/main.rs
--- a/rust/rhg/src/main.rs
+++ b/rust/rhg/src/main.rs
@@ -18,6 +18,9 @@
 mod blackbox;
 mod color;
 mod error;
+mod renames {
+    pub(crate) mod copytracing;
+}
 mod ui;
 pub mod utils {
     pub mod path_utils;
diff --git a/rust/rhg/src/renames/copytracing.rs b/rust/rhg/src/renames/copytracing.rs
new file mode 100644
--- /dev/null
+++ b/rust/rhg/src/renames/copytracing.rs
@@ -0,0 +1,53 @@
+use hg::{
+    copy_tracing::{
+        copytracing_v1::{pathcopies_gen, Copies},
+        filenode_graph_access::{
+            vanilla::{VanillaFilenodeGraphAccess, VanillaManifestAccess},
+            FilelogOpener,
+        },
+    },
+    errors::HgError,
+    matchers::Matcher,
+    repo::Repo,
+    revlog::options::default_revlog_options,
+    Node,
+};
+
+pub fn pathcopies(
+    repo: &Repo,
+    matcher: &(impl Matcher + ?Sized),
+    x: Node,
+    y: Node,
+) -> Result<Copies, HgError> {
+    let xm = repo.manifest_for_node(x)?;
+    let ym = repo.manifest_for_node(y)?;
+    let x = (x, &xm);
+    let y = (y, &ym);
+    let parallel = repo.config().get_bool(b"rust", b"copytracing.parallel")?;
+    let store_vfs = repo.store_vfs();
+    let filelog_opener = {
+        FilelogOpener {
+            store_vfs: &store_vfs,
+            open_options: default_revlog_options(
+                repo.config(),
+                repo.requirements(),
+                hg::revlog::RevlogType::Filelog,
+            )?,
+        }
+    };
+
+    let changelog = repo.changelog()?;
+    let manifest_access = VanillaManifestAccess::new(repo)?;
+    let manifest_access = manifest_access.borrow();
+
+    let graph_access = &VanillaFilenodeGraphAccess { filelog_opener };
+    pathcopies_gen(
+        &changelog,
+        &manifest_access,
+        graph_access,
+        parallel,
+        matcher,
+        x,
+        y,
+    )
+}
# HG changeset patch
# User Arseniy Alekseyev <aalekseyev@janestreet.com>
# Date 1741122151 18000
#      Tue Mar 04 16:02:31 2025 -0500
# Node ID 2c154c55ac045b1e73a16e750aafa1953cd106c8
# Parent  2437d682a6c38fac4a77bef8e6a44b2158cc7eeb
# EXP-Topic copytracing-in-rust
tests: make sure copytracing in rust stays working

diff --git a/tests/test-status.t b/tests/test-status.t
--- a/tests/test-status.t
+++ b/tests/test-status.t
@@ -497,7 +497,7 @@
 
   $ hg status --rev 0 --rev 1 unrelated
 
-  $ hg status -C --rev 0 --rev 1 added modified copied removed deleted
+  $ hg status -C --rev 0 --rev 1 added modified copied removed deleted --config rhg.on-unsupported=abort
   M modified
   A added
   A copied