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' } } }}
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'
}
}
}
}