/*
 * 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::num::NonZeroU64;
use std::sync::Arc;

use anyhow::bail;
use anyhow::format_err;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use blobrepo::BlobRepo;
use bookmarks::BookmarkKey;
use borrowed::borrowed;
use clap::ArgMatches;
use cmdlib::args;
use cmdlib::args::MononokeMatches;
use cmdlib::helpers;
use cmdlib_x_repo::create_commit_syncer_from_matches;
use context::CoreContext;
use cross_repo_sync::create_commit_syncer_lease;
use cross_repo_sync::find_toposorted_unsynced_ancestors;
use cross_repo_sync::types::Source;
use cross_repo_sync::types::Target;
use cross_repo_sync::validation::verify_working_copy_with_version_fast_path;
use cross_repo_sync::CandidateSelectionHint;
use cross_repo_sync::CommitSyncContext;
use cross_repo_sync::CommitSyncOutcome;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync::ConcreteRepo as CrossRepo;
use derived_data::BonsaiDerived;
use fbinit::FacebookInit;
use fsnodes::RootFsnodeId;
use futures::compat::Future01CompatExt;
use futures::future::try_join;
use futures::future::try_join_all;
use futures::Stream;
use futures::StreamExt;
use futures::TryStreamExt;
use live_commit_sync_config::CfgrLiveCommitSyncConfig;
use live_commit_sync_config::LiveCommitSyncConfig;
use manifest::Entry;
use manifest::ManifestOps;
use manifest::PathOrPrefix;
use metaconfig_types::CommitSyncConfigVersion;
use metaconfig_types::MetadataDatabaseConfig;
use mononoke_api_types::InnerRepo;
use mononoke_types::ChangesetId;
use mononoke_types::FileChange;
use mononoke_types::MPath;
use mononoke_types::RepositoryId;
use movers::get_small_to_large_mover;
use movers::Mover;
use regex::Regex;
use repo_blobstore::RepoBlobstoreRef;
use repo_identity::RepoIdentityRef;
use slog::info;
use slog::warn;
#[cfg(fbcode_build)]
use sql_ext::facebook::MyAdmin;
use sql_ext::replication::NoReplicaLagMonitor;
use sql_ext::replication::ReplicaLagMonitor;
use sql_ext::replication::WaitForReplicationConfig;
use synced_commit_mapping::EquivalentWorkingCopyEntry;
use synced_commit_mapping::SqlSyncedCommitMapping;
use synced_commit_mapping::SyncedCommitMapping;
use synced_commit_mapping::SyncedCommitMappingEntry;
use synced_commit_mapping::WorkingCopyEquivalence;
use tokio::fs::read_to_string;
use tokio::fs::File;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;

mod catchup;
mod cli;
mod gradual_merge;
mod manual_commit_sync;
mod merging;
mod sync_diamond_merge;

use megarepolib::chunking::even_chunker_with_max_size;
use megarepolib::chunking::parse_chunking_hint;
use megarepolib::chunking::path_chunker_from_hint;
use megarepolib::chunking::Chunker;
use megarepolib::commit_sync_config_utils::diff_small_repo_commit_sync_configs;
use megarepolib::common::create_and_save_bonsai;
use megarepolib::common::delete_files_in_chunks;
use megarepolib::common::StackPosition;
use megarepolib::history_fixup_delete::create_history_fixup_deletes;
use megarepolib::history_fixup_delete::HistoryFixupDeletes;
use megarepolib::perform_move;
use megarepolib::perform_stack_move;
use megarepolib::pre_merge_delete::create_pre_merge_delete;
use megarepolib::pre_merge_delete::PreMergeDelete;
use megarepolib::working_copy::get_working_copy_paths_by_prefixes;

use crate::cli::cs_args_from_matches;
use crate::cli::get_catchup_head_delete_commits_cs_args_factory;
use crate::cli::get_delete_commits_cs_args_factory;
use crate::cli::get_gradual_merge_commits_cs_args_factory;
use crate::cli::setup_app;
use crate::cli::BACKFILL_NOOP_MAPPING;
use crate::cli::BASE_COMMIT_HASH;
use crate::cli::BONSAI_MERGE;
use crate::cli::BONSAI_MERGE_P1;
use crate::cli::BONSAI_MERGE_P2;
use crate::cli::CATCHUP_DELETE_HEAD;
use crate::cli::CATCHUP_VALIDATE_COMMAND;
use crate::cli::CHANGESET;
use crate::cli::CHECK_PUSH_REDIRECTION_PREREQS;
use crate::cli::CHUNKING_HINT_FILE;
use crate::cli::COMMIT_BOOKMARK;
use crate::cli::COMMIT_HASH;
use crate::cli::COMMIT_HASH_CORRECT_HISTORY;
use crate::cli::DELETE_NO_LONGER_BOUND_FILES_FROM_LARGE_REPO;
use crate::cli::DELETION_CHUNK_SIZE;
use crate::cli::DIFF_MAPPING_VERSIONS;
use crate::cli::DRY_RUN;
use crate::cli::EVEN_CHUNK_SIZE;
use crate::cli::FIRST_PARENT;
use crate::cli::GRADUAL_DELETE;
use crate::cli::GRADUAL_MERGE;
use crate::cli::GRADUAL_MERGE_PROGRESS;
use crate::cli::HEAD_BOOKMARK;
use crate::cli::HISTORY_FIXUP_DELETE;
use crate::cli::INPUT_FILE;
use crate::cli::LAST_DELETION_COMMIT;
use crate::cli::LIMIT;
use crate::cli::MANUAL_COMMIT_SYNC;
use crate::cli::MAPPING_VERSION_NAME;
use crate::cli::MARK_NOT_SYNCED_COMMAND;
use crate::cli::MAX_NUM_OF_MOVES_IN_COMMIT;
use crate::cli::MERGE;
use crate::cli::MOVE;
use crate::cli::ORIGIN_REPO;
use crate::cli::OVERWRITE;
use crate::cli::PARENTS;
use crate::cli::PATH;
use crate::cli::PATHS_FILE;
use crate::cli::PATH_PREFIX;
use crate::cli::PATH_REGEX;
use crate::cli::PRE_DELETION_COMMIT;
use crate::cli::PRE_MERGE_DELETE;
use crate::cli::RUN_MOVER;
use crate::cli::SECOND_PARENT;
use crate::cli::SELECT_PARENTS_AUTOMATICALLY;
use crate::cli::SOURCE_CHANGESET;
use crate::cli::SYNC_COMMIT_AND_ANCESTORS;
use crate::cli::SYNC_DIAMOND_MERGE;
use crate::cli::TARGET_CHANGESET;
use crate::cli::TO_MERGE_CS_ID;
use crate::cli::VERSION;
use crate::cli::WAIT_SECS;
use crate::merging::perform_merge;

async fn run_move<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
    live_commit_sync_config: CfgrLiveCommitSyncConfig,
) -> Result<(), Error> {
    let origin_repo =
        RepositoryId::new(args::get_i32_opt(sub_m, ORIGIN_REPO).expect("Origin repo is missing"));
    let resulting_changeset_args = cs_args_from_matches(sub_m);
    let move_parent = sub_m.value_of(CHANGESET).unwrap().to_owned();

    let mapping_version_name = sub_m
        .value_of(MAPPING_VERSION_NAME)
        .ok_or_else(|| format_err!("mapping-version-name is not specified"))?;
    let mapping_version = CommitSyncConfigVersion(mapping_version_name.to_string());

    let commit_sync_config = live_commit_sync_config
        .get_commit_sync_config_by_version(origin_repo, &mapping_version)
        .await?;
    let mover = get_small_to_large_mover(&commit_sync_config, origin_repo).unwrap();

    let max_num_of_moves_in_commit: Option<NonZeroU64> =
        args::get_and_parse_opt(sub_m, MAX_NUM_OF_MOVES_IN_COMMIT);

    let (repo, resulting_changeset_args) = try_join(
        args::not_shardmanager_compatible::open_repo::<BlobRepo>(
            ctx.fb,
            &ctx.logger().clone(),
            matches,
        ),
        resulting_changeset_args.compat(),
    )
    .await?;

    let parent_bcs_id = helpers::csid_resolve(ctx, &repo, move_parent).await?;

    if let Some(max_num_of_moves_in_commit) = max_num_of_moves_in_commit {
        let changesets = perform_stack_move(
            ctx,
            &repo,
            parent_bcs_id,
            mover,
            max_num_of_moves_in_commit,
            |num: StackPosition| {
                let mut args = resulting_changeset_args.clone();
                let message = args.message + &format!(" #{}", num.0);
                args.message = message;
                args
            },
        )
        .await?;
        info!(
            ctx.logger(),
            "created {} commits, with the last commit {:?}",
            changesets.len(),
            changesets.last()
        );
    } else {
        perform_move(ctx, &repo, parent_bcs_id, mover, resulting_changeset_args).await?;
    }
    Ok(())
}

async fn run_merge<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let first_parent = sub_m.value_of(FIRST_PARENT).unwrap().to_owned();
    let second_parent = sub_m.value_of(SECOND_PARENT).unwrap().to_owned();
    let resulting_changeset_args = cs_args_from_matches(sub_m);
    let (repo, resulting_changeset_args) = try_join(
        args::not_shardmanager_compatible::open_repo::<BlobRepo>(
            ctx.fb,
            &ctx.logger().clone(),
            matches,
        ),
        resulting_changeset_args.compat(),
    )
    .await?;

    let first_parent_fut = helpers::csid_resolve(ctx, &repo, first_parent);
    let second_parent_fut = helpers::csid_resolve(ctx, &repo, second_parent);
    let (first_parent, second_parent) = try_join(first_parent_fut, second_parent_fut).await?;

    info!(ctx.logger(), "Creating a merge commit");
    perform_merge(
        ctx.clone(),
        repo.clone(),
        first_parent,
        second_parent,
        resulting_changeset_args,
    )
    .await
    .map(|_| ())
}

async fn run_sync_diamond_merge<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let config_store = matches.config_store();
    let source_repo_id =
        args::not_shardmanager_compatible::get_source_repo_id(config_store, matches)?;
    let target_repo_id =
        args::not_shardmanager_compatible::get_target_repo_id(config_store, matches)?;
    let maybe_bookmark = sub_m
        .value_of(cli::COMMIT_BOOKMARK)
        .map(BookmarkKey::new)
        .transpose()?;

    let bookmark = maybe_bookmark.ok_or_else(|| Error::msg("bookmark must be specified"))?;

    let source_repo = args::open_repo_with_repo_id(ctx.fb, ctx.logger(), source_repo_id, matches);
    let target_repo = args::open_repo_with_repo_id(ctx.fb, ctx.logger(), target_repo_id, matches);
    let mapping = args::not_shardmanager_compatible::open_source_sql::<SqlSyncedCommitMapping>(
        ctx.fb,
        config_store,
        matches,
    )?;

    let merge_commit_hash = sub_m.value_of(COMMIT_HASH).unwrap().to_owned();
    let (source_repo, target_repo): (InnerRepo, InnerRepo) =
        try_join(source_repo, target_repo).await?;

    let source_merge_cs_id =
        helpers::csid_resolve(ctx, &source_repo.blob_repo, merge_commit_hash).await?;

    let config_store = matches.config_store();
    let live_commit_sync_config = CfgrLiveCommitSyncConfig::new(ctx.logger(), config_store)?;

    let caching = matches.caching();
    let x_repo_syncer_lease = create_commit_syncer_lease(ctx.fb, caching)?;

    sync_diamond_merge::do_sync_diamond_merge(
        ctx,
        source_repo,
        target_repo,
        source_merge_cs_id,
        mapping,
        bookmark,
        Arc::new(live_commit_sync_config),
        x_repo_syncer_lease,
    )
    .await
    .map(|_| ())
}

async fn run_pre_merge_delete<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let repo: BlobRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, &ctx.logger().clone(), matches)
            .await?;

    let delete_cs_args_factory = get_delete_commits_cs_args_factory(sub_m)?;

    let chunker = match sub_m.value_of(CHUNKING_HINT_FILE) {
        Some(hint_file) => {
            let hint_str = std::fs::read_to_string(hint_file)?;
            let hint = parse_chunking_hint(hint_str)?;
            path_chunker_from_hint(hint)?
        }
        None => {
            let even_chunk_size: usize = sub_m
                .value_of(EVEN_CHUNK_SIZE)
                .ok_or_else(|| {
                    format_err!(
                        "either {} or {} is required",
                        CHUNKING_HINT_FILE,
                        EVEN_CHUNK_SIZE
                    )
                })?
                .parse::<usize>()?;
            even_chunker_with_max_size(even_chunk_size)?
        }
    };

    let parent_bcs_id = {
        let hash = sub_m.value_of(COMMIT_HASH).unwrap().to_owned();
        helpers::csid_resolve(ctx, &repo, hash).await?
    };

    let base_bcs_id = {
        match sub_m.value_of(BASE_COMMIT_HASH) {
            Some(hash) => {
                let bcs_id = helpers::csid_resolve(ctx, &repo, hash).await?;
                Some(bcs_id)
            }
            None => None,
        }
    };
    let pmd = create_pre_merge_delete(
        ctx,
        &repo,
        parent_bcs_id,
        chunker,
        delete_cs_args_factory,
        base_bcs_id,
    )
    .await?;

    let PreMergeDelete { mut delete_commits } = pmd;

    info!(
        ctx.logger(),
        "Listing deletion commits in top-to-bottom order (first commit is a descendant of the last)"
    );
    delete_commits.reverse();
    for delete_commit in delete_commits {
        println!("{}", delete_commit);
    }

    Ok(())
}

async fn run_history_fixup_delete<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let repo: BlobRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, &ctx.logger().clone(), matches)
            .await?;

    let delete_cs_args_factory = get_delete_commits_cs_args_factory(sub_m)?;

    let even_chunk_size: usize = sub_m
        .value_of(EVEN_CHUNK_SIZE)
        .ok_or_else(|| {
            format_err!(
                "either {} or {} is required",
                CHUNKING_HINT_FILE,
                EVEN_CHUNK_SIZE
            )
        })?
        .parse::<usize>()?;
    let chunker = even_chunker_with_max_size(even_chunk_size)?;

    let fixup_bcs_id = {
        let hash = sub_m.value_of(COMMIT_HASH).unwrap().to_owned();
        helpers::csid_resolve(ctx, repo.clone(), hash).await?
    };

    let correct_bcs_id = {
        let hash = sub_m
            .value_of(COMMIT_HASH_CORRECT_HISTORY)
            .unwrap()
            .to_owned();
        helpers::csid_resolve(ctx, repo.clone(), hash).await?
    };
    let paths_file = sub_m.value_of(PATHS_FILE).unwrap().to_owned();
    let s = read_to_string(&paths_file).await?;
    let paths: Vec<MPath> = s.lines().map(MPath::new).collect::<Result<Vec<MPath>>>()?;
    let hfd = create_history_fixup_deletes(
        ctx,
        &repo,
        fixup_bcs_id,
        chunker,
        delete_cs_args_factory,
        correct_bcs_id,
        paths,
    )
    .await?;

    let HistoryFixupDeletes {
        mut delete_commits_fixup_branch,
        mut delete_commits_correct_branch,
    } = hfd;

    info!(
        ctx.logger(),
        "Listing deletion commits for fixup branch in top-to-bottom order (first commit is a descendant of the last)"
    );
    delete_commits_fixup_branch.reverse();
    for delete_commit in delete_commits_fixup_branch {
        println!("{}", delete_commit);
    }

    info!(
        ctx.logger(),
        "Listing deletion commits for branch with correct history in top-to-bottom order (first commit is a descendant of the last)"
    );
    delete_commits_correct_branch.reverse();
    for delete_commit in delete_commits_correct_branch {
        println!("{}", delete_commit);
    }

    Ok(())
}

async fn run_gradual_delete<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let repo: BlobRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, &ctx.logger().clone(), matches)
            .await?;

    let delete_cs_args_factory = get_delete_commits_cs_args_factory(sub_m)?;

    let chunker: Chunker<MPath> = {
        let even_chunk_size: usize = sub_m
            .value_of(EVEN_CHUNK_SIZE)
            .ok_or_else(|| format_err!("{} is required", EVEN_CHUNK_SIZE))?
            .parse::<usize>()?;
        even_chunker_with_max_size(even_chunk_size)?
    };

    let parent_bcs_id = {
        let hash = sub_m.value_of(COMMIT_HASH).unwrap().to_owned();
        helpers::csid_resolve(ctx, &repo, hash).await?
    };

    let path_prefixes: Vec<_> = sub_m
        .values_of(PATH)
        .unwrap()
        .map(MPath::new)
        .collect::<Result<Vec<_>, Error>>()?;
    info!(
        ctx.logger(),
        "Gathering working copy files under {:?}", path_prefixes
    );
    let paths =
        get_working_copy_paths_by_prefixes(ctx, &repo, parent_bcs_id, path_prefixes).await?;
    info!(ctx.logger(), "{} paths to be deleted", paths.len());

    info!(ctx.logger(), "Starting deletion");
    let delete_commits = delete_files_in_chunks(
        ctx,
        &repo,
        parent_bcs_id,
        paths,
        &chunker,
        &delete_cs_args_factory,
        false, /* skip_last_chunk */
    )
    .await?;
    info!(ctx.logger(), "Deletion finished");
    info!(
        ctx.logger(),
        "Listing commits in an ancestor-descendant order"
    );
    for delete_commit in delete_commits {
        println!("{}", delete_commit);
    }

    Ok(())
}

async fn run_bonsai_merge<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let repo: BlobRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, &ctx.logger().clone(), matches)
            .await?;

    let (p1, p2) = try_join(
        async {
            let p1 = sub_m.value_of(BONSAI_MERGE_P1).unwrap().to_owned();
            helpers::csid_resolve(ctx, &repo, p1).await
        },
        async {
            let p2 = sub_m.value_of(BONSAI_MERGE_P2).unwrap().to_owned();
            helpers::csid_resolve(ctx, &repo, p2).await
        },
    )
    .await?;

    let cs_args = cs_args_from_matches(sub_m).compat().await?;

    let merge_cs_id =
        create_and_save_bonsai(ctx, &repo, vec![p1, p2], Default::default(), cs_args).await?;

    println!("{}", merge_cs_id);

    Ok(())
}

async fn run_gradual_merge<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let config_store = matches.config_store();
    let repo: InnerRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, ctx.logger(), matches).await?;

    let last_deletion_commit = sub_m
        .value_of(LAST_DELETION_COMMIT)
        .ok_or_else(|| format_err!("last deletion commit is not specified"))?;
    let pre_deletion_commit = sub_m
        .value_of(PRE_DELETION_COMMIT)
        .ok_or_else(|| format_err!("pre deletion commit is not specified"))?;
    let bookmark = sub_m
        .value_of(COMMIT_BOOKMARK)
        .ok_or_else(|| format_err!("bookmark where to merge is not specified"))?;
    let dry_run = sub_m.is_present(DRY_RUN);

    let limit = args::get_usize_opt(sub_m, LIMIT);
    let (_, repo_config) =
        args::get_config_by_repoid(config_store, matches, repo.blob_repo.repo_identity().id())?;
    let last_deletion_commit = helpers::csid_resolve(ctx, &repo.blob_repo, last_deletion_commit);
    let pre_deletion_commit = helpers::csid_resolve(ctx, &repo.blob_repo, pre_deletion_commit);

    let (last_deletion_commit, pre_deletion_commit) =
        try_join(last_deletion_commit, pre_deletion_commit).await?;

    let merge_changeset_args_factory = get_gradual_merge_commits_cs_args_factory(sub_m)?;
    let params = gradual_merge::GradualMergeParams {
        pre_deletion_commit,
        last_deletion_commit,
        bookmark_to_merge_into: BookmarkKey::new(bookmark)?,
        merge_changeset_args_factory,
        limit,
        dry_run,
    };
    gradual_merge::gradual_merge(ctx, &repo, &params, &repo_config.pushrebase.flags).await?;

    Ok(())
}

async fn run_gradual_merge_progress<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let repo: InnerRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, ctx.logger(), matches).await?;

    let last_deletion_commit = sub_m
        .value_of(LAST_DELETION_COMMIT)
        .ok_or_else(|| format_err!("last deletion commit is not specified"))?;
    let pre_deletion_commit = sub_m
        .value_of(PRE_DELETION_COMMIT)
        .ok_or_else(|| format_err!("pre deletion commit is not specified"))?;
    let bookmark = sub_m
        .value_of(COMMIT_BOOKMARK)
        .ok_or_else(|| format_err!("bookmark where to merge is not specified"))?;

    let last_deletion_commit = helpers::csid_resolve(ctx, &repo.blob_repo, last_deletion_commit);
    let pre_deletion_commit = helpers::csid_resolve(ctx, &repo.blob_repo, pre_deletion_commit);

    let (last_deletion_commit, pre_deletion_commit) =
        try_join(last_deletion_commit, pre_deletion_commit).await?;

    let (done, total) = gradual_merge::gradual_merge_progress(
        ctx,
        &repo,
        &pre_deletion_commit,
        &last_deletion_commit,
        &BookmarkKey::new(bookmark)?,
    )
    .await?;

    println!("{}/{}", done, total);

    Ok(())
}

async fn run_manual_commit_sync<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let commit_syncer = create_commit_syncer_from_matches::<InnerRepo>(ctx, matches, None).await?;

    let target_repo = commit_syncer.get_target_repo();
    let target_repo_parents = if sub_m.is_present(SELECT_PARENTS_AUTOMATICALLY) {
        None
    } else {
        let target_repo_parents = sub_m.values_of(PARENTS);
        match target_repo_parents {
            Some(target_repo_parents) => Some(
                try_join_all(
                    target_repo_parents
                        .into_iter()
                        .map(|p| helpers::csid_resolve(ctx, target_repo, p)),
                )
                .await?,
            ),
            None => Some(vec![]),
        }
    };

    let source_cs = sub_m
        .value_of(CHANGESET)
        .ok_or_else(|| format_err!("{} not set", CHANGESET))?;
    let source_repo = commit_syncer.get_source_repo();
    let source_cs_id = helpers::csid_resolve(ctx, source_repo, source_cs).await?;

    let mapping_version_name = sub_m
        .value_of(MAPPING_VERSION_NAME)
        .ok_or_else(|| format_err!("mapping-version-name is not specified"))?;

    let target_cs_id = manual_commit_sync::manual_commit_sync(
        ctx,
        &commit_syncer,
        source_cs_id,
        target_repo_parents,
        CommitSyncConfigVersion(mapping_version_name.to_string()),
    )
    .await?;
    info!(ctx.logger(), "target cs id is {:?}", target_cs_id);
    Ok(())
}

async fn run_check_push_redirection_prereqs<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let commit_syncer = create_commit_syncer_from_matches::<CrossRepo>(ctx, matches, None).await?;

    let target_repo = commit_syncer.get_target_repo();
    let source_repo = commit_syncer.get_source_repo();

    info!(
        ctx.logger(),
        "Resolving source chageset in {}",
        source_repo.repo_identity().name()
    );
    let source_cs_id = helpers::csid_resolve(
        ctx,
        source_repo,
        sub_m
            .value_of(SOURCE_CHANGESET)
            .ok_or_else(|| format_err!("{} not set", SOURCE_CHANGESET))?,
    )
    .await?;

    info!(
        ctx.logger(),
        "Resolving target changeset in {}",
        target_repo.repo_identity().name()
    );
    let target_cs_id = helpers::csid_resolve(
        ctx,
        target_repo,
        sub_m
            .value_of(TARGET_CHANGESET)
            .ok_or_else(|| format_err!("{} not set", TARGET_CHANGESET))?,
    )
    .await?;

    let version = CommitSyncConfigVersion(
        sub_m
            .value_of(VERSION)
            .ok_or_else(|| format_err!("{} not set", VERSION))?
            .to_string(),
    );

    info!(
        ctx.logger(),
        "Checking push-redirection prerequisites for {}({})->{}({}), {:?}",
        source_cs_id,
        source_repo.repo_identity().name(),
        target_cs_id,
        target_repo.repo_identity().name(),
        version,
    );

    let config_store = matches.config_store();
    let live_commit_sync_config = CfgrLiveCommitSyncConfig::new(ctx.logger(), config_store)?;
    verify_working_copy_with_version_fast_path(
        ctx,
        &commit_syncer,
        Source(source_cs_id),
        Target(target_cs_id),
        &version,
        Arc::new(live_commit_sync_config),
    )
    .await
}

async fn run_catchup_delete_head<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let repo: BlobRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, &ctx.logger().clone(), matches)
            .await?;

    let head_bookmark = sub_m
        .value_of(HEAD_BOOKMARK)
        .ok_or_else(|| format_err!("{} not set", HEAD_BOOKMARK))?;

    let head_bookmark = BookmarkKey::new(head_bookmark)?;

    let to_merge_cs_id = sub_m
        .value_of(TO_MERGE_CS_ID)
        .ok_or_else(|| format_err!("{} not set", TO_MERGE_CS_ID))?;
    let to_merge_cs_id = helpers::csid_resolve(ctx, &repo, to_merge_cs_id).await?;

    let path_regex = sub_m
        .value_of(PATH_REGEX)
        .ok_or_else(|| format_err!("{} not set", PATH_REGEX))?;
    let path_regex = Regex::new(path_regex)?;

    let deletion_chunk_size = args::get_usize(&sub_m, DELETION_CHUNK_SIZE, 10000);

    let config_store = matches.config_store();
    let cs_args_factory = get_catchup_head_delete_commits_cs_args_factory(sub_m)?;
    let (_, repo_config) = args::not_shardmanager_compatible::get_config(config_store, matches)?;

    let wait_secs = args::get_u64(&sub_m, WAIT_SECS, 0);

    catchup::create_deletion_head_commits(
        ctx,
        &repo,
        head_bookmark,
        to_merge_cs_id,
        path_regex,
        deletion_chunk_size,
        cs_args_factory,
        &repo_config.pushrebase.flags,
        wait_secs,
    )
    .await?;
    Ok(())
}

async fn run_mover<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let commit_syncer = create_commit_syncer_from_matches::<CrossRepo>(ctx, matches, None).await?;
    let version = get_version(sub_m)?;
    let mover = commit_syncer.get_mover_by_version(&version).await?;
    let path = sub_m
        .value_of(PATH)
        .ok_or_else(|| format_err!("{} not set", PATH))?;
    let path = MPath::new(path)?;
    println!("{:?}", mover(&path));
    Ok(())
}

async fn run_catchup_validate<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let repo: BlobRepo =
        args::not_shardmanager_compatible::open_repo(ctx.fb, &ctx.logger().clone(), matches)
            .await?;

    let result_commit = sub_m
        .value_of(COMMIT_HASH)
        .ok_or_else(|| format_err!("{} not set", COMMIT_HASH))?;
    let to_merge_cs_id = sub_m
        .value_of(TO_MERGE_CS_ID)
        .ok_or_else(|| format_err!("{} not set", TO_MERGE_CS_ID))?;
    let result_commit = helpers::csid_resolve(ctx, &repo, result_commit);

    let to_merge_cs_id = helpers::csid_resolve(ctx, &repo, to_merge_cs_id);

    let (result_commit, to_merge_cs_id) = try_join(result_commit, to_merge_cs_id).await?;

    let path_regex = sub_m
        .value_of(PATH_REGEX)
        .ok_or_else(|| format_err!("{} not set", PATH_REGEX))?;
    let path_regex = Regex::new(path_regex)?;

    catchup::validate(ctx, &repo, result_commit, to_merge_cs_id, path_regex).await?;

    Ok(())
}

async fn run_mark_not_synced<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let commit_syncer = create_commit_syncer_from_matches::<CrossRepo>(ctx, matches, None).await?;

    let small_repo = commit_syncer.get_small_repo();
    let large_repo = commit_syncer.get_large_repo();
    let mapping = commit_syncer.get_mapping();

    let mapping_version_name = sub_m
        .value_of(MAPPING_VERSION_NAME)
        .ok_or_else(|| format_err!("{} is supposed to be set", MAPPING_VERSION_NAME))?;
    let mapping_version_name = CommitSyncConfigVersion(mapping_version_name.to_string());
    if !commit_syncer.version_exists(&mapping_version_name).await? {
        return Err(format_err!("{} version is not found", mapping_version_name));
    }

    let overwrite = sub_m.is_present(OVERWRITE);

    let input_file = sub_m
        .value_of(INPUT_FILE)
        .ok_or_else(|| format_err!("input-file is not specified"))?;
    let inputfile = File::open(&input_file)
        .await
        .with_context(|| format!("Failed to open {}", input_file))?;
    let reader = BufReader::new(inputfile);

    let ctx = &ctx;
    let mapping_version_name = &mapping_version_name;
    let s = tokio_stream::wrappers::LinesStream::new(reader.lines())
        .map_err(Error::from)
        .map_ok(move |line| async move {
            let cs_id = helpers::csid_resolve(ctx, large_repo, line).await?;

            let existing_value = mapping
                .get_equivalent_working_copy(
                    ctx,
                    large_repo.repo_identity().id(),
                    cs_id,
                    small_repo.repo_identity().id(),
                )
                .await?;

            if overwrite {
                if let Some(WorkingCopyEquivalence::WorkingCopy(_, _)) = existing_value {
                    return Err(format_err!("unexpected working copy found for {}", cs_id));
                }
            } else if existing_value.is_some() {
                info!(ctx.logger(), "{} already have mapping", cs_id);
                return Ok(1);
            }

            let wc_entry = EquivalentWorkingCopyEntry {
                large_repo_id: large_repo.repo_identity().id(),
                large_bcs_id: cs_id,
                small_repo_id: small_repo.repo_identity().id(),
                small_bcs_id: None,
                version_name: Some(mapping_version_name.clone()),
            };
            let res = if overwrite {
                mapping
                    .overwrite_equivalent_working_copy(ctx, wc_entry)
                    .await?
            } else {
                mapping
                    .insert_equivalent_working_copy(ctx, wc_entry)
                    .await?
            };
            if !res {
                warn!(
                    ctx.logger(),
                    "failed to insert NotSyncedMapping entry for {}", cs_id
                );
            }

            // Processed a single entry
            Ok(1)
        })
        .try_buffer_unordered(100);

    process_stream_and_wait_for_replication(ctx, matches, &commit_syncer, s).await?;

    Ok(())
}

async fn run_backfill_noop_mapping<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let commit_syncer = create_commit_syncer_from_matches::<CrossRepo>(ctx, matches, None).await?;

    let small_repo = commit_syncer.get_small_repo();
    let large_repo = commit_syncer.get_large_repo();

    info!(
        ctx.logger(),
        "small repo: {}, large repo: {}",
        small_repo.repo_identity().name(),
        large_repo.repo_identity().name(),
    );
    let mapping_version_name = sub_m
        .value_of(MAPPING_VERSION_NAME)
        .ok_or_else(|| format_err!("mapping-version-name is not specified"))?;
    let mapping_version_name = CommitSyncConfigVersion(mapping_version_name.to_string());
    if !commit_syncer.version_exists(&mapping_version_name).await? {
        return Err(format_err!("{} version is not found", mapping_version_name));
    }

    let input_file = sub_m
        .value_of(INPUT_FILE)
        .ok_or_else(|| format_err!("input-file is not specified"))?;

    let inputfile = File::open(&input_file)
        .await
        .with_context(|| format!("Failed to open {}", input_file))?;
    let reader = BufReader::new(inputfile);

    let s = tokio_stream::wrappers::LinesStream::new(reader.lines())
        .map_err(Error::from)
        .map_ok({
            borrowed!(ctx, commit_syncer, mapping_version_name);
            move |cs_id| async move {
                let small_cs_id = helpers::csid_resolve(ctx, small_repo, cs_id.clone());

                let large_cs_id = helpers::csid_resolve(ctx, large_repo, cs_id);

                let (small_cs_id, large_cs_id) = try_join(small_cs_id, large_cs_id).await?;

                let entry = SyncedCommitMappingEntry {
                    large_repo_id: large_repo.repo_identity().id(),
                    large_bcs_id: large_cs_id,
                    small_repo_id: small_repo.repo_identity().id(),
                    small_bcs_id: small_cs_id,
                    version_name: Some(mapping_version_name.clone()),
                    source_repo: Some(commit_syncer.get_source_repo_type()),
                };
                Ok(entry)
            }
        })
        .try_buffer_unordered(100)
        .chunks(100)
        .then({
            borrowed!(commit_syncer, ctx);
            move |chunk| async move {
                let mapping = &commit_syncer.mapping;
                let chunk: Result<Vec<_>, Error> = chunk.into_iter().collect();
                let chunk = chunk?;
                let len = chunk.len();
                mapping.add_bulk(ctx, chunk).await?;
                Result::<_, Error>::Ok(len as u64)
            }
        })
        .boxed();

    process_stream_and_wait_for_replication(ctx, matches, &commit_syncer, s).await?;

    Ok(())
}

async fn run_diff_mapping_versions<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let config_store = matches.config_store();
    let source_repo_id =
        args::not_shardmanager_compatible::get_source_repo_id(config_store, matches)?;
    let target_repo_id =
        args::not_shardmanager_compatible::get_target_repo_id(config_store, matches)?;

    let mapping_version_names = sub_m
        .values_of(MAPPING_VERSION_NAME)
        .ok_or_else(|| format_err!("{} is supposed to be set", MAPPING_VERSION_NAME))?;

    let config_store = matches.config_store();
    let live_commit_sync_config = CfgrLiveCommitSyncConfig::new(ctx.logger(), config_store)?;

    let mut commit_sync_configs = vec![];
    for version in mapping_version_names {
        let version = CommitSyncConfigVersion(version.to_string());
        let config = live_commit_sync_config
            .get_commit_sync_config_by_version(target_repo_id, &version)
            .await?;
        commit_sync_configs.push(config);
    }

    if commit_sync_configs.len() != 2 {
        return Err(format_err!(
            "{} should have exactly 2 values",
            MAPPING_VERSION_NAME
        ));
    }

    // Validate that both versions related to the same config.
    let from = commit_sync_configs.remove(0);
    let to = commit_sync_configs.remove(0);
    if from.large_repo_id != to.large_repo_id {
        return Err(format_err!(
            "different large repo ids: {} vs {}",
            from.large_repo_id,
            to.large_repo_id
        ));
    }

    let small_repo_id = if from.large_repo_id == target_repo_id {
        source_repo_id
    } else {
        target_repo_id
    };

    if !from.small_repos.contains_key(&small_repo_id) {
        return Err(format_err!(
            "{} doesn't have small repo id {}",
            from.version_name,
            small_repo_id,
        ));
    }

    if !to.small_repos.contains_key(&small_repo_id) {
        return Err(format_err!(
            "{} doesn't have small repo id {}",
            to.version_name,
            small_repo_id,
        ));
    }

    let from_small_commit_sync_config = from
        .small_repos
        .get(&small_repo_id)
        .cloned()
        .ok_or_else(|| format_err!("{} not found in {}", small_repo_id, from.version_name))?;
    let to_small_commit_sync_config = to
        .small_repos
        .get(&small_repo_id)
        .cloned()
        .ok_or_else(|| format_err!("{} not found in {}", small_repo_id, to.version_name))?;

    let diff = diff_small_repo_commit_sync_configs(
        from_small_commit_sync_config,
        to_small_commit_sync_config,
    );

    if let Some((from, to)) = diff.default_action_change {
        println!("default action change: {:?} to {:?}", from, to);
    }

    let mut mapping_added = diff.mapping_added.into_iter().collect::<Vec<_>>();
    mapping_added.sort();
    for (path_from, path_to) in mapping_added {
        println!("mapping added: {} => {}", path_from, path_to);
    }

    let mut mapping_changed = diff.mapping_changed.into_iter().collect::<Vec<_>>();
    mapping_changed.sort();
    for (path_from, (before, after)) in mapping_changed {
        println!("mapping changed: {} => {} vs {}", path_from, before, after);
    }

    let mut mapping_removed = diff.mapping_removed.into_iter().collect::<Vec<_>>();
    mapping_removed.sort();
    for (path_from, path_to) in mapping_removed {
        println!("mapping removed: {} => {}", path_from, path_to);
    }

    Ok(())
}

async fn process_stream_and_wait_for_replication<'a, R: cross_repo_sync::Repo>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    commit_syncer: &CommitSyncer<SqlSyncedCommitMapping, R>,
    mut s: impl Stream<Item = Result<u64>> + std::marker::Unpin,
) -> Result<(), Error> {
    let config_store = matches.config_store();
    let small_repo = commit_syncer.get_small_repo();
    let large_repo = commit_syncer.get_large_repo();

    let (_, small_repo_config) =
        args::get_config_by_repoid(config_store, matches, small_repo.repo_identity().id())?;
    let (_, large_repo_config) =
        args::get_config_by_repoid(config_store, matches, large_repo.repo_identity().id())?;
    if small_repo_config.storage_config.metadata != large_repo_config.storage_config.metadata {
        return Err(format_err!(
            "{} and {} have different db metadata configs: {:?} vs {:?}",
            small_repo.repo_identity().name(),
            large_repo.repo_identity().name(),
            small_repo_config.storage_config.metadata,
            large_repo_config.storage_config.metadata,
        ));
    }
    let storage_config = small_repo_config.storage_config;

    let db_address = match &storage_config.metadata {
        MetadataDatabaseConfig::Local(_) => None,
        MetadataDatabaseConfig::Remote(remote_config) => {
            Some(remote_config.primary.db_address.clone())
        }
    };

    let wait_config = WaitForReplicationConfig::default().with_logger(ctx.logger());
    let replica_lag_monitor: Arc<dyn ReplicaLagMonitor> = match db_address {
        None => Arc::new(NoReplicaLagMonitor()),
        Some(address) => {
            #[cfg(fbcode_build)]
            {
                let my_admin = MyAdmin::new(ctx.fb).context("building myadmin client")?;
                Arc::new(my_admin.single_shard_lag_monitor(address))
            }
            #[cfg(not(fbcode_build))]
            {
                let _address = address;
                Arc::new(NoReplicaLagMonitor())
            }
        }
    };

    let mut total = 0;
    let mut batch = 0;
    while let Some(chunk_size) = s.try_next().await? {
        total += chunk_size;

        batch += chunk_size;
        if batch < 100 {
            continue;
        }
        info!(ctx.logger(), "processed {} changesets", total);
        batch %= 100;
        replica_lag_monitor
            .wait_for_replication(&|| wait_config.clone())
            .await?;
    }

    Ok(())
}

async fn run_sync_commit_and_ancestors<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let commit_syncer =
        create_commit_syncer_from_matches::<cross_repo_sync::ConcreteRepo>(ctx, matches, None)
            .await?;

    let source_commit_hash = sub_m
        .value_of(COMMIT_HASH)
        .ok_or_else(|| format_err!("{} not specified", COMMIT_HASH))?;

    let source_cs_id =
        helpers::csid_resolve(ctx, commit_syncer.get_source_repo(), source_commit_hash).await?;

    let (unsynced_ancestors, _) =
        find_toposorted_unsynced_ancestors(ctx, &commit_syncer, source_cs_id).await?;

    for ancestor in unsynced_ancestors {
        commit_syncer
            .unsafe_sync_commit(
                ctx,
                ancestor,
                CandidateSelectionHint::Only,
                CommitSyncContext::AdminChangeMapping,
            )
            .await?;
    }

    let commit_sync_outcome = commit_syncer
        .get_commit_sync_outcome(ctx, source_cs_id)
        .await?
        .ok_or_else(|| format_err!("was not able to remap a commit {}", source_cs_id))?;
    info!(ctx.logger(), "remapped to {:?}", commit_sync_outcome);

    Ok(())
}

fn get_version(matches: &ArgMatches<'_>) -> Result<CommitSyncConfigVersion> {
    Ok(CommitSyncConfigVersion(
        matches
            .value_of(VERSION)
            .ok_or_else(|| format_err!("{} not set", VERSION))?
            .to_string(),
    ))
}

async fn run_delete_no_longer_bound_files_from_large_repo<'a>(
    ctx: &CoreContext,
    matches: &MononokeMatches<'a>,
    sub_m: &ArgMatches<'a>,
) -> Result<(), Error> {
    let commit_syncer = create_commit_syncer_from_matches::<CrossRepo>(ctx, matches, None).await?;
    let large_repo = commit_syncer.get_large_repo();
    if commit_syncer.get_source_repo().repo_identity().id() != large_repo.repo_identity().id() {
        return Err(format_err!("source repo must be large!"));
    }

    let cs_id = sub_m
        .value_of(COMMIT_HASH)
        .ok_or_else(|| format_err!("{} not specified", COMMIT_HASH))?;
    let cs_id = helpers::csid_resolve(ctx, commit_syncer.get_source_repo(), cs_id).await?;

    // Find all files under a given path
    let prefix = sub_m.value_of(PATH_PREFIX).context("prefix is not set")?;
    let root_fsnode_id = RootFsnodeId::derive(ctx, large_repo, cs_id).await?;
    let entries = root_fsnode_id
        .fsnode_id()
        .find_entries(
            ctx.clone(),
            large_repo.repo_blobstore().clone(),
            vec![PathOrPrefix::Prefix(Some(MPath::new(prefix)?))],
        )
        .try_collect::<Vec<_>>()
        .await?;

    // Now find which files does not remap to a small repo - these files we want to delete
    let mover = find_mover_for_commit(ctx, &commit_syncer, cs_id).await?;

    let mut to_delete = vec![];
    for (path, entry) in entries {
        if let Entry::Leaf(_) = entry {
            let path = path.unwrap();
            if mover(&path)?.is_none() {
                to_delete.push(path);
            }
        }
    }

    if to_delete.is_empty() {
        info!(ctx.logger(), "nothing to delete, exiting");
        return Ok(());
    }
    info!(ctx.logger(), "need to delete {} paths", to_delete.len());

    let resulting_changeset_args = cs_args_from_matches(sub_m).compat().await?;
    let deletion_cs_id = create_and_save_bonsai(
        ctx,
        large_repo,
        vec![cs_id],
        to_delete
            .into_iter()
            .map(|file| (file, FileChange::Deletion))
            .collect(),
        resulting_changeset_args,
    )
    .await?;

    info!(ctx.logger(), "created changeset {}", deletion_cs_id);

    Ok(())
}

async fn find_mover_for_commit<R: cross_repo_sync::Repo>(
    ctx: &CoreContext,
    commit_syncer: &CommitSyncer<SqlSyncedCommitMapping, R>,
    cs_id: ChangesetId,
) -> Result<Mover, Error> {
    let maybe_sync_outcome = commit_syncer.get_commit_sync_outcome(ctx, cs_id).await?;

    let sync_outcome = maybe_sync_outcome.context("source commit was not remapped yet")?;
    use CommitSyncOutcome::*;
    let mover = match sync_outcome {
        NotSyncCandidate(_) => {
            return Err(format_err!(
                "commit is a not sync candidate, can't get a mover for this commit"
            ));
        }
        RewrittenAs(_, version) | EquivalentWorkingCopyAncestor(_, version) => {
            commit_syncer.get_mover_by_version(&version).await?
        }
    };

    Ok(mover)
}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
    let app = setup_app();
    let (matches, _runtime) = app.get_matches(fb)?;
    let logger = matches.logger();
    let config_store = matches.config_store();
    let ctx = CoreContext::new_with_logger(fb, logger.clone());
    let ctx = &ctx;

    let subcommand_future = async {
        match matches.subcommand() {
            (BACKFILL_NOOP_MAPPING, Some(sub_m)) => {
                run_backfill_noop_mapping(ctx, &matches, sub_m).await
            }
            (BONSAI_MERGE, Some(sub_m)) => run_bonsai_merge(ctx, &matches, sub_m).await,
            (CHECK_PUSH_REDIRECTION_PREREQS, Some(sub_m)) => {
                run_check_push_redirection_prereqs(ctx, &matches, sub_m).await
            }
            (DIFF_MAPPING_VERSIONS, Some(sub_m)) => {
                run_diff_mapping_versions(ctx, &matches, sub_m).await
            }
            (MANUAL_COMMIT_SYNC, Some(sub_m)) => run_manual_commit_sync(ctx, &matches, sub_m).await,
            (MARK_NOT_SYNCED_COMMAND, Some(sub_m)) => {
                run_mark_not_synced(ctx, &matches, sub_m).await
            }
            (MERGE, Some(sub_m)) => run_merge(ctx, &matches, sub_m).await,
            (MOVE, Some(sub_m)) => {
                let live_commit_sync_config =
                    CfgrLiveCommitSyncConfig::new(ctx.logger(), config_store)?;
                run_move(ctx, &matches, sub_m, live_commit_sync_config).await
            }
            (RUN_MOVER, Some(sub_m)) => run_mover(ctx, &matches, sub_m).await,
            (SYNC_COMMIT_AND_ANCESTORS, Some(sub_m)) => {
                run_sync_commit_and_ancestors(ctx, &matches, sub_m).await
            }
            (SYNC_DIAMOND_MERGE, Some(sub_m)) => run_sync_diamond_merge(ctx, &matches, sub_m).await,

            // All commands relevant to gradual merge
            (CATCHUP_DELETE_HEAD, Some(sub_m)) => {
                run_catchup_delete_head(ctx, &matches, sub_m).await
            }
            (CATCHUP_VALIDATE_COMMAND, Some(sub_m)) => {
                run_catchup_validate(ctx, &matches, sub_m).await
            }
            (GRADUAL_DELETE, Some(sub_m)) => run_gradual_delete(ctx, &matches, sub_m).await,
            (GRADUAL_MERGE, Some(sub_m)) => run_gradual_merge(ctx, &matches, sub_m).await,
            (GRADUAL_MERGE_PROGRESS, Some(sub_m)) => {
                run_gradual_merge_progress(ctx, &matches, sub_m).await
            }
            (PRE_MERGE_DELETE, Some(sub_m)) => run_pre_merge_delete(ctx, &matches, sub_m).await,
            (HISTORY_FIXUP_DELETE, Some(sub_m)) => {
                run_history_fixup_delete(ctx, &matches, sub_m).await
            }
            (DELETE_NO_LONGER_BOUND_FILES_FROM_LARGE_REPO, Some(sub_m)) => {
                run_delete_no_longer_bound_files_from_large_repo(ctx, &matches, sub_m).await
            }
            _ => bail!("oh no, wrong arguments provided!"),
        }
    };

    helpers::block_execute(
        subcommand_future,
        fb,
        "megarepotool",
        logger,
        &matches,
        cmdlib::monitoring::AliveService,
    )
}


/*
 * 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::Error;
use blobstore::Loadable;
use context::CoreContext;
use cross_repo_sync::CommitSyncContext;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync::Repo as CrossRepo;
use metaconfig_types::CommitSyncConfigVersion;
use mononoke_types::ChangesetId;
use synced_commit_mapping::SyncedCommitMapping;

/// This operation is useful immediately after a small repo is merged into a large repo.
/// See example below
///
/// ```text
///   B' <- manually synced commit from small repo (in small repo it is commit B)
///   |
///   BM <- "big merge"
///  /  \
/// ...  O <- big move commit i.e. commit that moves small repo files in correct location
///      |
///      A <- commit that was copied from small repo. It is identical between small and large repos.
///
/// Immediately after a small repo is merged into a large one we need to tell that a commit B and all of
/// its ancestors from small repo needs to be based on top of "big merge" commit in large repo rather than on top of
/// commit A.
/// The function below can be used to achieve exactly that.
/// ```
pub async fn manual_commit_sync<M: SyncedCommitMapping + Clone + 'static, R: CrossRepo>(
    ctx: &CoreContext,
    commit_syncer: &CommitSyncer<M, R>,
    source_cs_id: ChangesetId,
    target_repo_parents: Option<Vec<ChangesetId>>,
    mapping_version: CommitSyncConfigVersion,
) -> Result<Option<ChangesetId>, Error> {
    if let Some(target_repo_parents) = target_repo_parents {
        let source_repo = commit_syncer.get_source_repo();
        let source_cs = source_cs_id.load(ctx, source_repo.repo_blobstore()).await?;
        let source_parents: Vec<_> = source_cs.parents().collect();
        if source_parents.len() != target_repo_parents.len() {
            return Err(anyhow!(
                "wrong number of parents: source repo has {} parents, while {} target repo parents specified",
                source_parents.len(),
                target_repo_parents.len(),
            ));
        }

        let remapped_parents = source_parents
            .into_iter()
            .zip(target_repo_parents.into_iter())
            .collect::<HashMap<_, _>>();

        commit_syncer
            .unsafe_always_rewrite_sync_commit(
                ctx,
                source_cs_id,
                Some(remapped_parents),
                &mapping_version,
                CommitSyncContext::ManualCommitSync,
            )
            .await
    } else {
        commit_syncer
            .unsafe_always_rewrite_sync_commit(
                ctx,
                source_cs_id,
                None,
                &mapping_version,
                CommitSyncContext::ManualCommitSync,
            )
            .await
    }
}

#[cfg(test)]
mod test {
    use cross_repo_sync_test_utils::init_small_large_repo;
    use cross_repo_sync_test_utils::xrepo_mapping_version_with_small_repo;
    use fbinit::FacebookInit;
    use maplit::hashmap;
    use mononoke_types::MPath;
    use tests_utils::list_working_copy_utf8;
    use tests_utils::resolve_cs_id;
    use tests_utils::CreateCommitContext;

    use super::*;

    #[fbinit::test]
    async fn test_manual_commit_sync(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);

        // Small and large repo look like that
        //
        // Small repo:
        //
        // O <- file3: "content3"
        // |
        // O <- file2: "content" <- "premove" bookmark, "megarepo_start" bookmark
        // |
        // O <- file: "content"
        //
        // Large repo
        // O <- file3: "content3"
        // |
        // O <- moves file -> prefix/file, file2 -> prefix/file2, "megarepo_start" bookmark
        // |
        // O <- file2: "content" <- "premove" bookmark
        // |
        // O <- file: "content"

        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let small_to_large = syncers.small_to_large;
        let small_repo = small_to_large.get_source_repo();
        let large_repo = small_to_large.get_target_repo();

        // Create a commit on top of "premove" bookmark in a small repo, and then
        // manually sync it on top of big move bookmark.
        let premove_cs_id = resolve_cs_id(&ctx, &small_repo, "premove").await?;
        let commit_to_sync = CreateCommitContext::new(&ctx, &small_repo, vec![premove_cs_id])
            .add_file("some_other_file", "some_content")
            .commit()
            .await?;

        let bigmove = resolve_cs_id(&ctx, &large_repo, "megarepo_start").await?;

        let maybe_synced_commit = manual_commit_sync(
            &ctx,
            &small_to_large,
            commit_to_sync,
            Some(vec![bigmove]),
            xrepo_mapping_version_with_small_repo(),
        )
        .await?;

        let synced_commit = maybe_synced_commit.ok_or_else(|| anyhow!("commit was not synced"))?;
        let wc = list_working_copy_utf8(&ctx, &large_repo, synced_commit).await?;

        assert_eq!(
            hashmap! {
                MPath::new("prefix/file")? => "content".to_string(),
                MPath::new("prefix/file2")? => "content".to_string(),
                MPath::new("prefix/some_other_file")? => "some_content".to_string(),
            },
            wc
        );
        Ok(())
    }

    #[fbinit::test]
    async fn test_manual_commit_sync_select_parents_automatically(
        fb: FacebookInit,
    ) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);

        // Small and large repo look like that
        //
        // Small repo:
        //
        // O <- file3: "content3"
        // |
        // O <- file2: "content" <- "premove" bookmark, "megarepo_start" bookmark
        // |
        // O <- file: "content"
        //
        // Large repo
        // O <- file3: "content3"
        // |
        // O <- moves file -> prefix/file, file2 -> prefix/file2, "megarepo_start" bookmark
        // |
        // O <- file2: "content" <- "premove" bookmark
        // |
        // O <- file: "content"

        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let large_to_small = syncers.large_to_small;
        let large_repo = large_to_small.get_source_repo();
        let small_repo = large_to_small.get_target_repo();

        // Create a commit on top of "master" bookmark in a large repo, and then
        // manually sync it on top of megarepo_start bookmark.
        let megarepo_start_cs_id = resolve_cs_id(&ctx, &large_repo, "master").await?;
        let commit_to_sync =
            CreateCommitContext::new(&ctx, &large_repo, vec![megarepo_start_cs_id])
                .add_file("prefix/some_other_file", "some_content")
                .commit()
                .await?;

        let maybe_synced_commit = manual_commit_sync(
            &ctx,
            &large_to_small,
            commit_to_sync,
            None,
            xrepo_mapping_version_with_small_repo(),
        )
        .await?;

        let synced_cs_id = maybe_synced_commit.ok_or_else(|| anyhow!("commit was not synced"))?;
        // Check that parents were correctly selected

        let small_repo_master = resolve_cs_id(&ctx, &small_repo, "master").await?;
        let synced_cs = synced_cs_id
            .load(&ctx, &small_repo.repo_blobstore().clone())
            .await?;
        assert_eq!(
            synced_cs.parents().collect::<Vec<_>>(),
            vec![small_repo_master]
        );

        // Check working copy
        let wc = list_working_copy_utf8(&ctx, &small_repo, synced_cs_id).await?;

        assert_eq!(
            hashmap! {
                MPath::new("file")? => "content".to_string(),
                MPath::new("file2")? => "content".to_string(),
                MPath::new("file3")? => "content3".to_string(),
                MPath::new("some_other_file")? => "some_content".to_string(),
            },
            wc
        );
        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::format_err;
use anyhow::Error;
use blobrepo::BlobRepo;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use cloned::cloned;
use context::CoreContext;
use futures::try_join;
use megarepolib::common::create_save_and_generate_hg_changeset;
use megarepolib::common::ChangesetArgs;
use megarepolib::working_copy::get_colliding_paths_between_commits;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::HgChangesetId;
use mononoke_types::ChangesetId;
use slog::info;

async fn fail_on_path_conflicts(
    ctx: &CoreContext,
    repo: &BlobRepo,
    hg_cs_id_1: HgChangesetId,
    hg_cs_id_2: HgChangesetId,
) -> Result<(), Error> {
    info!(ctx.logger(), "Checking if there are any path conflicts");
    let (bcs_1, bcs_2) = try_join!(
        repo.bonsai_hg_mapping().get_bonsai_from_hg(ctx, hg_cs_id_1),
        repo.bonsai_hg_mapping().get_bonsai_from_hg(ctx, hg_cs_id_2)
    )?;
    let collisions =
        get_colliding_paths_between_commits(ctx, repo, bcs_1.unwrap(), bcs_2.unwrap()).await?;
    if !collisions.is_empty() {
        Err(format_err!(
            "There are paths present in both parents: {:?} ...",
            collisions.iter().take(10).collect::<Vec<_>>(),
        ))
    } else {
        info!(ctx.logger(), "Done checking path conflicts");
        Ok(())
    }
}

pub async fn perform_merge(
    ctx: CoreContext,
    repo: BlobRepo,
    first_bcs_id: ChangesetId,
    second_bcs_id: ChangesetId,
    resulting_changeset_args: ChangesetArgs,
) -> Result<HgChangesetId, Error> {
    cloned!(ctx, repo);
    let (first_hg_cs_id, second_hg_cs_id) = try_join!(
        repo.derive_hg_changeset(&ctx, first_bcs_id.clone()),
        repo.derive_hg_changeset(&ctx, second_bcs_id.clone()),
    )?;
    fail_on_path_conflicts(&ctx, &repo, first_hg_cs_id, second_hg_cs_id).await?;
    info!(
        ctx.logger(),
        "Creating a merge bonsai changeset with parents: {:?}, {:?}", &first_bcs_id, &second_bcs_id
    );
    create_save_and_generate_hg_changeset(
        &ctx,
        &repo,
        vec![first_bcs_id, second_bcs_id],
        Default::default(),
        resulting_changeset_args,
    )
    .await
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use fbinit::FacebookInit;
    use fixtures::MergeEven;
    use fixtures::TestRepoFixture;

    use super::*;

    #[fbinit::test]
    async fn test_path_conflict_detection(fb: FacebookInit) {
        let repo = MergeEven::getrepo(fb).await;
        let ctx = CoreContext::test_mock(fb);
        let p1 = HgChangesetId::from_str("4f7f3fd428bec1a48f9314414b063c706d9c1aed").unwrap();
        let p2 = HgChangesetId::from_str("16839021e338500b3cf7c9b871c8a07351697d68").unwrap();
        assert!(
            fail_on_path_conflicts(&ctx, &repo, p1, p2).await.is_err(),
            "path conflicts should've been detected"
        );
    }
}


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

//! This is a very hacky temporary tool that's used with only one purpose -
//! to half-manually sync a diamond merge commit from a small repo into a large repo.
//! NOTE - this is not a production quality tool, but rather a best effort attempt to
//! half-automate a rare case that might occur. Tool most likely doesn't cover all the cases.
//! USE WITH CARE!

use std::collections::HashMap;
use std::sync::Arc;

use anyhow::format_err;
use anyhow::Context;
use anyhow::Error;
use blobrepo::AsBlobRepo;
use blobrepo::BlobRepo;
use blobrepo_utils::convert_diff_result_into_file_change_for_diamond_merge;
use blobstore::Loadable;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use cacheblob::LeaseOps;
use changeset_fetcher::ChangesetFetcherRef;
use cloned::cloned;
use commit_graph::CommitGraphRef;
use commit_transformation::upload_commits;
use context::CoreContext;
use cross_repo_sync::create_commit_syncers;
use cross_repo_sync::rewrite_commit;
use cross_repo_sync::update_mapping_with_version;
use cross_repo_sync::CandidateSelectionHint;
use cross_repo_sync::CommitSyncContext;
use cross_repo_sync::CommitSyncOutcome;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync::Syncers;
use futures::compat::Future01CompatExt;
use futures::future::FutureExt;
use futures::future::TryFutureExt;
use futures::stream::futures_unordered::FuturesUnordered;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures_ext::BoxStream;
use futures_ext::StreamExt as _;
use futures_old::Future;
use futures_old::IntoFuture;
use futures_old::Stream;
use live_commit_sync_config::LiveCommitSyncConfig;
use manifest::bonsai_diff;
use manifest::BonsaiDiffFileChange;
use maplit::hashmap;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::HgFileNodeId;
use mercurial_types::HgManifestId;
use metaconfig_types::CommitSyncConfigVersion;
use mononoke_api_types::InnerRepo;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use mononoke_types::FileChange;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreRef;
use slog::info;
use slog::warn;
use sorted_vector_map::SortedVectorMap;
use synced_commit_mapping::SqlSyncedCommitMapping;

/// The function syncs merge commit M from a small repo into a large repo.
/// It's designed to handle a case described below
///
/// ```text
///   Small repo state
///       M
///     |   \
///     P1  |   <- P1 must already be synced
///     |   |
///     |   P2 <- might not be synced yet
///    ...  |
///     |   /
///     |  /
///      ROOT
///
///   Large repo state
///
///     O   <- ONTO value (i.e. where onto_bookmark points to)
///    ...  <- commits from another small repo
///     |
///     P1' <- synced P1 commit from small repo
///     |
///     OVR' <- Potentially there can be commits from another repo between root and P1!
///      |
///     ROOT' <- synced ROOT commit
///
///
/// Most of the complexity stems from two facts
/// 1) If parents have different file content, then merge commit must have a file change entry for them
/// 2) that large repo might have rewritten commits from another small repo between ROOT' and P1'.
///
/// That means that rewritten M' bonsai object must contain file change entries that were changed
/// in OVR* commits.
///
/// So the function works as follows:
/// 1) Syncs all ROOT::P2 commits - nothing difficult here, just rewrite and save to large repo.
///    Those commits are expected to be non-merges for simplicity
/// 2) Create new merge commit
///    a) First find all the changed files from another small repo - those need to be in the merge repo
///       NOTE - we expect that all changes from this small repo are already in the bonsai changeset
///    b) Add file changes from previous step in the merge commit
///    c) Change parents
/// 3) Save merge commit in large repo
/// 4) Update the bookmark
/// ```
pub async fn do_sync_diamond_merge(
    ctx: &CoreContext,
    small_repo: InnerRepo,
    large_repo: InnerRepo,
    small_merge_cs_id: ChangesetId,
    mapping: SqlSyncedCommitMapping,
    onto_bookmark: BookmarkKey,
    live_commit_sync_config: Arc<dyn LiveCommitSyncConfig>,
    lease: Arc<dyn LeaseOps>,
) -> Result<(), Error> {
    info!(
        ctx.logger(),
        "Preparing to sync a merge commit {}...", small_merge_cs_id
    );

    let parents = small_repo
        .blob_repo
        .changeset_fetcher()
        .get_parents(ctx, small_merge_cs_id)
        .await?;

    let (p1, p2) = validate_parents(parents)?;

    let new_branch = find_new_branch_oldest_first(ctx.clone(), &small_repo, p1, p2).await?;

    let syncers = create_commit_syncers(
        ctx,
        small_repo.clone(),
        large_repo.clone(),
        mapping,
        live_commit_sync_config,
        lease,
    )?;

    let small_root = find_root(&new_branch)?;

    info!(
        ctx.logger(),
        "{} new commits are going to be merged in",
        new_branch.len()
    );
    for bcs in new_branch {
        let cs_id = bcs.get_changeset_id();
        let parents = bcs.parents().collect::<Vec<_>>();
        if parents.len() > 1 {
            return Err(format_err!(
                "{} from branch contains more than one parent",
                cs_id
            ));
        }
        info!(ctx.logger(), "syncing commit from new branch {}", cs_id);
        // It is unclear if we can do something better than use an `Only`
        // hint here. Current thinking is: let the sync fail if one of
        // the `new_branch` commits rewrites into 2 commits in the target
        // repo. Manual remediation would be needed in that case.
        syncers
            .small_to_large
            .unsafe_sync_commit(
                ctx,
                cs_id,
                CandidateSelectionHint::Only,
                CommitSyncContext::SyncDiamondMerge,
            )
            .await?;
    }

    let maybe_onto_value = large_repo
        .bookmarks()
        .get(ctx.clone(), &onto_bookmark)
        .await?;

    let onto_value =
        maybe_onto_value.ok_or_else(|| format_err!("cannot find bookmark {}", onto_bookmark))?;

    let (rewritten, version_for_merge) = create_rewritten_merge_commit(
        ctx.clone(),
        small_merge_cs_id,
        &small_repo,
        &large_repo,
        &syncers,
        small_root,
        onto_value,
    )
    .await?;

    let new_merge_cs_id = rewritten.get_changeset_id();
    info!(ctx.logger(), "uploading merge commit {}", new_merge_cs_id);
    upload_commits(ctx, vec![rewritten], &small_repo.blob_repo, &large_repo).await?;

    update_mapping_with_version(
        ctx,
        hashmap! {small_merge_cs_id => new_merge_cs_id},
        &syncers.small_to_large,
        &version_for_merge,
    )
    .await?;

    let mut book_txn = large_repo.bookmarks().create_transaction(ctx.clone());
    book_txn.force_set(
        &onto_bookmark,
        new_merge_cs_id,
        BookmarkUpdateReason::ManualMove,
    )?;
    book_txn.commit().await?;

    warn!(
        ctx.logger(),
        "It is recommended to run 'mononoke_admin crossrepo verify-wc' for {}!", new_merge_cs_id
    );
    Ok(())
}

async fn create_rewritten_merge_commit(
    ctx: CoreContext,
    small_merge_cs_id: ChangesetId,
    small_repo: &InnerRepo,
    large_repo: &InnerRepo,
    syncers: &Syncers<SqlSyncedCommitMapping, InnerRepo>,
    small_root: ChangesetId,
    onto_value: ChangesetId,
) -> Result<(BonsaiChangeset, CommitSyncConfigVersion), Error> {
    let merge_bcs = small_merge_cs_id
        .load(&ctx, small_repo.repo_blobstore())
        .await?;

    let parents = merge_bcs.parents().collect();
    let (p1, p2) = validate_parents(parents)?;

    let merge_bcs = merge_bcs.into_mut();

    // For simplicity sake allow doing the diamond merge only if there were
    // no changes in the mapping versions.
    let (large_root, root_version) = remap_commit(ctx.clone(), &syncers.small_to_large, small_root)
        .await
        .context("error remapping small root commit")?;

    let (_, version_p1) = remap_commit(ctx.clone(), &syncers.small_to_large, p1)
        .await
        .context("error remapping small p1 commit")?;
    let (remapped_p2, version_p2) = remap_commit(ctx.clone(), &syncers.small_to_large, p2)
        .await
        .context("error remapping small p2 commit")?;

    if version_p1 != version_p2 {
        return Err(format_err!(
            "Parents are remapped with different commit sync config versions: {} vs {}",
            version_p1,
            version_p2
        ));
    }

    if root_version != version_p1 {
        return Err(format_err!(
            "Commit sync version of root commit is different from p1 version: {} vs {}",
            root_version,
            version_p1,
        ));
    }

    let remapped_parents = hashmap! {
        p1 => onto_value,
        p2 => remapped_p2,
    };
    let maybe_rewritten = rewrite_commit(
        &ctx,
        merge_bcs,
        &remapped_parents,
        syncers
            .small_to_large
            .get_mover_by_version(&version_p1)
            .await?,
        syncers.small_to_large.get_source_repo(),
        Default::default(),
    )
    .await?;
    let mut rewritten =
        maybe_rewritten.ok_or_else(|| Error::msg("merge commit was unexpectedly rewritten out"))?;

    let mut additional_file_changes = generate_additional_file_changes(
        ctx.clone(),
        large_root,
        large_repo,
        &syncers.large_to_small,
        onto_value,
        &root_version,
    )
    .await?;

    for (path, fc) in rewritten.file_changes {
        additional_file_changes.insert(path, fc);
    }
    rewritten.file_changes = additional_file_changes;
    let cs_id = rewritten.freeze()?;
    Ok((cs_id, version_p1))
}

/// This function finds all the changed file between root and onto that are from another small repo.
/// These files needed to be added to the new merge commit to preserve bonsai semantic.
async fn generate_additional_file_changes(
    ctx: CoreContext,
    root: ChangesetId,
    large_repo: &(impl AsBlobRepo + RepoBlobstoreRef),
    large_to_small: &CommitSyncer<SqlSyncedCommitMapping, InnerRepo>,
    onto_value: ChangesetId,
    version: &CommitSyncConfigVersion,
) -> Result<SortedVectorMap<MPath, FileChange>, Error> {
    let bonsai_diff = find_bonsai_diff(ctx.clone(), large_repo.as_blob_repo(), root, onto_value)
        .collect()
        .compat()
        .await?;

    let additional_file_changes = FuturesUnordered::new();
    for diff_res in bonsai_diff {
        match diff_res {
            BonsaiDiffFileChange::Changed(ref path, ..)
            | BonsaiDiffFileChange::ChangedReusedId(ref path, ..)
            | BonsaiDiffFileChange::Deleted(ref path) => {
                let maybe_new_path = large_to_small.get_mover_by_version(version).await?(path)?;
                if maybe_new_path.is_some() {
                    continue;
                }
            }
        }

        let fc = convert_diff_result_into_file_change_for_diamond_merge(&ctx, large_repo, diff_res);
        additional_file_changes.push(fc);
    }

    additional_file_changes
        .try_collect::<SortedVectorMap<_, _>>()
        .await
}

async fn remap_commit(
    ctx: CoreContext,
    small_to_large_commit_syncer: &CommitSyncer<SqlSyncedCommitMapping, InnerRepo>,
    cs_id: ChangesetId,
) -> Result<(ChangesetId, CommitSyncConfigVersion), Error> {
    let maybe_sync_outcome = small_to_large_commit_syncer
        .get_commit_sync_outcome(&ctx, cs_id)
        .await?;

    let sync_outcome = maybe_sync_outcome.ok_or_else(|| {
        format_err!(
            "{} from small repo hasn't been remapped in large repo",
            cs_id
        )
    })?;

    use CommitSyncOutcome::*;
    match sync_outcome {
        RewrittenAs(ref cs_id, ref version) => Ok((*cs_id, version.clone())),
        _ => Err(format_err!(
            "unexpected commit sync outcome for root, got {:?}",
            sync_outcome
        )),
    }
}

fn find_root(new_branch: &Vec<BonsaiChangeset>) -> Result<ChangesetId, Error> {
    let mut cs_to_parents: HashMap<_, Vec<_>> = HashMap::new();
    for bcs in new_branch {
        let cs_id = bcs.get_changeset_id();
        cs_to_parents.insert(cs_id, bcs.parents().collect());
    }

    let mut roots = vec![];
    for parents in cs_to_parents.values() {
        for p in parents {
            if !cs_to_parents.contains_key(p) {
                roots.push(p);
            }
        }
    }

    validate_roots(roots).map(|root| *root)
}

async fn find_new_branch_oldest_first(
    ctx: CoreContext,
    small_repo: &InnerRepo,
    p1: ChangesetId,
    p2: ChangesetId,
) -> Result<Vec<BonsaiChangeset>, Error> {
    let new_branch = small_repo
        .commit_graph()
        .ancestors_difference_stream(&ctx, vec![p2], vec![p1])
        .await?
        .map_ok({
            cloned!(ctx, small_repo);
            move |cs| {
                cloned!(ctx, small_repo);
                async move { Ok(cs.load(&ctx, small_repo.blob_repo.repo_blobstore()).await?) }
            }
        })
        .try_buffered(100)
        .try_collect::<Vec<_>>()
        .await?;

    Ok(new_branch.into_iter().rev().collect())
}

fn validate_parents(parents: Vec<ChangesetId>) -> Result<(ChangesetId, ChangesetId), Error> {
    if parents.len() > 2 {
        return Err(format_err!(
            "too many parents, expected only 2: {:?}",
            parents
        ));
    }
    let p1 = parents
        .get(0)
        .ok_or_else(|| Error::msg("not a merge commit"))?;
    let p2 = parents
        .get(1)
        .ok_or_else(|| Error::msg("not a merge commit"))?;

    Ok((*p1, *p2))
}

fn validate_roots(roots: Vec<&ChangesetId>) -> Result<&ChangesetId, Error> {
    if roots.len() > 1 {
        return Err(format_err!("too many roots, expected only 1: {:?}", roots));
    }

    roots
        .get(0)
        .cloned()
        .ok_or_else(|| Error::msg("no roots found, this is not a diamond merge"))
}

fn find_bonsai_diff(
    ctx: CoreContext,
    repo: &BlobRepo,
    ancestor: ChangesetId,
    descendant: ChangesetId,
) -> BoxStream<BonsaiDiffFileChange<HgFileNodeId>, Error> {
    (
        id_to_manifestid(ctx.clone(), repo.clone(), descendant),
        id_to_manifestid(ctx.clone(), repo.clone(), ancestor),
    )
        .into_future()
        .map({
            cloned!(ctx, repo);
            move |(d_mf, a_mf)| {
                bonsai_diff(
                    ctx,
                    repo.repo_blobstore().clone(),
                    d_mf,
                    Some(a_mf).into_iter().collect(),
                )
                .boxed()
                .compat()
            }
        })
        .flatten_stream()
        .boxify()
}

fn id_to_manifestid(
    ctx: CoreContext,
    repo: BlobRepo,
    bcs_id: ChangesetId,
) -> impl Future<Item = HgManifestId, Error = Error> {
    async move {
        let cs_id = repo.derive_hg_changeset(&ctx, bcs_id).await?;
        let cs = cs_id.load(&ctx, repo.repo_blobstore()).await?;
        Ok(cs.manifestid())
    }
    .boxed()
    .compat()
}


/*
 * 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 clap_old::Arg;
use clap_old::SubCommand;
use cmdlib::args;
use cmdlib::args::MononokeClapApp;

pub const ARG_ONCE: &str = "once";
pub const ARG_COMMIT: &str = "commit";
pub const ARG_TAIL: &str = "tail";
pub const ARG_TARGET_BOOKMARK: &str = "target-bookmark";
pub const ARG_CATCH_UP_ONCE: &str = "catch-up-once";
pub const ARG_LOG_TO_SCUBA: &str = "log-to-scuba";
pub const ARG_BACKSYNC_BACKPRESSURE_REPOS_IDS: &str = "backsync-backpressure-repo-ids";
pub const ARG_HG_SYNC_BACKPRESSURE: &str = "hg-sync-backpressure";
pub const ARG_DERIVED_DATA_TYPES: &str = "derived-data-types";
pub const ARG_SLEEP_SECS: &str = "sleep-secs";
pub const ARG_BOOKMARK_REGEX: &str = "bookmark-regex";
pub const ARG_PUSHREBASE_REWRITE_DATES: &str = "pushrebase-rewrite-dates";

pub fn create_app<'a, 'b>() -> MononokeClapApp<'a, 'b> {
    let app = args::MononokeAppBuilder::new("Mononoke cross-repo sync job")
        .with_fb303_args()
        .with_scribe_args()
        .with_source_and_target_repos()
        .build();

    let app = app
        .arg(
            Arg::with_name(ARG_LOG_TO_SCUBA)
                .long(ARG_LOG_TO_SCUBA)
                .takes_value(false)
                .help("Whether the progress of the tailer should be logged to scuba"),
        )
        .arg(
            Arg::with_name(ARG_PUSHREBASE_REWRITE_DATES)
                .long(ARG_PUSHREBASE_REWRITE_DATES)
                .required(false)
                .help("Rewrite date to current date while rebasing onto large repo"),
        );

    let once = SubCommand::with_name(ARG_ONCE)
        .about("Syncs a single commit")
        .arg(
            Arg::with_name(ARG_TARGET_BOOKMARK)
                .long(ARG_TARGET_BOOKMARK)
                .takes_value(true)
                .required(false)
                .help("A bookmark in the target repo to sync to"),
        )
        .arg(
            Arg::with_name(ARG_COMMIT)
                .long(ARG_COMMIT)
                .takes_value(true)
                .required(true)
                .help("A commit to sync"),
        );

    let tail = SubCommand::with_name(ARG_TAIL)
        .about("Syncs commits in a loop")
        .arg(
            Arg::with_name(ARG_SLEEP_SECS)
                .long(ARG_SLEEP_SECS)
                .takes_value(true)
                .help("Sleep this many seconds on the no-op iteration/while applying backpressure"),
        )
        .arg(
            Arg::with_name(ARG_CATCH_UP_ONCE)
                .long(ARG_CATCH_UP_ONCE)
                .takes_value(false)
                .help(
                    "Only catch up until the current state of \
                     bookmarks_update_log and do not wait indefinitely",
                ),
        )
        .arg(
            Arg::with_name(ARG_BACKSYNC_BACKPRESSURE_REPOS_IDS)
                .long(ARG_BACKSYNC_BACKPRESSURE_REPOS_IDS)
                .takes_value(true)
                .multiple(true)
                .required(false)
                .help(
                    "Monitors how many entries to backsync in the queue for other repos and pauses syncing if queue is too large",
                ),
        )
        .arg(
            Arg::with_name(ARG_HG_SYNC_BACKPRESSURE)
                .long(ARG_HG_SYNC_BACKPRESSURE)
                .required(false)
                .help("Waits until new commits created in the target repo are synced to hg"),
        )
        .arg(
            Arg::with_name(ARG_DERIVED_DATA_TYPES)
                .long(ARG_DERIVED_DATA_TYPES)
                .takes_value(true)
                .multiple(true)
                .required(false)
                .help(
                    "derived data to derive in target repo after sync",
                ),
        )
        .arg(
            Arg::with_name(ARG_BOOKMARK_REGEX)
                .long(ARG_BOOKMARK_REGEX)
                .takes_value(true)
                .required(false)
                .help(
                    "sync only bookmarks that match the regex",
                ),
        );

    let app = app.subcommand(once).subcommand(tail);
    app
}


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

#![feature(auto_traits)]
#![feature(trait_alias)]

//! Mononoke Cross Repo sync job
//!
//! This is a special job used to tail "small" Mononoke repo into "large" Mononoke repo when
//! small repo is a source of truth (i.e. "hg push" go directly to small repo).
//! At the moment there two main limitations:
//! 1) Syncing of some merge commits is not supported
//! 2) Root commits and their descendants that are not merged into a main line
//! aren't going to be synced. For example,
//
//! ```text
//!   O <- main bookmark
//!   |
//!   O
//!   |   A <- new_bookmark, that added a new root commit
//!   O   |
//!    ...
//!
//!   Commit A, its ancestors and new_bookmark aren't going to be synced to the large repo.
//!   However if commit A gets merged into a mainline e.g.
//!   O <- main bookmark
//!   | \
//!   O  \
//!   |   A <- new_bookmark, that added a new root commit
//!   O   |
//!    ...
//!
//!   Then commit A and all of its ancestors WILL be synced to the large repo, however
//!   new_bookmark still WILL NOT be synced to the large repo.
//!
//! This job does tailing by following bookmark update log of the small repo and replaying
//! each commit into the large repo. Note that some bookmarks called "common_pushrebase_bookmarks"
//! are treated specially, see comments in the code for more details
//! ```

use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;

use anyhow::format_err;
use anyhow::Error;
use anyhow::Result;
use backsyncer::format_counter as format_backsyncer_counter;
use bonsai_hg_mapping::BonsaiHgMappingArc;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::Freshness;
use cached_config::ConfigStore;
use changesets::ChangesetsArc;
use clap_old::ArgMatches;
use cmdlib::args;
use cmdlib::args::MononokeMatches;
use cmdlib::helpers;
use cmdlib::monitoring;
use cmdlib_x_repo::create_commit_syncer_from_matches;
use commit_graph::CommitGraphArc;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use context::SessionContainer;
use cross_repo_sync::types::Source;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync::ConcreteRepo as CrossRepo;
use cross_repo_sync::PushrebaseRewriteDates;
use derived_data_utils::derive_data_for_csids;
use fbinit::FacebookInit;
use filenodes::FilenodesArc;
use futures::future;
use futures::future::try_join;
use futures::stream;
use futures::stream::TryStreamExt;
use futures::StreamExt;
use futures_stats::TimedFutureExt;
use live_commit_sync_config::CfgrLiveCommitSyncConfig;
use live_commit_sync_config::LiveCommitSyncConfig;
use mononoke_api_types::InnerRepo;
use mononoke_hg_sync_job_helper_lib::wait_for_latest_log_id_to_be_synced;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;
use mutable_counters::ArcMutableCounters;
use mutable_counters::MutableCountersArc;
use mutable_counters::MutableCountersRef;
use regex::Regex;
use repo_derived_data::RepoDerivedDataArc;
use repo_derived_data::RepoDerivedDataRef;
use repo_identity::RepoIdentityRef;
use scuba_ext::MononokeScubaSampleBuilder;
use slog::debug;
use slog::error;
use slog::info;
use slog::warn;
use synced_commit_mapping::SyncedCommitMapping;

use crate::sync::SyncResult;

mod cli;
mod reporting;
mod setup;
mod sync;

use crate::cli::create_app;
use crate::cli::ARG_BACKSYNC_BACKPRESSURE_REPOS_IDS;
use crate::cli::ARG_BOOKMARK_REGEX;
use crate::cli::ARG_CATCH_UP_ONCE;
use crate::cli::ARG_DERIVED_DATA_TYPES;
use crate::cli::ARG_HG_SYNC_BACKPRESSURE;
use crate::cli::ARG_ONCE;
use crate::cli::ARG_PUSHREBASE_REWRITE_DATES;
use crate::cli::ARG_TAIL;
use crate::cli::ARG_TARGET_BOOKMARK;
use crate::reporting::add_common_fields;
use crate::reporting::log_bookmark_update_result;
use crate::reporting::log_noop_iteration;
use crate::setup::get_scuba_sample;
use crate::setup::get_sleep_duration;
use crate::setup::get_starting_commit;
use crate::sync::sync_commit_and_ancestors;
use crate::sync::sync_single_bookmark_update_log;

pub trait Repo = cross_repo_sync::Repo
    + RepoDerivedDataArc
    + RepoDerivedDataRef
    + ChangesetsArc
    + FilenodesArc
    + BonsaiHgMappingArc
    + MutableCountersRef
    + RepoIdentityRef
    + CommitGraphRef
    + CommitGraphArc;

fn print_error(ctx: CoreContext, error: &Error) {
    error!(ctx.logger(), "{}", error);
    for cause in error.chain().skip(1) {
        error!(ctx.logger(), "caused by: {}", cause);
    }
}

async fn run_in_single_commit_mode<M: SyncedCommitMapping + Clone + 'static, R: Repo>(
    ctx: &CoreContext,
    bcs: ChangesetId,
    commit_syncer: CommitSyncer<M, R>,
    scuba_sample: MononokeScubaSampleBuilder,
    maybe_bookmark: Option<BookmarkKey>,
    common_bookmarks: HashSet<BookmarkKey>,
    pushrebase_rewrite_dates: PushrebaseRewriteDates,
) -> Result<(), Error> {
    info!(
        ctx.logger(),
        "Checking if {} is already synced {}->{}",
        bcs,
        commit_syncer.repos.get_source_repo().repo_identity().id(),
        commit_syncer.repos.get_target_repo().repo_identity().id()
    );
    if commit_syncer
        .commit_sync_outcome_exists(ctx, Source(bcs))
        .await?
    {
        info!(ctx.logger(), "{} is already synced", bcs);
        return Ok(());
    }

    let res = sync_commit_and_ancestors(
        ctx,
        &commit_syncer,
        None, // from_cs_id,
        bcs,
        maybe_bookmark,
        &common_bookmarks,
        scuba_sample,
        pushrebase_rewrite_dates,
    )
    .await;

    if res.is_ok() {
        info!(ctx.logger(), "successful sync");
    }
    res.map(|_| ())
}

enum TailingArgs<M, R> {
    CatchUpOnce(CommitSyncer<M, R>),
    LoopForever(CommitSyncer<M, R>, ConfigStore),
}

async fn run_in_tailing_mode<M: SyncedCommitMapping + Clone + 'static, R: Repo>(
    ctx: &CoreContext,
    target_mutable_counters: ArcMutableCounters,
    common_pushrebase_bookmarks: HashSet<BookmarkKey>,
    base_scuba_sample: MononokeScubaSampleBuilder,
    backpressure_params: BackpressureParams,
    derived_data_types: Vec<String>,
    tailing_args: TailingArgs<M, R>,
    sleep_duration: Duration,
    maybe_bookmark_regex: Option<Regex>,
    pushrebase_rewrite_dates: PushrebaseRewriteDates,
) -> Result<(), Error> {
    match tailing_args {
        TailingArgs::CatchUpOnce(commit_syncer) => {
            let scuba_sample = MononokeScubaSampleBuilder::with_discard();
            tail(
                ctx,
                &commit_syncer,
                &target_mutable_counters,
                scuba_sample,
                &common_pushrebase_bookmarks,
                &backpressure_params,
                &derived_data_types,
                sleep_duration,
                &maybe_bookmark_regex,
                pushrebase_rewrite_dates,
            )
            .await?;
        }
        TailingArgs::LoopForever(commit_syncer, config_store) => {
            let live_commit_sync_config =
                Arc::new(CfgrLiveCommitSyncConfig::new(ctx.logger(), &config_store)?);
            let source_repo_id = commit_syncer.get_source_repo().repo_identity().id();

            loop {
                let scuba_sample = base_scuba_sample.clone();
                // We only care about public pushes because draft pushes are not in the bookmark
                // update log at all.
                let enabled =
                    live_commit_sync_config.push_redirector_enabled_for_public(source_repo_id);

                // Pushredirection is enabled - we need to disable forward sync in that case
                if enabled {
                    log_noop_iteration(scuba_sample);
                    tokio::time::sleep(sleep_duration).await;
                    continue;
                }

                let synced_something = tail(
                    ctx,
                    &commit_syncer,
                    &target_mutable_counters,
                    scuba_sample.clone(),
                    &common_pushrebase_bookmarks,
                    &backpressure_params,
                    &derived_data_types,
                    sleep_duration,
                    &maybe_bookmark_regex,
                    pushrebase_rewrite_dates,
                )
                .await?;

                if !synced_something {
                    log_noop_iteration(scuba_sample);
                    tokio::time::sleep(sleep_duration).await;
                }
            }
        }
    }

    Ok(())
}

async fn tail<M: SyncedCommitMapping + Clone + 'static, R: Repo>(
    ctx: &CoreContext,
    commit_syncer: &CommitSyncer<M, R>,
    target_mutable_counters: &ArcMutableCounters,
    mut scuba_sample: MononokeScubaSampleBuilder,
    common_pushrebase_bookmarks: &HashSet<BookmarkKey>,
    backpressure_params: &BackpressureParams,
    derived_data_types: &[String],
    sleep_duration: Duration,
    maybe_bookmark_regex: &Option<Regex>,
    pushrebase_rewrite_dates: PushrebaseRewriteDates,
) -> Result<bool, Error> {
    let source_repo = commit_syncer.get_source_repo();
    let bookmark_update_log = source_repo.bookmark_update_log();
    let counter = format_counter(commit_syncer);

    let maybe_start_id = target_mutable_counters.get_counter(ctx, &counter).await?;
    let start_id = maybe_start_id.ok_or_else(|| format_err!("counter not found"))?;
    let limit = 10;
    let log_entries = bookmark_update_log
        .read_next_bookmark_log_entries(ctx.clone(), start_id as u64, limit, Freshness::MaybeStale)
        .try_collect::<Vec<_>>()
        .await?;

    let remaining_entries = commit_syncer
        .get_source_repo()
        .bookmark_update_log()
        .count_further_bookmark_log_entries(ctx.clone(), start_id as u64, None)
        .await?;

    if log_entries.is_empty() {
        log_noop_iteration(scuba_sample.clone());
        Ok(false)
    } else {
        scuba_sample.add("queue_size", remaining_entries);
        info!(ctx.logger(), "queue size is {}", remaining_entries);

        for entry in log_entries {
            let entry_id = entry.id;
            scuba_sample.add("entry_id", entry.id);

            let mut skip = false;
            if let Some(regex) = maybe_bookmark_regex {
                if !regex.is_match(entry.bookmark_name.as_str()) {
                    skip = true;
                }
            }

            if !skip {
                let (stats, res) = sync_single_bookmark_update_log(
                    ctx,
                    commit_syncer,
                    entry,
                    common_pushrebase_bookmarks,
                    scuba_sample.clone(),
                    pushrebase_rewrite_dates,
                )
                .timed()
                .await;

                log_bookmark_update_result(ctx, entry_id, scuba_sample.clone(), &res, stats);
                let maybe_synced_css = res?;

                if let SyncResult::Synced(synced_css) = maybe_synced_css {
                    derive_data_for_csids(
                        ctx,
                        commit_syncer.get_target_repo(),
                        synced_css,
                        derived_data_types,
                    )?
                    .await?;

                    maybe_apply_backpressure(
                        ctx,
                        backpressure_params,
                        commit_syncer.get_target_repo(),
                        scuba_sample.clone(),
                        sleep_duration,
                    )
                    .await?;
                }
            } else {
                info!(
                    ctx.logger(),
                    "skipping log entry #{} for {}", entry.id, entry.bookmark_name
                );
                let mut scuba_sample = scuba_sample.clone();
                scuba_sample.add("source_bookmark_name", format!("{}", entry.bookmark_name));
                scuba_sample.add("skipped", true);
                scuba_sample.log();
            }

            // Note that updating the counter might fail after successful sync of the commits.
            // This is expected - next run will try to update the counter again without
            // re-syncing the commits.
            target_mutable_counters
                .set_counter(ctx, &counter, entry_id, None)
                .await?;
        }
        Ok(true)
    }
}

async fn maybe_apply_backpressure(
    ctx: &CoreContext,
    backpressure_params: &BackpressureParams,
    target_repo: &impl Repo,
    scuba_sample: MononokeScubaSampleBuilder,
    sleep_duration: Duration,
) -> Result<(), Error> {
    let target_repo_id = target_repo.repo_identity().id();
    let limit = 10;
    loop {
        let max_further_entries = stream::iter(&backpressure_params.backsync_repos)
            .map(Ok)
            .map_ok(|repo| {
                async move {
                    let repo_id = repo.repo_identity().id();
                    let backsyncer_counter = format_backsyncer_counter(&target_repo_id);
                    let maybe_counter = repo
                        .mutable_counters()
                        .get_counter(ctx, &backsyncer_counter)
                        .await?;

                    match maybe_counter {
                        Some(counter) => {
                            let bookmark_update_log = repo.bookmark_update_log();
                            debug!(ctx.logger(), "repo {}, counter {}", repo_id, counter);
                            bookmark_update_log
                                .count_further_bookmark_log_entries(
                                    ctx.clone(),
                                    counter as u64,
                                    None, // exclude_reason
                                )
                                .await
                        }
                        None => {
                            warn!(
                                ctx.logger(),
                                "backsyncer counter not found for repo {}!", repo_id,
                            );
                            Ok(0)
                        }
                    }
                }
            })
            .try_buffer_unordered(100)
            .try_fold(0, |acc, x| future::ready(Ok(::std::cmp::max(acc, x))))
            .await?;

        if max_further_entries > limit {
            reporting::log_backpressure(ctx, max_further_entries, scuba_sample.clone());
            tokio::time::sleep(sleep_duration).await;
        } else {
            break;
        }
    }

    if backpressure_params.wait_for_target_repo_hg_sync {
        wait_for_latest_log_id_to_be_synced(ctx, target_repo, sleep_duration).await?;
    }
    Ok(())
}

fn format_counter<M: SyncedCommitMapping + Clone + 'static, R: Repo>(
    commit_syncer: &CommitSyncer<M, R>,
) -> String {
    let source_repo_id = commit_syncer.get_source_repo_id();
    format!("xreposync_from_{}", source_repo_id)
}

async fn run<'a>(
    fb: FacebookInit,
    ctx: CoreContext,
    matches: &'a MononokeMatches<'a>,
) -> Result<(), Error> {
    let config_store = matches.config_store();
    let mut scuba_sample = get_scuba_sample(ctx.clone(), matches);

    let source_repo_id =
        args::not_shardmanager_compatible::get_source_repo_id(config_store, matches)?;
    let target_repo_id =
        args::not_shardmanager_compatible::get_target_repo_id(config_store, matches)?;

    let logger = ctx.logger();
    let source_repo = args::open_repo_with_repo_id(fb, logger, source_repo_id, matches);
    let target_repo = args::open_repo_with_repo_id(fb, logger, target_repo_id, matches);

    let (source_repo, target_repo): (InnerRepo, InnerRepo) =
        try_join(source_repo, target_repo).await?;

    let commit_syncer = create_commit_syncer_from_matches::<InnerRepo>(&ctx, matches, None).await?;

    let live_commit_sync_config = Arc::new(CfgrLiveCommitSyncConfig::new(logger, config_store)?);
    let common_commit_sync_config =
        live_commit_sync_config.get_common_config(source_repo.blob_repo.repo_identity().id())?;

    let pushrebase_rewrite_dates = if matches.is_present(ARG_PUSHREBASE_REWRITE_DATES) {
        PushrebaseRewriteDates::Yes
    } else {
        PushrebaseRewriteDates::No
    };

    let common_bookmarks: HashSet<_> = common_commit_sync_config
        .common_pushrebase_bookmarks
        .clone()
        .into_iter()
        .collect();

    let target_mutable_counters = target_repo.mutable_counters_arc();
    match matches.subcommand() {
        (ARG_ONCE, Some(sub_m)) => {
            add_common_fields(&mut scuba_sample, &commit_syncer);
            let maybe_target_bookmark = sub_m
                .value_of(ARG_TARGET_BOOKMARK)
                .map(BookmarkKey::new)
                .transpose()?;
            let bcs = get_starting_commit(&ctx, sub_m, source_repo.blob_repo.clone()).await?;

            run_in_single_commit_mode(
                &ctx,
                bcs,
                commit_syncer,
                scuba_sample,
                maybe_target_bookmark,
                common_bookmarks,
                pushrebase_rewrite_dates,
            )
            .await
        }
        (ARG_TAIL, Some(sub_m)) => {
            add_common_fields(&mut scuba_sample, &commit_syncer);

            let sleep_duration = get_sleep_duration(sub_m)?;
            let tailing_args = if sub_m.is_present(ARG_CATCH_UP_ONCE) {
                TailingArgs::CatchUpOnce(commit_syncer)
            } else {
                let config_store = matches.config_store();

                TailingArgs::LoopForever(commit_syncer, config_store.clone())
            };

            let backpressure_params = BackpressureParams::new(&ctx, matches, sub_m).await?;

            let derived_data_types: Vec<String> = match sub_m.values_of(ARG_DERIVED_DATA_TYPES) {
                Some(derived_data_types) => derived_data_types
                    .into_iter()
                    .map(String::from)
                    .collect::<Vec<_>>(),
                None => vec![],
            };

            let maybe_bookmark_regex = match sub_m.value_of(ARG_BOOKMARK_REGEX) {
                Some(regex) => Some(Regex::new(regex)?),
                None => None,
            };

            run_in_tailing_mode(
                &ctx,
                target_mutable_counters,
                common_bookmarks,
                scuba_sample,
                backpressure_params,
                derived_data_types,
                tailing_args,
                sleep_duration,
                maybe_bookmark_regex,
                pushrebase_rewrite_dates,
            )
            .await
        }
        (incorrect, _) => Err(format_err!(
            "Incorrect mode of operation specified: {}",
            incorrect
        )),
    }
}

struct BackpressureParams {
    backsync_repos: Vec<CrossRepo>,
    wait_for_target_repo_hg_sync: bool,
}

impl BackpressureParams {
    async fn new<'a>(
        ctx: &CoreContext,
        matches: &'a MononokeMatches<'a>,
        sub_m: &'a ArgMatches<'a>,
    ) -> Result<Self, Error> {
        let backsync_repos_ids = sub_m.values_of(ARG_BACKSYNC_BACKPRESSURE_REPOS_IDS);
        let backsync_repos = match backsync_repos_ids {
            Some(backsync_repos_ids) => {
                let backsync_repos = stream::iter(backsync_repos_ids.into_iter().map(|repo_id| {
                    let repo_id = repo_id.parse::<i32>()?;
                    Ok(repo_id)
                }))
                .map_ok(|repo_id| {
                    args::open_repo_with_repo_id(
                        ctx.fb,
                        ctx.logger(),
                        RepositoryId::new(repo_id),
                        matches,
                    )
                })
                .try_buffer_unordered(100)
                .try_collect::<Vec<_>>();
                backsync_repos.await?
            }
            None => vec![],
        };

        let wait_for_target_repo_hg_sync = sub_m.is_present(ARG_HG_SYNC_BACKPRESSURE);
        Ok(Self {
            backsync_repos,
            wait_for_target_repo_hg_sync,
        })
    }
}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
    let app = create_app();
    let (matches, _runtime) = app.get_matches(fb)?;
    let logger = matches.logger();
    let session = SessionContainer::new_with_defaults(fb);
    let ctx = session.new_context_with_scribe(
        logger.clone(),
        MononokeScubaSampleBuilder::with_discard(),
        args::get_scribe(fb, &matches)?,
    );

    let res = helpers::block_execute(
        run(fb, ctx.clone(), &matches),
        fb,
        "x_repo_sync_job",
        ctx.logger(),
        &matches,
        monitoring::AliveService,
    );

    if let Err(ref err) = res {
        print_error(ctx, err);
    }
    res
}