/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;

use anyhow::Result;
use formatter::Formattable;
use formatter::ListFormatter;
use serde::Serialize;
use types::path::RepoPathRelativizer;
use types::RepoPath;
use types::RepoPathBuf;

/// Config that determines how the output of `hg status` will be printed to the console.
pub struct PrintConfig {
    /// Determines which types of statuses will be displayed.
    pub status_types: PrintConfigStatusTypes,
    /// If true, the status will not be printed: only the path.
    pub no_status: bool,
    /// If true, for each file that was copied/moved, the source of copy/move will be printed
    /// below the destination.
    pub copies: bool,
    /// Termination character for the line: in practice this is '\0' or '\n'.
    pub endl: char,
    /// If true, paths are printed relative to the root of the repository; otherwise, they are
    /// printed relative to getcwd(2).
    pub root_relative: bool,
}

/// This struct covers the possible set of values for `hg status`. Used in conjunction with
/// PrintConfig: paths will only be included in the output of `hg status` if their corresponding
/// status is true in this struct.
pub struct PrintConfigStatusTypes {
    pub modified: bool,
    pub added: bool,
    pub removed: bool,
    pub deleted: bool,
    pub clean: bool,
    pub unknown: bool,
    pub ignored: bool,
}

enum PrintGroup {
    Modified,
    Added,
    Removed,
    Deleted,
    Unknown,
    Ignored,
    Clean,
}

/// Wrapper around an ordinary PathRelativizer that honors the --root-relative flag to `hg status`.
struct HgStatusPathRelativizer {
    relativizer: Option<RepoPathRelativizer>,
}

impl HgStatusPathRelativizer {
    /// * `root_relative` true if --root-relative was specified.
    /// * `relativizer` comes from HgArgs.relativizer.
    pub fn new(root_relative: bool, relativizer: RepoPathRelativizer) -> HgStatusPathRelativizer {
        let relativizer = match (root_relative, relativizer) {
            (false, r) => Some(r),
            _ => None,
        };
        HgStatusPathRelativizer { relativizer }
    }

    /// Returns a String that is suitable for display to the user.
    ///
    /// If `root_relative` is true, the path returned will be relative to the working directory.
    pub fn relativize(&self, repo_path: &RepoPath) -> String {
        let out = match self.relativizer {
            Some(ref relativizer) => relativizer.relativize(repo_path),
            None => repo_path.to_string(),
        };

        if !out.is_empty() {
            out
        } else {
            // In the rare event that the relativized path results in the empty string, print "."
            // instead so the user does not end up with an empty line.
            String::from(".")
        }
    }
}

#[derive(Serialize)]
struct StatusEntry<'a> {
    path: String,

    status: &'a str,

    #[serde(skip_serializing_if = "Option::is_none")]
    copy: Option<String>,

    #[serde(skip_serializing)]
    style: &'a str,

    #[serde(skip_serializing)]
    print_config: &'a PrintConfig,
}

impl<'a> Formattable for StatusEntry<'a> {
    fn format_plain(
        &self,
        _options: &formatter::formatter::FormatOptions,
        writer: &mut dyn formatter::StyleWrite,
    ) -> Result<(), anyhow::Error> {
        let status = if self.print_config.no_status {
            "".to_owned()
        } else {
            format!("{} ", self.status)
        };

        writer.write_styled(
            self.style,
            &format!("{}{}{}", status, self.path, self.print_config.endl),
        )?;

        if self.print_config.copies {
            if let Some(p) = &self.copy {
                writer.write_styled(
                    "status.copied",
                    &format!("  {}{}", p, self.print_config.endl),
                )?;
            }
        }
        Ok(())
    }
}

pub fn print_status(
    mut formatter: Box<dyn ListFormatter>,
    relativizer: RepoPathRelativizer,
    print_config: &PrintConfig,
    status: &status::Status,
    copymap: &HashMap<RepoPathBuf, RepoPathBuf>,
) -> Result<()> {
    formatter.begin_list()?;

    let relativizer = HgStatusPathRelativizer::new(print_config.root_relative, relativizer);
    let mut print_group =
        |print_group, enabled: bool, group: &mut dyn Iterator<Item = &RepoPathBuf>| -> Result<()> {
            if !enabled {
                return Ok(());
            }

            // `hg config | grep color` did not yield the entries for color.status listed on
            // https://www.mercurial-scm.org/wiki/ColorExtension. At Meta, we seem to match
            // the defaults listed on the wiki page, except we don't change the background color.
            let (status, style) = match print_group {
                PrintGroup::Modified => ("M", "status.modified"),
                PrintGroup::Added => ("A", "status.added"),
                PrintGroup::Removed => ("R", "status.removed"),
                PrintGroup::Deleted => ("!", "status.deleted"),
                PrintGroup::Unknown => ("?", "status.unknown"),
                PrintGroup::Ignored => ("I", "status.ignored"),
                PrintGroup::Clean => ("C", "status.clean"),
            };

            let mut group = group.collect::<Vec<_>>();
            group.sort();
            for path in group {
                formatter.format_item(&StatusEntry {
                    path: relativizer.relativize(path),
                    status,
                    copy: copymap.get(path).map(|p| relativizer.relativize(p)),
                    style,
                    print_config,
                })?;
            }
            Ok(())
        };

    print_group(
        PrintGroup::Modified,
        print_config.status_types.modified,
        &mut status.modified(),
    )?;
    print_group(
        PrintGroup::Added,
        print_config.status_types.added,
        &mut status.added(),
    )?;
    print_group(
        PrintGroup::Removed,
        print_config.status_types.removed,
        &mut status.removed(),
    )?;
    print_group(
        PrintGroup::Deleted,
        print_config.status_types.deleted,
        &mut status.deleted(),
    )?;
    print_group(
        PrintGroup::Unknown,
        print_config.status_types.unknown,
        &mut status.unknown(),
    )?;
    print_group(
        PrintGroup::Ignored,
        print_config.status_types.ignored,
        &mut status.ignored(),
    )?;
    print_group(
        PrintGroup::Clean,
        print_config.status_types.clean,
        &mut status.clean(),
    )?;

    formatter.end_list()?;

    Ok(())
}

#[cfg(test)]
impl Default for PrintConfig {
    fn default() -> Self {
        PrintConfig {
            status_types: PrintConfigStatusTypes {
                modified: true,
                added: true,
                removed: true,
                deleted: true,
                clean: false,
                unknown: true,
                ignored: false,
            },
            no_status: false,
            copies: false,
            endl: '\n',
            root_relative: false,
        }
    }
}

#[cfg(test)]
mod test {
    use std::collections::BTreeMap;
    use std::str;

    use clidispatch::io::IO;
    use formatter::formatter::get_formatter;
    use formatter::formatter::FormatOptions;

    use super::*;

    fn repo_path_buf(path: &str) -> RepoPathBuf {
        RepoPathBuf::from_string(path.to_string()).unwrap()
    }

    fn extract_output(io: IO) -> (String, String) {
        let stdout = io.with_output(|o| o.as_any().downcast_ref::<Vec<u8>>().unwrap().clone());
        let stdout = str::from_utf8(&stdout).unwrap().to_string();
        let stderr = io.with_error(|e| {
            e.as_ref()
                .unwrap()
                .as_any()
                .downcast_ref::<Vec<u8>>()
                .unwrap()
                .clone()
        });
        let stderr = str::from_utf8(&stderr).unwrap().to_string();
        (stdout, stderr)
    }

    #[derive(Default)]
    struct PrintTestCase {
        print_config: PrintConfig,
        status: status::Status,
        copymap: HashMap<RepoPathBuf, RepoPathBuf>,
        stdout: String,
        stderr: String,
        color: bool,
    }

    /// Helper function for testing `print_status`.
    fn test_print(test_case: PrintTestCase) {
        let relativizer = RepoPathRelativizer::new("/repo", "/repo");
        let tin = "".as_bytes();
        let tout = Vec::new();
        let terr = Vec::new();
        let io = IO::new(tin, tout, Some(terr));
        let options = FormatOptions {
            debug: false,
            verbose: false,
            quiet: false,
            color: test_case.color,
            debug_color: false,
        };

        let mut config: BTreeMap<&str, &str> = BTreeMap::new();
        config.insert("color.status.added", "green");
        config.insert("color.status.deleted", "cyan");
        config.insert("color.status.ignored", "black");
        config.insert("color.status.modified", "blue");
        config.insert("color.status.removed", "red");
        config.insert("color.status.unknown", "magenta");

        let fm = get_formatter(&config, "status", "", options, Box::new(io.output())).unwrap();
        print_status(
            fm,
            relativizer,
            &test_case.print_config,
            &test_case.status,
            &test_case.copymap,
        )
        .unwrap();
        let (actual_output, actual_error) = extract_output(io);
        assert_eq!(actual_output, test_case.stdout);
        assert_eq!(actual_error, test_case.stderr);
    }

    // XXX: PathRelativizer is problematic on OSX.
    #[cfg(target_os = "linux")]
    #[test]
    fn test_empty() {
        let status = status::StatusBuilder::new().build();

        test_print(PrintTestCase {
            status,
            ..Default::default()
        });
    }

    #[test]
    fn test_print_status() {
        let status = status::StatusBuilder::new()
            .modified(vec![repo_path_buf("modified.txt")])
            .added(vec![
                repo_path_buf("added.txt"),
                repo_path_buf("added_even_though_normally_ignored.txt"),
                repo_path_buf("added_other_parent.txt"),
            ])
            .removed(vec![
                repo_path_buf("modified_and_marked_for_removal.txt"),
                repo_path_buf("removed.txt"),
            ])
            .deleted(vec![repo_path_buf(
                "removed_but_not_marked_for_removal.txt",
            )])
            .unknown(vec![repo_path_buf("unknown.txt")])
            .ignored(vec![repo_path_buf("ignored.txt")])
            .build();

        // We have to slice [1..] to strip the leading newline.
        let no_arg_stdout = r#"
M modified.txt
A added.txt
A added_even_though_normally_ignored.txt
A added_other_parent.txt
R modified_and_marked_for_removal.txt
R removed.txt
! removed_but_not_marked_for_removal.txt
? unknown.txt
"#[1..]
            .to_string();
        test_print(PrintTestCase {
            status: status.clone(),
            stdout: no_arg_stdout,
            ..Default::default()
        });

        // We have to slice [1..] to strip the leading newline.
        let mardui_stdout = r#"
M modified.txt
A added.txt
A added_even_though_normally_ignored.txt
A added_other_parent.txt
R modified_and_marked_for_removal.txt
R removed.txt
! removed_but_not_marked_for_removal.txt
? unknown.txt
I ignored.txt
"#[1..]
            .to_string();
        let mut print_config = PrintConfig::default();
        print_config.status_types.ignored = true;
        test_print(PrintTestCase {
            status: status.clone(),
            print_config,
            stdout: mardui_stdout,
            ..Default::default()
        });

        let mardui_color_stdout = concat!(
            "\x1b[34mM modified.txt\x1b[39m\n",
            "\x1b[32mA added.txt\x1b[39m\n",
            "\x1b[32mA added_even_though_normally_ignored.txt\x1b[39m\n",
            "\x1b[32mA added_other_parent.txt\x1b[39m\n",
            "\x1b[31mR modified_and_marked_for_removal.txt\x1b[39m\n",
            "\x1b[31mR removed.txt\x1b[39m\n",
            "\x1b[36m! removed_but_not_marked_for_removal.txt\x1b[39m\n",
            "\x1b[35m? unknown.txt\x1b[39m\n",
            "\x1b[30mI ignored.txt\x1b[39m\n",
        );
        let mut print_config = PrintConfig::default();
        print_config.status_types.ignored = true;
        test_print(PrintTestCase {
            status,
            print_config,
            stdout: mardui_color_stdout.to_string(),
            color: true,
            ..Default::default()
        });
    }

    // XXX: PathRelativizer is problematic on OSX.
    #[cfg(target_os = "linux")]
    #[test]
    fn no_status_flag() {
        let status = status::StatusBuilder::new()
            .modified(vec![repo_path_buf("file.txt")])
            .build();

        let print_config = PrintConfig {
            no_status: true,
            ..PrintConfig::default()
        };

        test_print(PrintTestCase {
            status,
            print_config,
            stdout: "file.txt\n".to_owned(),
            ..Default::default()
        });
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::io;
use std::path::Path;
use std::sync::Arc;

use dag::delegate;
use dag::Set;
use dag::Vertex;
use futures::stream::BoxStream;
use minibytes::Bytes;

use crate::AppendCommits;
use crate::DescribeBackend;
use crate::HgCommit;
use crate::HgCommits;
use crate::ParentlessHgCommit;
use crate::ReadCommitText;
use crate::Result;
use crate::RevlogCommits;
use crate::StreamCommitText;
use crate::StripCommits;

/// Segmented Changelog + Revlog.
///
/// Use segmented changelog for the commit graph algorithms and IdMap.
/// Use revlog for fallback commit messages. Double writes to revlog.
pub struct DoubleWriteCommits {
    revlog: RevlogCommits,
    commits: HgCommits,
}

impl DoubleWriteCommits {
    pub fn new(revlog_dir: &Path, dag_path: &Path, commits_path: &Path) -> Result<Self> {
        let commits = HgCommits::new(dag_path, commits_path)?;
        let revlog = RevlogCommits::new(revlog_dir)?;
        Ok(Self { revlog, commits })
    }
}

#[async_trait::async_trait]
impl AppendCommits for DoubleWriteCommits {
    async fn add_commits(&mut self, commits: &[HgCommit]) -> Result<()> {
        self.revlog.add_commits(commits).await?;
        self.commits.add_commits(commits).await?;
        Ok(())
    }

    async fn flush(&mut self, master_heads: &[Vertex]) -> Result<()> {
        self.revlog.flush(master_heads).await?;
        self.commits.flush(master_heads).await?;
        Ok(())
    }

    async fn flush_commit_data(&mut self) -> Result<()> {
        self.revlog.flush_commit_data().await?;
        self.commits.flush_commit_data().await?;
        Ok(())
    }
}

#[async_trait::async_trait]
impl ReadCommitText for DoubleWriteCommits {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        match self.commits.get_commit_raw_text(vertex).await {
            Ok(None) => self.revlog.get_commit_raw_text(vertex).await,
            result => result,
        }
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        self.revlog.to_dyn_read_commit_text()
    }
}

impl StreamCommitText for DoubleWriteCommits {
    fn stream_commit_raw_text(
        &self,
        stream: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>> {
        self.revlog.stream_commit_raw_text(stream)
    }
}

#[async_trait::async_trait]
impl StripCommits for DoubleWriteCommits {
    async fn strip_commits(&mut self, set: Set) -> Result<()> {
        self.revlog.strip_commits(set.clone()).await?;
        self.commits.strip_commits(set).await?;
        Ok(())
    }
}

delegate!(CheckIntegrity | IdConvert | IdMapSnapshot | PrefixLookup | DagAlgorithm, DoubleWriteCommits => self.commits);

impl DescribeBackend for DoubleWriteCommits {
    fn algorithm_backend(&self) -> &'static str {
        "segments"
    }

    fn describe_backend(&self) -> String {
        format!(
            r#"Backend (doublewrite):
  Local:
    Segments + IdMap: {}
    Zstore: {}
    Revlog + Nodemap: {}
Feature Providers:
  Commit Graph Algorithms:
    Segments
  Commit Hash / Rev Lookup:
    IdMap
  Commit Data (user, message):
    Zstore (incomplete)
    Revlog
"#,
            self.commits.dag_path.display(),
            self.commits.commits_path.display(),
            self.revlog.dir.join("00changelog.{i,d,nodemap}").display(),
        )
    }

    fn explain_internals(&self, w: &mut dyn io::Write) -> io::Result<()> {
        self.commits.explain_internals(w)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use dag::Vertex;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CommitError {
    #[error(transparent)]
    Dag(#[from] dag::Error),

    #[error("hash mismatch ({0:?} != {1:?})")]
    HashMismatch(Vertex, Vertex),

    #[error("{0} is unsupported")]
    Unsupported(&'static str),

    #[error("{0} is required for opening commits")]
    OpenRequirements(&'static str),

    #[error("unable to read {0}: `{1}")]
    FileReadError(&'static str, std::io::Error),
}

impl From<std::io::Error> for CommitError {
    fn from(err: std::io::Error) -> Self {
        Self::Dag(dag::errors::BackendError::from(err).into())
    }
}

impl From<anyhow::Error> for CommitError {
    fn from(err: anyhow::Error) -> Self {
        Self::Dag(dag::errors::BackendError::from(err).into())
    }
}

impl From<revlogindex::Error> for CommitError {
    fn from(err: revlogindex::Error) -> Self {
        anyhow::Error::from(err).into()
    }
}

impl From<zstore::Error> for CommitError {
    fn from(err: zstore::Error) -> Self {
        anyhow::Error::from(err).into()
    }
}

impl From<gitdag::git2::Error> for CommitError {
    fn from(err: gitdag::git2::Error) -> Self {
        anyhow::Error::from(err).into()
    }
}

impl From<metalog::Error> for CommitError {
    fn from(err: metalog::Error) -> Self {
        anyhow::Error::from(err).into()
    }
}

impl From<types::hash::LengthMismatchError> for CommitError {
    fn from(err: types::hash::LengthMismatchError) -> Self {
        anyhow::Error::from(err).into()
    }
}

pub fn test_only(name: &str) -> CommitError {
    dag::Error::Programming(format!("{} should only be used in tests", name)).into()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use dag::delegate;
use dag::errors::NotFoundError;
use dag::ops::DagPersistent;
use dag::Group;
use dag::Set;
use dag::Vertex;
use dag::VertexListWithOptions;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use gitdag::git2;
use gitdag::GitDag;
use metalog::MetaLog;
use minibytes::Bytes;
use parking_lot::Mutex;
use storemodel::ReadRootTreeIds;
use types::HgId;

use crate::utils;
use crate::AppendCommits;
use crate::DescribeBackend;
use crate::GraphNode;
use crate::HgCommit;
use crate::ParentlessHgCommit;
use crate::ReadCommitText;
use crate::Result;
use crate::StreamCommitText;
use crate::StripCommits;

/// Git Commits with segments index.
///
/// Use segmented changelog for the commit graph algorithms and IdMap.
/// Use libgit2 for commit messages.
///
/// This is currently read-only, because the `add_commits` API is
/// coupled with the HG SHA1 implementation details and Git does not
/// use HG's SHA1.
///
/// It does not support migrating to other formats, because of SHA1
/// incompatibility.
///
/// In the future when we abstract away the HG SHA1 logic, we can
/// revisit and build something writable based on this.
pub struct GitSegmentedCommits {
    git_repo: Arc<Mutex<git2::Repository>>,
    dag: GitDag,
    dag_path: PathBuf,
    git_path: PathBuf,
}

impl GitSegmentedCommits {
    pub fn new(git_dir: &Path, dag_dir: &Path) -> Result<Self> {
        let git_repo = git2::Repository::open(git_dir)?;
        // open_git_repo has side effect building up the segments
        let dag = GitDag::open_git_repo(&git_repo, dag_dir, "refs/remotes/origin/master")?;
        let dag_path = dag_dir.to_path_buf();
        let git_path = git_dir.to_path_buf();
        Ok(Self {
            git_repo: Arc::new(Mutex::new(git_repo)),
            dag,
            dag_path,
            git_path,
        })
    }

    /// Rewrite metalog bookmarks, remotenames to match git references.
    /// The reverse of `metalog_to_git_references`, used at the start of a transaction.
    pub fn git_references_to_metalog(&self, metalog: &mut MetaLog) -> Result<()> {
        let refs = self.dag.git_references();

        let mut bookmarks = BTreeMap::new();
        let mut remotenames = BTreeMap::new();
        let mut visibleheads = Vec::new();

        for (name, vertex) in refs {
            let names: Vec<&str> = name.splitn(3, '/').collect();
            let id = match HgId::from_slice(vertex.as_ref()) {
                Ok(id) => id,
                Err(_) => continue,
            };
            match &names[..] {
                ["refs", "remotes", name] => {
                    // Treat as a remotename
                    if name.contains('/') && !name.ends_with("/HEAD") && !name.starts_with("tags/")
                    {
                        remotenames.insert(name.to_string(), id);
                    }
                }
                ["refs", "heads", name] => {
                    // Treat as a local bookmark.
                    if name != &"HEAD" {
                        bookmarks.insert(name.to_string(), id);
                    }
                }
                ["refs", "tags", name] => {
                    // Treat as a remotename prefixed with `tags/`.
                    if name != &"HEAD" {
                        let name = format!("tags/{}", name);
                        remotenames.insert(name, id);
                    }
                }
                ["refs", "visibleheads", _name] => {
                    visibleheads.push(id);
                }
                _ => {}
            }
        }

        let encoded_bookmarks = refencode::encode_bookmarks(&bookmarks);
        let encoded_remotenames = refencode::encode_remotenames(&remotenames);
        let encoded_visibleheads = refencode::encode_visibleheads(&visibleheads);
        metalog.set("bookmarks", encoded_bookmarks.as_ref())?;
        metalog.set("remotenames", encoded_remotenames.as_ref())?;
        metalog.set("visibleheads", encoded_visibleheads.as_ref())?;
        let mut opts = metalog::CommitOptions::default();
        opts.message = "sync from git";
        metalog.commit(opts)?;

        Ok(())
    }

    /// Rewrite git references to match bookmarks, remotenames in metalog.
    /// The reverse of `git_references_to_metalog`, used at the end of a transaction.
    pub fn metalog_to_git_references(&self, metalog: &MetaLog) -> Result<()> {
        let expected_refs = {
            let mut refs: BTreeMap<String, git2::Oid> = Default::default();
            if let Some(encoded) = metalog.get("bookmarks")? {
                let decoded = refencode::decode_bookmarks(&encoded)?;
                for (name, hgid) in decoded {
                    let name = format!("refs/heads/{}", name);
                    refs.insert(name, hgid_to_git_oid(hgid));
                }
            }
            if let Some(encoded) = metalog.get("remotenames")? {
                let decoded = refencode::decode_remotenames(&encoded)?;
                for (name, hgid) in decoded {
                    let name = if let Some(tag) = name.strip_prefix("tags/") {
                        format!("refs/tags/{}", tag)
                    } else {
                        format!("refs/remotes/{}", name)
                    };
                    refs.insert(name, hgid_to_git_oid(hgid));
                }
            }
            if let Some(encoded) = metalog.get("visibleheads")? {
                let decoded = refencode::decode_visibleheads(&encoded)?;
                for hgid in decoded {
                    let name = format!("refs/visibleheads/{}", hgid.to_hex());
                    refs.insert(name, hgid_to_git_oid(hgid));
                }
            }
            refs
        };

        {
            let reflog_message = format!(
                "{}\nRootId: {}",
                metalog.message(),
                metalog.root_id().to_hex()
            );
            let repo = self.git_repo.lock();
            let mut handled_ref_names = HashSet::with_capacity(expected_refs.len());
            for reference in repo.references()? {
                let mut reference = reference?;
                let name = match reference.name() {
                    Some(n) => n,
                    None => continue,
                };
                handled_ref_names.insert(name.to_string());
                // Only care about managed ref names. Skip HEAD or FETCH_HEAD
                // or refs/something_else/*. See git_references_to_metalog
                // for managed refs.
                let names: Vec<&str> = name.splitn(3, '/').collect();
                let managed: bool = match &names[..] {
                    ["refs", "remotes", _] => true,
                    ["refs", "heads", _] => true,
                    ["refs", "tags", _] => true,
                    ["refs", "visibleheads", _] => true,
                    _ => false,
                };
                if !managed {
                    continue;
                }
                let expected_oid = expected_refs.get(name);
                match expected_oid {
                    None => reference.delete()?,
                    Some(&oid) => {
                        if let Ok(obj) = reference.peel(git2::ObjectType::Commit) {
                            if obj.id() != oid {
                                repo.reference(name, oid, true, &reflog_message)?;
                            }
                        }
                    }
                }
            }
            for (name, oid) in expected_refs {
                if handled_ref_names.contains(name.as_str()) {
                    continue;
                }
                repo.reference(&name, oid, true, &reflog_message)?;
            }
        }

        Ok(())
    }
}

#[async_trait::async_trait]
impl AppendCommits for GitSegmentedCommits {
    async fn add_commits(&mut self, commits: &[HgCommit]) -> Result<()> {
        // Write to git odb.
        // Raw text format should be in git, although the type name is HgCommit.
        {
            let repo = self.git_repo.lock();
            let odb = repo.odb()?;
            for commit in commits {
                let oid = odb.write(git2::ObjectType::Commit, commit.raw_text.as_ref())?;
                if oid.as_ref() != commit.vertex.as_ref() {
                    return Err(crate::Error::HashMismatch(
                        Vertex::copy_from(oid.as_ref()),
                        commit.vertex.clone(),
                    ));
                }
            }
        }

        // Write to segments.
        let graph_nodes = utils::commits_to_graph_nodes(commits);
        self.add_graph_nodes(&graph_nodes).await?;

        Ok(())
    }

    async fn add_graph_nodes(&mut self, graph_nodes: &[GraphNode]) -> Result<()> {
        utils::add_graph_nodes_to_dag(&mut *self.dag, graph_nodes).await
    }

    async fn flush(&mut self, master_heads: &[Vertex]) -> Result<()> {
        let heads = VertexListWithOptions::from(master_heads).with_highest_group(Group::MASTER);
        self.dag.flush(&heads).await?;
        Ok(())
    }

    async fn flush_commit_data(&mut self) -> Result<()> {
        Ok(())
    }

    fn update_references_to_match_metalog(&mut self, metalog: &MetaLog) -> Result<()> {
        self.metalog_to_git_references(metalog)
    }
}

#[async_trait::async_trait]
impl ReadCommitText for GitSegmentedCommits {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        self.git_repo.get_commit_raw_text(vertex).await
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        self.git_repo.to_dyn_read_commit_text()
    }

    fn to_dyn_read_root_tree_ids(&self) -> Arc<dyn ReadRootTreeIds + Send + Sync> {
        // The default impl works. But ReadCommitText has overhead constructing
        // the hg text. Bypass that overhead.
        Arc::new(Wrapper(self.git_repo.clone()))
    }
}

#[async_trait::async_trait]
impl ReadCommitText for Arc<Mutex<git2::Repository>> {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        let oid = match git2::Oid::from_bytes(vertex.as_ref()) {
            Ok(oid) => oid,
            Err(_) => return Ok(None),
        };
        let repo = self.lock();
        let commit = match repo.find_commit(oid) {
            Ok(commit) => commit,
            Err(e) if e.code() == git2::ErrorCode::NotFound => {
                return Ok(crate::revlog::get_hard_coded_commit_text(vertex));
            }
            Err(e) => return Err(e.into()),
        };
        let text = to_hg_text(&commit);
        Ok(Some(text))
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        Arc::new(self.clone())
    }
}

// Workaround orphan rule
struct Wrapper<T>(T);

#[async_trait::async_trait]
impl ReadRootTreeIds for Wrapper<Arc<Mutex<git2::Repository>>> {
    async fn read_root_tree_ids(&self, commits: Vec<HgId>) -> anyhow::Result<Vec<(HgId, HgId)>> {
        let mut result = Vec::with_capacity(commits.len());
        let repo = self.0.lock();
        for commit_hgid in commits {
            if commit_hgid.is_null() {
                continue;
            }

            let oid = hgid_to_git_oid(commit_hgid);
            let commit = repo.find_commit(oid)?;
            let tree_id = commit.tree_id();
            let tree_hgid =
                HgId::from_slice(tree_id.as_bytes()).expect("git Oid should convert to HgId");
            result.push((commit_hgid, tree_hgid));
        }
        Ok(result)
    }
}

impl StreamCommitText for GitSegmentedCommits {
    fn stream_commit_raw_text(
        &self,
        stream: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>> {
        let git_repo = git2::Repository::open(&self.git_path)?;
        let stream = stream.map(move |item| {
            let vertex = item?;
            let oid = match git2::Oid::from_bytes(vertex.as_ref()) {
                Ok(oid) => oid,
                Err(_) => return vertex.not_found().map_err(Into::into),
            };
            let commit = git_repo.find_commit(oid)?;
            let raw_text = to_hg_text(&commit);
            Ok(ParentlessHgCommit { vertex, raw_text })
        });
        Ok(Box::pin(stream))
    }
}

#[async_trait::async_trait]
impl StripCommits for GitSegmentedCommits {
    async fn strip_commits(&mut self, _set: Set) -> Result<()> {
        Err(crate::Error::Unsupported("strip for git backend"))
    }
}

delegate!(CheckIntegrity | IdConvert | IdMapSnapshot | PrefixLookup | DagAlgorithm, GitSegmentedCommits => self.dag);

impl DescribeBackend for GitSegmentedCommits {
    fn algorithm_backend(&self) -> &'static str {
        "segments"
    }

    fn describe_backend(&self) -> String {
        format!(
            r#"Backend (segmented git):
  Local:
    Segments + IdMap: {}
    Git: {}
Feature Providers:
  Commit Graph Algorithms:
    Segments
  Commit Hash / Rev Lookup:
    IdMap
  Commit Data (user, message):
    Git
"#,
            self.dag_path.display(),
            self.git_path.display(),
        )
    }

    fn explain_internals(&self, w: &mut dyn io::Write) -> io::Result<()> {
        write!(w, "{:?}", &*self.dag)
    }
}

fn to_hex(oid: git2::Oid) -> String {
    const HEX_CHARS: &[u8] = b"0123456789abcdef";
    let bytes = oid.as_bytes();
    let mut v = Vec::with_capacity(bytes.len() * 2);
    for &byte in bytes {
        v.push(HEX_CHARS[(byte >> 4) as usize]);
        v.push(HEX_CHARS[(byte & 0xf) as usize]);
    }
    unsafe { String::from_utf8_unchecked(v) }
}

// For "Wed, 23 Nov 2022 17:47:30 -0800",
//
// git commit message: "1669254450 -0800"
// hg commit message:  "1669254450 28800"
// libgit2 Time::offset_minutes: -480

fn to_hg_date_text(time: &git2::Time) -> String {
    // See above. Convert -480 to 28800.
    let hg_date_offset_seconds = -time.offset_minutes() * 60;
    format!("{} {}", time.seconds(), hg_date_offset_seconds)
}

/// Convert a git commit to hg commit text.
fn to_hg_text(commit: &git2::Commit) -> Bytes {
    // 222 is calculated from debugshell in linux.git:
    // max(len(cl.revision(n))-len(repo[n].description().encode('utf8')) for n in cl.dag.all().take(50000))
    let len = commit.message_bytes().len() + 222;
    let mut result = Vec::with_capacity(len);
    let mut write = |s: &[u8]| result.extend_from_slice(s);

    fn utf8<'a>(s: &'a [u8]) -> Cow<'a, str> {
        String::from_utf8_lossy(s)
    }

    // Construct the commit using (faked) hg format:
    // manifest hex + "\n" + user + "\n" + date + (extra) + "\n" + (files) + "\n" + desc

    // manifest hex
    write(to_hex(commit.tree_id()).as_bytes());
    write(b"\n");

    // user
    let author = commit.author();
    write(utf8(author.name_bytes()).as_bytes());
    write(b" <");
    write(utf8(author.email_bytes()).as_bytes());
    write(b">\n");

    // date
    write(to_hg_date_text(&author.when()).as_bytes());

    // extras (committer)
    let committer = commit.committer();
    write(b" committer:");
    write(utf8(committer.name_bytes()).as_bytes());
    write(b" <");
    write(utf8(committer.email_bytes()).as_bytes());
    write(b">\0committer_date:");
    write(to_hg_date_text(&committer.when()).as_bytes());
    write(b"\n");

    // files
    // NOTE: currently ignored.
    write(b"\n");

    // message
    write(utf8(commit.message_bytes()).as_bytes());

    result.into()
}

fn hgid_to_git_oid(id: HgId) -> git2::Oid {
    git2::Oid::from_bytes(id.as_ref()).expect("HgId should convert to git2::Oid")
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use dag::delegate;
use dag::errors::NotFoundError;
use dag::ops::DagAlgorithm;
use dag::ops::DagPersistent;
use dag::ops::DagStrip;
use dag::Dag;
use dag::Group;
use dag::Set;
use dag::Vertex;
use dag::VertexListWithOptions;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use minibytes::Bytes;
use parking_lot::RwLock;
use zstore::Id20;
use zstore::Zstore;

use crate::utils;
use crate::AppendCommits;
use crate::DescribeBackend;
use crate::GraphNode;
use crate::HgCommit;
use crate::ParentlessHgCommit;
use crate::ReadCommitText;
use crate::Result;
use crate::StreamCommitText;
use crate::StripCommits;

/// Commits using the HG SHA1 hash function. Stored on disk.
pub struct HgCommits {
    commits: Arc<RwLock<Zstore>>,
    pub(crate) commits_path: PathBuf,
    pub(crate) dag: Dag,
    pub(crate) dag_path: PathBuf,
}

impl HgCommits {
    pub fn new(dag_path: &Path, commits_path: &Path) -> Result<Self> {
        let result = Self {
            dag: Dag::open(dag_path)?,
            dag_path: dag_path.to_path_buf(),
            commits: Arc::new(RwLock::new(Zstore::open(commits_path)?)),
            commits_path: commits_path.to_path_buf(),
        };
        Ok(result)
    }

    /// Import another DAG. `main` specifies the main branch for commit graph
    /// optimization.
    pub async fn import_dag(&mut self, other: impl DagAlgorithm, main: Set) -> Result<()> {
        self.dag.import_and_flush(&other, main).await?;
        Ok(())
    }

    pub(crate) fn commit_data_store(&self) -> Arc<RwLock<Zstore>> {
        self.commits.clone()
    }
}

#[async_trait::async_trait]
impl AppendCommits for HgCommits {
    async fn add_commits(&mut self, commits: &[HgCommit]) -> Result<()> {
        fn null_id() -> Vertex {
            Vertex::copy_from(Id20::null_id().as_ref())
        }

        // The SHA1 of hg commit includes sorted(p1, p2) as header.
        fn text_with_header(raw_text: &[u8], parents: &[Vertex]) -> Result<Vec<u8>> {
            let mut result = Vec::with_capacity(raw_text.len() + Id20::len() * 2);
            let (p1, p2) = (
                parents.get(0).cloned().unwrap_or_else(null_id),
                parents.get(1).cloned().unwrap_or_else(null_id),
            );
            if p1 < p2 {
                result.write_all(p1.as_ref())?;
                result.write_all(p2.as_ref())?;
            } else {
                result.write_all(p2.as_ref())?;
                result.write_all(p1.as_ref())?;
            }
            result.write_all(&raw_text)?;
            Ok(result)
        }

        // Write commit data to zstore.
        for commit in commits {
            let text = text_with_header(&commit.raw_text, &commit.parents)?;
            let vertex = Vertex::copy_from(self.commits.write().insert(&text, &[])?.as_ref());
            if vertex != commit.vertex {
                return Err(crate::Error::HashMismatch(vertex, commit.vertex.clone()));
            }
        }

        // Write commit graph to DAG.
        let graph_nodes = utils::commits_to_graph_nodes(commits);
        self.add_graph_nodes(&graph_nodes).await?;

        Ok(())
    }

    async fn add_graph_nodes(&mut self, graph_nodes: &[GraphNode]) -> Result<()> {
        utils::add_graph_nodes_to_dag(&mut self.dag, graph_nodes).await
    }

    async fn flush(&mut self, master_heads: &[Vertex]) -> Result<()> {
        self.flush_commit_data().await?;
        let heads = VertexListWithOptions::from(master_heads).with_highest_group(Group::MASTER);
        self.dag.flush(&heads).await?;
        Ok(())
    }

    async fn flush_commit_data(&mut self) -> Result<()> {
        let mut zstore = self.commits.write();
        zstore.flush()?;
        Ok(())
    }
}

#[async_trait::async_trait]
impl ReadCommitText for HgCommits {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        self.commits.get_commit_raw_text(vertex).await
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        self.commits.to_dyn_read_commit_text()
    }
}

#[async_trait::async_trait]
impl ReadCommitText for Arc<RwLock<Zstore>> {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        let id = Id20::from_slice(vertex.as_ref())?;
        match self.read().get(id)? {
            Some(bytes) => Ok(Some(bytes.slice(Id20::len() * 2..))),
            None => Ok(crate::revlog::get_hard_coded_commit_text(vertex)),
        }
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        Arc::new(self.clone())
    }
}

impl StreamCommitText for HgCommits {
    fn stream_commit_raw_text(
        &self,
        stream: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>> {
        let zstore = Zstore::open(&self.commits_path)?;
        let stream = stream.map(move |item| {
            let vertex = item?;
            let id = Id20::from_slice(vertex.as_ref())?;
            // Mercurial hard-coded special-case that does not match SHA1.
            let raw_text = if id.is_null() || id.is_wdir() {
                Default::default()
            } else {
                match zstore.get(id)? {
                    Some(raw_data) => raw_data.slice(Id20::len() * 2..),
                    None => return vertex.not_found().map_err(Into::into),
                }
            };
            Ok(ParentlessHgCommit { vertex, raw_text })
        });
        Ok(Box::pin(stream))
    }
}

#[async_trait::async_trait]
impl StripCommits for HgCommits {
    async fn strip_commits(&mut self, set: Set) -> Result<()> {
        self.dag.strip(&set).await.map_err(Into::into)
    }
}

delegate!(CheckIntegrity | IdConvert | IdMapSnapshot | PrefixLookup | DagAlgorithm, HgCommits => self.dag);

impl DescribeBackend for HgCommits {
    fn algorithm_backend(&self) -> &'static str {
        "segments"
    }

    fn describe_backend(&self) -> String {
        format!(
            r#"Backend (non-lazy segments):
  Local:
    Segments + IdMap: {}
    Zstore: {}
Feature Providers:
  Commit Graph Algorithms:
    Segments
  Commit Hash / Rev Lookup:
    IdMap
  Commit Data (user, message):
    Zstore
"#,
            self.dag_path.display(),
            self.commits_path.display()
        )
    }

    fn explain_internals(&self, w: &mut dyn io::Write) -> io::Result<()> {
        write!(w, "{:?}", &self.dag)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashSet;
use std::io;
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::Arc;

use async_trait::async_trait;
use dag::delegate;
use dag::ops::DagAlgorithm;
use dag::ops::DagImportCloneData;
use dag::ops::DagImportPullData;
use dag::ops::DagPersistent;
use dag::protocol::AncestorPath;
use dag::protocol::RemoteIdConvertProtocol;
use dag::CloneData;
use dag::Location;
use dag::Set;
use dag::Vertex;
use dag::VertexListWithOptions;
use edenapi::types::CommitLocationToHashRequest;
use edenapi::EdenApi;
use futures::stream;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use minibytes::Bytes;
use parking_lot::RwLock;
use streams::HybridResolver;
use streams::HybridStream;
use tracing::instrument;
use zstore::Id20;
use zstore::Zstore;

use crate::AppendCommits;
use crate::DescribeBackend;
use crate::HgCommit;
use crate::HgCommits;
use crate::ParentlessHgCommit;
use crate::ReadCommitText;
use crate::Result;
use crate::RevlogCommits;
use crate::StreamCommitText;
use crate::StripCommits;

/// Segmented Changelog + Revlog (Optional) + Remote.
///
/// Use segmented changelog for the commit graph algorithms and IdMap.
/// Optionally writes to revlog just for fallback.
///
/// Use edenapi to resolve public commit messages and hashes.
pub struct HybridCommits {
    revlog: Option<RevlogCommits>,
    commits: HgCommits,
    client: Arc<dyn EdenApi>,
    lazy_hash_desc: String,
}

const EDENSCM_DISABLE_REMOTE_RESOLVE: &str = "EDENSCM_DISABLE_REMOTE_RESOLVE";
const EDENSCM_REMOTE_ID_THRESHOLD: &str = "EDENSCM_REMOTE_ID_THRESHOLD";
const EDENSCM_REMOTE_NAME_THRESHOLD: &str = "EDENSCM_REMOTE_NAME_THRESHOLD";

struct EdenApiProtocol {
    client: Arc<dyn EdenApi>,

    /// Manually disabled names defined by `EDENSCM_DISABLE_REMOTE_RESOLVE`
    /// in the form `hex1,hex2,...`.
    disabled_names: HashSet<Vertex>,

    /// Manually disabled ID resolution after `N` entries.
    /// Set by `EDENSCM_REMOTE_ID_THRESHOLD=N`.
    remote_id_threshold: Option<usize>,
    remote_id_current: AtomicUsize,

    /// Manually disabled name resolution after `N` entries.
    /// Set by `EDENSCM_REMOTE_NAME_THRESHOLD=N`.
    remote_name_threshold: Option<usize>,
    remote_name_current: AtomicUsize,
}

fn to_dag_error<E: Into<anyhow::Error>>(e: E) -> dag::Error {
    dag::errors::BackendError::Other(e.into()).into()
}

#[async_trait]
impl RemoteIdConvertProtocol for EdenApiProtocol {
    async fn resolve_names_to_relative_paths(
        &self,
        heads: Vec<Vertex>,
        names: Vec<Vertex>,
    ) -> dag::Result<Vec<(AncestorPath, Vec<Vertex>)>> {
        let mut pairs = Vec::with_capacity(names.len());
        let response_vec = {
            if heads.is_empty() {
                // Not an error case. Just do not resolve anything.
                return Ok(Vec::new());
            }
            let mut hgids = Vec::with_capacity(names.len());
            for name in names {
                if self.disabled_names.contains(&name) {
                    let msg = format!(
                        "Resolving {:?} is disabled via {}",
                        name, EDENSCM_DISABLE_REMOTE_RESOLVE
                    );
                    return Err(dag::errors::BackendError::Generic(msg).into());
                }
                if let Some(threshold) = self.remote_name_threshold {
                    let current = self.remote_name_current.fetch_add(1, SeqCst);
                    if current >= threshold {
                        let msg = format!(
                            "Resolving name {:?} exceeds threshold {} set by {}",
                            name, threshold, EDENSCM_REMOTE_NAME_THRESHOLD
                        );
                        return Err(dag::errors::BackendError::Generic(msg).into());
                    }
                }
                hgids.push(Id20::from_slice(name.as_ref()).map_err(to_dag_error)?);
            }
            let heads: Vec<_> = heads
                .iter()
                .map(|v| Id20::from_slice(v.as_ref()).map_err(to_dag_error))
                .collect::<dag::Result<Vec<_>>>()?;
            self.client
                .commit_hash_to_location(heads, hgids)
                .await
                .map_err(to_dag_error)?
        };
        for response in response_vec {
            if let Some(location) = response.result.map_err(to_dag_error)? {
                let path = AncestorPath {
                    x: Vertex::copy_from(location.descendant.as_ref()),
                    n: location.distance,
                    batch_size: 1,
                };
                let name = Vertex::copy_from(response.hgid.as_ref());
                pairs.push((path, vec![name]));
            }
        }
        Ok(pairs)
    }

    async fn resolve_relative_paths_to_names(
        &self,
        paths: Vec<AncestorPath>,
    ) -> dag::Result<Vec<(AncestorPath, Vec<Vertex>)>> {
        if let Some(threshold) = self.remote_id_threshold {
            let current = self.remote_id_current.fetch_add(1, SeqCst);
            if current >= threshold {
                let msg = format!(
                    "Resolving id exceeds threshold {} set by {}",
                    threshold, EDENSCM_REMOTE_ID_THRESHOLD
                );
                return Err(dag::errors::BackendError::Generic(msg).into());
            }
        }
        let mut pairs = Vec::with_capacity(paths.len());
        let response_vec = {
            let mut requests = Vec::with_capacity(paths.len());
            for path in paths {
                let descendant = Id20::from_slice(path.x.as_ref()).map_err(to_dag_error)?;
                requests.push(CommitLocationToHashRequest {
                    location: Location {
                        descendant,
                        distance: path.n,
                    },
                    count: path.batch_size,
                });
            }
            self.client
                .commit_location_to_hash(requests)
                .await
                .map_err(to_dag_error)?
        };
        for response in response_vec {
            let path = AncestorPath {
                x: Vertex::copy_from(response.location.descendant.as_ref()),
                n: response.location.distance,
                batch_size: response.count,
            };
            let names = response
                .hgids
                .into_iter()
                .map(|n| Vertex::copy_from(n.as_ref()))
                .collect();
            pairs.push((path, names));
        }
        Ok(pairs)
    }
}

impl HybridCommits {
    pub fn new(
        revlog_dir: Option<&Path>,
        dag_path: &Path,
        commits_path: &Path,
        client: Arc<dyn EdenApi>,
    ) -> Result<Self> {
        let commits = HgCommits::new(dag_path, commits_path)?;
        let revlog = match revlog_dir {
            Some(revlog_dir) => Some(RevlogCommits::new(revlog_dir)?),
            None => None,
        };
        Ok(Self {
            revlog,
            commits,
            client,
            lazy_hash_desc: "not lazy".to_string(),
        })
    }

    /// Enable fetching commit hashes lazily via EdenAPI.
    pub fn enable_lazy_commit_hashes(&mut self) {
        let mut disabled_names: HashSet<Vertex> = Default::default();
        if let Ok(env) = std::env::var(EDENSCM_DISABLE_REMOTE_RESOLVE) {
            for hex in env.split(",") {
                if let Ok(name) = Vertex::from_hex(hex.as_ref()) {
                    disabled_names.insert(name);
                }
            }
        }
        let remote_id_threshold = if let Ok(env) = std::env::var(EDENSCM_REMOTE_ID_THRESHOLD) {
            env.parse::<usize>().ok()
        } else {
            None
        };
        let remote_name_threshold = if let Ok(env) = std::env::var(EDENSCM_REMOTE_NAME_THRESHOLD) {
            env.parse::<usize>().ok()
        } else {
            None
        };
        let protocol = EdenApiProtocol {
            client: self.client.clone(),
            disabled_names,
            remote_id_threshold,
            remote_id_current: Default::default(),
            remote_name_threshold,
            remote_name_current: Default::default(),
        };
        self.commits.dag.set_remote_protocol(Arc::new(protocol));
        self.lazy_hash_desc = format!("lazy, using EdenAPI");
    }

    /// Enable fetching commit hashes lazily via another "segments".
    /// directory locally. This is for testing purpose.
    pub fn enable_lazy_commit_hashes_from_local_segments(&mut self, dag_path: &Path) -> Result<()> {
        let dag = dag::Dag::open(dag_path)?;
        self.commits.dag.set_remote_protocol(Arc::new(dag));
        self.lazy_hash_desc = format!("lazy, using local segments ({})", dag_path.display());
        Ok(())
    }

    fn to_hybrid_commit_text(&self) -> HybridCommitTextReader {
        HybridCommitTextReader {
            zstore: self.commits.commit_data_store(),
            client: self.client.clone(),
        }
    }
}

#[async_trait::async_trait]
impl AppendCommits for HybridCommits {
    async fn add_commits(&mut self, commits: &[HgCommit]) -> Result<()> {
        if let Some(revlog) = self.revlog.as_mut() {
            revlog.add_commits(commits).await?;
        }
        self.commits.add_commits(commits).await?;
        Ok(())
    }

    async fn flush(&mut self, master_heads: &[Vertex]) -> Result<()> {
        if let Some(revlog) = self.revlog.as_mut() {
            revlog.flush(master_heads).await?;
        }
        self.commits.flush(master_heads).await?;
        Ok(())
    }

    async fn flush_commit_data(&mut self) -> Result<()> {
        if let Some(revlog) = self.revlog.as_mut() {
            revlog.flush_commit_data().await?;
        }
        self.commits.flush_commit_data().await?;
        self.commits.dag.flush_cached_idmap().await?;
        Ok(())
    }

    async fn add_graph_nodes(&mut self, graph_nodes: &[crate::GraphNode]) -> Result<()> {
        if self.revlog.is_some() {
            return Err(crate::Error::Unsupported(
                "add_graph_nodes is not supported for revlog backend",
            ));
        }
        self.commits.add_graph_nodes(graph_nodes).await?;
        Ok(())
    }

    async fn import_clone_data(&mut self, clone_data: CloneData<Vertex>) -> Result<()> {
        if self.revlog.is_some() {
            return Err(crate::Error::Unsupported(
                "import_clone_data is not supported for revlog backend",
            ));
        }
        if self.commits.dag.all().await?.count().await? > 0 {
            return Err(crate::Error::Unsupported(
                "import_clone_data can only be used in an empty repo",
            ));
        }
        if !self.commits.dag.is_vertex_lazy() {
            return Err(crate::Error::Unsupported(
                "import_clone_data can only be used in commit graph with lazy vertexes",
            ));
        }
        self.commits.dag.import_clone_data(clone_data).await?;
        Ok(())
    }

    async fn import_pull_data(
        &mut self,
        clone_data: CloneData<Vertex>,
        heads: &VertexListWithOptions,
    ) -> Result<()> {
        if self.revlog.is_some() {
            return Err(crate::Error::Unsupported(
                "import_pull_data is not supported for revlog backend",
            ));
        }
        if !self.commits.dag.is_vertex_lazy() {
            return Err(crate::Error::Unsupported(
                "import_pull_data can only be used in commit graph with lazy vertexes",
            ));
        }
        self.commits.dag.import_pull_data(clone_data, heads).await?;
        Ok(())
    }
}

/// Subset of HybridCommits useful to read commit text.
#[derive(Clone)]
struct HybridCommitTextReader {
    zstore: Arc<RwLock<Zstore>>,
    client: Arc<dyn EdenApi>,
}

#[async_trait::async_trait]
impl ReadCommitText for HybridCommits {
    async fn get_commit_raw_text_list(&self, vertexes: &[Vertex]) -> Result<Vec<Bytes>> {
        self.to_hybrid_commit_text()
            .get_commit_raw_text_list(vertexes)
            .await
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        Arc::new(self.to_hybrid_commit_text())
    }
}

#[async_trait::async_trait]
impl ReadCommitText for HybridCommitTextReader {
    async fn get_commit_raw_text_list(&self, vertexes: &[Vertex]) -> Result<Vec<Bytes>> {
        let vertexes: Vec<Vertex> = vertexes.to_vec();
        let stream =
            self.stream_commit_raw_text(Box::pin(stream::iter(vertexes.into_iter().map(Ok))))?;
        let commits: Vec<Bytes> = stream.map(|c| c.map(|c| c.raw_text)).try_collect().await?;
        Ok(commits)
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        Arc::new(self.clone())
    }
}

impl StreamCommitText for HybridCommits {
    fn stream_commit_raw_text(
        &self,
        input: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>> {
        self.to_hybrid_commit_text().stream_commit_raw_text(input)
    }
}

impl StreamCommitText for HybridCommitTextReader {
    fn stream_commit_raw_text(
        &self,
        input: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>> {
        let zstore = self.zstore.clone();
        let client = self.client.clone();
        let resolver = Resolver { client, zstore };
        let buffer_size = 10000;
        let retry_limit = 0;
        let stream = HybridStream::new(input, resolver, buffer_size, retry_limit);
        let stream = stream.map_ok(|(vertex, raw_text)| ParentlessHgCommit { vertex, raw_text });
        Ok(Box::pin(stream))
    }
}

#[async_trait::async_trait]
impl StripCommits for HybridCommits {
    async fn strip_commits(&mut self, set: Set) -> Result<()> {
        if let Some(revlog) = self.revlog.as_mut() {
            revlog.strip_commits(set.clone()).await?;
        }
        self.commits.strip_commits(set).await?;
        Ok(())
    }
}

struct Resolver {
    client: Arc<dyn EdenApi>,
    zstore: Arc<RwLock<Zstore>>,
}

impl Drop for Resolver {
    fn drop(&mut self) {
        // Write commit data back to zstore, best effort.
        let _ = self.zstore.write().flush();
    }
}

#[async_trait]
impl HybridResolver<Vertex, Bytes, anyhow::Error> for Resolver {
    fn resolve_local(&mut self, vertex: &Vertex) -> anyhow::Result<Option<Bytes>> {
        let id = Id20::from_slice(vertex.as_ref())?;
        match self.zstore.read().get(id)? {
            Some(bytes) => Ok(Some(bytes.slice(Id20::len() * 2..))),
            None => Ok(crate::revlog::get_hard_coded_commit_text(vertex)),
        }
    }

    #[instrument(level = "debug", skip(self))]
    async fn resolve_remote(
        &self,
        input: &[Vertex],
    ) -> anyhow::Result<BoxStream<'static, anyhow::Result<(Vertex, Bytes)>>> {
        let ids: Vec<Id20> = input
            .iter()
            .map(|i| Id20::from_slice(i.as_ref()))
            .collect::<std::result::Result<Vec<_>, _>>()?;
        let client = self.client.clone();
        let response = client.commit_revlog_data(ids).await?;
        let zstore = self.zstore.clone();
        let commits = response.entries.map(move |e| {
            let e = e?;
            let written_id = zstore.write().insert(&e.revlog_data, &[])?;
            if !written_id.is_null() && written_id != e.hgid {
                anyhow::bail!(
                    "server returned commit-text pair ({}, {:?}) has mismatched SHA1: {}",
                    e.hgid.to_hex(),
                    e.revlog_data,
                    written_id.to_hex(),
                );
            }
            let bytes = &e.revlog_data[Id20::len() * 2..];
            let input_output = (
                Vertex::copy_from(e.hgid.as_ref()),
                Bytes::copy_from_slice(&bytes),
            );
            Ok(input_output)
        });
        Ok(Box::pin(commits) as BoxStream<'_, _>)
    }

    fn retry_error(&self, _attempt: usize, input: &[Vertex]) -> anyhow::Error {
        anyhow::format_err!("cannot resolve {:?} remotely", input)
    }
}

delegate!(CheckIntegrity | IdConvert | IdMapSnapshot | PrefixLookup | DagAlgorithm, HybridCommits => self.commits);

impl DescribeBackend for HybridCommits {
    fn algorithm_backend(&self) -> &'static str {
        "segments"
    }

    fn describe_backend(&self) -> String {
        let (backend, revlog_path, revlog_usage) = match self.revlog.as_ref() {
            Some(revlog) => {
                let path = revlog.dir.join("00changelog.{i,d,nodemap}");
                (
                    "hybrid",
                    path.display().to_string(),
                    "present, not used for reading",
                )
            }
            None => ("lazytext", "(not used)".to_string(), "(not used)"),
        };
        format!(
            r#"Backend ({}):
  Local:
    Segments + IdMap: {}
    Zstore: {}
    Revlog + Nodemap: {}
Feature Providers:
  Commit Graph Algorithms:
    Segments
  Commit Hash / Rev Lookup:
    IdMap
  Commit Data (user, message):
    Zstore (incomplete, draft)
    EdenAPI (remaining, public)
    Revlog {}
Commit Hashes: {}
"#,
            backend,
            self.commits.dag_path.display(),
            self.commits.commits_path.display(),
            revlog_path,
            revlog_usage,
            &self.lazy_hash_desc,
        )
    }

    fn explain_internals(&self, w: &mut dyn io::Write) -> io::Result<()> {
        self.commits.explain_internals(w)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! # hgcommits
//!
//! Commits stored in HG format and backed by efficient `dag` structures.

use std::io;
use std::sync::Arc;

use dag::errors::NotFoundError;
use dag::ops::CheckIntegrity;
use dag::ops::IdConvert;
use dag::ops::IdMapSnapshot;
use dag::ops::PrefixLookup;
use dag::ops::ToIdSet;
use dag::ops::ToSet;
use dag::CloneData;
use dag::DagAlgorithm;
use dag::Vertex;
use dag::VertexListWithOptions;
use futures::future::try_join_all;
use futures::stream::BoxStream;
use metalog::MetaLog;
use minibytes::Bytes;
use serde::Deserialize;
use serde::Serialize;
use storemodel::ReadRootTreeIds;

#[async_trait::async_trait]
pub trait ReadCommitText: Sync {
    /// Read raw text for a commit, in hg commit format.
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        let list = self.get_commit_raw_text_list(&[vertex.clone()]).await?;
        Ok(Some(list.into_iter().next().unwrap()))
    }

    /// Read commit text in batch. Any of the missing commits would cause an error.
    async fn get_commit_raw_text_list(&self, vertexes: &[Vertex]) -> Result<Vec<Bytes>> {
        try_join_all(vertexes.iter().map(|v| async move {
            match self.get_commit_raw_text(v).await {
                Err(e) => Err(e),
                Ok(None) => v.not_found().map_err(|e| e.into()),
                Ok(Some(b)) => Ok(b),
            }
        }))
        .await
    }

    /// Return a trait object to resolve root tree ids from commit ids.
    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync>;

    /// Return a trait object to resolve root tree ids from commit ids.
    fn to_dyn_read_root_tree_ids(&self) -> Arc<dyn ReadRootTreeIds + Send + Sync> {
        let reader = self.to_dyn_read_commit_text();
        let reader = trait_impls::ArcReadCommitText(reader);
        Arc::new(reader)
    }
}

pub trait StreamCommitText {
    /// Get commit raw text in a stream fashion.
    fn stream_commit_raw_text(
        &self,
        stream: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>>;
}

#[async_trait::async_trait]
pub trait AppendCommits: Send + Sync {
    /// Add commits. They stay in-memory until `flush`.
    async fn add_commits(&mut self, commits: &[HgCommit]) -> Result<()>;

    /// Write in-memory changes to disk.
    ///
    /// This function does more things than `flush_commit_data`.
    async fn flush(&mut self, master_heads: &[Vertex]) -> Result<()>;

    /// Write buffered commit data to disk.
    ///
    /// For the revlog backend, this also write the commit graph to disk.
    async fn flush_commit_data(&mut self) -> Result<()>;

    /// Add nodes to the graph without data (commit message).
    /// This is only supported by lazy backends.
    /// Use `flush` to write changes to disk.
    async fn add_graph_nodes(&mut self, graph_nodes: &[GraphNode]) -> Result<()> {
        let _ = graph_nodes;
        Err(crate::Error::Unsupported(
            "add_graph_nodes is not supported by this backend",
        ))
    }

    /// Import clone data and flush.
    /// This is only supported by lazy backends and can only be used in an empty repo.
    async fn import_clone_data(&mut self, clone_data: CloneData<Vertex>) -> Result<()> {
        let _ = clone_data;
        Err(crate::Error::Unsupported(
            "import_clone_data is not supported by this backend",
        ))
    }

    /// Import data from master fast forward pull.
    /// This is only supported by lazy backends. Can be used on non-empty repo.
    async fn import_pull_data(
        &mut self,
        clone_data: CloneData<Vertex>,
        heads: &VertexListWithOptions,
    ) -> Result<()> {
        let _ = (clone_data, heads);
        Err(crate::Error::Unsupported(
            "import_pull_data is not supported by this backend",
        ))
    }

    /// Update references to match metalog.
    ///
    /// This is not needed if metalog is the source of truth.
    /// However, if metalog is synced from git references, then this
    /// method is needed to sync metalog back to git references.
    fn update_references_to_match_metalog(&mut self, metalog: &MetaLog) -> Result<()> {
        let _ = metalog;
        Ok(())
    }
}

pub trait DescribeBackend {
    /// Name of the DagAlgorithm backend.
    fn algorithm_backend(&self) -> &'static str;

    /// Describe what storage backend is being used.
    fn describe_backend(&self) -> String;

    /// Write human-readable internal data to `w`.
    /// For segments backend, this writes segments data.
    fn explain_internals(&self, w: &mut dyn io::Write) -> io::Result<()>;
}

/// A combination of other traits: commit read/write + DAG algorithms.
pub trait DagCommits:
    ReadCommitText
    + StripCommits
    + AppendCommits
    + CheckIntegrity
    + DescribeBackend
    + DagAlgorithm
    + IdConvert
    + IdMapSnapshot
    + PrefixLookup
    + ToIdSet
    + ToSet
{
}

impl DagCommits for HgCommits {}
impl DagCommits for HybridCommits {}
impl DagCommits for MemHgCommits {}
impl DagCommits for RevlogCommits {}
impl DagCommits for DoubleWriteCommits {}
impl DagCommits for GitSegmentedCommits {}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GraphNode {
    pub vertex: Vertex,
    pub parents: Vec<Vertex>,
}

/// Parameter used by `add_commits`.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HgCommit {
    pub vertex: Vertex,
    pub parents: Vec<Vertex>,
    pub raw_text: Bytes,
}

/// Return type used by `stream_commit_raw_text`.
#[derive(Serialize, Deserialize, Debug)]
pub struct ParentlessHgCommit {
    pub vertex: Vertex,
    pub raw_text: Bytes,
}

mod doublewrite;
pub(crate) mod errors;
mod git;
mod hgsha1commits;
mod hybrid;
mod memhgcommits;
mod revlog;
mod strip;
pub mod trait_impls;
mod utils;

pub use doublewrite::DoubleWriteCommits;
pub use errors::CommitError as Error;
pub use git::GitSegmentedCommits;
pub use hgsha1commits::HgCommits;
pub use hybrid::HybridCommits;
pub use memhgcommits::MemHgCommits;
pub use revlog::RevlogCommits;
pub use strip::StripCommits;
pub type Result<T> = std::result::Result<T, Error>;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::collections::HashSet;
use std::io;
use std::sync::Arc;

use dag::delegate;
use dag::errors::NotFoundError;
use dag::ops::DagAddHeads;
use dag::ops::DagStrip;
use dag::MemDag;
use dag::Set;
use dag::Vertex;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use minibytes::Bytes;

use crate::AppendCommits;
use crate::DescribeBackend;
use crate::GraphNode;
use crate::HgCommit;
use crate::ParentlessHgCommit;
use crate::ReadCommitText;
use crate::Result;
use crate::StreamCommitText;
use crate::StripCommits;

/// HG commits in memory.
pub struct MemHgCommits {
    commits: HashMap<Vertex, Bytes>,
    dag: MemDag,
}

impl MemHgCommits {
    pub fn new() -> Result<Self> {
        let result = Self {
            dag: MemDag::new(),
            commits: HashMap::new(),
        };
        Ok(result)
    }
}

#[async_trait::async_trait]
impl AppendCommits for MemHgCommits {
    async fn add_commits(&mut self, commits: &[HgCommit]) -> Result<()> {
        // Write commit data to zstore.
        for commit in commits {
            self.commits
                .insert(commit.vertex.clone(), commit.raw_text.clone());
        }

        // Write commit graph to DAG.
        let graph_nodes = commits
            .iter()
            .map(|c| GraphNode {
                vertex: c.vertex.clone(),
                parents: c.parents.clone(),
            })
            .collect::<Vec<_>>();
        self.add_graph_nodes(&graph_nodes).await?;

        Ok(())
    }

    async fn flush(&mut self, _master_heads: &[Vertex]) -> Result<()> {
        Ok(())
    }

    async fn flush_commit_data(&mut self) -> Result<()> {
        Ok(())
    }

    async fn add_graph_nodes(&mut self, graph_nodes: &[GraphNode]) -> Result<()> {
        // Write commit graph to DAG.
        let parents: HashMap<Vertex, Vec<Vertex>> = graph_nodes
            .iter()
            .cloned()
            .map(|c| (c.vertex, c.parents))
            .collect();
        let heads: Vec<Vertex> = {
            let mut non_heads = HashSet::new();
            for graph_node in graph_nodes {
                for parent in graph_node.parents.iter() {
                    non_heads.insert(parent);
                }
            }
            graph_nodes
                .iter()
                .map(|c| &c.vertex)
                .filter(|v| !non_heads.contains(v))
                .cloned()
                .collect()
        };
        self.dag.add_heads(&parents, &heads.into()).await?;
        Ok(())
    }
}

#[async_trait::async_trait]
impl ReadCommitText for MemHgCommits {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        self.commits.get_commit_raw_text(vertex).await
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        self.commits.to_dyn_read_commit_text()
    }
}

#[async_trait::async_trait]
impl ReadCommitText for HashMap<Vertex, Bytes> {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        Ok(self.get(vertex).cloned())
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        Arc::new(self.clone())
    }
}

impl StreamCommitText for MemHgCommits {
    fn stream_commit_raw_text(
        &self,
        stream: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>> {
        let commits = self.commits.clone();
        let stream = stream.map(move |item| {
            let vertex = item?;
            match commits.get(&vertex) {
                Some(raw_text) => {
                    let raw_text = raw_text.clone();
                    Ok(ParentlessHgCommit { vertex, raw_text })
                }
                None => vertex.not_found().map_err(Into::into),
            }
        });
        Ok(Box::pin(stream))
    }
}

#[async_trait::async_trait]
impl StripCommits for MemHgCommits {
    async fn strip_commits(&mut self, set: Set) -> Result<()> {
        self.dag.strip(&set).await.map_err(Into::into)
    }
}

delegate!(CheckIntegrity | IdConvert | IdMapSnapshot | PrefixLookup | DagAlgorithm, MemHgCommits => self.dag);

impl DescribeBackend for MemHgCommits {
    fn algorithm_backend(&self) -> &'static str {
        "segments"
    }

    fn describe_backend(&self) -> String {
        r#"Backend (memory):
  Local:
    Memory
Feature Providers:
  Commit Graph Algorithms:
    Memory
  Commit Hash / Rev Lookup:
    Memory
  Commit Data (user, message):
    Memory
"#
        .to_string()
    }

    fn explain_internals(&self, w: &mut dyn io::Write) -> io::Result<()> {
        write!(w, "{:?}", &self.dag)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use dag::delegate;
use dag::errors::programming;
use dag::errors::NotFoundError;
use dag::nonblocking::non_blocking_result;
use dag::ops::IdConvert;
use dag::Group;
use dag::Set;
use dag::Vertex;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use minibytes::Bytes;
use revlogindex::RevlogIndex;
use zstore::Id20;

use crate::strip;
use crate::AppendCommits;
use crate::DescribeBackend;
use crate::HgCommit;
use crate::ParentlessHgCommit;
use crate::ReadCommitText;
use crate::Result;
use crate::StreamCommitText;
use crate::StripCommits;

/// HG commits stored on disk using the revlog format.
#[derive(Clone)]
pub struct RevlogCommits {
    revlog: RevlogIndex,
    pub(crate) dir: PathBuf,
}

/// Hardcoded commit hashes defied by hg.
pub(crate) fn get_hard_coded_commit_text(vertex: &Vertex) -> Option<Bytes> {
    let vertex = vertex.as_ref();
    if vertex == Id20::null_id().as_ref() || vertex == Id20::wdir_id().as_ref() {
        Some(Default::default())
    } else {
        None
    }
}

impl RevlogCommits {
    pub fn new(dir: &Path) -> Result<Self> {
        let index_path = dir.join("00changelog.i");
        let nodemap_path = dir.join("00changelog.nodemap");
        let revlog = RevlogIndex::new(&index_path, &nodemap_path)?;
        Ok(Self {
            revlog,
            dir: dir.to_path_buf(),
        })
    }
}

#[async_trait::async_trait]
impl AppendCommits for RevlogCommits {
    async fn add_commits(&mut self, commits: &[HgCommit]) -> Result<()> {
        // Topo sort nodes since EdenAPI returns nodes sorted lexically.
        // We try to keep a stable order relative to the input since revlog
        // insertion order can affect tests.

        let mut vertex_to_commit: HashMap<Vertex, &HgCommit> =
            commits.iter().map(|c| (c.vertex.clone(), c)).collect();
        // Tracks reverse dependency so we can enqueue the child after parent is added.
        let mut parent_to_children: HashMap<Vertex, Vec<&HgCommit>> = HashMap::new();
        // Counter to know when all a child's parents have been added.
        let mut parent_count: HashMap<Vertex, usize> = HashMap::new();
        // Queue of nodes not waiting on parents to be processed.
        let mut queue: Vec<&HgCommit> = Vec::new();

        for c in commits {
            let mut pending_parents = 0;
            for pv in c.parents.iter() {
                if let Some(pc) = vertex_to_commit.get(pv) {
                    parent_to_children
                        .entry(pc.vertex.clone())
                        .or_default()
                        .push(c);
                    pending_parents += 1;
                }
            }
            if pending_parents == 0 {
                // Parents are not present in args - assume we are good to go.
                queue.push(c);
            } else {
                parent_count.insert(c.vertex.clone(), pending_parents);
            }
        }

        while let Some(commit) = queue.pop() {
            let mut parent_revs = Vec::with_capacity(commit.parents.len());
            for parent in &commit.parents {
                parent_revs.push(self.revlog.vertex_id(parent.clone()).await?.0 as u32);
            }
            self.revlog
                .insert(commit.vertex.clone(), parent_revs, commit.raw_text.clone());

            // Remove so we can make sure we processed all the nodes, later.
            vertex_to_commit.remove(&commit.vertex);

            for child in parent_to_children
                .get(&commit.vertex)
                .map(|v| v.as_slice())
                .unwrap_or_default()
            {
                if let Some(parent_count) = parent_count.get_mut(&child.vertex) {
                    *parent_count -= 1;
                    if *parent_count == 0 {
                        // We were this child's last pending parent.
                        queue.push(child);
                    }
                }
            }
        }

        if !vertex_to_commit.is_empty() {
            programming("commits form a cycle when adding to revlog")?;
        }

        Ok(())
    }

    async fn flush(&mut self, _master_heads: &[Vertex]) -> Result<()> {
        self.revlog.flush()?;
        Ok(())
    }

    async fn flush_commit_data(&mut self) -> Result<()> {
        self.revlog.flush()?;
        Ok(())
    }
}

#[async_trait::async_trait]
impl ReadCommitText for RevlogCommits {
    async fn get_commit_raw_text(&self, vertex: &Vertex) -> Result<Option<Bytes>> {
        match self
            .vertex_id_with_max_group(vertex, Group::NON_MASTER)
            .await?
        {
            Some(id) => Ok(Some(self.revlog.raw_data(id.0 as u32)?)),
            None => Ok(get_hard_coded_commit_text(vertex)),
        }
    }

    fn to_dyn_read_commit_text(&self) -> Arc<dyn ReadCommitText + Send + Sync> {
        Arc::new(self.clone())
    }
}

impl StreamCommitText for RevlogCommits {
    fn stream_commit_raw_text(
        &self,
        stream: BoxStream<'static, anyhow::Result<Vertex>>,
    ) -> Result<BoxStream<'static, anyhow::Result<ParentlessHgCommit>>> {
        let revlog = self.revlog.get_snapshot();
        let stream = stream.map(move |item| {
            let vertex = item?;
            // Mercurial hard-coded special-case that does not match SHA1.
            if let Some(raw_text) = get_hard_coded_commit_text(&vertex) {
                return Ok(ParentlessHgCommit { vertex, raw_text });
            }
            match non_blocking_result(revlog.vertex_id_with_max_group(&vertex, Group::NON_MASTER))?
            {
                Some(id) => {
                    let raw_text = revlog.raw_data(id.0 as u32)?;
                    Ok(ParentlessHgCommit { vertex, raw_text })
                }
                None => vertex.not_found().map_err(Into::into),
            }
        });
        Ok(Box::pin(stream))
    }
}

#[async_trait::async_trait]
impl StripCommits for RevlogCommits {
    async fn strip_commits(&mut self, set: Set) -> Result<()> {
        let old_dir = &self.dir;
        let new_dir = old_dir.join("strip");
        let _ = fs::create_dir(&new_dir);
        let mut new = Self::new(&new_dir)?;
        strip::migrate_commits(self, &mut new, set).await?;
        drop(new);
        strip::racy_unsafe_move_files(&new_dir, old_dir)?;
        *self = Self::new(old_dir)?;
        Ok(())
    }
}

delegate!(CheckIntegrity | IdConvert | IdMapSnapshot | PrefixLookup | DagAlgorithm, RevlogCommits => self.revlog);

impl DescribeBackend for RevlogCommits {
    fn algorithm_backend(&self) -> &'static str {
        "revlog"
    }

    fn describe_backend(&self) -> String {
        format!(
            r#"Backend (revlog):
  Local:
    Revlog: {}
    Nodemap: {}
Feature Providers:
  Commit Graph Algorithms:
    Revlog
  Commit Hash / Rev Lookup:
    Nodemap
  Commit Data (user, message):
    Revlog
"#,
            self.dir.join("00changelog.{i,d}").display(),
            self.dir.join("00changelog.nodemap").display(),
        )
    }

    fn explain_internals(&self, w: &mut dyn io::Write) -> io::Result<()> {
        writeln!(w, "(RevlogIndex explain_internals is not yet implemented)")
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fs;
use std::path::Path;

use dag::errors::NotFoundError;
// TODO: Consider migrating to async, or get rid of strip in tests.
use dag::nameset::SyncNameSetQuery;
use dag::DagAlgorithm;
use dag::Set;
use dag::Vertex;

use crate::AppendCommits;
use crate::HgCommit;
use crate::ReadCommitText;
use crate::Result;

#[async_trait::async_trait]
pub trait StripCommits {
    /// Strip commits. This is for legacy tests only that wouldn't be used
    /// much in production. The callsite should take care of locking or
    /// otherwise risk data race and loss.
    async fn strip_commits(&mut self, set: Set) -> Result<()>;
}

/// Enumerate all commits in `orig`, re-insert them to `new` except for `strip_set::`.
/// This has unacceptable time complexity so it can only be used in tests.
pub(crate) async fn migrate_commits(
    orig: &(impl ReadCommitText + DagAlgorithm),
    new: &mut impl AppendCommits,
    strip_set: Set,
) -> Result<()> {
    if std::env::var_os("TESTTMP").is_none() {
        return Err(crate::errors::test_only("strip"));
    }
    let set = orig.all().await? - orig.descendants(strip_set).await?;
    let heads: Vec<Vertex> = orig
        .heads(set.clone())
        .await?
        .iter_rev()?
        .collect::<dag::Result<Vec<_>>>()?;
    let mut commits: Vec<HgCommit> = Vec::with_capacity(set.count()?);
    // This is inefficient - one by one fetching via async.
    // However the strip code paths only exist to support legacy `.t`
    // tests that use real strips. It's not used anywhere in production.
    // So no optimization is done here.
    for vertex in set.iter_rev()? {
        let vertex = vertex?;
        let raw_text = match orig.get_commit_raw_text(&vertex).await? {
            Some(text) => text,
            None => return Err(vertex.not_found_error().into()),
        };
        let parents = orig.parent_names(vertex.clone()).await?;
        let commit = HgCommit {
            vertex,
            parents,
            raw_text,
        };
        commits.push(commit);
    }
    new.add_commits(&commits).await?;
    new.flush(&heads).await?;
    Ok(())
}

/// Move files and directories in `src_dir` to `dst_dir`.
/// Existing files are moved to `old.${epoch}`.
/// Racy. Should be used in non-production setup.
pub(crate) fn racy_unsafe_move_files(src_dir: &Path, dst_dir: &Path) -> Result<()> {
    if std::env::var_os("TESTTMP").is_none() {
        return Err(crate::errors::test_only("racy_unsafe_move_files"));
    }
    let backup_dir = {
        let mut epoch = 0;
        loop {
            let dir = dst_dir.join(format!("old.{}", epoch));
            if dir.exists() {
                epoch += 1;
            } else {
                fs::create_dir(&dir)?;
                break dir;
            }
        }
    };
    for entry in fs::read_dir(src_dir)? {
        let entry = entry?;
        let name = entry.file_name();
        let src_path = src_dir.join(&name);
        let dst_path = dst_dir.join(&name);
        let backup_path = backup_dir.join(&name);
        fs::rename(&dst_path, backup_path)?;
        fs::rename(src_path, &dst_path)?;
    }
    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Implementing traits in other crates.

use std::sync::Arc;

use anyhow::bail;
use async_trait::async_trait;
use dag::Vertex;
use storemodel::ReadRootTreeIds;
use types::HgId;

use crate::ReadCommitText;

// Workaround Rust's orphan rule.
pub struct ArcReadCommitText(pub Arc<dyn ReadCommitText + Send + Sync>);

#[async_trait]
impl ReadRootTreeIds for ArcReadCommitText {
    async fn read_root_tree_ids(&self, commits: Vec<HgId>) -> anyhow::Result<Vec<(HgId, HgId)>> {
        let vertexes: Vec<Vertex> = commits
            .iter()
            .map(|c| Vertex::copy_from(c.as_ref()))
            .collect();
        commits
            .into_iter()
            .zip(self.0.get_commit_raw_text_list(&vertexes).await?)
            .map(|(c, t)| {
                // `t` is an empty string for the null commit, so return the nullid as the tree id.
                if c == *HgId::null_id() {
                    Ok((c, *HgId::null_id()))
                } else {
                    Ok((c, extract_tree_root_id_from_raw_hg_text(t.as_ref())?))
                }
            })
            .collect::<anyhow::Result<Vec<_>>>()
    }
}

fn extract_tree_root_id_from_raw_hg_text(text: &[u8]) -> anyhow::Result<HgId> {
    // The first 40-bytes are hex tree id.
    let hex_tree_id = match text.get(0..HgId::hex_len()) {
        Some(id) => id,
        None => bail!("incomplete hg commit text"),
    };
    let id = HgId::from_hex(hex_tree_id)?;
    Ok(id)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::collections::HashSet;

use dag::ops::DagAddHeads;
use dag::Dag;
use dag::Vertex;

use crate::GraphNode;
use crate::HgCommit;
use crate::Result;

pub(crate) fn commits_to_graph_nodes(commits: &[HgCommit]) -> Vec<GraphNode> {
    commits
        .iter()
        .map(|c| GraphNode {
            vertex: c.vertex.clone(),
            parents: c.parents.clone(),
        })
        .collect::<Vec<_>>()
}

pub(crate) async fn add_graph_nodes_to_dag(dag: &mut Dag, graph_nodes: &[GraphNode]) -> Result<()> {
    // Write commit graph to DAG.
    let parents: HashMap<Vertex, Vec<Vertex>> = graph_nodes
        .iter()
        .cloned()
        .map(|c| (c.vertex, c.parents))
        .collect();
    let heads: Vec<Vertex> = {
        let mut non_heads = HashSet::new();
        for graph_node in graph_nodes {
            for parent in graph_node.parents.iter() {
                non_heads.insert(parent);
            }
        }
        graph_nodes
            .iter()
            .map(|c| &c.vertex)
            .filter(|v| !non_heads.contains(v))
            .cloned()
            .collect()
    };
    dag.add_heads(&parents, &heads.into()).await?;
    Ok(())
}