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

//! Connection management.

use std::net::ToSocketAddrs;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Error;
use fbinit::FacebookInit;
use sharding_ext::encode_repo_name;
use source_control::client::make_SourceControlService;
use source_control::client::SourceControlService;
use x2pclient::X2pClientBuilder;

const DEFAULT_TIER: &str = "shardmanager:mononoke.scs";

const CONN_TIMEOUT_MS: u32 = 5000;
const RECV_TIMEOUT_MS: u32 = 30_000;

#[derive(Clone)]
pub(crate) struct Connection {
    client: Arc<dyn SourceControlService + Sync>,
}

impl Connection {
    /// Build a connection from a `host:port` string.
    #[cfg(not(target_os = "windows"))]
    pub fn from_host_port(fb: FacebookInit, host_port: impl AsRef<str>) -> Result<Self, Error> {
        use thriftclient::ThriftChannelBuilder;

        let mut addrs = host_port.as_ref().to_socket_addrs()?;
        let addr = addrs.next().expect("no address found");
        let client = ThriftChannelBuilder::from_sock_addr(fb, addr)?
            .with_conn_timeout(CONN_TIMEOUT_MS)
            .with_recv_timeout(RECV_TIMEOUT_MS)
            .with_secure(true)
            .build_client(make_SourceControlService)?;
        Ok(Self { client })
    }

    /// Build a connection from a `host:port` string.
    #[cfg(target_os = "windows")]
    pub fn from_host_port(_fb: FacebookInit, _host_port: impl AsRef<str>) -> Result<Self, Error> {
        Err(anyhow!(
            "Connection to host and port is not supported on this platform"
        ))
    }

    /// Build a connection from a tier name via servicerouter.
    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
    pub fn from_tier_name_via_sr(
        fb: FacebookInit,
        client_id: String,
        tier: impl AsRef<str>,
        shardmanager_domain: Option<&str>,
    ) -> Result<Self, Error> {
        use maplit::hashmap;
        use rand::distributions::Alphanumeric;
        use rand::Rng;
        use srclient::ClientParams;
        use srclient::SRChannelBuilder;

        let correlator: String = rand::thread_rng()
            .sample_iter(&Alphanumeric)
            .take(16)
            .map(char::from)
            .collect();
        let headers = hashmap! {
            String::from("client_type") => String::from("scsc CLI"),
            String::from("client_correlator") => correlator,
        };
        let conn_config = hashmap! {
            String::from("client_id") => client_id,
        };

        let client = SRChannelBuilder::from_service_name(fb, tier.as_ref())?
            .with_conn_config(&conn_config)
            .with_persistent_headers(headers);

        let client = if let Some(shardmanager_domain) = shardmanager_domain {
            let client_params = ClientParams::new()
                .with_shard_manager_domain(encode_repo_name(shardmanager_domain));
            client.with_client_params(client_params)
        } else {
            client
        }
        .build_client(make_SourceControlService)?;
        Ok(Self { client })
    }

    /// Build a connection from a tier name via servicerouter.
    #[cfg(any(target_os = "macos", target_os = "windows"))]
    pub fn from_tier_name_via_sr(
        _fb: FacebookInit,
        _client_id: String,
        _tier: impl AsRef<str>,
        _shardmanager_domain: Option<&str>,
    ) -> Result<Self, Error> {
        Err(anyhow!(
            "Connection via ServiceRouter is not supported on this platform"
        ))
    }

    /// Build a connection from a tier name via x2p.
    pub fn from_tier_name_via_x2p(
        fb: FacebookInit,
        _client_id: String,
        tier: impl AsRef<str>,
        shardmanager_domain: Option<&str>,
    ) -> Result<Self, Error> {
        let client = X2pClientBuilder::from_service_name(fb, tier.as_ref());
        let client = if let Some(sm_domain) = shardmanager_domain {
            client.with_shard_manager_domain(encode_repo_name(sm_domain))
        } else {
            client
        }
        .build_client(make_SourceControlService)?;

        Ok(Self { client })
    }

    /// Build a connection from a tier name.
    pub fn from_tier_name(
        fb: FacebookInit,
        client_id: String,
        tier: impl AsRef<str>,
        shardmanager_domain: Option<&str>,
    ) -> Result<Self, Error> {
        match x2pclient::get_env(fb) {
            x2pclient::Environment::Prod => {
                Self::from_tier_name_via_sr(fb, client_id, tier, shardmanager_domain)
            }
            x2pclient::Environment::Corp => {
                Self::from_tier_name_via_x2p(fb, client_id, tier, shardmanager_domain)
            }
            other_env => Err(anyhow!("{} not supported", other_env)),
        }
    }
}

#[derive(clap::Args)]
pub(super) struct ConnectionArgs {
    #[clap(long, default_value = "scsc-default-client", global = true)]
    /// Name of the client for quota attribution and logging.
    client_id: String,
    #[clap(long, short, default_value = DEFAULT_TIER, global = true)]
    /// Connect to SCS through given tier.
    tier: String,
    #[clap(long, short = 'H', conflicts_with = "tier", global = true)]
    /// Connect to SCS through a given host and port pair, format HOST:PORT.
    host: Option<String>,
}

impl ConnectionArgs {
    pub fn get_connection(
        &self,
        fb: FacebookInit,
        repo: Option<&str>,
    ) -> Result<Connection, Error> {
        if let Some(host_port) = &self.host {
            Connection::from_host_port(fb, host_port)
        } else {
            Connection::from_tier_name(fb, self.client_id.clone(), &self.tier, repo)
        }
    }
}

impl std::ops::Deref for Connection {
    type Target = dyn SourceControlService + Sync;
    fn deref(&self) -> &Self::Target {
        &*self.client
    }
}


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

//! Command-line client for the Source Control Service.

use std::env;
use std::io::stderr;
use std::io::stdout;
use std::io::IsTerminal;
use std::process::ExitCode;

use ansi_term::Colour;
use base_app::BaseApp;
use clap::ArgMatches;
use clap::CommandFactory;
use clap::FromArgMatches;
use clap::Parser;
use fbinit::FacebookInit;

use crate::connection::Connection;
use crate::connection::ConnectionArgs;
use crate::render::OutputTarget;

mod args;
mod commands;
mod connection;
pub(crate) mod library;
mod render;
pub(crate) mod util;

lazy_static::lazy_static! {
    static ref SHORT_VERSION: String = {
        #[cfg(target_os = "windows")]
        {
            String::from("for Windows")
        }
        #[cfg(not(target_os = "windows"))]
        {
            use build_info::BuildInfo;
            format!(
                "{}-{}",
                BuildInfo::get_package_version(),
                BuildInfo::get_package_release(),
            )
        }
    };
    static ref LONG_VERSION: String = {
        #[cfg(target_os = "windows")]
        {
            String::from("(BuildInfo not available on Windows)")
        }
        #[cfg(not(target_os = "windows"))]
        {
            use build_info::BuildInfo;
            format!("{:#?}", BuildInfo)
        }
    };
}

pub(crate) struct ScscApp {
    matches: ArgMatches,
    connection_args: ConnectionArgs,
    target: OutputTarget,
    fb: FacebookInit,
}

impl ScscApp {
    fn get_connection(&self, repo: Option<&str>) -> anyhow::Result<Connection> {
        self.connection_args.get_connection(self.fb, repo)
    }
}

impl BaseApp for ScscApp {
    fn subcommand(&self) -> Option<(&str, &ArgMatches)> {
        self.matches.subcommand()
    }
}

#[derive(Parser)]
#[clap(
    name = "Source Control Service client",
    version(&**SHORT_VERSION),
    long_version(&**LONG_VERSION),
    term_width(textwrap::termwidth()),
)]
/// Send requests to the Source Control Service
struct ScscArgs {
    /// Should the output of the command be JSON?
    #[clap(long, global = true)]
    json: bool,

    #[clap(flatten)]
    connection_args: ConnectionArgs,
}

async fn main_impl(fb: FacebookInit) -> anyhow::Result<()> {
    let subcommands = commands::subcommands();
    assert!(!subcommands.is_empty());
    let app = ScscArgs::command()
        .subcommands(subcommands)
        .subcommand_required(true)
        .arg_required_else_help(true);
    let matches = app.get_matches();
    let common_args = ScscArgs::from_arg_matches(&matches)?;
    let connection_args = common_args.connection_args;
    let target = if common_args.json {
        OutputTarget::Json
    } else if stdout().is_terminal() {
        OutputTarget::Tty
    } else {
        OutputTarget::Pipe
    };
    let app = ScscApp {
        matches,
        connection_args,
        target,
        fb,
    };
    commands::dispatch(app).await
}

#[fbinit::main]
async fn main(fb: FacebookInit) -> ExitCode {
    if let Err(e) = main_impl(fb).await {
        let prog_name = env::args().next().unwrap_or_else(|| "scsc".to_string());
        if stderr().is_terminal() {
            eprintln!(
                "{}: {} {:#}",
                prog_name,
                Colour::Red.bold().paint("error:"),
                e
            );
        } else {
            eprintln!("{}: error: {:#}", prog_name, e);
        }
        return ExitCode::FAILURE;
    }
    ExitCode::SUCCESS
}


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

//! Rendering of responses.
use std::io::Write;

use anyhow::Result;
use auto_impl::auto_impl;
use futures::stream;
use futures::stream::Stream;
use futures::stream::TryStreamExt;

// Auto-impl for Box so that render streams can contain either R: Render
// or Box<dyn Render>.
#[auto_impl(Box)]
/// A renderable item.  This trait should be implemented by anything that can
/// be output from a command.
pub(crate) trait Render: Send {
    type Args;
    /// Render output suitable for human users.
    fn render(&self, _matches: &Self::Args, _write: &mut dyn Write) -> Result<()> {
        Ok(())
    }

    /// Render output suitable for human users to a terminal or console.
    fn render_tty(&self, matches: &Self::Args, write: &mut dyn Write) -> Result<()> {
        self.render(matches, write)
    }

    /// Render as a JSON value.
    fn render_json(&self, _matches: &Self::Args, _write: &mut dyn Write) -> Result<()> {
        Ok(())
    }
}

#[derive(Copy, Clone, Debug)]
pub(crate) enum OutputTarget {
    Tty,
    Pipe,
    Json,
}

impl OutputTarget {
    /// Render the output for a command invocation.
    pub(crate) async fn render<R: Render>(
        self,
        matches: &R::Args,
        objs: impl Stream<Item = Result<R>>,
    ) -> Result<()> {
        objs.try_for_each(move |output| async move {
            let mut stdout = std::io::stdout();
            match self {
                OutputTarget::Tty => {
                    output.render_tty(matches, &mut stdout)?;
                }
                OutputTarget::Pipe => {
                    output.render(matches, &mut stdout)?;
                }
                OutputTarget::Json => {
                    output.render_json(matches, &mut stdout)?;
                    writeln!(&mut stdout)?;
                }
            }
            Ok(())
        })
        .await
    }

    /// Render a single element for a command invocation
    pub(crate) async fn render_one<R: Render>(self, matches: &R::Args, obj: R) -> Result<()> {
        self.render(matches, stream::once(futures::future::ok(obj)))
            .await
    }
}


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

//! Utility traits and functions.

use num::PrimInt;

/// Returns `single` if the given number is 1, or `plural` otherwise.
pub(crate) fn plural<'a, T: PrimInt>(n: T, single: &'a str, plural: &'a str) -> &'a str {
    if n == T::one() { single } else { plural }
}

fn byte_count(size: i64, unit_single: &str, unit_plural: &str, multiple: &[&str]) -> String {
    const UNIT_LIMIT: i64 = 9999;
    match (size, multiple.split_last()) {
        (std::i64::MIN..=UNIT_LIMIT, _) | (_, None) => {
            format!("{}{}", size, plural(size, unit_single, unit_plural))
        }
        (size, Some((last_multiple, multiple))) => {
            let mut divisor = 1024;
            for unit in multiple.iter() {
                if size < (UNIT_LIMIT + 1) * divisor {
                    return format!("{:.2}{}", (size as f64) / (divisor as f64), unit);
                }
                divisor *= 1024;
            }
            format!("{:.2}{}", (size as f64) / (divisor as f64), last_multiple)
        }
    }
}

/// Convert a byte count to a human-readable representation of the byte count
/// using appropriate IEC suffixes.
pub(crate) fn byte_count_iec(size: i64) -> String {
    let suffixes = [
        " KiB", " MiB", " GiB", " TiB", " PiB", " EiB", " ZiB", " YiB",
    ];
    byte_count(size, " byte", " bytes", &suffixes)
}

/// Convert a byte count to a human-readable representation of the byte count
/// using short suffixes.
#[allow(dead_code)]
pub(crate) fn byte_count_short(size: i64) -> String {
    byte_count(size, "", "", &["K", "M", "G", "T", "P", "E", "Z", "Y"])
}


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

//! Arguments for Commit IDs and Commit Identity Schemes

use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashSet;
use std::fmt;
use std::num::NonZeroU64;

use anyhow::format_err;
use anyhow::Error;
use clap::builder::ValueRange;
use clap::Arg;
use clap::ArgAction;
use clap::ArgGroup;
use clap::ArgMatches;
use clap::Args;
use faster_hex::hex_decode;
use faster_hex::hex_string;
use futures::future::try_join_all;
use futures::future::FutureExt;
use futures::stream::FuturesOrdered;
use futures::stream::TryStreamExt;
use source_control::types as thrift;

use crate::connection::Connection;

pub(crate) const ARG_COMMIT_ID: &str = "commit-id";
pub(crate) const ARG_BOOKMARK: &str = "bookmark";
pub(crate) const ARG_HG_COMMIT_ID: &str = "hg-commit-id";
pub(crate) const ARG_BONSAI_ID: &str = "bonsai-id";
pub(crate) const ARG_GIT_SHA1: &str = "git-sha1";
pub(crate) const ARG_GLOBALREV: &str = "globalrev";
pub(crate) const ARG_SVNREV: &str = "svnrev";
pub(crate) const ARG_BUBBLE_ID: &str = "bubble-id";
pub(crate) const ARG_SNAPSHOT_ID: &str = "snapshot-id";

#[derive(strum::EnumString, strum::Display, Clone, PartialEq, Eq, Hash)]
#[strum(serialize_all = "kebab-case")]
pub(crate) enum Scheme {
    Bonsai,
    Hg,
    Git,
    Globalrev,
    Svnrev,
}

impl Scheme {
    pub(crate) fn into_thrift(self) -> thrift::CommitIdentityScheme {
        match self {
            Self::Hg => thrift::CommitIdentityScheme::HG,
            Self::Bonsai => thrift::CommitIdentityScheme::BONSAI,
            Self::Git => thrift::CommitIdentityScheme::GIT,
            Self::Globalrev => thrift::CommitIdentityScheme::GLOBALREV,
            Self::Svnrev => thrift::CommitIdentityScheme::SVNREV,
        }
    }
}

#[derive(Args, Clone)]
pub(crate) struct SchemeArgs {
    #[clap(long, short('S'), default_value = "hg", value_delimiter = ',')]
    /// Commit identity schemes to display
    schemes: Vec<Scheme>,
}

impl SchemeArgs {
    pub(crate) fn into_request_schemes(self) -> BTreeSet<thrift::CommitIdentityScheme> {
        self.schemes.into_iter().map(Scheme::into_thrift).collect()
    }

    pub(crate) fn scheme_string_set(&self) -> HashSet<String> {
        self.schemes.iter().map(ToString::to_string).collect()
    }
}

/// Add arguments for specifying commit_ids.
///
/// The user can specify commit_ids by bookmark, hg commit ID, bonsai changeset ID, or
/// a generic "commit_id", for which we will try to work out which kind of identifier
/// the user has provided.
fn add_commit_id_args_impl(cmd: clap::Command, required: bool, multiple: bool) -> clap::Command {
    let num_args: ValueRange = if multiple { (1..).into() } else { 1.into() };
    cmd.arg(
        Arg::new(ARG_COMMIT_ID)
            .short('i')
            .long("commit-id")
            .num_args(num_args)
            .number_of_values(1)
            .action(ArgAction::Append)
            .help("Commit ID to query (bonsai or Hg)"),
    )
    .arg(
        Arg::new(ARG_BOOKMARK)
            .short('B')
            .long("bookmark")
            .num_args(num_args)
            .action(ArgAction::Append)
            .number_of_values(1)
            .help("Bookmark to query"),
    )
    .arg(
        Arg::new(ARG_HG_COMMIT_ID)
            .long("hg-commit-id")
            .num_args(num_args)
            .action(ArgAction::Append)
            .number_of_values(1)
            .help("Hg commit ID to query"),
    )
    .arg(
        Arg::new(ARG_BONSAI_ID)
            .long("bonsai-id")
            .num_args(num_args)
            .action(ArgAction::Append)
            .number_of_values(1)
            .help("Bonsai ID to query"),
    )
    .arg(
        Arg::new(ARG_SNAPSHOT_ID)
            .long("snapshot-id")
            .num_args(num_args)
            .action(ArgAction::Append)
            .number_of_values(1)
            .help("Snapshot ID to query"),
    )
    .arg(
        Arg::new(ARG_GIT_SHA1)
            .long("git")
            .num_args(num_args)
            .action(ArgAction::Append)
            .number_of_values(1)
            .help("Git SHA-1 to query"),
    )
    .arg(
        Arg::new(ARG_GLOBALREV)
            .long("globalrev")
            .num_args(num_args)
            .action(ArgAction::Append)
            .number_of_values(1)
            .help("Globalrev to query"),
    )
    .arg(
        Arg::new(ARG_SVNREV)
            .long("svnrev")
            .num_args(num_args)
            .action(ArgAction::Append)
            .number_of_values(1)
            .help("SVN revision to query"),
    )
    .arg(
        Arg::new(ARG_BUBBLE_ID)
            .long("bubble-id")
            .num_args(1)
            .number_of_values(1)
            .help("Bubble id on which to check the bonsai commits"),
    )
    .group(
        ArgGroup::new("commit")
            .args([
                ARG_COMMIT_ID,
                ARG_BOOKMARK,
                ARG_HG_COMMIT_ID,
                ARG_BONSAI_ID,
                ARG_SNAPSHOT_ID,
                ARG_GIT_SHA1,
                ARG_GLOBALREV,
                ARG_SVNREV,
            ])
            .multiple(multiple)
            .required(required),
    )
}

fn get_commit_ids(matches: &ArgMatches) -> Result<Vec<CommitId>, clap::Error> {
    get_commit_ids_impl(matches)
        .map_err(|err| clap::Error::raw(clap::error::ErrorKind::ValueValidation, err))
}

/// Get the commit ids specified by the user.  They are returned in the order specified.
fn get_commit_ids_impl(matches: &ArgMatches) -> Result<Vec<CommitId>, Error> {
    let bubble_id = matches
        .get_one::<String>(ARG_BUBBLE_ID)
        .map(|id| id.parse::<NonZeroU64>())
        .transpose()?;
    let mut commit_ids = BTreeMap::new();
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_BOOKMARK),
        matches.get_many::<String>(ARG_BOOKMARK),
    ) {
        for (index, value) in indices.zip(values) {
            commit_ids.insert(index, CommitId::Bookmark(value.clone()));
        }
    }
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_HG_COMMIT_ID),
        matches.get_many::<String>(ARG_HG_COMMIT_ID),
    ) {
        for (index, value) in indices.zip(values) {
            let mut id = [0; 20];
            hex_decode(value.as_bytes(), &mut id)?;
            commit_ids.insert(index, CommitId::HgId(id));
        }
    }
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_BONSAI_ID),
        matches.get_many::<String>(ARG_BONSAI_ID),
    ) {
        for (index, value) in indices.zip(values) {
            let mut id = [0; 32];
            hex_decode(value.as_bytes(), &mut id)?;
            commit_ids.insert(index, CommitId::BonsaiId(id));
        }
    }
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_SNAPSHOT_ID),
        matches.get_many::<String>(ARG_SNAPSHOT_ID),
    ) {
        for (index, value) in indices.zip(values) {
            let mut id = [0; 32];
            hex_decode(value.as_bytes(), &mut id)?;
            commit_ids.insert(index, CommitId::EphemeralBonsai(id, bubble_id));
        }
    }
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_GIT_SHA1),
        matches.get_many::<String>(ARG_GIT_SHA1),
    ) {
        for (index, value) in indices.zip(values) {
            let mut hash = [0; 20];
            hex_decode(value.as_bytes(), &mut hash)?;
            commit_ids.insert(index, CommitId::GitSha1(hash));
        }
    }
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_GLOBALREV),
        matches.get_many::<String>(ARG_GLOBALREV),
    ) {
        for (index, value) in indices.zip(values) {
            commit_ids.insert(index, CommitId::Globalrev(value.parse::<u64>()?));
        }
    }
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_SVNREV),
        matches.get_many::<String>(ARG_SVNREV),
    ) {
        for (index, value) in indices.zip(values) {
            commit_ids.insert(index, CommitId::Svnrev(value.parse::<u64>()?));
        }
    }
    if let (Some(indices), Some(values)) = (
        matches.indices_of(ARG_COMMIT_ID),
        matches.get_many::<String>(ARG_COMMIT_ID),
    ) {
        for (index, value) in indices.zip(values) {
            commit_ids.insert(index, CommitId::Resolve(value.to_string()));
        }
    }
    Ok(commit_ids.into_values().collect())
}

// Unfortunately we can't use clap derive API here directly because we care about
// the order of arguments, but we still implement proper clap traits so that it
// can be used in conjunction with derive API in other parts of the code.
#[derive(Clone)]
pub(crate) struct CommitIdArgs {
    commit_id: CommitId,
}

impl CommitIdArgs {
    pub fn into_commit_id(self) -> CommitId {
        self.commit_id
    }
}

impl clap::FromArgMatches for CommitIdArgs {
    fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
        let mut commit_ids = get_commit_ids(matches)?;
        if commit_ids.len() == 1 {
            Ok(CommitIdArgs {
                commit_id: commit_ids.pop().unwrap(),
            })
        } else {
            panic!("expected single commit id")
        }
    }

    fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
        let mut commit_ids = get_commit_ids(matches)?;
        use std::cmp::Ordering;
        match commit_ids.len().cmp(&1) {
            Ordering::Equal => {
                self.commit_id = commit_ids.pop().unwrap();
            }
            Ordering::Less => {}
            Ordering::Greater => panic!("expected single commit id"),
        }

        Ok(())
    }
}

impl clap::Args for CommitIdArgs {
    fn augment_args(cmd: clap::Command) -> clap::Command {
        add_commit_id_args_impl(cmd, true, false)
    }

    fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
        add_commit_id_args_impl(cmd, true, false)
    }
}

#[derive(Clone)]
pub(crate) struct OptionalCommitIdArgs {
    commit_id: Option<CommitId>,
}

impl OptionalCommitIdArgs {
    pub fn into_commit_id(self) -> Option<CommitId> {
        self.commit_id
    }
}

impl clap::FromArgMatches for OptionalCommitIdArgs {
    fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
        let commit_ids = get_commit_ids(matches)?;
        Ok(Self {
            commit_id: commit_ids.into_iter().next(),
        })
    }

    fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
        let commit_ids = get_commit_ids(matches)?;
        self.commit_id = commit_ids.into_iter().next();
        Ok(())
    }
}

impl clap::Args for OptionalCommitIdArgs {
    fn augment_args(cmd: clap::Command) -> clap::Command {
        add_commit_id_args_impl(cmd, false, false)
    }

    fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
        add_commit_id_args_impl(cmd, false, false)
    }
}

#[derive(Clone)]
/// 0 or more commit ids
pub(crate) struct CommitIdsArgs {
    commit_ids: Vec<CommitId>,
}

impl CommitIdsArgs {
    pub fn into_commit_ids(self) -> Vec<CommitId> {
        self.commit_ids
    }
}

impl clap::FromArgMatches for CommitIdsArgs {
    fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
        let commit_ids = get_commit_ids(matches)?;
        Ok(CommitIdsArgs { commit_ids })
    }

    fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
        self.commit_ids = get_commit_ids(matches)?;
        Ok(())
    }
}

impl clap::Args for CommitIdsArgs {
    fn augment_args(cmd: clap::Command) -> clap::Command {
        add_commit_id_args_impl(cmd, false, true)
    }

    fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
        add_commit_id_args_impl(cmd, false, true)
    }
}

/// A `CommitId` is any of the ways a user can specify a commit.
#[derive(Clone)]
pub(crate) enum CommitId {
    /// Commit ID is of an unknown type that must be resolved.
    Resolve(String),

    /// Bonsai ID.
    BonsaiId([u8; 32]),

    /// Bonsai ID with bubble
    EphemeralBonsai([u8; 32], Option<NonZeroU64>),

    /// Hg commit ID.
    HgId([u8; 20]),

    /// Git SHA-1.
    GitSha1([u8; 20]),

    /// Globalrev.
    Globalrev(u64),

    /// SVN revision.
    Svnrev(u64),

    /// A bookmark name.
    Bookmark(String),
}

impl CommitId {
    pub(crate) fn into_bookmark_name(self) -> Result<String, Error> {
        match self {
            Self::Bookmark(name) => Ok(name),
            _ => anyhow::bail!("expected bookmark name (got {})", self),
        }
    }
}

impl fmt::Display for CommitId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CommitId::Resolve(id) => write!(f, "commit id '{}'", id),
            CommitId::BonsaiId(bonsai) => write!(f, "bonsai id '{}'", hex_string(bonsai)),
            CommitId::EphemeralBonsai(bonsai, bubble_id) => write!(
                f,
                "bonsai id '{}' on bubble {}",
                hex_string(bonsai),
                bubble_id.map_or_else(|| "unknown".to_string(), |id| id.to_string()),
            ),
            CommitId::HgId(id) => write!(f, "hg commit id '{}'", hex_string(id)),
            CommitId::GitSha1(id) => write!(f, "git sha1 '{}'", hex_string(id)),
            CommitId::Globalrev(rev) => write!(f, "globalrev '{}'", rev),
            CommitId::Svnrev(rev) => write!(f, "svn revision '{}'", rev),
            CommitId::Bookmark(bookmark) => write!(f, "bookmark '{}'", bookmark),
        }
    }
}

/// Try to resolve a bookmark name to a commit ID.
async fn try_resolve_bookmark(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    bookmark: impl Into<String>,
) -> Result<Option<thrift::CommitId>, Error> {
    let params = thrift::RepoResolveBookmarkParams {
        bookmark_name: bookmark.into(),
        identity_schemes: Some(thrift::CommitIdentityScheme::BONSAI)
            .into_iter()
            .collect(),
        ..Default::default()
    };
    let response = conn.repo_resolve_bookmark(repo, &params).await?;
    Ok(response
        .ids
        .and_then(|ids| ids.get(&thrift::CommitIdentityScheme::BONSAI).cloned()))
}

/// Try to resolve a hex string to an hg commit ID (it can be prefix of the full hash)
async fn try_resolve_hg_commit_id(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    value: impl AsRef<str>,
) -> Result<Option<thrift::CommitId>, Error> {
    // Possible prefix should be valid to be passed to `repo_resolve_commit_prefix`:
    if value.as_ref().len() > 40 || value.as_ref().chars().any(|c| !c.is_ascii_hexdigit()) {
        return Ok(None);
    }

    let resp = conn
        .repo_resolve_commit_prefix(
            repo,
            &thrift::RepoResolveCommitPrefixParams {
                identity_schemes: Some(thrift::CommitIdentityScheme::BONSAI)
                    .into_iter()
                    .collect(),
                prefix_scheme: thrift::CommitIdentityScheme::HG,
                prefix: value.as_ref().into(),
                ..Default::default()
            },
        )
        .await?;

    if let thrift::RepoResolveCommitPrefixResponseType::AMBIGUOUS = resp.resolved_type {
        eprintln!(
            "note: several hg commits with the prefix '{}' exist",
            value.as_ref()
        );
    }

    Ok(resp
        .ids
        .and_then(|ids| ids.get(&thrift::CommitIdentityScheme::BONSAI).cloned()))
}

/// Try to resolve a hex string to a bonsai changeset ID (it can be prefix of the full hash)
async fn try_resolve_bonsai_id(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    value: impl AsRef<str>,
) -> Result<Option<thrift::CommitId>, Error> {
    // Possible prefix should be valid to be passed to `repo_resolve_commit_prefix`:
    if value.as_ref().len() > 64 || value.as_ref().chars().any(|c| !c.is_ascii_hexdigit()) {
        return Ok(None);
    }

    let resp = conn
        .repo_resolve_commit_prefix(
            repo,
            &thrift::RepoResolveCommitPrefixParams {
                identity_schemes: Some(thrift::CommitIdentityScheme::BONSAI)
                    .into_iter()
                    .collect(),
                prefix_scheme: thrift::CommitIdentityScheme::BONSAI,
                prefix: value.as_ref().into(),
                ..Default::default()
            },
        )
        .await?;

    if let thrift::RepoResolveCommitPrefixResponseType::AMBIGUOUS = resp.resolved_type {
        eprintln!(
            "note: several bonsai commits with the prefix '{}' exist",
            value.as_ref()
        );
    }

    Ok(resp
        .ids
        .and_then(|ids| ids.get(&thrift::CommitIdentityScheme::BONSAI).cloned()))
}

/// Try to resolve a git sha1 string to a commit ID.
async fn try_resolve_git_sha1(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    value: impl AsRef<str>,
) -> Result<Option<thrift::CommitId>, Error> {
    let mut id = [0; 20];
    if hex_decode(value.as_ref().as_bytes(), &mut id).is_err() {
        return Ok(None);
    }
    let params = thrift::CommitLookupParams {
        identity_schemes: Some(thrift::CommitIdentityScheme::BONSAI)
            .into_iter()
            .collect(),
        ..Default::default()
    };
    let commit = thrift::CommitSpecifier {
        repo: repo.clone(),
        id: thrift::CommitId::git(id.to_vec()),
        ..Default::default()
    };
    let response = conn.commit_lookup(&commit, &params).await?;
    Ok(response
        .ids
        .and_then(|ids| ids.get(&thrift::CommitIdentityScheme::BONSAI).cloned()))
}

/// Try to resolve a globalrev string to a commit ID.
async fn try_resolve_globalrev(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    globalrev: impl AsRef<str>,
) -> Result<Option<thrift::CommitId>, Error> {
    match globalrev.as_ref().parse::<i64>() {
        Ok(globalrev) => {
            let params = thrift::CommitLookupParams {
                identity_schemes: Some(thrift::CommitIdentityScheme::BONSAI)
                    .into_iter()
                    .collect(),
                ..Default::default()
            };
            let commit = thrift::CommitSpecifier {
                repo: repo.clone(),
                id: thrift::CommitId::globalrev(globalrev),
                ..Default::default()
            };
            let response = conn.commit_lookup(&commit, &params).await?;
            Ok(response
                .ids
                .and_then(|ids| ids.get(&thrift::CommitIdentityScheme::BONSAI).cloned()))
        }
        Err(_) => Ok(None),
    }
}

/// Try to resolve a svn revision number to a commit ID.
async fn try_resolve_svnrev(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    globalrev: impl AsRef<str>,
) -> Result<Option<thrift::CommitId>, Error> {
    match globalrev.as_ref().parse::<i64>() {
        Ok(globalrev) => {
            let params = thrift::CommitLookupParams {
                identity_schemes: Some(thrift::CommitIdentityScheme::BONSAI)
                    .into_iter()
                    .collect(),
                ..Default::default()
            };
            let commit = thrift::CommitSpecifier {
                repo: repo.clone(),
                id: thrift::CommitId::svnrev(globalrev),
                ..Default::default()
            };
            let response = conn.commit_lookup(&commit, &params).await?;
            Ok(response
                .ids
                .and_then(|ids| ids.get(&thrift::CommitIdentityScheme::BONSAI).cloned()))
        }
        Err(_) => Ok(None),
    }
}

/// Resolve commit IDs into a thrift commit identifier.
///
/// Simple commit ID types (hashes of a known type) are converted to the right
/// commit ID variant.
///
/// Other commit ID types, like bookmark names or hashes of an unknown type may
/// involve a call to the server to resolve.
pub(crate) async fn resolve_commit_ids(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    commit_ids: impl IntoIterator<Item = &CommitId>,
) -> Result<Vec<thrift::CommitId>, Error> {
    commit_ids
        .into_iter()
        .map(|commit_id| {
            async move {
                match commit_id {
                    CommitId::BonsaiId(bonsai) => Ok(thrift::CommitId::bonsai(bonsai.to_vec())),
                    CommitId::EphemeralBonsai(bonsai, bubble) => Ok(
                        thrift::CommitId::ephemeral_bonsai(thrift::EphemeralBonsai {
                            bonsai_id: bonsai.to_vec(),
                            bubble_id: bubble.map_or(0, NonZeroU64::get).try_into()?,
                            ..Default::default()
                        }),
                    ),
                    CommitId::HgId(hg) => Ok(thrift::CommitId::hg(hg.to_vec())),
                    CommitId::GitSha1(hash) => Ok(thrift::CommitId::git(hash.to_vec())),
                    CommitId::Globalrev(rev) => Ok(thrift::CommitId::globalrev((*rev).try_into()?)),
                    CommitId::Svnrev(rev) => Ok(thrift::CommitId::svnrev((*rev).try_into()?)),
                    CommitId::Bookmark(bookmark) => try_resolve_bookmark(conn, repo, bookmark)
                        .await?
                        .ok_or_else(|| format_err!("bookmark not found: {}", bookmark)),
                    CommitId::Resolve(commit_id) => {
                        let resolvers = vec![
                            try_resolve_bonsai_id(conn, repo, commit_id).boxed(),
                            try_resolve_hg_commit_id(conn, repo, commit_id).boxed(),
                            try_resolve_git_sha1(conn, repo, commit_id).boxed(),
                            try_resolve_globalrev(conn, repo, commit_id).boxed(),
                            try_resolve_svnrev(conn, repo, commit_id).boxed(),
                        ];
                        let candidates: Vec<_> = try_join_all(resolvers.into_iter())
                            .await?
                            .into_iter()
                            .flatten()
                            .collect();
                        match candidates.as_slice() {
                            [] => Err(format_err!("commit not found: {}", commit_id)),
                            [id] => Ok(id.clone()),
                            _ => {
                                // This commit ID resolves to different
                                // commits in different schemes. This is a
                                // cross-scheme hash collision (e.g. two
                                // commits exist where the Hg commit ID of
                                // one matches the Git commit ID of the
                                // other).
                                //
                                // In practice this should be very rare, but
                                // handle it here. Users will need to
                                // specify which scheme should be used by
                                // using the appropriate argument to specify
                                // the ID (e.g. --hg-commit-id).
                                Err(format_err!("ambiguous commit id: {}", commit_id))
                            }
                        }
                    }
                }
            }
        })
        .collect::<FuturesOrdered<_>>()
        .try_collect()
        .await
}

/// Resolve a single commit ID.
pub(crate) async fn resolve_commit_id(
    conn: &Connection,
    repo: &thrift::RepoSpecifier,
    commit_id: &CommitId,
) -> Result<thrift::CommitId, Error> {
    let commit_ids = resolve_commit_ids(conn, repo, Some(commit_id).into_iter()).await?;
    Ok(commit_ids.into_iter().next().expect("commit id expected"))
}

/// Map commit IDs to the scheme name and string representation of the commit ID.
pub(crate) fn map_commit_ids<'a>(
    ids: impl Iterator<Item = &'a thrift::CommitId>,
) -> BTreeMap<String, String> {
    ids.filter_map(map_commit_id).collect()
}

/// Map a commit ID to its scheme name and string representation.
pub(crate) fn map_commit_id(id: &thrift::CommitId) -> Option<(String, String)> {
    match id {
        thrift::CommitId::bonsai(hash) => Some((String::from("bonsai"), hex_string(hash))),
        thrift::CommitId::hg(hash) => Some((String::from("hg"), hex_string(hash))),
        thrift::CommitId::git(hash) => Some((String::from("git"), hex_string(hash))),
        thrift::CommitId::globalrev(rev) => Some((String::from("globalrev"), rev.to_string())),
        thrift::CommitId::svnrev(rev) => Some((String::from("svnrev"), rev.to_string())),
        _ => None,
    }
}


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

use source_control::types as thrift;

#[derive(clap::Args, Clone)]
pub(crate) struct DerivedDataTypeArgs {
    #[clap(long, short)]
    /// Type of derived data to derive
    derived_data_type: thrift::DerivedDataType,
}

impl DerivedDataTypeArgs {
    pub fn into_derived_data_type(self) -> thrift::DerivedDataType {
        self.derived_data_type
    }
}


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

pub(crate) mod commit_id;
pub(crate) mod derived_data_type;
pub(crate) mod path;
pub(crate) mod progress;
pub(crate) mod pushvars;
pub(crate) mod repo;
pub(crate) mod service_id;
pub(crate) mod sparse_profiles;


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

//! Arguments for path selection.

#[derive(clap::Args, Clone)]
pub(crate) struct PathArgs {
    #[clap(long, short)]
    /// Path
    pub path: String,
}


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

//! Adapters for rendering progress

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

use anyhow::Result;
use futures::stream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;

use crate::render::Render;

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ProgressOutput {
    message: String,
    value: u64,
    total: u64,
}

#[derive(clap::Parser)]
pub(crate) struct ProgressArgs {
    #[clap(long)]
    /// Don't show the progress bar
    no_progress: bool,
}

impl ProgressOutput {
    pub(crate) fn new(message: String, value: u64, total: u64) -> Self {
        Self {
            message,
            value,
            total,
        }
    }
}

enum ProgressItem<I: Render<Args = ()>> {
    Item(Result<I>),
    Complete,
    Timer,
}

pub(crate) enum ProgressRender<I: Render<Args = ()>> {
    Item(I),
    Suspend,
    Clear,
    Output(ProgressOutput),
}

impl ProgressArgs {
    pub(crate) fn render_progress<I: Render<Args = ()> + 'static>(
        &self,
        render_stream: impl Stream<Item = Result<I>> + Send + 'static,
        get_progress: impl Fn() -> ProgressOutput + Send + Clone + 'static,
    ) -> impl Stream<Item = Result<ProgressRender<I>>> + Send + 'static {
        if self.no_progress {
            return render_stream.map_ok(ProgressRender::Item).left_stream();
        }

        let render_stream = render_stream
            .map(ProgressItem::Item)
            .chain(stream::once(async { ProgressItem::Complete }));

        let timer = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
            Duration::from_millis(100),
        ))
        .map(|_| ProgressItem::Timer);

        stream::select(timer, render_stream)
            .take_while(|item| {
                let complete = match item {
                    ProgressItem::Complete => true,
                    _ => false,
                };
                async move { !complete }
            })
            .chain(stream::once(async { ProgressItem::Complete }))
            .map({
                let mut last_progress = None;
                move |item| match item {
                    ProgressItem::Item(value) => {
                        if let Some(progress) = last_progress.clone() {
                            stream::iter(vec![
                                Ok(ProgressRender::Suspend),
                                value.map(ProgressRender::Item),
                                Ok(ProgressRender::Output(progress)),
                            ])
                            .boxed()
                        } else {
                            stream::once(async { value.map(ProgressRender::Item) }).boxed()
                        }
                    }
                    ProgressItem::Timer => {
                        let progress = get_progress();
                        if last_progress.as_ref() != Some(&progress) {
                            last_progress = Some(progress.clone());
                            stream::once(async move { Ok(ProgressRender::Output(progress)) })
                                .boxed()
                        } else {
                            stream::empty().boxed()
                        }
                    }
                    ProgressItem::Complete => {
                        if last_progress.is_some() {
                            last_progress = None;
                            stream::once(async { Ok(ProgressRender::Clear) }).boxed()
                        } else {
                            stream::empty().boxed()
                        }
                    }
                }
            })
            .flatten()
            .right_stream()
    }
}

impl<I: Render<Args = ()>> Render for ProgressRender<I> {
    type Args = ();

    fn render_tty(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        match self {
            Self::Item(i) => i.render_tty(args, w)?,
            Self::Output(out) => {
                let width = textwrap::termwidth().saturating_sub(5);
                if width > 10 {
                    let bar_width = width * out.value as usize / out.total as usize;
                    let bar_text = format!("  {:width$}  ", out.message, width = width);
                    write!(
                        w,
                        "\r\x1B[K\x1B[42;30m{0}\x1B[m{1}\r",
                        &bar_text[..bar_width],
                        &bar_text[bar_width..]
                    )?;
                    w.flush()?;
                }
            }
            Self::Suspend => write!(w, "\r\x1B[K")?,
            Self::Clear => {
                write!(w, "\r\x1B[K")?;
                w.flush()?;
            }
        };
        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.
 */

//! Arguments for pushvars.

use std::collections::BTreeMap;

use anyhow::Result;

#[derive(Clone)]
struct PushvarEntry(String, String);

impl std::str::FromStr for PushvarEntry {
    type Err = anyhow::Error;

    fn from_str(txt: &str) -> Result<Self> {
        let mut pushvar_parts = txt.splitn(2, '=');
        match (pushvar_parts.next(), pushvar_parts.next()) {
            (Some(name), Some(value)) => Ok(Self(name.to_string(), value.to_string())),
            _ => {
                anyhow::bail!(
                    "Pushvar specification must be of the form 'name=value', received '{}'",
                    txt
                )
            }
        }
    }
}

#[derive(clap::Args, Clone)]
/// Add arguments for specifying pushvars.
pub(crate) struct PushvarArgs {
    #[clap(long)]
    /// Pushvar (name=value) to send with write operation
    pushvar: Vec<PushvarEntry>,
}

impl PushvarArgs {
    /// Get specified pushvars.
    pub(crate) fn into_pushvars(self) -> Option<BTreeMap<String, Vec<u8>>> {
        if !self.pushvar.is_empty() {
            Some(
                self.pushvar
                    .into_iter()
                    .map(|e| (e.0, e.1.into_bytes()))
                    .collect(),
            )
        } else {
            None
        }
    }
}


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

//! Arguments for repository selection.

use source_control::types as thrift;

#[derive(clap::Args, Clone)]
pub(crate) struct RepoArgs {
    #[clap(long, short = 'R')]
    /// Repository name
    repo: String,
}

impl RepoArgs {
    pub fn into_repo_specifier(self) -> thrift::RepoSpecifier {
        thrift::RepoSpecifier {
            name: self.repo,
            ..Default::default()
        }
    }
}


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

//! Arguments for Service Identities

#[derive(clap::Args)]
pub(crate) struct ServiceIdArgs {
    #[clap(long)]
    /// Service identity to perform write operation as
    pub(crate) service_id: Option<String>,
}


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

use source_control::types as thrift;

#[derive(clap::Args, Clone)]
pub(crate) struct SparseProfilesArgs {
    #[clap(long, short = 'P', value_delimiter = ',')]
    /// Sparse profiles to calculate the size for (defaults to all profiles)
    sparse_profiles: Vec<String>,
}

impl SparseProfilesArgs {
    pub fn into_sparse_profiles(self) -> thrift::SparseProfiles {
        if self.sparse_profiles.is_empty() {
            thrift::SparseProfiles::all_profiles(thrift::AllSparseProfiles {
                ..Default::default()
            })
        } else {
            thrift::SparseProfiles::profiles(self.sparse_profiles)
        }
    }
}


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

//! Fetch the blame of a file

use std::fmt::Write as _;
use std::io::Write;

use anyhow::format_err;
use anyhow::Result;
use clap::Parser;
use maplit::btreeset;
use serde_json::json;
use source_control::types as thrift;
use unicode_truncate::Alignment;
use unicode_truncate::UnicodeTruncateStr;
use unicode_width::UnicodeWidthStr;

use crate::args::commit_id::map_commit_ids;
use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitIdArgs;
use crate::args::commit_id::SchemeArgs;
use crate::args::path::PathArgs;
use crate::args::repo::RepoArgs;
use crate::library::commit_id::render_commit_id;
use crate::library::datetime;
use crate::render::Render;
use crate::ScscApp;

const DEFAULT_TITLE_WIDTH: usize = 32;

#[derive(Parser)]
/// Fetch the blame of a file
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(flatten)]
    path_args: PathArgs,

    #[clap(long)]
    /// Show blame for the first parent of the commit
    parent: bool,
    #[clap(long, conflicts_with = "parent")]
    /// Show blame for the Nth parent of the commit
    parent_index: Option<usize>,
    #[clap(long, short)]
    /// List the author
    user: bool,
    #[clap(long, short)]
    /// List the date
    date: bool,
    #[clap(short = 'q')]
    /// List the date in short format
    date_short: bool,
    #[clap(long, short)]
    /// Show current line number
    line_number: bool,
    #[clap(long, short)]
    /// Show origin line number
    origin_line_number: bool,
    #[clap(long, short = 'O')]
    /// Show origin path if different from current path
    origin_path: bool,
    #[clap(long, short = 'P')]
    /// Show the line range in the parent this line replaces
    parent_line_range: bool,
    #[clap(long, short = 'T')]
    /// Show the title (first line of the commit message) of the blamed changeset
    title: bool,
    #[clap(long, default_value_t = DEFAULT_TITLE_WIDTH)]
    /// Set thc maxiucbdrkccuefmum width of the title (if shown)
    title_width: usize,
    #[clap(long, short = 'n')]
    /// Show numbers for commits (specific to this blame revision)
    commit_number: bool,
    #[clap(long)]
    /// Do not show commit ids
    no_commit_id: bool,
    #[clap(long)]
    /// Follow mutable overrides to the history that make it more user friendly and 'correct'
    follow_mutable_history: bool,
}

struct BlameOut {
    blame: thrift::Blame,
}

impl Render for BlameOut {
    type Args = CommandArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        let schemes = args.scheme_args.scheme_string_set();
        let path = args.path_args.path.clone();
        let title_width = args.title_width;
        let number_width = |n| ((n + 1) as f32).log10().ceil() as usize;

        match self.blame {
            thrift::Blame::blame_compact(ref blame) => {
                let max_commit_number_width = number_width(
                    blame
                        .commit_numbers
                        .iter()
                        .flatten()
                        .copied()
                        .max()
                        .unwrap_or(0),
                );
                let max_author_width = blame.authors.iter().map(|a| a.width()).max().unwrap_or(0);
                let max_line_width =
                    number_width(blame.lines.iter().map(|l| l.line).max().unwrap_or(0) + 1);

                let max_origin_line_width =
                    number_width(blame.lines.iter().map(|l| l.origin_line).max().unwrap_or(0));
                let max_origin_path_width = blame
                    .lines
                    .iter()
                    .map(|l| {
                        let origin_path = blame.paths[l.path_index as usize].as_str();
                        if origin_path != path {
                            origin_path.width()
                        } else {
                            0
                        }
                    })
                    .max()
                    .unwrap_or(0);
                let max_parent_line_range_width = blame
                    .lines
                    .iter()
                    .map(|l| {
                        let parent_index_width = l
                            .parent_index
                            .map_or(0, |i| if i > 0 { number_width(i) + 2 } else { 0 });
                        let parent_path_width = l
                            .parent_path_index
                            .map_or(0, |p| blame.paths[p as usize].width() + 2);
                        let parent_range_width = match (l.parent_start_line, l.parent_range_length)
                        {
                            (Some(start), Some(0)) => number_width(start) + 1,
                            (Some(start), Some(len)) => {
                                number_width(start) + number_width(start + len - 1) + 1
                            }
                            _ => 0,
                        };
                        parent_index_width + parent_path_width + parent_range_width
                    })
                    .max()
                    .unwrap_or(0);

                for line in blame.lines.iter() {
                    let mut separator = "";
                    if args.user {
                        let author = blame.authors[line.author_index as usize].as_str();
                        write!(
                            w,
                            "{}",
                            author.unicode_pad(max_author_width, Alignment::Right, false),
                        )?;
                        separator = " ";
                    }
                    if args.commit_number {
                        if let Some(commit_numbers) = &blame.commit_numbers {
                            let commit_number =
                                format!("#{}", commit_numbers[line.commit_id_index as usize]);
                            write!(
                                w,
                                "{}{:>width$}",
                                separator,
                                commit_number,
                                width = max_commit_number_width + 1
                            )?
                        }
                        separator = " ";
                    }
                    if !args.no_commit_id {
                        write!(w, "{}", separator)?;
                        render_commit_id(
                            None,
                            " ",
                            "blamed changeset",
                            &map_commit_ids(
                                blame.commit_ids[line.commit_id_index as usize].values(),
                            ),
                            &schemes,
                            w,
                        )?;
                        separator = " ";
                    }
                    if args.date || args.date_short {
                        let blame_date = datetime(&blame.dates[line.date_index as usize]);
                        let blame_date_formatted = if args.date_short {
                            blame_date.format("%F")
                        } else {
                            blame_date.format("%+")
                        };
                        write!(w, "{}{}", separator, blame_date_formatted)?;
                        separator = " ";
                    }
                    if !separator.is_empty() {
                        separator = ":";
                    }
                    if args.title {
                        let title = match line.title_index {
                            Some(title_index) => match blame.titles.as_ref() {
                                Some(titles) => titles[title_index as usize].as_str(),
                                None => "",
                            },
                            None => "",
                        };
                        write!(
                            w,
                            "{}{}",
                            separator,
                            title.unicode_pad(title_width, Alignment::Left, true)
                        )?;
                        separator = ":";
                    }
                    if args.parent_line_range {
                        let mut plr = String::with_capacity(max_parent_line_range_width);
                        if let Some(parent_index) = line.parent_index {
                            if parent_index != 0 {
                                write!(plr, "({})", parent_index)?;
                            }
                        }
                        if let Some(path_index) = line.parent_path_index {
                            write!(plr, "[{}]", blame.paths[path_index as usize])?;
                        }
                        if let (Some(start), Some(length)) =
                            (line.parent_start_line, line.parent_range_length)
                        {
                            if length == 0 {
                                write!(plr, "+{}", start)?;
                            } else {
                                write!(plr, "{}-{}", start, start + length - 1)?;
                            }
                        }
                        write!(
                            w,
                            "{}{}",
                            separator,
                            plr.unicode_pad(max_parent_line_range_width, Alignment::Right, false)
                        )?;
                        separator = ":";
                    }
                    if args.origin_path {
                        let origin_path = blame.paths[line.path_index as usize].as_str();
                        write!(
                            w,
                            "{}{}",
                            separator,
                            if origin_path != path { origin_path } else { "" }.unicode_pad(
                                max_origin_path_width,
                                Alignment::Right,
                                false
                            )
                        )?;
                        separator = ":";
                    }
                    if args.origin_line_number {
                        write!(
                            w,
                            "{}{:>width$}",
                            separator,
                            line.origin_line,
                            width = max_origin_line_width
                        )?;
                        separator = ":";
                    }
                    if args.line_number {
                        write!(
                            w,
                            "{}{:>width$}",
                            separator,
                            line.line,
                            width = max_line_width
                        )?;
                        separator = ":";
                    }
                    if !separator.is_empty() {
                        separator = ": ";
                    }
                    write!(
                        w,
                        "{}{}\n",
                        separator,
                        line.contents.as_deref().unwrap_or_default()
                    )?;
                }
                Ok(())
            }
            thrift::Blame::UnknownField(id) => {
                Err(format_err!("Unknown thrift::Blame field id: {}", id))
            }
        }
    }

    fn render_json(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        match self.blame {
            thrift::Blame::blame_compact(ref blame) => {
                let mut lines = Vec::new();
                let use_short_date = args.date_short;
                for line in blame.lines.iter() {
                    let blame_date = datetime(&blame.dates[line.date_index as usize]);
                    let formatted_blame_date = if use_short_date {
                        blame_date.format("%F")
                    } else {
                        blame_date.format("%+")
                    };
                    let mut line_json = json!({
                        "contents": line.contents.as_deref().unwrap_or_default(),
                        "commit": map_commit_ids(blame.commit_ids[line.commit_id_index as usize].values()),
                        "path": blame.paths[line.path_index as usize],
                        "line": line.line,
                        "author": blame.authors[line.author_index as usize],
                        "datetime": formatted_blame_date.to_string(),
                        "origin_line": line.origin_line,
                    });
                    let mut insert = |key, value| {
                        line_json
                            .as_object_mut()
                            .expect("line must be an object")
                            .insert(String::from(key), value);
                    };
                    if let Some(commit_numbers) = &blame.commit_numbers {
                        insert(
                            "commit_number",
                            commit_numbers[line.commit_id_index as usize].into(),
                        );
                    }
                    if let (Some(title_index), Some(titles)) =
                        (line.title_index, blame.titles.as_ref())
                    {
                        insert("title", titles[title_index as usize].clone().into());
                    }
                    if let Some(parent_index) = line.parent_index {
                        insert("parent_index", parent_index.into());
                        if let Some(parent_commit_ids) = &blame.parent_commit_ids {
                            let parents = &parent_commit_ids[line.commit_id_index as usize];
                            insert(
                                "parent",
                                json!(map_commit_ids(parents[parent_index as usize].values())),
                            );
                        }
                    }
                    if let Some(path_index) = line.parent_path_index {
                        insert(
                            "parent_path",
                            blame.paths[path_index as usize].clone().into(),
                        );
                    }
                    if let (Some(start), Some(length)) =
                        (line.parent_start_line, line.parent_range_length)
                    {
                        insert("parent_start_line", start.into());
                        insert("parent_range_length", length.into());
                    }
                    lines.push(line_json);
                }
                Ok(serde_json::to_writer(w, &lines)?)
            }
            thrift::Blame::UnknownField(id) => {
                Err(format_err!("Unknown thrift::Blame field id: {}", id))
            }
        }
    }
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.clone().into_repo_specifier();
    let conn = app.get_connection(Some(&repo.name))?;
    let commit_id = args.commit_id_args.clone().into_commit_id();
    let id = resolve_commit_id(&conn, &repo, &commit_id).await?;

    let mut commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };

    let parent_index = if args.parent {
        Some(0)
    } else {
        args.parent_index
    };

    if let Some(parent_index) = parent_index {
        let params = thrift::CommitInfoParams {
            identity_schemes: btreeset! { thrift::CommitIdentityScheme::BONSAI },
            ..Default::default()
        };
        let response = conn.commit_info(&commit, &params).await?;
        commit.id.clone_from(
            response
                .parents
                .get(parent_index)
                .ok_or_else(|| {
                    format_err!("Commit does not have a parent with index {}", parent_index)
                })?
                .get(&thrift::CommitIdentityScheme::BONSAI)
                .ok_or_else(|| {
                    format_err!(
                        "Could not determine ID of commit's parent with index {}",
                        parent_index
                    )
                })?,
        );
    }
    let path = args.path_args.path.clone();
    let commit_and_path = thrift::CommitPathSpecifier {
        commit,
        path,
        ..Default::default()
    };

    let identity_schemes = args.scheme_args.clone().into_request_schemes();
    let follow_mutable_file_history = Some(args.follow_mutable_history);

    let params = thrift::CommitPathBlameParams {
        format: thrift::BlameFormat::COMPACT,
        identity_schemes,
        format_options: Some(btreeset! {
            thrift::BlameFormatOption::INCLUDE_CONTENTS,
            thrift::BlameFormatOption::INCLUDE_TITLE,
            thrift::BlameFormatOption::INCLUDE_PARENT,
            thrift::BlameFormatOption::INCLUDE_COMMIT_NUMBERS,
        }),
        follow_mutable_file_history,
        ..Default::default()
    };
    let response = conn.commit_path_blame(&commit_and_path, &params).await?;
    app.target
        .render_one(
            &args,
            BlameOut {
                blame: response.blame,
            },
        )
        .await
}


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

//! Fetch the contents of a file.

use std::io::Write;

use anyhow::Result;
use clap::Parser;
use futures::future;
use futures::stream;
use futures::stream::StreamExt;
use futures::TryFutureExt;
use serde_json::json;
use source_control::types as thrift;

use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitIdArgs;
use crate::args::path::PathArgs;
use crate::args::repo::RepoArgs;
use crate::render::Render;
use crate::ScscApp;

/// Chunk size for requests.
const CHUNK_SIZE: i64 = source_control::FILE_CONTENT_CHUNK_RECOMMENDED_SIZE;

/// Number of concurrent fetches for very large files.
const CONCURRENT_FETCHES: usize = 10;

#[derive(Parser)]
/// Fetch the contents of a file
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(flatten)]
    path_args: PathArgs,
}

struct CatOutput {
    offset: u64,
    data: Vec<u8>,
}

impl Render for CatOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        w.write_all(self.data.as_slice())?;
        Ok(())
    }

    fn render_json(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        let output = match std::str::from_utf8(self.data.as_slice()) {
            Ok(data) => json!({
                "offset": self.offset,
                "data": data,
            }),
            Err(_) => json!({
                "offset": self.offset,
                "hex": faster_hex::hex_string(self.data.as_slice())
            }),
        };
        Ok(serde_json::to_writer(w, &output)?)
    }
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.clone().into_repo_specifier();
    let conn = app.get_connection(Some(&repo.name))?;
    let commit_id = args.commit_id_args.clone().into_commit_id();
    let id = resolve_commit_id(&conn, &repo, &commit_id).await?;
    let commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let path = args.path_args.path.clone();
    let file = thrift::FileSpecifier::by_commit_path(thrift::CommitPathSpecifier {
        commit,
        path,
        ..Default::default()
    });

    // Request the first chunk of the file.
    let params = thrift::FileContentChunkParams {
        offset: 0,
        size: CHUNK_SIZE,
        ..Default::default()
    };
    let response = conn.file_content_chunk(&file, &params).await?;
    let output = CatOutput {
        offset: response.offset as u64,
        data: response.data,
    };

    let file_size = response.file_size;
    let stream = stream::once(future::ok(output)).chain(
        stream::iter((CHUNK_SIZE..file_size).step_by(CHUNK_SIZE as usize))
            .map(move |offset| {
                let params = thrift::FileContentChunkParams {
                    offset,
                    size: CHUNK_SIZE,
                    ..Default::default()
                };
                conn.file_content_chunk(&file, &params)
                    .map_err(anyhow::Error::from)
            })
            .buffered(CONCURRENT_FETCHES)
            .then(|response| {
                future::ready(response.map(|response| CatOutput {
                    offset: response.offset as u64,
                    data: response.data,
                }))
            }),
    );
    app.target.render(&args, stream).await
}


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

//! Find common base of two commits

use std::collections::BTreeMap;
use std::io::Write;

use anyhow::bail;
use anyhow::Result;
use serde::Serialize;
use source_control::types as thrift;

use crate::args::commit_id::map_commit_ids;
use crate::args::commit_id::resolve_commit_ids;
use crate::args::commit_id::CommitIdsArgs;
use crate::args::commit_id::SchemeArgs;
use crate::args::repo::RepoArgs;
use crate::library::commit_id::render_commit_id;
use crate::render::Render;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Finds a common base of two commits.
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,
}

#[derive(Serialize)]
struct CommonBaseOutput {
    #[serde(skip)]
    pub requested: (String, String),
    pub exists: bool,
    pub ids: BTreeMap<String, String>,
}

impl Render for CommonBaseOutput {
    type Args = CommandArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        if self.exists {
            let schemes = args.scheme_args.scheme_string_set();
            render_commit_id(None, "\n", "common base", &self.ids, &schemes, w)?;
            write!(w, "\n")?;
        } else {
            bail!(
                "a common ancestor of {} and {} does not exist\n",
                self.requested.0,
                self.requested.1
            );
        }
        Ok(())
    }

    fn render_json(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        Ok(serde_json::to_writer(w, self)?)
    }
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.clone().into_repo_specifier();
    let commit_ids = args.commit_ids_args.clone().into_commit_ids();
    let conn = app.get_connection(Some(&repo.name))?;
    let ids = resolve_commit_ids(&conn, &repo, &commit_ids).await?;
    let ids = match ids.as_slice() {
        [id0, id1] => (id0.clone(), id1.clone()),
        _ => bail!("expected 2 commit_ids (got {})", commit_ids.len()),
    };
    let commit = thrift::CommitSpecifier {
        repo,
        id: ids.0,
        ..Default::default()
    };
    let params = thrift::CommitCommonBaseWithParams {
        other_commit_id: ids.1,
        identity_schemes: args.scheme_args.clone().into_request_schemes(),
        ..Default::default()
    };
    let response = conn.commit_common_base_with(&commit, &params).await?;
    let ids = match &response.ids {
        Some(ids) => map_commit_ids(ids.values()),
        None => BTreeMap::new(),
    };
    let output = CommonBaseOutput {
        requested: (commit_ids[0].to_string(), commit_ids[1].to_string()),
        exists: response.exists,
        ids,
    };
    app.target.render_one(&args, output).await
}


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

use anyhow::Result;
use source_control::types as thrift;

use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitIdArgs;
use crate::args::pushvars::PushvarArgs;
use crate::args::repo::RepoArgs;
use crate::args::service_id::ServiceIdArgs;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Create a bookmark
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(flatten)]
    service_id_args: ServiceIdArgs,
    #[clap(flatten)]
    pushvar_args: PushvarArgs,
    #[clap(long, short)]
    /// Name of the bookmark to create
    name: String,
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.clone().into_repo_specifier();
    let commit_id = args.commit_id_args.clone().into_commit_id();
    let conn = app.get_connection(Some(&repo.name))?;
    let id = resolve_commit_id(&conn, &repo, &commit_id).await?;
    let bookmark = args.name.clone();
    let service_identity = args.service_id_args.service_id.clone();
    let pushvars = args.pushvar_args.into_pushvars();

    let params = thrift::RepoCreateBookmarkParams {
        bookmark,
        target: id,
        service_identity,
        pushvars,
        ..Default::default()
    };
    conn.repo_create_bookmark(&repo, &params).await?;
    Ok(())
}


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

use anyhow::Result;
use source_control::types as thrift;

use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::OptionalCommitIdArgs;
use crate::args::pushvars::PushvarArgs;
use crate::args::repo::RepoArgs;
use crate::args::service_id::ServiceIdArgs;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Delete a bookmark
///
/// If a commit id is provided, the bookmark is only deleted if it currently
/// points at that commit.
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    service_id_args: ServiceIdArgs,
    #[clap(flatten)]
    pushvar_args: PushvarArgs,
    #[clap(flatten)]
    commit_id_args: OptionalCommitIdArgs,
    #[clap(long, short)]
    /// Name of the bookmark to delete
    name: String,
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.into_repo_specifier();
    let commit_id = args.commit_id_args.into_commit_id();
    let conn = app.get_connection(Some(&repo.name))?;
    let old_target = match commit_id {
        Some(commit_id) => Some(resolve_commit_id(&conn, &repo, &commit_id).await?),
        None => None,
    };
    let bookmark = args.name;
    let service_identity = args.service_id_args.service_id;
    let pushvars = args.pushvar_args.into_pushvars();

    let params = thrift::RepoDeleteBookmarkParams {
        bookmark,
        old_target,
        service_identity,
        pushvars,
        ..Default::default()
    };
    conn.repo_delete_bookmark(&repo, &params).await?;
    Ok(())
}


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

//! Diff two commits

use std::collections::BTreeSet;
use std::io::Write;

use anyhow::bail;
use anyhow::Result;
use clap::ValueEnum;
use maplit::btreeset;
use serde::Serialize;
use source_control as thrift;

use crate::args::commit_id::resolve_commit_ids;
use crate::args::commit_id::CommitIdsArgs;
use crate::args::repo::RepoArgs;
use crate::library::diff::diff_files;
use crate::render::Render;
use crate::ScscApp;

#[derive(ValueEnum, Clone, Copy, Debug)]
enum DiffFormat {
    RawDiff,
    MetadataDiff,
}

#[derive(clap::Parser)]
/// Diff files between two commits
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,
    #[clap(long, short)]
    /// Paths to diff.
    path: Option<Vec<String>>,
    #[clap(long)]
    /// Show copies/moves as adds/deletes.
    skip_copies_renames: bool,
    #[clap(long)]
    /// Only list differing paths instead of printing the diff.
    paths_only: bool,
    #[clap(long, conflicts_with = "paths_only")]
    /// Instead of generating a real diff let's generate a placeholder diff that just says that file differs.
    placeholders_only: bool,
    #[clap(long, short = 'O')]
    /// Generate diff in repository order.
    ordered: bool,
    #[clap(long, requires = "ordered")]
    /// Generate ordered diff after a given path.
    after: Option<String>,
    #[clap(long, short, default_value_t = 100, requires = "ordered")]
    /// Generate ordered diff for at most LIMIT files.
    limit: usize,
    #[clap(long, short = 's')]
    /// Limit the total size in bytes of returned diffs.
    diff_size_limit: Option<i64>,
    /// The format of the diff.
    #[clap(long, short = 'f', value_enum, default_value_t = DiffFormat::RawDiff)]
    diff_format: DiffFormat,
}

#[derive(Serialize)]
struct PathsOnlyOutput {
    files: Vec<thrift::CommitCompareFile>,
}

impl Render for PathsOnlyOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        for file in &self.files {
            match (file.base_file.as_ref(), file.other_file.as_ref()) {
                // The letters as in git-status
                (Some(base_file), None) => writeln!(w, "A {}", base_file.path)?,
                (None, Some(other_file)) => writeln!(w, "D {}", other_file.path)?,
                (Some(base_file), Some(other_file)) => match file.copy_info {
                    thrift::CopyInfo::NONE => writeln!(w, "M {}", base_file.path)?,
                    thrift::CopyInfo::MOVE => {
                        writeln!(w, "R {} -> {}", other_file.path, base_file.path)?;
                    }
                    thrift::CopyInfo::COPY => {
                        writeln!(w, "C {} -> {}", other_file.path, base_file.path)?;
                    }
                    // There is no more possibilities but thrift doesn't know about it.
                    _ => bail!("unrecognized CopyInfo!"),
                },
                // Also rather impossible
                (None, None) => bail!("empty file pair received!"),
            }
        }
        Ok(())
    }

    fn render_json(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        Ok(serde_json::to_writer(w, self)?)
    }
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.clone().into_repo_specifier();
    let commit_ids = args.commit_ids_args.clone().into_commit_ids();
    if commit_ids.len() > 2 || commit_ids.is_empty() {
        bail!("expected 1 or 2 commit_ids (got {})", commit_ids.len())
    }
    let paths = args.path.clone();
    let conn = app.get_connection(Some(&repo.name))?;
    let commit_ids = resolve_commit_ids(&conn, &repo, &commit_ids).await?;
    let mut identity_schemes = BTreeSet::new();
    identity_schemes.insert(thrift::CommitIdentityScheme::BONSAI);

    let commits: Vec<_> = commit_ids
        .into_iter()
        .map(|id| thrift::CommitSpecifier {
            repo: repo.clone(),
            id,
            ..Default::default()
        })
        .collect();
    let (other_commit, base_commit) = if commits.len() == 1 {
        (None, commits[0].clone())
    } else {
        (commits.get(0), commits[1].clone())
    };
    let ordered_params = if args.ordered {
        let after_path = args.after.clone();
        let limit: i64 = args.limit.try_into()?;
        Some(thrift::CommitCompareOrderedParams {
            after_path,
            limit,
            ..Default::default()
        })
    } else {
        None
    };
    let diff_format = match args.diff_format {
        DiffFormat::RawDiff => thrift::DiffFormat::RAW_DIFF,
        DiffFormat::MetadataDiff => thrift::DiffFormat::METADATA_DIFF,
    };

    let params = thrift::CommitCompareParams {
        other_commit_id: other_commit.map(|c| c.id.clone()),
        skip_copies_renames: args.skip_copies_renames,
        identity_schemes,
        paths,
        compare_items: btreeset! {thrift::CommitCompareItem::FILES},
        ordered_params,
        ..Default::default()
    };
    let response = conn.commit_compare(&base_commit, &params).await?;

    if args.paths_only {
        return app
            .target
            .render_one(
                &args,
                PathsOnlyOutput {
                    files: response.diff_files,
                },
            )
            .await;
    }

    let other_commit_id = match response.other_commit_ids {
        None => None,
        Some(other_commit_ids) => {
            let (_scheme, other_commit_id) = other_commit_ids
                .into_iter()
                .next()
                .expect("expected commit id");
            Some(other_commit_id)
        }
    };

    let placeholder_only = args.placeholders_only;
    let paths_sizes = response.diff_files.iter().map(|diff_file| {
        let pair_size = diff_file.base_file.as_ref().map_or(0, |f| f.info.file_size)
            + diff_file
                .other_file
                .as_ref()
                .map_or(0, |f| f.info.file_size);
        (
            thrift::CommitFileDiffsParamsPathPair {
                base_path: diff_file.base_file.as_ref().map(|f| f.path.clone()),
                other_path: diff_file.other_file.as_ref().map(|f| f.path.clone()),
                copy_info: diff_file.copy_info,
                generate_placeholder_diff: Some(
                    placeholder_only || pair_size > thrift::COMMIT_FILE_DIFFS_SIZE_LIMIT,
                ),
                ..Default::default()
            },
            pair_size,
        )
    });
    app.target
        .render(
            &(),
            diff_files(
                &conn,
                base_commit.clone(),
                other_commit_id,
                paths_sizes,
                args.diff_size_limit,
                diff_format,
            ),
        )
        .await
}