# HG changeset patch
# User Georges Racinet <georges.racinet@octobus.net>
# Date 1693435147 -7200
#      Thu Aug 31 00:39:07 2023 +0200
# Branch stable
# Node ID 934afef8417338bfe88527f098fd677dbc47849e
# Parent  74c6bbd8b086214d3ba1e8bf14f0f4827c479310
rhgitaly::mercurial::RecursiveDirIterator

This is the core engine for one of the three modes of
`CommitService.GetTreeEntries`.

diff --git a/rust/rhgitaly/src/mercurial.rs b/rust/rhgitaly/src/mercurial.rs
--- a/rust/rhgitaly/src/mercurial.rs
+++ b/rust/rhgitaly/src/mercurial.rs
@@ -5,6 +5,8 @@
 //
 // SPDX-License-Identifier: GPL-2.0-or-later
 //! Utilities for Mercurial content handling
+use std::collections::VecDeque;
+
 use hg::changelog::Changelog;
 use hg::errors::HgError;
 use hg::manifest::{Manifest, ManifestEntry, Manifestlog};
@@ -13,6 +15,7 @@
 use hg::utils::hg_path::HgPath;
 
 use crate::gitaly::{tree_entry::EntryType, ObjectType, TreeEntry};
+use crate::util::common_subpath_split;
 
 use super::git;
 use super::oid::{blob_oid, tree_oid};
@@ -489,6 +492,119 @@
     }
 }
 
+pub struct RecursiveDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    /// Is expected to be the repetition of an incoming request "oid" field,
+    /// often something like `branch/default` and not a real OID
+    commit_oid: String,
+    changeset_node: Node,
+    root_oid: String,
+    manifest_dir_iter: ManifestDirIterator<'a, 'm, IM>,
+    current_subdir: &'m [u8],
+    to_yield: VecDeque<Result<TreeEntry, HgError>>,
+}
+
+impl<'a, 'm, IM> RecursiveDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    pub fn new(commit_oid: String, changeset_node: Node, manifest: IM, path: &'a [u8]) -> Self {
+        let root_oid = tree_oid(&changeset_node, b"");
+        Self {
+            commit_oid,
+            changeset_node,
+            root_oid,
+            manifest_dir_iter: ManifestDirIterator::new(manifest, path),
+            current_subdir: "".as_ref(),
+            /// Queue of `TreeEntry` messages that have to be emitted.
+            ///
+            /// Each manifest entry can give rise to several `TreeEntry` messages to yield,
+            /// because of intermediate directories. Hence we store them in this queue.
+            ///
+            /// Each call to `next()` pops either pops from the queue or consumes one or several
+            /// manifest entries, possibly pushing several more entries in the queue beside yielding
+            /// one.
+            to_yield: VecDeque::new(),
+        }
+    }
+
+    fn tree_entry(
+        &self,
+        path: &[u8],
+        oid: String,
+        obj_type: EntryType,
+        mode: i32,
+    ) -> Result<TreeEntry, HgError> {
+        Ok(TreeEntry {
+            oid,
+            mode,
+            commit_oid: self.commit_oid.clone(),
+            root_oid: self.root_oid.clone(),
+            path: path.to_vec(),
+            r#type: obj_type as i32,
+            flat_path: Vec::new(), // resulting flat_path is always default for recursive queries
+        })
+    }
+}
+
+impl<'a, 'm, IM> Iterator for RecursiveDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    type Item = Result<TreeEntry, HgError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(entry) = self.to_yield.pop_front() {
+            return Some(entry);
+        }
+
+        while let Some(entry_res) = self.manifest_dir_iter.next() {
+            match entry_res {
+                Err(e) => {
+                    return Some(Err(e));
+                }
+                Ok(entry) => {
+                    let ep = entry.path.as_bytes();
+                    let sp = &ep[self.manifest_dir_iter.prefix_len()..];
+
+                    let (mut subdir, rem_idx) = common_subpath_split(self.current_subdir, sp);
+                    for i in rem_idx..sp.len() {
+                        if sp[i] != b'/' {
+                            continue;
+                        }
+                        subdir = &sp[..i];
+                        let fulldir = &ep[..i + self.manifest_dir_iter.prefix_len()];
+                        self.to_yield.push_back(self.tree_entry(
+                            fulldir,
+                            tree_oid(&self.changeset_node, fulldir),
+                            EntryType::Tree,
+                            git::OBJECT_MODE_TREE,
+                        ));
+                    }
+                    self.current_subdir = subdir;
+
+                    match git_perms(&entry) {
+                        Err(e) => {
+                            self.to_yield.push_back(Err(e));
+                        }
+                        Ok(mode) => {
+                            self.to_yield.push_back(self.tree_entry(
+                                ep,
+                                blob_oid(&self.changeset_node, ep),
+                                EntryType::Blob,
+                                mode,
+                            ));
+                        }
+                    }
+                }
+            }
+        }
+        self.to_yield.pop_front()
+    }
+}
+
 #[cfg(test)]
 mod tests {
 
@@ -742,4 +858,74 @@
             ]
         );
     }
+
+    #[test]
+    fn test_recursive_dir_iter() {
+        let cs_node = Node::from_hex(b"1234567812345678123456781234567812345678").unwrap();
+
+        let manifest = paths_manifest(vec!["foo/a"]);
+        let iter =
+            RecursiveDirIterator::new("branch/test".to_owned(), cs_node, manifest.into_iter(), &[]);
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_tree(&cs_node, b"foo"),
+                testing_blob(&cs_node, b"foo/a"),
+            ]
+        );
+
+        let manifest = paths_manifest(vec![
+            "foo.",
+            "foo/a",
+            "foo/sub/a",
+            "foo/sub/b",
+            "foo/subb",
+            "foo0",
+        ]);
+        let iter = RecursiveDirIterator::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob(&cs_node, b"foo/a"),
+                testing_tree(&cs_node, b"foo/sub"),
+                testing_blob(&cs_node, b"foo/sub/a"),
+                testing_blob(&cs_node, b"foo/sub/b"),
+                testing_blob(&cs_node, b"foo/subb"),
+            ]
+        );
+
+        let manifest = paths_manifest(vec![
+            "foo.",
+            "foo/a",
+            "foo/sub/a",
+            "foo/sub/ssub/b",
+            "foo/subb",
+            "foo0",
+        ]);
+        let iter = RecursiveDirIterator::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob(&cs_node, b"foo/a"),
+                testing_tree(&cs_node, b"foo/sub"),
+                testing_blob(&cs_node, b"foo/sub/a"),
+                testing_tree(&cs_node, b"foo/sub/ssub"),
+                testing_blob(&cs_node, b"foo/sub/ssub/b"),
+                testing_blob(&cs_node, b"foo/subb"),
+            ]
+        );
+    }
 }