diff --git a/mercurial/configitems.toml b/mercurial/configitems.toml index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_bWVyY3VyaWFsL2NvbmZpZ2l0ZW1zLnRvbWw=..2c154c55ac045b1e73a16e750aafa1953cd106c8_bWVyY3VyaWFsL2NvbmZpZ2l0ZW1zLnRvbWw= 100644 --- 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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9oZy1jb3JlL3NyYy9jb3B5X3RyYWNpbmcucnM=..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9jb3B5X3RyYWNpbmcucnM= 100644 --- 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 index 0000000000000000000000000000000000000000..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9jb3B5X3RyYWNpbmcvY29weXRyYWNpbmdfdjEucnM= --- /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 index 0000000000000000000000000000000000000000..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9jb3B5X3RyYWNpbmcvZmlsZW5vZGVfZ3JhcGhfYWNjZXNzLnJz --- /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 index 0000000000000000000000000000000000000000..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9jb3B5X3RyYWNpbmcvdHlwZXMvbW9kLnJz --- /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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9oZy1jb3JlL3NyYy9vcGVyYXRpb25zL21vZC5ycw==..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9vcGVyYXRpb25zL21vZC5ycw== 100644 --- 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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9oZy1jb3JlL3NyYy9vcGVyYXRpb25zL3N0YXR1c19yZXZfcmV2LnJz..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9vcGVyYXRpb25zL3N0YXR1c19yZXZfcmV2LnJz 100644 --- 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; @@ -22,6 +23,7 @@ } /// What copy/rename information to report. +#[derive(Copy, Clone)] pub enum ListCopies { /// Report copies only for added files. Added, @@ -30,6 +32,7 @@ } /// 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. @@ -34,7 +37,7 @@ /// 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> { @@ -38,10 +41,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<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<CopyStrategy<'a>>, } fn manifest_for_rev(repo: &Repo, rev: Revision) -> Result<Manifest, HgError> { @@ -53,8 +63,8 @@ }) } -pub fn status_rev_rev_no_copies( +pub fn status_rev_rev( repo: &Repo, rev1: Revision, rev2: Revision, narrow_matcher: Box<dyn Matcher>, @@ -57,9 +67,10 @@ 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, @@ -61,9 +72,9 @@ ) -> 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), }) } @@ -80,8 +91,8 @@ 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))), }) } impl StatusRevRev<'_> { @@ -84,8 +95,17 @@ }) } 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>> { @@ -88,6 +108,15 @@ 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(); @@ -92,5 +121,6 @@ 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)); @@ -94,4 +124,6 @@ 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; @@ -97,5 +129,5 @@ - 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,8 +142,8 @@ } } }; - if !self.narrow_matcher.matches(path) { + if !narrow_matcher.matches(path) { return Ok(None); } let path = StatusPath { path: Cow::Borrowed(path), @@ -114,13 +146,18 @@ 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. @@ -121,13 +158,14 @@ }; 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<CopyStrategy<'a>>, + manifest1: &Manifest, path: &HgPath, status: DiffStatus, entry: Option<&ManifestEntry>, ) -> Result<Option<HgPathBuf>, HgError> { let Some(entry) = entry else { return Ok(None) }; @@ -129,8 +167,8 @@ 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(strategy) = copies else { return Ok(None); }; @@ -135,11 +173,3 @@ return Ok(None); }; - match (list, status) { - (ListCopies::Added, DiffStatus::Added) => {} - ( - ListCopies::AddedOrModified, - DiffStatus::Added | DiffStatus::Modified, - ) => {} - _ => return Ok(None), - } match strategy { @@ -145,6 +175,14 @@ 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())?; if let Some(copy) = data.metadata()?.parse()?.copy { @@ -147,9 +185,9 @@ 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() { + if manifest1.find_by_path(copy)?.is_some() { return Ok(Some(copy.to_owned())); } } } @@ -152,5 +190,8 @@ return Ok(Some(copy.to_owned())); } } } + CopyStrategy::Precomputed(copies) => { + return Ok(copies.get(path).map(|p| p.to_owned())) + } } @@ -156,4 +197,5 @@ } + 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 index 0000000000000000000000000000000000000000..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9vcGVyYXRpb25zL3N0YXR1c19yZXZfcmV2X25vY29weS5ycw== --- /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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9oZy1jb3JlL3NyYy9yZXZsb2cvZmlsZWxvZy5ycw==..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9yZXZsb2cvZmlsZWxvZy5ycw== 100644 --- 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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9oZy1jb3JlL3NyYy9yZXZsb2cvbW9kLnJz..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9yZXZsb2cvbW9kLnJz 100644 --- 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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9oZy1jb3JlL3NyYy9yZXZsb2cvbm9kZS5ycw==..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9oZy1jb3JlL3NyYy9yZXZsb2cvbm9kZS5ycw== 100644 --- a/rust/hg-core/src/revlog/node.rs +++ b/rust/hg-core/src/revlog/node.rs @@ -56,6 +56,6 @@ /// 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 { @@ -60,6 +60,6 @@ #[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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9yaGcvc3JjL2NvbW1hbmRzL3N0YXR1cy5ycw==..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9yaGcvc3JjL2NvbW1hbmRzL3N0YXR1cy5ycw== 100644 --- 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 index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_cnVzdC9yaGcvc3JjL21haW4ucnM=..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9yaGcvc3JjL21haW4ucnM= 100644 --- 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 index 0000000000000000000000000000000000000000..2c154c55ac045b1e73a16e750aafa1953cd106c8_cnVzdC9yaGcvc3JjL3JlbmFtZXMvY29weXRyYWNpbmcucnM= --- /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, + ) +} diff --git a/tests/test-status.t b/tests/test-status.t index 0471bf04ddf1231456a848d0965b3d05bf00bf8f_dGVzdHMvdGVzdC1zdGF0dXMudA==..2c154c55ac045b1e73a16e750aafa1953cd106c8_dGVzdHMvdGVzdC1zdGF0dXMudA== 100644 --- 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