/*
 * 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::fmt;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::SystemTime;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::format_err;
use anyhow::Context;
use anyhow::Result;
use async_runtime::try_block_unless_interrupted as block_on;
use futures::stream;
use futures::try_join;
use futures::Stream;
use futures::StreamExt;
use io::IO;
use manifest::FileMetadata;
use manifest::FileType;
use manifest::Manifest;
use manifest_tree::Diff;
use manifest_tree::ReadTreeManifest;
use manifest_tree::TreeManifest;
use minibytes::Bytes;
use parking_lot::Mutex;
use pathmatcher::AlwaysMatcher;
use pathmatcher::Matcher;
use pathmatcher::UnionMatcher;
use progress_model::ProgressBar;
use progress_model::Registry;
use repo::repo::Repo;
use storemodel::ReadFileContents;
use tracing::debug;
use tracing::instrument;
use tracing::warn;
use treestate::dirstate;
use treestate::filestate::FileStateV2;
use treestate::filestate::StateFlags;
use treestate::treestate::TreeState;
use types::hgid::NULL_ID;
use types::HgId;
use types::Key;
use types::RepoPath;
use types::RepoPathBuf;
use vfs::AsyncVfsWriter;
use vfs::UpdateFlag;
use vfs::VFS;
use workingcopy::sparse;
use workingcopy::workingcopy::WorkingCopy;

#[allow(dead_code)]
mod actions;
pub mod clone;
#[allow(dead_code)]
mod conflict;
#[allow(dead_code)]
mod merge;

pub use actions::Action;
pub use actions::ActionMap;
use configmodel::Config;
use configmodel::ConfigExt;
pub use conflict::Conflict;
pub use merge::Merge;
pub use merge::MergeResult;
use status::FileStatus;
use status::Status;
use tokio::runtime::Handle;

const VFS_BATCH_SIZE: usize = 100;

type ArcMatcher = Arc<dyn Matcher + Sync + Send>;

/// Contains lists of files to be removed / updated during checkout.
pub struct CheckoutPlan {
    /// Files to be removed.
    remove: Vec<RepoPathBuf>,
    /// Files that needs their content updated.
    update_content: Vec<UpdateContentAction>,
    filtered_update_content: Vec<UpdateContentAction>,
    /// Files that only need X flag updated.
    update_meta: Vec<UpdateMetaAction>,
    progress: Option<Mutex<CheckoutProgress>>,
    checkout: Checkout,
}

struct CheckoutProgress {
    file: File,
    vfs: VFS,
    /// Recording of the file time and size that have already been written.
    state: HashMap<RepoPathBuf, (HgId, u128, u64)>,
}

/// Update content and (possibly) metadata on the file
#[derive(Clone, Debug)]
struct UpdateContentAction {
    /// Path to file.
    path: RepoPathBuf,
    /// If content has changed, HgId of new content.
    content_hgid: HgId,
    /// New file type.
    file_type: FileType,
    /// Whether this is a new file.
    new_file: bool,
}

/// Only update metadata on the file, do not update content
#[derive(Debug)]
struct UpdateMetaAction {
    /// Path to file.
    path: RepoPathBuf,
    /// true if need to set executable flag, false if need to remove it.
    set_x_flag: bool,
}

#[derive(Default)]
pub struct CheckoutStats {
    removed: AtomicUsize,
    updated: AtomicUsize,
    meta_updated: AtomicUsize,
    written_bytes: AtomicUsize,
}

const DEFAULT_CONCURRENCY: usize = 16;
const MAX_CHECK_UNKNOWN: usize = 5000;

#[derive(Clone)]
pub struct Checkout {
    vfs: VFS,
    concurrency: usize,
}

impl Checkout {
    pub fn default_config(vfs: VFS) -> Self {
        Self {
            vfs,
            concurrency: DEFAULT_CONCURRENCY,
        }
    }

    pub fn from_config(vfs: VFS, config: &dyn Config) -> Result<Self> {
        let concurrency = config
            .get_opt("nativecheckout", "concurrency")
            .map_err(|e| format_err!("Failed to parse nativecheckout.concurrency: {}", e))?;
        let concurrency = concurrency.unwrap_or(DEFAULT_CONCURRENCY);
        Ok(Self { vfs, concurrency })
    }

    pub fn plan_action_map(&self, map: ActionMap) -> CheckoutPlan {
        CheckoutPlan::from_action_map(self.clone(), map)
    }
}

impl CheckoutPlan {
    fn from_action_map(checkout: Checkout, map: ActionMap) -> Self {
        let mut remove = vec![];
        let mut update_content = vec![];
        let mut update_meta = vec![];
        for (path, action) in map.into_iter() {
            match action {
                Action::Remove => remove.push(path),
                Action::UpdateExec(set_x_flag) => {
                    update_meta.push(UpdateMetaAction { path, set_x_flag })
                }
                Action::Update(up) => {
                    update_content.push(UpdateContentAction::new(path, up.to, up.from.is_none()))
                }
            }
        }
        let filtered_update_content = update_content.clone();
        Self {
            remove,
            update_content,
            filtered_update_content,
            update_meta,
            progress: None,
            checkout,
        }
    }

    pub fn add_progress(&mut self, path: &Path) -> Result<()> {
        let vfs = &self.checkout.vfs;
        let progress = if path.exists() {
            match CheckoutProgress::load(path, vfs.clone()) {
                Ok(p) => p,
                Err(e) => {
                    debug!("Failed to load CheckoutProgress with {:?}", e);
                    CheckoutProgress::new(path, vfs.clone())?
                }
            }
        } else {
            CheckoutProgress::new(path, vfs.clone())?
        };
        self.filtered_update_content = progress.filter_already_written(&self.update_content);
        self.progress = Some(Mutex::new(progress));
        Ok(())
    }

    /// Applies plan to the root using store to fetch data.
    /// This async function offloads file system operation to tokio blocking thread pool.
    /// It limits number of concurrent fs operations to Checkout::concurrency.
    ///
    /// This function also designed to leverage async storage API.
    /// When updating content of the file/symlink, this function first creates list of HgId
    /// it needs to fetch. This list is then converted to stream and fed into storage for fetching
    ///
    /// As storage starts returning blobs of data, we start to kick off fs write operations in
    /// the tokio async worker pool. If more then Checkout::concurrency fs operations are pending, we
    /// stop polling storage stream, until one of pending fs operations complete
    ///
    /// This function fails fast and returns error when first checkout operation fails.
    /// Pending storage futures are dropped when error is returned
    pub async fn apply_store(
        &self,
        store: &dyn ReadFileContents<Error = anyhow::Error>,
    ) -> Result<CheckoutStats> {
        let vfs = &self.checkout.vfs;
        debug!(
            "Skipping checking out {} files since they're already written",
            self.update_content.len() - self.filtered_update_content.len()
        );
        let total = self.filtered_update_content.len() + self.remove.len() + self.update_meta.len();
        let bar = &ProgressBar::new("Updating", total as u64, "files");
        Registry::main().register_progress_bar(bar);
        let async_vfs = &AsyncVfsWriter::spawn_new(vfs.clone(), 16);
        let stats = CheckoutStats::default();
        let stats_ref = &stats;

        let remove_files = stream::iter(self.remove.clone().into_iter())
            .chunks(VFS_BATCH_SIZE)
            .map(|paths| Self::remove_files(async_vfs, stats_ref, paths, bar));
        let remove_files = remove_files.buffer_unordered(self.checkout.concurrency);

        Self::process_work_stream(remove_files).await?;

        let actions: HashMap<_, _> = self
            .filtered_update_content
            .iter()
            .map(|u| (u.make_key(), u.clone()))
            .collect();
        let keys: Vec<_> = actions.keys().cloned().collect();

        let data_stream = store.read_file_contents(keys).await;

        let update_content = data_stream.map(|result| -> Result<_> {
            let (data, key) = result?;
            let action = actions
                .get(&key)
                .ok_or_else(|| format_err!("Storage returned unknown key {}", key))?;
            let path = action.path.clone();
            let flag = type_to_flag(&action.file_type);
            Ok((path, action.content_hgid, data, flag))
        });

        let progress_ref = self.progress.as_ref();
        let update_content = update_content
            .chunks(VFS_BATCH_SIZE)
            .map(|actions| async move {
                let actions: Result<Vec<_>, _> = actions.into_iter().collect();
                Self::write_files(async_vfs, stats_ref, actions?, progress_ref, bar).await
            });

        let update_content = update_content.buffer_unordered(self.checkout.concurrency);

        let update_meta = stream::iter(self.update_meta.iter()).map(|action| {
            Self::set_exec_on_file(async_vfs, stats_ref, &action.path, action.set_x_flag, bar)
        });
        let update_meta = update_meta.buffer_unordered(self.checkout.concurrency);

        let update_content = Self::process_work_stream(update_content);
        let update_meta = Self::process_work_stream(update_meta);

        try_join!(update_content, update_meta)?;

        Ok(stats)
    }

    #[instrument(skip_all, err)]
    pub fn blocking_apply_store(
        &self,
        store: &dyn ReadFileContents<Error = anyhow::Error>,
    ) -> Result<CheckoutStats> {
        block_on(self.apply_store(store))
    }

    pub async fn apply_store_dry_run(
        &self,
        store: &dyn ReadFileContents<Error = anyhow::Error>,
    ) -> Result<(usize, u64)> {
        let keys = self
            .filtered_update_content
            .iter()
            .map(UpdateContentAction::make_key);
        let mut stream = store.read_file_contents(keys.collect()).await;
        let (mut count, mut size) = (0, 0);
        while let Some(result) = stream.next().await {
            let (bytes, _) = result?;
            count += 1;
            size += bytes.len() as u64;
        }
        Ok((count, size))
    }

    pub fn check_conflicts(&self, status: &Status) -> Vec<&RepoPath> {
        let mut conflicts = vec![];
        for file in self.all_files() {
            // Unknown files are handled separately in check_unknown_files
            if !matches!(status.status(file), None | Some(FileStatus::Unknown)) {
                conflicts.push(file.as_repo_path());
            }
        }
        conflicts
    }

    pub async fn check_unknown_files(
        &self,
        manifest: &impl Manifest,
        store: &dyn ReadFileContents<Error = anyhow::Error>,
        tree_state: &mut TreeState,
        status: &Status,
    ) -> Result<Vec<RepoPathBuf>> {
        let vfs = &self.checkout.vfs;
        let mut check_content = vec![];

        let new_files: Vec<_> = self.new_file_actions().collect();

        let bar = ProgressBar::register_new("Checking untracked", new_files.len() as u64, "files");
        for file_action in new_files {
            let file: &RepoPathBuf = &file_action.path;

            bar.increase_position(1);
            if !matches!(status.status(file), Some(FileStatus::Unknown)) {
                continue;
            }
            bar.set_message(file.to_string());

            let state = if vfs.case_sensitive() {
                tree_state.get(file)?
            } else {
                let matches = tree_state.get_keys_ignorecase(file)?;
                let mut matches = matches.into_iter();
                let next = matches.next();
                match next {
                    None => None,
                    Some(next) => {
                        if let Some(extra) = matches.next() {
                            warn!(
                                "TreeState::get_ignorecase found multiple files on case insensitive fs for {}: {:?}, {:?}",
                                file, next, extra
                            );
                        }
                        tree_state.get(next)?
                    }
                }
            };
            let unknown = match state {
                None => true,
                Some(state) => !state.state.intersects(
                    StateFlags::EXIST_P1 | StateFlags::EXIST_P2 | StateFlags::EXIST_NEXT,
                ),
            };
            if unknown && matches!(vfs.is_file(file), Ok(true)) {
                let repo_path = file.as_repo_path();
                let hgid = match manifest.get_file(repo_path)? {
                    Some(m) => m.hgid,
                    None => bail!(
                        "{} not found in manifest when checking for unknown files",
                        repo_path
                    ),
                };
                let key = Key::new(file.clone(), hgid);
                check_content.push(key);
            }
        }

        if check_content.len() > MAX_CHECK_UNKNOWN {
            warn!(
                "Working directory has {} untracked files, not going to check their content. Use --clean to overwrite files without checking",
                check_content.len()
            );
            let unknowns = check_content.into_iter().map(|k| k.path).collect();
            return Ok(unknowns);
        }

        let check_content = store
            .read_file_contents(check_content)
            .await
            .chunks(VFS_BATCH_SIZE)
            .map(|v| {
                let vfs = vfs.clone();
                Handle::current().spawn_blocking(move || -> Result<Vec<RepoPathBuf>> {
                    let v: std::result::Result<Vec<_>, _> = v.into_iter().collect();
                    Self::check_content(&vfs, v?)
                })
            })
            .buffer_unordered(self.checkout.concurrency)
            .map(|r| r?);

        let unknowns = Self::process_vec_work_stream(check_content).await?;

        Ok(unknowns)
    }

    /// Drains stream returning error if one of futures fail
    async fn process_work_stream<S: Stream<Item = Result<()>> + Unpin>(
        mut stream: S,
    ) -> Result<()> {
        while let Some(result) = stream.next().await {
            result?;
        }
        Ok(())
    }

    async fn process_vec_work_stream<R: Send, S: Stream<Item = Result<Vec<R>>> + Unpin>(
        mut stream: S,
    ) -> Result<Vec<R>> {
        let mut r = vec![];
        while let Some(result) = stream.next().await {
            r.append(&mut result?);
        }
        Ok(r)
    }

    fn check_content(vfs: &VFS, files: Vec<(Bytes, Key)>) -> Result<Vec<RepoPathBuf>> {
        let mut result = vec![];
        for file in files {
            let path = &file.1.path;
            match Self::check_file(vfs, file.0, path) {
                Err(err) => {
                    warn!("Can not check {}: {}", path, err);
                    result.push(path.clone())
                }
                Ok(false) => result.push(path.clone()),
                Ok(true) => {}
            }
        }
        Ok(result)
    }

    fn check_file(vfs: &VFS, expected_content: Bytes, path: &RepoPath) -> Result<bool> {
        let actual_content = vfs.read(path)?;
        Ok(actual_content.eq(&expected_content))
    }

    // Functions below use blocking fs operations in spawn_blocking proc.
    // As of today tokio::fs operations do the same.
    // Since we do multiple fs calls inside, it is beneficial to 'pack'
    // all of them into single spawn_blocking.
    async fn write_files(
        async_vfs: &AsyncVfsWriter,
        stats: &CheckoutStats,
        actions: Vec<(RepoPathBuf, HgId, Bytes, UpdateFlag)>,
        progress: Option<&Mutex<CheckoutProgress>>,
        bar: &Arc<ProgressBar>,
    ) -> Result<()> {
        let count = actions.len();

        let first_file = actions
            .get(0)
            .expect("Cant have empty actions in write_files")
            .0
            .to_string();
        bar.set_message(first_file);

        let paths: Vec<_> = actions
            .iter()
            .map(|(path, hgid, _, _)| (hgid.clone(), path.as_repo_path().to_owned()))
            .collect();
        let actions = actions
            .into_iter()
            .map(|(path, _, content, flag)| (path, content, flag));
        let w = async_vfs.write_batch(actions).await?;
        stats.updated.fetch_add(count, Ordering::Relaxed);
        stats.written_bytes.fetch_add(w, Ordering::Relaxed);

        if let Some(progress) = progress {
            progress.lock().record_writes(paths);
            fail::fail_point!("checkout-post-progress", |_| { bail!("oh no!") });
        }
        bar.increase_position(count as u64);

        Ok(())
    }

    async fn remove_files(
        async_vfs: &AsyncVfsWriter,
        stats: &CheckoutStats,
        paths: Vec<RepoPathBuf>,
        bar: &Arc<ProgressBar>,
    ) -> Result<()> {
        let count = paths.len();
        async_vfs.remove_batch(paths).await?;
        stats.removed.fetch_add(count, Ordering::Relaxed);
        bar.increase_position(count as u64);
        Ok(())
    }

    async fn set_exec_on_file(
        async_vfs: &AsyncVfsWriter,
        stats: &CheckoutStats,
        path: &RepoPath,
        flag: bool,
        bar: &Arc<ProgressBar>,
    ) -> Result<()> {
        async_vfs
            .set_executable(path.to_owned(), flag)
            .await
            .context(format!("Updating exec on {}", path))?;
        stats.meta_updated.fetch_add(1, Ordering::Relaxed);
        bar.increase_position(1);
        Ok(())
    }

    pub fn removed_files(&self) -> impl Iterator<Item = &RepoPathBuf> {
        self.remove.iter()
    }

    pub fn updated_content_files(&self) -> impl Iterator<Item = &RepoPathBuf> {
        self.update_content.iter().map(|u| &u.path)
    }

    pub fn updated_meta_files(&self) -> impl Iterator<Item = &RepoPathBuf> {
        self.update_meta.iter().map(|u| &u.path)
    }

    fn new_file_actions(&self) -> impl Iterator<Item = &UpdateContentAction> {
        // todo - index new files so that this function don't need to be O(total_files_changed)test-update-names.t.err
        self.filtered_update_content.iter().filter(|u| u.new_file)
    }

    pub fn all_files(&self) -> impl Iterator<Item = &RepoPathBuf> {
        self.update_content
            .iter()
            .map(|u| &u.path)
            .chain(self.remove.iter())
            .chain(self.update_meta.iter().map(|u| &u.path))
    }

    /// Returns (updated, removed)
    pub fn stats(&self) -> (usize, usize) {
        (
            self.update_meta.len() + self.update_content.len(),
            self.remove.len(),
        )
    }

    pub fn vfs(&self) -> &VFS {
        &self.checkout.vfs
    }

    #[cfg(test)]
    pub fn empty(vfs: VFS) -> Self {
        Self {
            remove: vec![],
            update_content: vec![],
            filtered_update_content: vec![],
            update_meta: vec![],
            progress: None,
            checkout: Checkout::default_config(vfs),
        }
    }
}

impl CheckoutProgress {
    pub fn new(path: &Path, vfs: VFS) -> Result<Self> {
        Ok(CheckoutProgress {
            file: util::file::create(path)?,
            vfs,
            state: HashMap::new(),
        })
    }

    /// Loads the serialized checkout progress from disk. The format is one row per file written,
    /// consisting of space separated hg file hash, mtime in milliseconds, file length, and file
    /// path and a trailing \0 character.
    ///
    ///   <40_char_hg_hash> <mtime_in_millis> <written_file_length> <file_path>\0
    ///
    pub fn load(path: &Path, vfs: VFS) -> Result<Self> {
        let mut state: HashMap<RepoPathBuf, (HgId, u128, u64)> = HashMap::new();

        let file = util::file::open(path, "r")?;
        let mut reader = BufReader::new(file);
        let mut buffer = vec![];
        loop {
            reader.read_until(0, &mut buffer)?;
            if buffer.is_empty() {
                break;
            }
            let (path, (hgid, time, size)) = match (|| -> Result<_> {
                let mut split = buffer.splitn(4, |c| *c == b' ');
                let hgid = HgId::from_hex(
                    split
                        .next()
                        .ok_or_else(|| anyhow!("invalid checkout update hgid format"))?,
                )?;

                let time = std::str::from_utf8(
                    split
                        .next()
                        .ok_or_else(|| anyhow!("invalid checkout update time format"))?,
                )?
                .parse::<u128>()?;

                let size = std::str::from_utf8(
                    split
                        .next()
                        .ok_or_else(|| anyhow!("invalid checkout update size format"))?,
                )?
                .parse::<u64>()?;

                let path = split
                    .next()
                    .ok_or_else(|| anyhow!("invalid checkout update path format"))?;
                let path = &path[..path.len() - 1];
                let path = RepoPathBuf::from_string(std::str::from_utf8(path)?.to_string())?;

                Ok((path, (hgid, time, size)))
            })() {
                Ok(entry) => entry,
                Err(_) => {
                    buffer.clear();
                    continue;
                }
            };

            state.insert(path, (hgid, time, size));
            buffer.clear();
        }

        Ok(CheckoutProgress {
            file: util::file::open(path, "ca")?,
            vfs,
            state,
        })
    }

    fn record_writes(&mut self, paths: Vec<(HgId, RepoPathBuf)>) {
        for (hgid, path) in paths.into_iter() {
            // Don't report write failures, just let the checkout continue.
            let _ = (|| -> Result<()> {
                let stat = self.vfs.metadata(&path)?;
                let time = stat
                    .modified()?
                    .duration_since(SystemTime::UNIX_EPOCH)?
                    .as_millis();

                self.file
                    .write_all(
                        format!("{} {} {} {}\0", hgid.to_hex(), time, stat.len(), path).as_bytes(),
                    )
                    .map_err(|e| e.into())
            })();
        }
    }

    fn filter_already_written<'a>(
        &self,
        actions: &[UpdateContentAction],
    ) -> Vec<UpdateContentAction> {
        // TODO: This should be done in parallel. Maybe with the new vfs async batch APIs?
        let bar = ProgressBar::register_new("Filtering existing", actions.len() as u64, "files");
        actions
            .iter()
            .filter(move |action| {
                let path = &action.path;
                if let Some((hgid, time, size)) = &self.state.get(path) {
                    if *hgid != action.content_hgid {
                        return true;
                    }

                    bar.increase_position(1);
                    bar.set_message(path.to_string());

                    if let Ok(stat) = self.vfs.metadata(path) {
                        let time_matches = stat
                            .modified()
                            .map(|t| {
                                t.duration_since(SystemTime::UNIX_EPOCH)
                                    .map(|d| d.as_millis() == *time)
                                    .unwrap_or(false)
                            })
                            .unwrap_or(false);
                        if time_matches && &stat.len() == size {
                            return false;
                        }
                    }
                }
                true
            })
            .map(|a| a.clone())
            .collect()
    }
}

fn type_to_flag(ft: &FileType) -> UpdateFlag {
    match ft {
        FileType::Regular => UpdateFlag::Regular,
        FileType::Executable => UpdateFlag::Executable,
        FileType::Symlink => UpdateFlag::Symlink,
        FileType::GitSubmodule => {
            panic!("bug: GitSubmodule should be filtered out earlier by ActionMap")
        }
    }
}

impl UpdateContentAction {
    pub fn new(path: RepoPathBuf, meta: FileMetadata, new_file: bool) -> Self {
        Self {
            path,
            content_hgid: meta.hgid,
            file_type: meta.file_type,
            new_file,
        }
    }

    pub fn make_key(&self) -> Key {
        Key::new(self.path.clone(), self.content_hgid)
    }
}

impl AsRef<RepoPath> for UpdateContentAction {
    fn as_ref(&self) -> &RepoPath {
        &self.path
    }
}

impl AsRef<RepoPath> for UpdateMetaAction {
    fn as_ref(&self) -> &RepoPath {
        &self.path
    }
}

#[cfg(test)]
// todo - consider moving some of this code to vfs / separate test create
// todo parallel execution for the test
mod test {
    use std::collections::HashMap;
    use std::fs::create_dir;
    use std::path::Path;

    use anyhow::ensure;
    use anyhow::Context;
    use futures::stream::BoxStream;
    use manifest_tree::testutil::make_tree_manifest_from_meta;
    use manifest_tree::testutil::TestStore;
    use manifest_tree::Diff;
    use pathmatcher::AlwaysMatcher;
    use quickcheck::Arbitrary;
    use quickcheck::Gen;
    use tempfile::TempDir;
    use types::testutil::generate_repo_paths;
    use walkdir::DirEntry;
    use walkdir::WalkDir;

    use super::*;

    #[tokio::test]
    async fn test_basic_checkout() -> Result<()> {
        // Pattern - lowercase_path_[hgid!=1]_[flags!=normal]
        let a = (rp("A"), FileMetadata::regular(hgid(1)));
        let a_2 = (rp("A"), FileMetadata::regular(hgid(2)));
        let a_e = (rp("A"), FileMetadata::executable(hgid(1)));
        let a_s = (rp("A"), FileMetadata::symlink(hgid(1)));
        let b = (rp("B"), FileMetadata::regular(hgid(1)));
        let ab = (rp("A/B"), FileMetadata::regular(hgid(1)));
        let cd = (rp("C/D"), FileMetadata::regular(hgid(1)));

        // update file
        assert_checkout(&[a.clone()], &[a_2.clone()]).await?;
        // mv file
        assert_checkout(&[a.clone()], &[b.clone()]).await?;
        // add / rm file
        assert_checkout_symmetrical(&[a.clone()], &[a.clone(), b.clone()]).await?;
        // regular<->exec
        assert_checkout_symmetrical(&[a.clone()], &[a_e.clone()]).await?;
        // regular<->symlink
        assert_checkout_symmetrical(&[a.clone()], &[a_s.clone()]).await?;
        // dir <-> file with the same name
        assert_checkout_symmetrical(&[ab.clone()], &[a.clone()]).await?;
        // create / rm dir
        assert_checkout_symmetrical(&[ab.clone()], &[b.clone()]).await?;
        // mv file between dirs
        assert_checkout(&[ab.clone()], &[cd.clone()]).await?;

        Ok(())
    }

    #[tokio::test]
    async fn test_checkout_generated() -> Result<()> {
        let trees = generate_trees(6, 50);
        for a in trees.iter() {
            for b in trees.iter() {
                if a == b {
                    continue;
                }
                assert_checkout(a, b).await?;
            }
        }
        Ok(())
    }

    #[test]
    fn test_progress_parsing() -> Result<()> {
        let tempdir = tempfile::tempdir()?;
        let working_path = tempdir.path().to_path_buf().join("workingdir");
        create_dir(working_path.as_path()).unwrap();
        let vfs = VFS::new(working_path.clone())?;
        let path = tempdir.path().to_path_buf().join("updateprogress");
        let mut progress = CheckoutProgress::new(&path, vfs.clone())?;
        let file_path = RepoPathBuf::from_string("file".to_string())?;
        vfs.write(file_path.as_repo_path(), &[0b0, 0b01], UpdateFlag::Regular)?;
        let id = hgid(1);
        progress.record_writes(vec![(id, file_path.clone())]);

        let progress = CheckoutProgress::load(&path, vfs.clone())?;
        assert_eq!(progress.state.len(), 1);
        assert_eq!(progress.state.get(&file_path).unwrap().0, id);
        Ok(())
    }

    fn generate_trees(tree_size: usize, count: usize) -> Vec<Vec<(RepoPathBuf, FileMetadata)>> {
        let mut result = vec![];
        let mut gen = Gen::new(5);
        let paths = generate_repo_paths(tree_size * count, &mut gen);

        for i in 0..count {
            let mut tree = vec![];
            for idx in 0..tree_size {
                let meta = FileMetadata::arbitrary(&mut gen);
                let path = paths.get(i * tree_size / 2 + idx).unwrap().clone();
                tree.push((path, meta));
            }
            result.push(tree)
        }
        result
    }

    fn rp(p: &str) -> RepoPathBuf {
        RepoPathBuf::from_string(p.to_string()).unwrap()
    }

    fn hgid(p: u8) -> HgId {
        let mut r = HgId::default().into_byte_array();
        r[0] = p;
        HgId::from_byte_array(r)
    }

    async fn assert_checkout_symmetrical(
        a: &[(RepoPathBuf, FileMetadata)],
        b: &[(RepoPathBuf, FileMetadata)],
    ) -> Result<()> {
        assert_checkout(a, b).await?;
        assert_checkout(b, a).await
    }

    async fn assert_checkout(
        from: &[(RepoPathBuf, FileMetadata)],
        to: &[(RepoPathBuf, FileMetadata)],
    ) -> Result<()> {
        let tempdir = tempfile::tempdir()?;
        if let Err(e) = assert_checkout_impl(from, to, &tempdir).await {
            eprintln!("===");
            eprintln!("Failed transitioning from tree");
            print_tree(&from);
            eprintln!("To tree");
            print_tree(&to);
            eprintln!("===");
            eprintln!(
                "Working directory: {} (not deleted)",
                tempdir.into_path().display()
            );
            return Err(e);
        }
        Ok(())
    }

    async fn assert_checkout_impl(
        from: &[(RepoPathBuf, FileMetadata)],
        to: &[(RepoPathBuf, FileMetadata)],
        tempdir: &TempDir,
    ) -> Result<()> {
        let working_path = tempdir.path().to_path_buf().join("workingdir");
        create_dir(working_path.as_path()).unwrap();
        let vfs = VFS::new(working_path.clone())?;
        roll_out_fs(&vfs, from)?;

        let store = Arc::new(TestStore::new());
        let matcher = AlwaysMatcher::new();
        let left_tree = make_tree_manifest_from_meta(store.clone(), from.iter().cloned());
        let right_tree = make_tree_manifest_from_meta(store, to.iter().cloned());
        let diff = Diff::new(&left_tree, &right_tree, &matcher).unwrap();
        let vfs = VFS::new(working_path.clone())?;
        let checkout = Checkout::default_config(vfs);
        let plan = checkout
            .plan_action_map(ActionMap::from_diff(diff).context("Plan construction failed")?);

        // Use clean vfs for test
        plan.apply_store(&DummyFileContentStore)
            .await
            .context("Plan execution failed")?;

        assert_fs(&working_path, to)
    }

    fn print_tree(t: &[(RepoPathBuf, FileMetadata)]) {
        for (path, meta) in t {
            eprintln!("{} [{:?}]", path, meta);
        }
    }

    fn roll_out_fs(vfs: &VFS, files: &[(RepoPathBuf, FileMetadata)]) -> Result<()> {
        for (path, meta) in files {
            let flag = type_to_flag(&meta.file_type);
            let data = hgid_file(&meta.hgid);
            vfs.write(path.as_repo_path(), &data, flag)?;
        }
        Ok(())
    }

    fn assert_fs(root: &Path, expected: &[(RepoPathBuf, FileMetadata)]) -> Result<()> {
        let mut expected: HashMap<_, _> = expected.iter().cloned().collect();
        for dir in WalkDir::new(root).into_iter() {
            let dir = dir?;
            if dir.file_type().is_dir() {
                assert_not_empty_dir(&dir)?;
                continue;
            }
            let rel_path = dir.path().strip_prefix(root)?;
            let rel_path = into_repo_path(rel_path.to_string_lossy().into_owned());
            let rel_path = RepoPathBuf::from_string(rel_path)?;
            let expected_meta = if let Some(m) = expected.remove(&rel_path) {
                m
            } else {
                bail!("Checkout created unexpected file {}", rel_path);
            };
            assert_metadata(&expected_meta, &dir)?;
        }
        if !expected.is_empty() {
            bail!(
                "Some files are not present after checkout: {:?}",
                expected.keys().collect::<Vec<_>>()
            );
        }
        Ok(())
    }

    #[cfg(not(windows))]
    fn into_repo_path(path: String) -> String {
        path
    }

    #[cfg(windows)]
    fn into_repo_path(path: String) -> String {
        path.replace("\\", "/")
    }

    fn assert_not_empty_dir(dir: &DirEntry) -> Result<()> {
        let mut rd = std::fs::read_dir(dir.path())?;
        if rd.next().is_none() {
            bail!("Unexpected empty dir: {}", dir.path().display())
        }
        Ok(())
    }

    fn assert_metadata(expected: &FileMetadata, actual: &DirEntry) -> Result<()> {
        match expected.file_type {
            FileType::Regular => assert_regular(actual),
            FileType::Executable => assert_exec(actual),
            FileType::Symlink => assert_symlink(actual),
            FileType::GitSubmodule => Ok(()),
        }
    }

    // When compiling on unknown platform will get function not defined compile error and will need to address it

    #[cfg(unix)] // This is where PermissionsExt is defined
    fn assert_regular(actual: &DirEntry) -> Result<()> {
        use std::os::unix::fs::PermissionsExt;
        let meta = actual.metadata()?;
        ensure!(
            meta.permissions().mode() & 0o111 == 0,
            "Expected {} to be a regular file, actual mode {:#o}",
            actual.path().display(),
            meta.permissions().mode()
        );
        Ok(())
    }

    #[cfg(unix)]
    fn assert_exec(actual: &DirEntry) -> Result<()> {
        use std::os::unix::fs::PermissionsExt;
        let meta = actual.metadata()?;
        ensure!(
            meta.permissions().mode() & 0o111 != 0,
            "Expected {} to be a executable file, actual mode {:#o}",
            actual.path().display(),
            meta.permissions().mode()
        );
        Ok(())
    }

    #[cfg(unix)]
    fn assert_symlink(actual: &DirEntry) -> Result<()> {
        ensure!(
            actual.path_is_symlink(),
            "Expected {} to be a symlink",
            actual.path().display()
        );
        Ok(())
    }

    #[cfg(windows)]
    fn assert_regular(_actual: &DirEntry) -> Result<()> {
        Ok(())
    }

    #[cfg(windows)]
    fn assert_exec(_actual: &DirEntry) -> Result<()> {
        Ok(())
    }

    #[cfg(windows)]
    fn assert_symlink(_actual: &DirEntry) -> Result<()> {
        Ok(())
    }

    struct DummyFileContentStore;

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

        async fn read_file_contents(&self, keys: Vec<Key>) -> BoxStream<Result<(Bytes, Key)>> {
            stream::iter(keys)
                .map(|key| Ok((hgid_file(&key.hgid).into(), key)))
                .boxed()
        }

        async fn read_rename_metadata(
            &self,
            _keys: Vec<Key>,
        ) -> BoxStream<Result<(Key, Option<Key>), Self::Error>> {
            stream::empty().boxed()
        }
    }

    fn hgid_file(hgid: &HgId) -> Vec<u8> {
        hgid.to_string().into_bytes()
    }
}

impl fmt::Display for CheckoutPlan {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for r in &self.remove {
            writeln!(f, "rm {}", r)?;
        }
        for u in &self.update_content {
            let ft = match u.file_type {
                FileType::Executable => "(x)",
                FileType::Symlink => "(s)",
                FileType::Regular => "",
                FileType::GitSubmodule => continue,
            };
            writeln!(f, "up {}=>{}{}", u.path, u.content_hgid, ft)?;
        }
        for u in &self.update_meta {
            let ch = if u.set_x_flag { "+x" } else { "-x" };
            writeln!(f, "{} {}", ch, u.path)?;
        }
        Ok(())
    }
}

pub fn file_state(vfs: &VFS, path: &RepoPath) -> Result<FileStateV2> {
    let meta = vfs.metadata(path)?;
    #[cfg(unix)]
    let mode = std::os::unix::fs::PermissionsExt::mode(&meta.permissions());
    #[cfg(windows)]
    let mode = 0o644; // todo figure this out
    let mtime = meta
        .modified()?
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs();
    let mtime = truncate_u64("mtime", path, mtime);
    let size = meta.len();
    let size = truncate_u64("size", path, size);
    let state = StateFlags::EXIST_P1 | StateFlags::EXIST_NEXT;
    Ok(FileStateV2 {
        mode,
        size,
        mtime,
        state,
        copied: None,
    })
}

fn truncate_u64(f: &str, path: &RepoPath, v: u64) -> i32 {
    const RANGE_MASK: u64 = 0x7FFFFFFF;
    let truncated = v & RANGE_MASK;
    if truncated != v {
        warn!("{} for {} is truncated {}=>{}", f, path, v, truncated);
    }
    truncated as i32
}

pub fn checkout(
    io: &IO,
    repo: &mut Repo,
    wc: &mut WorkingCopy,
    target_commit: HgId,
) -> Result<(usize, usize)> {
    wc.ensure_locked()?;

    let current_commit = wc.parents()?.into_iter().next().unwrap_or(NULL_ID);

    let tree_resolver = repo.tree_resolver()?;
    let current_mf = tree_resolver.get(&current_commit)?;
    let target_mf = tree_resolver.get(&target_commit)?;

    let (sparse_matcher, sparse_change) =
        create_sparse_matchers(repo, wc.vfs(), &current_mf.read(), &target_mf.read())?;

    // 1. Create the plan
    let plan = create_plan(
        wc.vfs(),
        repo.config(),
        &*current_mf.read(),
        &*target_mf.read(),
        &sparse_matcher,
        sparse_change,
    )?;

    // 2. Check if status is dirty
    let status = wc.status(
        sparse_matcher.clone(),
        SystemTime::UNIX_EPOCH,
        repo.config(),
        io,
    )?;

    let conflicts = plan.check_conflicts(&status);
    if !conflicts.is_empty() {
        bail!(
            "{:?} conflicting file changes:\n {}",
            conflicts.len(),
            conflicts
                .iter()
                .take(5)
                .map(|p| p.as_str())
                .collect::<Vec<_>>()
                .join("\n "),
        );
    }

    // 3. Execute the plan
    block_on(plan.apply_store(&repo.file_store()?))?;

    // 4. Update the treestate parents, dirstate
    wc.set_parents(&mut [target_commit].iter())?;
    record_updates(&plan, &wc.vfs(), &mut wc.treestate().lock())?;
    dirstate::flush(wc.vfs().root(), &mut wc.treestate().lock(), repo.locker())?;

    Ok(plan.stats())
}

fn create_sparse_matchers(
    repo: &mut Repo,
    vfs: &VFS,
    current_mf: &TreeManifest,
    target_mf: &TreeManifest,
) -> Result<(ArcMatcher, Option<(ArcMatcher, ArcMatcher)>)> {
    let dot_path = repo.dot_hg_path().to_owned();
    if util::file::exists(dot_path.join("sparse"))?.is_none() {
        return Ok((Arc::new(AlwaysMatcher::new()), None));
    }

    let overrides = sparse::config_overrides(repo.config());

    let (current_sparse, current_hash) = sparse::repo_matcher_with_overrides(
        vfs,
        &dot_path,
        current_mf.clone(),
        repo.file_store()?,
        &overrides,
    )?
    .unwrap_or_else(|| {
        let matcher: Arc<dyn Matcher + Sync + Send> = Arc::new(AlwaysMatcher::new());
        (matcher, 0)
    });

    let (target_sparse, target_hash) = sparse::repo_matcher_with_overrides(
        vfs,
        &dot_path,
        target_mf.clone(),
        repo.file_store()?,
        &overrides,
    )?
    .unwrap_or_else(|| {
        let matcher: Arc<dyn Matcher + Sync + Send> = Arc::new(AlwaysMatcher::new());
        (matcher, 0)
    });

    let sparse_matcher: ArcMatcher = Arc::new(UnionMatcher::new(vec![
        current_sparse.clone(),
        target_sparse.clone(),
    ]));
    let sparse_change = if current_hash != target_hash {
        Some((current_sparse, target_sparse))
    } else {
        None
    };

    Ok((sparse_matcher, sparse_change))
}

fn create_plan(
    vfs: &VFS,
    config: &dyn Config,
    current_mf: &TreeManifest,
    target_mf: &TreeManifest,
    matcher: &dyn Matcher,
    sparse_change: Option<(ArcMatcher, ArcMatcher)>,
) -> Result<CheckoutPlan> {
    let diff = Diff::new(current_mf, target_mf, &matcher)?;
    let mut actions = ActionMap::from_diff(diff)?;

    if let Some((old_sparse, new_sparse)) = sparse_change {
        actions =
            actions.with_sparse_profile_change(old_sparse, new_sparse, current_mf, target_mf)?;
    }
    let checkout = Checkout::from_config(vfs.clone(), &config)?;
    let plan = checkout.plan_action_map(actions);
    // if let Some(progress_path) = progress_path {
    //     plan.add_progress(progress_path.as_path()).map_pyerr(py)?;
    // }

    Ok(plan)
}

fn record_updates(plan: &CheckoutPlan, vfs: &VFS, treestate: &mut TreeState) -> Result<()> {
    let bar = ProgressBar::register_new("recording", plan.all_files().count() as u64, "files");

    for removed in plan.removed_files() {
        treestate.remove(removed)?;
        bar.increase_position(1);
    }

    for updated in plan
        .updated_content_files()
        .chain(plan.updated_meta_files())
    {
        let fstate = file_state(vfs, updated)?;
        treestate.insert(updated, &fstate)?;
        bar.increase_position(1);
    }

    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::HashSet;
use std::fmt;

use anyhow::bail;
use anyhow::Result;
use manifest::FileType;
use manifest::FsNodeMetadata;
use manifest::Manifest;
use pathmatcher::AlwaysMatcher;
use types::RepoPathBuf;

use crate::actions::Action;
use crate::actions::ActionMap;
use crate::actions::UpdateAction;
use crate::conflict::Conflict;
use crate::conflict::ConflictState;

/// Merge operation settings
pub struct Merge {}

/// Contains result of the mere, separated by update actions and conflicts
pub struct MergeResult<M: Manifest> {
    dest: M,
    actions: ActionMap,
    conflicts: ConflictState,
}

pub enum ActionOrConflict {
    Action(Action),
    Conflict(Conflict),
}

impl Merge {
    // dest          result
    // |             |
    // |  src   =>   dest
    // |  /          |
    // base          base
    // in terms of merge.py:
    //    'dest' = local  = m1/n1/fl1
    //    'src'  = remote = m2/n2/fl2
    pub fn merge<M: Manifest + Clone>(
        &self,
        src: &M,
        dest: &M,
        base: &M,
    ) -> Result<MergeResult<M>> {
        let matcher = AlwaysMatcher::new();
        let diff = base.diff(dest, &matcher)?;
        let dest_actions = ActionMap::from_diff(diff)?;
        let diff = base.diff(src, &matcher)?;
        let src_actions = ActionMap::from_diff(diff)?;
        let dest_files: HashSet<_> = dest_actions.keys().collect();
        let src_files = src_actions.keys().collect();
        let union = dest_files.union(&src_files);
        let mut result = MergeResult::new_empty(dest.clone());
        for file in union {
            let ac = match (src_actions.get(*file), dest_actions.get(*file)) {
                (None, Some(_a)) => continue, // Already in destination
                (Some(a), None) => ActionOrConflict::Action(*a),
                (Some(s), Some(d)) => {
                    if let Some(ac) = self.resolve_conflict(*s, *d) {
                        ac
                    } else {
                        continue;
                    }
                }
                (None, None) => unreachable!(),
            };
            let file = (*file).clone();
            result.insert_new(file, ac);
        }
        Ok(result)
    }

    fn resolve_conflict(&self, src: Action, dest: Action) -> Option<ActionOrConflict> {
        Some(match (src, dest) {
            (Action::Remove, Action::Remove) => return None,
            (Action::Update(s), Action::Update(d)) => both_changed(s, d),
            (Action::UpdateExec(s), Action::UpdateExec(d)) => {
                assert!(s == d); // Can not be otherwise
                ActionOrConflict::Action(Action::UpdateExec(s))
            }

            // exists only in dest / local / m1
            (Action::Update(up), Action::Remove) => {
                ActionOrConflict::Conflict(Conflict::DstRemovedSrcChanged(up))
            }

            // mercurial handles this differently - it actually raise it as a conflict since file has "changed" and removed
            // but more logical is probably to just remove the file and ignore flag update on the other side
            (Action::UpdateExec(_), Action::Remove) => return None,

            // exists only in src / remote / m2
            (Action::Remove, Action::Update(up)) => {
                ActionOrConflict::Conflict(Conflict::SrcRemovedDstChanged(up))
            }
            // same as mirror case above, for mercurial this is a conflict
            (Action::Remove, Action::UpdateExec(_)) => ActionOrConflict::Action(Action::Remove),

            // flag conflicts - todo - implement
            (Action::Update(_), Action::UpdateExec(_)) => unimplemented!(),
            (Action::UpdateExec(_), Action::Update(_)) => unimplemented!(),
        })
    }
}

fn both_changed(src: UpdateAction, dest: UpdateAction) -> ActionOrConflict {
    assert_eq!(dest.from, src.from);
    ActionOrConflict::Conflict(Conflict::BothChanged {
        ancestor: dest.from,
        src: src.to,
        dest: dest.to,
    })
}

impl<M: Manifest> MergeResult<M> {
    pub fn new_empty(dest: M) -> Self {
        Self {
            dest,
            actions: Default::default(),
            conflicts: Default::default(),
        }
    }

    pub fn insert_new(&mut self, file: RepoPathBuf, ac: ActionOrConflict) {
        match ac {
            ActionOrConflict::Action(action) => {
                self.actions.insert(file, action);
            }
            ActionOrConflict::Conflict(conflict) => {
                self.conflicts.insert(file, conflict);
            }
        }
    }

    pub fn has_conflicts(&self) -> bool {
        !self.conflicts.is_empty()
    }

    /// If MergeResult has no conflicts, it can be directly converted into Manifest.
    pub fn try_into_manifest(self) -> Result<Option<M>> {
        if self.has_conflicts() {
            return Ok(None);
        }
        let mut m = self.dest;
        let (removes, updates): (Vec<_>, Vec<_>) = self
            .actions
            .into_iter()
            .partition(|(_, x)| matches!(x, Action::Remove));
        for (file, _) in removes {
            m.remove(&file)?;
        }
        for (file, action) in updates {
            match action {
                Action::Update(up) => {
                    m.insert(file, up.to)?;
                }
                Action::Remove => unreachable!(),
                Action::UpdateExec(up) => {
                    let meta = m.get(&file)?;
                    let mut meta = match meta {
                        Some(FsNodeMetadata::File(m)) => m,
                        Some(FsNodeMetadata::Directory(_)) => {
                            bail!(
                                "Failed to apply to manifest: {} is a directory, expected file",
                                file
                            )
                        }
                        None => bail!("Failed to apply to manifest: {} not found", file),
                    };

                    assert!(meta.file_type != FileType::Symlink);
                    if up {
                        meta.file_type = FileType::Executable;
                    } else {
                        meta.file_type = FileType::Regular;
                    }

                    m.insert(file, meta)?;
                }
            }
        }
        Ok(Some(m))
    }

    pub fn try_actions(&self) -> Option<&ActionMap> {
        if self.has_conflicts() {
            return None;
        }
        Some(&self.actions)
    }

    pub fn conflicts(&self) -> &ConflictState {
        &self.conflicts
    }

    pub fn actions(&self) -> &ActionMap {
        &self.actions
    }

    pub fn into_actions_and_conflicts(self) -> (ActionMap, ConflictState) {
        (self.actions, self.conflicts)
    }
}

impl<T: Manifest> fmt::Display for MergeResult<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}\n{}", self.actions, self.conflicts)
    }
}


/*
 * 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::ops::Deref;

use anyhow::Result;
use cliparser::parser::Flag;
use cliparser::parser::ParseOutput;
use cliparser::parser::StructFlags;
use configloader::config::ConfigSet;
use repo::repo::Repo;
use workingcopy::workingcopy::WorkingCopy;

use crate::io::IO;
use crate::OptionalRepo;
use crate::ReqCtx;

pub enum CommandFunc {
    NoRepo(Box<dyn Fn(ParseOutput, &IO, &mut ConfigSet) -> Result<u8>>),
    OptionalRepo(Box<dyn Fn(ParseOutput, &IO, &mut OptionalRepo) -> Result<u8>>),
    Repo(Box<dyn Fn(ParseOutput, &IO, &mut Repo) -> Result<u8>>),
    WorkingCopy(Box<dyn Fn(ParseOutput, &IO, &mut Repo, &mut WorkingCopy) -> Result<u8>>),
}

pub struct CommandDefinition {
    aliases: String,
    doc: String,
    flags_func: fn() -> Vec<Flag>,
    func: CommandFunc,
    synopsis: Option<String>,
}

impl CommandDefinition {
    pub fn new(
        aliases: impl ToString,
        doc: impl ToString,
        flags_func: fn() -> Vec<Flag>,
        func: CommandFunc,
        synopsis: Option<impl ToString>,
    ) -> Self {
        CommandDefinition {
            aliases: aliases.to_string(),
            doc: doc.to_string(),
            flags_func,
            func,
            synopsis: synopsis.map(|s| s.to_string()),
        }
    }

    pub fn flags(&self) -> Vec<Flag> {
        (self.flags_func)()
    }

    pub fn aliases(&self) -> &str {
        &self.aliases
    }

    pub fn doc(&self) -> &str {
        &self.doc
    }

    pub fn func(&self) -> &CommandFunc {
        &self.func
    }

    pub fn synopsis(&self) -> Option<&str> {
        self.synopsis.as_deref()
    }

    pub fn main_alias(&self) -> &str {
        if let Some(name) = self.aliases.split('|').next() {
            name
        } else {
            ""
        }
    }
}

#[derive(Default)]
pub struct CommandTable {
    commands: BTreeMap<String, CommandDefinition>,

    /// Alias name -> Command name.
    alias: BTreeMap<String, String>,
}

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

    /// Insert aliases to `alias` field.
    ///
    /// For example, `insert_aliases("config|cfg")` will insert
    /// `{"config": "config|cfg", "cfg": "config|cfg"}` to `alias`.
    fn insert_aliases<'a>(&mut self, aliases: &'a str) {
        if !aliases.contains('|') {
            return;
        }
        for name in aliases.split('|') {
            self.alias.insert(name.to_string(), aliases.to_string());
        }
    }

    /// Look up a command by name. Consider aliases.
    pub fn get(&self, name: &str) -> Option<&CommandDefinition> {
        let name = self.alias.get(name).map(AsRef::as_ref).unwrap_or(name);
        self.commands.get(name)
    }
}

impl Deref for CommandTable {
    type Target = BTreeMap<String, CommandDefinition>;

    fn deref(&self) -> &Self::Target {
        &self.commands
    }
}

pub trait Register<FN, T> {
    fn register(&mut self, f: FN, aliases: &str, doc: &str, synopsis: Option<&str>);
}

// OptionalRepo commands.
impl<S, FN> Register<FN, ((), S)> for CommandTable
where
    S: TryFrom<ParseOutput, Error = anyhow::Error> + StructFlags,
    FN: Fn(ReqCtx<S>, &mut OptionalRepo) -> Result<u8> + 'static,
{
    fn register(&mut self, f: FN, aliases: &str, doc: &str, synopsis: Option<&str>) {
        self.insert_aliases(aliases);
        let func = move |opts: ParseOutput, io: &IO, repo: &mut OptionalRepo| {
            f(ReqCtx::new(opts, io.clone())?, repo)
        };
        let func = CommandFunc::OptionalRepo(Box::new(func));
        let def = CommandDefinition::new(aliases, doc, S::flags, func, synopsis);
        self.commands.insert(aliases.to_string(), def);
    }
}

// Repo commands.
impl<S, FN> Register<FN, ((), (), S)> for CommandTable
where
    S: TryFrom<ParseOutput, Error = anyhow::Error> + StructFlags,
    FN: Fn(ReqCtx<S>, &mut Repo) -> Result<u8> + 'static,
{
    fn register(&mut self, f: FN, aliases: &str, doc: &str, synopsis: Option<&str>) {
        self.insert_aliases(aliases);
        let func = move |opts: ParseOutput, io: &IO, repo: &mut Repo| {
            f(ReqCtx::new(opts, io.clone())?, repo)
        };
        let func = CommandFunc::Repo(Box::new(func));
        let def = CommandDefinition::new(aliases, doc, S::flags, func, synopsis);
        self.commands.insert(aliases.to_string(), def);
    }
}

// NoRepo commands.
impl<S, FN> Register<FN, ((), (), (), S)> for CommandTable
where
    S: TryFrom<ParseOutput, Error = anyhow::Error> + StructFlags,
    FN: Fn(ReqCtx<S>, &mut ConfigSet) -> Result<u8> + 'static,
{
    fn register(&mut self, f: FN, aliases: &str, doc: &str, synopsis: Option<&str>) {
        self.insert_aliases(aliases);
        let func = move |opts: ParseOutput, io: &IO, config: &mut ConfigSet| {
            f(ReqCtx::new(opts, io.clone())?, config)
        };
        let func = CommandFunc::NoRepo(Box::new(func));
        let def = CommandDefinition::new(aliases, doc, S::flags, func, synopsis);
        self.commands.insert(aliases.to_string(), def);
    }
}

// WorkingCopy commands.
impl<S, FN> Register<FN, ((), (), (), (), S)> for CommandTable
where
    S: TryFrom<ParseOutput, Error = anyhow::Error> + StructFlags,
    FN: Fn(ReqCtx<S>, &mut Repo, &mut WorkingCopy) -> Result<u8> + 'static,
{
    fn register(&mut self, f: FN, aliases: &str, doc: &str, synopsis: Option<&str>) {
        self.insert_aliases(aliases);
        let func =
            move |opts: ParseOutput, io: &IO, repo: &mut Repo, working_copy: &mut WorkingCopy| {
                f(ReqCtx::new(opts, io.clone())?, repo, working_copy)
            };
        let func = CommandFunc::WorkingCopy(Box::new(func));
        let def = CommandDefinition::new(aliases, doc, S::flags, func, synopsis);
        self.commands.insert(aliases.to_string(), def);
    }
}


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

use anyhow::Result;
use cliparser::parser::ParseOutput;
use configmodel::convert::FromConfigValue;
use configmodel::Config;
use configmodel::ConfigExt;
use hgplain::is_plain;
use io::IsTty;
use io::IO;
use termlogger::TermLogger;

use crate::global_flags::HgGlobalOpts;

/// CoreContext is a container for common facilities intended to be
/// passed into upper level library code.
pub struct CoreContext {
    pub io: IO,
    pub global_opts: HgGlobalOpts,
}

/// RequestContext is a container object to organize CLI facilities.
pub struct RequestContext<O>
where
    O: TryFrom<ParseOutput, Error = anyhow::Error>,
{
    pub core: CoreContext,
    pub opts: O,
}

impl<O> RequestContext<O>
where
    O: TryFrom<ParseOutput, Error = anyhow::Error>,
{
    pub(crate) fn new(p: ParseOutput, io: IO) -> Result<Self> {
        Ok(Self {
            core: CoreContext {
                io,
                global_opts: p.clone().try_into()?,
            },
            opts: p.try_into()?,
        })
    }

    pub fn io(&self) -> &IO {
        &self.core.io
    }

    pub fn global_opts(&self) -> &HgGlobalOpts {
        &self.core.global_opts
    }

    pub fn maybe_start_pager(&self, config: &dyn Config) -> Result<()> {
        let (enable_pager, reason) =
            if bool::try_from_str(&self.core.global_opts.pager).unwrap_or(false) {
                (true, "--pager")
            } else if is_plain(Some("pager")) {
                (false, "plain")
            } else if self.core.global_opts.pager != "auto" {
                (false, "--pager")
            } else if !self.core.io.output().is_tty() {
                (false, "not tty")
            } else if !config.get_or("ui", "paginate", || true)? {
                (false, "ui.paginate")
            } else {
                (true, "auto")
            };

        tracing::debug!(enable_pager, reason, "maybe starting pager");

        if enable_pager {
            self.core.io.start_pager(config)?;
        }

        Ok(())
    }

    pub fn logger(&self) -> TermLogger {
        TermLogger::new(self.io())
            .with_quiet(self.global_opts().quiet)
            .with_verbose(self.global_opts().verbose)
    }
}


/*
 * 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::env;
use std::path::Path;
use std::sync::atomic::Ordering::SeqCst;

use anyhow::Error;
use cliparser::alias::expand_aliases;
use cliparser::alias::find_command_name;
use cliparser::parser::ParseError;
use cliparser::parser::ParseOptions;
use cliparser::parser::ParseOutput;
use cliparser::parser::StructFlags;
use configloader::config::ConfigSet;
use configmodel::Config;
use configmodel::ConfigExt;
use repo::repo::Repo;

use crate::command::CommandDefinition;
use crate::command::CommandFunc;
use crate::command::CommandTable;
use crate::errors;
use crate::errors::UnknownCommand;
use crate::global_flags::HgGlobalOpts;
use crate::io::IO;
use crate::OptionalRepo;

type Result<T, E = Error> = std::result::Result<T, E>;

/// Similar to `env::args()`. But does not panic.
pub fn args() -> Result<Vec<String>> {
    env::args_os()
        .map(|os| {
            os.into_string()
                .map_err(|_| errors::NonUTF8Arguments.into())
        })
        .collect()
}

fn add_global_flag_derived_configs(repo: &mut OptionalRepo, global_opts: HgGlobalOpts) {
    if let OptionalRepo::Some(_) = repo {
        if global_opts.hidden {
            let config = repo.config_mut();
            config.set("visibility", "all-heads", Some("true"), &"--hidden".into());
        }
    }

    let config = repo.config_mut();
    if global_opts.trace || global_opts.traceback {
        config.set("ui", "traceback", Some("on"), &"--traceback".into());
    }
    if global_opts.profile {
        config.set("profiling", "enabled", Some("true"), &"--profile".into());
    }
    if !global_opts.color.is_empty() {
        config.set(
            "ui",
            "color",
            Some(global_opts.color.as_str()),
            &"--color".into(),
        );
    }
    if global_opts.verbose || global_opts.debug || global_opts.quiet {
        config.set(
            "ui",
            "verbose",
            Some(global_opts.verbose.to_string().as_str()),
            &"--verbose".into(),
        );
        config.set(
            "ui",
            "debug",
            Some(global_opts.debug.to_string().as_str()),
            &"--debug".into(),
        );
        config.set(
            "ui",
            "quiet",
            Some(global_opts.quiet.to_string().as_str()),
            &"--quiet".into(),
        );
    }
    if global_opts.noninteractive {
        config.set("ui", "interactive", Some("off"), &"-y".into());
    }
}

fn last_chance_to_abort(opts: &HgGlobalOpts) -> Result<()> {
    if opts.profile {
        return Err(errors::Abort("--profile does not support Rust commands (yet)".into()).into());
    }

    if opts.help {
        return Err(errors::FallbackToPython("--help option requested".to_owned()).into());
    }

    Ok(())
}

fn early_parse(args: &[String]) -> Result<ParseOutput, ParseError> {
    ParseOptions::new()
        .ignore_prefix(true)
        .early_parse(true)
        .flags(HgGlobalOpts::flags())
        .flag_alias("repo", "repository")
        .parse_args(args)
}

fn parse(definition: &CommandDefinition, args: &Vec<String>) -> Result<ParseOutput, ParseError> {
    let flags = definition
        .flags()
        .into_iter()
        .chain(HgGlobalOpts::flags().into_iter())
        .collect();
    ParseOptions::new()
        .error_on_unknown_opts(true)
        .flags(flags)
        .flag_alias("repo", "repository")
        .parse_args(args)
}

fn initialize_blackbox(optional_repo: &OptionalRepo) -> Result<()> {
    if let OptionalRepo::Some(repo) = optional_repo {
        let config = repo.config();
        let max_size = config
            .get_or("blackbox", "maxsize", || {
                configloader::convert::ByteCount::from(1u64 << 12)
            })?
            .value();
        let max_files = config.get_or("blackbox", "maxfiles", || 3)?;
        let path = repo.shared_dot_hg_path().join("blackbox/v1");
        if let Ok(blackbox) = ::blackbox::BlackboxOptions::new()
            .max_bytes_per_log(max_size)
            .max_log_count(max_files as u8)
            .open(path)
        {
            ::blackbox::init(blackbox);
        }
    }
    Ok(())
}

fn initialize_indexedlog(config: &ConfigSet) -> Result<()> {
    if cfg!(unix) {
        let chmod_file = config.get_or("permissions", "chmod-file", || -1)?;
        if chmod_file >= 0 {
            indexedlog::config::CHMOD_FILE.store(chmod_file, SeqCst);
        }

        let chmod_dir = config.get_or("permissions", "chmod-dir", || -1)?;
        if chmod_dir >= 0 {
            indexedlog::config::CHMOD_DIR.store(chmod_dir, SeqCst);
        }

        let use_symlink_atomic_write: bool =
            config.get_or_default("format", "use-symlink-atomic-write")?;
        indexedlog::config::SYMLINK_ATOMIC_WRITE.store(use_symlink_atomic_write, SeqCst);
    }

    if let Some(max_chain_len) =
        config.get_opt::<u32>("storage", "indexedlog-max-index-checksum-chain-len")?
    {
        indexedlog::config::INDEX_CHECKSUM_MAX_CHAIN_LEN.store(max_chain_len, SeqCst);
    }

    let fsync: bool = config.get_or_default("storage", "indexedlog-fsync")?;
    indexedlog::config::set_global_fsync(fsync);

    Ok(())
}

pub fn parse_global_opts(args: &[String]) -> Result<HgGlobalOpts> {
    let early_result = early_parse(args)?;
    early_result.try_into()
}

pub struct Dispatcher {
    args: Vec<String>,
    early_result: ParseOutput,
    global_opts: HgGlobalOpts,
    optional_repo: OptionalRepo,
}

fn version_args(binary_path: &str) -> Vec<String> {
    vec![binary_path.to_string(), "version".to_string()]
}

impl Dispatcher {
    /// Load configs. Prepare to run a command.
    pub fn from_args(mut args: Vec<String>) -> Result<Self> {
        if args.get(1).map(|s| s.as_ref()) == Some("--version") {
            args = version_args(&args[0]);
        }

        let mut early_result = early_parse(&args[1..])?;
        let global_opts: HgGlobalOpts = early_result.clone().try_into()?;
        if global_opts.version {
            args = version_args(&args[0]);
            early_result = early_parse(&args[1..])?;
        }

        let cwd = if global_opts.cwd.is_empty() {
            Path::new(".")
        } else {
            Path::new(&global_opts.cwd)
        };
        let cwd = util::path::absolute(cwd)?;

        // Load repo and configuration.
        match OptionalRepo::from_global_opts(&global_opts, &cwd) {
            Ok(optional_repo) => Ok(Self {
                args,
                early_result,
                global_opts,
                optional_repo,
            }),
            Err(err) => {
                // If we failed to load the repo, make one last ditch effort to load a repo-less config.
                // This might allow us to run the network doctor even if this repo's dynamic config is not loadable.
                if let Ok(config) =
                    configloader::hg::load(None, &global_opts.config, &global_opts.configfile)
                {
                    Err(errors::triage_error(&config, err, None))
                } else {
                    Err(err)
                }
            }
        }
    }

    pub fn args(&self) -> &[String] {
        &self.args
    }

    /// Get a reference to the parsed config.
    pub fn config(&self) -> &ConfigSet {
        self.optional_repo.config()
    }

    /// Get a reference to the global options.
    pub fn global_opts(&self) -> &HgGlobalOpts {
        &self.global_opts
    }

    pub fn repo(&self) -> Option<&Repo> {
        match &self.optional_repo {
            OptionalRepo::Some(repo) => Some(repo),
            _ => None,
        }
    }

    /// Replace OptionalRepo::Some with OptionalRepo::None(config)
    /// where config is not influenced by the current repo.
    pub fn convert_to_repoless_config(&mut self) -> Result<()> {
        if matches!(self.optional_repo, OptionalRepo::Some(_)) {
            self.optional_repo = OptionalRepo::None(self.load_repoless_config()?)
        }

        Ok(())
    }

    fn load_repoless_config(&self) -> Result<ConfigSet> {
        configloader::hg::load(None, &self.global_opts.config, &self.global_opts.configfile)
    }

    fn default_command(&self) -> Result<String, UnknownCommand> {
        // Passing in --verbose also disables this behavior,
        // but that option is handled somewhere else
        if self.global_opts.help || hgplain::is_plain(None) {
            return Err(errors::UnknownCommand(String::new()));
        }
        Ok(if let OptionalRepo::Some(repo) = &self.optional_repo {
            repo.config().get("commands", "naked-default.in-repo")
        } else {
            self.optional_repo
                .config()
                .get("commands", "naked-default.no-repo")
        }
        .ok_or_else(|| errors::UnknownCommand(String::new()))?
        .to_string())
    }

    fn prepare_command<'a>(
        &mut self,
        command_table: &'a CommandTable,
        io: &IO,
    ) -> Result<(&'a CommandDefinition, ParseOutput)> {
        let config = self.optional_repo.config();

        if !self.global_opts.cwd.is_empty() {
            env::set_current_dir(&self.global_opts.cwd)?;
        }

        initialize_indexedlog(&config)?;

        // Prepare alias handling.
        let alias_lookup = |name: &str| {
            // [alias] can have "<name>:doc" entries that are not commands. Skip them.
            if name.contains(":") {
                return None;
            }

            match (config.get("alias", name), config.get("defaults", name)) {
                (None, None) => None,
                (Some(v), None) => Some(v.to_string()),
                (None, Some(v)) => Some(format!("{} {}", name, v.as_ref())),
                (Some(a), Some(d)) => {
                    // XXX: This makes defaults override alias if there are conflicted
                    // flags. The desired behavior is to make alias override defaults.
                    // However, [defaults] is deprecated and is likely only used
                    // by tests. So this might be fine.
                    Some(format!("{} {}", a.as_ref(), d.as_ref()))
                }
            }
        };

        let first_arg = if let Some(first_arg) = self.early_result.args.get(0) {
            first_arg.clone()
        } else {
            let default_command = self.default_command()?;
            self.args.insert(1, default_command.clone());
            self.early_result.args = vec![default_command.clone()];
            self.early_result.first_arg_index = 0;
            default_command
        };

        let args = self.args[1..].to_vec();
        let early_result = &self.early_result;
        let first_arg_index = early_result.first_arg_index();

        // This should hold true since `first_arg` is not empty (tested above).
        // Therefore positional args is non-empty and first_arg_index should be
        // an index in args.
        debug_assert!(first_arg_index < args.len());
        debug_assert_eq!(args[first_arg_index], first_arg);

        // The difference between args, expanded and new_args is:
        // - args are unchanged arguments provided by the user, unless only global options are provided.
        //   args can have global flags before command name.
        //   for example, ["--traceback", "log", "-Gvr", "master"]
        //                                      ^^^^^ first_arg_index, "log" is "command_name"
        // - expanded: includes alias expansion result
        //   no global flags before command name.
        //   for example, with alias "log = log -f", ["log", "-Gvr", "master"]
        //   will be expanded to ["log", "-f", "-Gvr", "master"].
        // - new_args: final args to parse, like expanded with global flags.
        //   ["--traceback", "log", "-f", "-Gvr", "master"].

        // If only global options are provided and some conditions are met (see `self.default_command` for details),
        // then the command/alias provided in commands.naked-default.{in|no}-repo is inserted at the beginning
        // and the difference mentioned above is kept. For instance, if :
        // - The `commands.naked-default.in-repo` config is set to "stj"
        // - The `alias.stj` config is set to "stj = status -Tjson --verbose"
        // - The command is run in a repo,
        // - The original args are ["--verbose", "--traceback"]
        // Then the final values of the variables mentioned above are:
        // - args     => ["stj", "--verbose", "--traceback"]
        // - expanded => ["status", "-Tjson", "--verbose", "--verbose", "--traceback"]
        // - new_args => ["status", "-Tjson", "--verbose", "--verbose", "--traceback"]

        let command_name = first_arg.to_string();
        let (expanded, _first_arg_index) = expand_aliases(alias_lookup, &args[first_arg_index..])?;
        let (command_name, command_arg_len) =
            find_command_name(|name| command_table.get(name).is_some(), &expanded)
                .ok_or_else(|| errors::UnknownCommand(command_name))?;
        tracing::info!(
            name = "log:command-row",
            command = AsRef::<str>::as_ref(&command_name)
        );

        let mut new_args = Vec::with_capacity(args.len());
        new_args.extend_from_slice(&args[..first_arg_index]);
        new_args.push(command_name.clone());
        new_args.extend_from_slice(&expanded[command_arg_len..]);

        let def = command_table.get(&command_name).unwrap();
        let parsed = parse(def, &new_args)?;

        let global_opts: HgGlobalOpts = parsed.clone().try_into()?;
        last_chance_to_abort(&global_opts)?;

        initialize_blackbox(&self.optional_repo)?;

        if global_opts.pager == "always" {
            io.start_pager(self.optional_repo.config())?;
        }

        Ok((def, parsed))
    }

    /// Run a command. Return exit code if the command completes.
    pub fn run_command<'a>(
        &mut self,
        command_table: &'a CommandTable,
        io: &IO,
    ) -> (Option<&'a CommandDefinition>, Result<u8>) {
        let (handler, parsed) = match self.prepare_command(command_table, io) {
            Ok((name, args)) => (name, args),
            Err(e) => return (None, Err(e)),
        };

        let res = || -> Result<u8> {
            add_global_flag_derived_configs(&mut self.optional_repo, parsed.clone().try_into()?);
            tracing::debug!("command handled by a Rust function");
            match handler.func() {
                CommandFunc::Repo(f) => f(parsed, io, self.repo_mut()?),
                CommandFunc::OptionalRepo(f) => f(parsed, io, &mut self.optional_repo),
                CommandFunc::NoRepo(f) => {
                    self.convert_to_repoless_config()?;
                    f(parsed, io, self.optional_repo.config_mut())
                }
                CommandFunc::WorkingCopy(f) => {
                    let repo = self.repo_mut()?;
                    if !repo.config().get_or_default("workingcopy", "use-rust")? {
                        tracing::warn!(
                            "command requires working copy but Rust working copy is disabled"
                        );
                        // TODO(T131699257): Migrate all tests to use Rust
                        // workingcopy and removed fallback to Python.
                        return Err(errors::FallbackToPython("requested command that uses working copy but workingcopy.use-rust not set to True".to_owned()).into());
                    }
                    let path = repo.path().to_owned();
                    let mut wc = repo.working_copy(&path)?;
                    f(parsed, io, repo, &mut wc)
                }
            }
        }();

        (Some(handler), res)
    }

    fn repo_mut(&mut self) -> Result<&mut Repo> {
        match self.optional_repo {
            OptionalRepo::Some(ref mut repo) => Ok(repo),
            OptionalRepo::None(_) => {
                // FIXME: Try to "infer repo" here.
                Err(errors::RepoRequired(
                    env::current_dir()
                        .ok()
                        .map(|p| p.to_string_lossy().to_string())
                        .unwrap_or_default(),
                )
                .into())
            }
        }
    }
}


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

use std::borrow::Cow;

use configloader::config::ConfigSet;
use configmodel::ConfigExt;
use thiserror::Error;
#[cfg(feature = "eden")]
use thrift_types::edenfs as eden;

#[derive(Debug, Error)]
#[error("cannot decode arguments")]
pub struct NonUTF8Arguments;

pub use cliparser::errors::InvalidArguments;

#[derive(Debug, Error)]
// This error message isn't user facing yet, so let's just say "sl".
#[error("unknown command '{0}'\n(use 'sl help' to get help)")]
pub struct UnknownCommand(pub String);

/// Explicitly fallback to Python code path.
///
/// Ideally this does not exist.
#[derive(Debug, Error)]
#[error("Feature not supported in Rust implementation; falling back to Python due to: {0}")]
pub struct FallbackToPython(pub String);

#[derive(Debug, Error)]
#[error("")]
pub struct FailedFallbackToPython;

#[derive(Debug, Error)]
#[error(
    "'{0}' is not inside a repository, but this command requires a repository!\n(use 'cd' to go to a directory inside a repository and try again)"
)]
pub struct RepoRequired(pub String);

#[derive(Debug, Error)]
#[error("malformed --config option: '{0}' (use --config section.name=value)")]
pub struct MalformedConfigOption(pub String);

#[derive(Debug, Error)]
#[error("{0}")]
pub struct Abort(pub Cow<'static, str>);

/// Print an error suitable for end-user consumption.
///
/// This function adds `hg:` or `abort:` to error messages.
pub fn print_error(err: &anyhow::Error, io: &crate::io::IO, _args: &[String]) {
    use cliparser::parser::ParseError;
    let cli_name = identity::cli_name();
    if err.downcast_ref::<configloader::Error>().is_some() {
        let _ = io.write_err(format!("{cli_name}: parse error: {err}\n"));
    } else if err.downcast_ref::<configloader::Errors>().is_some() {
        let _ = io.write_err(format!("{cli_name}: parse errors: {err}\n"));
    } else if let Some(ParseError::AmbiguousCommand {
        command_name: _,
        possibilities,
    }) = err.downcast_ref::<ParseError>()
    {
        let _ = io.write_err(format!("{cli_name}: {err}:\n"));
        for possibility in possibilities {
            // UX: Colorize the output once `io` can output colors.
            let _ = io.write_err(format!("     {}\n", possibility));
        }
    } else {
        #[cfg(feature = "eden")]
        {
            if let Some(eden::errors::eden_service::GetScmStatusV2Error::ex(e)) =
                err.downcast_ref::<eden::errors::eden_service::GetScmStatusV2Error>()
            {
                let _ = io.write_err(format!("abort: {}\n", e.message));
                let _ = io.flush();
                return;
            }
        }

        // Ideally we'd identify expected errors and unexpected errors and print the full {:?}
        // output for unexpected errors. Today we can't make that distinction though, so for now we
        // print it in the user-friendly way.
        let _ = io.write_err(format!("abort: {}\n", err));
    }
}

/// Optionally transform an error into something more friendly to the user.
pub fn triage_error(
    config: &ConfigSet,
    cmd_err: anyhow::Error,
    command_name: Option<&str>,
) -> anyhow::Error {
    if types::errors::is_network_error(&cmd_err)
        && config
            .get_or_default("experimental", "network-doctor")
            .unwrap_or(false)
    {
        match network_doctor::Doctor::new().diagnose(config) {
            Ok(()) => cmd_err,
            Err(diagnosis) =>
            // TODO: colorize diagnosis, vary output by verbose/quiet
            {
                anyhow::anyhow!(
                    "command failed due to network error\n\n{}\n\nDetails:\n\n{:?}\n\nOriginal error:\n\n{:?}\n",
                    diagnosis.treatment(config),
                    diagnosis,
                    cmd_err
                )
            }
        }
    } else {
        if let Some(command_name) = command_name {
            if cmd_err.is::<FallbackToPython>()
                && config
                    .get_or_default::<Vec<String>>("commands", "force-rust")
                    .map_or(false, |config| config.contains(&command_name.to_string()))
            {
                return anyhow::Error::new(FailedFallbackToPython);
            }
        }
        cmd_err
    }
}

#[cfg(all(test, feature = "eden"))]
mod tests {
    use std::io::Cursor;

    use super::*;

    #[test]
    fn test_status_error_msg() {
        // Construct error and parameters
        let error_msg = "cannot compute status while a checkout is currently in progress";
        let expected_error = format!("abort: {}\n", error_msg);

        let error: anyhow::Error =
            eden::errors::eden_service::GetScmStatusV2Error::ex(eden::EdenError {
                message: error_msg.to_string(),
                errorCode: Some(255),
                errorType: eden::EdenErrorType::CHECKOUT_IN_PROGRESS,
                ..Default::default()
            })
            .into();

        let tin = Cursor::new(Vec::new());
        let tout = Cursor::new(Vec::new());
        let terr = Cursor::new(Vec::new());
        let mut io = crate::io::IO::new(tin, tout, Some(terr));

        // Call print_error with error and in-memory IO stream
        print_error(&error, &mut io, &[] as &[String]);

        // Make sure error message is formatted correctly.
        io.with_error(|e| {
            if let Some(actual_error_wrapped) = e {
                let any = actual_error_wrapped.as_any();
                if let Some(c) = any.downcast_ref::<std::io::Cursor<Vec<u8>>>() {
                    let actual_error = c.clone().into_inner();
                    assert_eq!(String::from_utf8(actual_error).unwrap(), expected_error);
                }
            }
        });
    }
}


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

use cliparser::define_flags;

define_flags! {
    pub struct HgGlobalOpts {
        /// repository root directory or name of overlay bundle file
        #[short('R')]
        repository: String,

        /// change working directory
        cwd: String,

        /// do not prompt, automatically pick the first choice for all prompts
        #[short('y')]
        noninteractive: bool,

        /// suppress output
        #[short('q')]
        quiet: bool,

        /// enable additional output
        #[short('v')]
        verbose: bool,

        /// when to colorize (boolean, always, auto, never, or debug)
        color: String,

        /// set/override config option (use 'section.name=value')
        config: Vec<String>,

        /// enables the given config file
        configfile: Vec<String>,

        /// enable debugging output
        debug: bool,

        /// start debugger
        debugger: bool,

        /// set the charset encoding
        encoding: String,

        /// set the charset encoding mode
        encodingmode: String = "strict",

        /// do not verify server certificate
        insecure: bool,

        /// always print a traceback on exception
        traceback: bool,

        /// enable more detailed tracing
        trace: bool,

        /// time how long the command takes
        time: bool,

        /// print command execution profile
        profile: bool,

        /// output version information and exit
        version: bool,

        /// display help and exit
        #[short('h')]
        help: bool,

        /// consider hidden changesets
        hidden: bool,

        /// when to paginate (boolean, always, auto, or never)
        pager: String = "auto",

        #[args]
        args: Vec<String>,
    }
}


/*
 * 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)]

pub mod command;
pub mod context;
pub mod dispatch;
pub mod errors;
pub mod global_flags;
pub mod optional_repo;
pub mod util;

pub use context::RequestContext as ReqCtx;
pub use io;
pub use optional_repo::OptionalRepo;
pub use termlogger::TermLogger;


/*
 * 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::path::Path;

use anyhow::Result;
use configloader::config::ConfigSet;
use repo::errors;
use repo::repo::Repo;

use crate::global_flags::HgGlobalOpts;

/// Either an optional [`Repo`] which owns a [`ConfigSet`], or a [`ConfigSet`]
/// without a repo.
pub enum OptionalRepo {
    Some(Repo),
    None(ConfigSet),
}

impl OptionalRepo {
    /// Optionally load a repo from the specified "current directory".
    ///
    /// Return None if there is no repo found from the current directory or its
    /// parent directories.
    fn from_cwd(opts: &HgGlobalOpts, cwd: impl AsRef<Path>) -> Result<OptionalRepo> {
        if let Some((path, _)) = identity::sniff_root(&util::path::absolute(cwd)?)? {
            let repo = Repo::load(path, &opts.config, &opts.configfile)?;
            Ok(OptionalRepo::Some(repo))
        } else {
            Ok(OptionalRepo::None(configloader::hg::load(
                None,
                &opts.config,
                &opts.configfile,
            )?))
        }
    }

    /// Load the repo from the global --repository (or --repo, -R) flag.
    ///
    /// -R can be either a directory or a bundle file.
    pub fn from_global_opts(opts: &HgGlobalOpts, cwd: impl AsRef<Path>) -> Result<OptionalRepo> {
        let repository_path: &Path = opts.repository.as_ref();
        if repository_path.as_os_str().is_empty() {
            // --repo is not specified, only use cwd.
            return Self::from_cwd(opts, cwd);
        }

        let cwd = cwd.as_ref();
        let full_repository_path =
            if repository_path == Path::new(".") || repository_path == Path::new("") {
                cwd.to_path_buf()
            } else {
                cwd.join(repository_path)
            };
        if let Ok(path) = util::path::absolute(&full_repository_path) {
            if identity::sniff_dir(&path)?.is_some() {
                let repo = Repo::load(path, &opts.config, &opts.configfile)?;
                return Ok(OptionalRepo::Some(repo));
            } else if path.is_file() {
                // 'path' is a bundle path
                return Self::from_cwd(opts, cwd);
            }
        }
        Err(errors::RepoNotFound(repository_path.display().to_string()).into())
    }

    pub fn config_mut(&mut self) -> &mut ConfigSet {
        match self {
            OptionalRepo::Some(ref mut repo) => repo.config_mut(),
            OptionalRepo::None(ref mut config) => config,
        }
    }

    pub fn config(&self) -> &ConfigSet {
        match self {
            OptionalRepo::Some(ref repo) => repo.config(),
            OptionalRepo::None(ref config) => config,
        }
    }
}


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

#[macro_export]
macro_rules! abort_if {
    ( $cond:expr, $($arg:tt)+ ) => {
        if $cond {
            abort!($($arg)*);
        }
    };
}

#[macro_export]
macro_rules! abort {
    ( $msg:expr ) => {
        return Err(clidispatch::errors::Abort($msg.into()).into());
    };
    ( $($arg:tt)+ ) => {
        return Err(clidispatch::errors::Abort(format!($($arg)*).into()).into());
    };
}

#[macro_export]
macro_rules! fallback {
    ( $msg:expr ) => {
        return Err(clidispatch::errors::FallbackToPython($msg.into()).into());
    };
    ( $($arg:tt)+ ) => {
        return Err(clidispatch::errors::FallbackToPython(format!($($arg)*).into()).into());
    };
}

#[cfg(test)]
mod test {
    use crate as clidispatch;

    fn abort_if_simple(should_abort: bool) -> anyhow::Result<()> {
        abort_if!(should_abort, "error!");
        Ok(())
    }

    fn abort_if_format(should_abort: bool) -> anyhow::Result<()> {
        abort_if!(should_abort, "error: {}", "banana");
        Ok(())
    }

    #[test]
    fn test_abort_if() {
        assert!(abort_if_simple(false).is_ok());
        assert_eq!(format!("{}", abort_if_simple(true).unwrap_err()), "error!");

        assert!(abort_if_format(false).is_ok());
        assert_eq!(
            format!("{}", abort_if_format(true).unwrap_err()),
            "error: banana",
        );
    }

    fn abort_simple() -> anyhow::Result<()> {
        abort!("error!");
    }

    fn abort_format() -> anyhow::Result<()> {
        abort!("error: {}", "banana");
    }

    #[test]
    fn test_abort() {
        assert_eq!(format!("{}", abort_simple().unwrap_err()), "error!");
        assert_eq!(format!("{}", abort_format().unwrap_err()), "error: banana",);
    }
}


/*
 * 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 anyhow::anyhow;
use anyhow::Result;
use configmodel::Config;
use configmodel::ConfigExt;
use hostname::get_hostname;
use serde::Deserialize;
use serde::Serialize;

pub const CLIENT_INFO_HEADER: &str = "X-Client-Info";

#[cfg(fbcode_build)]
mod facebook;
#[cfg(not(fbcode_build))]
mod oss;
use facebook::get_fb_client_info;
use facebook::FbClientInfo;
#[cfg(not(fbcode_build))]
use oss as facebook;

#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct ClientInfo {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub u64token: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hostname: Option<String>,
    #[serde(flatten)]
    pub fb: FbClientInfo,
}

impl ClientInfo {
    pub fn new(config: &dyn Config) -> Result<Self> {
        let fb = get_fb_client_info();

        let u64token = config.get_opt::<u64>("clientinfo", "u64token")?;
        let hostname = get_hostname().ok();

        Ok(ClientInfo {
            u64token,
            hostname,
            fb,
        })
    }

    pub fn into_json(&self) -> Result<String> {
        serde_json::to_string(self).map_err(|e| anyhow!(e))
    }
}


/*
 * 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 serde::Deserialize;
use serde::Serialize;

#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct FbClientInfo {}

impl FbClientInfo {
    pub fn tw_job(&self) -> Option<&str> {
        None
    }

    pub fn tw_task(&self) -> Option<&str> {
        None
    }

    pub fn sandcastle_nonce(&self) -> Option<&str> {
        None
    }

    pub fn sandcastle_alias(&self) -> Option<&str> {
        None
    }
}

pub fn get_fb_client_info() -> FbClientInfo {
    FbClientInfo {}
}