/*
 * 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::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

use configloader::config::ConfigSet;
use configmodel::Config;
use configmodel::ConfigExt;
use identity::Identity;

use crate::constants::*;
use crate::errors::InitError;

pub fn init_hg_repo(
    root_path: &Path,
    config: &ConfigSet,
    repo_config_contents: Option<String>,
) -> Result<(), InitError> {
    if !root_path.exists() {
        create_dir(root_path)?;
    }

    let ident = identity::default();

    let hg_path = root_path.join(ident.dot_dir());
    if hg_path.exists() {
        return Err(InitError::ExistingRepoError(PathBuf::from(root_path)));
    }
    create_dir(&hg_path)?;

    write_reponame(&hg_path, config)?;
    write_changelog(&hg_path)?;
    write_repo_config(&hg_path, repo_config_contents, &ident)?;
    write_requirements(&hg_path, config)?;
    write_store_requirements(&hg_path, config)?;
    // TODO(sggutier): Add cleanup for the .hg directory in the event of an error

    fail::fail_point!("repo-init");

    Ok(())
}

fn create_dir(path: &Path) -> Result<(), InitError> {
    match fs::create_dir_all(path) {
        Err(err) => Err(InitError::DirectoryCreationError(
            path.to_str().unwrap().to_string(),
            err,
        )),
        _ => Ok(()),
    }
}

fn create_file(path: &Path, contents: &[u8]) -> Result<(), InitError> {
    let mut file = match File::create(path) {
        Ok(file) => file,
        Err(err) => {
            return Err(InitError::FileCreationError(PathBuf::from(path), err));
        }
    };
    match file.write_all(contents) {
        Ok(_) => Ok(()),
        Err(err) => {
            fs::remove_file(path).ok();
            Err(InitError::FileCreationError(PathBuf::from(path), err))
        }
    }
}

fn write_reponame<T: AsRef<Path>>(path: T, config: &ConfigSet) -> Result<(), InitError> {
    let path = path.as_ref();
    if let Some(reponame) = config.get("remotefilelog", "reponame") {
        let reponame_path = path.join(REPONAME_FILE);
        if !reponame_path.exists() {
            create_file(reponame_path.as_path(), reponame.as_bytes())?;
        }
    }
    Ok(())
}

// TODO(sggutier): We want to avoid creating this file in the first place
fn write_changelog(path: &Path) -> Result<(), InitError> {
    let changelog_path = path.join(CHANGELOG_FILE);
    create_file(
        changelog_path.as_path(),
        b"\0\0\01 dummy changelog to prevent using the old repo layout",
    )
}

fn write_repo_config(
    path: &Path,
    repo_config_contents: Option<String>,
    identity: &Identity,
) -> Result<(), InitError> {
    if let Some(repo_config_contents) = repo_config_contents {
        let repo_config_path = path.join(identity.config_repo_file());
        create_file(repo_config_path.as_path(), repo_config_contents.as_bytes())?;
    };
    Ok(())
}

fn write_requirements_file(path: &Path, requirements: HashSet<&str>) -> Result<(), InitError> {
    let mut requirements: Vec<_> = requirements.into_iter().collect();
    requirements.sort_unstable();
    requirements.push("");
    let requirements_path = path.join(REQUIREMENTS_FILE);
    create_file(
        requirements_path.as_path(),
        requirements.join("\n").as_bytes(),
    )
}

fn write_requirements(path: &Path, config: &ConfigSet) -> Result<(), InitError> {
    let mut requirements = HashSet::from([
        "lz4revlog",
        "revlogv1",
        "store",
        "fncache",
        "dotencode",
        "treestate",
    ]);

    if config
        .get_or_default("format", "generaldelta")
        .unwrap_or_default()
        || config
            .get_or_default("format", "usegeneraldelta")
            .unwrap_or_default()
    {
        requirements.insert("generaldelta");
    }
    if config
        .get_or_default("experimental", "windows-symlinks")
        .unwrap_or_default()
    {
        requirements.insert("windowssymlinks");
    }

    write_requirements_file(path, requirements)
}

fn write_store_requirements(path: &Path, config: &ConfigSet) -> Result<(), InitError> {
    let store_path = path.join(STORE_PATH);
    let store_path = store_path.as_path();
    create_dir(store_path)?;
    let mut requirements = HashSet::from(["visibleheads"]);
    if config
        .get_or("format", "use-segmented-changelog", is_test)
        .unwrap_or(false)
    {
        requirements.insert("invalidatelinkrev");
        requirements.insert("segmentedchangelog");
    }

    if config
        .get_or("experimental", "narrow-heads", || true)
        .unwrap_or(true)
    {
        requirements.insert("narrowheads");
    }

    write_requirements_file(store_path, requirements)
}

fn is_test() -> bool {
    std::env::var_os("TESTTMP").is_some()
}

#[cfg(test)]
mod tests {
    use configloader::config::Options;

    use super::*;

    #[test]
    fn test_reponame() {
        let tmp = tempfile::tempdir().unwrap();
        let mut config = ConfigSet::new();
        write_reponame(tmp.path(), &config).unwrap();
        let reponame_path = tmp.path().join(REPONAME_FILE);
        let reponame_path = reponame_path.as_path();
        assert!(!reponame_path.exists());
        config.set(
            "remotefilelog",
            "reponame",
            Some("thename"),
            &Options::new(),
        );
        write_reponame(tmp.path(), &config).unwrap();
        assert_eq!(fs::read_to_string(reponame_path).unwrap(), "thename");

        config.set(
            "remotefilelog",
            "reponame",
            Some("newname"),
            &Options::new(),
        );
        write_reponame(tmp.path(), &config).unwrap();
        // Reponame should not be written into if it already exists
        assert_eq!(fs::read_to_string(reponame_path).unwrap(), "thename");
    }

    #[test]
    fn test_requirements() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join(REQUIREMENTS_FILE);

        let mut config = ConfigSet::new();

        write_requirements(tmp.path(), &config).unwrap();
        assert_eq!(
            fs::read_to_string(&path).unwrap(),
            r#"dotencode
fncache
lz4revlog
revlogv1
store
treestate
"#
        );

        config.set("format", "usegeneraldelta", Some("true"), &"".into());
        write_requirements(tmp.path(), &config).unwrap();
        assert_eq!(
            fs::read_to_string(&path).unwrap(),
            r#"dotencode
fncache
generaldelta
lz4revlog
revlogv1
store
treestate
"#
        );
    }

    #[test]
    fn test_store_requirements() {
        let tmp = tempfile::tempdir().unwrap();
        let mut config = ConfigSet::new();
        let options = Options::new();
        let storepath = tmp.path().join(STORE_PATH);
        let path = storepath.join(REQUIREMENTS_FILE);
        let path = path.as_path();
        let mut expected = vec!["narrowheads", "visibleheads", ""];

        write_store_requirements(tmp.path(), &config).unwrap();
        assert_eq!(fs::read_to_string(path).unwrap(), expected.join("\n"));
        fs::remove_dir_all(storepath.as_path()).unwrap();

        expected.insert(0, "invalidatelinkrev");
        expected.insert(2, "segmentedchangelog");
        config.set("format", "use-segmented-changelog", Some("true"), &options);
        write_store_requirements(tmp.path(), &config).unwrap();
        assert_eq!(fs::read_to_string(path).unwrap(), expected.join("\n"));
        fs::remove_dir_all(storepath.as_path()).unwrap();

        config.set("experimental", "narrow-heads", Some("no"), &options);
        expected.remove(1);
        write_store_requirements(tmp.path(), &config).unwrap();
        assert_eq!(fs::read_to_string(path).unwrap(), expected.join("\n"));
    }

    #[test]
    fn test_init_hg_repo() {
        let tmp = tempfile::tempdir().unwrap();
        let repo_path = tmp.path().join("somerepo");
        let hg_path = repo_path.join(".sl");

        init_hg_repo(repo_path.as_path(), &mut ConfigSet::new(), None).unwrap();
        assert!(repo_path.exists());
        assert!(hg_path.exists());
        assert!(hg_path.join(CHANGELOG_FILE).exists());
        assert!(hg_path.join(REQUIREMENTS_FILE).exists());
        assert!(hg_path.join(STORE_PATH).exists());
        assert!(hg_path.join(STORE_PATH).join(REQUIREMENTS_FILE).exists());

        fs::remove_dir_all(repo_path.as_path()).unwrap();
        create_dir(repo_path.as_path()).unwrap();
        init_hg_repo(repo_path.as_path(), &mut ConfigSet::new(), None).unwrap();

        fs::remove_dir_all(repo_path.as_path()).unwrap();
        create_dir(repo_path.as_path()).unwrap();
        create_dir(hg_path.as_path()).unwrap();
        let error_str = format!(
            "repository `{}` already exists",
            repo_path.to_str().unwrap()
        );
        let err = init_hg_repo(repo_path.as_path(), &mut ConfigSet::new(), None)
            .err()
            .unwrap();
        assert!(matches!(err, InitError::ExistingRepoError(_)));
        assert_eq!(err.to_string(), error_str);
    }

    #[test]
    fn test_directory_creation() {
        let tmpdir = tempfile::tempdir().unwrap();

        // Test recursive directory creation
        let new_dir_path = tmpdir.path().join("some").join("nested").join("directory");
        create_dir(new_dir_path.as_path()).unwrap();
        init_hg_repo(new_dir_path.as_path(), &mut ConfigSet::new(), None).unwrap();

        // Test getting an error when unable to create directory
        let new_dir_path = tmpdir.path().join("foo");
        File::create(new_dir_path.as_path()).unwrap();
        let err = init_hg_repo(new_dir_path.as_path(), &mut ConfigSet::new(), None)
            .err()
            .unwrap();
        let partial_error_str =
            format!("unable to create directory at `{}", new_dir_path.display(),);
        assert!(matches!(err, InitError::DirectoryCreationError(_, _)));
        assert_eq!(
            err.to_string()[..partial_error_str.len()],
            partial_error_str
        );
    }
}


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

#![allow(dead_code)]

mod commits;
pub mod constants;
pub mod errors;
mod init;
pub mod repo;
mod requirements;
pub mod trees;


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

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use configloader::config::ConfigSet;
use configloader::Config;
use configmodel::ConfigExt;
use eagerepo::EagerRepo;
use eagerepo::EagerRepoStore;
use edenapi::Builder;
use edenapi::EdenApi;
use edenapi::EdenApiError;
use hgcommits::DagCommits;
use metalog::MetaLog;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use parking_lot::RwLock;
use repolock::RepoLocker;
use revisionstore::scmstore;
use revisionstore::scmstore::FileStoreBuilder;
use revisionstore::scmstore::TreeStoreBuilder;
use revisionstore::trait_impls::ArcFileStore;
use revisionstore::EdenApiFileStore;
use revisionstore::EdenApiTreeStore;
use revisionstore::MemcacheStore;
use revsets::utils as revset_utils;
use storemodel::ReadFileContents;
use storemodel::RefreshableReadFileContents;
use storemodel::RefreshableTreeStore;
use storemodel::TreeStore;
use treestate::dirstate::Dirstate;
use treestate::dirstate::TreeStateFields;
use treestate::serialization::Serializable;
use treestate::treestate::TreeState;
use types::repo::StorageFormat;
use types::HgId;
use util::path::absolute;
use vfs::VFS;
use workingcopy::filesystem::FileSystemType;
use workingcopy::workingcopy::WorkingCopy;

use crate::commits::open_dag_commits;
use crate::constants::SUPPORTED_DEFAULT_REQUIREMENTS;
use crate::constants::SUPPORTED_STORE_REQUIREMENTS;
use crate::errors;
use crate::init;
use crate::requirements::Requirements;
use crate::trees::TreeManifestResolver;

pub struct Repo {
    path: PathBuf,
    ident: identity::Identity,
    config: ConfigSet,
    shared_path: PathBuf,
    shared_ident: identity::Identity,
    store_path: PathBuf,
    dot_hg_path: PathBuf,
    shared_dot_hg_path: PathBuf,
    pub requirements: Requirements,
    pub store_requirements: Requirements,
    repo_name: Option<String>,
    metalog: Option<Arc<RwLock<MetaLog>>>,
    eden_api: Option<Arc<dyn EdenApi>>,
    dag_commits: Option<Arc<RwLock<Box<dyn DagCommits + Send + 'static>>>>,
    file_store: Option<Arc<dyn RefreshableReadFileContents<Error = anyhow::Error> + Send + Sync>>,
    file_scm_store: Option<Arc<scmstore::FileStore>>,
    tree_store: Option<Arc<dyn RefreshableTreeStore + Send + Sync>>,
    tree_scm_store: Option<Arc<scmstore::TreeStore>>,
    eager_store: Option<EagerRepoStore>,
    locker: Arc<RepoLocker>,
}

impl Repo {
    pub fn init(
        root_path: &Path,
        config: &ConfigSet,
        repo_config_contents: Option<String>,
        extra_config_values: &[String],
    ) -> Result<Repo> {
        let root_path = absolute(root_path)?;
        init::init_hg_repo(&root_path, config, repo_config_contents)?;
        let mut repo = Self::load(&root_path, extra_config_values, &[])?;
        repo.metalog()?.write().init_tracked()?;
        Ok(repo)
    }

    /// Load the repo from explicit path.
    ///
    /// Load repo configurations.
    pub fn load<P>(
        path: P,
        extra_config_values: &[String],
        extra_config_files: &[String],
    ) -> Result<Self>
    where
        P: Into<PathBuf>,
    {
        Self::build(path, extra_config_values, extra_config_files, None)
    }

    /// Loads the repo at given path, eschewing any config loading in
    /// favor of given config. This method exists so Python can create
    /// a Repo that uses the Python config verbatim without worrying
    /// about mixing CLI config overrides back in.
    pub fn load_with_config<P>(path: P, config: ConfigSet) -> Result<Self>
    where
        P: Into<PathBuf>,
    {
        Self::build(path, &[], &[], Some(config))
    }

    fn build<P>(
        path: P,
        extra_config_values: &[String],
        extra_config_files: &[String],
        config: Option<ConfigSet>,
    ) -> Result<Self>
    where
        P: Into<PathBuf>,
    {
        let path = path.into();
        assert!(path.is_absolute());

        assert!(
            config.is_none() || (extra_config_values.is_empty() && extra_config_files.is_empty()),
            "Don't pass a config and CLI overrides to Repo::build"
        );

        let ident = match identity::sniff_dir(&path)? {
            Some(ident) => ident,
            None => {
                return Err(errors::RepoNotFound(path.to_string_lossy().to_string()).into());
            }
        };

        let config = match config {
            Some(config) => config,
            None => configloader::hg::load(Some(&path), extra_config_values, extra_config_files)?,
        };

        let dot_hg_path = path.join(ident.dot_dir());

        let (shared_path, shared_ident) = match read_sharedpath(&dot_hg_path)? {
            Some((path, ident)) => (path, ident),
            None => (path.clone(), ident.clone()),
        };
        let shared_dot_hg_path = shared_path.join(shared_ident.dot_dir());
        let store_path = shared_dot_hg_path.join("store");

        let repo_name = configloader::hg::read_repo_name_from_disk(&shared_dot_hg_path)
            .ok()
            .or_else(|| {
                config
                    .get("remotefilelog", "reponame")
                    .map(|v| v.to_string())
            });

        let requirements = Requirements::open(
            &dot_hg_path.join("requires"),
            Lazy::force(&SUPPORTED_DEFAULT_REQUIREMENTS),
        )?;
        let store_requirements = Requirements::open(
            &store_path.join("requires"),
            Lazy::force(&SUPPORTED_STORE_REQUIREMENTS),
        )?;

        let locker = Arc::new(RepoLocker::new(&config, store_path.clone())?);

        Ok(Repo {
            path,
            ident,
            config,
            shared_path,
            shared_ident,
            store_path,
            dot_hg_path,
            shared_dot_hg_path,
            requirements,
            store_requirements,
            repo_name,
            metalog: None,
            eden_api: None,
            dag_commits: None,
            file_store: None,
            file_scm_store: None,
            tree_store: None,
            tree_scm_store: None,
            eager_store: None,
            locker,
        })
    }

    pub fn lock(&self) -> Result<repolock::RepoLockHandle, repolock::LockError> {
        self.locker.lock_store()
    }

    pub fn ensure_locked(&self) -> Result<(), repolock::LockError> {
        self.locker.ensure_store_locked()
    }

    pub fn reload_requires(&mut self) -> Result<()> {
        self.requirements = Requirements::open(
            &self.dot_hg_path.join("requires"),
            Lazy::force(&SUPPORTED_DEFAULT_REQUIREMENTS),
        )?;
        self.store_requirements = Requirements::open(
            &self.store_path.join("requires"),
            Lazy::force(&SUPPORTED_STORE_REQUIREMENTS),
        )?;
        Ok(())
    }

    /// Return the store path.
    pub fn store_path(&self) -> &Path {
        &self.store_path
    }

    /// Return the shared repo root. If the repo is not shared, return the
    /// repo root.
    pub fn shared_path(&self) -> &Path {
        &self.shared_path
    }

    /// Repo root path, without `.hg`.
    pub fn path(&self) -> &Path {
        self.path.as_path()
    }

    /// Repo root path, with `.hg`. Equivalent to self.path().join(".hg")
    pub fn dot_hg_path(&self) -> &Path {
        &self.dot_hg_path
    }

    /// Repo shared root path, with `.hg`. Equivalent to self.shared_path().join(".hg")
    pub fn shared_dot_hg_path(&self) -> &Path {
        &self.shared_dot_hg_path
    }

    pub fn config(&self) -> &ConfigSet {
        &self.config
    }

    pub fn config_mut(&mut self) -> &mut ConfigSet {
        &mut self.config
    }

    pub fn locker(&self) -> &Arc<RepoLocker> {
        &self.locker
    }

    pub fn repo_name(&self) -> Option<&str> {
        self.repo_name.as_ref().map(|s| s.as_ref())
    }

    pub fn config_path(&self) -> PathBuf {
        self.dot_hg_path.join(self.ident.config_repo_file())
    }

    pub fn metalog(&mut self) -> Result<Arc<RwLock<MetaLog>>> {
        match &self.metalog {
            Some(metalog) => Ok(metalog.clone()),
            None => {
                let metalog_path = self.metalog_path();
                let metalog = MetaLog::open_from_env(metalog_path.as_path())?;
                let metalog = Arc::new(RwLock::new(metalog));
                self.metalog = Some(metalog.clone());
                Ok(metalog)
            }
        }
    }

    pub fn invalidate_metalog(&mut self) {
        if self.metalog.is_some() {
            self.metalog = None;
        }
    }

    pub fn metalog_path(&self) -> PathBuf {
        self.store_path.join("metalog")
    }

    /// Constructs the EdenAPI client.
    ///
    /// This requires configs like `paths.default`. Avoid calling this function for
    /// local-only operations.
    pub fn eden_api(&mut self) -> Result<Arc<dyn EdenApi>, EdenApiError> {
        match &self.eden_api {
            Some(eden_api) => Ok(eden_api.clone()),
            None => {
                tracing::trace!(target: "repo::eden_api", "creating edenapi");
                let correlator = edenapi::DEFAULT_CORRELATOR.as_str();
                tracing::trace!(target: "repo::eden_api", "getting edenapi builder");
                let eden_api = Builder::from_config(&self.config)?
                    .correlator(Some(correlator))
                    .build()?;
                tracing::info!(url=eden_api.url(), path=?self.path, "EdenApi built");
                self.eden_api = Some(eden_api.clone());
                Ok(eden_api)
            }
        }
    }

    /// Constructs EdenAPI client if it should be constructed.
    ///
    /// Returns `None` if EdenAPI should not be used.
    pub fn optional_eden_api(&mut self) -> Result<Option<Arc<dyn EdenApi>>, EdenApiError> {
        if matches!(
            self.config.get_opt::<bool>("edenapi", "enable"),
            Ok(Some(false))
        ) {
            tracing::trace!(target: "repo::eden_api", "disabled because edenapi.enable is false");
            return Ok(None);
        }
        let path = self.config.get("paths", "default");
        match path {
            None => {
                tracing::trace!(target: "repo::eden_api", "disabled because paths.default is not set");
                return Ok(None);
            }
            Some(path) => {
                // EagerRepo URLs (test:, eager: file path).
                if path.starts_with("test:")
                    || path.starts_with("eager:")
                    || (!path.contains("://") && EagerRepo::url_to_dir(&path).is_some())
                {
                    tracing::trace!(target: "repo::eden_api", "using EagerRepo at {}", &path);
                    return Ok(Some(self.eden_api()?));
                }
                // Legacy tests are incompatible with EdenAPI.
                // They use None or file or ssh scheme with dummyssh.
                if path.starts_with("file:") {
                    tracing::trace!(target: "repo::eden_api", "disabled because paths.default is not set");
                    return Ok(None);
                } else if path.starts_with("ssh:") {
                    if let Some(ssh) = self.config.get("ui", "ssh") {
                        if ssh.contains("dummyssh") {
                            tracing::trace!(target: "repo::eden_api", "disabled because paths.default uses ssh scheme and dummyssh is in use");
                            return Ok(None);
                        }
                    }
                }
                // Explicitly set EdenAPI URLs.
                // Ideally we can make paths.default derive the edenapi URLs. But "push" is not on
                // EdenAPI yet. So we have to wait.
                if self.config.get_nonempty("edenapi", "url").is_none()
                    || self
                        .config
                        .get_nonempty("remotefilelog", "reponame")
                        .is_none()
                {
                    tracing::trace!(target: "repo::eden_api", "disabled because edenapi.url or remotefilelog.reponame is not set");
                    return Ok(None);
                }

                tracing::trace!(target: "repo::eden_api", "proceeding with path {}, reponame {:?}", path, self.config.get("remotefilelog", "reponame"));
            }
        }
        Ok(Some(self.eden_api()?))
    }

    pub fn dag_commits(&mut self) -> Result<Arc<RwLock<Box<dyn DagCommits + Send + 'static>>>> {
        match &self.dag_commits {
            Some(commits) => Ok(commits.clone()),
            None => {
                let commits = open_dag_commits(self)?;
                let commits = Arc::new(RwLock::new(commits));
                self.dag_commits = Some(commits.clone());
                Ok(commits)
            }
        }
    }

    pub fn invalidate_dag_commits(&mut self) -> Result<()> {
        if let Some(dag_commits) = &mut self.dag_commits {
            let dag_commits = dag_commits.clone();
            let mut dag_commits = dag_commits.write();
            *dag_commits = open_dag_commits(self)?;
        }
        Ok(())
    }

    pub fn remote_bookmarks(&mut self) -> Result<BTreeMap<String, HgId>> {
        match self.metalog()?.read().get("remotenames")? {
            Some(rn) => Ok(refencode::decode_remotenames(&rn)?),
            None => Err(errors::RemotenamesMetalogKeyError.into()),
        }
    }

    pub fn add_requirement(&mut self, requirement: &str) -> Result<()> {
        self.requirements.add(requirement);
        self.requirements.flush()?;
        Ok(())
    }

    pub fn add_store_requirement(&mut self, requirement: &str) -> Result<()> {
        self.store_requirements.add(requirement);
        self.store_requirements.flush()?;
        Ok(())
    }

    pub fn storage_format(&self) -> StorageFormat {
        if self.store_requirements.contains("remotefilelog") {
            StorageFormat::RemoteFilelog
        } else if self.store_requirements.contains("git") {
            StorageFormat::Git
        } else if self.store_requirements.contains("eagerepo") {
            StorageFormat::Eagerepo
        } else {
            StorageFormat::Revlog
        }
    }

    fn git_dir(&self) -> Result<PathBuf> {
        if !self.storage_format().is_git() {
            bail!("repo is not using git");
        }

        const GIT_DIR_FILE: &str = "gitdir";
        Ok(self.store_path.join(util::file::read_to_string(
            self.store_path.join(GIT_DIR_FILE),
        )?))
    }

    pub fn file_store(
        &mut self,
    ) -> Result<Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>> {
        if let Some(fs) = &self.file_store {
            return Ok(Arc::new(fs.clone()));
        }

        if let Some((store, _)) = self.try_construct_file_tree_store()? {
            return Ok(store);
        }

        tracing::trace!(target: "repo::file_store", "creating edenapi");
        let eden_api = self.optional_eden_api()?;

        tracing::trace!(target: "repo::file_store", "building filestore");
        let mut file_builder = FileStoreBuilder::new(self.config())
            .local_path(self.store_path())
            .correlator(edenapi::DEFAULT_CORRELATOR.as_str());

        if let Some(eden_api) = eden_api {
            tracing::trace!(target: "repo::file_store", "enabling edenapi");
            file_builder = file_builder.edenapi(EdenApiFileStore::new(eden_api));
        } else {
            tracing::trace!(target: "repo::file_store", "disabling edenapi");
            file_builder = file_builder.override_edenapi(false);
        }

        tracing::trace!(target: "repo::file_store", "configuring aux data");
        if self.config.get_or_default("scmstore", "auxindexedlog")? {
            file_builder = file_builder.store_aux_data();
        }

        tracing::trace!(target: "repo::file_store", "configuring memcache");
        if self
            .config
            .get_nonempty("remotefilelog", "cachekey")
            .is_some()
        {
            file_builder = file_builder.memcache(Arc::new(MemcacheStore::new(&self.config)?));
        }

        tracing::trace!(target: "repo::file_store", "building file store");
        let file_store = file_builder.build().context("when building FileStore")?;

        let fs = Arc::new(file_store);
        self.file_scm_store = Some(fs.clone());

        let fs = Arc::new(ArcFileStore(fs));

        self.file_store = Some(fs.clone());
        tracing::trace!(target: "repo::file_store", "filestore created");

        Ok(fs)
    }

    // This should only be used to share stores with Python.
    pub fn file_scm_store(&self) -> Option<Arc<scmstore::FileStore>> {
        self.file_scm_store.clone()
    }

    pub fn tree_store(&mut self) -> Result<Arc<dyn TreeStore + Send + Sync>> {
        if let Some(ts) = &self.tree_store {
            return Ok(Arc::new(ts.clone()));
        }

        if let Some((_, store)) = self.try_construct_file_tree_store()? {
            return Ok(store);
        }

        let eden_api = self.optional_eden_api()?;
        let mut tree_builder = TreeStoreBuilder::new(self.config())
            .local_path(self.store_path())
            .suffix("manifests");

        if let Some(eden_api) = eden_api {
            tracing::trace!(target: "repo::tree_store", "enabling edenapi");
            tree_builder = tree_builder.edenapi(EdenApiTreeStore::new(eden_api));
        } else {
            tracing::trace!(target: "repo::tree_store", "disabling edenapi");
            tree_builder = tree_builder.override_edenapi(false);
        }
        let ts = Arc::new(tree_builder.build()?);
        self.tree_scm_store = Some(ts.clone());
        self.tree_store = Some(ts.clone());
        Ok(ts)
    }

    // This should only be used to share stores with Python.
    pub fn tree_scm_store(&self) -> Option<Arc<scmstore::TreeStore>> {
        self.tree_scm_store.clone()
    }

    // This should only be used to share stores with Python.
    pub fn eager_store(&self) -> Option<EagerRepoStore> {
        self.eager_store.clone()
    }

    pub fn tree_resolver(&mut self) -> Result<TreeManifestResolver> {
        Ok(TreeManifestResolver::new(
            self.dag_commits()?,
            self.tree_store()?,
        ))
    }

    pub fn resolve_commit(&mut self, treestate: &TreeState, change_id: &str) -> Result<HgId> {
        revset_utils::resolve_single(
            change_id,
            self.dag_commits()?.read().id_map_snapshot()?.as_ref(),
            &*self.metalog()?.read(),
            treestate,
        )
        .map_err(|e| e.into())
    }

    pub fn invalidate_stores(&mut self) -> Result<()> {
        if let Some(file_store) = &self.file_store {
            file_store.refresh()?;
        }
        if let Some(tree_store) = &self.tree_store {
            tree_store.refresh()?;
        }
        Ok(())
    }

    pub fn working_copy(&mut self, path: &Path) -> Result<WorkingCopy, errors::InvalidWorkingCopy> {
        let is_eden = self.requirements.contains("eden");
        let fsmonitor_ext = self.config.get("extensions", "fsmonitor");
        let fsmonitor_mode = self.config.get_nonempty("fsmonitor", "mode");
        let is_watchman = if fsmonitor_ext.is_none() || fsmonitor_ext == Some("!".into()) {
            false
        } else {
            fsmonitor_mode.is_none() || fsmonitor_mode == Some("on".into())
        };
        let filesystem = match (is_eden, is_watchman) {
            (true, _) => FileSystemType::Eden,
            (false, true) => FileSystemType::Watchman,
            (false, false) => FileSystemType::Normal,
        };

        tracing::trace!(target: "repo::workingcopy", "initializing vfs at {path:?}");
        let vfs = VFS::new(path.to_path_buf())?;
        let case_sensitive = vfs.case_sensitive();
        tracing::trace!(target: "repo::workingcopy", "case sensitive: {case_sensitive}");

        let dirstate_path = path.join(self.ident.dot_dir()).join("dirstate");
        tracing::trace!(target: "repo::workingcopy", dirstate_path=?dirstate_path);

        let treestate = match filesystem {
            FileSystemType::Eden => {
                tracing::trace!(target: "repo::workingcopy", "loading edenfs dirstate");
                TreeState::from_eden_dirstate(dirstate_path, case_sensitive)?
            }
            _ => {
                let treestate_path = path.join(self.ident.dot_dir()).join("treestate");
                if util::file::exists(&dirstate_path)
                    .map_err(anyhow::Error::from)?
                    .is_some()
                {
                    tracing::trace!(target: "repo::workingcopy", "reading dirstate file");
                    let mut buf =
                        util::file::open(dirstate_path, "r").map_err(anyhow::Error::from)?;
                    tracing::trace!(target: "repo::workingcopy", "deserializing dirstate");
                    let dirstate = Dirstate::deserialize(&mut buf)?;
                    let fields = dirstate
                        .tree_state
                        .ok_or_else(|| anyhow!("missing treestate fields on dirstate"))?;

                    let filename = fields.tree_filename;
                    let root_id = fields.tree_root_id;
                    tracing::trace!(target: "repo::workingcopy", "loading treestate {filename} {root_id:?}");
                    TreeState::open(treestate_path.join(filename), root_id, case_sensitive)?
                } else {
                    tracing::trace!(target: "repo::workingcopy", "creating treestate");
                    let (treestate, root_id) = TreeState::new(&treestate_path, case_sensitive)?;

                    tracing::trace!(target: "repo::workingcopy", "creating dirstate");
                    let dirstate = Dirstate {
                        p1: *HgId::null_id(),
                        p2: *HgId::null_id(),
                        tree_state: Some(TreeStateFields {
                            tree_filename: treestate.file_name()?,
                            tree_root_id: root_id,
                            // TODO: set threshold
                            repack_threshold: None,
                        }),
                    };

                    tracing::trace!(target: "repo::workingcopy", "creating dirstate file");
                    let mut file =
                        util::file::create(dirstate_path).map_err(anyhow::Error::from)?;

                    tracing::trace!(target: "repo::workingcopy", "serializing dirstate");
                    dirstate.serialize(&mut file)?;
                    treestate
                }
            }
        };
        tracing::trace!(target: "repo::workingcopy", "treestate loaded");
        let treestate = Arc::new(Mutex::new(treestate));

        tracing::trace!(target: "repo::workingcopy", "creating file store");
        let file_store = self.file_store()?;

        tracing::trace!(target: "repo::workingcopy", "creating tree resolver");
        let tree_resolver = Arc::new(self.tree_resolver()?);

        Ok(WorkingCopy::new(
            vfs,
            self.storage_format(),
            filesystem,
            treestate,
            tree_resolver,
            file_store,
            &self.config,
            self.locker.clone(),
        )?)
    }

    async fn get_root_tree_id(&mut self, commit_id: HgId) -> Result<HgId> {
        let commit_store = self.dag_commits()?.read().to_dyn_read_root_tree_ids();
        let tree_ids = commit_store.read_root_tree_ids(vec![commit_id]).await?;
        Ok(tree_ids[0].1)
    }

    /// Construct both file and tree store if they are backed by the same storage.
    /// Return None if they are not backed by the same storage.
    /// Return Some((file_store, tree_store)) if they are constructed.
    fn try_construct_file_tree_store(
        &mut self,
    ) -> Result<
        Option<(
            Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>,
            Arc<dyn TreeStore + Send + Sync>,
        )>,
    > {
        if self.storage_format().is_git() {
            let git_store = Arc::new(
                gitstore::GitStore::open(&self.git_dir()?).context("opening git tree store")?,
            );
            // Set both stores to share underlying git store.
            self.file_store = Some(git_store.clone());
            self.tree_store = Some(git_store.clone());
            return Ok(Some((git_store.clone(), git_store)));
        }
        if self.storage_format().is_eager() {
            let store = EagerRepoStore::open(&self.store_path.join("hgcommits").join("v1"))?;
            self.eager_store = Some(store.clone());
            let store = Arc::new(store);
            self.file_store = Some(store.clone());
            self.tree_store = Some(store.clone());
            return Ok(Some((store.clone(), store)));
        }
        return Ok(None);
    }

    pub fn correlator(&self) -> &'static str {
        edenapi::DEFAULT_CORRELATOR.as_str()
    }
}

fn read_sharedpath(dot_path: &Path) -> Result<Option<(PathBuf, identity::Identity)>> {
    let sharedpath = fs::read_to_string(dot_path.join("sharedpath"))
        .ok()
        .map(|s| PathBuf::from(s))
        .and_then(|p| Some(PathBuf::from(p.parent()?)));

    if let Some(mut possible_path) = sharedpath {
        // sharedpath can be relative to our dot dir.
        possible_path = dot_path.join(possible_path);

        if !possible_path.is_dir() {
            return Err(
                errors::InvalidSharedPath(possible_path.to_string_lossy().to_string()).into(),
            );
        }

        return match identity::sniff_dir(&possible_path)? {
            Some(ident) => Ok(Some((possible_path, ident))),
            None => {
                Err(errors::InvalidSharedPath(possible_path.to_string_lossy().to_string()).into())
            }
        };
    }

    Ok(None)
}

impl std::fmt::Debug for Repo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Repo")
            .field("path", &self.path)
            .field("repo_name", &self.repo_name)
            .finish()
    }
}


/*
 * 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::fs;
use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

use util::errors::IOContext;
use util::errors::IOResult;

use crate::errors::RequirementsOpenError;
use crate::errors::UnsupportedRequirements;

/// `Requirements` contains a set of strings, tracks features/capabilities that
/// are *required* for clients to interface with the repository.
///
/// Requirements are often associated with actual on-disk formats. Different
/// requirements might require different code paths to process the on-disk
/// structure.
#[derive(Debug, Clone)]
pub struct Requirements {
    path: PathBuf,
    requirements: HashSet<String>,
    dirty: bool,
}

impl Requirements {
    /// Load requirements from the given path.
    ///
    /// If the given path does not exist, it is treated as an empty file.
    pub fn open(path: &Path, supported: &HashSet<String>) -> Result<Self, RequirementsOpenError> {
        let requirements: HashSet<String> = match fs::read_to_string(path) {
            Ok(s) => s.split_whitespace().map(|s| s.to_string()).collect(),
            Err(e) if e.kind() == io::ErrorKind::NotFound => Default::default(),
            Err(e) => {
                return Err(RequirementsOpenError::IOError(
                    Err::<Self, std::io::Error>(e)
                        .path_context("error opening file", path)
                        .unwrap_err(),
                ));
            }
        };
        let mut unsupported = requirements.difference(supported).peekable();
        if unsupported.peek().is_some() {
            let mut unsupported_vec: Vec<_> = unsupported.cloned().collect();
            unsupported_vec.sort();
            return Err(RequirementsOpenError::UnsupportedRequirements(
                UnsupportedRequirements(unsupported_vec.join(", ")),
            ));
        }
        let path = path.to_path_buf();
        let dirty = false;
        Ok(Self {
            path,
            requirements,
            dirty,
        })
    }

    /// Returns true if the given feature is enabled.
    pub fn contains(&self, name: &str) -> bool {
        self.requirements.contains(name)
    }

    /// Add a requirement. It is buffered in memory until `flush()`.
    ///
    /// This is usually part of a complex operation and protected by a
    /// filesystem lock.
    pub fn add(&mut self, name: &str) {
        let inserted = self.requirements.insert(name.to_string());
        self.dirty = self.dirty || inserted;
    }

    /// Write requirement changes to disk.
    ///
    /// This is usually part of a complex operation and protected by a
    /// filesystem lock.
    pub fn flush(&mut self) -> IOResult<()> {
        if self.dirty {
            util::file::atomic_write(&self.path, |f| {
                let mut requires: Vec<&str> =
                    self.requirements.iter().map(|s| s.as_str()).collect();
                requires.sort_unstable();
                let mut text = requires.join("\n");
                if !text.is_empty() {
                    text.push('\n');
                }
                f.write_all(text.as_bytes())
            })?;
            self.dirty = false;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;

    #[test]
    fn test_requires_basic() {
        let tmp = tempdir().unwrap();
        let path = tmp.path().join("requires");
        let allowed = HashSet::<String>::from(["a".to_owned(), "b".to_owned(), "c".to_owned()]);
        let mut reqs = Requirements::open(&path, &allowed).unwrap();

        assert!(!reqs.contains("a"));
        assert!(!reqs.contains("b"));

        reqs.add("a");
        assert!(reqs.contains("a"));
        assert!(!reqs.contains("b"));

        // add() buffers changes.
        let reqs2 = Requirements::open(&path, &allowed).unwrap();
        assert!(!reqs2.contains("a"));

        // add() again is not an error.
        reqs.add("a");
        reqs.add("b");

        // flush() writes changes.
        reqs.flush().unwrap();
        let reqs2 = Requirements::open(&path, &allowed).unwrap();
        assert!(reqs2.contains("a"));
        assert!(reqs2.contains("b"));
        assert!(!reqs2.contains("c"));

        assert!(reqs.contains("a"));
        assert!(reqs.contains("b"));
        assert!(!reqs.contains("c"));
    }

    #[test]
    fn test_unallowed_requirements() {
        let tmp = tempdir().unwrap();
        let path = tmp.path().join("requires");
        let allowed = HashSet::<String>::from(["a".to_owned()]);
        let mut reqs = Requirements::open(&path, &allowed).unwrap();
        reqs.add("foo");
        reqs.add("bar");
        reqs.flush().unwrap();
        let err = Requirements::open(&path, &allowed).err().unwrap();
        assert!(matches!(
            err,
            RequirementsOpenError::UnsupportedRequirements(_)
        ));
        assert_eq!(
            err.to_string(),
            r#"repository requires unknown features: bar, foo
(see https://mercurial-scm.org/wiki/MissingRequirement for more information)"#
        );
    }
}


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

use anyhow::Result;
use hgcommits::DagCommits;
use manifest_tree::ReadTreeManifest;
use manifest_tree::TreeManifest;
use manifest_tree::TreeStore;
use parking_lot::RwLock;
use types::HgId;

pub struct TreeManifestResolver {
    dag_commits: Arc<RwLock<Box<dyn DagCommits + Send + 'static>>>,
    tree_store: Arc<dyn TreeStore + Send + Sync>,
}
impl TreeManifestResolver {
    pub fn new(
        dag_commits: Arc<RwLock<Box<dyn DagCommits + Send + 'static>>>,
        tree_store: Arc<dyn TreeStore + Send + Sync>,
    ) -> Self {
        TreeManifestResolver {
            dag_commits,
            tree_store,
        }
    }
}

impl ReadTreeManifest for TreeManifestResolver {
    fn get(&self, commit_id: &HgId) -> Result<Arc<RwLock<TreeManifest>>> {
        let commit_store = self.dag_commits.read().to_dyn_read_root_tree_ids();
        let tree_ids =
            async_runtime::block_on(commit_store.read_root_tree_ids(vec![commit_id.clone()]))?;

        let mf = if tree_ids.is_empty() {
            TreeManifest::ephemeral(self.tree_store.clone())
        } else {
            TreeManifest::durable(self.tree_store.clone(), tree_ids[0].1)
        };

        Ok(Arc::new(RwLock::new(mf)))
    }
}


/*
 * 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::error;
use std::fmt;
use std::fs::File;
#[cfg(unix)]
use std::fs::Permissions;
use std::io::Write;
use std::num::NonZeroU64;
use std::ops::Add;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use std::time::SystemTime;

use configmodel::Config;
use configmodel::ConfigExt;
use fs2::FileExt;
use parking_lot::Mutex;
use util::errors::IOContext;
use util::errors::IOError;
use util::errors::IOResult;
use util::lock::PathLock;

const WORKING_COPY_NAME: &str = "wlock";
const STORE_NAME: &str = "lock";

pub struct RepoLocker {
    inner: Arc<Mutex<RepoLockerInner>>,
}

struct RepoLockerInner {
    config: LockConfigs,
    store_path: PathBuf,
    store_lock: Option<ReentrantLockHandle>,
    wc_locks: HashMap<PathBuf, ReentrantLockHandle>,
}

pub struct RepoLockHandle {
    locker: Arc<Mutex<RepoLockerInner>>,
    store: bool,
    wc_path: Option<PathBuf>,
}

#[derive(Debug)]
struct ReentrantLockHandle {
    _lock: LockHandle,
    ref_count: NonZeroU64,
    // Was this lock acquired out of order, such as the latter of repo.lock()
    // followed by repo.wlock(wait=False).
    out_of_order: bool,
}

impl ReentrantLockHandle {
    pub fn new(lock: LockHandle, out_of_order: bool) -> Self {
        Self {
            _lock: lock,
            ref_count: NonZeroU64::new(1).unwrap(),
            out_of_order,
        }
    }

    pub fn inc_ref_count(&mut self) {
        self.ref_count = self.ref_count.checked_add(1).unwrap();
    }

    pub fn dec_ref_count(&mut self) {
        self.ref_count = NonZeroU64::new(self.ref_count.get() - 1).unwrap();
    }

    pub fn ref_count(&self) -> u64 {
        self.ref_count.get()
    }

    pub fn out_of_order(&self) -> bool {
        self.out_of_order
    }
}

struct LockConfigs {
    pub deadline: Duration,
    pub warn_deadline: Duration,
    pub backoff: Duration,
}

impl LockConfigs {
    pub fn new(config: &dyn Config) -> anyhow::Result<Self, LockError> {
        let deadline =
            Duration::from_secs_f64(config.get_or_default::<f64>("ui", "timeout")?.max(0_f64));

        let warn_deadline = Duration::from_secs_f64(
            config
                .get_or_default::<f64>("ui", "timeout.warn")?
                .max(0_f64),
        );

        let backoff = Duration::from_secs_f64(
            config
                .get_or::<f64>("devel", "lock_backoff", || 1.0)?
                .max(0_f64),
        );
        Ok(LockConfigs {
            deadline,
            warn_deadline,
            backoff,
        })
    }
}

impl RepoLocker {
    pub fn new(config: &dyn Config, store_path: PathBuf) -> anyhow::Result<Self, LockError> {
        Ok(RepoLocker {
            inner: Arc::new(Mutex::new(RepoLockerInner {
                config: LockConfigs::new(config)?,
                store_path,
                store_lock: None,
                wc_locks: HashMap::new(),
            })),
        })
    }

    pub fn lock_store(&self) -> anyhow::Result<RepoLockHandle, LockError> {
        self.lock_store_maybe_wait(true)
    }

    pub fn try_lock_store(&self) -> anyhow::Result<RepoLockHandle, LockError> {
        self.lock_store_maybe_wait(false)
    }

    fn lock_store_maybe_wait(&self, wait: bool) -> anyhow::Result<RepoLockHandle, LockError> {
        let mut inner = self.inner.lock();
        inner.lock_store(wait)?;
        Ok(RepoLockHandle::new_store_lock(self.inner.clone()))
    }

    pub fn ensure_store_locked(&self) -> anyhow::Result<(), LockError> {
        let inner = self.inner.lock();
        if inner.store_lock.is_some() {
            Ok(())
        } else {
            Err(LockError::NotHeld(
                inner
                    .store_path
                    .join(STORE_NAME)
                    .to_string_lossy()
                    .to_string(),
            ))
        }
    }

    pub fn lock_working_copy(
        &self,
        wc_dot_hg: PathBuf,
    ) -> anyhow::Result<RepoLockHandle, LockError> {
        self.lock_working_copy_maybe_wait(wc_dot_hg, true)
    }

    pub fn try_lock_working_copy(
        &self,
        wc_dot_hg: PathBuf,
    ) -> anyhow::Result<RepoLockHandle, LockError> {
        self.lock_working_copy_maybe_wait(wc_dot_hg, false)
    }

    fn lock_working_copy_maybe_wait(
        &self,
        wc_dot_hg: PathBuf,
        wait: bool,
    ) -> anyhow::Result<RepoLockHandle, LockError> {
        let mut inner = self.inner.lock();
        inner.lock_working_copy(wc_dot_hg.clone(), wait)?;
        Ok(RepoLockHandle::new_working_copy_lock(
            self.inner.clone(),
            wc_dot_hg,
        ))
    }

    pub fn ensure_working_copy_locked(&self, wc_path: &Path) -> anyhow::Result<(), LockError> {
        let inner = self.inner.lock();
        if inner.wc_locks.contains_key(wc_path) {
            Ok(())
        } else {
            Err(LockError::NotHeld(
                wc_path
                    .join(WORKING_COPY_NAME)
                    .to_string_lossy()
                    .to_string(),
            ))
        }
    }
}

impl RepoLockerInner {
    pub fn lock_store(&mut self, wait: bool) -> anyhow::Result<(), LockError> {
        if let Some(store_lock) = &mut self.store_lock {
            store_lock.inc_ref_count();
        } else {
            let handle = if wait {
                lock(
                    &self.config,
                    &self.store_path,
                    STORE_NAME,
                    lock_contents()?.as_bytes(),
                )?
            } else {
                try_lock(&self.store_path, STORE_NAME, lock_contents()?.as_bytes())?
            };
            self.store_lock = Some(ReentrantLockHandle::new(handle, false));
        }
        Ok(())
    }

    pub fn lock_working_copy(
        &mut self,
        wc_dot_hg: PathBuf,
        wait: bool,
    ) -> anyhow::Result<(), LockError> {
        if let Some(wc_lock) = self.wc_locks.get_mut(&wc_dot_hg) {
            wc_lock.inc_ref_count();
        } else {
            // TODO: Should we check that this working copy is actually related to this store?
            let handle = if wait {
                // Only check out-of-order if we are waiting. If we aren't
                // waiting and the the deadlock condition occurs (i.e. other
                // process locks wlock and is waiting for store lock), our wlock
                // acquisition will simply fail and no deadlock will occur.
                if self.store_lock.is_some() {
                    return Err(LockError::OutOfOrder(
                        "must not take store lock before wlock".to_string(),
                    ));
                }

                lock(
                    &self.config,
                    &wc_dot_hg,
                    WORKING_COPY_NAME,
                    lock_contents()?.as_bytes(),
                )?
            } else {
                try_lock(&wc_dot_hg, WORKING_COPY_NAME, lock_contents()?.as_bytes())?
            };

            self.wc_locks.insert(
                wc_dot_hg,
                ReentrantLockHandle::new(handle, self.store_lock.is_some()),
            );
        }
        Ok(())
    }
}

impl RepoLockHandle {
    fn new_store_lock(locker: Arc<Mutex<RepoLockerInner>>) -> Self {
        RepoLockHandle {
            locker,
            store: true,
            wc_path: None,
        }
    }

    fn new_working_copy_lock(locker: Arc<Mutex<RepoLockerInner>>, wc_path: PathBuf) -> Self {
        RepoLockHandle {
            locker,
            store: false,
            wc_path: Some(wc_path),
        }
    }

    /// Return ref count for this lock handle. Ref count of 1 means lock will be
    /// relased when this handle is dropped.
    pub fn count(&self) -> u64 {
        let locker = self.locker.lock();
        if self.store {
            locker.store_lock.as_ref().unwrap().ref_count()
        } else {
            locker
                .wc_locks
                .get(self.wc_path.as_ref().unwrap())
                .unwrap()
                .ref_count()
        }
    }
}

impl fmt::Debug for RepoLockHandle {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        let locker = self.locker.lock();
        if self.store {
            if let Some(handle) = &locker.store_lock {
                fmt::Debug::fmt(&handle, f)?;
            } else {
                f.write_str("invalid store lock")?;
            }
        }
        if let Some(wc_path) = &self.wc_path {
            if let Some(handle) = locker.wc_locks.get(wc_path.as_path()) {
                fmt::Debug::fmt(&handle, f)?;
            } else {
                f.write_fmt(format_args!("invalid wc lock {:?}", wc_path))?;
            }
        }
        Ok(())
    }
}

impl Drop for RepoLockHandle {
    fn drop(&mut self) {
        let mut locker = self.locker.lock();
        if self.store {
            let store_lock = locker.store_lock.as_mut().unwrap();
            let lock_count = store_lock.ref_count();
            if lock_count > 1 {
                store_lock.dec_ref_count();
            } else {
                let _ = locker.store_lock.take();
            }
        }
        if let Some(wc_path) = &self.wc_path {
            let wc_lock = locker.wc_locks.get_mut(wc_path.as_path()).unwrap();
            let lock_count = wc_lock.ref_count();
            if lock_count > 1 {
                wc_lock.dec_ref_count();
            } else {
                if !wc_lock.out_of_order() && locker.store_lock.is_some() {
                    panic!("attempted to release wlock before lock");
                }
                locker.wc_locks.remove(wc_path.as_path());
            }
        }
    }
}

fn lock_contents() -> anyhow::Result<String, LockError> {
    Ok(format!("{}:{}", util::sys::hostname()?, std::process::id()))
}

/// lock loops until it can acquire the specified lock, subject to
/// ui.timeout timeout. Errors other than lock contention are
/// propagated immediately with no retries.
fn lock(
    config: &LockConfigs,
    dir: &Path,
    name: &str,
    contents: &[u8],
) -> anyhow::Result<LockHandle, LockError> {
    let now = SystemTime::now();

    let deadline = now.add(config.deadline);

    let warn_deadline = now.add(config.warn_deadline);

    let backoff = config.backoff;

    loop {
        match try_lock(dir, name, contents) {
            Ok(h) => return Ok(h),
            Err(err) => match err {
                LockError::Contended(_) => {
                    // TODO: add user friendly debugging similar to Python locks.

                    let now = SystemTime::now();
                    if now >= warn_deadline {
                        tracing::warn!(name, "lock contended");
                    } else {
                        tracing::info!(name, "lock contended");
                    };

                    if now >= deadline {
                        return Err(err);
                    }

                    sleep(backoff)
                }
                _ => return Err(err),
            },
        }
    }
}

struct LockPaths {
    legacy: PathBuf,
    dir: PathBuf,
    data: PathBuf,
    lock: PathBuf,
}

impl LockPaths {
    pub fn new(dir: &Path, name: &str) -> Self {
        let dir_lock = dir.join(".dir_lock");
        let legacy = dir.join(name);

        let name = sanitize_lock_name(name);
        let data = dir.join(name).with_extension("data");
        let lock = data.with_extension("lock");

        Self {
            legacy,
            dir: dir_lock,
            data,
            lock,
        }
    }
}

/// try_lock attempts to acquire an advisory file lock and write
/// specified contents. Lock acquisition and content writing are
/// atomic as long as the content reader also uses this method. If
/// the lock is not available, LockContendederror is returned
/// immediately containing the lock's current contents.
pub fn try_lock(dir: &Path, name: &str, contents: &[u8]) -> anyhow::Result<LockHandle, LockError> {
    let lock_paths = LockPaths::new(dir, name);

    // Our locking strategy uses three files:
    //   1. An empty advisory lock file at the directory level.
    //   2. An empty advisory lock file named <name>.lock. This file is returned.
    //   3. A plain file named <name>.data which contains the contents.
    //
    //  Readers and writers acquire the directory lock first. This
    //  ensures atomicity across lock acquisition and content
    //  writing.
    let _dir_lock = PathLock::exclusive(lock_paths.dir)?;

    #[cfg(unix)]
    let _ = _dir_lock
        .as_file()
        .set_permissions(Permissions::from_mode(0o666));

    let lock_file = util::file::open(&lock_paths.lock, "wc")?;

    #[cfg(unix)]
    let _ = lock_file.set_permissions(Permissions::from_mode(0o666));

    match lock_file.try_lock_exclusive() {
        Ok(_) => {}
        Err(err) if err.kind() == fs2::lock_contended_error().kind() => {
            let contents = util::file::read(&lock_paths.data)?;
            return Err(LockContendedError {
                path: lock_paths.data,
                contents,
            }
            .into());
        }
        Err(err) => {
            return Err(
                IOError::from_path(err, "error locking lock file", &lock_paths.lock).into(),
            );
        }
    };

    // Create the legacy lock file to maintain compatibility for
    // external code that checks directly for .hg/wlock as an
    // indication of "is an hg operation in progress".
    if let Ok(mut legacy_file) = File::create(&lock_paths.legacy) {
        // Also write lock contents for compatibility with Python readers.
        let _ = legacy_file.write_all(contents.as_ref());

        #[cfg(unix)]
        let _ = legacy_file.set_permissions(Permissions::from_mode(0o644));
    }

    let mut contents_file = util::file::open(&lock_paths.data, "wct")?;
    #[cfg(unix)]
    let _ = contents_file.set_permissions(Permissions::from_mode(0o666));
    contents_file
        .write_all(contents.as_ref())
        .path_context("error write lock contents", &lock_paths.data)?;

    Ok(LockHandle {
        path: lock_paths.lock,
        lock: lock_file,
        legacy_path: lock_paths.legacy,
    })
}

#[derive(Debug)]
pub struct LockHandle {
    path: PathBuf,
    lock: File,
    legacy_path: PathBuf,
}

impl LockHandle {
    pub fn unlock(&mut self) -> IOResult<()> {
        self.unlink_legacy();
        self.lock
            .unlock()
            .path_context("error unlocking lock file", &self.path)
    }

    fn unlink_legacy(&mut self) {
        let _ = util::path::remove_file(&self.legacy_path);
    }
}

impl Drop for LockHandle {
    fn drop(&mut self) {
        self.unlink_legacy();
    }
}

fn sanitize_lock_name(name: &str) -> String {
    // Avoid letting a caller specify "foo.lock" and accidentally
    // interfering with the underlying locking details. This is
    // mainly for compatibility during python lock transition to
    // avoid a python lock "foo.lock" accidentally colliding with
    // the rust lock file.
    name.replace('.', "_")
}

#[derive(thiserror::Error, Debug)]
pub enum LockError {
    #[error(transparent)]
    ConfigError(#[from] configmodel::Error),
    #[error(transparent)]
    Contended(#[from] LockContendedError),
    #[error(transparent)]
    Io(#[from] IOError),
    #[error("{0}")]
    OutOfOrder(String),
    #[error("lock is not held: {0}")]
    NotHeld(String),
}

#[derive(Debug)]
pub struct LockContendedError {
    pub path: PathBuf,
    pub contents: Vec<u8>,
}

impl error::Error for LockContendedError {}

impl fmt::Display for LockContendedError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "lock {:?} contended", self.path)
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use std::thread;
    use std::thread::spawn;

    use anyhow::Result;

    use super::*;

    #[test]
    fn test_try_lock() -> Result<()> {
        let tmp = tempfile::tempdir()?;

        {
            let _foo_lock = try_lock(tmp.path(), "foo", "some contents".as_bytes())?;

            // Can get current lock data via lock contended error.
            if let Err(LockError::Contended(LockContendedError { contents, .. })) =
                try_lock(tmp.path(), "foo", "bar".as_bytes())
            {
                assert_eq!("some contents".as_bytes(), contents);
            } else {
                panic!("expected LockContendedError")
            }
        }

        // Now we can acquire "foo" lock since above lock has been dropped.
        let _foo_lock = try_lock(tmp.path(), "foo", "some contents".as_bytes())?;

        Ok(())
    }

    #[test]
    #[cfg(unix)]
    fn test_try_lock_permissions() -> Result<()> {
        let tmp = tempfile::tempdir()?;

        try_lock(tmp.path(), "foo", "some contents".as_bytes())?;

        let assert_666 = |name: &str| {
            assert_eq!(
                tmp.path()
                    .join(name)
                    .metadata()
                    .unwrap()
                    .permissions()
                    .mode()
                    & 0o777,
                0o666
            );
        };

        assert_666(".dir_lock");
        assert_666("foo.lock");
        assert_666("foo.data");

        Ok(())
    }

    // Test readers never see incomplete or inconsistent lock data
    // contents.
    #[test]
    fn test_lock_atomicity() -> Result<()> {
        let tmp = tempfile::tempdir()?;

        // Two threads taking turns with the lock. If lock is
        // unavailable, the thread knows the lock contents should be
        // that of the other thread (because there are only two
        // threads).
        let threads: Vec<_> = vec!["a", "b"]
            .into_iter()
            .map(|c| {
                // Make contents big so we include the case where
                // writing the contents takes multiple writes.
                let my_contents = c.repeat(1_000_000);
                let other = if c == "a" { "b" } else { "a" };
                let other_contents = other.repeat(1_000_000);
                let path = tmp.path().to_path_buf();
                thread::spawn(move || {
                    for _ in 0..10 {
                        match try_lock(&path, "foo", my_contents.as_bytes()) {
                            Ok(_) => {}
                            Err(LockError::Contended(LockContendedError { contents, .. })) => {
                                assert_eq!(other_contents.as_bytes(), contents);
                            }
                            _ => panic!("unexpected result"),
                        }
                    }
                })
            })
            .collect();

        for t in threads {
            t.join().unwrap();
        }

        Ok(())
    }

    #[test]
    fn test_lock_loop() -> Result<()> {
        let tmp = tempfile::tempdir()?;

        let mut cfg = BTreeMap::from([("ui.timeout", "0.001"), ("devel.lock_backoff", "0.001")]);
        let lock_cfg = LockConfigs::new(&cfg)?;

        let first = lock(&lock_cfg, tmp.path(), "foo", "contents".as_bytes())?;

        assert!(matches!(
            lock(&lock_cfg, tmp.path(), "foo", "contents".as_bytes()),
            Err(LockError::Contended(_))
        ));

        cfg.insert("ui.timeout", "60");

        let lock_cfg = LockConfigs::new(&cfg)?;
        let dropper = spawn(move || {
            sleep(Duration::from_millis(5));
            drop(first);
        });

        assert!(lock(&lock_cfg, tmp.path(), "foo", "contents".as_bytes()).is_ok());

        dropper.join().unwrap();

        Ok(())
    }

    #[test]
    fn test_working_copy() -> Result<()> {
        let tmp = tempfile::tempdir()?;

        let cfg = Box::new(BTreeMap::<&str, &str>::new());

        let locker = RepoLocker::new(&cfg, tmp.path().to_path_buf())?;

        let _wlock = locker.lock_working_copy(tmp.path().to_path_buf())?;

        // Make sure locked the right file, and check the contents.
        match try_lock(tmp.path(), WORKING_COPY_NAME, "foo".as_bytes()) {
            Err(LockError::Contended(LockContendedError { contents, .. })) => {
                assert_eq!(
                    String::from_utf8(contents)?,
                    format!("{}:{}", util::sys::hostname()?, std::process::id())
                );
            }
            _ => panic!("lock should be contended"),
        };

        Ok(())
    }

    #[test]
    fn test_lock_order_verification() -> Result<()> {
        let store_tmp = tempfile::tempdir()?;
        let wc_tmp = tempfile::tempdir()?;

        let cfg = Box::new(BTreeMap::<&str, &str>::new());

        let locker = RepoLocker::new(&cfg, store_tmp.path().to_path_buf())?;

        {
            let _wlock = locker.lock_working_copy(wc_tmp.path().to_path_buf())?;
            let _lock = locker.lock_store()?;
        }
        {
            let _lock = locker.lock_store()?;
            match locker.lock_working_copy(wc_tmp.path().to_path_buf()) {
                Err(LockError::OutOfOrder(_)) => {}
                result => panic!("wlock should be required before lock: {:?}", result),
            };
        }

        Ok(())
    }

    #[test]
    fn test_ensure_lock() -> Result<()> {
        let store_tmp = tempfile::tempdir()?;
        let wc_tmp = tempfile::tempdir()?;

        let cfg = BTreeMap::<&str, &str>::new();
        let locker = RepoLocker::new(&cfg, store_tmp.path().to_path_buf())?;

        locker
            .ensure_working_copy_locked(wc_tmp.path())
            .unwrap_err();
        locker.ensure_store_locked().unwrap_err();

        let _wlock = locker.lock_working_copy(wc_tmp.path().to_path_buf())?;
        locker.ensure_working_copy_locked(wc_tmp.path()).unwrap();
        locker.ensure_store_locked().unwrap_err();

        let _lock = locker.lock_store()?;
        locker.ensure_working_copy_locked(wc_tmp.path()).unwrap();
        locker.ensure_store_locked().unwrap();

        Ok(())
    }

    #[test]
    fn test_taking_lock_twice() -> Result<()> {
        let store_tmp = tempfile::tempdir()?;
        let wc_tmp = tempfile::tempdir()?;

        let cfg = BTreeMap::<&str, &str>::new();
        let locker = RepoLocker::new(&cfg, store_tmp.path().to_path_buf())?;

        let _wclock1 = locker.lock_working_copy(wc_tmp.path().to_path_buf())?;
        let _wclock2 = locker.lock_working_copy(wc_tmp.path().to_path_buf())?;

        let _lock1 = locker.lock_store()?;
        let _lock2 = locker.lock_store()?;

        drop(_lock1);
        assert!(locker.ensure_store_locked().is_ok());
        drop(_lock2);
        assert!(locker.ensure_store_locked().is_err());
        drop(_wclock1);
        assert!(locker.ensure_working_copy_locked(wc_tmp.path()).is_ok());
        drop(_wclock2);
        assert!(locker.ensure_working_copy_locked(wc_tmp.path()).is_err());

        Ok(())
    }

    #[test]
    #[should_panic]
    fn test_bad_lock_release_order() {
        let store_tmp = tempfile::tempdir().unwrap();
        let wc_tmp = tempfile::tempdir().unwrap();

        let cfg = BTreeMap::<&str, &str>::new();
        let locker = RepoLocker::new(&cfg, store_tmp.path().to_path_buf()).unwrap();

        let _wclock1 = locker
            .lock_working_copy(wc_tmp.path().to_path_buf())
            .unwrap();
        let _wclock2 = locker
            .lock_working_copy(wc_tmp.path().to_path_buf())
            .unwrap();

        let _lock1 = locker.lock_store().unwrap();
        let _lock2 = locker.lock_store().unwrap();

        drop(_wclock1);
        drop(_wclock2);
    }

    #[test]
    fn test_interesting_acquisition_order() {
        let tmp_dir = tempfile::tempdir().unwrap();

        let cfg = BTreeMap::<&str, &str>::new();
        let locker = RepoLocker::new(&cfg, tmp_dir.path().to_path_buf()).unwrap();

        // Lock wlock, then store lock, then wlock again.
        let _wclock1 = locker
            .lock_working_copy(tmp_dir.path().to_path_buf())
            .unwrap();
        let _storelock = locker.lock_store().unwrap();
        let _wclock2 = locker
            .lock_working_copy(tmp_dir.path().to_path_buf())
            .unwrap();
    }

    #[test]
    fn test_try_repo_lock() {
        let tmp_dir = tempfile::tempdir().unwrap();

        let cfg = BTreeMap::<&str, &str>::new();
        let locker1 = RepoLocker::new(&cfg, tmp_dir.path().to_path_buf()).unwrap();
        let locker2 = RepoLocker::new(&cfg, tmp_dir.path().to_path_buf()).unwrap();

        let _lock1 = locker1.try_lock_store().unwrap();
        let _lock2 = locker1.try_lock_store().unwrap();

        // Returns immediately with error.
        assert!(matches!(
            locker2.try_lock_store().unwrap_err(),
            LockError::Contended(_)
        ));
    }

    #[test]
    fn test_lock_legacy_compat() -> Result<()> {
        let tmp = tempfile::tempdir()?;

        let legacy_path = tmp.path().join("foo");

        // legacy path doesn't exist (i.e. rust-only lock mode)
        {
            {
                let _foo_lock = try_lock(tmp.path(), "foo", "some contents".as_bytes())?;
                assert!(legacy_path.exists());
            }

            // clean up legacy file
            assert!(!legacy_path.exists());
        }

        // Legacy path already exists - clean it up.
        {
            File::create(&legacy_path)?;

            {
                let _foo_lock = try_lock(tmp.path(), "foo", "some contents".as_bytes())?;
                assert!(legacy_path.exists());
            }

            // clean up legacy file
            assert!(!legacy_path.exists());
        }

        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::convert::AsRef;

use anyhow::Context;
use anyhow::Result;
use percent_encoding::percent_decode_str;
use percent_encoding::utf8_percent_encode;
use percent_encoding::AsciiSet;
use percent_encoding::NON_ALPHANUMERIC;

/// All non-alphanumeric characters (except hypens, underscores, and periods)
/// found in the repo's name will be percent-encoded before being used in URLs.
/// Characters allowed in a repo name (like `+` and `/`) since they are reserved
/// characters according to RFC 3986 section 2.2 Reserved Characters (January 2005)
const RESERVED_CHARS: &AsciiSet = &NON_ALPHANUMERIC
    .remove(b'_')
    .remove(b'-')
    .remove(b'.')
    .add(b'+')
    .add(b'/');

pub fn encode_repo_name(repo_name: impl AsRef<str>) -> String {
    utf8_percent_encode(repo_name.as_ref(), RESERVED_CHARS).to_string()
}

pub fn decode_repo_name(repo_name_encoded: impl AsRef<str>) -> Result<String> {
    Ok(percent_decode_str(repo_name_encoded.as_ref())
        .decode_utf8()
        .context("Repo name must be utf-8 percent encoded")?
        .to_owned()
        .to_string())
}