/*
 * 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 ancestors_difference;
mod backfill;
mod backfill_one;
mod checkpoints;
mod children;
mod common_base;
mod range_stream;
mod segments;
mod slice_ancestors;
mod update_preloaded;

use ancestors_difference::AncestorsDifferenceArgs;
use anyhow::Result;
use backfill::BackfillArgs;
use backfill_one::BackfillOneArgs;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use changeset_fetcher::ChangesetFetcher;
use changesets::Changesets;
use children::ChildrenArgs;
use clap::Parser;
use clap::Subcommand;
use commit_graph::CommitGraph;
use common_base::CommonBaseArgs;
use metaconfig_types::RepoConfig;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use range_stream::RangeStreamArgs;
use repo_blobstore::RepoBlobstore;
use repo_identity::RepoIdentity;
use segments::SegmentsArgs;
use slice_ancestors::SliceAncestorsArgs;
use update_preloaded::UpdatePreloadedArgs;

#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

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

#[derive(Subcommand)]
pub enum CommitGraphSubcommand {
    /// Backfill commit graph entries
    Backfill(BackfillArgs),
    /// Backfill a commit and all of its missing ancestors.
    BackfillOne(BackfillOneArgs),
    /// Display ids of all commits that are ancestors of one set of commits (heads),
    /// excluding ancestors of another set of commits (common) in reverse topological order.
    AncestorsDifference(AncestorsDifferenceArgs),
    /// Display ids of all commits that are simultaneously a descendant of one commit (start)
    /// and an ancestor of another (end) in topological order.
    RangeStream(RangeStreamArgs),
    /// Update preloaded commit graph and upload it to blobstore.
    UpdatePreloaded(UpdatePreloadedArgs),
    /// Display ids of all the highest generation commits among the common ancestors of two commits.
    CommonBase(CommonBaseArgs),
    /// Slices ancestors of given commits and displays commits IDs of frontiers for each slice.
    SliceAncestors(SliceAncestorsArgs),
    /// Display ids of all children commits of a given commit.
    Children(ChildrenArgs),
    /// Display segments representing ancestors of one set of commits (heads), excluding
    /// ancestors of another set of commits (common) in reverse topological order.
    Segments(SegmentsArgs),
}

#[facet::container]
pub struct Repo {
    #[facet]
    changesets: dyn Changesets,

    #[facet]
    changeset_fetcher: dyn ChangesetFetcher,

    #[facet]
    commit_graph: CommitGraph,

    #[facet]
    config: RepoConfig,

    #[facet]
    id: 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,
}

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

    match args.subcommand {
        CommitGraphSubcommand::Backfill(args) => backfill::backfill(&ctx, &app, &repo, args).await,
        CommitGraphSubcommand::BackfillOne(args) => {
            backfill_one::backfill_one(&ctx, &repo, args).await
        }
        CommitGraphSubcommand::AncestorsDifference(args) => {
            ancestors_difference::ancestors_difference(&ctx, &repo, args).await
        }
        CommitGraphSubcommand::RangeStream(args) => {
            range_stream::range_stream(&ctx, &repo, args).await
        }
        CommitGraphSubcommand::UpdatePreloaded(args) => {
            update_preloaded::update_preloaded(&ctx, &app, &repo, args).await
        }
        CommitGraphSubcommand::CommonBase(args) => {
            common_base::common_base(&ctx, &repo, args).await
        }
        CommitGraphSubcommand::SliceAncestors(args) => {
            slice_ancestors::slice_ancestors(&ctx, &repo, args).await
        }
        CommitGraphSubcommand::Children(args) => children::children(&ctx, &repo, args).await,
        CommitGraphSubcommand::Segments(args) => segments::segments(&ctx, &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 clap::Args;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use futures::try_join;
use futures::StreamExt;

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

#[derive(Args)]
pub struct RangeStreamArgs {
    /// Commit ID of the start of the range.
    #[clap(long)]
    start: String,

    /// Commit ID of the end of the range
    #[clap(long)]
    end: String,
}

pub async fn range_stream(ctx: &CoreContext, repo: &Repo, args: RangeStreamArgs) -> Result<()> {
    let (start, end) = try_join!(
        parse_commit_id(ctx, repo, &args.start),
        parse_commit_id(ctx, repo, &args.end),
    )?;

    let mut range_stream = repo.commit_graph().range_stream(ctx, start, end).await?;

    while let Some(cs_id) = range_stream.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 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 SegmentsArgs {
    /// 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>,

    /// Verify the correctness of the returned segments using ancestors_difference.
    #[clap(long)]
    verify: bool,
}

pub async fn segments(ctx: &CoreContext, repo: &Repo, args: SegmentsArgs) -> 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 segments = match args.verify {
        true => {
            repo.commit_graph()
                .verified_ancestors_difference_segments(ctx, heads, common)
                .await?
        }
        false => {
            repo.commit_graph()
                .ancestors_difference_segments(ctx, heads, common)
                .await?
        }
    };

    for segment in segments {
        println!("{}", segment);
    }

    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::CommitGraphArc;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use futures::future::try_join_all;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;

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

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

    /// The size (in generation numbers) of each slice.
    #[clap(long)]
    slice_size: u64,

    /// Minimum generation to stop slicing at.
    #[clap(long)]
    min_generation: u64,
}

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

    let slices = repo
        .commit_graph()
        .slice_ancestors(
            ctx,
            heads,
            |cs_ids| async {
                stream::iter(cs_ids)
                    .map(|cs_id| async move {
                        anyhow::Ok((
                            cs_id,
                            repo.commit_graph_arc()
                                .changeset_generation_required(ctx, cs_id)
                                .await?,
                        ))
                    })
                    .buffered(5)
                    .try_filter_map(|(cs_id, gen)| async move {
                        if gen.value() >= args.min_generation {
                            Ok(Some(cs_id))
                        } else {
                            Ok(None)
                        }
                    })
                    .try_collect()
                    .await
            },
            args.slice_size,
        )
        .await?;

    for (gen_group, cs_ids) in slices {
        print!("slice [{}, {}):", gen_group, gen_group + args.slice_size);
        for cs_id in cs_ids {
            print!(" {}", cs_id);
        }
        println!();
    }

    Ok(())
}


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

use std::collections::HashMap;

use anyhow::anyhow;
use anyhow::Result;
use blobstore::Blobstore;
use clap::Args;
use commit_graph_types::edges::ChangesetEdges;
use context::CoreContext;
use fbthrift::compact_protocol;
use metaconfig_types::RepoConfigRef;
use mononoke_app::MononokeApp;
use mononoke_types::BlobstoreBytes;
use mononoke_types::ChangesetId;
use preloaded_commit_graph_storage::ExtendablePreloadedEdges;
use rendezvous::RendezVousOptions;
use repo_blobstore::RepoBlobstoreRef;
use repo_identity::RepoIdentityRef;
use sql_commit_graph_storage::SqlCommitGraphStorage;
use sql_commit_graph_storage::SqlCommitGraphStorageBuilder;
use tokio::time::sleep;
use tokio::time::Duration;

use super::Repo;

#[derive(Args)]
pub struct UpdatePreloadedArgs {
    /// Blobstore key for the preloaded commit graph.
    #[clap(long)]
    blobstore_key: String,

    /// Whether to rebuild the preloaded commit graph or start
    /// from the previous blob.
    #[clap(long)]
    rebuild: bool,

    /// Number of times to retry fetching changeset edges
    /// from the database.
    #[clap(long, default_value_t = 0)]
    sql_retries: u64,

    /// Maximum number of changeset edges in a chunk
    /// fetched from the database.
    #[clap(long)]
    chunk_size: u64,

    /// Sleep time between fetching changeset edges in milliseconds.
    #[clap(long)]
    sleep_ms: u64,

    /// Sleep time before exiting the program in seconds.
    #[clap(long, default_value_t = 60)]
    sleep_before_exit_secs: u64,
}

async fn try_fetch_chunk(
    ctx: &CoreContext,
    sql_storage: &SqlCommitGraphStorage,
    start_id: u64,
    end_id: u64,
    chunk_size: u64,
    mut sql_retries: u64,
    sleep_ms: u64,
) -> Result<HashMap<ChangesetId, ChangesetEdges>> {
    loop {
        match sql_storage
            .fetch_many_edges_in_id_range(ctx, start_id, end_id, chunk_size, false)
            .await
        {
            Ok(edges) => return Ok(edges),
            Err(err) => match sql_retries {
                0 => return Err(err),
                _ => {
                    println!("{:?}", err);
                    println!("Retrying fetching changeset edges");

                    sql_retries -= 1;
                    sleep(Duration::from_millis(sleep_ms)).await;
                }
            },
        }
    }
}

pub(super) async fn update_preloaded(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo: &Repo,
    args: UpdatePreloadedArgs,
) -> 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 preloaded_edges = match args.rebuild {
        false => match repo.repo_blobstore().get(ctx, &args.blobstore_key).await? {
            Some(bytes) => {
                preloaded_commit_graph_storage::deserialize_preloaded_edges(bytes.into_raw_bytes())?
            }
            None => Default::default(),
        },
        true => Default::default(),
    };

    // The newly added changesets all have higher sql ids than the maximum
    // id from the previously preloaded changesets.
    let mut start_id = preloaded_edges
        .max_sql_id
        .map_or(1, |id| id.saturating_add(1));
    // Query the maximum sql id for this repo only once to avoid tailing
    // new changesets.
    let end_id = sql_storage.max_id(ctx, false).await?.unwrap_or(0);

    println!(
        "Updating with changesets having sql ids between {} and {} inclusive",
        start_id, end_id
    );

    let mut extendable_preloaded_edges =
        ExtendablePreloadedEdges::from_preloaded_edges(preloaded_edges);

    while start_id <= end_id {
        // Tries to fetch the first chunk_size changeset edges between
        // start_id and end_id.
        let edges_chunk = try_fetch_chunk(
            ctx,
            &sql_storage,
            start_id,
            end_id,
            args.chunk_size,
            args.sql_retries,
            args.sleep_ms,
        )
        .await?;

        if edges_chunk.is_empty() {
            break;
        }

        // Query the maximum sql id from the fetched chunk to fetch the next
        // chunks from after it.
        let max_id_in_chunk = sql_storage
            .max_id_in_range(ctx, start_id, end_id, edges_chunk.len() as u64, false)
            .await?
            .ok_or_else(|| anyhow!("Chunk is not empty but couldn't find max id"))?;

        println!(
            "Fetched chunk containing {} edges. Maximum sql id in chunk is {}",
            edges_chunk.len(),
            max_id_in_chunk
        );

        for (_cs_id, edges) in edges_chunk {
            extendable_preloaded_edges.add(edges)?;
        }
        extendable_preloaded_edges.update_max_sql_id(max_id_in_chunk);
        start_id = max_id_in_chunk + 1;

        println!("Extended preloaded edges with chunk");

        sleep(Duration::from_millis(args.sleep_ms)).await;
    }
    println!("Deserializing preloaded edges");

    let bytes = compact_protocol::serialize(
        &extendable_preloaded_edges
            .into_preloaded_edges()
            .to_thrift()?,
    );

    println!("Deserialized preloaded edges into {} bytes", bytes.len());

    repo.repo_blobstore()
        .put(ctx, args.blobstore_key, BlobstoreBytes::from_bytes(bytes))
        .await?;

    // In the case of a multiplexed blobstore, the put operation can exit after it succeeds
    // in one inner blobstore before finishing in all, and leave the rest running in the
    // background. This sleep tries to prevent exiting early before they all finish.
    tokio::time::sleep(Duration::from_secs(args.sleep_before_exit_secs)).await;

    println!("Uploaded updated preloaded edges to blobstore");

    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::Result;
use clap::Args;
use context::CoreContext;
use futures::future::try_join_all;
use futures::TryFutureExt;

use super::Repo;

// By default, at most 50 expired bubbles will be cleaned up in one go.
const DEFAULT_MAX_BUBBLES_FOR_CLEANUP: u32 = 50;
const DEFAULT_CUTOFF_FOR_CLEANUP: u32 = 24 * 60 * 60;

#[derive(Args)]
/// Subcommand to cleanup expired bubbles within the ephemeral store.
pub struct EphemeralStoreCleanUpArgs {
    /// Duration in seconds for which the bubbles should already
    /// be expired. Defaults to the number of seconds in a day.
    #[clap(long, short = 'c', default_value_t = DEFAULT_CUTOFF_FOR_CLEANUP)]
    cutoff: u32,

    /// The maximum number of bubbles that can be cleaned up in
    /// one run of the command.
    #[clap(long, short = 'l', default_value_t = DEFAULT_MAX_BUBBLES_FOR_CLEANUP)]
    limit: u32,

    /// When set, the command won't actually cleanup the bubbles but
    /// instead just lists the bubble IDs that will be cleaned-up on
    /// a non-dryrun of this command.
    #[clap(long, short = 'n')]
    dryrun: bool,
}

pub async fn clean_bubbles(
    ctx: &CoreContext,
    repo: &Repo,
    args: EphemeralStoreCleanUpArgs,
) -> Result<()> {
    let cutoff_duration = Duration::from_secs(args.cutoff.into());
    let expired_bubbles = repo
        .repo_ephemeral_store
        .get_expired_bubbles(cutoff_duration, args.limit)
        .await?;
    if expired_bubbles.is_empty() {
        println!("No expired bubbles found for deletion based on input provided");
        return Ok(());
    } else {
        println!(
            "Fetched {} expired bubbles for deletion",
            expired_bubbles.len()
        );
    }
    if !args.dryrun {
        let delete_futures = expired_bubbles.iter().map(|id| {
            repo.repo_ephemeral_store
                .delete_bubble(*id, ctx)
                .map_ok(|count| (id.clone(), count))
        });
        let bubbleid_and_count = try_join_all(delete_futures).await?;
        for (id, count) in bubbleid_and_count.iter() {
            println!(
                "Cleaned up bubble {} and deleted {} blob keys contained in it",
                id, count
            );
        }
    } else {
        println!(
            "Executing cleanup in dry-run mode. The following bubbles were fetched for deletion:"
        );
        println!("{:?}", expired_bubbles);
    }
    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::str::FromStr;

use anyhow::anyhow;
use anyhow::Result;
use clap::Args;
use ephemeral_blobstore::BubbleId;
use mononoke_types::ChangesetId;

use super::Repo;

#[derive(Args)]
/// Subcommand to describe metadata associated with a bubble within the ephemeral store.
pub struct EphemeralStoreInfoArgs {
    /// The ID of any one of the changesets for which the bubble metadata is
    /// requested.
    #[clap(long, short = 'i', conflicts_with = "bubble_id")]
    changeset_id: Option<String>,

    /// The ID of the bubble for which the metadata is requested.
    #[clap(long, short = 'b', conflicts_with = "changeset_id")]
    bubble_id: Option<BubbleId>,
}

pub async fn bubble_info(repo: &Repo, args: EphemeralStoreInfoArgs) -> Result<()> {
    let bubble_id = match (&args.bubble_id, &args.changeset_id) {
        (None, Some(id)) => repo
            .repo_ephemeral_store
            .bubble_from_changeset(&ChangesetId::from_str(id)?)
            .await?
            .ok_or_else(|| anyhow!("No bubble exists for changeset ID {}", id)),
        (Some(id), _) => Ok(*id),
        (_, _) => Err(anyhow!(
            "Need to provide either changeset ID or bubble ID as input"
        )),
    }?;
    let changeset_ids = match &args.changeset_id {
        None => {
            repo.repo_ephemeral_store
                .changesets_from_bubble(&bubble_id)
                .await?
        }
        Some(id) => vec![ChangesetId::from_str(id)?],
    };
    let bubble = repo.repo_ephemeral_store.open_bubble_raw(bubble_id).await?;
    println!(
        "BubbleID: {}\nChangesetIDs: {:?}\nRepoID: {}\nExpiryDate: {}\nStatus: {}\nBlobstorePrefix: {}",
        bubble_id,
        &changeset_ids,
        repo.repo_identity.id(),
        bubble.expires_at(),
        bubble.expired(),
        bubble_id.prefix(),
    );
    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::str::FromStr;

use anyhow::anyhow;
use anyhow::Result;
use clap::Args;
use context::CoreContext;
use ephemeral_blobstore::BubbleId;
use mononoke_types::ChangesetId;

use super::Repo;

// By default, at most 100 blob keys will be listed for a bubble.
const DEFAULT_MAX_KEYS_FOR_LIST: u32 = 100;

#[derive(Args)]
/// Subcommand to list out the keys of the blobs in a bubble within the ephemeral store.
pub struct EphemeralStoreListArgs {
    /// The ID of any one of the changesets for which the bubble blob data needs to
    /// be listed.
    #[clap(long, short = 'i', conflicts_with = "bubble_id")]
    changeset_id: Option<String>,

    /// The ID of the bubble for which the blob data needs to be listed.
    #[clap(long, short = 'b', conflicts_with = "changeset_id")]
    bubble_id: Option<BubbleId>,

    /// The maximum number of blob keys listed in the output. Defaults to 100.
    #[clap(long, short = 'l', default_value_t = DEFAULT_MAX_KEYS_FOR_LIST)]
    limit: u32,

    /// If specified, the search range starts from this key.
    #[clap(long)]
    start_from: Option<String>,

    /// If specified, the blob keys are returned in sorted order.
    #[clap(long)]
    ordered: bool,
}

pub async fn list_keys(ctx: &CoreContext, repo: &Repo, args: EphemeralStoreListArgs) -> Result<()> {
    let bubble_id = match (&args.bubble_id, &args.changeset_id) {
        (None, Some(id)) => repo
            .repo_ephemeral_store
            .bubble_from_changeset(&ChangesetId::from_str(id)?)
            .await?
            .ok_or_else(|| anyhow!("No bubble exists for changeset ID {}", id)),
        (Some(id), _) => Ok(*id),
        (_, _) => Err(anyhow!(
            "Need to provide either changeset ID or bubble ID as input"
        )),
    }?;
    let mut keys = repo
        .repo_ephemeral_store
        .keys_in_bubble(bubble_id, ctx, args.start_from, args.limit)
        .await?;
    if args.ordered {
        keys.sort();
    }
    for key in keys.iter() {
        println!("{}{}", bubble_id.prefix(), key);
    }
    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::path::PathBuf;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use clap::Args;
use context::CoreContext;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStoreRef;
use futures::TryStreamExt;
use repo_blobstore::RepoBlobstoreRef;
use tokio::io::AsyncWriteExt;
use tokio::io::BufWriter;

use super::FilestoreItemIdArgs;
use super::Repo;

#[derive(Args)]
pub struct FilestoreFetchArgs {
    #[clap(flatten)]
    item_id: FilestoreItemIdArgs,

    #[clap(long)]
    bubble_id: Option<BubbleId>,

    #[clap(long, short = 'o', value_name = "FILE")]
    output: Option<PathBuf>,
}

pub async fn fetch(ctx: &CoreContext, repo: &Repo, fetch_args: FilestoreFetchArgs) -> Result<()> {
    let fetch_key = fetch_args.item_id.fetch_key()?;
    let blobstore = match fetch_args.bubble_id {
        Some(bubble_id) => {
            let bubble = repo.repo_ephemeral_store().open_bubble(bubble_id).await?;
            bubble.wrap_repo_blobstore(repo.repo_blobstore().clone())
        }
        None => repo.repo_blobstore().clone(),
    };
    let mut stream = filestore::fetch(blobstore, ctx.clone(), &fetch_key)
        .await
        .context("Failed to fetch from filestore")?
        .ok_or_else(|| anyhow!("Content not found"))?;

    match fetch_args.output {
        Some(path) => {
            let file = tokio::fs::File::create(path).await?;
            let mut out = BufWriter::new(file);
            while let Some(b) = stream
                .try_next()
                .await
                .context("Failed to read from filestore")?
            {
                out.write_all(b.as_ref())
                    .await
                    .context("Failed to write to file")?;
            }
            out.shutdown()
                .await
                .context("Failed to finish writing to file")?;
        }
        None => {
            let mut out = tokio::io::stdout();
            while let Some(b) = stream
                .try_next()
                .await
                .context("Failed to read from filestore")?
            {
                out.write_all(b.as_ref())
                    .await
                    .context("Failed to write to output")?;
            }
        }
    }

    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::Loadable;
use clap::Args;
use context::CoreContext;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStoreRef;
use mononoke_types::FileContents;
use repo_blobstore::RepoBlobstoreRef;

use super::FilestoreItemIdArgs;
use super::Repo;

#[derive(Args)]
pub struct FilestoreIsChunkedArgs {
    #[clap(flatten)]
    item_id: FilestoreItemIdArgs,

    #[clap(long)]
    bubble_id: Option<BubbleId>,
}

pub async fn is_chunked(
    ctx: &CoreContext,
    repo: &Repo,
    metadata_args: FilestoreIsChunkedArgs,
) -> Result<()> {
    let fetch_key = metadata_args.item_id.fetch_key()?;
    let blobstore = match metadata_args.bubble_id {
        Some(bubble_id) => {
            let bubble = repo.repo_ephemeral_store().open_bubble(bubble_id).await?;
            bubble.wrap_repo_blobstore(repo.repo_blobstore().clone())
        }
        None => repo.repo_blobstore().clone(),
    };
    let metadata = filestore::get_metadata(&blobstore, ctx, &fetch_key)
        .await
        .context("Failed to get metadata from filestore")?;

    match metadata {
        Some(metadata) => {
            let file_contents = metadata
                .content_id
                .load(ctx, &blobstore)
                .await
                .context("Failed to load content blob")?;
            match file_contents {
                FileContents::Bytes(_) => {
                    println!("not chunked");
                }
                FileContents::Chunked(_) => {
                    println!("chunked");
                }
            }
        }
        None => {
            println!("content id not found");
        }
    }

    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 clap::Args;
use context::CoreContext;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStoreRef;
use repo_blobstore::RepoBlobstoreRef;

use super::FilestoreItemIdArgs;
use super::Repo;

#[derive(Args)]
pub struct FilestoreMetadataArgs {
    #[clap(flatten)]
    item_id: FilestoreItemIdArgs,

    #[clap(long)]
    bubble_id: Option<BubbleId>,
}

pub async fn metadata(
    ctx: &CoreContext,
    repo: &Repo,
    metadata_args: FilestoreMetadataArgs,
) -> Result<()> {
    let fetch_key = metadata_args.item_id.fetch_key()?;
    let blobstore = match metadata_args.bubble_id {
        Some(bubble_id) => {
            let bubble = repo.repo_ephemeral_store().open_bubble(bubble_id).await?;
            bubble.wrap_repo_blobstore(repo.repo_blobstore().clone())
        }
        None => repo.repo_blobstore().clone(),
    };
    let metadata = filestore::get_metadata(&blobstore, ctx, &fetch_key)
        .await
        .context("Failed to get metadata from filestore")?;

    println!("{:#?}", metadata);

    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::path::PathBuf;

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use bytes::BytesMut;
use clap::Args;
use context::CoreContext;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStoreRef;
use filestore::FilestoreConfigRef;
use filestore::StoreRequest;
use futures::stream::TryStreamExt;
use repo_blobstore::RepoBlobstoreRef;
use tokio::io::BufReader;
use tokio_util::codec::BytesCodec;
use tokio_util::codec::FramedRead;

use super::Repo;

#[derive(Args)]
pub struct FilestoreStoreArgs {
    file: PathBuf,

    #[clap(long)]
    bubble_id: Option<BubbleId>,
}

pub async fn store(ctx: &CoreContext, repo: &Repo, store_args: FilestoreStoreArgs) -> Result<()> {
    let file = tokio::fs::File::open(&store_args.file)
        .await
        .context("Failed to open file")?;
    let len = file
        .metadata()
        .await
        .context("Failed to get file metadata")?
        .len();
    let blobstore = match store_args.bubble_id {
        Some(bubble_id) => {
            let bubble = repo.repo_ephemeral_store().open_bubble(bubble_id).await?;
            bubble.wrap_repo_blobstore(repo.repo_blobstore().clone())
        }
        None => repo.repo_blobstore().clone(),
    };
    let data = FramedRead::new(BufReader::new(file), BytesCodec::new())
        .map_ok(BytesMut::freeze)
        .map_err(Error::from);
    let metadata = filestore::store(
        &blobstore,
        repo.filestore_config().clone(),
        ctx,
        &StoreRequest::new(len),
        data,
    )
    .await
    .context("Failed to write to filestore")?;

    println!(
        "Wrote {} ({} bytes)",
        metadata.content_id, metadata.total_size
    );

    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::path::PathBuf;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use clap::Args;
use context::CoreContext;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStoreRef;
use filestore::Alias;
use filestore::FetchKey;
use repo_blobstore::RepoBlobstoreRef;

use super::FilestoreItemIdArgs;
use super::Repo;

#[derive(Args)]
pub struct FilestoreVerifyArgs {
    #[clap(flatten)]
    item_id: FilestoreItemIdArgs,

    #[clap(long)]
    bubble_id: Option<BubbleId>,

    #[clap(long, short = 'o', value_name = "FILE")]
    output: Option<PathBuf>,
}

pub async fn verify(
    ctx: &CoreContext,
    repo: &Repo,
    verify_args: FilestoreVerifyArgs,
) -> Result<()> {
    let fetch_key = verify_args.item_id.fetch_key()?;
    let blobstore = match verify_args.bubble_id {
        Some(bubble_id) => {
            let bubble = repo.repo_ephemeral_store().open_bubble(bubble_id).await?;
            bubble.wrap_repo_blobstore(repo.repo_blobstore().clone())
        }
        None => repo.repo_blobstore().clone(),
    };
    let metadata = filestore::get_metadata(&blobstore, ctx, &fetch_key)
        .await
        .context("Failed to get metadata from filestore")?
        .ok_or_else(|| anyhow!("Content not found"))?;

    let (content_id, sha1, sha256, git_sha1, seeded_blake3) = futures::future::join5(
        filestore::fetch(
            &blobstore,
            ctx.clone(),
            &FetchKey::Canonical(metadata.content_id),
        ),
        filestore::fetch(
            &blobstore,
            ctx.clone(),
            &FetchKey::Aliased(Alias::Sha1(metadata.sha1)),
        ),
        filestore::fetch(
            &blobstore,
            ctx.clone(),
            &FetchKey::Aliased(Alias::Sha256(metadata.sha256)),
        ),
        filestore::fetch(
            &blobstore,
            ctx.clone(),
            &FetchKey::Aliased(Alias::GitSha1(metadata.git_sha1.sha1())),
        ),
        filestore::fetch(
            &blobstore,
            ctx.clone(),
            &FetchKey::Aliased(Alias::SeededBlake3(metadata.seeded_blake3)),
        ),
    )
    .await;

    println!("content_id: {}", content_id.is_ok());
    println!("sha1: {}", sha1.is_ok());
    println!("sha256: {}", sha256.is_ok());
    println!("git_sha1: {}", git_sha1.is_ok());
    println!("seeded_blake3: {}", seeded_blake3.is_ok());

    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::bail;
use anyhow::Context;
use anyhow::Result;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::Freshness;
use clap::Args;
use context::CoreContext;
use futures::stream::StreamExt;

use super::Repo;

#[derive(Args)]
pub struct HgSyncInspectArgs {
    /// Sync log entry to inspect
    id: i64,
}

pub async fn inspect(
    ctx: &CoreContext,
    repo: &Repo,
    inspect_args: HgSyncInspectArgs,
) -> Result<()> {
    let log_entry = repo
        .bookmark_update_log()
        .read_next_bookmark_log_entries(
            ctx.clone(),
            (inspect_args.id - 1).try_into().context("Invalid log id")?,
            1,
            Freshness::MostRecent,
        )
        .next()
        .await
        .ok_or_else(|| anyhow!("No log entries found"))??;

    if log_entry.id != inspect_args.id {
        bail!("No entry with id {} found", inspect_args.id);
    }

    println!("Bookmark: {}", log_entry.bookmark_name);
    match (&log_entry.from_changeset_id, &log_entry.to_changeset_id) {
        (None, Some(csid)) => println!("Created at: {}", csid),
        (Some(old), Some(new)) => println!("Moved from: {}\nMoved to: {}", old, new),
        (Some(csid), None) => println!("Deleted from: {}", csid),
        _ => bail!("Invalid log entry"),
    }

    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::bail;
use anyhow::Context;
use anyhow::Result;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::BookmarkUpdateReason;
use clap::Args;
use context::CoreContext;
use mutable_counters::MutableCountersRef;
use repo_identity::RepoIdentityRef;

use super::Repo;
use super::LATEST_REPLAYED_REQUEST_KEY;

#[derive(Args)]
pub struct HgSyncLastProcessedArgs {
    /// Set the last processed log entry to a specific id
    #[clap(long, value_name = "ID")]
    set: Option<i64>,

    /// Set the last processed log entry such that we skip over blobimport
    /// entries
    #[clap(long, conflicts_with = "set")]
    skip_blobimport: bool,

    /// When skipping over blobimport entries, don't update the last processed
    /// entry, just print what would happen
    #[clap(
        long,
        short = 'n',
        requires = "skip_blobimport",
        conflicts_with = "set"
    )] // You would think this "conflicts_with" wouldn't be required, but it is, due
    // to a weird interaction with the `conflicts_with` on `skip_blobimport`.
    // See upstream issue: https://github.com/clap-rs/clap/issues/4520
    dry_run: bool,
}

pub async fn last_processed(
    ctx: &CoreContext,
    repo: &Repo,
    last_processed_args: HgSyncLastProcessedArgs,
) -> Result<()> {
    let repo_name = repo.repo_identity().name();
    let repo_id = repo.repo_identity().id();

    let maybe_counter = repo
        .mutable_counters()
        .get_counter(ctx, LATEST_REPLAYED_REQUEST_KEY)
        .await?;

    if let Some(counter) = maybe_counter {
        println!(
            "Counter for {} ({}) has value {}",
            repo_name, repo_id, counter
        );
    } else {
        println!("No counter found for {} ({})", repo_name, repo_id);
    }

    if let Some(new_value) = last_processed_args.set {
        repo.mutable_counters()
            .set_counter(ctx, LATEST_REPLAYED_REQUEST_KEY, new_value, None)
            .await
            .with_context(|| {
                format!(
                    "Failed to set counter for {} ({}) to {}",
                    repo_name, repo_id, new_value
                )
            })?;

        println!(
            "Counter for {} ({}) set to {}",
            repo_name, repo_id, new_value
        );
    } else if last_processed_args.skip_blobimport {
        let old_value =
            maybe_counter.ok_or_else(|| anyhow!("Cannot update counter without a value"))?;
        let new_value = repo
            .bookmark_update_log()
            .skip_over_bookmark_log_entries_with_reason(
                ctx.clone(),
                old_value.try_into()?,
                BookmarkUpdateReason::Blobimport,
            )
            .await?
            .ok_or_else(|| anyhow!("No valid counter position to skip ahead to"))?;

        if last_processed_args.dry_run {
            println!(
                "Counter for {} ({}) would be updated to {}",
                repo_name, repo_id, new_value
            );
        } else {
            let success = repo
                .mutable_counters()
                .set_counter(
                    ctx,
                    LATEST_REPLAYED_REQUEST_KEY,
                    new_value.try_into()?,
                    Some(old_value),
                )
                .await?;
            if success {
                println!(
                    "Counter for {} ({}) was updated to {}",
                    repo_name, repo_id, new_value
                );
            } else {
                bail!("Update failed due to update conflict");
            }
        }
    }

    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::BookmarkUpdateLogRef;
use bookmarks::BookmarkUpdateReason;
use clap::Args;
use context::CoreContext;
use mutable_counters::MutableCountersRef;
use repo_identity::RepoIdentityRef;

use super::Repo;
use super::LATEST_REPLAYED_REQUEST_KEY;

#[derive(Args)]
pub struct HgSyncRemainsArgs {
    /// Just print the number of entries
    #[clap(long, short = 'q')]
    quiet: bool,

    /// Exclude blobimport entries from the count
    #[clap(long)]
    without_blobimport: bool,
}

pub async fn remains(
    ctx: &CoreContext,
    repo: &Repo,
    remains_args: HgSyncRemainsArgs,
) -> Result<()> {
    let repo_name = repo.repo_identity().name();
    let repo_id = repo.repo_identity().id();

    // This is not quite correct: the counter not existing and having the
    // value 0 are actually different, but this is an edge case we will ignore.
    let counter = repo
        .mutable_counters()
        .get_counter(ctx, LATEST_REPLAYED_REQUEST_KEY)
        .await?
        .unwrap_or(0)
        .try_into()?;

    let exclude_reason = remains_args
        .without_blobimport
        .then_some(BookmarkUpdateReason::Blobimport);

    let remaining = repo
        .bookmark_update_log()
        .count_further_bookmark_log_entries(ctx.clone(), counter, exclude_reason)
        .await
        .with_context(|| {
            format!(
                "Failed to fetch remaining bundles to replay for {} ({})",
                repo_name, repo_id
            )
        })?;

    if remains_args.quiet {
        println!("{}", remaining);
    } else {
        let kind = if remains_args.without_blobimport {
            "non-blobimport bundles"
        } else {
            "bundles"
        };

        println!(
            "Remaining {} to replay in {} ({}): {}",
            kind, repo_name, repo_id, remaining
        );
    }

    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 bookmarks::BookmarkUpdateLogRef;
use bookmarks::Freshness;
use clap::Args;
use context::CoreContext;
use futures::stream::TryStreamExt;
use mutable_counters::MutableCountersRef;

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

#[derive(Args)]
pub struct HgSyncShowArgs {
    /// How many bundles to show
    #[clap(long, short = 'l', default_value_t = 10)]
    limit: u64,
}

pub async fn show(ctx: &CoreContext, repo: &Repo, show_args: HgSyncShowArgs) -> Result<()> {
    // This is not quite correct: the counter not existing and having the
    // value 0 are actually different, but this is an edge case we will ignore.
    let counter = repo
        .mutable_counters()
        .get_counter(ctx, LATEST_REPLAYED_REQUEST_KEY)
        .await?
        .unwrap_or(0)
        .try_into()?;

    repo.bookmark_update_log()
        .read_next_bookmark_log_entries(
            ctx.clone(),
            counter,
            show_args.limit,
            Freshness::MostRecent,
        )
        .map_ok(|entry| async move {
            BookmarkLogEntry::new(
                ctx,
                repo,
                entry.timestamp,
                entry.bookmark_name,
                entry.reason,
                entry.to_changeset_id,
                Some(entry.id.try_into()?),
                &[IdentityScheme::Hg],
            )
            .await
        })
        .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 anyhow::Result;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::BookmarkUpdateReason;
use clap::Args;
use context::CoreContext;
use mutable_counters::MutableCountersRef;
use repo_identity::RepoIdentityRef;

use super::Repo;
use super::LATEST_REPLAYED_REQUEST_KEY;

#[derive(Args)]
pub struct HgSyncVerifyArgs {}

pub async fn verify(ctx: &CoreContext, repo: &Repo, _verify_args: HgSyncVerifyArgs) -> Result<()> {
    let repo_name = repo.repo_identity().name();
    let repo_id = repo.repo_identity().id();

    // This is not quite correct: the counter not existing and having the
    // value 0 are actually different, but this is an edge case we will ignore.
    let counter = repo
        .mutable_counters()
        .get_counter(ctx, LATEST_REPLAYED_REQUEST_KEY)
        .await?
        .unwrap_or(0)
        .try_into()?;

    let counts = repo
        .bookmark_update_log()
        .count_further_bookmark_log_entries_by_reason(ctx.clone(), counter)
        .await?;

    let mut blobimports = 0;
    let mut others = 0;
    for (reason, count) in counts {
        if reason == BookmarkUpdateReason::Blobimport {
            blobimports += count;
        } else {
            others += count;
        }
    }

    match (blobimports > 0, others > 0) {
        (true, true) => {
            println!(
                "Remaining bundles to replay in {} ({}) are not consistent: found {} blobimports and {} non-blobimports",
                repo_name, repo_id, blobimports, others
            );
        }
        (true, false) => {
            println!(
                "All remaining bundles in {} ({}) are blobimports (found {})",
                repo_name, repo_id, blobimports,
            );
        }
        (false, true) => {
            println!(
                "All remaining bundles in {} ({}) are non-blobimports (found {})",
                repo_name, repo_id, others,
            );
        }
        (false, false) => {
            println!("No replay data found in {} ({})", repo_name, repo_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 anyhow::Result;
use clap::Args;
use context::CoreContext;
use mutable_counters::MutableCountersRef;

use super::Repo;

#[derive(Args)]
pub struct GetArgs {
    /// The name of the counter
    counter_name: String,
}

pub async fn get(ctx: &CoreContext, repo: &Repo, get_args: GetArgs) -> Result<()> {
    let mutable_counters = repo.mutable_counters();
    let maybe_value = mutable_counters
        .get_counter(ctx, get_args.counter_name.as_ref())
        .await?;
    println!("{:?}", maybe_value);
    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 context::CoreContext;
use mutable_counters::MutableCountersRef;

use super::Repo;

pub async fn list(ctx: &CoreContext, repo: &Repo) -> Result<()> {
    let mutable_counters = repo.mutable_counters();
    let counters = mutable_counters.get_all_counters(ctx).await?;

    for (name, value) in counters {
        println!("{:<30}={}", name, value);
    }

    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 context::CoreContext;
use mutable_counters::MutableCountersRef;
use repo_identity::RepoIdentityRef;

use super::Repo;

#[derive(Args)]
pub struct SetArgs {
    /// The name of the counter
    counter_name: String,
    /// The value to be set for the counter
    value: i64,
    /// The previous value of the counter. If provided, the counter will be updated only if its
    /// previous value matches the one provided as input
    #[clap(long)]
    prev_value: Option<i64>,
}

pub async fn set(ctx: &CoreContext, repo: &Repo, set_args: SetArgs) -> Result<()> {
    let mutable_counters = repo.mutable_counters();
    let name = set_args.counter_name.as_ref();
    let was_set = mutable_counters
        .set_counter(ctx, name, set_args.value, set_args.prev_value)
        .await?;
    let repo_id = repo.repo_identity().id();
    let repo_name = repo.repo_identity().name();
    if was_set {
        println!(
            "Value of {} in repo {}(Id: {}) set to {}",
            name, repo_name, repo_id, set_args.value
        );
    } else {
        println!(
            "Value of {} in repo {}(Id: {}) was NOT set to {}. The previous value of the counter did not match {:?}",
            name,
            repo_name,
            repo_id,
            set_args.value,
            set_args.prev_value.clone()
        );
    }
    Ok(())
}


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

use std::collections::HashMap;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use changesets::ChangesetsRef;
use clap::Args;
use context::CoreContext;
use futures::TryStreamExt;
use manifest::Entry;
use manifest::ManifestOps;
use manifest::PathOrPrefix;
use mononoke_types::mpath_element_iter;
use mononoke_types::ChangesetId;
use mononoke_types::FileUnodeId;
use mononoke_types::MPath;
use mononoke_types::ManifestUnodeId;
use mutable_renames::MutableRenameEntry;
use mutable_renames::MutableRenamesRef;
use repo_blobstore::RepoBlobstoreArc;
use repo_derived_data::RepoDerivedDataRef;
use unodes::RootUnodeManifestId;

use super::Repo;
use crate::commands::mutable_renames::copy_immutable;
use crate::commit_id::parse_commit_id;

#[derive(Args)]
pub struct AddArgs {
    #[clap(long)]
    /// The source CS of the mutable history
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    src_commit_id: String,

    #[clap(long)]
    /// The source path (copy from)
    src_path: String,

    #[clap(long)]
    /// The destination CS of the mutable history
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    dst_commit_id: String,

    #[clap(long)]
    /// The destination path (copied to)
    dst_path: String,

    #[clap(long)]
    /// If set, do not recurse and create mutable history entries
    /// to cover all sub-paths
    no_recurse: bool,

    #[clap(long)]
    /// Do not actually do the database change; just list the work that would be done
    dry_run: bool,

    #[clap(long, hide = true)]
    no_preserve_root: bool,
}

pub async fn add(ctx: &CoreContext, repo: &Repo, add_args: AddArgs) -> Result<()> {
    let src_cs_id = parse_commit_id(ctx, repo, &add_args.src_commit_id).await?;
    let src_path = MPath::new_opt(&add_args.src_path)?;
    let dst_cs_id = parse_commit_id(ctx, repo, &add_args.dst_commit_id).await?;
    let dst_path = MPath::new_opt(&add_args.dst_path)?;

    if (src_path.is_none() || dst_path.is_none()) && !add_args.no_preserve_root {
        bail!(concat!(
            "Source or destination is root folder of repository. If ",
            "this is on purpose, use the --no-preserve-root option."
        ))
    }

    // If we don't have mutable renames on a commit already, copy over the
    // immutable renames before adding new ones
    if !repo
        .mutable_renames()
        .has_rename_uncached(ctx, dst_cs_id)
        .await?
    {
        copy_immutable::copy_immutable_impl(ctx, repo, dst_cs_id).await?;
    }

    let src_root_unode_id = repo
        .repo_derived_data()
        .derive::<RootUnodeManifestId>(ctx, src_cs_id)
        .await?;
    let dst_root_unode_id = repo
        .repo_derived_data()
        .derive::<RootUnodeManifestId>(ctx, dst_cs_id)
        .await?;

    let src_entry = src_root_unode_id
        .manifest_unode_id()
        .find_entry(ctx.clone(), repo.repo_blobstore_arc(), src_path.clone())
        .await?
        .with_context(|| {
            format!(
                "Cannot load source manifest entry, does `{}` exist?",
                MPath::display_opt(src_path.as_ref())
            )
        })?;

    let entries = match (add_args.no_recurse, &src_entry) {
        (true, _) | (_, Entry::Leaf(_)) => {
            let dst_entry = dst_root_unode_id
                .manifest_unode_id()
                .find_entry(ctx.clone(), repo.repo_blobstore_arc(), dst_path.clone())
                .await?
                .with_context(|| {
                    format!(
                        "Cannot load destination manifest entry, does `{}` exist?",
                        MPath::display_opt(dst_path.as_ref())
                    )
                })?;
            Ok(vec![create_mutable_rename(
                src_cs_id, src_path, src_entry, dst_cs_id, dst_path, dst_entry,
            )??])
        }
        (false, Entry::Tree(src_unode_manifest)) => {
            let mut src_entries: HashMap<_, _> = src_unode_manifest
                .list_all_entries(ctx.clone(), repo.repo_blobstore_arc())
                .try_collect()
                .await?;
            let mut dst_entries: HashMap<_, _> = dst_root_unode_id
                .manifest_unode_id()
                .find_entries(
                    ctx.clone(),
                    repo.repo_blobstore_arc(),
                    [PathOrPrefix::Prefix(dst_path.clone())],
                )
                .try_collect()
                .await?;

            let first = match (src_entries.remove(&None), dst_entries.remove(&dst_path)) {
                (Some(src_entry), Some(dst_entry)) => Some(create_mutable_rename(
                    src_cs_id,
                    src_path.clone(),
                    src_entry,
                    dst_cs_id,
                    dst_path.clone(),
                    dst_entry,
                )?),
                (None, _) => {
                    bail!("Source checked earlier, but has vanished! The repo may be corrupt!")
                }
                (_, None) => bail!(
                    "Cannot load destination manifest entry, does `{}` exist?",
                    MPath::display_opt(dst_path.as_ref())
                ),
            };

            first.into_iter().chain(src_entries
                .into_iter()
                .filter_map(|(src_entry_path, src_entry)| {
                    let dst_entry_path =
                        MPath::join_opt(dst_path.as_ref(), mpath_element_iter(&src_entry_path));
                    let src_entry_path =
                        MPath::join_opt(src_path.as_ref(), mpath_element_iter(&src_entry_path));
                    let dst_entry = dst_entries.remove(&dst_entry_path);

                    if let Some(dst_entry) = dst_entry {
                        create_mutable_rename(src_cs_id, src_entry_path, src_entry, dst_cs_id, dst_entry_path, dst_entry).map_err(|e| {eprintln!("{}",e); e}).ok()
                    } else {
                        eprintln!(
                            "Source path `{}` has no matching destination `{}` - no entry created.",
                            MPath::display_opt(src_entry_path.as_ref()),
                            MPath::display_opt(dst_entry_path.as_ref())
                        );
                        None
                    }
                }))
                .collect()
        }
    }?;

    if !add_args.dry_run {
        repo.mutable_renames()
            .add_or_overwrite_renames(ctx, repo.changesets(), entries)
            .await?;
    }
    Ok(())
}

fn create_mutable_rename(
    src_cs_id: ChangesetId,
    src_path: Option<MPath>,
    src_entry: Entry<ManifestUnodeId, FileUnodeId>,
    dst_cs_id: ChangesetId,
    dst_path: Option<MPath>,
    dst_entry: Entry<ManifestUnodeId, FileUnodeId>,
) -> Result<Result<MutableRenameEntry>> {
    if dst_entry.is_tree() != src_entry.is_tree() {
        bail!(
            "Source `{}` is a {}, destination `{}` is a {} - no entry created.",
            MPath::display_opt(src_path.as_ref()),
            entry_to_type(&src_entry),
            MPath::display_opt(dst_path.as_ref()),
            entry_to_type(&dst_entry)
        );
    }

    println!(
        "Creating entry for source {} `{}` to destination {} `{}`",
        entry_to_type(&src_entry),
        MPath::display_opt(src_path.as_ref()),
        entry_to_type(&dst_entry),
        MPath::display_opt(dst_path.as_ref())
    );

    Ok(MutableRenameEntry::new(
        dst_cs_id, dst_path, src_cs_id, src_path, src_entry,
    ))
}

fn entry_to_type<T, L>(entry: &Entry<T, L>) -> &'static str {
    if entry.is_tree() { "directory" } else { "file" }
}


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

use anyhow::Result;
use clap::Args;
use context::CoreContext;
use mutable_renames::MutableRenamesRef;

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

#[derive(Args)]
pub struct CheckCommitArgs {
    /// Commit ID to check
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    commit_id: String,

    #[clap(long)]
    /// Bypass the cache and go straight to MySQL
    bypass_cache: bool,
}

pub async fn check_commit(
    ctx: &CoreContext,
    repo: &Repo,
    check_commit_args: CheckCommitArgs,
) -> Result<()> {
    let target_commit = parse_commit_id(ctx, repo, &check_commit_args.commit_id).await?;

    let has_rename = if check_commit_args.bypass_cache {
        repo.mutable_renames()
            .has_rename_uncached(ctx, target_commit)
            .await?
    } else {
        repo.mutable_renames()
            .has_rename(ctx, target_commit)
            .await?
    };

    if has_rename {
        println!("Commit has mutable renames associated with some paths");
    } else {
        println!("No mutable renames associated with this commit");
    }
    Ok(())
}


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

use std::collections::HashMap;
use std::collections::HashSet;

use anyhow::bail;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use blobstore::Loadable;
use changesets::ChangesetsRef;
use clap::Args;
use context::CoreContext;
use futures::stream;
use futures::StreamExt;
use futures::TryStreamExt;
use manifest::ManifestOps;
use mononoke_types::ChangesetId;
use mutable_renames::MutableRenameEntry;
use mutable_renames::MutableRenamesRef;
use repo_blobstore::RepoBlobstoreArc;
use repo_blobstore::RepoBlobstoreRef;
use repo_derived_data::RepoDerivedDataRef;
use unodes::RootUnodeManifestId;

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

#[derive(Args)]
pub struct CopyImmutableArgs {
    /// The CS to copy immutable history to mutable from
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    commit_id: String,
}

pub async fn copy_immutable(ctx: &CoreContext, repo: &Repo, args: CopyImmutableArgs) -> Result<()> {
    let dst_cs_id = parse_commit_id(ctx, repo, &args.commit_id).await?;
    copy_immutable_impl(ctx, repo, dst_cs_id).await
}

pub async fn copy_immutable_impl(
    ctx: &CoreContext,
    repo: &Repo,
    dst_cs_id: ChangesetId,
) -> Result<()> {
    let bonsai_cs = dst_cs_id.load(ctx, repo.repo_blobstore()).await?;

    let changes_with_copies: Vec<_> = bonsai_cs
        .file_changes()
        .filter_map(|(dst_path, change)| {
            change
                .copy_from()
                .map(|(src_path, src_cs_id)| (dst_path.clone(), src_path.clone(), *src_cs_id))
        })
        .collect();

    let src_cs_ids: HashSet<ChangesetId> = changes_with_copies
        .iter()
        .map(|(_, _, cs_id)| *cs_id)
        .collect();
    let src_unode_manifest_ids: HashMap<_, RootUnodeManifestId> =
        stream::iter(src_cs_ids.into_iter().map(|cs_id| async move {
            let src_unode_manifest_id = repo
                .repo_derived_data()
                .derive::<RootUnodeManifestId>(ctx, cs_id)
                .await?;
            Ok::<_, Error>((cs_id, src_unode_manifest_id))
        }))
        .buffer_unordered(100)
        .try_collect()
        .await?;

    let entries = stream::iter(changes_with_copies.into_iter().map({
        let src_unode_manifest_ids = &src_unode_manifest_ids;
        move |(dst_path, src_path, src_cs_id)| async move {
            if let Some(src_unode_manifest_id) = src_unode_manifest_ids.get(&src_cs_id) {
                let src_entry = src_unode_manifest_id
                    .manifest_unode_id()
                    .find_entry(
                        ctx.clone(),
                        repo.repo_blobstore_arc(),
                        Some(src_path.clone()),
                    )
                    .await?
                    .with_context(|| {
                        format!(
                            "Cannot load source manifest entry, does `{}` exist in `{}`?",
                            src_path, src_cs_id,
                        )
                    })?;

                println!(
                    "Creating entry for `{}` copied to `{}`",
                    &src_path, &dst_path
                );
                MutableRenameEntry::new(
                    dst_cs_id,
                    Some(dst_path),
                    src_cs_id,
                    Some(src_path),
                    src_entry,
                )
            } else {
                bail!(
                    "Could not find unode manifest for path {} in cs {}!",
                    src_path,
                    src_cs_id
                );
            }
        }
    }))
    .buffer_unordered(100)
    .try_collect()
    .await?;

    repo.mutable_renames()
        .add_or_overwrite_renames(ctx, repo.changesets(), entries)
        .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::Result;
use clap::Args;
use context::CoreContext;
use mononoke_types::MPath;
use mutable_renames::MutableRenamesRef;

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

#[derive(Args)]
pub struct GetArgs {
    /// Commit ID to fetch renames from
    ///
    /// This can be any commit id type.  Specify 'scheme=id' to disambiguate
    /// commit identity scheme (e.g. 'hg=HASH', 'globalrev=REV').
    commit_id: String,

    #[clap(long)]
    /// The path you wish to check
    path: String,

    #[clap(long)]
    /// Bypass the cache and go straight to MySQL
    bypass_cache: bool,
}

pub async fn get(ctx: &CoreContext, repo: &Repo, get_args: GetArgs) -> Result<()> {
    let target_commit = parse_commit_id(ctx, repo, &get_args.commit_id).await?;

    let mpath = MPath::new_opt(&get_args.path)?;

    let maybe_entry = if get_args.bypass_cache {
        repo.mutable_renames()
            .get_rename_uncached(ctx, target_commit, mpath)
            .await?
    } else {
        repo.mutable_renames()
            .get_rename(ctx, target_commit, mpath)
            .await?
    };

    match maybe_entry {
        None => println!("No mutable rename for that path and commit"),
        Some(entry) => {
            println!(
                "Source path `{}`, source bonsai CS {}, source unode {:?}",
                entry
                    .src_path()
                    .as_ref()
                    .map_or(String::new(), |p| p.to_string()),
                entry.src_cs_id(),
                entry.src_unode()
            );
        }
    }
    Ok(())
}


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

use std::collections::HashMap;
use std::collections::HashSet;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use bookmarks::BookmarkKey;
use bookmarks::BookmarksRef;
use clap::ArgGroup;
use clap::Args;
use context::CoreContext;
use fsnodes::RootFsnodeId;
use futures::stream::TryStreamExt;
use manifest::Entry;
use manifest::ManifestOps;
use mononoke_app::args::RepoArgs;
use mononoke_app::args::RepoBlobstoreArgs;
use mononoke_app::MononokeApp;
use mononoke_types::typed_hash::RedactionKeyListId;
use mononoke_types::BlobstoreKey;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreArc;
use repo_derived_data::RepoDerivedDataRef;

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

#[derive(Args)]
#[clap(group(ArgGroup::new("files-input=file").args(&["files", "input_file"]).required(true)))]
pub struct RedactionCreateKeyListArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    #[clap(long, short = 'i')]
    commit_id: String,

    /// Fail if any of the content to be redacted is reachable from this main
    /// bookmark unless --force is set.
    #[clap(long, default_value = "master")]
    main_bookmark: BookmarkKey,

    /// Force content redaction even if content is reachable from the main
    /// bookmark.
    #[clap(long)]
    force: bool,

    /// Name of a file with a list of filenames to redact.
    #[clap(long)]
    input_file: Option<PathBuf>,

    /// Name of a file to write the new key to.
    #[clap(long)]
    output_file: Option<PathBuf>,

    /// Files to redact
    #[clap(value_name = "FILE")]
    files: Vec<String>,
}

#[derive(Args)]
pub struct RedactionCreateKeyListFromIdsArgs {
    #[clap(flatten)]
    repo_blobstore_args: RepoBlobstoreArgs,

    /// Blobstore keys to redact
    #[clap(value_name = "KEY")]
    keys: Vec<String>,

    /// Name of a file to write the new key to.
    #[clap(long)]
    output_file: Option<PathBuf>,
}

#[derive(Args)]
pub struct RedactionFetchKeyListArgs {
    #[clap(flatten)]
    repo_blobstore_args: RepoBlobstoreArgs,

    /// Redaction key list id, as obtained from `create-key-list` or `create-key-list-from-id`
    #[clap(value_name = "KEY LIST ID")]
    key_list_id: RedactionKeyListId,

    /// Name of a file to write the key list to.
    #[clap(long)]
    output_file: Option<PathBuf>,
}

pub async fn fetch_key_list(
    ctx: &CoreContext,
    app: &MononokeApp,
    args: RedactionFetchKeyListArgs,
) -> Result<()> {
    let redaction_blobstore = app.redaction_config_blobstore().await?;
    let key_list = redaction::fetch_key_list(ctx, &redaction_blobstore, args.key_list_id).await?;
    if let Some(output_file) = args.output_file.as_deref() {
        let mut output = File::create(output_file).with_context(|| {
            format!(
                "Failed to open output file '{}'",
                output_file.to_string_lossy()
            )
        })?;
        for key in key_list.keys {
            output
                .write(format!("{}\n", key).as_bytes())
                .with_context(|| {
                    format!(
                        "Failed to write to output file '{}'",
                        output_file.to_string_lossy()
                    )
                })?;
        }
    }
    Ok(())
}

async fn create_key_list(
    ctx: &CoreContext,
    app: &MononokeApp,
    keys: Vec<String>,
    output_file: Option<&Path>,
) -> Result<()> {
    let redaction_blobstore = app.redaction_config_blobstore().await?;
    let darkstorm_blobstore = app.redaction_config_blobstore_for_darkstorm().await?;
    let key_list_id =
        redaction::create_key_list(ctx, &redaction_blobstore, &darkstorm_blobstore, keys).await?;
    if let Some(output_file) = output_file {
        let mut output = File::create(output_file).with_context(|| {
            format!(
                "Failed to open output file '{}'",
                output_file.to_string_lossy()
            )
        })?;
        output
            .write_all(key_list_id.to_string().as_bytes())
            .with_context(|| {
                format!(
                    "Failed to write to output file '{}'",
                    output_file.to_string_lossy()
                )
            })?;
    }
    Ok(())
}

/// Returns the content keys for the given paths.
async fn content_keys_for_paths(
    ctx: &CoreContext,
    repo: &Repo,
    cs_id: ChangesetId,
    paths: Vec<MPath>,
) -> Result<HashSet<String>> {
    let root_fsnode_id = repo
        .repo_derived_data()
        .derive::<RootFsnodeId>(ctx, cs_id)
        .await?;
    let path_content_keys = root_fsnode_id
        .fsnode_id()
        .find_entries(ctx.clone(), repo.repo_blobstore_arc(), paths.clone())
        .try_filter_map(|(path, entry)| async move {
            match (path, entry) {
                (Some(path), Entry::Leaf(fsnode_file)) => {
                    Ok(Some((path, fsnode_file.content_id().blobstore_key())))
                }
                _ => Ok(None),
            }
        })
        .try_collect::<HashMap<_, _>>()
        .await?;

    let mut missing_paths = 0;
    for path in paths.iter() {
        if !path_content_keys.contains_key(path) {
            eprintln!("Missing file: {}", path);
            missing_paths += 1;
        }
    }
    if missing_paths > 0 {
        bail!("Failed to find {} files in this commit", missing_paths);
    }

    Ok(path_content_keys.into_values().collect())
}

pub async fn create_key_list_from_commit_files(
    ctx: &CoreContext,
    app: &MononokeApp,
    create_args: RedactionCreateKeyListArgs,
) -> Result<()> {
    let mut files = create_args
        .files
        .iter()
        .map(MPath::new)
        .collect::<Result<Vec<_>>>()?;
    if let Some(input_file) = create_args.input_file {
        let input_file =
            BufReader::new(File::open(input_file).context("Failed to open input file")?);
        for line in input_file.lines() {
            files.push(MPath::new(line?)?);
        }
    }
    if files.is_empty() {
        bail!("No files to redact");
    }
    let repo: Repo = app
        .open_repo(&create_args.repo_args)
        .await
        .context("Failed to open repo")?;

    let cs_id = parse_commit_id(ctx, &repo, &create_args.commit_id).await?;

    let keys = content_keys_for_paths(ctx, &repo, cs_id, files).await?;

    println!(
        "Checking redacted content doesn't exist in '{}' bookmark",
        create_args.main_bookmark
    );
    let main_cs_id = repo
        .bookmarks()
        .get(ctx.clone(), &create_args.main_bookmark)
        .await?
        .ok_or_else(|| {
            anyhow!(
                "Main bookmark '{}' does not exist",
                create_args.main_bookmark
            )
        })?;
    let main_redacted = paths_for_content_keys(ctx, &repo, main_cs_id, &keys).await?;

    if main_redacted.is_empty() {
        println!(
            "No files would be redacted in the main bookmark ({})",
            create_args.main_bookmark
        );
    } else {
        for (path, content_id) in main_redacted.iter() {
            println!(
                "Redacted content in main bookmark: {} {}",
                path,
                content_id.blobstore_key(),
            );
        }
        if create_args.force {
            println!(
                "Creating key list despite {} files being redacted in the main bookmark ({}) (--force)",
                main_redacted.len(),
                create_args.main_bookmark
            );
        } else {
            bail!(
                "Refusing to create key list because {} files would be redacted in the main bookmark ({})",
                main_redacted.len(),
                create_args.main_bookmark
            );
        }
    }

    create_key_list(
        ctx,
        app,
        keys.into_iter().collect(),
        create_args.output_file.as_deref(),
    )
    .await
}

pub async fn create_key_list_from_blobstore_keys(
    ctx: &CoreContext,
    app: &MononokeApp,
    create_args: RedactionCreateKeyListFromIdsArgs,
) -> Result<()> {
    create_key_list(
        ctx,
        app,
        create_args.keys,
        create_args.output_file.as_deref(),
    )
    .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 std::collections::HashSet;
use std::sync::Arc;

use anyhow::Context;
use anyhow::Result;
use blobstore::Loadable;
use clap::Args;
use context::CoreContext;
use fsnodes::RootFsnodeId;
use futures::stream::TryStreamExt;
use manifest::ManifestOps;
use metaconfig_types::RepoConfigRef;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mononoke_types::BlobstoreKey;
use mononoke_types::ChangesetId;
use mononoke_types::ContentId;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreArc;
use repo_blobstore::RepoBlobstoreRef;
use repo_derived_data::RepoDerivedDataRef;

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

#[derive(Args)]
pub struct RedactionListArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    #[clap(long, short = 'i')]
    commit_id: String,
}

/// Returns paths and content ids whose content matches the given keys in the
/// given commit
pub(super) async fn paths_for_content_keys(
    ctx: &CoreContext,
    repo: &Repo,
    cs_id: ChangesetId,
    keys: &HashSet<String>,
) -> Result<Vec<(MPath, ContentId)>> {
    let root_fsnode_id = repo
        .repo_derived_data()
        .derive::<RootFsnodeId>(ctx, cs_id)
        .await?;
    let file_count = root_fsnode_id
        .fsnode_id()
        .load(ctx, repo.repo_blobstore())
        .await?
        .summary()
        .descendant_files_count;
    let mut processed = 0;
    let mut paths = Vec::new();
    let mut entries = root_fsnode_id
        .fsnode_id()
        .list_leaf_entries(ctx.clone(), repo.repo_blobstore_arc());
    while let Some((path, fsnode_file)) = entries.try_next().await? {
        processed += 1;
        if processed % 100_000 == 0 {
            if paths.is_empty() {
                println!("Processed files: {}/{}", processed, file_count);
            } else {
                println!(
                    "Processed files: {}/{} ({} found so far)",
                    processed,
                    file_count,
                    paths.len()
                );
            }
        }
        if keys.contains(&fsnode_file.content_id().blobstore_key()) {
            paths.push((path, fsnode_file.content_id().clone()));
        }
    }
    Ok(paths)
}

pub async fn list(
    ctx: &CoreContext,
    app: &MononokeApp,
    list_args: RedactionListArgs,
) -> Result<()> {
    let repo: Repo = app
        .open_repo(&list_args.repo_args)
        .await
        .context("Failed to open repo")?;

    let cs_id = parse_commit_id(ctx, &repo, &list_args.commit_id).await?;

    // We don't have a way to get the keys for the redacted blobs out of the
    // repo blobstore, so we must ask the factory to load them again.  Until
    // SqlRedactedBlobs are removed, we need to know the metadata database
    // config for this.
    let db_config = &repo.repo_config().storage_config.metadata;
    let common_config = Arc::new(app.repo_configs().common.clone());
    let redacted_blobs = app
        .repo_factory()
        .redacted_blobs(ctx.clone(), db_config, &common_config)
        .await?;
    let redacted_map = redacted_blobs.redacted();
    let keys = redacted_map.keys().cloned().collect();

    println!("Searching for redacted paths in {}", cs_id);
    let mut redacted_paths = paths_for_content_keys(ctx, &repo, cs_id, &keys).await?;
    println!("Found {} redacted paths", redacted_paths.len());

    redacted_paths.sort_by(|a, b| a.0.cmp(&b.0));
    for (path, content_id) in redacted_paths {
        if let Some(meta) = redacted_map.get(&content_id.blobstore_key()) {
            let log_only = if meta.log_only { " (log only)" } else { "" };
            println!("{:20}: {}{}", meta.task, path, log_only);
        }
    }

    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::BookmarkKey;
use bookmarks::BookmarksRef;
use clap::Args;
use context::CoreContext;
use repo_identity::RepoIdentityRef;

use super::Repo;

#[derive(Args)]
pub struct RepoInfoArgs {}

pub async fn repo_info(ctx: &CoreContext, repo: &Repo, _args: RepoInfoArgs) -> Result<()> {
    println!("Repo: {}", repo.repo_identity().name());
    println!("Repo-Id: {}", repo.repo_identity().id());
    let main_bookmark = BookmarkKey::new("master")?;
    let main_bookmark_value = repo
        .bookmarks()
        .get(ctx.clone(), &main_bookmark)
        .await
        .with_context(|| format!("Failed to resolve main bookmark ({})", main_bookmark))?
        .as_ref()
        .map(ToString::to_string);
    println!(
        "Main-Bookmark: {} {}",
        main_bookmark,
        main_bookmark_value.as_deref().unwrap_or("(not set)")
    );
    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::borrow::Cow;

use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use live_commit_sync_config::CfgrCurrentCommitSyncConfig;
use live_commit_sync_config::RepoGroup;
use mononoke_app::args::MultiRepoArgs;
use mononoke_app::MononokeApp;
use question::Answer;
use question::Question;
use repo_identity::RepoIdentityRef;
use repo_lock::RepoLockRef;
use repo_lock::RepoLockState;

use super::Repo;

async fn repos_in_group<'a>(
    app: &'_ MononokeApp,
    repo: &'a Repo,
    group: RepoGroup,
    action: &'_ str,
    single_repo: bool,
) -> Result<Vec<Cow<'a, Repo>>> {
    if single_repo {
        Ok(vec![Cow::Borrowed(repo)])
    } else {
        let repos = app
            .open_repos::<Repo>(&MultiRepoArgs {
                repo_id: group
                    .into_vec()
                    .into_iter()
                    .map(|repo_id| repo_id.id())
                    .collect(),
                repo_name: vec![],
            })
            .await?;
        if repos.len() > 1 {
            let q = format!(
                "{} all repos [{}]?",
                action,
                repos.iter().map(|r| r.repo_identity().name()).join(", ")
            );
            if Question::new(&q).confirm() != Answer::YES {
                anyhow::bail!("Not doing operation on user request")
            }
        }
        Ok(repos.into_iter().map(Cow::Owned).collect())
    }
}

#[derive(Args)]
pub struct RepoLockArgs {
    /// Why is the repo being locked
    #[clap(long)]
    reason: String,
    /// Lock this single repo even if it's part of a megarepo
    #[clap(long)]
    single_repo: bool,
}

pub async fn repo_lock(app: &MononokeApp, repo: &Repo, args: RepoLockArgs) -> Result<()> {
    let RepoLockArgs {
        reason,
        single_repo,
    } = args;
    let config = CfgrCurrentCommitSyncConfig::new(app.config_store())?;
    let group = config.repo_group(repo.repo_identity.id()).await?;
    let repos = repos_in_group(app, repo, group, "Lock", single_repo).await?;
    // If necessary to optimise, we need to do a single SQL query for this. A bit tricky because
    // most of our things are made for a single repo.
    for repo in repos {
        repo.repo_lock()
            .set_repo_lock(RepoLockState::Locked(reason.clone()))
            .await?;
        println!("{} locked", repo.repo_identity().name());
    }
    Ok(())
}

#[derive(Args)]
pub struct RepoUnlockArgs {
    /// USE WITH CARE. You must make sure the same path is not writable from both repos,
    /// to prevent repo divergence.
    #[clap(long)]
    allow_disabled_pushredirection: bool,
    /// Lock this single repo even if it's part of a megarepo
    #[clap(long)]
    single_repo: bool,
}

pub async fn repo_unlock(app: &MononokeApp, repo: &Repo, args: RepoUnlockArgs) -> Result<()> {
    let RepoUnlockArgs {
        allow_disabled_pushredirection,
        single_repo,
    } = args;
    let config = CfgrCurrentCommitSyncConfig::new(app.config_store())?;
    let group = config.repo_group(repo.repo_identity.id()).await?;
    if !allow_disabled_pushredirection {
        if let Some(repos) = group.small_repos_with_pushredirection_disabled(&config) {
            anyhow::bail!(
                concat!(
                    "The following repos have pushredirection config set but disabled: {:?}\n",
                    "Be careful it does not lead to repo divergence. If this is expected, ",
                    "re-run this command with --allow-disabled-pushredirection"
                ),
                repos
            )
        }
    }
    let repos = repos_in_group(app, repo, group, "Unlock", single_repo).await?;
    for repo in repos {
        repo.repo_lock()
            .set_repo_lock(RepoLockState::Unlocked)
            .await?;
        println!("{} unlocked", repo.repo_identity().name());
    }
    Ok(())
}

#[derive(Args)]
pub struct RepoShowLockArgs {}

pub async fn repo_show_lock(_app: &MononokeApp, repo: &Repo, args: RepoShowLockArgs) -> Result<()> {
    let RepoShowLockArgs {} = args;
    let state = repo.repo_lock().check_repo_lock().await?;
    let state = match state {
        RepoLockState::Unlocked => "unlocked".to_string(),
        RepoLockState::Locked(reason) => format!("locked with reason: {}", reason),
    };
    println!("{} is {}", repo.repo_identity().name(), state);
    println!("Consider using `newadmin repos show-locks` to see locks on all repos");
    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 clap::Parser;
use mononoke_app::MononokeApp;
use regex::Regex;

#[derive(Parser)]
pub struct ReposListArgs {
    /// Pattern to match against repo names.
    pattern: Option<String>,
}

pub async fn repos_list(app: MononokeApp, args: ReposListArgs) -> Result<()> {
    let ReposListArgs { pattern } = args;
    let pattern = pattern
        .as_deref()
        .map(Regex::new)
        .transpose()
        .context("Failed to parse pattern")?;

    let configs = app.repo_configs();
    let mut repos = configs.repos.iter().collect::<Vec<_>>();
    repos.sort_unstable_by_key(|(_repo_name, repo_config)| repo_config.repoid);

    for (repo_name, repo_config) in repos.into_iter() {
        if let Some(pattern) = &pattern {
            if !pattern.is_match(repo_name) {
                continue;
            }
        }
        println!("{} {}", repo_config.repoid, repo_name);
    }

    Ok(())
}


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

use std::collections::HashMap;
use std::collections::HashSet;

use anyhow::Result;
use clap::Parser;
use metaconfig_types::MetadataDatabaseConfig;
use metaconfig_types::RepoReadOnly;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mononoke_types::RepositoryId;
use repo_lock::RepoLock;
use repo_lock::RepoLockState;

#[derive(Parser)]
pub struct ReposShowLocksArgs {
    /// Only show locked repos and omit all the unlocked ones
    #[clap(long)]
    only_locked: bool,
}

#[facet::container]
struct Repo {
    #[facet]
    lock: dyn RepoLock,
}

pub async fn repos_show_locks(app: MononokeApp, args: ReposShowLocksArgs) -> Result<()> {
    let ReposShowLocksArgs { only_locked } = args;

    let mut dbs = HashSet::new();
    let mut repos_with_unique_dbs = vec![];

    for (name, config) in app.repo_configs().repos.iter() {
        if config.readonly == RepoReadOnly::ReadWrite {
            if let MetadataDatabaseConfig::Remote(remote) = &config.storage_config.metadata {
                if dbs.insert(&remote.primary) {
                    repos_with_unique_dbs.push(name.clone());
                }
            }
        }
    }

    let mut id_to_name: HashMap<RepositoryId, String> = app
        .repo_configs()
        .repos
        .iter()
        .map(|(name, config)| (config.repoid, name.clone()))
        .collect();

    let mut all_status = vec![];

    for name in repos_with_unique_dbs {
        let repo: Repo = app.open_repo(&RepoArgs::from_repo_name(name)).await?;
        for (repo_id, state) in repo.lock.all_repos_lock().await? {
            if !only_locked || matches!(state, RepoLockState::Locked(_)) {
                let repo_name = id_to_name
                    .remove(&repo_id)
                    .unwrap_or_else(|| format!("Repo id {}", repo_id));
                all_status.push((repo_name, state))
            }
        }
    }
    // Let's be consistent about the order we print stuff, to reduce confusion
    all_status.sort_unstable_by_key(|(name, _)| name.clone());
    for (name, state) in all_status {
        println!("{:20} {:?}", name, state);
    }
    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::Parser;
use fbinit::FacebookInit;
use mononoke_app::args::MultiRepoArgs;
use mononoke_app::MononokeApp;
use mononoke_app::MononokeAppBuilder;
use repo_identity::RepoIdentity;
use repo_identity::RepoIdentityRef;

/// Display the repo identity of the chosen repo.
#[derive(Parser)]
struct ExampleArgs {
    #[clap(flatten)]
    repos: MultiRepoArgs,
}

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

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
    let ext = additional::TestAppExtension { default: Some(42) };
    MononokeAppBuilder::new(fb)
        .with_app_extension(ext)
        .build::<ExampleArgs>()?
        .run_basic(async_main)
}

async fn async_main(app: MononokeApp) -> Result<()> {
    let args: ExampleArgs = app.args()?;
    let test_args = app.extension_args::<additional::TestAppExtension>()?;

    let repos: Vec<Repo> = app.open_repos(&args.repos).await?;

    for repo in repos {
        println!(
            "Repo Id: {} Name: {}",
            repo.repo_identity().id(),
            repo.repo_identity().name(),
        );
    }
    if let Some(test_arg) = test_args.test_arg {
        println!("Test arg: {}", test_arg);
    }

    Ok(())
}

/// Example module for hooking into command line arguments that modify the
/// environment.
mod additional {
    use clap::Args;
    use mononoke_app::AppExtension;

    // This struct defines the command line arguments, and is a normal
    // implementor of `clap::Args`.
    #[derive(Args)]
    pub struct TestArgs {
        /// Test argument.
        #[clap(long, help_heading = "TEST OPTIONS")]
        pub test_arg: Option<u32>,
    }

    // This struct defines the extension.  It can be used to store default
    // values and other items necessary for the arguments to operate.
    pub struct TestAppExtension {
        pub default: Option<u32>,
    }

    impl AppExtension for TestAppExtension {
        type Args = TestArgs;

        fn arg_defaults(&self) -> Vec<(&'static str, String)> {
            if let Some(default) = self.default {
                vec![("test-arg", default.to_string())]
            } else {
                Vec::new()
            }
        }
    }
}


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

use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;

use anyhow::bail;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use executor_lib::RepoShardedProcess;
use executor_lib::RepoShardedProcessExecutor;
use executor_lib::ShardedProcessExecutor;
use fbinit::FacebookInit;
use mononoke_app::MononokeApp;
use mononoke_app::MononokeAppBuilder;
use once_cell::sync::OnceCell;
use sharding_ext::RepoShard;
use slog::info;
use slog::Logger;
use tokio::time;

/// Test application for validating integration behavior with ShardManager.
/// The below can be used with new/existing jobs before migrating them to
/// the sharded execution model.
/// The application can be used to validate:
/// -> Proper repo distribution according to assigned weights.
/// -> Sanity check of core functionality for target BP before onboarding.

/// Struct representing the Background Process that needs to be executed
/// by the Mononoke Sharded Process Manager. This struct can contain state
/// that applies to all repos and need to be one-time initialized. Examples
/// include FacebookInit, MononokeApp, CoreContext, etc.
pub struct TestProcess {
    app: MononokeApp,
}

impl TestProcess {
    fn new(app: MononokeApp) -> Self {
        Self { app }
    }
}

#[async_trait]
impl RepoShardedProcess for TestProcess {
    /// Method responsible for performing the initial setup of the BP in context of
    /// the provided repo. This method should ONLY contain code necessary to build
    /// state (in form of the struct that implements RepoShardedProcessExecutor trait)
    /// that is required to execute the job. The repo (or related entity)
    /// should be included as part of the RepoShardedProcessExecutor state.
    async fn setup(&self, repo: &RepoShard) -> Result<Arc<dyn RepoShardedProcessExecutor>> {
        // Since this is a test application, there is no actual state construction
        // to be performed here. In common cases, this would involve generating the
        // Repo struct or related entity utilizing a factory and then storing the
        // generated entities as part of the returned struct.
        Ok(Arc::new(TestProcessExecutor::new(&self.app, repo)))
    }
}

/// Struct representing the execution of the Backgroung Process over
/// a particular repo. This struct can contain state that is specific
/// to an individual repo's execution. It can also include signalling
/// mechanism to indicate that the execution of a particular repo needs
/// to be stopped or paused.
pub struct TestProcessExecutor {
    terminate_execution: Arc<AtomicBool>,
    repo: RepoShard,
    logger: Logger,
}

impl TestProcessExecutor {
    fn new(app: &MononokeApp, repo: &RepoShard) -> Self {
        Self {
            terminate_execution: Arc::new(AtomicBool::new(false)),
            logger: app.logger().clone(),
            repo: repo.clone(),
        }
    }
}

/// __NOTE__: This is a free-function and is invoked by both the sharded
/// and non-sharded execution. This model should be utilized when migrating
/// existing jobs with the following objectives:
/// 1. Separation and isolation of sharded execution vs non-sharded execution
/// 2. No changes to functionality (in case the BP has multi-repo logic built
///    into its core, e.g. Walker)
/// 3. Preventing rewrite of the existing repo-parsing logic from CLI args.
/// Note that even for existing jobs, the eventual goal would be to migrate
/// to the below model.

/// For new jobs or existing jobs that do not have the above properties,
/// the below function should not be a free function. It should instead be
/// included as part of the RepoShardedProcessExecutor trait. In case of SM
/// based invocation, the executor will use this method to dynamically provide
/// repos to the BP. In case of non-SM or CLI based invocation, the `run`
/// method will parse the repos from CLI args and then pass them to the
/// `execute()` method within the trait. The entire logic will be housed in
/// the structs that implement these traits and both the BPE and CLI run
/// function will invoke the same methods.

/// Function representing the work that need to be done on the repo
/// as part of this BP.
async fn do_busy_work(
    logger: &Logger,
    repo: &RepoShard,
    terminate_execution: Arc<AtomicBool>,
) -> Result<()> {
    info!(
        logger,
        "Beginning execution of test process for repo {}", repo,
    );
    let mut iteration = 1;
    loop {
        info!(
            logger,
            "Executing iteration {} of test process for repo {}", iteration, repo,
        );
        // Equivalent to heavy work being done by the process for a repo.
        time::sleep(time::Duration::from_secs(SECS_IN_MINUTE)).await;
        // Completed one iteration of the heavy work. Good time to check if
        // the manager expects us to stop working on this repo.
        if terminate_execution.load(Ordering::Relaxed) {
            // Termination requested. Time to perform clean-up activities
            // before we give up the repo. E.g. pushing out write from memory
            // to actual storage, flushing logs, releasing locks, etc.
            info!(
                logger,
                "Finishing execution of test process for repo {} after {} iterations",
                repo,
                iteration,
            );
            // Finally return
            return Ok(());
        } else {
            // The manager doesn't expect us to give up the current repo at the
            // moment. Continue executing future iterations.
            iteration += 1;
        }
        // Fail at every 10th iteration and let the framework reassign this task
        if iteration % 10 == 0 {
            bail!(
                "Sample transient error for repo {} after {} iterations",
                repo,
                iteration
            )
        }
    }
}

#[async_trait]
impl RepoShardedProcessExecutor for TestProcessExecutor {
    /// Core logic for executing the BPE + logic to check some form of
    /// signal from the corresponding stop() method. The stop() method
    /// will only give a heads-up, the actual cleaning and state-save
    /// activities need to be performed as part of the execute() call.
    /// Post the completion of stop() callback, the execute() method
    /// has timeout secs to finish its book-keeping activities.
    async fn execute(&self) -> Result<()> {
        do_busy_work(
            &self.logger,
            &self.repo,
            Arc::clone(&self.terminate_execution),
        )
        .await
    }

    /// Note that this method is responsible ONLY for signalling the termination
    /// of the execution of the repo for the given BP. Once the control returns
    /// from this method, the executor waits for timeout seconds (specified during
    /// executor construction) before initiating forced termination of the executing
    /// process. If there are any book-keeping activities pending, they should be
    /// completed in the main execution method during this timeout period.
    async fn stop(&self) -> Result<()> {
        // Signal the BP to stop executing this repo.
        self.terminate_execution.store(true, Ordering::Relaxed);
        Ok(())
    }
}

/// Arguments for a sample command line app.
#[derive(Parser)]
struct TestArgs {
    /// The name of ShardManager service to be used when the walker
    /// functionality is desired to be executed in a sharded setting.
    #[clap(long)]
    pub sharded_service_name: Option<String>,
    /// The repo for which the job needs to be executed when sharded
    /// execution is not desired.
    #[clap(long, conflicts_with = "sharded_service_name")]
    pub repo_name: Option<String>,
}

/// Keep it global until regional deployment is desired.
const SM_SERVICE_SCOPE: &str = "global";
/// Adjust the value based on the time taken to perform
/// cleanup of a BP execution instance over a repo. Max is
/// 180 seconds. Ideally, should be under 60 seconds.
const SM_CLEANUP_TIMEOUT_SECS: u64 = 120;
/// Constant representing seconds in a minute.
const SECS_IN_MINUTE: u64 = 60;

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
    let app = MononokeAppBuilder::new(fb).build::<TestArgs>()?;
    app.run_basic(run)
}

async fn run(app: MononokeApp) -> Result<()> {
    // The below flag can be used to determine if the current BP execution
    // should occur in sharded or non-sharded manner. The flag can be read
    // from static config, or DB table or based on CLI value specified along
    // with the actual command. If the sharded-service-name is non-empty, it
    // indicates that the job needs to run in sharded mode.
    match &app.args::<TestArgs>()?.sharded_service_name {
        // If sharded execution is not desired, the repo passed to on-repo-
        // load shoud be derived through CLI args or some other source.
        None => run_unsharded(app).await,
        Some(service_name) => run_sharded(app, service_name.to_string()).await,
    }
}

async fn run_sharded(app: MononokeApp, sharded_service_name: String) -> Result<()> {
    let process = TestProcess::new(app);
    let logger = process.app.logger().clone();
    /// The name of the deployed ShardManager job that will orchestrate
    /// the below BP. For testing purposes, keep it mononoke.shardmanager.test
    /// and deploy the resultant binary to mononoke_shardmanager_test TW job.
    /// mononoke.shardmanager.test currently works with 28 repos and 7 task
    /// replicas.
    // The service name needs to be 'static to satisfy SM contract
    static SM_SERVICE_NAME: OnceCell<String> = OnceCell::new();
    // For sharded execution, we need to first create the executor.
    let mut executor = ShardedProcessExecutor::new(
        process.app.fb,
        process.app.runtime().clone(),
        &logger,
        SM_SERVICE_NAME.get_or_init(|| sharded_service_name),
        SM_SERVICE_SCOPE,
        SM_CLEANUP_TIMEOUT_SECS,
        Arc::new(process),
        true, // enable shard (repo) level healing
    )?;
    executor
        .block_and_execute(&logger, Arc::new(AtomicBool::new(false)))
        .await
}

async fn run_unsharded(app: MononokeApp) -> Result<()> {
    let repo_name = app
        .args::<TestArgs>()?
        .repo_name
        .expect("Repo name needs to be provided when executing in unsharded mode");
    let repo = RepoShard::from_shard_id(&repo_name)?;
    // Terminate execution can still be used to halt execution even in unsharded mode.
    // For this example, we are immediately terminating after one loop.
    let terminate_execution = Arc::new(AtomicBool::new(true));
    do_busy_work(app.logger(), &repo, Arc::clone(&terminate_execution)).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.
 */

mononoke_app::subcommands! {
    mod lfs;
}


/*
 * 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::Parser;
use fbinit::FacebookInit;
use mononoke_app::MononokeApp;
use mononoke_app::MononokeAppBuilder;

mod commands;

#[derive(Parser)]
struct ImportArgs {}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
    let subcommands = commands::subcommands();
    let app = MononokeAppBuilder::new(fb).build_with_subcommands::<ImportArgs>(subcommands)?;
    app.run_basic(async_main)
}

async fn async_main(app: MononokeApp) -> Result<()> {
    commands::dispatch(app).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::Context;
use anyhow::Result;
use borrowed::borrowed;
use bytes::Bytes;
use clap::Parser;
use filestore::FilestoreConfig;
use futures::stream;
use futures::TryStreamExt;
use lfs_import_lib::lfs_upload;
use mercurial_types::blobs::File;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use repo_blobstore::RepoBlobstore;

/// Import LFS blobs
#[derive(Parser)]
pub struct CommandArgs {
    /// LFS Helper
    lfs_helper: String,

    /// Raw LFS pointers to be imported
    #[clap(required(true))]
    pointers: Vec<String>,

    #[clap(flatten)]
    repo_args: RepoArgs,

    /// The number of OIDs to process in parallel
    #[clap(long, default_value = "16")]
    concurrency: usize,
}

#[facet::container]
pub struct Repo {
    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    filestore_config: FilestoreConfig,
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let entries: Vec<_> = args
        .pointers
        .into_iter()
        .map(|e| File::new(Bytes::copy_from_slice(e.as_bytes()), None, None).get_lfs_content())
        .collect();

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

    let ctx = app.new_basic_context();

    stream::iter(entries)
        .try_for_each_concurrent(args.concurrency, {
            borrowed!(ctx, repo, args.lfs_helper);
            move |lfs| async move {
                lfs_upload(ctx, repo, lfs_helper, &lfs).await?;
                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.
 */

mononoke_app::subcommands! {
    mod create_bonsai;
    mod drawdag;
    mod modify_bookmark;
}