Skip to content

Commit 61c002b

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 f1ba7bd commit 61c002b

File tree

3 files changed

+100
-196
lines changed

3 files changed

+100
-196
lines changed

gitoxide-core/src/repository/status.rs

+92-191
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,102 @@ 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+
.should_interrupt_shared(&gix::interrupt::IS_INTERRUPTED)
52+
.index_worktree_options_mut(|opts| {
53+
opts.thread_limit = thread_limit;
54+
opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive);
55+
})
56+
.index_worktree_submodules(match submodules {
57+
Some(mode) => {
58+
let ignore = match mode {
59+
Submodules::All => gix::submodule::config::Ignore::None,
60+
Submodules::RefChange => gix::submodule::config::Ignore::Dirty,
61+
Submodules::Modifications => gix::submodule::config::Ignore::Untracked,
62+
Submodules::None => gix::submodule::config::Ignore::All,
63+
};
64+
gix::status::Submodule::Given {
65+
ignore,
66+
check_dirty: false,
10567
}
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-
}
137-
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-
}
68+
}
69+
None => gix::status::Submodule::AsConfigured { check_dirty: false },
70+
})
71+
.into_index_worktree_iter(pathspecs)?;
72+
for item in iter.by_ref() {
73+
let item = item?;
74+
match item {
75+
Item::Modification {
76+
entry: _,
77+
entry_index: _,
78+
rela_path,
79+
status,
80+
} => print_index_entry_status(&mut out, prefix, rela_path.as_ref(), status)?,
81+
Item::DirectoryContents {
82+
entry,
83+
collapsed_directory_status,
84+
} => {
85+
if collapsed_directory_status.is_none() {
86+
writeln!(
87+
out,
88+
"{status: >3} {rela_path}",
89+
status = "?",
90+
rela_path =
91+
gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display()
92+
)?;
15093
}
15194
}
95+
Item::Rewrite { .. } => {}
15296
}
153-
index.write(gix::index::write::Options {
154-
extensions: Default::default(),
155-
skip_hash: false, // TODO: make this based on configuration
156-
})?;
15797
}
158-
159-
if statistics {
160-
writeln!(err, "{outcome:#?}").ok();
161-
writeln!(err, "{walk_outcome:#?}").ok();
98+
if gix::interrupt::is_triggered() {
99+
bail!("interrupted by user");
162100
}
163101

164-
writeln!(err, "\nhead -> index and untracked files aren't implemented yet")?;
165-
progress.show_throughput(start);
166-
Ok(())
167-
}
168-
169-
#[derive(Clone)]
170-
struct Submodule;
102+
let out = iter.outcome_mut().expect("successful iteration has outcome");
171103

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)
104+
if out.has_changes() && allow_write {
105+
out.write_changes().transpose()?;
178106
}
179-
}
180107

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();
108+
if statistics {
109+
writeln!(err, "{outcome:#?}", outcome = out.index_worktree).ok();
208110
}
111+
112+
writeln!(err, "\nhead -> index isn't implemented yet")?;
113+
progress.init(Some(out.index.entries().len()), gix::progress::count("files"));
114+
progress.set(out.index.entries().len());
115+
progress.show_throughput(start);
116+
Ok(())
209117
}
210118

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-
};
119+
fn print_index_entry_status(
120+
out: &mut dyn std::io::Write,
121+
prefix: &Path,
122+
rela_path: &BStr,
123+
status: EntryStatus<(), gix::submodule::Status>,
124+
) -> std::io::Result<()> {
125+
let char_storage;
126+
let status = match status {
127+
EntryStatus::Conflict(conflict) => as_str(conflict),
128+
EntryStatus::Change(change) => {
129+
char_storage = change_to_char(&change);
130+
std::str::from_utf8(std::slice::from_ref(&char_storage)).expect("valid ASCII")
131+
}
132+
EntryStatus::NeedsUpdate(_stat) => {
133+
return Ok(());
134+
}
135+
EntryStatus::IntentToAdd => "A",
136+
};
235137

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-
}
138+
let rela_path = gix::path::from_bstr(rela_path);
139+
let display_path = gix::path::relativize_with_prefix(&rela_path, prefix);
140+
writeln!(out, "{status: >3} {}", display_path.display())
240141
}
241142

242143
fn as_str(c: Conflict) -> &'static str {
@@ -251,7 +152,7 @@ fn as_str(c: Conflict) -> &'static str {
251152
}
252153
}
253154

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

src/plumbing/main.rs

+3-2
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

+5-3
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)