Skip to content

Commit 5b8140f

Browse files
committed
feat!: add status::Platform::into_iter() for obtaining a complete status.
Note that it is still possible to disable the head-index status. Types moved around, effectivey removing the `iter::` module for most more general types, i.e. those that are quite genericlally useful in a status.
1 parent a6f397f commit 5b8140f

File tree

8 files changed

+524
-385
lines changed

8 files changed

+524
-385
lines changed

gitoxide-core/src/repository/status.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::bail;
22
use gix::bstr::{BStr, BString, ByteSlice};
3-
use gix::status::index_worktree::iter::Item;
3+
use gix::status::index_worktree::Item;
44
use gix_status::index_as_worktree::{Change, Conflict, EntryStatus};
55
use std::path::Path;
66

gix/src/status/index_worktree.rs

+112-372
Large diffs are not rendered by default.

gix/src/status/iter/mod.rs

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
use crate::bstr::BString;
2+
use crate::config::cache::util::ApplyLeniencyDefault;
3+
use crate::status::index_worktree::BuiltinSubmoduleStatus;
4+
use crate::status::{index_worktree, Platform};
5+
use crate::worktree::IndexPersistedOrInMemory;
6+
use gix_status::index_as_worktree::{Change, EntryStatus};
7+
8+
pub(super) mod types;
9+
use types::{ApplyChange, Item, Iter, Outcome};
10+
11+
/// Lifecycle
12+
impl<Progress> Platform<'_, Progress>
13+
where
14+
Progress: gix_features::progress::Progress,
15+
{
16+
/// Turn the platform into an iterator for changes between the head-tree and the index, and the index and the working tree,
17+
/// while optionally listing untracked and/or ignored files.
18+
///
19+
/// * `patterns`
20+
/// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered.
21+
#[doc(alias = "diff_index_to_workdir", alias = "git2")]
22+
pub fn into_iter(
23+
self,
24+
patterns: impl IntoIterator<Item = BString>,
25+
) -> Result<Iter, crate::status::into_iter::Error> {
26+
let index = match self.index {
27+
None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?),
28+
Some(index) => index,
29+
};
30+
31+
let obtain_tree_id = || -> Result<Option<gix_hash::ObjectId>, crate::status::into_iter::Error> {
32+
Ok(match self.head_tree {
33+
Some(None) => Some(self.repo.head_tree_id()?.into()),
34+
Some(Some(tree_id)) => Some(tree_id.into()),
35+
None => None,
36+
})
37+
};
38+
39+
let skip_hash = self
40+
.repo
41+
.config
42+
.resolved
43+
.boolean(crate::config::tree::Index::SKIP_HASH)
44+
.map(|res| crate::config::tree::Index::SKIP_HASH.enrich_error(res))
45+
.transpose()
46+
.with_lenient_default(self.repo.config.lenient_config)?
47+
.unwrap_or_default();
48+
let should_interrupt = self.should_interrupt.clone().unwrap_or_default();
49+
let submodule = BuiltinSubmoduleStatus::new(self.repo.clone().into_sync(), self.submodules)?;
50+
#[cfg(feature = "parallel")]
51+
{
52+
let (tx, rx) = std::sync::mpsc::channel();
53+
let mut collect = Collect { tx };
54+
let patterns: Vec<_> = patterns.into_iter().collect();
55+
let join = std::thread::Builder::new()
56+
.name("gix::status::index_worktree::producer".into())
57+
.spawn({
58+
let repo = self.repo.clone().into_sync();
59+
let options = self.index_worktree_options;
60+
let should_interrupt = should_interrupt.clone();
61+
let mut progress = self.progress;
62+
move || -> Result<_, index_worktree::Error> {
63+
let repo = repo.to_thread_local();
64+
let out = repo.index_worktree_status(
65+
&index,
66+
patterns,
67+
&mut collect,
68+
gix_status::index_as_worktree::traits::FastEq,
69+
submodule,
70+
&mut progress,
71+
&should_interrupt,
72+
options,
73+
)?;
74+
Ok(Outcome {
75+
index_worktree: out,
76+
index,
77+
changes: None,
78+
skip_hash,
79+
})
80+
}
81+
})
82+
.map_err(crate::status::into_iter::Error::SpawnThread)?;
83+
84+
Ok(Iter {
85+
rx_and_join: Some((rx, join)),
86+
should_interrupt,
87+
index_changes: Vec::new(),
88+
out: None,
89+
})
90+
}
91+
#[cfg(not(feature = "parallel"))]
92+
{
93+
let mut collect = Collect { items: Vec::new() };
94+
95+
let repo = self.repo.clone().into_sync();
96+
let options = self.index_worktree_options;
97+
let mut progress = self.progress;
98+
let repo = repo.to_thread_local();
99+
let items = match obtain_tree_id()? {
100+
Some(tree_id) => {
101+
// self.repo.tree_index_status(&tree_id);
102+
todo!()
103+
}
104+
None => Vec::new().into_iter(),
105+
};
106+
let out = repo.index_worktree_status(
107+
&index,
108+
patterns,
109+
&mut collect,
110+
gix_status::index_as_worktree::traits::FastEq,
111+
submodule,
112+
&mut progress,
113+
&should_interrupt,
114+
options,
115+
)?;
116+
let mut iter = Iter {
117+
items,
118+
index_changes: Vec::new(),
119+
out: None,
120+
};
121+
let mut out = Outcome {
122+
index_worktree: out,
123+
index,
124+
changes: None,
125+
skip_hash,
126+
};
127+
let items = collect
128+
.items
129+
.into_iter()
130+
.filter_map(|item| iter.maybe_keep_index_change(item))
131+
.collect::<Vec<_>>();
132+
out.changes = (!iter.index_changes.is_empty()).then(|| std::mem::take(&mut iter.index_changes));
133+
iter.items = items.into_iter();
134+
iter.out = Some(out);
135+
Ok(iter)
136+
}
137+
}
138+
}
139+
140+
/// The error returned for each item returned by [`Iter`].
141+
pub type Error = index_worktree::Error;
142+
143+
impl Iterator for Iter {
144+
type Item = Result<Item, Error>;
145+
146+
fn next(&mut self) -> Option<Self::Item> {
147+
#[cfg(feature = "parallel")]
148+
loop {
149+
let (rx, _join) = self.rx_and_join.as_ref()?;
150+
match rx.recv().ok() {
151+
Some(item) => {
152+
if let Some(item) = self.maybe_keep_index_change(item) {
153+
break Some(Ok(item));
154+
}
155+
continue;
156+
}
157+
None => {
158+
let (_rx, handle) = self.rx_and_join.take()?;
159+
break match handle.join().expect("no panic") {
160+
Ok(mut out) => {
161+
out.changes = Some(std::mem::take(&mut self.index_changes));
162+
self.out = Some(out);
163+
None
164+
}
165+
Err(err) => Some(Err(err)),
166+
};
167+
}
168+
}
169+
}
170+
#[cfg(not(feature = "parallel"))]
171+
self.items.next().map(Ok)
172+
}
173+
}
174+
175+
/// Access
176+
impl Iter {
177+
/// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed.
178+
pub fn outcome_mut(&mut self) -> Option<&mut Outcome> {
179+
self.out.as_mut()
180+
}
181+
182+
/// Turn the iterator into the iteration outcome, which is `None` on error or if the iteration
183+
/// isn't complete.
184+
pub fn into_outcome(mut self) -> Option<Outcome> {
185+
self.out.take()
186+
}
187+
}
188+
189+
impl Iter {
190+
fn maybe_keep_index_change(&mut self, item: Item) -> Option<Item> {
191+
let change = match item {
192+
Item::IndexWorktree(index_worktree::Item::Modification {
193+
status: EntryStatus::NeedsUpdate(stat),
194+
entry_index,
195+
..
196+
}) => (entry_index, ApplyChange::NewStat(stat)),
197+
Item::IndexWorktree(index_worktree::Item::Modification {
198+
status:
199+
EntryStatus::Change(Change::Modification {
200+
set_entry_stat_size_zero,
201+
..
202+
}),
203+
entry_index,
204+
..
205+
}) if set_entry_stat_size_zero => (entry_index, ApplyChange::SetSizeToZero),
206+
_ => return Some(item),
207+
};
208+
209+
self.index_changes.push(change);
210+
None
211+
}
212+
}
213+
214+
#[cfg(feature = "parallel")]
215+
impl Drop for Iter {
216+
fn drop(&mut self) {
217+
crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt);
218+
}
219+
}
220+
221+
struct Collect {
222+
#[cfg(feature = "parallel")]
223+
tx: std::sync::mpsc::Sender<Item>,
224+
#[cfg(not(feature = "parallel"))]
225+
items: Vec<Item>,
226+
}
227+
228+
impl<'index> gix_status::index_as_worktree_with_renames::VisitEntry<'index> for Collect {
229+
type ContentChange =
230+
<gix_status::index_as_worktree::traits::FastEq as gix_status::index_as_worktree::traits::CompareBlobs>::Output;
231+
type SubmoduleStatus = <BuiltinSubmoduleStatus as gix_status::index_as_worktree::traits::SubmoduleStatus>::Output;
232+
233+
fn visit_entry(
234+
&mut self,
235+
entry: gix_status::index_as_worktree_with_renames::Entry<'index, Self::ContentChange, Self::SubmoduleStatus>,
236+
) {
237+
// NOTE: we assume that the receiver triggers interruption so the operation will stop if the receiver is down.
238+
let item = Item::IndexWorktree(entry.into());
239+
#[cfg(feature = "parallel")]
240+
self.tx.send(item).ok();
241+
#[cfg(not(feature = "parallel"))]
242+
self.items.push(item);
243+
}
244+
}

gix/src/status/iter/types.rs

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use crate::status::index_worktree;
2+
use crate::worktree::IndexPersistedOrInMemory;
3+
4+
/// An iterator for changes between the index and the worktree and the head-tree and the index.
5+
///
6+
/// Note that depending on the underlying configuration, there might be a significant delay until the first
7+
/// item is received due to the buffering necessary to perform rename tracking and/or sorting.
8+
///
9+
/// ### Submodules
10+
///
11+
/// Note that submodules can be set to 'inactive', which will not exclude them from the status operation, similar to
12+
/// how `git status` includes them.
13+
///
14+
/// ### Index Changes
15+
///
16+
/// Changes to the index are collected, and it's possible to write the index back using [Outcome::write_changes()].
17+
/// Note that these changes are not observable, they will always be kept.
18+
///
19+
/// ### Parallel Operation
20+
///
21+
/// Note that without the `parallel` feature, the iterator becomes 'serial', which means all status will be computed in advance
22+
/// and it's non-interruptible, yielding worse performance for is-dirty checks for instance as interruptions won't happen.
23+
/// It's a crutch that is just there to make single-threaded applications possible at all, as it's not really an iterator
24+
/// anymore. If this matters, better run [Repository::index_worktree_status()](crate::Repository::index_worktree_status) by hand
25+
/// as it provides all control one would need, just not as an iterator.
26+
///
27+
/// Also, even with `parallel` set, the first call to `next()` will block until there is an item available, without a chance
28+
/// to interrupt unless [`status::Platform::should_interrupt_*()`](crate::status::Platform::should_interrupt_shared()) was
29+
/// configured.
30+
pub struct Iter {
31+
#[cfg(feature = "parallel")]
32+
#[allow(clippy::type_complexity)]
33+
pub(super) rx_and_join: Option<(
34+
std::sync::mpsc::Receiver<Item>,
35+
std::thread::JoinHandle<Result<Outcome, index_worktree::Error>>,
36+
)>,
37+
#[cfg(feature = "parallel")]
38+
pub(super) should_interrupt: crate::status::OwnedOrStaticAtomicBool,
39+
/// Without parallelization, the iterator has to buffer all changes in advance.
40+
#[cfg(not(feature = "parallel"))]
41+
pub(super) items: std::vec::IntoIter<Item>,
42+
/// The outcome of the operation, only available once the operation has ended.
43+
pub(in crate::status) out: Option<Outcome>,
44+
/// The set of `(entry_index, change)` we extracted in order to potentially write back the index with the changes applied.
45+
pub(super) index_changes: Vec<(usize, ApplyChange)>,
46+
}
47+
48+
/// The item produced by the iterator
49+
#[derive(Clone, PartialEq, Debug)]
50+
pub enum Item {
51+
/// A change between the index and the worktree.
52+
///
53+
/// Note that untracked changes are also collected here.
54+
IndexWorktree(index_worktree::Item),
55+
/// A change between the three of `HEAD` and the index.
56+
TreeIndex,
57+
}
58+
59+
/// The data the thread sends over to the receiving iterator.
60+
pub struct Outcome {
61+
/// The outcome of the index-to-worktree comparison operation.
62+
pub index_worktree: gix_status::index_as_worktree_with_renames::Outcome,
63+
/// The index that was used for the operation.
64+
pub index: IndexPersistedOrInMemory,
65+
pub(super) skip_hash: bool,
66+
pub(super) changes: Option<Vec<(usize, ApplyChange)>>,
67+
}
68+
69+
impl Outcome {
70+
/// Returns `true` if the index has received currently unapplied changes that *should* be written back.
71+
///
72+
/// If they are not written back, subsequent `status` operations will take longer to complete, whereas the
73+
/// additional work can be prevented by writing the changes back to the index.
74+
pub fn has_changes(&self) -> bool {
75+
self.changes.as_ref().map_or(false, |changes| !changes.is_empty())
76+
}
77+
78+
/// Write the changes if there are any back to the index file.
79+
/// This can only be done once as the changes are consumed in the process, if there were any.
80+
pub fn write_changes(&mut self) -> Option<Result<(), gix_index::file::write::Error>> {
81+
let _span = gix_features::trace::coarse!("gix::status::index_worktree::Outcome::write_changes()");
82+
let changes = self.changes.take()?;
83+
let mut index = match &self.index {
84+
IndexPersistedOrInMemory::Persisted(persisted) => (***persisted).clone(),
85+
IndexPersistedOrInMemory::InMemory(index) => index.clone(),
86+
};
87+
88+
let entries = index.entries_mut();
89+
for (entry_index, change) in changes {
90+
let entry = &mut entries[entry_index];
91+
match change {
92+
ApplyChange::SetSizeToZero => {
93+
entry.stat.size = 0;
94+
}
95+
ApplyChange::NewStat(new_stat) => {
96+
entry.stat = new_stat;
97+
}
98+
}
99+
}
100+
101+
Some(index.write(crate::index::write::Options {
102+
extensions: Default::default(),
103+
skip_hash: self.skip_hash,
104+
}))
105+
}
106+
}
107+
108+
pub(super) enum ApplyChange {
109+
SetSizeToZero,
110+
NewStat(crate::index::entry::Stat),
111+
}
112+
113+
impl From<index_worktree::Item> for Item {
114+
fn from(value: index_worktree::Item) -> Self {
115+
Item::IndexWorktree(value)
116+
}
117+
}

0 commit comments

Comments
 (0)