/*
 * 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::sync::Arc;

use configmodel::Config;
use configmodel::ConfigExt;
use dag::ops::DagAlgorithm;
use dag::ops::DagExportCloneData;
use dag::ops::DagExportPullData;
use dag::ops::PrefixLookup;
use dag::protocol::AncestorPath;
use dag::protocol::RemoteIdConvertProtocol;
use dag::Location;
use dag::Set;
use dag::Vertex;
use dag::VertexName;
use edenapi::configmodel;
use edenapi::types::make_hash_lookup_request;
use edenapi::types::BookmarkEntry;
use edenapi::types::CommitGraphEntry;
use edenapi::types::CommitHashLookupResponse;
use edenapi::types::CommitHashToLocationResponse;
use edenapi::types::CommitKnownResponse;
use edenapi::types::CommitLocationToHashRequest;
use edenapi::types::CommitLocationToHashResponse;
use edenapi::types::CommitMutationsResponse;
use edenapi::types::CommitRevlogData;
use edenapi::types::FileContent;
use edenapi::types::FileEntry;
use edenapi::types::FileResponse;
use edenapi::types::FileSpec;
use edenapi::types::HgId;
use edenapi::types::HistoryEntry;
use edenapi::types::Key;
use edenapi::types::NodeInfo;
use edenapi::types::Parents;
use edenapi::types::RepoPathBuf;
use edenapi::types::TreeAttributes;
use edenapi::types::TreeEntry;
use edenapi::EdenApi;
use edenapi::EdenApiError;
use edenapi::Response;
use edenapi::ResponseMeta;
use edenapi_trait as edenapi;
use futures::stream::BoxStream;
use futures::stream::TryStreamExt;
use futures::StreamExt;
use http::StatusCode;
use http::Version;
use nonblocking::non_blocking_result;
use tracing::debug;
use tracing::trace;

use crate::EagerRepo;

#[async_trait::async_trait]
impl EdenApi for EagerRepo {
    fn url(&self) -> Option<String> {
        Some(format!("eager:{}", self.dir.display()))
    }

    async fn health(&self) -> edenapi::Result<ResponseMeta> {
        Ok(default_response_meta())
    }

    async fn capabilities(&self) -> Result<Vec<String>, EdenApiError> {
        Ok(vec!["segmented-changelog".to_string()])
    }

    async fn files(&self, keys: Vec<Key>) -> edenapi::Result<Response<FileResponse>> {
        debug!("files {}", debug_key_list(&keys));
        let mut values = Vec::with_capacity(keys.len());
        for key in keys {
            let id = key.hgid;
            let data = self.get_sha1_blob_for_api(id, "files")?;
            let (p1, p2) = extract_p1_p2(&data);
            let parents = Parents::new(p1, p2);
            let entry = FileEntry {
                key: key.clone(),
                parents,
                // PERF: to_vec().into() converts minibytes::Bytes to bytes::Bytes.
                content: Some(FileContent {
                    hg_file_blob: extract_body(&data).to_vec().into(),
                    metadata: Default::default(),
                }),
                aux_data: None,
            };
            let response = FileResponse {
                key,
                result: Ok(entry),
            };
            values.push(Ok(response));
        }
        Ok(convert_to_response(values))
    }

    async fn files_attrs(&self, reqs: Vec<FileSpec>) -> edenapi::Result<Response<FileResponse>> {
        debug!("files {}", debug_spec_list(&reqs));
        let mut values = Vec::with_capacity(reqs.len());
        for spec in reqs {
            let key = spec.key;
            let id = key.hgid;
            let data = self.get_sha1_blob_for_api(id, "files_attrs")?;
            let (p1, p2) = extract_p1_p2(&data);
            let parents = Parents::new(p1, p2);
            // TODO(meyer): Actually implement aux data here.
            let entry = FileEntry {
                key: key.clone(),
                parents,
                // PERF: to_vec().into() converts minibytes::Bytes to bytes::Bytes.
                content: Some(FileContent {
                    hg_file_blob: extract_body(&data).to_vec().into(),
                    metadata: Default::default(),
                }),
                aux_data: None,
            };
            let response = FileResponse {
                key,
                result: Ok(entry),
            };
            values.push(Ok(response));
        }
        Ok(convert_to_response(values))
    }

    async fn history(
        &self,
        keys: Vec<Key>,
        _length: Option<u32>,
    ) -> edenapi::Result<Response<HistoryEntry>> {
        debug!("history {}", debug_key_list(&keys));
        let mut values = Vec::new();
        let mut visited: HashSet<Key> = Default::default();
        let mut to_visit: Vec<Key> = keys;
        while let Some(key) = to_visit.pop() {
            if !visited.insert(key.clone()) {
                continue;
            }
            let data = self.get_sha1_blob_for_api(key.hgid, "history")?;
            // NOTE: Order of p1, p2 are not preserved, unlike revlog hg.
            // It should be okay correctness-wise.
            let (p1, p2) = extract_p1_p2(&data);
            let mut key1 = Key {
                path: key.path.clone(),
                hgid: p1,
            };
            let mut key2 = Key {
                path: key.path.clone(),
                hgid: p2,
            };
            if let Some(renamed_from) = extract_rename(extract_body(&data)) {
                if p1.is_null() {
                    key1 = renamed_from;
                } else {
                    key2 = renamed_from;
                }
            }
            if !p1.is_null() {
                to_visit.push(key1.clone());
            }
            if !p2.is_null() {
                to_visit.push(key2.clone());
            }
            let entry = HistoryEntry {
                key,
                nodeinfo: NodeInfo {
                    parents: [key1, key2],
                    linknode: *HgId::null_id(),
                },
            };
            values.push(Ok(entry));
        }
        Ok(convert_to_response(values))
    }

    async fn trees(
        &self,
        keys: Vec<Key>,
        attributes: Option<TreeAttributes>,
    ) -> edenapi::Result<Response<Result<TreeEntry, edenapi::types::EdenApiServerError>>> {
        debug!("trees {}", debug_key_list(&keys));
        let mut values = Vec::new();
        let attributes = attributes.unwrap_or_default();
        if attributes.child_metadata {
            return Err(self.not_implemented_error(
                "EagerRepo does not support child_metadata for trees".to_string(),
                "trees",
            ));
        }
        for key in keys {
            let data = self.get_sha1_blob_for_api(key.hgid, "trees")?;
            let mut entry = TreeEntry::default();
            entry.key = key;
            if attributes.manifest_blob {
                // PERF: to_vec().into() converts minibytes::Bytes to bytes::Bytes.
                entry.data = Some(extract_body(&data).to_vec().into());
            }
            if attributes.parents {
                let (p1, p2) = extract_p1_p2(&data);
                let parents = Parents::new(p1, p2);
                entry.parents = Some(parents);
            }
            assert!(!attributes.child_metadata, "checked above");
            values.push(Ok(Ok(entry)));
        }
        Ok(convert_to_response(values))
    }

    async fn commit_revlog_data(
        &self,
        hgids: Vec<HgId>,
    ) -> edenapi::Result<Response<CommitRevlogData>> {
        debug!("revlog_data {}", debug_hgid_list(&hgids));
        let mut values = Vec::new();
        for id in hgids {
            let data = self.get_sha1_blob_for_api(id, "commit_revlog_data")?;
            let data = CommitRevlogData {
                hgid: id,
                // PERF: to_vec().into() converts minibytes::Bytes to bytes::Bytes.
                revlog_data: data.to_vec().into(),
            };
            values.push(Ok(data));
        }
        Ok(convert_to_response(values))
    }

    async fn clone_data(&self) -> edenapi::Result<dag::CloneData<HgId>> {
        debug!("clone_data");
        let clone_data = self.dag().export_clone_data().await.map_err(map_dag_err)?;
        convert_clone_data(clone_data)
    }

    async fn pull_lazy(
        &self,
        common: Vec<HgId>,
        missing: Vec<HgId>,
    ) -> Result<dag::CloneData<HgId>, EdenApiError> {
        ::fail::fail_point!("eagerepo::api::pulllazy", |_| {
            Err(EdenApiError::NotSupported)
        });

        debug!("pull_lazy");
        let common = to_vec_vertex(&common);
        let missing = to_vec_vertex(&missing);
        let set = self
            .dag()
            .only(
                Set::from_static_names(missing),
                Set::from_static_names(common),
            )
            .await
            .map_err(map_dag_err)?;
        let clone_data = self
            .dag()
            .export_pull_data(&set)
            .await
            .map_err(map_dag_err)?;
        convert_clone_data(clone_data)
    }

    async fn commit_location_to_hash(
        &self,
        requests: Vec<CommitLocationToHashRequest>,
    ) -> edenapi::Result<Vec<CommitLocationToHashResponse>> {
        let path_names: Vec<(AncestorPath, Vec<Vertex>)> = {
            let paths: Vec<AncestorPath> = requests
                .into_iter()
                .map(|r| AncestorPath {
                    x: Vertex::copy_from(r.location.descendant.as_ref()),
                    n: r.location.distance,
                    batch_size: r.count,
                })
                .collect();
            self.dag()
                .resolve_relative_paths_to_names(paths)
                .await
                .map_err(map_dag_err)?
        };

        check_convert_to_hgid(path_names.iter().flat_map(|i| i.1.iter()))?;
        check_convert_to_hgid(path_names.iter().map(|i| &i.0.x))?;

        let values: edenapi::Result<Vec<CommitLocationToHashResponse>> = path_names
            .into_iter()
            .map(|(p, ns)| {
                let count = ns.len();
                let response = CommitLocationToHashResponse {
                    location: Location {
                        descendant: HgId::from_slice(p.x.as_ref()).unwrap(), // unwrap: checked above
                        distance: p.n,
                    },
                    hgids: ns
                        .into_iter()
                        .map(|n| HgId::from_slice(n.as_ref()).unwrap()) // unwrap: checked above
                        .collect(),
                    count: count as _,
                };
                Ok(response)
            })
            .collect();

        values
    }

    async fn commit_hash_to_location(
        &self,
        master_heads: Vec<HgId>,
        hgids: Vec<HgId>,
    ) -> edenapi::Result<Vec<CommitHashToLocationResponse>> {
        let path_names: Vec<(AncestorPath, Vec<Vertex>)> = {
            let heads: Vec<Vertex> = to_vec_vertex(&master_heads);
            let names: Vec<Vertex> = to_vec_vertex(&hgids);
            self.dag()
                .resolve_names_to_relative_paths(heads, names)
                .await
                .map_err(map_dag_err)?
        };

        check_convert_to_hgid(path_names.iter().flat_map(|i| i.1.iter()))?;
        check_convert_to_hgid(path_names.iter().map(|i| &i.0.x))?;

        let values: edenapi::Result<Vec<CommitHashToLocationResponse>> = path_names
            .into_iter()
            .flat_map(|(p, ns)| {
                ns.into_iter()
                    .enumerate()
                    .map(|(i, n)| {
                        CommitHashToLocationResponse {
                            hgid: HgId::from_slice(n.as_ref()).unwrap(), // unwrap: checked above
                            result: Ok(Some(Location {
                                descendant: HgId::from_slice(p.x.as_ref()).unwrap(), // unwrap: checked above
                                distance: p.n + (i as u64),
                            })),
                        }
                    })
                    .collect::<Vec<_>>()
            })
            .map(Ok)
            .collect();

        // For hgids outside the master group, just ignore them.
        // It's okay to return them with result "None" too.

        values
    }

    async fn commit_known(&self, hgids: Vec<HgId>) -> edenapi::Result<Vec<CommitKnownResponse>> {
        debug!("commit_known {}", debug_hgid_list(&hgids));
        let mut values = Vec::new();
        for id in hgids {
            let known = self.get_sha1_blob(id).map_err(map_crate_err)?.is_some();
            let response = CommitKnownResponse {
                hgid: id,
                known: Ok(known),
            };
            values.push(response);
        }
        Ok(values)
    }

    async fn commit_graph(
        &self,
        heads: Vec<HgId>,
        common: Vec<HgId>,
    ) -> Result<Vec<CommitGraphEntry>, EdenApiError> {
        debug!(
            "commit_graph {} {}",
            debug_hgid_list(&heads),
            debug_hgid_list(&common),
        );
        let heads =
            dag::Set::from_static_names(heads.iter().map(|v| Vertex::copy_from(v.as_ref())));
        let common =
            dag::Set::from_static_names(common.iter().map(|v| Vertex::copy_from(v.as_ref())));
        let graph = self.dag().only(heads, common).await.map_err(map_dag_err)?;
        let stream = graph.iter_rev().await.map_err(map_dag_err)?;
        let stream: BoxStream<edenapi::Result<CommitGraphEntry>> = stream
            .then(|s| async move {
                let s = s?;
                let hgid = HgId::from_slice(s.as_ref()).unwrap();
                let parents = self.dag().parent_names(s).await?;
                let parents: Vec<HgId> = parents
                    .into_iter()
                    .map(|v| HgId::from_slice(v.as_ref()).unwrap())
                    .collect();
                let entry = CommitGraphEntry {
                    hgid,
                    parents,
                    is_draft: None,
                };
                Ok(entry)
            })
            .map_err(map_dag_err)
            .boxed();
        let values: edenapi::Result<Vec<CommitGraphEntry>> = stream.try_collect().await;
        values
    }

    async fn bookmarks(&self, bookmarks: Vec<String>) -> edenapi::Result<Vec<BookmarkEntry>> {
        debug!("bookmarks {}", debug_string_list(&bookmarks),);
        let mut values = Vec::new();
        let map = self.get_bookmarks_map().map_err(map_crate_err)?;
        for name in bookmarks {
            let opt_id = map.get(&name).cloned();
            let entry = BookmarkEntry {
                bookmark: name,
                hgid: opt_id,
            };
            values.push(entry);
        }
        Ok(values)
    }

    async fn hash_prefixes_lookup(
        &self,
        prefixes: Vec<String>,
    ) -> Result<Vec<CommitHashLookupResponse>, EdenApiError> {
        prefixes
            .into_iter()
            .map(
                move |prefix| -> Result<CommitHashLookupResponse, EdenApiError> {
                    let req = make_hash_lookup_request(prefix.clone())?;
                    let resp = non_blocking_result(
                        self.dag().vertexes_by_hex_prefix(prefix.as_bytes(), 100),
                    )
                    .map_err(|e| EdenApiError::Other(e.into()));
                    resp.and_then(|vertexes| {
                        Ok(CommitHashLookupResponse {
                            request: req,
                            hgids: vertexes
                                .into_iter()
                                .map(|vertex| {
                                    HgId::from_hex(vertex.to_hex().as_bytes())
                                        .map_err(anyhow::Error::from)
                                })
                                .collect::<Result<Vec<_>, _>>()?,
                        })
                    })
                },
            )
            .collect()
    }

    async fn commit_mutations(
        &self,
        mut commits: Vec<HgId>,
    ) -> Result<Vec<CommitMutationsResponse>, EdenApiError> {
        commits.sort();
        debug!("commit_mutations {}", debug_hgid_list(&commits));
        let _ = (commits,);
        Ok(vec![])
    }
}

impl EagerRepo {
    fn get_sha1_blob_for_api(&self, id: HgId, handler: &str) -> edenapi::Result<minibytes::Bytes> {
        // Emulate the HTTP errors.
        match self.get_sha1_blob(id) {
            Ok(None) => {
                trace!(" not found: {}", id.to_hex());
                Err(EdenApiError::HttpError {
                    status: StatusCode::NOT_FOUND,
                    message: format!("{} cannot be found", id.to_hex()),
                    headers: Default::default(),
                    url: self.url(handler),
                })
            }
            Ok(Some(data)) => {
                trace!(" found: {}, {} bytes", id.to_hex(), data.len());
                Ok(data)
            }
            Err(e) => Err(EdenApiError::HttpError {
                status: StatusCode::INTERNAL_SERVER_ERROR,
                message: format!("{:?}", e),
                headers: Default::default(),
                url: self.url(handler),
            }),
        }
    }

    /// Not implement error.
    fn not_implemented_error(&self, message: String, handler: &str) -> EdenApiError {
        EdenApiError::HttpError {
            status: StatusCode::NOT_IMPLEMENTED,
            message,
            headers: Default::default(),
            url: self.url(handler),
        }
    }

    /// Provide the URL for HTTP error reporting.
    fn url(&self, handler: &str) -> String {
        format!("eager://{}/{}", self.dir.display(), handler)
    }
}

/// Optionally build `EdenApi` from config.
///
/// If the config does not specify eagerepo-based `EdenApi`, return `Ok(None)`.
pub fn edenapi_from_config(config: &dyn Config) -> edenapi::Result<Option<Arc<dyn EdenApi>>> {
    for (section, name) in [("paths", "default"), ("edenapi", "url")] {
        if let Ok(value) = config.get_or_default::<String>(section, name) {
            if let Some(path) = EagerRepo::url_to_dir(&value) {
                let repo = EagerRepo::open(&path, None)
                    .map_err(|e| edenapi::EdenApiError::Other(e.into()))?;
                return Ok(Some(Arc::new(repo)));
            }
        }
    }
    Ok(None)
}

fn default_response_meta() -> ResponseMeta {
    ResponseMeta {
        version: Version::HTTP_11,
        status: StatusCode::OK,
        server: Some("EagerRepo".to_string()),
        ..Default::default()
    }
}

fn extract_body(data_with_p1p2_prefix: &[u8]) -> &[u8] {
    &data_with_p1p2_prefix[HgId::len() * 2..]
}

fn extract_p1_p2(data: &[u8]) -> (HgId, HgId) {
    let p2 = HgId::from_slice(&data[..HgId::len()]).unwrap();
    let p1 = HgId::from_slice(&data[HgId::len()..(HgId::len() * 2)]).unwrap();
    (p1, p2)
}

/// Extract rename metadata from filelog header (if rename exists).
/// data is not prefixed by hashes.
///
/// See `filelog.py:parsemeta`.
fn extract_rename(data: &[u8]) -> Option<Key> {
    if data.starts_with(b"\x01\n") {
        let data = &data[2..];
        if let Some(pos) = data.windows(2).position(|needle| needle == b"\x01\n") {
            let header = String::from_utf8_lossy(&data[..pos]);
            let mut path = None;
            let mut rev = None;
            for line in header.lines() {
                let kv: Vec<&str> = line.split(": ").collect();
                if let [k, v] = &kv[..] {
                    if *k == "copy" {
                        path = RepoPathBuf::from_string(v.to_string()).ok();
                    } else if *k == "copyrev" {
                        rev = HgId::from_hex(v.as_bytes()).ok();
                    }
                }
            }
            if let (Some(path), Some(rev)) = (path, rev) {
                return Some(Key {
                    path: path.into(),
                    hgid: rev,
                });
            }
        }
    }
    None
}

/// Convert `Vec<T>` to `Response<T>`.
fn convert_to_response<T: Send + Sync + 'static>(values: Vec<edenapi::Result<T>>) -> Response<T> {
    Response {
        stats: Box::pin(async { Ok(Default::default()) }),
        entries: Box::pin(futures::stream::iter(values)),
    }
}

fn check_convert_to_hgid<'a>(vertexes: impl Iterator<Item = &'a Vertex>) -> edenapi::Result<()> {
    for v in vertexes {
        let _ = HgId::from_slice(v.as_ref()).map_err(|e| EdenApiError::Other(e.into()))?;
    }
    Ok(())
}

fn to_vec_vertex(ids: &[HgId]) -> Vec<Vertex> {
    ids.iter().map(|i| Vertex::copy_from(i.as_ref())).collect()
}

fn convert_clone_data(
    clone_data: dag::CloneData<VertexName>,
) -> edenapi::Result<dag::CloneData<HgId>> {
    check_convert_to_hgid(clone_data.idmap.values())?;
    let clone_data = dag::CloneData {
        flat_segments: clone_data.flat_segments,
        idmap: clone_data
            .idmap
            .into_iter()
            .map(|(k, v)| (k, HgId::from_slice(v.as_ref()).unwrap())) // unwrap: checked above
            .collect(),
    };
    Ok(clone_data)
}

fn map_dag_err(e: dag::Error) -> EdenApiError {
    EdenApiError::Other(e.into())
}

fn map_crate_err(e: crate::Error) -> EdenApiError {
    EdenApiError::Other(e.into())
}

fn debug_key_list(keys: &[Key]) -> String {
    debug_list(keys, |k| k.hgid.to_hex())
}

fn debug_spec_list(reqs: &[FileSpec]) -> String {
    debug_list(reqs, |s| s.key.hgid.to_hex())
}

fn debug_hgid_list(ids: &[HgId]) -> String {
    debug_list(ids, |i| i.to_hex())
}

fn debug_string_list(s: &[String]) -> String {
    debug_list(s, |s| s.clone())
}

fn debug_list<T>(keys: &[T], func: impl Fn(&T) -> String) -> String {
    let limit = 5;
    let msg = keys
        .iter()
        .take(limit)
        .map(|k| func(k))
        .collect::<Vec<_>>()
        .join(", ");
    if keys.len() > limit {
        format!("{} and {} more", msg, keys.len() - limit)
    } else {
        msg
    }
}


/*
 * 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::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use configloader::config::ConfigSet;
use configmodel::ConfigExt;
use dag::ops::DagAddHeads;
use dag::ops::DagPersistent;
use dag::Dag;
use dag::Group;
use dag::Vertex;
use dag::VertexListWithOptions;
use manifest_tree::FileType;
use manifest_tree::Flag;
use manifest_tree::TreeEntry;
use metalog::CommitOptions;
use metalog::MetaLog;
use minibytes::Bytes;
use parking_lot::RwLock;
use storemodel::TreeFormat;
use zstore::Id20;
use zstore::Zstore;

use crate::Result;

/// Non-lazy, pure Rust, local repo implementation.
///
/// Mainly useful as a simple "server repo" in tests that can replace ssh remote
/// repos and exercise EdenApi features.
///
/// Format-wise, an eager repo includes:
///
/// ## SHA1 Key/Value Content Store
///
/// See [`EagerRepoStore`].
///
/// ## Commit Graph
///
/// Commit hashes and parent commit hashes.
///
/// Currently backed by the [`dag::Dag`]. It handles the main complexity.
///
///
/// ## Metadata
///
/// Bookmarks, tip, remote bookmarks, visible heads, etc.
///
/// Format is made compatible with the Python code. Only bookmarks is
/// implemented for now to support testing use-cases.
///
/// Currently backed by [`metalog::MetaLog`]. It's a lightweight source control
/// for atomic metadata changes.
pub struct EagerRepo {
    pub(crate) dag: Dag,
    store: EagerRepoStore,
    metalog: MetaLog,
    pub(crate) dir: PathBuf,
}

/// Storage used by `EagerRepo`. Wrapped by `Arc<RwLock>` for easier sharing.
///
/// File, tree, commit contents.
///
/// SHA1 is verifiable. For HG this means `sorted([p1, p2])` and filelog rename
/// metadata is included in values.
///
/// This is meant to be mainly a content store. We currently "abuse" it to
/// answer filelog history. The filelog (filenode) and linknodes are
/// considered tech-debt and we hope to replace them with fastlog APIs which
/// serve sub-graph with `(commit, path)` as graph nodes.
///
/// We don't use `(p1, p2)` for commit parents because it loses the parent
/// order. The DAG storage is used to answer commit parents instead.
///
/// Currently backed by [`zstore::Zstore`]. For simplicity, we don't use the
/// zstore delta-compress features, and don't store different types separately.
#[derive(Clone)]
pub struct EagerRepoStore {
    pub(crate) inner: Arc<RwLock<Zstore>>,
}

impl EagerRepoStore {
    /// Open an [`EagerRepoStore`] at the given directory.
    /// Create an empty store on demand.
    pub fn open(dir: &Path) -> Result<Self> {
        let inner = Zstore::open(dir)?;
        Ok(Self {
            inner: Arc::new(RwLock::new(inner)),
        })
    }

    /// Flush changes to disk.
    pub fn flush(&self) -> Result<()> {
        let mut inner = self.inner.write();
        inner.flush()?;
        Ok(())
    }

    /// Insert SHA1 blob to zstore.
    /// In hg's case, the `data` is `min(p1, p2) + max(p1, p2) + text`.
    pub fn add_sha1_blob(&self, data: &[u8], bases: &[Id20]) -> Result<Id20> {
        let mut inner = self.inner.write();
        Ok(inner.insert(data, bases)?)
    }

    /// Read SHA1 blob from zstore.
    pub fn get_sha1_blob(&self, id: Id20) -> Result<Option<Bytes>> {
        let inner = self.inner.read();
        Ok(inner.get(id)?)
    }

    /// Read the blob with its p1, p2 prefix removed.
    pub fn get_content(&self, id: Id20) -> Result<Option<Bytes>> {
        // Prefix in bytes of the hg SHA1s in the eagerepo data.
        const HG_SHA1_PREFIX: usize = Id20::len() * 2;
        // Special null case.
        if id.is_null() {
            return Ok(Some(Bytes::default()));
        }
        match self.get_sha1_blob(id)? {
            None => Ok(None),
            Some(data) => Ok(Some(data.slice(HG_SHA1_PREFIX..))),
        }
    }

    /// Check files and trees referenced by the `id` are present.
    /// Missing paths are pushed to `missing`.
    fn find_missing_references<'a>(
        &self,
        id: Id20,
        flag: Flag,
        path: PathInfo,
        missing: &mut Vec<PathInfo>,
    ) -> Result<()> {
        // Cannot check submodule reference.
        if matches!(flag, Flag::File(FileType::GitSubmodule)) {
            return Ok(());
        }
        // Check file or tree reference.
        let content = match self.get_content(id)? {
            Some(content) => content,
            None => {
                missing.push(path);
                return Ok(());
            }
        };
        // Check subfiles or subtrees.
        if matches!(flag, Flag::Directory) {
            let entry = TreeEntry(content, TreeFormat::Hg);
            for element in entry.elements() {
                let element = element?;
                let name = element.component.into_string();
                let path = path.join(name);
                self.find_missing_references(element.hgid, element.flag, path, missing)?;
            }
        }
        Ok(())
    }
}

/// Used by `check_tree`, `check_file` to report missing path.
#[derive(Clone)]
struct PathInfo(Arc<PathInfoInner>);
struct PathInfoInner {
    name: String,
    parent: Option<PathInfo>,
}

impl fmt::Display for PathInfo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let inner = &self.0;
        if let Some(parent) = inner.parent.as_ref() {
            parent.fmt(f)?;
        }
        if !inner.name.is_empty() {
            write!(f, "/{}", inner.name)?;
        }
        Ok(())
    }
}

impl PathInfo {
    fn root() -> Self {
        let inner = PathInfoInner {
            name: String::new(),
            parent: None,
        };
        Self(Arc::new(inner))
    }

    fn join(&self, name: String) -> Self {
        let inner = PathInfoInner {
            name,
            parent: Some(self.clone()),
        };
        Self(Arc::new(inner))
    }
}

impl EagerRepo {
    /// Open an [`EagerRepo`] at the given directory. Create an empty repo on demand.
    pub fn open(dir: &Path, config: Option<&ConfigSet>) -> Result<Self> {
        let ident = identity::sniff_dir(dir)?.unwrap_or_else(identity::default);
        // Attempt to match directory layout of a real client repo.
        let hg_dir = dir.join(ident.dot_dir());
        let store_dir = hg_dir.join("store");
        let dag = Dag::open(store_dir.join("segments").join("v1"))?;
        let store = EagerRepoStore::open(&store_dir.join("hgcommits").join("v1"))?;
        let metalog = MetaLog::open(store_dir.join("metalog"), None)?;
        // Write "requires" files.
        let windows_symlinks = config.map_or(false, |config| {
            config
                .get_or_default("experimental", "windows-symlinks")
                .unwrap_or_default()
        });
        write_requires(
            &hg_dir,
            if windows_symlinks {
                &["store", "treestate", "windowssymlinks"]
            } else {
                &["store", "treestate"]
            },
        )?;
        write_requires(
            &store_dir,
            &[
                "narrowheads",
                "visibleheads",
                "segmentedchangelog",
                "eagerepo",
                "invalidatelinkrev",
            ],
        )?;
        let repo = Self {
            dag,
            store,
            metalog,
            dir: dir.to_path_buf(),
        };
        Ok(repo)
    }

    /// Convert an URL to a directory path that can be passed to `open`.
    ///
    /// Supported URLs:
    /// - `eager:dir_path`, `eager://dir_path`
    /// - `test:name`, `test://name`: same as `eager:$TESTTMP/server-repos/name`
    /// - `/path/to/dir` where the path is a EagerRepo.
    pub fn url_to_dir(value: &str) -> Option<PathBuf> {
        let prefix = "eager:";
        if let Some(path) = value.strip_prefix(prefix) {
            let path: PathBuf = if cfg!(windows) {
                // Remove '//' prefix from Windows file path. This makes it
                // possible to use paths like 'eager://C:\foo\bar'.
                let path = path.trim_start_matches('/');
                // Replace '/' with '\' on Windows so one can write code like
                // eager://$TESTTMP/foo/bar in test. This is important if
                // $TESTTMP is a UNC path, since / won't work with a UNC path.
                let path = path.replace('/', "\\");
                Path::new(&path).to_path_buf()
            } else {
                Path::new(path).to_path_buf()
            };
            return Some(path);
        }
        let prefix = "test:";
        if let Some(path) = value.strip_prefix(prefix) {
            let path = path.trim_start_matches('/');
            if let Ok(tmp) = std::env::var("TESTTMP") {
                let tmp: &Path = Path::new(&tmp);
                let path = tmp.join(path);
                return Some(path);
            }
        }
        let path = Path::new(value);
        if path.is_absolute() && path.is_dir() {
            if let Ok(Some(ident)) = identity::sniff_dir(path) {
                // Check store requirements
                let store_requirement_path =
                    path.join(ident.dot_dir()).join("store").join("requires");
                if let Ok(s) = std::fs::read_to_string(store_requirement_path) {
                    if s.lines().any(|s| s == "eagerepo") {
                        return Some(path.to_path_buf());
                    }
                }
            }
        }
        None
    }

    /// Write pending changes to disk.
    pub async fn flush(&mut self) -> Result<()> {
        self.store.flush()?;
        let master_heads = {
            let books = self.get_bookmarks_map()?;
            let mut heads = Vec::new();
            for name in ["master", "main"] {
                if let Some(id) = books.get(name) {
                    heads.push(Vertex::copy_from(id.as_ref()));
                    break;
                }
            }
            VertexListWithOptions::from(heads).with_highest_group(Group::MASTER)
        };
        self.dag.flush(&master_heads).await?;
        let opts = CommitOptions::default();
        self.metalog.commit(opts)?;
        Ok(())
    }

    // The following APIs provide low-level ways to read or write the repo.
    //
    // They are used for push before EdenApi provides push related APIs.

    /// Insert SHA1 blob to zstore.
    /// In hg's case, the `data` is `min(p1, p2) + max(p1, p2) + text`.
    pub fn add_sha1_blob(&mut self, data: &[u8]) -> Result<Id20> {
        // SPACE: This does not utilize zstore's delta features to save space.
        self.store.add_sha1_blob(data, &[])
    }

    /// Read SHA1 blob from zstore.
    pub fn get_sha1_blob(&self, id: Id20) -> Result<Option<Bytes>> {
        self.store.get_sha1_blob(id)
    }

    /// Insert a commit. Return the commit hash.
    pub async fn add_commit(&mut self, parents: &[Id20], raw_text: &[u8]) -> Result<Id20> {
        let parents: Vec<Vertex> = parents
            .iter()
            .map(|v| Vertex::copy_from(v.as_ref()))
            .collect();
        let id: Id20 = {
            let data = hg_sha1_text(&parents, raw_text);
            self.add_sha1_blob(&data)?
        };
        let vertex: Vertex = { Vertex::copy_from(id.as_ref()) };

        // Check paths referred by the commit are present.
        //
        // PERF: This is sub-optimal for large working copies.
        // Ideally we check per tree insertion and only checks
        // the root tree without recursion. But that requires
        // new APIs to insert trees, and insert trees in a
        // certain order.
        if let Some(hex_tree_id) = raw_text.get(0..Id20::hex_len()) {
            if let Ok(tree_id) = Id20::from_hex(hex_tree_id) {
                let mut missing = Vec::new();
                let path = PathInfo::root();
                self.store
                    .find_missing_references(tree_id, Flag::Directory, path, &mut missing)?;
                if !missing.is_empty() {
                    let paths = missing.into_iter().map(|p| p.to_string()).collect();
                    return Err(crate::Error::CommitMissingPaths(
                        vertex,
                        Vertex::copy_from(tree_id.as_ref()),
                        paths,
                    ));
                }
            }
        }

        let parent_map: HashMap<Vertex, Vec<Vertex>> =
            vec![(vertex.clone(), parents)].into_iter().collect();
        self.dag
            .add_heads(&parent_map, &vec![vertex].into())
            .await?;
        Ok(id)
    }

    /// Update or remove a single bookmark.
    pub fn set_bookmark(&mut self, name: &str, id: Option<Id20>) -> Result<()> {
        let mut bookmarks = self.get_bookmarks_map()?;
        match id {
            None => bookmarks.remove(name),
            Some(id) => bookmarks.insert(name.to_string(), id),
        };
        self.set_bookmarks_map(bookmarks)?;
        Ok(())
    }

    /// Get bookmarks.
    pub fn get_bookmarks_map(&self) -> Result<BTreeMap<String, Id20>> {
        // Attempt to match the format used by a real client repo.
        let text: String = {
            let data = self.metalog.get("bookmarks")?;
            let opt_text = data.map(|b| String::from_utf8_lossy(&b).to_string());
            opt_text.unwrap_or_default()
        };
        let map = text
            .lines()
            .filter_map(|line| {
                // example line: d59acbf094f61c10b72dff3d0e6085b5c75d14f4 foo
                let words: Vec<&str> = line.split_whitespace().collect();
                if words.len() == 2 {
                    if let Ok(id) = Id20::from_hex(words[0].as_bytes()) {
                        return Some((words[1].to_string(), id));
                    }
                }
                None
            })
            .collect();
        Ok(map)
    }

    /// Set bookmarks.
    pub fn set_bookmarks_map(&mut self, map: BTreeMap<String, Id20>) -> Result<()> {
        for (name, id) in map.iter() {
            if self.store.get_content(*id)?.is_none() {
                return Err(crate::Error::BookmarkMissingCommit(
                    name.to_string(),
                    Vertex::copy_from(id.as_ref()),
                ));
            }
        }
        let text = map
            .into_iter()
            .map(|(name, id)| format!("{} {}\n", id.to_hex(), name))
            .collect::<Vec<_>>()
            .concat();
        self.metalog.set("bookmarks", text.as_bytes())?;
        Ok(())
    }

    /// Obtain a reference to the commit graph.
    pub fn dag(&self) -> &Dag {
        &self.dag
    }

    /// Obtain a reference to the metalog.
    pub fn metalog(&self) -> &MetaLog {
        &self.metalog
    }

    /// Obtain an instance to the store.
    pub fn store(&self) -> EagerRepoStore {
        self.store.clone()
    }
}

/// Convert parents and raw_text to HG SHA1 text format.
fn hg_sha1_text(parents: &[Vertex], raw_text: &[u8]) -> Vec<u8> {
    fn null_id() -> Vertex {
        Vertex::copy_from(Id20::null_id().as_ref())
    }
    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.extend_from_slice(p1.as_ref());
        result.extend_from_slice(p2.as_ref());
    } else {
        result.extend_from_slice(p2.as_ref());
        result.extend_from_slice(p1.as_ref());
    }
    result.extend_from_slice(&raw_text);
    result
}

/// Write "requires" in the given directory, if it does not exist already.
/// If "requires" exists and does not match the given content, raise an error.
fn write_requires(dir: &Path, requires: &[&'static str]) -> Result<()> {
    let path = dir.join("requires");
    match fs::OpenOptions::new()
        .create_new(true)
        .write(true)
        .open(&path)
    {
        Ok(mut f) => {
            let mut requires: String = requires.join("\n");
            requires.push('\n');
            f.write_all(requires.as_bytes())?;
            Ok(())
        }
        Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
            let actual: BTreeSet<String> = fs::read_to_string(&path)?
                .lines()
                .map(|l| l.to_string())
                .collect();
            let expected: BTreeSet<String> = requires.iter().map(|r| r.to_string()).collect();
            let unsupported: Vec<String> = actual.difference(&expected).cloned().collect();
            let missing: Vec<String> = expected.difference(&actual).cloned().collect();
            if unsupported.is_empty() && missing.is_empty() {
                Ok(())
            } else {
                Err(crate::Error::RequirementsMismatch(
                    path.display().to_string(),
                    unsupported,
                    missing,
                ))
            }
        }
        Err(e) => Err(e.into()),
    }
}

#[cfg(test)]
mod tests {
    use manifest_tree::PathComponentBuf;
    use manifest_tree::TreeElement;

    use super::*;

    #[tokio::test]
    async fn test_read_write_blob() {
        let dir = tempfile::tempdir().unwrap();
        let dir = dir.path();

        let mut repo = EagerRepo::open(dir, None).unwrap();
        let text = &b"blob-text-foo-bar"[..];
        let id = repo.add_sha1_blob(text).unwrap();
        assert_eq!(repo.get_sha1_blob(id).unwrap().as_deref(), Some(text));

        // Pending changes are invisible until flush.
        let repo2 = EagerRepo::open(dir, None).unwrap();
        assert!(repo2.get_sha1_blob(id).unwrap().is_none());

        repo.flush().await.unwrap();

        let repo2 = EagerRepo::open(dir, None).unwrap();
        assert_eq!(repo2.get_sha1_blob(id).unwrap().as_deref(), Some(text));
    }

    #[tokio::test]
    async fn test_add_commit() {
        let dir = tempfile::tempdir().unwrap();
        let dir = dir.path();

        let mut repo = EagerRepo::open(dir, None).unwrap();
        let commit1 = repo.add_commit(&[], b"A").await.unwrap();
        let commit2 = repo.add_commit(&[], b"B").await.unwrap();
        let _commit3 = repo.add_commit(&[commit1, commit2], b"C").await.unwrap();
        repo.flush().await.unwrap();

        let repo2 = EagerRepo::open(dir, None).unwrap();
        let rendered = dag::render::render_namedag(repo2.dag(), |v| {
            let id = Id20::from_slice(v.as_ref()).unwrap();
            let blob = repo2.get_sha1_blob(id).unwrap().unwrap();
            Some(String::from_utf8_lossy(&blob[Id20::len() * 2..]).to_string())
        })
        .unwrap();
        assert_eq!(
            rendered,
            r#"
            o    53cceda7b244d25793af31655d682c7fe67d7650 C
            â”œâ”€â•®
            â”‚ o  35e7525ce3a48913275d7061dd9a867ffef1e34d B
            â”‚
            o  005d992c5dcf32993668f7cede29d296c494a5d9 A"#
        );
    }

    #[tokio::test]
    async fn test_read_write_bookmarks() {
        let dir = tempfile::tempdir().unwrap();
        let dir = dir.path();

        let mut repo = EagerRepo::open(dir, None).unwrap();
        let commit1 = repo.add_commit(&[], b"A").await.unwrap();
        let commit2 = repo.add_commit(&[], b"B").await.unwrap();
        repo.set_bookmark("c1", Some(commit1)).unwrap();
        repo.set_bookmark("stable", Some(commit1)).unwrap();
        repo.set_bookmark("main", Some(commit2)).unwrap();
        repo.flush().await.unwrap();

        let mut repo = EagerRepo::open(dir, None).unwrap();
        assert_eq!(
            format!("{:#?}", repo.get_bookmarks_map().unwrap()),
            r#"{
    "c1": HgId("005d992c5dcf32993668f7cede29d296c494a5d9"),
    "main": HgId("35e7525ce3a48913275d7061dd9a867ffef1e34d"),
    "stable": HgId("005d992c5dcf32993668f7cede29d296c494a5d9"),
}"#
        );
        repo.set_bookmark("c1", None).unwrap();
        repo.set_bookmark("stable", Some(commit2)).unwrap();
        assert_eq!(
            format!("{:#?}", repo.get_bookmarks_map().unwrap()),
            r#"{
    "main": HgId("35e7525ce3a48913275d7061dd9a867ffef1e34d"),
    "stable": HgId("35e7525ce3a48913275d7061dd9a867ffef1e34d"),
}"#
        );
    }

    #[test]
    fn test_requires_mismatch() {
        let dir = tempfile::tempdir().unwrap();
        let dir = dir.path();

        let repo = EagerRepo::open(dir, None).unwrap();
        drop(repo);

        let ident = identity::sniff_dir(dir)
            .unwrap()
            .unwrap_or_else(identity::default);
        fs::write(
            dir.join(ident.dot_dir()).join("requires"),
            "store\nremotefilelog\n",
        )
        .unwrap();

        let err = EagerRepo::open(dir, None).map(|_| ()).unwrap_err();
        match err {
            crate::Error::RequirementsMismatch(_, unsupported, missing) => {
                assert_eq!(unsupported, ["remotefilelog"]);
                assert_eq!(missing, ["treestate"]);
            }
            _ => panic!("expect RequirementsMismatch, got {:?}", err),
        }
    }

    #[tokio::test]
    async fn test_add_commit_find_missing_referencess() {
        let dir = tempfile::tempdir().unwrap();
        let dir = dir.path();

        let mut repo = EagerRepo::open(dir, None).unwrap();
        let missing_id = missing_id();

        // Root tree missing.
        let err = repo
            .add_commit(&[], missing_id.to_hex().as_bytes())
            .await
            .unwrap_err();
        assert_eq!(
            err.to_string(),
            "when adding commit e9644aa7950f61cfe12d510c623692698fee0e4c with root tree 35e7525ce3a48913275d7061dd9a867ffef1e34d, referenced paths [\"\"] are not present"
        );

        // Subdir or subfile missing.
        let p =
            |s: &str| -> PathComponentBuf { PathComponentBuf::from_string(s.to_string()).unwrap() };
        let subtree_content = TreeEntry::from_elements(
            vec![
                TreeElement::new(p("a"), missing_id, Flag::Directory),
                TreeElement::new(p("b"), missing_id, Flag::File(FileType::Regular)),
            ],
            TreeFormat::Hg,
        )
        .to_bytes();
        let subtree_id = repo
            .add_sha1_blob(&hg_sha1_text(&[], &subtree_content))
            .unwrap();
        let root_tree_content = TreeEntry::from_elements(
            vec![
                TreeElement::new(p("c"), subtree_id, Flag::Directory),
                TreeElement::new(p("d"), missing_id, Flag::File(FileType::Regular)),
            ],
            TreeFormat::Hg,
        )
        .to_bytes();
        let root_tree_id = repo
            .add_sha1_blob(&hg_sha1_text(&[], &root_tree_content))
            .unwrap();
        let err = repo
            .add_commit(&[], root_tree_id.to_hex().as_bytes())
            .await
            .unwrap_err();
        assert_eq!(
            err.to_string(),
            "when adding commit 6870320ce60748a99108dd1be33b52b58b277baa with root tree 5a725b18a26fd10416fd93c5bd26fa0265ac2579, referenced paths [\"/c/a\", \"/c/b\", \"/d\"] are not present"
        );
    }

    #[test]
    fn test_set_bookmark_missing_commit() {
        let dir = tempfile::tempdir().unwrap();
        let dir = dir.path();

        let mut repo = EagerRepo::open(dir, None).unwrap();
        let missing_id = missing_id();

        let err = repo.set_bookmark("a", Some(missing_id)).unwrap_err();
        assert_eq!(
            err.to_string(),
            "when moving bookmark \"a\" to 35e7525ce3a48913275d7061dd9a867ffef1e34d, the commit does not exist"
        );
    }

    #[test]
    fn test_open_config() {
        let dir = tempfile::tempdir().unwrap();
        let dir = dir.path();
        let mut config = ConfigSet::new();
        let options = configloader::config::Options::new();

        config.set("experimental", "windows-symlinks", Some("true"), &options);

        EagerRepo::open(dir, Some(&config)).unwrap();

        assert_eq!(
            fs::read_to_string(dir.join(identity::default().dot_dir()).join("requires")).unwrap(),
            r#"store
treestate
windowssymlinks
"#
        );
    }

    fn missing_id() -> Id20 {
        Id20::from_hex(b"35e7525ce3a48913275d7061dd9a867ffef1e34d").unwrap()
    }
}


/*
 * 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 Error {
    #[error(transparent)]
    Dag(#[from] dag::Error),

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

    #[error(
        "EagerRepo detected unsupported requires at {0}:\n  Unsupported: {1:?}\n  Missing: {2:?}\n(is there a non-EagerRepo created accidentally at the same location?)"
    )]
    RequirementsMismatch(String, Vec<String>, Vec<String>),

    #[error(
        "when adding commit {0:?} with root tree {1:?}, referenced paths {2:?} are not present"
    )]
    CommitMissingPaths(Vertex, Vertex, Vec<String>),

    #[error("when moving bookmark {0:?} to {1:?}, the commit does not exist")]
    BookmarkMissingCommit(String, Vertex),
}

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

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

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

impl From<metalog::Error> for Error {
    fn from(err: metalog::Error) -> Self {
        anyhow::Error::from(err).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.
 */

//! Provides a non-lazy repo that serves as a "server" repo in tests.
//!
//! Main goals:
//! - Pure Rust. No Python dependencies.
//! - Serve as EdenApi without going through real networking stack.
//! - Replace SSH reps in tests, which is slow and unreliable on Windows.
//!
//! Although it's currently intended to be a test server repo. It is
//! in theory not too difficult to make it useful as a kind of small
//! client repo.
//!
//! [`EagerRepo`] is the main struct.
//!
//! The word "eager" comes from "eager evaluation", the opposite of
//! "lazy evaluation".

mod api;
mod eager_repo;
mod errors;
mod trait_impls;

pub use api::edenapi_from_config;
pub use eager_repo::EagerRepo;
pub use eager_repo::EagerRepoStore;
pub use errors::Error;
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.
 */

//! Implement traits from other crates.

use async_trait::async_trait;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use hgstore::separate_metadata;
use hgstore::strip_metadata;
use storemodel::types;
use storemodel::ReadFileContents;
use storemodel::RefreshableReadFileContents;
use storemodel::RefreshableTreeStore;
use storemodel::TreeFormat;
use storemodel::TreeStore;
use types::HgId;
use types::Key;
use types::RepoPath;

use crate::EagerRepoStore;

// storemodel traits

#[async_trait]
impl ReadFileContents for EagerRepoStore {
    type Error = anyhow::Error;

    async fn read_file_contents(
        &self,
        keys: Vec<Key>,
    ) -> BoxStream<Result<(minibytes::Bytes, Key), Self::Error>> {
        let iter = keys.into_iter().map(|k| {
            let id = k.hgid;
            let data = match self.get_content(id)? {
                Some(data) => separate_metadata(&data)?.0,
                None => anyhow::bail!("no such file: {:?}", &k),
            };
            Ok((data, k))
        });
        futures::stream::iter(iter).boxed()
    }

    async fn read_rename_metadata(
        &self,
        keys: Vec<Key>,
    ) -> BoxStream<Result<(Key, Option<Key>), Self::Error>> {
        let iter = keys.into_iter().map(|k| {
            let id = k.hgid;
            let copy_from = match self.get_content(id)? {
                Some(data) => strip_metadata(&data)?.1,
                None => anyhow::bail!("no such file: {:?}", &k),
            };
            Ok((k, copy_from))
        });
        futures::stream::iter(iter).boxed()
    }
}

impl TreeStore for EagerRepoStore {
    fn get(&self, path: &RepoPath, hgid: HgId) -> anyhow::Result<minibytes::Bytes> {
        let data = match self.get_content(hgid)? {
            Some(data) => data,
            None => anyhow::bail!("no such tree: {:?} {:?}", path, hgid),
        };
        Ok(data)
    }

    fn insert(&self, _path: &RepoPath, _hgid: HgId, _data: minibytes::Bytes) -> anyhow::Result<()> {
        anyhow::bail!("insert cannot be used for Hg trees");
    }

    fn format(&self) -> TreeFormat {
        TreeFormat::Hg
    }
}

impl RefreshableReadFileContents for EagerRepoStore {
    fn refresh(&self) -> Result<(), Self::Error> {
        let mut inner = self.inner.write();
        inner.flush()?;
        Ok(())
    }
}

impl RefreshableTreeStore for EagerRepoStore {
    fn refresh(&self) -> anyhow::Result<()> {
        let mut inner = self.inner.write();
        inner.flush()?;
        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.
 */

use std::collections::HashMap;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use cloned::cloned;
use crossbeam::channel;
use edenapi::api::EdenApi;
use edenapi_types::AnyFileContentId;
use edenapi_types::AnyId;
use edenapi_types::FileType;
use edenapi_types::UploadToken;
use futures::stream;
use futures::StreamExt;
use futures::TryStreamExt;
use minibytes::Bytes;
use types::RepoPathBuf;
use vfs::AsyncVfsWriter;
use vfs::UpdateFlag;
use vfs::VFS;

use crate::calc_contentid;

fn abs_path(root: &RepoPathBuf, rel_path: &RepoPathBuf) -> RepoPathBuf {
    let mut abs_path = root.clone();
    abs_path.push(rel_path);
    abs_path
}

struct OnDiskOptimizationResponse {
    /// Paths which don't have the correct content.
    incorrect_paths: Vec<(RepoPathBuf, FileType)>,
    /// Paths that have the correct content, but maybe not the correct permissions.
    correct_paths: Vec<(RepoPathBuf, FileType)>,
    /// If the content was found, it is returned.
    content: Option<Bytes>,
}

/// If the desired file is already on disk, usually, from a previous snapshot restore,
/// we can just read it from disk and filter the paths based on which are still outdated.
async fn on_disk_optimization(
    root: &RepoPathBuf,
    paths: Vec<(RepoPathBuf, FileType)>,
    token: &UploadToken,
    // Try not to fail if possible
    conservative: bool,
) -> Result<OnDiskOptimizationResponse> {
    let desired_cid = match token.data.id {
        AnyId::AnyFileContentId(AnyFileContentId::ContentId(cid)) => cid,
        _ => {
            if conservative {
                return Ok(OnDiskOptimizationResponse {
                    incorrect_paths: paths,
                    correct_paths: vec![],
                    content: None,
                });
            } else {
                bail!("Token not in ContentId format")
            }
        }
    };
    let (send_content, recv_content) = channel::unbounded();
    let (send_correct_paths, recv_correct_paths) = channel::unbounded();
    let incorrect_paths = stream::iter(paths)
        .map(Result::<_>::Ok)
        .try_filter_map(|(rel_path, file_type)| {
            let abs_path = abs_path(root, &rel_path);
            cloned!(send_content, send_correct_paths);
            async move {
                let future = {
                    cloned!(rel_path);
                    async move {
                        let abs_path = abs_path.as_repo_path().as_str();
                        let is_symlink = tokio::fs::symlink_metadata(abs_path)
                            .await?
                            .file_type()
                            .is_symlink();
                        let bytes = match file_type {
                            FileType::Executable | FileType::Regular => {
                                if is_symlink {
                                    bail!("File '{}' is a symlink", rel_path)
                                }
                                Bytes::from_owner(tokio::fs::read(abs_path).await?)
                            }
                            FileType::Symlink => {
                                if !is_symlink {
                                    bail!("File '{}' is not a symlink", rel_path)
                                }
                                let link = tokio::fs::read_link(abs_path).await?;
                                let to = link.to_str().context("invalid path")?.as_bytes();
                                Bytes::copy_from_slice(to)
                            }
                        };
                        let content_id = calc_contentid(&bytes);
                        if content_id == desired_cid {
                            if send_content.is_empty() {
                                let _ = send_content.send(bytes);
                            }
                            let _ = send_correct_paths.send((rel_path, file_type));
                            Ok(None)
                        } else {
                            Ok(Some((rel_path, file_type)))
                        }
                    }
                };
                match future.await {
                    Ok(r) => Ok(r),
                    Err(_) if conservative => return Ok(Some((rel_path, file_type))),
                    Err(err) => Err(err.into()),
                }
            }
        })
        .try_collect()
        .await?;
    Ok(OnDiskOptimizationResponse {
        incorrect_paths,
        correct_paths: recv_correct_paths.try_iter().collect(),
        content: recv_content.try_recv().ok(),
    })
}

struct MergedTokens {
    token: UploadToken,
    paths: Vec<(RepoPathBuf, FileType)>,
}

fn merge_tokens(
    files: impl IntoIterator<Item = (RepoPathBuf, UploadToken, FileType)>,
) -> impl Iterator<Item = MergedTokens> {
    let to_download =
        files
            .into_iter()
            .fold(HashMap::new(), |mut map, (path, token, file_type)| {
                map.entry((token.data.id.clone(), token.data.bubble_id))
                    .or_insert_with(|| MergedTokens {
                        token,
                        paths: vec![],
                    })
                    .paths
                    .push((path, file_type));
                map
            });

    to_download.into_iter().map(|(_, value)| value)
}

const WORKERS: usize = 10;

/// Return which files differ in content or symlinkness from the given upload tokens.
/// Note: does not return files that differ in executable permission.
pub async fn check_files(
    root: &RepoPathBuf,
    files: impl IntoIterator<Item = (RepoPathBuf, UploadToken, FileType)>,
) -> Result<Vec<RepoPathBuf>> {
    stream::iter(merge_tokens(files).map(|value| async move {
        let response = on_disk_optimization(root, value.paths, &value.token, false).await?;
        let paths = response.incorrect_paths.into_iter().map(|(path, _)| path);
        Result::<_>::Ok(stream::iter(paths).map(Result::<_>::Ok))
    }))
    .buffered(WORKERS)
    .try_flatten()
    .try_collect()
    .await
}

pub async fn download_files(
    api: &(impl EdenApi + ?Sized),
    root: &RepoPathBuf,
    files: impl IntoIterator<Item = (RepoPathBuf, UploadToken, FileType)>,
) -> Result<()> {
    let vfs = VFS::new(std::path::PathBuf::from(root.as_str()))?;
    let writer = AsyncVfsWriter::spawn_new(vfs, WORKERS);
    let writer = &writer;

    stream::iter(merge_tokens(files).map(|value| async move {
        let OnDiskOptimizationResponse {
            correct_paths,
            incorrect_paths,
            content,
        } = on_disk_optimization(root, value.paths, &value.token, true).await?;
        for (path, file_type) in correct_paths {
            match file_type {
                FileType::Regular => writer.set_executable(path, false).await?,
                FileType::Executable => writer.set_executable(path, true).await?,
                FileType::Symlink => {}
            }
        }
        let len = incorrect_paths.len();
        if len == 0 {
            return Ok(());
        }
        let content = match content {
            Some(bytes) => bytes,
            None => api.download_file(value.token).await?,
        };
        let write_paths = incorrect_paths
            .into_iter()
            // We're zipping and using repeat_n to avoid cloning the
            // whole content unecessarily. One file should be the most
            // common case.
            .zip(itertools::repeat_n(content, len))
            .map(|((path, file_type), content)| {
                (
                    path,
                    content,
                    match file_type {
                        FileType::Regular => UpdateFlag::Regular,
                        FileType::Symlink => UpdateFlag::Symlink,
                        FileType::Executable => UpdateFlag::Executable,
                    },
                )
            });
        writer.write_batch(write_paths).await?;

        Ok(())
    }))
    .buffered(WORKERS)
    .try_collect()
    .await
}


/*
 * 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.
 */

//! This module has auto-implemented functions that expand on EdenAPI functionalities.
// Think of it as a EdenAPIExt trait full of auto-implemented functions.
// It's not implemented like that because trait implementations can't be split in
// multiple files, so this is instead implemented as many functions in different files.
// Always use the format:
// fn my_function(api: &(impl EdenApi + ?Sized), other_args...) -> ... {...}
// this way the function can be called from inside any trait that extends EdenAPI.

mod files;
mod snapshot;
mod util;

pub use files::check_files;
pub use files::download_files;
pub use snapshot::upload_snapshot;
pub use util::calc_contentid;


/*
 * 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::BTreeMap;
use std::collections::BTreeSet;
use std::num::NonZeroU64;
use std::time::Duration;

use anyhow::bail;
use anyhow::format_err;
use anyhow::Context;
use anyhow::Result;
use edenapi::api::EdenApi;
use edenapi_types::AnyFileContentId;
use edenapi_types::AnyId;
use edenapi_types::BonsaiChangesetContent;
use edenapi_types::BonsaiFileChange;
use edenapi_types::ContentId;
use edenapi_types::FileType;
use edenapi_types::RepoPathBuf;
use edenapi_types::SnapshotRawData;
use edenapi_types::SnapshotRawFiles;
use edenapi_types::UploadSnapshotResponse;
use futures::StreamExt;
use futures::TryStreamExt;
use minibytes::Bytes;

use crate::util::calc_contentid;

#[derive(PartialEq, Eq)]
enum TrackedType {
    Tracked,
    Untracked,
}
use TrackedType::*;

struct FileMetadata(RepoPathBuf, FileType, ContentId, TrackedType);
struct FileData(ContentId, Bytes);

fn load_files(
    root: &RepoPathBuf,
    rel_path: RepoPathBuf,
    file_type: FileType,
    tracked: TrackedType,
) -> Result<(FileMetadata, FileData)> {
    let mut abs_path = root.clone();
    abs_path.push(&rel_path);
    let abs_path = abs_path.as_repo_path().as_str();
    let content = match file_type {
        FileType::Symlink => {
            let link = std::fs::read_link(abs_path)?;
            let to = link
                .to_str()
                .context("symlink is not valid UTF-8")?
                .as_bytes();
            Bytes::copy_from_slice(to)
        }
        FileType::Regular | FileType::Executable => Bytes::from_owner(std::fs::read(abs_path)?),
    };
    let content_id = calc_contentid(&content);
    Ok((
        FileMetadata(rel_path, file_type, content_id, tracked),
        FileData(content_id, content),
    ))
}

pub async fn upload_snapshot(
    api: &(impl EdenApi + ?Sized),
    data: SnapshotRawData,
    custom_duration_secs: Option<u64>,
    copy_from_bubble_id: Option<NonZeroU64>,
    use_bubble: Option<NonZeroU64>,
    labels: Option<Vec<String>>,
) -> Result<UploadSnapshotResponse> {
    let SnapshotRawData {
        files,
        author,
        hg_parents,
        time,
        tz,
    } = data;
    let SnapshotRawFiles {
        root,
        modified,
        added,
        removed,
        untracked,
        missing,
    } = files;
    let (need_upload, mut upload_data): (Vec<_>, Vec<_>) = modified
        .into_iter()
        .chain(added.into_iter())
        .map(|(p, t)| (p, t, Tracked))
        .chain(
            // TODO(yancouto): Don't upload untracked files if they're too big.
            untracked.into_iter().map(|(p, t)| (p, t, Untracked)),
        )
        // rel_path is relative to the repo root
        .map(|(rel_path, file_type, tracked)| -> anyhow::Result<_> {
            load_files(&root, rel_path.clone(), file_type, tracked)
                .with_context(|| anyhow::anyhow!("Failed to load file {}", rel_path))
        })
        // Let's ignore file not found errors, they might come from transient files that disappeared.
        .filter_map(|res| match res {
            Ok(ok) => Some(Ok(ok)),
            Err(err) => match err.downcast_ref::<std::io::Error>() {
                Some(io_error) if io_error.kind() == std::io::ErrorKind::NotFound => None,
                _ => Some(Err(err)),
            },
        })
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .unzip();

    // Deduplicate upload data
    let mut uniques = BTreeSet::new();
    upload_data.retain(|FileData(content_id, _)| uniques.insert(*content_id));
    let upload_data = upload_data
        .into_iter()
        .map(|FileData(content_id, data)| (AnyFileContentId::ContentId(content_id), data))
        .collect();

    let bubble_id = if let Some(id) = use_bubble {
        id
    } else {
        api.ephemeral_prepare(custom_duration_secs.map(Duration::from_secs), labels)
            .await?
            .entries
            .next()
            .await
            .context("Failed to create ephemeral bubble")??
            .bubble_id
    };
    let file_content_tokens = {
        let downcast_error = "incorrect upload token, failed to downcast 'token.data.id' to 'AnyId::AnyFileContentId::ContentId' type";
        // upload file contents first, receiving upload tokens
        api.process_files_upload(upload_data, Some(bubble_id), copy_from_bubble_id)
            .await?
            .entries
            .try_collect::<Vec<_>>()
            .await?
            .into_iter()
            .map(|token| {
                let content_id = match token.data.id {
                    AnyId::AnyFileContentId(AnyFileContentId::ContentId(id)) => id,
                    _ => bail!(downcast_error),
                };
                Ok((content_id, token))
            })
            .collect::<Result<BTreeMap<_, _>, _>>()?
    };
    let mut response = api
        .upload_bonsai_changeset(
            BonsaiChangesetContent {
                hg_parents,
                author,
                time,
                tz,
                extra: vec![],
                file_changes: need_upload
                    .into_iter()
                    .map(|FileMetadata(path, file_type, cid, tracked)| {
                        let upload_token = file_content_tokens
                            .get(&cid)
                            .with_context(|| {
                                format_err!(
                                    "unexpected error: upload token is missing for ContentId({})",
                                    cid
                                )
                            })?
                            .clone();
                        let change = if tracked == Tracked {
                            BonsaiFileChange::Change {
                                file_type,
                                upload_token,
                            }
                        } else {
                            BonsaiFileChange::UntrackedChange {
                                file_type,
                                upload_token,
                            }
                        };
                        Ok((path, change))
                    })
                    .chain(
                        removed
                            .into_iter()
                            .map(|path| Ok((path, BonsaiFileChange::Deletion))),
                    )
                    .chain(
                        missing
                            .into_iter()
                            .map(|path| Ok((path, BonsaiFileChange::UntrackedDeletion))),
                    )
                    .collect::<anyhow::Result<_>>()?,
                message: "".to_string(),
                is_snapshot: true,
            },
            Some(bubble_id),
        )
        .await?;
    let changeset_response = response
        .entries
        .next()
        .await
        .context("Failed to create changeset")??;
    Ok(UploadSnapshotResponse {
        changeset_token: changeset_response.token,
        bubble_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::io::Write;

use blake2::digest::Update;
use blake2::digest::VariableOutput;
use blake2::VarBlake2b;
use edenapi_types::ContentId;

pub fn calc_contentid(data: &[u8]) -> ContentId {
    let mut hash = VarBlake2b::new_keyed(b"content", ContentId::len());
    hash.update(data);
    let mut ret = [0u8; ContentId::len()];
    hash.finalize_variable(|res| {
        if let Err(e) = ret.as_mut().write_all(res) {
            panic!(
                "{}-byte array must work with {}-byte blake2b: {:?}",
                ContentId::len(),
                ContentId::len(),
                e
            );
        }
    });
    ContentId::from(ret)
}


/*
 * 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::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Error;
use configmodel::convert::FromConfigValue;
use configmodel::ConfigExt;
use http_client::Encoding;
use http_client::HttpVersion;
use http_client::MinTransferSpeed;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use rand::distributions::Alphanumeric;
use rand::thread_rng;
use rand::Rng;
use url::Url;

use crate::client::Client;
use crate::errors::ConfigError;
use crate::errors::EdenApiError;
use crate::EdenApi;

lazy_static! {
    pub static ref DEFAULT_CORRELATOR: String = thread_rng()
        .sample_iter(Alphanumeric)
        .take(16)
        .map(char::from)
        .collect();
}

/// External function that constructs other kinds of `EdenApi` from config.
static CUSTOM_BUILD_FUNCS: Lazy<
    RwLock<
        Vec<
            Box<
                dyn (Fn(&dyn configmodel::Config) -> Result<Option<Arc<dyn EdenApi>>, EdenApiError>)
                    + Send
                    + Sync
                    + 'static,
            >,
        >,
    >,
> = Lazy::new(Default::default);

/// Builder for creating new EdenAPI clients.
pub struct Builder<'a> {
    config: &'a dyn configmodel::Config,
    correlator: Option<String>,
    repo_name: Option<String>,
}

impl<'a> Builder<'a> {
    /// Populate a `Builder` from a Mercurial configuration.
    pub fn from_config(config: &'a dyn configmodel::Config) -> Result<Self, EdenApiError> {
        let builder = Self {
            config,
            correlator: None,
            repo_name: None,
        };
        Ok(builder)
    }

    /// Unique identifier that will be logged by both the client and server for
    /// every request, allowing log entries on both sides to be correlated. Also
    /// allows correlating multiple requests that were made by the same instance
    /// of the client.
    pub fn correlator(mut self, correlator: Option<impl ToString>) -> Self {
        self.correlator = correlator.map(|s| s.to_string());
        self
    }

    /// Configure repo name for client. This is only used by the Http Client.
    pub fn repo_name(mut self, repo_name: Option<impl ToString>) -> Self {
        self.repo_name = repo_name.map(|s| s.to_string());
        self
    }

    /// Build the client.
    pub fn build(self) -> Result<Arc<dyn EdenApi>, EdenApiError> {
        {
            let funcs = CUSTOM_BUILD_FUNCS.read();
            for func in funcs.iter() {
                if let Some(client) = func(self.config)? {
                    return Ok(client);
                }
            }
        }

        let mut builder = HttpClientBuilder::from_config(self.config)?.correlator(self.correlator);

        if let Some(repo_name) = &self.repo_name {
            builder = builder.repo_name(repo_name);
        }

        Ok(Arc::new(builder.build()?))
    }

    /// Register a customized builder that can produce a non-HTTP `EdenApi` from config.
    pub fn register_customize_build_func<F>(func: F)
    where
        F: (Fn(&dyn configmodel::Config) -> Result<Option<Arc<dyn EdenApi>>, EdenApiError>)
            + Send
            + Sync
            + 'static,
        F: Copy,
    {
        tracing::debug!(
            "registered {} to edenapi Builder",
            std::any::type_name::<F>()
        );
        CUSTOM_BUILD_FUNCS.write().push(Box::new(func))
    }
}

/// Builder for creating new HTTP EdenAPI clients.
///
/// You probably want to use [`Builder`] instead.
#[derive(Debug, Default)]
pub struct HttpClientBuilder {
    repo_name: Option<String>,
    server_url: Option<Url>,
    headers: HashMap<String, String>,
    max_files: Option<usize>,
    max_trees: Option<usize>,
    try_route_consistently: bool,
    max_history: Option<usize>,
    max_location_to_hash: Option<usize>,
    max_commit_mutations: Option<usize>,
    max_commit_translate_id: Option<usize>,
    timeout: Option<Duration>,
    debug: bool,
    correlator: Option<String>,
    http_version: Option<HttpVersion>,
    log_dir: Option<PathBuf>,
    encoding: Option<Encoding>,
    min_transfer_speed: Option<MinTransferSpeed>,
    max_retry_per_request: usize,
    http_config: http_client::Config,
}

impl HttpClientBuilder {
    pub fn new() -> Self {
        Default::default()
    }

    /// Build the HTTP client.
    pub fn build(self) -> Result<Client, EdenApiError> {
        self.try_into().map(Client::with_config)
    }

    /// Populate a `HttpClientBuilder` from a Mercurial configuration.
    pub fn from_config(config: &dyn configmodel::Config) -> Result<Self, EdenApiError> {
        // XXX: Ideally, the repo name would be a required field, obtained from a `Repo` object from
        // the `clidispatch` crate. Unforunately, not all callsites presently have access to a
        // populated `Repo` object, and it isn't trivial to just initialize one (requires a path to
        // the on-disk repo) or to plumb one through (which might not be possible for usage outside
        // of a Mercurial process, such as by EdenFS). For now, let's just allow setting the
        // reponame later via `repo_name` method.
        let mut repo_name = get_config::<String>(config, "remotefilelog", "reponame")?;
        if repo_name.as_deref() == Some("") {
            repo_name = None;
        }

        let server_url = get_required_config::<String>(config, "edenapi", "url")?
            .parse::<Url>()
            .map_err(|e| ConfigError::Invalid("edenapi.url".into(), e.into()))?;

        let mut headers = get_config::<String>(config, "edenapi", "headers")?
            .map(parse_headers)
            .transpose()
            .map_err(|e| ConfigError::Invalid("edenapi.headers".into(), e.into()))?
            .unwrap_or_default();
        headers.insert(
            "User-Agent".to_string(),
            format!("EdenSCM/{}", version::VERSION),
        );

        let max_requests = get_config(config, "edenapi", "maxrequests")?;
        let max_files = get_config(config, "edenapi", "maxfiles")?;
        let max_trees = get_config(config, "edenapi", "maxtrees")?;
        let try_route_consistently =
            get_config(config, "edenapi", "try-route-consistently")?.unwrap_or_default();
        let max_history = get_config(config, "edenapi", "maxhistory")?;
        let max_location_to_hash = get_config(config, "edenapi", "maxlocationtohash")?;
        let max_commit_mutations = get_config(config, "edenapi", "maxcommitmutations")?;
        let max_commit_translate_id = get_config(config, "edenapi", "maxcommittranslateid")?;
        let timeout = get_config(config, "edenapi", "timeout")?.map(Duration::from_secs);
        let debug = get_config(config, "edenapi", "debug")?.unwrap_or_default();
        let http_version =
            get_config(config, "edenapi", "http-version")?.unwrap_or_else(|| "2".to_string());
        let http_version = Some(match http_version.as_str() {
            "1.1" => HttpVersion::V11,
            "2" => HttpVersion::V2,
            x => {
                return Err(EdenApiError::BadConfig(ConfigError::Invalid(
                    "edenapi.http-version".into(),
                    anyhow!("invalid http version {}", x),
                )));
            }
        });
        let log_dir = get_config(config, "edenapi", "logdir")?;
        let encoding =
            get_config::<String>(config, "edenapi", "encoding")?.map(|s| Encoding::from(&*s));
        let low_speed_grace_period =
            get_config::<u64>(config, "edenapi", "low-speed-grace-period-seconds")?
                .unwrap_or_default();
        let min_transfer_speed =
            get_config::<u32>(config, "edenapi", "low-speed-min-bytes-per-second")?.map(
                |min_bytes_per_second| MinTransferSpeed {
                    min_bytes_per_second,
                    grace_period: Duration::from_secs(low_speed_grace_period),
                },
            );
        let max_retry_per_request =
            get_config::<usize>(config, "edenapi", "max-retry-per-request")?.unwrap_or(3);

        let mut http_config = hg_http::http_config(config, &server_url)?;
        http_config.verbose_stats |= debug;
        http_config.max_concurrent_requests = max_requests;

        let builder = HttpClientBuilder {
            repo_name,
            server_url: Some(server_url),
            headers,
            max_files,
            max_trees,
            try_route_consistently,
            max_history,
            max_location_to_hash,
            max_commit_mutations,
            max_commit_translate_id,
            timeout,
            debug,
            correlator: None,
            http_version,
            log_dir,
            encoding,
            min_transfer_speed,
            max_retry_per_request,
            http_config,
        };

        tracing::debug!(?builder);
        Ok(builder)
    }

    /// Set the repo name.
    pub fn repo_name(mut self, repo_name: &str) -> Self {
        self.repo_name = Some(repo_name.into());
        self
    }

    /// Set the server URL.
    pub fn server_url(mut self, url: Url) -> Self {
        self.server_url = Some(url);
        self
    }

    /// Extra HTTP headers that should be sent with each request.
    pub fn headers<T, K, V>(mut self, headers: T) -> Self
    where
        T: IntoIterator<Item = (K, V)>,
        K: ToString,
        V: ToString,
    {
        let headers = headers
            .into_iter()
            .map(|(k, v)| (k.to_string(), v.to_string()));
        self.headers.extend(headers);
        self
    }

    /// Add an extra HTTP header that should be sent with each request.
    pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self {
        self.headers.insert(name.to_string(), value.to_string());
        self
    }

    /// Maximum number of concurrent HTTP requests allowed.
    pub fn max_requests(mut self, size: Option<usize>) -> Self {
        self.http_config.max_concurrent_requests = size;
        self
    }

    /// Maximum number of keys per file request. Larger requests will be
    /// split up into concurrently-sent batches.
    pub fn max_files(mut self, size: Option<usize>) -> Self {
        self.max_files = size;
        self
    }

    /// Maximum number of keys per tree request. Larger requests will be
    /// split up into concurrently-sent batches.
    pub fn max_trees(mut self, size: Option<usize>) -> Self {
        self.max_trees = size;
        self
    }

    /// Maximum number of keys per history request. Larger requests will be
    /// split up into concurrently-sent batches.
    pub fn max_history(mut self, size: Option<usize>) -> Self {
        self.max_history = size;
        self
    }

    /// Maximum number of locations per location to has request. Larger requests will be split up
    /// into concurrently-sent batches.
    pub fn max_location_to_hash(mut self, size: Option<usize>) -> Self {
        self.max_location_to_hash = size;
        self
    }

    /// Maximum number of retries per request.
    pub fn max_retry_per_request(mut self, max: usize) -> Self {
        self.max_retry_per_request = max;
        self
    }

    /// Timeout for HTTP requests sent by the client.
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// Unique identifier that will be logged by both the client and server for
    /// every request, allowing log entries on both sides to be correlated. Also
    /// allows correlating multiple requests that were made by the same instance
    /// of the client.
    pub fn correlator(mut self, correlator: Option<impl ToString>) -> Self {
        self.correlator = correlator.map(|s| s.to_string());
        self
    }

    /// Set the HTTP version that the client should use.
    pub fn http_version(mut self, version: HttpVersion) -> Self {
        self.http_version = Some(version);
        self
    }

    /// If specified, the client will write a JSON version of every request
    /// it sends to the specified directory. This is primarily useful for
    /// debugging.
    pub fn log_dir(mut self, dir: impl AsRef<Path>) -> Self {
        self.log_dir = Some(dir.as_ref().into());
        self
    }

    /// If enabled, convert the user's client certificate from PEM to PKCS#12
    /// prior to use. This is required on platforms that do not natively support
    /// PEM certificates, such as Windows.
    pub fn convert_cert(mut self, enable: bool) -> Self {
        self.http_config.convert_cert = enable;
        self
    }
}

fn get_config<T: FromConfigValue>(
    config: &dyn configmodel::Config,
    section: &str,
    name: &str,
) -> Result<Option<T>, ConfigError> {
    config
        .get_opt::<T>(section, name)
        .map_err(|e| ConfigError::Invalid(format!("{}.{}", section, name), e.into()))
}

fn get_required_config<T: FromConfigValue>(
    config: &dyn configmodel::Config,
    section: &str,
    name: &str,
) -> Result<T, ConfigError> {
    Ok(get_config::<T>(config, section, name)?
        .ok_or_else(|| ConfigError::Missing(format!("{}.{}", section, name)))?)
}

/// Configuration for a `Client`. Essentially has the same fields as a
/// `HttpClientBuilder`, but required fields are not optional and values have
/// been appropriately parsed and validated.
#[derive(Debug)]
pub(crate) struct Config {
    #[allow(dead_code)]
    pub(crate) repo_name: String,
    pub(crate) server_url: Url,
    pub(crate) headers: HashMap<String, String>,
    pub(crate) max_files: Option<usize>,
    pub(crate) max_trees: Option<usize>,
    pub(crate) try_route_consistently: bool,
    pub(crate) max_history: Option<usize>,
    pub(crate) max_location_to_hash: Option<usize>,
    pub(crate) max_commit_mutations: Option<usize>,
    pub(crate) max_commit_translate_id: Option<usize>,
    pub(crate) timeout: Option<Duration>,
    #[allow(dead_code)]
    pub(crate) debug: bool,
    pub(crate) correlator: Option<String>,
    pub(crate) http_version: Option<HttpVersion>,
    pub(crate) log_dir: Option<PathBuf>,
    pub(crate) encoding: Option<Encoding>,
    pub(crate) min_transfer_speed: Option<MinTransferSpeed>,
    pub(crate) max_retry_per_request: usize,
    pub(crate) http_config: http_client::Config,
}

impl TryFrom<HttpClientBuilder> for Config {
    type Error = EdenApiError;

    fn try_from(builder: HttpClientBuilder) -> Result<Self, Self::Error> {
        let HttpClientBuilder {
            repo_name,
            server_url,
            headers,
            max_files,
            max_trees,
            try_route_consistently,
            max_history,
            max_location_to_hash,
            max_commit_mutations,
            max_commit_translate_id,
            timeout,
            debug,
            correlator,
            http_version,
            log_dir,
            encoding,
            min_transfer_speed,
            max_retry_per_request,
            http_config,
        } = builder;

        // Check for missing required fields.
        let repo_name = repo_name.ok_or(ConfigError::Missing("remotefilelog.reponame".into()))?;
        let mut server_url = server_url.ok_or(ConfigError::Missing("edenapi.url".into()))?;

        // Ensure the base URL's path ends with a slash so that `Url::join`
        // won't strip the final path component.
        if !server_url.path().ends_with('/') {
            let path = format!("{}/", server_url.path());
            server_url.set_path(&path);
        }

        // Setting these to 0 is the same as None.
        let max_files = max_files.filter(|n| *n > 0);
        let max_trees = max_trees.filter(|n| *n > 0);
        let max_history = max_history.filter(|n| *n > 0);

        Ok(Config {
            repo_name,
            server_url,
            headers,
            max_files,
            max_trees,
            try_route_consistently,
            max_history,
            max_location_to_hash,
            max_commit_mutations,
            max_commit_translate_id,
            timeout,
            debug,
            correlator,
            http_version,
            log_dir,
            encoding,
            min_transfer_speed,
            max_retry_per_request,
            http_config,
        })
    }
}

/// Parse headers from a JSON object.
fn parse_headers(headers: impl AsRef<str>) -> Result<HashMap<String, String>, Error> {
    Ok(serde_json::from_str(headers.as_ref())
        .context(format!("Not a valid JSON object: {:?}", headers.as_ref()))?)
}