/*
 * 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 fetch;
mod is_chunked;
mod metadata;
mod store;
mod verify;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use clap::ArgGroup;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use ephemeral_blobstore::RepoEphemeralStore;
use fetch::FilestoreFetchArgs;
use filestore::Alias;
use filestore::FetchKey;
use filestore::FilestoreConfig;
use is_chunked::FilestoreIsChunkedArgs;
use metadata::FilestoreMetadataArgs;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mononoke_types::hash::Blake3;
use mononoke_types::hash::GitSha1;
use mononoke_types::hash::Sha1;
use mononoke_types::hash::Sha256;
use mononoke_types::ContentId;
use repo_blobstore::RepoBlobstore;
use store::FilestoreStoreArgs;
use verify::FilestoreVerifyArgs;

/// Inspect and interact with the filestore.
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

    #[clap(subcommand)]
    subcommand: FilestoreSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    repo_ephemeral_store: RepoEphemeralStore,

    #[facet]
    filestore_config: FilestoreConfig,
}

#[derive(Subcommand)]
pub enum FilestoreSubcommand {
    /// Fetch a file from the filestore
    Fetch(FilestoreFetchArgs),
    /// Store a file in the filestore
    Store(FilestoreStoreArgs),
    /// Show metadata for a file in the filestore
    Metadata(FilestoreMetadataArgs),
    /// Show whether or not a file in the filestore is chunked
    IsChunked(FilestoreIsChunkedArgs),
    /// Verify a file is fetchable by all of its id types
    Verify(FilestoreVerifyArgs),
}

#[derive(Args)]
#[clap(group(ArgGroup::new("filestore-item-id").args(&["content_id", "content_sha1", "content_sha256"]).required(true)))]
pub struct FilestoreItemIdArgs {
    #[clap(long, short = 'i')]
    content_id: Option<ContentId>,

    #[clap(long)]
    content_sha1: Option<Sha1>,

    #[clap(long)]
    content_sha256: Option<Sha256>,

    #[clap(long)]
    content_seeded_blake3: Option<Blake3>,

    #[clap(long)]
    content_git_sha1: Option<GitSha1>,
}

impl FilestoreItemIdArgs {
    fn fetch_key(&self) -> Result<FetchKey> {
        if let Some(content_id) = self.content_id {
            Ok(FetchKey::Canonical(content_id))
        } else if let Some(sha1) = self.content_sha1 {
            Ok(FetchKey::Aliased(Alias::Sha1(sha1)))
        } else if let Some(sha256) = self.content_sha256 {
            Ok(FetchKey::Aliased(Alias::Sha256(sha256)))
        } else if let Some(seeded_blake3) = self.content_seeded_blake3 {
            Ok(FetchKey::Aliased(Alias::SeededBlake3(seeded_blake3)))
        } else if let Some(git_sha1) = self.content_git_sha1 {
            Ok(FetchKey::Aliased(Alias::GitSha1(git_sha1)))
        } else {
            Err(anyhow!("Filestore item id required"))
        }
    }
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo_unredacted(&args.repo)
        .await
        .context("Failed to open repo")?;

    match args.subcommand {
        FilestoreSubcommand::Fetch(fetch_args) => fetch::fetch(&ctx, &repo, fetch_args).await?,
        FilestoreSubcommand::Store(store_args) => store::store(&ctx, &repo, store_args).await?,
        FilestoreSubcommand::Metadata(metadata_args) => {
            metadata::metadata(&ctx, &repo, metadata_args).await?
        }
        FilestoreSubcommand::IsChunked(is_chunked_args) => {
            is_chunked::is_chunked(&ctx, &repo, is_chunked_args).await?
        }
        FilestoreSubcommand::Verify(verify_args) => {
            verify::verify(&ctx, &repo, verify_args).await?
        }
    }

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

mod inspect;
mod last_processed;
mod remains;
mod show;
mod verify;

use anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use bookmarks::BookmarkUpdateLog;
use clap::Parser;
use clap::Subcommand;
use ephemeral_blobstore::RepoEphemeralStore;
use inspect::HgSyncInspectArgs;
use last_processed::HgSyncLastProcessedArgs;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mutable_counters::MutableCounters;
use remains::HgSyncRemainsArgs;
use repo_blobstore::RepoBlobstore;
use repo_identity::RepoIdentity;
use show::HgSyncShowArgs;
use verify::HgSyncVerifyArgs;

const LATEST_REPLAYED_REQUEST_KEY: &str = "latest-replayed-request";

/// Inspect and interact with synchronization to Mercurial repositories.
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

    #[clap(subcommand)]
    subcommand: HgSyncSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    repo_identity: RepoIdentity,

    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    repo_ephemeral_store: RepoEphemeralStore,

    #[facet]
    bookmark_update_log: dyn BookmarkUpdateLog,

    #[facet]
    mutable_counters: dyn MutableCounters,
}

#[derive(Subcommand)]
pub enum HgSyncSubcommand {
    /// Print information about a hg-sync log entry
    Inspect(HgSyncInspectArgs),
    /// Show or update the last processed log entry id
    LastProcessed(HgSyncLastProcessedArgs),
    /// Show how many bundles are remaining to be synced
    Remains(HgSyncRemainsArgs),
    /// Show the next bundles to be synced
    Show(HgSyncShowArgs),
    /// Verify that all remaining bundles are of consistent types
    Verify(HgSyncVerifyArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo(&args.repo)
        .await
        .context("Failed to open repo")?;

    match args.subcommand {
        HgSyncSubcommand::Inspect(inspect_args) => {
            inspect::inspect(&ctx, &repo, inspect_args).await?
        }
        HgSyncSubcommand::LastProcessed(last_processed_args) => {
            last_processed::last_processed(&ctx, &repo, last_processed_args).await?
        }
        HgSyncSubcommand::Remains(remains_args) => {
            remains::remains(&ctx, &repo, remains_args).await?
        }
        HgSyncSubcommand::Show(show_args) => show::show(&ctx, &repo, show_args).await?,
        HgSyncSubcommand::Verify(verify_args) => verify::verify(&ctx, &repo, verify_args).await?,
    }

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

mod get;
mod list;
mod set;

use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use clap::Subcommand;
use get::GetArgs;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mutable_counters::MutableCounters;
use repo_identity::RepoIdentity;
use set::SetArgs;

/// Get, set or list mutable counters
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

    #[clap(subcommand)]
    subcommand: MutableCountersSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    mutable_counters: dyn MutableCounters,
    #[facet]
    repo_identity: RepoIdentity,
}

#[derive(Subcommand)]
pub enum MutableCountersSubcommand {
    /// Get the current value of a mutable counter in a repo.
    Get(GetArgs),
    /// Set the value of a mutable counter in a repo. If the counter doesn't exist, create it.
    Set(SetArgs),
    /// List all the mutable counters in repos along with their values.
    List,
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();
    let repo: Repo = app
        .open_repo(&args.repo)
        .await
        .context("Failed to open repo")?;

    match args.subcommand {
        MutableCountersSubcommand::Get(args) => get::get(&ctx, &repo, args).await?,
        MutableCountersSubcommand::Set(args) => set::set(&ctx, &repo, args).await?,
        MutableCountersSubcommand::List => list::list(&ctx, &repo).await?,
    }
    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.
 */

mod add;
mod check_commit;
mod copy_immutable;
mod get;

use add::AddArgs;
use anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use changesets::Changesets;
use check_commit::CheckCommitArgs;
use clap::Parser;
use clap::Subcommand;
use copy_immutable::CopyImmutableArgs;
use get::GetArgs;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mutable_renames::MutableRenames;
use repo_blobstore::RepoBlobstore;
use repo_derived_data::RepoDerivedData;

/// Fetch and update mutable renames information
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

    #[clap(subcommand)]
    subcommand: MutableRenamesSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    repo_derived_data: RepoDerivedData,

    #[facet]
    mutable_renames: MutableRenames,

    #[facet]
    changesets: dyn Changesets,
}

#[derive(Subcommand)]
pub enum MutableRenamesSubcommand {
    /// Determine if a commit has mutable rename information attached
    CheckCommit(CheckCommitArgs),
    /// Get mutable rename information for a given commit, path pair
    Get(GetArgs),
    /// Add new mutable renames to your repo
    Add(AddArgs),
    /// Copy immutable renames to mutable renames
    CopyImmutable(CopyImmutableArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo(&args.repo)
        .await
        .context("Failed to open repo")?;

    match args.subcommand {
        MutableRenamesSubcommand::CheckCommit(args) => {
            check_commit::check_commit(&ctx, &repo, args).await?
        }
        MutableRenamesSubcommand::Get(args) => get::get(&ctx, &repo, args).await?,
        MutableRenamesSubcommand::Add(args) => add::add(&ctx, &repo, args).await?,
        MutableRenamesSubcommand::CopyImmutable(args) => {
            copy_immutable::copy_immutable(&ctx, &repo, args).await?
        }
    }

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

mod create_key_list;
mod list;

use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use bookmarks::Bookmarks;
use clap::Parser;
use clap::Subcommand;
use create_key_list::RedactionCreateKeyListArgs;
use create_key_list::RedactionCreateKeyListFromIdsArgs;
use create_key_list::RedactionFetchKeyListArgs;
use list::RedactionListArgs;
use metaconfig_types::RepoConfig;
use mononoke_app::MononokeApp;
use repo_blobstore::RepoBlobstore;
use repo_derived_data::RepoDerivedData;

/// Manage repository bookmarks
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(subcommand)]
    subcommand: RedactionSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    repo_config: RepoConfig,

    #[facet]
    bookmarks: dyn Bookmarks,

    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    repo_derived_data: RepoDerivedData,
}

#[derive(Subcommand)]
pub enum RedactionSubcommand {
    /// Create a key list using files in a changeset.
    CreateKeyList(RedactionCreateKeyListArgs),
    /// Create a key list using content ids.
    CreateKeyListFromIds(RedactionCreateKeyListFromIdsArgs),
    /// List the redacted files in a commit.
    List(RedactionListArgs),
    /// Fetch a key list from its key list id.
    FetchKeyList(RedactionFetchKeyListArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    match args.subcommand {
        RedactionSubcommand::CreateKeyList(create_args) => {
            create_key_list::create_key_list_from_commit_files(&ctx, &app, create_args).await?
        }
        RedactionSubcommand::CreateKeyListFromIds(create_args) => {
            create_key_list::create_key_list_from_blobstore_keys(&ctx, &app, create_args).await?
        }
        RedactionSubcommand::List(list_args) => list::list(&ctx, &app, list_args).await?,
        RedactionSubcommand::FetchKeyList(fetch_args) => {
            create_key_list::fetch_key_list(&ctx, &app, fetch_args).await?
        }
    }

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

mod info;
mod lock;

use anyhow::Result;
use bookmarks::Bookmarks;
use clap::Parser;
use clap::Subcommand;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use repo_identity::RepoIdentity;
use repo_lock::RepoLock;

/// Operations over a whole repo
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

    #[clap(subcommand)]
    subcommand: RepoSubcommand,
}

#[derive(Clone)]
#[facet::container]
pub struct Repo {
    #[facet]
    repo_identity: RepoIdentity,

    #[facet]
    bookmarks: dyn Bookmarks,

    #[facet]
    lock: dyn RepoLock,
}

#[derive(Subcommand)]
pub enum RepoSubcommand {
    /// Show information about a repository
    Info(info::RepoInfoArgs),
    /// Lock a repository
    Lock(lock::RepoLockArgs),
    /// Unlock a repository
    Unlock(lock::RepoUnlockArgs),
    /// Show current lock status of a repository
    ShowLock(lock::RepoShowLockArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();
    let repo: Repo = app.open_repo(&args.repo).await?;

    use RepoSubcommand::*;
    match args.subcommand {
        Info(args) => info::repo_info(&ctx, &repo, args).await?,
        Lock(args) => lock::repo_lock(&app, &repo, args).await?,
        Unlock(args) => lock::repo_unlock(&app, &repo, args).await?,
        ShowLock(args) => lock::repo_show_lock(&app, &repo, args).await?,
    }
    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.
 */

mod list;
mod show_locks;

use anyhow::Result;
use clap::Parser;
use clap::Subcommand;
use mononoke_app::MononokeApp;

/// Operations over a whole repo
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(subcommand)]
    subcommand: ReposSubcommand,
}

#[derive(Subcommand)]
pub enum ReposSubcommand {
    /// List configured repositories
    List(list::ReposListArgs),
    /// Show all locks currently active
    ShowLocks(show_locks::ReposShowLocksArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    use ReposSubcommand::*;
    match args.subcommand {
        List(args) => list::repos_list(app, args).await?,
        ShowLocks(args) => show_locks::repos_show_locks(app, args).await?,
    }
    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 anyhow::anyhow;
use anyhow::Error;
use anyhow::Result;
use async_requests::types::MegarepoAsynchronousRequestResult;
use async_requests::types::RowId;
use async_requests::types::ThriftMegarepoAsynchronousRequestParams;
use clap::Args;
use context::CoreContext;
use megarepo_api::MegarepoApi;
use megarepo_error::MegarepoError;
use source_control::MegarepoAddBranchingTargetResult;
use source_control::MegarepoAddTargetResult;
use source_control::MegarepoChangeTargetConfigResult;
use source_control::MegarepoRemergeSourceResult;
use source_control::MegarepoSyncChangesetResult;

#[derive(Args)]
/// Changes the request status to ready and put error as result.
/// (this won't stop any currently running workers immediately)
pub struct AsyncRequestsAbortArgs {
    /// ID of the request.
    #[clap(long)]
    request_id: u64,
}

pub async fn abort_request(
    args: AsyncRequestsAbortArgs,
    ctx: CoreContext,
    megarepo: MegarepoApi,
) -> Result<(), Error> {
    let repos_and_queues = megarepo.all_async_method_request_queues(&ctx).await?;

    let row_id = args.request_id;

    for (_repo_ids, queue) in repos_and_queues {
        if let Some((request_id, _entry, params, maybe_result)) =
            queue.get_request_by_id(&ctx, &RowId(row_id)).await?
        {
            if maybe_result.is_none() {
                let err = MegarepoError::InternalError(anyhow!("aborted from CLI!").into());
                let result: MegarepoAsynchronousRequestResult  = match params.thrift() {
                    ThriftMegarepoAsynchronousRequestParams::megarepo_sync_changeset_params(_) => {
                        MegarepoSyncChangesetResult::error(err.into()).into()
                    }
                    ThriftMegarepoAsynchronousRequestParams::megarepo_add_target_params(_) => {
                        MegarepoAddTargetResult::error(err.into()).into()
                    }
                    ThriftMegarepoAsynchronousRequestParams::megarepo_change_target_params(_) => {
                        MegarepoChangeTargetConfigResult::error(err.into()).into()
                    }
                    ThriftMegarepoAsynchronousRequestParams::megarepo_remerge_source_params(_) => {
                        MegarepoRemergeSourceResult::error(err.into()).into()
                    }
                    ThriftMegarepoAsynchronousRequestParams::megarepo_add_branching_target_params(_) => {
                        MegarepoAddBranchingTargetResult::error(err.into()).into()
                    }
                    _ => return Err(anyhow!("unknown request type!"))
                };
                queue.complete(&ctx, &request_id, result).await?;
            } else {
                return Err(anyhow!("Request already completed."));
            }
            return Ok(());
        }
    }
    Err(anyhow!("Request not found."))
}


/*
 * 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::Error;
use anyhow::Result;
use async_requests::types::ThriftMegarepoAsynchronousRequestParams;
use clap::Args;
use context::CoreContext;
use megarepo_api::MegarepoApi;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime;
use mononoke_types::Timestamp;
use prettytable::format;
use prettytable::row;
use prettytable::Table;

#[derive(Args)]
/// Lists asynchronous requests (by default the ones active
/// now or updated within last 5 mins).
pub struct AsyncRequestsListArgs {
    /// Limits the results to the requests updated
    /// in the last N seconds.
    #[clap(long, default_value = "3600")]
    lookback: i64,
}

pub async fn list_requests(
    args: AsyncRequestsListArgs,
    ctx: CoreContext,
    megarepo: MegarepoApi,
) -> Result<(), Error> {
    let repos_and_queues = megarepo.all_async_method_request_queues(&ctx).await?;
    let lookback = args.lookback;
    let mut table = Table::new();
    table.set_titles(row![
        "Request id",
        "Method",
        "Status",
        "Target bookmark",
        "Source name (sync_changeset)",
        "Source Changeset (sync_changeset)",
        "Created at",
        "Ready at",
        "Duration",
    ]);
    for (repo_ids, queue) in repos_and_queues {
        let res = queue
            .list_requests(
                &ctx,
                &repo_ids,
                Some(&Timestamp::from_timestamp_secs(
                    Timestamp::now().timestamp_seconds() - lookback,
                )),
            )
            .await?;
        for (req_id, entry, params) in res.into_iter() {
            let (source_name, changeset_id) = match params.thrift() {
                ThriftMegarepoAsynchronousRequestParams::megarepo_sync_changeset_params(params) => {
                    (
                        params.source_name.clone(),
                        ChangesetId::from_bytes(params.cs_id.clone())?.to_string(),
                    )
                }
                _ => ("".to_string(), "".to_string()),
            };
            let created_at: DateTime = entry.created_at.into();
            let ready_at: Option<DateTime> = entry.ready_at.map(|t| t.into());
            let ready_at_str =
                ready_at.map_or_else(|| "Not finished".to_string(), |t| t.to_string());
            let duration = if let Some(ready_at) = ready_at {
                let duration = ready_at.into_chrono() - created_at.into_chrono();
                duration.to_string()
            } else {
                "Not finished".to_string()
            };
            table.add_row(row![
                req_id.0,
                req_id.1,
                entry.status,
                params.target()?.bookmark,
                &source_name,
                &changeset_id,
                &created_at,
                &ready_at_str,
                duration,
            ]);
        }
    }
    table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
    table.printstd();

    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 anyhow::anyhow;
use anyhow::Error;
use anyhow::Result;
use async_requests::types::RowId;
use clap::Args;
use context::CoreContext;
use megarepo_api::MegarepoApi;

#[derive(Args)]
/// Changes the request status to ready and put error as result.
/// (this won't stop any currently running workers immediately)
pub struct AsyncRequestsRequeueArgs {
    /// ID of the request.
    #[clap(long)]
    request_id: u64,
}

pub async fn requeue_request(
    args: AsyncRequestsRequeueArgs,
    ctx: CoreContext,
    megarepo: MegarepoApi,
) -> Result<(), Error> {
    let repos_and_queues = megarepo.all_async_method_request_queues(&ctx).await?;

    let row_id = args.request_id;

    for (_repo_ids, queue) in repos_and_queues {
        if let Some((request_id, _entry, _params, _maybe_result)) =
            queue.get_request_by_id(&ctx, &RowId(row_id)).await?
        {
            queue.requeue(&ctx, request_id).await?;
            return Ok(());
        }
    }
    Err(anyhow!("Request not found."))
}


/*
 * 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::Error;
use anyhow::Result;
use async_requests::types::IntoConfigFormat;
use async_requests::types::MegarepoAsynchronousRequestParams;
use async_requests::types::MegarepoAsynchronousRequestResult;
use async_requests::types::RowId;
use async_requests::types::ThriftMegarepoAsynchronousRequestParams;
use async_requests::types::ThriftMegarepoAsynchronousRequestResult;
use async_requests::types::ThriftMegarepoSyncChangesetResult;
use clap::Args;
use context::CoreContext;
use megarepo_api::MegarepoApi;
use mononoke_api::Mononoke;
use mononoke_types::ChangesetId;

#[derive(Args)]
/// Subcommand responsible for showing the request
/// details.
pub struct AsyncRequestsShowArgs {
    /// ID of the request.
    #[clap(long)]
    request_id: u64,
}

struct ParamsWrapper<'a>(&'a Mononoke, MegarepoAsynchronousRequestParams);

impl<'a> std::fmt::Debug for ParamsWrapper<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // impl Debug for HexArray here
        match self.1.thrift() {
            ThriftMegarepoAsynchronousRequestParams::megarepo_add_target_params(params) => f
                .debug_struct("MegarepoAddTargetParams")
                .field(
                    "config_with_new_target",
                    &params
                        .config_with_new_target
                        .clone()
                        .into_config_format(self.0),
                )
                .field(
                    "changesets_to_merge",
                    &params
                        .changesets_to_merge
                        .iter()
                        .map(|(k, v)| (k.clone(), ChangesetId::from_bytes(v)))
                        .collect::<Vec<_>>(),
                )
                .field("message", &params.message)
                .finish()?,
            ThriftMegarepoAsynchronousRequestParams::megarepo_change_target_params(params) => f
                .debug_struct("MegarepoAddTargetParams")
                .field("target", &params.target)
                .field("new_version", &params.new_version)
                .field(
                    "target_location",
                    &ChangesetId::from_bytes(&params.target_location),
                )
                .field(
                    "changesets_to_merge",
                    &params
                        .changesets_to_merge
                        .iter()
                        .map(|(k, v)| (k.clone(), ChangesetId::from_bytes(v)))
                        .collect::<Vec<_>>(),
                )
                .field("message", &params.message)
                .finish()?,
            ThriftMegarepoAsynchronousRequestParams::megarepo_sync_changeset_params(params) => f
                .debug_struct("MegarepoSyncChangesetParams")
                .field("source_name", &params.source_name)
                .field("target", &params.target)
                .field("cs_id", &ChangesetId::from_bytes(&params.cs_id))
                .field(
                    "target_location",
                    &ChangesetId::from_bytes(&params.target_location),
                )
                .finish()?,
            other => f.write_str(format!("{:?}", other).as_str())?,
        }
        Ok(())
    }
}

struct ResultsWrapper<'a>(&'a Mononoke, Option<MegarepoAsynchronousRequestResult>);
impl<'a> std::fmt::Debug for ResultsWrapper<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // impl Debug for HexArray here
        match &self.1 {
            Some(res) => match res.thrift() {
                ThriftMegarepoAsynchronousRequestResult::megarepo_sync_changeset_result(
                    ThriftMegarepoSyncChangesetResult::success(result),
                ) => f
                    .debug_struct("MegarepoSyncChangesetResponse")
                    .field("cs_id", &ChangesetId::from_bytes(&result.cs_id))
                    .finish()?,
                other => f.write_str(format!("{:?}", other).as_str())?,
            },
            None => (),
        }
        Ok(())
    }
}

pub async fn show_request(
    args: AsyncRequestsShowArgs,
    ctx: CoreContext,
    megarepo: MegarepoApi,
) -> Result<(), Error> {
    let repos_and_queues = megarepo.all_async_method_request_queues(&ctx).await?;

    let row_id = args.request_id;

    for (_repo_ids, queue) in repos_and_queues {
        if let Some((_request_id, entry, params, maybe_result)) =
            queue.get_request_by_id(&ctx, &RowId(row_id)).await?
        {
            println!(
                "Entry: {:#?}\nParams: {:#?}\nResult: {:#?}",
                entry,
                ParamsWrapper(&megarepo.mononoke(), params),
                ResultsWrapper(&megarepo.mononoke(), maybe_result),
            );
            return Ok(());
        }
    }
    Err(anyhow!("Request not found."))
}


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

use anyhow::Context;
use anyhow::Result;
use blobstore::Blobstore;
use blobstore::BlobstoreGetData;
use bytes::Bytes;
use changeset_info::ChangesetInfo;
use chrono::Local;
use chrono::TimeZone;
use clap::Args;
use clap::ValueEnum;
use cmdlib_displaying::hexdump;
use context::CoreContext;
use git_types::Tree as GitTree;
use mercurial_types::HgChangesetEnvelope;
use mercurial_types::HgFileEnvelope;
use mercurial_types::HgManifestEnvelope;
use mononoke_types::basename_suffix_skeleton_manifest::BasenameSuffixSkeletonManifest;
use mononoke_types::basename_suffix_skeleton_manifest::BssmEntry;
use mononoke_types::blame_v2::BlameV2;
use mononoke_types::deleted_manifest_v2::DeletedManifestV2;
use mononoke_types::fastlog_batch::FastlogBatch;
use mononoke_types::fsnode::Fsnode;
use mononoke_types::sharded_map::ShardedMapNode;
use mononoke_types::skeleton_manifest::SkeletonManifest;
use mononoke_types::typed_hash::DeletedManifestV2Id;
use mononoke_types::unode::FileUnode;
use mononoke_types::unode::ManifestUnode;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ContentAlias;
use mononoke_types::ContentChunk;
use mononoke_types::ContentMetadataV2;
use mononoke_types::FileContents;
use mononoke_types::ThriftConvert;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

#[derive(Args)]
pub struct BlobstoreFetchArgs {
    /// Write raw blob bytes to the given filename instead of
    /// printing to stdout.
    #[clap(long, short = 'o', value_name = "FILE")]
    output: Option<PathBuf>,

    /// Blobstore key to fetch.
    #[clap(required = true)]
    key: String,

    /// Don't show blob info header.
    #[clap(long, short = 'q')]
    quiet: bool,

    /// Decode as a particular type.
    #[clap(long, value_enum, default_value = "auto")]
    decode_as: DecodeAs,
}

#[derive(ValueEnum, Copy, Clone, Eq, PartialEq)]
pub enum DecodeAs {
    Hex,
    Auto,
    Changeset,
    Content,
    ContentChunk,
    HgChangeset,
    HgManifest,
    HgFilenode,
    GitTree,
    SkeletonManifest,
    Fsnode,
    ContentMetadataV2,
    Alias,
    FileUnode,
    ManifestUnode,
    FastlogBatch,
    DeletedManifestV2MapNode,
    DeletedManifestV2,
    BlameV2,
    BasenameSuffixSkeletonManifestMapNode,
    BasenameSuffixSkeletonManifest,
    ChangesetInfo,
}

impl DecodeAs {
    fn from_key_prefix(key: &str) -> Option<Self> {
        for index in Some(0)
            .into_iter()
            .chain(key.match_indices('.').map(|(index, _)| index + 1))
        {
            for (prefix, auto_decode_as) in [
                ("changeset.", DecodeAs::Changeset),
                ("content.", DecodeAs::Content),
                ("chunk.", DecodeAs::ContentChunk),
                ("hgchangeset.", DecodeAs::HgChangeset),
                ("hgmanifest.", DecodeAs::HgManifest),
                ("hgfilenode.", DecodeAs::HgFilenode),
                ("git.tree.", DecodeAs::GitTree),
                ("skeletonmanifest.", DecodeAs::SkeletonManifest),
                ("fsnode.", DecodeAs::Fsnode),
                ("content_metadata2.", DecodeAs::ContentMetadataV2),
                ("alias.", DecodeAs::Alias),
                ("fileunode.", DecodeAs::FileUnode),
                ("manifestunode.", DecodeAs::ManifestUnode),
                ("fastlogbatch.", DecodeAs::FastlogBatch),
                (
                    "deletedmanifest2.mapnode.",
                    DecodeAs::DeletedManifestV2MapNode,
                ),
                ("deletedmanifest2.", DecodeAs::DeletedManifestV2),
                ("blame_v2.", DecodeAs::BlameV2),
                (
                    "bssm.mapnode.",
                    DecodeAs::BasenameSuffixSkeletonManifestMapNode,
                ),
                ("bssm.", DecodeAs::BasenameSuffixSkeletonManifest),
                ("changeset_info.", DecodeAs::ChangesetInfo),
            ] {
                if key[index..].starts_with(prefix) {
                    return Some(auto_decode_as);
                }
            }
        }
        None
    }
}

enum Decoded {
    None,
    Fail(String),
    Display(String),
    Hexdump(Bytes),
}

impl Decoded {
    fn try_display<T: std::fmt::Display, E: std::fmt::Display>(data: Result<T, E>) -> Decoded {
        match data {
            Ok(data) => Decoded::Display(data.to_string()),
            Err(err) => Decoded::Fail(err.to_string()),
        }
    }

    fn try_debug<T: std::fmt::Debug, E: std::fmt::Display>(data: Result<T, E>) -> Decoded {
        match data {
            Ok(data) => Decoded::Display(format!("{:#?}", data)),
            Err(err) => Decoded::Fail(err.to_string()),
        }
    }
}

fn decode(key: &str, data: BlobstoreGetData, mut decode_as: DecodeAs) -> Decoded {
    if decode_as == DecodeAs::Auto {
        if let Some(auto_decode_as) = DecodeAs::from_key_prefix(key) {
            decode_as = auto_decode_as;
        }
    }
    match decode_as {
        DecodeAs::Hex | DecodeAs::Auto => Decoded::None,
        DecodeAs::Changeset => Decoded::try_debug(BonsaiChangeset::from_bytes(data.as_raw_bytes())),
        DecodeAs::Content => match FileContents::from_encoded_bytes(data.into_raw_bytes()) {
            Ok(FileContents::Bytes(data)) => Decoded::Hexdump(data),
            Ok(FileContents::Chunked(chunked)) => Decoded::Display(format!("{:#?}", chunked)),
            Err(err) => Decoded::Fail(err.to_string()),
        },
        DecodeAs::ContentChunk => match ContentChunk::from_encoded_bytes(data.into_raw_bytes()) {
            Ok(chunk) => Decoded::Hexdump(chunk.into_bytes()),
            Err(err) => Decoded::Fail(err.to_string()),
        },
        DecodeAs::HgChangeset => Decoded::try_display(HgChangesetEnvelope::from_blob(data.into())),
        DecodeAs::HgManifest => Decoded::try_display(HgManifestEnvelope::from_blob(data.into())),
        DecodeAs::HgFilenode => Decoded::try_display(HgFileEnvelope::from_blob(data.into())),
        DecodeAs::GitTree => Decoded::try_display(GitTree::try_from(data)),
        DecodeAs::SkeletonManifest => {
            Decoded::try_debug(SkeletonManifest::from_bytes(data.into_raw_bytes().as_ref()))
        }
        DecodeAs::Fsnode => Decoded::try_debug(Fsnode::from_bytes(data.into_raw_bytes().as_ref())),
        DecodeAs::ContentMetadataV2 => Decoded::try_debug(ContentMetadataV2::from_bytes(
            data.into_raw_bytes().as_ref(),
        )),
        DecodeAs::Alias => Decoded::try_debug(ContentAlias::from_bytes(data.into_raw_bytes())),
        DecodeAs::FileUnode => {
            Decoded::try_debug(FileUnode::from_bytes(data.into_raw_bytes().as_ref()))
        }
        DecodeAs::ManifestUnode => {
            Decoded::try_debug(ManifestUnode::from_bytes(data.into_raw_bytes().as_ref()))
        }
        DecodeAs::FastlogBatch => {
            Decoded::try_debug(FastlogBatch::from_bytes(&data.into_raw_bytes()))
        }
        DecodeAs::DeletedManifestV2 => {
            Decoded::try_debug(DeletedManifestV2::from_bytes(&data.into_raw_bytes()))
        }
        DecodeAs::DeletedManifestV2MapNode => {
            Decoded::try_debug(ShardedMapNode::<DeletedManifestV2Id>::from_bytes(
                &data.into_raw_bytes(),
            ))
        }
        DecodeAs::BlameV2 => {
            Decoded::try_debug(BlameV2::from_bytes(data.into_raw_bytes().as_ref()))
        }
        DecodeAs::BasenameSuffixSkeletonManifest => Decoded::try_debug(
            BasenameSuffixSkeletonManifest::from_bytes(&data.into_raw_bytes()),
        ),
        DecodeAs::BasenameSuffixSkeletonManifestMapNode => {
            Decoded::try_debug(ShardedMapNode::<BssmEntry>::from_bytes(
                &data.into_raw_bytes(),
            ))
        }
        DecodeAs::ChangesetInfo => {
            Decoded::try_debug(ChangesetInfo::from_bytes(&data.into_raw_bytes()))
        }
    }
}

pub async fn fetch(
    ctx: &CoreContext,
    blobstore: &dyn Blobstore,
    fetch_args: BlobstoreFetchArgs,
) -> Result<()> {
    let value = blobstore
        .get(ctx, &fetch_args.key)
        .await
        .context("Failed to fetch blob")?;
    match value {
        None => {
            writeln!(std::io::stderr(), "No blob exists for {}", fetch_args.key)?;
        }
        Some(value) => {
            if !fetch_args.quiet {
                writeln!(std::io::stdout(), "Key: {}", fetch_args.key)?;
                if let Some(ctime) = value.as_meta().ctime() {
                    writeln!(
                        std::io::stdout(),
                        "Ctime: {} ({})",
                        ctime,
                        Local.timestamp_opt(ctime, 0).unwrap()
                    )?;
                }
                if let Some(sizes) = value.as_meta().sizes() {
                    writeln!(
                        std::io::stdout(),
                        "Size: {} ({} compressed)",
                        value.len(),
                        sizes.unique_compressed_size
                    )?;
                } else {
                    writeln!(std::io::stdout(), "Size: {}", value.len())?;
                }
                writeln!(std::io::stdout())?;
            }
            if let Some(output) = fetch_args.output {
                let mut file = File::create(output)
                    .await
                    .context("Failed to create output file")?;
                file.write_all(value.as_raw_bytes())
                    .await
                    .context("Failed to write to output file")?;
                file.flush().await?;
            } else {
                let bytes = value.as_raw_bytes().clone();
                match decode(&fetch_args.key, value, fetch_args.decode_as) {
                    Decoded::Display(decoded) => {
                        writeln!(std::io::stdout(), "{}", decoded)?;
                    }
                    Decoded::Hexdump(data) => {
                        hexdump(std::io::stdout(), data)?;
                    }
                    Decoded::Fail(err) => {
                        writeln!(std::io::stderr(), "Failed to decode: {}", err)?;
                        // Fall back to dumping as raw hex
                        hexdump(std::io::stdout(), bytes)?;
                    }
                    Decoded::None => {
                        hexdump(std::io::stdout(), bytes)?;
                    }
                }
            }
        }
    }

    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 anyhow::Context;
use anyhow::Result;
use blobstore::Blobstore;
use clap::Args;
use context::CoreContext;
use futures::stream;
use futures::StreamExt;

#[derive(Args)]
pub struct BlobstoreFetchManyArgs {
    /// File with whitespace separated keys
    #[clap(long)]
    keys_file: String,

    /// How many fetches to do concurrently
    #[clap(long, default_value_t = 50)]
    concurrency: usize,
}

#[derive(Default)]
struct Stats {
    present: usize,
    missing: usize,
    failed: usize,
}

impl Stats {
    fn present() -> Self {
        Self {
            present: 1,
            missing: 0,
            failed: 0,
        }
    }
    fn missing() -> Self {
        Self {
            present: 0,
            missing: 1,
            failed: 0,
        }
    }
    fn failed() -> Self {
        Self {
            present: 0,
            missing: 0,
            failed: 1,
        }
    }
}

impl std::iter::Sum for Stats {
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
        let mut ans = Self::default();
        for stat in iter {
            ans.present += stat.present;
            ans.missing += stat.missing;
            ans.failed += stat.failed;
        }
        ans
    }
}

pub async fn fetch_many(
    ctx: &CoreContext,
    blobstore: &dyn Blobstore,
    args: BlobstoreFetchManyArgs,
) -> Result<()> {
    let text = std::fs::read_to_string(args.keys_file).context("Reading keys file")?;
    let keys = text.split_whitespace();
    let stats: Stats = stream::iter(keys)
        .map(|key| async move {
            match blobstore.get(ctx, key).await {
                Err(_) => Stats::failed(),
                Ok(Some(_)) => Stats::present(),
                Ok(None) => Stats::missing(),
            }
        })
        // Prevents compiler bug
        .boxed()
        .buffer_unordered(args.concurrency)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .sum();

    println!(
        "present: {}\nmissing: {}\nfailed: {}",
        stats.present, stats.missing, stats.failed
    );

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

use anyhow::Context;
use anyhow::Result;
use blobstore::Blobstore;
use blobstore::BlobstoreBytes;
use clap::Args;
use context::CoreContext;

#[derive(Args)]
pub struct BlobstoreUploadArgs {
    /// Read raw blob bytes from the given filename
    #[clap(long, value_name = "FILE")]
    value_file: PathBuf,

    /// Blobstore key to fetch.
    #[clap(long)]
    key: String,
}

pub async fn upload(
    ctx: &CoreContext,
    blobstore: &dyn Blobstore,
    upload_args: BlobstoreUploadArgs,
) -> Result<()> {
    let data = tokio::fs::read(upload_args.value_file)
        .await
        .context("Failed to read value file")?;

    writeln!(
        std::io::stdout(),
        "Writing {} bytes to blobstore key {}",
        data.len(),
        upload_args.key
    )?;

    blobstore
        .put(ctx, upload_args.key, BlobstoreBytes::from_bytes(data))
        .await
        .context("Failed to upload blob")?;

    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::time::Duration;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use bookmarks_movement::check_bookmark_sync_config;
use bookmarks_movement::BookmarkKind;
use clap::Args;
use context::CoreContext;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct BookmarksDeleteArgs {
    /// Name of the bookmark to delete
    name: BookmarkKey,

    /// Force deleting of bookmark in repos with pushredirection enabled
    /// (WARNING: this may break megarepo sync)
    #[clap(long)]
    force_megarepo: bool,

    /// Delete a scratch bookmark
    ///
    /// Normally whether a bookmark is scratch or not is determined by
    /// a regex pattern in repository config.  This command does not use
    /// that configuration, and you must specify whether or not the
    /// bookmark is scratch using this flag.
    #[clap(long)]
    scratch: bool,

    /// Specify the expected current value for the bookmark.
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    #[clap(long)]
    old_commit_id: Option<String>,
}

pub async fn delete(
    ctx: &CoreContext,
    repo: &Repo,
    delete_args: BookmarksDeleteArgs,
) -> Result<()> {
    let kind = if delete_args.scratch {
        BookmarkKind::Scratch
    } else {
        BookmarkKind::Publishing
    };
    let old_value = if let Some(old_commit_id) = &delete_args.old_commit_id {
        parse_commit_id(ctx, repo, old_commit_id).await?
    } else {
        repo.bookmarks()
            .get(ctx.clone(), &delete_args.name)
            .await
            .with_context(|| format!("Failed to resolve bookmark '{}'", delete_args.name))?
            .ok_or_else(|| {
                anyhow!(
                    "Cannot delete non-existent {} bookmark {}",
                    kind.to_string(),
                    delete_args.name
                )
            })?
    };

    println!(
        "Deleting {} bookmark {} at {}",
        kind, delete_args.name, old_value,
    );

    if let Err(e) = check_bookmark_sync_config(repo, &delete_args.name, kind) {
        if delete_args.force_megarepo {
            println!("Deleting bookmark in megarepo-synced repository (--force-megarepo)");
            println!("Waiting 3 seconds. Ctrl-C now if you did not intend this - risk of SEV!");
            tokio::time::sleep(Duration::from_secs(3)).await;
        } else {
            return Err(e).context("Refusing to delete bookmark in megarepo-synced repository");
        }
    };

    // Wait 1s to allow for Ctrl-C
    tokio::time::sleep(Duration::from_secs(1)).await;

    let mut transaction = repo.bookmarks().create_transaction(ctx.clone());

    match kind {
        BookmarkKind::Publishing | BookmarkKind::PullDefaultPublishing => {
            transaction.delete(
                &delete_args.name,
                old_value,
                BookmarkUpdateReason::ManualMove,
            )?;
        }
        BookmarkKind::Scratch => {
            transaction.delete_scratch(&delete_args.name, old_value)?;
        }
    }
    transaction.commit().await?;
    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 anyhow::Context;
use anyhow::Result;
use bookmarks::BookmarkCategory;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkName;
use bookmarks::BookmarksRef;
use clap::Args;
use context::CoreContext;

use super::Repo;
use crate::commit_id::print_commit_id;
use crate::commit_id::IdentityScheme;

#[derive(Args)]
pub struct BookmarksGetArgs {
    /// Name of the bookmark to get
    name: BookmarkName,

    /// Category of the bookmark to get
    #[clap(long, default_value = "branch")]
    category: BookmarkCategory,

    /// Commit identity schemes to display
    #[clap(long, short='S', value_enum, default_values = &["bonsai"], use_value_delimiter = true)]
    schemes: Vec<IdentityScheme>,
}

pub async fn get(ctx: &CoreContext, repo: &Repo, get_args: BookmarksGetArgs) -> Result<()> {
    let key = BookmarkKey::with_name_and_category(get_args.name, get_args.category);
    let bookmark_value = repo
        .bookmarks()
        .get(ctx.clone(), &key)
        .await
        .with_context(|| format!("Failed to resolve bookmark '{}'", key))?;
    match bookmark_value {
        None => println!("(not set)"),
        Some(cs_id) => {
            // If the bookmark is a tag, print the ID of the changeset containing the
            // metadata associated with the tag along with the changeset that it points to.
            if key.category() == &BookmarkCategory::Tag {
                let metadata_changeset = repo
                    .bonsai_tag_mapping
                    .get_changeset_by_tag_name(key.name().clone().into_string())
                    .await?;
                match metadata_changeset {
                    Some(metadata_changeset) => {
                        println!("Metadata changeset for tag bookmark {}: ", key.name());
                        print_commit_id(ctx, repo, &[], metadata_changeset).await?;
                    }
                    None => println!(
                        "Metadata changeset doesn't exist for tag bookmark {}",
                        key.name()
                    ),
                }
                println!("Changeset pointed to by the tag bookmark {}", key.name());
            }
            print_commit_id(ctx, repo, &get_args.schemes, cs_id).await?
        }
    }

    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::fmt;

use anyhow::Error;
use anyhow::Result;
use bookmarks::Bookmark;
use bookmarks::BookmarkCategory;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkKind;
use bookmarks::BookmarkPagination;
use bookmarks::BookmarkPrefix;
use bookmarks::BookmarksRef;
use bookmarks::Freshness;
use clap::Args;
use context::CoreContext;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use mononoke_types::ChangesetId;

use super::Repo;
use crate::commit_id::IdentityScheme;

#[derive(Args)]
pub struct BookmarksListArgs {
    /// Kind of bookmarks to list
    #[clap(long = "kind", short = 'k', value_name = "KIND", value_enum, default_values = &["publishing", "pull-default-publishing"])]
    kinds: Vec<BookmarkKind>,

    /// Categories of bookmarks to list
    #[clap(long = "category", short = 'c', value_name = "CATEGORY", value_enum, default_values = &["branch", "tag", "note"])]
    categories: Vec<BookmarkCategory>,

    /// Prefix of bookmarks to list
    #[clap(long)]
    prefix: Option<BookmarkPrefix>,

    /// Show at most this number of bookmarks
    #[clap(long, short = 'l', default_value_t = 100)]
    limit: u64,

    /// Show bookmarks after this name (continue pagination)
    #[clap(long)]
    after: Option<BookmarkKey>,

    /// Commit identity schemes to display
    #[clap(long, short='S', value_enum, default_values = &["bonsai"], use_value_delimiter = true)]
    schemes: Vec<IdentityScheme>,

    /// Request most recent values of bookmarks (bypass caches and replicas)
    #[clap(long)]
    fresh: bool,
}

struct BookmarkValue {
    bookmark: Bookmark,
    ids: Vec<(IdentityScheme, String)>,
}

impl BookmarkValue {
    async fn new(
        ctx: &CoreContext,
        repo: &Repo,
        bookmark: Bookmark,
        changeset_id: ChangesetId,
        schemes: &[IdentityScheme],
    ) -> Result<Self> {
        let ids = stream::iter(schemes.iter().copied())
            .map(|scheme| {
                Ok::<_, Error>(async move {
                    match scheme.map_commit_id(ctx, repo, changeset_id).await? {
                        Some(commit_id) => Ok(Some((scheme, commit_id))),
                        None => Ok(None),
                    }
                })
            })
            .try_buffered(10)
            .try_filter_map(|commit_id| async move { Ok(commit_id) })
            .try_collect()
            .await?;
        Ok(BookmarkValue { bookmark, ids })
    }
}

impl fmt::Display for BookmarkValue {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self.ids.as_slice() {
            [] => {}
            [(_, id)] => write!(fmt, "{} ", id)?,
            ids => {
                for (scheme, id) in ids {
                    write!(fmt, "{}={} ", scheme, id)?;
                }
            }
        }
        write!(fmt, "{}", self.bookmark.name())?;
        Ok(())
    }
}

pub async fn list(ctx: &CoreContext, repo: &Repo, list_args: BookmarksListArgs) -> Result<()> {
    let freshness = if list_args.fresh {
        Freshness::MostRecent
    } else {
        Freshness::MaybeStale
    };
    let prefix = list_args.prefix.unwrap_or_else(BookmarkPrefix::empty);
    let pagination = list_args
        .after
        .map(|bookmark_key| bookmark_key.into_name())
        .map_or(BookmarkPagination::FromStart, BookmarkPagination::After);
    repo.bookmarks()
        .list(
            ctx.clone(),
            freshness,
            &prefix,
            &list_args.categories,
            &list_args.kinds,
            &pagination,
            list_args.limit,
        )
        .map_ok(|(bookmark, cs_id)| {
            BookmarkValue::new(ctx, repo, bookmark, cs_id, &list_args.schemes)
        })
        .try_buffered(100)
        .try_for_each(|value| async move {
            println!("{}", value);
            Ok(())
        })
        .await?;
    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 anyhow::anyhow;
use anyhow::Result;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::Freshness;
use clap::Args;
use context::CoreContext;
use futures::stream::TryStreamExt;
use mononoke_types::DateTime;

use super::Repo;
use crate::bookmark_log_entry::BookmarkLogEntry;
use crate::commit_id::IdentityScheme;

#[derive(Args)]
pub struct BookmarksLogArgs {
    /// Name of the bookmark to show the log for
    name: BookmarkKey,

    /// Commit identity schemes to display
    #[clap(long, short='S', value_enum, default_values = &["bonsai"], use_value_delimiter = true)]
    schemes: Vec<IdentityScheme>,

    /// Limit the number of entries returned
    #[clap(long, short = 'l', default_value_t = 25)]
    limit: u32,

    /// Filter log records to those starting at this time
    /// (either absolute time, or e.g. "2 hours ago").
    #[clap(long, short = 's')]
    start_time: Option<DateTime>,

    /// Filter log records to those ending at this time
    /// (either absolute time, or e.g. "2 hours ago").
    #[clap(long, short = 'e', requires = "start_time")]
    end_time: Option<DateTime>,
}

pub async fn log(ctx: &CoreContext, repo: &Repo, log_args: BookmarksLogArgs) -> Result<()> {
    let entries = if log_args.start_time.is_some() || log_args.end_time.is_some() {
        let start = log_args
            .start_time
            .ok_or_else(|| anyhow!("end-time requires start-time"))?;
        let end = log_args.end_time.unwrap_or_else(DateTime::now);
        repo.bookmark_update_log()
            .list_bookmark_log_entries_ts_in_range(
                ctx.clone(),
                log_args.name.clone(),
                log_args.limit,
                start.into(),
                end.into(),
            )
    } else {
        repo.bookmark_update_log().list_bookmark_log_entries(
            ctx.clone(),
            log_args.name.clone(),
            log_args.limit,
            None,
            Freshness::MostRecent,
        )
    };

    entries
        .map_ok(|(entry_id, cs_id, reason, timestamp)| {
            BookmarkLogEntry::new(
                ctx,
                repo,
                timestamp,
                log_args.name.clone(),
                reason,
                cs_id,
                Some(entry_id),
                &log_args.schemes,
            )
        })
        .try_buffered(100)
        .try_for_each(|entry| async move {
            println!("{}", entry);
            Ok(())
        })
        .await?;
    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::time::Duration;

use anyhow::Context;
use anyhow::Result;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use bookmarks_movement::check_bookmark_sync_config;
use bookmarks_movement::BookmarkKind;
use clap::Args;
use context::CoreContext;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct BookmarksSetArgs {
    /// Name of the bookmark to set
    name: BookmarkKey,

    /// Commit ID to set the bookmark to
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    commit_id: String,

    /// Force setting of bookmark in repos with pushredirection enabled
    /// (WARNING: this may break megarepo sync)
    #[clap(long)]
    force_megarepo: bool,

    /// Mark the bookmark being created or modified as scratch
    ///
    /// Normally whether a bookmark is scratch or not is determined by
    /// a regex pattern in repository config.  This command does not use
    /// that configuration, and you must specify whether or not the
    /// bookmark is scratch using this flag.
    #[clap(long)]
    scratch: bool,

    /// Specify the expected current value for the bookmark.
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    #[clap(long)]
    old_commit_id: Option<String>,

    /// Only set this bookmark if it does not already exist.
    #[clap(long, conflicts_with = "old_commit_id")]
    create_only: bool,
}

pub async fn set(ctx: &CoreContext, repo: &Repo, set_args: BookmarksSetArgs) -> Result<()> {
    let kind = if set_args.scratch {
        BookmarkKind::Scratch
    } else {
        BookmarkKind::Publishing
    };
    let target = parse_commit_id(ctx, repo, &set_args.commit_id).await?;
    let old_value = if set_args.create_only {
        None
    } else if let Some(old_commit_id) = &set_args.old_commit_id {
        Some(parse_commit_id(ctx, repo, old_commit_id).await?)
    } else {
        repo.bookmarks()
            .get(ctx.clone(), &set_args.name)
            .await
            .with_context(|| format!("Failed to resolve bookmark '{}'", set_args.name))?
    };

    match old_value {
        Some(old_value) => {
            println!(
                "Updating {} bookmark {} from {} to {}",
                kind, set_args.name, old_value, target
            );
        }
        None => {
            println!("Creating {} bookmark {} at {}", kind, set_args.name, target);
        }
    }

    if let Err(e) = check_bookmark_sync_config(repo, &set_args.name, kind) {
        if set_args.force_megarepo {
            println!("Moving bookmark in megarepo-synced repository (--force-megarepo)");
            println!("Waiting 3 seconds. Ctrl-C now if you did not intend this - risk of SEV!");
            tokio::time::sleep(Duration::from_secs(3)).await;
        } else {
            return Err(e).context("Refusing to move bookmark in megarepo-synced repository");
        }
    };

    // Wait 1s to allow for Ctrl-C
    tokio::time::sleep(Duration::from_secs(1)).await;

    let mut transaction = repo.bookmarks().create_transaction(ctx.clone());

    match (old_value, kind) {
        (Some(old_value), BookmarkKind::Publishing | BookmarkKind::PullDefaultPublishing) => {
            transaction.update(
                &set_args.name,
                target,
                old_value,
                BookmarkUpdateReason::ManualMove,
            )?;
        }
        (None, BookmarkKind::Publishing | BookmarkKind::PullDefaultPublishing) => {
            transaction.create(&set_args.name, target, BookmarkUpdateReason::ManualMove)?;
        }
        (Some(old_value), BookmarkKind::Scratch) => {
            transaction.update_scratch(&set_args.name, target, old_value)?;
        }
        (None, BookmarkKind::Scratch) => {
            transaction.create_scratch(&set_args.name, target)?;
        }
    }
    transaction.commit().await?;
    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::BinaryHeap;
use std::collections::HashMap;

use anyhow::Context;
use anyhow::Result;
use blobstore::Loadable;
use changeset_fetcher::ArcChangesetFetcher;
use changeset_fetcher::ChangesetFetcherArc;
use clap::Args;
use context::CoreContext;
use dag::render::Ancestor;
use dag::render::GraphRowRenderer;
use dag::render::Renderer;
use futures::future::join_all;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use mononoke_types::Generation;
use repo_blobstore::RepoBlobstoreRef;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct ChangelogGraphArgs {
    /// Initial changesets to start displaying from
    #[clap(long, short = 'i', use_value_delimiter = true)]
    changesets: Vec<String>,

    /// Maximum distance from the initial changesets to any displayed changeset
    #[clap(long, short, default_value_t = 10)]
    limit: usize,

    /// Display commit message for all displayed changesets
    #[clap(long, short = 'M')]
    display_message: bool,

    /// Display bonsai id for all displayed changesets
    #[clap(long, short = 'I')]
    display_id: bool,

    /// Display commit author for all displayed changesets
    #[clap(long, short = 'A')]
    display_author: bool,

    /// Display commit author date for all displayed changesets
    #[clap(long, short = 'D')]
    display_author_date: bool,
}

pub fn get_message(graph_args: &ChangelogGraphArgs, cs: BonsaiChangeset) -> String {
    let mut message_vec = Vec::new();

    if graph_args.display_message {
        message_vec.push(format!("message: {}", cs.message()));
    }

    if graph_args.display_id {
        message_vec.push(format!("id: {}", cs.get_changeset_id()));
    }

    if graph_args.display_author {
        message_vec.push(format!("author: {}", cs.author()));
    }

    if graph_args.display_author_date {
        message_vec.push(format!("author date: {}", cs.author_date()));
    }

    message_vec.join(", ")
}

pub async fn graph_changesets(
    ctx: &CoreContext,
    repo: &Repo,
    graph_args: ChangelogGraphArgs,
    changeset_fetcher: ArcChangesetFetcher,
    changesets: Vec<ChangesetId>,
) -> Result<()> {
    let blobstore = repo.repo_blobstore();

    let mut minimum_distance: HashMap<ChangesetId, usize> =
        changesets.iter().cloned().map(|id| (id, 0)).collect();

    let mut to_visit: BinaryHeap<(Generation, ChangesetId)> =
        join_all(changesets.into_iter().map(|head| {
            let ctx = ctx;
            let changeset_fetcher = &changeset_fetcher;
            async move {
                Ok((
                    changeset_fetcher.get_generation_number(ctx, head).await?,
                    head,
                ))
            }
        }))
        .await
        .into_iter()
        .collect::<Result<_>>()?;

    let mut renderer = GraphRowRenderer::<ChangesetId>::new()
        .output()
        .build_box_drawing();

    while let Some((_, hash)) = to_visit.pop() {
        let parents = changeset_fetcher.get_parents(ctx, hash).await?;
        let current_distance = *minimum_distance.get(&hash).unwrap();

        if current_distance > graph_args.limit {
            println!(
                "{}",
                renderer
                    .next_row(hash, Vec::new(), String::from("~"), String::from(""))
                    .trim_end()
            );
            continue;
        }

        let cs = hash
            .load(ctx, blobstore)
            .await
            .with_context(|| format!("Failed to load changeset {}", hash))?;

        println!(
            "{}",
            renderer
                .next_row(
                    hash,
                    parents.iter().cloned().map(Ancestor::Parent).collect(),
                    String::from("o"),
                    get_message(&graph_args, cs),
                )
                .trim_end()
        );

        for parent_id in parents.into_iter() {
            if let Some(&distance) = minimum_distance.get(&parent_id) {
                if current_distance + 1 < distance {
                    minimum_distance.insert(parent_id, current_distance + 1);
                }
            } else {
                let parent_generation = changeset_fetcher
                    .get_generation_number(ctx, parent_id)
                    .await?;

                minimum_distance.insert(parent_id, current_distance + 1);
                to_visit.push((parent_generation, parent_id));
            }
        }
    }

    Ok(())
}

pub async fn graph(ctx: &CoreContext, repo: &Repo, graph_args: ChangelogGraphArgs) -> Result<()> {
    let changeset_fetcher = repo.changeset_fetcher_arc();

    let mut changesets = join_all(
        graph_args
            .changesets
            .iter()
            .map(move |head| async move { parse_commit_id(ctx, repo, head).await }),
    )
    .await
    .into_iter()
    .collect::<Result<Vec<ChangesetId>>>()?;

    changesets.sort();
    changesets.dedup();

    graph_changesets(ctx, repo, graph_args, changeset_fetcher, changesets).await
}


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

use anyhow::Result;
use changeset_fetcher::ChangesetFetcherArc;
use clap::Args;
use context::CoreContext;
use futures::compat::Stream01CompatExt;
use futures::StreamExt;
use futures::TryStreamExt;
use revset::AncestorsNodeStream;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct ChangelogListAncestorsArgs {
    /// Changeset to start at
    #[clap(long, short = 'i')]
    changeset_id: String,

    /// Number of ancestors to fetch
    #[clap(long, short, default_value_t = 10)]
    limit: usize,
}

pub async fn list_ancestors(
    ctx: &CoreContext,
    repo: &Repo,
    list_ancestors_args: ChangelogListAncestorsArgs,
) -> Result<()> {
    let start = parse_commit_id(ctx, repo, &list_ancestors_args.changeset_id).await?;

    let mut ancestors = AncestorsNodeStream::new(ctx.clone(), &repo.changeset_fetcher_arc(), start)
        .compat()
        .take(list_ancestors_args.limit);

    while let Some(cs_id) = ancestors.try_next().await? {
        println!("{}", cs_id);
    }

    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 anyhow::Error;
use anyhow::Result;
use blobstore::Loadable;
use bookmarks::BookmarkKey;
use clap::Args;
use context::CoreContext;
use futures::stream;
use futures::StreamExt;
use futures::TryFutureExt;
use futures::TryStreamExt;
use metaconfig_types::RepoConfigRef;
use repo_blobstore::RepoBlobstoreRef;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct CommitPushrebaseArgs {
    /// Destination Bookmark to pushrebase onto
    #[clap(long, short = 'B')]
    bookmark: BookmarkKey,

    /// Source Commit ID to pushrebase (bottom of the stack if pushrebasing a stack)
    #[clap(long, short = 's')]
    source: String,

    /// Top Commit ID of the source stack, if pushrebasing a stack
    #[clap(long, short = 't')]
    top: Option<String>,
}

pub async fn pushrebase(
    ctx: &CoreContext,
    repo: &Repo,
    pushrebase_args: CommitPushrebaseArgs,
) -> Result<()> {
    let source = parse_commit_id(ctx, repo, &pushrebase_args.source).await?;

    let csids = if let Some(top) = &pushrebase_args.top {
        let top = parse_commit_id(ctx, repo, top).await?;
        super::resolve_stack(ctx, repo, source, top).await?
    } else {
        vec![source]
    };

    let pushrebase_flags = &repo.repo_config().pushrebase.flags;
    let pushrebase_hooks = bookmarks_movement::get_pushrebase_hooks(
        ctx,
        repo,
        &pushrebase_args.bookmark,
        &repo.repo_config().pushrebase,
    )
    .map_err(Error::from)?;

    let bonsais = stream::iter(csids)
        .map(|csid| async move {
            csid.load(ctx, repo.repo_blobstore())
                .map_err(Error::from)
                .await
        })
        .buffered(100)
        .try_collect::<HashSet<_>>()
        .await?;

    let result = pushrebase::do_pushrebase_bonsai(
        ctx,
        repo,
        pushrebase_flags,
        &pushrebase_args.bookmark,
        &bonsais,
        &pushrebase_hooks,
    )
    .map_err(Error::from)
    .await?;

    println!("{}", result.head);

    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 anyhow::bail;
use anyhow::Error;
use anyhow::Result;
use blobstore::Loadable;
use clap::Args;
use context::CoreContext;
use mononoke_types::ChangesetId;
use mononoke_types::FileChange;
use repo_blobstore::RepoBlobstoreRef;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct CommitRebaseArgs {
    /// Destination Commit ID to rebase onto
    #[clap(long, short = 'd')]
    dest: String,

    /// Source Commit ID to rebase (bottom of the stack if rebasing a stack)
    #[clap(long, short = 's')]
    source: String,

    /// Top Commit ID of the source stack, if rebasing a stack
    #[clap(long, short = 't')]
    top: Option<String>,

    /// Skip rebase validity checks (only use if you know what you're doing).
    #[clap(long)]
    skip_rebase_validity_checks: bool,
}

pub async fn rebase(ctx: &CoreContext, repo: &Repo, rebase_args: CommitRebaseArgs) -> Result<()> {
    if !rebase_args.skip_rebase_validity_checks {
        bail!("You must provide --skip-rebase-validity-checks to this command");
    }

    let dest = parse_commit_id(ctx, repo, &rebase_args.dest).await?;
    let source = parse_commit_id(ctx, repo, &rebase_args.source).await?;

    if let Some(top) = &rebase_args.top {
        let top = parse_commit_id(ctx, repo, top).await?;
        let csids = super::resolve_stack(ctx, repo, source, top).await?;
        let mut dest = dest;
        for csid in csids {
            let rebased = rebase_single_changeset(ctx, repo, csid, dest).await?;
            println!("{}", rebased);
            dest = rebased;
        }
    } else {
        let rebased = rebase_single_changeset(ctx, repo, source, dest).await?;
        println!("{}", rebased);
    }

    Ok(())
}

async fn rebase_single_changeset(
    ctx: &CoreContext,
    repo: &Repo,
    source: ChangesetId,
    dest: ChangesetId,
) -> Result<ChangesetId> {
    let bcs = source
        .load(ctx, repo.repo_blobstore())
        .await
        .map_err(Error::from)?;
    let mut rebased = bcs.into_mut();
    if rebased.parents.is_empty() {
        rebased.parents.push(dest);
    } else {
        rebased.parents[0] = dest;
    }

    for file_change in rebased.file_changes.values_mut() {
        match file_change {
            FileChange::Change(fc) => {
                if let Some((_, ref mut parent)) = fc.copy_from_mut() {
                    *parent = dest;
                }
            }
            FileChange::Deletion
            | FileChange::UntrackedDeletion
            | FileChange::UntrackedChange(_) => {}
        }
    }

    let rebased = rebased.freeze()?;
    let rebased_cs_id = rebased.get_changeset_id();
    changesets_creation::save_changesets(ctx, repo, vec![rebased]).await?;
    Ok(rebased_cs_id)
}


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

use std::collections::BTreeMap;

use anyhow::bail;
use anyhow::Result;
use blobstore::Loadable;
use changesets_creation::save_changesets;
use clap::ArgGroup;
use clap::Args;
use context::CoreContext;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use mononoke_types::FileChange;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreRef;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
#[clap(group(ArgGroup::new("file-size-and-num").args(&["commit_file_size", "commit_file_num"]).multiple(true)))]
pub struct CommitSplitArgs {
    /// Commit ID to split
    commit_id: String,

    /// Target sum of the size of files in each commit (in bytes)
    #[clap(long)]
    commit_file_size: Option<u64>,

    /// Target number of files in each commit
    #[clap(long)]
    commit_file_num: Option<u64>,
}

pub async fn split(ctx: &CoreContext, repo: &Repo, split_args: CommitSplitArgs) -> Result<()> {
    let cs_id = parse_commit_id(ctx, repo, &split_args.commit_id).await?;

    let stack = split_commit(
        ctx,
        repo,
        cs_id,
        split_args.commit_file_size,
        split_args.commit_file_num,
    )
    .await?;

    println!("Split {} into {} commits", cs_id, stack.len());

    Ok(())
}

async fn split_commit(
    ctx: &CoreContext,
    repo: &Repo,
    cs_id: ChangesetId,
    commit_file_size: Option<u64>,
    commit_file_num: Option<u64>,
) -> Result<Vec<ChangesetId>> {
    let bcs = cs_id.load(ctx, repo.repo_blobstore()).await?;

    if bcs.is_merge() {
        bail!("splitting merges is not supported!");
    }

    let mut parent = bcs.parents().next();
    let mut current_file_changes = BTreeMap::new();
    let mut current_file_size: u64 = 0;

    // We want copy/move sources and destination to be in one commit.
    // So let's group them here
    let mut file_groups: BTreeMap<MPath, Vec<(MPath, FileChange)>> = BTreeMap::new();
    let file_changes = bcs.file_changes_map();
    for (path, fc) in file_changes.iter() {
        if let FileChange::Change(ft) = fc {
            if let Some((from_path, _)) = ft.copy_from() {
                file_groups
                    .entry(from_path.clone())
                    .or_default()
                    .push((path.clone(), fc.clone()));
                continue;
            }
        }

        file_groups
            .entry(path.clone())
            .or_default()
            .push((path.clone(), fc.clone()));
    }

    let mut result = vec![];
    for file_group in file_groups.values() {
        let mut should_create_new_commit = false;
        if let Some(commit_file_num) = commit_file_num {
            if commit_file_num <= current_file_changes.len() as u64 {
                should_create_new_commit = true;
            }
        }

        if let Some(commit_file_size) = commit_file_size {
            if commit_file_size <= current_file_size {
                should_create_new_commit = true;
            }
        }

        if should_create_new_commit {
            let num_of_files = current_file_changes.len();
            let cs_id = create_new_commit(
                ctx,
                repo,
                bcs.clone(),
                parent.clone(),
                &mut current_file_changes,
            )
            .await?;
            parent = Some(cs_id);
            println!(
                "{} size: {} files: {}",
                cs_id, current_file_size, num_of_files
            );
            current_file_changes.clear();
            current_file_size = 0;
            result.extend(parent);
        }

        for (path, fc) in file_group {
            let new_fc = modify_file_change_parent(path, fc, parent)?;
            if let FileChange::Change(fc) = &new_fc {
                current_file_size += fc.size();
            }
            current_file_changes.insert(path.clone(), new_fc);
        }
    }

    if !current_file_changes.is_empty() {
        let num_of_files = current_file_changes.len();
        let cs_id = create_new_commit(
            ctx,
            repo,
            bcs.clone(),
            parent.clone(),
            &mut current_file_changes,
        )
        .await?;
        result.push(cs_id);
        println!(
            "{} size: {} files: {}",
            cs_id, current_file_size, num_of_files
        );
    }

    Ok(result)
}

fn modify_file_change_parent(
    path: &MPath,
    fc: &FileChange,
    parent: Option<ChangesetId>,
) -> Result<FileChange> {
    let new_fc = match fc {
        FileChange::Change(fc) => {
            // We need to fix copy info and change the parent
            if let Some((from_path, _)) = fc.copy_from() {
                let copy_from = if let Some(parent) = parent {
                    (from_path.clone(), parent)
                } else {
                    bail!(
                        "invalid bonsai changeset - it's a root commit, but has copy info for {}",
                        path
                    );
                };

                FileChange::Change(fc.with_new_copy_from(Some(copy_from)))
            } else {
                FileChange::Change(fc.clone())
            }
        }
        FileChange::Deletion => fc.clone(),
        FileChange::UntrackedDeletion | FileChange::UntrackedChange(_) => {
            bail!("cannot split snapshots");
        }
    };
    Ok(new_fc)
}

async fn create_new_commit(
    ctx: &CoreContext,
    repo: &Repo,
    bcs: BonsaiChangeset,
    parent: Option<ChangesetId>,
    current_file_changes: &mut BTreeMap<MPath, FileChange>,
) -> Result<ChangesetId> {
    let mut new_bcs = bcs.clone().into_mut();
    new_bcs.parents = parent.into_iter().collect();
    new_bcs.file_changes = std::mem::take(current_file_changes).into();
    let new_bcs = new_bcs.freeze()?;
    let cs_id = new_bcs.get_changeset_id();
    save_changesets(ctx, repo, vec![new_bcs]).await?;
    Ok(cs_id)
}

#[cfg(test)]
mod test {
    use bonsai_git_mapping::BonsaiGitMapping;
    use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
    use bonsai_hg_mapping::BonsaiHgMapping;
    use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
    use bookmarks::Bookmarks;
    use changeset_fetcher::ChangesetFetcher;
    use changesets::Changesets;
    use fbinit::FacebookInit;
    use filestore::FilestoreConfig;
    use maplit::hashmap;
    use repo_blobstore::RepoBlobstore;
    use repo_blobstore::RepoBlobstoreRef;
    use repo_derived_data::RepoDerivedData;
    use tests_utils::list_working_copy_utf8;
    use tests_utils::CreateCommitContext;

    use super::*;

    #[facet::container]
    struct BasicTestRepo {
        #[delegate(
            dyn BonsaiHgMapping,
            dyn BonsaiGitMapping,
            dyn BonsaiGlobalrevMapping,
            dyn BonsaiSvnrevMapping,
            dyn Changesets,
            RepoBlobstore,
        )]
        repo: Repo,

        #[facet]
        bookmarks: dyn Bookmarks,

        #[facet]
        filestore_config: FilestoreConfig,

        #[facet]
        repo_derived_data: RepoDerivedData,

        #[facet]
        changeset_fetcher: dyn ChangesetFetcher,
    }

    #[fbinit::test]
    async fn test_split_commit_simple(fb: FacebookInit) -> Result<()> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;

        let root = CreateCommitContext::new_root(&ctx, &repo)
            .add_file("first", "a")
            .add_file("second", "b")
            .add_file("third", "c")
            .commit()
            .await?;

        let split = split_commit(
            &ctx,
            &repo.repo,
            root,
            None,    // file size
            Some(1), // file num
        )
        .await?;

        assert_eq!(split.len(), 3);
        {
            // Make sure it has the right contents
            let cs_id = *split.last().unwrap();
            let wc = list_working_copy_utf8(&ctx, &repo, cs_id).await?;
            assert_eq!(
                wc,
                hashmap! {
                    MPath::new("first")? => "a".to_string(),
                    MPath::new("second")? => "b".to_string(),
                    MPath::new("third")? => "c".to_string(),
                }
            );

            let bcs = split[0].load(&ctx, repo.repo_blobstore()).await?;
            let parents: Vec<ChangesetId> = vec![];
            assert_eq!(parents, bcs.parents().collect::<Vec<ChangesetId>>());
            let bcs = split[1].load(&ctx, repo.repo_blobstore()).await?;
            assert_eq!(vec![split[0]], bcs.parents().collect::<Vec<_>>());
            let bcs = split[2].load(&ctx, repo.repo_blobstore()).await?;
            assert_eq!(vec![split[1]], bcs.parents().collect::<Vec<_>>());
        }

        // Now split by file size
        let split = split_commit(
            &ctx,
            &repo.repo,
            root,
            Some(1), // file size
            None,    // file num
        )
        .await?;

        assert_eq!(split.len(), 3);

        let split = split_commit(
            &ctx,
            &repo.repo,
            root,
            None,    // file size
            Some(3), // file num
        )
        .await?;

        assert_eq!(split.len(), 1);
        {
            let cs_id = *split.last().unwrap();
            // Make sure it has the right contents
            let wc = list_working_copy_utf8(&ctx, &repo, cs_id).await?;
            assert_eq!(
                wc,
                hashmap! {
                    MPath::new("first")? => "a".to_string(),
                    MPath::new("second")? => "b".to_string(),
                    MPath::new("third")? => "c".to_string(),
                }
            );
        }

        Ok(())
    }

    #[fbinit::test]
    async fn test_split_commit_with_renames(fb: FacebookInit) -> Result<()> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;

        let root = CreateCommitContext::new_root(&ctx, &repo)
            .add_file("first", "a")
            .commit()
            .await?;
        let to_split = CreateCommitContext::new(&ctx, &repo, vec![root])
            .add_file_with_copy_info("second", "a", (root, "first"))
            .delete_file("first")
            .add_file("third", "c")
            .commit()
            .await?;

        let split = split_commit(
            &ctx,
            &repo.repo,
            to_split,
            None,    // file size
            Some(1), // file num
        )
        .await?;

        assert_eq!(split.len(), 2);
        {
            // Make sure it has the right contents
            let cs_id = *split.last().unwrap();
            let wc = list_working_copy_utf8(&ctx, &repo, cs_id).await?;
            assert_eq!(
                wc,
                hashmap! {
                    MPath::new("second")? => "a".to_string(),
                    MPath::new("third")? => "c".to_string(),
                }
            );
        }

        Ok(())
    }

    #[fbinit::test]
    async fn test_split_commit_renamed_to_multiple_dest(fb: FacebookInit) -> Result<()> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;

        let root = CreateCommitContext::new_root(&ctx, &repo)
            .add_file("first", "a")
            .commit()
            .await?;
        let to_split = CreateCommitContext::new(&ctx, &repo, vec![root])
            .delete_file("first")
            .add_file_with_copy_info("second", "a", (root, "first"))
            .add_file_with_copy_info("third", "c", (root, "first"))
            .commit()
            .await?;

        let split = split_commit(
            &ctx,
            &repo.repo,
            to_split,
            None,    // file size
            Some(1), // file num
        )
        .await?;

        assert_eq!(split.len(), 1);
        {
            // Make sure it has the right contents
            let cs_id = *split.last().unwrap();
            let wc = list_working_copy_utf8(&ctx, &repo, cs_id).await?;
            assert_eq!(
                wc,
                hashmap! {
                    MPath::new("second")? => "a".to_string(),
                    MPath::new("third")? => "c".to_string(),
                }
            );
            let bcs = cs_id.load(&ctx, repo.repo_blobstore()).await?;
            assert_eq!(bcs.file_changes_map().len(), 3);
        }

        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 anyhow::Result;
use clap::Args;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use futures::future::try_join_all;
use futures::StreamExt;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct AncestorsDifferenceArgs {
    /// Commit IDs to display ancestors of.
    #[clap(long, use_value_delimiter = true)]
    heads: Vec<String>,

    /// Commit IDs to exclude ancestors of.
    #[clap(long, use_value_delimiter = true)]
    common: Vec<String>,
}

pub async fn ancestors_difference(
    ctx: &CoreContext,
    repo: &Repo,
    args: AncestorsDifferenceArgs,
) -> Result<()> {
    let heads: Vec<_> = try_join_all(
        args.heads
            .iter()
            .map(|id| parse_commit_id(ctx, repo, id))
            .collect::<Vec<_>>(),
    )
    .await?;
    let common: Vec<_> = try_join_all(
        args.common
            .iter()
            .map(|id| parse_commit_id(ctx, repo, id))
            .collect::<Vec<_>>(),
    )
    .await?;

    let mut ancestors_difference_stream = Box::pin(
        repo.commit_graph()
            .ancestors_difference_stream(ctx, heads, common)
            .await?,
    );
    while let Some(ancestor_result) = ancestors_difference_stream.next().await {
        println!("{}", ancestor_result?);
    }

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

use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use buffered_commit_graph_storage::BufferedCommitGraphStorage;
use bulkops::Direction;
use bulkops::PublicChangesetBulkFetch;
use caching_commit_graph_storage::CachingCommitGraphStorage;
use caching_ext::CacheHandlerFactory;
use clap::Args;
use commit_graph::CommitGraph;
use commit_graph_types::storage::CommitGraphStorage;
use context::CoreContext;
use futures::StreamExt;
use futures::TryStreamExt;
use futures_stats::TimedFutureExt;
use memcache::MemcacheClient;
use metaconfig_types::RepoConfigRef;
use mononoke_app::MononokeApp;
use mononoke_types::ChangesetId;
use phases::ArcPhases;
use phases::Phases;
use rendezvous::RendezVousOptions;
use repo_identity::RepoIdentityRef;
use sql_commit_graph_storage::SqlCommitGraphStorageBuilder;

use super::checkpoints::CommitGraphBackfillerCheckpoints;
use super::Repo;

#[derive(Args)]
pub struct BackfillArgs {
    /// Which id to start backfilling from. Use 0 if nothing is backfilled.
    /// If not provided loads it from commit_graph_backfiller_checkpoints
    /// (0 if not found).
    #[clap(long)]
    start_id: Option<u64>,

    /// The maximum number of changeset edges allowed to be stored in
    /// memory in the commit graph.
    #[clap(long)]
    max_in_memory_size: usize,

    /// The maximum number of changesets in a chunk fetched from
    /// the changesets table.
    #[clap(long)]
    chunk_size: usize,
}

// We need to pretend all commits are public because we want to backfill all
struct FakeAllCommitsPublic;

#[async_trait]
impl Phases for FakeAllCommitsPublic {
    async fn add_reachable_as_public(
        &self,
        _ctx: &CoreContext,
        _heads: Vec<ChangesetId>,
    ) -> Result<Vec<ChangesetId>> {
        unimplemented!()
    }

    async fn add_public_with_known_public_ancestors(
        &self,
        _ctx: &CoreContext,
        _csids: Vec<ChangesetId>,
    ) -> Result<()> {
        unimplemented!()
    }

    async fn get_public(
        &self,
        _ctx: &CoreContext,
        csids: Vec<ChangesetId>,
        _ephemeral_derive: bool,
    ) -> Result<HashSet<ChangesetId>> {
        Ok(csids.into_iter().collect())
    }

    async fn get_cached_public(
        &self,
        _ctx: &CoreContext,
        csids: Vec<ChangesetId>,
    ) -> Result<HashSet<ChangesetId>> {
        Ok(csids.into_iter().collect())
    }

    async fn list_all_public(&self, _ctx: &CoreContext) -> Result<Vec<ChangesetId>> {
        unimplemented!()
    }

    fn with_frozen_public_heads(&self, _heads: Vec<ChangesetId>) -> ArcPhases {
        unimplemented!()
    }
}

async fn backfill_impl(
    ctx: &CoreContext,
    commit_graph: &CommitGraph,
    buffered_sql_storage: &BufferedCommitGraphStorage,
    checkpoints: &CommitGraphBackfillerCheckpoints,
    repo: &Repo,
    args: BackfillArgs,
) -> Result<()> {
    let fetcher =
        PublicChangesetBulkFetch::new(repo.changesets.clone(), Arc::new(FakeAllCommitsPublic))
            .with_read_from_master(true);

    let start_id = match args.start_id {
        Some(start_id) => start_id,
        None => checkpoints
            .load_checkpoint(repo.repo_identity().id())
            .await?
            .map_or(0, |id| id.saturating_add(1)),
    };

    fetcher
        .fetch_bounded_with_id(ctx, Direction::OldestFirst, Some((start_id, u64::MAX)))
        .try_chunks(args.chunk_size)
        .map(|r| r.context("Error chunking"))
        .try_for_each(|entries| async move {
            println!("Doing chunk with {} entries", entries.len());

            let last_finished_id = match (entries.first(), entries.last()) {
                (Some(first_entry), Some(last_entry)) => {
                    println!(
                        "Chunk starts at id {} and ends at id {}",
                        first_entry.1, last_entry.1
                    );
                    Some(last_entry.1)
                }
                _ => None,
            };

            let (stats, result) = async move {
                for (entry, _) in &entries {
                    commit_graph
                        .add(ctx, entry.cs_id, entry.parents.clone().into())
                        .await?;
                }
                Ok::<(), anyhow::Error>(())
            }
            .timed()
            .await;

            println!("Done in memory in {:?}", stats);
            result?;

            let (stats, result) = buffered_sql_storage.flush(ctx).timed().await;
            println!("Written to db in {:?}", stats);
            result?;

            if let Some(last_finished_id) = last_finished_id {
                checkpoints
                    .update_checkpoint(repo.repo_identity().id(), last_finished_id)
                    .await?;
            }

            Ok(())
        })
        .await?;

    println!("Backfilled everything starting at id {}", start_id);
    Ok(())
}

pub(super) async fn backfill(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo: &Repo,
    args: BackfillArgs,
) -> Result<()> {
    let sql_storage = app
        .repo_factory()
        .sql_factory(&repo.repo_config().storage_config.metadata)
        .await?
        .open::<SqlCommitGraphStorageBuilder>()
        .await?
        .build(
            RendezVousOptions {
                free_connections: 5,
            },
            repo.repo_identity().id(),
        );
    let maybe_cached_sql_storage: Arc<dyn CommitGraphStorage> = match app.environment().caching {
        environment::Caching::Enabled(_) => {
            if let Some(cachelib_pool) = cachelib::get_volatile_pool("commit_graph")
                .with_context(|| "Missing commit_graph cache pool")?
            {
                let memcache_client = MemcacheClient::new(ctx.fb)?;
                Arc::new(CachingCommitGraphStorage::new(
                    Arc::new(sql_storage),
                    CacheHandlerFactory::Shared {
                        cachelib_pool,
                        memcache_client,
                    },
                ))
            } else {
                Arc::new(sql_storage)
            }
        }
        _ => Arc::new(sql_storage),
    };
    let buffered_sql_storage = Arc::new(BufferedCommitGraphStorage::new(
        maybe_cached_sql_storage,
        args.max_in_memory_size,
    ));

    let commit_graph = CommitGraph::new(buffered_sql_storage.clone());

    let checkpoints: CommitGraphBackfillerCheckpoints = app
        .repo_factory()
        .sql_factory(&repo.repo_config().storage_config.metadata)
        .await?
        .open()
        .await?;

    backfill_impl(
        ctx,
        &commit_graph,
        &buffered_sql_storage,
        &checkpoints,
        repo,
        args,
    )
    .await
}


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

use anyhow::Result;
use changeset_fetcher::ChangesetFetcherArc;
use clap::Args;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use futures_stats::TimedFutureExt;
use smallvec::ToSmallVec;
use vec1::vec1;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct BackfillOneArgs {
    /// Commit ID to backfill
    #[clap(long, short = 'i')]
    commit_id: String,
}

pub(super) async fn backfill_one(
    ctx: &CoreContext,
    repo: &Repo,
    args: BackfillOneArgs,
) -> Result<()> {
    let cs_id = parse_commit_id(ctx, repo, &args.commit_id).await?;

    let changeset_fetcher = repo.changeset_fetcher_arc();
    let parents = changeset_fetcher
        .get_parents(ctx, cs_id)
        .await?
        .to_smallvec();

    let (stats, result) = repo
        .commit_graph()
        .add_recursive(ctx, changeset_fetcher, vec1![(cs_id, parents)])
        .timed()
        .await;

    println!("Finished backfilling in {:?}", stats);
    println!("{} changesets added to commit graph", result?);

    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 anyhow::Result;
use mononoke_types::RepositoryId;
use sql_construct::SqlConstruct;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::mononoke_queries;
use sql_ext::SqlConnections;

pub struct CommitGraphBackfillerCheckpoints {
    connections: SqlConnections,
}

impl SqlConstruct for CommitGraphBackfillerCheckpoints {
    const LABEL: &'static str = "commit_graph_backfiller_checkpoints";

    const CREATION_QUERY: &'static str =
        include_str!("../../../schemas/sqlite-commit-graph-backfiller-checkpoints.sql");

    fn from_sql_connections(connections: SqlConnections) -> Self {
        Self { connections }
    }
}

mononoke_queries! {
    read SelectCheckpoint(
        repo_id: RepositoryId,
    ) -> (Option<u64>) {
        "SELECT last_finished_id FROM commit_graph_backfiller_checkpoints WHERE repo_id={repo_id}"
    }

    write UpdateCheckpoints(
        values: (
            repo_id: RepositoryId,
            last_finished_id: u64,
        ),
    ) {
        none,
        "REPLACE INTO commit_graph_backfiller_checkpoints (repo_id, last_finished_id) VALUES {values}"
    }
}

impl CommitGraphBackfillerCheckpoints {
    pub async fn load_checkpoint(&self, repo_id: RepositoryId) -> Result<Option<u64>> {
        let rows =
            SelectCheckpoint::query(&self.connections.read_master_connection, &repo_id).await?;
        Ok(rows
            .first()
            .and_then(|(last_finished_id,)| *last_finished_id))
    }

    pub async fn update_checkpoint(
        &self,
        repo_id: RepositoryId,
        last_finished_id: u64,
    ) -> Result<()> {
        UpdateCheckpoints::query(
            &self.connections.write_connection,
            &[(&repo_id, &last_finished_id)],
        )
        .await?;
        Ok(())
    }
}

impl SqlConstructFromMetadataDatabaseConfig for CommitGraphBackfillerCheckpoints {}

#[cfg(test)]
mod tests {
    use fbinit::FacebookInit;

    use super::*;

    #[fbinit::test]
    async fn test_checkpoints(_fb: FacebookInit) -> Result<()> {
        let checkpoints = CommitGraphBackfillerCheckpoints::with_sqlite_in_memory()?;

        let first_repo_id = RepositoryId::new(111);
        let second_repo_id = RepositoryId::new(222);

        assert_eq!(checkpoints.load_checkpoint(first_repo_id).await?, None);
        assert_eq!(checkpoints.load_checkpoint(second_repo_id).await?, None);

        checkpoints.update_checkpoint(first_repo_id, 100).await?;

        assert_eq!(checkpoints.load_checkpoint(first_repo_id).await?, Some(100));
        assert_eq!(checkpoints.load_checkpoint(second_repo_id).await?, None);

        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 anyhow::Result;
use clap::Args;
use commit_graph::CommitGraphRef;
use context::CoreContext;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct ChildrenArgs {
    /// Commit ID to display children of.
    #[clap(long, short = 'i')]
    cs_id: String,
}

pub async fn children(ctx: &CoreContext, repo: &Repo, args: ChildrenArgs) -> Result<()> {
    let cs_id = parse_commit_id(ctx, repo, &args.cs_id).await?;

    let children = repo.commit_graph().changeset_children(ctx, cs_id).await?;

    for child in children {
        println!("{}", child);
    }

    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 anyhow::bail;
use anyhow::Result;
use clap::Args;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use futures::future::try_join_all;

use super::Repo;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct CommonBaseArgs {
    /// The ids of the two commits to calculate the common base for.
    #[clap(long, short = 'i')]
    cs_ids: Vec<String>,
}

pub async fn common_base(ctx: &CoreContext, repo: &Repo, args: CommonBaseArgs) -> Result<()> {
    let cs_ids: Vec<_> = try_join_all(
        args.cs_ids
            .iter()
            .map(|id| parse_commit_id(ctx, repo, id))
            .collect::<Vec<_>>(),
    )
    .await?;

    if cs_ids.len() != 2 {
        bail!("Must provide exactly two commit IDs.");
    }

    let common_base_cs_ids = repo
        .commit_graph()
        .common_base(ctx, cs_ids[0], cs_ids[1])
        .await?;

    for cs_id in common_base_cs_ids {
        println!("{}", cs_id);
    }

    Ok(())
}