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
83 * [gix-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-command)83 * [gix-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-command)
84 * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt)84 * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt)
85 * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec)85 * [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)
89 * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs)86 * [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)
90 * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree)89 * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree)
91 * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap)90 * [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)_
92 * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date)93 * [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)_
95 * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive)94 * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive)
95* **idea** _(just a name placeholder)_
96 * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note)96 * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note)
97 * [gix-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fetchhead)97 * [gix-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fetchhead)
98 * [gix-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-filter)98 * [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
690 * [x] proper handling of worktree related refs690 * [x] proper handling of worktree related refs
691 * [ ] create, move, remove, and repair691 * [ ] create, move, remove, and repair
692 * [x] access exclude information692 * [x] access exclude information
693 * [ ] access attribute information693 * [x] access attribute information
694 * [x] respect `core.worktree` configuration694 * [x] respect `core.worktree` configuration
695 - **deviation**695 - **deviation**
696 * The delicate interplay between `GIT_COMMON_DIR` and `GIT_WORK_TREE` isn't implemented.696 * 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 @@
1use std::path::Path;
2
3use crate::index::{parse_file, Options};
4
5pub 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")]
35pub(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
71pub(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
5 pub format: crate::OutputFormat,5 pub format: crate::OutputFormat,
6}6}
7 7
8mod entries;
9pub use entries::entries;
10
11pub mod information;8pub mod information;
12 9
13fn parse_file(index_path: impl AsRef<Path>, object_hash: gix::hash::Kind) -> anyhow::Result<gix::index::File> {10fn 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 @@
1use std::io;1use std::io;
2 2
3use anyhow::{bail, Context};3use anyhow::bail;
4use gix::prelude::FindExt;4use gix::prelude::FindExt;
5 5
6use crate::OutputFormat;6use crate::OutputFormat;
@@ -31,11 +31,12 @@ pub fn query( · 24 unmodified lines
31 bail!("JSON output isn't implemented yet");31 bail!("JSON output isn't implemented yet");
32 }32 }
33 33
34 let worktree = repo34 let index = repo.index()?;
35 .worktree()35 let mut cache = repo.excludes(
36 .with_context(|| "Cannot check excludes without a current worktree")?;36 &index,
37 let index = worktree.index()?;37 Some(gix::ignore::Search::from_overrides(overrides)),
38 let mut cache = worktree.excludes(&index, Some(gix::ignore::Search::from_overrides(overrides)))?;38 Default::default(),
39 )?;
39 40
40 let prefix = repo.prefix().expect("worktree - we have an index by now")?;41 let prefix = repo.prefix().expect("worktree - we have an index by now")?;
41 42
gitoxide-core/src/repository/index/entries.rs+228−0
@@ -0,0 +1,228 @@
1#[derive(Debug)]
2pub 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)]
10pub 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
17pub(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
34 34
35 Ok(())35 Ok(())
36}36}
37
38pub mod entries;
39pub use entries::function::entries;
gix-bitmap/src/ewah.rs+4−0
@@ -1,13 +1,17 @@
1use std::convert::TryInto;1use std::convert::TryInto;
2 2
3///
3pub mod decode {4pub mod decode {
5 /// The error returned by [`decode()`][super::decode()].
4 #[derive(Debug, thiserror::Error)]6 #[derive(Debug, thiserror::Error)]
7 #[allow(missing_docs)]
5 pub enum Error {8 pub enum Error {
6 #[error("{}", message)]9 #[error("{}", message)]
7 Corrupt { message: &'static str },10 Corrupt { message: &'static str },
8 }11 }
9}12}
10 13
14/// Decode `data` as EWAH bitmap.
11pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> {15pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> {
12 use self::decode::Error;16 use self::decode::Error;
13 use crate::decode;17 use crate::decode;
gix-bitmap/src/lib.rs+1−2
@@ -1,8 +1,7 @@
1//! An implementation of the shared parts of git bitmaps used in `gix-pack`, `gix-index` and `gix-worktree`.1//! An implementation of the shared parts of git bitmaps used in `gix-pack`, `gix-index` and `gix-worktree`.
2//!2//!
3//! Note that many tests are performed indirectly by tests in the aforementioned consumer crates.3//! Note that many tests are performed indirectly by tests in the aforementioned consumer crates.
4#![deny(rust_2018_idioms, unsafe_code)]4#![deny(rust_2018_idioms, unsafe_code, missing_docs)]
5#![allow(missing_docs)]
6 5
7/// Bitmap utilities for the advanced word-aligned hybrid bitmap6/// Bitmap utilities for the advanced word-aligned hybrid bitmap
8pub mod ewah;7pub mod ewah;
gix-fs/src/lib.rs+2−1
@@ -1,5 +1,5 @@
1//! A crate with file-system specific utilities.1//! A crate with file-system specific utilities.
2#![deny(rust_2018_idioms)]2#![deny(rust_2018_idioms, missing_docs)]
3#![forbid(unsafe_code)]3#![forbid(unsafe_code)]
4 4
5/// Common knowledge about the worktree that is needed across most interactions with the work tree5/// 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
51 current_is_directory: bool,51 current_is_directory: bool,
52}52}
53 53
54///
54pub mod stack;55pub mod stack;
gix-fs/src/symlink.rs+10−0
@@ -1,16 +1,21 @@
1use std::{io, io::ErrorKind::AlreadyExists, path::Path};1use std::{io, io::ErrorKind::AlreadyExists, path::Path};
2 2
3#[cfg(not(windows))]3#[cfg(not(windows))]
4/// Create a new symlink at `link` which points to `original`.
4pub fn create(original: &Path, link: &Path) -> io::Result<()> {5pub fn create(original: &Path, link: &Path) -> io::Result<()> {
5 std::os::unix::fs::symlink(original, link)6 std::os::unix::fs::symlink(original, link)
6}7}
7 8
8#[cfg(not(windows))]9#[cfg(not(windows))]
10/// Remove a symlink.
11///
12/// Note that on only on windows this is special.
9pub fn remove(path: &Path) -> io::Result<()> {13pub fn remove(path: &Path) -> io::Result<()> {
10 std::fs::remove_file(path)14 std::fs::remove_file(path)
11}15}
12 16
13// TODO: use the `symlink` crate once it can delete directory symlinks17// TODO: use the `symlink` crate once it can delete directory symlinks
18/// Remove a symlink.
14#[cfg(windows)]19#[cfg(windows)]
15pub fn remove(path: &Path) -> io::Result<()> {20pub fn remove(path: &Path) -> io::Result<()> {
16 if let Ok(meta) = std::fs::metadata(path) {21 if let Ok(meta) = std::fs::metadata(path) {
@@ -25,6 +30,7 @@ pub fn remove(path: &Path) -> io::Result<()> { · 8 unmodified lines
25}30}
26 31
27#[cfg(windows)]32#[cfg(windows)]
33/// Create a new symlink at `link` which points to `original`.
28pub fn create(original: &Path, link: &Path) -> io::Result<()> {34pub fn create(original: &Path, link: &Path) -> io::Result<()> {
29 use std::os::windows::fs::{symlink_dir, symlink_file};35 use std::os::windows::fs::{symlink_dir, symlink_file};
30 // TODO: figure out if links to links count as files or whatever they point at36 // 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
36}42}
37 43
38#[cfg(not(windows))]44#[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.
39pub fn is_collision_error(err: &std::io::Error) -> bool {47pub fn is_collision_error(err: &std::io::Error) -> bool {
40 // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively48 // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively
41 err.kind() == AlreadyExists49 err.kind() == AlreadyExists
@@ -45,6 +53,8 @@ pub fn is_collision_error(err: &std::io::Error) -> bool { · 3 unmodified lines
45}53}
46 54
47#[cfg(windows)]55#[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.
48pub fn is_collision_error(err: &std::io::Error) -> bool {58pub fn is_collision_error(err: &std::io::Error) -> bool {
49 err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied59 err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied
50}60}
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
85/// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`.85/// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`.
86pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into<Cow<'a, BStr>>, path: impl Into<&'b BStr>) -> Cow<'a, BStr> {86pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into<Cow<'a, BStr>>, path: impl Into<&'b BStr>) -> Cow<'a, BStr> {
87 let mut base = base.into();87 let mut base = base.into();
88 if base.last() != Some(&b'/') {88 if !base.is_empty() && base.last() != Some(&b'/') {
89 base.to_mut().push(b'/');89 base.to_mut().push(b'/');
90 }90 }
91 base.to_mut().extend_from_slice(path.into());91 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
56 assert_eq!(join_bstr_unix_pathsep(b("base/"), ""), b("base/"));56 assert_eq!(join_bstr_unix_pathsep(b("base/"), ""), b("base/"));
57 }57 }
58 #[test]58 #[test]
59 fn empty_base_creates_absolute_paths() {59 fn empty_base_leaves_everything_untouched() {
60 assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("/"));60 assert_eq!(join_bstr_unix_pathsep(b(""), ""), b(""));
61 assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("/hi"));61 assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("hi"));
62 assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("//hi"));62 assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("/hi"));
63 }63 }
64}64}
gix-utils/src/lib.rs+1−1
@@ -1,7 +1,7 @@
1//! A crate with utilities that don't need feature toggles.1//! A crate with utilities that don't need feature toggles.
2//!2//!
3//! If they would need feature toggles, they should be in `gix-features` instead.3//! 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)]
5#![forbid(unsafe_code)]5#![forbid(unsafe_code)]
6 6
7///7///
gix-worktree/src/cache/delegate.rs+54−28
@@ -1,12 +1,30 @@
1use crate::cache::State;1use crate::cache::State;
2use crate::PathIdMapping;2use crate::PathIdMapping;
3 3
4pub 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))]
7pub 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
20pub(crate) struct StackDelegate<'a, Find> {
5 pub state: &'a mut State,21 pub state: &'a mut State,
6 pub buf: &'a mut Vec<u8>,22 pub buf: &'a mut Vec<u8>,
7 pub is_dir: bool,23 pub is_dir: bool,
8 pub id_mappings: &'a Vec<PathIdMapping>,24 pub id_mappings: &'a Vec<PathIdMapping>,
9 pub find: Find,25 pub find: Find,
26 pub case: gix_glob::pattern::Case,
27 pub statistics: &'a mut super::Statistics,
10}28}
11 29
12impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find>30impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find>
@@ -15,72 +33,86 @@ where · 2 unmodified lines
15 E: std::error::Error + Send + Sync + 'static,33 E: std::error::Error + Send + Sync + 'static,
16{34{
17 fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> {35 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 }
18 match &mut self.state {49 match &mut self.state {
19 State::CreateDirectoryAndAttributesStack { attributes, .. } => {50 State::CreateDirectoryAndAttributesStack { attributes, .. } => {
20 attributes.push_directory(51 attributes.push_directory(
21 stack.root(),52 stack.root(),
22 stack.current(),53 stack.current(),
54 rela_dir,
23 self.buf,55 self.buf,
24 self.id_mappings,56 self.id_mappings,
25 &mut self.find,57 &mut self.find,
58 &mut self.statistics.attributes,
26 )?;59 )?;
27 }60 }
28 State::AttributesAndIgnoreStack { ignore, attributes } => {61 State::AttributesAndIgnoreStack { ignore, attributes } => {
29 attributes.push_directory(62 attributes.push_directory(
30 stack.root(),63 stack.root(),
31 stack.current(),64 stack.current(),
65 rela_dir,
32 self.buf,66 self.buf,
33 self.id_mappings,67 self.id_mappings,
34 &mut self.find,68 &mut self.find,
69 &mut self.statistics.attributes,
35 )?;70 )?;
36 ignore.push_directory(71 ignore.push_directory(
37 stack.root(),72 stack.root(),
38 stack.current(),73 stack.current(),
74 rela_dir,
39 self.buf,75 self.buf,
40 self.id_mappings,76 self.id_mappings,
41 &mut self.find,77 &mut self.find,
78 self.case,
79 &mut self.statistics.ignore,
42 )?80 )?
43 }81 }
44 State::IgnoreStack(ignore) => ignore.push_directory(82 State::IgnoreStack(ignore) => ignore.push_directory(
45 stack.root(),83 stack.root(),
46 stack.current(),84 stack.current(),
85 rela_dir,
47 self.buf,86 self.buf,
48 self.id_mappings,87 self.id_mappings,
49 &mut self.find,88 &mut self.find,
89 self.case,
90 &mut self.statistics.ignore,
50 )?,91 )?,
51 }92 }
52 Ok(())93 Ok(())
53 }94 }
54 95
55 fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> {96 fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> {
97 self.statistics.delegate.push_element += 1;
56 match &mut self.state {98 match &mut self.state {
57 State::CreateDirectoryAndAttributesStack {99 State::CreateDirectoryAndAttributesStack {
58 #[cfg(debug_assertions)]
59 test_mkdir_calls,
60 unlink_on_collision,100 unlink_on_collision,
61 attributes: _,101 attributes: _,
62 } => {102 } => create_leading_directory(
63 #[cfg(debug_assertions)]103 is_last_component,
64 {104 stack,
65 create_leading_directory(105 self.is_dir,
66 is_last_component,106 &mut self.statistics.delegate.num_mkdir_calls,
67 stack,107 *unlink_on_collision,
68 self.is_dir,108 )?,
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 }
78 State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {}109 State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {}
79 }110 }
80 Ok(())111 Ok(())
81 }112 }
82 113
83 fn pop_directory(&mut self) {114 fn pop_directory(&mut self) {
115 self.statistics.delegate.pop_directory += 1;
84 match &mut self.state {116 match &mut self.state {
85 State::CreateDirectoryAndAttributesStack { attributes, .. } => {117 State::CreateDirectoryAndAttributesStack { attributes, .. } => {
86 attributes.pop_directory();118 attributes.pop_directory();
@@ -100,16 +132,13 @@ fn create_leading_directory( · 13 unmodified lines
100 is_last_component: bool,132 is_last_component: bool,
101 stack: &gix_fs::Stack,133 stack: &gix_fs::Stack,
102 is_dir: bool,134 is_dir: bool,
103 #[cfg(debug_assertions)] mkdir_calls: &mut usize,135 mkdir_calls: &mut usize,
104 unlink_on_collision: bool,136 unlink_on_collision: bool,
105) -> std::io::Result<()> {137) -> std::io::Result<()> {
106 if is_last_component && !is_dir {138 if is_last_component && !is_dir {
107 return Ok(());139 return Ok(());
108 }140 }
109 #[cfg(debug_assertions)]141 *mkdir_calls += 1;
110 {
111 *mkdir_calls += 1;
112 }
113 match std::fs::create_dir(stack.current()) {142 match std::fs::create_dir(stack.current()) {
114 Ok(()) => Ok(()),143 Ok(()) => Ok(()),
115 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {144 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
@@ -122,10 +151,7 @@ fn create_leading_directory( · 6 unmodified lines
122 } else {151 } else {
123 std::fs::remove_file(stack.current())?;152 std::fs::remove_file(stack.current())?;
124 }153 }
125 #[cfg(debug_assertions)]154 *mkdir_calls += 1;
126 {
127 *mkdir_calls += 1;
128 }
129 std::fs::create_dir(stack.current())155 std::fs::create_dir(stack.current())
130 } else {156 } else {
131 Err(err)157 Err(err)
gix-worktree/src/cache/mod.rs+38−35
@@ -7,16 +7,26 @@ use gix_hash::oid; · 6 unmodified lines
7use super::Cache;7use super::Cache;
8use crate::PathIdMapping;8use crate::PathIdMapping;
9 9
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))]
13pub 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
10#[derive(Clone)]24#[derive(Clone)]
11pub enum State {25pub enum State {
12 /// Useful for checkout where directories need creation, but we need to access attributes as well.26 /// Useful for checkout where directories need creation, but we need to access attributes as well.
13 CreateDirectoryAndAttributesStack {27 CreateDirectoryAndAttributesStack {
14 /// If there is a symlink or a file in our path, try to unlink it before creating the directory.28 /// If there is a symlink or a file in our path, try to unlink it before creating the directory.
15 unlink_on_collision: bool,29 unlink_on_collision: bool,
16
17 /// just for testing
18 #[cfg(debug_assertions)]
19 test_mkdir_calls: usize,
20 /// State to handle attribute information30 /// State to handle attribute information
21 attributes: state::Attributes,31 attributes: state::Attributes,
22 },32 },
@@ -31,35 +41,6 @@ pub enum State { · 8 unmodified lines
31 IgnoreStack(state::Ignore),41 IgnoreStack(state::Ignore),
32}42}
33 43
34#[cfg(debug_assertions)]
35/// debug builds only for use in tests.
36impl 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
63#[must_use]44#[must_use]
64pub struct Platform<'a> {45pub struct Platform<'a> {
65 parent: &'a Cache,46 parent: &'a Cache,
@@ -87,11 +68,12 @@ impl Cache { · 21 unmodified lines
87 case,68 case,
88 buf,69 buf,
89 id_mappings,70 id_mappings,
71 statistics: Statistics::default(),
90 }72 }
91 }73 }
92}74}
93 75
94/// Mutable Access76/// Entry points for attribute query
95impl Cache {77impl Cache {
96 /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no78 /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no
97 /// symlinks are in that path.79 /// symlinks are in that path.
@@ -110,12 +92,15 @@ impl Cache { · 12 unmodified lines
110 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,92 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
111 E: std::error::Error + Send + Sync + 'static,93 E: std::error::Error + Send + Sync + 'static,
112 {94 {
95 self.statistics.platforms += 1;
113 let mut delegate = StackDelegate {96 let mut delegate = StackDelegate {
114 state: &mut self.state,97 state: &mut self.state,
115 buf: &mut self.buf,98 buf: &mut self.buf,
116 is_dir: is_dir.unwrap_or(false),99 is_dir: is_dir.unwrap_or(false),
117 id_mappings: &self.id_mappings,100 id_mappings: &self.id_mappings,
118 find,101 find,
102 case: self.case,
103 statistics: &mut self.statistics,
119 };104 };
120 self.stack.make_relative_path_current(relative, &mut delegate)?;105 self.stack.make_relative_path_current(relative, &mut delegate)?;
121 Ok(Platform { parent: self, is_dir })106 Ok(Platform { parent: self, is_dir })
@@ -152,8 +137,25 @@ impl Cache { · 30 unmodified lines
152 }137 }
153}138}
154 139
140/// Mutation
141impl 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
155/// Access153/// Access
156impl Cache {154impl Cache {
155 /// Return the statistics we gathered thus far.
156 pub fn statistics(&self) -> &Statistics {
157 &self.statistics
158 }
157 /// Return the state for introspection.159 /// Return the state for introspection.
158 pub fn state(&self) -> &State {160 pub fn state(&self) -> &State {
159 &self.state161 &self.state
@@ -167,7 +169,8 @@ impl Cache { · 7 unmodified lines
167 }169 }
168}170}
169 171
170mod delegate;172///
173pub mod delegate;
171use delegate::StackDelegate;174use delegate::StackDelegate;
172 175
173mod platform;176mod platform;
gix-worktree/src/cache/state/attributes.rs+25−25
@@ -6,6 +6,18 @@ use gix_glob::pattern::Case; · 5 unmodified lines
6use crate::cache::state::{AttributeMatchGroup, Attributes};6use crate::cache::state::{AttributeMatchGroup, Attributes};
7use bstr::{BStr, ByteSlice};7use bstr::{BStr, ByteSlice};
8 8
9/// Various aggregate numbers related [`Attributes`].
10#[derive(Default, Clone, Copy, Debug)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub 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
9/// Decide where to read `.gitattributes` files from.21/// Decide where to read `.gitattributes` files from.
10#[derive(Default, Debug, Clone, Copy)]22#[derive(Default, Debug, Clone, Copy)]
11pub enum Source {23pub enum Source {
@@ -42,7 +54,6 @@ impl Attributes { · 30 unmodified lines
42 pub fn new(54 pub fn new(
43 globals: AttributeMatchGroup,55 globals: AttributeMatchGroup,
44 info_attributes: Option<PathBuf>,56 info_attributes: Option<PathBuf>,
45 case: Case,
46 source: Source,57 source: Source,
47 collection: gix_attributes::search::MetadataCollection,58 collection: gix_attributes::search::MetadataCollection,
48 ) -> Self {59 ) -> Self {
@@ -50,7 +61,6 @@ impl Attributes { · 1 unmodified line
50 globals,61 globals,
51 stack: Default::default(),62 stack: Default::default(),
52 info_attributes,63 info_attributes,
53 case,
54 source,64 source,
55 collection,65 collection,
56 }66 }
@@ -62,31 +72,21 @@ impl Attributes { · 5 unmodified lines
62 self.stack.pop_pattern_list().expect("something to pop");72 self.stack.pop_pattern_list().expect("something to pop");
63 }73 }
64 74
75 #[allow(clippy::too_many_arguments)]
65 pub(crate) fn push_directory<Find, E>(76 pub(crate) fn push_directory<Find, E>(
66 &mut self,77 &mut self,
67 root: &Path,78 root: &Path,
68 dir: &Path,79 dir: &Path,
80 rela_dir: &BStr,
69 buf: &mut Vec<u8>,81 buf: &mut Vec<u8>,
70 id_mappings: &[PathIdMapping],82 id_mappings: &[PathIdMapping],
71 mut find: Find,83 mut find: Find,
84 stats: &mut Statistics,
72 ) -> std::io::Result<()>85 ) -> std::io::Result<()>
73 where86 where
74 Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>,87 Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>,
75 E: std::error::Error + Send + Sync + 'static,88 E: std::error::Error + Send + Sync + 'static,
76 {89 {
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
90 let attr_path_relative =90 let attr_path_relative =
91 gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes"));91 gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes"));
92 let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref()));92 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
103 self.stack103 self.stack
104 .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection);104 .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection);
105 added = true;105 added = true;
106 stats.patterns_buffers += 1;
106 }107 }
107 if !added && matches!(self.source, Source::IdMappingThenWorktree) {108 if !added && matches!(self.source, Source::IdMappingThenWorktree) {
108 added = self.stack.add_patterns_file(109 added = self.stack.add_patterns_file(
@@ -112,6 +113,8 @@ impl Attributes { · 3 unmodified lines
112 buf,113 buf,
113 &mut self.collection,114 &mut self.collection,
114 )?;115 )?;
116 stats.pattern_files += usize::from(added);
117 stats.tried_pattern_files += 1;
115 }118 }
116 }119 }
117 Source::WorktreeThenIdMapping => {120 Source::WorktreeThenIdMapping => {
@@ -122,6 +125,8 @@ impl Attributes { · 4 unmodified lines
122 buf,125 buf,
123 &mut self.collection,126 &mut self.collection,
124 )?;127 )?;
128 stats.pattern_files += usize::from(added);
129 stats.tried_pattern_files += 1;
125 if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) {130 if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) {
126 let blob = find(&id_mappings[idx].1, buf)131 let blob = find(&id_mappings[idx].1, buf)
127 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;132 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
@@ -129,6 +134,7 @@ impl Attributes { · 1 unmodified line
129 self.stack134 self.stack
130 .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection);135 .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection);
131 added = true;136 added = true;
137 stats.patterns_buffers += 1;
132 }138 }
133 }139 }
134 }140 }
@@ -142,8 +148,11 @@ impl Attributes { · 7 unmodified lines
142 // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is148 // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is
143 // by nature never popped, and follows the root, as global.149 // by nature never popped, and follows the root, as global.
144 if let Some(info_attr) = self.info_attributes.take() {150 if let Some(info_attr) = self.info_attributes.take() {
145 self.stack151 let added = self
152 .stack
146 .add_patterns_file(info_attr, true, None, buf, &mut self.collection)?;153 .add_patterns_file(info_attr, true, None, buf, &mut self.collection)?;
154 stats.pattern_files += usize::from(added);
155 stats.tried_pattern_files += 1;
147 }156 }
148 157
149 Ok(())158 Ok(())
@@ -168,15 +177,6 @@ impl Attributes { · 18 unmodified lines
168 }177 }
169}178}
170 179
171/// Builder
172impl 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
180/// Attribute matching specific methods180/// Attribute matching specific methods
181impl Cache {181impl Cache {
182 /// Creates a new container to store match outcomes for all attribute matches.182 /// 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 @@
1use std::path::Path;1use std::path::Path;
2 2
3use crate::cache::state::Ignore;
3use crate::{cache::state::IgnoreMatchGroup, PathIdMapping};4use crate::{cache::state::IgnoreMatchGroup, PathIdMapping};
4use bstr::{BStr, BString, ByteSlice};5use bstr::{BStr, ByteSlice};
5use gix_glob::pattern::Case;6use gix_glob::pattern::Case;
6 7
7/// State related to the exclusion of files.8/// Decide where to read `.gitignore` files from.
8#[derive(Default, Clone)]9#[derive(Default, Debug, Clone, Copy)]
9#[allow(unused)]10pub enum Source {
10pub struct Ignore {11 /// Retrieve ignore files from id mappings, see
11 /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to12 /// [State::id_mappings_from_index()][crate::cache::State::id_mappings_from_index()].
12 /// be consulted.13 ///
13 overrides: IgnoreMatchGroup,14 /// These mappings are typically produced from an index.
14 /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed.15 /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index.
15 stack: IgnoreMatchGroup,16 ///
16 /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last.17 /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts
17 globals: IgnoreMatchGroup,18 /// of the history which aren't checked out.
18 /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the19 IdMapping,
19 /// currently set directory had a pattern matched. Note that this one could be negated.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.
20 /// (index into match groups, index into list of pattern lists, index into pattern list)21 #[default]
21 matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>,22 WorktreeThenIdMappingIfNotSkipped,
22 /// The name of the file to look for in directories.23}
23 pub(crate) exclude_file_name_for_directories: BString,24
24 /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine25/// Various aggregate numbers related [`Ignore`].
25 /// to know if an entire path can be ignored as a parent directory is ignored.26#[derive(Default, Clone, Copy, Debug)]
26 pub(crate) case: Case,27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub 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,
27}35}
28 36
29impl Ignore {37impl Ignore {
38 /// Configure gitignore file matching by providing the immutable groups being `overrides` and `globals`, while letting the directory
39 /// stack be dynamic.
40 ///
30 /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory41 /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory
31 /// ignore files within the repository, defaults to`.gitignore`.42 /// 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
35 pub fn new(43 pub fn new(
36 overrides: IgnoreMatchGroup,44 overrides: IgnoreMatchGroup,
37 globals: IgnoreMatchGroup,45 globals: IgnoreMatchGroup,
38 exclude_file_name_for_directories: Option<&BStr>,46 exclude_file_name_for_directories: Option<&BStr>,
39 case: Case,47 source: Source,
40 ) -> Self {48 ) -> Self {
41 Ignore {49 Ignore {
42 case,
43 overrides,50 overrides,
44 globals,51 globals,
45 stack: Default::default(),52 stack: Default::default(),
@@ -47,6 +54,7 @@ impl Ignore { · 1 unmodified line
47 exclude_file_name_for_directories: exclude_file_name_for_directories54 exclude_file_name_for_directories: exclude_file_name_for_directories
48 .map(ToOwned::to_owned)55 .map(ToOwned::to_owned)
49 .unwrap_or_else(|| ".gitignore".into()),56 .unwrap_or_else(|| ".gitignore".into()),
57 source,
50 }58 }
51 }59 }
52}60}
@@ -135,55 +143,71 @@ impl Ignore { · 82 unmodified lines
135 })143 })
136 }144 }
137 145
146 #[allow(clippy::too_many_arguments)]
138 pub(crate) fn push_directory<Find, E>(147 pub(crate) fn push_directory<Find, E>(
139 &mut self,148 &mut self,
140 root: &Path,149 root: &Path,
141 dir: &Path,150 dir: &Path,
151 rela_dir: &BStr,
142 buf: &mut Vec<u8>,152 buf: &mut Vec<u8>,
143 id_mappings: &[PathIdMapping],153 id_mappings: &[PathIdMapping],
144 mut find: Find,154 mut find: Find,
155 case: Case,
156 stats: &mut Statistics,
145 ) -> std::io::Result<()>157 ) -> std::io::Result<()>
146 where158 where
147 Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>,159 Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>,
148 E: std::error::Error + Send + Sync + 'static,160 E: std::error::Error + Send + Sync + 'static,
149 {161 {
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 }
162 self.matched_directory_patterns_stack162 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));
164 164
165 let ignore_path_relative =165 let ignore_path_relative =
166 gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore"));166 gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore"));
167 let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref()));167 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();168 match self.source {
169 if !gix_glob::search::add_patterns_file(169 Source::IdMapping => {
170 &mut self.stack.patterns,170 match ignore_file_in_index {
171 dir.join(".gitignore"),171 Ok(idx) => {
172 follow_symlinks,172 let ignore_blob = find(&id_mappings[idx].1, buf)
173 Some(root),173 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
174 buf,174 let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
175 )? {175 self.stack
176 match ignore_file_in_index {176 .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")));
177 Ok(idx) => {177 stats.patterns_buffers += 1;
178 let ignore_blob = find(&id_mappings[idx].1, buf)178 }
179 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;179 Err(_) => {
180 let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());180 // Need one stack level per component so push and pop matches.
181 self.stack181 self.stack.patterns.push(Default::default())
182 .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")));182 }
183 }183 }
184 Err(_) => {184 }
185 // Need one stack level per component so push and pop matches.185 Source::WorktreeThenIdMappingIfNotSkipped => {
186 self.stack.patterns.push(Default::default())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 }
187 }211 }
188 }212 }
189 }213 }
gix-worktree/src/cache/state/mod.rs+36−11
@@ -1,6 +1,6 @@
1use std::path::PathBuf;1use std::path::PathBuf;
2 2
3use bstr::ByteSlice;3use bstr::{BString, ByteSlice};
4use gix_glob::pattern::Case;4use gix_glob::pattern::Case;
5 5
6use crate::{cache::State, PathIdMapping};6use crate::{cache::State, PathIdMapping};
@@ -23,17 +23,36 @@ pub struct Attributes { · 16 unmodified lines
23 info_attributes: Option<PathBuf>,23 info_attributes: Option<PathBuf>,
24 /// A lookup table to accelerate searches.24 /// A lookup table to accelerate searches.
25 collection: gix_attributes::search::MetadataCollection,25 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,
29 /// Where to read `.gitattributes` data from.26 /// Where to read `.gitattributes` data from.
30 source: attributes::Source,27 source: attributes::Source,
31}28}
32 29
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)]
34pub 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
33///52///
34pub mod attributes;53pub mod attributes;
35mod ignore;54///
36pub use ignore::Ignore;55pub mod ignore;
37 56
38/// Initialization57/// Initialization
39impl State {58impl State {
@@ -41,8 +60,6 @@ impl State { · 1 unmodified line
41 pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self {60 pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self {
42 State::CreateDirectoryAndAttributesStack {61 State::CreateDirectoryAndAttributesStack {
43 unlink_on_collision,62 unlink_on_collision,
44 #[cfg(debug_assertions)]
45 test_mkdir_calls: 0,
46 attributes,63 attributes,
47 }64 }
48 }65 }
@@ -76,6 +93,7 @@ impl State { · 27 unmodified lines
76 &self,93 &self,
77 index: &gix_index::State,94 index: &gix_index::State,
78 paths: &gix_index::PathStorageRef,95 paths: &gix_index::PathStorageRef,
96 ignore_source: ignore::Source,
79 case: Case,97 case: Case,
80 ) -> Vec<PathIdMapping> {98 ) -> Vec<PathIdMapping> {
81 let a1_backing;99 let a1_backing;
@@ -118,9 +136,16 @@ impl State { · 36 unmodified lines
118 }136 }
119 .then_some(t.1)137 .then_some(t.1)
120 })?;138 })?;
121 // See https://github.com/git/git/blob/master/dir.c#L912:L912139 if is_ignore {
122 if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {140 match ignore_source {
123 return None;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 };
124 }149 }
125 Some((path.to_owned(), entry.id))150 Some((path.to_owned(), entry.id))
126 } else {151 } else {
gix-worktree/src/checkout/function.rs+2−2
@@ -57,8 +57,8 @@ where · 56 unmodified lines
57 None,57 None,
58 );58 );
59 59
60 let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone().with_case(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, case);61 let attribute_files = state.id_mappings_from_index(index, paths, Default::default(), case);
62 let mut ctx = chunk::Context {62 let mut ctx = chunk::Context {
63 buf: Vec::new(),63 buf: Vec::new(),
64 path_cache: Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files),64 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
45 case: gix_glob::pattern::Case,45 case: gix_glob::pattern::Case,
46 /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions.46 /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions.
47 id_mappings: Vec<PathIdMapping>,47 id_mappings: Vec<PathIdMapping>,
48 statistics: cache::Statistics,
48}49}
49 50
50pub(crate) type PathIdMapping = (BString, gix_hash::ObjectId);51pub(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 @@
1version https://git-lfs.github.com/spec/v1
2oid sha256:38df6900cdb0ad620e158a3cc19325053658b9cfcb8987779af634813d957f6c
3size 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
111other-dir-with-ignore/other-sub-level-dir-anywhere/111other-dir-with-ignore/other-sub-level-dir-anywhere/
112dir-with-ignore/negated112dir-with-ignore/negated
113dir-with-ignore/negated-dir/hello113dir-with-ignore/negated-dir/hello
114User-file-ANYWHERE
115User-Dir-ANYWHERE
114a/b/C116a/b/C
115a/B/c117a/B/c
116A/B/C118A/B/C
gix-worktree/tests/worktree/cache/attributes.rs+0−1
@@ -24,7 +24,6 @@ fn baseline() -> crate::Result { · 23 unmodified lines
24 state::Attributes::new(24 state::Attributes::new(
25 gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?,25 gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?,
26 Some(git_dir.join("info").join("attributes")),26 Some(git_dir.join("info").join("attributes")),
27 case,
28 gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping,27 gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping,
29 collection,28 collection,
30 ),29 ),
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
17 Vec::new(),17 Vec::new(),
18 Default::default(),18 Default::default(),
19 );19 );
20 assert_eq!(cache.num_mkdir_calls(), 0);20 assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0);
21 21
22 let path = cache.at_path("hello", Some(false), panic_on_find)?.path();22 let path = cache.at_path("hello", Some(false), panic_on_find)?.path();
23 assert!(!path.parent().unwrap().exists(), "prefix itself is never created");23 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);
25 Ok(())25 Ok(())
26}26}
27 27
@@ -43,7 +43,7 @@ fn directory_paths_are_created_in_full() { · 15 unmodified lines
43 assert!(path.parent().unwrap().is_dir(), "dir exists");43 assert!(path.parent().unwrap().is_dir(), "dir exists");
44 }44 }
45 45
46 assert_eq!(cache.num_mkdir_calls(), 3);46 assert_eq!(cache.statistics().delegate.num_mkdir_calls, 3);
47}47}
48 48
49#[test]49#[test]
@@ -54,7 +54,7 @@ fn existing_directories_are_fine() -> crate::Result { · 4 unmodified lines
54 let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path();54 let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path();
55 assert!(path.parent().unwrap().is_dir(), "directory is still present");55 assert!(path.parent().unwrap().is_dir(), "directory is still present");
56 assert!(!path.exists(), "it won't create the file");56 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);
58 Ok(())58 Ok(())
59}59}
60 60
@@ -67,7 +67,12 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R · 6 unmodified lines
67 std::fs::write(tmp.path().join("file-in-dir"), &[])?;67 std::fs::write(tmp.path().join("file-in-dir"), &[])?;
68 68
69 for dirname in &["file-in-dir", "link-to-dir"] {69 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 }
71 let relative_path = format!("{}/file", dirname);76 let relative_path = format!("{}/file", dirname);
72 assert_eq!(77 assert_eq!(
73 cache78 cache
@@ -78,20 +83,25 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R · 4 unmodified lines
78 );83 );
79 }84 }
80 assert_eq!(85 assert_eq!(
81 cache.num_mkdir_calls(),86 cache.statistics().delegate.num_mkdir_calls,
82 2,87 2,
83 "it tries to create each directory once, but it's a file"88 "it tries to create each directory once, but it's a file"
84 );89 );
85 cache.reset_mkdir_calls();90 cache.take_statistics();
86 for dirname in &["link-to-dir", "file-in-dir"] {91 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 }
88 let relative_path = format!("{}/file", dirname);98 let relative_path = format!("{}/file", dirname);
89 let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path();99 let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path();
90 assert!(path.parent().unwrap().is_dir(), "directory was forcefully created");100 assert!(path.parent().unwrap().is_dir(), "directory was forcefully created");
91 assert!(!path.exists());101 assert!(!path.exists());
92 }102 }
93 assert_eq!(103 assert_eq!(
94 cache.num_mkdir_calls(),104 cache.statistics().delegate.num_mkdir_calls,
95 4,105 4,
96 "like before, but it unlinks what's there and tries again"106 "like before, but it unlinks what's there and tries again"
97 );107 );
gix-worktree/tests/worktree/cache/ignore.rs+4−13
@@ -1,6 +1,7 @@
1use bstr::{BStr, ByteSlice};1use bstr::{BStr, ByteSlice};
2use gix_glob::pattern::Case;2use gix_glob::pattern::Case;
3use gix_odb::FindExt;3use gix_odb::FindExt;
4use gix_worktree::cache::state::ignore::Source;
4use gix_worktree::Cache;5use gix_worktree::Cache;
5 6
6use crate::hex_to_id;7use crate::hex_to_id;
@@ -43,7 +44,7 @@ fn special_exclude_cases_we_handle_differently() { · 36 unmodified lines
43 Default::default(),44 Default::default(),
44 gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(),45 gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(),
45 None,46 None,
46 case,47 Source::WorktreeThenIdMappingIfNotSkipped,
47 ),48 ),
48 );49 );
49 let mut cache = Cache::new(&dir, state, case, buf, Default::default());50 let mut cache = Cache::new(&dir, state, case, buf, Default::default());
@@ -104,11 +105,11 @@ fn check_against_baseline() -> crate::Result { · 54 unmodified lines
104 gix_ignore::Search::from_overrides(vec!["!force-include"]),105 gix_ignore::Search::from_overrides(vec!["!force-include"]),
105 gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?,106 gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?,
106 None,107 None,
107 case,108 Source::WorktreeThenIdMappingIfNotSkipped,
108 ),109 ),
109 );110 );
110 let paths_storage = index.take_path_backing();111 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);
112 assert_eq!(113 assert_eq!(
113 attribute_files_in_index,114 attribute_files_in_index,
114 vec![(115 vec![(
@@ -156,15 +157,5 @@ fn check_against_baseline() -> crate::Result { · 41 unmodified lines
156 }157 }
157 }158 }
158 }159 }
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");
169 Ok(())160 Ok(())
170}161}
gix/Cargo.toml+1−0
@@ -67,6 +67,7 @@ serde = [ "dep:serde", · 66 unmodified lines
67 "gix-attributes/serde",67 "gix-attributes/serde",
68 "gix-ignore/serde",68 "gix-ignore/serde",
69 "gix-revision/serde",69 "gix-revision/serde",
70 "gix-worktree/serde",
70 "gix-credentials/serde"]71 "gix-credentials/serde"]
71 72
72## Re-export the progress tree root which allows to obtain progress from various functions which take `impl gix::Progress`.73## 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)]
4pub 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
2use std::{borrow::Cow, path::PathBuf, time::Duration};2use std::{borrow::Cow, path::PathBuf, time::Duration};
3 3
4use gix_attributes::Source;4use gix_attributes::Source;
5use gix_glob::pattern::Case;
6use gix_lock::acquire::Fail;5use gix_lock::acquire::Fail;
7 6
8use crate::{7use crate::{
@@ -167,18 +166,14 @@ impl Cache { · 158 unmodified lines
167 executable_bit: boolean(self, "core.fileMode", &Core::FILE_MODE, true)?,166 executable_bit: boolean(self, "core.fileMode", &Core::FILE_MODE, true)?,
168 symlink: boolean(self, "core.symlinks", &Core::SYMLINKS, true)?,167 symlink: boolean(self, "core.symlinks", &Core::SYMLINKS, true)?,
169 };168 };
170 let case = if capabilities.ignore_case {
171 Case::Fold
172 } else {
173 Case::Sensitive
174 };
175 Ok(gix_worktree::checkout::Options {169 Ok(gix_worktree::checkout::Options {
176 attributes: self.assemble_attribute_globals(170 attributes: self
177 git_dir,171 .assemble_attribute_globals(
178 case,172 git_dir,
179 gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree,173 gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree,
180 self.attributes,174 self.attributes,
181 )?,175 )?
176 .0,
182 fs: capabilities,177 fs: capabilities,
183 thread_limit,178 thread_limit,
184 destination_is_initially_empty: false,179 destination_is_initially_empty: false,
@@ -199,14 +194,31 @@ impl Cache { · 14 unmodified lines
199 })194 })
200 }195 }
201 196
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 }
202 // TODO: at least one test, maybe related to core.attributesFile configuration.215 // TODO: at least one test, maybe related to core.attributesFile configuration.
203 fn assemble_attribute_globals(216 pub(crate) fn assemble_attribute_globals(
204 &self,217 &self,
205 git_dir: &std::path::Path,218 git_dir: &std::path::Path,
206 case: gix_glob::pattern::Case,
207 source: gix_worktree::cache::state::attributes::Source,219 source: gix_worktree::cache::state::attributes::Source,
208 attributes: crate::permissions::Attributes,220 attributes: crate::open::permissions::Attributes,
209 ) -> Result<gix_worktree::cache::state::Attributes, config::attribute_stack::Error> {221 ) -> Result<(gix_worktree::cache::state::Attributes, Vec<u8>), config::attribute_stack::Error> {
210 let configured_or_user_attributes = match self222 let configured_or_user_attributes = match self
211 .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name)223 .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name)
212 .transpose()?224 .transpose()?
@@ -232,13 +244,13 @@ impl Cache { · 19 unmodified lines
232 let info_attributes_path = git_dir.join("info").join("attributes");244 let info_attributes_path = git_dir.join("info").join("attributes");
233 let mut buf = Vec::new();245 let mut buf = Vec::new();
234 let mut collection = gix_attributes::search::MetadataCollection::default();246 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(
236 gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?,248 gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?,
237 Some(info_attributes_path),249 Some(info_attributes_path),
238 case,
239 source,250 source,
240 collection,251 collection,
241 ))252 );
253 Ok((res, buf))
242 }254 }
243 255
244 pub(crate) fn xdg_config_path(256 pub(crate) fn xdg_config_path(
gix/src/config/cache/init.rs+8−8
@@ -12,7 +12,7 @@ use crate::{ · 11 unmodified lines
12 tree::{gitoxide, Core, Http},12 tree::{gitoxide, Core, Http},
13 Cache,13 Cache,
14 },14 },
15 repository,15 open,
16};16};
17 17
18/// Initialization18/// Initialization
@@ -32,7 +32,7 @@ impl Cache { · 13 unmodified lines
32 filter_config_section: fn(&gix_config::file::Metadata) -> bool,32 filter_config_section: fn(&gix_config::file::Metadata) -> bool,
33 git_install_dir: Option<&std::path::Path>,33 git_install_dir: Option<&std::path::Path>,
34 home: Option<&std::path::Path>,34 home: Option<&std::path::Path>,
35 environment @ repository::permissions::Environment {35 environment @ open::permissions::Environment {
36 git_prefix,36 git_prefix,
37 ssh_prefix: _,37 ssh_prefix: _,
38 xdg_config_home: _,38 xdg_config_home: _,
@@ -40,16 +40,16 @@ impl Cache { · 1 unmodified line
40 http_transport,40 http_transport,
41 identity,41 identity,
42 objects,42 objects,
43 }: repository::permissions::Environment,43 }: open::permissions::Environment,
44 attributes: repository::permissions::Attributes,44 attributes: open::permissions::Attributes,
45 repository::permissions::Config {45 open::permissions::Config {
46 git_binary: use_installation,46 git_binary: use_installation,
47 system: use_system,47 system: use_system,
48 git: use_git,48 git: use_git,
49 user: use_user,49 user: use_user,
50 env: use_env,50 env: use_env,
51 includes: use_includes,51 includes: use_includes,
52 }: repository::permissions::Config,52 }: open::permissions::Config,
53 lenient_config: bool,53 lenient_config: bool,
54 api_config_overrides: &[BString],54 api_config_overrides: &[BString],
55 cli_config_overrides: &[BString],55 cli_config_overrides: &[BString],
@@ -233,12 +233,12 @@ impl Cache { · 177 unmodified lines
233 }233 }
234 234
235 pub(crate) fn make_source_env(235 pub(crate) fn make_source_env(
236 crate::permissions::Environment {236 crate::open::permissions::Environment {
237 xdg_config_home,237 xdg_config_home,
238 git_prefix,238 git_prefix,
239 home,239 home,
240 ..240 ..
241 }: crate::permissions::Environment,241 }: open::permissions::Environment,
242 ) -> impl FnMut(&str) -> Option<OsString> {242 ) -> impl FnMut(&str) -> Option<OsString> {
243 move |name| {243 move |name| {
244 match name {244 match name {
gix/src/config/mod.rs+19−2
@@ -118,6 +118,23 @@ pub mod checkout_options { · 117 unmodified lines
118 }118 }
119}119}
120 120
121///
122pub 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
121///138///
122pub mod attribute_stack {139pub mod attribute_stack {
123 /// The error produced when setting up the attribute stack to query `gitattributes`.140 /// The error produced when setting up the attribute stack to query `gitattributes`.
@@ -462,7 +479,7 @@ pub(crate) struct Cache { · 338 unmodified lines
462 /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient.479 /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient.
463 /// Also available in options! Keep in sync!480 /// Also available in options! Keep in sync!
464 pub lenient_config: bool,481 pub lenient_config: bool,
465 attributes: crate::permissions::Attributes,482 attributes: crate::open::permissions::Attributes,
466 environment: crate::permissions::Environment,483 environment: crate::open::permissions::Environment,
467 // TODO: make core.precomposeUnicode available as well.484 // TODO: make core.precomposeUnicode available as well.
468}485}
gix/src/config/tree/mod.rs+6−3
@@ -38,6 +38,8 @@ pub(crate) mod root { · 37 unmodified lines
38 pub const GITOXIDE: sections::Gitoxide = sections::Gitoxide;38 pub const GITOXIDE: sections::Gitoxide = sections::Gitoxide;
39 /// The `http` section.39 /// The `http` section.
40 pub const HTTP: sections::Http = sections::Http;40 pub const HTTP: sections::Http = sections::Http;
41 /// The `index` section.
42 pub const INDEX: sections::Index = sections::Index;
41 /// The `init` section.43 /// The `init` section.
42 pub const INIT: sections::Init = sections::Init;44 pub const INIT: sections::Init = sections::Init;
43 /// The `pack` section.45 /// The `pack` section.
@@ -69,6 +71,7 @@ pub(crate) mod root { · 25 unmodified lines
69 &Self::EXTENSIONS,71 &Self::EXTENSIONS,
70 &Self::GITOXIDE,72 &Self::GITOXIDE,
71 &Self::HTTP,73 &Self::HTTP,
74 &Self::INDEX,
72 &Self::INIT,75 &Self::INIT,
73 &Self::PACK,76 &Self::PACK,
74 &Self::PROTOCOL,77 &Self::PROTOCOL,
@@ -84,9 +87,9 @@ pub(crate) mod root { · 9 unmodified lines
84 87
85mod sections;88mod sections;
86pub use sections::{89pub use sections::{
87 branch, checkout, core, credential, diff, extensions, gitoxide, http, protocol, remote, ssh, Author, Branch,90 branch, checkout, core, credential, diff, extensions, gitoxide, http, index, protocol, remote, ssh, Author, Branch,
88 Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Init, Pack, Protocol, Remote, Safe,91 Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Index, Init, Pack, Protocol,
89 Ssh, Url, User,92 Remote, Safe, Ssh, Url, User,
90};93};
91 94
92/// Generic value implementations for static instantiation.95/// Generic value implementations for static instantiation.
gix/src/config/tree/sections/index.rs+62−0
@@ -0,0 +1,62 @@
1use crate::{
2 config,
3 config::tree::{keys, Index, Key, Section},
4};
5
6impl 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.
13pub type IndexThreads = keys::Any<validate::IndexThreads>;
14
15mod 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
42impl Section for Index {
43 fn name(&self) -> &str {
44 "index"
45 }
46
47 fn keys(&self) -> &[&dyn Key] {
48 &[&Self::THREADS]
49 }
50}
51
52mod 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
55pub struct Http;55pub struct Http;
56pub mod http;56pub mod http;
57 57
58/// The `index` top-level section.
59#[derive(Copy, Clone, Default)]
60pub struct Index;
61pub mod index;
62
58/// The `init` top-level section.63/// The `init` top-level section.
59#[derive(Copy, Clone, Default)]64#[derive(Copy, Clone, Default)]
60pub struct Init;65pub struct Init;
gix/src/lib.rs+3−7
@@ -99,6 +99,9 @@ pub use hash::{oid, ObjectId}; · 98 unmodified lines
99 99
100pub mod interrupt;100pub mod interrupt;
101 101
102///
103pub mod attributes;
104
102mod ext;105mod ext;
103///106///
104pub mod prelude;107pub mod prelude;
@@ -223,13 +226,6 @@ pub fn open_opts(directory: impl Into<std::path::PathBuf>, options: open::Option · 118 unmodified lines
223 ThreadSafeRepository::open_opts(directory, options).map(Into::into)226 ThreadSafeRepository::open_opts(directory, options).map(Into::into)
224}227}
225 228
226///
227pub mod permission;
228
229///
230pub mod permissions;
231pub use repository::permissions::Permissions;
232
233///229///
234pub mod create;230pub mod create;
235 231
gix/src/open/mod.rs+14−3
@@ -1,6 +1,17 @@
1use std::path::PathBuf;1use std::path::PathBuf;
2 2
3use crate::{bstr::BString, config, permission, Permissions};3use crate::{bstr::BString, config};
4
5/// Permissions associated with various resources of a git repository
6#[derive(Debug, Clone)]
7pub 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}
4 15
5/// The options used in [`ThreadSafeRepository::open_opts()`][crate::ThreadSafeRepository::open_opts()].16/// The options used in [`ThreadSafeRepository::open_opts()`][crate::ThreadSafeRepository::open_opts()].
6///17///
@@ -44,11 +55,11 @@ pub enum Error { · 37 unmodified lines
44 #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())]55 #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())]
45 UnsafeGitDir { path: PathBuf },56 UnsafeGitDir { path: PathBuf },
46 #[error(transparent)]57 #[error(transparent)]
47 EnvironmentAccessDenied(#[from] permission::env_var::resource::Error),58 EnvironmentAccessDenied(#[from] gix_sec::permission::Error<std::path::PathBuf>),
48}59}
49 60
50mod options;61mod options;
51 62pub mod permissions;
52mod repository;63mod repository;
53 64
54#[cfg(test)]65#[cfg(test)]
gix/src/open/options.rs+1−1
@@ -1,7 +1,7 @@
1use std::path::PathBuf;1use std::path::PathBuf;
2 2
3use super::{Error, Options};3use super::{Error, Options};
4use crate::{bstr::BString, config, Permissions, ThreadSafeRepository};4use crate::{bstr::BString, config, open::Permissions, ThreadSafeRepository};
5 5
6impl Default for Options {6impl Default for Options {
7 fn default() -> Self {7 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].
2use crate::open::Permissions;
3use 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)]
9pub 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
34impl 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
60impl 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)]
70pub 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
87impl 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
107impl 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)]
115pub 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
142impl 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
172impl 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
202impl 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
211impl 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
10 cache::{interpolate_context, util::ApplyLeniency},10 cache::{interpolate_context, util::ApplyLeniency},
11 tree::{gitoxide, Core, Key, Safe},11 tree::{gitoxide, Core, Key, Safe},
12 },12 },
13 permission, Permissions, ThreadSafeRepository,13 open::Permissions,
14 ThreadSafeRepository,
14};15};
15 16
16#[derive(Default, Clone)]17#[derive(Default, Clone)]
@@ -26,7 +27,7 @@ pub(crate) struct EnvironmentOverrides { · 9 unmodified lines
26}27}
27 28
28impl EnvironmentOverrides {29impl 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>> {
30 let mut worktree_dir = None;31 let mut worktree_dir = None;
31 if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) {32 if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) {
32 worktree_dir = PathBuf::from(path).into();33 worktree_dir = PathBuf::from(path).into();
gix/src/permission.rs+0−8
@@ -1,8 +0,0 @@
1///
2pub 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 @@
1pub use crate::repository::permissions::{Attributes, Config, Environment};
gix/src/repository/attributes.rs+50−0
@@ -0,0 +1,50 @@
1//! exclude information
2use crate::Repository;
3
4impl 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
2use crate::{config, Repository};
3impl 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
19 }19 }
20}20}
21 21
22mod attributes;
22mod cache;23mod cache;
23mod config;24mod config;
25mod excludes;
24pub(crate) mod identity;26pub(crate) mod identity;
25mod impls;27mod impls;
26mod init;28mod init;
27mod location;29mod location;
28mod object;30mod object;
29pub(crate) mod permissions;
30mod reference;31mod reference;
31mod remote;32mod remote;
32mod revision;33mod 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].
2use crate::open::Permissions;
1use gix_sec::Trust;3use gix_sec::Trust;
2 4
3/// Permissions associated with various resources of a git repository
4#[derive(Debug, Clone)]
5pub 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
14/// Configure from which sources git configuration may be loaded.5/// Configure from which sources git configuration may be loaded.
15///6///
16/// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness.7/// 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 @@
1use crate::config::cache::util::ApplyLeniencyDefault;
1use crate::{worktree, Worktree};2use crate::{worktree, Worktree};
2 3
3/// Worktree iteration4/// Interact with individual worktrees and their information.
4impl crate::Repository {5impl crate::Repository {
5 /// Return a list of all _linked_ worktrees sorted by private git dir path as a lightweight proxy.6 /// Return a list of all _linked_ worktrees sorted by private git dir path as a lightweight proxy.
6 ///7 ///
@@ -25,10 +26,6 @@ impl crate::Repository { · 18 unmodified lines
25 res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir));26 res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir));
26 Ok(res)27 Ok(res)
27 }28 }
28}
29
30/// Interact with individual worktrees and their information.
31impl crate::Repository {
32 /// Return the repository owning the main worktree, typically from a linked worktree.29 /// Return the repository owning the main worktree, typically from a linked worktree.
33 ///30 ///
34 /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree.31 /// 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
58 ///55 ///
59 /// It will use the `index.threads` configuration key to learn how many threads to use.56 /// It will use the `index.threads` configuration key to learn how many threads to use.
60 /// Note that it may fail if there is no index.57 /// Note that it may fail if there is no index.
61 // TODO: test
62 pub fn open_index(&self) -> Result<gix_index::File, worktree::open_index::Error> {58 pub fn open_index(&self) -> Result<gix_index::File, worktree::open_index::Error> {
63 let thread_limit = self59 let thread_limit = self
64 .config60 .config
65 .resolved61 .resolved
66 .boolean("index", None, "threads")62 .string("index", None, "threads")
67 .map(|res| {63 .map(|value| crate::config::tree::Index::THREADS.try_into_index_threads(value))
68 res.map(|value| usize::from(!value)).or_else(|err| {64 .transpose()
69 gix_config::Integer::try_from(err.input.as_ref())65 .with_lenient_default(self.config.lenient_config)?;
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()?;
78 gix_index::File::at(66 gix_index::File::at(
79 self.index_path(),67 self.index_path(),
80 self.object_hash(),68 self.object_hash(),
gix/src/worktree/mod.rs+45−44
@@ -71,18 +71,12 @@ pub mod proxy; · 70 unmodified lines
71 71
72///72///
73pub mod open_index {73pub mod open_index {
74 use crate::bstr::BString;
75
76 /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()].74 /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()].
77 #[derive(Debug, thiserror::Error)]75 #[derive(Debug, thiserror::Error)]
78 #[allow(missing_docs)]76 #[allow(missing_docs)]
79 pub enum Error {77 pub enum Error {
80 #[error("Could not interpret value '{}' as 'index.threads'", .value)]78 #[error(transparent)]
81 ConfigIndexThreads {79 ConfigIndexThreads(#[from] crate::config::key::GenericErrorWithValue),
82 value: BString,
83 #[source]
84 err: gix_config::value::Error,
85 },
86 #[error(transparent)]80 #[error(transparent)]
87 IndexFile(#[from] gix_index::file::init::Error),81 IndexFile(#[from] gix_index::file::init::Error),
88 }82 }
@@ -102,18 +96,14 @@ pub mod open_index { · 13 unmodified lines
102 96
103///97///
104pub mod excludes {98pub mod excludes {
105 use std::path::PathBuf;
106
107 /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()].99 /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()].
108 #[derive(Debug, thiserror::Error)]100 #[derive(Debug, thiserror::Error)]
109 #[allow(missing_docs)]101 #[allow(missing_docs)]
110 pub enum Error {102 pub enum Error {
111 #[error("Could not read repository exclude.")]
112 Io(#[from] std::io::Error),
113 #[error(transparent)]103 #[error(transparent)]
114 EnvironmentPermission(#[from] gix_sec::permission::Error<PathBuf>),104 OpenIndex(#[from] crate::worktree::open_index::Error),
115 #[error("The value for `core.excludesFile` could not be read from configuration")]105 #[error(transparent)]
116 ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error),106 CreateCache(#[from] crate::config::exclude_stack::Error),
117 }107 }
118 108
119 impl<'repo> crate::Worktree<'repo> {109 impl<'repo> crate::Worktree<'repo> {
@@ -124,35 +114,46 @@ pub mod excludes { · 4 unmodified lines
124 /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file.114 /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file.
125 /// * `$GIT_DIR/info/exclude` if present.115 /// * `$GIT_DIR/info/exclude` if present.
126 ///116 ///
127 /// `index` may be used to obtain `.gitignore` files directly from the index under certain conditions.117 /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use
128 // TODO: test, provide higher-level interface that is much easier to use and doesn't panic when accessing entries118 /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes.
129 // by non-relative path.119 pub fn excludes(&self, overrides: Option<gix_ignore::Search>) -> Result<gix_worktree::Cache, Error> {
130 // TODO: `index` might be so special (given the conditions we are talking about) that it's better obtained internally120 let index = self.index()?;
131 // so the caller won't have to care.121 Ok(self.parent.excludes(
132 pub fn excludes(122 &index,
133 &self,123 overrides,
134 index: &gix_index::State,124 gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
135 overrides: Option<gix_ignore::Search>,125 )?)
136 ) -> Result<gix_worktree::Cache, Error> {126 }
137 let repo = self.parent;127 }
138 let case = if repo.config.ignore_case {128}
139 gix_glob::pattern::Case::Fold129
140 } else {130///
141 gix_glob::pattern::Case::Sensitive131pub mod attributes {
142 };132 /// The error returned by [`Worktree::attributes()`][crate::Worktree::attributes()].
143 let mut buf = Vec::with_capacity(512);133 #[derive(Debug, thiserror::Error)]
144 let excludes_file = match repo.config.excludes_file().transpose()? {134 #[allow(missing_docs)]
145 Some(user_path) => Some(user_path),135 pub enum Error {
146 None => repo.config.xdg_config_path("ignore")?,136 #[error(transparent)]
147 };137 OpenIndex(#[from] crate::worktree::open_index::Error),
148 let state = gix_worktree::cache::State::IgnoreStack(gix_worktree::cache::state::Ignore::new(138 #[error(transparent)]
149 overrides.unwrap_or_default(),139 CreateCache(#[from] crate::attributes::Error),
150 gix_ignore::Search::from_git_dir(repo.git_dir(), excludes_file, &mut buf)?,140 }
151 None,141
152 case,142 impl<'repo> crate::Worktree<'repo> {
153 ));143 /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes.
154 let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case);144 ///
155 Ok(gix_worktree::Cache::new(self.path, state, case, buf, attribute_list))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 )?)
156 }157 }
157 }158 }
158}159}
gix/tests/config/tree.rs+24−0
@@ -351,6 +351,30 @@ mod core { · 350 unmodified lines
351 }351 }
352}352}
353 353
354mod 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
354mod extensions {378mod extensions {
355 use gix::config::tree::{Extensions, Key};379 use gix::config::tree::{Extensions, Key};
356 380
gix/tests/remote/connect.rs+4−4
@@ -28,12 +28,12 @@ mod blocking_io { · 27 unmodified lines
28 let _env = env_value.map(|value| gix_testtools::Env::new().set("GIT_PROTOCOL_FROM_USER", value));28 let _env = env_value.map(|value| gix_testtools::Env::new().set("GIT_PROTOCOL_FROM_USER", value));
29 let repo = gix::open_opts(29 let repo = gix::open_opts(
30 remote::repo("protocol_file_user").git_dir(),30 remote::repo("protocol_file_user").git_dir(),
31 gix::open::Options::isolated().permissions(gix::Permissions {31 gix::open::Options::isolated().permissions(gix::open::Permissions {
32 env: gix::permissions::Environment {32 env: gix::open::permissions::Environment {
33 git_prefix: gix_sec::Permission::Allow,33 git_prefix: gix_sec::Permission::Allow,
34 ..gix::permissions::Environment::all()34 ..gix::open::permissions::Environment::all()
35 },35 },
36 ..gix::Permissions::isolated()36 ..gix::open::Permissions::isolated()
37 }),37 }),
38 )?;38 )?;
39 let remote = repo.find_remote("origin")?;39 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
29 .set("GIT_CONFIG_VALUE_0", work_dir.join("c.config").display().to_string());29 .set("GIT_CONFIG_VALUE_0", work_dir.join("c.config").display().to_string());
30 let repo = gix::open_opts(30 let repo = gix::open_opts(
31 repo.git_dir(),31 repo.git_dir(),
32 repo.open_options().clone().with(trust).permissions(gix::Permissions {32 repo.open_options()
33 env: gix::permissions::Environment {33 .clone()
34 xdg_config_home: Permission::Deny,34 .with(trust)
35 home: Permission::Deny,35 .permissions(gix::open::Permissions {
36 ..gix::permissions::Environment::all()36 env: gix::open::permissions::Environment {
37 },37 xdg_config_home: Permission::Deny,
38 ..Default::default()38 home: Permission::Deny,
39 }),39 ..gix::open::permissions::Environment::all()
40 },
41 ..Default::default()
42 }),
40 )?;43 )?;
41 44
42 assert_eq!(45 assert_eq!(
@@ -148,11 +151,11 @@ fn author_from_different_config_sections() -> crate::Result { · 105 unmodified lines
148 .clone()151 .clone()
149 .config_overrides(None::<&str>)152 .config_overrides(None::<&str>)
150 .with(gix_sec::Trust::Full)153 .with(gix_sec::Trust::Full)
151 .permissions(gix::Permissions {154 .permissions(gix::open::Permissions {
152 env: gix::permissions::Environment {155 env: gix::open::permissions::Environment {
153 xdg_config_home: Permission::Deny,156 xdg_config_home: Permission::Deny,
154 home: Permission::Deny,157 home: Permission::Deny,
155 ..gix::permissions::Environment::all()158 ..gix::open::permissions::Environment::all()
156 },159 },
157 ..Default::default()160 ..Default::default()
158 }),161 }),
src/plumbing/main.rs+42−10
@@ -339,16 +339,6 @@ pub fn main() -> Result<()> { · 338 unmodified lines
339 )339 )
340 },340 },
341 ),341 ),
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 ),
352 free::index::Subcommands::Verify => prepare_and_run(342 free::index::Subcommands::Verify => prepare_and_run(
353 "index-verify",343 "index-verify",
354 auto_verbose,344 auto_verbose,
@@ -865,6 +855,37 @@ pub fn main() -> Result<()> { · 510 unmodified lines
865 ),855 ),
866 },856 },
867 Subcommands::Index(cmd) => match cmd {857 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 ),
868 index::Subcommands::FromTree {889 index::Subcommands::FromTree {
869 force,890 force,
870 index_output_path,891 index_output_path,
@@ -899,3 +920,14 @@ fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode { · 28 unmodified lines
899 (false, false) => verify::Mode::HashCrc32,920 (false, false) => verify::Mode::HashCrc32,
900 }921 }
901}922}
923
924#[cfg(test)]
925mod 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
55 pub enum Subcommands {55 pub enum Subcommands {
56 /// Validate constraints and assumptions of an index along with its integrity.56 /// Validate constraints and assumptions of an index along with its integrity.
57 Verify,57 Verify,
58 /// Print all entries to standard output
59 Entries,
60 /// Print information about the index structure58 /// Print information about the index structure
61 Info {59 Info {
62 /// Do not extract specific extension information to gain only a superficial idea of the index's composition.60 /// 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
107 /// Possible values are "none" and "tree-traversal". Default is "none".105 /// Possible values are "none" and "tree-traversal". Default is "none".
108 expansion: Option<core::pack::create::ObjectExpansion>,106 expansion: Option<core::pack::create::ObjectExpansion>,
109 107
110 #[clap(long, default_value_t = 3, requires = "nondeterministic-count")]108 #[clap(long, default_value_t = 3, requires = "nondeterministic_count")]
111 /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting109 /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting
112 /// to the globally configured threads.110 /// to the globally configured threads.
113 ///111 ///
src/plumbing/options/mod.rs+16−2
@@ -361,11 +361,11 @@ pub mod commit { · 360 unmodified lines
361 /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.361 /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
362 Describe {362 Describe {
363 /// Use annotated tag references only, not all tags.363 /// 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"))]
365 annotated_tags: bool,365 annotated_tags: bool,
366 366
367 /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches.367 /// 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"))]
369 all_refs: bool,369 all_refs: bool,
370 370
371 /// Only follow the first parent when traversing the commit graph.371 /// Only follow the first parent when traversing the commit graph.
@@ -471,6 +471,20 @@ pub mod index { · 99 unmodified lines
471 471
472 #[derive(Debug, clap::Subcommand)]472 #[derive(Debug, clap::Subcommand)]
473 pub enum Subcommands {473 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 },
474 /// Create an index from a tree-ish.488 /// Create an index from a tree-ish.
475 #[clap(visible_alias = "read-tree")]489 #[clap(visible_alias = "read-tree")]
476 FromTree {490 FromTree {
src/shared.rs+5−1
@@ -113,7 +113,11 @@ pub mod pretty { · 112 unmodified lines
113 crate::shared::init_env_logger();113 crate::shared::init_env_logger();
114 114
115 match (verbose, progress) {115 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 }
117 (true, false) => {121 (true, false) => {
118 use crate::shared::{self, STANDARD_RANGE};122 use crate::shared::{self, STANDARD_RANGE};
119 let progress = shared::progress_tree();123 let progress = shared::progress_tree();