Skip to content

gix-status-improvements #1030

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4c03fdb
feat!: add `hash::bytes_with_header()`, also make it 32bit compatible.
Byron Sep 28, 2023
9e7c3e1
adapt to `gix-features`
Byron Sep 28, 2023
1568948
add more traces to potentially longer-running operations
Byron Sep 28, 2023
a1794b5
fix!: use `PathStorageRef` in place of `&PathStorage`
Byron Sep 28, 2023
c044919
add symlink checking for `gix status`
Byron Sep 23, 2023
53de126
feat!: add support for submodule status
Byron Sep 27, 2023
0d01eb2
feat!: provide statistics at the end of a index status operation
Byron Sep 27, 2023
0e10b62
feat: a way for `status` to stop early.
Byron Sep 27, 2023
f159775
feat!: add `Stack::from_state_and_ignore_case()`.
Byron Sep 28, 2023
34318fa
use latest utilities from `gix-worktree`
Byron Sep 28, 2023
260c781
fix!: don't provide path to object-retrieval callback of `Pipeline::c…
Byron Sep 28, 2023
9283a9d
fix!: `encode::loose_header()` now supports large objects even on 32 …
Byron Sep 28, 2023
ffcb110
adapt to changes in `gix-object`
Byron Sep 28, 2023
de66b4c
feat: `status` now supports filters.
Byron Sep 28, 2023
b55a8d5
feat!: add entries-relative index to each change.
Byron Oct 3, 2023
60c948f
feat!: replace `conflict` marker with detailed decoding of stages.
Byron Oct 4, 2023
54fb7c2
adapt to changes in `gix-status`
Byron Sep 27, 2023
7d9ecdd
feat: add `Repository::index_or_empty()`.
Byron Sep 27, 2023
7ba2fa1
feat: `gix status -s/--statistics` to obtain additional information o…
Byron Sep 27, 2023
b5b50f8
fix: hanging `Read` implementation of `pipeline::convert::ToGitOutcome`.
Byron Sep 28, 2023
46e5919
feat: `gix status` auto-writes changed indices.
Byron Sep 28, 2023
f929d42
trust Ctime again
Byron Sep 29, 2023
67a0220
fix gix-command tests on windows
Byron Oct 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crate-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,9 @@ Make it the best-performing implementation and the most convenient one.

### gix-status
* [x] differences between index and worktree to turn index into worktree
* [ ] differences between tree and index to turn tree into index
- [ ] rename tracking
* [ ] differences between index and index to learn what changed
- [ ] rename tracking
* [ ] untracked files
* [ ] fast answer to 'is it dirty'.
*
Expand Down
7 changes: 4 additions & 3 deletions gitoxide-core/src/repository/index/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,10 @@ pub(crate) mod function {
out,
"{} {}{:?} {} {}{}{}",
match entry.flags.stage() {
0 => "BASE ",
1 => "OURS ",
2 => "THEIRS ",
0 => " ",
1 => "BASE ",
2 => "OURS ",
3 => "THEIRS ",
_ => "UNKNOWN",
},
if entry.flags.is_empty() {
Expand Down
177 changes: 141 additions & 36 deletions gitoxide-core/src/repository/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use gix::bstr::{BStr, BString};
use gix::index::Entry;
use gix::prelude::FindExt;
use gix::Progress;
use gix_status::index_as_worktree::content::FastEq;
use gix_status::index_as_worktree::Change;
use gix_status::index_as_worktree::traits::FastEq;
use gix_status::index_as_worktree::{Change, Conflict, EntryStatus};

pub enum Submodules {
/// display all information about submodules, including ref changes, modifications and untracked files.
Expand All @@ -20,6 +20,8 @@ pub struct Options {
pub format: OutputFormat,
pub submodules: Submodules,
pub thread_limit: Option<usize>,
pub statistics: bool,
pub allow_write: bool,
}

pub fn show(
Expand All @@ -33,12 +35,14 @@ pub fn show(
// TODO: implement this
submodules: _,
thread_limit,
allow_write,
statistics,
}: Options,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
bail!("Only human format is supported right now");
}
let mut index = repo.index()?;
let mut index = repo.index_or_empty()?;
let index = gix::threading::make_mut(&mut index);
let pathspec = repo.pathspec(
pathspecs,
Expand All @@ -48,73 +52,174 @@ pub fn show(
)?;
let mut progress = progress.add_child("traverse index");
let start = std::time::Instant::now();
gix_status::index_as_worktree(
let options = gix_status::index_as_worktree::Options {
fs: repo.filesystem_options()?,
thread_limit,
stat: repo.stat_options()?,
attributes: match repo
.attributes_only(
index,
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
)?
.detach()
.state_mut()
{
gix::worktree::stack::State::AttributesStack(attrs) => std::mem::take(attrs),
// TODO: this should be nicer by creating attributes directly, but it's a private API
_ => unreachable!("state must be attributes stack only"),
},
};
let mut printer = Printer {
out,
changes: Vec::new(),
};
let outcome = gix_status::index_as_worktree(
index,
repo.work_dir()
.context("This operation cannot be run on a bare repository")?,
&mut Printer(out),
&mut printer,
FastEq,
Submodule,
{
let odb = repo.objects.clone().into_arc()?;
move |id, buf| odb.find_blob(id, buf)
},
&mut progress,
pathspec.detach()?,
gix_status::index_as_worktree::Options {
fs: repo.filesystem_options()?,
thread_limit,
stat: repo.stat_options()?,
},
repo.filter_pipeline(Some(gix::hash::ObjectId::empty_tree(repo.object_hash())))?
.0
.into_parts()
.0,
&gix::interrupt::IS_INTERRUPTED,
options,
)?;

if outcome.entries_to_update != 0 && allow_write {
{
let entries = index.entries_mut();
for (entry_index, change) in printer.changes {
let entry = &mut entries[entry_index];
match change {
ApplyChange::SetSizeToZero => {
entry.stat.size = 0;
}
ApplyChange::NewStat(new_stat) => {
entry.stat = new_stat;
}
}
}
}
index.write(gix::index::write::Options {
extensions: Default::default(),
skip_hash: false, // TODO: make this based on configuration
})?;
}

if statistics {
writeln!(err, "{outcome:#?}").ok();
}

writeln!(err, "\nhead -> index and untracked files aren't implemented yet")?;
progress.show_throughput(start);
Ok(())
}

struct Printer<W>(W);
#[derive(Clone)]
struct Submodule;

impl gix_status::index_as_worktree::traits::SubmoduleStatus for Submodule {
type Output = ();
type Error = std::convert::Infallible;

fn status(&mut self, _entry: &Entry, _rela_path: &BStr) -> Result<Option<Self::Output>, Self::Error> {
Ok(None)
}
}

struct Printer<W> {
out: W,
changes: Vec<(usize, ApplyChange)>,
}

enum ApplyChange {
SetSizeToZero,
NewStat(gix::index::entry::Stat),
}

impl<'index, W> gix_status::index_as_worktree::VisitEntry<'index> for Printer<W>
where
W: std::io::Write,
{
type ContentChange = ();
type SubmoduleStatus = ();

fn visit_entry(
&mut self,
entry: &'index Entry,
_entries: &'index [Entry],
_entry: &'index Entry,
entry_index: usize,
rela_path: &'index BStr,
change: Option<Change<Self::ContentChange>>,
conflict: bool,
status: EntryStatus<Self::ContentChange>,
) {
self.visit_inner(entry, rela_path, change, conflict).ok();
self.visit_inner(entry_index, rela_path, status).ok();
}
}

impl<W: std::io::Write> Printer<W> {
fn visit_inner(
&mut self,
_entry: &Entry,
rela_path: &BStr,
change: Option<Change<()>>,
conflict: bool,
) -> anyhow::Result<()> {
if let Some(change) = conflict
.then_some('U')
.or_else(|| change.as_ref().and_then(change_to_char))
{
writeln!(&mut self.0, "{change} {rela_path}")?;
}
Ok(())
fn visit_inner(&mut self, entry_index: usize, rela_path: &BStr, status: EntryStatus<()>) -> std::io::Result<()> {
let char_storage;
let status = match status {
EntryStatus::Conflict(conflict) => as_str(conflict),
EntryStatus::Change(change) => {
if matches!(
change,
Change::Modification {
set_entry_stat_size_zero: true,
..
}
) {
self.changes.push((entry_index, ApplyChange::SetSizeToZero))
}
char_storage = change_to_char(&change);
std::str::from_utf8(std::slice::from_ref(&char_storage)).expect("valid ASCII")
}
EntryStatus::NeedsUpdate(stat) => {
self.changes.push((entry_index, ApplyChange::NewStat(stat)));
return Ok(());
}
EntryStatus::IntentToAdd => "A",
};

writeln!(&mut self.out, "{status: >3} {rela_path}")
}
}

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<()>) -> Option<char> {
fn change_to_char(change: &Change<()>) -> u8 {
// Known status letters: https://github.com/git/git/blob/6807fcfedab84bc8cd0fbf721bc13c4e68cda9ae/diff.h#L613
Some(match change {
Change::Removed => 'D',
Change::Type => 'T',
Change::Modification { .. } => 'M',
Change::IntentToAdd => return None,
})
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'
}
}
}
}
18 changes: 6 additions & 12 deletions gix-command/tests/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,32 +82,26 @@ mod spawn {

#[test]
fn sh_shell_specific_script_code_with_single_extra_arg() -> crate::Result {
let out = gix_command::prepare("echo")
let out = gix_command::prepare("printf")
.with_shell()
.arg("1")
.spawn()?
.wait_with_output()?;
assert!(out.status.success());
#[cfg(not(windows))]
assert_eq!(out.stdout.as_bstr(), "1\n");
#[cfg(windows)]
assert_eq!(out.stdout.as_bstr(), "1\r\n");
assert_eq!(out.stdout.as_bstr(), "1");
Ok(())
}

#[test]
fn sh_shell_specific_script_code_with_multiple_extra_args() -> crate::Result {
let out = gix_command::prepare("echo")
let out = gix_command::prepare("printf")
.with_shell()
.arg("1")
.arg("2")
.arg("%s")
.arg("arg")
.spawn()?
.wait_with_output()?;
assert!(out.status.success());
#[cfg(not(windows))]
assert_eq!(out.stdout.as_bstr(), "1 2\n");
#[cfg(windows)]
assert_eq!(out.stdout.as_bstr(), "1 2\r\n");
assert_eq!(out.stdout.as_bstr(), "arg");
Ok(())
}
}
Expand Down
28 changes: 21 additions & 7 deletions gix-features/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub fn hasher(kind: gix_hash::Kind) -> Sha1 {
#[cfg(all(feature = "progress", any(feature = "rustsha1", feature = "fast-sha1")))]
pub fn bytes_of_file(
path: &std::path::Path,
num_bytes_from_start: usize,
num_bytes_from_start: u64,
kind: gix_hash::Kind,
progress: &mut dyn crate::progress::Progress,
should_interrupt: &std::sync::atomic::AtomicBool,
Expand All @@ -110,28 +110,42 @@ pub fn bytes_of_file(
)
}

/// Similar to [`bytes_of_file`], but operates on an already open file.
/// Similar to [`bytes_of_file`], but operates on a stream of bytes.
#[cfg(all(feature = "progress", any(feature = "rustsha1", feature = "fast-sha1")))]
pub fn bytes(
read: &mut dyn std::io::Read,
num_bytes_from_start: usize,
num_bytes_from_start: u64,
kind: gix_hash::Kind,
progress: &mut dyn crate::progress::Progress,
should_interrupt: &std::sync::atomic::AtomicBool,
) -> std::io::Result<gix_hash::ObjectId> {
let mut hasher = hasher(kind);
bytes_with_hasher(read, num_bytes_from_start, hasher(kind), progress, should_interrupt)
}

/// Similar to [`bytes()`], but takes a `hasher` instead of a hash kind.
#[cfg(all(feature = "progress", any(feature = "rustsha1", feature = "fast-sha1")))]
pub fn bytes_with_hasher(
read: &mut dyn std::io::Read,
num_bytes_from_start: u64,
mut hasher: Sha1,
progress: &mut dyn crate::progress::Progress,
should_interrupt: &std::sync::atomic::AtomicBool,
) -> std::io::Result<gix_hash::ObjectId> {
let start = std::time::Instant::now();
// init progress before the possibility for failure, as convenience in case people want to recover
progress.init(Some(num_bytes_from_start), crate::progress::bytes());
progress.init(
Some(num_bytes_from_start as prodash::progress::Step),
crate::progress::bytes(),
);

const BUF_SIZE: usize = u16::MAX as usize;
let mut buf = [0u8; BUF_SIZE];
let mut bytes_left = num_bytes_from_start;

while bytes_left > 0 {
let out = &mut buf[..BUF_SIZE.min(bytes_left)];
let out = &mut buf[..BUF_SIZE.min(bytes_left as usize)];
read.read_exact(out)?;
bytes_left -= out.len();
bytes_left -= out.len() as u64;
progress.inc_by(out.len());
hasher.update(out);
if should_interrupt.load(std::sync::atomic::Ordering::SeqCst) {
Expand Down
Loading