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

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

use anyhow::Context;
use async_runtime::block_unless_interrupted as block_on;
use clidispatch::abort;
use clidispatch::abort_if;
use clidispatch::errors;
use clidispatch::fallback;
use clidispatch::ReqCtx;
use clidispatch::TermLogger;
use cliparser::define_flags;
use configloader::hg::resolve_custom_scheme;
use configmodel::Config;
use configmodel::ConfigExt;
use configmodel::ValueSource;
use migration::feature::deprecate;
use repo::repo::Repo;
use repo_name::encode_repo_name;
use tracing::instrument;
use types::HgId;
use url::Url;
use util::path::absolute;

use super::ConfigSet;
use super::Result;
use crate::HgPython;

static SEGMENTED_CHANGELOG_CAPABILITY: &str = "segmented-changelog";

define_flags! {
    pub struct CloneOpts {
        /// clone an empty working directory
        #[short('U')]
        noupdate: bool,

        /// revision or branch to check out
        #[short('u')]
        #[argtype("REV")]
        updaterev: String,

        /// include the specified changeset (DEPRECATED)
        #[short('r')]
        #[argtype("REV")]
        rev: String,

        /// use pull protocol to copy metadata (DEPRECATED)
        pull: bool,

        /// clone with minimal data processing (DEPRECATED)
        stream: bool,

        /// "use remotefilelog (only turn it off in legacy tests) (ADVANCED)"
        shallow: bool = true,

        /// "use git protocol (EXPERIMENTAL)"
        git: bool,

        /// enable a sparse profile
        enable_profile: Vec<String>,

        /// files to include in a sparse profile (DEPRECATED)
        include: String,

        /// files to exclude in a sparse profile (DEPRECATED)
        exclude: String,

        /// use EdenFs (EXPERIMENTAL)
        eden: bool,

        /// location of the backing repo to be used or created (EXPERIMENTAL)
        eden_backing_repo: String,

        #[arg]
        source: String,

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

impl CloneOpts {
    fn source_url(&self) -> Result<(Url, Option<String>)> {
        let mut url = Url::parse(&self.source)?;
        let frag = url.fragment().map(|f| f.to_string());
        url.set_fragment(None);
        Ok((url, frag))
    }
}

pub fn run(mut ctx: ReqCtx<CloneOpts>, config: &mut ConfigSet) -> Result<u8> {
    let mut logger = ctx.logger();

    let deprecated_options = [
        ("--rev", "rev-option", ctx.opts.rev.is_empty()),
        (
            "--include",
            "clone-include-option",
            ctx.opts.include.is_empty(),
        ),
        (
            "--exclude",
            "clone-exclude-option",
            ctx.opts.exclude.is_empty(),
        ),
    ];
    for (option_name, option_config, option_is_empty) in deprecated_options {
        if !option_is_empty {
            deprecate(
                config,
                option_config,
                format!("the {} option has been deprecated", option_name),
            )?;
        }
    }

    abort_if!(
        !ctx.opts.eden && !ctx.opts.eden_backing_repo.is_empty(),
        "--eden-backing-repo requires --eden",
    );

    abort_if!(
        !ctx.opts.enable_profile.is_empty() && ctx.opts.eden,
        "--enable-profile is not compatible with --eden",
    );

    abort_if!(
        ctx.opts.eden && ctx.opts.noupdate,
        "--noupdate is not compatible with --eden",
    );

    let force_rust = config
        .get_or_default::<Vec<String>>("commands", "force-rust")?
        .contains(&"clone".to_owned());
    let use_rust = force_rust || config.get_or_default("clone", "use-rust")?;
    if !use_rust {
        abort_if!(
            ctx.opts.eden,
            "--eden requires --config clone.use-rust=True"
        );

        logger.verbose("Falling back to Python clone (config not enabled)");
        fallback!("clone.use-rust not set to True");
    }

    let source_url = match ctx.opts.source_url() {
        Err(_) => fallback!("invalid URL"),
        Ok((url, _)) => match resolve_custom_scheme(config, url.clone())?.scheme() {
            "mononoke" | "eager" | "test" => url,
            _ => fallback!("unsupported URL scheme"),
        },
    };

    if !ctx.opts.updaterev.is_empty()
        || !ctx.opts.rev.is_empty()
        || ctx.opts.pull
        || ctx.opts.stream
        || !ctx.opts.shallow
        || ctx.opts.git
    {
        abort_if!(
            ctx.opts.eden,
            "some specified options are not compatible with --eden"
        );

        logger.verbose("Falling back to Python clone (incompatible options)");
        fallback!("one or more unsupported options in Rust clone");
    }

    config.set("paths", "default", Some(source_url.as_str()), &"arg".into());

    let reponame = match config.get_opt::<String>("remotefilelog", "reponame")? {
        // This gets the reponame from the --configfile config. Ignore
        // bogus "no-repo" value that dynamicconfig sets when there is
        // no repo name.
        Some(c) if c != "no-repo" => {
            logger.verbose(|| format!("Repo name is {} from config", c));
            c
        }
        Some(_) | None => match configloader::hg::repo_name_from_url(config, &ctx.opts.source) {
            Some(name) => {
                logger.verbose(|| format!("Repo name is {} via URL {}", name, ctx.opts.source));
                config.set(
                    "remotefilelog",
                    "reponame",
                    Some(&name),
                    &"clone source".into(),
                );
                name
            }
            None => abort!("could not determine repo name"),
        },
    };

    let destination = match ctx.opts.args.pop() {
        Some(dest) => absolute(dest).with_context(|| "Cannot get absolute destination path")?,
        None => {
            abort_if!(
                hgplain::is_plain(Some("default_clone_dir")),
                "DEST must be specified because HGPLAIN is enabled",
            );

            // Change "some/repo" into "repo". There is an argument to
            // defaulting to "some_repo" or similar if the canonical repo name
            // contains a slash, but using just "repo" probably has less
            // friction with current workflows/expectations.
            let basename = match reponame.split(&['/', '\\']).last() {
                Some(name) if !name.is_empty() => name,
                _ => abort!("invalid reponame {reponame}"),
            };

            clone::get_default_destination_directory(config)?.join(basename)
        }
    };

    logger.info(format!(
        "Cloning {} into {}",
        reponame,
        destination.display(),
    ));

    let clone_type_str = if ctx.opts.eden {
        "eden_fs"
    } else if !ctx.opts.enable_profile.is_empty() {
        "sparse"
    } else {
        "full"
    };
    tracing::trace!("performing rust clone");
    tracing::debug!(target: "clone_info", rust_clone="true", repo=reponame, clone_type=clone_type_str, is_update_clone=!ctx.opts.noupdate);
    if !ctx.opts.enable_profile.is_empty() {
        tracing::debug!(target: "clone_info", cloned_sparse_profiles=ctx.opts.enable_profile.join(" "));
    }

    if let Some(ident) = identity::sniff_dir(&destination)? {
        abort!(
            "{} directory already exists at clone destination {}",
            ident.dot_dir(),
            destination.display(),
        );
    }

    if ctx.opts.eden {
        let backing_path = if !ctx.opts.eden_backing_repo.is_empty() {
            PathBuf::from(&ctx.opts.eden_backing_repo)
        } else if let Some(dir) = clone::get_default_eden_backing_directory(config)? {
            dir.join(encode_repo_name(&reponame))
        } else {
            abort!("please specify --eden-backing-repo");
        };

        let mut backing_repo = if identity::sniff_dir(&backing_path)?.is_none() {
            logger.verbose(|| {
                format!(
                    "Cloning {} backing repo to {}",
                    reponame,
                    backing_path.display(),
                )
            });
            try_clone_metadata(&ctx, &mut logger, config, &reponame, &backing_path)?
        } else {
            Repo::load(
                &backing_path,
                &ctx.global_opts().config,
                &ctx.global_opts().configfile,
            )?
        };
        let target_rev =
            get_update_target(&mut logger, &mut backing_repo, &ctx.opts)?.map(|(rev, _)| rev);
        logger.verbose(|| {
            format!(
                "Performing EdenFS clone {}@{} from {} to {}",
                reponame,
                target_rev.map_or(String::new(), |t| t.to_hex()),
                backing_path.display(),
                destination.display(),
            )
        });
        clone::eden_clone(&backing_repo, &destination, target_rev)?;
    } else {
        let mut repo = try_clone_metadata(&ctx, &mut logger, config, &reponame, &destination)?;

        let target_rev = get_update_target(&mut logger, &mut repo, &ctx.opts)?;
        if let Some((target_rev, bm)) = &target_rev {
            logger.info(format!("Checking out '{}'", bm));
            logger.verbose(|| {
                format!(
                    "Initializing non-EdenFS working copy to commit {}",
                    target_rev.to_hex(),
                )
            });
        } else {
            logger.verbose("Initializing empty non-EdenFS working copy");
        }

        clone::init_working_copy(
            &mut logger,
            &mut repo,
            target_rev.map(|(rev, _)| rev),
            ctx.opts.enable_profile.clone(),
        )?;
    }

    Ok(0)
}

fn try_clone_metadata(
    ctx: &ReqCtx<CloneOpts>,
    logger: &mut TermLogger,
    config: &mut ConfigSet,
    reponame: &str,
    destination: &Path,
) -> Result<Repo> {
    let dest_preexists = destination.exists();
    match clone_metadata(ctx, logger, config, reponame, destination) {
        Err(e) => {
            let removal_dir = if dest_preexists {
                let ident = identity::sniff_dir(destination)?.unwrap_or_else(identity::default);
                destination.join(ident.dot_dir())
            } else {
                destination.to_path_buf()
            };
            if !ctx.global_opts().debug {
                fs::remove_dir_all(removal_dir)?;
            }
            Err(e)
        }
        Ok(repo) => Ok(repo),
    }
}

#[instrument(skip_all, fields(repo=reponame), err)]
fn clone_metadata(
    ctx: &ReqCtx<CloneOpts>,
    logger: &mut TermLogger,
    config: &mut ConfigSet,
    reponame: &str,
    destination: &Path,
) -> Result<Repo> {
    let mut includes = ctx.global_opts().configfile.clone();
    if let Some(mut repo_config) = config.get_opt::<PathBuf>("clone", "repo-specific-config-dir")? {
        repo_config.push(format!("{}.rc", encode_repo_name(reponame)));
        if repo_config.exists() {
            let repo_config = repo_config.into_os_string().into_string().unwrap();
            if !includes.contains(&repo_config) {
                includes.push(repo_config);
            }
        }
    }

    let mut repo_config_file_content = includes
        .into_iter()
        .map(|file| format!("%include {}\n", file))
        .collect::<String>();

    if !repo_config_file_content.is_empty() {
        repo_config_file_content.push('\n');
    }

    let (url, frag) = ctx.opts.source_url()?;
    if let Some(frag) = frag {
        config.set(
            "remotenames",
            "selectivepulldefault",
            Some(frag),
            &"clone source".into(),
        );
    }

    repo_config_file_content.push_str(format!("[paths]\ndefault = {}\n", url.as_str()).as_str());

    // Some config values are inherent to the repo and should be persisted if passed to clone.
    // This is analagous to persisting the --configfile args above.
    for (section, name) in &[("remotenames", "selectivepulldefault")] {
        if let Some(&ValueSource {
            ref source,
            value: Some(ref value),
            ..
        }) = config.get_sources(section, name).last()
        {
            if *source == "--config" || *source == "clone source" {
                repo_config_file_content.push_str(&format!("\n[{section}]\n{name} = {value}\n"));
            }
        }
    }

    let mut repo = Repo::init(
        destination,
        config,
        Some(repo_config_file_content),
        &ctx.global_opts().config,
    )?;
    repo.add_requirement("remotefilelog")?;

    let edenapi = repo.eden_api()?;

    let capabilities: Vec<String> =
        block_on(edenapi.capabilities())?.map_err(|e| e.tag_network())?;

    let segmented_changelog = capabilities
        .iter()
        .any(|cap| cap == SEGMENTED_CHANGELOG_CAPABILITY);

    if segmented_changelog {
        repo.add_store_requirement("lazychangelog")?;

        let bookmark_names: Vec<String> = get_selective_bookmarks(&repo)?;
        let metalog = repo.metalog()?;
        let commits = repo.dag_commits()?;
        tracing::trace!("fetching lazy commit data and bookmarks");
        let bookmark_ids = exchange::clone(
            config,
            edenapi,
            &mut metalog.write(),
            &mut commits.write(),
            bookmark_names,
        )?;
        logger.verbose(|| format!("Pulled bookmarks {:?}", bookmark_ids));
    } else {
        revlog_clone(repo.config(), logger, ctx, destination)?;
        // reload the repo to pick up any changes written out by the revlog clone
        // such as metalog remotenames writes
        repo = Repo::load(
            destination,
            &ctx.global_opts().config,
            &ctx.global_opts().configfile,
        )?;
    }

    ::fail::fail_point!("run::clone", |_| {
        abort!("Injected clone failure");
    });
    Ok(repo)
}

pub fn revlog_clone(
    config: &ConfigSet,
    logger: &mut TermLogger,
    ctx: &ReqCtx<CloneOpts>,
    root: &Path,
) -> Result<()> {
    let mut args = vec![
        identity::cli_name().to_string(),
        "debugrevlogclone".to_string(),
        ctx.opts.source_url()?.0.to_string(),
        "-R".to_string(),
        root.to_string_lossy().to_string(),
    ];

    for config in ctx.global_opts().config.iter() {
        args.push("--config".into());
        args.push(config.into());
    }
    if ctx.global_opts().quiet {
        args.push("-q".into());
    }
    if ctx.global_opts().verbose {
        args.push("-v".into());
    }
    if ctx.global_opts().debug {
        args.push("--debug".into());
    }

    logger.verbose(|| format!("Running {}", args.join(" ")));

    let hg_python = HgPython::new(&args);

    abort_if!(
        hg_python.run_hg(args, ctx.io(), config) != 0,
        "Cloning revlog failed"
    );
    Ok(())
}

fn get_selective_bookmarks(repo: &Repo) -> Result<Vec<String>> {
    Ok(repo
        .config()
        .must_get("remotenames", "selectivepulldefault")?)
}

#[instrument(skip_all, err, ret)]
fn get_update_target(
    logger: &mut TermLogger,
    repo: &mut Repo,
    clone_opts: &CloneOpts,
) -> Result<Option<(HgId, String)>> {
    if clone_opts.noupdate {
        return Ok(None);
    }
    let selective_bookmarks = get_selective_bookmarks(repo)?;
    let main_bookmark = selective_bookmarks
        .first()
        .ok_or_else(|| {
            errors::Abort("remotenames.selectivepulldefault config list is empty".into())
        })?
        .clone();

    let remote_bookmark = exchange::convert_to_remote(repo.config(), &main_bookmark)?;
    let remote_bookmarks = repo.remote_bookmarks()?;

    match remote_bookmarks.get(&remote_bookmark) {
        Some(rev) => Ok(Some((rev.clone(), main_bookmark))),
        None => {
            logger.info(format!(
                "Server has no '{}' bookmark - skipping checkout.",
                remote_bookmark,
            ));
            Ok(None)
        }
    }
}

pub fn aliases() -> &'static str {
    "clone"
}

pub fn doc() -> &'static str {
    r#"make a copy of an existing repository

    Create a copy of an existing repository in a new directory.

    If no destination directory name is specified, it defaults to the
    basename of the source.

    The location of the source is added to the new repository's
    config file as the default to be used for future pulls.

    Sources are typically URLs. The following URL schemes are assumed
    to be a Git repo: ``git``, ``git+file``, ``git+ftp``, ``git+ftps``,
    ``git+http``, ``git+https``, ``git+ssh``, ``ssh`` and ``https``.

    Scp-like URLs of the form ``user@host:path`` are converted to
    ``ssh://user@host/path``.

    Other URL schemes are assumed to point to an EdenAPI capable repo.

    The ``--git`` option forces the source to be interpreted as a Git repo.

    To check out a particular version, use ``-u/--update``, or
    ``-U/--noupdate`` to create a clone with no working copy.

    If specified, the ``--enable-profile`` option should refer to a
    sparse profile within the source repo to filter the contents of
    the new working copy. See :prog:`help -e sparse` for details.

    .. container:: verbose

      Examples:

      - clone a remote repository to a new directory named some_repo::

          @prog@ clone https://example.com/some_repo

    .. container:: verbose

      As an experimental feature, if specified the source URL fragment
      is persisted as the repo's main bookmark.

    Returns 0 on success."#
}

pub fn synopsis() -> Option<&'static str> {
    Some("[OPTION]... SOURCE [DEST]")
}


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

use std::borrow::Cow;
use std::collections::BTreeSet;

use anyhow::bail;
use clidispatch::abort;
use clidispatch::abort_if;
use clidispatch::errors;
use clidispatch::OptionalRepo;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use configloader::Config;
use configmodel::ConfigExt;
use configmodel::ValueSource;
use formatter::formatter::FormatOptions;
use formatter::formatter::Formattable;
use formatter::formatter::ListFormatter;
use minibytes::Text;
use serde::ser::Serialize;
use serde::ser::SerializeStruct;
use serde::ser::Serializer;

use super::get_formatter;
use super::ConfigSet;
use super::Result;
use crate::commands::FormatterOpts;

define_flags! {
    pub struct ConfigOpts {
        /// edit config, implying --user if no other flags set (DEPRECATED)
        #[short('e')]
        edit: bool,

        /// edit user config, opening in editor if no args given
        #[short('u')]
        user: bool,

        /// edit repository config, opening in editor if no args given
        #[short('l')]
        local: bool,

        /// edit system config, opening in editor if no args given (DEPRECATED)
        #[short('g')]
        global: bool,

        /// edit system config, opening in editor if no args given
        #[short('s')]
        system: bool,

        formatter_opts: FormatterOpts,

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

pub fn run(ctx: ReqCtx<ConfigOpts>, repo: &mut OptionalRepo) -> Result<u8> {
    let config = repo.config();
    let force_rust = config
        .get_or_default::<Vec<String>>("commands", "force-rust")?
        .contains(&"config".to_owned());
    let use_rust = force_rust || config.get_or_default("config", "use-rust")?;

    if !use_rust {
        bail!(errors::FallbackToPython(
            "config.use-rust not set to True".to_owned()
        ));
    }

    if ctx.opts.edit || ctx.opts.local || ctx.opts.global || ctx.opts.user || ctx.opts.system {
        bail!(errors::FallbackToPython(
            "config edit options not supported in Rust".to_owned()
        ));
    }

    let config = repo.config();
    let mut formatter = get_formatter(
        config,
        short_name(),
        &ctx.opts.formatter_opts.template,
        ctx.global_opts(),
        Box::new(ctx.io().output()),
    )?;

    ctx.maybe_start_pager(repo.config())?;

    formatter.begin_list()?;
    let exit_code = show_configs(ctx, config, formatter.as_mut())?;
    formatter.end_list()?;

    Ok(exit_code)
}

struct ConfigItem<'a> {
    source: String,
    all_sources: Cow<'a, [ValueSource]>,
    section: &'a str,
    key: &'a str,
    value: Option<String>,
    single_item: bool,
    builtin: bool,
}

impl<'a> Serialize for ConfigItem<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut item = serializer.serialize_struct("ConfigItem", 3)?;
        let name = format!("{}.{}", self.section, self.key);
        item.serialize_field("name", name.as_str())?;
        item.serialize_field("source", &self.source)?;

        item.serialize_field("value", &self.value)?;

        item.end()
    }
}

impl<'a> Formattable for ConfigItem<'a> {
    fn format_plain(
        &self,
        options: &FormatOptions,
        writer: &mut dyn formatter::StyleWrite,
    ) -> std::result::Result<(), anyhow::Error> {
        let value: &str = match &self.value {
            Some(value) => value.as_ref(),
            None if options.debug => "<%unset>",
            _ => return Ok(()),
        };

        let source_section = if options.debug {
            format!("{}: ", self.source)
        } else {
            "".to_string()
        };
        let kv_section = if !self.single_item {
            format!("{}.{}=", self.section, self.key)
        } else {
            "".to_string()
        };
        write!(
            writer,
            "{}{}{}\n",
            source_section,
            kv_section,
            value.replace('\n', "\\n")
        )?;

        if options.debug && options.verbose {
            for s in self.all_sources.iter().rev().skip(1) {
                let value = match &s.value {
                    None => Text::from_static("<%unset>"),
                    Some(value) => value.clone(),
                };
                write!(
                    writer,
                    "  {}: {kv_section}{}\n",
                    source_to_display_string(s),
                    value.replace('\n', "\\n"),
                )?;
            }
        }

        Ok(())
    }
}

fn get_config_item<'a>(
    config: &'a ConfigSet,
    section: &'a str,
    key: &'a str,
    single_item: bool,
    debug: bool,
) -> Option<ConfigItem<'a>> {
    let all_sources = config.get_sources(section, key);
    let config_value_source = match all_sources.last() {
        None => {
            return None;
        }
        Some(last) => last,
    };

    let value = config_value_source.value();

    // Don't expose %unset unless --debug was specified.
    if value.is_none() && !debug {
        return None;
    }

    Some(ConfigItem {
        source: source_to_display_string(config_value_source),
        section,
        key,
        value: value.as_ref().map(|v| v.to_string()),
        single_item,
        builtin: config_value_source.source().starts_with("builtin:"),
        all_sources,
    })
}

fn source_to_display_string(source: &ValueSource) -> String {
    source
        .location()
        .and_then(|(location, range)| {
            source.file_content().map(|file| {
                let line = 1 + file
                    .slice(0..range.start)
                    .chars()
                    .filter(|ch| *ch == '\n')
                    .count();
                if !location.as_os_str().is_empty() {
                    format!("{}:{}", location.display(), line)
                } else {
                    format!("{}:{}", source.source(), line)
                }
            })
        })
        .unwrap_or_else(|| source.source().to_string())
}

fn show_configs(
    ctx: ReqCtx<ConfigOpts>,
    config: &ConfigSet,
    formatter: &mut dyn ListFormatter,
) -> Result<u8> {
    let verbose = ctx.global_opts().verbose;
    let debug = ctx.global_opts().debug;

    let requested_items: Vec<_> = ctx
        .opts
        .args
        .iter()
        .filter(|a| a.contains('.'))
        .cloned()
        .collect();
    let requested_sections: BTreeSet<_> = ctx
        .opts
        .args
        .into_iter()
        .filter_map(|a| {
            if !a.contains('.') {
                Some(Text::from(a))
            } else {
                None
            }
        })
        .collect();

    abort_if!(requested_items.len() > 1, "only one config item permitted");
    abort_if!(
        !requested_items.is_empty() && !requested_sections.is_empty(),
        "combining sections and items not permitted"
    );

    if requested_items.len() == 1 {
        let item = &requested_items[0];
        let parts: Vec<_> = item.splitn(2, '.').collect();

        if let Some(item) = get_config_item(config, parts[0], parts[1], true, debug) {
            formatter.format_item(&item)?;
            return Ok(0);
        }
        // Config is expected to return an empty string if anything goes wrong
        return Ok(1);
    }

    let config_sections: BTreeSet<Text> = config.sections().as_ref().iter().cloned().collect();
    let empty_selection = requested_sections.is_empty();
    let selected_sections: Box<dyn Iterator<Item = &Text>> = if empty_selection {
        Box::new(config_sections.iter())
    } else {
        Box::new(requested_sections.intersection(&config_sections))
    };
    let mut selected_sections = selected_sections.peekable();

    if selected_sections.peek().is_none() {
        return Ok(1);
    }

    for section in selected_sections {
        let mut keys = config.keys(section);
        keys.sort();
        for key in keys {
            if let Some(item) = get_config_item(config, section, &key, false, debug) {
                if empty_selection && item.builtin && !verbose {
                    continue;
                }
                formatter.format_item(&item)?;
            }
        }
    }

    Ok(0)
}

pub fn aliases() -> &'static str {
    "config|showconfig|debugconfig|conf|confi"
}

pub fn doc() -> &'static str {
    r#"show config settings

    With no arguments, print names and values of all config items.

    With one argument of the form ``section.name``, print just the value
    of that config item.

    With multiple arguments, print names and values of all config
    items with matching section names.

    With ``--user``, edit the user-level config file. With ``--system``,
    edit the system-wide config file. With ``--local``, edit the
    repository-level config file. If there are no arguments, spawn
    an editor to edit the config file. If there are arguments in
    ``section.name=value`` or ``section.name value`` format, the appropriate
    config file will be updated directly without spawning an editor.

    With ``--debug``, the source (filename and line number) is printed
    for each config item.

    See :prog:`help config` for more information about config files.

    Returns 0 on success, 1 if NAME does not exist.

    "#
}

pub fn synopsis() -> Option<&'static str> {
    Some("[OPTION]... [NAME]...")
}

fn short_name() -> &'static str {
    "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.
 */

use std::io::Write;

use anyhow::Context;
use clidispatch::abort;
use clidispatch::abort_if;
use clidispatch::OptionalRepo;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use configloader::hg::all_existing_system_paths;
use configloader::hg::default_user_config_path;

use super::Result;

define_flags! {
    pub struct DebugConfigLocationOpts {
        /// show the path of the user's config
        #[short('u')]
        user: bool,

        /// show the path of the current repo (if inside a repo)
        #[short('l')]
        local: bool,

        /// show the path of the system config file
        #[short('s')]
        system: bool,
    }
}

pub fn run(ctx: ReqCtx<DebugConfigLocationOpts>, repo: &mut OptionalRepo) -> Result<u8> {
    let optcnt = (ctx.opts.user as i32) + (ctx.opts.local as i32) + (ctx.opts.system as i32);

    abort_if!(
        optcnt > 1,
        "must select at most one of --user, --local, or --system"
    );

    let show_all = optcnt == 0;

    if show_all || ctx.opts.user {
        if show_all {
            write!(ctx.io().output(), "User config path: ")?;
        }
        write!(
            ctx.io().output(),
            "{}\n",
            default_user_config_path()?.display()
        )?;
    }

    if show_all || ctx.opts.local {
        if let OptionalRepo::Some(repo) = repo {
            if show_all {
                write!(ctx.io().output(), "Repo config path: ")?;
            }
            write!(ctx.io().output(), "{}\n", repo.config_path().display())?;
        } else if !show_all {
            abort!("--local must be used inside a repo");
        }
    }

    if show_all || ctx.opts.system {
        let id = identity::default();
        let path = all_existing_system_paths(&id)
            .chain(id.system_config_path().into_iter())
            .next()
            .with_context(|| "unable to determine system config location")?;
        if show_all {
            write!(ctx.io().output(), "System config path: ")?;
        }
        write!(ctx.io().output(), "{}\n", path.display())?;
    }

    Ok(0)
}

pub fn aliases() -> &'static str {
    "configfile"
}

pub fn doc() -> &'static str {
    "shows the location of the selected config file"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

pub use super::define_flags;
pub use super::ConfigSet;
pub use super::NoOpts;
pub use super::Repo;
pub use super::Result;
pub use super::IO;

commands! {
    mod args;
    mod dumpdynamicconfig;
    mod dumpindexedlog;
    mod dumptrace;
    mod dynamicconfig;
    mod fsync;
    mod http;
    mod networkdoctor;
    mod python;
    mod racyoutput;
    mod revsets;
    mod runlog;
    mod scmstore;
    mod scmstorereplay;
    mod segmentclone;
    mod segmentgraph;
    mod segmentpull;
    mod store;
    mod top;
}

define_flags! {
    pub struct DebugArgsOpts {
        #[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.
 */

use anyhow::bail;
use anyhow::Result;
use clidispatch::errors;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use configmodel::ConfigExt;
use repo::repo::Repo;
use workingcopy::workingcopy::WorkingCopy;

use super::MergeToolOpts;

define_flags! {
    pub struct GotoOpts {
        /// discard uncommitted changes (no backup)
        #[short('C')]
        clean: bool,

        /// require clean working copy
        #[short('c')]
        check: bool,

        /// merge uncommitted changes
        #[short('m')]
        merge: bool,

        /// tipmost revision matching date (ADVANCED)
        #[short('d')]
        #[argtype("DATE")]
        date: String,

        /// revision
        #[short('r')]
        #[argtype("REV")]
        rev: String,

        /// update without activating bookmarks
        inactive: bool,

        /// resume interrupted update --merge (ADVANCED)
        r#continue: bool,

        merge_opts: MergeToolOpts,

        /// create new bookmark
        #[short('B')]
        #[argtype("VALUE")]
        bookmark: String,

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

pub fn run(ctx: ReqCtx<GotoOpts>, repo: &mut Repo, wc: &mut WorkingCopy) -> Result<u8> {
    // Missing features (in roughly priority order):
    // - edenfs checkout support
    // - --clean support
    // - progressfile and --continue
    // - updatestate file maintaince
    // - Activating/deactivating bookmarks
    // - Checking unknown files (do we need this?)
    //
    // Features to deprecate/not support:
    // - --merge, --inactive, --date, --check

    if !repo.config().get_or_default("checkout", "use-rust")? {
        return Err(errors::FallbackToPython("checkout.use-rust is False".to_owned()).into());
    };

    let mut dest: Vec<&String> = ctx.opts.args.iter().collect();
    if !ctx.opts.rev.is_empty() {
        dest.push(&ctx.opts.rev);
    }

    if dest.len() == 0 {
        bail!(identity::default().punch(
            r#"You must specify a destination to update to, for example "@prog@ goto main"."#,
        ));
    }
    if dest.len() > 1 {
        bail!(
            "checkout requires exactly one destination commit: {:?}",
            dest
        );
    }
    let dest: String = dest[0].clone();

    if ctx.opts.clean
        || ctx.opts.check
        || ctx.opts.merge
        || !ctx.opts.date.is_empty()
        || ctx.opts.r#continue
    {
        tracing::debug!(target: "checkout_info", status_detail="unsupported_args");
        return Err(errors::FallbackToPython(
            "one or more unsupported options in Rust checkout".to_owned(),
        )
        .into());
    }

    let target = match repo.resolve_commit(&wc.treestate().lock(), &dest) {
        Ok(target) => target,
        Err(_) => {
            return Err(errors::FallbackToPython(
                "unable to resolve checkout destination".to_owned(),
            )
            .into());
        }
    };

    let _wlock = wc.lock();
    let _lock = repo.lock();
    let (updated, removed) = checkout::checkout(ctx.io(), repo, wc, target)?;

    if !ctx.global_opts().quiet {
        ctx.io().write(format!(
            "{} files updated, 0 files merged, {} files removed, 0 files unresolved\n",
            updated, removed
        ))?;
    }

    Ok(0)
}

pub fn aliases() -> &'static str {
    "goto|go|update|up|checkout|co|upd|upda|updat|che|chec|check|checko|checkou"
}

pub fn doc() -> &'static str {
    r#"update working copy to a given commit

Update your working copy to the given destination commit. More
precisely, make the destination commit the current commit and update the
contents of all files in your working copy to match their state in the
destination commit.

By default, if you attempt to go to a commit while you have pending
changes, and the destination commit is not an ancestor or descendant of
the current commit, the checkout will abort. However, if the destination
commit is an ancestor or descendant of the current commit, the pending
changes will be merged with the destination.

Use one of the following flags to modify this behavior::

    --check: abort if there are pending changes

    --clean: permanently discard any pending changes (use with caution)

    --merge: always attempt to merge the pending changes into the destination

If merge conflicts occur during update, @Product@ enters an unfinished
merge state. If this happens, fix the conflicts manually and then run
:prog:`commit` to exit the unfinished merge state and save your changes
in a new commit. Alternatively, run :prog:`goto --clean` to discard your
pending changes.

Specify null as the destination commit to get an empty working copy
(sometimes known as a bare repository).

Returns 0 on success, 1 if there are unresolved files."#
}

pub fn synopsis() -> Option<&'static str> {
    Some("[OPTION]... [REV]")
}


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

use super::define_flags;
use super::Repo;
use super::Result;

define_flags! {
    pub struct RootOpts {
        /// show root of the shared repo
        shared: bool,

        /// join root with the repo dot dir (e.g. ".sl") (EXPERIMENTAL)
        dotdir: bool,
    }
}

pub fn run(ctx: ReqCtx<RootOpts>, repo: &mut Repo) -> Result<u8> {
    let path = match (ctx.opts.shared, ctx.opts.dotdir) {
        (false, false) => repo.path(),
        (false, true) => repo.dot_hg_path(),
        (true, false) => repo.shared_path(),
        (true, true) => repo.shared_dot_hg_path(),
    };

    ctx.io().write(format!(
        "{}\n",
        util::path::strip_unc_prefix(&path).display()
    ))?;
    Ok(0)
}

pub fn aliases() -> &'static str {
    "root"
}

pub fn doc() -> &'static str {
    r#"print the repository's root (top) of the current working directory

    Print the root directory of the current repository.

    Frequently useful in shells scripts and automation to run commands like::

       $  ./$(sl root)/bin/script.py

    Returns 0 on success."#
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

mod print;

use std::sync::Arc;
use std::time::SystemTime;

use anyhow::Result;
use clidispatch::errors;
use clidispatch::fallback;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use configloader::configmodel::ConfigExt;
use pathmatcher::AlwaysMatcher;
use print::PrintConfig;
use print::PrintConfigStatusTypes;
use repo::repo::Repo;
use status::needs_morestatus_extension;
use types::path::RepoPathRelativizer;
use workingcopy::workingcopy::WorkingCopy;

use super::get_formatter;
use crate::commands::FormatterOpts;
use crate::commands::WalkOpts;

define_flags! {
    pub struct StatusOpts {
        /// show status of all files
        #[short('A')]
        all: bool,

        /// show only modified files
        #[short('m')]
        modified: bool,

        /// show only added files
        #[short('a')]
        added: bool,

        /// show only removed files
        #[short('r')]
        removed: bool,

        /// show only deleted (but tracked) files
        #[short('d')]
        deleted: bool,

        /// show only files without changes
        #[short('c')]
        clean: bool,

        /// show only unknown (not tracked) files
        #[short('u')]
        unknown: bool,

        /// show only ignored files
        #[short('i')]
        ignored: bool,

        /// hide status prefix
        #[short('n')]
        no_status: bool,

        /// show the terse output (EXPERIMENTAL)
        #[short('t')]
        terse: String,

        /// show source of copied files
        #[short('C')]
        copies: bool,

        /// end filenames with NUL, for use with xargs
        #[short('0')]
        print0: bool,

        /// show difference from revision
        #[argtype("REV")]
        rev: Vec<String>,

        /// list the changed files of a revision
        #[argtype("REV")]
        change: String,

        /// show status relative to root
        root_relative: Option<bool>,

        walk_opts: WalkOpts,
        formatter_opts: FormatterOpts,

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

pub fn run(ctx: ReqCtx<StatusOpts>, repo: &mut Repo, wc: &mut WorkingCopy) -> Result<u8> {
    let rev_check = ctx.opts.rev.is_empty() || (ctx.opts.rev.len() == 1 && ctx.opts.rev[0] == ".");

    let args_check =
        ctx.opts.args.is_empty() || (ctx.opts.args.len() == 1 && ctx.opts.args[0] == "re:.");

    if ctx.opts.all
        || !ctx.opts.change.is_empty()
        || !ctx.opts.terse.is_empty()
        || !rev_check
        || !ctx.opts.walk_opts.include.is_empty()
        || !ctx.opts.walk_opts.exclude.is_empty()
        || !args_check
        || ctx.opts.ignored
        || ctx.opts.clean
    {
        tracing::debug!(target: "status_info", status_detail="unsupported_args");
        fallback!("one or more unsupported options in Rust status");
    }

    if repo.storage_format().is_git() {
        tracing::debug!(target: "status_info", status_detail="git");
        fallback!("git format unsupported (submodules)");
    }

    if needs_morestatus_extension(repo.dot_hg_path(), wc.treestate().lock().parents().count()) {
        tracing::debug!(target: "status_info", status_detail="morestatus_needed");
        fallback!("morestatus functionality needed");
    }

    let StatusOpts {
        modified,
        added,
        removed,
        deleted,
        clean,
        unknown,
        ignored,
        ..
    } = ctx.opts;

    let status_types = if modified || added || removed || deleted || clean || unknown || ignored {
        PrintConfigStatusTypes {
            modified,
            added,
            removed,
            deleted,
            clean,
            unknown,
            ignored,
        }
    } else {
        PrintConfigStatusTypes {
            modified: true,
            added: true,
            removed: true,
            deleted: true,
            clean: false,
            unknown: !ctx.global_opts().quiet,
            ignored: false,
        }
    };
    let print_config = PrintConfig {
        status_types,
        no_status: ctx.opts.no_status,
        copies: ctx.opts.copies
            || repo
                .config()
                .get_or::<bool>("ui", "statuscopies", || false)?,
        endl: if ctx.opts.print0 { '\0' } else { '\n' },
        root_relative: ctx
            .opts
            .root_relative
            .unwrap_or_else(|| hgplain::is_plain(None)),
    };

    let (status, copymap) = match repo.config().get_or_default("status", "use-rust")? {
        true => {
            tracing::debug!(target: "status_info", status_mode="rust");

            let matcher = Arc::new(AlwaysMatcher::new());
            let status = wc.status(
                matcher.clone(),
                SystemTime::UNIX_EPOCH,
                repo.config(),
                ctx.io(),
            )?;

            // This should be passed the "full" matcher including
            // ignores, sparse, etc., but in practice probably doesn't
            // make a difference.
            let copymap = wc.copymap(matcher)?.into_iter().collect();

            (status, copymap)
        }
        false => {
            #[cfg(feature = "eden")]
            {
                use anyhow::anyhow;

                tracing::debug!(target: "status_info", status_mode="fastpath");

                // Attempt to fetch status information from EdenFS.
                let (status, copymap) = edenfs_client::status::maybe_status_fastpath(
                    repo.dot_hg_path(),
                    ctx.io(),
                    print_config.status_types.ignored,
                )
                .map_err(|e| match e
                    .downcast_ref::<edenfs_client::status::OperationNotSupported>()
                {
                    Some(_) => {
                        tracing::debug!(target: "status_info", status_detail="fastpath_edenfs_error");
                        anyhow!(errors::FallbackToPython(
                            "unsupported edenfs operation".to_owned()
                        ))
                    },
                    None => e,
                })?;
                (status, copymap)
            }
            #[cfg(not(feature = "eden"))]
            {
                tracing::debug!(target: "status_info", status_detail="fastpath_edenfs_disabled");
                fallback!("EdenFS disabled for Rust status and status.use-rust not set to True");
            }
        }
    };

    let cwd = std::env::current_dir()?;
    let relativizer = RepoPathRelativizer::new(cwd, repo.path());
    let formatter = get_formatter(
        repo.config(),
        "status",
        ctx.opts.formatter_opts.template.as_str(),
        ctx.global_opts(),
        Box::new(ctx.io().output()),
    )?;

    let mut lgr = ctx.logger();
    for invalid in status.invalid_path() {
        lgr.warn(format!(
            "skipping invalid filename: '{}'",
            util::utf8::escape_non_utf8(invalid)
        ));
    }

    for invalid in status.invalid_type() {
        lgr.warn(format!("{invalid}: invalid file type"));
    }

    ctx.maybe_start_pager(repo.config())?;

    print::print_status(formatter, relativizer, &print_config, &status, &copymap)?;

    Ok(0)
}

pub fn aliases() -> &'static str {
    "status|st|sta|stat|statu"
}

pub fn doc() -> &'static str {
    r#"list files with pending changes

    Show status of files in the working copy using the following status
    indicators::

      M = modified
      A = added
      R = removed
      C = clean
      ! = missing (deleted by a non-@prog@ command, but still tracked)
      ? = not tracked
      I = ignored
        = origin of the previous file (with --copies)

    By default, shows files that have been modified, added, removed,
    deleted, or that are unknown (corresponding to the options ``-mardu``,
    respectively). Files that are unmodified, ignored, or the source of
    a copy/move operation are not listed.

    To control the exact statuses that are shown, specify the relevant
    flags (like ``-rd`` to show only files that are removed or deleted).
    Additionally, specify ``-q/--quiet`` to hide both unknown and ignored
    files.

    To show the status of specific files, provide a list of files to
    match. To include or exclude files using patterns or filesets, use
    ``-I`` or ``-X``.

    If ``--rev`` is specified and only one revision is given, it is used as
    the base revision. If two revisions are given, the differences between
    them are shown. The ``--change`` option can also be used as a shortcut
    to list the changed files of a revision from its first parent.

    .. note::

       :prog:`status` might appear to disagree with :prog:`diff` if permissions
       have changed or a merge has occurred, because the standard diff
       format does not report permission changes and :prog:`diff` only
       reports changes relative to one merge parent.

    .. container:: verbose

      The ``-t/--terse`` option abbreviates the output by showing only the directory
      name if all the files in it share the same status. The option takes an
      argument indicating the statuses to abbreviate: 'm' for 'modified', 'a'
      for 'added', 'r' for 'removed', 'd' for 'deleted', 'u' for 'unknown', 'i'
      for 'ignored' and 'c' for clean.

      It abbreviates only those statuses which are passed. Note that clean and
      ignored files are not displayed with ``--terse ic`` unless the ``-c/--clean``
      and ``-i/--ignored`` options are also used.

      The ``-v/--verbose`` option shows information when the repository is in an
      unfinished merge, shelve, rebase state, etc. You can have this behavior
      turned on by default by enabling the ``commands.status.verbose`` config option.

      You can skip displaying some of these states by setting
      ``commands.status.skipstates`` to one or more of: 'bisect', 'graft',
      'histedit', 'merge', 'rebase', or 'unshelve'.

      Examples:

      - show changes in the working directory relative to a
        commit::

          @prog@ status --rev 88a692db8

      - show changes in the working copy relative to the
        current directory (see :prog:`help patterns` for more information)::

          @prog@ status re:

      - show all changes including copies in a commit::

          @prog@ status --copies --change 88a692db8

      - get a NUL separated list of added files, suitable for xargs::

          @prog@ status -an0

      - show more information about the repository status, abbreviating
        added, removed, modified, deleted, and untracked paths::

          @prog@ status -v -t mardu

    Returns 0 on success."#
}

pub fn synopsis() -> Option<&'static str> {
    Some("[OPTION]... [FILE]...")
}


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

use super::ConfigSet;
use super::NoOpts;
use super::Result;

pub fn run(ctx: ReqCtx<NoOpts>, _config: &mut ConfigSet) -> Result<u8> {
    let id = identity::default();
    ctx.io()
        .write(format!("{} {}\n", id.product_name(), ::version::VERSION))?;
    Ok(0)
}

pub fn aliases() -> &'static str {
    "version|vers|versi|versio"
}

pub fn doc() -> &'static str {
    "output version and copyright information"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::fs::File;
use std::io::Write;

use anyhow::Context;
use clidispatch::ReqCtx;
use treestate::serialization::Serializable;
use types::HgId;

use super::NoOpts;
use super::Repo;
use super::Result;

pub fn run(ctx: ReqCtx<NoOpts>, repo: &mut Repo) -> Result<u8> {
    let mut stdout = ctx.io().output();

    let dirstate_path = repo.dot_hg_path().join("dirstate");
    let mut dirstate_file = match File::open(&dirstate_path) {
        Ok(f) => f,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
            // Show zeros to indicate lack of parent.
            write!(stdout, "{}\n", HgId::null_id().to_hex())?;
            return Ok(0);
        }
        Err(err) => {
            return Err(err).with_context(|| {
                format!("error opening dirstate file {}", dirstate_path.display())
            });
        }
    };

    let dirstate = treestate::dirstate::Dirstate::deserialize(&mut dirstate_file)?;

    write!(stdout, "{}\n", dirstate.p1.to_hex())?;
    if !dirstate.p2.is_null() {
        write!(stdout, "{}\n", dirstate.p2.to_hex())?;
    }

    Ok(0)
}

pub fn aliases() -> &'static str {
    "whereami"
}

pub fn doc() -> &'static str {
    r#"output the working copy's parent hashes

If there are no parents, an all zeros hash is emitted.
If there are two parents, both will be emitted, newline separated.
"#
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use super::ConfigSet;
use super::DebugArgsOpts;
use super::Result;

pub fn run(ctx: ReqCtx<DebugArgsOpts>, _config: &mut ConfigSet) -> Result<u8> {
    match ctx.io().write(format!("{:?}\n", ctx.opts.args)) {
        Ok(_) => Ok(0),
        Err(_) => Ok(255),
    }
}

pub fn aliases() -> &'static str {
    "debug-args"
}

pub fn doc() -> &'static str {
    "print arguments received"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


/*
 * 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 clidispatch::ReqCtx;
#[cfg(feature = "fb")]
use configloader::hg::calculate_dynamicconfig;
#[cfg(feature = "fb")]
use configmodel::ConfigExt;

use super::define_flags;
use super::ConfigSet;
use super::Result;

define_flags! {
    pub struct DebugDumpConfigOpts {
        /// repository name
        reponame: Option<String>,

        /// user name
        username: String,

        /// host name to fetch a canary config from
        canary: Option<String>,

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

pub fn run(ctx: ReqCtx<DebugDumpConfigOpts>, config: &mut ConfigSet) -> Result<u8> {
    #[cfg(feature = "fb")]
    {
        let reponame = ctx.opts.reponame;
        let mut username = ctx.opts.username;
        if username.is_empty() {
            username = config.get_opt("ui", "username")?.unwrap_or_default();
        }
        let canary = ctx.opts.canary;

        let temp_dir = std::env::temp_dir();
        let generated = calculate_dynamicconfig(
            temp_dir,
            reponame,
            canary,
            username,
            config.get_opt("auth_proxy", "unix_socket_path")?,
        )?;

        if ctx.opts.args.is_empty() {
            ctx.core.io.write(generated.to_string())?;
        } else {
            for arg in ctx.opts.args {
                let split: Vec<_> = arg.splitn(2, ".").collect();
                if let [section, name] = split[..] {
                    let value: String = generated.get_opt(section, name)?.unwrap_or_default();
                    ctx.core.io.write(format!("{}\n", value))?;
                }
            }
        }
    }
    #[cfg(not(feature = "fb"))]
    let _ = (ctx, config);

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugdumpdynamicconfig"
}

pub fn doc() -> &'static str {
    "print the dynamic configuration

Without arguments, print the dynamic config in hgrc format.
Otherwise, print config values specified by the arguments.
An argument should be in the format ``section.name``.
"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;
use std::path::Path;

use clidispatch::ReqCtx;

use super::ConfigSet;
use super::DebugArgsOpts;
use super::Result;

pub fn run(ctx: ReqCtx<DebugArgsOpts>, _config: &mut ConfigSet) -> Result<u8> {
    let mut ferr = ctx.io().error();
    let mut fout = ctx.io().output();
    for path in ctx.opts.args {
        let _ = write!(fout, "{}\n", path);
        let path = Path::new(&path);
        if let Ok(meta) = indexedlog::log::LogMetadata::read_file(path) {
            write!(ferr, "Metadata File {:?}\n{:?}\n", path, meta)?;
        } else if path.is_dir() {
            // Treate it as Log.
            let log = indexedlog::log::Log::open(path, Vec::new())?;
            write!(ferr, "Log Directory {:?}:\n{:#?}\n", path, log)?;
        } else if path.is_file() {
            // Treate it as Index.
            let idx = indexedlog::index::OpenOptions::new().open(path)?;
            write!(ferr, "Index File {:?}\n{:?}\n", path, idx)?;
        } else {
            write!(ferr, "Path {:?} is not a file or directory.\n\n", path)?;
        }
    }
    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugdumpindexedlog|debugindexedlogdump"
}

pub fn doc() -> &'static str {
    "dump indexedlog data"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


/*
 * 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 blackbox::event::Event;
use blackbox::json;
use blackbox::SessionId;
use clidispatch::errors;
use clidispatch::ReqCtx;

use super::define_flags;
use super::Repo;
use super::Result;

define_flags! {
    pub struct DumpTraceOpts {
        /// time range
        #[short('t')]
        time_range: String = "since 15 minutes ago",

        /// blackbox session id (overrides --time-range)
        #[short('s')]
        session_id: i64,

        /// output path (.txt, .json, .json.gz, .spans.json)
        #[short('o')]
        output_path: String,
    }
}

pub fn run(ctx: ReqCtx<DumpTraceOpts>, _repo: &mut Repo) -> Result<u8> {
    let entries = {
        let blackbox = blackbox::SINGLETON.lock();
        let session_ids = if ctx.opts.session_id != 0 {
            vec![SessionId(ctx.opts.session_id as u64)]
        } else if let Some(range) = hgtime::HgTime::parse_range(&ctx.opts.time_range) {
            // Blackbox uses milliseconds. HgTime uses seconds.
            let ratio = 1000;
            blackbox.session_ids_by_pattern(&json!({"start": {
                 "timestamp_ms": ["range", range.start.unixtime.saturating_mul(ratio), range.end.unixtime.saturating_mul(ratio)]
             }})).into_iter().collect()
        } else {
            return Err(
                errors::Abort("both --time-range and --session-id are invalid".into()).into(),
            );
        };
        blackbox.entries_by_session_ids(session_ids)
    };

    let mut tracing_data_list = Vec::new();
    for entry in entries {
        if let Event::TracingData { serialized } = entry.data {
            if let Ok(uncompressed) = zstd::stream::decode_all(&serialized.0[..]) {
                if let Ok(data) = mincode::deserialize(&uncompressed) {
                    tracing_data_list.push(data)
                }
            }
        }
    }
    let merged = tracing_collector::TracingData::merge(tracing_data_list);

    crate::run::write_trace(ctx.io(), &ctx.opts.output_path, &merged)?;

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugdumptrace"
}

pub fn doc() -> &'static str {
    "export tracing information"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


/*
 * 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 clidispatch::ReqCtx;
#[cfg(feature = "fb")]
use configloader::hg::generate_dynamicconfig;
#[cfg(feature = "fb")]
use configmodel::Config;
#[cfg(feature = "fb")]
use configmodel::ConfigExt;

use super::define_flags;
use super::Repo;
use super::Result;

define_flags! {
    pub struct DebugDynamicConfigOpts {
        /// Host name to fetch a canary config from.
        canary: Option<String>,
    }
}

pub fn run(ctx: ReqCtx<DebugDynamicConfigOpts>, repo: &mut Repo) -> Result<u8> {
    #[cfg(feature = "fb")]
    {
        let username = repo
            .config()
            .get("ui", "username")
            .and_then(|u| Some(u.to_string()))
            .unwrap_or_else(|| "".to_string());

        generate_dynamicconfig(
            Some(repo.shared_dot_hg_path()),
            repo.repo_name(),
            ctx.opts.canary,
            username,
            repo.config().get_opt("auth_proxy", "unix_socket_path")?,
        )?;
    }
    #[cfg(not(feature = "fb"))]
    let _ = (ctx, repo);

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugdynamicconfig"
}

pub fn doc() -> &'static str {
    "generate the dynamic configuration"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


/*
 * 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 clidispatch::ReqCtx;
use configmodel::ConfigExt;

use super::NoOpts;
use super::Repo;
use super::Result;

pub fn run(_ctx: ReqCtx<NoOpts>, repo: &mut Repo) -> Result<u8> {
    let store_path = repo.store_path();
    let patterns = [
        "indexedlogdatastore/*",
        "indexedloghistorystore/*",
        "00changelog.*",
        "hgcommits/**/*",
        "segments/**/*",
        "mutation/**/*",
        "metalog/**/*",
        "allheads/**/*",
        "lfs/**/*",
    ];
    fsyncglob::fsync_glob(store_path, &patterns, None);

    let dot_hg_path = repo.dot_hg_path();
    let patterns = ["treestate/*", "dirstate"];
    fsyncglob::fsync_glob(dot_hg_path, &patterns, None);

    if let Some(Some(cache_path)) = repo
        .config()
        .get_opt::<String>("remotefilelog", "cachepath")
        .ok()
    {
        let patterns = ["*/indexedlog*/*", "*/lfs/*"];
        fsyncglob::fsync_glob(Path::new(&cache_path), &patterns, None);
    }

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugfsync"
}

pub fn doc() -> &'static str {
    "call fsync on newly modified key storage files"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


/*
 * 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 async_runtime::block_unless_interrupted as block_on;
use clidispatch::ReqCtx;

use super::NoOpts;
use super::Repo;
use super::Result;

pub fn run(ctx: ReqCtx<NoOpts>, repo: &mut Repo) -> Result<u8> {
    let client = edenapi::Builder::from_config(repo.config())?.build()?;
    let meta = block_on(client.health())?;
    ctx.io().write(format!("{:#?}\n", &meta))?;
    Ok(0)
}

pub fn aliases() -> &'static str {
    "debughttp"
}

pub fn doc() -> &'static str {
    "check whether the EdenAPI server is reachable"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;

use clidispatch::OptionalRepo;
use clidispatch::ReqCtx;
use configloader::config::Options;

use super::NoOpts;
use super::Result;

pub fn run(ctx: ReqCtx<NoOpts>, repo: &mut OptionalRepo) -> Result<u8> {
    // Set a default repo so we can build valid edenapi URLs outside a repo.
    if let OptionalRepo::None(ref mut config) = repo {
        config.set(
            "remotefilelog",
            "reponame",
            Some("fbsource"),
            &Options::new().source("networkdoctor.rs"),
        );
    }

    let mut stdout = ctx.io().output();
    match network_doctor::Doctor::new().diagnose(repo.config()) {
        Ok(()) => write!(stdout, "No network problems detected.\n")?,
        Err(d) => write!(stdout, "{}\n\n{}\n", d.treatment(repo.config()), d)?,
    };
    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugnetworkdoctor"
}

pub fn doc() -> &'static str {
    "run the (Rust) network doctor"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


/*
 * 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 clidispatch::errors;
use clidispatch::ReqCtx;

use super::define_flags;
use super::ConfigSet;
use super::Result;

define_flags! {
    pub struct DebugPythonOpts {
        #[args]
        args: Vec<String>,
    }
}

pub fn run(_ctx: ReqCtx<DebugPythonOpts>, _config: &mut ConfigSet) -> Result<u8> {
    let e = errors::Abort("wrong debugpython code path used".into());
    Err(e.into())
}

pub fn aliases() -> &'static str {
    "debugpython|debugpy"
}

pub fn doc() -> &'static str {
    "run python interpreter"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering::Relaxed;
use std::thread;
use std::thread::JoinHandle;
use std::time::Duration;

use clidispatch::ReqCtx;
use cliparser::define_flags;
use progress_model::IoSample;
use progress_model::IoTimeSeries;
use progress_model::ProgressBar;
use progress_model::Registry;

use super::ConfigSet;
use super::Result;
use super::IO;

define_flags! {
    pub struct DebugRacyOutputOpts {
        /// number of time series to show
        time_series: i64 = 1,

        /// number of progress bars to show
        progress_bars: i64 = 10,

        /// total for a progress bar
        progress_total: i64 = 500,

        /// max update interval for a progress bar, in milliseconds
        progress_interval_ms: i64 = 200,

        /// total outputs
        output_total: i64 = 100,

        /// max interval, in milliseconds for outputs
        output_interval_ms: i64 = 1000,
    }
}

pub fn run(ctx: ReqCtx<DebugRacyOutputOpts>, _config: &mut ConfigSet) -> Result<u8> {
    add_time_series(ctx.opts.time_series as _);
    add_progress_bar_threads(
        ctx.opts.progress_bars as _,
        ctx.opts.progress_total as _,
        ctx.opts.progress_interval_ms as _,
    );
    write_random_outputs(
        ctx.io(),
        ctx.opts.output_total as _,
        ctx.opts.output_interval_ms as _,
    );
    Ok(0)
}

fn add_time_series(count: usize) {
    let registry = Registry::main();
    for _ in 0..count {
        let time_series = IoTimeSeries::new("Metric", "items");
        let take_sample = {
            || {
                static TOTAL: AtomicU64 = AtomicU64::new(0);
                let v = TOTAL.fetch_add(rand::random::<u64>() % 0xffff, Relaxed);
                IoSample::from_io_bytes_count(v, v, v)
            }
        };
        let task = time_series.async_sampling(take_sample, IoTimeSeries::default_sample_interval());
        async_runtime::spawn(task);
        registry.register_io_time_series(&time_series);
    }
}

fn add_progress_bar_threads(
    num_threads: usize,
    total: u64,
    interval_ms: u64,
) -> Vec<JoinHandle<()>> {
    let threads: Vec<JoinHandle<_>> = (0..num_threads)
        .map(|_| {
            let bar = ProgressBar::register_new("Progress", total, "items");
            thread::spawn(move || {
                for _i in 0..total {
                    bar.increase_position(1);
                    sleep_random_ms(interval_ms);
                }
            })
        })
        .collect();
    threads
}

fn write_random_outputs(io: &IO, total: usize, interval_ms: u64) {
    let mut write = {
        let mut out = io.output();
        let mut err = io.error();
        move |use_out: bool, eol: bool, i: usize| {
            let msg = format!(
                "{}.{}{}",
                if use_out { "out" } else { "err" },
                i,
                if eol { ".\n" } else { "," }
            );
            let _ = if use_out {
                out.write(msg.as_bytes())
            } else {
                err.write(msg.as_bytes())
            };
        }
    };
    for i in 0..total {
        let use_out = rand::random();
        write(use_out, false, i);
        sleep_random_ms(5);
        write(use_out, true, i);
        sleep_random_ms(interval_ms);
    }
}

fn sleep_random_ms(max_ms: u64) {
    thread::sleep(Duration::from_millis(rand::random::<u64>() % max_ms));
}

pub fn aliases() -> &'static str {
    "debugracyoutput"
}

pub fn doc() -> &'static str {
    "exercise racy stdout / stderr / progress outputs"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;
use std::ops::Deref;

use clidispatch::ReqCtx;
use cliparser::define_flags;
use revsets::utils::resolve_single;
use workingcopy::workingcopy::WorkingCopy;

use super::Repo;
use super::Result;

define_flags! {
    pub struct DebugRevsetOpts {
        #[arg]
        rev: String,
    }
}

pub fn run(ctx: ReqCtx<DebugRevsetOpts>, repo: &mut Repo, wc: &mut WorkingCopy) -> Result<u8> {
    let changelog = repo.dag_commits()?;
    let id_map = changelog.read().id_map_snapshot()?;
    let resolved_revset = resolve_single(
        &ctx.opts.rev,
        id_map.as_ref(),
        &repo.metalog()?.read(),
        wc.treestate().lock().deref(),
    )?;

    write!(ctx.io().output(), "{}\n", resolved_revset.to_hex())?;

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugrevset"
}

pub fn doc() -> &'static str {
    "resolves a single revset and outputs its commit hash"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;

use anyhow::Result;
use clidispatch::errors;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use repo::repo::Repo;

define_flags! {
    pub struct DebugRunlogOpts {
        /// display entries for exited/crashed commands (ADVANCED)
        ended: bool,

        /// output template (only allows "json")
        #[short('T')]
        template: String,
    }
}

enum Format {
    Text,
    Json,
}

pub fn run(ctx: ReqCtx<DebugRunlogOpts>, repo: &mut Repo) -> Result<u8> {
    let mut stdout = ctx.io().output();
    let mut stderr = ctx.io().error();

    let format = match ctx.opts.template.as_str() {
        "json" => Format::Json,
        "" => Format::Text,
        _ => return Err(errors::Abort("invalid template (only \"json\" supported)".into()).into()),
    };

    for entry in runlog::FileStore::entry_iter(repo.shared_dot_hg_path())? {
        let (entry, running) = match entry {
            Ok((entry, running)) => (entry, running),
            Err(err) => {
                // Unlikely, but it is possible to have incomplete Json files.
                write!(stderr, "Error reading runlog entry: {:?}\n", err)?;
                continue;
            }
        };

        if ctx.opts.ended == running {
            continue;
        }

        match format {
            Format::Text => {
                write!(stdout, "{:#?}\n", entry)?;
            }
            Format::Json => {
                serde_json::to_writer(&mut stdout, &entry)?;
                stdout.write_all(&[b'\n'])?;
            }
        }
    }

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugrunlog"
}

pub fn doc() -> &'static str {
    "display runlog entries"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;

use async_runtime::block_on;
use async_runtime::stream_to_iter as block_on_stream;
use clidispatch::errors;
use clidispatch::ReqCtx;
use configloader::config::ConfigSet;
use revisionstore::scmstore::file_to_async_key_stream;
use revisionstore::scmstore::FetchMode;
use revisionstore::scmstore::FileAttributes;
use revisionstore::scmstore::FileStoreBuilder;
use revisionstore::scmstore::TreeStoreBuilder;
use types::Key;

use super::define_flags;
use super::Repo;
use super::Result;
use super::IO;

define_flags! {
    pub struct DebugScmStoreOpts {
        /// Run the python version of the command instead (actually runs mostly in rust, but uses store constructed for python, with legacy fallback).
        python: bool,

        /// Fetch mode (file or tree)
        mode: String,

        /// Input file containing keys to fetch (hgid,path separated by newlines)
        path: String,

        /// Only check for the entity locally, don't make a remote request
        local: bool,
    }
}

enum FetchType {
    File,
    Tree,
}

pub fn run(ctx: ReqCtx<DebugScmStoreOpts>, repo: &mut Repo) -> Result<u8> {
    if ctx.opts.python {
        return Err(errors::FallbackToPython("--python option selected".to_owned()).into());
    }

    let mode = match ctx.opts.mode.as_ref() {
        "file" => FetchType::File,
        "tree" => FetchType::Tree,
        _ => return Err(errors::Abort("'mode' must be one of 'file' or 'tree'".into()).into()),
    };

    let keys: Vec<_> =
        block_on_stream(block_on(file_to_async_key_stream(ctx.opts.path.into()))?).collect();

    let config = repo.config();

    match mode {
        FetchType::File => fetch_files(&ctx.core.io, config, keys, ctx.opts.local)?,
        FetchType::Tree => fetch_trees(&ctx.core.io, config, keys, ctx.opts.local)?,
    }

    Ok(0)
}

fn fetch_files(io: &IO, config: &ConfigSet, keys: Vec<Key>, local: bool) -> Result<()> {
    let file_builder = FileStoreBuilder::new(&config);
    let store = file_builder.build()?;

    let mut stdout = io.output();

    let fetch_mode = if local {
        FetchMode::LocalOnly
    } else {
        FetchMode::AllowRemote
    };

    let fetch_result = store.fetch(
        keys.into_iter(),
        FileAttributes {
            content: true,
            aux_data: true,
        },
        fetch_mode,
    );

    let (found, missing, _errors) = fetch_result.consume();
    for (_, file) in found.into_iter() {
        write!(stdout, "Successfully fetched file: {:#?}\n", file)?;
    }
    for (key, _) in missing.into_iter() {
        write!(stdout, "Failed to fetch file: {:#?}\n", key)?;
    }

    Ok(())
}

fn fetch_trees(io: &IO, config: &ConfigSet, keys: Vec<Key>, local: bool) -> Result<()> {
    let mut tree_builder = TreeStoreBuilder::new(config);
    tree_builder = tree_builder.suffix("manifests");
    let store = tree_builder.build()?;

    let mut stdout = io.output();

    let fetch_mode = if local {
        FetchMode::LocalOnly
    } else {
        FetchMode::AllowRemote
    };

    let fetch_result = store.fetch_batch(keys.into_iter(), fetch_mode);

    let (found, missing, _errors) = fetch_result.consume();
    for complete in found.into_iter() {
        write!(stdout, "Successfully fetched tree: {:#?}\n", complete)?;
    }
    for incomplete in missing.into_iter() {
        write!(stdout, "Failed to fetch tree: {:#?}\n", incomplete)?;
    }

    Ok(())
}

pub fn aliases() -> &'static str {
    "debugscmstore"
}

pub fn doc() -> &'static str {
    "test file and tree fetching using scmstore"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;
use std::time::Instant;

use clidispatch::ReqCtx;
use revisionstore::scmstore::activitylogger;
use revisionstore::scmstore::FetchMode;
use revisionstore::scmstore::FileStoreBuilder;

use super::define_flags;
use super::Repo;
use super::Result;

define_flags! {
    pub struct DebugScmStoreReplayOpts {
        /// Path to JSON activity log
        path: String,
    }
}

pub fn run(ctx: ReqCtx<DebugScmStoreReplayOpts>, repo: &mut Repo) -> Result<u8> {
    // TODO: Take into account log timings to yield a more faithful
    // reproduction of fetch activity, particularly concurrent fetches.

    let file_builder = FileStoreBuilder::new(repo.config());
    let store = file_builder.local_path(repo.store_path()).build()?;

    let mut stdout = ctx.io().output();
    let mut stderr = ctx.io().error();

    let (mut key_count, mut fetch_count) = (0, 0);
    let start_instant = Instant::now();
    for log in activitylogger::log_iter(ctx.opts.path)? {
        let log = log?;
        match log.op {
            activitylogger::ActivityType::FileFetch => {
                key_count += log.keys.len();
                fetch_count += 1;
                let result = store.fetch(log.keys.into_iter(), log.attrs, FetchMode::AllowRemote);
                match result.missing() {
                    Ok(failed) => {
                        if failed.len() > 0 {
                            write!(stderr, "Failed to fetch keys {:?}\n", failed)?;
                        }
                    }
                    Err(err) => write!(stderr, "Fetch error: {:#?}\n", err)?,
                };
            }
        }
    }

    write!(
        stdout,
        "Fetched {} keys across {} fetches in {:?}\n",
        key_count,
        fetch_count,
        start_instant.elapsed()
    )?;

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugscmstorereplay"
}

pub fn doc() -> &'static str {
    "replay scmstore activity log"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use anyhow::Context;
use async_runtime::block_on;
use async_runtime::block_unless_interrupted;
use clidispatch::errors;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use dag::namedag::IndexedLogNameDagPath;
use dag::ops::DagImportCloneData;
use dag::ops::DagPersistent;
use dag::ops::Open;
use dag::CloneData;
use dag::Group;
use dag::VertexListWithOptions;
use dag::VertexName;
use progress_model::ProgressBar;

use super::ConfigSet;
use super::Result;

define_flags! {
    pub struct StatusOpts {
        #[arg]
        reponame: String,

        #[arg]
        dest: String,
    }
}
pub fn run(ctx: ReqCtx<StatusOpts>, config: &mut ConfigSet) -> Result<u8> {
    let reponame = ctx.opts.reponame;
    let destination = PathBuf::from(&ctx.opts.dest);

    if destination.exists() {
        return Err(
            errors::Abort(format!("destination {} exists", destination.display()).into()).into(),
        );
    }

    config.set(
        "remotefilelog",
        "reponame",
        Some(reponame.clone()),
        &"arg".into(),
    );
    let edenapi_client = edenapi::Builder::from_config(config)?.build()?;

    let clone_data = match block_unless_interrupted(edenapi_client.clone_data()) {
        Err(e) => Err(anyhow::Error::from(e)),
        Ok(Err(e)) => Err(anyhow::Error::from(e)),
        Ok(Ok(v)) => Ok(v),
    }
    .context("error cloning segmented changelog")?;

    let ident = identity::default();
    let dot_path = destination.join(ident.dot_dir());

    let mut segments_path = dot_path.clone();
    segments_path.push("store");
    segments_path.push("segments");
    segments_path.push("v1");
    let namedag_path = IndexedLogNameDagPath(segments_path);

    let mut namedag = namedag_path
        .open()
        .context("error opening segmented changelog")?;

    let len = clone_data.idmap.len();
    let bar = ProgressBar::register_new("Building", len as _, "commits");
    let idmap: BTreeMap<_, _> = clone_data
        .idmap
        .into_iter()
        .map(|(k, v)| {
            bar.increase_position(1);
            (k, VertexName::copy_from(&v.into_byte_array()))
        })
        .collect();

    let master = idmap.iter().max_by_key(|i| i.0).map(|i| i.1.clone());
    if let Some(master) = master {
        let vertex_clone_data = CloneData {
            flat_segments: clone_data.flat_segments,
            idmap,
        };
        block_on(namedag.import_clone_data(vertex_clone_data))
            .context("error importing segmented changelog")?;

        let heads =
            VertexListWithOptions::from(vec![master.clone()]).with_highest_group(Group::MASTER);
        block_on(namedag.flush(&heads)).context("error writing segmented changelog to disk")?;

        let mut remotenames_path = dot_path.clone();
        remotenames_path.push("store");
        remotenames_path.push("remotenames");

        fs::write(
            remotenames_path,
            format!("{} bookmarks remote/master\n", master.to_hex()).as_bytes(),
        )
        .context("error writing to remotenames")?;
    }

    fs::write(
        dot_path.join("requires"),
        b"dotencode\n\
          fncache\n\
          generaldelta\n\
          remotefilelog\n\
          store\n\
          treestate\n",
    )
    .context("error writing to requires")?;

    let mut store_requires_path = dot_path.clone();
    store_requires_path.push("store");
    store_requires_path.push("requires");

    fs::write(
        store_requires_path,
        b"lazychangelog\n\
          narrowheads\n\
          visibleheads\n",
    )
    .context("error writing to store requires")?;

    fs::write(
        dot_path.join(ident.config_repo_file()),
        format!(
            "[paths]\n\
             default = ssh://hg.vip.facebook.com//data/scm/{0}\n\
             %include /etc/mercurial/repo-specific/{0}.rc\n",
            reponame
        )
        .as_bytes(),
    )
    .context("error writing to store requires")?;

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugsegmentclone"
}

pub fn doc() -> &'static str {
    "clone a repository using segmented changelog"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;

use clidispatch::errors;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use dag::render::render_segment_dag;

use super::Repo;
use super::Result;

define_flags! {
    pub struct GraphOpts {
        /// segment level (0 is flat)
        #[short('l')]
        level: i64 = 0,

        /// segment group (master|non_master)
        #[short('g')]
        group: String = "master",
    }
}

pub fn run(ctx: ReqCtx<GraphOpts>, repo: &mut Repo) -> Result<u8> {
    let group = match ctx.opts.group.as_ref() {
        "master" => dag::Group::MASTER,
        "non_master" => dag::Group::NON_MASTER,
        _ => return Err(errors::Abort("invalid group".into()).into()),
    };

    let level: dag::Level = match ctx.opts.level.try_into() {
        Ok(level) => level,
        _ => return Err(errors::Abort("invalid level".into()).into()),
    };

    let mut out = ctx.io().output();
    write!(out, "{}, Level: {}\n", group, level)?;

    let dag = dag::Dag::open(repo.store_path().join("segments/v1"))?;
    render_segment_dag(out, &dag, level, group)?;

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugsegmentgraph"
}

pub fn doc() -> &'static str {
    "display segment graph for a given group and level"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


/*
 * 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::Context;
use async_runtime::block_unless_interrupted as block_on;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use dag::namedag::IndexedLogNameDagPath;
use dag::ops::DagImportPullData;
use dag::ops::DagPersistent;
use dag::ops::Open;
use dag::CloneData;
use dag::Group;
use dag::VertexListWithOptions;
use dag::VertexName;
use types::HgId;

use super::Repo;
use super::Result;

define_flags! {
    pub struct StatusOpts {
        #[arg]
        from: String,

        #[arg]
        to: String,
    }
}

pub fn run(ctx: ReqCtx<StatusOpts>, repo: &mut Repo) -> Result<u8> {
    let config = repo.config();

    let edenapi_client = edenapi::Builder::from_config(config)?.build()?;

    let mut segments_path = repo.dot_hg_path().to_path_buf();
    segments_path.push("store");
    segments_path.push("segments");
    segments_path.push("v1");
    let namedag_path = IndexedLogNameDagPath(segments_path);
    let mut namedag = namedag_path
        .open()
        .context("error opening segmented changelog")?;

    let from = HgId::from_hex(ctx.opts.from.as_bytes()).unwrap();
    let to = HgId::from_hex(ctx.opts.to.as_bytes()).unwrap();
    let pull_data = block_on(edenapi_client.pull_fast_forward_master(from, to))
        .context("error pulling segmented changelog")??;

    ctx.io().write(format!(
        "Got {} segments and {} ids\n",
        pull_data.flat_segments.segments.len(),
        pull_data.idmap.len()
    ))?;

    let idmap = pull_data
        .idmap
        .into_iter()
        .map(|(k, v)| (k, VertexName::copy_from(&v.into_byte_array())))
        .collect();

    let vertex_pull_data = CloneData {
        flat_segments: pull_data.flat_segments,
        idmap,
    };
    let heads = VertexListWithOptions::from(vec![VertexName::copy_from(ctx.opts.to.as_bytes())])
        .with_highest_group(Group::MASTER);

    block_on(namedag.import_pull_data(vertex_pull_data, &heads))
        .context("error importing segmented changelog")??;

    let master = VertexName::copy_from(&to.into_byte_array());
    let heads = VertexListWithOptions::from(vec![master.clone()]).with_highest_group(Group::MASTER);
    block_on(namedag.flush(&heads)).context("error writing segmented changelog to disk")??;

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugsegmentpull"
}

pub fn doc() -> &'static str {
    "pull a repository using segmented changelog. This command does not do discovery and requrires specifying old/new master revisions"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use clidispatch::ReqCtx;
use configloader::convert::ByteCount;
use configmodel::ConfigExt;
use revisionstore::CorruptionPolicy;
use revisionstore::DataPackStore;
use revisionstore::ExtStoredPolicy;
use revisionstore::HgIdDataStore;
use revisionstore::IndexedLogHgIdDataStore;
use revisionstore::IndexedLogHgIdDataStoreConfig;
use revisionstore::StoreKey;
use revisionstore::StoreResult;
use revisionstore::StoreType;
use revisionstore::UnionHgIdDataStore;
use types::HgId;
use types::Key;
use types::RepoPathBuf;

use super::define_flags;
use super::Repo;
use super::Result;

define_flags! {
    pub struct DebugstoreOpts {
        /// print blob contents
        content: bool,

        #[arg]
        path: String,

        #[arg]
        hgid: String,
    }
}

pub fn run(ctx: ReqCtx<DebugstoreOpts>, repo: &mut Repo) -> Result<u8> {
    let path = RepoPathBuf::from_string(ctx.opts.path)?;
    let hgid = HgId::from_str(&ctx.opts.hgid)?;
    let config = repo.config();

    let packs_path = revisionstore::util::get_cache_path(config, &Some("packs"))?;
    let packstore = Box::new(DataPackStore::new(
        packs_path,
        CorruptionPolicy::IGNORE,
        None,
        ExtStoredPolicy::Use,
    ));

    let datastore_path = revisionstore::util::get_cache_path(config, &Some("indexedlogdatastore"))?;

    let max_log_count = config.get_opt::<u8>("indexedlog", "data.max-log-count")?;
    let max_bytes_per_log = config.get_opt::<ByteCount>("indexedlog", "data.max-bytes-per-log")?;
    let max_bytes = config.get_opt::<ByteCount>("remotefilelog", "cachelimit")?;
    let indexedlog_config = IndexedLogHgIdDataStoreConfig {
        max_log_count,
        max_bytes_per_log,
        max_bytes,
    };

    let indexedstore = Box::new(
        IndexedLogHgIdDataStore::new(
            datastore_path,
            ExtStoredPolicy::Use,
            &indexedlog_config,
            StoreType::Local,
        )
        .unwrap(),
    );
    let mut unionstore: UnionHgIdDataStore<Box<dyn HgIdDataStore>> = UnionHgIdDataStore::new();
    unionstore.add(packstore);
    unionstore.add(indexedstore);
    let k = Key::new(path, hgid);
    if let StoreResult::Found(content) = unionstore.get(StoreKey::hgid(k))? {
        ctx.core.io.write(content)?;
    }
    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugstore"
}

pub fn doc() -> &'static str {
    "print information about blobstore"
}

pub fn synopsis() -> Option<&'static str> {
    None
}


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

use std::io::Write;
use std::thread::sleep;
use std::time::Duration;

use clidispatch::io::IsTty;
use clidispatch::ReqCtx;
use cliparser::define_flags;
use comfy_table::Table;
use debugtop::TableGenerator;

use super::Repo;
use super::Result;

define_flags! {
    pub struct DebugTopOpts {
        /// rate to refresh in milliseconds
        #[short('r')]
        refresh_rate: i64 = 1000,

        /// amount of milliseconds to show a process after it finishes
        #[short('d')]
        reap_delay: i64 = 2000,

        /// columns separated by comma; shows all if none is specified
        #[short('c')]
        columns: String = "",
    }
}

pub fn run(ctx: ReqCtx<DebugTopOpts>, repo: &mut Repo) -> Result<u8> {
    let mut stdout = ctx.io().output();
    let mut stderr = ctx.io().error();
    let running_in_tty = stdout.is_tty();
    let refresh_rate = ctx.opts.refresh_rate.max(0) as u64;
    let reap_delay = chrono::Duration::milliseconds(ctx.opts.reap_delay);

    let mut table_generator = match TableGenerator::new(ctx.opts.columns, reap_delay) {
        Err(unexpected_columns) => {
            for column in unexpected_columns.iter() {
                write!(stderr, "Error: column \"{}\" was not expected\n", column)?;
            }
            return Ok(22);
        }
        Ok(table_generator) => table_generator,
    };

    loop {
        let mut table = Table::new();
        table.set_header(table_generator.column_titles());
        let entries = runlog::FileStore::entry_iter(repo.shared_dot_hg_path())?;
        for row in table_generator.generate_rows(entries, chrono::offset::Utc::now) {
            table.add_row(row);
        }
        if !running_in_tty {
            write!(stdout, "{}\n", table)?;
            break;
        }
        ctx.core
            .io
            .set_progress_str(format!("{}\n", table).as_str())?;
        sleep(Duration::from_millis(refresh_rate));
    }

    Ok(0)
}

pub fn aliases() -> &'static str {
    "debugtop"
}

pub fn doc() -> &'static str {
    "outputs information about all running commands for the current repository"
}

pub fn synopsis() -> Option<&'static str> {
    None
}