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

//! Recursively fetch the contents of a directory.

use std::borrow::Cow;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use bytesize::ByteSize;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use futures::pin_mut;
use futures::stream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use source_control::types as thrift;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncWriteExt;

use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitIdArgs;
use crate::args::path::PathArgs;
use crate::args::progress::ProgressArgs;
use crate::args::progress::ProgressOutput;
use crate::args::repo::RepoArgs;
use crate::connection::Connection;
use crate::library::path_tree::PathItem;
use crate::library::path_tree::PathTree;
use crate::render::Render;
use crate::ScscApp;

/// Chunk size for requests.
const TREE_CHUNK_SIZE: i64 = source_control::TREE_LIST_MAX_LIMIT;
const FILE_CHUNK_SIZE: i64 = source_control::FILE_CONTENT_CHUNK_RECOMMENDED_SIZE;

/// Number of concurrent fetches.
const CONCURRENT_TREE_FETCHES: usize = 4;
const CONCURRENT_FILE_FETCHES: usize = 4;

#[derive(clap::Parser)]
/// Recursively fetch the contents of a directory
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(flatten)]
    path_args: PathArgs,
    #[clap(flatten)]
    progress_args: ProgressArgs,
    #[clap(long, short)]
    /// Destination to export to
    output: PathBuf,
    #[clap(long, short)]
    /// Show paths of files fetched
    verbose: bool,
    #[clap(long)]
    /// Create parent directories of the destination if they do not exist
    make_parent_dirs: bool,
    #[clap(long)]
    /// Filename of a file containing a list of paths (relative to PATH) to export
    path_list_file: Option<String>,
    #[clap(long)]
    /// Perform additional requests to try for case insensitive matches
    case_insensitive: bool,
}

/// Returns a stream of the names of the entries in a single directory `path`.
async fn stream_tree_elements(
    connection: &Connection,
    commit: &thrift::CommitSpecifier,
    path: &str,
) -> Result<impl Stream<Item = Result<String>>> {
    let tree = thrift::TreeSpecifier::by_commit_path(thrift::CommitPathSpecifier {
        commit: commit.clone(),
        path: path.to_string(),
        ..Default::default()
    });
    let response = connection
        .tree_list(
            &tree,
            &thrift::TreeListParams {
                offset: 0,
                limit: source_control::TREE_LIST_MAX_LIMIT,
                ..Default::default()
            },
        )
        .await?;

    Ok(stream::iter(response.entries)
        .map(|entry| Ok(entry.name))
        .chain(
            stream::iter(
                (source_control::TREE_LIST_MAX_LIMIT..response.count)
                    .step_by(source_control::TREE_LIST_MAX_LIMIT as usize),
            )
            .map({
                let connection = connection.clone();
                move |offset| {
                    connection.tree_list(
                        &tree,
                        &thrift::TreeListParams {
                            offset,
                            limit: source_control::TREE_LIST_MAX_LIMIT,
                            ..Default::default()
                        },
                    )
                }
            })
            .buffered(10)
            .and_then(move |response| async move {
                Ok(stream::iter(response.entries).map(|entry| Ok(entry.name)))
            })
            .try_flatten(),
        ))
}

/// Mode of casefold operation for path element comparisons.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Casefold {
    /// Paths should be compared case sensitively.
    Sensitive,

    /// Paths should be compared by folding to lowercase and comparing
    /// the lowercase version.
    Insensitive,
}

impl Casefold {
    /// Returns the appropriate case-folded version of a given path element.
    fn of<'a>(self, s: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
        match self {
            Casefold::Sensitive => s.into(),
            Casefold::Insensitive => s.into().to_lowercase().into(),
        }
    }
}

/// Returns an arbitrary case insensitive match of `subpath` within the (case
/// sensitive) `target_dir`, or `None` if there is no such match.
fn case_insensitive_subpath<'a>(
    connection: &'a Connection,
    commit: &'a thrift::CommitSpecifier,
    target_dir: &'a str,
    subpath: &'a str,
) -> BoxFuture<'a, Result<Option<String>>> {
    async move {
        let (target_elem, target_subpath) = subpath.split_once('/').unwrap_or((subpath, ""));
        let target_elem_lower = target_elem.to_lowercase();
        let elements = stream_tree_elements(connection, commit, target_dir).await?;
        pin_mut!(elements);
        while let Some(elem) = elements.try_next().await? {
            if elem.to_lowercase() == target_elem_lower {
                let target = format!("{}/{}", target_dir, elem);
                if target_subpath.is_empty() {
                    return Ok(Some(target));
                } else if let Some(response) =
                    // We've found a possible directory to look into - recurse
                    // to see if we can find the full path.
                    case_insensitive_subpath(
                        connection,
                        commit,
                        &target,
                        target_subpath,
                    )
                    .await?
                {
                    return Ok(Some(response));
                }
            }
        }
        Ok(None)
    }
    .boxed()
}

/// Returns an iterator over pairs of each of the path prefixes of `path` and
/// the subpath within that prefix, finishing with the full path relative to
/// the root.  Initial trailing slashes are removed from the path.
///
/// ```
/// assert_eq!(
///    iter_path_prefixes("a/b/c").collect::<Vec<_>>,
///    vec![("a/b", "c"), ("a", "b/c"), ("", "a/b/c")],
/// );
/// ```
fn iter_path_prefixes(path: &str) -> impl Iterator<Item = (&str, &str)> {
    let path = path.trim_end_matches('/');
    path.rmatch_indices('/')
        .map(|(slash, _)| (&path[..slash], &path[slash + 1..]))
        .chain(Some(("", path)))
}

/// Returns an arbitrary case-insensitive match of `path`, or `None` if there
/// is no such match.
async fn case_insensitive_path(
    connection: &Connection,
    commit: &thrift::CommitSpecifier,
    path: &str,
) -> Result<Option<String>> {
    // Heuristic: typically it's the last few path elements that actually need
    // casefolding, so start from the end of the path and look up parent
    // directories one by one.
    for (target_dir, target_subpath) in iter_path_prefixes(path) {
        let (target_elem, target_subpath) = target_subpath
            .split_once('/')
            .unwrap_or((target_subpath, ""));
        let target_elem_lower = target_elem.to_lowercase();
        let elements = stream_tree_elements(connection, commit, target_dir).await?;
        pin_mut!(elements);
        while let Some(elem) = elements.try_next().await? {
            if elem.to_lowercase() == target_elem_lower {
                let target = format!("{}/{}", target_dir, elem);
                if let Some(response) =
                    case_insensitive_subpath(connection, commit, &target, target_subpath).await?
                {
                    return Ok(Some(response));
                }
            }
        }
    }
    Ok(None)
}

fn join_path(path: &str, elem: &str) -> String {
    let mut path = path.to_string();
    if !path.is_empty() && !path.ends_with('/') {
        path.push('/');
    }
    path.push_str(elem);
    path
}

fn export_tree_entry(
    path: &str,
    destination: &Path,
    entry: thrift::TreeEntry,
) -> Result<ExportItem> {
    match entry.info {
        thrift::EntryInfo::tree(info) => Ok(ExportItem::Tree {
            path: join_path(path, &entry.name),
            id: info.id,
            destination: destination.join(&entry.name),
            filter: None,
        }),
        thrift::EntryInfo::file(info) => Ok(ExportItem::File {
            path: join_path(path, &entry.name),
            id: info.id,
            destination: destination.join(&entry.name),
            size: info.file_size as u64,
            type_: entry.r#type,
        }),
        _ => {
            bail!("malformed response format for '{}'", entry.name);
        }
    }
}

fn export_filtered_tree_entry(
    path: &str,
    destination: &Path,
    entry: thrift::TreeEntry,
    filter: &mut PathTree,
    casefold: Casefold,
) -> Result<Option<ExportItem>> {
    match (filter.remove(&casefold.of(&entry.name)), entry.info) {
        (None, _) => Ok(None),
        (Some(item), thrift::EntryInfo::tree(info)) => {
            let subfilter = match item {
                PathItem::Target | PathItem::TargetDir => None,
                PathItem::Dir(tree) => Some(tree),
            };
            Ok(Some(ExportItem::Tree {
                path: join_path(path, &entry.name),
                id: info.id,
                destination: destination.join(&entry.name),
                filter: subfilter,
            }))
        }
        (Some(PathItem::Dir(_) | PathItem::TargetDir), thrift::EntryInfo::file(_)) => Ok(None),
        (Some(PathItem::Target), thrift::EntryInfo::file(info)) => Ok(Some(ExportItem::File {
            path: join_path(path, &entry.name),
            id: info.id,
            destination: destination.join(&entry.name),
            size: info.file_size as u64,
            type_: entry.r#type,
        })),
        _ => bail!("malformed response format for '{}'", entry.name),
    }
}

async fn export_tree(
    connection: Connection,
    repo: thrift::RepoSpecifier,
    path: String,
    id: Vec<u8>,
    destination: PathBuf,
    filter: Option<PathTree>,
    casefold: Casefold,
) -> Result<Vec<ExportItem>> {
    tokio::fs::create_dir(&destination).await?;
    let tree = thrift::TreeSpecifier::by_id(thrift::TreeIdSpecifier {
        repo,
        id,
        ..Default::default()
    });
    let params = thrift::TreeListParams {
        offset: 0,
        limit: TREE_CHUNK_SIZE,
        ..Default::default()
    };
    let response = connection.tree_list(&tree, &params).await?;
    let count = response.count;
    let other_tree_chunks =
        stream::iter((TREE_CHUNK_SIZE..count).step_by(TREE_CHUNK_SIZE as usize))
            .map({
                |offset| {
                    // Request subsequent chunks of the directory listing.
                    let connection = connection.clone();
                    let tree = tree.clone();
                    async move {
                        let params = thrift::TreeListParams {
                            offset,
                            limit: TREE_CHUNK_SIZE,
                            ..Default::default()
                        };
                        anyhow::Ok(connection.tree_list(&tree, &params).await?.entries)
                    }
                }
            })
            .buffered(CONCURRENT_TREE_FETCHES)
            .try_collect::<Vec<_>>()
            .await?;

    let output = if let Some(mut filter) = filter {
        Some(response.entries)
            .into_iter()
            .chain(other_tree_chunks)
            .flatten()
            .filter_map(|entry| {
                export_filtered_tree_entry(&path, &destination, entry, &mut filter, casefold)
                    .transpose()
            })
            .collect::<Result<_, _>>()?
    } else {
        Some(response.entries)
            .into_iter()
            .chain(other_tree_chunks)
            .flatten()
            .map(|entry| export_tree_entry(&path, &destination, entry))
            .collect::<Result<_, _>>()?
    };
    Ok(output)
}

async fn export_file(
    connection: Connection,
    repo: thrift::RepoSpecifier,
    id: Vec<u8>,
    destination: PathBuf,
    size: u64,
    type_: thrift::EntryType,
    bytes_written: &Arc<AtomicU64>,
) -> Result<()> {
    let file = thrift::FileSpecifier::by_id(thrift::FileIdSpecifier {
        repo,
        id,
        ..Default::default()
    });
    let mut responses = stream::iter((0..size).step_by(FILE_CHUNK_SIZE as usize))
        .map({
            move |offset| {
                let params = thrift::FileContentChunkParams {
                    offset: offset as i64,
                    size: FILE_CHUNK_SIZE,
                    ..Default::default()
                };
                connection.file_content_chunk(&file, &params)
            }
        })
        .buffered(CONCURRENT_FILE_FETCHES);

    #[cfg(unix)]
    {
        if type_ == thrift::EntryType::LINK {
            use std::ffi::OsStr;
            use std::os::unix::ffi::OsStrExt;
            let mut target = Vec::new();
            while let Some(response) = responses.try_next().await? {
                target.extend_from_slice(response.data.as_slice());
            }
            tokio::fs::symlink(OsStr::from_bytes(target.as_slice()), &destination).await?;
            bytes_written.fetch_add(size, Ordering::Relaxed);
            return Ok(());
        }
    }

    let mut out_file = tokio::fs::File::create(&destination).await?;
    while let Some(response) = responses.try_next().await? {
        let len = response.data.len() as u64;
        out_file.write_all(&response.data).await?;
        bytes_written.fetch_add(len, Ordering::Relaxed);
    }

    #[cfg(unix)]
    {
        if type_ == thrift::EntryType::EXEC {
            // Tokio doesn't support setting permissions yet, so we must use
            // the standard library.
            use std::os::unix::fs::PermissionsExt;
            let out_file = out_file.into_std().await;
            tokio::task::spawn_blocking(move || {
                let metadata = out_file.metadata()?;
                let mut permissions = metadata.permissions();
                let mode = permissions.mode();
                // Propagate read permissions to execute permissions.
                permissions.set_mode(mode | ((mode & 0o444) >> 2));
                std::fs::set_permissions(&destination, permissions)?;
                anyhow::Ok(())
            })
            .await??;
        }
    }

    Ok(())
}

async fn export_item(
    connection: Connection,
    repo: thrift::RepoSpecifier,
    item: ExportItem,
    casefold: Casefold,
    files_written: Arc<AtomicU64>,
    bytes_written: Arc<AtomicU64>,
) -> Result<(Option<String>, Vec<ExportItem>)> {
    match item {
        ExportItem::Tree {
            path,
            id,
            destination,
            filter,
        } => {
            let items =
                export_tree(connection, repo, path, id, destination, filter, casefold).await?;
            Ok((None, items))
        }
        ExportItem::File {
            path,
            id,
            destination,
            size,
            type_,
        } => {
            export_file(
                connection,
                repo,
                id,
                destination,
                size,
                type_,
                &bytes_written,
            )
            .await?;
            files_written.fetch_add(1, Ordering::Relaxed);
            Ok((Some(path), Vec::new()))
        }
    }
}

enum ExportItem {
    Tree {
        path: String,
        id: Vec<u8>,
        destination: PathBuf,
        filter: Option<PathTree>,
    },
    File {
        path: String,
        id: Vec<u8>,
        destination: PathBuf,
        size: u64,
        type_: thrift::EntryType,
    },
}

struct ExportedFile {
    path: String,
}

impl Render for ExportedFile {
    type Args = ();

    fn render_tty(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        writeln!(w, "{}", self.path)?;
        Ok(())
    }
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let destination: PathBuf = args.output;
    if destination.exists() {
        bail!(
            "destination ({}) already exists",
            destination.to_string_lossy()
        );
    }
    let casefold = if args.case_insensitive {
        Casefold::Insensitive
    } else {
        Casefold::Sensitive
    };

    let path_tree = match args.path_list_file {
        Some(path_list_file) => {
            let file = tokio::fs::File::open(path_list_file)
                .await
                .context("failed to open path list file")?;
            let lines = tokio::io::BufReader::new(file).lines();
            let stream = tokio_stream::wrappers::LinesStream::new(lines);

            let path_tree = stream
                .map_ok(|path| casefold.of(path).into_owned())
                .try_collect::<PathTree>()
                .await
                .context("failed to load path list file")?;
            Some(path_tree)
        }
        None => None,
    };

    if args.make_parent_dirs {
        if let Some(parent) = destination.parent() {
            if !parent.exists() {
                tokio::fs::create_dir_all(parent)
                    .await
                    .context("failed to create parent directories")?;
            }
        }
    }

    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 commit = thrift::CommitSpecifier {
        repo: repo.clone(),
        id,
        ..Default::default()
    };
    let path = args.path_args.clone().path;
    let mut commit_path = thrift::CommitPathSpecifier {
        commit: commit.clone(),
        path: path.clone(),
        ..Default::default()
    };

    let params = thrift::CommitPathInfoParams {
        ..Default::default()
    };
    let response = {
        let mut response = conn.commit_path_info(&commit_path, &params).await?;
        if !response.exists && casefold == Casefold::Insensitive {
            if let Some(case_path) = case_insensitive_path(&conn, &commit, &path).await? {
                commit_path.path = case_path;
                response = conn.commit_path_info(&commit_path, &params).await?;
            }
        }
        response
    };

    if !response.exists {
        bail!("'{}' does not exist in {}", path, commit_id);
    }

    let file_count;
    let total_size;
    let files_written = Arc::new(AtomicU64::new(0));
    let bytes_written = Arc::new(AtomicU64::new(0));

    let item = match (response.r#type, response.info) {
        (Some(_type), Some(thrift::EntryInfo::tree(info))) => {
            file_count = info.descendant_files_count as u64;
            total_size = info.descendant_files_total_size as u64;
            ExportItem::Tree {
                path,
                id: info.id,
                destination,
                filter: path_tree,
            }
        }
        (Some(type_), Some(thrift::EntryInfo::file(info))) => {
            file_count = 1;
            total_size = info.file_size as u64;
            if path_tree.is_some() {
                // A list of paths to filter has been provided, but the target
                // is a file, so none of the paths can possible match.
                return Ok(());
            }
            ExportItem::File {
                path,
                id: info.id,
                destination,
                size: info.file_size as u64,
                type_,
            }
        }
        _ => {
            bail!("malformed response for '{}' in {}", path, commit_id);
        }
    };

    let stream = bounded_traversal::bounded_traversal_stream(100, Some(item), {
        let files_written = files_written.clone();
        let bytes_written = bytes_written.clone();
        move |item| {
            export_item(
                conn.clone(),
                repo.clone(),
                item,
                casefold,
                files_written.clone(),
                bytes_written.clone(),
            )
            .boxed()
        }
    });

    let stream = if args.verbose {
        stream
            .try_filter_map(|path| async move { Ok(path.map(|path| ExportedFile { path })) })
            .left_stream()
    } else {
        stream
            .try_filter_map(|_path| async move { Ok(None) })
            .right_stream()
    };

    let render = args.progress_args.render_progress(stream, move || {
        let files_written = files_written.load(Ordering::Relaxed);
        let bytes_written = bytes_written.load(Ordering::Relaxed);
        let message = format!(
            "Exported {:>5}/{:>5} files, {:>8}/{:>8}",
            files_written,
            file_count,
            ByteSize::b(bytes_written).to_string_as(true),
            ByteSize::b(total_size).to_string_as(true),
        );
        ProgressOutput::new(message, bytes_written, total_size)
    });
    app.target.render(&(), render).await
}


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

use std::io::Write;

use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use serde::Serialize;
use source_control::types as thrift;

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

#[derive(Parser)]
/// Find all files inside a certain dir or with certain filename
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,

    #[clap(long, short)]
    /// Subdir to look at
    prefix: Option<Vec<String>>,
    #[clap(long, short)]
    /// Filename to filter on
    filename: Option<Vec<String>>,
    #[clap(long, short)]
    /// Suffix of the basenames to filter on,
    suffix: Option<Vec<String>>,
    #[clap(long)]
    /// If provided (even empty), response is sorted and starts from the given name
    after: Option<String>,
    #[clap(long, default_value_t = 100)]
    /// Maximum number of paths to return
    limit: u64,
}

#[derive(Serialize)]
struct FileListOutput(Vec<String>);

impl Render for FileListOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        for file in &self.0 {
            write!(w, "{}\n", file)?;
        }
        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_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 prefixes = args.prefix.clone();
    let basenames = args.filename.clone();
    let basename_suffixes = args.suffix.clone();
    let after = args.after.clone();
    let limit: i64 = args.limit.try_into().context("limit too large")?;

    let commit_specifier = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let params = thrift::CommitFindFilesParams {
        limit,
        after,
        basenames,
        basename_suffixes,
        prefixes,
        ..Default::default()
    };
    let response = conn.commit_find_files(&commit_specifier, &params).await?;
    app.target
        .render_one(&args, FileListOutput(response.files))
        .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.
 */

//! Display information about a commit, directory, or file.

use std::collections::HashSet;
use std::io::Write;

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

use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitIdArgs;
use crate::args::commit_id::SchemeArgs;
use crate::args::repo::RepoArgs;
use crate::library::bookmark::render_bookmark_info;
use crate::library::bookmark::BookmarkInfo;
use crate::library::commit::render_commit_info;
use crate::library::commit::CommitInfo;
use crate::render::Render;
use crate::util::byte_count_iec;
use crate::util::plural;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Fetch info about a commit, directory, file or bookmark
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(long, short, num_args = 1..)]
    /// Path or paths to display info for.  If not present, display info about
    /// the commit.  To display directory info about the root directory, use
    /// -p ""
    path: Option<Vec<String>>,
    #[clap(long, conflicts_with = "path")]
    /// Display info about bookmark itself rather than the commit it points to
    bookmark_info: bool,
}

struct CommitInfoOutput {
    commit: CommitInfo,
    requested: String,
    schemes: HashSet<String>,
}

impl Render for CommitInfoOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        render_commit_info(&self.commit, &self.requested, &self.schemes, w)
    }

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

struct BookmarkInfoOutput {
    bookmark_info: BookmarkInfo,
    requested: String,
    schemes: HashSet<String>,
}

impl Render for BookmarkInfoOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        render_bookmark_info(&self.bookmark_info, &self.requested, &self.schemes, w)
    }

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

#[derive(Serialize)]
struct TreeInfoOutput {
    path: String,
    r#type: String,
    id: String,
    simple_format_sha1: String,
    simple_format_sha256: String,
    child_files_count: i64,
    child_files_total_size: i64,
    child_dirs_count: i64,
    descendant_files_count: i64,
    descendant_files_total_size: i64,
}

impl Render for TreeInfoOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        write!(w, "Path: {}\n", self.path)?;
        write!(w, "Type: {}\n", self.r#type)?;
        write!(w, "Id: {}\n", self.id)?;
        write!(w, "Simple-Format-SHA1: {}\n", self.simple_format_sha1)?;
        write!(w, "Simple-Format-SHA256: {}\n", self.simple_format_sha256)?;
        write!(
            w,
            "Children: {} {} ({}), {} {}\n",
            self.child_files_count,
            plural(self.child_files_count, "file", "files"),
            byte_count_iec(self.child_files_total_size),
            self.child_dirs_count,
            plural(self.child_dirs_count, "dir", "dirs"),
        )?;
        write!(
            w,
            "Descendants: {} {} ({})\n",
            self.descendant_files_count,
            plural(self.descendant_files_count, "file", "files"),
            byte_count_iec(self.descendant_files_total_size)
        )?;
        Ok(())
    }

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

#[derive(Serialize)]
struct FileInfoOutput {
    path: String,
    r#type: String,
    size: i64,
    id: String,
    content_sha1: String,
    content_sha256: String,
}

impl Render for FileInfoOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        write!(w, "Path: {}\n", self.path)?;
        write!(w, "Type: {}\n", self.r#type)?;
        write!(w, "Id: {}\n", self.id)?;
        write!(w, "Content-SHA1: {}\n", self.content_sha1)?;
        write!(w, "Content-SHA256: {}\n", self.content_sha256)?;
        write!(w, "Size: {}\n", byte_count_iec(self.size))?;
        Ok(())
    }

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

async fn commit_info(app: ScscApp, args: CommandArgs, repo: thrift::RepoSpecifier) -> Result<()> {
    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 commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let params = thrift::CommitInfoParams {
        identity_schemes: args.scheme_args.clone().into_request_schemes(),
        ..Default::default()
    };
    let response = conn.commit_info(&commit, &params).await?;

    let commit_info = CommitInfo::try_from(&response)?;
    let output = CommitInfoOutput {
        commit: commit_info,
        requested: commit_id.to_string(),
        schemes: args.scheme_args.scheme_string_set(),
    };
    app.target.render_one(&args, output).await
}

async fn bookmark_info(app: ScscApp, args: CommandArgs, repo: thrift::RepoSpecifier) -> Result<()> {
    let conn = app.get_connection(Some(&repo.name))?;
    let bookmark_name = args
        .commit_id_args
        .clone()
        .into_commit_id()
        .into_bookmark_name()?;
    let params = thrift::RepoBookmarkInfoParams {
        bookmark_name: bookmark_name.clone(),
        identity_schemes: args.scheme_args.clone().into_request_schemes(),
        ..Default::default()
    };
    let response = conn.repo_bookmark_info(&repo, &params).await?;
    let info = response
        .info
        .ok_or_else(|| anyhow!("Bookmark doesn't exit"))?;

    let info = BookmarkInfo::try_from(&info)?;
    let output = BookmarkInfoOutput {
        bookmark_info: info,
        requested: bookmark_name,
        schemes: args.scheme_args.scheme_string_set(),
    };
    app.target.render_one(&args, output).await
}

async fn path_info(
    app: ScscApp,
    args: CommandArgs,
    repo: thrift::RepoSpecifier,
    path: String,
) -> Result<()> {
    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 commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let commit_path = thrift::CommitPathSpecifier {
        commit,
        path: path.clone(),
        ..Default::default()
    };
    let params = thrift::CommitPathInfoParams {
        ..Default::default()
    };
    let response = conn.commit_path_info(&commit_path, &params).await?;
    if response.exists {
        match (response.r#type, response.info) {
            (Some(entry_type), Some(thrift::EntryInfo::tree(info))) => {
                app.target
                    .render_one(&args, tree_info(path, entry_type, info))
                    .await
            }
            (Some(entry_type), Some(thrift::EntryInfo::file(info))) => {
                app.target
                    .render_one(&args, file_info(path, entry_type, info))
                    .await
            }
            _ => {
                bail!("malformed response for '{}' in {}", path, commit_id)
            }
        }
    } else {
        bail!("'{}' does not exist in {}", path, commit_id)
    }
}

async fn multiple_path_info(
    app: ScscApp,
    args: CommandArgs,
    repo: thrift::RepoSpecifier,
    paths: Vec<String>,
) -> Result<()> {
    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 commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let params = thrift::CommitMultiplePathInfoParams {
        paths,
        ..Default::default()
    };
    let response = conn.commit_multiple_path_info(&commit, &params).await?;

    let output = stream::iter(response.path_info).map(move |(path, commit_info)| {
        match (commit_info.r#type, commit_info.info) {
            (Some(entry_type), Some(thrift::EntryInfo::tree(info))) => {
                Ok(Box::new(tree_info(path, entry_type, info))
                    as Box<dyn Render<Args = CommandArgs>>)
            }
            (Some(entry_type), Some(thrift::EntryInfo::file(info))) => {
                Ok(Box::new(file_info(path, entry_type, info))
                    as Box<dyn Render<Args = CommandArgs>>)
            }
            _ => {
                bail!("malformed response for '{}'", path);
            }
        }
    });
    app.target.render(&args, output).await
}

fn tree_info(
    path: String,
    entry_type: thrift::EntryType,
    info: thrift::TreeInfo,
) -> TreeInfoOutput {
    let id = faster_hex::hex_string(&info.id);
    let simple_format_sha1 = faster_hex::hex_string(&info.simple_format_sha1);
    let simple_format_sha256 = faster_hex::hex_string(&info.simple_format_sha256);
    TreeInfoOutput {
        path,
        r#type: entry_type.to_string().to_lowercase(),
        id,
        simple_format_sha1,
        simple_format_sha256,
        child_files_count: info.child_files_count,
        child_dirs_count: info.child_dirs_count,
        child_files_total_size: info.child_files_total_size,
        descendant_files_count: info.descendant_files_count,
        descendant_files_total_size: info.descendant_files_total_size,
    }
}

fn file_info(
    path: String,
    entry_type: thrift::EntryType,
    info: thrift::FileInfo,
) -> FileInfoOutput {
    let id = faster_hex::hex_string(&info.id);
    let content_sha1 = faster_hex::hex_string(&info.content_sha1);
    let content_sha256 = faster_hex::hex_string(&info.content_sha256);
    FileInfoOutput {
        path,
        r#type: entry_type.to_string().to_lowercase(),
        id,
        content_sha1,
        content_sha256,
        size: info.file_size,
    }
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.clone().into_repo_specifier();

    match args.path.as_deref() {
        Some([path]) => {
            let path = path.clone();
            path_info(app, args, repo, path.clone()).await
        }
        Some(paths) => {
            let paths = paths.to_vec();
            multiple_path_info(app, args, repo, paths).await
        }
        None => {
            if args.bookmark_info {
                bookmark_info(app, args, repo).await
            } else {
                commit_info(app, args, repo).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::io::Write;

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

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::render::Render;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Finds whether the first provided commit is an ancestor of the second one.
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,
}

#[derive(Serialize)]
struct IsAncestorOutput {
    result: bool,
}

impl Render for IsAncestorOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        writeln!(w, "{:?}", self.result)?;
        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?;
    if ids.len() != 2 {
        bail!("expected 2 commit_ids (got {})", commit_ids.len())
    }
    let commit = thrift::CommitSpecifier {
        repo,
        id: ids[0].clone(),
        ..Default::default()
    };
    let params = thrift::CommitIsAncestorOfParams {
        descendant_commit_id: ids[1].clone(),
        ..Default::default()
    };
    let response = conn.commit_is_ancestor_of(&commit, &params).await?;
    let output = IsAncestorOutput { result: response };
    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 std::collections::BTreeMap;
use std::io::Write;

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

use crate::args::commit_id::map_commit_id;
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::pushvars::PushvarArgs;
use crate::args::repo::RepoArgs;
use crate::args::service_id::ServiceIdArgs;
use crate::library::commit_id::render_commit_id;
use crate::render::Render;
use crate::ScscApp;

#[derive(clap::Parser)]

/// Land a stack of commits
///
/// Provide two commits: the first is the head of a stack, and the second is
/// public commit the stack is based on.  The stack of commits between these
/// two commits will be landed onto the named bookmark via pushrebase.
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,
    #[clap(flatten)]
    service_id_args: ServiceIdArgs,
    #[clap(flatten)]
    pushvar_args: PushvarArgs,
    #[clap(long, short)]
    /// Name of the bookmark to land to
    name: String,
}

#[derive(Serialize)]
struct PushrebaseRebasedCommit {
    old_bonsai_id: String,
    new_ids: BTreeMap<String, String>,
}

#[derive(Serialize)]
struct PushrebaseOutcomeOutput {
    bookmark: String,
    head: BTreeMap<String, String>,
    rebased_commits: Vec<PushrebaseRebasedCommit>,
    retries: i64,
    distance: i64,
}

impl Render for PushrebaseOutcomeOutput {
    type Args = SchemeArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        let schemes = args.scheme_string_set();
        write!(
            w,
            "In {} retries across distance {}\n",
            self.retries, self.distance
        )?;
        write!(w, "{} updated to", self.bookmark)?;
        render_commit_id(
            Some(("", "    ")),
            "\n",
            &self.bookmark,
            &self.head,
            &schemes,
            w,
        )?;
        write!(w, "\n")?;
        for rebase in self.rebased_commits.iter() {
            write!(w, "{} => ", rebase.old_bonsai_id)?;
            render_commit_id(None, ", ", "new commit", &rebase.new_ids, &schemes, w)?;
            write!(w, "\n")?;
        }
        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.into_repo_specifier();
    let commit_ids = args.commit_ids_args.into_commit_ids();
    if commit_ids.len() != 2 {
        bail!("expected 2 commit_ids (got {})", commit_ids.len())
    }
    let conn = app.get_connection(Some(&repo.name))?;
    let ids = resolve_commit_ids(&conn, &repo, &commit_ids).await?;
    let bookmark = args.name;
    let service_identity = args.service_id_args.service_id;
    let pushvars = args.pushvar_args.into_pushvars();

    let (head, base) = match ids.as_slice() {
        [head_id, base_id] => (head_id.clone(), base_id.clone()),
        _ => bail!("expected 1 or 2 commit_ids (got {})", ids.len()),
    };

    let params = thrift::RepoLandStackParams {
        bookmark: bookmark.clone(),
        head,
        base,
        identity_schemes: args.scheme_args.clone().into_request_schemes(),
        old_identity_schemes: Some(btreeset! { thrift::CommitIdentityScheme::BONSAI }),
        service_identity,
        pushvars,
        ..Default::default()
    };
    let outcome = conn
        .repo_land_stack(&repo, &params)
        .await?
        .pushrebase_outcome;
    let head = map_commit_ids(outcome.head.values());
    let mut rebased_commits = outcome
        .rebased_commits
        .into_iter()
        .map(|rebase| {
            let (_, old_bonsai_id) = map_commit_id(
                rebase
                    .old_ids
                    .get(&thrift::CommitIdentityScheme::BONSAI)
                    .ok_or_else(|| anyhow!("bonsai id missing from response"))?,
            )
            .ok_or_else(|| anyhow!("bonsai id should be mappable"))?;
            let new_ids = map_commit_ids(rebase.new_ids.values());
            Ok(PushrebaseRebasedCommit {
                old_bonsai_id,
                new_ids,
            })
        })
        .collect::<Result<Vec<_>>>()?;
    rebased_commits.sort_unstable_by(|a, b| a.old_bonsai_id.cmp(&b.old_bonsai_id));
    let output = PushrebaseOutcomeOutput {
        bookmark,
        head,
        rebased_commits,
        distance: outcome.pushrebase_distance,
        retries: outcome.retry_num,
    };
    app.target.render_one(&args.scheme_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 std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashSet;
use std::io::Write;

use anyhow::Result;
use futures::stream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use serde::Serialize;
use source_control::types as thrift;

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

#[derive(clap::Parser)]
/// List bookmarks and their current commits
///
/// If a commit id is provided, the results are limited to descendants
/// of that commit.
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,
    #[clap(long)]
    /// Limit bookmarks to those starting with a certain prefix
    prefix: Option<String>,
    #[clap(long, short, default_value_t = 100)]
    /// Limit the number of bookmarks
    limit: usize,
    #[clap(long)]
    /// Only show bookmarks after the provided name
    after: Option<String>,
    #[clap(long, short)]
    /// Only show the bookmark names
    name_only: bool,
    #[clap(long)]
    /// Include scratch bookmarks in results
    include_scratch: bool,
}

#[derive(Serialize)]
struct BookmarkOutput {
    name: String,
    ids: BTreeMap<String, String>,
}

impl Render for BookmarkOutput {
    type Args = CommandArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        let name_only = args.name_only;
        if name_only {
            write!(w, "{}\n", self.name)?;
        } else {
            let schemes: HashSet<String> = args.scheme_args.scheme_string_set();
            if schemes.len() == 1 {
                write!(w, "{:<40} ", self.name)?;
                render_commit_id(None, "\n", &self.name, &self.ids, &schemes, w)?;
            } else {
                write!(w, "{}", self.name)?;
                render_commit_id(Some(("", "    ")), "\n", &self.name, &self.ids, &schemes, w)?;
            }
            write!(w, "\n")?;
        }
        Ok(())
    }

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

fn repo_list_bookmarks(
    connection: Connection,
    repo: thrift::RepoSpecifier,
    limit: Option<i64>,
    after: Option<String>,
    prefix: Option<String>,
    include_scratch: bool,
    identity_schemes: BTreeSet<thrift::CommitIdentityScheme>,
) -> impl Stream<Item = Result<(String, BTreeMap<String, String>)>> {
    stream::try_unfold(Some((after, limit)), move |state| {
        let connection = connection.clone();
        let repo = repo.clone();
        let identity_schemes = identity_schemes.clone();
        let prefix = prefix.clone();
        async move {
            if let Some((after, limit)) = state {
                let (limit, remaining) = limit.map_or((0, None), |limit| {
                    let size = limit.min(source_control::consts::REPO_LIST_BOOKMARKS_MAX_LIMIT);
                    (size, Some(limit.saturating_sub(size)))
                });

                let params = thrift::RepoListBookmarksParams {
                    include_scratch,
                    bookmark_prefix: prefix.unwrap_or_default(),
                    limit,
                    after,
                    identity_schemes: identity_schemes.clone(),
                    ..Default::default()
                };
                let response = connection.repo_list_bookmarks(&repo, &params).await?;
                let bookmarks = response
                    .bookmarks
                    .into_iter()
                    .map(|(name, ids)| Ok((name, map_commit_ids(ids.values()))));
                let next_state = response
                    .continue_after
                    .map(|after| (Some(after), remaining))
                    .filter(|_| remaining.map_or(true, |r| r > 0));

                anyhow::Ok(Some((stream::iter(bookmarks), next_state)))
            } else {
                anyhow::Ok(None)
            }
        }
    })
    .try_flatten()
}

fn commit_list_descendant_bookmarks(
    connection: Connection,
    commit: thrift::CommitSpecifier,
    limit: Option<i64>,
    after: Option<String>,
    prefix: Option<String>,
    include_scratch: bool,
    identity_schemes: BTreeSet<thrift::CommitIdentityScheme>,
) -> impl Stream<Item = Result<(String, BTreeMap<String, String>)>> {
    stream::try_unfold(Some((after, limit)), move |state| {
        let connection = connection.clone();
        let commit = commit.clone();
        let identity_schemes = identity_schemes.clone();
        let prefix = prefix.clone();
        async move {
            if let Some((after, limit)) = state {
                if limit == Some(0) {
                    return Ok(None);
                }
                let params = thrift::CommitListDescendantBookmarksParams {
                    include_scratch,
                    bookmark_prefix: prefix.unwrap_or_default(),
                    limit: source_control::consts::COMMIT_LIST_DESCENDANT_BOOKMARKS_MAX_LIMIT,
                    after,
                    identity_schemes: identity_schemes.clone(),
                    ..Default::default()
                };
                let response = connection
                    .commit_list_descendant_bookmarks(&commit, &params)
                    .await?;
                let mut count = response.bookmarks.len() as i64;
                if let Some(limit) = limit {
                    count = count.min(limit);
                }
                let bookmarks = response
                    .bookmarks
                    .into_iter()
                    .take(count as usize)
                    .map(|(name, ids)| Ok((name, map_commit_ids(ids.values()))));
                let next_state = response
                    .continue_after
                    .map(|after| (Some(after), limit.map(|limit| limit.saturating_sub(count))));
                anyhow::Ok(Some((stream::iter(bookmarks), next_state)))
            } else {
                anyhow::Ok(None)
            }
        }
    })
    .try_flatten()
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.clone().into_repo_specifier();

    let limit: i64 = args.limit.try_into()?;

    let after = args.after.clone();
    let prefix = args.prefix.clone();
    let include_scratch = args.include_scratch;

    let conn = app.get_connection(Some(&repo.name))?;
    let bookmarks = match args.commit_ids_args.clone().into_commit_ids().as_slice() {
        [ref commit_id] => {
            let id = resolve_commit_id(&conn, &repo, commit_id).await?;
            let commit = thrift::CommitSpecifier {
                repo,
                id,
                ..Default::default()
            };
            commit_list_descendant_bookmarks(
                conn.clone(),
                commit,
                Some(limit),
                after.map(String::from),
                prefix.map(String::from),
                include_scratch,
                args.scheme_args.clone().into_request_schemes(),
            )
            .left_stream()
        }
        [] => repo_list_bookmarks(
            conn.clone(),
            repo,
            Some(limit),
            after.map(String::from),
            prefix.map(String::from),
            include_scratch,
            args.scheme_args.clone().into_request_schemes(),
        )
        .right_stream(),
        _ => anyhow::bail!("At most one commit must be specified"),
    };
    app.target
        .render(
            &args,
            bookmarks.map_ok(|(name, ids)| BookmarkOutput { name, ids }),
        )
        .await
}


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

use std::collections::HashSet;
use std::io::Write;

use anyhow::Result;
use chrono::naive::NaiveDateTime;
use chrono::DateTime;
use source_control::types as thrift;

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::render_commit_info;
use crate::library::commit::render_commit_summary;
use crate::library::commit::CommitInfo as CommitInfoOutput;
use crate::render::Render;
use crate::ScscApp;

/// Show the history of a commit or a path in a commit
///
/// If a second commit id is provided, the results are limited to descendants
/// of that commit.
#[derive(clap::Parser)]
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,
    #[clap(long, short)]
    /// Optional path to query history
    path: Option<String>,
    #[clap(long, short, default_value_t = 10)]
    /// Limit history length
    limit: u32,
    #[clap(long, short)]
    /// Show history and skip first [SKIP] commits
    skip: Option<u32>,
    #[clap(long, conflicts_with = "skip")]
    /// Show only commits after the given date or timestamp. The given time must be after 1970-01-01 00:00:00 UTC.
    /// Format: YYYY-MM-DD HH:MM:SS [+HH:MM]
    after: Option<String>,
    #[clap(long, conflicts_with = "skip")]
    /// Show only commits before the given date or timestamp. The given time must be after 1970-01-01 00:00:00 UTC.
    /// Format: YYYY-MM-DD HH:MM:SS [+HH:MM]
    before: Option<String>,
    #[clap(long, short)]
    /// Show the full commit message of each commit
    verbose: bool,
    #[clap(long)]
    /// Track history across deletion i.e. if a path was deleted then added back
    history_across_deletions: bool,
    #[clap(long)]
    /// Follow mutable overrides to the history that make it more user friendly and 'correct'
    follow_mutable_history: bool,
}

struct LogOutput {
    requested: String,
    schemes: HashSet<String>,
    history: Vec<CommitInfoOutput>,
}

impl Render for LogOutput {
    type Args = CommandArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        let verbose = args.verbose;
        for commit in &self.history {
            if verbose {
                render_commit_info(commit, &self.requested, &self.schemes, w)?;
                write!(w, "\n")?;
            } else {
                render_commit_summary(commit, &self.requested, &self.schemes, w)?;
                write!(w, "\n")?;
            }
        }
        Ok(())
    }

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

fn convert_to_ts(date_str: Option<&str>) -> Result<Option<i64>> {
    if let Some(date_str) = date_str {
        let ts = if let Ok(date) = DateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S %:z") {
            date.timestamp()
        } else if let Ok(naive) = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S") {
            naive.timestamp()
        } else {
            date_str.parse::<i64>()?
        };

        if ts > 0 {
            return Ok(Some(ts));
        }
        anyhow::bail!(
            "The given date or timestamp must be after 1970-01-01 00:00:00 UTC: {:?}",
            date_str
        )
    }

    Ok(None)
}

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() {
        anyhow::bail!("expected 1 or 2 commit_ids (got {})", commit_ids.len())
    }
    let conn = app.get_connection(Some(&repo.name))?;
    let ids = resolve_commit_ids(&conn, &repo, &commit_ids).await?;
    let id = ids[0].clone();
    let descendants_of = ids.get(1).cloned();
    let commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let path = args.path.clone();

    let limit: i32 = args.limit.try_into()?;
    let skip: i32 = args.skip.unwrap_or(0).try_into()?;

    let before_timestamp = convert_to_ts(args.before.as_deref())?;
    let after_timestamp = convert_to_ts(args.after.as_deref())?;
    let follow_history_across_deletions = args.history_across_deletions;
    let follow_mutable_file_history = Some(args.follow_mutable_history);
    let identity_schemes = args.scheme_args.clone().into_request_schemes();

    let response = match path {
        Some(path) => {
            let commit_and_path = thrift::CommitPathSpecifier {
                commit,
                path,
                ..Default::default()
            };
            let params = thrift::CommitPathHistoryParams {
                format: thrift::HistoryFormat::COMMIT_INFO,
                limit,
                skip,
                before_timestamp,
                after_timestamp,
                identity_schemes,
                follow_history_across_deletions,
                descendants_of,
                exclude_changeset_and_ancestors: None,
                follow_mutable_file_history,
                ..Default::default()
            };
            conn.commit_path_history(&commit_and_path, &params)
                .await?
                .history
        }
        None => {
            let params = thrift::CommitHistoryParams {
                format: thrift::HistoryFormat::COMMIT_INFO,
                limit,
                skip,
                before_timestamp,
                after_timestamp,
                identity_schemes,
                descendants_of,
                exclude_changeset_and_ancestors: None,
                ..Default::default()
            };
            conn.commit_history(&commit, &params).await?.history
        }
    };

    let mut history = vec![];
    match response {
        thrift::History::commit_infos(commits) => {
            for commit in commits {
                let commit_info = CommitInfoOutput::try_from(&commit)?;
                history.push(commit_info);
            }

            let output = LogOutput {
                history,
                requested: commit_ids[0].to_string(),
                schemes: args.scheme_args.scheme_string_set(),
            };
            app.target.render_one(&args, output).await
        }
        thrift::History::UnknownField(id) => {
            anyhow::bail!("Unknown thrift::History field id: {}", id)
        }
        _ => anyhow::bail!("Unexpected thrift::History format"),
    }
}


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

//! Look up a bookmark or commit id.

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_id;
use crate::args::commit_id::CommitIdArgs;
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)]
/// Look up a bookmark or commit id
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
}

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

impl Render for LookupOutput {
    type Args = SchemeArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        if self.exists {
            let schemes = args.scheme_string_set();
            render_commit_id(None, "\n", &self.requested, &self.ids, &schemes, w)?;
            write!(w, "\n")?;
        } else {
            bail!("{} does not exist\n", self.requested);
        }
        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_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 commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let params = thrift::CommitLookupParams {
        identity_schemes: args.scheme_args.clone().into_request_schemes(),
        ..Default::default()
    };
    let response = conn.commit_lookup(&commit, &params).await?;
    let ids = match &response.ids {
        Some(ids) => map_commit_ids(ids.values()),
        None => BTreeMap::new(),
    };
    let output = LookupOutput {
        requested: commit_id.to_string(),
        exists: response.exists,
        ids,
    };
    app.target.render_one(&args.scheme_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.
 */

//! List the contents of a directory.

use std::io::Write;

use anyhow::bail;
use anyhow::Result;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures::Stream;
use serde::Serialize;
use source_control::types as thrift;

use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitIdArgs;
use crate::args::commit_id::SchemeArgs;
use crate::args::repo::RepoArgs;
use crate::connection::Connection;
use crate::render::Render;
use crate::util::byte_count_short;
use crate::ScscApp;

const CHUNK_SIZE: i64 = source_control::TREE_LIST_MAX_LIMIT;

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

/// Number of concurrent fetches for item info (e.g. symlink target).
const CONCURRENT_ITEM_FETCHES: usize = 100;

const MAX_LINK_NAME_LEN: i64 = 4096;

#[derive(clap::Parser)]
/// List the contents of a directory
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(long, short, default_value = "")]
    /// Path to list
    path: String,
    #[clap(long, short)]
    /// Show hidden files (starting with '.')
    all: bool,
    #[clap(long, short)]
    /// Show additional information for each entry
    long: bool,
}

#[derive(Serialize)]
#[serde(untagged)]
enum LsEntryOutput {
    Tree {
        id: String,
        simple_format_sha1: String,
        simple_format_sha256: String,
        child_files_count: i64,
        child_files_total_size: i64,
        child_dirs_count: i64,
        descendant_files_count: i64,
        descendant_files_total_size: i64,
    },
    File {
        id: String,
        size: i64,
        content_sha1: String,
        content_sha256: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        link_target: Option<String>,
    },
}

#[derive(Serialize)]
struct LsOutput {
    name: String,
    r#type: String,
    #[serde(flatten)]
    entry: LsEntryOutput,
}

impl LsOutput {
    fn render_short(&self, w: &mut dyn Write) -> Result<()> {
        match self.entry {
            LsEntryOutput::Tree { .. } => write!(w, "{}/\n", self.name)?,
            LsEntryOutput::File { .. } => write!(w, "{}\n", self.name)?,
        }
        Ok(())
    }

    fn render_long(&self, w: &mut dyn Write) -> Result<()> {
        match &self.entry {
            LsEntryOutput::Tree {
                descendant_files_total_size,
                ..
            } => {
                write!(
                    w,
                    "{}  {:>8}  {}/\n",
                    self.r#type.to_string().to_lowercase(),
                    byte_count_short(*descendant_files_total_size),
                    self.name
                )?;
            }
            LsEntryOutput::File {
                size,
                link_target: Some(link_target),
                ..
            } => {
                write!(
                    w,
                    "{}  {:>8}  {} -> {}\n",
                    self.r#type.to_string().to_lowercase(),
                    byte_count_short(*size),
                    self.name,
                    link_target,
                )?;
            }
            LsEntryOutput::File { size, .. } => {
                write!(
                    w,
                    "{}  {:>8}  {}\n",
                    self.r#type.to_string().to_lowercase(),
                    byte_count_short(*size),
                    self.name
                )?;
            }
        }
        Ok(())
    }
}

impl Render for LsOutput {
    type Args = CommandArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        if args.all || !self.name.starts_with('.') {
            if args.long {
                self.render_long(w)?;
            } else {
                self.render_short(w)?;
            }
        }
        Ok(())
    }

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

async fn fetch_link_target(
    connection: Connection,
    repo: thrift::RepoSpecifier,
    id: Vec<u8>,
) -> Option<String> {
    let file = thrift::FileSpecifier::by_id(thrift::FileIdSpecifier {
        repo,
        id,
        ..Default::default()
    });
    let params = thrift::FileContentChunkParams {
        offset: 0,
        size: MAX_LINK_NAME_LEN,
        ..Default::default()
    };
    match connection.file_content_chunk(&file, &params).await {
        Ok(response) => Some(String::from_utf8_lossy(&response.data).into_owned()),
        Err(_) => None,
    }
}

fn list_output(
    connection: Connection,
    repo: thrift::RepoSpecifier,
    response: thrift::TreeListResponse,
    long: bool,
) -> impl Stream<Item = Result<LsOutput>> {
    stream::iter(response.entries)
        .map(move |entry| {
            let connection = connection.clone();
            let repo = repo.clone();
            async move {
                let entry_output = match entry.info {
                    thrift::EntryInfo::tree(info) => {
                        let id = faster_hex::hex_string(&info.id);
                        let simple_format_sha1 = faster_hex::hex_string(&info.simple_format_sha1);
                        let simple_format_sha256 =
                            faster_hex::hex_string(&info.simple_format_sha256);
                        LsEntryOutput::Tree {
                            id,
                            simple_format_sha1,
                            simple_format_sha256,
                            child_files_count: info.child_files_count,
                            child_dirs_count: info.child_dirs_count,
                            child_files_total_size: info.child_files_total_size,
                            descendant_files_count: info.descendant_files_count,
                            descendant_files_total_size: info.descendant_files_total_size,
                        }
                    }
                    thrift::EntryInfo::file(info) => {
                        let id = faster_hex::hex_string(&info.id);
                        let content_sha1 = faster_hex::hex_string(&info.content_sha1);
                        let content_sha256 = faster_hex::hex_string(&info.content_sha256);
                        let link_target = if long && entry.r#type == thrift::EntryType::LINK {
                            fetch_link_target(connection.clone(), repo.clone(), info.id.clone())
                                .await
                        } else {
                            None
                        };
                        LsEntryOutput::File {
                            id,
                            content_sha1,
                            content_sha256,
                            size: info.file_size,
                            link_target,
                        }
                    }
                    _ => {
                        bail!("malformed response format for '{}'", entry.name);
                    }
                };
                Ok(LsOutput {
                    name: entry.name,
                    r#type: entry.r#type.to_string().to_lowercase(),
                    entry: entry_output,
                })
            }
        })
        .buffered(CONCURRENT_ITEM_FETCHES)
}

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 commit = thrift::CommitSpecifier {
        repo: repo.clone(),
        id,
        ..Default::default()
    };
    let path = args.path.clone();
    let tree = thrift::TreeSpecifier::by_commit_path(thrift::CommitPathSpecifier {
        commit,
        path,
        ..Default::default()
    });

    // Request the first chunk of the directory listing.
    let params = thrift::TreeListParams {
        offset: 0,
        limit: CHUNK_SIZE,
        ..Default::default()
    };
    let response = conn.tree_list(&tree, &params).await?;
    let count = response.count;
    let long = args.long;
    let output = list_output(conn.clone(), repo.clone(), response, long).chain(
        stream::iter((CHUNK_SIZE..count).step_by(CHUNK_SIZE as usize))
            .map({
                let connection = conn.clone();
                move |offset| {
                    // Request subsequent chunks of the directory listing.
                    let params = thrift::TreeListParams {
                        offset,
                        limit: CHUNK_SIZE,
                        ..Default::default()
                    };
                    connection.tree_list(&tree, &params)
                }
            })
            .buffered(CONCURRENT_FETCHES)
            .then(move |response| {
                let repo = repo.clone();
                let connection = conn.clone();
                async move {
                    response.map(move |response| {
                        list_output(connection.clone(), repo.clone(), response, long)
                    })
                }
            })
            .try_flatten(),
    );
    app.target.render(&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::bail;
use anyhow::Result;
use source_control::types as thrift;

use crate::args::commit_id::resolve_commit_ids;
use crate::args::commit_id::CommitIdsArgs;
use crate::args::pushvars::PushvarArgs;
use crate::args::repo::RepoArgs;
use crate::args::service_id::ServiceIdArgs;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Move a bookmark
///
/// If two commits are provided, then move the bookmark from the first commit
/// to the second commit, failing if the bookmark didn't previously point at
/// the first commit.
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,
    #[clap(flatten)]
    service_id_args: ServiceIdArgs,
    #[clap(flatten)]
    pushvar_args: PushvarArgs,
    #[clap(long, short)]
    /// Name of the bookmark to move
    name: String,
    #[clap(long)]
    /// Allow non-fast-forward moves (if permitted for this bookmark)
    allow_non_fast_forward_move: bool,
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo_args.into_repo_specifier();
    let commit_ids = args.commit_ids_args.into_commit_ids();
    if commit_ids.len() != 1 && commit_ids.len() != 2 {
        bail!("expected 1 or 2 commit_ids (got {})", commit_ids.len())
    }
    let conn = app.get_connection(Some(&repo.name))?;
    let ids = resolve_commit_ids(&conn, &repo, &commit_ids).await?;
    let bookmark = args.name;
    let service_identity = args.service_id_args.service_id;

    let (old_target, target) = match ids.as_slice() {
        [id] => (None, id.clone()),
        [old_id, new_id] => (Some(old_id.clone()), new_id.clone()),
        _ => bail!("expected 1 or 2 commit_ids (got {})", ids.len()),
    };
    let allow_non_fast_forward_move = args.allow_non_fast_forward_move;
    let pushvars = args.pushvar_args.into_pushvars();

    let params = thrift::RepoMoveBookmarkParams {
        bookmark,
        target,
        old_target,
        service_identity,
        allow_non_fast_forward_move,
        pushvars,
        ..Default::default()
    };
    conn.repo_move_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_ids;
use crate::args::commit_id::CommitIdsArgs;
use crate::args::derived_data_type::DerivedDataTypeArgs;
use crate::args::repo::RepoArgs;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Prepare a commit by deriving the required derived data type
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,
    #[clap(flatten)]
    commit_ids: CommitIdsArgs,
    #[clap(flatten)]
    derived_data_type: DerivedDataTypeArgs,
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let repo = args.repo.clone().into_repo_specifier();
    let conn = app.get_connection(Some(&repo.name))?;
    let params = thrift::RepoPrepareCommitsParams {
        commits: resolve_commit_ids(&conn, &repo, &args.commit_ids.clone().into_commit_ids())
            .await?,
        derived_data_type: args.derived_data_type.clone().into_derived_data_type(),
        ..Default::default()
    };
    conn.repo_prepare_commits(&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 std::collections::BTreeMap;
use std::io::Write;

use anyhow::bail;
use anyhow::Result;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use serde::Serialize;
use source_control::types as thrift;

use crate::args::commit_id::map_commit_id;
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::repo::RepoArgs;
use crate::library::commit_id::render_commit_id;
use crate::render::Render;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Find pushrebase history for a public commit by traversing mappings
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
}

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

impl Render for CommitLookupOutput {
    type Args = CommandArgs;

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

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

#[derive(Serialize)]
struct PushrebaseLookupOutput {
    commit_lookups: Vec<CommitLookupOutput>,
}

impl Render for PushrebaseLookupOutput {
    type Args = CommandArgs;

    fn render(&self, args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        for (i, commit) in self.commit_lookups.iter().enumerate() {
            if i > 0 {
                write!(w, "--\n")?;
            }
            commit.render(args, w)?;
        }
        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_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 commit = thrift::CommitSpecifier {
        repo,
        id,
        ..Default::default()
    };
    let pushrebase_history = conn
        .commit_lookup_pushrebase_history(
            &commit,
            &thrift::CommitLookupPushrebaseHistoryParams {
                ..Default::default()
            },
        )
        .await?;
    let lookup_params = thrift::CommitLookupParams {
        identity_schemes: args.scheme_args.clone().into_request_schemes(),
        ..Default::default()
    };
    let commit_lookups: Vec<_> = stream::iter(pushrebase_history.history.clone())
        .map(|commit| conn.commit_lookup(&commit, &lookup_params))
        .buffered(10)
        .try_collect()
        .await?;
    let commit_lookups: Vec<_> = pushrebase_history
        .history
        .into_iter()
        .zip(commit_lookups)
        .filter_map(|(commit, commit_lookup)| {
            let ids = match &commit_lookup.ids {
                Some(ids) => map_commit_ids(ids.values()),
                None => BTreeMap::new(),
            };

            if let Some((_, id)) = map_commit_id(&commit.id) {
                Some(CommitLookupOutput {
                    repo_name: commit.repo.name,
                    requested: id,
                    exists: commit_lookup.exists,
                    ids,
                })
            } else {
                None
            }
        })
        .collect();
    let output = PushrebaseLookupOutput { commit_lookups };
    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.
 */

//! List repositories.

use std::io::Write;

use anyhow::Result;
use clap::Parser;
use serde::Serialize;
use source_control::types as thrift;

use crate::render::Render;
use crate::ScscApp;

#[derive(Parser)]
/// List repositories
pub(super) struct CommandArgs {}

#[derive(Serialize)]
struct ReposOutput {
    repos: Vec<String>,
}

impl Render for ReposOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        for repo in self.repos.iter() {
            write!(w, "{}\n", repo)?;
        }
        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 params = thrift::ListReposParams {
        ..Default::default()
    };
    let conn = app.get_connection(None)?;
    let repos = conn.list_repos(&params).await?;
    app.target
        .render_one(
            &args,
            ReposOutput {
                repos: repos.into_iter().map(|repo| repo.name).collect(),
            },
        )
        .await
}


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

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

use anyhow::Result;
use serde::Serialize;
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::render::Render;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Run hooks on a commit without pushing it
///
/// Provide a commit and the bookmark you plan to push to.
/// The hooks that would run when you push this commit to bookmark will run now
/// and their outcomes will be reported. A success does NOT guarantee
/// the commit will successfully land (e.g. conflicts may prevent landing).
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(flatten)]
    pushvar_args: PushvarArgs,
    #[clap(long)]
    /// Name of the bookmark you would push to if pushing for real
    to: String,
}

#[derive(Serialize)]
#[serde(tag = "status")]
enum HookOutcome {
    Accepted,
    Rejected { reason: String },
}

#[derive(Serialize)]
struct RunHooksOutput {
    commit: String,
    bookmark: String,
    outcomes: BTreeMap<String, HookOutcome>,
}

impl Render for RunHooksOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        write!(
            w,
            "Hook outcomes when dry-run landing {} to bookmark {}:\n\n",
            self.commit, self.bookmark
        )?;
        for (hook_name, outcome) in &self.outcomes {
            write!(w, "{} => ", hook_name)?;
            match outcome {
                HookOutcome::Accepted => write!(w, "ACCEPTED\n")?,
                HookOutcome::Rejected { reason } => write!(w, "REJECTED: {}\n", reason)?,
            };
        }
        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 original_commit_id = args.commit_id_args.clone().into_commit_id();
    let conn = app.get_connection(Some(&repo.name))?;
    let commit_id = resolve_commit_id(&conn, &repo, &original_commit_id).await?;
    let commit_specifier = thrift::CommitSpecifier {
        id: commit_id,
        repo,
        ..Default::default()
    };
    let bookmark: String = args.to.clone();
    let pushvars = args.pushvar_args.clone().into_pushvars();

    let params = thrift::CommitRunHooksParams {
        bookmark: bookmark.clone(),
        pushvars,
        ..Default::default()
    };
    let response = conn.commit_run_hooks(&commit_specifier, &params).await?;
    let outcomes = response
        .outcomes
        .into_iter()
        .map(|(name, outcome)| {
            Ok((
                name,
                match outcome {
                    thrift::HookOutcome::accepted(_) => HookOutcome::Accepted,
                    thrift::HookOutcome::rejections(rejs) => HookOutcome::Rejected {
                        reason: rejs
                            .into_iter()
                            .map(|rej| rej.long_description)
                            .collect::<Vec<_>>()
                            .join("\n"),
                    },
                    thrift::HookOutcome::UnknownField(_) => anyhow::bail!("Unknown hook outcome"),
                },
            ))
        })
        .collect::<Result<_>>()?;
    let output = RunHooksOutput {
        commit: original_commit_id.to_string(),
        bookmark,
        outcomes,
    };
    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 std::io::Write;

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

use crate::args::commit_id::resolve_commit_ids;
use crate::args::commit_id::CommitIdsArgs;
use crate::args::repo::RepoArgs;
use crate::args::sparse_profiles::SparseProfilesArgs;
use crate::render::Render;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Calculate the size change for each sparse profile between two given commits
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    #[clap(flatten)]
    commit_ids_args: CommitIdsArgs,

    #[clap(flatten)]
    sparse_profiles_args: SparseProfilesArgs,
}

#[derive(Serialize)]
struct SparseProfileDeltaOutput {
    changed_sparse_profiles: Option<thrift::SparseProfileDeltaSizes>,
}

impl Render for SparseProfileDeltaOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        if let Some(delta_sizes) = &self.changed_sparse_profiles {
            if !delta_sizes.size_changes.is_empty() {
                for (profile_name, profile_change) in delta_sizes.size_changes.iter() {
                    match profile_change.change {
                        thrift::SparseProfileChangeElement::added(thrift::SparseProfileAdded {
                            size,
                            ..
                        }) => writeln!(w, "profile {} was added, size: {}", profile_name, size)?,
                        thrift::SparseProfileChangeElement::removed(
                            thrift::SparseProfileRemoved { previous_size, .. },
                        ) => writeln!(
                            w,
                            "profile {} was removed, previous size: {}",
                            profile_name, previous_size
                        )?,
                        thrift::SparseProfileChangeElement::changed(
                            thrift::SparseProfileSizeChanged { size_change, .. },
                        ) => writeln!(
                            w,
                            "profile {} was changed, size change: {}",
                            profile_name, size_change
                        )?,
                        _ => bail!("unrecognized change!"),
                    };
                }
            } else {
                writeln!(w, "no changes found")?;
            }
        } else {
            writeln!(w, "no changes found")?;
        }

        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 {
        bail!("expected 2 commit_ids (got {})", commit_ids.len())
    }

    let conn = app.get_connection(Some(&repo.name))?;
    let commit_ids = resolve_commit_ids(&conn, &repo, &commit_ids).await?;

    let profiles = args.sparse_profiles_args.clone().into_sparse_profiles();

    let commit = thrift::CommitSpecifier {
        repo: repo.clone(),
        id: commit_ids[0].clone(),
        ..Default::default()
    };

    let params = thrift::CommitSparseProfileDeltaParams {
        other_id: commit_ids[1].clone(),
        profiles,
        ..Default::default()
    };

    let response = conn.commit_sparse_profile_delta(&commit, &params).await?;

    let output = SparseProfileDeltaOutput {
        changed_sparse_profiles: response.changed_sparse_profiles,
    };

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

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

use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitIdArgs;
use crate::args::repo::RepoArgs;
use crate::args::sparse_profiles::SparseProfilesArgs;
use crate::render::Render;
use crate::ScscApp;

#[derive(clap::Parser)]
/// Calculate the total size of each sparse profile for a given commit
pub(super) struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    #[clap(flatten)]
    commit_id_args: CommitIdArgs,

    #[clap(flatten)]
    sparse_profiles_args: SparseProfilesArgs,
}

#[derive(Serialize)]
struct SparseProfileSizeOutput {
    profiles_size: thrift::SparseProfileSizes,
}

impl Render for SparseProfileSizeOutput {
    type Args = CommandArgs;

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        if self.profiles_size.sizes.is_empty() {
            writeln!(w, "no profiles to display")?;
        } else {
            for (profile_name, thrift::SparseProfileSize { size, .. }) in
                self.profiles_size.sizes.iter()
            {
                writeln!(w, "profile: {}, size: {}", profile_name, size)?;
            }
        }
        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_id = args.commit_id_args.clone().into_commit_id();
    let conn = app.get_connection(Some(&repo.name))?;
    let commit_id = resolve_commit_id(&conn, &repo, &commit_id).await?;

    let profiles = args.sparse_profiles_args.clone().into_sparse_profiles();

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

    let params = thrift::CommitSparseProfileSizeParams {
        profiles,
        ..Default::default()
    };

    let response = conn.commit_sparse_profile_size(&commit, &params).await?;

    let output = SparseProfileSizeOutput {
        profiles_size: response.profiles_size,
    };

    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 std::collections::BTreeMap;

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

use crate::args::commit_id::map_commit_ids;
use crate::args::commit_id::resolve_commit_id;
use crate::args::commit_id::CommitId;
use crate::args::commit_id::CommitIdArgs;
use crate::args::commit_id::SchemeArgs;
use crate::commands::lookup::LookupOutput;
use crate::connection::Connection;
use crate::ScscApp;

#[derive(clap::Parser)]
#[clap(group(
    clap::ArgGroup::new("hint")
    .args(&["hint_exact_commit", "hint_ancestor_of_commit", "hint_descendant_of_commit",
            "hint_ancestor_of_bookmark", "hint_descendant_of_bookmark"])
))]
/// Sync a commit between repositories
pub(super) struct CommandArgs {
    #[clap(flatten)]
    scheme_args: SchemeArgs,
    #[clap(flatten)]
    commit_id_args: CommitIdArgs,
    #[clap(long)]
    /// Source repository name
    source_repo: String,
    #[clap(long)]
    /// Target repository name
    target_repo: String,
    #[clap(long)]
    /// For Source Control use only. A commit to use as an Exact CandidateSelectionHint
    hint_exact_commit: Option<String>,
    #[clap(long)]
    /// For Source Control use only. A commit to use as an OnlyOrAncestorOfCommit CandidateSelectionHint
    hint_ancestor_of_commit: Option<String>,
    #[clap(long)]
    /// For Source Control use only. A commit to use as an OnlyOrDescendantOfCommit CandidateSelectionHint
    hint_descendant_of_commit: Option<String>,
    #[clap(long)]
    /// For Source Control use only. A bookmark to use as an OnlyOrAncestorOfBookmark CandidateSelectionHint
    hint_ancestor_of_bookmark: Option<String>,
    #[clap(long)]
    /// For Source Control use only. A bookmark to use as an OnlyOrDescendantOfBookmark CandidateSelectionHint
    hint_descendant_of_bookmark: Option<String>,
}

async fn build_commit_hint(
    connection: &Connection,
    target_repo: &thrift::RepoSpecifier,
    commit_id: &str,
    constructor: impl Fn(thrift::CommitId) -> thrift::CandidateSelectionHint,
) -> Result<thrift::CandidateSelectionHint> {
    let to_resolve = CommitId::Resolve(commit_id.to_string());
    let commit_id = resolve_commit_id(connection, target_repo, &to_resolve).await?;
    Ok(constructor(commit_id))
}

async fn build_hint(
    args: &CommandArgs,
    connection: &Connection,
    target_repo: &thrift::RepoSpecifier,
) -> Result<Option<thrift::CandidateSelectionHint>> {
    if let Some(commit_id) = &args.hint_exact_commit {
        Ok(Some(
            build_commit_hint(
                connection,
                target_repo,
                commit_id,
                thrift::CandidateSelectionHint::exact,
            )
            .await?,
        ))
    } else if let Some(commit_id) = &args.hint_ancestor_of_commit {
        Ok(Some(
            build_commit_hint(
                connection,
                target_repo,
                commit_id,
                thrift::CandidateSelectionHint::commit_ancestor,
            )
            .await?,
        ))
    } else if let Some(commit_id) = &args.hint_descendant_of_commit {
        Ok(Some(
            build_commit_hint(
                connection,
                target_repo,
                commit_id,
                thrift::CandidateSelectionHint::commit_descendant,
            )
            .await?,
        ))
    } else if let Some(bookmark) = args.hint_ancestor_of_bookmark.clone() {
        Ok(Some(thrift::CandidateSelectionHint::bookmark_ancestor(
            bookmark,
        )))
    } else if let Some(bookmark) = args.hint_descendant_of_bookmark.clone() {
        Ok(Some(thrift::CandidateSelectionHint::bookmark_descendant(
            bookmark,
        )))
    } else {
        Ok(None)
    }
}

pub(super) async fn run(app: ScscApp, args: CommandArgs) -> Result<()> {
    let source_repo = get_repo_specifier(args.source_repo.clone());
    let target_repo = get_repo_specifier(args.target_repo.clone());

    let commit_id = args.commit_id_args.clone().into_commit_id();
    let src_repo_conn = app.get_connection(Some(&source_repo.name))?;
    let id = resolve_commit_id(&src_repo_conn, &source_repo, &commit_id).await?;
    let target_repo_conn = app.get_connection(Some(&target_repo.name))?;
    let hint = build_hint(&args, &target_repo_conn, &target_repo).await?;

    let commit = thrift::CommitSpecifier {
        repo: source_repo,
        id,
        ..Default::default()
    };
    let params = thrift::CommitLookupXRepoParams {
        other_repo: target_repo,
        identity_schemes: args.scheme_args.clone().into_request_schemes(),
        candidate_selection_hint: hint,
        ..Default::default()
    };
    // XXX Repos for xrepo methods need to be available on all servers,
    // no matter if they're sharded or not, because SM doesn't support
    // shard colocation.
    let response = src_repo_conn.commit_lookup_xrepo(&commit, &params).await?;
    let ids = match &response.ids {
        Some(ids) => map_commit_ids(ids.values()),
        None => BTreeMap::new(),
    };

    let output = LookupOutput {
        requested: commit_id.to_string(),
        exists: response.exists,
        ids,
    };

    app.target.render_one(&args.scheme_args, output).await
}

fn get_repo_specifier(name: String) -> thrift::RepoSpecifier {
    thrift::RepoSpecifier {
        name,
        ..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.
 */

//! Helper library for rendering bookmark info

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

use anyhow::Error;
use chrono::FixedOffset;
use chrono::Local;
use chrono::TimeZone;
use serde::Serialize;
use source_control::types as thrift;

use crate::args::commit_id::map_commit_ids;
use crate::library::commit_id::render_commit_id;

#[derive(Serialize)]
pub(crate) struct BookmarkInfo {
    pub r#type: String, // For JSON output, always "bookmark".
    pub warm_ids: BTreeMap<String, String>,
    pub fresh_ids: BTreeMap<String, String>,
    pub last_update_timestamp_ns: i64,
}

impl TryFrom<&thrift::BookmarkInfo> for BookmarkInfo {
    type Error = Error;

    fn try_from(bookmark: &thrift::BookmarkInfo) -> Result<BookmarkInfo, Error> {
        let warm_ids = map_commit_ids(bookmark.warm_ids.values());
        let fresh_ids = map_commit_ids(bookmark.fresh_ids.values());
        let last_update_timestamp_ns = bookmark.last_update_timestamp_ns;

        Ok(BookmarkInfo {
            r#type: "bookmark".to_string(),
            warm_ids,
            fresh_ids,
            last_update_timestamp_ns,
        })
    }
}

pub(crate) fn render_bookmark_info(
    bookmark_info: &BookmarkInfo,
    requested: &str,
    schemes: &HashSet<String>,
    w: &mut dyn Write,
) -> Result<(), Error> {
    render_commit_id(
        Some(("Warm value", "    ")),
        "\n",
        requested,
        &bookmark_info.warm_ids,
        schemes,
        w,
    )?;
    write!(w, "\n")?;
    render_commit_id(
        Some(("Fresh value", "    ")),
        "\n",
        requested,
        &bookmark_info.fresh_ids,
        schemes,
        w,
    )?;
    write!(w, "\n")?;
    let date = FixedOffset::west_opt(0)
        .unwrap()
        .timestamp_nanos(bookmark_info.last_update_timestamp_ns);
    let date_str = date.to_string();
    let local_date_str = date.with_timezone(&Local).to_string();
    if date_str != local_date_str {
        write!(w, "Last update: {} ({})\n", date_str, local_date_str)?;
    } else {
        write!(w, "Last update: {}\n", date_str)?;
    }
    Ok(())
}