Skip to content

Commit 34db78d

Browse files
committed
feat: gix status with submodule and rewrite support.
Submodule changes are now picked up as long as the submodule is in the index. Further, it's possible to enable rename-tracking between index and worktree separately.
1 parent d78d21c commit 34db78d

File tree

3 files changed

+98
-195
lines changed

3 files changed

+98
-195
lines changed

gitoxide-core/src/repository/status.rs

Lines changed: 90 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
use anyhow::{bail, Context};
2-
use gix::bstr::ByteSlice;
3-
use gix::{
4-
bstr::{BStr, BString},
5-
index::Entry,
6-
Progress,
7-
};
8-
use gix_status::index_as_worktree::{traits::FastEq, Change, Conflict, EntryStatus};
9-
use std::path::{Path, PathBuf};
1+
use anyhow::bail;
2+
use gix::bstr::{BStr, BString};
3+
use gix::status::index_worktree::iter::Item;
4+
use gix_status::index_as_worktree::{Change, Conflict, EntryStatus};
5+
use std::path::Path;
106

117
use crate::OutputFormat;
128

@@ -17,11 +13,13 @@ pub enum Submodules {
1713
RefChange,
1814
/// See if there are worktree modifications compared to the index, but do not check for untracked files.
1915
Modifications,
16+
/// Ignore all submodule changes.
17+
None,
2018
}
2119

2220
pub struct Options {
2321
pub format: OutputFormat,
24-
pub submodules: Submodules,
22+
pub submodules: Option<Submodules>,
2523
pub thread_limit: Option<usize>,
2624
pub statistics: bool,
2725
pub allow_write: bool,
@@ -30,13 +28,12 @@ pub struct Options {
3028
pub fn show(
3129
repo: gix::Repository,
3230
pathspecs: Vec<BString>,
33-
out: impl std::io::Write,
31+
mut out: impl std::io::Write,
3432
mut err: impl std::io::Write,
35-
mut progress: impl gix::NestedProgress,
33+
mut progress: impl gix::NestedProgress + 'static,
3634
Options {
3735
format,
38-
// TODO: implement this
39-
submodules: _,
36+
submodules,
4037
thread_limit,
4138
allow_write,
4239
statistics,
@@ -45,198 +42,101 @@ pub fn show(
4542
if format != OutputFormat::Human {
4643
bail!("Only human format is supported right now");
4744
}
48-
let mut index = repo.index_or_empty()?;
49-
let index = gix::threading::make_mut(&mut index);
50-
let mut progress = progress.add_child("traverse index");
45+
5146
let start = std::time::Instant::now();
52-
let stack = repo
53-
.attributes_only(
54-
index,
55-
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
56-
)?
57-
.detach();
58-
let pathspec = gix::Pathspec::new(&repo, false, pathspecs.iter().map(|p| p.as_bstr()), true, || {
59-
Ok(stack.clone())
60-
})?;
61-
let options = gix_status::index_as_worktree::Options {
62-
fs: repo.filesystem_options()?,
63-
thread_limit,
64-
stat: repo.stat_options()?,
65-
};
6647
let prefix = repo.prefix()?.unwrap_or(Path::new(""));
67-
let mut printer = Printer {
68-
out,
69-
changes: Vec::new(),
70-
prefix: prefix.to_owned(),
71-
};
72-
let filter_pipeline = repo
73-
.filter_pipeline(Some(gix::hash::ObjectId::empty_tree(repo.object_hash())))?
74-
.0
75-
.into_parts()
76-
.0;
77-
let ctx = gix_status::index_as_worktree::Context {
78-
pathspec: pathspec.into_parts().0,
79-
stack,
80-
filter: filter_pipeline,
81-
should_interrupt: &gix::interrupt::IS_INTERRUPTED,
82-
};
83-
let mut collect = gix::dir::walk::delegate::Collect::default();
84-
let (outcome, walk_outcome) = gix::features::parallel::threads(|scope| -> anyhow::Result<_> {
85-
// TODO: it's either this, or not running both in parallel and setting UPTODATE flags whereever
86-
// there is no modification. This can save disk queries as dirwalk can then trust what's in
87-
// the index regarding the type.
88-
// NOTE: collect here as rename-tracking needs that anyway.
89-
let walk_outcome = gix::features::parallel::build_thread()
90-
.name("gix status::dirwalk".into())
91-
.spawn_scoped(scope, {
92-
let repo = repo.clone().into_sync();
93-
let index = &index;
94-
let collect = &mut collect;
95-
move || -> anyhow::Result<_> {
96-
let repo = repo.to_thread_local();
97-
let outcome = repo.dirwalk(
98-
index,
99-
pathspecs,
100-
repo.dirwalk_options()?
101-
.emit_untracked(gix::dir::walk::EmissionMode::CollapseDirectory),
102-
collect,
103-
)?;
104-
Ok(outcome.dirwalk)
48+
let index_progress = progress.add_child("traverse index");
49+
let mut iter = repo
50+
.status(index_progress)?
51+
.index_worktree_options_mut(|opts| {
52+
opts.thread_limit = thread_limit;
53+
opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive);
54+
})
55+
.index_worktree_submodules(match submodules {
56+
Some(mode) => {
57+
let ignore = match mode {
58+
Submodules::All => gix::submodule::config::Ignore::None,
59+
Submodules::RefChange => gix::submodule::config::Ignore::Dirty,
60+
Submodules::Modifications => gix::submodule::config::Ignore::Untracked,
61+
Submodules::None => gix::submodule::config::Ignore::All,
62+
};
63+
gix::status::Submodule::Given {
64+
ignore,
65+
check_dirty: false,
10566
}
106-
})?;
107-
108-
let outcome = gix_status::index_as_worktree(
109-
index,
110-
repo.work_dir()
111-
.context("This operation cannot be run on a bare repository")?,
112-
&mut printer,
113-
FastEq,
114-
Submodule,
115-
repo.objects.clone().into_arc()?,
116-
&mut progress,
117-
ctx,
118-
options,
119-
)?;
120-
121-
let walk_outcome = walk_outcome.join().expect("no panic")?;
122-
Ok((outcome, walk_outcome))
123-
})?;
124-
125-
for entry in collect
126-
.into_entries_by_path()
127-
.into_iter()
128-
.filter_map(|(entry, dir_status)| dir_status.is_none().then_some(entry))
129-
{
130-
writeln!(
131-
printer.out,
132-
"{status: >3} {rela_path}",
133-
status = "?",
134-
rela_path = gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display()
135-
)?;
136-
}
67+
}
68+
None => gix::status::Submodule::AsConfigured { check_dirty: false },
69+
})
70+
.into_index_worktree_iter(pathspecs)?;
71+
for item in iter.by_ref() {
72+
let item = item?;
73+
if gix::interrupt::is_triggered() {
74+
bail!("interrupted by user");
75+
}
13776

138-
if outcome.entries_to_update != 0 && allow_write {
139-
{
140-
let entries = index.entries_mut();
141-
for (entry_index, change) in printer.changes {
142-
let entry = &mut entries[entry_index];
143-
match change {
144-
ApplyChange::SetSizeToZero => {
145-
entry.stat.size = 0;
146-
}
147-
ApplyChange::NewStat(new_stat) => {
148-
entry.stat = new_stat;
149-
}
77+
match item {
78+
Item::Modification {
79+
entry: _,
80+
entry_index: _,
81+
rela_path,
82+
status,
83+
} => print_index_entry_status(&mut out, prefix, rela_path.as_ref(), status)?,
84+
Item::DirectoryContents {
85+
entry,
86+
collapsed_directory_status,
87+
} => {
88+
if collapsed_directory_status.is_none() {
89+
writeln!(
90+
out,
91+
"{status: >3} {rela_path}",
92+
status = "?",
93+
rela_path =
94+
gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display()
95+
)?;
15096
}
15197
}
98+
Item::Rewrite { .. } => {}
15299
}
153-
index.write(gix::index::write::Options {
154-
extensions: Default::default(),
155-
skip_hash: false, // TODO: make this based on configuration
156-
})?;
100+
}
101+
let out = iter.outcome_mut().expect("successful iteration has outcome");
102+
103+
if out.has_changes() && allow_write {
104+
out.write_changes().transpose()?;
157105
}
158106

159107
if statistics {
160-
writeln!(err, "{outcome:#?}").ok();
161-
writeln!(err, "{walk_outcome:#?}").ok();
108+
writeln!(err, "{outcome:#?}", outcome = out.index_worktree).ok();
162109
}
163110

164-
writeln!(err, "\nhead -> index and untracked files aren't implemented yet")?;
111+
writeln!(err, "\nhead -> index isn't implemented yet")?;
112+
progress.init(Some(out.index.entries().len()), gix::progress::count("files"));
113+
progress.set(out.index.entries().len());
165114
progress.show_throughput(start);
166115
Ok(())
167116
}
168117

169-
#[derive(Clone)]
170-
struct Submodule;
171-
172-
impl gix_status::index_as_worktree::traits::SubmoduleStatus for Submodule {
173-
type Output = ();
174-
type Error = std::convert::Infallible;
175-
176-
fn status(&mut self, _entry: &Entry, _rela_path: &BStr) -> Result<Option<Self::Output>, Self::Error> {
177-
Ok(None)
178-
}
179-
}
180-
181-
struct Printer<W> {
182-
out: W,
183-
changes: Vec<(usize, ApplyChange)>,
184-
prefix: PathBuf,
185-
}
186-
187-
enum ApplyChange {
188-
SetSizeToZero,
189-
NewStat(gix::index::entry::Stat),
190-
}
191-
192-
impl<'index, W> gix_status::index_as_worktree::VisitEntry<'index> for Printer<W>
193-
where
194-
W: std::io::Write,
195-
{
196-
type ContentChange = ();
197-
type SubmoduleStatus = ();
198-
199-
fn visit_entry(
200-
&mut self,
201-
_entries: &'index [Entry],
202-
_entry: &'index Entry,
203-
entry_index: usize,
204-
rela_path: &'index BStr,
205-
status: EntryStatus<Self::ContentChange>,
206-
) {
207-
self.visit_inner(entry_index, rela_path, status).ok();
208-
}
209-
}
210-
211-
impl<W: std::io::Write> Printer<W> {
212-
fn visit_inner(&mut self, entry_index: usize, rela_path: &BStr, status: EntryStatus<()>) -> std::io::Result<()> {
213-
let char_storage;
214-
let status = match status {
215-
EntryStatus::Conflict(conflict) => as_str(conflict),
216-
EntryStatus::Change(change) => {
217-
if matches!(
218-
change,
219-
Change::Modification {
220-
set_entry_stat_size_zero: true,
221-
..
222-
}
223-
) {
224-
self.changes.push((entry_index, ApplyChange::SetSizeToZero))
225-
}
226-
char_storage = change_to_char(&change);
227-
std::str::from_utf8(std::slice::from_ref(&char_storage)).expect("valid ASCII")
228-
}
229-
EntryStatus::NeedsUpdate(stat) => {
230-
self.changes.push((entry_index, ApplyChange::NewStat(stat)));
231-
return Ok(());
232-
}
233-
EntryStatus::IntentToAdd => "A",
234-
};
118+
fn print_index_entry_status(
119+
out: &mut dyn std::io::Write,
120+
prefix: &Path,
121+
rela_path: &BStr,
122+
status: EntryStatus<(), gix::submodule::Status>,
123+
) -> std::io::Result<()> {
124+
let char_storage;
125+
let status = match status {
126+
EntryStatus::Conflict(conflict) => as_str(conflict),
127+
EntryStatus::Change(change) => {
128+
char_storage = change_to_char(&change);
129+
std::str::from_utf8(std::slice::from_ref(&char_storage)).expect("valid ASCII")
130+
}
131+
EntryStatus::NeedsUpdate(_stat) => {
132+
return Ok(());
133+
}
134+
EntryStatus::IntentToAdd => "A",
135+
};
235136

236-
let rela_path = gix::path::from_bstr(rela_path);
237-
let display_path = gix::path::relativize_with_prefix(&rela_path, &self.prefix);
238-
writeln!(&mut self.out, "{status: >3} {}", display_path.display())
239-
}
137+
let rela_path = gix::path::from_bstr(rela_path);
138+
let display_path = gix::path::relativize_with_prefix(&rela_path, prefix);
139+
writeln!(out, "{status: >3} {}", display_path.display())
240140
}
241141

242142
fn as_str(c: Conflict) -> &'static str {
@@ -251,7 +151,7 @@ fn as_str(c: Conflict) -> &'static str {
251151
}
252152
}
253153

254-
fn change_to_char(change: &Change<()>) -> u8 {
154+
fn change_to_char(change: &Change<(), gix::submodule::Status>) -> u8 {
255155
// Known status letters: https://github.com/git/git/blob/6807fcfedab84bc8cd0fbf721bc13c4e68cda9ae/diff.h#L613
256156
match change {
257157
Change::Removed => b'D',

src/plumbing/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,12 @@ pub fn main() -> Result<()> {
230230
statistics,
231231
thread_limit: thread_limit.or(cfg!(target_os = "macos").then_some(3)), // TODO: make this a configurable when in `gix`, this seems to be optimal on MacOS, linux scales though! MacOS also scales if reading a lot of files for refresh index
232232
allow_write: !no_write,
233-
submodules: match submodules {
233+
submodules: submodules.map(|submodules| match submodules {
234234
Submodules::All => core::repository::status::Submodules::All,
235235
Submodules::RefChange => core::repository::status::Submodules::RefChange,
236236
Submodules::Modifications => core::repository::status::Submodules::Modifications,
237-
},
237+
Submodules::None => core::repository::status::Submodules::None,
238+
}),
238239
},
239240
)
240241
},

src/plumbing/options/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,16 @@ pub mod status {
213213
RefChange,
214214
/// See if there are worktree modifications compared to the index, but do not check for untracked files.
215215
Modifications,
216+
/// Ignore all submodule changes.
217+
None,
216218
}
217219

218220
#[derive(Debug, clap::Parser)]
219221
#[command(about = "compute repository status similar to `git status`")]
220222
pub struct Platform {
221-
/// Define how to display submodule status.
222-
#[clap(long, default_value = "all")]
223-
pub submodules: Submodules,
223+
/// Define how to display the submodule status. Defaults to git configuration if unset.
224+
#[clap(long)]
225+
pub submodules: Option<Submodules>,
224226
/// Print additional statistics to help understanding performance.
225227
#[clap(long, short = 's')]
226228
pub statistics: bool,

0 commit comments

Comments
 (0)