Skip to content

Commit 2d01125

Browse files
committed
feat!: various improvements
1 parent 832b345 commit 2d01125

File tree

7 files changed

+106
-11
lines changed

7 files changed

+106
-11
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-status/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ gix-hash = { version = "^0.13.0", path = "../gix-hash" }
2020
gix-object = { version = "^0.36.0", path = "../gix-object" }
2121
gix-path = { version = "^0.10.0", path = "../gix-path" }
2222
gix-features = { version = "^0.34.0", path = "../gix-features" }
23+
gix-pathspec = { version = "0.2.0", path = "../gix-pathspec" }
2324

2425
thiserror = "1.0.26"
2526
filetime = "0.2.15"

gix-status/src/index_as_worktree/function.rs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::sync::atomic::Ordering;
12
use std::{io, marker::PhantomData, path::Path};
23

34
use bstr::BStr;
@@ -11,7 +12,7 @@ use crate::{
1112
types::{Error, Options},
1213
Change, VisitEntry,
1314
},
14-
read,
15+
read, Pathspec,
1516
};
1617

1718
/// Calculates the changes that need to be applied to an `index` to match the state of the `worktree` and makes them
@@ -24,12 +25,15 @@ use crate::{
2425
/// Note that this isn't technically quite what this function does as this also provides some additional information,
2526
/// like whether a file has conflicts, and files that were added with `git add` are shown as a special
2627
/// changes despite not technically requiring a change to the index since `git add` already added the file to the index.
28+
#[allow(clippy::too_many_arguments)]
2729
pub fn index_as_worktree<'index, T, Find, E>(
2830
index: &'index mut gix_index::State,
2931
worktree: &Path,
3032
collector: &mut impl VisitEntry<'index, ContentChange = T>,
3133
compare: impl CompareBlobs<Output = T> + Send + Clone,
3234
find: Find,
35+
progress: &mut dyn gix_features::progress::Progress,
36+
pathspec: impl Pathspec + Send + Clone,
3337
options: Options,
3438
) -> Result<(), Error>
3539
where
@@ -43,14 +47,22 @@ where
4347
let timestamp = index.timestamp();
4448
index.set_timestamp(FileTime::now());
4549
let (chunk_size, thread_limit, _) = gix_features::parallel::optimize_chunk_size_and_thread_limit(
46-
100,
50+
500, // just like git
4751
index.entries().len().into(),
4852
options.thread_limit,
4953
None,
5054
);
55+
56+
let range = index
57+
.prefixed_entries_range(pathspec.common_prefix())
58+
.unwrap_or(0..index.entries().len());
5159
let (entries, path_backing) = index.entries_mut_and_pathbacking();
60+
let entries = &mut entries[range];
61+
progress.init(entries.len().into(), gix_features::progress::count("files"));
62+
let count = progress.counter();
63+
5264
in_parallel_if(
53-
|| true, // TODO: heuristic: when is parallelization not worth it?
65+
|| true, // TODO: heuristic: when is parallelization not worth it? Git says 500 items per thread, but to 20 threads, we can be more fine-grained though.
5466
entries.chunks_mut(chunk_size),
5567
thread_limit,
5668
{
@@ -65,15 +77,20 @@ where
6577
worktree,
6678
options,
6779
},
68-
compare.clone(),
69-
find.clone(),
80+
compare,
81+
find,
82+
pathspec,
7083
)
7184
}
7285
},
73-
|entries, (state, diff, find)| {
86+
|entries, (state, diff, find, pathspec)| {
7487
entries
7588
.iter_mut()
76-
.filter_map(|entry| state.process(entry, diff, find))
89+
.filter_map(|entry| {
90+
let res = state.process(entry, diff, find, pathspec);
91+
count.fetch_add(1, Ordering::Relaxed);
92+
res
93+
})
7794
.collect()
7895
},
7996
ReduceChange {
@@ -101,10 +118,11 @@ impl<'index> State<'_, 'index> {
101118
entry: &'index mut gix_index::Entry,
102119
diff: &mut impl CompareBlobs<Output = T>,
103120
find: &mut Find,
121+
pathspec: &mut impl Pathspec,
104122
) -> Option<StatusResult<'index, T>>
105123
where
106124
E: std::error::Error + Send + Sync + 'static,
107-
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
125+
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
108126
{
109127
let conflict = match entry.stage() {
110128
0 => false,
@@ -120,6 +138,9 @@ impl<'index> State<'_, 'index> {
120138
return None;
121139
}
122140
let path = entry.path_in(self.path_backing);
141+
if !pathspec.is_included(path, Some(false)) {
142+
return None;
143+
}
123144
let status = self.compute_status(&mut *entry, path, diff, find);
124145
Some(status.map(move |status| (&*entry, path, status, conflict)))
125146
}
@@ -172,7 +193,7 @@ impl<'index> State<'_, 'index> {
172193
) -> Result<Option<Change<T>>, Error>
173194
where
174195
E: std::error::Error + Send + Sync + 'static,
175-
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
196+
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
176197
{
177198
// TODO fs cache
178199
let worktree_path = gix_path::try_from_bstr(git_path).map_err(|_| Error::IllformedUtf8)?;

gix-status/src/index_as_worktree/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub enum Error {
1414
Find(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
1515
}
1616

17-
#[derive(Clone, Default)]
17+
#[derive(Clone, Debug, Default)]
1818
/// Options that control how the index status with a worktree is computed.
1919
pub struct Options {
2020
/// Capabilities of the file system which affect the status computation.

gix-status/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,22 @@
1212
pub mod read;
1313

1414
pub mod index_as_worktree;
15+
16+
use bstr::BStr;
1517
pub use index_as_worktree::function::index_as_worktree;
18+
19+
/// A trait to facilitate working working with pathspecs.
20+
pub trait Pathspec {
21+
/// Return the portion of the prefix among all of the pathspecs involved in this search, or an empty string if
22+
/// there is none. It doesn't have to end at a directory boundary though, nor does it denote a directory.
23+
///
24+
/// Note that the common_prefix is always matched case-sensitively, and it is useful to skip large portions of input.
25+
/// Further, excluded pathspecs don't participate which makes this common prefix inclusive. To work correclty though,
26+
/// one will have to additionally match paths that have the common prefix with that pathspec itself to assure it is
27+
/// not excluded.
28+
fn common_prefix(&self) -> &BStr;
29+
30+
/// Return `true` if `relative_path` is included in this pathspec.
31+
/// `is_dir` is `true` if `relative_path` is a directory.
32+
fn is_included(&mut self, relative_path: &BStr, is_dir: Option<bool>) -> bool;
33+
}

gix-status/tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ gix-fs = { path = "../../gix-fs" }
2424
gix-hash = { path = "../../gix-hash" }
2525
gix-object = { path = "../../gix-object" }
2626
gix-features = { path = "../../gix-features" }
27-
27+
gix-pathspec = { version = "0.2.0", path = "../../gix-pathspec" }
2828
filetime = "0.2.15"
2929
bstr = { version = "1.3.0", default-features = false }
3030

gix-status/tests/status/index_as_worktree.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,25 @@ const TEST_OPTIONS: index::entry::stat::Options = index::entry::stat::Options {
2929
};
3030

3131
fn fixture(name: &str, expected_status: &[(&BStr, Option<Change>, bool)]) {
32+
fixture_filtered(name, &[], expected_status)
33+
}
34+
35+
fn fixture_filtered(name: &str, pathspecs: &[&str], expected_status: &[(&BStr, Option<Change>, bool)]) {
3236
let worktree = fixture_path(name);
3337
let git_dir = worktree.join(".git");
3438
let mut index =
3539
gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default()).unwrap();
3640
let mut recorder = Recorder::default();
41+
let search = gix_pathspec::Search::from_specs(to_pathspecs(pathspecs), None, std::path::Path::new(""))
42+
.expect("valid specs can be normalized");
3743
index_as_worktree(
3844
&mut index,
3945
&worktree,
4046
&mut recorder,
4147
FastEq,
4248
|_, _| Ok::<_, std::convert::Infallible>(gix_object::BlobRef { data: &[] }),
49+
&mut gix_features::progress::Discard,
50+
Pathspec(search),
4351
Options {
4452
fs: gix_fs::Capabilities::probe(&git_dir),
4553
stat: TEST_OPTIONS,
@@ -51,6 +59,13 @@ fn fixture(name: &str, expected_status: &[(&BStr, Option<Change>, bool)]) {
5159
assert_eq!(recorder.records, expected_status)
5260
}
5361

62+
fn to_pathspecs(input: &[&str]) -> Vec<gix_pathspec::Pattern> {
63+
input
64+
.iter()
65+
.map(|pattern| gix_pathspec::parse(pattern.as_bytes(), Default::default()).expect("known to be valid"))
66+
.collect()
67+
}
68+
5469
#[test]
5570
fn removed() {
5671
fixture(
@@ -62,6 +77,15 @@ fn removed() {
6277
(BStr::new(b"executable"), Some(Change::Removed), false),
6378
],
6479
);
80+
81+
fixture_filtered(
82+
"status_removed",
83+
&["dir"],
84+
&[
85+
(BStr::new(b"dir/content"), Some(Change::Removed), false),
86+
(BStr::new(b"dir/sub-dir/symlink"), Some(Change::Removed), false),
87+
],
88+
);
6589
}
6690

6791
#[test]
@@ -180,6 +204,8 @@ fn racy_git() {
180204
&mut recorder,
181205
counter.clone(),
182206
|_, _| Err(std::io::Error::new(std::io::ErrorKind::Other, "no odb access expected")),
207+
&mut gix_features::progress::Discard,
208+
Pathspec::default(),
183209
Options {
184210
fs,
185211
stat: TEST_OPTIONS,
@@ -201,6 +227,8 @@ fn racy_git() {
201227
&mut recorder,
202228
counter,
203229
|_, _| Err(std::io::Error::new(std::io::ErrorKind::Other, "no odb access expected")),
230+
&mut gix_features::progress::Discard,
231+
Pathspec::default(),
204232
Options {
205233
fs,
206234
stat: TEST_OPTIONS,
@@ -226,3 +254,28 @@ fn racy_git() {
226254
"racy change is correctly detected"
227255
);
228256
}
257+
258+
#[derive(Clone)]
259+
struct Pathspec(gix_pathspec::Search);
260+
261+
impl Default for Pathspec {
262+
fn default() -> Self {
263+
let search = gix_pathspec::Search::from_specs(to_pathspecs(&[]), None, std::path::Path::new(""))
264+
.expect("empty is always valid");
265+
Self(search)
266+
}
267+
}
268+
269+
impl gix_status::Pathspec for Pathspec {
270+
fn common_prefix(&self) -> &BStr {
271+
self.0.common_prefix()
272+
}
273+
274+
fn is_included(&mut self, relative_path: &BStr, is_dir: Option<bool>) -> bool {
275+
self.0
276+
.pattern_matching_relative_path(relative_path, is_dir, &mut |_, _, _, _| {
277+
unreachable!("we don't use attributes in our pathspecs")
278+
})
279+
.map_or(false, |m| !m.is_excluded())
280+
}
281+
}

0 commit comments

Comments
 (0)