Skip to content
Snippets Groups Projects
Commit 721ac5a4 authored by Georges Racinet's avatar Georges Racinet
Browse files

rhgitaly::mercurial::ls_path: listing a subset of manifest

This will be useful to implement `CommitService.TreeEntry`.
There is plenty of room for performance improvement, notably:

- the position of the first relevant entry could be found
  by binary search instead of a linear scan
- we could try and clone less

But it is possible that this would not be really needed, given
the performance and scalability boost given by RHGitaly over
HGitaly, before Mercurial manifests start using a more efficient
layout, making this code obsolete.
parent 87b6970c
No related branches found
No related tags found
3 merge requests!186Merging stable branch into default,!184Merged/adapted oldstable into stable for RHGitaly methods,!179RHGitaly CommitService.TreeEntry implementation
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
use super::git; use super::git;
use super::oid::blob_oid; use super::oid::blob_oid;
use tracing::debug;
#[derive(Debug, Default)] #[derive(Debug, Default)]
/// Common representation for metadata of content presented as if they were Git inner objects. /// Common representation for metadata of content presented as if they were Git inner objects.
...@@ -119,3 +120,269 @@ ...@@ -119,3 +120,269 @@
)) ))
} }
} }
/// Relevant metadata a directory item , which can be a file or a sub-directory
///
/// The `manifest_entry` field is `None` if and only if the item is a sub-directory.
/// Otherwise the [`ManifestEntry`] provides metadata of the file.
#[derive(Debug)]
pub struct DirectoryEntry<'m> {
pub relative_path: Vec<u8>,
pub mode: i32,
pub manifest_entry: Option<ManifestEntry<'m>>,
}
impl DirectoryEntry<'_> {
pub fn is_file(&self) -> bool {
self.manifest_entry.is_some()
}
}
impl PartialEq for DirectoryEntry<'_> {
fn eq(&self, other: &Self) -> bool {
if self.relative_path != other.relative_path || self.mode != other.mode {
false
} else {
match (self.manifest_entry.as_ref(), other.manifest_entry.as_ref()) {
(Some(e), Some(f)) => manifest_entry_eq(e, f),
(None, None) => true,
_ => false,
}
}
}
}
fn manifest_entry_eq<'m>(e: &ManifestEntry<'m>, f: &ManifestEntry<'m>) -> bool {
// TODO upstream implement PartialEq for ManifestEntry, pretty much as this
e.path == f.path && e.hex_node_id == f.hex_node_id
}
impl Eq for DirectoryEntry<'_> {}
/// Represent the content at some path, either a file, with a [`ManifestEntry`] for metadata,
/// or a directory with its entries.
#[derive(Debug)]
pub enum PathContent<'m> {
File(ManifestEntry<'m>),
Directory(Vec<DirectoryEntry<'m>>),
NotFound,
}
impl PartialEq for PathContent<'_> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(PathContent::NotFound, PathContent::NotFound) => true,
(PathContent::File(e), PathContent::File(f)) => manifest_entry_eq(e, f),
(PathContent::Directory(v), PathContent::Directory(w)) => v == w,
_ => false,
}
}
}
impl Eq for PathContent<'_> {}
/// Return the list of entries that are sitting right at path.
///
/// Similarly to `/bin/ls`, if `path` is a directory, its contents are listed, otherwise
/// file metadata are returned.
pub fn ls_path<'m>(manifest: &'m Manifest, path: &[u8]) -> Result<PathContent<'m>, RevlogError> {
inner_ls_path(manifest.iter(), path)
}
fn inner_ls_path<'m, IM>(manifest: IM, path: &[u8]) -> Result<PathContent<'m>, RevlogError>
where
IM: IntoIterator<Item = Result<ManifestEntry<'m>, HgError>>,
{
let mut listing = Vec::new();
let pl = path.len();
for entry_res in manifest {
let entry = entry_res?;
let ep = entry.path.as_bytes();
if !ep.starts_with(path) {
if !listing.is_empty() {
break;
} else {
continue;
}
}
debug!("Considering manifest path {:?}", ep);
// exact match is always first, because manifest is lexicographically ordered
if ep.len() == pl {
// so ep == path
debug!("Exact match for {:?} and {:?}", ep, pl);
return Ok(PathContent::File(entry));
}
debug!("Checking if we are in a subdir with {:?}", ep);
if ep[pl] != b'/' {
if !listing.is_empty() {
break;
} else {
continue;
}
}
let rel_path = &ep[pl + 1..];
debug!("Analyzing subdir for {:?} (rel_path={:?})", ep, rel_path);
// first iteration on split can be `None` only if rel_path is empty,
// which would mean in this context that entry.path has a trailing slash
// (guaranteed not to happen in the Manifest data structure)
if let Some(sub_rel_path) = rel_path.split(|c| *c == b'/').next() {
if sub_rel_path.len() == rel_path.len() {
listing.push(DirectoryEntry {
relative_path: rel_path.to_vec(),
mode: git_perms(&entry)?,
manifest_entry: Some(entry),
});
} else {
if let Some(previous) = listing.last() {
if previous.relative_path == sub_rel_path {
continue;
}
}
listing.push(DirectoryEntry {
relative_path: sub_rel_path.to_vec(),
mode: git::OBJECT_MODE_TREE,
manifest_entry: None,
});
}
}
}
Ok(if listing.is_empty() {
PathContent::NotFound
} else {
PathContent::Directory(listing)
})
}
#[cfg(test)]
mod tests {
use super::*;
/// Return a [`ManifestEntry`] suitable for tests that care only about paths
fn path_manifest_entry(path: &'static str) -> ManifestEntry {
ManifestEntry {
path: HgPath::new(path.as_bytes()),
hex_node_id: b"",
flags: None,
}
}
/// Return something suitable to test the algorithm behind [`ls_path`] from only paths.
///
/// For [`ls_path`] correctness, the node ids and flags don't matter much, hence we give
/// them fixed, arbitrary values.
/// For convenience paths are string slices, static not to bother with lifetimes.
fn paths_manifest(paths: Vec<&'static str>) -> Vec<Result<ManifestEntry, HgError>> {
paths
.into_iter()
.map(|path| Ok(path_manifest_entry(path)))
.collect()
}
#[test]
fn test_ls_path_simple() {
let manifest = paths_manifest(vec!["bar", "foo/some", "foo0"]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"foo").unwrap(),
PathContent::Directory(vec![DirectoryEntry {
relative_path: b"some".to_vec(),
mode: git::OBJECT_MODE_NON_EXECUTABLE,
manifest_entry: Some(path_manifest_entry("foo/some")),
}])
);
// other climbing case
let manifest = paths_manifest(vec!["foo/some", "other"]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"foo").unwrap(),
PathContent::Directory(vec![DirectoryEntry {
relative_path: b"some".to_vec(),
mode: git::OBJECT_MODE_NON_EXECUTABLE,
manifest_entry: Some(path_manifest_entry("foo/some")),
}])
);
// other diving case
let manifest = paths_manifest(vec!["foo.", "foo/some"]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"foo").unwrap(),
PathContent::Directory(vec![DirectoryEntry {
relative_path: b"some".to_vec(),
mode: git::OBJECT_MODE_NON_EXECUTABLE,
manifest_entry: Some(path_manifest_entry("foo/some")),
}])
);
let manifest = paths_manifest(vec!["bar", "foo/some", "foo0"]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"bar").unwrap(),
PathContent::File(path_manifest_entry("bar")),
);
let manifest = paths_manifest(vec!["bar", "foo/some", "foo0"]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"foo0").unwrap(),
PathContent::File(path_manifest_entry("foo0")),
);
let manifest = paths_manifest(vec!["bar", "foo"]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"alien").unwrap(),
PathContent::NotFound,
);
}
#[test]
fn test_ls_path_dir_several_files() {
let manifest = paths_manifest(vec!["foo.", "foo/a", "foo/b", "foo0"]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"foo").unwrap(),
PathContent::Directory(vec![
DirectoryEntry {
relative_path: b"a".to_vec(),
mode: git::OBJECT_MODE_NON_EXECUTABLE,
manifest_entry: Some(path_manifest_entry("foo/a")),
},
DirectoryEntry {
relative_path: b"b".to_vec(),
mode: git::OBJECT_MODE_NON_EXECUTABLE,
manifest_entry: Some(path_manifest_entry("foo/b")),
}
])
)
}
#[test]
fn test_ls_path_dir_with_sub() {
let manifest = paths_manifest(vec![
"foo.",
"foo/a",
"foo/sub/a",
"foo/sub/b",
"foo/subb",
"foo0",
]);
assert_eq!(
inner_ls_path(manifest.into_iter(), b"foo").unwrap(),
PathContent::Directory(vec![
DirectoryEntry {
relative_path: b"a".to_vec(),
mode: git::OBJECT_MODE_NON_EXECUTABLE,
manifest_entry: Some(path_manifest_entry("foo/a")),
},
DirectoryEntry {
relative_path: b"sub".to_vec(),
mode: git::OBJECT_MODE_TREE,
manifest_entry: None,
},
DirectoryEntry {
relative_path: b"subb".to_vec(),
mode: git::OBJECT_MODE_NON_EXECUTABLE,
manifest_entry: Some(path_manifest_entry("foo/subb")),
},
])
)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment