History

Merge branch 'index-entries-attrs'

Sebastian Thiel committed 3 years ago

Commit f37a9302 parents 3456c84 df28b7d54 files · +1162 −461Browse files

Unified · Split

54 files
  1. README.md +5 −5
  2. crate-status.md +1 −1
  3. gitoxide-core/src/index/entries.rs +0 −94
  4. gitoxide-core/src/index/mod.rs +0 −3
  5. gitoxide-core/src/repository/exclude.rs +7 −6
  6. gitoxide-core/src/repository/index/entries.rs +228 −0
  7. gitoxide-core/src/repository/index/mod.rs +3 −0
  8. gix-bitmap/src/ewah.rs +4 −0
  9. gix-bitmap/src/lib.rs +1 −2
  10. gix-fs/src/lib.rs +2 −1
  11. gix-fs/src/symlink.rs +10 −0
  12. gix-path/src/convert.rs +1 −1
  13. gix-path/tests/convert/mod.rs +4 −4
  14. gix-utils/src/lib.rs +1 −1
  15. gix-worktree/src/cache/delegate.rs +54 −28
  16. gix-worktree/src/cache/mod.rs +38 −35
  17. gix-worktree/src/cache/state/attributes.rs +25 −25
  18. gix-worktree/src/cache/state/ignore.rs +81 −57
  19. gix-worktree/src/cache/state/mod.rs +36 −11
  20. gix-worktree/src/checkout/function.rs +2 −2
  21. gix-worktree/src/lib.rs +1 −0
  22. gix-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz +0 −3
  23. gix-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh +2 −0
  24. gix-worktree/tests/worktree/cache/attributes.rs +0 −1
  25. gix-worktree/tests/worktree/cache/create_directory.rs +19 −9
  26. gix-worktree/tests/worktree/cache/ignore.rs +4 −13
  27. gix/Cargo.toml +1 −0
  28. gix/src/attributes.rs +9 −0
  29. gix/src/config/cache/access.rs +31 −19
  30. gix/src/config/cache/init.rs +8 −8
  31. gix/src/config/mod.rs +19 −2
  32. gix/src/config/tree/mod.rs +6 −3
  33. gix/src/config/tree/sections/index.rs +62 −0
  34. gix/src/config/tree/sections/mod.rs +5 −0
  35. gix/src/lib.rs +3 −7
  36. gix/src/open/mod.rs +14 −3
  37. gix/src/open/options.rs +1 −1
  38. gix/src/open/permissions.rs +215 −0
  39. gix/src/open/repository.rs +3 −2
  40. gix/src/permission.rs +0 −8
  41. gix/src/permissions.rs +0 −1
  42. gix/src/repository/attributes.rs +50 −0
  43. gix/src/repository/excludes.rs +45 −0
  44. gix/src/repository/mod.rs +2 −1
  45. gix/src/repository/permissions.rs +2 −11
  46. gix/src/repository/worktree.rs +6 −18
  47. gix/src/worktree/mod.rs +45 −44
  48. gix/tests/config/tree.rs +24 −0
  49. gix/tests/remote/connect.rs +4 −4
  50. gix/tests/repository/config/identity.rs +14 −11
  51. src/plumbing/main.rs +42 −10
  52. src/plumbing/options/free.rs +1 −3
  53. src/plumbing/options/mod.rs +16 −2
  54. src/shared.rs +5 −1
README.md+5−5
@@ -83,16 +83,16 @@ is usable to some extent. · 82 unmodified lines
8383 * [gix-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-command)
8484 * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt)
8585 * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec)
86− * `gitoxide-core`
87−* **very early** _(possibly without any documentation and many rough edges)_
88− * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils)
8986 * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs)
87+ * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils)
88+ * [gix-hashtable](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-hashtable)
9089 * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree)
9190 * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap)
91+ * `gitoxide-core`
92+* **very early** _(possibly without any documentation and many rough edges)_
9293 * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date)
93− * [gix-hashtable](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-hashtable)
94−* **idea** _(just a name placeholder)_
9594 * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive)
95+* **idea** _(just a name placeholder)_
9696 * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note)
9797 * [gix-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fetchhead)
9898 * [gix-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-filter)
crate-status.md+1−1
@@ -690,7 +690,7 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README. · 689 unmodified lines
690690 * [x] proper handling of worktree related refs
691691 * [ ] create, move, remove, and repair
692692 * [x] access exclude information
693− * [ ] access attribute information
693+ * [x] access attribute information
694694 * [x] respect `core.worktree` configuration
695695 - **deviation**
696696 * The delicate interplay between `GIT_COMMON_DIR` and `GIT_WORK_TREE` isn't implemented.
gitoxide-core/src/index/entries.rs+0−94
@@ -1,94 +0,0 @@
1−use std::path::Path;
2−
3−use crate::index::{parse_file, Options};
4−
5−pub fn entries(
6− index_path: impl AsRef<Path>,
7− mut out: impl std::io::Write,
8− Options { object_hash, format }: Options,
9−) -> anyhow::Result<()> {
10− use crate::OutputFormat::*;
11− let file = parse_file(index_path, object_hash)?;
12−
13− #[cfg(feature = "serde")]
14− if let Json = format {
15− out.write_all(b"[\n")?;
16− }
17−
18− let mut entries = file.entries().iter().peekable();
19− while let Some(entry) = entries.next() {
20− match format {
21− Human => to_human(&mut out, &file, entry)?,
22− #[cfg(feature = "serde")]
23− Json => to_json(&mut out, &file, entry, entries.peek().is_none())?,
24− }
25− }
26−
27− #[cfg(feature = "serde")]
28− if let Json = format {
29− out.write_all(b"]\n")?;
30− }
31− Ok(())
32−}
33−
34−#[cfg(feature = "serde")]
35−pub(crate) fn to_json(
36− mut out: &mut impl std::io::Write,
37− file: &gix::index::File,
38− entry: &gix::index::Entry,
39− is_last: bool,
40−) -> anyhow::Result<()> {
41− use gix::bstr::ByteSlice;
42−
43− #[cfg_attr(feature = "serde", derive(serde::Serialize))]
44− struct Entry<'a> {
45− stat: &'a gix::index::entry::Stat,
46− hex_id: String,
47− flags: u32,
48− mode: u32,
49− path: std::borrow::Cow<'a, str>,
50− }
51−
52− serde_json::to_writer(
53− &mut out,
54− &Entry {
55− stat: &entry.stat,
56− hex_id: entry.id.to_hex().to_string(),
57− flags: entry.flags.bits(),
58− mode: entry.mode.bits(),
59− path: entry.path(file).to_str_lossy(),
60− },
61− )?;
62−
63− if is_last {
64− out.write_all(b"\n")?;
65− } else {
66− out.write_all(b",\n")?;
67− }
68− Ok(())
69−}
70−
71−pub(crate) fn to_human(
72− out: &mut impl std::io::Write,
73− file: &gix::index::File,
74− entry: &gix::index::Entry,
75−) -> std::io::Result<()> {
76− writeln!(
77− out,
78− "{} {}{:?} {} {}",
79− match entry.flags.stage() {
80− 0 => "BASE ",
81− 1 => "OURS ",
82− 2 => "THEIRS ",
83− _ => "UNKNOWN",
84− },
85− if entry.flags.is_empty() {
86− "".to_string()
87− } else {
88− format!("{:?} ", entry.flags)
89− },
90− entry.mode,
91− entry.id,
92− entry.path(file)
93− )
94−}
gitoxide-core/src/index/mod.rs+0−3
@@ -5,9 +5,6 @@ pub struct Options { · 4 unmodified lines
55 pub format: crate::OutputFormat,
66 }
77
8−mod entries;
9−pub use entries::entries;
10−
118 pub mod information;
129
1310 fn parse_file(index_path: impl AsRef<Path>, object_hash: gix::hash::Kind) -> anyhow::Result<gix::index::File> {
gitoxide-core/src/repository/exclude.rs+7−6
@@ -1,6 +1,6 @@
11 use std::io;
22
3−use anyhow::{bail, Context};
3+use anyhow::bail;
44 use gix::prelude::FindExt;
55
66 use crate::OutputFormat;
@@ -31,11 +31,12 @@ pub fn query( · 24 unmodified lines
3131 bail!("JSON output isn't implemented yet");
3232 }
3333
34− let worktree = repo
35− .worktree()
36− .with_context(|| "Cannot check excludes without a current worktree")?;
37− let index = worktree.index()?;
38− let mut cache = worktree.excludes(&index, Some(gix::ignore::Search::from_overrides(overrides)))?;
34+ let index = repo.index()?;
35+ let mut cache = repo.excludes(
36+ &index,
37+ Some(gix::ignore::Search::from_overrides(overrides)),
38+ Default::default(),
39+ )?;
3940
4041 let prefix = repo.prefix().expect("worktree - we have an index by now")?;
4142
gitoxide-core/src/repository/index/entries.rs+228−0
@@ -0,0 +1,228 @@
1+#[derive(Debug)]
2+pub struct Options {
3+ pub format: crate::OutputFormat,
4+ /// If true, also show attributes
5+ pub attributes: Option<Attributes>,
6+ pub statistics: bool,
7+}
8+
9+#[derive(Debug)]
10+pub enum Attributes {
11+ /// Look at worktree attributes and index as fallback.
12+ WorktreeAndIndex,
13+ /// Look at attributes from index files only.
14+ Index,
15+}
16+
17+pub(crate) mod function {
18+ use crate::repository::index::entries::{Attributes, Options};
19+ use gix::attrs::State;
20+ use gix::bstr::ByteSlice;
21+ use gix::odb::FindExt;
22+ use std::borrow::Cow;
23+ use std::io::{BufWriter, Write};
24+
25+ pub fn entries(
26+ repo: gix::Repository,
27+ out: impl std::io::Write,
28+ mut err: impl std::io::Write,
29+ Options {
30+ format,
31+ attributes,
32+ statistics,
33+ }: Options,
34+ ) -> anyhow::Result<()> {
35+ use crate::OutputFormat::*;
36+ let index = repo.index()?;
37+ let mut cache = attributes
38+ .map(|attrs| {
39+ repo.attributes(
40+ &index,
41+ match attrs {
42+ Attributes::WorktreeAndIndex => {
43+ gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping
44+ }
45+ Attributes::Index => gix::worktree::cache::state::attributes::Source::IdMapping,
46+ },
47+ match attrs {
48+ Attributes::WorktreeAndIndex => {
49+ gix::worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped
50+ }
51+ Attributes::Index => gix::worktree::cache::state::ignore::Source::IdMapping,
52+ },
53+ None,
54+ )
55+ .map(|cache| (cache.attribute_matches(), cache))
56+ })
57+ .transpose()?;
58+ let mut stats = Statistics {
59+ entries: index.entries().len(),
60+ ..Default::default()
61+ };
62+
63+ let mut out = BufWriter::new(out);
64+ #[cfg(feature = "serde")]
65+ if let Json = format {
66+ out.write_all(b"[\n")?;
67+ }
68+ let mut entries = index.entries().iter().peekable();
69+ while let Some(entry) = entries.next() {
70+ let attrs = cache
71+ .as_mut()
72+ .map(|(attrs, cache)| {
73+ cache
74+ .at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf))
75+ .map(|entry| {
76+ let is_excluded = entry.is_excluded();
77+ stats.excluded += usize::from(is_excluded);
78+ let attributes: Vec<_> = {
79+ entry.matching_attributes(attrs);
80+ attrs.iter().map(|m| m.assignment.to_owned()).collect()
81+ };
82+ stats.with_attributes += usize::from(!attributes.is_empty());
83+ Attrs {
84+ is_excluded,
85+ attributes,
86+ }
87+ })
88+ })
89+ .transpose()?;
90+ match format {
91+ Human => to_human(&mut out, &index, entry, attrs)?,
92+ #[cfg(feature = "serde")]
93+ Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?,
94+ }
95+ }
96+
97+ #[cfg(feature = "serde")]
98+ if format == Json {
99+ out.write_all(b"]\n")?;
100+ out.flush()?;
101+ if statistics {
102+ serde_json::to_writer_pretty(&mut err, &stats)?;
103+ }
104+ }
105+ if format == Human && statistics {
106+ out.flush()?;
107+ stats.cache = cache.map(|c| *c.1.statistics());
108+ writeln!(err, "{:#?}", stats)?;
109+ }
110+ Ok(())
111+ }
112+
113+ #[cfg_attr(feature = "serde", derive(serde::Serialize))]
114+ struct Attrs {
115+ is_excluded: bool,
116+ attributes: Vec<gix::attrs::Assignment>,
117+ }
118+
119+ #[cfg_attr(feature = "serde", derive(serde::Serialize))]
120+ #[derive(Default, Debug)]
121+ struct Statistics {
122+ #[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial.
123+ pub entries: usize,
124+ pub excluded: usize,
125+ pub with_attributes: usize,
126+ pub cache: Option<gix::worktree::cache::Statistics>,
127+ }
128+
129+ #[cfg(feature = "serde")]
130+ fn to_json(
131+ mut out: &mut impl std::io::Write,
132+ index: &gix::index::File,
133+ entry: &gix::index::Entry,
134+ attrs: Option<Attrs>,
135+ is_last: bool,
136+ ) -> anyhow::Result<()> {
137+ #[cfg_attr(feature = "serde", derive(serde::Serialize))]
138+ struct Entry<'a> {
139+ stat: &'a gix::index::entry::Stat,
140+ hex_id: String,
141+ flags: u32,
142+ mode: u32,
143+ path: std::borrow::Cow<'a, str>,
144+ meta: Option<Attrs>,
145+ }
146+
147+ serde_json::to_writer(
148+ &mut out,
149+ &Entry {
150+ stat: &entry.stat,
151+ hex_id: entry.id.to_hex().to_string(),
152+ flags: entry.flags.bits(),
153+ mode: entry.mode.bits(),
154+ path: entry.path(index).to_str_lossy(),
155+ meta: attrs,
156+ },
157+ )?;
158+
159+ if is_last {
160+ out.write_all(b"\n")?;
161+ } else {
162+ out.write_all(b",\n")?;
163+ }
164+ Ok(())
165+ }
166+
167+ fn to_human(
168+ out: &mut impl std::io::Write,
169+ file: &gix::index::File,
170+ entry: &gix::index::Entry,
171+ attrs: Option<Attrs>,
172+ ) -> std::io::Result<()> {
173+ writeln!(
174+ out,
175+ "{} {}{:?} {} {}{}",
176+ match entry.flags.stage() {
177+ 0 => "BASE ",
178+ 1 => "OURS ",
179+ 2 => "THEIRS ",
180+ _ => "UNKNOWN",
181+ },
182+ if entry.flags.is_empty() {
183+ "".to_string()
184+ } else {
185+ format!("{:?} ", entry.flags)
186+ },
187+ entry.mode,
188+ entry.id,
189+ entry.path(file),
190+ attrs
191+ .map(|a| {
192+ let mut buf = String::new();
193+ if a.is_excluded {
194+ buf.push_str(" ❌");
195+ }
196+ if !a.attributes.is_empty() {
197+ buf.push_str(" (");
198+ for assignment in a.attributes {
199+ match assignment.state {
200+ State::Set => {
201+ buf.push_str(assignment.name.as_str());
202+ }
203+ State::Unset => {
204+ buf.push('-');
205+ buf.push_str(assignment.name.as_str());
206+ }
207+ State::Value(v) => {
208+ buf.push_str(assignment.name.as_str());
209+ buf.push('=');
210+ buf.push_str(v.as_ref().as_bstr().to_str_lossy().as_ref());
211+ }
212+ State::Unspecified => {
213+ buf.push('!');
214+ buf.push_str(assignment.name.as_str());
215+ }
216+ }
217+ buf.push_str(", ");
218+ }
219+ buf.pop();
220+ buf.pop();
221+ buf.push(')');
222+ }
223+ buf.into()
224+ })
225+ .unwrap_or(Cow::Borrowed(""))
226+ )
227+ }
228+}
gitoxide-core/src/repository/index/mod.rsrenamed from gitoxide-core/src/repository/index.rs+3−0
@@ -34,3 +34,6 @@ pub fn from_tree( · 33 unmodified lines
3434
3535 Ok(())
3636 }
37+
38+pub mod entries;
39+pub use entries::function::entries;
gix-bitmap/src/ewah.rs+4−0
@@ -1,13 +1,17 @@
11 use std::convert::TryInto;
22
3+///
34 pub mod decode {
5+ /// The error returned by [`decode()`][super::decode()].
46 #[derive(Debug, thiserror::Error)]
7+ #[allow(missing_docs)]
58 pub enum Error {
69 #[error("{}", message)]
710 Corrupt { message: &'static str },
811 }
912 }
1013
14+/// Decode `data` as EWAH bitmap.
1115 pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> {
1216 use self::decode::Error;
1317 use crate::decode;
gix-bitmap/src/lib.rs+1−2
@@ -1,8 +1,7 @@
11 //! An implementation of the shared parts of git bitmaps used in `gix-pack`, `gix-index` and `gix-worktree`.
22 //!
33 //! Note that many tests are performed indirectly by tests in the aforementioned consumer crates.
4−#![deny(rust_2018_idioms, unsafe_code)]
5−#![allow(missing_docs)]
4+#![deny(rust_2018_idioms, unsafe_code, missing_docs)]
65
76 /// Bitmap utilities for the advanced word-aligned hybrid bitmap
87 pub mod ewah;
gix-fs/src/lib.rs+2−1
@@ -1,5 +1,5 @@
11 //! A crate with file-system specific utilities.
2−#![deny(rust_2018_idioms)]
2+#![deny(rust_2018_idioms, missing_docs)]
33 #![forbid(unsafe_code)]
44
55 /// Common knowledge about the worktree that is needed across most interactions with the work tree
@@ -51,4 +51,5 @@ pub struct Stack { · 45 unmodified lines
5151 current_is_directory: bool,
5252 }
5353
54+///
5455 pub mod stack;
gix-fs/src/symlink.rs+10−0
@@ -1,16 +1,21 @@
11 use std::{io, io::ErrorKind::AlreadyExists, path::Path};
22
33 #[cfg(not(windows))]
4+/// Create a new symlink at `link` which points to `original`.
45 pub fn create(original: &Path, link: &Path) -> io::Result<()> {
56 std::os::unix::fs::symlink(original, link)
67 }
78
89 #[cfg(not(windows))]
10+/// Remove a symlink.
11+///
12+/// Note that on only on windows this is special.
913 pub fn remove(path: &Path) -> io::Result<()> {
1014 std::fs::remove_file(path)
1115 }
1216
1317 // TODO: use the `symlink` crate once it can delete directory symlinks
18+/// Remove a symlink.
1419 #[cfg(windows)]
1520 pub fn remove(path: &Path) -> io::Result<()> {
1621 if let Ok(meta) = std::fs::metadata(path) {
@@ -25,6 +30,7 @@ pub fn remove(path: &Path) -> io::Result<()> { · 8 unmodified lines
2530 }
2631
2732 #[cfg(windows)]
33+/// Create a new symlink at `link` which points to `original`.
2834 pub fn create(original: &Path, link: &Path) -> io::Result<()> {
2935 use std::os::windows::fs::{symlink_dir, symlink_file};
3036 // TODO: figure out if links to links count as files or whatever they point at
@@ -36,6 +42,8 @@ pub fn create(original: &Path, link: &Path) -> io::Result<()> { · 5 unmodified lines
3642 }
3743
3844 #[cfg(not(windows))]
45+/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link`
46+/// already exists as filesystem object.
3947 pub fn is_collision_error(err: &std::io::Error) -> bool {
4048 // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively
4149 err.kind() == AlreadyExists
@@ -45,6 +53,8 @@ pub fn is_collision_error(err: &std::io::Error) -> bool { · 3 unmodified lines
4553 }
4654
4755 #[cfg(windows)]
56+/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link`
57+/// already exists as filesystem object.
4858 pub fn is_collision_error(err: &std::io::Error) -> bool {
4959 err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied
5060 }
gix-path/src/convert.rs+1−1
@@ -85,7 +85,7 @@ pub fn into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> { · 84 unmodified lines
8585 /// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`.
8686 pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into<Cow<'a, BStr>>, path: impl Into<&'b BStr>) -> Cow<'a, BStr> {
8787 let mut base = base.into();
88− if base.last() != Some(&b'/') {
88+ if !base.is_empty() && base.last() != Some(&b'/') {
8989 base.to_mut().push(b'/');
9090 }
9191 base.to_mut().extend_from_slice(path.into());
gix-path/tests/convert/mod.rs+4−4
@@ -56,9 +56,9 @@ mod join_bstr_unix_pathsep { · 55 unmodified lines
5656 assert_eq!(join_bstr_unix_pathsep(b("base/"), ""), b("base/"));
5757 }
5858 #[test]
59− fn empty_base_creates_absolute_paths() {
60− assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("/"));
61− assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("/hi"));
62− assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("//hi"));
59+ fn empty_base_leaves_everything_untouched() {
60+ assert_eq!(join_bstr_unix_pathsep(b(""), ""), b(""));
61+ assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("hi"));
62+ assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("/hi"));
6363 }
6464 }
gix-utils/src/lib.rs+1−1
@@ -1,7 +1,7 @@
11 //! A crate with utilities that don't need feature toggles.
22 //!
33 //! If they would need feature toggles, they should be in `gix-features` instead.
4−#![deny(rust_2018_idioms)]
4+#![deny(rust_2018_idioms, missing_docs)]
55 #![forbid(unsafe_code)]
66
77 ///
gix-worktree/src/cache/delegate.rs+54−28
@@ -1,12 +1,30 @@
11 use crate::cache::State;
22 use crate::PathIdMapping;
33
4−pub struct StackDelegate<'a, Find> {
4+/// Various aggregate numbers related to the stack delegate itself.
5+#[derive(Default, Clone, Copy, Debug)]
6+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7+pub struct Statistics {
8+ /// The amount of `std::fs::create_dir` calls.
9+ ///
10+ /// This only happens if we are in the respective mode to create leading directories efficiently.
11+ pub num_mkdir_calls: usize,
12+ /// Amount of calls to push a path element.
13+ pub push_element: usize,
14+ /// Amount of calls to push a directory.
15+ pub push_directory: usize,
16+ /// Amount of calls to pop a directory.
17+ pub pop_directory: usize,
18+}
19+
20+pub(crate) struct StackDelegate<'a, Find> {
521 pub state: &'a mut State,
622 pub buf: &'a mut Vec<u8>,
723 pub is_dir: bool,
824 pub id_mappings: &'a Vec<PathIdMapping>,
925 pub find: Find,
26+ pub case: gix_glob::pattern::Case,
27+ pub statistics: &'a mut super::Statistics,
1028 }
1129
1230 impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find>
@@ -15,72 +33,86 @@ where · 2 unmodified lines
1533 E: std::error::Error + Send + Sync + 'static,
1634 {
1735 fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> {
36+ self.statistics.delegate.push_directory += 1;
37+ let dir_bstr = gix_path::into_bstr(stack.current());
38+ let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos(
39+ gix_path::into_bstr(stack.root()).as_ref(),
40+ dir_bstr.as_ref(),
41+ None,
42+ self.case,
43+ )
44+ .expect("dir in root")
45+ .0;
46+ if rela_dir.starts_with(b"/") {
47+ rela_dir = &rela_dir[1..];
48+ }
1849 match &mut self.state {
1950 State::CreateDirectoryAndAttributesStack { attributes, .. } => {
2051 attributes.push_directory(
2152 stack.root(),
2253 stack.current(),
54+ rela_dir,
2355 self.buf,
2456 self.id_mappings,
2557 &mut self.find,
58+ &mut self.statistics.attributes,
2659 )?;
2760 }
2861 State::AttributesAndIgnoreStack { ignore, attributes } => {
2962 attributes.push_directory(
3063 stack.root(),
3164 stack.current(),
65+ rela_dir,
3266 self.buf,
3367 self.id_mappings,
3468 &mut self.find,
69+ &mut self.statistics.attributes,
3570 )?;
3671 ignore.push_directory(
3772 stack.root(),
3873 stack.current(),
74+ rela_dir,
3975 self.buf,
4076 self.id_mappings,
4177 &mut self.find,
78+ self.case,
79+ &mut self.statistics.ignore,
4280 )?
4381 }
4482 State::IgnoreStack(ignore) => ignore.push_directory(
4583 stack.root(),
4684 stack.current(),
85+ rela_dir,
4786 self.buf,
4887 self.id_mappings,
4988 &mut self.find,
89+ self.case,
90+ &mut self.statistics.ignore,
5091 )?,
5192 }
5293 Ok(())
5394 }
5495
5596 fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> {
97+ self.statistics.delegate.push_element += 1;
5698 match &mut self.state {
5799 State::CreateDirectoryAndAttributesStack {
58− #[cfg(debug_assertions)]
59− test_mkdir_calls,
60100 unlink_on_collision,
61101 attributes: _,
62− } => {
63− #[cfg(debug_assertions)]
64− {
65− create_leading_directory(
66− is_last_component,
67− stack,
68− self.is_dir,
69− test_mkdir_calls,
70− *unlink_on_collision,
71− )?
72− }
73− #[cfg(not(debug_assertions))]
74− {
75− create_leading_directory(is_last_component, stack, self.is_dir, *unlink_on_collision)?
76− }
77− }
102+ } => create_leading_directory(
103+ is_last_component,
104+ stack,
105+ self.is_dir,
106+ &mut self.statistics.delegate.num_mkdir_calls,
107+ *unlink_on_collision,
108+ )?,
78109 State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {}
79110 }
80111 Ok(())
81112 }
82113
83114 fn pop_directory(&mut self) {
115+ self.statistics.delegate.pop_directory += 1;
84116 match &mut self.state {
85117 State::CreateDirectoryAndAttributesStack { attributes, .. } => {
86118 attributes.pop_directory();
@@ -100,16 +132,13 @@ fn create_leading_directory( · 13 unmodified lines
100132 is_last_component: bool,
101133 stack: &gix_fs::Stack,
102134 is_dir: bool,
103− #[cfg(debug_assertions)] mkdir_calls: &mut usize,
135+ mkdir_calls: &mut usize,
104136 unlink_on_collision: bool,
105137 ) -> std::io::Result<()> {
106138 if is_last_component && !is_dir {
107139 return Ok(());
108140 }
109− #[cfg(debug_assertions)]
110− {
111− *mkdir_calls += 1;
112− }
141+ *mkdir_calls += 1;
113142 match std::fs::create_dir(stack.current()) {
114143 Ok(()) => Ok(()),
115144 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
@@ -122,10 +151,7 @@ fn create_leading_directory( · 6 unmodified lines
122151 } else {
123152 std::fs::remove_file(stack.current())?;
124153 }
125− #[cfg(debug_assertions)]
126− {
127− *mkdir_calls += 1;
128− }
154+ *mkdir_calls += 1;
129155 std::fs::create_dir(stack.current())
130156 } else {
131157 Err(err)
gix-worktree/src/cache/mod.rs+38−35
@@ -7,16 +7,26 @@ use gix_hash::oid; · 6 unmodified lines
77 use super::Cache;
88 use crate::PathIdMapping;
99
10+/// Various aggregate numbers collected from when the corresponding [`Cache`] was instantiated.
11+#[derive(Default, Clone, Copy, Debug)]
12+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13+pub struct Statistics {
14+ /// The amount of platforms created to do further matching.
15+ pub platforms: usize,
16+ /// Information about the stack delegate.
17+ pub delegate: delegate::Statistics,
18+ /// Information about attributes
19+ pub attributes: state::attributes::Statistics,
20+ /// Information about the ignore stack
21+ pub ignore: state::ignore::Statistics,
22+}
23+
1024 #[derive(Clone)]
1125 pub enum State {
1226 /// Useful for checkout where directories need creation, but we need to access attributes as well.
1327 CreateDirectoryAndAttributesStack {
1428 /// If there is a symlink or a file in our path, try to unlink it before creating the directory.
1529 unlink_on_collision: bool,
16−
17− /// just for testing
18− #[cfg(debug_assertions)]
19− test_mkdir_calls: usize,
2030 /// State to handle attribute information
2131 attributes: state::Attributes,
2232 },
@@ -31,35 +41,6 @@ pub enum State { · 8 unmodified lines
3141 IgnoreStack(state::Ignore),
3242 }
3343
34−#[cfg(debug_assertions)]
35−/// debug builds only for use in tests.
36−impl Cache {
37− pub fn set_case(&mut self, case: gix_glob::pattern::Case) {
38− self.case = case;
39− }
40− pub fn num_mkdir_calls(&self) -> usize {
41− match self.state {
42− State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } => test_mkdir_calls,
43− _ => 0,
44− }
45− }
46−
47− pub fn reset_mkdir_calls(&mut self) {
48− if let State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } = &mut self.state {
49− *test_mkdir_calls = 0;
50− }
51− }
52−
53− pub fn unlink_on_collision(&mut self, value: bool) {
54− if let State::CreateDirectoryAndAttributesStack {
55− unlink_on_collision, ..
56− } = &mut self.state
57− {
58− *unlink_on_collision = value;
59− }
60− }
61−}
62−
6344 #[must_use]
6445 pub struct Platform<'a> {
6546 parent: &'a Cache,
@@ -87,11 +68,12 @@ impl Cache { · 21 unmodified lines
8768 case,
8869 buf,
8970 id_mappings,
71+ statistics: Statistics::default(),
9072 }
9173 }
9274 }
9375
94−/// Mutable Access
76+/// Entry points for attribute query
9577 impl Cache {
9678 /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no
9779 /// symlinks are in that path.
@@ -110,12 +92,15 @@ impl Cache { · 12 unmodified lines
11092 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
11193 E: std::error::Error + Send + Sync + 'static,
11294 {
95+ self.statistics.platforms += 1;
11396 let mut delegate = StackDelegate {
11497 state: &mut self.state,
11598 buf: &mut self.buf,
11699 is_dir: is_dir.unwrap_or(false),
117100 id_mappings: &self.id_mappings,
118101 find,
102+ case: self.case,
103+ statistics: &mut self.statistics,
119104 };
120105 self.stack.make_relative_path_current(relative, &mut delegate)?;
121106 Ok(Platform { parent: self, is_dir })
@@ -152,8 +137,25 @@ impl Cache { · 30 unmodified lines
152137 }
153138 }
154139
140+/// Mutation
141+impl Cache {
142+ /// Reset the statistics after returning them.
143+ pub fn take_statistics(&mut self) -> Statistics {
144+ std::mem::take(&mut self.statistics)
145+ }
146+
147+ /// Return our state for applying changes.
148+ pub fn state_mut(&mut self) -> &mut State {
149+ &mut self.state
150+ }
151+}
152+
155153 /// Access
156154 impl Cache {
155+ /// Return the statistics we gathered thus far.
156+ pub fn statistics(&self) -> &Statistics {
157+ &self.statistics
158+ }
157159 /// Return the state for introspection.
158160 pub fn state(&self) -> &State {
159161 &self.state
@@ -167,7 +169,8 @@ impl Cache { · 7 unmodified lines
167169 }
168170 }
169171
170−mod delegate;
172+///
173+pub mod delegate;
171174 use delegate::StackDelegate;
172175
173176 mod platform;
gix-worktree/src/cache/state/attributes.rs+25−25
@@ -6,6 +6,18 @@ use gix_glob::pattern::Case; · 5 unmodified lines
66 use crate::cache::state::{AttributeMatchGroup, Attributes};
77 use bstr::{BStr, ByteSlice};
88
9+/// Various aggregate numbers related [`Attributes`].
10+#[derive(Default, Clone, Copy, Debug)]
11+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12+pub struct Statistics {
13+ /// Amount of patterns buffers read from the index.
14+ pub patterns_buffers: usize,
15+ /// Amount of pattern files read from disk.
16+ pub pattern_files: usize,
17+ /// Amount of pattern files we tried to find on disk.
18+ pub tried_pattern_files: usize,
19+}
20+
921 /// Decide where to read `.gitattributes` files from.
1022 #[derive(Default, Debug, Clone, Copy)]
1123 pub enum Source {
@@ -42,7 +54,6 @@ impl Attributes { · 30 unmodified lines
4254 pub fn new(
4355 globals: AttributeMatchGroup,
4456 info_attributes: Option<PathBuf>,
45− case: Case,
4657 source: Source,
4758 collection: gix_attributes::search::MetadataCollection,
4859 ) -> Self {
@@ -50,7 +61,6 @@ impl Attributes { · 1 unmodified line
5061 globals,
5162 stack: Default::default(),
5263 info_attributes,
53− case,
5464 source,
5565 collection,
5666 }
@@ -62,31 +72,21 @@ impl Attributes { · 5 unmodified lines
6272 self.stack.pop_pattern_list().expect("something to pop");
6373 }
6474
75+ #[allow(clippy::too_many_arguments)]
6576 pub(crate) fn push_directory<Find, E>(
6677 &mut self,
6778 root: &Path,
6879 dir: &Path,
80+ rela_dir: &BStr,
6981 buf: &mut Vec<u8>,
7082 id_mappings: &[PathIdMapping],
7183 mut find: Find,
84+ stats: &mut Statistics,
7285 ) -> std::io::Result<()>
7386 where
7487 Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>,
7588 E: std::error::Error + Send + Sync + 'static,
7689 {
77− let dir_bstr = gix_path::into_bstr(dir);
78− let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos(
79− gix_path::into_bstr(root).as_ref(),
80− dir_bstr.as_ref(),
81− None,
82− self.case,
83− )
84− .expect("dir in root")
85− .0;
86− if rela_dir.starts_with(b"/") {
87− rela_dir = &rela_dir[1..];
88− }
89−
9090 let attr_path_relative =
9191 gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes"));
9292 let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref()));
@@ -103,6 +103,7 @@ impl Attributes { · 10 unmodified lines
103103 self.stack
104104 .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection);
105105 added = true;
106+ stats.patterns_buffers += 1;
106107 }
107108 if !added && matches!(self.source, Source::IdMappingThenWorktree) {
108109 added = self.stack.add_patterns_file(
@@ -112,6 +113,8 @@ impl Attributes { · 3 unmodified lines
112113 buf,
113114 &mut self.collection,
114115 )?;
116+ stats.pattern_files += usize::from(added);
117+ stats.tried_pattern_files += 1;
115118 }
116119 }
117120 Source::WorktreeThenIdMapping => {
@@ -122,6 +125,8 @@ impl Attributes { · 4 unmodified lines
122125 buf,
123126 &mut self.collection,
124127 )?;
128+ stats.pattern_files += usize::from(added);
129+ stats.tried_pattern_files += 1;
125130 if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) {
126131 let blob = find(&id_mappings[idx].1, buf)
127132 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
@@ -129,6 +134,7 @@ impl Attributes { · 1 unmodified line
129134 self.stack
130135 .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection);
131136 added = true;
137+ stats.patterns_buffers += 1;
132138 }
133139 }
134140 }
@@ -142,8 +148,11 @@ impl Attributes { · 7 unmodified lines
142148 // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is
143149 // by nature never popped, and follows the root, as global.
144150 if let Some(info_attr) = self.info_attributes.take() {
145− self.stack
151+ let added = self
152+ .stack
146153 .add_patterns_file(info_attr, true, None, buf, &mut self.collection)?;
154+ stats.pattern_files += usize::from(added);
155+ stats.tried_pattern_files += 1;
147156 }
148157
149158 Ok(())
@@ -168,15 +177,6 @@ impl Attributes { · 18 unmodified lines
168177 }
169178 }
170179
171−/// Builder
172−impl Attributes {
173− /// Set the case to use when matching attributes to paths.
174− pub fn with_case(mut self, case: gix_glob::pattern::Case) -> Self {
175− self.case = case;
176− self
177− }
178−}
179−
180180 /// Attribute matching specific methods
181181 impl Cache {
182182 /// Creates a new container to store match outcomes for all attribute matches.
gix-worktree/src/cache/state/ignore.rs+81−57
@@ -1,45 +1,52 @@
11 use std::path::Path;
22
3+use crate::cache::state::Ignore;
34 use crate::{cache::state::IgnoreMatchGroup, PathIdMapping};
4−use bstr::{BStr, BString, ByteSlice};
5+use bstr::{BStr, ByteSlice};
56 use gix_glob::pattern::Case;
67
7−/// State related to the exclusion of files.
8−#[derive(Default, Clone)]
9−#[allow(unused)]
10−pub struct Ignore {
11− /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to
12− /// be consulted.
13− overrides: IgnoreMatchGroup,
14− /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed.
15− stack: IgnoreMatchGroup,
16− /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last.
17− globals: IgnoreMatchGroup,
18− /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the
19− /// currently set directory had a pattern matched. Note that this one could be negated.
20− /// (index into match groups, index into list of pattern lists, index into pattern list)
21− matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>,
22− /// The name of the file to look for in directories.
23− pub(crate) exclude_file_name_for_directories: BString,
24− /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine
25− /// to know if an entire path can be ignored as a parent directory is ignored.
26− pub(crate) case: Case,
8+/// Decide where to read `.gitignore` files from.
9+#[derive(Default, Debug, Clone, Copy)]
10+pub enum Source {
11+ /// Retrieve ignore files from id mappings, see
12+ /// [State::id_mappings_from_index()][crate::cache::State::id_mappings_from_index()].
13+ ///
14+ /// These mappings are typically produced from an index.
15+ /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index.
16+ ///
17+ /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts
18+ /// of the history which aren't checked out.
19+ IdMapping,
20+ /// Read from the worktree and if not present, read them from the id mappings *if* these don't have the skip-worktree bit set.
21+ #[default]
22+ WorktreeThenIdMappingIfNotSkipped,
23+}
24+
25+/// Various aggregate numbers related [`Ignore`].
26+#[derive(Default, Clone, Copy, Debug)]
27+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28+pub struct Statistics {
29+ /// Amount of patterns buffers read from the index.
30+ pub patterns_buffers: usize,
31+ /// Amount of pattern files read from disk.
32+ pub pattern_files: usize,
33+ /// Amount of pattern files we tried to find on disk.
34+ pub tried_pattern_files: usize,
2735 }
2836
2937 impl Ignore {
38+ /// Configure gitignore file matching by providing the immutable groups being `overrides` and `globals`, while letting the directory
39+ /// stack be dynamic.
40+ ///
3041 /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory
3142 /// ignore files within the repository, defaults to`.gitignore`.
32− ///
33− // This is what it should be able represent: https://github.com/git/git/blob/140b9478dad5d19543c1cb4fd293ccec228f1240/dir.c#L3354
34− // TODO: more docs
3543 pub fn new(
3644 overrides: IgnoreMatchGroup,
3745 globals: IgnoreMatchGroup,
3846 exclude_file_name_for_directories: Option<&BStr>,
39− case: Case,
47+ source: Source,
4048 ) -> Self {
4149 Ignore {
42− case,
4350 overrides,
4451 globals,
4552 stack: Default::default(),
@@ -47,6 +54,7 @@ impl Ignore { · 1 unmodified line
4754 exclude_file_name_for_directories: exclude_file_name_for_directories
4855 .map(ToOwned::to_owned)
4956 .unwrap_or_else(|| ".gitignore".into()),
57+ source,
5058 }
5159 }
5260 }
@@ -135,55 +143,71 @@ impl Ignore { · 82 unmodified lines
135143 })
136144 }
137145
146+ #[allow(clippy::too_many_arguments)]
138147 pub(crate) fn push_directory<Find, E>(
139148 &mut self,
140149 root: &Path,
141150 dir: &Path,
151+ rela_dir: &BStr,
142152 buf: &mut Vec<u8>,
143153 id_mappings: &[PathIdMapping],
144154 mut find: Find,
155+ case: Case,
156+ stats: &mut Statistics,
145157 ) -> std::io::Result<()>
146158 where
147159 Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>,
148160 E: std::error::Error + Send + Sync + 'static,
149161 {
150− let dir_bstr = gix_path::into_bstr(dir);
151− let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos(
152− gix_path::into_bstr(root).as_ref(),
153− dir_bstr.as_ref(),
154− None,
155− self.case,
156− )
157− .expect("dir in root")
158− .0;
159− if rela_dir.starts_with(b"/") {
160− rela_dir = &rela_dir[1..];
161− }
162162 self.matched_directory_patterns_stack
163− .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), self.case));
163+ .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case));
164164
165165 let ignore_path_relative =
166166 gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore"));
167167 let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref()));
168− let follow_symlinks = ignore_file_in_index.is_err();
169− if !gix_glob::search::add_patterns_file(
170− &mut self.stack.patterns,
171− dir.join(".gitignore"),
172− follow_symlinks,
173− Some(root),
174− buf,
175− )? {
176− match ignore_file_in_index {
177− Ok(idx) => {
178− let ignore_blob = find(&id_mappings[idx].1, buf)
179− .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
180− let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
181− self.stack
182− .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")));
168+ match self.source {
169+ Source::IdMapping => {
170+ match ignore_file_in_index {
171+ Ok(idx) => {
172+ let ignore_blob = find(&id_mappings[idx].1, buf)
173+ .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
174+ let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
175+ self.stack
176+ .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")));
177+ stats.patterns_buffers += 1;
178+ }
179+ Err(_) => {
180+ // Need one stack level per component so push and pop matches.
181+ self.stack.patterns.push(Default::default())
182+ }
183183 }
184− Err(_) => {
185− // Need one stack level per component so push and pop matches.
186− self.stack.patterns.push(Default::default())
184+ }
185+ Source::WorktreeThenIdMappingIfNotSkipped => {
186+ let follow_symlinks = ignore_file_in_index.is_err();
187+ let added = gix_glob::search::add_patterns_file(
188+ &mut self.stack.patterns,
189+ dir.join(".gitignore"),
190+ follow_symlinks,
191+ Some(root),
192+ buf,
193+ )?;
194+ stats.pattern_files += usize::from(added);
195+ stats.tried_pattern_files += 1;
196+ if !added {
197+ match ignore_file_in_index {
198+ Ok(idx) => {
199+ let ignore_blob = find(&id_mappings[idx].1, buf)
200+ .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
201+ let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
202+ self.stack
203+ .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")));
204+ stats.patterns_buffers += 1;
205+ }
206+ Err(_) => {
207+ // Need one stack level per component so push and pop matches.
208+ self.stack.patterns.push(Default::default())
209+ }
210+ }
187211 }
188212 }
189213 }
gix-worktree/src/cache/state/mod.rs+36−11
@@ -1,6 +1,6 @@
11 use std::path::PathBuf;
22
3−use bstr::ByteSlice;
3+use bstr::{BString, ByteSlice};
44 use gix_glob::pattern::Case;
55
66 use crate::{cache::State, PathIdMapping};
@@ -23,17 +23,36 @@ pub struct Attributes { · 16 unmodified lines
2323 info_attributes: Option<PathBuf>,
2424 /// A lookup table to accelerate searches.
2525 collection: gix_attributes::search::MetadataCollection,
26− /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine
27− /// to know if an entire path can be ignored as a parent directory is ignored.
28− case: Case,
2926 /// Where to read `.gitattributes` data from.
3027 source: attributes::Source,
3128 }
3229
30+/// State related to the exclusion of files, supporting static overrides and globals, along with a stack of dynamically read
31+/// ignore files from disk or from the index each time the directory changes.
32+#[derive(Default, Clone)]
33+#[allow(unused)]
34+pub struct Ignore {
35+ /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to
36+ /// be consulted.
37+ overrides: IgnoreMatchGroup,
38+ /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed.
39+ stack: IgnoreMatchGroup,
40+ /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last.
41+ globals: IgnoreMatchGroup,
42+ /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the
43+ /// currently set directory had a pattern matched. Note that this one could be negated.
44+ /// (index into match groups, index into list of pattern lists, index into pattern list)
45+ matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>,
46+ /// The name of the file to look for in directories.
47+ pub(crate) exclude_file_name_for_directories: BString,
48+ /// Where to read ignore files from
49+ source: ignore::Source,
50+}
51+
3352 ///
3453 pub mod attributes;
35−mod ignore;
36−pub use ignore::Ignore;
54+///
55+pub mod ignore;
3756
3857 /// Initialization
3958 impl State {
@@ -41,8 +60,6 @@ impl State { · 1 unmodified line
4160 pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self {
4261 State::CreateDirectoryAndAttributesStack {
4362 unlink_on_collision,
44− #[cfg(debug_assertions)]
45− test_mkdir_calls: 0,
4663 attributes,
4764 }
4865 }
@@ -76,6 +93,7 @@ impl State { · 27 unmodified lines
7693 &self,
7794 index: &gix_index::State,
7895 paths: &gix_index::PathStorageRef,
96+ ignore_source: ignore::Source,
7997 case: Case,
8098 ) -> Vec<PathIdMapping> {
8199 let a1_backing;
@@ -118,9 +136,16 @@ impl State { · 36 unmodified lines
118136 }
119137 .then_some(t.1)
120138 })?;
121− // See https://github.com/git/git/blob/master/dir.c#L912:L912
122− if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
123− return None;
139+ if is_ignore {
140+ match ignore_source {
141+ ignore::Source::IdMapping => {}
142+ ignore::Source::WorktreeThenIdMappingIfNotSkipped => {
143+ // See https://github.com/git/git/blob/master/dir.c#L912:L912
144+ if !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
145+ return None;
146+ }
147+ }
148+ };
124149 }
125150 Some((path.to_owned(), entry.id))
126151 } else {
gix-worktree/src/checkout/function.rs+2−2
@@ -57,8 +57,8 @@ where · 56 unmodified lines
5757 None,
5858 );
5959
60− let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone().with_case(case));
61− let attribute_files = state.id_mappings_from_index(index, paths, case);
60+ let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone());
61+ let attribute_files = state.id_mappings_from_index(index, paths, Default::default(), case);
6262 let mut ctx = chunk::Context {
6363 buf: Vec::new(),
6464 path_cache: Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files),
gix-worktree/src/lib.rs+1−0
@@ -45,6 +45,7 @@ pub struct Cache { · 44 unmodified lines
4545 case: gix_glob::pattern::Case,
4646 /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions.
4747 id_mappings: Vec<PathIdMapping>,
48+ statistics: cache::Statistics,
4849 }
4950
5051 pub(crate) type PathIdMapping = (BString, gix_hash::ObjectId);
gix-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz+0−3
@@ -1,3 +0,0 @@
1−version https://git-lfs.github.com/spec/v1
2−oid sha256:38df6900cdb0ad620e158a3cc19325053658b9cfcb8987779af634813d957f6c
3−size 11088
gix-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh+2−0
@@ -111,6 +111,8 @@ other-dir-with-ignore/other-sub-level-dir-anywhere/hello · 110 unmodified lines
111111 other-dir-with-ignore/other-sub-level-dir-anywhere/
112112 dir-with-ignore/negated
113113 dir-with-ignore/negated-dir/hello
114+User-file-ANYWHERE
115+User-Dir-ANYWHERE
114116 a/b/C
115117 a/B/c
116118 A/B/C
gix-worktree/tests/worktree/cache/attributes.rs+0−1
@@ -24,7 +24,6 @@ fn baseline() -> crate::Result { · 23 unmodified lines
2424 state::Attributes::new(
2525 gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?,
2626 Some(git_dir.join("info").join("attributes")),
27− case,
2827 gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping,
2928 collection,
3029 ),
gix-worktree/tests/worktree/cache/create_directory.rs+19−9
@@ -17,11 +17,11 @@ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate · 16 unmodified lines
1717 Vec::new(),
1818 Default::default(),
1919 );
20− assert_eq!(cache.num_mkdir_calls(), 0);
20+ assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0);
2121
2222 let path = cache.at_path("hello", Some(false), panic_on_find)?.path();
2323 assert!(!path.parent().unwrap().exists(), "prefix itself is never created");
24− assert_eq!(cache.num_mkdir_calls(), 0);
24+ assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0);
2525 Ok(())
2626 }
2727
@@ -43,7 +43,7 @@ fn directory_paths_are_created_in_full() { · 15 unmodified lines
4343 assert!(path.parent().unwrap().is_dir(), "dir exists");
4444 }
4545
46− assert_eq!(cache.num_mkdir_calls(), 3);
46+ assert_eq!(cache.statistics().delegate.num_mkdir_calls, 3);
4747 }
4848
4949 #[test]
@@ -54,7 +54,7 @@ fn existing_directories_are_fine() -> crate::Result { · 4 unmodified lines
5454 let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path();
5555 assert!(path.parent().unwrap().is_dir(), "directory is still present");
5656 assert!(!path.exists(), "it won't create the file");
57− assert_eq!(cache.num_mkdir_calls(), 1);
57+ assert_eq!(cache.statistics().delegate.num_mkdir_calls, 1);
5858 Ok(())
5959 }
6060
@@ -67,7 +67,12 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R · 6 unmodified lines
6767 std::fs::write(tmp.path().join("file-in-dir"), &[])?;
6868
6969 for dirname in &["file-in-dir", "link-to-dir"] {
70− cache.unlink_on_collision(false);
70+ if let cache::State::CreateDirectoryAndAttributesStack {
71+ unlink_on_collision, ..
72+ } = cache.state_mut()
73+ {
74+ *unlink_on_collision = false;
75+ }
7176 let relative_path = format!("{}/file", dirname);
7277 assert_eq!(
7378 cache
@@ -78,20 +83,25 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R · 4 unmodified lines
7883 );
7984 }
8085 assert_eq!(
81− cache.num_mkdir_calls(),
86+ cache.statistics().delegate.num_mkdir_calls,
8287 2,
8388 "it tries to create each directory once, but it's a file"
8489 );
85− cache.reset_mkdir_calls();
90+ cache.take_statistics();
8691 for dirname in &["link-to-dir", "file-in-dir"] {
87− cache.unlink_on_collision(true);
92+ if let cache::State::CreateDirectoryAndAttributesStack {
93+ unlink_on_collision, ..
94+ } = cache.state_mut()
95+ {
96+ *unlink_on_collision = true;
97+ }
8898 let relative_path = format!("{}/file", dirname);
8999 let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path();
90100 assert!(path.parent().unwrap().is_dir(), "directory was forcefully created");
91101 assert!(!path.exists());
92102 }
93103 assert_eq!(
94− cache.num_mkdir_calls(),
104+ cache.statistics().delegate.num_mkdir_calls,
95105 4,
96106 "like before, but it unlinks what's there and tries again"
97107 );
gix-worktree/tests/worktree/cache/ignore.rs+4−13
@@ -1,6 +1,7 @@
11 use bstr::{BStr, ByteSlice};
22 use gix_glob::pattern::Case;
33 use gix_odb::FindExt;
4+use gix_worktree::cache::state::ignore::Source;
45 use gix_worktree::Cache;
56
67 use crate::hex_to_id;
@@ -43,7 +44,7 @@ fn special_exclude_cases_we_handle_differently() { · 36 unmodified lines
4344 Default::default(),
4445 gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(),
4546 None,
46− case,
47+ Source::WorktreeThenIdMappingIfNotSkipped,
4748 ),
4849 );
4950 let mut cache = Cache::new(&dir, state, case, buf, Default::default());
@@ -104,11 +105,11 @@ fn check_against_baseline() -> crate::Result { · 54 unmodified lines
104105 gix_ignore::Search::from_overrides(vec!["!force-include"]),
105106 gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?,
106107 None,
107− case,
108+ Source::WorktreeThenIdMappingIfNotSkipped,
108109 ),
109110 );
110111 let paths_storage = index.take_path_backing();
111− let attribute_files_in_index = state.id_mappings_from_index(&index, &paths_storage, case);
112+ let attribute_files_in_index = state.id_mappings_from_index(&index, &paths_storage, Default::default(), case);
112113 assert_eq!(
113114 attribute_files_in_index,
114115 vec![(
@@ -156,15 +157,5 @@ fn check_against_baseline() -> crate::Result { · 41 unmodified lines
156157 }
157158 }
158159 }
159−
160− cache.set_case(Case::Fold);
161− let platform = cache.at_entry("User-file-ANYWHERE", Some(false), |oid, buf| odb.find_blob(oid, buf))?;
162− let m = platform.matching_exclude_pattern().expect("match");
163− assert_eq!(m.pattern.text, "user-file-anywhere");
164−
165− cache.set_case(Case::Fold);
166− let platform = cache.at_entry("User-Dir-ANYWHERE", Some(true), |oid, buf| odb.find_blob(oid, buf))?;
167− let m = platform.matching_exclude_pattern().expect("match");
168− assert_eq!(m.pattern.text, "user-Dir-anywhere");
169160 Ok(())
170161 }
gix/Cargo.toml+1−0
@@ -67,6 +67,7 @@ serde = [ "dep:serde", · 66 unmodified lines
6767 "gix-attributes/serde",
6868 "gix-ignore/serde",
6969 "gix-revision/serde",
70+ "gix-worktree/serde",
7071 "gix-credentials/serde"]
7172
7273 ## Re-export the progress tree root which allows to obtain progress from various functions which take `impl gix::Progress`.
gix/src/attributes.rs+9−0
@@ -0,0 +1,9 @@
1+/// The error returned by [`Repository::attributes()`][crate::Repository::attributes()].
2+#[derive(Debug, thiserror::Error)]
3+#[allow(missing_docs)]
4+pub enum Error {
5+ #[error(transparent)]
6+ ConfigureAttributes(#[from] crate::config::attribute_stack::Error),
7+ #[error(transparent)]
8+ ConfigureExcludes(#[from] crate::config::exclude_stack::Error),
9+}
gix/src/config/cache/access.rs+31−19
@@ -2,7 +2,6 @@ · 1 unmodified line
22 use std::{borrow::Cow, path::PathBuf, time::Duration};
33
44 use gix_attributes::Source;
5−use gix_glob::pattern::Case;
65 use gix_lock::acquire::Fail;
76
87 use crate::{
@@ -167,18 +166,14 @@ impl Cache { · 158 unmodified lines
167166 executable_bit: boolean(self, "core.fileMode", &Core::FILE_MODE, true)?,
168167 symlink: boolean(self, "core.symlinks", &Core::SYMLINKS, true)?,
169168 };
170− let case = if capabilities.ignore_case {
171− Case::Fold
172− } else {
173− Case::Sensitive
174− };
175169 Ok(gix_worktree::checkout::Options {
176− attributes: self.assemble_attribute_globals(
177− git_dir,
178− case,
179− gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree,
180− self.attributes,
181− )?,
170+ attributes: self
171+ .assemble_attribute_globals(
172+ git_dir,
173+ gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree,
174+ self.attributes,
175+ )?
176+ .0,
182177 fs: capabilities,
183178 thread_limit,
184179 destination_is_initially_empty: false,
@@ -199,14 +194,31 @@ impl Cache { · 14 unmodified lines
199194 })
200195 }
201196
197+ pub(crate) fn assemble_exclude_globals(
198+ &self,
199+ git_dir: &std::path::Path,
200+ overrides: Option<gix_ignore::Search>,
201+ source: gix_worktree::cache::state::ignore::Source,
202+ buf: &mut Vec<u8>,
203+ ) -> Result<gix_worktree::cache::state::Ignore, config::exclude_stack::Error> {
204+ let excludes_file = match self.excludes_file().transpose()? {
205+ Some(user_path) => Some(user_path),
206+ None => self.xdg_config_path("ignore")?,
207+ };
208+ Ok(gix_worktree::cache::state::Ignore::new(
209+ overrides.unwrap_or_default(),
210+ gix_ignore::Search::from_git_dir(git_dir, excludes_file, buf)?,
211+ None,
212+ source,
213+ ))
214+ }
202215 // TODO: at least one test, maybe related to core.attributesFile configuration.
203− fn assemble_attribute_globals(
216+ pub(crate) fn assemble_attribute_globals(
204217 &self,
205218 git_dir: &std::path::Path,
206− case: gix_glob::pattern::Case,
207219 source: gix_worktree::cache::state::attributes::Source,
208− attributes: crate::permissions::Attributes,
209− ) -> Result<gix_worktree::cache::state::Attributes, config::attribute_stack::Error> {
220+ attributes: crate::open::permissions::Attributes,
221+ ) -> Result<(gix_worktree::cache::state::Attributes, Vec<u8>), config::attribute_stack::Error> {
210222 let configured_or_user_attributes = match self
211223 .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name)
212224 .transpose()?
@@ -232,13 +244,13 @@ impl Cache { · 19 unmodified lines
232244 let info_attributes_path = git_dir.join("info").join("attributes");
233245 let mut buf = Vec::new();
234246 let mut collection = gix_attributes::search::MetadataCollection::default();
235− Ok(gix_worktree::cache::state::Attributes::new(
247+ let res = gix_worktree::cache::state::Attributes::new(
236248 gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?,
237249 Some(info_attributes_path),
238− case,
239250 source,
240251 collection,
241− ))
252+ );
253+ Ok((res, buf))
242254 }
243255
244256 pub(crate) fn xdg_config_path(
gix/src/config/cache/init.rs+8−8
@@ -12,7 +12,7 @@ use crate::{ · 11 unmodified lines
1212 tree::{gitoxide, Core, Http},
1313 Cache,
1414 },
15− repository,
15+ open,
1616 };
1717
1818 /// Initialization
@@ -32,7 +32,7 @@ impl Cache { · 13 unmodified lines
3232 filter_config_section: fn(&gix_config::file::Metadata) -> bool,
3333 git_install_dir: Option<&std::path::Path>,
3434 home: Option<&std::path::Path>,
35− environment @ repository::permissions::Environment {
35+ environment @ open::permissions::Environment {
3636 git_prefix,
3737 ssh_prefix: _,
3838 xdg_config_home: _,
@@ -40,16 +40,16 @@ impl Cache { · 1 unmodified line
4040 http_transport,
4141 identity,
4242 objects,
43− }: repository::permissions::Environment,
44− attributes: repository::permissions::Attributes,
45− repository::permissions::Config {
43+ }: open::permissions::Environment,
44+ attributes: open::permissions::Attributes,
45+ open::permissions::Config {
4646 git_binary: use_installation,
4747 system: use_system,
4848 git: use_git,
4949 user: use_user,
5050 env: use_env,
5151 includes: use_includes,
52− }: repository::permissions::Config,
52+ }: open::permissions::Config,
5353 lenient_config: bool,
5454 api_config_overrides: &[BString],
5555 cli_config_overrides: &[BString],
@@ -233,12 +233,12 @@ impl Cache { · 177 unmodified lines
233233 }
234234
235235 pub(crate) fn make_source_env(
236− crate::permissions::Environment {
236+ crate::open::permissions::Environment {
237237 xdg_config_home,
238238 git_prefix,
239239 home,
240240 ..
241− }: crate::permissions::Environment,
241+ }: open::permissions::Environment,
242242 ) -> impl FnMut(&str) -> Option<OsString> {
243243 move |name| {
244244 match name {
gix/src/config/mod.rs+19−2
@@ -118,6 +118,23 @@ pub mod checkout_options { · 117 unmodified lines
118118 }
119119 }
120120
121+///
122+pub mod exclude_stack {
123+ use std::path::PathBuf;
124+
125+ /// The error produced when setting up a stack to query `gitignore` information.
126+ #[derive(Debug, thiserror::Error)]
127+ #[allow(missing_docs)]
128+ pub enum Error {
129+ #[error("Could not read repository exclude")]
130+ Io(#[from] std::io::Error),
131+ #[error(transparent)]
132+ EnvironmentPermission(#[from] gix_sec::permission::Error<PathBuf>),
133+ #[error("The value for `core.excludesFile` could not be read from configuration")]
134+ ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error),
135+ }
136+}
137+
121138 ///
122139 pub mod attribute_stack {
123140 /// The error produced when setting up the attribute stack to query `gitattributes`.
@@ -462,7 +479,7 @@ pub(crate) struct Cache { · 338 unmodified lines
462479 /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient.
463480 /// Also available in options! Keep in sync!
464481 pub lenient_config: bool,
465− attributes: crate::permissions::Attributes,
466− environment: crate::permissions::Environment,
482+ attributes: crate::open::permissions::Attributes,
483+ environment: crate::open::permissions::Environment,
467484 // TODO: make core.precomposeUnicode available as well.
468485 }
gix/src/config/tree/mod.rs+6−3
@@ -38,6 +38,8 @@ pub(crate) mod root { · 37 unmodified lines
3838 pub const GITOXIDE: sections::Gitoxide = sections::Gitoxide;
3939 /// The `http` section.
4040 pub const HTTP: sections::Http = sections::Http;
41+ /// The `index` section.
42+ pub const INDEX: sections::Index = sections::Index;
4143 /// The `init` section.
4244 pub const INIT: sections::Init = sections::Init;
4345 /// The `pack` section.
@@ -69,6 +71,7 @@ pub(crate) mod root { · 25 unmodified lines
6971 &Self::EXTENSIONS,
7072 &Self::GITOXIDE,
7173 &Self::HTTP,
74+ &Self::INDEX,
7275 &Self::INIT,
7376 &Self::PACK,
7477 &Self::PROTOCOL,
@@ -84,9 +87,9 @@ pub(crate) mod root { · 9 unmodified lines
8487
8588 mod sections;
8689 pub use sections::{
87− branch, checkout, core, credential, diff, extensions, gitoxide, http, protocol, remote, ssh, Author, Branch,
88− Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Init, Pack, Protocol, Remote, Safe,
89− Ssh, Url, User,
90+ branch, checkout, core, credential, diff, extensions, gitoxide, http, index, protocol, remote, ssh, Author, Branch,
91+ Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Index, Init, Pack, Protocol,
92+ Remote, Safe, Ssh, Url, User,
9093 };
9194
9295 /// Generic value implementations for static instantiation.
gix/src/config/tree/sections/index.rs+62−0
@@ -0,0 +1,62 @@
1+use crate::{
2+ config,
3+ config::tree::{keys, Index, Key, Section},
4+};
5+
6+impl Index {
7+ /// The `index.threads` key.
8+ pub const THREADS: IndexThreads =
9+ IndexThreads::new_with_validate("threads", &config::Tree::INDEX, validate::IndexThreads);
10+}
11+
12+/// The `index.threads` key.
13+pub type IndexThreads = keys::Any<validate::IndexThreads>;
14+
15+mod index_threads {
16+ use crate::bstr::BStr;
17+ use crate::config;
18+ use crate::config::key::GenericErrorWithValue;
19+ use crate::config::tree::index::IndexThreads;
20+ use std::borrow::Cow;
21+
22+ impl IndexThreads {
23+ /// Parse `value` into the amount of threads to use, with `1` being single-threaded, or `0` indicating
24+ /// to select the amount of threads, with any other number being the specific amount of threads to use.
25+ pub fn try_into_index_threads(
26+ &'static self,
27+ value: Cow<'_, BStr>,
28+ ) -> Result<usize, config::key::GenericErrorWithValue> {
29+ gix_config::Integer::try_from(value.as_ref())
30+ .ok()
31+ .and_then(|i| i.to_decimal().and_then(|i| i.try_into().ok()))
32+ .or_else(|| {
33+ gix_config::Boolean::try_from(value.as_ref())
34+ .ok()
35+ .map(|b| if b.0 { 0 } else { 1 })
36+ })
37+ .ok_or_else(|| GenericErrorWithValue::from_value(self, value.into_owned()))
38+ }
39+ }
40+}
41+
42+impl Section for Index {
43+ fn name(&self) -> &str {
44+ "index"
45+ }
46+
47+ fn keys(&self) -> &[&dyn Key] {
48+ &[&Self::THREADS]
49+ }
50+}
51+
52+mod validate {
53+ use crate::{bstr::BStr, config::tree::keys};
54+
55+ pub struct IndexThreads;
56+ impl keys::Validate for IndexThreads {
57+ fn validate(&self, value: &BStr) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
58+ super::Index::THREADS.try_into_index_threads(value.into())?;
59+ Ok(())
60+ }
61+ }
62+}
gix/src/config/tree/sections/mod.rs+5−0
@@ -55,6 +55,11 @@ pub mod gitoxide; · 54 unmodified lines
5555 pub struct Http;
5656 pub mod http;
5757
58+/// The `index` top-level section.
59+#[derive(Copy, Clone, Default)]
60+pub struct Index;
61+pub mod index;
62+
5863 /// The `init` top-level section.
5964 #[derive(Copy, Clone, Default)]
6065 pub struct Init;
gix/src/lib.rs+3−7
@@ -99,6 +99,9 @@ pub use hash::{oid, ObjectId}; · 98 unmodified lines
9999
100100 pub mod interrupt;
101101
102+///
103+pub mod attributes;
104+
102105 mod ext;
103106 ///
104107 pub mod prelude;
@@ -223,13 +226,6 @@ pub fn open_opts(directory: impl Into<std::path::PathBuf>, options: open::Option · 118 unmodified lines
223226 ThreadSafeRepository::open_opts(directory, options).map(Into::into)
224227 }
225228
226−///
227−pub mod permission;
228−
229−///
230−pub mod permissions;
231−pub use repository::permissions::Permissions;
232−
233229 ///
234230 pub mod create;
235231
gix/src/open/mod.rs+14−3
@@ -1,6 +1,17 @@
11 use std::path::PathBuf;
22
3−use crate::{bstr::BString, config, permission, Permissions};
3+use crate::{bstr::BString, config};
4+
5+/// Permissions associated with various resources of a git repository
6+#[derive(Debug, Clone)]
7+pub struct Permissions {
8+ /// Control which environment variables may be accessed.
9+ pub env: permissions::Environment,
10+ /// Permissions related where git configuration should be loaded from.
11+ pub config: permissions::Config,
12+ /// Permissions related to where `gitattributes` should be loaded from.
13+ pub attributes: permissions::Attributes,
14+}
415
516 /// The options used in [`ThreadSafeRepository::open_opts()`][crate::ThreadSafeRepository::open_opts()].
617 ///
@@ -44,11 +55,11 @@ pub enum Error { · 37 unmodified lines
4455 #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())]
4556 UnsafeGitDir { path: PathBuf },
4657 #[error(transparent)]
47− EnvironmentAccessDenied(#[from] permission::env_var::resource::Error),
58+ EnvironmentAccessDenied(#[from] gix_sec::permission::Error<std::path::PathBuf>),
4859 }
4960
5061 mod options;
51−
62+pub mod permissions;
5263 mod repository;
5364
5465 #[cfg(test)]
gix/src/open/options.rs+1−1
@@ -1,7 +1,7 @@
11 use std::path::PathBuf;
22
33 use super::{Error, Options};
4−use crate::{bstr::BString, config, Permissions, ThreadSafeRepository};
4+use crate::{bstr::BString, config, open::Permissions, ThreadSafeRepository};
55
66 impl Default for Options {
77 fn default() -> Self {
gix/src/open/permissions.rs+215−0
@@ -0,0 +1,215 @@
1+//! Various permissions to define what can be done when operating a [`Repository`][crate::Repository].
2+use crate::open::Permissions;
3+use gix_sec::Trust;
4+
5+/// Configure from which sources git configuration may be loaded.
6+///
7+/// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness.
8+#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)]
9+pub struct Config {
10+ /// The git binary may come with configuration as part of its configuration, and if this is true (default false)
11+ /// we will load the configuration of the git binary, if present and not a duplicate of the ones below.
12+ ///
13+ /// It's disabled by default as it may involve executing the git binary once per execution of the application.
14+ pub git_binary: bool,
15+ /// Whether to use the system configuration.
16+ /// This is defined as `$(prefix)/etc/gitconfig` on unix.
17+ pub system: bool,
18+ /// Whether to use the git application configuration.
19+ ///
20+ /// A platform defined location for where a user's git application configuration should be located.
21+ /// If `$XDG_CONFIG_HOME` is not set or empty, `$HOME/.config/git/config` will be used
22+ /// on unix.
23+ pub git: bool,
24+ /// Whether to use the user configuration.
25+ /// This is usually `~/.gitconfig` on unix.
26+ pub user: bool,
27+ /// Whether to use the configuration from environment variables.
28+ pub env: bool,
29+ /// Whether to follow include files are encountered in loaded configuration,
30+ /// via `include` and `includeIf` sections.
31+ pub includes: bool,
32+}
33+
34+impl Config {
35+ /// Allow everything which usually relates to a fully trusted environment
36+ pub fn all() -> Self {
37+ Config {
38+ git_binary: false,
39+ system: true,
40+ git: true,
41+ user: true,
42+ env: true,
43+ includes: true,
44+ }
45+ }
46+
47+ /// Load only configuration local to the git repository.
48+ pub fn isolated() -> Self {
49+ Config {
50+ git_binary: false,
51+ system: false,
52+ git: false,
53+ user: false,
54+ env: false,
55+ includes: false,
56+ }
57+ }
58+}
59+
60+impl Default for Config {
61+ fn default() -> Self {
62+ Self::all()
63+ }
64+}
65+
66+/// Configure from which `gitattribute` files may be loaded.
67+///
68+/// Note that `.gitattribute` files from within the repository are always loaded.
69+#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)]
70+pub struct Attributes {
71+ /// The git binary may come with attribute configuration in its installation directory, and if this is true (default false)
72+ /// we will load the configuration of the git binary.
73+ ///
74+ /// It's disabled by default as it involves executing the git binary once per execution of the application.
75+ pub git_binary: bool,
76+ /// Whether to use the system configuration.
77+ /// This is typically defined as `$(prefix)/etc/gitconfig`.
78+ pub system: bool,
79+ /// Whether to use the git application configuration.
80+ ///
81+ /// A platform defined location for where a user's git application configuration should be located.
82+ /// If `$XDG_CONFIG_HOME` is not set or empty, `$HOME/.config/git/attributes` will be used
83+ /// on unix.
84+ pub git: bool,
85+}
86+
87+impl Attributes {
88+ /// Allow everything which usually relates to a fully trusted environment
89+ pub fn all() -> Self {
90+ Attributes {
91+ git_binary: false,
92+ system: true,
93+ git: true,
94+ }
95+ }
96+
97+ /// Allow loading attributes that are local to the git repository.
98+ pub fn isolated() -> Self {
99+ Attributes {
100+ git_binary: false,
101+ system: false,
102+ git: false,
103+ }
104+ }
105+}
106+
107+impl Default for Attributes {
108+ fn default() -> Self {
109+ Self::all()
110+ }
111+}
112+
113+/// Permissions related to the usage of environment variables
114+#[derive(Debug, Clone, Copy)]
115+pub struct Environment {
116+ /// Control whether resources pointed to by `XDG_CONFIG_HOME` can be used when looking up common configuration values.
117+ ///
118+ /// Note that [`gix_sec::Permission::Forbid`] will cause the operation to abort if a resource is set via the XDG config environment.
119+ pub xdg_config_home: gix_sec::Permission,
120+ /// Control the way resources pointed to by the home directory (similar to `xdg_config_home`) may be used.
121+ pub home: gix_sec::Permission,
122+ /// Control if environment variables to configure the HTTP transport, like `http_proxy` may be used.
123+ ///
124+ /// Note that http-transport related environment variables prefixed with `GIT_` may also be included here
125+ /// if they match this category like `GIT_HTTP_USER_AGENT`.
126+ pub http_transport: gix_sec::Permission,
127+ /// Control if the `EMAIL` environment variables may be read.
128+ ///
129+ /// Note that identity related environment variables prefixed with `GIT_` may also be included here
130+ /// if they match this category.
131+ pub identity: gix_sec::Permission,
132+ /// Control if environment variables related to the object database are handled. This includes features and performance
133+ /// options alike.
134+ pub objects: gix_sec::Permission,
135+ /// Control if resources pointed to by `GIT_*` prefixed environment variables can be used, **but only** if they
136+ /// are not contained in any other category. This is a catch-all section.
137+ pub git_prefix: gix_sec::Permission,
138+ /// Control if resources pointed to by `SSH_*` prefixed environment variables can be used (like `SSH_ASKPASS`)
139+ pub ssh_prefix: gix_sec::Permission,
140+}
141+
142+impl Environment {
143+ /// Allow access to the entire environment.
144+ pub fn all() -> Self {
145+ let allow = gix_sec::Permission::Allow;
146+ Environment {
147+ xdg_config_home: allow,
148+ home: allow,
149+ git_prefix: allow,
150+ ssh_prefix: allow,
151+ http_transport: allow,
152+ identity: allow,
153+ objects: allow,
154+ }
155+ }
156+
157+ /// Don't allow loading any environment variables.
158+ pub fn isolated() -> Self {
159+ let deny = gix_sec::Permission::Deny;
160+ Environment {
161+ xdg_config_home: deny,
162+ home: deny,
163+ ssh_prefix: deny,
164+ git_prefix: deny,
165+ http_transport: deny,
166+ identity: deny,
167+ objects: deny,
168+ }
169+ }
170+}
171+
172+impl Permissions {
173+ /// Secure permissions are similar to `all()`
174+ pub fn secure() -> Self {
175+ Permissions {
176+ env: Environment::all(),
177+ config: Config::all(),
178+ attributes: Attributes::all(),
179+ }
180+ }
181+
182+ /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically
183+ /// does with owned repositories.
184+ pub fn all() -> Self {
185+ Permissions {
186+ env: Environment::all(),
187+ config: Config::all(),
188+ attributes: Attributes::all(),
189+ }
190+ }
191+
192+ /// Don't read any but the local git configuration and deny reading any environment variables.
193+ pub fn isolated() -> Self {
194+ Permissions {
195+ config: Config::isolated(),
196+ attributes: Attributes::isolated(),
197+ env: Environment::isolated(),
198+ }
199+ }
200+}
201+
202+impl gix_sec::trust::DefaultForLevel for Permissions {
203+ fn default_for_level(level: Trust) -> Self {
204+ match level {
205+ Trust::Full => Permissions::all(),
206+ Trust::Reduced => Permissions::secure(),
207+ }
208+ }
209+}
210+
211+impl Default for Permissions {
212+ fn default() -> Self {
213+ Permissions::secure()
214+ }
215+}
gix/src/open/repository.rs+3−2
@@ -10,7 +10,8 @@ use crate::{ · 9 unmodified lines
1010 cache::{interpolate_context, util::ApplyLeniency},
1111 tree::{gitoxide, Core, Key, Safe},
1212 },
13− permission, Permissions, ThreadSafeRepository,
13+ open::Permissions,
14+ ThreadSafeRepository,
1415 };
1516
1617 #[derive(Default, Clone)]
@@ -26,7 +27,7 @@ pub(crate) struct EnvironmentOverrides { · 9 unmodified lines
2627 }
2728
2829 impl EnvironmentOverrides {
29− fn from_env() -> Result<Self, permission::env_var::resource::Error> {
30+ fn from_env() -> Result<Self, gix_sec::permission::Error<std::path::PathBuf>> {
3031 let mut worktree_dir = None;
3132 if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) {
3233 worktree_dir = PathBuf::from(path).into();
gix/src/permission.rs+0−8
@@ -1,8 +0,0 @@
1−///
2−pub mod env_var {
3− ///
4− pub mod resource {
5− ///
6− pub type Error = gix_sec::permission::Error<std::path::PathBuf>;
7− }
8−}
gix/src/permissions.rs+0−1
@@ -1 +0,0 @@
1−pub use crate::repository::permissions::{Attributes, Config, Environment};
gix/src/repository/attributes.rs+50−0
@@ -0,0 +1,50 @@
1+//! exclude information
2+use crate::Repository;
3+
4+impl Repository {
5+ /// Configure a file-system cache for accessing git attributes *and* excludes on a per-path basis.
6+ ///
7+ /// Use `attribute_source` to specify where to read attributes from. Also note that exclude information will
8+ /// always try to read `.gitignore` files from disk before trying to read it from the `index`.
9+ ///
10+ /// Note that no worktree is required for this to work, even though access to in-tree `.gitattributes` and `.gitignore` files
11+ /// would require a non-empty `index` that represents a git tree.
12+ ///
13+ /// This takes into consideration all the usual repository configuration, namely:
14+ ///
15+ /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file.
16+ /// * `$GIT_DIR/info/exclude|attributes` if present.
17+ // TODO: test, provide higher-level custom Cache wrapper that is much easier to use and doesn't panic when accessing entries
18+ // by non-relative path.
19+ pub fn attributes(
20+ &self,
21+ index: &gix_index::State,
22+ attributes_source: gix_worktree::cache::state::attributes::Source,
23+ ignore_source: gix_worktree::cache::state::ignore::Source,
24+ exclude_overrides: Option<gix_ignore::Search>,
25+ ) -> Result<gix_worktree::Cache, crate::attributes::Error> {
26+ let case = if self.config.ignore_case {
27+ gix_glob::pattern::Case::Fold
28+ } else {
29+ gix_glob::pattern::Case::Sensitive
30+ };
31+ let (attributes, mut buf) = self.config.assemble_attribute_globals(
32+ self.git_dir(),
33+ attributes_source,
34+ self.options.permissions.attributes,
35+ )?;
36+ let ignore =
37+ self.config
38+ .assemble_exclude_globals(self.git_dir(), exclude_overrides, ignore_source, &mut buf)?;
39+ let state = gix_worktree::cache::State::AttributesAndIgnoreStack { attributes, ignore };
40+ let attribute_list = state.id_mappings_from_index(index, index.path_backing(), ignore_source, case);
41+ Ok(gix_worktree::Cache::new(
42+ // this is alright as we don't cause mutation of that directory, it's virtual.
43+ self.work_dir().unwrap_or(self.git_dir()),
44+ state,
45+ case,
46+ buf,
47+ attribute_list,
48+ ))
49+ }
50+}
gix/src/repository/excludes.rs+45−0
@@ -0,0 +1,45 @@
1+//! exclude information
2+use crate::{config, Repository};
3+impl Repository {
4+ /// Configure a file-system cache checking if files below the repository are excluded, reading `.gitignore` files from
5+ /// the specified `source`.
6+ ///
7+ /// Note that no worktree is required for this to work, even though access to in-tree `.gitignore` files would require
8+ /// a non-empty `index` that represents a tree with `.gitignore` files.
9+ ///
10+ /// This takes into consideration all the usual repository configuration, namely:
11+ ///
12+ /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file.
13+ /// * `$GIT_DIR/info/exclude` if present.
14+ ///
15+ /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use
16+ /// [`Repository::attributes()`] for accessing both attributes and excludes.
17+ // TODO: test, provide higher-level custom Cache wrapper that is much easier to use and doesn't panic when accessing entries
18+ // by non-relative path.
19+ pub fn excludes(
20+ &self,
21+ index: &gix_index::State,
22+ overrides: Option<gix_ignore::Search>,
23+ source: gix_worktree::cache::state::ignore::Source,
24+ ) -> Result<gix_worktree::Cache, config::exclude_stack::Error> {
25+ let case = if self.config.ignore_case {
26+ gix_glob::pattern::Case::Fold
27+ } else {
28+ gix_glob::pattern::Case::Sensitive
29+ };
30+ let mut buf = Vec::with_capacity(512);
31+ let ignore = self
32+ .config
33+ .assemble_exclude_globals(self.git_dir(), overrides, source, &mut buf)?;
34+ let state = gix_worktree::cache::State::IgnoreStack(ignore);
35+ let attribute_list = state.id_mappings_from_index(index, index.path_backing(), source, case);
36+ Ok(gix_worktree::Cache::new(
37+ // this is alright as we don't cause mutation of that directory, it's virtual.
38+ self.work_dir().unwrap_or(self.git_dir()),
39+ state,
40+ case,
41+ buf,
42+ attribute_list,
43+ ))
44+ }
45+}
gix/src/repository/mod.rs+2−1
@@ -19,14 +19,15 @@ impl crate::Repository { · 18 unmodified lines
1919 }
2020 }
2121
22+mod attributes;
2223 mod cache;
2324 mod config;
25+mod excludes;
2426 pub(crate) mod identity;
2527 mod impls;
2628 mod init;
2729 mod location;
2830 mod object;
29−pub(crate) mod permissions;
3031 mod reference;
3132 mod remote;
3233 mod revision;
gix/src/repository/permissions.rs+2−11
@@ -1,16 +1,7 @@
1+//! Various permissions to define what can be done when operating a [`Repository`][crate::Repository].
2+use crate::open::Permissions;
13 use gix_sec::Trust;
24
3−/// Permissions associated with various resources of a git repository
4−#[derive(Debug, Clone)]
5−pub struct Permissions {
6− /// Control which environment variables may be accessed.
7− pub env: Environment,
8− /// Permissions related where git configuration should be loaded from.
9− pub config: Config,
10− /// Permissions related to where `gitattributes` should be loaded from.
11− pub attributes: Attributes,
12−}
13−
145 /// Configure from which sources git configuration may be loaded.
156 ///
167 /// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness.
gix/src/repository/worktree.rs+6−18
@@ -1,6 +1,7 @@
1+use crate::config::cache::util::ApplyLeniencyDefault;
12 use crate::{worktree, Worktree};
23
3−/// Worktree iteration
4+/// Interact with individual worktrees and their information.
45 impl crate::Repository {
56 /// Return a list of all _linked_ worktrees sorted by private git dir path as a lightweight proxy.
67 ///
@@ -25,10 +26,6 @@ impl crate::Repository { · 18 unmodified lines
2526 res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir));
2627 Ok(res)
2728 }
28−}
29−
30−/// Interact with individual worktrees and their information.
31−impl crate::Repository {
3229 /// Return the repository owning the main worktree, typically from a linked worktree.
3330 ///
3431 /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree.
@@ -58,23 +55,14 @@ impl crate::Repository { · 23 unmodified lines
5855 ///
5956 /// It will use the `index.threads` configuration key to learn how many threads to use.
6057 /// Note that it may fail if there is no index.
61− // TODO: test
6258 pub fn open_index(&self) -> Result<gix_index::File, worktree::open_index::Error> {
6359 let thread_limit = self
6460 .config
6561 .resolved
66− .boolean("index", None, "threads")
67− .map(|res| {
68− res.map(|value| usize::from(!value)).or_else(|err| {
69− gix_config::Integer::try_from(err.input.as_ref())
70− .map_err(|err| worktree::open_index::Error::ConfigIndexThreads {
71− value: err.input.clone(),
72− err,
73− })
74− .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1))
75− })
76− })
77− .transpose()?;
62+ .string("index", None, "threads")
63+ .map(|value| crate::config::tree::Index::THREADS.try_into_index_threads(value))
64+ .transpose()
65+ .with_lenient_default(self.config.lenient_config)?;
7866 gix_index::File::at(
7967 self.index_path(),
8068 self.object_hash(),
gix/src/worktree/mod.rs+45−44
@@ -71,18 +71,12 @@ pub mod proxy; · 70 unmodified lines
7171
7272 ///
7373 pub mod open_index {
74− use crate::bstr::BString;
75−
7674 /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()].
7775 #[derive(Debug, thiserror::Error)]
7876 #[allow(missing_docs)]
7977 pub enum Error {
80− #[error("Could not interpret value '{}' as 'index.threads'", .value)]
81− ConfigIndexThreads {
82− value: BString,
83− #[source]
84− err: gix_config::value::Error,
85− },
78+ #[error(transparent)]
79+ ConfigIndexThreads(#[from] crate::config::key::GenericErrorWithValue),
8680 #[error(transparent)]
8781 IndexFile(#[from] gix_index::file::init::Error),
8882 }
@@ -102,18 +96,14 @@ pub mod open_index { · 13 unmodified lines
10296
10397 ///
10498 pub mod excludes {
105− use std::path::PathBuf;
106−
10799 /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()].
108100 #[derive(Debug, thiserror::Error)]
109101 #[allow(missing_docs)]
110102 pub enum Error {
111− #[error("Could not read repository exclude.")]
112− Io(#[from] std::io::Error),
113103 #[error(transparent)]
114− EnvironmentPermission(#[from] gix_sec::permission::Error<PathBuf>),
115− #[error("The value for `core.excludesFile` could not be read from configuration")]
116− ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error),
104+ OpenIndex(#[from] crate::worktree::open_index::Error),
105+ #[error(transparent)]
106+ CreateCache(#[from] crate::config::exclude_stack::Error),
117107 }
118108
119109 impl<'repo> crate::Worktree<'repo> {
@@ -124,35 +114,46 @@ pub mod excludes { · 4 unmodified lines
124114 /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file.
125115 /// * `$GIT_DIR/info/exclude` if present.
126116 ///
127− /// `index` may be used to obtain `.gitignore` files directly from the index under certain conditions.
128− // TODO: test, provide higher-level interface that is much easier to use and doesn't panic when accessing entries
129− // by non-relative path.
130− // TODO: `index` might be so special (given the conditions we are talking about) that it's better obtained internally
131− // so the caller won't have to care.
132− pub fn excludes(
133− &self,
134− index: &gix_index::State,
135− overrides: Option<gix_ignore::Search>,
136− ) -> Result<gix_worktree::Cache, Error> {
137− let repo = self.parent;
138− let case = if repo.config.ignore_case {
139− gix_glob::pattern::Case::Fold
140− } else {
141− gix_glob::pattern::Case::Sensitive
142− };
143− let mut buf = Vec::with_capacity(512);
144− let excludes_file = match repo.config.excludes_file().transpose()? {
145− Some(user_path) => Some(user_path),
146− None => repo.config.xdg_config_path("ignore")?,
147− };
148− let state = gix_worktree::cache::State::IgnoreStack(gix_worktree::cache::state::Ignore::new(
149− overrides.unwrap_or_default(),
150− gix_ignore::Search::from_git_dir(repo.git_dir(), excludes_file, &mut buf)?,
151− None,
152− case,
153− ));
154− let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case);
155− Ok(gix_worktree::Cache::new(self.path, state, case, buf, attribute_list))
117+ /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use
118+ /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes.
119+ pub fn excludes(&self, overrides: Option<gix_ignore::Search>) -> Result<gix_worktree::Cache, Error> {
120+ let index = self.index()?;
121+ Ok(self.parent.excludes(
122+ &index,
123+ overrides,
124+ gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
125+ )?)
126+ }
127+ }
128+}
129+
130+///
131+pub mod attributes {
132+ /// The error returned by [`Worktree::attributes()`][crate::Worktree::attributes()].
133+ #[derive(Debug, thiserror::Error)]
134+ #[allow(missing_docs)]
135+ pub enum Error {
136+ #[error(transparent)]
137+ OpenIndex(#[from] crate::worktree::open_index::Error),
138+ #[error(transparent)]
139+ CreateCache(#[from] crate::attributes::Error),
140+ }
141+
142+ impl<'repo> crate::Worktree<'repo> {
143+ /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes.
144+ ///
145+ /// This takes into consideration all the usual repository configuration, namely:
146+ ///
147+ /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file.
148+ /// * `$GIT_DIR/info/exclude|attributes` if present.
149+ pub fn attributes(&self, overrides: Option<gix_ignore::Search>) -> Result<gix_worktree::Cache, Error> {
150+ let index = self.index()?;
151+ Ok(self.parent.attributes(
152+ &index,
153+ gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping,
154+ gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
155+ overrides,
156+ )?)
156157 }
157158 }
158159 }
gix/tests/config/tree.rs+24−0
@@ -351,6 +351,30 @@ mod core { · 350 unmodified lines
351351 }
352352 }
353353
354+mod index {
355+ use crate::config::tree::bcow;
356+ use gix::config::tree::{Index, Key};
357+
358+ #[test]
359+ fn threads() {
360+ for (value, expected) in [("false", 1), ("true", 0), ("0", 0), ("1", 1), ("2", 2), ("12", 12)] {
361+ assert_eq!(
362+ Index::THREADS.try_into_index_threads(bcow(value)).unwrap(),
363+ expected,
364+ "{value}"
365+ );
366+ assert!(Index::THREADS.validate(value.into()).is_ok());
367+ }
368+ assert_eq!(
369+ Index::THREADS
370+ .try_into_index_threads(bcow("nothing"))
371+ .unwrap_err()
372+ .to_string(),
373+ "The key \"index.threads=nothing\" was invalid"
374+ );
375+ }
376+}
377+
354378 mod extensions {
355379 use gix::config::tree::{Extensions, Key};
356380
gix/tests/remote/connect.rs+4−4
@@ -28,12 +28,12 @@ mod blocking_io { · 27 unmodified lines
2828 let _env = env_value.map(|value| gix_testtools::Env::new().set("GIT_PROTOCOL_FROM_USER", value));
2929 let repo = gix::open_opts(
3030 remote::repo("protocol_file_user").git_dir(),
31− gix::open::Options::isolated().permissions(gix::Permissions {
32− env: gix::permissions::Environment {
31+ gix::open::Options::isolated().permissions(gix::open::Permissions {
32+ env: gix::open::permissions::Environment {
3333 git_prefix: gix_sec::Permission::Allow,
34− ..gix::permissions::Environment::all()
34+ ..gix::open::permissions::Environment::all()
3535 },
36− ..gix::Permissions::isolated()
36+ ..gix::open::Permissions::isolated()
3737 }),
3838 )?;
3939 let remote = repo.find_remote("origin")?;
gix/tests/repository/config/identity.rs+14−11
@@ -29,14 +29,17 @@ fn author_and_committer_and_fallback() -> crate::Result { · 28 unmodified lines
2929 .set("GIT_CONFIG_VALUE_0", work_dir.join("c.config").display().to_string());
3030 let repo = gix::open_opts(
3131 repo.git_dir(),
32− repo.open_options().clone().with(trust).permissions(gix::Permissions {
33− env: gix::permissions::Environment {
34− xdg_config_home: Permission::Deny,
35− home: Permission::Deny,
36− ..gix::permissions::Environment::all()
37− },
38− ..Default::default()
39− }),
32+ repo.open_options()
33+ .clone()
34+ .with(trust)
35+ .permissions(gix::open::Permissions {
36+ env: gix::open::permissions::Environment {
37+ xdg_config_home: Permission::Deny,
38+ home: Permission::Deny,
39+ ..gix::open::permissions::Environment::all()
40+ },
41+ ..Default::default()
42+ }),
4043 )?;
4144
4245 assert_eq!(
@@ -148,11 +151,11 @@ fn author_from_different_config_sections() -> crate::Result { · 105 unmodified lines
148151 .clone()
149152 .config_overrides(None::<&str>)
150153 .with(gix_sec::Trust::Full)
151− .permissions(gix::Permissions {
152− env: gix::permissions::Environment {
154+ .permissions(gix::open::Permissions {
155+ env: gix::open::permissions::Environment {
153156 xdg_config_home: Permission::Deny,
154157 home: Permission::Deny,
155− ..gix::permissions::Environment::all()
158+ ..gix::open::permissions::Environment::all()
156159 },
157160 ..Default::default()
158161 }),
src/plumbing/main.rs+42−10
@@ -339,16 +339,6 @@ pub fn main() -> Result<()> { · 338 unmodified lines
339339 )
340340 },
341341 ),
342− free::index::Subcommands::Entries => prepare_and_run(
343− "index-entries",
344− verbose,
345− progress,
346− progress_keep_open,
347− None,
348− move |_progress, out, _err| {
349− core::index::entries(index_path, out, core::index::Options { object_hash, format })
350− },
351− ),
352342 free::index::Subcommands::Verify => prepare_and_run(
353343 "index-verify",
354344 auto_verbose,
@@ -865,6 +855,37 @@ pub fn main() -> Result<()> { · 510 unmodified lines
865855 ),
866856 },
867857 Subcommands::Index(cmd) => match cmd {
858+ index::Subcommands::Entries {
859+ no_attributes,
860+ attributes_from_index,
861+ statistics,
862+ } => prepare_and_run(
863+ "index-entries",
864+ verbose,
865+ progress,
866+ progress_keep_open,
867+ None,
868+ move |_progress, out, err| {
869+ core::repository::index::entries(
870+ repository(Mode::LenientWithGitInstallConfig)?,
871+ out,
872+ err,
873+ core::repository::index::entries::Options {
874+ format,
875+ attributes: if no_attributes {
876+ None
877+ } else {
878+ Some(if attributes_from_index {
879+ core::repository::index::entries::Attributes::Index
880+ } else {
881+ core::repository::index::entries::Attributes::WorktreeAndIndex
882+ })
883+ },
884+ statistics,
885+ },
886+ )
887+ },
888+ ),
868889 index::Subcommands::FromTree {
869890 force,
870891 index_output_path,
@@ -899,3 +920,14 @@ fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode { · 28 unmodified lines
899920 (false, false) => verify::Mode::HashCrc32,
900921 }
901922 }
923+
924+#[cfg(test)]
925+mod tests {
926+ use super::*;
927+
928+ #[test]
929+ fn clap() {
930+ use clap::CommandFactory;
931+ Args::command().debug_assert();
932+ }
933+}
src/plumbing/options/free.rs+1−3
@@ -55,8 +55,6 @@ pub mod index { · 54 unmodified lines
5555 pub enum Subcommands {
5656 /// Validate constraints and assumptions of an index along with its integrity.
5757 Verify,
58− /// Print all entries to standard output
59− Entries,
6058 /// Print information about the index structure
6159 Info {
6260 /// Do not extract specific extension information to gain only a superficial idea of the index's composition.
@@ -107,7 +105,7 @@ pub mod pack { · 44 unmodified lines
107105 /// Possible values are "none" and "tree-traversal". Default is "none".
108106 expansion: Option<core::pack::create::ObjectExpansion>,
109107
110− #[clap(long, default_value_t = 3, requires = "nondeterministic-count")]
108+ #[clap(long, default_value_t = 3, requires = "nondeterministic_count")]
111109 /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting
112110 /// to the globally configured threads.
113111 ///
src/plumbing/options/mod.rs+16−2
@@ -361,11 +361,11 @@ pub mod commit { · 360 unmodified lines
361361 /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
362362 Describe {
363363 /// Use annotated tag references only, not all tags.
364− #[clap(long, short = 't', conflicts_with("all-refs"))]
364+ #[clap(long, short = 't', conflicts_with("all_refs"))]
365365 annotated_tags: bool,
366366
367367 /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches.
368− #[clap(long, short = 'a', conflicts_with("annotated-tags"))]
368+ #[clap(long, short = 'a', conflicts_with("annotated_tags"))]
369369 all_refs: bool,
370370
371371 /// Only follow the first parent when traversing the commit graph.
@@ -471,6 +471,20 @@ pub mod index { · 99 unmodified lines
471471
472472 #[derive(Debug, clap::Subcommand)]
473473 pub enum Subcommands {
474+ /// Print all entries to standard output
475+ Entries {
476+ /// Do not visualize excluded entries or attributes per path.
477+ #[clap(long)]
478+ no_attributes: bool,
479+ /// Load attribute and ignore files from the index, don't look at the worktree.
480+ ///
481+ /// This is to see what IO for probing attribute/ignore files does to performance.
482+ #[clap(long, short = 'i', conflicts_with = "no_attributes")]
483+ attributes_from_index: bool,
484+ /// Print various statistics to stderr
485+ #[clap(long, short = 's')]
486+ statistics: bool,
487+ },
474488 /// Create an index from a tree-ish.
475489 #[clap(visible_alias = "read-tree")]
476490 FromTree {
src/shared.rs+5−1
@@ -113,7 +113,11 @@ pub mod pretty { · 112 unmodified lines
113113 crate::shared::init_env_logger();
114114
115115 match (verbose, progress) {
116− (false, false) => run(progress::DoOrDiscard::from(None), &mut stdout(), &mut stderr()),
116+ (false, false) => {
117+ let stdout = stdout();
118+ let mut stdout_lock = stdout.lock();
119+ run(progress::DoOrDiscard::from(None), &mut stdout_lock, &mut stderr())
120+ }
117121 (true, false) => {
118122 use crate::shared::{self, STANDARD_RANGE};
119123 let progress = shared::progress_tree();