Sebastian Thielthanks clippyd533f0c4 months agoHistory
270 lines · 9.8 KB · Rust
RawDownload
use std::path::Path; use anyhow::bail;use gix::{    bstr::{BStr, BString, ByteSlice},    status::{self, index_worktree},};use gix_status::index_as_worktree::{Change, Conflict, EntryStatus}; use crate::OutputFormat; pub enum Submodules {    /// display all information about submodules, including ref changes, modifications and untracked files.    All,    /// Compare only the configuration of the superprojects commit with the actually checked out `HEAD` commit.    RefChange,    /// See if there are worktree modifications compared to the index, but do not check for untracked files.    Modifications,    /// Ignore all submodule changes.    None,} #[derive(Copy, Clone)]pub enum Ignored {    Collapsed,    Matching,} #[derive(Copy, Clone)]pub enum Format {    Simplified,    PorcelainV2,} pub struct Options {    pub ignored: Option<Ignored>,    pub format: Format,    pub output_format: OutputFormat,    pub submodules: Option<Submodules>,    pub thread_limit: Option<usize>,    pub statistics: bool,    pub allow_write: bool,    pub index_worktree_renames: Option<f32>,    pub untracked: Option<gix::status::UntrackedFiles>,} pub fn show(    repo: gix::Repository,    pathspecs: Vec<BString>,    mut out: impl std::io::Write,    mut err: impl std::io::Write,    mut progress: impl gix::NestedProgress + 'static,    Options {        ignored,        format,        output_format,        submodules,        thread_limit,        allow_write,        statistics,        index_worktree_renames,        untracked,    }: Options,) -> anyhow::Result<()> {    if output_format != OutputFormat::Human {        bail!("Only human format is supported right now");    }    if !matches!(format, Format::Simplified) {        bail!("Only the simplified format is currently implemented");    }     let start = std::time::Instant::now();    let prefix = repo.prefix()?.unwrap_or(Path::new(""));    let index_progress = progress.add_child("traverse index");    let mut status = repo        .status(index_progress)?        .should_interrupt_shared(&gix::interrupt::IS_INTERRUPTED);    if let Some(untracked) = untracked {        status = status.untracked_files(untracked);    }    let mut iter = status        .index_worktree_options_mut(|opts| {            if let Some((opts, ignored)) = opts.dirwalk_options.as_mut().zip(ignored) {                opts.set_emit_ignored(Some(match ignored {                    Ignored::Collapsed => gix::dir::walk::EmissionMode::CollapseDirectory,                    Ignored::Matching => gix::dir::walk::EmissionMode::Matching,                }));            }            opts.rewrites = index_worktree_renames.map(|percentage| gix::diff::Rewrites {                copies: None,                percentage: Some(percentage),                limit: 0,                track_empty: false,            });            if opts.rewrites.is_some() {                opts.dirwalk_options.iter_mut().for_each(|opts| {                    opts.set_emit_untracked(gix::dir::walk::EmissionMode::Matching);                    if ignored.is_some() {                        opts.set_emit_ignored(Some(gix::dir::walk::EmissionMode::Matching));                    }                });            }            opts.thread_limit = thread_limit;            opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive);        })        .index_worktree_submodules(match submodules {            Some(mode) => {                let ignore = match mode {                    Submodules::All => gix::submodule::config::Ignore::None,                    Submodules::RefChange => gix::submodule::config::Ignore::Dirty,                    Submodules::Modifications => gix::submodule::config::Ignore::Untracked,                    Submodules::None => gix::submodule::config::Ignore::All,                };                gix::status::Submodule::Given {                    ignore,                    check_dirty: false,                }            }            None => gix::status::Submodule::AsConfigured { check_dirty: false },        })        .into_iter(pathspecs)?;     for item in iter.by_ref() {        let item = item?;        match item {            status::Item::TreeIndex(change) => {                let (location, _, _, _) = change.fields();                let status = match change {                    gix::diff::index::Change::Addition { .. } => "A",                    gix::diff::index::Change::Deletion { .. } => "D",                    gix::diff::index::Change::Modification { .. } => "M",                    gix::diff::index::Change::Rewrite {                        ref source_location, ..                    } => {                        let source_location = gix::path::from_bstr(source_location.as_ref());                        let source_location = gix::path::relativize_with_prefix(&source_location, prefix);                        writeln!(                            out,                            "{status: >2}  {source_rela_path} → {dest_rela_path}",                            status = "R",                            source_rela_path = source_location.display(),                            dest_rela_path =                                gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(),                        )?;                        continue;                    }                };                writeln!(                    out,                    "{status: >2}  {rela_path}",                    rela_path = gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(),                )?;            }            status::Item::IndexWorktree(index_worktree::Item::Modification {                entry: _,                entry_index: _,                rela_path,                status,            }) => print_index_entry_status(&mut out, prefix, rela_path.as_ref(), status)?,            status::Item::IndexWorktree(index_worktree::Item::DirectoryContents {                entry,                collapsed_directory_status,            }) => {                if collapsed_directory_status.is_none() {                    writeln!(                        out,                        "{status: >3} {rela_path}{slash}",                        status = "?",                        rela_path =                            gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display(),                        slash = if entry.disk_kind.unwrap_or(gix::dir::entry::Kind::File).is_dir() {                            "/"                        } else {                            ""                        }                    )?;                }            }            status::Item::IndexWorktree(index_worktree::Item::Rewrite {                source, dirwalk_entry, ..            }) => {                // TODO: handle multi-status characters, there can also be modifications at the same time as determined by their ID and potentially diffstats.                writeln!(                    out,                    "{status: >3} {source_rela_path} → {dest_rela_path}",                    status = "R",                    source_rela_path =                        gix::path::relativize_with_prefix(&gix::path::from_bstr(source.rela_path()), prefix).display(),                    dest_rela_path = gix::path::relativize_with_prefix(                        &gix::path::from_bstr(dirwalk_entry.rela_path.as_bstr()),                        prefix                    )                    .display(),                )?;            }        }    }    if gix::interrupt::is_triggered() {        bail!("interrupted by user");    }     let out = iter.outcome_mut().expect("successful iteration has outcome");     if out.has_changes() && allow_write {        out.write_changes().transpose()?;    }     if statistics {        writeln!(err, "{outcome:#?}", outcome = out.index_worktree).ok();    }     progress.init(Some(out.worktree_index.entries().len()), gix::progress::count("files"));    progress.set(out.worktree_index.entries().len());    progress.show_throughput(start);    Ok(())} fn print_index_entry_status(    out: &mut dyn std::io::Write,    prefix: &Path,    rela_path: &BStr,    status: EntryStatus<(), gix::submodule::Status>,) -> std::io::Result<()> {    let char_storage;    let status = match status {        EntryStatus::Conflict { summary, entries: _ } => as_str(summary),        EntryStatus::Change(change) => {            char_storage = change_to_char(&change);            std::str::from_utf8(std::slice::from_ref(&char_storage)).expect("valid ASCII")        }        EntryStatus::NeedsUpdate(_stat) => {            return Ok(());        }        EntryStatus::IntentToAdd => "A",    };     let rela_path = gix::path::from_bstr(rela_path);    let display_path = gix::path::relativize_with_prefix(&rela_path, prefix);    writeln!(out, "{status: >3} {}", display_path.display())} fn as_str(c: Conflict) -> &'static str {    match c {        Conflict::BothDeleted => "DD",        Conflict::AddedByUs => "AU",        Conflict::DeletedByThem => "UD",        Conflict::AddedByThem => "UA",        Conflict::DeletedByUs => "DU",        Conflict::BothAdded => "AA",        Conflict::BothModified => "UU",    }} fn change_to_char(change: &Change<(), gix::submodule::Status>) -> u8 {    // Known status letters: https://github.com/git/git/blob/6807fcfedab84bc8cd0fbf721bc13c4e68cda9ae/diff.h#L613    match change {        Change::Removed => b'D',        Change::Type { .. } => b'T',        Change::SubmoduleModification(_) => b'M',        Change::Modification {            executable_bit_changed, ..        } => {            if *executable_bit_changed {                b'X'            } else {                b'M'            }        }    }}