changeset 52398:bde718849153

rhg: support status --change, including --copies It works by parsing copy information from filelog metadata headers. The --rev --rev --copies case still falls back to Python since that will require constructing a map like pathcopies does in copies.py. As in Python, rhg by default only reports copies for newly added files. With devel.copy-tracing.trace-all-files=True, it also does it for modified files.
author Mitchell Kember <mkember@janestreet.com>
date Tue, 03 Dec 2024 09:44:57 -0500
parents 42bd36bbed67
children 5ff6fba7c4c5
files rust/hg-core/src/operations/mod.rs rust/hg-core/src/operations/status_rev_rev.rs rust/rhg/src/commands/status.rs
diffstat 3 files changed, 133 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- a/rust/hg-core/src/operations/mod.rs	Tue Dec 03 09:37:34 2024 -0500
+++ b/rust/hg-core/src/operations/mod.rs	Tue Dec 03 09:44:57 2024 -0500
@@ -12,4 +12,7 @@
     list_rev_tracked_files, list_revset_tracked_files, ExpandedManifestEntry,
     FilesForRev,
 };
-pub use status_rev_rev::{status_rev_rev_no_copies, DiffStatus, StatusRevRev};
+pub use status_rev_rev::{
+    status_change, status_rev_rev_no_copies, DiffStatus, ListCopies,
+    StatusRevRev,
+};
--- a/rust/hg-core/src/operations/status_rev_rev.rs	Tue Dec 03 09:37:34 2024 -0500
+++ b/rust/hg-core/src/operations/status_rev_rev.rs	Tue Dec 03 09:44:57 2024 -0500
@@ -1,12 +1,15 @@
+use std::borrow::Cow;
+
+use crate::dirstate::status::StatusPath;
 use crate::errors::HgError;
 use crate::matchers::Matcher;
 use crate::repo::Repo;
-use crate::revlog::manifest::Manifest;
+use crate::revlog::manifest::{Manifest, ManifestEntry};
 use crate::utils::filter_map_results;
-use crate::utils::hg_path::HgPath;
+use crate::utils::hg_path::{HgPath, HgPathBuf};
 use crate::utils::merge_join_results_by;
 
-use crate::Revision;
+use crate::{Revision, NULL_REVISION};
 
 use itertools::EitherOrBoth;
 
@@ -18,10 +21,27 @@
     Modified,
 }
 
-pub struct StatusRevRev {
+/// What copy/rename information to report.
+pub enum ListCopies {
+    /// Report copies only for added files.
+    Added,
+    /// Report copies for files that are added or modified.
+    AddedOrModified,
+}
+
+/// Strategy for determining a file's copy source.
+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
+}
+
+pub struct StatusRevRev<'a> {
     manifest1: Manifest,
     manifest2: Manifest,
     narrow_matcher: Box<dyn Matcher>,
+    copies: Option<(ListCopies, CopyStrategy<'a>)>,
 }
 
 fn manifest_for_rev(repo: &Repo, rev: Revision) -> Result<Manifest, HgError> {
@@ -39,19 +59,36 @@
     rev2: Revision,
     narrow_matcher: Box<dyn Matcher>,
 ) -> Result<StatusRevRev, HgError> {
-    let manifest1 = manifest_for_rev(repo, rev1)?;
-    let manifest2 = manifest_for_rev(repo, rev2)?;
     Ok(StatusRevRev {
-        manifest1,
-        manifest2,
+        manifest1: manifest_for_rev(repo, rev1)?,
+        manifest2: manifest_for_rev(repo, rev2)?,
         narrow_matcher,
+        copies: None,
     })
 }
 
-impl StatusRevRev {
+/// Computes the status of `rev` against its first parent.
+pub fn status_change(
+    repo: &Repo,
+    rev: Revision,
+    narrow_matcher: Box<dyn Matcher>,
+    list_copies: Option<ListCopies>,
+) -> Result<StatusRevRev, HgError> {
+    let parent = repo.changelog()?.revlog.get_entry(rev)?.p1();
+    let parent = parent.unwrap_or(NULL_REVISION);
+    Ok(StatusRevRev {
+        manifest1: manifest_for_rev(repo, parent)?,
+        manifest2: manifest_for_rev(repo, rev)?,
+        narrow_matcher,
+        copies: list_copies.map(|list| (list, CopyStrategy::Change(repo))),
+    })
+}
+
+impl StatusRevRev<'_> {
     pub fn iter(
         &self,
-    ) -> impl Iterator<Item = Result<(&HgPath, DiffStatus), HgError>> {
+    ) -> impl Iterator<Item = Result<(StatusPath<'_>, DiffStatus), HgError>>
+    {
         let iter1 = self.manifest1.iter();
         let iter2 = self.manifest2.iter();
 
@@ -59,15 +96,9 @@
             merge_join_results_by(iter1, iter2, |i1, i2| i1.path.cmp(i2.path));
 
         filter_map_results(merged, |entry| {
-            let (path, status) = match entry {
-                EitherOrBoth::Left(entry) => {
-                    let path = entry.path;
-                    (path, DiffStatus::Removed)
-                }
-                EitherOrBoth::Right(entry) => {
-                    let path = entry.path;
-                    (path, DiffStatus::Added)
-                }
+            let (path, status) = match &entry {
+                EitherOrBoth::Left(entry) => (entry.path, DiffStatus::Removed),
+                EitherOrBoth::Right(entry) => (entry.path, DiffStatus::Added),
                 EitherOrBoth::Both(entry1, entry2) => {
                     let path = entry1.path;
                     if entry1.node_id().unwrap() == entry2.node_id().unwrap()
@@ -79,11 +110,50 @@
                     }
                 }
             };
-            Ok(if self.narrow_matcher.matches(path) {
-                Some((path, status))
-            } else {
-                None
-            })
+            if !self.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),
+            };
+            Ok(Some((path, status)))
         })
     }
+
+    /// Returns the path that a file was copied from, if it should be reported.
+    fn find_copy_source(
+        &self,
+        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 {
+            return Ok(None);
+        };
+        match (list, status) {
+            (ListCopies::Added, DiffStatus::Added) => {}
+            (
+                ListCopies::AddedOrModified,
+                DiffStatus::Added | DiffStatus::Modified,
+            ) => {}
+            _ => return Ok(None),
+        }
+        match strategy {
+            CopyStrategy::Change(repo) => {
+                let data = repo
+                    .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() {
+                        return Ok(Some(copy.to_owned()));
+                    }
+                }
+            }
+        }
+        Ok(None)
+    }
 }
--- a/rust/rhg/src/commands/status.rs	Tue Dec 03 09:37:34 2024 -0500
+++ b/rust/rhg/src/commands/status.rs	Tue Dec 03 09:44:57 2024 -0500
@@ -35,7 +35,6 @@
 use hg::{self, narrow, sparse};
 use log::info;
 use rayon::prelude::*;
-use std::borrow::Cow;
 use std::io;
 use std::mem::take;
 use std::path::PathBuf;
@@ -156,6 +155,13 @@
                 .action(clap::ArgAction::Append)
                 .value_name("REV"),
         )
+        .arg(
+            Arg::new("change")
+                .help("list the changed files of a revision")
+                .long("change")
+                .value_name("REV")
+                .conflicts_with("rev"),
+        )
 }
 
 fn parse_revpair(
@@ -263,6 +269,7 @@
     let args = invocation.subcommand_args;
 
     let revs = args.get_many::<String>("rev");
+    let change = args.get_one::<String>("change");
     let print0 = args.get_flag("print0");
     let verbose = args.get_flag("verbose")
         || config.get_bool(b"ui", b"verbose")?
@@ -302,6 +309,9 @@
 
     let repo = invocation.repo?;
     let revpair = parse_revpair(repo, revs.map(|i| i.cloned().collect()))?;
+    let change = change
+        .map(|rev| hg::revset::resolve_single(rev, repo))
+        .transpose()?;
 
     if verbose && has_unfinished_state(repo)? {
         return Err(CommandError::unsupported(
@@ -490,23 +500,33 @@
         repo,
     )?;
 
-    if let Some((rev1, rev2)) = revpair {
+    if revpair.is_some() || change.is_some() {
         let mut ds_status = DirstateStatus::default();
-        if list_copies {
-            return Err(CommandError::unsupported(
-                "status --rev --rev with copy information is not implemented yet",
-            ));
-        }
-
-        let stat = hg::operations::status_rev_rev_no_copies(
-            repo, rev1, rev2, matcher,
-        )?;
+        let list_copies = if list_copies {
+            if config.get_bool(b"devel", b"copy-tracing.trace-all-files")? {
+                Some(hg::operations::ListCopies::AddedOrModified)
+            } else {
+                Some(hg::operations::ListCopies::Added)
+            }
+        } else {
+            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,
+            )?
+        } else if let Some(rev) = change {
+            hg::operations::status_change(repo, rev, matcher, list_copies)?
+        } else {
+            unreachable!();
+        };
         for entry in stat.iter() {
             let (path, status) = entry?;
-            let path = StatusPath {
-                path: Cow::Borrowed(path),
-                copy_source: None,
-            };
             match status {
                 hg::operations::DiffStatus::Removed => {
                     if display_states.removed {