History

Merge branch 'worktree-stack'

Sebastian Thiel committed 4 years ago

Commit 98da8ba2 parents f06f666 3a41d5c113 files · +4083 −1918Browse files

Unified · Split

113 files
  1. Cargo.lock +31 −1
  2. Cargo.toml +1 −0
  3. README.md +3 −0
  4. crate-status.md +9 −1
  5. etc/check-package-size.sh +2 −1
  6. git-attributes/Cargo.toml +3 −1
  7. git-attributes/src/lib.rs +61 −66
  8. git-attributes/src/match_group.rs +354 −0
  9. git-attributes/src/parse/attribute.rs +6 −6
  10. git-attributes/tests/attributes.rs +1 −0
  11. git-attributes/tests/fixtures/make_global_and_external_and_dir_ignores.sh +77 −0
  12. git-attributes/tests/match_group/mod.rs +108 −5
  13. git-attributes/tests/parse/attribute.rs +11 −11
  14. git-attributes/tests/parse/ignore.rs +4 −4
  15. git-config/Cargo.toml +2 −1
  16. git-config/src/file/error.rs +0 −35
  17. git-config/src/file/git_config.rs +191 −171
  18. git-config/src/file/mod.rs +0 −2
  19. git-config/src/file/resolved.rs +0 −3
  20. git-config/src/file/section.rs +13 −27
  21. git-config/src/file/value.rs +9 −21
  22. git-config/src/fs.rs +6 −8
  23. git-config/src/lib.rs +57 −0
  24. git-config/src/parser.rs +0 −36
  25. git-config/src/values.rs +101 −148
  26. git-config/tests/git_config/mod.rs +139 −108
  27. git-config/tests/value/mod.rs +22 −7
  28. git-features/Cargo.toml +0 −10
  29. git-features/src/lib.rs +0 −1
  30. git-features/src/path.rs +0 −234
  31. git-features/tests/path.rs +0 −30
  32. git-glob/src/lib.rs +6 −4
  33. git-glob/src/parse.rs +3 −3
  34. git-glob/src/pattern.rs +37 −34
  35. git-glob/tests/fixtures/generated-archives/make_baseline.tar.xz +2 −2
  36. git-glob/tests/fixtures/make_baseline.sh +5 −2
  37. git-glob/tests/glob.rs +1 −1
  38. git-glob/tests/matching/mod.rs +0 −279
  39. git-glob/tests/parse/mod.rs +2 −2
  40. git-glob/tests/pattern/matching.rs +326 −0
  41. git-glob/tests/pattern/mod.rs +19 −0
  42. git-glob/tests/wildmatch/mod.rs +7 −3
  43. git-index/src/access.rs +39 −1
  44. git-index/src/decode/mod.rs +2 −1
  45. git-index/src/entry.rs +4 −0
  46. git-index/src/lib.rs +4 −1
  47. git-odb/Cargo.toml +2 −1
  48. git-odb/src/alternate/parse.rs +1 −1
  49. git-pack/Cargo.toml +1 −0
  50. git-pack/src/multi_index/chunk.rs +1 −1
  51. git-path/CHANGELOG.md +42 −0
  52. git-path/Cargo.toml +16 −0
  53. git-path/src/convert.rs +197 −0
  54. git-path/src/lib.rs +52 −0
  55. git-path/src/spec.rs +51 −0
  56. git-path/tests/path.rs +21 −0
  57. git-ref/Cargo.toml +2 −1
  58. git-ref/src/fullname.rs +1 −1
  59. git-ref/src/name.rs +3 −3
  60. git-ref/src/namespace.rs +3 −10
  61. git-ref/src/store/file/find.rs +1 −1
  62. git-ref/src/store/file/loose/iter.rs +6 −7
  63. git-ref/src/store/file/mod.rs +2 −6
  64. git-repository/Cargo.toml +6 −3
  65. git-repository/src/config.rs +85 −33
  66. git-repository/src/lib.rs +20 −4
  67. git-repository/src/open.rs +13 −3
  68. git-repository/src/path/discover.rs +47 −23
  69. git-repository/src/path/is.rs +1 −0
  70. git-repository/src/path/mod.rs +16 −1
  71. git-repository/src/repository/init.rs +32 −0
  72. git-repository/src/repository/location.rs +25 −4
  73. git-repository/src/repository/mod.rs +4 −90
  74. git-repository/src/repository/permissions.rs +67 −0
  75. git-repository/src/repository/snapshots.rs +1 −1
  76. git-repository/src/repository/worktree.rs +21 −0
  77. git-repository/src/types.rs +17 −8
  78. git-repository/src/worktree.rs +129 −0
  79. git-repository/tests/discover/mod.rs +13 −10
  80. git-sec/Cargo.toml +1 −0
  81. git-sec/src/lib.rs +66 −5
  82. git-url/Cargo.toml +1 −0
  83. git-url/src/expand_path.rs +1 −1
  84. git-worktree/Cargo.toml +3 −0
  85. git-worktree/src/fs/cache.rs +0 −185
  86. git-worktree/src/fs/cache/mod.rs +139 −0
  87. git-worktree/src/fs/cache/platform.rs +165 −0
  88. git-worktree/src/fs/cache/state.rs +287 −0
  89. git-worktree/src/fs/mod.rs +15 −3
  90. git-worktree/src/fs/stack.rs +29 −5
  91. git-worktree/src/index/checkout.rs +4 −4
  92. git-worktree/src/index/entry.rs +9 −9
  93. git-worktree/src/index/mod.rs +46 −35
  94. git-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz +3 −0
  95. git-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh +112 −0
  96. git-worktree/tests/worktree-multi-threaded.rs +2 −0
  97. git-worktree/tests/worktree/fs/cache.rs +148 −29
  98. git-worktree/tests/worktree/fs/mod.rs +1 −0
  99. git-worktree/tests/worktree/fs/stack/mod.rs +144 −0
  100. gitoxide-core/src/organize.rs +1 −1
  101. gitoxide-core/src/pack/receive.rs +2 −1
  102. gitoxide-core/src/repository/commit.rs +1 −4
  103. gitoxide-core/src/repository/exclude.rs +68 −0
  104. gitoxide-core/src/repository/mailmap.rs +2 −3
  105. gitoxide-core/src/repository/mod.rs +2 −0
  106. gitoxide-core/src/repository/odb.rs +3 −6
  107. gitoxide-core/src/repository/tree.rs +3 −5
  108. gitoxide-core/src/repository/verify.rs +2 −3
  109. src/plumbing/main.rs +177 −130
  110. src/plumbing/options.rs +30 −0
  111. src/porcelain/main.rs +3 −3
  112. src/shared.rs +45 −2
  113. tests/tools/src/lib.rs +1 −1
Cargo.lock+31−1
@@ -503,6 +503,15 @@ version = "0.1.1" · 502 unmodified lines
503503 source = "registry+https://github.com/rust-lang/crates.io-index"
504504 checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35"
505505
506+[[package]]
507+name = "compact_str"
508+version = "0.3.2"
509+source = "registry+https://github.com/rust-lang/crates.io-index"
510+checksum = "d0e60dedcb8b23cedf6f23ee35ecf5c7889961e99f26f79ab196aaf4a8b48608"
511+dependencies = [
512+ "serde",
513+]
514+
506515 [[package]]
507516 name = "concurrent-queue"
508517 version = "1.2.2"
@@ -1076,8 +1085,10 @@ name = "git-attributes" · 567 unmodified lines
10761085 version = "0.1.0"
10771086 dependencies = [
10781087 "bstr",
1088+ "compact_str",
10791089 "git-features",
10801090 "git-glob",
1091+ "git-path",
10811092 "git-quote",
10821093 "git-testtools",
10831094 "quick-error",
@@ -1122,15 +1133,16 @@ dependencies = [ · 38 unmodified lines
11221133 "criterion",
11231134 "dirs",
11241135 "git-features",
1136+ "git-path",
11251137 "git-sec",
11261138 "memchr",
11271139 "nom",
11281140 "pwd",
1129− "quick-error",
11301141 "serde",
11311142 "serde_derive",
11321143 "serial_test",
11331144 "tempfile",
1145+ "thiserror",
11341146 "unicode-bom",
11351147 ]
11361148
@@ -1300,6 +1312,7 @@ dependencies = [ · 163 unmodified lines
13001312 "git-hash",
13011313 "git-object",
13021314 "git-pack",
1315+ "git-path",
13031316 "git-quote",
13041317 "git-testtools",
13051318 "parking_lot 0.12.0",
@@ -1325,6 +1338,7 @@ dependencies = [ · 19 unmodified lines
13251338 "git-hash",
13261339 "git-object",
13271340 "git-odb",
1341+ "git-path",
13281342 "git-tempfile",
13291343 "git-testtools",
13301344 "git-traverse",
@@ -1356,6 +1370,13 @@ dependencies = [ · 25 unmodified lines
13561370 "serde",
13571371 ]
13581372
1373+[[package]]
1374+name = "git-path"
1375+version = "0.1.0"
1376+dependencies = [
1377+ "bstr",
1378+]
1379+
13591380 [[package]]
13601381 name = "git-pathspec"
13611382 version = "0.0.0"
@@ -1406,6 +1427,7 @@ dependencies = [ · 44 unmodified lines
14061427 "git-lock",
14071428 "git-object",
14081429 "git-odb",
1430+ "git-path",
14091431 "git-tempfile",
14101432 "git-testtools",
14111433 "git-validate",
@@ -1425,6 +1447,7 @@ dependencies = [ · 13 unmodified lines
14251447 "clru",
14261448 "document-features",
14271449 "git-actor",
1450+ "git-attributes",
14281451 "git-config",
14291452 "git-credentials",
14301453 "git-diff",
@@ -1437,6 +1460,7 @@ dependencies = [ · 6 unmodified lines
14371460 "git-object",
14381461 "git-odb",
14391462 "git-pack",
1463+ "git-path",
14401464 "git-protocol",
14411465 "git-ref",
14421466 "git-revision",
@@ -1448,6 +1472,7 @@ dependencies = [ · 5 unmodified lines
14481472 "git-url",
14491473 "git-validate",
14501474 "git-worktree",
1475+ "is_ci",
14511476 "log",
14521477 "signal-hook",
14531478 "tempfile",
@@ -1477,6 +1502,7 @@ dependencies = [ · 23 unmodified lines
14771502 "libc",
14781503 "serde",
14791504 "tempfile",
1505+ "thiserror",
14801506 "windows",
14811507 ]
14821508
@@ -1571,6 +1597,7 @@ version = "0.4.0" · 88 unmodified lines
15711597 dependencies = [
15721598 "bstr",
15731599 "git-features",
1600+ "git-path",
15741601 "home",
15751602 "quick-error",
15761603 "serde",
@@ -1592,11 +1619,14 @@ version = "0.1.0" · 15 unmodified lines
15921619 dependencies = [
15931620 "bstr",
15941621 "document-features",
1622+ "git-attributes",
15951623 "git-features",
1624+ "git-glob",
15961625 "git-hash",
15971626 "git-index",
15981627 "git-object",
15991628 "git-odb",
1629+ "git-path",
16001630 "git-testtools",
16011631 "io-close",
16021632 "serde",
Cargo.toml+1−0
@@ -161,6 +161,7 @@ members = [ · 160 unmodified lines
161161 "git-lock",
162162 "git-attributes",
163163 "git-pathspec",
164+ "git-path",
164165 "git-repository",
165166 "gitoxide-core",
166167 "git-tui",
README.md+3−0
@@ -48,6 +48,8 @@ Please see _'Development Status'_ for a listing of all crates and their capabili · 47 unmodified lines
4848 * **mailmap**
4949 * [x] **verify** - check entries of a mailmap file for parse errors and display them
5050 * **repository**
51+ * **exclude**
52+ * [x] **query** - check if path specs are excluded via gits exclusion rules like `.gitignore`.
5153 * **verify** - validate a whole repository, for now only the object database.
5254 * **commit**
5355 * [x] **describe** - identify a commit by its closest tag in its past
@@ -122,6 +124,7 @@ Crates that seem feature complete and need to see some more use before they can · 68 unmodified lines
122124 * [git-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-bitmap)
123125 * [git-revision](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-revision)
124126 * [git-attributes](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-attributes)
127+ * [git-path](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-path)
125128 * **idea**
126129 * [git-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-note)
127130 * [git-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-filter)
crate-status.md+9−1
@@ -221,6 +221,13 @@ Check out the [performance discussion][git-traverse-performance] as well. · 220 unmodified lines
221221 * [x] parsing
222222 * [x] lookup and mapping of author names
223223
224+### git-path
225+* [x] transformations to and from bytes
226+* [x] conversions between different platforms
227+* **spec**
228+ * [ ] parse
229+ * [ ] check for match
230+
224231 ### git-pathspec
225232 * [ ] parse
226233 * [ ] check for match
@@ -429,7 +436,8 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. · 202 unmodified lines
429436 * **refs**
430437 * [ ] run transaction hooks and handle special repository states like quarantine
431438 * [ ] support for different backends like `files` and `reftable`
432− * [ ] worktrees
439+ * **worktrees**
440+ * [ ] open a repository with worktrees
433441 * [ ] remotes with push and pull
434442 * [x] mailmap
435443 * [x] object replacements (`git replace`)
etc/check-package-size.sh+2−1
@@ -19,6 +19,7 @@ echo "in root: gitoxide CLI" · 18 unmodified lines
1919 (enter cargo-smart-release && indent cargo diet -n --package-size-limit 90KB)
2020 (enter git-actor && indent cargo diet -n --package-size-limit 5KB)
2121 (enter git-pathspec && indent cargo diet -n --package-size-limit 5KB)
22+(enter git-path && indent cargo diet -n --package-size-limit 10KB)
2223 (enter git-attributes && indent cargo diet -n --package-size-limit 10KB)
2324 (enter git-index && indent cargo diet -n --package-size-limit 30KB)
2425 (enter git-worktree && indent cargo diet -n --package-size-limit 25KB)
@@ -51,6 +52,6 @@ echo "in root: gitoxide CLI" · 26 unmodified lines
5152 (enter git-odb && indent cargo diet -n --package-size-limit 120KB)
5253 (enter git-protocol && indent cargo diet -n --package-size-limit 50KB)
5354 (enter git-packetline && indent cargo diet -n --package-size-limit 35KB)
54−(enter git-repository && indent cargo diet -n --package-size-limit 90KB)
55+(enter git-repository && indent cargo diet -n --package-size-limit 100KB)
5556 (enter git-transport && indent cargo diet -n --package-size-limit 50KB)
5657 (enter gitoxide-core && indent cargo diet -n --package-size-limit 70KB)
git-attributes/Cargo.toml+3−1
@@ -13,12 +13,13 @@ doctest = false · 12 unmodified lines
1313
1414 [features]
1515 ## Data structures implement `serde::Serialize` and `serde::Deserialize`.
16−serde1 = ["serde", "bstr/serde1", "git-glob/serde1"]
16+serde1 = ["serde", "bstr/serde1", "git-glob/serde1", "compact_str/serde"]
1717
1818 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1919
2020 [dependencies]
2121 git-features = { version = "^0.20.0", path = "../git-features" }
22+git-path = { version = "^0.1.0", path = "../git-path" }
2223 git-quote = { version = "^0.2.0", path = "../git-quote" }
2324 git-glob = { version = "^0.2.0", path = "../git-glob" }
2425
@@ -26,6 +27,7 @@ bstr = { version = "0.2.13", default-features = false, features = ["std"]} · 1 unmodified line
2627 unicode-bom = "1.1.4"
2728 quick-error = "2.0.0"
2829 serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
30+compact_str = "0.3.2"
2931
3032 [dev-dependencies]
3133 git-testtools = { path = "../tests/tools"}
git-attributes/src/lib.rs+61−66
@@ -2,10 +2,17 @@ · 1 unmodified line
22 #![deny(rust_2018_idioms)]
33
44 use bstr::{BStr, BString};
5+use compact_str::CompactStr;
6+use std::path::PathBuf;
57
8+pub use git_glob as glob;
9+
10+/// The state an attribute can be in, referencing the value.
11+///
12+/// Note that this doesn't contain the name.
613 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
714 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
8−pub enum State<'a> {
15+pub enum StateRef<'a> {
916 /// The attribute is listed, or has the special value 'true'
1017 Set,
1118 /// The attribute has the special value 'false', or was prefixed with a `-` sign.
@@ -18,11 +25,38 @@ pub enum State<'a> { · 6 unmodified lines
1825 Unspecified,
1926 }
2027
21−/// A grouping of lists of patterns while possibly keeping associated to their base path.
28+/// The state an attribute can be in, owning the value.
2229 ///
23−/// Patterns with base path are queryable relative to that base, otherwise they are relative to the repository root.
30+/// Note that this doesn't contain the name.
31+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
32+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
33+pub enum State {
34+ /// The attribute is listed, or has the special value 'true'
35+ Set,
36+ /// The attribute has the special value 'false', or was prefixed with a `-` sign.
37+ Unset,
38+ /// The attribute is set to the given value, which followed the `=` sign.
39+ /// Note that values can be empty.
40+ Value(compact_str::CompactStr),
41+ /// The attribute isn't mentioned with a given path or is explicitly set to `Unspecified` using the `!` sign.
42+ Unspecified,
43+}
44+
45+/// Name an attribute and describe it's assigned state.
2446 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
25−pub struct MatchGroup<T: match_group::Tag> {
47+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
48+pub struct Assignment {
49+ /// The name of the attribute.
50+ pub name: CompactStr,
51+ /// The state of the attribute.
52+ pub state: State,
53+}
54+
55+/// A grouping of lists of patterns while possibly keeping associated to their base path.
56+///
57+/// Pattern lists with base path are queryable relative to that base, otherwise they are relative to the repository root.
58+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)]
59+pub struct MatchGroup<T: match_group::Pattern = Attributes> {
2660 /// A list of pattern lists, each representing a patterns from a file or specified by hand, in the order they were
2761 /// specified in.
2862 ///
@@ -30,75 +64,36 @@ pub struct MatchGroup<T: match_group::Tag> { · 1 unmodified line
3064 pub patterns: Vec<PatternList<T>>,
3165 }
3266
33−/// A list of patterns with an optional names, for matching against it.
34−#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
35−pub struct PatternList<T: match_group::Tag> {
36− /// Patterns and their associated data in the order they were loaded in or specified.
67+/// A list of patterns which optionally know where they were loaded from and what their base is.
68+///
69+/// Knowing their base which is relative to a source directory, it will ignore all path to match against
70+/// that don't also start with said base.
71+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)]
72+pub struct PatternList<T: match_group::Pattern> {
73+ /// Patterns and their associated data in the order they were loaded in or specified,
74+ /// the line number in its source file or its sequence number (_`(pattern, value, line_number)`_).
3775 ///
3876 /// During matching, this order is reversed.
39− pub patterns: Vec<(git_glob::Pattern, T::Value)>,
77+ pub patterns: Vec<PatternMapping<T::Value>>,
4078
41− /// The path at which the patterns are located in a format suitable for matches, or `None` if the patterns
42− /// are relative to the worktree root.
43− base: Option<BString>,
44−}
45−
46−mod match_group {
47− use crate::{MatchGroup, PatternList};
48− use std::ffi::OsString;
49− use std::path::PathBuf;
50−
51− /// A marker trait to identify the type of a description.
52− pub trait Tag: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd {
53− /// The value associated with a pattern.
54− type Value: PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Clone;
55− }
56−
57− /// Identify ignore patterns.
58− #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
59− pub struct Ignore;
60− impl Tag for Ignore {
61− type Value = ();
62− }
79+ /// The path from which the patterns were read, or `None` if the patterns
80+ /// don't originate in a file on disk.
81+ pub source: Option<PathBuf>,
6382
64− /// Identify patterns with attributes.
65− #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
66− pub struct Attributes;
67− impl Tag for Attributes {
68− /// TODO: identify the actual value, should be name/State pairs, but there is the question of storage.
69− type Value = ();
70− }
71−
72− impl MatchGroup<Ignore> {
73− /// See [PatternList::<Ignore>::from_overrides()] for details.
74− pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
75− MatchGroup {
76− patterns: vec![PatternList::<Ignore>::from_overrides(patterns)],
77− }
78− }
79− }
83+ /// The parent directory of source, or `None` if the patterns are _global_ to match against the repository root.
84+ /// It's processed to contain slashes only and to end with a trailing slash, and is relative to the repository root.
85+ pub base: Option<BString>,
86+}
8087
81− impl PatternList<Ignore> {
82− /// Parse a list of patterns, using slashes as path separators
83− pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
84− PatternList {
85− patterns: patterns
86− .into_iter()
87− .map(Into::into)
88− .filter_map(|pattern| {
89− let pattern = git_features::path::into_bytes(PathBuf::from(pattern)).ok()?;
90− git_glob::parse(pattern.as_ref()).map(|p| (p, ()))
91− })
92− .collect(),
93− base: None,
94− }
95− }
96− }
88+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
89+pub struct PatternMapping<T> {
90+ pub pattern: git_glob::Pattern,
91+ pub value: T,
92+ pub sequence_number: usize,
9793 }
98−pub use match_group::{Attributes, Ignore, Tag};
9994
100−pub type Files = MatchGroup<Attributes>;
101−pub type IgnoreFiles = MatchGroup<Ignore>;
95+mod match_group;
96+pub use match_group::{Attributes, Ignore, Match, Pattern};
10297
10398 pub mod parse;
10499
git-attributes/src/match_group.rs+354−0
@@ -0,0 +1,354 @@
1+use crate::{Assignment, MatchGroup, PatternList, PatternMapping, State, StateRef};
2+use bstr::{BStr, BString, ByteSlice, ByteVec};
3+use std::ffi::OsString;
4+use std::io::Read;
5+use std::path::{Path, PathBuf};
6+
7+impl<'a> From<StateRef<'a>> for State {
8+ fn from(s: StateRef<'a>) -> Self {
9+ match s {
10+ StateRef::Value(v) => State::Value(v.to_str().expect("no illformed unicode").into()),
11+ StateRef::Set => State::Set,
12+ StateRef::Unset => State::Unset,
13+ StateRef::Unspecified => State::Unspecified,
14+ }
15+ }
16+}
17+
18+fn attrs_to_assignments<'a>(
19+ attrs: impl Iterator<Item = Result<(&'a BStr, StateRef<'a>), crate::parse::Error>>,
20+) -> Result<Vec<Assignment>, crate::parse::Error> {
21+ attrs
22+ .map(|res| {
23+ res.map(|(name, state)| Assignment {
24+ name: name.to_str().expect("no illformed unicode").into(),
25+ state: state.into(),
26+ })
27+ })
28+ .collect()
29+}
30+
31+/// A marker trait to identify the type of a description.
32+pub trait Pattern: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Default {
33+ /// The value associated with a pattern.
34+ type Value: PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Clone;
35+
36+ /// Parse all patterns in `bytes` line by line, ignoring lines with errors, and collect them.
37+ fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>>;
38+
39+ fn use_pattern(pattern: &git_glob::Pattern) -> bool;
40+}
41+
42+/// Identify ignore patterns.
43+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)]
44+pub struct Ignore;
45+
46+impl Pattern for Ignore {
47+ type Value = ();
48+
49+ fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>> {
50+ crate::parse::ignore(bytes)
51+ .map(|(pattern, line_number)| PatternMapping {
52+ pattern,
53+ value: (),
54+ sequence_number: line_number,
55+ })
56+ .collect()
57+ }
58+
59+ fn use_pattern(_pattern: &git_glob::Pattern) -> bool {
60+ true
61+ }
62+}
63+
64+/// A value of an attribute pattern, which is either a macro definition or
65+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
66+pub enum Value {
67+ MacroAttributes(Vec<Assignment>),
68+ Attributes(Vec<Assignment>),
69+}
70+
71+/// Identify patterns with attributes.
72+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)]
73+pub struct Attributes;
74+
75+impl Pattern for Attributes {
76+ type Value = Value;
77+
78+ fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>> {
79+ crate::parse(bytes)
80+ .filter_map(Result::ok)
81+ .filter_map(|(pattern_kind, attrs, line_number)| {
82+ let (pattern, value) = match pattern_kind {
83+ crate::parse::Kind::Macro(macro_name) => (
84+ git_glob::Pattern {
85+ text: macro_name,
86+ mode: git_glob::pattern::Mode::all(),
87+ first_wildcard_pos: None,
88+ },
89+ Value::MacroAttributes(attrs_to_assignments(attrs).ok()?),
90+ ),
91+ crate::parse::Kind::Pattern(p) => (
92+ (!p.is_negative()).then(|| p)?,
93+ Value::Attributes(attrs_to_assignments(attrs).ok()?),
94+ ),
95+ };
96+ PatternMapping {
97+ pattern,
98+ value,
99+ sequence_number: line_number,
100+ }
101+ .into()
102+ })
103+ .collect()
104+ }
105+
106+ fn use_pattern(pattern: &git_glob::Pattern) -> bool {
107+ pattern.mode != git_glob::pattern::Mode::all()
108+ }
109+}
110+
111+/// Describes a matching value within a [`MatchGroup`].
112+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
113+pub struct Match<'a, T> {
114+ pub pattern: &'a git_glob::Pattern,
115+ /// The value associated with the pattern.
116+ pub value: &'a T,
117+ /// The path to the source from which the pattern was loaded, or `None` if it was specified by other means.
118+ pub source: Option<&'a Path>,
119+ /// The line at which the pattern was found in its `source` file, or the occurrence in which it was provided.
120+ pub sequence_number: usize,
121+}
122+
123+impl<T> MatchGroup<T>
124+where
125+ T: Pattern,
126+{
127+ /// Match `relative_path`, a path relative to the repository containing all patterns, and return the first match if available.
128+ // TODO: better docs
129+ pub fn pattern_matching_relative_path<'a>(
130+ &self,
131+ relative_path: impl Into<&'a BStr>,
132+ is_dir: Option<bool>,
133+ case: git_glob::pattern::Case,
134+ ) -> Option<Match<'_, T::Value>> {
135+ let relative_path = relative_path.into();
136+ let basename_pos = relative_path.rfind(b"/").map(|p| p + 1);
137+ self.patterns
138+ .iter()
139+ .rev()
140+ .find_map(|pl| pl.pattern_matching_relative_path(relative_path, basename_pos, is_dir, case))
141+ }
142+}
143+
144+impl MatchGroup<Ignore> {
145+ /// Given `git_dir`, a `.git` repository, load ignore patterns from `info/exclude` and from `excludes_file` if it
146+ /// is provided.
147+ /// Note that it's not considered an error if the provided `excludes_file` does not exist.
148+ pub fn from_git_dir(
149+ git_dir: impl AsRef<Path>,
150+ excludes_file: Option<PathBuf>,
151+ buf: &mut Vec<u8>,
152+ ) -> std::io::Result<Self> {
153+ let mut group = Self::default();
154+
155+ let follow_symlinks = true;
156+ // order matters! More important ones first.
157+ group.patterns.extend(
158+ excludes_file
159+ .map(|file| PatternList::<Ignore>::from_file(file, None, follow_symlinks, buf))
160+ .transpose()?
161+ .flatten(),
162+ );
163+ group.patterns.extend(PatternList::<Ignore>::from_file(
164+ git_dir.as_ref().join("info").join("exclude"),
165+ None,
166+ follow_symlinks,
167+ buf,
168+ )?);
169+ Ok(group)
170+ }
171+
172+ /// See [PatternList::<Ignore>::from_overrides()] for details.
173+ pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
174+ MatchGroup {
175+ patterns: vec![PatternList::<Ignore>::from_overrides(patterns)],
176+ }
177+ }
178+
179+ /// Add the given file at `source` if it exists, otherwise do nothing. If a `root` is provided, it's not considered a global file anymore.
180+ /// Returns true if the file was added, or false if it didn't exist.
181+ pub fn add_patterns_file(
182+ &mut self,
183+ source: impl Into<PathBuf>,
184+ follow_symlinks: bool,
185+ root: Option<&Path>,
186+ buf: &mut Vec<u8>,
187+ ) -> std::io::Result<bool> {
188+ let previous_len = self.patterns.len();
189+ self.patterns.extend(PatternList::<Ignore>::from_file(
190+ source.into(),
191+ root,
192+ follow_symlinks,
193+ buf,
194+ )?);
195+ Ok(self.patterns.len() != previous_len)
196+ }
197+
198+ pub fn add_patterns_buffer(&mut self, bytes: &[u8], source: impl Into<PathBuf>, root: Option<&Path>) {
199+ self.patterns
200+ .push(PatternList::<Ignore>::from_bytes(bytes, source.into(), root));
201+ }
202+}
203+
204+fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec<u8>) -> std::io::Result<bool> {
205+ buf.clear();
206+ let file = if follow_symlinks {
207+ std::fs::File::open(path)
208+ } else {
209+ git_features::fs::open_options_no_follow().read(true).open(path)
210+ };
211+ Ok(match file {
212+ Ok(mut file) => {
213+ file.read_to_end(buf)?;
214+ true
215+ }
216+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
217+ Err(err) => return Err(err),
218+ })
219+}
220+
221+impl<T> PatternList<T>
222+where
223+ T: Pattern,
224+{
225+ /// `source` is the location of the `bytes` which represent a list of patterns line by line.
226+ pub fn from_bytes(bytes: &[u8], source: impl Into<PathBuf>, root: Option<&Path>) -> Self {
227+ let source = source.into();
228+ let patterns = T::bytes_to_patterns(bytes);
229+
230+ let base = root
231+ .and_then(|root| source.parent().expect("file").strip_prefix(root).ok())
232+ .and_then(|base| {
233+ (!base.as_os_str().is_empty()).then(|| {
234+ let mut base: BString =
235+ git_path::to_unix_separators_on_windows(git_path::into_bstr(base)).into_owned();
236+
237+ base.push_byte(b'/');
238+ base
239+ })
240+ });
241+ PatternList {
242+ patterns,
243+ source: Some(source),
244+ base,
245+ }
246+ }
247+ pub fn from_file(
248+ source: impl Into<PathBuf>,
249+ root: Option<&Path>,
250+ follow_symlinks: bool,
251+ buf: &mut Vec<u8>,
252+ ) -> std::io::Result<Option<Self>> {
253+ let source = source.into();
254+ Ok(read_in_full_ignore_missing(&source, follow_symlinks, buf)?.then(|| Self::from_bytes(buf, source, root)))
255+ }
256+}
257+
258+impl<T> PatternList<T>
259+where
260+ T: Pattern,
261+{
262+ pub fn pattern_matching_relative_path(
263+ &self,
264+ relative_path: &BStr,
265+ basename_pos: Option<usize>,
266+ is_dir: Option<bool>,
267+ case: git_glob::pattern::Case,
268+ ) -> Option<Match<'_, T::Value>> {
269+ let (relative_path, basename_start_pos) =
270+ self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?;
271+ self.patterns
272+ .iter()
273+ .rev()
274+ .filter(|pm| T::use_pattern(&pm.pattern))
275+ .find_map(
276+ |PatternMapping {
277+ pattern,
278+ value,
279+ sequence_number,
280+ }| {
281+ pattern
282+ .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case)
283+ .then(|| Match {
284+ pattern,
285+ value,
286+ source: self.source.as_deref(),
287+ sequence_number: *sequence_number,
288+ })
289+ },
290+ )
291+ }
292+
293+ pub fn pattern_idx_matching_relative_path(
294+ &self,
295+ relative_path: &BStr,
296+ basename_pos: Option<usize>,
297+ is_dir: Option<bool>,
298+ case: git_glob::pattern::Case,
299+ ) -> Option<usize> {
300+ let (relative_path, basename_start_pos) =
301+ self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?;
302+ self.patterns
303+ .iter()
304+ .enumerate()
305+ .rev()
306+ .filter(|(_, pm)| T::use_pattern(&pm.pattern))
307+ .find_map(|(idx, pm)| {
308+ pm.pattern
309+ .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case)
310+ .then(|| idx)
311+ })
312+ }
313+
314+ fn strip_base_handle_recompute_basename_pos<'a>(
315+ &self,
316+ relative_path: &'a BStr,
317+ basename_pos: Option<usize>,
318+ ) -> Option<(&'a BStr, Option<usize>)> {
319+ match self.base.as_deref() {
320+ Some(base) => (
321+ relative_path.strip_prefix(base.as_slice())?.as_bstr(),
322+ basename_pos.and_then(|pos| {
323+ let pos = pos - base.len();
324+ (pos != 0).then(|| pos)
325+ }),
326+ ),
327+ None => (relative_path, basename_pos),
328+ }
329+ .into()
330+ }
331+}
332+
333+impl PatternList<Ignore> {
334+ /// Parse a list of patterns, using slashes as path separators
335+ pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
336+ PatternList {
337+ patterns: patterns
338+ .into_iter()
339+ .map(Into::into)
340+ .enumerate()
341+ .filter_map(|(seq_id, pattern)| {
342+ let pattern = git_path::try_into_bstr(PathBuf::from(pattern)).ok()?;
343+ git_glob::parse(pattern.as_ref()).map(|p| PatternMapping {
344+ pattern: p,
345+ value: (),
346+ sequence_number: seq_id,
347+ })
348+ })
349+ .collect(),
350+ source: None,
351+ base: None,
352+ }
353+ }
354+}
git-attributes/src/parse/attribute.rs+6−6
@@ -56,20 +56,20 @@ impl<'a> Iter<'a> { · 55 unmodified lines
5656 }
5757 }
5858
59− fn parse_attr(&self, attr: &'a [u8]) -> Result<(&'a BStr, crate::State<'a>), Error> {
59+ fn parse_attr(&self, attr: &'a [u8]) -> Result<(&'a BStr, crate::StateRef<'a>), Error> {
6060 let mut tokens = attr.splitn(2, |b| *b == b'=');
6161 let attr = tokens.next().expect("attr itself").as_bstr();
6262 let possibly_value = tokens.next();
6363 let (attr, state) = if attr.first() == Some(&b'-') {
64− (&attr[1..], crate::State::Unset)
64+ (&attr[1..], crate::StateRef::Unset)
6565 } else if attr.first() == Some(&b'!') {
66− (&attr[1..], crate::State::Unspecified)
66+ (&attr[1..], crate::StateRef::Unspecified)
6767 } else {
6868 (
6969 attr,
7070 possibly_value
71− .map(|v| crate::State::Value(v.as_bstr()))
72− .unwrap_or(crate::State::Set),
71+ .map(|v| crate::StateRef::Value(v.as_bstr()))
72+ .unwrap_or(crate::StateRef::Set),
7373 )
7474 };
7575 Ok((check_attr(attr, self.line_no)?, state))
@@ -95,7 +95,7 @@ fn check_attr(attr: &BStr, line_number: usize) -> Result<&BStr, Error> { · 19 unmodified lines
9595 }
9696
9797 impl<'a> Iterator for Iter<'a> {
98− type Item = Result<(&'a BStr, crate::State<'a>), Error>;
98+ type Item = Result<(&'a BStr, crate::StateRef<'a>), Error>;
9999
100100 fn next(&mut self) -> Option<Self::Item> {
101101 let attr = self.attrs.next().filter(|a| !a.is_empty())?;
git-attributes/tests/attributes.rs+1−0
@@ -1,2 +1,3 @@
1+pub use git_testtools::Result;
12 mod match_group;
23 mod parse;
git-attributes/tests/fixtures/make_global_and_external_and_dir_ignores.sh+77−0
@@ -0,0 +1,77 @@
1+#!/bin/bash
2+set -eu -o pipefail
3+
4+cat <<EOF >user.exclude
5+# a custom exclude configured per user
6+user-file-anywhere
7+/user-file-from-top
8+
9+user-dir-anywhere/
10+/user-dir-from-top
11+
12+user-subdir/file
13+**/user-subdir-anywhere/file
14+EOF
15+
16+mkdir repo;
17+(cd repo
18+ git init -q
19+ git config core.excludesFile ../user.exclude
20+
21+ cat <<EOF >.git/info/exclude
22+# a sample .git/info/exclude
23+file-anywhere
24+/file-from-top
25+
26+dir-anywhere/
27+/dir-from-top
28+
29+subdir/file
30+**/subdir-anywhere/file
31+EOF
32+
33+ cat <<EOF >.gitignore
34+# a sample .gitignore
35+top-level-local-file-anywhere
36+EOF
37+
38+ mkdir dir-with-ignore
39+ cat <<EOF >dir-with-ignore/.gitignore
40+# a sample .gitignore
41+sub-level-local-file-anywhere
42+EOF
43+
44+ git add .gitignore dir-with-ignore
45+ git commit --allow-empty -m "init"
46+
47+ mkdir user-dir-anywhere user-dir-from-top dir-anywhere dir-from-top
48+ mkdir -p dir/user-dir-anywhere dir/dir-anywhere
49+
50+ git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || :
51+user-file-anywhere
52+dir/user-file-anywhere
53+user-file-from-top
54+no-match/user-file-from-top
55+user-dir-anywhere
56+user-dir-from-top
57+no-match/user-dir-from-top
58+user-subdir/file
59+subdir/user-subdir-anywhere/file
60+file-anywhere
61+dir/file-anywhere
62+file-from-top
63+no-match/file-from-top
64+dir-anywhere
65+dir/dir-anywhere
66+dir-from-top
67+no-match/dir-from-top
68+subdir/file
69+subdir/subdir-anywhere/file
70+top-level-local-file-anywhere
71+dir/top-level-local-file-anywhere
72+no-match/sub-level-local-file-anywhere
73+dir-with-ignore/sub-level-local-file-anywhere
74+dir-with-ignore/sub-dir/sub-level-local-file-anywhere
75+EOF
76+
77+)
git-attributes/tests/match_group/mod.rs+108−5
@@ -1,14 +1,117 @@
11 mod ignore {
2− use git_attributes::Ignore;
2+ use bstr::{BStr, ByteSlice};
3+ use git_attributes::{Ignore, Match, MatchGroup};
4+ use git_glob::pattern::Case;
5+ use std::io::Read;
6+
7+ struct Expectations<'a> {
8+ lines: bstr::Lines<'a>,
9+ }
10+
11+ impl<'a> Iterator for Expectations<'a> {
12+ type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>);
13+
14+ fn next(&mut self) -> Option<Self::Item> {
15+ let line = self.lines.next()?;
16+ let (left, value) = line.split_at(line.find_byte(b'\t').unwrap());
17+ let value = value[1..].as_bstr();
18+
19+ let source_and_line = if left == b"::" {
20+ None
21+ } else {
22+ let mut tokens = left.split(|b| *b == b':');
23+ let source = tokens.next().unwrap().as_bstr();
24+ let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap();
25+ let pattern = tokens.next().unwrap().as_bstr();
26+ Some((source, line_number, pattern))
27+ };
28+ Some((value, source_and_line))
29+ }
30+ }
31+
32+ #[test]
33+ fn from_git_dir() -> crate::Result {
34+ let dir = git_testtools::scripted_fixture_repo_read_only("make_global_and_external_and_dir_ignores.sh")?;
35+ let repo_dir = dir.join("repo");
36+ let git_dir = repo_dir.join(".git");
37+ let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?;
38+ let mut buf = Vec::new();
39+ let mut group = MatchGroup::from_git_dir(git_dir, Some(dir.join("user.exclude")), &mut buf)?;
40+
41+ assert!(
42+ !group.add_patterns_file("not-a-file", false, None, &mut buf)?,
43+ "missing files are no problem and cause a negative response"
44+ );
45+ assert!(
46+ group.add_patterns_file(repo_dir.join(".gitignore"), true, repo_dir.as_path().into(), &mut buf)?,
47+ "existing files return true"
48+ );
49+
50+ buf.clear();
51+ let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore");
52+ std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?;
53+ group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into());
54+
55+ for (path, source_and_line) in (Expectations {
56+ lines: baseline.lines(),
57+ }) {
58+ let actual = group.pattern_matching_relative_path(
59+ path,
60+ repo_dir
61+ .join(path.to_str_lossy().as_ref())
62+ .metadata()
63+ .ok()
64+ .map(|m| m.is_dir()),
65+ Case::Sensitive,
66+ );
67+ match (actual, source_and_line) {
68+ (
69+ Some(Match {
70+ sequence_number,
71+ pattern: _,
72+ source,
73+ value: _,
74+ }),
75+ Some((expected_source, line, _expected_pattern)),
76+ ) => {
77+ assert_eq!(sequence_number, line, "our counting should match the one used in git");
78+ assert_eq!(
79+ source.map(|p| p.canonicalize().unwrap()),
80+ Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?)
81+ );
82+ }
83+ (None, None) => {}
84+ (actual, expected) => panic!("actual {:?} should match {:?} with path '{}'", actual, expected, path),
85+ }
86+ }
87+ Ok(())
88+ }
389
490 #[test]
5− fn init_from_overrides() {
91+ fn from_overrides() {
692 let input = ["simple", "pattern/"];
7− let patterns = git_attributes::MatchGroup::<Ignore>::from_overrides(input).patterns;
8− assert_eq!(patterns.len(), 1);
93+ let group = git_attributes::MatchGroup::<Ignore>::from_overrides(input);
94+ assert_eq!(
95+ group.pattern_matching_relative_path("Simple", None, git_glob::pattern::Case::Fold),
96+ Some(pattern_to_match(&git_glob::parse("simple").unwrap(), 0))
97+ );
98+ assert_eq!(
99+ group.pattern_matching_relative_path("pattern", Some(true), git_glob::pattern::Case::Sensitive),
100+ Some(pattern_to_match(&git_glob::parse("pattern/").unwrap(), 1))
101+ );
102+ assert_eq!(group.patterns.len(), 1);
9103 assert_eq!(
10104 git_attributes::PatternList::<Ignore>::from_overrides(input),
11− patterns.into_iter().next().unwrap()
105+ group.patterns.into_iter().next().unwrap()
12106 );
13107 }
108+
109+ fn pattern_to_match(pattern: &git_glob::Pattern, sequence_number: usize) -> Match<'_, ()> {
110+ Match {
111+ pattern,
112+ value: &(),
113+ source: None,
114+ sequence_number,
115+ }
116+ }
14117 }
git-attributes/tests/parse/attribute.rs+11−11
@@ -1,5 +1,5 @@
11 use bstr::{BStr, ByteSlice};
2−use git_attributes::{parse, State};
2+use git_attributes::{parse, StateRef};
33 use git_glob::pattern::Mode;
44 use git_testtools::fixture_bytes;
55
@@ -30,7 +30,7 @@ fn line_numbers_are_counted_correctly() { · 24 unmodified lines
3030 (pattern(r"!foo.html", Mode::NO_SUB_DIR, None), vec![set("x")], 8),
3131 (pattern(r"#a/path", Mode::empty(), None), vec![unset("a")], 10),
3232 (
33− pattern(r"/*", Mode::ABSOLUTE | Mode::NO_SUB_DIR | Mode::ENDS_WITH, Some(1)),
33+ pattern(r"*", Mode::ABSOLUTE | Mode::NO_SUB_DIR | Mode::ENDS_WITH, Some(0)),
3434 vec![unspecified("b")],
3535 11
3636 ),
@@ -249,22 +249,22 @@ fn trailing_whitespace_in_attributes_is_ignored() { · 212 unmodified lines
249249 );
250250 }
251251
252−type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::State<'a>)>, usize);
252+type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::StateRef<'a>)>, usize);
253253
254−fn set(attr: &str) -> (&BStr, State) {
255− (attr.as_bytes().as_bstr(), State::Set)
254+fn set(attr: &str) -> (&BStr, StateRef) {
255+ (attr.as_bytes().as_bstr(), StateRef::Set)
256256 }
257257
258−fn unset(attr: &str) -> (&BStr, State) {
259− (attr.as_bytes().as_bstr(), State::Unset)
258+fn unset(attr: &str) -> (&BStr, StateRef) {
259+ (attr.as_bytes().as_bstr(), StateRef::Unset)
260260 }
261261
262−fn unspecified(attr: &str) -> (&BStr, State) {
263− (attr.as_bytes().as_bstr(), State::Unspecified)
262+fn unspecified(attr: &str) -> (&BStr, StateRef) {
263+ (attr.as_bytes().as_bstr(), StateRef::Unspecified)
264264 }
265265
266−fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, State<'b>) {
267− (attr.as_bytes().as_bstr(), State::Value(value.as_bytes().as_bstr()))
266+fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, StateRef<'b>) {
267+ (attr.as_bytes().as_bstr(), StateRef::Value(value.as_bytes().as_bstr()))
268268 }
269269
270270 fn pattern(name: &str, flags: git_glob::pattern::Mode, first_wildcard_pos: Option<usize>) -> parse::Kind {
git-attributes/tests/parse/ignore.rs+4−4
@@ -20,10 +20,10 @@ fn line_numbers_are_counted_correctly() { · 19 unmodified lines
2020 ("*.[oa]".into(), Mode::NO_SUB_DIR, 2),
2121 ("*.html".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH, 5),
2222 ("foo.html".into(), Mode::NO_SUB_DIR | Mode::NEGATIVE, 8),
23− ("/*".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH | Mode::ABSOLUTE, 11),
24− ("/foo".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR | Mode::ABSOLUTE, 12),
25− ("/foo/*".into(), Mode::ABSOLUTE, 13),
26− ("/foo/bar".into(), Mode::ABSOLUTE | Mode::NEGATIVE, 14)
23+ ("*".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH | Mode::ABSOLUTE, 11),
24+ ("foo".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR | Mode::ABSOLUTE, 12),
25+ ("foo/*".into(), Mode::ABSOLUTE, 13),
26+ ("foo/bar".into(), Mode::ABSOLUTE | Mode::NEGATIVE, 14)
2727 ]
2828 );
2929 }
git-config/Cargo.toml+2−1
@@ -15,6 +15,7 @@ include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] · 14 unmodified lines
1515
1616 [dependencies]
1717 git-features = { version = "^0.20.0", path = "../git-features"}
18+git-path = { version = "^0.1.0", path = "../git-path" }
1819 git-sec = { version = "^0.1.0", path = "../git-sec" }
1920
2021 dirs = "4"
@@ -22,7 +23,7 @@ nom = { version = "7", default_features = false, features = [ "std" ] } · 1 unmodified line
2223 memchr = "2"
2324 serde_crate = { version = "1", package = "serde", optional = true }
2425 pwd = "1.3.1"
25−quick-error = "2.0.0"
26+thiserror = "1.0.26"
2627 unicode-bom = "1.1.4"
2728 bstr = { version = "0.2.13", default-features = false, features = ["std"] }
2829
git-config/src/file/error.rs+0−35
@@ -1,35 +0,0 @@
1−use std::{error::Error, fmt::Display};
2−
3−use crate::parser::SectionHeaderName;
4−// TODO Consider replacing with quick_error
5−/// All possible error types that may occur from interacting with
6−/// [`GitConfig`](super::GitConfig).
7−#[allow(clippy::module_name_repetitions)]
8−#[derive(PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Debug)]
9−pub enum GitConfigError<'a> {
10− /// The requested section does not exist.
11− SectionDoesNotExist(SectionHeaderName<'a>),
12− /// The requested subsection does not exist.
13− SubSectionDoesNotExist(Option<&'a str>),
14− /// The key does not exist in the requested section.
15− KeyDoesNotExist,
16− /// The conversion into the provided type for methods such as
17− /// [`GitConfig::value`](super::GitConfig::value) failed.
18− FailedConversion,
19−}
20−
21−impl Display for GitConfigError<'_> {
22− fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23− match self {
24− Self::SectionDoesNotExist(s) => write!(f, "Section '{}' does not exist.", s),
25− Self::SubSectionDoesNotExist(s) => match s {
26− Some(s) => write!(f, "Subsection '{}' does not exist.", s),
27− None => write!(f, "Top level section does not exist."),
28− },
29− Self::KeyDoesNotExist => write!(f, "The name for a value provided does not exist."),
30− Self::FailedConversion => write!(f, "Failed to convert to specified type."),
31− }
32− }
33−}
34−
35−impl Error for GitConfigError<'_> {}
git-config/src/file/git_config.rs+191−171
@@ -1,3 +1,4 @@
1+use bstr::BStr;
12 use std::{
23 borrow::Cow,
34 collections::{HashMap, VecDeque},
@@ -8,17 +9,16 @@ use std::{ · 4 unmodified lines
89
910 use crate::{
1011 file::{
11− error::GitConfigError,
1212 section::{MutableSection, SectionBody},
1313 value::{EntryData, MutableMultiValue, MutableValue},
1414 Index, Size,
1515 },
16− parser,
16+ lookup, parser,
1717 parser::{
1818 parse_from_bytes, parse_from_path, parse_from_str, Error, Event, Key, ParsedSectionHeader, Parser,
1919 SectionHeaderName,
2020 },
21− values,
21+ value, values,
2222 };
2323
2424 /// The section ID is a monotonically increasing ID used to refer to sections.
@@ -61,7 +61,7 @@ pub(super) enum LookupTreeNode<'a> { · 36 unmodified lines
6161 ///
6262 /// `git` is flexible enough to allow users to set a key multiple times in
6363 /// any number of identically named sections. When this is the case, the key
64−/// is known as a "multivar". In this case, `get_raw_value` follows the
64+/// is known as a "multivar". In this case, `raw_value` follows the
6565 /// "last one wins" approach that `git-config` internally uses for multivar
6666 /// resolution.
6767 ///
@@ -78,7 +78,7 @@ pub(super) enum LookupTreeNode<'a> { · 10 unmodified lines
7878 /// e = f g h
7979 /// ```
8080 ///
81−/// Calling methods that fetch or set only one value (such as [`get_raw_value`])
81+/// Calling methods that fetch or set only one value (such as [`raw_value`])
8282 /// key `a` with the above config will fetch `d` or replace `d`, since the last
8383 /// valid config key/value pair is `a = d`:
8484 ///
@@ -87,14 +87,14 @@ pub(super) enum LookupTreeNode<'a> { · 2 unmodified lines
8787 /// # use std::borrow::Cow;
8888 /// # use std::convert::TryFrom;
8989 /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
90−/// assert_eq!(git_config.get_raw_value("core", None, "a"), Ok(Cow::Borrowed("d".as_bytes())));
90+/// assert_eq!(git_config.raw_value("core", None, "a").unwrap(), Cow::Borrowed("d".as_bytes()));
9191 /// ```
9292 ///
9393 /// Consider the `multi` variants of the methods instead, if you want to work
9494 /// with all values instead.
9595 ///
9696 /// [`ResolvedGitConfig`]: crate::file::ResolvedGitConfig
97−/// [`get_raw_value`]: Self::get_raw_value
97+/// [`raw_value`]: Self::raw_value
9898 #[derive(PartialEq, Eq, Clone, Debug, Default)]
9999 pub struct GitConfig<'event> {
100100 /// The list of events that occur before an actual section. Since a
@@ -121,32 +121,20 @@ pub struct GitConfig<'event> { · 20 unmodified lines
121121 pub mod from_paths {
122122 use std::borrow::Cow;
123123
124− use quick_error::quick_error;
125−
126124 use crate::{parser, values::path::interpolate};
127125
128− quick_error! {
129− #[derive(Debug)]
130− /// The error returned by [`GitConfig::from_paths()`][super::GitConfig::from_paths()] and [`GitConfig::from_env_paths()`][super::GitConfig::from_env_paths()].
131− #[allow(missing_docs)]
132− pub enum Error {
133− ParserOrIoError(err: parser::ParserOrIoError<'static>) {
134− display("Could not read config")
135− source(err)
136− from()
137− }
138− Interpolate(err: interpolate::Error) {
139− display("Could not interpolate path")
140− source(err)
141− from()
142− }
143− IncludeDepthExceeded { max_depth: u8 } {
144− display("The maximum allowed length {} of the file include chain built by following nested includes is exceeded", max_depth)
145− }
146− MissingConfigPath {
147− display("Include paths from environment variables must not be relative.")
148− }
149− }
126+ /// The error returned by [`GitConfig::from_paths()`][super::GitConfig::from_paths()] and [`GitConfig::from_env_paths()`][super::GitConfig::from_env_paths()].
127+ #[derive(Debug, thiserror::Error)]
128+ #[allow(missing_docs)]
129+ pub enum Error {
130+ #[error(transparent)]
131+ ParserOrIoError(#[from] parser::ParserOrIoError<'static>),
132+ #[error(transparent)]
133+ Interpolate(#[from] interpolate::Error),
134+ #[error("The maximum allowed length {} of the file include chain built by following nested includes is exceeded", .max_depth)]
135+ IncludeDepthExceeded { max_depth: u8 },
136+ #[error("Include paths from environment variables must not be relative")]
137+ MissingConfigPath,
150138 }
151139
152140 /// Options when loading git config using [`GitConfig::from_paths()`][super::GitConfig::from_paths()].
@@ -175,45 +163,30 @@ pub mod from_paths { · 22 unmodified lines
175163 }
176164
177165 pub mod from_env {
178− use quick_error::quick_error;
179−
180166 use super::from_paths;
181167 use crate::values::path::interpolate;
182168
183− quick_error! {
184− #[derive(Debug)]
185− /// Represents the errors that may occur when calling [`GitConfig::from_env`][crate::file::GitConfig::from_env()].
186− #[allow(missing_docs)]
187− pub enum Error {
188− ParseError (err: String) {
189− display("GIT_CONFIG_COUNT was not a positive integer: {}", err)
190− }
191− InvalidKeyId (key_id: usize) {
192− display("GIT_CONFIG_KEY_{} was not set.", key_id)
193− }
194− InvalidKeyValue (key_id: usize, key_val: String) {
195− display("GIT_CONFIG_KEY_{} was set to an invalid value: {}", key_id, key_val)
196− }
197− InvalidValueId (value_id: usize) {
198− display("GIT_CONFIG_VALUE_{} was not set.", value_id)
199− }
200− PathInterpolationError (err: interpolate::Error) {
201− display("Could not interpolate path while loading a config file.")
202− source(err)
203− from()
204− }
205− FromPathsError (err: from_paths::Error) {
206− display("Could not load config from a file")
207− source(err)
208− from()
209− }
210− }
169+ /// Represents the errors that may occur when calling [`GitConfig::from_env`][crate::file::GitConfig::from_env()].
170+ #[derive(Debug, thiserror::Error)]
171+ #[allow(missing_docs)]
172+ pub enum Error {
173+ #[error("GIT_CONFIG_COUNT was not a positive integer: {}", .input)]
174+ ParseError { input: String },
175+ #[error("GIT_CONFIG_KEY_{} was not set", .key_id)]
176+ InvalidKeyId { key_id: usize },
177+ #[error("GIT_CONFIG_KEY_{} was set to an invalid value: {}", .key_id, .key_val)]
178+ InvalidKeyValue { key_id: usize, key_val: String },
179+ #[error("GIT_CONFIG_VALUE_{} was not set", .value_id)]
180+ InvalidValueId { value_id: usize },
181+ #[error(transparent)]
182+ PathInterpolationError(#[from] interpolate::Error),
183+ #[error(transparent)]
184+ FromPathsError(#[from] from_paths::Error),
211185 }
212186 }
213187
214188 impl<'event> GitConfig<'event> {
215189 /// Constructs an empty `git-config` file.
216− #[inline]
217190 #[must_use]
218191 pub fn new() -> Self {
219192 Self::default()
@@ -225,7 +198,6 @@ impl<'event> GitConfig<'event> { · 5 unmodified lines
225198 ///
226199 /// Returns an error if there was an IO error or if the file wasn't a valid
227200 /// git-config file.
228− #[inline]
229201 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {
230202 parse_from_path(path).map(Self::from)
231203 }
@@ -239,7 +211,6 @@ impl<'event> GitConfig<'event> { · 7 unmodified lines
239211 /// git-config file.
240212 ///
241213 /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-FILES
242− #[inline]
243214 pub fn from_paths(paths: Vec<PathBuf>, options: &from_paths::Options) -> Result<Self, from_paths::Error> {
244215 let mut target = Self::new();
245216 for path in paths {
@@ -375,14 +346,16 @@ impl<'event> GitConfig<'event> { · 129 unmodified lines
375346 pub fn from_env(options: &from_paths::Options) -> Result<Option<Self>, from_env::Error> {
376347 use std::env;
377348 let count: usize = match env::var("GIT_CONFIG_COUNT") {
378− Ok(v) => v.parse().map_err(|_| from_env::Error::ParseError(v))?,
349+ Ok(v) => v.parse().map_err(|_| from_env::Error::ParseError { input: v })?,
379350 Err(_) => return Ok(None),
380351 };
381352
382353 let mut config = Self::new();
383354 for i in 0..count {
384− let key = env::var(format!("GIT_CONFIG_KEY_{}", i)).map_err(|_| from_env::Error::InvalidKeyId(i))?;
385− let value = env::var(format!("GIT_CONFIG_VALUE_{}", i)).map_err(|_| from_env::Error::InvalidValueId(i))?;
355+ let key =
356+ env::var(format!("GIT_CONFIG_KEY_{}", i)).map_err(|_| from_env::Error::InvalidKeyId { key_id: i })?;
357+ let value = env::var(format!("GIT_CONFIG_VALUE_{}", i))
358+ .map_err(|_| from_env::Error::InvalidValueId { value_id: i })?;
386359 if let Some((section_name, maybe_subsection)) = key.split_once('.') {
387360 let (subsection, key) = if let Some((subsection, key)) = maybe_subsection.rsplit_once('.') {
388361 (Some(subsection), key)
@@ -406,7 +379,10 @@ impl<'event> GitConfig<'event> { · 17 unmodified lines
406379 Cow::Owned(value.into_bytes()),
407380 );
408381 } else {
409− return Err(from_env::Error::InvalidKeyValue(i, key.to_string()));
382+ return Err(from_env::Error::InvalidKeyValue {
383+ key_id: i,
384+ key_val: key.to_string(),
385+ });
410386 }
411387 }
412388
@@ -432,7 +408,7 @@ impl<'event> GitConfig<'event> { · 19 unmodified lines
432408 /// # Examples
433409 ///
434410 /// ```
435− /// # use git_config::file::{GitConfig, GitConfigError};
411+ /// # use git_config::file::{GitConfig};
436412 /// # use git_config::values::{Integer, Boolean};
437413 /// # use std::borrow::Cow;
438414 /// # use std::convert::TryFrom;
@@ -457,15 +433,69 @@ impl<'event> GitConfig<'event> { · 18 unmodified lines
457433 ///
458434 /// [`values`]: crate::values
459435 /// [`TryFrom`]: std::convert::TryFrom
460− #[inline]
461− pub fn value<'lookup, T: TryFrom<Cow<'event, [u8]>>>(
436+ pub fn value<T: TryFrom<Cow<'event, [u8]>>>(
462437 &'event self,
463− section_name: &'lookup str,
464− subsection_name: Option<&'lookup str>,
465− key: &'lookup str,
466− ) -> Result<T, GitConfigError<'lookup>> {
467− T::try_from(self.get_raw_value(section_name, subsection_name, key)?)
468− .map_err(|_| GitConfigError::FailedConversion)
438+ section_name: &str,
439+ subsection_name: Option<&str>,
440+ key: &str,
441+ ) -> Result<T, lookup::Error<T::Error>> {
442+ T::try_from(self.raw_value(section_name, subsection_name, key)?).map_err(lookup::Error::FailedConversion)
443+ }
444+
445+ /// Like [`value()`][GitConfig::value()], but returning an `Option` if the value wasn't found.
446+ pub fn try_value<T: TryFrom<Cow<'event, [u8]>>>(
447+ &'event self,
448+ section_name: &str,
449+ subsection_name: Option<&str>,
450+ key: &str,
451+ ) -> Option<Result<T, T::Error>> {
452+ self.raw_value(section_name, subsection_name, key).ok().map(T::try_from)
453+ }
454+
455+ /// Like [`value()`][GitConfig::value()], but returning an `Option` if the string wasn't found.
456+ ///
457+ /// As strings perform no conversions, this will never fail.
458+ pub fn string(
459+ &'event self,
460+ section_name: &str,
461+ subsection_name: Option<&str>,
462+ key: &str,
463+ ) -> Option<Cow<'event, BStr>> {
464+ self.raw_value(section_name, subsection_name, key)
465+ .ok()
466+ .map(|v| values::String::from(v).value)
467+ }
468+
469+ /// Like [`value()`][GitConfig::value()], but returning an `Option` if the path wasn't found.
470+ ///
471+ /// Note that this path is not vetted and should only point to resources which can't be used
472+ /// to pose a security risk.
473+ ///
474+ /// As paths perform no conversions, this will never fail.
475+ // TODO: add `secure_path()` or similar to make use of our knowledge of the trust associated with each configuration
476+ // file, maybe even remove the insecure version to force every caller to ask themselves if the resource can
477+ // be used securely or not.
478+ pub fn path(
479+ &'event self,
480+ section_name: &str,
481+ subsection_name: Option<&str>,
482+ key: &str,
483+ ) -> Option<values::Path<'event>> {
484+ self.raw_value(section_name, subsection_name, key)
485+ .ok()
486+ .map(values::Path::from)
487+ }
488+
489+ /// Like [`value()`][GitConfig::value()], but returning an `Option` if the boolean wasn't found.
490+ pub fn boolean(
491+ &'event self,
492+ section_name: &str,
493+ subsection_name: Option<&str>,
494+ key: &str,
495+ ) -> Option<Result<bool, value::parse::Error>> {
496+ self.raw_value(section_name, subsection_name, key)
497+ .ok()
498+ .map(|v| values::Boolean::try_from(v).map(|b| b.to_bool()))
469499 }
470500
471501 /// Returns all interpreted values given a section, an optional subsection
@@ -481,7 +511,7 @@ impl<'event> GitConfig<'event> { · 9 unmodified lines
481511 /// # Examples
482512 ///
483513 /// ```
484− /// # use git_config::file::{GitConfig, GitConfigError};
514+ /// # use git_config::file::{GitConfig};
485515 /// # use git_config::values::{Integer, Bytes, Boolean, TrueVariant};
486516 /// # use std::borrow::Cow;
487517 /// # use std::convert::TryFrom;
@@ -507,7 +537,7 @@ impl<'event> GitConfig<'event> { · 19 unmodified lines
507537 /// // ... or explicitly declare the type to avoid the turbofish
508538 /// let c_value: Vec<Bytes> = git_config.multi_value("core", None, "c")?;
509539 /// assert_eq!(c_value, vec![Bytes { value: Cow::Borrowed(b"g") }]);
510− /// # Ok::<(), GitConfigError>(())
540+ /// # Ok::<(), Box<dyn std::error::Error>>(())
511541 /// ```
512542 ///
513543 /// # Errors
@@ -518,18 +548,17 @@ impl<'event> GitConfig<'event> { · 4 unmodified lines
518548 ///
519549 /// [`values`]: crate::values
520550 /// [`TryFrom`]: std::convert::TryFrom
521− #[inline]
522551 pub fn multi_value<'lookup, T: TryFrom<Cow<'event, [u8]>>>(
523552 &'event self,
524553 section_name: &'lookup str,
525554 subsection_name: Option<&'lookup str>,
526555 key: &'lookup str,
527− ) -> Result<Vec<T>, GitConfigError<'lookup>> {
528− self.get_raw_multi_value(section_name, subsection_name, key)?
556+ ) -> Result<Vec<T>, lookup::Error<T::Error>> {
557+ self.raw_multi_value(section_name, subsection_name, key)?
529558 .into_iter()
530559 .map(T::try_from)
531560 .collect::<Result<Vec<_>, _>>()
532− .map_err(|_| GitConfigError::FailedConversion)
561+ .map_err(lookup::Error::FailedConversion)
533562 }
534563
535564 /// Returns an immutable section reference.
@@ -542,15 +571,10 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
542571 &mut self,
543572 section_name: &'lookup str,
544573 subsection_name: Option<&'lookup str>,
545− ) -> Result<&SectionBody<'event>, GitConfigError<'lookup>> {
546− let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
547− let id = section_ids
548− .last()
549− .expect("Section lookup vec was empty, internal invariant violated");
550− Ok(self
551− .sections
552− .get(id)
553− .expect("Section did not have id from lookup, internal invariant violated"))
574+ ) -> Result<&SectionBody<'event>, lookup::existing::Error> {
575+ let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
576+ let id = section_ids.last().expect("BUG: Section lookup vec was empty");
577+ Ok(self.sections.get(id).expect("BUG: Section did not have id from lookup"))
554578 }
555579
556580 /// Returns an mutable section reference.
@@ -563,14 +587,14 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
563587 &mut self,
564588 section_name: &'lookup str,
565589 subsection_name: Option<&'lookup str>,
566− ) -> Result<MutableSection<'_, 'event>, GitConfigError<'lookup>> {
567− let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
568− let id = section_ids
569− .last()
570− .expect("Section lookup vec was empty, internal invariant violated");
571− Ok(MutableSection::new(self.sections.get_mut(id).expect(
572− "Section did not have id from lookup, internal invariant violated",
573− )))
590+ ) -> Result<MutableSection<'_, 'event>, lookup::existing::Error> {
591+ let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
592+ let id = section_ids.last().expect("BUG: Section lookup vec was empty");
593+ Ok(MutableSection::new(
594+ self.sections
595+ .get_mut(id)
596+ .expect("BUG: Section did not have id from lookup"),
597+ ))
574598 }
575599
576600 /// Gets all sections that match the provided name, ignoring any subsections.
@@ -591,7 +615,7 @@ impl<'event> GitConfig<'event> { · 14 unmodified lines
591615 /// Calling this method will yield all sections:
592616 ///
593617 /// ```
594− /// # use git_config::file::{GitConfig, GitConfigError};
618+ /// # use git_config::file::{GitConfig};
595619 /// # use git_config::values::{Integer, Boolean, TrueVariant};
596620 /// # use std::borrow::Cow;
597621 /// # use std::convert::TryFrom;
@@ -608,7 +632,7 @@ impl<'event> GitConfig<'event> { · 10 unmodified lines
608632 /// ```
609633 #[must_use]
610634 pub fn sections_by_name<'lookup>(&self, section_name: &'lookup str) -> Vec<&SectionBody<'event>> {
611− self.get_section_ids_by_name(section_name)
635+ self.section_ids_by_name(section_name)
612636 .unwrap_or_default()
613637 .into_iter()
614638 .map(|id| {
@@ -635,7 +659,7 @@ impl<'event> GitConfig<'event> { · 20 unmodified lines
635659 /// Calling this method will yield all section bodies and their header:
636660 ///
637661 /// ```rust
638− /// use git_config::file::{GitConfig, GitConfigError};
662+ /// use git_config::file::{GitConfig};
639663 /// use git_config::parser::Key;
640664 /// use std::borrow::Cow;
641665 /// use std::convert::TryFrom;
@@ -669,7 +693,7 @@ impl<'event> GitConfig<'event> { · 27 unmodified lines
669693 &self,
670694 section_name: &'lookup str,
671695 ) -> Vec<(&ParsedSectionHeader<'event>, &SectionBody<'event>)> {
672− self.get_section_ids_by_name(section_name)
696+ self.section_ids_by_name(section_name)
673697 .unwrap_or_default()
674698 .into_iter()
675699 .map(|id| {
@@ -694,7 +718,7 @@ impl<'event> GitConfig<'event> { · 18 unmodified lines
694718 /// Creating a new empty section:
695719 ///
696720 /// ```
697− /// # use git_config::file::{GitConfig, GitConfigError};
721+ /// # use git_config::file::{GitConfig};
698722 /// # use std::convert::TryFrom;
699723 /// let mut git_config = GitConfig::new();
700724 /// let _section = git_config.new_section("hello", Some("world".into()));
@@ -704,7 +728,7 @@ impl<'event> GitConfig<'event> { · 3 unmodified lines
704728 /// Creating a new empty section and adding values to it:
705729 ///
706730 /// ```
707− /// # use git_config::file::{GitConfig, GitConfigError};
731+ /// # use git_config::file::{GitConfig};
708732 /// # use std::convert::TryFrom;
709733 /// let mut git_config = GitConfig::new();
710734 /// let mut section = git_config.new_section("hello", Some("world".into()));
@@ -732,7 +756,7 @@ impl<'event> GitConfig<'event> { · 21 unmodified lines
732756 /// Creating and removing a section:
733757 ///
734758 /// ```
735− /// # use git_config::file::{GitConfig, GitConfigError};
759+ /// # use git_config::file::{GitConfig};
736760 /// # use std::convert::TryFrom;
737761 /// let mut git_config = GitConfig::try_from(
738762 /// r#"[hello "world"]
@@ -746,7 +770,7 @@ impl<'event> GitConfig<'event> { · 7 unmodified lines
746770 /// Precedence example for removing sections with the same name:
747771 ///
748772 /// ```
749− /// # use git_config::file::{GitConfig, GitConfigError};
773+ /// # use git_config::file::{GitConfig};
750774 /// # use std::convert::TryFrom;
751775 /// let mut git_config = GitConfig::try_from(
752776 /// r#"[hello "world"]
@@ -764,7 +788,7 @@ impl<'event> GitConfig<'event> { · 11 unmodified lines
764788 subsection_name: impl Into<Option<&'lookup str>>,
765789 ) -> Option<SectionBody> {
766790 let id = self
767− .get_section_ids_by_name_and_subname(section_name, subsection_name.into())
791+ .section_ids_by_name_and_subname(section_name, subsection_name.into())
768792 .ok()?
769793 .pop()?;
770794 self.section_order.remove(
@@ -817,8 +841,8 @@ impl<'event> GitConfig<'event> { · 46 unmodified lines
817841 subsection_name: impl Into<Option<&'lookup str>>,
818842 new_section_name: impl Into<SectionHeaderName<'event>>,
819843 new_subsection_name: impl Into<Option<Cow<'event, str>>>,
820− ) -> Result<(), GitConfigError<'lookup>> {
821− let id = self.get_section_ids_by_name_and_subname(section_name, subsection_name.into())?;
844+ ) -> Result<(), lookup::existing::Error> {
845+ let id = self.section_ids_by_name_and_subname(section_name, subsection_name.into())?;
822846 let id = id
823847 .last()
824848 .expect("list of sections were empty, which violates invariant");
@@ -855,25 +879,25 @@ impl<'event> GitConfig<'event> { · 30 unmodified lines
855879 /// Returns an uninterpreted value given a section, an optional subsection
856880 /// and key.
857881 ///
858− /// Consider [`Self::get_raw_multi_value`] if you want to get all values of
882+ /// Consider [`Self::raw_multi_value`] if you want to get all values of
859883 /// a multivar instead.
860884 ///
861885 /// # Errors
862886 ///
863887 /// This function will return an error if the key is not in the requested
864888 /// section and subsection, or if the section and subsection do not exist.
865− pub fn get_raw_value<'lookup>(
889+ pub fn raw_value<'lookup>(
866890 &self,
867891 section_name: &'lookup str,
868892 subsection_name: Option<&'lookup str>,
869893 key: &'lookup str,
870− ) -> Result<Cow<'_, [u8]>, GitConfigError<'lookup>> {
894+ ) -> Result<Cow<'_, [u8]>, lookup::existing::Error> {
871895 // Note: cannot wrap around the raw_multi_value method because we need
872896 // to guarantee that the highest section id is used (so that we follow
873897 // the "last one wins" resolution strategy by `git-config`).
874898 let key = Key(key.into());
875899 for section_id in self
876− .get_section_ids_by_name_and_subname(section_name, subsection_name)?
900+ .section_ids_by_name_and_subname(section_name, subsection_name)?
877901 .iter()
878902 .rev()
879903 {
@@ -887,26 +911,26 @@ impl<'event> GitConfig<'event> { · 7 unmodified lines
887911 }
888912 }
889913
890− Err(GitConfigError::KeyDoesNotExist)
914+ Err(lookup::existing::Error::KeyMissing)
891915 }
892916
893917 /// Returns a mutable reference to an uninterpreted value given a section,
894918 /// an optional subsection and key.
895919 ///
896− /// Consider [`Self::get_raw_multi_value_mut`] if you want to get mutable
920+ /// Consider [`Self::raw_multi_value_mut`] if you want to get mutable
897921 /// references to all values of a multivar instead.
898922 ///
899923 /// # Errors
900924 ///
901925 /// This function will return an error if the key is not in the requested
902926 /// section and subsection, or if the section and subsection do not exist.
903− pub fn get_raw_value_mut<'lookup>(
927+ pub fn raw_value_mut<'lookup>(
904928 &mut self,
905929 section_name: &'lookup str,
906930 subsection_name: Option<&'lookup str>,
907931 key: &'lookup str,
908− ) -> Result<MutableValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> {
909− let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
932+ ) -> Result<MutableValue<'_, 'lookup, 'event>, lookup::existing::Error> {
933+ let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
910934 let key = Key(key.into());
911935
912936 for section_id in section_ids.iter().rev() {
@@ -955,7 +979,7 @@ impl<'event> GitConfig<'event> { · 42 unmodified lines
955979 ));
956980 }
957981
958− Err(GitConfigError::KeyDoesNotExist)
982+ Err(lookup::existing::Error::KeyMissing)
959983 }
960984
961985 /// Returns all uninterpreted values given a section, an optional subsection
@@ -981,16 +1005,16 @@ impl<'event> GitConfig<'event> { · 19 unmodified lines
9811005 /// # use std::convert::TryFrom;
9821006 /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
9831007 /// assert_eq!(
984− /// git_config.get_raw_multi_value("core", None, "a"),
985− /// Ok(vec![
1008+ /// git_config.raw_multi_value("core", None, "a").unwrap(),
1009+ /// vec![
9861010 /// Cow::<[u8]>::Borrowed(b"b"),
9871011 /// Cow::<[u8]>::Borrowed(b"c"),
9881012 /// Cow::<[u8]>::Borrowed(b"d"),
989− /// ]),
1013+ /// ],
9901014 /// );
9911015 /// ```
9921016 ///
993− /// Consider [`Self::get_raw_value`] if you want to get the resolved single
1017+ /// Consider [`Self::raw_value`] if you want to get the resolved single
9941018 /// value for a given key, if your key does not support multi-valued values.
9951019 ///
9961020 /// # Errors
@@ -998,14 +1022,14 @@ impl<'event> GitConfig<'event> { · 1 unmodified line
9981022 /// This function will return an error if the key is not in any requested
9991023 /// section and subsection, or if no instance of the section and subsections
10001024 /// exist.
1001− pub fn get_raw_multi_value<'lookup>(
1025+ pub fn raw_multi_value(
10021026 &self,
1003− section_name: &'lookup str,
1004− subsection_name: Option<&'lookup str>,
1005− key: &'lookup str,
1006− ) -> Result<Vec<Cow<'_, [u8]>>, GitConfigError<'lookup>> {
1027+ section_name: &str,
1028+ subsection_name: Option<&str>,
1029+ key: &str,
1030+ ) -> Result<Vec<Cow<'_, [u8]>>, lookup::existing::Error> {
10071031 let mut values = vec![];
1008− for section_id in self.get_section_ids_by_name_and_subname(section_name, subsection_name)? {
1032+ for section_id in self.section_ids_by_name_and_subname(section_name, subsection_name)? {
10091033 values.extend(
10101034 self.sections
10111035 .get(&section_id)
@@ -1017,7 +1041,7 @@ impl<'event> GitConfig<'event> { · 5 unmodified lines
10171041 }
10181042
10191043 if values.is_empty() {
1020− Err(GitConfigError::KeyDoesNotExist)
1044+ Err(lookup::existing::Error::KeyMissing)
10211045 } else {
10221046 Ok(values)
10231047 }
@@ -1041,12 +1065,12 @@ impl<'event> GitConfig<'event> { · 17 unmodified lines
10411065 /// Attempting to get all values of `a` yields the following:
10421066 ///
10431067 /// ```
1044− /// # use git_config::file::{GitConfig, GitConfigError};
1068+ /// # use git_config::file::{GitConfig};
10451069 /// # use std::borrow::Cow;
10461070 /// # use std::convert::TryFrom;
10471071 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
10481072 /// assert_eq!(
1049− /// git_config.get_raw_multi_value("core", None, "a")?,
1073+ /// git_config.raw_multi_value("core", None, "a")?,
10501074 /// vec![
10511075 /// Cow::Borrowed(b"b"),
10521076 /// Cow::Borrowed(b"c"),
@@ -1054,20 +1078,20 @@ impl<'event> GitConfig<'event> { · 1 unmodified line
10541078 /// ]
10551079 /// );
10561080 ///
1057− /// git_config.get_raw_multi_value_mut("core", None, "a")?.set_str_all("g");
1081+ /// git_config.raw_multi_value_mut("core", None, "a")?.set_str_all("g");
10581082 ///
10591083 /// assert_eq!(
1060− /// git_config.get_raw_multi_value("core", None, "a")?,
1084+ /// git_config.raw_multi_value("core", None, "a")?,
10611085 /// vec![
10621086 /// Cow::Borrowed(b"g"),
10631087 /// Cow::Borrowed(b"g"),
10641088 /// Cow::Borrowed(b"g")
10651089 /// ],
10661090 /// );
1067− /// # Ok::<(), GitConfigError>(())
1091+ /// # Ok::<(), git_config::lookup::existing::Error>(())
10681092 /// ```
10691093 ///
1070− /// Consider [`Self::get_raw_value`] if you want to get the resolved single
1094+ /// Consider [`Self::raw_value`] if you want to get the resolved single
10711095 /// value for a given key, if your key does not support multi-valued values.
10721096 ///
10731097 /// Note that this operation is relatively expensive, requiring a full
@@ -1078,13 +1102,13 @@ impl<'event> GitConfig<'event> { · 4 unmodified lines
10781102 /// This function will return an error if the key is not in any requested
10791103 /// section and subsection, or if no instance of the section and subsections
10801104 /// exist.
1081− pub fn get_raw_multi_value_mut<'lookup>(
1105+ pub fn raw_multi_value_mut<'lookup>(
10821106 &mut self,
10831107 section_name: &'lookup str,
10841108 subsection_name: Option<&'lookup str>,
10851109 key: &'lookup str,
1086− ) -> Result<MutableMultiValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> {
1087− let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
1110+ ) -> Result<MutableMultiValue<'_, 'lookup, 'event>, lookup::existing::Error> {
1111+ let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
10881112 let key = Key(key.into());
10891113
10901114 let mut offsets = HashMap::new();
@@ -1125,7 +1149,7 @@ impl<'event> GitConfig<'event> { · 34 unmodified lines
11251149 entries.sort();
11261150
11271151 if entries.is_empty() {
1128− Err(GitConfigError::KeyDoesNotExist)
1152+ Err(lookup::existing::Error::KeyMissing)
11291153 } else {
11301154 Ok(MutableMultiValue::new(&mut self.sections, key, entries, offsets))
11311155 }
@@ -1148,13 +1172,13 @@ impl<'event> GitConfig<'event> { · 16 unmodified lines
11481172 /// Setting a new value to the key `core.a` will yield the following:
11491173 ///
11501174 /// ```
1151− /// # use git_config::file::{GitConfig, GitConfigError};
1175+ /// # use git_config::file::{GitConfig};
11521176 /// # use std::borrow::Cow;
11531177 /// # use std::convert::TryFrom;
11541178 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
11551179 /// git_config.set_raw_value("core", None, "a", vec![b'e'])?;
1156− /// assert_eq!(git_config.get_raw_value("core", None, "a")?, Cow::Borrowed(b"e"));
1157− /// # Ok::<(), GitConfigError>(())
1180+ /// assert_eq!(git_config.raw_value("core", None, "a")?, Cow::Borrowed(b"e"));
1181+ /// # Ok::<(), Box<dyn std::error::Error>>(())
11581182 /// ```
11591183 ///
11601184 /// # Errors
@@ -1166,8 +1190,8 @@ impl<'event> GitConfig<'event> { · 5 unmodified lines
11661190 subsection_name: Option<&'lookup str>,
11671191 key: &'lookup str,
11681192 new_value: Vec<u8>,
1169− ) -> Result<(), GitConfigError<'lookup>> {
1170− self.get_raw_value_mut(section_name, subsection_name, key)
1193+ ) -> Result<(), lookup::existing::Error> {
1194+ self.raw_value_mut(section_name, subsection_name, key)
11711195 .map(|mut entry| entry.set_bytes(new_value))
11721196 }
11731197
@@ -1180,7 +1204,7 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
11801204 ///
11811205 /// **Note**: Mutation order is _not_ guaranteed and is non-deterministic.
11821206 /// If you need finer control over which values of the multivar are set,
1183− /// consider using [`get_raw_multi_value_mut`], which will let you iterate
1207+ /// consider using [`raw_multi_value_mut`], which will let you iterate
11841208 /// and check over the values instead. This is best used as a convenience
11851209 /// function for setting multivars whose values should be treated as an
11861210 /// unordered set.
@@ -1200,7 +1224,7 @@ impl<'event> GitConfig<'event> { · 13 unmodified lines
12001224 /// Setting an equal number of values:
12011225 ///
12021226 /// ```
1203− /// # use git_config::file::{GitConfig, GitConfigError};
1227+ /// # use git_config::file::{GitConfig};
12041228 /// # use std::borrow::Cow;
12051229 /// # use std::convert::TryFrom;
12061230 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
@@ -1210,17 +1234,17 @@ impl<'event> GitConfig<'event> { · 3 unmodified lines
12101234 /// Cow::Borrowed(b"z"),
12111235 /// ];
12121236 /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?;
1213− /// let fetched_config = git_config.get_raw_multi_value("core", None, "a")?;
1237+ /// let fetched_config = git_config.raw_multi_value("core", None, "a")?;
12141238 /// assert!(fetched_config.contains(&Cow::Borrowed(b"x")));
12151239 /// assert!(fetched_config.contains(&Cow::Borrowed(b"y")));
12161240 /// assert!(fetched_config.contains(&Cow::Borrowed(b"z")));
1217− /// # Ok::<(), GitConfigError>(())
1241+ /// # Ok::<(), git_config::lookup::existing::Error>(())
12181242 /// ```
12191243 ///
12201244 /// Setting less than the number of present values sets the first ones found:
12211245 ///
12221246 /// ```
1223− /// # use git_config::file::{GitConfig, GitConfigError};
1247+ /// # use git_config::file::{GitConfig};
12241248 /// # use std::borrow::Cow;
12251249 /// # use std::convert::TryFrom;
12261250 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
@@ -1229,16 +1253,16 @@ impl<'event> GitConfig<'event> { · 2 unmodified lines
12291253 /// Cow::Borrowed(b"y"),
12301254 /// ];
12311255 /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?;
1232− /// let fetched_config = git_config.get_raw_multi_value("core", None, "a")?;
1256+ /// let fetched_config = git_config.raw_multi_value("core", None, "a")?;
12331257 /// assert!(fetched_config.contains(&Cow::Borrowed(b"x")));
12341258 /// assert!(fetched_config.contains(&Cow::Borrowed(b"y")));
1235− /// # Ok::<(), GitConfigError>(())
1259+ /// # Ok::<(), git_config::lookup::existing::Error>(())
12361260 /// ```
12371261 ///
12381262 /// Setting more than the number of present values discards the rest:
12391263 ///
12401264 /// ```
1241− /// # use git_config::file::{GitConfig, GitConfigError};
1265+ /// # use git_config::file::{GitConfig};
12421266 /// # use std::borrow::Cow;
12431267 /// # use std::convert::TryFrom;
12441268 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
@@ -1249,23 +1273,23 @@ impl<'event> GitConfig<'event> { · 4 unmodified lines
12491273 /// Cow::Borrowed(b"discarded"),
12501274 /// ];
12511275 /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?;
1252− /// assert!(!git_config.get_raw_multi_value("core", None, "a")?.contains(&Cow::Borrowed(b"discarded")));
1253− /// # Ok::<(), GitConfigError>(())
1276+ /// assert!(!git_config.raw_multi_value("core", None, "a")?.contains(&Cow::Borrowed(b"discarded")));
1277+ /// # Ok::<(), git_config::lookup::existing::Error>(())
12541278 /// ```
12551279 ///
12561280 /// # Errors
12571281 ///
12581282 /// This errors if any lookup input (section, subsection, and key value) fails.
12591283 ///
1260− /// [`get_raw_multi_value_mut`]: Self::get_raw_multi_value_mut
1284+ /// [`raw_multi_value_mut`]: Self::raw_multi_value_mut
12611285 pub fn set_raw_multi_value<'lookup>(
12621286 &mut self,
12631287 section_name: &'lookup str,
12641288 subsection_name: Option<&'lookup str>,
12651289 key: &'lookup str,
12661290 new_values: impl Iterator<Item = Cow<'event, [u8]>>,
1267− ) -> Result<(), GitConfigError<'lookup>> {
1268− self.get_raw_multi_value_mut(section_name, subsection_name, key)
1291+ ) -> Result<(), lookup::existing::Error> {
1292+ self.raw_multi_value_mut(section_name, subsection_name, key)
12691293 .map(|mut v| v.set_values(new_values))
12701294 }
12711295 }
@@ -1321,16 +1345,16 @@ impl<'event> GitConfig<'event> { · 49 unmodified lines
13211345 }
13221346
13231347 /// Returns the mapping between section and subsection name to section ids.
1324− fn get_section_ids_by_name_and_subname<'lookup>(
1348+ fn section_ids_by_name_and_subname<'lookup>(
13251349 &self,
13261350 section_name: impl Into<SectionHeaderName<'lookup>>,
13271351 subsection_name: Option<&'lookup str>,
1328− ) -> Result<Vec<SectionId>, GitConfigError<'lookup>> {
1352+ ) -> Result<Vec<SectionId>, lookup::existing::Error> {
13291353 let section_name = section_name.into();
13301354 let section_ids = self
13311355 .section_lookup_tree
13321356 .get(&section_name)
1333− .ok_or(GitConfigError::SectionDoesNotExist(section_name))?;
1357+ .ok_or(lookup::existing::Error::SectionMissing)?;
13341358 let mut maybe_ids = None;
13351359 // Don't simplify if and matches here -- the for loop currently needs
13361360 // `n + 1` checks, while the if and matches will result in the for loop
@@ -1352,13 +1376,13 @@ impl<'event> GitConfig<'event> { · 15 unmodified lines
13521376 }
13531377 maybe_ids
13541378 .map(Vec::to_owned)
1355− .ok_or(GitConfigError::SubSectionDoesNotExist(subsection_name))
1379+ .ok_or(lookup::existing::Error::SubSectionMissing)
13561380 }
13571381
1358− fn get_section_ids_by_name<'lookup>(
1382+ fn section_ids_by_name<'lookup>(
13591383 &self,
13601384 section_name: impl Into<SectionHeaderName<'lookup>>,
1361− ) -> Result<Vec<SectionId>, GitConfigError<'lookup>> {
1385+ ) -> Result<Vec<SectionId>, lookup::existing::Error> {
13621386 let section_name = section_name.into();
13631387 self.section_lookup_tree
13641388 .get(&section_name)
@@ -1371,7 +1395,7 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
13711395 })
13721396 .collect()
13731397 })
1374− .ok_or(GitConfigError::SectionDoesNotExist(section_name))
1398+ .ok_or(lookup::existing::Error::SectionMissing)
13751399 }
13761400 }
13771401
@@ -1382,7 +1406,6 @@ impl<'a> TryFrom<&'a str> for GitConfig<'a> { · 4 unmodified lines
13821406 /// [`GitConfig`]. See [`parse_from_str`] for more information.
13831407 ///
13841408 /// [`parse_from_str`]: crate::parser::parse_from_str
1385− #[inline]
13861409 fn try_from(s: &'a str) -> Result<GitConfig<'a>, Self::Error> {
13871410 parse_from_str(s).map(Self::from)
13881411 }
@@ -1395,7 +1418,6 @@ impl<'a> TryFrom<&'a [u8]> for GitConfig<'a> { · 6 unmodified lines
13951418 //// a [`GitConfig`]. See [`parse_from_bytes`] for more information.
13961419 ///
13971420 /// [`parse_from_bytes`]: crate::parser::parse_from_bytes
1398− #[inline]
13991421 fn try_from(value: &'a [u8]) -> Result<GitConfig<'a>, Self::Error> {
14001422 parse_from_bytes(value).map(GitConfig::from)
14011423 }
@@ -1408,7 +1430,6 @@ impl<'a> TryFrom<&'a Vec<u8>> for GitConfig<'a> { · 6 unmodified lines
14081430 //// a [`GitConfig`]. See [`parse_from_bytes`] for more information.
14091431 ///
14101432 /// [`parse_from_bytes`]: crate::parser::parse_from_bytes
1411− #[inline]
14121433 fn try_from(value: &'a Vec<u8>) -> Result<GitConfig<'a>, Self::Error> {
14131434 parse_from_bytes(value).map(GitConfig::from)
14141435 }
@@ -1458,7 +1479,6 @@ impl<'a> From<Parser<'a>> for GitConfig<'a> { · 43 unmodified lines
14581479 }
14591480
14601481 impl From<GitConfig<'_>> for Vec<u8> {
1461− #[inline]
14621482 fn from(c: GitConfig) -> Self {
14631483 c.into()
14641484 }
git-config/src/file/mod.rs+0−2
@@ -1,6 +1,5 @@
11 //! This module provides a high level wrapper around a single `git-config` file.
22
3−mod error;
43 mod git_config;
54 mod resolved;
65 mod section;
@@ -8,7 +7,6 @@ mod value; · 1 unmodified line
87
98 use std::ops::{Add, AddAssign};
109
11−pub use error::*;
1210 pub use resolved::*;
1311 pub use section::*;
1412 pub use value::*;
git-config/src/file/resolved.rs+0−3
@@ -31,7 +31,6 @@ impl ResolvedGitConfig<'static> { · 30 unmodified lines
3131 ///
3232 /// This returns an error if an IO error occurs, or if the file is not a
3333 /// valid `git-config` file.
34− #[inline]
3534 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {
3635 GitConfig::open(path.as_ref()).map(Self::from)
3736 }
@@ -107,14 +106,12 @@ fn resolve_sections<'key, 'data>( · 69 unmodified lines
107106 impl TryFrom<&Path> for ResolvedGitConfig<'static> {
108107 type Error = parser::ParserOrIoError<'static>;
109108
110− #[inline]
111109 fn try_from(path: &Path) -> Result<Self, Self::Error> {
112110 Self::open(path)
113111 }
114112 }
115113
116114 impl<'data> From<GitConfig<'data>> for ResolvedGitConfig<'data> {
117− #[inline]
118115 fn from(config: GitConfig<'data>) -> Self {
119116 Self::from_config(config)
120117 }
git-config/src/file/section.rs+13−27
@@ -7,7 +7,8 @@ use std::{ · 6 unmodified lines
77 };
88
99 use crate::{
10− file::{error::GitConfigError, Index},
10+ file::Index,
11+ lookup,
1112 parser::{Event, Key},
1213 values::{normalize_cow, normalize_vec},
1314 };
@@ -72,7 +73,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 58 unmodified lines
7273 /// Returns the previous value if it replaced a value, or None if it adds
7374 /// the value.
7475 pub fn set(&mut self, key: Key<'event>, value: Cow<'event, [u8]>) -> Option<Cow<'event, [u8]>> {
75− let range = self.get_value_range_by_key(&key);
76+ let range = self.value_range_by_key(&key);
7677 if range.is_empty() {
7778 self.push(key, value);
7879 return None;
@@ -85,7 +86,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 6 unmodified lines
8586
8687 /// Removes the latest value by key and returns it, if it exists.
8788 pub fn remove(&mut self, key: &Key<'event>) -> Option<Cow<'event, [u8]>> {
88− let range = self.get_value_range_by_key(key);
89+ let range = self.value_range_by_key(key);
8990 if range.is_empty() {
9091 return None;
9192 }
@@ -113,14 +114,12 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 21 unmodified lines
113114
114115 /// Adds a new line event. Note that you don't need to call this unless
115116 /// you've disabled implicit newlines.
116− #[inline]
117117 pub fn push_newline(&mut self) {
118118 self.section.0.push(Event::Newline("\n".into()));
119119 }
120120
121121 /// Enables or disables automatically adding newline events after adding
122122 /// a value. This is enabled by default.
123− #[inline]
124123 pub fn implicit_newline(&mut self, on: bool) {
125124 self.implicit_newline = on;
126125 }
@@ -128,14 +127,12 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 1 unmodified line
128127 /// Sets the number of spaces before the start of a key value. By default,
129128 /// this is set to two. Set to 0 to disable adding whitespace before a key
130129 /// value.
131− #[inline]
132130 pub fn set_whitespace(&mut self, num: usize) {
133131 self.whitespace = num;
134132 }
135133
136134 /// Returns the number of whitespace this section will insert before the
137135 /// beginning of a key.
138− #[inline]
139136 #[must_use]
140137 pub const fn whitespace(&self) -> usize {
141138 self.whitespace
@@ -144,7 +141,6 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 2 unmodified lines
144141
145142 // Internal methods that may require exact indices for faster operations.
146143 impl<'borrow, 'event> MutableSection<'borrow, 'event> {
147− #[inline]
148144 pub(super) fn new(section: &'borrow mut SectionBody<'event>) -> Self {
149145 Self {
150146 section,
@@ -158,7 +154,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 7 unmodified lines
158154 key: &Key<'key>,
159155 start: Index,
160156 end: Index,
161− ) -> Result<Cow<'_, [u8]>, GitConfigError<'key>> {
157+ ) -> Result<Cow<'_, [u8]>, lookup::existing::Error> {
162158 let mut found_key = false;
163159 let mut latest_value = None;
164160 let mut partial_value = None;
@@ -188,10 +184,9 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 23 unmodified lines
188184 latest_value
189185 .map(normalize_cow)
190186 .or_else(|| partial_value.map(normalize_vec))
191− .ok_or(GitConfigError::KeyDoesNotExist)
187+ .ok_or(lookup::existing::Error::KeyMissing)
192188 }
193189
194− #[inline]
195190 pub(super) fn delete(&mut self, start: Index, end: Index) {
196191 self.section.0.drain(start.0..=end.0);
197192 }
@@ -206,7 +201,6 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 8 unmodified lines
206201 impl<'event> Deref for MutableSection<'_, 'event> {
207202 type Target = SectionBody<'event>;
208203
209− #[inline]
210204 fn deref(&self) -> &Self::Target {
211205 self.section
212206 }
@@ -227,7 +221,6 @@ impl<'event> SectionBody<'event> { · 14 unmodified lines
227221 }
228222
229223 /// Constructs a new empty section body.
230− #[inline]
231224 pub(super) fn new() -> Self {
232225 Self::default()
233226 }
@@ -240,7 +233,7 @@ impl<'event> SectionBody<'event> { · 6 unmodified lines
240233 #[allow(clippy::missing_panics_doc)]
241234 #[must_use]
242235 pub fn value(&self, key: &Key) -> Option<Cow<'event, [u8]>> {
243− let range = self.get_value_range_by_key(key);
236+ let range = self.value_range_by_key(key);
244237 if range.is_empty() {
245238 return None;
246239 }
@@ -276,10 +269,9 @@ impl<'event> SectionBody<'event> { · 29 unmodified lines
276269 /// # Errors
277270 ///
278271 /// Returns an error if the key was not found, or if the conversion failed.
279− #[inline]
280− pub fn value_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<T, GitConfigError<'event>> {
281− T::try_from(self.value(key).ok_or(GitConfigError::KeyDoesNotExist)?)
282− .map_err(|_| GitConfigError::FailedConversion)
272+ pub fn value_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<T, lookup::Error<T::Error>> {
273+ T::try_from(self.value(key).ok_or(lookup::existing::Error::KeyMissing)?)
274+ .map_err(lookup::Error::FailedConversion)
283275 }
284276
285277 /// Retrieves all values that have the provided key name. This may return
@@ -325,17 +317,15 @@ impl<'event> SectionBody<'event> { · 39 unmodified lines
325317 /// # Errors
326318 ///
327319 /// Returns an error if the conversion failed.
328− #[inline]
329− pub fn values_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<Vec<T>, GitConfigError<'event>> {
320+ pub fn values_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<Vec<T>, lookup::Error<T::Error>> {
330321 self.values(key)
331322 .into_iter()
332323 .map(T::try_from)
333324 .collect::<Result<Vec<T>, _>>()
334− .map_err(|_| GitConfigError::FailedConversion)
325+ .map_err(lookup::Error::FailedConversion)
335326 }
336327
337328 /// Returns an iterator visiting all keys in order.
338− #[inline]
339329 pub fn keys(&self) -> impl Iterator<Item = &Key<'event>> {
340330 self.0
341331 .iter()
@@ -353,14 +343,12 @@ impl<'event> SectionBody<'event> { · 11 unmodified lines
353343 }
354344
355345 /// Returns the number of entries in the section.
356− #[inline]
357346 #[must_use]
358347 pub fn len(&self) -> usize {
359348 self.0.iter().filter(|e| matches!(e, Event::Key(_))).count()
360349 }
361350
362351 /// Returns if the section is empty.
363− #[inline]
364352 #[must_use]
365353 pub fn is_empty(&self) -> bool {
366354 self.0.is_empty()
@@ -368,7 +356,7 @@ impl<'event> SectionBody<'event> { · 1 unmodified line
368356
369357 /// Returns the the range containing the value events for the section.
370358 /// If the value is not found, then this returns an empty range.
371− fn get_value_range_by_key(&self, key: &Key<'event>) -> Range<usize> {
359+ fn value_range_by_key(&self, key: &Key<'event>) -> Range<usize> {
372360 let mut values_start = 0;
373361 // value end needs to be offset by one so that the last value's index
374362 // is included in the range
@@ -406,7 +394,6 @@ impl<'event> IntoIterator for SectionBody<'event> { · 31 unmodified lines
406394
407395 type IntoIter = SectionBodyIter<'event>;
408396
409− #[inline]
410397 fn into_iter(self) -> Self::IntoIter {
411398 SectionBodyIter(self.0.into())
412399 }
@@ -448,7 +435,6 @@ impl<'event> Iterator for SectionBodyIter<'event> { · 35 unmodified lines
448435 impl FusedIterator for SectionBodyIter<'_> {}
449436
450437 impl<'event> From<Vec<Event<'event>>> for SectionBody<'event> {
451− #[inline]
452438 fn from(e: Vec<Event<'event>>) -> Self {
453439 Self(e)
454440 }
git-config/src/file/value.rs+9−21
@@ -6,11 +6,11 @@ use std::{ · 5 unmodified lines
66
77 use crate::{
88 file::{
9− error::GitConfigError,
109 git_config::SectionId,
1110 section::{MutableSection, SectionBody},
1211 Index, Size,
1312 },
13+ lookup,
1414 parser::{Event, Key},
1515 values::{normalize_bytes, normalize_vec},
1616 };
@@ -54,15 +54,13 @@ impl<'borrow, 'lookup, 'event> MutableValue<'borrow, 'lookup, 'event> { · 37 unmodified lines
5454 /// # Errors
5555 ///
5656 /// Returns an error if the lookup failed.
57− #[inline]
58− pub fn get(&self) -> Result<Cow<'_, [u8]>, GitConfigError> {
57+ pub fn get(&self) -> Result<Cow<'_, [u8]>, lookup::existing::Error> {
5958 self.section.get(&self.key, self.index, self.index + self.size)
6059 }
6160
6261 /// Update the value to the provided one. This modifies the value such that
6362 /// the Value event(s) are replaced with a single new event containing the
6463 /// new value.
65− #[inline]
6664 pub fn set_string(&mut self, input: String) {
6765 self.set_bytes(input.into_bytes());
6866 }
@@ -149,7 +147,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 80 unmodified lines
149147 /// # Errors
150148 ///
151149 /// Returns an error if the lookup failed.
152− pub fn get(&self) -> Result<Vec<Cow<'_, [u8]>>, GitConfigError> {
150+ pub fn get(&self) -> Result<Vec<Cow<'_, [u8]>>, lookup::existing::Error> {
153151 let mut found_key = false;
154152 let mut values = vec![];
155153 let mut partial_value = None;
@@ -160,7 +158,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 4 unmodified lines
160158 offset_index,
161159 } in &self.indices_and_sizes
162160 {
163− let (offset, size) = MutableMultiValue::get_index_and_size(&self.offsets, *section_id, *offset_index);
161+ let (offset, size) = MutableMultiValue::index_and_size(&self.offsets, *section_id, *offset_index);
164162 for event in &self
165163 .section
166164 .get(section_id)
@@ -190,14 +188,13 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 23 unmodified lines
190188 }
191189
192190 if values.is_empty() {
193− return Err(GitConfigError::KeyDoesNotExist);
191+ return Err(lookup::existing::Error::KeyMissing);
194192 }
195193
196194 Ok(values)
197195 }
198196
199197 /// Returns the size of values the multivar has.
200− #[inline]
201198 #[must_use]
202199 pub fn len(&self) -> usize {
203200 self.indices_and_sizes.len()
@@ -205,7 +202,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 1 unmodified line
205202
206203 /// Returns if the multivar has any values. This might occur if the value
207204 /// was deleted but not set with a new value.
208− #[inline]
209205 #[must_use]
210206 pub fn is_empty(&self) -> bool {
211207 self.indices_and_sizes.is_empty()
@@ -216,7 +212,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 4 unmodified lines
216212 /// # Safety
217213 ///
218214 /// This will panic if the index is out of range.
219− #[inline]
220215 pub fn set_string(&mut self, index: usize, input: String) {
221216 self.set_bytes(index, input.into_bytes());
222217 }
@@ -226,7 +221,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 3 unmodified lines
226221 /// # Safety
227222 ///
228223 /// This will panic if the index is out of range.
229− #[inline]
230224 pub fn set_bytes(&mut self, index: usize, input: Vec<u8>) {
231225 self.set_value(index, Cow::Owned(input));
232226 }
@@ -260,7 +254,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 27 unmodified lines
260254 /// remaining values are ignored.
261255 ///
262256 /// [`zip`]: std::iter::Iterator::zip
263− #[inline]
264257 pub fn set_values<'a: 'event>(&mut self, input: impl Iterator<Item = Cow<'a, [u8]>>) {
265258 for (
266259 EntryData {
@@ -285,14 +278,12 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 18 unmodified lines
285278
286279 /// Sets all values in this multivar to the provided one by copying the
287280 /// input for all values.
288− #[inline]
289281 pub fn set_str_all(&mut self, input: &str) {
290282 self.set_owned_values_all(input.as_bytes());
291283 }
292284
293285 /// Sets all values in this multivar to the provided one by copying the
294286 /// input bytes for all values.
295− #[inline]
296287 pub fn set_owned_values_all(&mut self, input: &[u8]) {
297288 for EntryData {
298289 section_id,
@@ -319,7 +310,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 20 unmodified lines
319310 /// need for a more ergonomic interface.
320311 ///
321312 /// [`GitConfig`]: super::GitConfig
322− #[inline]
323313 pub fn set_values_all<'a: 'event>(&mut self, input: &'a [u8]) {
324314 for EntryData {
325315 section_id,
@@ -347,7 +337,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 21 unmodified lines
347337 offset_index: usize,
348338 input: Cow<'a, [u8]>,
349339 ) {
350− let (offset, size) = MutableMultiValue::get_index_and_size(offsets, section_id, offset_index);
340+ let (offset, size) = MutableMultiValue::index_and_size(offsets, section_id, offset_index);
351341 section.as_mut().drain(offset..offset + size);
352342
353343 MutableMultiValue::set_offset(offsets, section_id, offset_index, 3);
@@ -369,7 +359,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 15 unmodified lines
369359 section_id,
370360 offset_index,
371361 } = &self.indices_and_sizes[index];
372− let (offset, size) = MutableMultiValue::get_index_and_size(&self.offsets, *section_id, *offset_index);
362+ let (offset, size) = MutableMultiValue::index_and_size(&self.offsets, *section_id, *offset_index);
373363 if size > 0 {
374364 self.section
375365 .get_mut(section_id)
@@ -390,7 +380,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 14 unmodified lines
390380 offset_index,
391381 } in &self.indices_and_sizes
392382 {
393− let (offset, size) = MutableMultiValue::get_index_and_size(&self.offsets, *section_id, *offset_index);
383+ let (offset, size) = MutableMultiValue::index_and_size(&self.offsets, *section_id, *offset_index);
394384 if size > 0 {
395385 self.section
396386 .get_mut(section_id)
@@ -405,8 +395,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 8 unmodified lines
405395
406396 // SectionId is the same size as a reference, which means it's just as
407397 // efficient passing in a value instead of a reference.
408− #[inline]
409− fn get_index_and_size(
398+ fn index_and_size(
410399 offsets: &'lookup HashMap<SectionId, Vec<usize>>,
411400 section_id: SectionId,
412401 offset_index: usize,
@@ -424,7 +413,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 11 unmodified lines
424413 //
425414 // SectionId is the same size as a reference, which means it's just as
426415 // efficient passing in a value instead of a reference.
427− #[inline]
428416 fn set_offset(
429417 offsets: &mut HashMap<SectionId, Vec<usize>>,
430418 section_id: SectionId,
git-config/src/fs.rs+6−8
@@ -7,7 +7,8 @@ use std::{ · 6 unmodified lines
77 path::{Path, PathBuf},
88 };
99
10−use crate::file::{from_paths, GitConfig, GitConfigError};
10+use crate::file::{from_paths, GitConfig};
11+use crate::lookup;
1112
1213 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1314 pub enum ConfigSource {
@@ -40,7 +41,6 @@ pub struct ConfigBuilder { · 26 unmodified lines
4041
4142 impl ConfigBuilder {
4243 /// Constructs a new builder that finds the default location
43− #[inline]
4444 #[must_use]
4545 pub fn new() -> Self {
4646 Self {
@@ -146,7 +146,6 @@ pub struct Config<'config> { · 99 unmodified lines
146146 }
147147
148148 impl<'config> Config<'config> {
149− #[inline]
150149 #[must_use]
151150 pub fn value<T: TryFrom<Cow<'config, [u8]>>>(
152151 &'config self,
@@ -177,13 +176,12 @@ impl<'config> Config<'config> { · 24 unmodified lines
177176 None
178177 }
179178
180− #[inline]
181179 pub fn try_value<'lookup, T: TryFrom<Cow<'config, [u8]>>>(
182180 &'config self,
183181 section_name: &'lookup str,
184182 subsection_name: Option<&'lookup str>,
185183 key: &'lookup str,
186− ) -> Result<Option<T>, GitConfigError<'lookup>> {
184+ ) -> Result<Option<T>, lookup::Error<T::Error>> {
187185 self.try_value_with_source(section_name, subsection_name, key)
188186 .map(|res| res.map(|(value, _)| value))
189187 }
@@ -197,7 +195,7 @@ impl<'config> Config<'config> { · 7 unmodified lines
197195 section_name: &'lookup str,
198196 subsection_name: Option<&'lookup str>,
199197 key: &'lookup str,
200− ) -> Result<Option<(T, ConfigSource)>, GitConfigError<'lookup>> {
198+ ) -> Result<Option<(T, ConfigSource)>, lookup::Error<T::Error>> {
201199 let mapping = self.mapping();
202200
203201 for (conf, source) in mapping.iter() {
@@ -227,7 +225,7 @@ impl<'config> Config<'config> { · 23 unmodified lines
227225 /// Retrieves the underlying [`GitConfig`] object, if one was found during
228226 /// initialization.
229227 #[must_use]
230− pub fn get_config(&self, source: ConfigSource) -> Option<&GitConfig<'config>> {
228+ pub fn config(&self, source: ConfigSource) -> Option<&GitConfig<'config>> {
231229 match source {
232230 ConfigSource::System => self.system_conf.as_ref(),
233231 ConfigSource::Global => self.global_conf.as_ref(),
@@ -241,7 +239,7 @@ impl<'config> Config<'config> { · 7 unmodified lines
241239 /// Retrieves the underlying [`GitConfig`] object as a mutable reference,
242240 /// if one was found during initialization.
243241 #[must_use]
244− pub fn get_config_mut(&mut self, source: ConfigSource) -> Option<&mut GitConfig<'config>> {
242+ pub fn config_mut(&mut self, source: ConfigSource) -> Option<&mut GitConfig<'config>> {
245243 match source {
246244 ConfigSource::System => self.system_conf.as_mut(),
247245 ConfigSource::Global => self.global_conf.as_mut(),
git-config/src/lib.rs+57−0
@@ -54,10 +54,67 @@ · 53 unmodified lines
5454 #[cfg(feature = "serde")]
5555 extern crate serde_crate as serde;
5656
57+pub mod lookup {
58+
59+ /// The error when looking up a value.
60+ #[derive(Debug, thiserror::Error)]
61+ pub enum Error<E> {
62+ #[error(transparent)]
63+ ValueMissing(#[from] crate::lookup::existing::Error),
64+ #[error(transparent)]
65+ FailedConversion(E),
66+ }
67+
68+ pub mod existing {
69+ /// The error when looking up a value that doesn't exist.
70+ #[derive(Debug, thiserror::Error)]
71+ pub enum Error {
72+ #[error("The requested section does not exist")]
73+ SectionMissing,
74+ #[error("The requested subsection does not exist")]
75+ SubSectionMissing,
76+ #[error("The key does not exist in the requested section")]
77+ KeyMissing,
78+ }
79+ }
80+}
81+
5782 pub mod file;
5883 pub mod fs;
5984 pub mod parser;
6085 pub mod values;
86+/// The future home of the `values` module (TODO).
87+pub mod value {
88+ pub mod parse {
89+ use bstr::BString;
90+
91+ /// The error returned when creating `Integer` from byte string.
92+ #[derive(Debug, thiserror::Error, Eq, PartialEq)]
93+ #[allow(missing_docs)]
94+ #[error("Could not decode '{}': {}", .input, .message)]
95+ pub struct Error {
96+ pub message: &'static str,
97+ pub input: BString,
98+ #[source]
99+ pub utf8_err: Option<std::str::Utf8Error>,
100+ }
101+
102+ impl Error {
103+ pub(crate) fn new(message: &'static str, input: impl Into<BString>) -> Self {
104+ Error {
105+ message,
106+ input: input.into(),
107+ utf8_err: None,
108+ }
109+ }
110+
111+ pub(crate) fn with_err(mut self, err: std::str::Utf8Error) -> Self {
112+ self.utf8_err = Some(err);
113+ self
114+ }
115+ }
116+ }
117+}
61118
62119 mod permissions {
63120 use crate::Permissions;
git-config/src/parser.rs+0−36
@@ -75,7 +75,6 @@ impl Event<'_> { · 74 unmodified lines
7575 /// Generates a byte representation of the value. This should be used when
7676 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
7777 /// guaranteed.
78− #[inline]
7978 #[must_use]
8079 pub fn to_vec(&self) -> Vec<u8> {
8180 self.into()
@@ -95,7 +94,6 @@ impl Event<'_> { · 13 unmodified lines
9594 /// not.
9695 ///
9796 /// [`clone`]: Self::clone
98− #[inline]
9997 #[must_use]
10098 pub fn to_owned(&self) -> Event<'static> {
10199 match self {
@@ -116,7 +114,6 @@ impl Display for Event<'_> { · 14 unmodified lines
116114 /// Note that this is a best-effort attempt at printing an `Event`. If
117115 /// there are non UTF-8 values in your config, this will _NOT_ render
118116 /// as read.
119− #[inline]
120117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121118 match self {
122119 Self::Value(e) | Self::ValueNotDone(e) | Self::ValueDone(e) => match std::str::from_utf8(e) {
@@ -133,14 +130,12 @@ impl Display for Event<'_> { · 10 unmodified lines
133130 }
134131
135132 impl From<Event<'_>> for Vec<u8> {
136− #[inline]
137133 fn from(event: Event) -> Self {
138134 event.into()
139135 }
140136 }
141137
142138 impl From<&Event<'_>> for Vec<u8> {
143− #[inline]
144139 fn from(event: &Event) -> Self {
145140 match event {
146141 Event::Value(e) | Event::ValueNotDone(e) | Event::ValueDone(e) => e.to_vec(),
@@ -177,7 +172,6 @@ impl ParsedSection<'_> { · 30 unmodified lines
177172 /// not.
178173 ///
179174 /// [`clone`]: Self::clone
180− #[inline]
181175 #[must_use]
182176 pub fn to_owned(&self) -> ParsedSection<'static> {
183177 ParsedSection {
@@ -218,7 +212,6 @@ macro_rules! generate_case_insensitive { · 34 unmodified lines
218212 /// while `clone` does not.
219213 ///
220214 /// [`clone`]: Self::clone
221− #[inline]
222215 #[must_use]
223216 pub fn to_owned(&self) -> $name<'static> {
224217 $name(Cow::Owned(self.0.clone().into_owned()))
@@ -226,21 +219,18 @@ macro_rules! generate_case_insensitive { · 1 unmodified line
226219 }
227220
228221 impl PartialEq for $name<'_> {
229− #[inline]
230222 fn eq(&self, other: &Self) -> bool {
231223 self.0.eq_ignore_ascii_case(&other.0)
232224 }
233225 }
234226
235227 impl Display for $name<'_> {
236− #[inline]
237228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238229 self.0.fmt(f)
239230 }
240231 }
241232
242233 impl PartialOrd for $name<'_> {
243− #[inline]
244234 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
245235 self.0
246236 .to_ascii_lowercase()
@@ -249,21 +239,18 @@ macro_rules! generate_case_insensitive { · 2 unmodified lines
249239 }
250240
251241 impl std::hash::Hash for $name<'_> {
252− #[inline]
253242 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
254243 self.0.to_ascii_lowercase().hash(state)
255244 }
256245 }
257246
258247 impl<'a> From<&'a str> for $name<'a> {
259− #[inline]
260248 fn from(s: &'a str) -> Self {
261249 Self(Cow::Borrowed(s))
262250 }
263251 }
264252
265253 impl<'a> From<Cow<'a, str>> for $name<'a> {
266− #[inline]
267254 fn from(s: Cow<'a, str>) -> Self {
268255 Self(s)
269256 }
@@ -272,7 +259,6 @@ macro_rules! generate_case_insensitive { · 2 unmodified lines
272259 impl<'a> std::ops::Deref for $name<'a> {
273260 type Target = $cow_inner_type;
274261
275− #[inline]
276262 fn deref(&self) -> &Self::Target {
277263 &self.0
278264 }
@@ -316,7 +302,6 @@ impl ParsedSectionHeader<'_> { · 37 unmodified lines
316302 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
317303 /// guaranteed.
318304 #[must_use]
319− #[inline]
320305 pub fn to_vec(&self) -> Vec<u8> {
321306 self.into()
322307 }
@@ -335,7 +320,6 @@ impl ParsedSectionHeader<'_> { · 12 unmodified lines
335320 /// not.
336321 ///
337322 /// [`clone`]: Self::clone
338− #[inline]
339323 #[must_use]
340324 pub fn to_owned(&self) -> ParsedSectionHeader<'static> {
341325 ParsedSectionHeader {
@@ -366,21 +350,18 @@ impl Display for ParsedSectionHeader<'_> { · 24 unmodified lines
366350 }
367351
368352 impl From<ParsedSectionHeader<'_>> for Vec<u8> {
369− #[inline]
370353 fn from(header: ParsedSectionHeader) -> Self {
371354 header.into()
372355 }
373356 }
374357
375358 impl From<&ParsedSectionHeader<'_>> for Vec<u8> {
376− #[inline]
377359 fn from(header: &ParsedSectionHeader) -> Self {
378360 header.to_string().into_bytes()
379361 }
380362 }
381363
382364 impl<'a> From<ParsedSectionHeader<'a>> for Event<'a> {
383− #[inline]
384365 fn from(header: ParsedSectionHeader) -> Event {
385366 Event::SectionHeader(header)
386367 }
@@ -410,7 +391,6 @@ impl ParsedComment<'_> { · 23 unmodified lines
410391 /// not.
411392 ///
412393 /// [`clone`]: Self::clone
413− #[inline]
414394 #[must_use]
415395 pub fn to_owned(&self) -> ParsedComment<'static> {
416396 ParsedComment {
@@ -435,14 +415,12 @@ impl Display for ParsedComment<'_> { · 18 unmodified lines
435415 }
436416
437417 impl From<ParsedComment<'_>> for Vec<u8> {
438− #[inline]
439418 fn from(c: ParsedComment) -> Self {
440419 c.into()
441420 }
442421 }
443422
444423 impl From<&ParsedComment<'_>> for Vec<u8> {
445− #[inline]
446424 fn from(c: &ParsedComment) -> Self {
447425 let mut values = vec![c.comment_tag as u8];
448426 values.extend(c.comment.iter());
@@ -463,14 +441,12 @@ pub struct Error<'a> { · 14 unmodified lines
463441 impl Error<'_> {
464442 /// The one-indexed line number where the error occurred. This is determined
465443 /// by the number of newlines that were successfully parsed.
466− #[inline]
467444 #[must_use]
468445 pub const fn line_number(&self) -> usize {
469446 self.line_number + 1
470447 }
471448
472449 /// The remaining data that was left unparsed.
473− #[inline]
474450 #[must_use]
475451 pub fn remaining_data(&self) -> &[u8] {
476452 &self.parsed_until
@@ -490,7 +466,6 @@ impl Error<'_> { · 13 unmodified lines
490466 /// not.
491467 ///
492468 /// [`clone`]: std::clone::Clone::clone
493− #[inline]
494469 #[must_use]
495470 pub fn to_owned(&self) -> Error<'static> {
496471 Error {
@@ -567,7 +542,6 @@ impl ParserOrIoError<'_> { · 70 unmodified lines
567542 }
568543
569544 impl Display for ParserOrIoError<'_> {
570− #[inline]
571545 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572546 match self {
573547 ParserOrIoError::Parser(e) => e.fmt(f),
@@ -577,7 +551,6 @@ impl Display for ParserOrIoError<'_> { · 3 unmodified lines
577551 }
578552
579553 impl From<std::io::Error> for ParserOrIoError<'_> {
580− #[inline]
581554 fn from(e: std::io::Error) -> Self {
582555 Self::Io(e)
583556 }
@@ -594,7 +567,6 @@ enum ParserNode { · 10 unmodified lines
594567 }
595568
596569 impl Display for ParserNode {
597− #[inline]
598570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
599571 match self {
600572 Self::SectionHeader => write!(f, "section header"),
@@ -822,7 +794,6 @@ impl<'a> Parser<'a> { · 221 unmodified lines
822794 /// a section) from the parser. Consider [`Parser::take_frontmatter`] if
823795 /// you need an owned copy only once. If that function was called, then this
824796 /// will always return an empty slice.
825− #[inline]
826797 #[must_use]
827798 pub fn frontmatter(&self) -> &[Event<'a>] {
828799 &self.frontmatter
@@ -832,7 +803,6 @@ impl<'a> Parser<'a> { · 3 unmodified lines
832803 /// a section) from the parser. Subsequent calls will return an empty vec.
833804 /// Consider [`Parser::frontmatter`] if you only need a reference to the
834805 /// frontmatter
835− #[inline]
836806 pub fn take_frontmatter(&mut self) -> Vec<Event<'a>> {
837807 std::mem::take(&mut self.frontmatter)
838808 }
@@ -840,7 +810,6 @@ impl<'a> Parser<'a> { · 1 unmodified line
840810 /// Returns the parsed sections from the parser. Consider
841811 /// [`Parser::take_sections`] if you need an owned copy only once. If that
842812 /// function was called, then this will always return an empty slice.
843− #[inline]
844813 #[must_use]
845814 pub fn sections(&self) -> &[ParsedSection<'a>] {
846815 &self.sections
@@ -849,7 +818,6 @@ impl<'a> Parser<'a> { · 2 unmodified lines
849818 /// Takes the parsed sections from the parser. Subsequent calls will return
850819 /// an empty vec. Consider [`Parser::sections`] if you only need a reference
851820 /// to the comments.
852− #[inline]
853821 pub fn take_sections(&mut self) -> Vec<ParsedSection<'a>> {
854822 let mut to_return = vec![];
855823 std::mem::swap(&mut self.sections, &mut to_return);
@@ -857,7 +825,6 @@ impl<'a> Parser<'a> { · 1 unmodified line
857825 }
858826
859827 /// Consumes the parser to produce a Vec of Events.
860− #[inline]
861828 #[must_use]
862829 pub fn into_vec(self) -> Vec<Event<'a>> {
863830 self.into_iter().collect()
@@ -878,7 +845,6 @@ impl<'a> Parser<'a> { · 14 unmodified lines
878845 impl<'a> TryFrom<&'a str> for Parser<'a> {
879846 type Error = Error<'a>;
880847
881− #[inline]
882848 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
883849 parse_from_str(value)
884850 }
@@ -887,7 +853,6 @@ impl<'a> TryFrom<&'a str> for Parser<'a> { · 2 unmodified lines
887853 impl<'a> TryFrom<&'a [u8]> for Parser<'a> {
888854 type Error = Error<'a>;
889855
890− #[inline]
891856 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
892857 parse_from_bytes(value)
893858 }
@@ -925,7 +890,6 @@ pub fn parse_from_path<P: AsRef<Path>>(path: P) -> Result<Parser<'static>, Parse · 31 unmodified lines
925890 /// Returns an error if the string provided is not a valid `git-config`.
926891 /// This generally is due to either invalid names or if there's extraneous
927892 /// data succeeding valid `git-config` data.
928−#[inline]
929893 pub fn parse_from_str(input: &str) -> Result<Parser, Error> {
930894 parse_from_bytes(input.as_bytes())
931895 }
git-config/src/values.rs+101−148
@@ -2,8 +2,8 @@ · 1 unmodified line
22
33 use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr};
44
5−use bstr::BStr;
6−use quick_error::quick_error;
5+use crate::value;
6+use bstr::{BStr, BString};
77 #[cfg(feature = "serde")]
88 use serde::{Serialize, Serializer};
99
@@ -120,21 +120,18 @@ pub fn normalize_cow(input: Cow<'_, [u8]>) -> Cow<'_, [u8]> { · 110 unmodified lines
120120 }
121121
122122 /// `&[u8]` variant of [`normalize_cow`].
123−#[inline]
124123 #[must_use]
125124 pub fn normalize_bytes(input: &[u8]) -> Cow<'_, [u8]> {
126125 normalize_cow(Cow::Borrowed(input))
127126 }
128127
129128 /// `Vec[u8]` variant of [`normalize_cow`].
130−#[inline]
131129 #[must_use]
132130 pub fn normalize_vec(input: Vec<u8>) -> Cow<'static, [u8]> {
133131 normalize_cow(Cow::Owned(input))
134132 }
135133
136134 /// [`str`] variant of [`normalize_cow`].
137−#[inline]
138135 #[must_use]
139136 pub fn normalize_str(input: &str) -> Cow<'_, [u8]> {
140137 normalize_bytes(input.as_bytes())
@@ -149,7 +146,6 @@ pub struct Bytes<'a> { · 8 unmodified lines
149146 }
150147
151148 impl<'a> From<&'a [u8]> for Bytes<'a> {
152− #[inline]
153149 fn from(s: &'a [u8]) -> Self {
154150 Self {
155151 value: Cow::Borrowed(s),
@@ -164,7 +160,6 @@ impl From<Vec<u8>> for Bytes<'_> { · 8 unmodified lines
164160 }
165161
166162 impl<'a> From<Cow<'a, [u8]>> for Bytes<'a> {
167− #[inline]
168163 fn from(c: Cow<'a, [u8]>) -> Self {
169164 match c {
170165 Cow::Borrowed(c) => Self::from(c),
@@ -181,7 +176,6 @@ pub struct String<'a> { · 10 unmodified lines
181176 }
182177
183178 impl<'a> From<Cow<'a, [u8]>> for String<'a> {
184− #[inline]
185179 fn from(c: Cow<'a, [u8]>) -> Self {
186180 String {
187181 value: match c {
@@ -198,38 +192,28 @@ pub mod path { · 10 unmodified lines
198192
199193 #[cfg(not(any(target_os = "android", target_os = "windows")))]
200194 use pwd::Passwd;
201− use quick_error::ResultExt;
202195
203196 use crate::values::Path;
204197
205198 pub mod interpolate {
206− use quick_error::quick_error;
207−
208− quick_error! {
209− #[derive(Debug)]
210− /// The error returned by [`Path::interpolate()`].
211− #[allow(missing_docs)]
212− pub enum Error {
213− Missing { what: &'static str } {
214− display("{} is missing", what)
215− }
216− Utf8Conversion(what: &'static str, err: git_features::path::Utf8Error) {
217− display("Ill-formed UTF-8 in {}", what)
218− context(what: &'static str, err: git_features::path::Utf8Error) -> (what, err)
219− source(err)
220− }
221− UsernameConversion(err: std::str::Utf8Error) {
222− display("Ill-formed UTF-8 in username")
223− source(err)
224− from()
225− }
226− PwdFileQuery {
227− display("User home info missing")
228− }
229− UserInterpolationUnsupported {
230− display("User interpolation is not available on this platform")
231− }
232− }
199+ /// The error returned by [`Path::interpolate()`][crate::values::Path::interpolate()].
200+ #[derive(Debug, thiserror::Error)]
201+ #[allow(missing_docs)]
202+ pub enum Error {
203+ #[error("{} is missing", .what)]
204+ Missing { what: &'static str },
205+ #[error("Ill-formed UTF-8 in {}", .what)]
206+ Utf8Conversion {
207+ what: &'static str,
208+ #[source]
209+ err: git_path::Utf8Error,
210+ },
211+ #[error("Ill-formed UTF-8 in username")]
212+ UsernameConversion(#[from] std::str::Utf8Error),
213+ #[error("User home info missing")]
214+ PwdFileQuery,
215+ #[error("User interpolation is not available on this platform")]
216+ UserInterpolationUnsupported,
233217 }
234218 }
235219
@@ -261,17 +245,25 @@ pub mod path { · 25 unmodified lines
261245 })?;
262246 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
263247 let path_without_trailing_slash =
264− git_features::path::from_byte_vec(path_without_trailing_slash).context("path past %(prefix)")?;
248+ git_path::try_from_bstring(path_without_trailing_slash).map_err(|err| {
249+ interpolate::Error::Utf8Conversion {
250+ what: "path past %(prefix)",
251+ err,
252+ }
253+ })?;
265254 Ok(git_install_dir.join(path_without_trailing_slash).into())
266255 } else if self.starts_with(USER_HOME) {
267256 let home_path = dirs::home_dir().ok_or(interpolate::Error::Missing { what: "home dir" })?;
268257 let (_prefix, val) = self.split_at(USER_HOME.len());
269− let val = git_features::path::from_bytes(val).context("path past ~/")?;
258+ let val = git_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion {
259+ what: "path past ~/",
260+ err,
261+ })?;
270262 Ok(home_path.join(val).into())
271263 } else if self.starts_with(b"~") && self.contains(&b'/') {
272264 self.interpolate_user()
273265 } else {
274− Ok(git_features::path::from_bytes(self.value).context("unexpanded path")?)
266+ Ok(git_path::from_bstr(self.value))
275267 }
276268 }
277269
@@ -293,8 +285,13 @@ pub mod path { · 15 unmodified lines
293285 .map_err(|_| interpolate::Error::PwdFileQuery)?
294286 .ok_or(interpolate::Error::Missing { what: "pwd user info" })?
295287 .dir;
296− let path_past_user_prefix = git_features::path::from_byte_slice(&path_with_leading_slash["/".len()..])
297− .context("path past ~user/")?;
288+ let path_past_user_prefix =
289+ git_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| {
290+ interpolate::Error::Utf8Conversion {
291+ what: "path past ~user/",
292+ err,
293+ }
294+ })?;
298295 Ok(std::path::PathBuf::from(home).join(path_past_user_prefix).into())
299296 }
300297 }
@@ -306,11 +303,11 @@ pub mod path { · 5 unmodified lines
306303 #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
307304 pub struct Path<'a> {
308305 /// The path string, un-interpolated
309− pub value: Cow<'a, [u8]>,
306+ pub value: Cow<'a, BStr>,
310307 }
311308
312309 impl<'a> std::ops::Deref for Path<'a> {
313− type Target = [u8];
310+ type Target = BStr;
314311
315312 fn deref(&self) -> &Self::Target {
316313 self.value.as_ref()
@@ -323,10 +320,20 @@ impl<'a> AsRef<[u8]> for Path<'a> { · 6 unmodified lines
323320 }
324321 }
325322
323+impl<'a> AsRef<BStr> for Path<'a> {
324+ fn as_ref(&self) -> &BStr {
325+ self.value.as_ref()
326+ }
327+}
328+
326329 impl<'a> From<Cow<'a, [u8]>> for Path<'a> {
327− #[inline]
328330 fn from(value: Cow<'a, [u8]>) -> Self {
329− Path { value }
331+ Path {
332+ value: match value {
333+ Cow::Borrowed(v) => Cow::Borrowed(v.into()),
334+ Cow::Owned(v) => Cow::Owned(v.into()),
335+ },
336+ }
330337 }
331338 }
332339
@@ -354,7 +361,6 @@ impl Boolean<'_> { · 21 unmodified lines
354361 /// Generates a byte representation of the value. This should be used when
355362 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
356363 /// guaranteed.
357− #[inline]
358364 #[must_use]
359365 pub fn to_vec(&self) -> Vec<u8> {
360366 self.into()
@@ -363,26 +369,21 @@ impl Boolean<'_> { · 2 unmodified lines
363369 /// Generates a byte representation of the value. This should be used when
364370 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
365371 /// guaranteed.
366− #[inline]
367372 #[must_use]
368373 pub fn as_bytes(&self) -> &[u8] {
369374 self.into()
370375 }
371376 }
372377
373−quick_error! {
374− #[derive(Debug, PartialEq)]
375− /// The error returned when creating `Boolean` from byte string.
376− #[allow(missing_docs)]
377− pub enum BooleanError {
378− InvalidFormat {
379− display("Invalid argument format")
380− }
381− }
378+fn bool_err(input: impl Into<BString>) -> value::parse::Error {
379+ value::parse::Error::new(
380+ "Booleans need to be 'no', 'off', 'false', 'zero' or 'yes', 'on', 'true', 'one'",
381+ input,
382+ )
382383 }
383384
384385 impl<'a> TryFrom<&'a [u8]> for Boolean<'a> {
385− type Error = BooleanError;
386+ type Error = value::parse::Error;
386387
387388 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
388389 if let Ok(v) = TrueVariant::try_from(value) {
@@ -400,12 +401,12 @@ impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { · 11 unmodified lines
400401 ));
401402 }
402403
403− Err(BooleanError::InvalidFormat)
404+ Err(bool_err(value))
404405 }
405406 }
406407
407408 impl TryFrom<Vec<u8>> for Boolean<'_> {
408− type Error = BooleanError;
409+ type Error = value::parse::Error;
409410
410411 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
411412 if value.eq_ignore_ascii_case(b"no")
@@ -424,7 +425,7 @@ impl TryFrom<Vec<u8>> for Boolean<'_> { · 12 unmodified lines
424425 }
425426
426427 impl<'a> TryFrom<Cow<'a, [u8]>> for Boolean<'a> {
427− type Error = BooleanError;
428+ type Error = value::parse::Error;
428429 fn try_from(c: Cow<'a, [u8]>) -> Result<Self, Self::Error> {
429430 match c {
430431 Cow::Borrowed(c) => Self::try_from(c),
@@ -434,7 +435,6 @@ impl<'a> TryFrom<Cow<'a, [u8]>> for Boolean<'a> { · 3 unmodified lines
434435 }
435436
436437 impl Display for Boolean<'_> {
437− #[inline]
438438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439439 match self {
440440 Boolean::True(v) => v.fmt(f),
@@ -444,7 +444,6 @@ impl Display for Boolean<'_> { · 3 unmodified lines
444444 }
445445
446446 impl From<Boolean<'_>> for bool {
447− #[inline]
448447 fn from(b: Boolean) -> Self {
449448 match b {
450449 Boolean::True(_) => true,
@@ -454,7 +453,6 @@ impl From<Boolean<'_>> for bool { · 3 unmodified lines
454453 }
455454
456455 impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] {
457− #[inline]
458456 fn from(b: &'b Boolean) -> Self {
459457 match b {
460458 Boolean::True(t) => t.into(),
@@ -464,14 +462,12 @@ impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] { · 3 unmodified lines
464462 }
465463
466464 impl From<Boolean<'_>> for Vec<u8> {
467− #[inline]
468465 fn from(b: Boolean) -> Self {
469466 b.into()
470467 }
471468 }
472469
473470 impl From<&Boolean<'_>> for Vec<u8> {
474− #[inline]
475471 fn from(b: &Boolean) -> Self {
476472 b.to_string().into_bytes()
477473 }
@@ -502,7 +498,7 @@ pub enum TrueVariant<'a> { · 24 unmodified lines
502498 }
503499
504500 impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> {
505− type Error = BooleanError;
501+ type Error = value::parse::Error;
506502
507503 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
508504 if value.eq_ignore_ascii_case(b"yes")
@@ -516,13 +512,13 @@ impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> { · 7 unmodified lines
516512 } else if value.is_empty() {
517513 Ok(Self::Implicit)
518514 } else {
519− Err(BooleanError::InvalidFormat)
515+ Err(bool_err(value))
520516 }
521517 }
522518 }
523519
524520 impl TryFrom<Vec<u8>> for TrueVariant<'_> {
525− type Error = BooleanError;
521+ type Error = value::parse::Error;
526522
527523 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
528524 if value.eq_ignore_ascii_case(b"yes")
@@ -536,7 +532,7 @@ impl TryFrom<Vec<u8>> for TrueVariant<'_> { · 7 unmodified lines
536532 } else if value.is_empty() {
537533 Ok(Self::Implicit)
538534 } else {
539− Err(BooleanError::InvalidFormat)
535+ Err(bool_err(value))
540536 }
541537 }
542538 }
@@ -552,7 +548,6 @@ impl Display for TrueVariant<'_> { · 9 unmodified lines
552548 }
553549
554550 impl<'a, 'b: 'a> From<&'b TrueVariant<'a>> for &'a [u8] {
555− #[inline]
556551 fn from(t: &'b TrueVariant<'a>) -> Self {
557552 match t {
558553 TrueVariant::Explicit(e) => e.as_bytes(),
@@ -641,31 +636,18 @@ impl Serialize for Integer { · 82 unmodified lines
641636 }
642637 }
643638
644−quick_error! {
645− #[derive(Debug)]
646− /// The error returned when creating `Integer` from byte string.
647− #[allow(missing_docs)]
648− pub enum IntegerError {
649− Utf8Conversion(err: std::str::Utf8Error) {
650− display("Ill-formed UTF-8")
651− source(err)
652− from()
653− }
654− InvalidFormat {
655− display("Invalid argument format")
656− }
657− InvalidSuffix {
658− display("Invalid suffix")
659− }
660− }
639+fn int_err(input: impl Into<BString>) -> value::parse::Error {
640+ value::parse::Error::new(
641+ "Intgers needs to be positive or negative numbers which may have a suffix like 1k, or 50G",
642+ input,
643+ )
661644 }
662645
663646 impl TryFrom<&[u8]> for Integer {
664− type Error = IntegerError;
647+ type Error = value::parse::Error;
665648
666− #[inline]
667649 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
668− let s = std::str::from_utf8(s)?;
650+ let s = std::str::from_utf8(s).map_err(|err| int_err(s).with_err(err))?;
669651 if let Ok(value) = s.parse() {
670652 return Ok(Self { value, suffix: None });
671653 }
@@ -673,7 +655,7 @@ impl TryFrom<&[u8]> for Integer { · 1 unmodified line
673655 // Assume we have a prefix at this point.
674656
675657 if s.len() <= 1 {
676− return Err(IntegerError::InvalidFormat);
658+ return Err(int_err(s));
677659 }
678660
679661 let (number, suffix) = s.split_at(s.len() - 1);
@@ -683,24 +665,22 @@ impl TryFrom<&[u8]> for Integer { · 3 unmodified lines
683665 suffix: Some(suffix),
684666 })
685667 } else {
686− Err(IntegerError::InvalidFormat)
668+ Err(int_err(s))
687669 }
688670 }
689671 }
690672
691673 impl TryFrom<Vec<u8>> for Integer {
692− type Error = IntegerError;
674+ type Error = value::parse::Error;
693675
694− #[inline]
695676 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
696677 Self::try_from(value.as_ref())
697678 }
698679 }
699680
700681 impl TryFrom<Cow<'_, [u8]>> for Integer {
701− type Error = IntegerError;
682+ type Error = value::parse::Error;
702683
703− #[inline]
704684 fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {
705685 match c {
706686 Cow::Borrowed(c) => Self::try_from(c),
@@ -710,14 +690,12 @@ impl TryFrom<Cow<'_, [u8]>> for Integer { · 3 unmodified lines
710690 }
711691
712692 impl From<Integer> for Vec<u8> {
713− #[inline]
714693 fn from(i: Integer) -> Self {
715694 i.into()
716695 }
717696 }
718697
719698 impl From<&Integer> for Vec<u8> {
720− #[inline]
721699 fn from(i: &Integer) -> Self {
722700 i.to_string().into_bytes()
723701 }
@@ -736,7 +714,6 @@ pub enum IntegerSuffix { · 12 unmodified lines
736714
737715 impl IntegerSuffix {
738716 /// Returns the number of bits that the suffix shifts left by.
739− #[inline]
740717 #[must_use]
741718 pub const fn bitwise_offset(self) -> usize {
742719 match self {
@@ -748,7 +725,6 @@ impl IntegerSuffix { · 5 unmodified lines
748725 }
749726
750727 impl Display for IntegerSuffix {
751− #[inline]
752728 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
753729 match self {
754730 Self::Kibi => write!(f, "k"),
@@ -773,32 +749,29 @@ impl Serialize for IntegerSuffix { · 18 unmodified lines
773749 }
774750
775751 impl FromStr for IntegerSuffix {
776− type Err = IntegerError;
752+ type Err = ();
777753
778− #[inline]
779754 fn from_str(s: &str) -> Result<Self, Self::Err> {
780755 match s {
781756 "k" | "K" => Ok(Self::Kibi),
782757 "m" | "M" => Ok(Self::Mebi),
783758 "g" | "G" => Ok(Self::Gibi),
784− _ => Err(IntegerError::InvalidSuffix),
759+ _ => Err(()),
785760 }
786761 }
787762 }
788763
789764 impl TryFrom<&[u8]> for IntegerSuffix {
790− type Error = IntegerError;
765+ type Error = ();
791766
792− #[inline]
793767 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
794− Self::from_str(std::str::from_utf8(s)?)
768+ Self::from_str(std::str::from_utf8(s).map_err(|_| ())?)
795769 }
796770 }
797771
798772 impl TryFrom<Vec<u8>> for IntegerSuffix {
799− type Error = IntegerError;
773+ type Error = ();
800774
801− #[inline]
802775 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
803776 Self::try_from(value.as_ref())
804777 }
@@ -825,7 +798,6 @@ impl Color { · 20 unmodified lines
825798 /// Generates a byte representation of the value. This should be used when
826799 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
827800 /// guaranteed.
828− #[inline]
829801 #[must_use]
830802 pub fn to_vec(&self) -> Vec<u8> {
831803 self.into()
@@ -861,31 +833,18 @@ impl Serialize for Color { · 29 unmodified lines
861833 }
862834 }
863835
864−quick_error! {
865− #[derive(Debug, PartialEq)]
866− ///
867− #[allow(missing_docs)]
868− pub enum ColorError {
869− Utf8Conversion(err: std::str::Utf8Error) {
870− display("Ill-formed UTF-8")
871− source(err)
872− from()
873− }
874− InvalidColorItem {
875− display("Invalid color item")
876− }
877− InvalidFormat {
878− display("Invalid argument format")
879− }
880− }
836+fn color_err(input: impl Into<BString>) -> value::parse::Error {
837+ value::parse::Error::new(
838+ "Colors are specific color values and their attributes, like 'brightred', or 'blue'",
839+ input,
840+ )
881841 }
882842
883843 impl TryFrom<&[u8]> for Color {
884− type Error = ColorError;
844+ type Error = value::parse::Error;
885845
886− #[inline]
887846 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
888− let s = std::str::from_utf8(s)?;
847+ let s = std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?;
889848 enum ColorItem {
890849 Value(ColorValue),
891850 Attr(ColorAttribute),
@@ -913,12 +872,12 @@ impl TryFrom<&[u8]> for Color { · 21 unmodified lines
913872 } else if new_self.background.is_none() {
914873 new_self.background = Some(v);
915874 } else {
916− return Err(ColorError::InvalidColorItem);
875+ return Err(color_err(s));
917876 }
918877 }
919878 ColorItem::Attr(a) => new_self.attributes.push(a),
920879 },
921− Err(_) => return Err(ColorError::InvalidColorItem),
880+ Err(_) => return Err(color_err(s)),
922881 }
923882 }
924883
@@ -927,18 +886,16 @@ impl TryFrom<&[u8]> for Color { · 2 unmodified lines
927886 }
928887
929888 impl TryFrom<Vec<u8>> for Color {
930− type Error = ColorError;
889+ type Error = value::parse::Error;
931890
932− #[inline]
933891 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
934892 Self::try_from(value.as_ref())
935893 }
936894 }
937895
938896 impl TryFrom<Cow<'_, [u8]>> for Color {
939− type Error = ColorError;
897+ type Error = value::parse::Error;
940898
941− #[inline]
942899 fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {
943900 match c {
944901 Cow::Borrowed(c) => Self::try_from(c),
@@ -948,14 +905,12 @@ impl TryFrom<Cow<'_, [u8]>> for Color { · 3 unmodified lines
948905 }
949906
950907 impl From<Color> for Vec<u8> {
951− #[inline]
952908 fn from(c: Color) -> Self {
953909 c.into()
954910 }
955911 }
956912
957913 impl From<&Color> for Vec<u8> {
958− #[inline]
959914 fn from(c: &Color) -> Self {
960915 c.to_string().into_bytes()
961916 }
@@ -1026,7 +981,7 @@ impl Serialize for ColorValue { · 64 unmodified lines
1026981 }
1027982
1028983 impl FromStr for ColorValue {
1029− type Err = ColorError;
984+ type Err = value::parse::Error;
1030985
1031986 fn from_str(s: &str) -> Result<Self, Self::Err> {
1032987 let mut s = s;
@@ -1039,7 +994,7 @@ impl FromStr for ColorValue { · 6 unmodified lines
1039994
1040995 match s {
1041996 "normal" if !bright => return Ok(Self::Normal),
1042− "normal" if bright => return Err(ColorError::InvalidFormat),
997+ "normal" if bright => return Err(color_err(s)),
1043998 "black" if !bright => return Ok(Self::Black),
1044999 "black" if bright => return Ok(Self::BrightBlack),
10451000 "red" if !bright => return Ok(Self::Red),
@@ -1077,16 +1032,15 @@ impl FromStr for ColorValue { · 31 unmodified lines
10771032 }
10781033 }
10791034
1080− Err(ColorError::InvalidFormat)
1035+ Err(color_err(s))
10811036 }
10821037 }
10831038
10841039 impl TryFrom<&[u8]> for ColorValue {
1085− type Error = ColorError;
1040+ type Error = value::parse::Error;
10861041
1087− #[inline]
10881042 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
1089− Self::from_str(std::str::from_utf8(s)?)
1043+ Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?)
10901044 }
10911045 }
10921046
@@ -1161,7 +1115,7 @@ impl Serialize for ColorAttribute { · 68 unmodified lines
11611115 }
11621116
11631117 impl FromStr for ColorAttribute {
1164− type Err = ColorError;
1118+ type Err = value::parse::Error;
11651119
11661120 fn from_str(s: &str) -> Result<Self, Self::Err> {
11671121 let inverted = s.starts_with("no");
@@ -1190,16 +1144,15 @@ impl FromStr for ColorAttribute { · 22 unmodified lines
11901144 "italic" if inverted => Ok(Self::NoItalic),
11911145 "strike" if !inverted => Ok(Self::Strike),
11921146 "strike" if inverted => Ok(Self::NoStrike),
1193− _ => Err(ColorError::InvalidFormat),
1147+ _ => Err(color_err(parsed)),
11941148 }
11951149 }
11961150 }
11971151
11981152 impl TryFrom<&[u8]> for ColorAttribute {
1199− type Error = ColorError;
1153+ type Error = value::parse::Error;
12001154
1201− #[inline]
12021155 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
1203− Self::from_str(std::str::from_utf8(s)?)
1156+ Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?)
12041157 }
12051158 }
git-config/tests/git_config/mod.rs+139−108
@@ -19,7 +19,7 @@ mod mutable_value { · 18 unmodified lines
1919 fn value_is_correct() {
2020 let mut git_config = init_config();
2121
22− let value = git_config.get_raw_value_mut("core", None, "a").unwrap();
22+ let value = git_config.raw_value_mut("core", None, "a").unwrap();
2323 assert_eq!(&*value.get().unwrap(), b"b100");
2424 }
2525
@@ -27,7 +27,7 @@ mod mutable_value { · 1 unmodified line
2727 fn set_string_cleanly_updates() {
2828 let mut git_config = init_config();
2929
30− let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap();
30+ let mut value = git_config.raw_value_mut("core", None, "a").unwrap();
3131 value.set_string("hello world".to_string());
3232 assert_eq!(
3333 git_config.to_string(),
@@ -38,7 +38,7 @@ mod mutable_value { · 4 unmodified lines
3838 e=f"#,
3939 );
4040
41− let mut value = git_config.get_raw_value_mut("core", None, "e").unwrap();
41+ let mut value = git_config.raw_value_mut("core", None, "e").unwrap();
4242 value.set_string(String::new());
4343 assert_eq!(
4444 git_config.to_string(),
@@ -54,7 +54,7 @@ mod mutable_value { · 9 unmodified lines
5454 fn delete_value() {
5555 let mut git_config = init_config();
5656
57− let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap();
57+ let mut value = git_config.raw_value_mut("core", None, "a").unwrap();
5858 value.delete();
5959 assert_eq!(
6060 git_config.to_string(),
@@ -63,7 +63,7 @@ mod mutable_value { · 2 unmodified lines
6363 e=f",
6464 );
6565
66− let mut value = git_config.get_raw_value_mut("core", None, "c").unwrap();
66+ let mut value = git_config.raw_value_mut("core", None, "c").unwrap();
6767 value.delete();
6868 assert_eq!(
6969 git_config.to_string(),
@@ -75,7 +75,7 @@ mod mutable_value { · 5 unmodified lines
7575 fn get_value_after_deleted() {
7676 let mut git_config = init_config();
7777
78− let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap();
78+ let mut value = git_config.raw_value_mut("core", None, "a").unwrap();
7979 value.delete();
8080 assert!(value.get().is_err());
8181 }
@@ -84,7 +84,7 @@ mod mutable_value { · 2 unmodified lines
8484 fn set_string_after_deleted() {
8585 let mut git_config = init_config();
8686
87− let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap();
87+ let mut value = git_config.raw_value_mut("core", None, "a").unwrap();
8888 value.delete();
8989 value.set_string("hello world".to_string());
9090 assert_eq!(
@@ -101,7 +101,7 @@ mod mutable_value { · 10 unmodified lines
101101 fn subsequent_delete_calls_are_noop() {
102102 let mut git_config = init_config();
103103
104− let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap();
104+ let mut value = git_config.raw_value_mut("core", None, "a").unwrap();
105105 for _ in 0..10 {
106106 value.delete();
107107 }
@@ -124,7 +124,7 @@ b · 16 unmodified lines
124124 e=f"#,
125125 )
126126 .unwrap();
127− let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap();
127+ let mut value = git_config.raw_value_mut("core", None, "a").unwrap();
128128 assert_eq!(&*value.get().unwrap(), b"b100b");
129129 value.delete();
130130 assert_eq!(
@@ -157,7 +157,7 @@ mod mutable_multi_value { · 26 unmodified lines
157157 fn value_is_correct() {
158158 let mut git_config = init_config();
159159
160− let value = git_config.get_raw_multi_value_mut("core", None, "a").unwrap();
160+ let value = git_config.raw_multi_value_mut("core", None, "a").unwrap();
161161 assert_eq!(
162162 &*value.get().unwrap(),
163163 vec![
@@ -171,17 +171,14 @@ mod mutable_multi_value { · 7 unmodified lines
171171 #[test]
172172 fn non_empty_sizes_are_correct() {
173173 let mut git_config = init_config();
174− assert_eq!(git_config.get_raw_multi_value_mut("core", None, "a").unwrap().len(), 3);
175− assert!(!git_config
176− .get_raw_multi_value_mut("core", None, "a")
177− .unwrap()
178− .is_empty());
174+ assert_eq!(git_config.raw_multi_value_mut("core", None, "a").unwrap().len(), 3);
175+ assert!(!git_config.raw_multi_value_mut("core", None, "a").unwrap().is_empty());
179176 }
180177
181178 #[test]
182179 fn set_value_at_start() {
183180 let mut git_config = init_config();
184− let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap();
181+ let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap();
185182 values.set_string(0, "Hello".to_string());
186183 assert_eq!(
187184 git_config.to_string(),
@@ -196,7 +193,7 @@ mod mutable_multi_value { · 8 unmodified lines
196193 #[test]
197194 fn set_value_at_end() {
198195 let mut git_config = init_config();
199− let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap();
196+ let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap();
200197 values.set_string(2, "Hello".to_string());
201198 assert_eq!(
202199 git_config.to_string(),
@@ -211,7 +208,7 @@ mod mutable_multi_value { · 8 unmodified lines
211208 #[test]
212209 fn set_values_all() {
213210 let mut git_config = init_config();
214− let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap();
211+ let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap();
215212 values.set_owned_values_all(b"Hello");
216213 assert_eq!(
217214 git_config.to_string(),
@@ -226,7 +223,7 @@ mod mutable_multi_value { · 8 unmodified lines
226223 #[test]
227224 fn delete() {
228225 let mut git_config = init_config();
229− let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap();
226+ let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap();
230227 values.delete(0);
231228 assert_eq!(
232229 git_config.to_string(),
@@ -239,7 +236,7 @@ mod mutable_multi_value { · 6 unmodified lines
239236 #[test]
240237 fn delete_all() {
241238 let mut git_config = init_config();
242− let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap();
239+ let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap();
243240 values.delete_all();
244241 assert!(values.get().is_err());
245242 assert_eq!(
@@ -261,7 +258,7 @@ b · 15 unmodified lines
261258 a"#,
262259 )
263260 .unwrap();
264− let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap();
261+ let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap();
265262
266263 assert_eq!(
267264 &*values.get().unwrap(),
@@ -320,8 +317,8 @@ mod from_paths_tests { · 52 unmodified lines
320317 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();
321318
322319 assert_eq!(
323− config.get_raw_value("core", None, "boolean"),
324− Ok(Cow::<[u8]>::Borrowed(b"true"))
320+ config.raw_value("core", None, "boolean").unwrap(),
321+ Cow::<[u8]>::Borrowed(b"true")
325322 );
326323
327324 assert_eq!(config.len(), 1);
@@ -390,26 +387,26 @@ mod from_paths_tests { · 62 unmodified lines
390387 let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap();
391388
392389 assert_eq!(
393− config.get_raw_value("core", None, "c"),
394− Ok(Cow::<[u8]>::Borrowed(b"12"))
390+ config.raw_value("core", None, "c").unwrap(),
391+ Cow::<[u8]>::Borrowed(b"12")
395392 );
396393 assert_eq!(
397− config.get_raw_value("core", None, "d"),
398− Ok(Cow::<[u8]>::Borrowed(b"41"))
394+ config.raw_value("core", None, "d").unwrap(),
395+ Cow::<[u8]>::Borrowed(b"41")
399396 );
400397 assert_eq!(
401− config.get_raw_value("http", None, "sslVerify"),
402− Ok(Cow::<[u8]>::Borrowed(b"false"))
398+ config.raw_value("http", None, "sslVerify").unwrap(),
399+ Cow::<[u8]>::Borrowed(b"false")
403400 );
404401
405402 assert_eq!(
406− config.get_raw_value("diff", None, "renames"),
407− Ok(Cow::<[u8]>::Borrowed(b"true"))
403+ config.raw_value("diff", None, "renames").unwrap(),
404+ Cow::<[u8]>::Borrowed(b"true")
408405 );
409406
410407 assert_eq!(
411− config.get_raw_value("core", None, "a"),
412− Ok(Cow::<[u8]>::Borrowed(b"false"))
408+ config.raw_value("core", None, "a").unwrap(),
409+ Cow::<[u8]>::Borrowed(b"false")
413410 );
414411 }
415412
@@ -452,7 +449,7 @@ mod from_paths_tests { · 36 unmodified lines
452449 let options = from_paths::Options::default();
453450 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();
454451 assert_eq!(
455− config.get_raw_multi_value("core", None, "i").unwrap(),
452+ config.raw_multi_value("core", None, "i").unwrap(),
456453 vec![
457454 Cow::Borrowed(b"0"),
458455 Cow::Borrowed(b"1"),
@@ -470,12 +467,18 @@ mod from_paths_tests { · 11 unmodified lines
470467 ..Default::default()
471468 };
472469 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();
473− assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"1")));
470+ assert_eq!(
471+ config.raw_value("core", None, "i").unwrap(),
472+ Cow::<[u8]>::Borrowed(b"1")
473+ );
474474
475475 // with default max_allowed_depth of 10 and 4 levels of includes, last level is read
476476 let options = from_paths::Options::default();
477477 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();
478− assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"4")));
478+ assert_eq!(
479+ config.raw_value("core", None, "i").unwrap(),
480+ Cow::<[u8]>::Borrowed(b"4")
481+ );
479482
480483 // with max_allowed_depth of 5, the base and 4 levels of includes, last level is read
481484 let options = from_paths::Options {
@@ -483,7 +486,10 @@ mod from_paths_tests { · 1 unmodified line
483486 ..Default::default()
484487 };
485488 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();
486− assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"4")));
489+ assert_eq!(
490+ config.raw_value("core", None, "i").unwrap(),
491+ Cow::<[u8]>::Borrowed(b"4")
492+ );
487493
488494 // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned
489495 let options = from_paths::Options {
@@ -503,7 +509,10 @@ mod from_paths_tests { · 13 unmodified lines
503509 ..Default::default()
504510 };
505511 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();
506− assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"2")));
512+ assert_eq!(
513+ config.raw_value("core", None, "i").unwrap(),
514+ Cow::<[u8]>::Borrowed(b"2")
515+ );
507516
508517 // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned
509518 let options = from_paths::Options {
@@ -552,8 +561,8 @@ mod from_paths_tests { · 42 unmodified lines
552561
553562 let config = GitConfig::from_paths(vec![a_path], &Default::default()).unwrap();
554563 assert_eq!(
555− config.get_raw_value("core", None, "b"),
556− Ok(Cow::<[u8]>::Borrowed(b"false"))
564+ config.raw_value("core", None, "b").unwrap(),
565+ Cow::<[u8]>::Borrowed(b"false")
557566 );
558567 }
559568
@@ -607,7 +616,7 @@ mod from_paths_tests { · 47 unmodified lines
607616 };
608617 let config = GitConfig::from_paths(vec![a_path], &options).unwrap();
609618 assert_eq!(
610− config.get_raw_multi_value("core", None, "b").unwrap(),
619+ config.raw_multi_value("core", None, "b").unwrap(),
611620 vec![
612621 Cow::Borrowed(b"0"),
613622 Cow::Borrowed(b"1"),
@@ -662,16 +671,19 @@ mod from_paths_tests { · 48 unmodified lines
662671
663672 let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap();
664673
665− assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"1")));
674+ assert_eq!(
675+ config.raw_value("core", None, "c").unwrap(),
676+ Cow::<[u8]>::Borrowed(b"1")
677+ );
666678
667679 assert_eq!(
668− config.get_raw_value("core", None, "b"),
669− Ok(Cow::<[u8]>::Borrowed(b"true"))
680+ config.raw_value("core", None, "b").unwrap(),
681+ Cow::<[u8]>::Borrowed(b"true")
670682 );
671683
672684 assert_eq!(
673− config.get_raw_value("core", None, "a"),
674− Ok(Cow::<[u8]>::Borrowed(b"false"))
685+ config.raw_value("core", None, "a").unwrap(),
686+ Cow::<[u8]>::Borrowed(b"false")
675687 );
676688 }
677689
@@ -695,18 +707,18 @@ mod from_paths_tests { · 17 unmodified lines
695707 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();
696708
697709 assert_eq!(
698− config.get_raw_value("core", None, "a"),
699− Ok(Cow::<[u8]>::Borrowed(b"false"))
710+ config.raw_value("core", None, "a").unwrap(),
711+ Cow::<[u8]>::Borrowed(b"false")
700712 );
701713
702714 assert_eq!(
703− config.get_raw_value("core", None, "b"),
704− Ok(Cow::<[u8]>::Borrowed(b"true"))
715+ config.raw_value("core", None, "b").unwrap(),
716+ Cow::<[u8]>::Borrowed(b"true")
705717 );
706718
707719 assert_eq!(
708− config.get_raw_value("core", None, "c"),
709− Ok(Cow::<[u8]>::Borrowed(b"true"))
720+ config.raw_value("core", None, "c").unwrap(),
721+ Cow::<[u8]>::Borrowed(b"true")
710722 );
711723
712724 assert_eq!(config.len(), 4);
@@ -735,12 +747,12 @@ mod from_paths_tests { · 22 unmodified lines
735747 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();
736748
737749 assert_eq!(
738− config.get_raw_multi_value("core", None, "key").unwrap(),
750+ config.raw_multi_value("core", None, "key").unwrap(),
739751 vec![Cow::Borrowed(b"a"), Cow::Borrowed(b"b"), Cow::Borrowed(b"c")]
740752 );
741753
742754 assert_eq!(
743− config.get_raw_multi_value("include", None, "path").unwrap(),
755+ config.raw_multi_value("include", None, "path").unwrap(),
744756 vec![Cow::Borrowed(b"d_path"), Cow::Borrowed(b"e_path")]
745757 );
746758
@@ -802,7 +814,7 @@ mod from_env_tests { · 55 unmodified lines
802814 fn parse_error_with_invalid_count() {
803815 let _env = Env::new().set("GIT_CONFIG_COUNT", "invalid");
804816 let err = GitConfig::from_env(&Options::default()).unwrap_err();
805− assert!(matches!(err, from_env::Error::ParseError(_)));
817+ assert!(matches!(err, from_env::Error::ParseError { .. }));
806818 }
807819
808820 #[test]
@@ -815,8 +827,8 @@ mod from_env_tests { · 6 unmodified lines
815827
816828 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();
817829 assert_eq!(
818− config.get_raw_value("core", None, "key"),
819− Ok(Cow::<[u8]>::Borrowed(b"value"))
830+ config.raw_value("core", None, "key").unwrap(),
831+ Cow::<[u8]>::Borrowed(b"value")
820832 );
821833
822834 assert_eq!(config.len(), 1);
@@ -836,9 +848,18 @@ mod from_env_tests { · 13 unmodified lines
836848
837849 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();
838850
839− assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"a")));
840− assert_eq!(config.get_raw_value("core", None, "b"), Ok(Cow::<[u8]>::Borrowed(b"b")));
841− assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"c")));
851+ assert_eq!(
852+ config.raw_value("core", None, "a").unwrap(),
853+ Cow::<[u8]>::Borrowed(b"a")
854+ );
855+ assert_eq!(
856+ config.raw_value("core", None, "b").unwrap(),
857+ Cow::<[u8]>::Borrowed(b"b")
858+ );
859+ assert_eq!(
860+ config.raw_value("core", None, "c").unwrap(),
861+ Cow::<[u8]>::Borrowed(b"c")
862+ );
842863 assert_eq!(config.len(), 3);
843864 }
844865
@@ -881,8 +902,8 @@ mod from_env_tests { · 36 unmodified lines
881902 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();
882903
883904 assert_eq!(
884− config.get_raw_value("core", None, "key"),
885− Ok(Cow::<[u8]>::Borrowed(b"changed"))
905+ config.raw_value("core", None, "key").unwrap(),
906+ Cow::<[u8]>::Borrowed(b"changed")
886907 );
887908 assert_eq!(config.len(), 5);
888909 }
@@ -892,64 +913,76 @@ mod from_env_tests { · 3 unmodified lines
892913 mod get_raw_value {
893914 use std::{borrow::Cow, convert::TryFrom};
894915
895− use git_config::{
896− file::{GitConfig, GitConfigError},
897− parser::SectionHeaderName,
898− };
916+ use git_config::{file::GitConfig, lookup};
899917
900918 #[test]
901919 fn single_section() {
902920 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
903− assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"b")));
904− assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"d")));
921+ assert_eq!(
922+ config.raw_value("core", None, "a").unwrap(),
923+ Cow::<[u8]>::Borrowed(b"b")
924+ );
925+ assert_eq!(
926+ config.raw_value("core", None, "c").unwrap(),
927+ Cow::<[u8]>::Borrowed(b"d")
928+ );
905929 }
906930
907931 #[test]
908932 fn last_one_wins_respected_in_section() {
909933 let config = GitConfig::try_from("[core]\na=b\na=d").unwrap();
910− assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"d")));
934+ assert_eq!(
935+ config.raw_value("core", None, "a").unwrap(),
936+ Cow::<[u8]>::Borrowed(b"d")
937+ );
911938 }
912939
913940 #[test]
914941 fn last_one_wins_respected_across_section() {
915942 let config = GitConfig::try_from("[core]\na=b\n[core]\na=d").unwrap();
916− assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"d")));
943+ assert_eq!(
944+ config.raw_value("core", None, "a").unwrap(),
945+ Cow::<[u8]>::Borrowed(b"d")
946+ );
917947 }
918948
919949 #[test]
920950 fn section_not_found() {
921951 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
922− assert_eq!(
923− config.get_raw_value("foo", None, "a"),
924− Err(GitConfigError::SectionDoesNotExist(SectionHeaderName("foo".into())))
925− );
952+ assert!(matches!(
953+ config.raw_value("foo", None, "a"),
954+ Err(lookup::existing::Error::SectionMissing)
955+ ));
926956 }
927957
928958 #[test]
929959 fn subsection_not_found() {
930960 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
931− assert_eq!(
932− config.get_raw_value("core", Some("a"), "a"),
933− Err(GitConfigError::SubSectionDoesNotExist(Some("a")))
934− );
961+ assert!(matches!(
962+ config.raw_value("core", Some("a"), "a"),
963+ Err(lookup::existing::Error::SubSectionMissing)
964+ ));
935965 }
936966
937967 #[test]
938968 fn key_not_found() {
939969 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
940− assert_eq!(
941− config.get_raw_value("core", None, "aaaaaa"),
942− Err(GitConfigError::KeyDoesNotExist)
943− );
970+ assert!(matches!(
971+ config.raw_value("core", None, "aaaaaa"),
972+ Err(lookup::existing::Error::KeyMissing)
973+ ));
944974 }
945975
946976 #[test]
947977 fn subsection_must_be_respected() {
948978 let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap();
949− assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"b")));
950979 assert_eq!(
951− config.get_raw_value("core", Some("a"), "a"),
952− Ok(Cow::<[u8]>::Borrowed(b"c"))
980+ config.raw_value("core", None, "a").unwrap(),
981+ Cow::<[u8]>::Borrowed(b"b")
982+ );
983+ assert_eq!(
984+ config.raw_value("core", Some("a"), "a").unwrap(),
985+ Cow::<[u8]>::Borrowed(b"c")
953986 );
954987 }
955988 }
@@ -1008,17 +1041,15 @@ mod get_value { · 52 unmodified lines
10081041 mod get_raw_multi_value {
10091042 use std::{borrow::Cow, convert::TryFrom};
10101043
1011− use git_config::{
1012− file::{GitConfig, GitConfigError},
1013− parser::SectionHeaderName,
1014− };
1044+ use git_config::file::GitConfig;
1045+ use git_config::lookup;
10151046
10161047 #[test]
10171048 fn single_value_is_identical_to_single_value_query() {
10181049 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
10191050 assert_eq!(
1020− vec![config.get_raw_value("core", None, "a").unwrap()],
1021− config.get_raw_multi_value("core", None, "a").unwrap()
1051+ vec![config.raw_value("core", None, "a").unwrap()],
1052+ config.raw_multi_value("core", None, "a").unwrap()
10221053 );
10231054 }
10241055
@@ -1026,7 +1057,7 @@ mod get_raw_multi_value { · 1 unmodified line
10261057 fn multi_value_in_section() {
10271058 let config = GitConfig::try_from("[core]\na=b\na=c").unwrap();
10281059 assert_eq!(
1029− config.get_raw_multi_value("core", None, "a").unwrap(),
1060+ config.raw_multi_value("core", None, "a").unwrap(),
10301061 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c")]
10311062 );
10321063 }
@@ -1035,7 +1066,7 @@ mod get_raw_multi_value { · 2 unmodified lines
10351066 fn multi_value_across_sections() {
10361067 let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d").unwrap();
10371068 assert_eq!(
1038− config.get_raw_multi_value("core", None, "a").unwrap(),
1069+ config.raw_multi_value("core", None, "a").unwrap(),
10391070 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")]
10401071 );
10411072 }
@@ -1043,39 +1074,39 @@ mod get_raw_multi_value { · 1 unmodified line
10431074 #[test]
10441075 fn section_not_found() {
10451076 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
1046− assert_eq!(
1047− config.get_raw_multi_value("foo", None, "a"),
1048− Err(GitConfigError::SectionDoesNotExist(SectionHeaderName("foo".into())))
1049− );
1077+ assert!(matches!(
1078+ config.raw_multi_value("foo", None, "a"),
1079+ Err(lookup::existing::Error::SectionMissing)
1080+ ));
10501081 }
10511082
10521083 #[test]
10531084 fn subsection_not_found() {
10541085 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
1055− assert_eq!(
1056− config.get_raw_multi_value("core", Some("a"), "a"),
1057− Err(GitConfigError::SubSectionDoesNotExist(Some("a")))
1058− );
1086+ assert!(matches!(
1087+ config.raw_multi_value("core", Some("a"), "a"),
1088+ Err(lookup::existing::Error::SubSectionMissing)
1089+ ));
10591090 }
10601091
10611092 #[test]
10621093 fn key_not_found() {
10631094 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
1064− assert_eq!(
1065− config.get_raw_multi_value("core", None, "aaaaaa"),
1066− Err(GitConfigError::KeyDoesNotExist)
1067− );
1095+ assert!(matches!(
1096+ config.raw_multi_value("core", None, "aaaaaa"),
1097+ Err(lookup::existing::Error::KeyMissing)
1098+ ));
10681099 }
10691100
10701101 #[test]
10711102 fn subsection_must_be_respected() {
10721103 let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap();
10731104 assert_eq!(
1074− config.get_raw_multi_value("core", None, "a").unwrap(),
1105+ config.raw_multi_value("core", None, "a").unwrap(),
10751106 vec![Cow::Borrowed(b"b")]
10761107 );
10771108 assert_eq!(
1078− config.get_raw_multi_value("core", Some("a"), "a").unwrap(),
1109+ config.raw_multi_value("core", Some("a"), "a").unwrap(),
10791110 vec![Cow::Borrowed(b"c")]
10801111 );
10811112 }
@@ -1084,7 +1115,7 @@ mod get_raw_multi_value { · 2 unmodified lines
10841115 fn non_relevant_subsection_is_ignored() {
10851116 let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d\n[core]g=g").unwrap();
10861117 assert_eq!(
1087− config.get_raw_multi_value("core", None, "a").unwrap(),
1118+ config.raw_multi_value("core", None, "a").unwrap(),
10881119 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")]
10891120 );
10901121 }
git-config/tests/value/mod.rs+22−7
@@ -23,11 +23,20 @@ fn get_value_for_all_provided_values() -> crate::Result { · 22 unmodified lines
2323 file.value::<Boolean>("core", None, "bool-explicit")?,
2424 Boolean::False(Cow::Borrowed("false"))
2525 );
26+ assert!(!file.boolean("core", None, "bool-explicit").expect("exists")?);
2627
2728 assert_eq!(
2829 file.value::<Boolean>("core", None, "bool-implicit")?,
2930 Boolean::True(TrueVariant::Implicit)
3031 );
32+ assert_eq!(
33+ file.try_value::<Boolean>("core", None, "bool-implicit")
34+ .expect("exists")?,
35+ Boolean::True(TrueVariant::Implicit)
36+ );
37+
38+ assert!(file.boolean("core", None, "bool-implicit").expect("present")?);
39+ assert_eq!(file.try_value::<String>("doesnt", None, "exist"), None);
3140
3241 assert_eq!(
3342 file.value::<Integer>("core", None, "integer-no-prefix")?,
@@ -69,15 +78,22 @@ fn get_value_for_all_provided_values() -> crate::Result { · 35 unmodified lines
6978 }
7079 );
7180
81+ assert_eq!(
82+ file.string("core", None, "other").expect("present").as_ref(),
83+ "hello world"
84+ );
85+
7286 let actual = file.value::<git_config::values::Path>("core", None, "location")?;
7387 assert_eq!(
74− &*actual,
75− "~/tmp".as_bytes(),
88+ &*actual, "~/tmp",
7689 "no interpolation occurs when querying a path due to lack of context"
7790 );
7891 let expected = PathBuf::from(format!("{}/tmp", dirs::home_dir().expect("empty home dir").display()));
7992 assert_eq!(actual.interpolate(None).unwrap(), expected);
8093
94+ let actual = file.path("core", None, "location").expect("present");
95+ assert_eq!(&*actual, "~/tmp",);
96+
8197 Ok(())
8298 }
8399
@@ -113,10 +129,9 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { · 29 unmodified lines
113129 fn section_names_are_case_insensitive() -> crate::Result {
114130 let config = "[core] bool-implicit";
115131 let file = GitConfig::try_from(config)?;
116− assert!(file.value::<Boolean>("core", None, "bool-implicit").is_ok());
117132 assert_eq!(
118− file.value::<Boolean>("core", None, "bool-implicit"),
119− file.value::<Boolean>("CORE", None, "bool-implicit")
133+ file.value::<Boolean>("core", None, "bool-implicit").unwrap(),
134+ file.value::<Boolean>("CORE", None, "bool-implicit").unwrap()
120135 );
121136
122137 Ok(())
@@ -130,8 +145,8 @@ fn value_names_are_case_insensitive() -> crate::Result { · 7 unmodified lines
130145 let file = GitConfig::try_from(config)?;
131146 assert_eq!(file.multi_value::<Boolean>("core", None, "a")?.len(), 2);
132147 assert_eq!(
133− file.value::<Boolean>("core", None, "a"),
134− file.value::<Boolean>("core", None, "A")
148+ file.value::<Boolean>("core", None, "a").unwrap(),
149+ file.value::<Boolean>("core", None, "A").unwrap()
135150 );
136151
137152 Ok(())
git-features/Cargo.toml+0−10
@@ -81,11 +81,6 @@ name = "pipe" · 80 unmodified lines
8181 path = "tests/pipe.rs"
8282 required-features = ["io-pipe"]
8383
84−[[test]]
85−name = "path"
86−path = "tests/path.rs"
87−required-features = ["bstr"]
88−
8984 [dependencies]
9085 #! ### Optional Dependencies
9186
@@ -122,11 +117,6 @@ quick-error = { version = "2.0.0", optional = true } · 30 unmodified lines
122117 ## make the `time` module available with access to the local time as configured by the system.
123118 time = { version = "0.3.2", optional = true, default-features = false, features = ["local-offset"] }
124119
125−
126−# path
127−## make bstr utilities available in the `path` modules, which itself is gated by the `path` feature.
128−bstr = { version = "0.2.17", optional = true, default-features = false, features = ["std"] }
129−
130120 document-features = { version = "0.2.0", optional = true }
131121
132122 [target.'cfg(unix)'.dependencies]
git-features/src/lib.rs+0−1
@@ -24,7 +24,6 @@ pub mod interrupt; · 23 unmodified lines
2424 #[cfg(feature = "io-pipe")]
2525 pub mod io;
2626 pub mod parallel;
27−pub mod path;
2827 #[cfg(feature = "progress")]
2928 pub mod progress;
3029 pub mod threading;
git-features/src/path.rs+0−234
@@ -1,234 +0,0 @@
1−//! ### Research
2−//!
3−//! * **windows**
4−//! - [`dirent.c`](https://github.com/git/git/blob/main/compat/win32/dirent.c#L31:L31) contains all implementation (seemingly) of opening directories and reading their entries, along with all path conversions (UTF-16 for windows). This is done on the fly so git can work with [in UTF-8](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12).
5−//! - mingw [is used for the conversion](https://github.com/git/git/blob/main/compat/mingw.h#L579:L579) and it appears they handle surrogates during the conversion, maybe some sort of non-strict UTF-8 converter? Actually it uses [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte)
6−//! under the hood which by now does fail if the UTF-8 would be invalid unicode, i.e. unicode pairs.
7−//! - `OsString` on windows already stores strings as WTF-8, which supports [surrogate pairs](https://unicodebook.readthedocs.io/unicode_encodings.html),
8−//! something that UTF-8 isn't allowed do it for security reasons, after all it's UTF-16 specific and exists only to extend
9−//! the encodable code-points.
10−//! - informative reading on [WTF-8](https://simonsapin.github.io/wtf-8/#motivation) which is the encoding used by Rust
11−//! internally that deals with surrogates and non-wellformed surrogates (those that aren't in pairs).
12−//! * **unix**
13−//! - It uses [opendir](https://man7.org/linux/man-pages/man3/opendir.3.html) and [readdir](https://man7.org/linux/man-pages/man3/readdir.3.html)
14−//! respectively. There is no encoding specified, except that these paths are null-terminated.
15−//!
16−//! ### Learnings
17−//!
18−//! Surrogate pairs are a way to extend the encodable value range in UTF-16 encodings, used primarily on windows and in Javascript.
19−//! For a long time these codepoints used for surrogates, always to be used in pairs, were not assigned, until…they were for rare
20−//! emojies and the likes. The unicode standard does not require surrogates to happen in pairs, even though by now unpaired surrogates
21−//! in UTF-16 are considered ill-formed, which aren't supposed to be converted to UTF-8 for example.
22−//!
23−//! This is the reason we have to deal with `to_string_lossy()`, it's _just_ for that quirk.
24−//!
25−//! This also means the only platform ever eligible to see conversion errors is windows, and there it's only older pre-vista
26−//! windows versions which incorrectly allow ill-formed UTF-16 strings. Newer versions don't perform such conversions anymore, for
27−//! example when going from UTF-16 to UTF-8, they will trigger an error.
28−//!
29−//! ### Conclusions
30−//!
31−//! Since [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) by now is
32−//! fixed (Vista onward) to produce valid UTF-8, lone surrogate codepoints will cause failure, which `git`
33−//! [doesn't care about](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12).
34−//!
35−//! We will, though, which means from now on we can just convert to UTF-8 on windows and bubble up errors where necessary,
36−//! preventing potential mismatched surrogate pairs to ever be saved on disk by gitoxide.
37−//!
38−//! Even though the error only exists on older windows versions, we will represent it in the type system through fallible function calls.
39−//! Callers may `.expect()` on the result to indicate they don't wish to handle this special and rare case. Note that servers should not
40−//! ever get into a code-path which does panic though.
41−
42−use std::{
43− borrow::Cow,
44− ffi::OsStr,
45− path::{Path, PathBuf},
46−};
47−
48−#[derive(Debug)]
49−/// The error type returned by [`into_bytes()`] and others may suffer from failed conversions from or to bytes.
50−pub struct Utf8Error;
51−
52−impl std::fmt::Display for Utf8Error {
53− fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54− f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input")
55− }
56−}
57−
58−impl std::error::Error for Utf8Error {}
59−
60−/// Like [`into_bytes()`], but takes `OsStr` as input for a lossless, but fallible, conversion.
61−pub fn os_str_into_bytes(path: &OsStr) -> Result<&[u8], Utf8Error> {
62− let path = into_bytes(Cow::Borrowed(path.as_ref()))?;
63− match path {
64− Cow::Borrowed(path) => Ok(path),
65− Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"),
66− }
67−}
68−
69−/// Convert the given path either into its raw bytes on unix or its UTF8 encoded counterpart on windows.
70−///
71−/// On windows, if the source Path contains ill-formed, lone surrogates, the UTF-8 conversion will fail
72−/// causing `Utf8Error` to be returned.
73−pub fn into_bytes<'a>(path: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, [u8]>, Utf8Error> {
74− let path = path.into();
75− let utf8_bytes = match path {
76− Cow::Owned(path) => Cow::Owned({
77− #[cfg(unix)]
78− let p = {
79− use std::os::unix::ffi::OsStringExt;
80− path.into_os_string().into_vec()
81− };
82− #[cfg(not(unix))]
83− let p: Vec<_> = path.into_os_string().into_string().map_err(|_| Utf8Error)?.into();
84− p
85− }),
86− Cow::Borrowed(path) => Cow::Borrowed({
87− #[cfg(unix)]
88− let p = {
89− use std::os::unix::ffi::OsStrExt;
90− path.as_os_str().as_bytes()
91− };
92− #[cfg(not(unix))]
93− let p = path.to_str().ok_or(Utf8Error)?.as_bytes();
94− p
95− }),
96− };
97− Ok(utf8_bytes)
98−}
99−
100−/// Similar to [`into_bytes()`] but panics if malformed surrogates are encountered on windows.
101−pub fn into_bytes_or_panic_on_windows<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, [u8]> {
102− into_bytes(path).expect("prefix path doesn't contain ill-formed UTF-8")
103−}
104−
105−/// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on unix.
106−///
107−/// On windows, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. There are some potential
108−/// git versions and windows installation which produce mal-formed UTF-16 if certain emojies are in the path. It's as rare as
109−/// it sounds, but possible.
110−pub fn from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> {
111− #[cfg(unix)]
112− let p = {
113− use std::os::unix::ffi::OsStrExt;
114− OsStr::from_bytes(input).as_ref()
115− };
116− #[cfg(not(unix))]
117− let p = Path::new(std::str::from_utf8(input).map_err(|_| Utf8Error)?);
118− Ok(p)
119−}
120−
121−/// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`.
122−pub fn from_bytes<'a>(input: impl Into<Cow<'a, [u8]>>) -> Result<Cow<'a, Path>, Utf8Error> {
123− let input = input.into();
124− match input {
125− Cow::Borrowed(input) => from_byte_slice(input).map(Cow::Borrowed),
126− Cow::Owned(input) => from_byte_vec(input).map(Cow::Owned),
127− }
128−}
129−
130−/// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input` as bstr.
131−#[cfg(feature = "bstr")]
132−pub fn from_bstr<'a>(input: impl Into<Cow<'a, bstr::BStr>>) -> Result<Cow<'a, Path>, Utf8Error> {
133− let input = input.into();
134− match input {
135− Cow::Borrowed(input) => from_byte_slice(input).map(Cow::Borrowed),
136− Cow::Owned(input) => from_byte_vec(input).map(Cow::Owned),
137− }
138−}
139−
140−/// Similar to [`from_byte_slice()`], but takes and produces owned data.
141−pub fn from_byte_vec(input: impl Into<Vec<u8>>) -> Result<PathBuf, Utf8Error> {
142− let input = input.into();
143− #[cfg(unix)]
144− let p = {
145− use std::os::unix::ffi::OsStringExt;
146− std::ffi::OsString::from_vec(input).into()
147− };
148− #[cfg(not(unix))]
149− let p = PathBuf::from(String::from_utf8(input).map_err(|_| Utf8Error)?);
150− Ok(p)
151−}
152−
153−/// Similar to [`from_byte_vec()`], but will panic if there is ill-formed UTF-8 in the `input`.
154−pub fn from_byte_vec_or_panic_on_windows(input: impl Into<Vec<u8>>) -> PathBuf {
155− from_byte_vec(input).expect("well-formed UTF-8 on windows")
156−}
157−
158−/// Similar to [`from_byte_slice()`], but will panic if there is ill-formed UTF-8 in the `input`.
159−pub fn from_byte_slice_or_panic_on_windows(input: &[u8]) -> &Path {
160− from_byte_slice(input).expect("well-formed UTF-8 on windows")
161−}
162−
163−/// Methods to handle paths as bytes and do conversions between them.
164−pub mod convert {
165− use std::borrow::Cow;
166−
167− fn replace<'a>(path: impl Into<Cow<'a, [u8]>>, find: u8, replace: u8) -> Cow<'a, [u8]> {
168− let path = path.into();
169− match path {
170− Cow::Owned(mut path) => {
171− for b in path.iter_mut().filter(|b| **b == find) {
172− *b = replace;
173− }
174− path.into()
175− }
176− Cow::Borrowed(path) => {
177− if !path.contains(&find) {
178− return path.into();
179− }
180− let mut path = path.to_owned();
181− for b in path.iter_mut().filter(|b| **b == find) {
182− *b = replace;
183− }
184− path.into()
185− }
186− }
187− }
188−
189− /// Replaces windows path separators with slashes.
190− pub fn to_native_separators<'a>(path: impl Into<Cow<'a, [u8]>>) -> Cow<'a, [u8]> {
191− #[cfg(not(windows))]
192− let p = to_unix_separators(path);
193− #[cfg(windows)]
194− let p = to_windows_separators(path);
195− p
196− }
197−
198− /// Convert paths with slashes to backslashes on windows and do nothing on unix.
199− pub fn to_windows_separators_on_windows_or_panic(path: &std::path::Path) -> Cow<'_, std::path::Path> {
200− #[cfg(not(windows))]
201− {
202− path.into()
203− }
204− #[cfg(windows)]
205− {
206− crate::path::from_byte_slice_or_panic_on_windows(
207− crate::path::convert::to_windows_separators(crate::path::into_bytes_or_panic_on_windows(path)).as_ref(),
208− )
209− .to_owned()
210− .into()
211− }
212− }
213−
214− /// Replaces windows path separators with slashes.
215− pub fn to_unix_separators<'a>(path: impl Into<Cow<'a, [u8]>>) -> Cow<'a, [u8]> {
216− replace(path, b'\\', b'/')
217− }
218−
219− /// Find backslashes and replace them with slashes, which typically resembles a unix path.
220− ///
221− /// No other transformation is performed, the caller must check other invariants.
222− pub fn to_windows_separators<'a>(path: impl Into<Cow<'a, [u8]>>) -> Cow<'a, [u8]> {
223− replace(path, b'/', b'\\')
224− }
225−
226− /// Obtain a `BStr` compatible `Cow` from one that is bytes.
227− #[cfg(feature = "bstr")]
228− pub fn into_bstr(path: Cow<'_, [u8]>) -> Cow<'_, bstr::BStr> {
229− match path {
230− Cow::Owned(p) => Cow::Owned(p.into()),
231− Cow::Borrowed(p) => Cow::Borrowed(p.into()),
232− }
233− }
234−}
git-features/tests/path.rs+0−30
@@ -1,30 +0,0 @@
1−mod bytes {
2− use bstr::ByteSlice;
3− use git_features::path;
4−
5− #[test]
6− fn assure_unix_separators() {
7− assert_eq!(
8− path::convert::to_unix_separators(b"no-backslash".as_ref()).as_bstr(),
9− "no-backslash"
10− );
11−
12− assert_eq!(
13− path::convert::to_unix_separators(b"\\a\\b\\\\".as_ref()).as_bstr(),
14− "/a/b//"
15− );
16− }
17−
18− #[test]
19− fn assure_windows_separators() {
20− assert_eq!(
21− path::convert::to_windows_separators(b"no-backslash".as_ref()).as_bstr(),
22− "no-backslash"
23− );
24−
25− assert_eq!(
26− path::convert::to_windows_separators(b"/a/b//".as_ref()).as_bstr(),
27− "\\a\\b\\\\"
28− );
29− }
30−}
git-glob/src/lib.rs+6−4
@@ -4,9 +4,9 @@ · 3 unmodified lines
44
55 use bstr::BString;
66
7−/// A glob pattern at a particular base path.
7+/// A glob pattern optimized for matching paths relative to a root directory.
88 ///
9−/// This closely models how patterns appear in a directory hierarchy of include or attribute files.
9+/// For normal globbing, use [`wildmatch()`] instead.
1010 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
1111 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
1212 pub struct Pattern {
@@ -28,6 +28,8 @@ pub use wildmatch::function::wildmatch; · 15 unmodified lines
2828 mod parse;
2929
3030 /// Create a [`Pattern`] by parsing `text` or return `None` if `text` is empty.
31−pub fn parse(text: &[u8]) -> Option<Pattern> {
32− Pattern::from_bytes(text)
31+///
32+/// Note that
33+pub fn parse(text: impl AsRef<[u8]>) -> Option<Pattern> {
34+ Pattern::from_bytes(text.as_ref())
3335 }
git-glob/src/parse.rs+3−3
@@ -26,6 +26,7 @@ pub fn pattern(mut pat: &[u8]) -> Option<(BString, pattern::Mode, Option<usize>) · 25 unmodified lines
2626 }
2727 if pat.first() == Some(&b'/') {
2828 mode |= Mode::ABSOLUTE;
29+ pat = &pat[1..];
2930 }
3031 let mut pat = truncate_non_escaped_trailing_spaces(pat);
3132 if pat.last() == Some(&b'/') {
@@ -33,11 +34,10 @@ pub fn pattern(mut pat: &[u8]) -> Option<(BString, pattern::Mode, Option<usize>) · 1 unmodified line
3334 pat.pop();
3435 }
3536
36− let relative_pattern = mode.contains(Mode::ABSOLUTE).then(|| &pat[1..]).unwrap_or(&pat);
37− if !relative_pattern.contains(&b'/') {
37+ if !pat.contains(&b'/') {
3838 mode |= Mode::NO_SUB_DIR;
3939 }
40− if relative_pattern.first() == Some(&b'*') && first_wildcard_pos(&relative_pattern[1..]).is_none() {
40+ if pat.first() == Some(&b'*') && first_wildcard_pos(&pat[1..]).is_none() {
4141 mode |= Mode::ENDS_WITH;
4242 }
4343
git-glob/src/pattern.rs+37−34
@@ -1,5 +1,6 @@
11 use bitflags::bitflags;
22 use bstr::{BStr, ByteSlice};
3+use std::fmt;
34
45 use crate::{pattern, wildmatch, Pattern};
56
@@ -36,6 +37,12 @@ pub enum Case { · 30 unmodified lines
3637 Fold,
3738 }
3839
40+impl Default for Case {
41+ fn default() -> Self {
42+ Case::Sensitive
43+ }
44+}
45+
3946 impl Pattern {
4047 /// Parse the given `text` as pattern, or return `None` if `text` was empty.
4148 pub fn from_bytes(text: &[u8]) -> Option<Self> {
@@ -58,15 +65,14 @@ impl Pattern { · 16 unmodified lines
5865 /// `basename_start_pos` is the index at which the `path`'s basename starts.
5966 ///
6067 /// Lastly, `case` folding can be configured as well.
61− ///
62− /// Note that this method uses shortcuts to accelerate simple patterns.
6368 pub fn matches_repo_relative_path<'a>(
6469 &self,
6570 path: impl Into<&'a BStr>,
6671 basename_start_pos: Option<usize>,
67− is_dir: bool,
72+ is_dir: Option<bool>,
6873 case: Case,
6974 ) -> bool {
75+ let is_dir = is_dir.unwrap_or(false);
7076 if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {
7177 return false;
7278 }
@@ -84,20 +90,11 @@ impl Pattern { · 11 unmodified lines
8490 );
8591 debug_assert!(!path.starts_with(b"/"), "input path must be relative");
8692
87− let (text, first_wildcard_pos) = self
88− .mode
89− .contains(pattern::Mode::ABSOLUTE)
90− .then(|| (self.text[1..].as_bstr(), self.first_wildcard_pos.map(|p| p - 1)))
91− .unwrap_or((self.text.as_bstr(), self.first_wildcard_pos));
92− if self.mode.contains(pattern::Mode::NO_SUB_DIR) {
93− let basename = if self.mode.contains(pattern::Mode::ABSOLUTE) {
94− path
95− } else {
96− &path[basename_start_pos.unwrap_or_default()..]
97− };
98− self.matches_inner(text, first_wildcard_pos, basename, flags)
93+ if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) {
94+ let basename = &path[basename_start_pos.unwrap_or_default()..];
95+ self.matches(basename, flags)
9996 } else {
100− self.matches_inner(text, first_wildcard_pos, path, flags)
97+ self.matches(path, flags)
10198 }
10299 }
103100
@@ -107,22 +104,12 @@ impl Pattern { · 3 unmodified lines
107104 /// strings with cases ignored as well. Note that the case folding performed here is ASCII only.
108105 ///
109106 /// Note that this method uses some shortcuts to accelerate simple patterns.
110− pub fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool {
111− self.matches_inner(self.text.as_bstr(), self.first_wildcard_pos, value, mode)
112− }
113−
114− fn matches_inner<'a>(
115− &self,
116− text: &BStr,
117− first_wildcard_pos: Option<usize>,
118− value: impl Into<&'a BStr>,
119− mode: wildmatch::Mode,
120− ) -> bool {
107+ fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool {
121108 let value = value.into();
122− match first_wildcard_pos {
109+ match self.first_wildcard_pos {
123110 // "*literal" case, overrides starts-with
124111 Some(pos) if self.mode.contains(pattern::Mode::ENDS_WITH) && !value.contains(&b'/') => {
125− let text = &text[pos + 1..];
112+ let text = &self.text[pos + 1..];
126113 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
127114 value
128115 .len()
@@ -137,22 +124,38 @@ impl Pattern { · 8 unmodified lines
137124 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
138125 if !value
139126 .get(..pos)
140− .map_or(false, |value| value.eq_ignore_ascii_case(&text[..pos]))
127+ .map_or(false, |value| value.eq_ignore_ascii_case(&self.text[..pos]))
141128 {
142129 return false;
143130 }
144− } else if !value.starts_with(&text[..pos]) {
131+ } else if !value.starts_with(&self.text[..pos]) {
145132 return false;
146133 }
147− crate::wildmatch(text.as_bstr(), value, mode)
134+ crate::wildmatch(self.text.as_bstr(), value, mode)
148135 }
149136 None => {
150137 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
151− text.eq_ignore_ascii_case(value)
138+ self.text.eq_ignore_ascii_case(value)
152139 } else {
153− text == value
140+ self.text == value
154141 }
155142 }
156143 }
157144 }
158145 }
146+
147+impl fmt::Display for Pattern {
148+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149+ if self.mode.contains(Mode::NEGATIVE) {
150+ "!".fmt(f)?;
151+ }
152+ if self.mode.contains(Mode::ABSOLUTE) {
153+ "/".fmt(f)?;
154+ }
155+ self.text.fmt(f)?;
156+ if self.mode.contains(Mode::MUST_BE_DIR) {
157+ "/".fmt(f)?;
158+ }
159+ Ok(())
160+ }
161+}
git-glob/tests/fixtures/generated-archives/make_baseline.tar.xz+2−2
@@ -1,3 +1,3 @@
11 version https://git-lfs.github.com/spec/v1
2−oid sha256:cdade0fe7f3df3ac737130a101b077c82f9de1dcb3d59d5964e5ffd920405e8d
3−size 10384
2+oid sha256:6bda59c1591dfd35f09cad5d09e2950b2a6ff885d9f7900535144a5275ab51c3
3+size 10428
git-glob/tests/fixtures/make_baseline.sh+5−2
@@ -10,8 +10,10 @@ while read -r pattern value; do · 9 unmodified lines
1010 echo "$pattern" > .gitignore
1111 echo "$value" | git check-ignore -vn --stdin 2>&1 || :
1212 done <<EOF >git-baseline.nmatch
13−*/\ XXX/\
14−*/\\ XXX/\
13+/*foo bam/barfoo/baz/bam
14+/*foo bar/bam/barfoo/baz/bam
15+foo foobaz
16+*/\' XXX/\'
1517 /*foo bar/foo
1618 /*foo bar/bazfoo
1719 foo*bar foo/baz/bar
@@ -71,6 +73,7 @@ while read -r pattern value; do · 53 unmodified lines
7173 echo "$pattern" > .gitignore
7274 echo "$value" | git check-ignore -vn --stdin 2>&1 || :
7375 done <<EOF >git-baseline.match
76+*/' XXX/'
7477 \a a
7578 \\\[a-z] \a
7679 \\\? \a
git-glob/tests/glob.rs+1−1
@@ -1,3 +1,3 @@
1−mod matching;
21 mod parse;
2+mod pattern;
33 mod wildmatch;
git-glob/tests/matching/mod.rs+0−279
@@ -1,280 +1 @@
1−use std::collections::BTreeSet;
21
3−use bstr::{BStr, ByteSlice};
4−use git_glob::{pattern, pattern::Case};
5−
6−#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)]
7−pub struct GitMatch<'a> {
8− pattern: &'a BStr,
9− value: &'a BStr,
10− /// True if git could match `value` with `pattern`
11− is_match: bool,
12−}
13−
14−pub struct Baseline<'a> {
15− inner: bstr::Lines<'a>,
16−}
17−
18−impl<'a> Iterator for Baseline<'a> {
19− type Item = GitMatch<'a>;
20−
21− fn next(&mut self) -> Option<Self::Item> {
22− let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' ');
23− let pattern = tokens.next().expect("pattern").as_bstr();
24− let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr();
25−
26− let git_match = self.inner.next()?;
27− let is_match = !git_match.starts_with(b"::\t");
28− Some(GitMatch {
29− pattern,
30− value,
31− is_match,
32− })
33− }
34−}
35−
36−impl<'a> Baseline<'a> {
37− fn new(input: &'a [u8]) -> Self {
38− Baseline {
39− inner: input.as_bstr().lines(),
40− }
41− }
42−}
43−
44−#[test]
45−fn compare_baseline_with_ours() {
46− let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap();
47− let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0);
48− let mut mismatches = Vec::new();
49− for (input_file, expected_matches, case) in &[
50− ("git-baseline.match", true, pattern::Case::Sensitive),
51− ("git-baseline.nmatch", false, pattern::Case::Sensitive),
52− ("git-baseline.match-icase", true, pattern::Case::Fold),
53− ] {
54− let input = std::fs::read(dir.join(*input_file)).unwrap();
55− let mut seen = BTreeSet::default();
56−
57− for m @ GitMatch {
58− pattern,
59− value,
60− is_match,
61− } in Baseline::new(&input)
62− {
63− total_matches += 1;
64− assert!(seen.insert(m), "duplicate match entry: {:?}", m);
65− assert_eq!(
66− is_match, *expected_matches,
67− "baseline for matches must be {} - check baseline and git version: {:?}",
68− expected_matches, m
69− );
70− match std::panic::catch_unwind(|| {
71− let pattern = pat(pattern);
72− pattern.matches_repo_relative_path(
73− value,
74− basename_start_pos(value),
75− false, // TODO: does it make sense to pretend it is a dir and see what happens?
76− *case,
77− )
78− }) {
79− Ok(actual_match) => {
80− if actual_match == is_match {
81− total_correct += 1;
82− } else {
83− mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches));
84− }
85− }
86− Err(_) => {
87− panics += 1;
88− continue;
89− }
90− };
91− }
92− }
93−
94− dbg!(mismatches);
95− assert_eq!(
96− total_correct,
97− total_matches - panics,
98− "We perfectly agree with git here"
99− );
100− assert_eq!(panics, 0);
101−}
102−
103−#[test]
104−fn non_dirs_for_must_be_dir_patterns_are_ignored() {
105− let pattern = pat("hello/");
106−
107− assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR));
108− assert_eq!(
109− pattern.text, "hello",
110− "a dir pattern doesn't actually end with the trailing slash"
111− );
112− let path = "hello";
113− assert!(
114− !pattern.matches_repo_relative_path(path, None, false /* is-dir */, Case::Sensitive),
115− "non-dirs never match a dir pattern"
116− );
117− assert!(
118− pattern.matches_repo_relative_path(path, None, true /* is-dir */, Case::Sensitive),
119− "dirs can match a dir pattern with the normal rules"
120− );
121−}
122−
123−#[test]
124−fn matches_of_absolute_paths_work() {
125− let input = "/hello/git";
126− let pat = pat(input);
127− assert!(
128− pat.matches(input, git_glob::wildmatch::Mode::empty()),
129− "patterns always match themselves"
130− );
131− assert!(
132− pat.matches(input, git_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL),
133− "patterns always match themselves, path mode doesn't change that"
134− );
135−}
136−
137−#[test]
138−fn basename_matches_from_end() {
139− let pat = &pat("foo");
140− assert!(match_file(pat, "FoO", Case::Fold));
141− assert!(!match_file(pat, "FoOo", Case::Fold));
142− assert!(!match_file(pat, "Foo", Case::Sensitive));
143− assert!(match_file(pat, "foo", Case::Sensitive));
144− assert!(!match_file(pat, "Foo", Case::Sensitive));
145− assert!(!match_file(pat, "barfoo", Case::Sensitive));
146−}
147−
148−#[test]
149−fn absolute_basename_matches_only_from_beginning() {
150− let pat = &pat("/foo");
151− assert!(match_file(pat, "FoO", Case::Fold));
152− assert!(!match_file(pat, "bar/Foo", Case::Fold));
153− assert!(match_file(pat, "foo", Case::Sensitive));
154− assert!(!match_file(pat, "Foo", Case::Sensitive));
155− assert!(!match_file(pat, "bar/foo", Case::Sensitive));
156−}
157−
158−#[test]
159−fn absolute_path_matches_only_from_beginning() {
160− let pat = &pat("/bar/foo");
161− assert!(!match_file(pat, "FoO", Case::Fold));
162− assert!(match_file(pat, "bar/Foo", Case::Fold));
163− assert!(!match_file(pat, "foo", Case::Sensitive));
164− assert!(match_file(pat, "bar/foo", Case::Sensitive));
165− assert!(!match_file(pat, "bar/Foo", Case::Sensitive));
166−}
167−
168−#[test]
169−fn absolute_path_with_recursive_glob_detects_mismatches_quickly() {
170− let pat = &pat("/bar/foo/**");
171− assert!(!match_file(pat, "FoO", Case::Fold));
172− assert!(!match_file(pat, "bar/Fooo", Case::Fold));
173− assert!(!match_file(pat, "baz/bar/Foo", Case::Fold));
174−}
175−
176−#[test]
177−fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() {
178− let pat = &pat("/bar/foo/**");
179− assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive));
180− assert!(match_file(pat, "bar/Foo/match", Case::Fold));
181−}
182−
183−#[test]
184−fn relative_path_does_not_match_from_end() {
185− let pattern = &pat("bar/foo");
186− assert!(!match_file(pattern, "FoO", Case::Fold));
187− assert!(match_file(pattern, "bar/Foo", Case::Fold));
188− assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold));
189− assert!(!match_file(pattern, "foo", Case::Sensitive));
190− assert!(match_file(pattern, "bar/foo", Case::Sensitive));
191− assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive));
192− assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive));
193−}
194−
195−#[test]
196−fn basename_glob_and_literal_is_ends_with() {
197− let pattern = &pat("*foo");
198− assert!(match_file(pattern, "FoO", Case::Fold));
199− assert!(match_file(pattern, "BarFoO", Case::Fold));
200− assert!(!match_file(pattern, "BarFoOo", Case::Fold));
201− assert!(!match_file(pattern, "Foo", Case::Sensitive));
202− assert!(!match_file(pattern, "BarFoo", Case::Sensitive));
203− assert!(match_file(pattern, "barfoo", Case::Sensitive));
204− assert!(!match_file(pattern, "barfooo", Case::Sensitive));
205−
206− assert!(match_file(pattern, "bar/foo", Case::Sensitive));
207− assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive));
208−}
209−
210−#[test]
211−fn special_cases_from_corpus() {
212− let pattern = &pat("foo*bar");
213− assert!(
214− !match_file(pattern, "foo/baz/bar", Case::Sensitive),
215− "asterisk does not match path separators"
216− );
217− let pattern = &pat("*some/path/to/hello.txt");
218− assert!(
219− !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive),
220− "asterisk doesn't match path separators"
221− );
222−
223− let pattern = &pat("/*foo.txt");
224− assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive));
225− assert!(
226− !match_file(pattern, "hello/foo.txt", Case::Sensitive),
227− "absolute single asterisk doesn't match paths"
228− );
229−}
230−
231−#[test]
232−fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() {
233− let pattern = &pat("/*foo");
234−
235− assert!(match_file(pattern, "FoO", Case::Fold));
236− assert!(match_file(pattern, "BarFoO", Case::Fold));
237− assert!(!match_file(pattern, "BarFoOo", Case::Fold));
238− assert!(!match_file(pattern, "Foo", Case::Sensitive));
239− assert!(!match_file(pattern, "BarFoo", Case::Sensitive));
240− assert!(match_file(pattern, "barfoo", Case::Sensitive));
241− assert!(!match_file(pattern, "barfooo", Case::Sensitive));
242−}
243−
244−#[test]
245−fn absolute_basename_glob_and_literal_is_glob_in_paths() {
246− let pattern = &pat("/*foo");
247−
248− assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /");
249− assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive));
250−}
251−
252−#[test]
253−fn negated_patterns_are_handled_by_caller() {
254− let pattern = &pat("!foo");
255− assert!(
256− match_file(pattern, "foo", Case::Sensitive),
257− "negative patterns match like any other"
258− );
259− assert!(
260− pattern.is_negative(),
261− "the caller checks for the negative flag and acts accordingly"
262− );
263−}
264−
265−fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern {
266− git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works")
267−}
268−
269−fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool {
270− match_path(pattern, path, false, case)
271−}
272−
273−fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: bool, case: Case) -> bool {
274− let path = path.into();
275− pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case)
276−}
277−
278−fn basename_start_pos(value: &BStr) -> Option<usize> {
279− value.rfind_byte(b'/').map(|pos| pos + 1)
280−}
git-glob/tests/parse/mod.rs+2−2
@@ -77,12 +77,12 @@ fn leading_exclamation_marks_can_be_escaped_with_backslash() { · 76 unmodified lines
7777 fn leading_slashes_mark_patterns_as_absolute() {
7878 assert_eq!(
7979 git_glob::parse(br"/absolute"),
80− pat("/absolute", Mode::NO_SUB_DIR | Mode::ABSOLUTE, None)
80+ pat("absolute", Mode::NO_SUB_DIR | Mode::ABSOLUTE, None)
8181 );
8282
8383 assert_eq!(
8484 git_glob::parse(br"/absolute/path"),
85− pat("/absolute/path", Mode::ABSOLUTE, None)
85+ pat("absolute/path", Mode::ABSOLUTE, None)
8686 );
8787 }
8888
git-glob/tests/pattern/matching.rs+326−0
@@ -0,0 +1,326 @@
1+use std::collections::BTreeSet;
2+
3+use bstr::{BStr, ByteSlice};
4+use git_glob::{pattern, pattern::Case};
5+
6+#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)]
7+pub struct GitMatch<'a> {
8+ pattern: &'a BStr,
9+ value: &'a BStr,
10+ /// True if git could match `value` with `pattern`
11+ is_match: bool,
12+}
13+
14+pub struct Baseline<'a> {
15+ inner: bstr::Lines<'a>,
16+}
17+
18+impl<'a> Iterator for Baseline<'a> {
19+ type Item = GitMatch<'a>;
20+
21+ fn next(&mut self) -> Option<Self::Item> {
22+ let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' ');
23+ let pattern = tokens.next().expect("pattern").as_bstr();
24+ let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr();
25+
26+ let git_match = self.inner.next()?;
27+ let is_match = !git_match.starts_with(b"::\t");
28+ Some(GitMatch {
29+ pattern,
30+ value,
31+ is_match,
32+ })
33+ }
34+}
35+
36+impl<'a> Baseline<'a> {
37+ fn new(input: &'a [u8]) -> Self {
38+ Baseline {
39+ inner: input.as_bstr().lines(),
40+ }
41+ }
42+}
43+
44+#[test]
45+fn compare_baseline_with_ours() {
46+ let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap();
47+ let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0);
48+ let mut mismatches = Vec::new();
49+ for (input_file, expected_matches, case) in &[
50+ ("git-baseline.match", true, pattern::Case::Sensitive),
51+ ("git-baseline.nmatch", false, pattern::Case::Sensitive),
52+ ("git-baseline.match-icase", true, pattern::Case::Fold),
53+ ] {
54+ let input = std::fs::read(dir.join(*input_file)).unwrap();
55+ let mut seen = BTreeSet::default();
56+
57+ for m @ GitMatch {
58+ pattern,
59+ value,
60+ is_match,
61+ } in Baseline::new(&input)
62+ {
63+ total_matches += 1;
64+ assert!(seen.insert(m), "duplicate match entry: {:?}", m);
65+ assert_eq!(
66+ is_match, *expected_matches,
67+ "baseline for matches must be {} - check baseline and git version: {:?}",
68+ expected_matches, m
69+ );
70+ match std::panic::catch_unwind(|| {
71+ let pattern = pat(pattern);
72+ pattern.matches_repo_relative_path(value, basename_start_pos(value), None, *case)
73+ }) {
74+ Ok(actual_match) => {
75+ if actual_match == is_match {
76+ total_correct += 1;
77+ } else {
78+ mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches));
79+ }
80+ }
81+ Err(_) => {
82+ panics += 1;
83+ continue;
84+ }
85+ };
86+ }
87+ }
88+
89+ dbg!(mismatches);
90+ assert_eq!(
91+ total_correct,
92+ total_matches - panics,
93+ "We perfectly agree with git here"
94+ );
95+ assert_eq!(panics, 0);
96+}
97+
98+#[test]
99+fn non_dirs_for_must_be_dir_patterns_are_ignored() {
100+ let pattern = pat("hello/");
101+
102+ assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR));
103+ assert_eq!(
104+ pattern.text, "hello",
105+ "a dir pattern doesn't actually end with the trailing slash"
106+ );
107+ let path = "hello";
108+ assert!(
109+ !pattern.matches_repo_relative_path(path, None, false.into() /* is-dir */, Case::Sensitive),
110+ "non-dirs never match a dir pattern"
111+ );
112+ assert!(
113+ pattern.matches_repo_relative_path(path, None, true.into() /* is-dir */, Case::Sensitive),
114+ "dirs can match a dir pattern with the normal rules"
115+ );
116+}
117+
118+#[test]
119+fn matches_of_absolute_paths_work() {
120+ let pattern = "/hello/git";
121+ assert!(
122+ git_glob::wildmatch(pattern.into(), pattern.into(), git_glob::wildmatch::Mode::empty()),
123+ "patterns always match themselves"
124+ );
125+ assert!(
126+ git_glob::wildmatch(
127+ pattern.into(),
128+ pattern.into(),
129+ git_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL
130+ ),
131+ "patterns always match themselves, path mode doesn't change that"
132+ );
133+}
134+
135+#[test]
136+fn basename_matches_from_end() {
137+ let pat = &pat("foo");
138+ assert!(match_file(pat, "FoO", Case::Fold));
139+ assert!(!match_file(pat, "FoOo", Case::Fold));
140+ assert!(!match_file(pat, "Foo", Case::Sensitive));
141+ assert!(match_file(pat, "foo", Case::Sensitive));
142+ assert!(!match_file(pat, "Foo", Case::Sensitive));
143+ assert!(!match_file(pat, "barfoo", Case::Sensitive));
144+}
145+
146+#[test]
147+fn absolute_basename_matches_only_from_beginning() {
148+ let pat = &pat("/foo");
149+ assert!(match_file(pat, "FoO", Case::Fold));
150+ assert!(!match_file(pat, "bar/Foo", Case::Fold));
151+ assert!(match_file(pat, "foo", Case::Sensitive));
152+ assert!(!match_file(pat, "Foo", Case::Sensitive));
153+ assert!(!match_file(pat, "bar/foo", Case::Sensitive));
154+}
155+
156+#[test]
157+fn absolute_path_matches_only_from_beginning() {
158+ let pat = &pat("/bar/foo");
159+ assert!(!match_file(pat, "FoO", Case::Fold));
160+ assert!(match_file(pat, "bar/Foo", Case::Fold));
161+ assert!(!match_file(pat, "foo", Case::Sensitive));
162+ assert!(match_file(pat, "bar/foo", Case::Sensitive));
163+ assert!(!match_file(pat, "bar/Foo", Case::Sensitive));
164+}
165+
166+#[test]
167+fn absolute_path_with_recursive_glob_detects_mismatches_quickly() {
168+ let pat = &pat("/bar/foo/**");
169+ assert!(!match_file(pat, "FoO", Case::Fold));
170+ assert!(!match_file(pat, "bar/Fooo", Case::Fold));
171+ assert!(!match_file(pat, "baz/bar/Foo", Case::Fold));
172+}
173+
174+#[test]
175+fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() {
176+ let pat = &pat("/bar/foo/**");
177+ assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive));
178+ assert!(match_file(pat, "bar/Foo/match", Case::Fold));
179+}
180+
181+#[test]
182+fn relative_path_does_not_match_from_end() {
183+ for pattern in &["bar/foo", "/bar/foo"] {
184+ let pattern = &pat(*pattern);
185+ assert!(!match_file(pattern, "FoO", Case::Fold));
186+ assert!(match_file(pattern, "bar/Foo", Case::Fold));
187+ assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold));
188+ assert!(!match_file(pattern, "foo", Case::Sensitive));
189+ assert!(match_file(pattern, "bar/foo", Case::Sensitive));
190+ assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive));
191+ assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive));
192+ }
193+}
194+
195+#[test]
196+fn basename_glob_and_literal_is_ends_with() {
197+ let pattern = &pat("*foo");
198+ assert!(match_file(pattern, "FoO", Case::Fold));
199+ assert!(match_file(pattern, "BarFoO", Case::Fold));
200+ assert!(!match_file(pattern, "BarFoOo", Case::Fold));
201+ assert!(!match_file(pattern, "Foo", Case::Sensitive));
202+ assert!(!match_file(pattern, "BarFoo", Case::Sensitive));
203+ assert!(match_file(pattern, "barfoo", Case::Sensitive));
204+ assert!(!match_file(pattern, "barfooo", Case::Sensitive));
205+
206+ assert!(match_file(pattern, "bar/foo", Case::Sensitive));
207+ assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive));
208+}
209+
210+#[test]
211+fn special_cases_from_corpus() {
212+ let pattern = &pat("foo*bar");
213+ assert!(
214+ !match_file(pattern, "foo/baz/bar", Case::Sensitive),
215+ "asterisk does not match path separators"
216+ );
217+ let pattern = &pat("*some/path/to/hello.txt");
218+ assert!(
219+ !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive),
220+ "asterisk doesn't match path separators"
221+ );
222+
223+ let pattern = &pat("/*foo.txt");
224+ assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive));
225+ assert!(
226+ !match_file(pattern, "hello/foo.txt", Case::Sensitive),
227+ "absolute single asterisk doesn't match paths"
228+ );
229+}
230+
231+#[test]
232+fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() {
233+ let pattern = &pat("/*foo");
234+
235+ assert!(match_file(pattern, "FoO", Case::Fold));
236+ assert!(match_file(pattern, "BarFoO", Case::Fold));
237+ assert!(!match_file(pattern, "BarFoOo", Case::Fold));
238+ assert!(!match_file(pattern, "Foo", Case::Sensitive));
239+ assert!(!match_file(pattern, "BarFoo", Case::Sensitive));
240+ assert!(match_file(pattern, "barfoo", Case::Sensitive));
241+ assert!(!match_file(pattern, "barfooo", Case::Sensitive));
242+}
243+
244+#[test]
245+fn absolute_basename_glob_and_literal_is_glob_in_paths() {
246+ let pattern = &pat("/*foo");
247+
248+ assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /");
249+ assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive));
250+}
251+
252+#[test]
253+fn negated_patterns_are_handled_by_caller() {
254+ let pattern = &pat("!foo");
255+ assert!(
256+ match_file(pattern, "foo", Case::Sensitive),
257+ "negative patterns match like any other"
258+ );
259+ assert!(
260+ pattern.is_negative(),
261+ "the caller checks for the negative flag and acts accordingly"
262+ );
263+}
264+#[test]
265+fn names_do_not_automatically_match_entire_directories() {
266+ // this feature is implemented with the directory stack.
267+ let pattern = &pat("foo");
268+ assert!(!match_file(pattern, "foobar", Case::Sensitive));
269+ assert!(!match_file(pattern, "foo/bar", Case::Sensitive));
270+ assert!(!match_file(pattern, "foo/bar/baz", Case::Sensitive));
271+}
272+
273+#[test]
274+fn directory_patterns_do_not_match_files_within_a_directory_as_well_like_slash_star_star() {
275+ // this feature is implemented with the directory stack, which excludes entire directories
276+ let pattern = &pat("dir/");
277+ assert!(!match_path(pattern, "dir/file", None, Case::Sensitive));
278+ assert!(!match_path(pattern, "base/dir/file", None, Case::Sensitive));
279+ assert!(!match_path(pattern, "base/ndir/file", None, Case::Sensitive));
280+ assert!(!match_path(pattern, "Dir/File", None, Case::Fold));
281+ assert!(!match_path(pattern, "Base/Dir/File", None, Case::Fold));
282+ assert!(!match_path(pattern, "dir2/file", None, Case::Sensitive));
283+
284+ let pattern = &pat("dir/sub-dir/");
285+ assert!(!match_path(pattern, "dir/sub-dir/file", None, Case::Sensitive));
286+ assert!(!match_path(pattern, "dir/Sub-dir/File", None, Case::Fold));
287+ assert!(!match_path(pattern, "dir/Sub-dir2/File", None, Case::Fold));
288+}
289+
290+#[test]
291+fn single_paths_match_anywhere() {
292+ let pattern = &pat("target");
293+ assert!(match_file(pattern, "dir/target", Case::Sensitive));
294+ assert!(!match_file(pattern, "dir/atarget", Case::Sensitive));
295+ assert!(!match_file(pattern, "dir/targeta", Case::Sensitive));
296+ assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive));
297+
298+ let pattern = &pat("target/");
299+ assert!(!match_file(pattern, "dir/target", Case::Sensitive));
300+ assert!(
301+ !match_path(pattern, "dir/target", None, Case::Sensitive),
302+ "it assumes unknown to not be a directory"
303+ );
304+ assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive));
305+ assert!(
306+ !match_path(pattern, "dir/target/", Some(true), Case::Sensitive),
307+ "we need sanitized paths that don't have trailing slashes"
308+ );
309+}
310+
311+fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern {
312+ git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works")
313+}
314+
315+fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool {
316+ match_path(pattern, path, false.into(), case)
317+}
318+
319+fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: Option<bool>, case: Case) -> bool {
320+ let path = path.into();
321+ pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case)
322+}
323+
324+fn basename_start_pos(value: &BStr) -> Option<usize> {
325+ value.rfind_byte(b'/').map(|pos| pos + 1)
326+}
git-glob/tests/pattern/mod.rs+19−0
@@ -0,0 +1,19 @@
1+use git_glob::pattern::Mode;
2+use git_glob::Pattern;
3+
4+#[test]
5+fn display() {
6+ fn pat(text: &str, mode: Mode) -> String {
7+ Pattern {
8+ text: text.into(),
9+ mode,
10+ first_wildcard_pos: None,
11+ }
12+ .to_string()
13+ }
14+ assert_eq!(pat("a", Mode::ABSOLUTE), "/a");
15+ assert_eq!(pat("a", Mode::MUST_BE_DIR), "a/");
16+ assert_eq!(pat("a", Mode::NEGATIVE), "!a");
17+ assert_eq!(pat("a", Mode::ABSOLUTE | Mode::NEGATIVE | Mode::MUST_BE_DIR), "!/a/");
18+}
19+mod matching;
git-glob/tests/wildmatch/mod.rs+7−3
@@ -1,3 +1,4 @@
1+use bstr::ByteSlice;
12 use std::{
23 fmt::{Debug, Display, Formatter},
34 panic::catch_unwind,
@@ -222,6 +223,7 @@ fn corpus() { · 218 unmodified lines
222223 }
223224 }
224225
226+ dbg!(&failures);
225227 assert_eq!(failures.len(), 0);
226228 assert_eq!(at_least_one_panic, 0, "not a single panic in any invocation");
227229
@@ -249,8 +251,10 @@ fn multi_match(pattern_text: &str, text: &str) -> (Pattern, MultiMatch) { · 21 unmodified lines
249251 let pattern = git_glob::Pattern::from_bytes(pattern_text.as_bytes()).expect("valid (enough) pattern");
250252 let actual_path_match: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Sensitive)).into();
251253 let actual_path_imatch: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Fold)).into();
252− let actual_glob_match: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::empty())).into();
253− let actual_glob_imatch: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::IGNORE_CASE)).into();
254+ let actual_glob_match: MatchResult =
255+ catch_unwind(|| git_glob::wildmatch(pattern.text.as_bstr(), text.into(), wildmatch::Mode::empty())).into();
256+ let actual_glob_imatch: MatchResult =
257+ catch_unwind(|| git_glob::wildmatch(pattern.text.as_bstr(), text.into(), wildmatch::Mode::IGNORE_CASE)).into();
254258 let actual = MultiMatch {
255259 path_match: actual_path_match,
256260 path_imatch: actual_path_imatch,
@@ -363,7 +367,7 @@ impl Display for MatchResult { · 106 unmodified lines
363367 }
364368
365369 fn match_file_path(pattern: &git_glob::Pattern, path: &str, case: Case) -> bool {
366− pattern.matches_repo_relative_path(path, basename_of(path), false /* is_dir */, case)
370+ pattern.matches_repo_relative_path(path, basename_of(path), false.into() /* is_dir */, case)
367371 }
368372 fn basename_of(path: &str) -> Option<usize> {
369373 path.rfind('/').map(|pos| pos + 1)
git-index/src/access.rs+39−1
@@ -1,6 +1,6 @@
11 use bstr::{BStr, ByteSlice};
22
3−use crate::{extension, Entry, State, Version};
3+use crate::{extension, Entry, PathStorage, State, Version};
44
55 impl State {
66 pub fn version(&self) -> Version {
@@ -10,6 +10,35 @@ impl State { · 3 unmodified lines
1010 pub fn entries(&self) -> &[Entry] {
1111 &self.entries
1212 }
13+ pub fn path_backing(&self) -> &PathStorage {
14+ &self.path_backing
15+ }
16+ pub fn take_path_backing(&mut self) -> PathStorage {
17+ assert_eq!(
18+ self.entries.is_empty(),
19+ self.path_backing.is_empty(),
20+ "BUG: cannot take out backing multiple times"
21+ );
22+ std::mem::take(&mut self.path_backing)
23+ }
24+
25+ pub fn return_path_backing(&mut self, backing: PathStorage) {
26+ assert!(
27+ self.path_backing.is_empty(),
28+ "BUG: return path backing only after taking it, once"
29+ );
30+ self.path_backing = backing;
31+ }
32+
33+ pub fn entries_with_paths_by_filter_map<'a, T>(
34+ &'a self,
35+ mut filter_map: impl FnMut(&'a BStr, &Entry) -> Option<T> + 'a,
36+ ) -> impl Iterator<Item = (&'a BStr, T)> + 'a {
37+ self.entries.iter().filter_map(move |e| {
38+ let p = e.path(self);
39+ filter_map(p, e).map(|t| (p, t))
40+ })
41+ }
1342 pub fn entries_mut(&mut self) -> &mut [Entry] {
1443 &mut self.entries
1544 }
@@ -20,6 +49,15 @@ impl State { · 4 unmodified lines
2049 (e, path)
2150 })
2251 }
52+ pub fn entries_mut_with_paths_in<'state, 'backing>(
53+ &'state mut self,
54+ backing: &'backing PathStorage,
55+ ) -> impl Iterator<Item = (&'state mut Entry, &'backing BStr)> {
56+ self.entries.iter_mut().map(move |e| {
57+ let path = (&backing[e.path.clone()]).as_bstr();
58+ (e, path)
59+ })
60+ }
2361 pub fn tree(&self) -> Option<&extension::Tree> {
2462 self.tree.as_ref()
2563 }
git-index/src/decode/mod.rs+2−1
@@ -41,12 +41,13 @@ use crate::util::read_u32; · 40 unmodified lines
4141 pub struct Options {
4242 pub object_hash: git_hash::Kind,
4343 /// If Some(_), we are allowed to use more than one thread. If Some(N), use no more than N threads. If Some(0)|None, use as many threads
44− /// as there are physical cores.
44+ /// as there are logical cores.
4545 ///
4646 /// This applies to loading extensions in parallel to entries if the common EOIE extension is available.
4747 /// It also allows to use multiple threads for loading entries if the IEOT extension is present.
4848 pub thread_limit: Option<usize>,
4949 /// The minimum size in bytes to load extensions in their own thread, assuming there is enough `num_threads` available.
50+ /// If set to 0, for example, extensions will always be read in their own thread if enough threads are available.
5051 pub min_extension_block_in_bytes_for_threading: usize,
5152 }
5253
git-index/src/entry.rs+4−0
@@ -143,6 +143,10 @@ mod access { · 142 unmodified lines
143143 (&state.path_backing[self.path.clone()]).as_bstr()
144144 }
145145
146+ pub fn path_in<'backing>(&self, backing: &'backing crate::PathStorage) -> &'backing BStr {
147+ (backing[self.path.clone()]).as_bstr()
148+ }
149+
146150 pub fn stage(&self) -> u32 {
147151 self.flags.stage()
148152 }
git-index/src/lib.rs+4−1
@@ -51,6 +51,9 @@ pub struct File { · 50 unmodified lines
5151 pub checksum: git_hash::ObjectId,
5252 }
5353
54+/// The type to use and store paths to all entries.
55+pub type PathStorage = Vec<u8>;
56+
5457 /// An in-memory cache of a fully parsed git index file.
5558 ///
5659 /// As opposed to a snapshot, it's meant to be altered and eventually be written back to disk or converted into a tree.
@@ -65,7 +68,7 @@ pub struct State { · 8 unmodified lines
6568 version: Version,
6669 entries: Vec<Entry>,
6770 /// A memory area keeping all index paths, in full length, independently of the index version.
68− path_backing: Vec<u8>,
71+ path_backing: PathStorage,
6972 /// True if one entry in the index has a special marker mode
7073 #[allow(dead_code)]
7174 is_sparse: bool,
git-odb/Cargo.toml+2−1
@@ -30,7 +30,8 @@ required-features = [] · 29 unmodified lines
3030 all-features = true
3131
3232 [dependencies]
33−git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32", "bstr"] }
33+git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32" ] }
34+git-path = { version = "^0.1.0", path = "../git-path" }
3435 git-hash = { version = "^0.9.3", path = "../git-hash" }
3536 git-quote = { version = "^0.2.0", path = "../git-quote" }
3637 git-object = { version = "^0.18.0", path = "../git-object" }
git-odb/src/alternate/parse.rs+1−1
@@ -20,7 +20,7 @@ pub(crate) fn content(input: &[u8]) -> Result<Vec<PathBuf>, Error> { · 19 unmodified lines
2020 continue;
2121 }
2222 out.push(
23− git_features::path::from_bstr(if line.starts_with(b"\"") {
23+ git_path::try_from_bstr(if line.starts_with(b"\"") {
2424 git_quote::ansi_c::undo(line)?.0
2525 } else {
2626 Cow::Borrowed(line)
git-pack/Cargo.toml+1−0
@@ -39,6 +39,7 @@ required-features = ["internal-testing-to-avoid-being-run-by-cargo-test-all"] · 38 unmodified lines
3939 [dependencies]
4040 git-features = { version = "^0.20.0", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] }
4141 git-hash = { version = "^0.9.3", path = "../git-hash" }
42+git-path = { version = "^0.1.0", path = "../git-path" }
4243 git-chunk = { version = "^0.3.0", path = "../git-chunk" }
4344 git-object = { version = "^0.18.0", path = "../git-object" }
4445 git-traverse = { version = "^0.14.0", path = "../git-traverse" }
git-pack/src/multi_index/chunk.rs+1−1
@@ -34,7 +34,7 @@ pub mod index_names { · 33 unmodified lines
3434 let null_byte_pos = chunk.find_byte(b'\0').ok_or(decode::Error::MissingNullByte)?;
3535
3636 let path = &chunk[..null_byte_pos];
37− let path = git_features::path::from_byte_slice(path)
37+ let path = git_path::try_from_byte_slice(path)
3838 .map_err(|_| decode::Error::PathEncoding {
3939 path: BString::from(path),
4040 })?
git-path/CHANGELOG.md+42−0
@@ -0,0 +1,42 @@
1+# Changelog
2+
3+All notable changes to this project will be documented in this file.
4+
5+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+## 0.1.0 (2022-04-28)
9+
10+### Refactor (BREAKING)
11+
12+ - <csr-id-54801592488416ef2bb0f34c5061b62189c35c5e/> various name changes for more convenient API
13+
14+### Commit Statistics
15+
16+<csr-read-only-do-not-edit/>
17+
18+ - 8 commits contributed to the release over the course of 1 calendar day.
19+ - 1 commit where understood as [conventional](https://www.conventionalcommits.org).
20+ - 1 unique issue was worked on: [#301](https://github.com/Byron/gitoxide/issues/301)
21+
22+### Commit Details
23+
24+<csr-read-only-do-not-edit/>
25+
26+<details><summary>view details</summary>
27+
28+ * **[#301](https://github.com/Byron/gitoxide/issues/301)**
29+ - frame for `gix repo exclude query` ([`a331314`](https://github.com/Byron/gitoxide/commit/a331314758629a93ba036245a5dd03cf4109dc52))
30+ - refactor ([`21d4076`](https://github.com/Byron/gitoxide/commit/21d407638285b728d0c64fabf2abe0e1948e9bec))
31+ - The first indication that directory-based excludes work ([`e868acc`](https://github.com/Byron/gitoxide/commit/e868acce2e7c3e2501497bf630e3a54f349ad38e))
32+ - various name changes for more convenient API ([`5480159`](https://github.com/Byron/gitoxide/commit/54801592488416ef2bb0f34c5061b62189c35c5e))
33+ - Use bstr intead of [u8] ([`9380e99`](https://github.com/Byron/gitoxide/commit/9380e9990065897e318b040f49b3c9a6de8bebb1))
34+ - Use `git-path` crate instead of `git_features::path` ([`47e607d`](https://github.com/Byron/gitoxide/commit/47e607dc256a43a3411406c645eb7ff04239dd3a))
35+ - Copy all existing functions from git-features::path to git-path:: ([`725e198`](https://github.com/Byron/gitoxide/commit/725e1985dc521d01ff9e1e89b6468ef62fc09656))
36+ - add empty git-path crate ([`8d13f81`](https://github.com/Byron/gitoxide/commit/8d13f81068b4663d322002a9617d39b307b63469))
37+</details>
38+
39+## 0.0.0 (2022-03-31)
40+
41+An empty crate without any content to reserve the name for the gitoxide project.
42+
git-path/Cargo.toml+16−0
@@ -0,0 +1,16 @@
1+[package]
2+name = "git-path"
3+version = "0.1.0"
4+repository = "https://github.com/Byron/gitoxide"
5+license = "MIT/Apache-2.0"
6+description = "A WIP crate of the gitoxide project dealing paths and their conversions"
7+authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
8+edition = "2018"
9+
10+[lib]
11+doctest = false
12+
13+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14+
15+[dependencies]
16+bstr = { version = "0.2.17", default-features = false, features = ["std"] }
git-path/src/convert.rs+197−0
@@ -0,0 +1,197 @@
1+use bstr::{BStr, BString};
2+use std::{
3+ borrow::Cow,
4+ ffi::OsStr,
5+ path::{Path, PathBuf},
6+};
7+
8+#[derive(Debug)]
9+/// The error type returned by [`into_bstr()`] and others may suffer from failed conversions from or to bytes.
10+pub struct Utf8Error;
11+
12+impl std::fmt::Display for Utf8Error {
13+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14+ f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input")
15+ }
16+}
17+
18+impl std::error::Error for Utf8Error {}
19+
20+/// Like [`into_bstr()`], but takes `OsStr` as input for a lossless, but fallible, conversion.
21+pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, Utf8Error> {
22+ let path = try_into_bstr(Cow::Borrowed(path.as_ref()))?;
23+ match path {
24+ Cow::Borrowed(path) => Ok(path),
25+ Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"),
26+ }
27+}
28+
29+/// Convert the given path either into its raw bytes on unix or its UTF8 encoded counterpart on windows.
30+///
31+/// On windows, if the source Path contains ill-formed, lone surrogates, the UTF-8 conversion will fail
32+/// causing `Utf8Error` to be returned.
33+pub fn try_into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, BStr>, Utf8Error> {
34+ let path = path.into();
35+ let path_str = match path {
36+ Cow::Owned(path) => Cow::Owned({
37+ #[cfg(unix)]
38+ let p: BString = {
39+ use std::os::unix::ffi::OsStringExt;
40+ path.into_os_string().into_vec().into()
41+ };
42+ #[cfg(not(unix))]
43+ let p: BString = path.into_os_string().into_string().map_err(|_| Utf8Error)?.into();
44+ p
45+ }),
46+ Cow::Borrowed(path) => Cow::Borrowed({
47+ #[cfg(unix)]
48+ let p: &BStr = {
49+ use std::os::unix::ffi::OsStrExt;
50+ path.as_os_str().as_bytes().into()
51+ };
52+ #[cfg(not(unix))]
53+ let p: &BStr = path.to_str().ok_or(Utf8Error)?.as_bytes().into();
54+ p
55+ }),
56+ };
57+ Ok(path_str)
58+}
59+
60+/// Similar to [`try_into_bstr()`] but **panics** if malformed surrogates are encountered on windows.
61+pub fn into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> {
62+ try_into_bstr(path).expect("prefix path doesn't contain ill-formed UTF-8")
63+}
64+
65+/// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on unix.
66+///
67+/// On windows, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. There are some potential
68+/// git versions and windows installation which produce mal-formed UTF-16 if certain emojies are in the path. It's as rare as
69+/// it sounds, but possible.
70+pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> {
71+ #[cfg(unix)]
72+ let p = {
73+ use std::os::unix::ffi::OsStrExt;
74+ OsStr::from_bytes(input).as_ref()
75+ };
76+ #[cfg(not(unix))]
77+ let p = Path::new(std::str::from_utf8(input).map_err(|_| Utf8Error)?);
78+ Ok(p)
79+}
80+
81+/// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`.
82+pub fn try_from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Result<Cow<'a, Path>, Utf8Error> {
83+ let input = input.into();
84+ match input {
85+ Cow::Borrowed(input) => try_from_byte_slice(input).map(Cow::Borrowed),
86+ Cow::Owned(input) => try_from_bstring(input).map(Cow::Owned),
87+ }
88+}
89+
90+/// Similar to [`try_from_bstr()`], but **panics** if malformed surrogates are encountered on windows.
91+pub fn from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Cow<'a, Path> {
92+ try_from_bstr(input).expect("prefix path doesn't contain ill-formed UTF-8")
93+}
94+
95+/// Similar to [`try_from_bstr()`], but takes and produces owned data.
96+pub fn try_from_bstring(input: impl Into<BString>) -> Result<PathBuf, Utf8Error> {
97+ let input = input.into();
98+ #[cfg(unix)]
99+ let p = {
100+ use std::os::unix::ffi::OsStringExt;
101+ std::ffi::OsString::from_vec(input.into()).into()
102+ };
103+ #[cfg(not(unix))]
104+ let p = {
105+ use bstr::ByteVec;
106+ PathBuf::from(
107+ {
108+ let v: Vec<_> = input.into();
109+ v
110+ }
111+ .into_string()
112+ .map_err(|_| Utf8Error)?,
113+ )
114+ };
115+ Ok(p)
116+}
117+
118+/// Similar to [`try_from_bstring()`], but will panic if there is ill-formed UTF-8 in the `input`.
119+pub fn from_bstring(input: impl Into<BString>) -> PathBuf {
120+ try_from_bstring(input).expect("well-formed UTF-8 on windows")
121+}
122+
123+/// Similar to [`try_from_byte_slice()`], but will panic if there is ill-formed UTF-8 in the `input`.
124+pub fn from_byte_slice(input: &[u8]) -> &Path {
125+ try_from_byte_slice(input).expect("well-formed UTF-8 on windows")
126+}
127+
128+fn replace<'a>(path: impl Into<Cow<'a, BStr>>, find: u8, replace: u8) -> Cow<'a, BStr> {
129+ let path = path.into();
130+ match path {
131+ Cow::Owned(mut path) => {
132+ for b in path.iter_mut().filter(|b| **b == find) {
133+ *b = replace;
134+ }
135+ path.into()
136+ }
137+ Cow::Borrowed(path) => {
138+ if !path.contains(&find) {
139+ return path.into();
140+ }
141+ let mut path = path.to_owned();
142+ for b in path.iter_mut().filter(|b| **b == find) {
143+ *b = replace;
144+ }
145+ path.into()
146+ }
147+ }
148+}
149+
150+/// Assures the given bytes use the native path separator.
151+pub fn to_native_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> {
152+ #[cfg(not(windows))]
153+ let p = to_unix_separators(path);
154+ #[cfg(windows)]
155+ let p = to_windows_separators(path);
156+ p
157+}
158+
159+/// Convert paths with slashes to backslashes on windows and do nothing on unix, but **panics** if malformed surrogates are encountered on windows.
160+pub fn to_native_path_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, std::path::Path> {
161+ #[cfg(not(windows))]
162+ {
163+ crate::from_bstr(path)
164+ }
165+ #[cfg(windows)]
166+ {
167+ crate::from_bstr(to_windows_separators(path))
168+ }
169+}
170+
171+/// Replaces windows path separators with slashes, but only do so on windows.
172+pub fn to_unix_separators_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> {
173+ #[cfg(windows)]
174+ {
175+ replace(path, b'\\', b'/')
176+ }
177+ #[cfg(not(windows))]
178+ {
179+ path.into()
180+ }
181+}
182+
183+/// Replaces windows path separators with slashes, unconditionally.
184+///
185+/// **Note** Do not use these and prefer the conditional versions of this method.
186+// TODO: use https://lib.rs/crates/path-slash to handle escapes
187+pub fn to_unix_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> {
188+ replace(path, b'\\', b'/')
189+}
190+
191+/// Find backslashes and replace them with slashes, which typically resembles a unix path, unconditionally.
192+///
193+/// **Note** Do not use these and prefer the conditional versions of this method.
194+// TODO: use https://lib.rs/crates/path-slash to handle escapes
195+pub fn to_windows_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> {
196+ replace(path, b'/', b'\\')
197+}
git-path/src/lib.rs+52−0
@@ -0,0 +1,52 @@
1+#![forbid(unsafe_code, rust_2018_idioms, missing_docs)]
2+//! ### Research
3+//!
4+//! * **windows**
5+//! - [`dirent.c`](https://github.com/git/git/blob/main/compat/win32/dirent.c#L31:L31) contains all implementation (seemingly) of opening directories and reading their entries, along with all path conversions (UTF-16 for windows). This is done on the fly so git can work with [in UTF-8](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12).
6+//! - mingw [is used for the conversion](https://github.com/git/git/blob/main/compat/mingw.h#L579:L579) and it appears they handle surrogates during the conversion, maybe some sort of non-strict UTF-8 converter? Actually it uses [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte)
7+//! under the hood which by now does fail if the UTF-8 would be invalid unicode, i.e. unicode pairs.
8+//! - `OsString` on windows already stores strings as WTF-8, which supports [surrogate pairs](https://unicodebook.readthedocs.io/unicode_encodings.html),
9+//! something that UTF-8 isn't allowed do it for security reasons, after all it's UTF-16 specific and exists only to extend
10+//! the encodable code-points.
11+//! - informative reading on [WTF-8](https://simonsapin.github.io/wtf-8/#motivation) which is the encoding used by Rust
12+//! internally that deals with surrogates and non-wellformed surrogates (those that aren't in pairs).
13+//! * **unix**
14+//! - It uses [opendir](https://man7.org/linux/man-pages/man3/opendir.3.html) and [readdir](https://man7.org/linux/man-pages/man3/readdir.3.html)
15+//! respectively. There is no encoding specified, except that these paths are null-terminated.
16+//!
17+//! ### Learnings
18+//!
19+//! Surrogate pairs are a way to extend the encodable value range in UTF-16 encodings, used primarily on windows and in Javascript.
20+//! For a long time these codepoints used for surrogates, always to be used in pairs, were not assigned, until…they were for rare
21+//! emojies and the likes. The unicode standard does not require surrogates to happen in pairs, even though by now unpaired surrogates
22+//! in UTF-16 are considered ill-formed, which aren't supposed to be converted to UTF-8 for example.
23+//!
24+//! This is the reason we have to deal with `to_string_lossy()`, it's _just_ for that quirk.
25+//!
26+//! This also means the only platform ever eligible to see conversion errors is windows, and there it's only older pre-vista
27+//! windows versions which incorrectly allow ill-formed UTF-16 strings. Newer versions don't perform such conversions anymore, for
28+//! example when going from UTF-16 to UTF-8, they will trigger an error.
29+//!
30+//! ### Conclusions
31+//!
32+//! Since [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) by now is
33+//! fixed (Vista onward) to produce valid UTF-8, lone surrogate codepoints will cause failure, which `git`
34+//! [doesn't care about](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12).
35+//!
36+//! We will, though, which means from now on we can just convert to UTF-8 on windows and bubble up errors where necessary,
37+//! preventing potential mismatched surrogate pairs to ever be saved on disk by gitoxide.
38+//!
39+//! Even though the error only exists on older windows versions, we will represent it in the type system through fallible function calls.
40+//! Callers may `.expect()` on the result to indicate they don't wish to handle this special and rare case. Note that servers should not
41+//! ever get into a code-path which does panic though.
42+
43+/// A dummy type to represent path specs and help finding all spots that take path specs once it is implemented.
44+
45+/// A preliminary version of a path-spec based on glances of the code.
46+#[derive(Clone, Debug)]
47+pub struct Spec(bstr::BString);
48+
49+mod convert;
50+mod spec;
51+
52+pub use convert::*;
git-path/src/spec.rs+51−0
@@ -0,0 +1,51 @@
1+use crate::Spec;
2+use bstr::{BStr, ByteSlice, ByteVec};
3+use std::ffi::OsStr;
4+
5+impl std::convert::TryFrom<&OsStr> for Spec {
6+ type Error = crate::Utf8Error;
7+
8+ fn try_from(value: &OsStr) -> Result<Self, Self::Error> {
9+ crate::os_str_into_bstr(value).map(|value| {
10+ assert_valid_hack(value);
11+ Spec(value.into())
12+ })
13+ }
14+}
15+
16+fn assert_valid_hack(input: &BStr) {
17+ assert!(!input.contains_str(b"/../"));
18+ assert!(!input.contains_str(b"/./"));
19+ assert!(!input.starts_with_str(b"../"));
20+ assert!(!input.starts_with_str(b"./"));
21+ assert!(!input.starts_with_str(b"/"));
22+}
23+
24+impl Spec {
25+ /// Parse `input` into a `Spec` or `None` if it could not be parsed
26+ // TODO: tests, actual implementation probably via `git-pathspec` to make use of the crate after all.
27+ pub fn from_bytes(input: &BStr) -> Option<Self> {
28+ assert_valid_hack(input);
29+ Spec(input.into()).into()
30+ }
31+ /// Return all paths described by this path spec, using slashes on all platforms.
32+ pub fn items(&self) -> impl Iterator<Item = &BStr> {
33+ std::iter::once(self.0.as_bstr())
34+ }
35+ /// Adjust this path specification according to the given `prefix`, which may be empty to indicate we are the at work-tree root.
36+ // TODO: this is a hack, needs test and time to do according to spec. This is just a minimum version to have -something-.
37+ pub fn apply_prefix(&mut self, prefix: &std::path::Path) -> &Self {
38+ // many more things we can't handle. `Path` never ends with trailing path separator.
39+ let prefix = crate::into_bstr(prefix);
40+ if !prefix.is_empty() {
41+ let mut prefix = crate::to_unix_separators_on_windows(prefix);
42+ {
43+ let path = prefix.to_mut();
44+ path.push_byte(b'/');
45+ path.extend_from_slice(&self.0);
46+ }
47+ self.0 = prefix.into_owned();
48+ }
49+ self
50+ }
51+}
git-path/tests/path.rs+21−0
@@ -0,0 +1,21 @@
1+mod convert {
2+ use bstr::ByteSlice;
3+ use git_path::{to_unix_separators, to_windows_separators};
4+
5+ #[test]
6+ fn assure_unix_separators() {
7+ assert_eq!(to_unix_separators(b"no-backslash".as_bstr()).as_bstr(), "no-backslash");
8+
9+ assert_eq!(to_unix_separators(b"\\a\\b\\\\".as_bstr()).as_bstr(), "/a/b//");
10+ }
11+
12+ #[test]
13+ fn assure_windows_separators() {
14+ assert_eq!(
15+ to_windows_separators(b"no-backslash".as_bstr()).as_bstr(),
16+ "no-backslash"
17+ );
18+
19+ assert_eq!(to_windows_separators(b"/a/b//".as_bstr()).as_bstr(), "\\a\\b\\\\");
20+ }
21+}
git-ref/Cargo.toml+2−1
@@ -25,7 +25,8 @@ required-features = ["internal-testing-git-features-parallel"] · 24 unmodified lines
2525 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2626
2727 [dependencies]
28−git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir", "bstr"]}
28+git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir"]}
29+git-path = { version = "^0.1.0", path = "../git-path" }
2930 git-hash = { version = "^0.9.3", path = "../git-hash" }
3031 git-object = { version = "^0.18.0", path = "../git-object" }
3132 git-validate = { version ="^0.5.3", path = "../git-validate" }
git-ref/src/fullname.rs+1−1
@@ -75,7 +75,7 @@ impl FullName { · 74 unmodified lines
7575
7676 /// Convert this name into the relative path, lossily, identifying the reference location relative to a repository
7777 pub fn to_path(&self) -> &Path {
78− git_features::path::from_byte_slice_or_panic_on_windows(&self.0)
78+ git_path::from_byte_slice(&self.0)
7979 }
8080
8181 /// Dissolve this instance and return the buffer.
git-ref/src/name.rs+3−3
@@ -26,7 +26,7 @@ impl Category { · 25 unmodified lines
2626 impl<'a> FullNameRef<'a> {
2727 /// Convert this name into the relative path identifying the reference location.
2828 pub fn to_path(self) -> &'a Path {
29− git_features::path::from_byte_slice_or_panic_on_windows(self.0)
29+ git_path::from_byte_slice(self.0)
3030 }
3131
3232 /// Return ourselves as byte string which is a valid refname
@@ -79,7 +79,7 @@ impl<'a> PartialNameRef<'a> { · 46 unmodified lines
7979 /// Convert this name into the relative path possibly identifying the reference location.
8080 /// Note that it may be only a partial path though.
8181 pub fn to_partial_path(&'a self) -> &'a Path {
82− git_features::path::from_byte_slice_or_panic_on_windows(self.0.as_ref())
82+ git_path::from_byte_slice(self.0.as_ref())
8383 }
8484
8585 /// Provide the name as binary string which is known to be a valid partial ref name.
@@ -122,7 +122,7 @@ impl<'a> TryFrom<&'a OsStr> for PartialNameRef<'a> { · 36 unmodified lines
122122 type Error = Error;
123123
124124 fn try_from(v: &'a OsStr) -> Result<Self, Self::Error> {
125− let v = git_features::path::os_str_into_bytes(v)
125+ let v = git_path::os_str_into_bstr(v)
126126 .map_err(|_| Error::Tag(git_validate::tag::name::Error::InvalidByte("<unknown encoding>".into())))?;
127127 Ok(PartialNameRef(
128128 git_validate::reference::name_partial(v.as_bstr())?.into(),
git-ref/src/namespace.rs+3−10
@@ -18,19 +18,12 @@ impl Namespace { · 17 unmodified lines
1818 }
1919 /// Return ourselves as a path for use within the filesystem.
2020 pub fn to_path(&self) -> &Path {
21− git_features::path::from_byte_slice_or_panic_on_windows(&self.0)
21+ git_path::from_byte_slice(&self.0)
2222 }
2323 /// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration.
2424 pub fn into_namespaced_prefix(mut self, prefix: impl AsRef<Path>) -> PathBuf {
25− self.0
26− .push_str(git_features::path::into_bytes_or_panic_on_windows(prefix.as_ref()));
27− git_features::path::from_byte_vec_or_panic_on_windows(
28− git_features::path::convert::to_native_separators({
29− let v: Vec<_> = self.0.into();
30− v
31− })
32− .into_owned(),
33− )
25+ self.0.push_str(git_path::into_bstr(prefix.as_ref()).as_ref());
26+ git_path::to_native_path_on_windows(self.0.clone()).into_owned()
3427 }
3528 }
3629
git-ref/src/store/file/find.rs+1−1
@@ -124,7 +124,7 @@ impl file::Store { · 123 unmodified lines
124124 };
125125 let relative_path = base.join(inbetween).join(relative_path);
126126
127− let path_to_open = git_features::path::convert::to_windows_separators_on_windows_or_panic(&relative_path);
127+ let path_to_open = git_path::to_native_path_on_windows(git_path::into_bstr(&relative_path));
128128 let contents = match self
129129 .ref_contents(&path_to_open)
130130 .map_err(|err| Error::ReadFileContents {
git-ref/src/store/file/loose/iter.rs+6−7
@@ -49,7 +49,7 @@ impl Iterator for SortedLoosePaths { · 48 unmodified lines
4949 .as_deref()
5050 .and_then(|prefix| full_path.file_name().map(|name| (prefix, name)))
5151 {
52− match git_features::path::os_str_into_bytes(name) {
52+ match git_path::os_str_into_bstr(name) {
5353 Ok(name) => {
5454 if !name.starts_with(prefix) {
5555 continue;
@@ -61,17 +61,16 @@ impl Iterator for SortedLoosePaths { · 5 unmodified lines
6161 let full_name = full_path
6262 .strip_prefix(&self.base)
6363 .expect("prefix-stripping cannot fail as prefix is our root");
64− let full_name = match git_features::path::into_bytes(full_name) {
64+ let full_name = match git_path::try_into_bstr(full_name) {
6565 Ok(name) => {
66− #[cfg(windows)]
67− let name = git_features::path::convert::to_unix_separators(name);
66+ let name = git_path::to_unix_separators_on_windows(name);
6867 name.into_owned()
6968 }
7069 Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways?
7170 };
7271
7372 if git_validate::reference::name_partial(full_name.as_bstr()).is_ok() {
74− let name = FullName(full_name.into());
73+ let name = FullName(full_name);
7574 return Some(Ok((full_path, name)));
7675 } else {
7776 continue;
@@ -201,8 +200,8 @@ impl file::Store { · 123 unmodified lines
201200 base.file_name()
202201 .map(ToOwned::to_owned)
203202 .map(|p| {
204− git_features::path::into_bytes(PathBuf::from(p))
205− .map(|p| BString::from(p.into_owned()))
203+ git_path::try_into_bstr(PathBuf::from(p))
204+ .map(|p| p.into_owned())
206205 .map_err(|_| {
207206 std::io::Error::new(
208207 std::io::ErrorKind::InvalidInput,
git-ref/src/store/file/mod.rs+2−6
@@ -53,12 +53,8 @@ pub struct Transaction<'s> { · 52 unmodified lines
5353 }
5454
5555 pub(in crate::store_impl::file) fn path_to_name<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> {
56− let path = git_features::path::into_bytes_or_panic_on_windows(path.into());
57−
58− #[cfg(windows)]
59− let path = git_features::path::convert::to_unix_separators(path);
60−
61− git_features::path::convert::into_bstr(path)
56+ let path = git_path::into_bstr(path.into());
57+ git_path::to_unix_separators_on_windows(path)
6258 }
6359
6460 ///
git-repository/Cargo.toml+6−3
@@ -41,7 +41,7 @@ one-stop-shop = [ "local", "local-time-support" ] · 40 unmodified lines
4141 #! ### Other
4242
4343 ## Data structures implement `serde::Serialize` and `serde::Deserialize`.
44−serde1 = ["git-pack/serde1", "git-object/serde1", "git-protocol/serde1", "git-transport/serde1", "git-ref/serde1", "git-odb/serde1", "git-index/serde1", "git-mailmap/serde1"]
44+serde1 = ["git-pack/serde1", "git-object/serde1", "git-protocol/serde1", "git-transport/serde1", "git-ref/serde1", "git-odb/serde1", "git-index/serde1", "git-mailmap/serde1", "git-attributes/serde1"]
4545 ## Activate other features that maximize performance, like usage of threads, `zlib-ng` and access to caching in object databases.
4646 ## **Note** that
4747 max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git-pack/pack-cache-lru-static", "git-pack/pack-cache-lru-dynamic"]
@@ -49,7 +49,7 @@ max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git- · 1 unmodified line
4949 local-time-support = ["git-actor/local-time-support"]
5050 ## Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible.
5151 ## Doing so is less stable than the stability tier 1 that `git-repository` is a member of.
52−unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials"]
52+unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials", "git-path", "git-attributes"]
5353 ## Print debugging information about usage of object database caches, useful for tuning cache sizes.
5454 cache-efficiency-debug = ["git-features/cache-efficiency-debug"]
5555
@@ -60,7 +60,7 @@ git-ref = { version = "^0.12.1", path = "../git-ref" } · 4 unmodified lines
6060 git-tempfile = { version = "^2.0.0", path = "../git-tempfile" }
6161 git-lock = { version = "^2.0.0", path = "../git-lock" }
6262 git-validate = { version ="^0.5.3", path = "../git-validate" }
63−git-sec = { version = "^0.1.0", path = "../git-sec" }
63+git-sec = { version = "^0.1.0", path = "../git-sec", features = ["thiserror"] }
6464
6565 git-config = { version = "^0.2.1", path = "../git-config" }
6666 git-odb = { version = "^0.28.0", path = "../git-odb" }
@@ -70,6 +70,7 @@ git-actor = { version = "^0.9.0", path = "../git-actor" } · 3 unmodified lines
7070 git-pack = { version = "^0.18.0", path = "../git-pack", features = ["object-cache-dynamic"] }
7171 git-revision = { version = "^0.1.0", path = "../git-revision" }
7272
73+git-path = { version = "^0.1.0", path = "../git-path", optional = true }
7374 git-url = { version = "^0.4.0", path = "../git-url", optional = true }
7475 git-traverse = { version = "^0.14.0", path = "../git-traverse" }
7576 git-protocol = { version = "^0.15.0", path = "../git-protocol", optional = true }
@@ -79,6 +80,7 @@ git-mailmap = { version = "^0.1.0", path = "../git-mailmap", optional = true } · 3 unmodified lines
7980 git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] }
8081
8182 # unstable only
83+git-attributes = { version = "^0.1.0", path = "../git-attributes", optional = true }
8284 git-glob = { version = "^0.2.0", path = "../git-glob", optional = true }
8385 git-credentials = { version = "^0.1.0", path = "../git-credentials", optional = true }
8486 git-index = { version = "^0.2.0", path = "../git-index", optional = true }
@@ -98,6 +100,7 @@ unicode-normalization = { version = "0.1.19", default-features = false } · 13 unmodified lines
98100
99101 [dev-dependencies]
100102 git-testtools = { path = "../tests/tools" }
103+is_ci = "1.1.1"
101104 anyhow = "1"
102105 tempfile = "3.2.0"
103106
git-repository/src/config.rs+85−33
@@ -1,4 +1,5 @@
11 use crate::bstr::BString;
2+use crate::permission::EnvVarResourcePermission;
23
34 #[derive(Debug, thiserror::Error)]
45 pub enum Error {
@@ -10,85 +11,112 @@ pub enum Error { · 5 unmodified lines
1011 EmptyValue { key: &'static str },
1112 #[error("Invalid value for 'core.abbrev' = '{}'. It must be between 4 and {}", .value, .max)]
1213 CoreAbbrev { value: BString, max: u8 },
14+ #[error("Value '{}' at key '{}' could not be decoded as boolean", .value, .key)]
15+ DecodeBoolean { key: String, value: BString },
16+ #[error(transparent)]
17+ PathInterpolation(#[from] git_config::values::path::interpolate::Error),
1318 }
1419
1520 /// Utility type to keep pre-obtained configuration values.
1621 #[derive(Debug, Clone)]
1722 pub(crate) struct Cache {
1823 // TODO: remove this once resolved is used without a feature dependency
19− #[cfg_attr(not(feature = "git-mailmap"), allow(dead_code))]
24+ #[cfg_attr(not(any(feature = "git-mailmap", feature = "git-index")), allow(dead_code))]
2025 pub resolved: crate::Config,
2126 /// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count.
2227 pub hex_len: Option<usize>,
23− /// true if the repository is designated as 'bare', without work tree
28+ /// true if the repository is designated as 'bare', without work tree.
2429 pub is_bare: bool,
25− /// The type of hash to use
30+ /// The type of hash to use.
2631 pub object_hash: git_hash::Kind,
2732 /// If true, multi-pack indices, whether present or not, may be used by the object database.
2833 pub use_multi_pack_index: bool,
34+ /// If true, we are on a case-insensitive file system.
35+ #[cfg_attr(not(feature = "git-index"), allow(dead_code))]
36+ pub ignore_case: bool,
37+ /// The path to the user-level excludes file to ignore certain files in the worktree.
38+ #[cfg_attr(not(feature = "git-index"), allow(dead_code))]
39+ pub excludes_file: Option<std::path::PathBuf>,
40+ /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable.
41+ #[cfg_attr(not(feature = "git-index"), allow(dead_code))]
42+ xdg_config_home_env: EnvVarResourcePermission,
43+ /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable.
44+ #[cfg_attr(not(feature = "git-index"), allow(dead_code))]
45+ home_env: EnvVarResourcePermission,
2946 // TODO: make core.precomposeUnicode available as well.
3047 }
3148
3249 mod cache {
33− use std::{borrow::Cow, convert::TryFrom};
50+ use std::convert::TryFrom;
51+ use std::path::PathBuf;
3452
3553 use git_config::{
3654 file::GitConfig,
37− values,
3855 values::{Boolean, Integer},
3956 };
4057
4158 use super::{Cache, Error};
4259 use crate::bstr::ByteSlice;
60+ use crate::permission::EnvVarResourcePermission;
4361
4462 impl Cache {
45− pub fn new(git_dir: &std::path::Path) -> Result<Self, Error> {
63+ pub fn new(
64+ git_dir: &std::path::Path,
65+ xdg_config_home_env: EnvVarResourcePermission,
66+ home_env: EnvVarResourcePermission,
67+ git_install_dir: Option<&std::path::Path>,
68+ ) -> Result<Self, Error> {
4669 let config = GitConfig::open(git_dir.join("config"))?;
47− let is_bare = config_bool(&config, "core.bare", false);
48− let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true);
70+
71+ let is_bare = config_bool(&config, "core.bare", false)?;
72+ let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true)?;
73+ let ignore_case = config_bool(&config, "core.ignorecase", false)?;
74+ let excludes_file = config
75+ .path("core", None, "excludesFile")
76+ .map(|p| p.interpolate(git_install_dir).map(|p| p.into_owned()))
77+ .transpose()?;
4978 let repo_format_version = config
5079 .value::<Integer>("core", None, "repositoryFormatVersion")
5180 .map_or(0, |v| v.value);
52− let object_hash = if repo_format_version == 1 {
53− if let Ok(format) = config.value::<Cow<'_, [u8]>>("extensions", None, "objectFormat") {
54− match format.as_ref() {
55− b"sha1" => git_hash::Kind::Sha1,
56− _ => {
57− return Err(Error::UnsupportedObjectFormat {
81+ let object_hash = (repo_format_version != 1)
82+ .then(|| Ok(git_hash::Kind::Sha1))
83+ .or_else(|| {
84+ config
85+ .raw_value("extensions", None, "objectFormat")
86+ .ok()
87+ .map(|format| match format.as_ref() {
88+ b"sha1" => Ok(git_hash::Kind::Sha1),
89+ _ => Err(Error::UnsupportedObjectFormat {
5890 name: format.to_vec().into(),
59− })
60− }
61− }
62− } else {
63− git_hash::Kind::Sha1
64− }
65− } else {
66− git_hash::Kind::Sha1
67− };
91+ }),
92+ })
93+ })
94+ .transpose()?
95+ .unwrap_or(git_hash::Kind::Sha1);
6896
6997 let mut hex_len = None;
70− if let Ok(hex_len_str) = config.value::<values::String<'_>>("core", None, "abbrev") {
71− if hex_len_str.value.trim().is_empty() {
98+ if let Some(hex_len_str) = config.string("core", None, "abbrev") {
99+ if hex_len_str.trim().is_empty() {
72100 return Err(Error::EmptyValue { key: "core.abbrev" });
73101 }
74− if hex_len_str.value.as_ref() != "auto" {
75− let value_bytes = hex_len_str.value.as_ref().as_ref();
102+ if hex_len_str.as_ref() != "auto" {
103+ let value_bytes = hex_len_str.as_ref().as_ref();
76104 if let Ok(Boolean::False(_)) = Boolean::try_from(value_bytes) {
77105 hex_len = object_hash.len_in_hex().into();
78106 } else {
79107 let value = Integer::try_from(value_bytes)
80108 .map_err(|_| Error::CoreAbbrev {
81− value: hex_len_str.value.clone().into_owned(),
109+ value: hex_len_str.clone().into_owned(),
82110 max: object_hash.len_in_hex() as u8,
83111 })?
84112 .to_decimal()
85113 .ok_or_else(|| Error::CoreAbbrev {
86− value: hex_len_str.value.clone().into_owned(),
114+ value: hex_len_str.clone().into_owned(),
87115 max: object_hash.len_in_hex() as u8,
88116 })?;
89117 if value < 4 || value as usize > object_hash.len_in_hex() {
90118 return Err(Error::CoreAbbrev {
91− value: hex_len_str.value.clone().into_owned(),
119+ value: hex_len_str.clone().into_owned(),
92120 max: object_hash.len_in_hex() as u8,
93121 });
94122 }
@@ -102,15 +130,39 @@ mod cache { · 7 unmodified lines
102130 use_multi_pack_index,
103131 object_hash,
104132 is_bare,
133+ ignore_case,
105134 hex_len,
135+ excludes_file,
136+ xdg_config_home_env,
137+ home_env,
106138 })
107139 }
140+
141+ /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations.
142+ #[cfg_attr(not(feature = "git-index"), allow(dead_code))]
143+ pub fn xdg_config_path(
144+ &self,
145+ resource_file_name: &str,
146+ ) -> Result<Option<PathBuf>, git_sec::permission::Error<PathBuf, git_sec::Permission>> {
147+ std::env::var_os("XDG_CONFIG_HOME")
148+ .map(|path| (path, &self.xdg_config_home_env))
149+ .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env)))
150+ .and_then(|(base, permission)| {
151+ let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name);
152+ permission.check(resource).transpose()
153+ })
154+ .transpose()
155+ }
108156 }
109157
110− fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> bool {
158+ fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> Result<bool, Error> {
111159 let (section, key) = key.split_once('.').expect("valid section.key format");
112160 config
113− .value::<Boolean<'_>>(section, None, key)
114− .map_or(default, |b| b.to_bool())
161+ .boolean(section, None, key)
162+ .unwrap_or(Ok(default))
163+ .map_err(|err| Error::DecodeBoolean {
164+ value: err.input,
165+ key: key.into(),
166+ })
115167 }
116168 }
git-repository/src/lib.rs+20−4
@@ -83,12 +83,14 @@ · 82 unmodified lines
8383 //! even if this crate doesn't, hence breaking downstream.
8484 //!
8585 //! `git_repository::`
86+//! * [`attrs`]
8687 //! * [`hash`]
8788 //! * [`url`]
8889 //! * [`actor`]
8990 //! * [`bstr`][bstr]
9091 //! * [`index`]
9192 //! * [`glob`]
93+//! * [`path`]
9294 //! * [`credentials`]
9395 //! * [`sec`]
9496 //! * [`worktree`]
@@ -124,6 +126,8 @@ use std::path::PathBuf; · 29 unmodified lines
124126 // This also means that their major version changes affect our major version, but that's alright as we directly expose their
125127 // APIs/instances anyway.
126128 pub use git_actor as actor;
129+#[cfg(all(feature = "unstable", feature = "git-attributes"))]
130+pub use git_attributes as attrs;
127131 #[cfg(all(feature = "unstable", feature = "git-credentials"))]
128132 pub use git_credentials as credentials;
129133 #[cfg(all(feature = "unstable", feature = "git-diff"))]
@@ -156,8 +160,6 @@ pub use git_url as url; · 26 unmodified lines
156160 #[doc(inline)]
157161 #[cfg(all(feature = "unstable", feature = "git-url"))]
158162 pub use git_url::Url;
159−#[cfg(all(feature = "unstable", feature = "git-worktree"))]
160−pub use git_worktree as worktree;
161163 pub use hash::{oid, ObjectId};
162164
163165 pub mod interrupt;
@@ -192,7 +194,9 @@ pub enum Path { · 28 unmodified lines
192194
193195 ///
194196 mod types;
195−pub use types::{Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree};
197+pub use types::{
198+ Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree, Worktree,
199+};
196200
197201 pub mod commit;
198202 pub mod head;
@@ -200,7 +204,6 @@ pub mod id; · 1 unmodified line
200204 pub mod object;
201205 pub mod reference;
202206 mod repository;
203−pub use repository::{permissions, permissions::Permissions};
204207 pub mod tag;
205208
206209 /// The kind of `Repository`
@@ -239,6 +242,16 @@ pub fn open(directory: impl Into<std::path::PathBuf>) -> Result<crate::Repositor · 32 unmodified lines
239242 ThreadSafeRepository::open(directory).map(Into::into)
240243 }
241244
245+///
246+pub mod permission {
247+ use git_sec::permission::Resource;
248+ use git_sec::Access;
249+
250+ /// A permission to control access to the resource pointed to an environment variable.
251+ pub type EnvVarResourcePermission = Access<Resource, git_sec::Permission>;
252+}
253+pub use repository::permissions::Permissions;
254+
242255 ///
243256 pub mod open;
244257
@@ -268,6 +281,9 @@ pub mod mailmap { · 23 unmodified lines
268281 }
269282 }
270283
284+///
285+pub mod worktree;
286+
271287 ///
272288 pub mod rev_parse {
273289 /// The error returned by [`crate::Repository::rev_parse()`].
git-repository/src/open.rs+13−3
@@ -153,10 +153,15 @@ impl crate::ThreadSafeRepository { · 152 unmodified lines
153153 Options {
154154 object_store_slots,
155155 replacement_objects,
156− permissions,
156+ permissions:
157+ Permissions {
158+ git_dir: git_dir_perm,
159+ xdg_config_home,
160+ home,
161+ },
157162 }: Options,
158163 ) -> Result<Self, Error> {
159− if *permissions.git_dir != git_sec::ReadWrite::all() {
164+ if *git_dir_perm != git_sec::ReadWrite::all() {
160165 // TODO: respect `save.directory`, which needs more support from git-config to do properly.
161166 return Err(Error::UnsafeGitDir { path: git_dir });
162167 }
@@ -164,7 +169,12 @@ impl crate::ThreadSafeRepository { · 1 unmodified line
164169 // This would be something read in later as have to first check for extensions. Also this means
165170 // that each worktree, even if accessible through this instance, has to come in its own Repository instance
166171 // as it may have its own configuration. That's fine actually.
167− let config = crate::config::Cache::new(&git_dir)?;
172+ let config = crate::config::Cache::new(
173+ &git_dir,
174+ xdg_config_home,
175+ home,
176+ crate::path::install_dir().ok().as_deref(),
177+ )?;
168178 match worktree_dir {
169179 None if !config.is_bare => {
170180 worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned());
git-repository/src/path/discover.rs+47−23
@@ -8,6 +8,12 @@ pub enum Error { · 7 unmodified lines
88 InaccessibleDirectory { path: PathBuf },
99 #[error("Could find a git repository in '{}' or in any of its parents", .path.display())]
1010 NoGitRepository { path: PathBuf },
11+ #[error("Could find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", .path.display(), .candidate.display())]
12+ NoTrustedGitRepository {
13+ path: PathBuf,
14+ candidate: PathBuf,
15+ required: git_sec::Trust,
16+ },
1117 #[error("Could not determine trust level for path '{}'.", .path.display())]
1218 CheckTrust {
1319 path: PathBuf,
@@ -57,23 +63,20 @@ pub(crate) mod function { · 43 unmodified lines
5763 // us the parent directory. (`Path::parent` just strips off the last
5864 // path component, which means it will not do what you expect when
5965 // working with paths paths that contain '..'.)
60− let directory = maybe_canonicalize(directory.as_ref()).map_err(|_| Error::InaccessibleDirectory {
61− path: directory.as_ref().into(),
62− })?;
63− if !directory.is_dir() {
64− return Err(Error::InaccessibleDirectory {
65− path: directory.into_owned(),
66− });
66+ let directory = directory.as_ref();
67+ let dir = maybe_canonicalize(directory).map_err(|_| Error::InaccessibleDirectory { path: directory.into() })?;
68+ let is_canonicalized = dir.as_ref() != directory;
69+ if !dir.is_dir() {
70+ return Err(Error::InaccessibleDirectory { path: dir.into_owned() });
6771 }
6872
69− let filter_by_trust =
70− |x: &std::path::Path, kind: crate::path::Kind| -> Result<Option<(crate::Path, git_sec::Trust)>, Error> {
71− let trust =
72− git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?;
73− Ok((trust >= required_trust).then(|| (crate::Path::from_dot_git_dir(x, kind), trust)))
74− };
73+ let filter_by_trust = |x: &std::path::Path| -> Result<Option<git_sec::Trust>, Error> {
74+ let trust =
75+ git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?;
76+ Ok((trust >= required_trust).then(|| (trust)))
77+ };
7578
76− let mut cursor = directory.clone();
79+ let mut cursor = dir.clone();
7780 'outer: loop {
7881 for append_dot_git in &[false, true] {
7982 if *append_dot_git {
@@ -83,11 +86,36 @@ pub(crate) mod function { · 3 unmodified lines
8386 }
8487 }
8588 if let Ok(kind) = path::is::git(&cursor) {
86− match filter_by_trust(&cursor, kind)? {
87− Some(res) => break 'outer Ok(res),
89+ match filter_by_trust(&cursor)? {
90+ Some(trust) => {
91+ // TODO: test this more
92+ let path = if is_canonicalized {
93+ match std::env::current_dir() {
94+ Ok(cwd) => cwd
95+ .strip_prefix(&cursor.parent().expect(".git appended"))
96+ .ok()
97+ .and_then(|p| {
98+ let short_path_components = p.components().count();
99+ (short_path_components < cursor.components().count()).then(|| {
100+ std::iter::repeat("..")
101+ .take(short_path_components)
102+ .chain(Some(".git"))
103+ .collect()
104+ })
105+ })
106+ .unwrap_or_else(|| cursor.into_owned()),
107+ Err(_) => cursor.into_owned(),
108+ }
109+ } else {
110+ cursor.into_owned()
111+ };
112+ break 'outer Ok((crate::Path::from_dot_git_dir(path, kind), trust));
113+ }
88114 None => {
89− break 'outer Err(Error::NoGitRepository {
90− path: directory.into_owned(),
115+ break 'outer Err(Error::NoTrustedGitRepository {
116+ path: dir.into_owned(),
117+ candidate: cursor.into_owned(),
118+ required: required_trust,
91119 })
92120 }
93121 }
@@ -100,11 +128,7 @@ pub(crate) mod function { · 6 unmodified lines
100128 }
101129 match cursor.parent() {
102130 Some(parent) => cursor = parent.to_owned().into(),
103− None => {
104− break Err(Error::NoGitRepository {
105− path: directory.into_owned(),
106− })
107− }
131+ None => break Err(Error::NoGitRepository { path: dir.into_owned() }),
108132 }
109133 }
110134 }
git-repository/src/path/is.rs+1−0
@@ -28,6 +28,7 @@ pub fn bare(git_dir_candidate: impl AsRef<Path>) -> bool { · 27 unmodified lines
2828 /// What constitutes a valid git repository, and what's yet to be implemented, returning the guessed repository kind
2929 /// purely based on the presence of files. Note that the git-config ultimately decides what's bare.
3030 ///
31+/// * [ ] git files
3132 /// * [x] a valid head
3233 /// * [ ] git common directory
3334 /// * [ ] respect GIT_COMMON_DIR
git-repository/src/path/mod.rs+16−1
@@ -2,6 +2,9 @@ use std::path::PathBuf; · 1 unmodified line
22
33 use crate::{Kind, Path};
44
5+#[cfg(all(feature = "unstable", feature = "git-path"))]
6+pub use git_path::*;
7+
58 ///
69 pub mod create;
710 ///
@@ -24,7 +27,11 @@ impl Path { · 16 unmodified lines
2427 pub fn from_dot_git_dir(dir: impl Into<PathBuf>, kind: Kind) -> Self {
2528 let dir = dir.into();
2629 match kind {
27− Kind::WorkTree => Path::WorkTree(dir.parent().expect("this is a sub-directory").to_owned()),
30+ Kind::WorkTree => Path::WorkTree(if dir == std::path::Path::new(".git") {
31+ PathBuf::from(".")
32+ } else {
33+ dir.parent().expect("this is a sub-directory").to_owned()
34+ }),
2835 Kind::Bare => Path::Repository(dir),
2936 }
3037 }
@@ -44,3 +51,11 @@ impl Path { · 13 unmodified lines
4451 }
4552 }
4653 }
54+
55+pub(crate) fn install_dir() -> std::io::Result<std::path::PathBuf> {
56+ std::env::current_exe().and_then(|exe| {
57+ exe.parent()
58+ .map(ToOwned::to_owned)
59+ .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no parent for current executable"))
60+ })
61+}
git-repository/src/repository/init.rs+32−0
@@ -0,0 +1,32 @@
1+use std::cell::RefCell;
2+
3+impl crate::Repository {
4+ pub(crate) fn from_refs_and_objects(
5+ refs: crate::RefStore,
6+ objects: crate::OdbHandle,
7+ work_tree: Option<std::path::PathBuf>,
8+ config: crate::config::Cache,
9+ ) -> Self {
10+ crate::Repository {
11+ bufs: RefCell::new(Vec::with_capacity(4)),
12+ work_tree,
13+ objects: {
14+ #[cfg(feature = "max-performance")]
15+ {
16+ objects.with_pack_cache(|| Box::new(git_pack::cache::lru::StaticLinkedList::<64>::default()))
17+ }
18+ #[cfg(not(feature = "max-performance"))]
19+ {
20+ objects
21+ }
22+ },
23+ refs,
24+ config,
25+ }
26+ }
27+
28+ /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data.
29+ pub fn into_sync(self) -> crate::ThreadSafeRepository {
30+ self.into()
31+ }
32+}
git-repository/src/repository/location.rs+25−4
@@ -12,10 +12,31 @@ impl crate::Repository { · 11 unmodified lines
1212 // TODO: tests, respect precomposeUnicode
1313 /// The directory of the binary path of the current process.
1414 pub fn install_dir(&self) -> std::io::Result<std::path::PathBuf> {
15− std::env::current_exe().and_then(|exe| {
16− exe.parent()
17− .map(ToOwned::to_owned)
18− .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no parent for current executable"))
15+ crate::path::install_dir()
16+ }
17+
18+ /// Returns the relative path which is the components between the working tree and the current working dir (CWD).
19+ /// Note that there may be `None` if there is no work tree, even though the `PathBuf` will be empty
20+ /// if the CWD is at the root of the work tree.
21+ // TODO: tests, details - there is a lot about environment variables to change things around.
22+ pub fn prefix(&self) -> Option<std::io::Result<std::path::PathBuf>> {
23+ self.work_tree.as_ref().map(|root| {
24+ root.canonicalize().and_then(|root| {
25+ std::env::current_dir().and_then(|cwd| {
26+ cwd.strip_prefix(&root)
27+ .map_err(|_| {
28+ std::io::Error::new(
29+ std::io::ErrorKind::Other,
30+ format!(
31+ "CWD '{}' isn't within the work tree '{}'",
32+ cwd.display(),
33+ root.display()
34+ ),
35+ )
36+ })
37+ .map(ToOwned::to_owned)
38+ })
39+ })
1940 })
2041 }
2142
git-repository/src/repository/mod.rs+4−90
@@ -39,98 +39,12 @@ impl crate::Repository { · 38 unmodified lines
3939 }
4040 }
4141
42−/// Various permissions for parts of git repositories.
43−pub mod permissions {
44− use git_sec::permission::Resource;
45− use git_sec::{Access, Trust};
46−
47− /// Permissions associated with various resources of a git repository
48− pub struct Permissions {
49− /// Control how a git-dir can be used.
50− ///
51− /// Note that a repository won't be usable at all unless read and write permissions are given.
52− pub git_dir: Access<Resource, git_sec::ReadWrite>,
53− }
54−
55− impl Permissions {
56− /// Return permissions similar to what git does when the repository isn't owned by the current user,
57− /// thus refusing all operations in it.
58− pub fn strict() -> Self {
59− Permissions {
60− git_dir: Access::resource(git_sec::ReadWrite::empty()),
61− }
62− }
63−
64− /// Return permissions that will not include configuration files not owned by the current user,
65− /// but trust system and global configuration files along with those which are owned by the current user.
66− ///
67− /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using
68− /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`.
69− pub fn secure() -> Self {
70− Permissions {
71− git_dir: Access::resource(git_sec::ReadWrite::all()),
72− }
73− }
74−
75− /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically
76− /// does with owned repositories.
77− pub fn all() -> Self {
78− Permissions {
79− git_dir: Access::resource(git_sec::ReadWrite::all()),
80− }
81− }
82− }
42+mod worktree;
8343
84− impl git_sec::trust::DefaultForLevel for Permissions {
85− fn default_for_level(level: Trust) -> Self {
86− match level {
87− Trust::Full => Permissions::all(),
88− Trust::Reduced => Permissions::secure(),
89− }
90− }
91− }
92−
93− impl Default for Permissions {
94− fn default() -> Self {
95− Permissions::secure()
96− }
97− }
98−}
99−
100−mod init {
101− use std::cell::RefCell;
102−
103− impl crate::Repository {
104− pub(crate) fn from_refs_and_objects(
105− refs: crate::RefStore,
106− objects: crate::OdbHandle,
107− work_tree: Option<std::path::PathBuf>,
108− config: crate::config::Cache,
109− ) -> Self {
110− crate::Repository {
111− bufs: RefCell::new(Vec::with_capacity(4)),
112− work_tree,
113− objects: {
114− #[cfg(feature = "max-performance")]
115− {
116− objects.with_pack_cache(|| Box::new(git_pack::cache::lru::StaticLinkedList::<64>::default()))
117− }
118− #[cfg(not(feature = "max-performance"))]
119− {
120− objects
121− }
122− },
123− refs,
124− config,
125− }
126− }
44+/// Various permissions for parts of git repositories.
45+pub(crate) mod permissions;
12746
128− /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data.
129− pub fn into_sync(self) -> crate::ThreadSafeRepository {
130− self.into()
131− }
132− }
133−}
47+mod init;
13448
13549 mod location;
13650
git-repository/src/repository/permissions.rs+67−0
@@ -0,0 +1,67 @@
1+use crate::permission::EnvVarResourcePermission;
2+use git_sec::permission::Resource;
3+use git_sec::{Access, Trust};
4+
5+/// Permissions associated with various resources of a git repository
6+pub struct Permissions {
7+ /// Control how a git-dir can be used.
8+ ///
9+ /// Note that a repository won't be usable at all unless read and write permissions are given.
10+ pub git_dir: Access<Resource, git_sec::ReadWrite>,
11+ /// Control whether resources pointed to by `XDG_CONFIG_HOME` can be used when looking up common configuration values.
12+ ///
13+ /// Note that [`git_sec::Permission::Forbid`] will cause the operation to abort if a resource is set via the XDG config environment.
14+ pub xdg_config_home: EnvVarResourcePermission,
15+ /// Control if resources pointed to by the
16+ pub home: EnvVarResourcePermission,
17+}
18+
19+impl Permissions {
20+ /// Return permissions similar to what git does when the repository isn't owned by the current user,
21+ /// thus refusing all operations in it.
22+ pub fn strict() -> Self {
23+ Permissions {
24+ git_dir: Access::resource(git_sec::ReadWrite::empty()),
25+ xdg_config_home: Access::resource(git_sec::Permission::Allow),
26+ home: Access::resource(git_sec::Permission::Allow),
27+ }
28+ }
29+
30+ /// Return permissions that will not include configuration files not owned by the current user,
31+ /// but trust system and global configuration files along with those which are owned by the current user.
32+ ///
33+ /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using
34+ /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`.
35+ pub fn secure() -> Self {
36+ Permissions {
37+ git_dir: Access::resource(git_sec::ReadWrite::all()),
38+ xdg_config_home: Access::resource(git_sec::Permission::Allow),
39+ home: Access::resource(git_sec::Permission::Allow),
40+ }
41+ }
42+
43+ /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically
44+ /// does with owned repositories.
45+ pub fn all() -> Self {
46+ Permissions {
47+ git_dir: Access::resource(git_sec::ReadWrite::all()),
48+ xdg_config_home: Access::resource(git_sec::Permission::Allow),
49+ home: Access::resource(git_sec::Permission::Allow),
50+ }
51+ }
52+}
53+
54+impl git_sec::trust::DefaultForLevel for Permissions {
55+ fn default_for_level(level: Trust) -> Self {
56+ match level {
57+ Trust::Full => Permissions::all(),
58+ Trust::Reduced => Permissions::secure(),
59+ }
60+ }
61+}
62+
63+impl Default for Permissions {
64+ fn default() -> Self {
65+ Permissions::secure()
66+ }
67+}
git-repository/src/repository/snapshots.rs+1−1
@@ -48,7 +48,7 @@ impl crate::Repository { · 47 unmodified lines
4848 let mut blob_id = self
4949 .config
5050 .resolved
51− .get_raw_value("mailmap", None, "blob")
51+ .raw_value("mailmap", None, "blob")
5252 .ok()
5353 .and_then(|spec| {
5454 // TODO: actually resolve this as spec (once we can do that)
git-repository/src/repository/worktree.rs+21−0
@@ -0,0 +1,21 @@
1+use crate::{worktree, Worktree};
2+
3+impl crate::Repository {
4+ /// Return a platform for interacting with worktrees
5+ pub fn worktree(&self) -> worktree::Platform<'_> {
6+ worktree::Platform { parent: self }
7+ }
8+}
9+
10+impl<'repo> worktree::Platform<'repo> {
11+ /// Return the currently set worktree if there is one.
12+ ///
13+ /// Note that there would be `None` if this repository is `bare` and the parent [`Repository`][crate::Repository] was instantiated without
14+ /// registered worktree in the current working dir.
15+ pub fn current(&self) -> Option<Worktree<'repo>> {
16+ self.parent.work_dir().map(|path| Worktree {
17+ parent: self.parent,
18+ path,
19+ })
20+ }
21+}
git-repository/src/types.rs+17−8
@@ -4,13 +4,22 @@ use git_hash::ObjectId; · 3 unmodified lines
44
55 use crate::head;
66
7+/// A worktree checkout containing the files of the repository in consumable form.
8+pub struct Worktree<'repo> {
9+ #[cfg_attr(not(feature = "git-index"), allow(dead_code))]
10+ pub(crate) parent: &'repo Repository,
11+ /// The root path of the checkout.
12+ #[allow(dead_code)]
13+ pub(crate) path: &'repo std::path::Path,
14+}
15+
716 /// The head reference, as created from looking at `.git/HEAD`, able to represent all of its possible states.
817 ///
918 /// Note that like [`Reference`], this type's data is snapshot of persisted state on disk.
1019 pub struct Head<'repo> {
1120 /// One of various possible states for the HEAD reference
1221 pub kind: head::Kind,
13− pub(crate) repo: &'repo crate::Repository,
22+ pub(crate) repo: &'repo Repository,
1423 }
1524
1625 /// An [ObjectId] with access to a repository.
@@ -18,7 +27,7 @@ pub struct Head<'repo> { · 1 unmodified line
1827 pub struct Id<'r> {
1928 /// The actual object id
2029 pub(crate) inner: ObjectId,
21− pub(crate) repo: &'r crate::Repository,
30+ pub(crate) repo: &'r Repository,
2231 }
2332
2433 /// A decoded object with a reference to its owning repository.
@@ -29,7 +38,7 @@ pub struct Object<'repo> { · 4 unmodified lines
2938 pub kind: git_object::Kind,
3039 /// The fully decoded object data
3140 pub data: Vec<u8>,
32− pub(crate) repo: &'repo crate::Repository,
41+ pub(crate) repo: &'repo Repository,
3342 }
3443
3544 impl<'a> Drop for Object<'a> {
@@ -44,7 +53,7 @@ pub struct Tree<'repo> { · 8 unmodified lines
4453 pub id: ObjectId,
4554 /// The fully decoded tree data
4655 pub data: Vec<u8>,
47− pub(crate) repo: &'repo crate::Repository,
56+ pub(crate) repo: &'repo Repository,
4857 }
4958
5059 impl<'a> Drop for Tree<'a> {
@@ -59,7 +68,7 @@ pub struct Tag<'repo> { · 8 unmodified lines
5968 pub id: ObjectId,
6069 /// The fully decoded tag data
6170 pub data: Vec<u8>,
62− pub(crate) repo: &'repo crate::Repository,
71+ pub(crate) repo: &'repo Repository,
6372 }
6473
6574 impl<'a> Drop for Tag<'a> {
@@ -74,7 +83,7 @@ pub struct Commit<'repo> { · 8 unmodified lines
7483 pub id: ObjectId,
7584 /// The fully decoded commit data
7685 pub data: Vec<u8>,
77− pub(crate) repo: &'repo crate::Repository,
86+ pub(crate) repo: &'repo Repository,
7887 }
7988
8089 impl<'a> Drop for Commit<'a> {
@@ -102,7 +111,7 @@ pub struct DetachedObject { · 21 unmodified lines
102111 pub struct Reference<'r> {
103112 /// The actual reference data
104113 pub inner: git_ref::Reference,
105− pub(crate) repo: &'r crate::Repository,
114+ pub(crate) repo: &'r Repository,
106115 }
107116
108117 /// A thread-local handle to interact with a repository from a single thread.
@@ -127,7 +136,7 @@ pub struct Repository { · 18 unmodified lines
127136 /// An instance with access to everything a git repository entails, best imagined as container implementing `Sync + Send` for _most_
128137 /// for system resources required to interact with a `git` repository which are loaded in once the instance is created.
129138 ///
130−/// Use this type to reference it in a threaded context for creation the creation of a thread-local [`Repositories`][crate::Repository].
139+/// Use this type to reference it in a threaded context for creation the creation of a thread-local [`Repositories`][Repository].
131140 ///
132141 /// Note that this type purposefully isn't very useful until it is converted into a thread-local repository with `to_thread_local()`,
133142 /// it's merely meant to be able to exist in a `Sync` context.
git-repository/src/worktree.rs+129−0
@@ -0,0 +1,129 @@
1+use crate::Repository;
2+#[cfg(all(feature = "unstable", feature = "git-worktree"))]
3+pub use git_worktree::*;
4+
5+///
6+#[cfg(feature = "git-index")]
7+pub mod open_index {
8+ use crate::bstr::BString;
9+
10+ /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()].
11+ #[derive(Debug, thiserror::Error)]
12+ #[allow(missing_docs)]
13+ pub enum Error {
14+ #[error("Could not interpret value '{}' as 'index.threads'", .value)]
15+ ConfigIndexThreads {
16+ value: BString,
17+ #[source]
18+ err: git_config::value::parse::Error,
19+ },
20+ #[error(transparent)]
21+ IndexFile(#[from] git_index::file::init::Error),
22+ }
23+}
24+
25+///
26+#[cfg(feature = "git-index")]
27+pub mod excludes {
28+ use std::path::PathBuf;
29+
30+ /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()].
31+ #[derive(Debug, thiserror::Error)]
32+ #[allow(missing_docs)]
33+ pub enum Error {
34+ #[error("Could not read repository exclude.")]
35+ Io(#[from] std::io::Error),
36+ #[error(transparent)]
37+ EnvironmentPermission(#[from] git_sec::permission::Error<PathBuf, git_sec::Permission>),
38+ }
39+}
40+
41+/// A structure to make the API more stuctured.
42+pub struct Platform<'repo> {
43+ pub(crate) parent: &'repo Repository,
44+}
45+
46+/// Access
47+impl<'repo> crate::Worktree<'repo> {
48+ /// Returns the root of the worktree under which all checked out files are located.
49+ pub fn root(&self) -> &std::path::Path {
50+ self.path
51+ }
52+}
53+
54+impl<'repo> crate::Worktree<'repo> {
55+ /// Configure a file-system cache checking if files below the repository are excluded.
56+ ///
57+ /// This takes into consideration all the usual repository configuration.
58+ // TODO: test
59+ #[cfg(feature = "git-index")]
60+ pub fn excludes<'a>(
61+ &self,
62+ index: &'a git_index::State,
63+ overrides: Option<git_attributes::MatchGroup<git_attributes::Ignore>>,
64+ ) -> Result<git_worktree::fs::Cache<'a>, excludes::Error> {
65+ let repo = self.parent;
66+ let case = repo
67+ .config
68+ .ignore_case
69+ .then(|| git_glob::pattern::Case::Fold)
70+ .unwrap_or_default();
71+ let mut buf = Vec::with_capacity(512);
72+ let state = git_worktree::fs::cache::State::IgnoreStack(git_worktree::fs::cache::state::Ignore::new(
73+ overrides.unwrap_or_default(),
74+ git_attributes::MatchGroup::<git_attributes::Ignore>::from_git_dir(
75+ repo.git_dir(),
76+ match repo.config.excludes_file.as_ref() {
77+ Some(user_path) => Some(user_path.to_owned()),
78+ None => repo.config.xdg_config_path("ignore")?,
79+ },
80+ &mut buf,
81+ )?,
82+ None,
83+ case,
84+ ));
85+ let attribute_list = state.build_attribute_list(index, index.path_backing(), case);
86+ Ok(git_worktree::fs::Cache::new(
87+ self.path,
88+ state,
89+ case,
90+ buf,
91+ attribute_list,
92+ ))
93+ }
94+
95+ // pub fn
96+ /// Open a new copy of the index file and decode it entirely.
97+ ///
98+ /// It will use the `index.threads` configuration key to learn how many threads to use.
99+ // TODO: test
100+ #[cfg(feature = "git-index")]
101+ pub fn open_index(&self) -> Result<git_index::File, crate::worktree::open_index::Error> {
102+ use std::convert::{TryFrom, TryInto};
103+ let repo = self.parent;
104+ let thread_limit = repo
105+ .config
106+ .resolved
107+ .boolean("index", None, "threads")
108+ .map(|res| {
109+ res.map(|value| if value { 0usize } else { 1 }).or_else(|err| {
110+ git_config::values::Integer::try_from(err.input.as_ref())
111+ .map_err(|err| crate::worktree::open_index::Error::ConfigIndexThreads {
112+ value: err.input.clone(),
113+ err,
114+ })
115+ .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1))
116+ })
117+ })
118+ .transpose()?;
119+ git_index::File::at(
120+ repo.git_dir().join("index"),
121+ git_index::decode::Options {
122+ object_hash: repo.object_hash(),
123+ thread_limit,
124+ min_extension_block_in_bytes_for_threading: 0,
125+ },
126+ )
127+ .map_err(Into::into)
128+ }
129+}
git-repository/tests/discover/mod.rs+13−10
@@ -1,5 +1,5 @@
11 mod existing {
2− use std::path::{Component, PathBuf};
2+ use std::path::PathBuf;
33
44 use git_repository::Kind;
55
@@ -71,17 +71,20 @@ mod existing { · 65 unmodified lines
7171 // up far enough. (This tests that `discover::existing` canonicalizes paths before
7272 // exploring ancestors.)
7373 let working_dir = repo_path()?;
74− let dir = working_dir.join("some/very/deeply/nested/subdir/../../../../../../..");
74+ let dir = working_dir.join("some/very/deeply/nested/subdir/../../../../../..");
7575 let (path, trust) = git_repository::path::discover(&dir)?;
7676 assert_eq!(path.kind(), Kind::WorkTree);
77− assert_eq!(
78− path.as_ref()
79− .components()
80− .filter(|c| matches!(c, Component::ParentDir | Component::CurDir))
81− .count(),
82− 0,
83− "there are no relative path components anymore"
84− );
77+ if !(cfg!(windows) && is_ci::cached()) {
78+ // On CI on windows we get a cursor like this with a question mark so our prefix check won't work.
79+ // We recover, but that means this assertion will fail.
80+ // &cursor = "\\\\?\\D:\\a\\gitoxide\\gitoxide\\.git"
81+ // &cwd = "D:\\a\\gitoxide\\gitoxide\\git-repository"
82+ assert_eq!(
83+ path.as_ref(),
84+ std::path::Path::new(".."),
85+ "there is only the minimal amount of relative path components to see this worktree"
86+ );
87+ }
8588 assert_ne!(
8689 path.as_ref().canonicalize()?,
8790 working_dir.canonicalize()?,
git-sec/Cargo.toml+1−0
@@ -19,6 +19,7 @@ serde1 = [ "serde" ] · 18 unmodified lines
1919 [dependencies]
2020 serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] }
2121 bitflags = "1.3.2"
22+thiserror = { version = "1.0.26", optional = true }
2223
2324 [target.'cfg(not(windows))'.dependencies]
2425 libc = "0.2.123"
git-sec/src/lib.rs+66−5
@@ -1,6 +1,7 @@
11 #![deny(unsafe_code, rust_2018_idioms, missing_docs)]
22 //! A shared trust model for `gitoxide` crates.
33
4+use std::fmt::{Debug, Display, Formatter};
45 use std::marker::PhantomData;
56 use std::ops::Deref;
67
@@ -75,19 +76,22 @@ pub mod trust { · 68 unmodified lines
7576 ///
7677 pub mod permission {
7778 use crate::Access;
79+ use std::fmt::{Debug, Display};
7880
7981 /// A marker trait to signal tags for permissions.
80− pub trait Tag {}
82+ pub trait Tag: Debug + Clone {}
8183
8284 /// A tag indicating that a permission is applying to the contents of a configuration file.
85+ #[derive(Debug, Clone)]
8386 pub struct Config;
8487 impl Tag for Config {}
8588
8689 /// A tag indicating that a permission is applying to the resource itself.
90+ #[derive(Debug, Clone)]
8791 pub struct Resource;
8892 impl Tag for Resource {}
8993
90− impl<P> Access<Config, P> {
94+ impl<P: Debug + Display + Clone> Access<Config, P> {
9195 /// Create a permission for values contained in git configuration files.
9296 ///
9397 /// This applies permissions to values contained inside of these files.
@@ -99,7 +103,7 @@ pub mod permission { · 5 unmodified lines
99103 }
100104 }
101105
102− impl<P> Access<Resource, P> {
106+ impl<P: Debug + Display + Clone> Access<Resource, P> {
103107 /// Create a permission a file or directory itself.
104108 ///
105109 /// This applies permissions to a configuration file itself and whether it can be used at all, or to a directory
@@ -111,6 +115,20 @@ pub mod permission { · 5 unmodified lines
111115 }
112116 }
113117 }
118+
119+ /// An error to use if an operation cannot proceed due to insufficient permissions.
120+ ///
121+ /// It's up to the implementation to decide which permission is required for an operation, and which one
122+ /// causes errors.
123+ #[cfg(feature = "thiserror")]
124+ #[derive(Debug, thiserror::Error)]
125+ #[error("Not allowed to handle resource {:?}: permission {}", .resource, .permission)]
126+ pub struct Error<R: Debug, P: Debug + Display> {
127+ /// The resource which cannot be used.
128+ pub resource: R,
129+ /// The permission causing it to be disallowed.
130+ pub permission: P,
131+ }
114132 }
115133
116134 /// Allow, deny or forbid using a resource or performing an action.
@@ -125,6 +143,36 @@ pub enum Permission { · 8 unmodified lines
125143 Allow,
126144 }
127145
146+impl Permission {
147+ /// Check this permissions and produce a reply to indicate if the `resource` can be used and in which way.
148+ ///
149+ /// Only if this permission is set to `Allow` will the resource be usable.
150+ #[cfg(feature = "thiserror")]
151+ pub fn check<R: Debug>(&self, resource: R) -> Result<Option<R>, permission::Error<R, Self>> {
152+ match self {
153+ Permission::Allow => Ok(Some(resource)),
154+ Permission::Deny => Ok(None),
155+ Permission::Forbid => Err(permission::Error {
156+ resource,
157+ permission: *self,
158+ }),
159+ }
160+ }
161+}
162+
163+impl Display for Permission {
164+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165+ Display::fmt(
166+ match self {
167+ Permission::Allow => "allowed",
168+ Permission::Deny => "denied",
169+ Permission::Forbid => "forbidden",
170+ },
171+ f,
172+ )
173+ }
174+}
175+
128176 bitflags::bitflags! {
129177 /// Whether something can be read or written.
130178 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
@@ -136,14 +184,27 @@ bitflags::bitflags! { · 5 unmodified lines
136184 }
137185 }
138186
187+impl Display for ReadWrite {
188+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
189+ Debug::fmt(self, f)
190+ }
191+}
192+
139193 /// A container to define tagged access permissions, rendering the permission read-only.
140−pub struct Access<T: permission::Tag, P> {
194+#[derive(Debug, Clone)]
195+pub struct Access<T: permission::Tag, P: Debug + Display + Clone> {
141196 /// The access permission itself.
142197 permission: P,
143198 _data: PhantomData<T>,
144199 }
145200
146−impl<T: permission::Tag, P> Deref for Access<T, P> {
201+impl<T: permission::Tag, P: Debug + Display + Clone> Display for Access<T, P> {
202+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
203+ Display::fmt(&self.permission, f)
204+ }
205+}
206+
207+impl<T: permission::Tag, P: Debug + Display + Clone> Deref for Access<T, P> {
147208 type Target = P;
148209
149210 fn deref(&self) -> &Self::Target {
git-url/Cargo.toml+1−0
@@ -20,6 +20,7 @@ serde1 = ["serde", "bstr/serde1"] · 19 unmodified lines
2020 [dependencies]
2121 serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]}
2222 git-features = { version = "^0.20.0", path = "../git-features" }
23+git-path = { version = "^0.1.0", path = "../git-path" }
2324 quick-error = "2.0.0"
2425 url = "2.1.1"
2526 bstr = { version = "0.2.13", default-features = false, features = ["std"] }
git-url/src/expand_path.rs+1−1
@@ -106,7 +106,7 @@ pub fn with( · 105 unmodified lines
106106 fn make_relative(path: &Path) -> PathBuf {
107107 path.components().skip(1).collect()
108108 }
109− let path = git_features::path::from_byte_slice(path).map_err(|_| Error::IllformedUtf8 { path: path.to_owned() })?;
109+ let path = git_path::try_from_byte_slice(path).map_err(|_| Error::IllformedUtf8 { path: path.to_owned() })?;
110110 Ok(match user {
111111 Some(user) => home_for_user(user)
112112 .ok_or_else(|| Error::MissingHome(user.to_owned().into()))?
git-worktree/Cargo.toml+3−0
@@ -33,6 +33,9 @@ internal-testing-to-avoid-being-run-by-cargo-test-all = [] · 32 unmodified lines
3333 git-index = { version = "^0.2.0", path = "../git-index" }
3434 git-hash = { version = "^0.9.3", path = "../git-hash" }
3535 git-object = { version = "^0.18.0", path = "../git-object" }
36+git-glob = { version = "^0.2.0", path = "../git-glob" }
37+git-path = { version = "^0.1.0", path = "../git-path" }
38+git-attributes = { version = "^0.1.0", path = "../git-attributes" }
3639 git-features = { version = "^0.20.0", path = "../git-features" }
3740
3841 serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
git-worktree/src/fs/cache.rs+0−185
@@ -1,185 +0,0 @@
1−use super::Cache;
2−use crate::fs::Stack;
3−use crate::{fs, os};
4−use std::path::{Path, PathBuf};
5−
6−#[derive(Clone)]
7−pub enum Mode {
8− /// Useful for checkout where directories need creation, but we need to access attributes as well.
9− CreateDirectoryAndProvideAttributes {
10− /// If there is a symlink or a file in our path, try to unlink it before creating the directory.
11− unlink_on_collision: bool,
12−
13− /// just for testing
14− #[cfg(debug_assertions)]
15− test_mkdir_calls: usize,
16− /// An additional per-user attributes file, similar to `$GIT_DIR/info/attributes`
17− attributes_file: Option<PathBuf>,
18− },
19− /// Used when adding files, requiring access to both attributes and ignore information.
20− ProvideAttributesAndIgnore {
21− /// An additional per-user excludes file, similar to `$GIT_DIR/info/exclude`. It's an error if it is set but can't be read/opened.
22− excludes_file: Option<PathBuf>,
23− /// An additional per-user attributes file, similar to `$GIT_DIR/info/attributes`
24− attributes_file: Option<PathBuf>,
25− },
26−}
27−
28−impl Mode {
29− /// Configure a mode to be suitable for checking out files.
30− pub fn checkout(unlink_on_collision: bool, attributes_file: Option<PathBuf>) -> Self {
31− Mode::CreateDirectoryAndProvideAttributes {
32− unlink_on_collision,
33− #[cfg(debug_assertions)]
34− test_mkdir_calls: 0,
35− attributes_file,
36− }
37− }
38−
39− /// Configure a mode for adding files.
40− pub fn add(excludes_file: Option<PathBuf>, attributes_file: Option<PathBuf>) -> Self {
41− Mode::ProvideAttributesAndIgnore {
42− excludes_file,
43− attributes_file,
44− }
45− }
46−}
47−
48−#[cfg(debug_assertions)]
49−impl Cache {
50− pub fn num_mkdir_calls(&self) -> usize {
51− match self.mode {
52− Mode::CreateDirectoryAndProvideAttributes { test_mkdir_calls, .. } => test_mkdir_calls,
53− _ => 0,
54− }
55− }
56−
57− pub fn reset_mkdir_calls(&mut self) {
58− if let Mode::CreateDirectoryAndProvideAttributes { test_mkdir_calls, .. } = &mut self.mode {
59− *test_mkdir_calls = 0;
60− }
61− }
62−
63− pub fn unlink_on_collision(&mut self, value: bool) {
64− if let Mode::CreateDirectoryAndProvideAttributes {
65− unlink_on_collision, ..
66− } = &mut self.mode
67− {
68− *unlink_on_collision = value;
69− }
70− }
71−}
72−
73−pub struct Platform<'a> {
74− parent: &'a Cache,
75−}
76−
77−impl<'a> Platform<'a> {
78− /// The full path to `relative` will be returned for use on the file system.
79− pub fn leading_dir(&self) -> &'a Path {
80− self.parent.stack.current()
81− }
82−}
83−
84−impl<'a> std::fmt::Debug for Platform<'a> {
85− fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86− std::fmt::Debug::fmt(&self.leading_dir(), f)
87− }
88−}
89−
90−impl Cache {
91− fn assure_init(&mut self) -> std::io::Result<()> {
92− Ok(())
93− }
94−}
95−
96−impl Cache {
97− /// Create a new instance with `worktree_root` being the base for all future paths we handle, assuming it to be valid which includes
98− /// symbolic links to be included in it as well.
99− pub fn new(worktree_root: impl Into<PathBuf>, mode: Mode) -> Self {
100− let root = worktree_root.into();
101− Cache {
102− stack: fs::Stack::new(root),
103− mode,
104− }
105− }
106−
107− /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories
108− /// unless `mode` indicates `relative` points to a directory itself in which case the entire resulting path is created as directory.
109− ///
110− /// Provide access to cached information for that `relative` entry via the platform returned.
111− pub fn at_entry(
112− &mut self,
113− relative: impl AsRef<Path>,
114− mode: git_index::entry::Mode,
115− ) -> std::io::Result<Platform<'_>> {
116− self.assure_init()?;
117− let op_mode = &mut self.mode;
118− self.stack.make_relative_path_current(
119− relative,
120− |components, stack: &fs::Stack| {
121− match op_mode {
122− Mode::CreateDirectoryAndProvideAttributes {
123− #[cfg(debug_assertions)]
124− test_mkdir_calls,
125− unlink_on_collision,
126− attributes_file: _,
127− } => {
128− #[cfg(debug_assertions)]
129− {
130− create_leading_directory(components, stack, mode, test_mkdir_calls, *unlink_on_collision)?
131− }
132− #[cfg(not(debug_assertions))]
133− {
134− create_leading_directory(components, stack, mode, *unlink_on_collision)?
135− }
136− }
137− Mode::ProvideAttributesAndIgnore { .. } => todo!(),
138− }
139− Ok(())
140− },
141− |_| {},
142− )?;
143− Ok(Platform { parent: self })
144− }
145−}
146−
147−fn create_leading_directory(
148− components: &mut std::iter::Peekable<std::path::Components<'_>>,
149− stack: &Stack,
150− mode: git_index::entry::Mode,
151− #[cfg(debug_assertions)] mkdir_calls: &mut usize,
152− unlink_on_collision: bool,
153−) -> std::io::Result<()> {
154− let target_is_dir = mode == git_index::entry::Mode::COMMIT || mode == git_index::entry::Mode::DIR;
155− if !(components.peek().is_some() || target_is_dir) {
156− return Ok(());
157− }
158− #[cfg(debug_assertions)]
159− {
160− *mkdir_calls += 1;
161− }
162− match std::fs::create_dir(stack.current()) {
163− Ok(()) => Ok(()),
164− Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
165− let meta = stack.current().symlink_metadata()?;
166− if meta.is_dir() {
167− Ok(())
168− } else if unlink_on_collision {
169− if meta.file_type().is_symlink() {
170− os::remove_symlink(stack.current())?;
171− } else {
172− std::fs::remove_file(stack.current())?;
173− }
174− #[cfg(debug_assertions)]
175− {
176− *mkdir_calls += 1;
177− }
178− std::fs::create_dir(stack.current())
179− } else {
180− Err(err)
181− }
182− }
183− Err(err) => Err(err),
184− }
185−}
git-worktree/src/fs/cache/mod.rs+139−0
@@ -0,0 +1,139 @@
1+use super::Cache;
2+use crate::fs;
3+use crate::fs::PathOidMapping;
4+use bstr::{BStr, ByteSlice};
5+use git_hash::oid;
6+use std::path::{Path, PathBuf};
7+
8+#[derive(Clone)]
9+pub enum State {
10+ /// Useful for checkout where directories need creation, but we need to access attributes as well.
11+ CreateDirectoryAndAttributesStack {
12+ /// If there is a symlink or a file in our path, try to unlink it before creating the directory.
13+ unlink_on_collision: bool,
14+
15+ /// just for testing
16+ #[cfg(debug_assertions)]
17+ test_mkdir_calls: usize,
18+ /// State to handle attribute information
19+ attributes: state::Attributes,
20+ },
21+ /// Used when adding files, requiring access to both attributes and ignore information, for example during add operations.
22+ AttributesAndIgnoreStack {
23+ /// State to handle attribute information
24+ attributes: state::Attributes,
25+ /// State to handle exclusion information
26+ ignore: state::Ignore,
27+ },
28+ /// Used when providing worktree status information.
29+ IgnoreStack(state::Ignore),
30+}
31+
32+#[cfg(debug_assertions)]
33+impl<'paths> Cache<'paths> {
34+ pub fn set_case(&mut self, case: git_glob::pattern::Case) {
35+ self.case = case;
36+ }
37+ pub fn num_mkdir_calls(&self) -> usize {
38+ match self.state {
39+ State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } => test_mkdir_calls,
40+ _ => 0,
41+ }
42+ }
43+
44+ pub fn reset_mkdir_calls(&mut self) {
45+ if let State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } = &mut self.state {
46+ *test_mkdir_calls = 0;
47+ }
48+ }
49+
50+ pub fn unlink_on_collision(&mut self, value: bool) {
51+ if let State::CreateDirectoryAndAttributesStack {
52+ unlink_on_collision, ..
53+ } = &mut self.state
54+ {
55+ *unlink_on_collision = value;
56+ }
57+ }
58+}
59+
60+#[must_use]
61+pub struct Platform<'a, 'paths> {
62+ parent: &'a Cache<'paths>,
63+ is_dir: Option<bool>,
64+}
65+
66+impl<'paths> Cache<'paths> {
67+ /// Create a new instance with `worktree_root` being the base for all future paths we handle, assuming it to be valid which includes
68+ /// symbolic links to be included in it as well.
69+ /// The `case` configures attribute and exclusion query case sensitivity.
70+ pub fn new(
71+ worktree_root: impl Into<PathBuf>,
72+ state: State,
73+ case: git_glob::pattern::Case,
74+ buf: Vec<u8>,
75+ attribute_files_in_index: Vec<PathOidMapping<'paths>>,
76+ ) -> Self {
77+ let root = worktree_root.into();
78+ Cache {
79+ stack: fs::Stack::new(root),
80+ state,
81+ case,
82+ buf,
83+ attribute_files_in_index,
84+ }
85+ }
86+
87+ /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories
88+ /// unless `is_dir` is known (`Some(…)`) then `relative` points to a directory itself in which case the entire resulting
89+ /// path is created as directory. If it's not known it is assumed to be a file.
90+ ///
91+ /// Provide access to cached information for that `relative` entry via the platform returned.
92+ pub fn at_path<Find, E>(
93+ &mut self,
94+ relative: impl AsRef<Path>,
95+ is_dir: Option<bool>,
96+ find: Find,
97+ ) -> std::io::Result<Platform<'_, 'paths>>
98+ where
99+ Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
100+ E: std::error::Error + Send + Sync + 'static,
101+ {
102+ let mut delegate = platform::StackDelegate {
103+ state: &mut self.state,
104+ buf: &mut self.buf,
105+ is_dir: is_dir.unwrap_or(false),
106+ attribute_files_in_index: &self.attribute_files_in_index,
107+ find,
108+ };
109+ self.stack.make_relative_path_current(relative, &mut delegate)?;
110+ Ok(Platform { parent: self, is_dir })
111+ }
112+
113+ /// **Panics** on illformed UTF8 in `relative`
114+ // TODO: more docs
115+ pub fn at_entry<'r, Find, E>(
116+ &mut self,
117+ relative: impl Into<&'r BStr>,
118+ is_dir: Option<bool>,
119+ find: Find,
120+ ) -> std::io::Result<Platform<'_, 'paths>>
121+ where
122+ Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
123+ E: std::error::Error + Send + Sync + 'static,
124+ {
125+ let relative = relative.into();
126+ let relative_path = git_path::from_bstr(relative);
127+
128+ self.at_path(
129+ relative_path,
130+ is_dir.or_else(|| relative.ends_with_str("/").then(|| true)),
131+ // is_dir,
132+ find,
133+ )
134+ }
135+}
136+
137+mod platform;
138+///
139+pub mod state;
git-worktree/src/fs/cache/platform.rs+165−0
@@ -0,0 +1,165 @@
1+use crate::fs;
2+use crate::fs::cache::{Platform, State};
3+use crate::fs::PathOidMapping;
4+use bstr::ByteSlice;
5+use git_hash::oid;
6+use std::path::Path;
7+
8+impl<'a, 'paths> Platform<'a, 'paths> {
9+ /// The full path to `relative` will be returned for use on the file system.
10+ pub fn path(&self) -> &'a Path {
11+ self.parent.stack.current()
12+ }
13+
14+ /// See if the currently set entry is excluded as per exclude and git-ignore files.
15+ ///
16+ /// # Panics
17+ ///
18+ /// If the cache was configured without exclude patterns.
19+ pub fn is_excluded(&self) -> bool {
20+ self.matching_exclude_pattern()
21+ .map_or(false, |m| m.pattern.is_negative())
22+ }
23+
24+ /// Check all exclude patterns to see if the currently set path matches any of them.
25+ ///
26+ /// Note that this pattern might be negated, and means this path in included.
27+ ///
28+ /// # Panics
29+ ///
30+ /// If the cache was configured without exclude patterns.
31+ pub fn matching_exclude_pattern(&self) -> Option<git_attributes::Match<'_, ()>> {
32+ let ignore = self.parent.state.ignore_or_panic();
33+ let relative_path =
34+ git_path::to_unix_separators_on_windows(git_path::into_bstr(self.parent.stack.current_relative.as_path()));
35+ ignore.matching_exclude_pattern(relative_path.as_bstr(), self.is_dir, self.parent.case)
36+ }
37+}
38+
39+impl<'a, 'paths> std::fmt::Debug for Platform<'a, 'paths> {
40+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41+ std::fmt::Debug::fmt(&self.path(), f)
42+ }
43+}
44+
45+pub struct StackDelegate<'a, 'paths, Find> {
46+ pub state: &'a mut State,
47+ pub buf: &'a mut Vec<u8>,
48+ pub is_dir: bool,
49+ pub attribute_files_in_index: &'a Vec<PathOidMapping<'paths>>,
50+ pub find: Find,
51+}
52+
53+impl<'a, 'paths, Find, E> fs::stack::Delegate for StackDelegate<'a, 'paths, Find>
54+where
55+ Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<git_object::BlobRef<'b>, E>,
56+ E: std::error::Error + Send + Sync + 'static,
57+{
58+ fn push_directory(&mut self, stack: &fs::Stack) -> std::io::Result<()> {
59+ match &mut self.state {
60+ State::CreateDirectoryAndAttributesStack { attributes: _, .. } => {
61+ // TODO: attributes
62+ }
63+ State::AttributesAndIgnoreStack { ignore, attributes: _ } => {
64+ // TODO: attributes
65+ ignore.push_directory(
66+ &stack.root,
67+ &stack.current,
68+ self.buf,
69+ self.attribute_files_in_index,
70+ &mut self.find,
71+ )?
72+ }
73+ State::IgnoreStack(ignore) => ignore.push_directory(
74+ &stack.root,
75+ &stack.current,
76+ self.buf,
77+ self.attribute_files_in_index,
78+ &mut self.find,
79+ )?,
80+ }
81+ Ok(())
82+ }
83+
84+ fn push(&mut self, is_last_component: bool, stack: &fs::Stack) -> std::io::Result<()> {
85+ match &mut self.state {
86+ State::CreateDirectoryAndAttributesStack {
87+ #[cfg(debug_assertions)]
88+ test_mkdir_calls,
89+ unlink_on_collision,
90+ attributes: _,
91+ } => {
92+ #[cfg(debug_assertions)]
93+ {
94+ create_leading_directory(
95+ is_last_component,
96+ stack,
97+ self.is_dir,
98+ test_mkdir_calls,
99+ *unlink_on_collision,
100+ )?
101+ }
102+ #[cfg(not(debug_assertions))]
103+ {
104+ create_leading_directory(is_last_component, stack, self.is_dir, *unlink_on_collision)?
105+ }
106+ }
107+ State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {}
108+ }
109+ Ok(())
110+ }
111+
112+ fn pop_directory(&mut self) {
113+ match &mut self.state {
114+ State::CreateDirectoryAndAttributesStack { attributes: _, .. } => {
115+ // TODO: attributes
116+ }
117+ State::AttributesAndIgnoreStack { attributes: _, ignore } => {
118+ // TODO: attributes
119+ ignore.pop_directory();
120+ }
121+ State::IgnoreStack(ignore) => {
122+ ignore.pop_directory();
123+ }
124+ }
125+ }
126+}
127+
128+fn create_leading_directory(
129+ is_last_component: bool,
130+ stack: &fs::Stack,
131+ is_dir: bool,
132+ #[cfg(debug_assertions)] mkdir_calls: &mut usize,
133+ unlink_on_collision: bool,
134+) -> std::io::Result<()> {
135+ if is_last_component && !is_dir {
136+ return Ok(());
137+ }
138+ #[cfg(debug_assertions)]
139+ {
140+ *mkdir_calls += 1;
141+ }
142+ match std::fs::create_dir(stack.current()) {
143+ Ok(()) => Ok(()),
144+ Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
145+ let meta = stack.current().symlink_metadata()?;
146+ if meta.is_dir() {
147+ Ok(())
148+ } else if unlink_on_collision {
149+ if meta.file_type().is_symlink() {
150+ crate::os::remove_symlink(stack.current())?;
151+ } else {
152+ std::fs::remove_file(stack.current())?;
153+ }
154+ #[cfg(debug_assertions)]
155+ {
156+ *mkdir_calls += 1;
157+ }
158+ std::fs::create_dir(stack.current())
159+ } else {
160+ Err(err)
161+ }
162+ }
163+ Err(err) => Err(err),
164+ }
165+}
git-worktree/src/fs/cache/state.rs+287−0
@@ -0,0 +1,287 @@
1+use crate::fs::cache::State;
2+use crate::fs::PathOidMapping;
3+use bstr::{BStr, BString, ByteSlice};
4+use git_glob::pattern::Case;
5+use git_hash::oid;
6+use std::path::Path;
7+
8+type AttributeMatchGroup = git_attributes::MatchGroup<git_attributes::Attributes>;
9+type IgnoreMatchGroup = git_attributes::MatchGroup<git_attributes::Ignore>;
10+
11+/// State related to attributes associated with files in the repository.
12+#[derive(Default, Clone)]
13+#[allow(unused)]
14+pub struct Attributes {
15+ /// Attribute patterns that match the currently set directory (in the stack).
16+ pub stack: AttributeMatchGroup,
17+ /// Attribute patterns which aren't tied to the repository root, hence are global. They are consulted last.
18+ pub globals: AttributeMatchGroup,
19+}
20+
21+/// State related to the exclusion of files.
22+#[derive(Default, Clone)]
23+#[allow(unused)]
24+pub struct Ignore {
25+ /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to
26+ /// be consulted.
27+ overrides: IgnoreMatchGroup,
28+ /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed.
29+ stack: IgnoreMatchGroup,
30+ /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last.
31+ globals: IgnoreMatchGroup,
32+ /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the
33+ /// currently set directory had a pattern matched. Note that this one could be negated.
34+ /// (index into match groups, index into list of pattern lists, index into pattern list)
35+ matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>,
36+ /// The name of the file to look for in directories.
37+ exclude_file_name_for_directories: BString,
38+ /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine
39+ /// to know if an entire path can be ignored as a parent directory is ignored.
40+ case: Case,
41+}
42+
43+impl Ignore {
44+ /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory
45+ /// ignore files within the repository, defaults to`.gitignore`.
46+ // TODO: more docs
47+ pub fn new(
48+ overrides: IgnoreMatchGroup,
49+ globals: IgnoreMatchGroup,
50+ exclude_file_name_for_directories: Option<&BStr>,
51+ case: Case,
52+ ) -> Self {
53+ Ignore {
54+ case,
55+ overrides,
56+ globals,
57+ stack: Default::default(),
58+ matched_directory_patterns_stack: Vec::with_capacity(6),
59+ exclude_file_name_for_directories: exclude_file_name_for_directories
60+ .map(ToOwned::to_owned)
61+ .unwrap_or_else(|| ".gitignore".into()),
62+ }
63+ }
64+}
65+
66+impl Ignore {
67+ pub(crate) fn pop_directory(&mut self) {
68+ self.matched_directory_patterns_stack.pop().expect("something to pop");
69+ self.stack.patterns.pop().expect("something to pop");
70+ }
71+ /// The match groups from lowest priority to highest.
72+ pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] {
73+ [&self.globals, &self.stack, &self.overrides]
74+ }
75+
76+ pub(crate) fn matching_exclude_pattern(
77+ &self,
78+ relative_path: &BStr,
79+ is_dir: Option<bool>,
80+ case: Case,
81+ ) -> Option<git_attributes::Match<'_, ()>> {
82+ let groups = self.match_groups();
83+ if let Some((source, mapping)) = self
84+ .matched_directory_patterns_stack
85+ .iter()
86+ .rev()
87+ .filter_map(|v| *v)
88+ .map(|(gidx, plidx, pidx)| {
89+ let list = &groups[gidx].patterns[plidx];
90+ (list.source.as_deref(), &list.patterns[pidx])
91+ })
92+ .next()
93+ {
94+ if !mapping.pattern.is_negative() {
95+ return git_attributes::Match {
96+ pattern: &mapping.pattern,
97+ value: &mapping.value,
98+ sequence_number: mapping.sequence_number,
99+ source,
100+ }
101+ .into();
102+ }
103+ }
104+ groups
105+ .iter()
106+ .rev()
107+ .find_map(|group| group.pattern_matching_relative_path(relative_path.as_ref(), is_dir, case))
108+ }
109+
110+ /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded.
111+ /// It returns a triple-index into our data structure from which a match can be reconstructed.
112+ pub(crate) fn matching_exclude_pattern_no_dir(
113+ &self,
114+ relative_path: &BStr,
115+ is_dir: Option<bool>,
116+ case: Case,
117+ ) -> Option<(usize, usize, usize)> {
118+ let groups = self.match_groups();
119+ groups.iter().enumerate().rev().find_map(|(gidx, group)| {
120+ let basename_pos = relative_path.rfind(b"/").map(|p| p + 1);
121+ group
122+ .patterns
123+ .iter()
124+ .enumerate()
125+ .rev()
126+ .find_map(|(plidx, pl)| {
127+ pl.pattern_idx_matching_relative_path(relative_path, basename_pos, is_dir, case)
128+ .map(|idx| (plidx, idx))
129+ })
130+ .map(|(plidx, pidx)| (gidx, plidx, pidx))
131+ })
132+ }
133+
134+ pub(crate) fn push_directory<Find, E>(
135+ &mut self,
136+ root: &Path,
137+ dir: &Path,
138+ buf: &mut Vec<u8>,
139+ attribute_files_in_index: &[PathOidMapping<'_>],
140+ mut find: Find,
141+ ) -> std::io::Result<()>
142+ where
143+ Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<git_object::BlobRef<'b>, E>,
144+ E: std::error::Error + Send + Sync + 'static,
145+ {
146+ let rela_dir = dir.strip_prefix(root).expect("dir in root");
147+ self.matched_directory_patterns_stack
148+ .push(self.matching_exclude_pattern_no_dir(git_path::into_bstr(rela_dir).as_ref(), Some(true), self.case));
149+
150+ let ignore_path_relative = rela_dir.join(".gitignore");
151+ let ignore_path_relative = git_path::to_unix_separators_on_windows(git_path::into_bstr(ignore_path_relative));
152+ let ignore_file_in_index =
153+ attribute_files_in_index.binary_search_by(|t| t.0.cmp(ignore_path_relative.as_ref()));
154+ let follow_symlinks = ignore_file_in_index.is_err();
155+ if !self
156+ .stack
157+ .add_patterns_file(dir.join(".gitignore"), follow_symlinks, Some(root), buf)?
158+ {
159+ match ignore_file_in_index {
160+ Ok(idx) => {
161+ let ignore_blob = find(&attribute_files_in_index[idx].1, buf)
162+ .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
163+ let ignore_path = git_path::from_bstring(ignore_path_relative.into_owned());
164+ self.stack
165+ .add_patterns_buffer(ignore_blob.data, ignore_path, Some(root));
166+ }
167+ Err(_) => {
168+ // Need one stack level per component so push and pop matches.
169+ self.stack.patterns.push(Default::default())
170+ }
171+ }
172+ }
173+ Ok(())
174+ }
175+}
176+
177+impl Attributes {
178+ pub fn new(globals: AttributeMatchGroup) -> Self {
179+ Attributes {
180+ globals,
181+ stack: Default::default(),
182+ }
183+ }
184+}
185+
186+impl From<AttributeMatchGroup> for Attributes {
187+ fn from(group: AttributeMatchGroup) -> Self {
188+ Attributes::new(group)
189+ }
190+}
191+
192+impl State {
193+ /// Configure a state to be suitable for checking out files.
194+ pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self {
195+ State::CreateDirectoryAndAttributesStack {
196+ unlink_on_collision,
197+ #[cfg(debug_assertions)]
198+ test_mkdir_calls: 0,
199+ attributes,
200+ }
201+ }
202+
203+ /// Configure a state for adding files.
204+ pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self {
205+ State::AttributesAndIgnoreStack { attributes, ignore }
206+ }
207+
208+ /// Configure a state for status retrieval.
209+ pub fn for_status(ignore: Ignore) -> Self {
210+ State::IgnoreStack(ignore)
211+ }
212+}
213+
214+impl State {
215+ /// Returns a vec of tuples of relative index paths along with the best usable OID for either ignore, attribute files or both.
216+ ///
217+ /// - ignores entries which aren't blobs
218+ /// - ignores ignore entries which are not skip-worktree
219+ /// - within merges, picks 'our' stage both for ignore and attribute files.
220+ pub fn build_attribute_list<'paths>(
221+ &self,
222+ index: &git_index::State,
223+ paths: &'paths git_index::PathStorage,
224+ case: Case,
225+ ) -> Vec<PathOidMapping<'paths>> {
226+ let a1_backing;
227+ let a2_backing;
228+ let names = match self {
229+ State::IgnoreStack(v) => {
230+ a1_backing = [(v.exclude_file_name_for_directories.as_bytes().as_bstr(), true)];
231+ a1_backing.as_ref()
232+ }
233+ State::AttributesAndIgnoreStack { ignore, .. } => {
234+ a2_backing = [
235+ (ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), true),
236+ (".gitattributes".into(), false),
237+ ];
238+ a2_backing.as_ref()
239+ }
240+ State::CreateDirectoryAndAttributesStack { .. } => {
241+ a1_backing = [(".gitattributes".into(), true)];
242+ a1_backing.as_ref()
243+ }
244+ };
245+
246+ index
247+ .entries()
248+ .iter()
249+ .filter_map(move |entry| {
250+ let path = entry.path_in(paths);
251+
252+ // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then
253+ // there won't be a stage 0.
254+ if entry.mode == git_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) {
255+ let basename = path
256+ .rfind_byte(b'/')
257+ .map(|pos| path[pos + 1..].as_bstr())
258+ .unwrap_or(path);
259+ let is_ignore = names.iter().find_map(|t| {
260+ match case {
261+ Case::Sensitive => basename == t.0,
262+ Case::Fold => basename.eq_ignore_ascii_case(t.0),
263+ }
264+ .then(|| t.1)
265+ })?;
266+ // See https://github.com/git/git/blob/master/dir.c#L912:L912
267+ if is_ignore && !entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) {
268+ return None;
269+ }
270+ Some((path, entry.id))
271+ } else {
272+ None
273+ }
274+ })
275+ .collect()
276+ }
277+
278+ pub(crate) fn ignore_or_panic(&self) -> &Ignore {
279+ match self {
280+ State::IgnoreStack(v) => v,
281+ State::AttributesAndIgnoreStack { ignore, .. } => ignore,
282+ State::CreateDirectoryAndAttributesStack { .. } => {
283+ unreachable!("BUG: must not try to check excludes without it being setup")
284+ }
285+ }
286+ }
287+}
git-worktree/src/fs/mod.rs+15−3
@@ -1,3 +1,4 @@
1+use bstr::BStr;
12 use std::path::PathBuf;
23
34 /// Common knowledge about the worktree that is needed across most interactions with the work tree
@@ -21,6 +22,7 @@ pub struct Capabilities { · 17 unmodified lines
2122 pub symlink: bool,
2223 }
2324
25+#[derive(Clone)]
2426 pub struct Stack {
2527 /// The prefix/root for all paths we handle.
2628 root: PathBuf,
@@ -30,6 +32,8 @@ pub struct Stack { · 3 unmodified lines
3032 current_relative: PathBuf,
3133 /// The amount of path components of 'current' beyond the roots components.
3234 valid_components: usize,
35+ /// If set, we assume the `current` element is a directory to affect calls to `(push|pop)_directory()`.
36+ current_is_directory: bool,
3337 }
3438
3539 /// A cache for efficiently executing operations on directories and files which are encountered in sorted order.
@@ -52,13 +56,21 @@ pub struct Stack { · 16 unmodified lines
5256 /// As directories are created, the cache will be adjusted to reflect the latest seen directory.
5357 ///
5458 /// The caching is only useful if consecutive calls to create a directory are using a sorted list of entries.
55−#[allow(unused)]
56−pub struct Cache {
59+#[derive(Clone)]
60+pub struct Cache<'paths> {
5761 stack: Stack,
5862 /// tells us what to do as we change paths.
59− mode: cache::Mode,
63+ state: cache::State,
64+ /// A buffer used when reading attribute or ignore files or their respective objects from the object database.
65+ buf: Vec<u8>,
66+ /// If case folding should happen when looking up attributes or exclusions.
67+ case: git_glob::pattern::Case,
68+ /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions.
69+ attribute_files_in_index: Vec<PathOidMapping<'paths>>,
6070 }
6171
72+pub(crate) type PathOidMapping<'paths> = (&'paths BStr, git_hash::ObjectId);
73+
6274 ///
6375 pub mod cache;
6476 ///
git-worktree/src/fs/stack.rs+29−5
@@ -15,6 +15,12 @@ impl Stack { · 14 unmodified lines
1515 }
1616 }
1717
18+pub trait Delegate {
19+ fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>;
20+ fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>;
21+ fn pop_directory(&mut self);
22+}
23+
1824 impl Stack {
1925 /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes
2026 /// symbolic links to be included in it as well.
@@ -25,6 +31,7 @@ impl Stack { · 4 unmodified lines
2531 current_relative: PathBuf::with_capacity(128),
2632 valid_components: 0,
2733 root,
34+ current_is_directory: true,
2835 }
2936 }
3037
@@ -32,17 +39,23 @@ impl Stack { · 1 unmodified line
3239 /// along with the stacks state for inspection to perform an operation that produces some data.
3340 ///
3441 /// The full path to `relative` will be returned along with the data returned by push_comp.
42+ /// Note that this only works correctly for the delegate's `push_directory()` and `pop_directory()` methods if
43+ /// `relative` paths are terminal, so point to their designated file or directory.
3544 pub fn make_relative_path_current(
3645 &mut self,
3746 relative: impl AsRef<Path>,
38− mut push_comp: impl FnMut(&mut std::iter::Peekable<std::path::Components<'_>>, &Self) -> std::io::Result<()>,
39− mut pop_comp: impl FnMut(&Self),
47+ delegate: &mut impl Delegate,
4048 ) -> std::io::Result<()> {
4149 let relative = relative.as_ref();
4250 debug_assert!(
4351 relative.is_relative(),
4452 "only index paths are handled correctly here, must be relative"
4553 );
54+ debug_assert!(!relative.to_string_lossy().is_empty(), "empty paths are not allowed");
55+
56+ if self.valid_components == 0 {
57+ delegate.push_directory(self)?;
58+ }
4659
4760 let mut components = relative.components().peekable();
4861 let mut existing_components = self.current_relative.components();
@@ -59,21 +72,32 @@ impl Stack { · 10 unmodified lines
5972 for _ in 0..self.valid_components - matching_components {
6073 self.current.pop();
6174 self.current_relative.pop();
62− pop_comp(&*self);
75+ if self.current_is_directory {
76+ delegate.pop_directory();
77+ }
78+ self.current_is_directory = true;
6379 }
6480 self.valid_components = matching_components;
6581
82+ if !self.current_is_directory && components.peek().is_some() {
83+ delegate.push_directory(self)?;
84+ }
85+
6686 while let Some(comp) = components.next() {
87+ let is_last_component = components.peek().is_none();
88+ self.current_is_directory = !is_last_component;
6789 self.current.push(comp);
6890 self.current_relative.push(comp);
6991 self.valid_components += 1;
70− let res = push_comp(&mut components, &*self);
92+ let res = delegate.push(is_last_component, self);
93+ if self.current_is_directory {
94+ delegate.push_directory(self)?;
95+ }
7196
7297 if let Err(err) = res {
7398 self.current.pop();
7499 self.current_relative.pop();
75100 self.valid_components -= 1;
76− pop_comp(&*self);
77101 return Err(err);
78102 }
79103 }
git-worktree/src/index/checkout.rs+4−4
@@ -1,5 +1,5 @@
11 use bstr::BString;
2−use std::path::PathBuf;
2+use git_attributes::Attributes;
33
44 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55 pub struct Collision {
@@ -57,8 +57,8 @@ pub struct Options { · 51 unmodified lines
5757 ///
5858 /// Default true.
5959 pub check_stat: bool,
60− /// The location of the per-user attributes file. It must exist if it is set, causing failure otherwise.
61− pub attributes_file: Option<PathBuf>,
60+ /// A group of attribute patterns that are applied globally, i.e. aren't rooted within the repository itself.
61+ pub attribute_globals: git_attributes::MatchGroup<Attributes>,
6262 }
6363
6464 impl Default for Options {
@@ -71,7 +71,7 @@ impl Default for Options { · 6 unmodified lines
7171 trust_ctime: true,
7272 check_stat: true,
7373 overwrite_existing: false,
74− attributes_file: None,
74+ attribute_globals: Default::default(),
7575 }
7676 }
7777 }
git-worktree/src/index/entry.rs+9−9
@@ -7,9 +7,9 @@ use io_close::Close; · 6 unmodified lines
77
88 use crate::{fs, index, os};
99
10−pub struct Context<'a, Find> {
10+pub struct Context<'a, 'paths, Find> {
1111 pub find: &'a mut Find,
12− pub path_cache: &'a mut fs::Cache,
12+ pub path_cache: &'a mut fs::Cache<'paths>,
1313 pub buf: &'a mut Vec<u8>,
1414 }
1515
@@ -17,7 +17,7 @@ pub struct Context<'a, Find> { · 1 unmodified line
1717 pub fn checkout<Find, E>(
1818 entry: &mut Entry,
1919 entry_path: &BStr,
20− Context { find, path_cache, buf }: Context<'_, Find>,
20+ Context { find, path_cache, buf }: Context<'_, '_, Find>,
2121 index::checkout::Options {
2222 fs: crate::fs::Capabilities {
2323 symlink,
@@ -33,11 +33,11 @@ where · 9 unmodified lines
3333 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
3434 E: std::error::Error + Send + Sync + 'static,
3535 {
36− let dest_relative =
37− git_features::path::from_byte_slice(entry_path).map_err(|_| index::checkout::Error::IllformedUtf8 {
38− path: entry_path.to_owned(),
39− })?;
40− let dest = path_cache.at_entry(dest_relative, entry.mode)?.leading_dir();
36+ let dest_relative = git_path::try_from_bstr(entry_path).map_err(|_| index::checkout::Error::IllformedUtf8 {
37+ path: entry_path.to_owned(),
38+ })?;
39+ let is_dir = Some(entry.mode == git_index::entry::Mode::COMMIT || entry.mode == git_index::entry::Mode::DIR);
40+ let dest = path_cache.at_path(dest_relative, is_dir, &mut *find)?.path();
4141
4242 let object_size = match entry.mode {
4343 git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => {
@@ -81,7 +81,7 @@ where · 37 unmodified lines
8181 oid: entry.id,
8282 path: dest.to_path_buf(),
8383 })?;
84− let symlink_destination = git_features::path::from_byte_slice(obj.data)
84+ let symlink_destination = git_path::try_from_byte_slice(obj.data)
8585 .map_err(|_| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?;
8686
8787 if symlink {
git-worktree/src/index/mod.rs+46−35
@@ -21,22 +21,36 @@ pub fn checkout<Find, E>( · 20 unmodified lines
2121 should_interrupt: &AtomicBool,
2222 options: checkout::Options,
2323 ) -> Result<checkout::Outcome, checkout::Error<E>>
24+where
25+ Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E> + Send + Clone,
26+ E: std::error::Error + Send + Sync + 'static,
27+{
28+ let paths = index.take_path_backing();
29+ let res = checkout_inner(index, &paths, dir, find, files, bytes, should_interrupt, options);
30+ index.return_path_backing(paths);
31+ res
32+}
33+#[allow(clippy::too_many_arguments)]
34+fn checkout_inner<Find, E>(
35+ index: &mut git_index::State,
36+ paths: &git_index::PathStorage,
37+ dir: impl Into<std::path::PathBuf>,
38+ find: Find,
39+ files: &mut impl Progress,
40+ bytes: &mut impl Progress,
41+ should_interrupt: &AtomicBool,
42+ options: checkout::Options,
43+) -> Result<checkout::Outcome, checkout::Error<E>>
2444 where
2545 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E> + Send + Clone,
2646 E: std::error::Error + Send + Sync + 'static,
2747 {
2848 let num_files = AtomicUsize::default();
2949 let dir = dir.into();
30−
31− let mut ctx = chunk::Context {
32− buf: Vec::new(),
33− path_cache: fs::Cache::new(
34− dir.clone(),
35− fs::cache::Mode::checkout(options.overwrite_existing, options.attributes_file.clone()),
36− ),
37− find: find.clone(),
38− options: options.clone(),
39− num_files: &num_files,
50+ let case = if options.fs.ignore_case {
51+ git_glob::pattern::Case::Fold
52+ } else {
53+ git_glob::pattern::Case::Sensitive
4054 };
4155 let (chunk_size, thread_limit, num_threads) = git_features::parallel::optimize_chunk_size_and_thread_limit(
4256 100,
@@ -45,15 +59,26 @@ where · 2 unmodified lines
4559 None,
4660 );
4761
48− let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths(), should_interrupt);
62+ let state = fs::cache::State::for_checkout(options.overwrite_existing, options.attribute_globals.clone().into());
63+ let attribute_files = state.build_attribute_list(index, paths, case);
64+ let mut ctx = chunk::Context {
65+ buf: Vec::new(),
66+ path_cache: fs::Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files),
67+ find,
68+ options,
69+ num_files: &num_files,
70+ };
71+
4972 let chunk::Outcome {
5073 mut collisions,
5174 mut errors,
5275 mut bytes_written,
5376 delayed,
5477 } = if num_threads == 1 {
78+ let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
5579 chunk::process(entries_with_paths, files, bytes, &mut ctx)?
5680 } else {
81+ let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
5782 in_parallel(
5883 git_features::iter::Chunks {
5984 inner: entries_with_paths,
@@ -61,23 +86,8 @@ where · 1 unmodified line
6186 },
6287 thread_limit,
6388 {
64− let num_files = &num_files;
65− move |_| {
66− (
67− progress::Discard,
68− progress::Discard,
69− chunk::Context {
70− find: find.clone(),
71− path_cache: fs::Cache::new(
72− dir.clone(),
73− fs::cache::Mode::checkout(options.overwrite_existing, options.attributes_file.clone()),
74− ),
75− buf: Vec::new(),
76− options: options.clone(),
77− num_files,
78− },
79− )
80− }
89+ let ctx = ctx.clone();
90+ move |_| (progress::Discard, progress::Discard, ctx.clone())
8191 },
8292 |chunk, (files, bytes, ctx)| chunk::process(chunk.into_iter(), files, bytes, ctx),
8393 chunk::Reduce {
@@ -103,7 +113,7 @@ where · 19 unmodified lines
103113 }
104114
105115 Ok(checkout::Outcome {
106− files_updated: ctx.num_files.load(Ordering::Relaxed),
116+ files_updated: num_files.load(Ordering::Relaxed),
107117 collisions,
108118 errors,
109119 bytes_written,
@@ -186,9 +196,10 @@ mod chunk { · 76 unmodified lines
186196 pub bytes_written: u64,
187197 }
188198
189− pub struct Context<'a, Find> {
199+ #[derive(Clone)]
200+ pub struct Context<'a, 'paths, Find: Clone> {
190201 pub find: Find,
191− pub path_cache: fs::Cache,
202+ pub path_cache: fs::Cache<'paths>,
192203 pub buf: Vec<u8>,
193204 pub options: checkout::Options,
194205 /// We keep these shared so that there is the chance for printing numbers that aren't looking like
@@ -200,10 +211,10 @@ mod chunk { · 5 unmodified lines
200211 entries_with_paths: impl Iterator<Item = (&'entry mut git_index::Entry, &'entry BStr)>,
201212 files: &mut impl Progress,
202213 bytes: &mut impl Progress,
203− ctx: &mut Context<'_, Find>,
214+ ctx: &mut Context<'_, '_, Find>,
204215 ) -> Result<Outcome<'entry>, checkout::Error<E>>
205216 where
206− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
217+ Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E> + Clone,
207218 E: std::error::Error + Send + Sync + 'static,
208219 {
209220 let mut delayed = Vec::new();
@@ -255,10 +266,10 @@ mod chunk { · 45 unmodified lines
255266 buf,
256267 options,
257268 num_files,
258− }: &mut Context<'_, Find>,
269+ }: &mut Context<'_, '_, Find>,
259270 ) -> Result<usize, checkout::Error<E>>
260271 where
261− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
272+ Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E> + Clone,
262273 E: std::error::Error + Send + Sync + 'static,
263274 {
264275 let res = entry::checkout(
git-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz+3−0
@@ -0,0 +1,3 @@
1+version https://git-lfs.github.com/spec/v1
2+oid sha256:ae297f2c067e2cc7b0c8eabfff721ff775a7d8266ffef979bde2458de2ab03c9
3+size 10944
git-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh+112−0
@@ -0,0 +1,112 @@
1+#!/bin/bash
2+set -eu -o pipefail
3+
4+cat <<EOF >user.exclude
5+# a custom exclude configured per user
6+user-file-anywhere
7+/user-file-from-top
8+
9+user-dir-anywhere/
10+/user-dir-from-top
11+
12+user-subdir/file
13+**/user-subdir-anywhere/file
14+EOF
15+
16+mkdir repo;
17+(cd repo
18+ git init -q
19+ git config core.excludesFile ../user.exclude
20+
21+ cat <<EOF >.git/info/exclude
22+# a sample .git/info/exclude
23+file-anywhere
24+/file-from-top
25+
26+dir-anywhere/
27+/dir-from-top
28+
29+subdir/file
30+**/subdir-anywhere/file
31+EOF
32+
33+ cat <<EOF >.gitignore
34+# a sample .gitignore
35+top-level-local-file-anywhere
36+EOF
37+
38+ mkdir dir-with-ignore
39+ cat <<EOF >dir-with-ignore/.gitignore
40+# a sample .gitignore
41+sub-level-local-file-anywhere
42+sub-level-dir-anywhere/
43+!/negated
44+/negated-dir/
45+!/negated-dir/
46+EOF
47+
48+ git add .gitignore dir-with-ignore
49+ git commit --allow-empty -m "init"
50+
51+ # just add this git-ignore file, so it's a new file that doesn't exist on disk.
52+ mkdir other-dir-with-ignore
53+ skip_worktree_ignore=other-dir-with-ignore/.gitignore
54+ cat <<EOF >"$skip_worktree_ignore"
55+# a sample .gitignore
56+other-sub-level-local-file-anywhere
57+other-sub-level-dir-anywhere/
58+EOF
59+ git add $skip_worktree_ignore && git update-index --skip-worktree $skip_worktree_ignore && rm $skip_worktree_ignore
60+
61+ mkdir user-dir-anywhere user-dir-from-top dir-anywhere dir-from-top
62+ mkdir -p dir/user-dir-anywhere dir/dir-anywhere
63+
64+ git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || :
65+dir-with-ignore/sub-level-dir-anywhere/
66+dir-with-ignore/foo/sub-level-dir-anywhere/
67+dir-with-ignore/sub-level-dir-anywhere
68+user-file-anywhere
69+dir/user-file-anywhere
70+user-file-from-top
71+no-match/user-file-from-top
72+user-dir-anywhere
73+user-dir-from-top
74+no-match/user-dir-from-top
75+user-subdir/file
76+subdir/user-subdir-anywhere/file
77+user-dir-anywhere/hello
78+dir/user-dir-anywhere/hello
79+file-anywhere
80+dir/file-anywhere
81+file-from-top
82+no-match/file-from-top
83+dir-anywhere
84+dir/dir-anywhere
85+dir-from-top
86+no-match/dir-from-top
87+subdir/file
88+subdir/subdir-anywhere/file
89+top-level-local-file-anywhere
90+dir/top-level-local-file-anywhere
91+no-match/sub-level-local-file-anywhere
92+dir-with-ignore/sub-level-local-file-anywhere
93+dir-with-ignore/sub-dir/sub-level-local-file-anywhere
94+other-dir-with-ignore/other-sub-level-local-file-anywhere
95+other-dir-with-ignore/sub-level-local-file-anywhere
96+other-dir-with-ignore/sub-dir/other-sub-level-local-file-anywhere
97+other-dir-with-ignore/no-match/sub-level-local-file-anywhere
98+non-existing/dir-anywhere
99+dir-anywhere/hello
100+dir/dir-anywhere/hello
101+no-match/sub-level-dir-anywhere/hello
102+no-match/other-sub-level-dir-anywhere/hello
103+dir-with-ignore/sub-level-dir-anywhere/hello
104+dir-with-ignore/sub-level-dir-anywhere/
105+other-dir-with-ignore/sub-level-dir-anywhere/hello
106+other-dir-with-ignore/other-sub-level-dir-anywhere/hello
107+other-dir-with-ignore/other-sub-level-dir-anywhere/
108+dir-with-ignore/negated
109+dir-with-ignore/negated-dir/hello
110+EOF
111+
112+)
git-worktree/tests/worktree-multi-threaded.rs+2−0
@@ -1,2 +1,4 @@
1+extern crate core;
2+
13 mod worktree;
24 use worktree::*;
git-worktree/tests/worktree/fs/cache.rs+148−29
@@ -1,39 +1,49 @@
11 mod create_directory {
22 use std::path::Path;
33
4− use git_index::entry::Mode;
54 use git_worktree::fs;
65 use tempfile::{tempdir, TempDir};
76
7+ fn panic_on_find<'buf>(
8+ _oid: &git_hash::oid,
9+ _buf: &'buf mut Vec<u8>,
10+ ) -> std::io::Result<git_object::BlobRef<'buf>> {
11+ unreachable!("find should nto be called")
12+ }
13+
814 #[test]
9− fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() {
10− let dir = tempdir().unwrap();
15+ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate::Result {
16+ let dir = tempdir()?;
1117 let mut cache = fs::Cache::new(
1218 dir.path().join("non-existing-root"),
13− fs::cache::Mode::checkout(false, None),
19+ fs::cache::State::for_checkout(false, Default::default()),
20+ Default::default(),
21+ Vec::new(),
22+ Default::default(),
1423 );
1524 assert_eq!(cache.num_mkdir_calls(), 0);
1625
17− let path = cache.at_entry("hello", Mode::FILE).unwrap().leading_dir();
26+ let path = cache.at_path("hello", Some(false), panic_on_find)?.path();
1827 assert!(!path.parent().unwrap().exists(), "prefix itself is never created");
1928 assert_eq!(cache.num_mkdir_calls(), 0);
29+ Ok(())
2030 }
2131
2232 #[test]
2333 fn directory_paths_are_created_in_full() {
2434 let (mut cache, _tmp) = new_cache();
2535
26− for (name, mode) in &[
27− ("dir", Mode::DIR),
28− ("submodule", Mode::COMMIT),
29− ("file", Mode::FILE),
30− ("exe", Mode::FILE_EXECUTABLE),
31− ("link", Mode::SYMLINK),
36+ for (name, is_dir) in &[
37+ ("dir", Some(true)),
38+ ("submodule", Some(true)),
39+ ("file", Some(false)),
40+ ("exe", Some(false)),
41+ ("link", None),
3242 ] {
3343 let path = cache
34− .at_entry(Path::new("dir").join(name), *mode)
44+ .at_path(Path::new("dir").join(name), *is_dir, panic_on_find)
3545 .unwrap()
36− .leading_dir();
46+ .path();
3747 assert!(path.parent().unwrap().is_dir(), "dir exists");
3848 }
3949
@@ -41,29 +51,33 @@ mod create_directory { · 1 unmodified line
4151 }
4252
4353 #[test]
44− fn existing_directories_are_fine() {
54+ fn existing_directories_are_fine() -> crate::Result {
4555 let (mut cache, tmp) = new_cache();
46− std::fs::create_dir(tmp.path().join("dir")).unwrap();
56+ std::fs::create_dir(tmp.path().join("dir"))?;
4757
48− let path = cache.at_entry("dir/file", Mode::FILE).unwrap().leading_dir();
58+ let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path();
4959 assert!(path.parent().unwrap().is_dir(), "directory is still present");
5060 assert!(!path.exists(), "it won't create the file");
5161 assert_eq!(cache.num_mkdir_calls(), 1);
62+ Ok(())
5263 }
5364
5465 #[test]
55− fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() {
66+ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::Result {
5667 let (mut cache, tmp) = new_cache();
5768 let forbidden = tmp.path().join("forbidden");
58− std::fs::create_dir(&forbidden).unwrap();
59− symlink::symlink_dir(&forbidden, tmp.path().join("link-to-dir")).unwrap();
60− std::fs::write(tmp.path().join("file-in-dir"), &[]).unwrap();
69+ std::fs::create_dir(&forbidden)?;
70+ symlink::symlink_dir(&forbidden, tmp.path().join("link-to-dir"))?;
71+ std::fs::write(tmp.path().join("file-in-dir"), &[])?;
6172
6273 for dirname in &["file-in-dir", "link-to-dir"] {
6374 cache.unlink_on_collision(false);
6475 let relative_path = format!("{}/file", dirname);
6576 assert_eq!(
66− cache.at_entry(&relative_path, Mode::FILE).unwrap_err().kind(),
77+ cache
78+ .at_path(&relative_path, Some(false), panic_on_find)
79+ .unwrap_err()
80+ .kind(),
6781 std::io::ErrorKind::AlreadyExists
6882 );
6983 }
@@ -76,7 +90,7 @@ mod create_directory { · 6 unmodified lines
7690 for dirname in &["link-to-dir", "file-in-dir"] {
7791 cache.unlink_on_collision(true);
7892 let relative_path = format!("{}/file", dirname);
79− let path = cache.at_entry(&relative_path, Mode::FILE).unwrap().leading_dir();
93+ let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path();
8094 assert!(path.parent().unwrap().is_dir(), "directory was forcefully created");
8195 assert!(!path.exists());
8296 }
@@ -85,26 +99,131 @@ mod create_directory { · 2 unmodified lines
8599 4,
86100 "like before, but it unlinks what's there and tries again"
87101 );
102+ Ok(())
88103 }
89104
90− fn new_cache() -> (fs::Cache, TempDir) {
105+ fn new_cache() -> (fs::Cache<'static>, TempDir) {
91106 let dir = tempdir().unwrap();
92− let cache = fs::Cache::new(dir.path(), fs::cache::Mode::checkout(false, None));
107+ let cache = fs::Cache::new(
108+ dir.path(),
109+ fs::cache::State::for_checkout(false, Default::default()),
110+ Default::default(),
111+ Vec::new(),
112+ Default::default(),
113+ );
93114 (cache, dir)
94115 }
95116 }
96117
97118 #[allow(unused)]
98−mod ignore_only {
119+mod ignore_and_attributes {
120+ use bstr::{BStr, ByteSlice};
99121 use std::path::Path;
100122
123+ use git_glob::pattern::Case;
101124 use git_index::entry::Mode;
125+ use git_odb::pack::bundle::write::Options;
126+ use git_odb::FindExt;
127+ use git_testtools::hex_to_id;
102128 use git_worktree::fs;
103129 use tempfile::{tempdir, TempDir};
104130
105− fn new_cache() -> fs::Cache {
106− let dir = git_testtools::scripted_fixture_repo_read_only("make_ignore_setup.sh").unwrap();
107− let cache = fs::Cache::new(dir, todo!()); // TODO: also test initialization
108− cache
131+ struct IgnoreExpectations<'a> {
132+ lines: bstr::Lines<'a>,
133+ }
134+
135+ impl<'a> Iterator for IgnoreExpectations<'a> {
136+ type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>);
137+
138+ fn next(&mut self) -> Option<Self::Item> {
139+ let line = self.lines.next()?;
140+ let (left, value) = line.split_at(line.find_byte(b'\t').unwrap());
141+ let value = value[1..].as_bstr();
142+
143+ let source_and_line = if left == b"::" {
144+ None
145+ } else {
146+ let mut tokens = left.split(|b| *b == b':');
147+ let source = tokens.next().unwrap().as_bstr();
148+ let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap();
149+ let pattern = tokens.next().unwrap().as_bstr();
150+ Some((source, line_number, pattern))
151+ };
152+ Some((value, source_and_line))
153+ }
154+ }
155+
156+ #[test]
157+ fn check_against_baseline() -> crate::Result {
158+ let dir = git_testtools::scripted_fixture_repo_read_only("make_ignore_and_attributes_setup.sh")?;
159+ let worktree_dir = dir.join("repo");
160+ let git_dir = worktree_dir.join(".git");
161+ let mut buf = Vec::new();
162+ let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?;
163+ let user_exclude_path = dir.join("user.exclude");
164+ assert!(user_exclude_path.is_file());
165+
166+ let mut index = git_index::File::at(git_dir.join("index"), Default::default())?;
167+ let odb = git_odb::at(git_dir.join("objects"))?;
168+ let case = git_glob::pattern::Case::Sensitive;
169+ let state = git_worktree::fs::cache::State::for_add(
170+ Default::default(), // TODO: attribute tests
171+ git_worktree::fs::cache::state::Ignore::new(
172+ git_attributes::MatchGroup::from_overrides(vec!["!force-include"]),
173+ git_attributes::MatchGroup::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?,
174+ None,
175+ case,
176+ ),
177+ );
178+ let paths_storage = index.take_path_backing();
179+ let attribute_files_in_index = state.build_attribute_list(&index.state, &paths_storage, case);
180+ assert_eq!(
181+ attribute_files_in_index,
182+ vec![(
183+ "other-dir-with-ignore/.gitignore".as_bytes().as_bstr(),
184+ hex_to_id("5c7e0ed672d3d31d83a3df61f13cc8f7b22d5bfd")
185+ )]
186+ );
187+ let mut cache = fs::Cache::new(&worktree_dir, state, case, buf, attribute_files_in_index);
188+
189+ for (relative_entry, source_and_line) in (IgnoreExpectations {
190+ lines: baseline.lines(),
191+ }) {
192+ let relative_path = git_path::from_byte_slice(relative_entry);
193+ let is_dir = worktree_dir.join(&relative_path).metadata().ok().map(|m| m.is_dir());
194+
195+ let platform = cache.at_entry(relative_entry, is_dir, |oid, buf| odb.find_blob(oid, buf))?;
196+
197+ let match_ = platform.matching_exclude_pattern();
198+ let is_excluded = platform.is_excluded();
199+ match (match_, source_and_line) {
200+ (None, None) => {
201+ assert!(!is_excluded);
202+ }
203+ (Some(m), Some((source_file, line, pattern))) => {
204+ assert_eq!(m.pattern.to_string(), pattern);
205+ assert_eq!(m.sequence_number, line);
206+ // Paths read from the index are relative to the repo, and they don't exist locally due tot skip-worktree
207+ if m.source.map_or(false, |p| p.exists()) {
208+ assert_eq!(
209+ m.source.map(|p| p.canonicalize().unwrap()),
210+ Some(worktree_dir.join(source_file.to_str_lossy().as_ref()).canonicalize()?)
211+ );
212+ }
213+ }
214+ (actual, expected) => {
215+ panic!(
216+ "actual {:?} didn't match {:?} at '{}'",
217+ actual, expected, relative_entry
218+ );
219+ }
220+ }
221+ }
222+
223+ cache.set_case(Case::Fold);
224+ let platform = cache.at_entry("User-file-ANYWHERE", Some(false), |oid, buf| odb.find_blob(oid, buf))?;
225+ let m = platform.matching_exclude_pattern().expect("match");
226+ assert_eq!(m.pattern.text, "user-file-anywhere");
227+ Ok(())
109228 }
110229 }
git-worktree/tests/worktree/fs/mod.rs+1−0
@@ -19,3 +19,4 @@ fn from_probing_cwd() { · 18 unmodified lines
1919 }
2020
2121 mod cache;
22+mod stack;
git-worktree/tests/worktree/fs/stack/mod.rs+144−0
@@ -0,0 +1,144 @@
1+use git_worktree::fs::Stack;
2+use std::path::{Path, PathBuf};
3+
4+#[derive(Debug, Default, Eq, PartialEq)]
5+struct Record {
6+ push_dir: usize,
7+ dirs: Vec<PathBuf>,
8+ push: usize,
9+}
10+
11+impl git_worktree::fs::stack::Delegate for Record {
12+ fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> {
13+ self.push_dir += 1;
14+ self.dirs.push(stack.current().into());
15+ Ok(())
16+ }
17+
18+ fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> {
19+ self.push += 1;
20+ Ok(())
21+ }
22+
23+ fn pop_directory(&mut self) {
24+ self.dirs.pop();
25+ }
26+}
27+
28+#[test]
29+fn delegate_calls_are_consistent() -> crate::Result {
30+ let root = PathBuf::from(".");
31+ let mut s = Stack::new(&root);
32+
33+ assert_eq!(s.current(), root);
34+ assert_eq!(s.current_relative(), Path::new(""));
35+
36+ let mut r = Record::default();
37+ s.make_relative_path_current("a/b", &mut r)?;
38+ let mut dirs = vec![root.clone(), root.join("a")];
39+ assert_eq!(
40+ r,
41+ Record {
42+ push_dir: 2,
43+ dirs: dirs.clone(),
44+ push: 2,
45+ }
46+ );
47+
48+ s.make_relative_path_current("a/b2", &mut r)?;
49+ assert_eq!(
50+ r,
51+ Record {
52+ push_dir: 2,
53+ dirs: dirs.clone(),
54+ push: 3,
55+ }
56+ );
57+
58+ s.make_relative_path_current("c/d/e", &mut r)?;
59+ dirs.pop();
60+ dirs.extend([root.join("c"), root.join("c").join("d")]);
61+ assert_eq!(
62+ r,
63+ Record {
64+ push_dir: 4,
65+ dirs: dirs.clone(),
66+ push: 6,
67+ }
68+ );
69+
70+ dirs.push(root.join("c").join("d").join("x"));
71+ s.make_relative_path_current("c/d/x/z", &mut r)?;
72+ assert_eq!(
73+ r,
74+ Record {
75+ push_dir: 5,
76+ dirs: dirs.clone(),
77+ push: 8,
78+ }
79+ );
80+
81+ dirs.drain(dirs.len() - 3..).count();
82+ s.make_relative_path_current("f", &mut r)?;
83+ assert_eq!(s.current_relative(), Path::new("f"));
84+ assert_eq!(
85+ r,
86+ Record {
87+ push_dir: 5,
88+ dirs: dirs.clone(),
89+ push: 9,
90+ }
91+ );
92+
93+ dirs.push(root.join("x"));
94+ s.make_relative_path_current("x/z", &mut r)?;
95+ assert_eq!(
96+ r,
97+ Record {
98+ push_dir: 6,
99+ dirs: dirs.clone(),
100+ push: 11,
101+ }
102+ );
103+
104+ dirs.push(root.join("x").join("z"));
105+ s.make_relative_path_current("x/z/a", &mut r)?;
106+ assert_eq!(
107+ r,
108+ Record {
109+ push_dir: 7,
110+ dirs: dirs.clone(),
111+ push: 12,
112+ }
113+ );
114+
115+ dirs.push(root.join("x").join("z").join("a"));
116+ dirs.push(root.join("x").join("z").join("a").join("b"));
117+ s.make_relative_path_current("x/z/a/b/c", &mut r)?;
118+ assert_eq!(
119+ r,
120+ Record {
121+ push_dir: 9,
122+ dirs: dirs.clone(),
123+ push: 14,
124+ }
125+ );
126+
127+ dirs.drain(dirs.len() - 2..).count();
128+ s.make_relative_path_current("x/z", &mut r)?;
129+ assert_eq!(
130+ r,
131+ Record {
132+ push_dir: 9,
133+ dirs: dirs.clone(),
134+ push: 14,
135+ }
136+ );
137+ assert_eq!(
138+ dirs.last(),
139+ Some(&PathBuf::from("./x/z")),
140+ "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though."
141+ );
142+
143+ Ok(())
144+}
gitoxide-core/src/organize.rs+1−1
@@ -257,7 +257,7 @@ where · 256 unmodified lines
257257 progress.fail(format!(
258258 "Error when handling directory {:?}: {}",
259259 path_to_move.display(),
260− err.to_string()
260+ err
261261 ));
262262 num_errors += 1;
263263 }
gitoxide-core/src/pack/receive.rs+2−1
@@ -4,6 +4,7 @@ use std::{ · 3 unmodified lines
44 sync::{atomic::AtomicBool, Arc},
55 };
66
7+pub use git_repository as git;
78 use git_repository::{
89 hash::ObjectId,
910 objs::bstr::{BString, ByteSlice},
@@ -304,7 +305,7 @@ fn print(out: &mut impl io::Write, res: pack::bundle::write::Outcome, refs: &[Re · 294 unmodified lines
304305 fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> {
305306 let assure_dir_exists = |path: &BString| {
306307 assert!(!path.starts_with_str("/"), "no ref start with a /, they are relative");
307− let path = directory.join(git_features::path::from_byte_slice_or_panic_on_windows(path));
308+ let path = directory.join(git::path::from_byte_slice(path));
308309 std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path)
309310 };
310311 for r in refs {
gitoxide-core/src/repository/commit.rs+1−4
@@ -1,10 +1,8 @@
1−use std::path::PathBuf;
2−
31 use anyhow::{Context, Result};
42 use git_repository as git;
53
64 pub fn describe(
7− repo: impl Into<PathBuf>,
5+ repo: git::Repository,
86 rev_spec: Option<&str>,
97 mut out: impl std::io::Write,
108 mut err: impl std::io::Write,
@@ -18,7 +16,6 @@ pub fn describe( · 7 unmodified lines
1816 long_format,
1917 }: describe::Options,
2018 ) -> Result<()> {
21− let repo = git::open(repo)?.apply_environment();
2219 let commit = match rev_spec {
2320 Some(spec) => repo.rev_parse(spec)?.object()?.try_into_commit()?,
2421 None => repo.head_commit()?,
gitoxide-core/src/repository/exclude.rs+68−0
@@ -0,0 +1,68 @@
1+use anyhow::{bail, Context};
2+use std::io;
3+
4+use crate::OutputFormat;
5+use git_repository as git;
6+use git_repository::prelude::FindExt;
7+
8+pub mod query {
9+ use crate::OutputFormat;
10+ use std::ffi::OsString;
11+
12+ pub struct Options {
13+ pub format: OutputFormat,
14+ pub overrides: Vec<OsString>,
15+ pub show_ignore_patterns: bool,
16+ }
17+}
18+
19+pub fn query(
20+ repo: git::Repository,
21+ pathspecs: impl Iterator<Item = git::path::Spec>,
22+ mut out: impl io::Write,
23+ query::Options {
24+ overrides,
25+ format,
26+ show_ignore_patterns,
27+ }: query::Options,
28+) -> anyhow::Result<()> {
29+ if format != OutputFormat::Human {
30+ bail!("JSON output isn't implemented yet");
31+ }
32+
33+ let worktree = repo
34+ .worktree()
35+ .current()
36+ .with_context(|| "Cannot check excludes without a current worktree")?;
37+ let index = worktree.open_index()?;
38+ let mut cache = worktree.excludes(
39+ &index.state,
40+ Some(git::attrs::MatchGroup::<git::attrs::Ignore>::from_overrides(overrides)),
41+ )?;
42+
43+ let prefix = repo.prefix().expect("worktree - we have an index by now")?;
44+
45+ for mut spec in pathspecs {
46+ for path in spec.apply_prefix(&prefix).items() {
47+ // TODO: what about paths that end in /? Pathspec might handle it, it's definitely something git considers
48+ // even if the directory doesn't exist. Seems to work as long as these are kept in the spec.
49+ let is_dir = git::path::from_bstr(path).metadata().ok().map(|m| m.is_dir());
50+ let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?;
51+ let match_ = entry
52+ .matching_exclude_pattern()
53+ .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then(|| m));
54+ match match_ {
55+ Some(m) => writeln!(
56+ out,
57+ "{}:{}:{}\t{}",
58+ m.source.map(|p| p.to_string_lossy()).unwrap_or_default(),
59+ m.sequence_number,
60+ m.pattern,
61+ path
62+ )?,
63+ None => writeln!(out, "::\t{}", path)?,
64+ }
65+ }
66+ }
67+ Ok(())
68+}
gitoxide-core/src/repository/mailmap.rs+2−3
@@ -1,4 +1,4 @@
1−use std::{io, path::PathBuf};
1+use std::io;
22
33 use git_repository as git;
44 #[cfg(feature = "serde1")]
@@ -29,7 +29,7 @@ impl<'a> From<Entry<'a>> for JsonEntry { · 24 unmodified lines
2929 }
3030
3131 pub fn entries(
32− repository: PathBuf,
32+ repo: git::Repository,
3333 format: OutputFormat,
3434 #[cfg_attr(not(feature = "serde1"), allow(unused_variables))] out: impl io::Write,
3535 mut err: impl io::Write,
@@ -38,7 +38,6 @@ pub fn entries( · 2 unmodified lines
3838 writeln!(err, "Defaulting to JSON as human format isn't implemented").ok();
3939 }
4040
41− let repo = git::open(repository)?.apply_environment();
4241 let mut mailmap = git::mailmap::Snapshot::default();
4342 if let Err(e) = repo.load_mailmap_into(&mut mailmap) {
4443 writeln!(err, "Error while loading mailmap, the first error is: {}", e).ok();
gitoxide-core/src/repository/mod.rs+2−0
@@ -16,3 +16,5 @@ pub mod verify; · 15 unmodified lines
1616 pub mod odb;
1717
1818 pub mod mailmap;
19+
20+pub mod exclude;
gitoxide-core/src/repository/odb.rs+3−6
@@ -1,4 +1,4 @@
1−use std::{io, path::PathBuf};
1+use std::io;
22
33 use anyhow::bail;
44 use git_repository as git;
@@ -22,7 +22,7 @@ mod info { · 17 unmodified lines
2222
2323 #[cfg_attr(not(feature = "serde1"), allow(unused_variables))]
2424 pub fn info(
25− repository: PathBuf,
25+ repo: git::Repository,
2626 format: OutputFormat,
2727 out: impl io::Write,
2828 mut err: impl io::Write,
@@ -31,7 +31,6 @@ pub fn info( · 2 unmodified lines
3131 writeln!(err, "Only JSON is implemented - using that instead")?;
3232 }
3333
34− let repo = git::open(repository)?.apply_environment();
3534 let store = repo.objects.store_ref();
3635 let stats = info::Statistics {
3736 path: store.path().into(),
@@ -49,13 +48,11 @@ pub fn info( · 11 unmodified lines
4948 Ok(())
5049 }
5150
52−pub fn entries(repository: PathBuf, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> {
51+pub fn entries(repo: git::Repository, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> {
5352 if format != OutputFormat::Human {
5453 bail!("Only human output format is supported at the moment");
5554 }
5655
57− let repo = git::open(repository)?.apply_environment();
58−
5956 for object in repo.objects.iter()? {
6057 let object = object?;
6158 writeln!(out, "{}", object)?;
gitoxide-core/src/repository/tree.rs+3−5
@@ -1,4 +1,4 @@
1−use std::{borrow::Cow, io, path::PathBuf};
1+use std::{borrow::Cow, io};
22
33 use anyhow::bail;
44 use git_repository as git;
@@ -117,7 +117,7 @@ mod entries { · 112 unmodified lines
117117
118118 #[cfg_attr(not(feature = "serde1"), allow(unused_variables))]
119119 pub fn info(
120− repository: PathBuf,
120+ repo: git::Repository,
121121 treeish: Option<&str>,
122122 extended: bool,
123123 format: OutputFormat,
@@ -128,7 +128,6 @@ pub fn info( · 4 unmodified lines
128128 writeln!(err, "Only JSON is implemented - using that instead")?;
129129 }
130130
131− let repo = git::open(repository)?.apply_environment();
132131 let tree = treeish_to_tree(treeish, &repo)?;
133132
134133 let mut delegate = entries::Traverse::new(extended.then(|| &repo), None);
@@ -144,7 +143,7 @@ pub fn info( · 9 unmodified lines
144143 }
145144
146145 pub fn entries(
147− repository: PathBuf,
146+ repo: git::Repository,
148147 treeish: Option<&str>,
149148 recursive: bool,
150149 extended: bool,
@@ -155,7 +154,6 @@ pub fn entries( · 4 unmodified lines
155154 bail!("Only human output format is supported at the moment");
156155 }
157156
158− let repo = git::open(repository)?.apply_environment();
159157 let tree = treeish_to_tree(treeish, &repo)?;
160158
161159 if recursive {
gitoxide-core/src/repository/verify.rs+2−3
@@ -1,4 +1,4 @@
1−use std::{path::PathBuf, sync::atomic::AtomicBool};
1+use std::sync::atomic::AtomicBool;
22
33 use git_repository as git;
44 use git_repository::Progress;
@@ -20,7 +20,7 @@ pub struct Context { · 15 unmodified lines
2020 pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
2121
2222 pub fn integrity(
23− repo: PathBuf,
23+ repo: git::Repository,
2424 mut out: impl std::io::Write,
2525 progress: impl Progress,
2626 should_interrupt: &AtomicBool,
@@ -31,7 +31,6 @@ pub fn integrity( · 4 unmodified lines
3131 algorithm,
3232 }: Context,
3333 ) -> anyhow::Result<()> {
34− let repo = git_repository::open(repo)?;
3534 #[cfg_attr(not(feature = "serde1"), allow(unused))]
3635 let mut outcome = repo.objects.store_ref().verify_integrity(
3736 progress,
src/plumbing/main.rs+177−130
@@ -9,6 +9,7 @@ use std::{ · 8 unmodified lines
99
1010 use anyhow::Result;
1111 use clap::Parser;
12+use git_repository::bstr::io::BufReadExt;
1213 use gitoxide_core as core;
1314 use gitoxide_core::pack::verify;
1415
@@ -155,133 +156,181 @@ pub fn main() -> Result<()> { · 140 unmodified lines
155156 },
156157 ),
157158 },
158− Subcommands::Repository(repo::Platform { repository, cmd }) => match cmd {
159− repo::Subcommands::Commit { cmd } => match cmd {
160− repo::commit::Subcommands::Describe {
161− annotated_tags,
162− all_refs,
163− first_parent,
164− always,
165− long,
166− statistics,
167− max_candidates,
168− rev_spec,
159+ Subcommands::Repository(repo::Platform { repository, cmd }) => {
160+ use git_repository as git;
161+ let repository = git::ThreadSafeRepository::discover(repository)?;
162+ match cmd {
163+ repo::Subcommands::Commit { cmd } => match cmd {
164+ repo::commit::Subcommands::Describe {
165+ annotated_tags,
166+ all_refs,
167+ first_parent,
168+ always,
169+ long,
170+ statistics,
171+ max_candidates,
172+ rev_spec,
173+ } => prepare_and_run(
174+ "repository-commit-describe",
175+ verbose,
176+ progress,
177+ progress_keep_open,
178+ None,
179+ move |_progress, out, err| {
180+ core::repository::commit::describe(
181+ repository.into(),
182+ rev_spec.as_deref(),
183+ out,
184+ err,
185+ core::repository::commit::describe::Options {
186+ all_tags: !annotated_tags,
187+ all_refs,
188+ long_format: long,
189+ first_parent,
190+ statistics,
191+ max_candidates,
192+ always,
193+ },
194+ )
195+ },
196+ ),
197+ },
198+ repo::Subcommands::Exclude { cmd } => match cmd {
199+ repo::exclude::Subcommands::Query {
200+ patterns,
201+ pathspecs,
202+ show_ignore_patterns,
203+ } => prepare_and_run(
204+ "repository-exclude-query",
205+ verbose,
206+ progress,
207+ progress_keep_open,
208+ None,
209+ move |_progress, out, _err| {
210+ use git::bstr::ByteSlice;
211+ core::repository::exclude::query(
212+ repository.into(),
213+ if pathspecs.is_empty() {
214+ Box::new(
215+ stdin_or_bail()?
216+ .byte_lines()
217+ .filter_map(Result::ok)
218+ .filter_map(|line| git::path::Spec::from_bytes(line.as_bstr())),
219+ ) as Box<dyn Iterator<Item = git::path::Spec>>
220+ } else {
221+ Box::new(pathspecs.into_iter())
222+ },
223+ out,
224+ core::repository::exclude::query::Options {
225+ format,
226+ show_ignore_patterns,
227+ overrides: patterns,
228+ },
229+ )
230+ },
231+ ),
232+ },
233+ repo::Subcommands::Mailmap { cmd } => match cmd {
234+ repo::mailmap::Subcommands::Entries => prepare_and_run(
235+ "repository-mailmap-entries",
236+ verbose,
237+ progress,
238+ progress_keep_open,
239+ None,
240+ move |_progress, out, err| {
241+ core::repository::mailmap::entries(repository.into(), format, out, err)
242+ },
243+ ),
244+ },
245+ repo::Subcommands::Odb { cmd } => match cmd {
246+ repo::odb::Subcommands::Entries => prepare_and_run(
247+ "repository-odb-entries",
248+ verbose,
249+ progress,
250+ progress_keep_open,
251+ None,
252+ move |_progress, out, _err| core::repository::odb::entries(repository.into(), format, out),
253+ ),
254+ repo::odb::Subcommands::Info => prepare_and_run(
255+ "repository-odb-info",
256+ verbose,
257+ progress,
258+ progress_keep_open,
259+ None,
260+ move |_progress, out, err| core::repository::odb::info(repository.into(), format, out, err),
261+ ),
262+ },
263+ repo::Subcommands::Tree { cmd } => match cmd {
264+ repo::tree::Subcommands::Entries {
265+ treeish,
266+ recursive,
267+ extended,
268+ } => prepare_and_run(
269+ "repository-tree-entries",
270+ verbose,
271+ progress,
272+ progress_keep_open,
273+ None,
274+ move |_progress, out, _err| {
275+ core::repository::tree::entries(
276+ repository.into(),
277+ treeish.as_deref(),
278+ recursive,
279+ extended,
280+ format,
281+ out,
282+ )
283+ },
284+ ),
285+ repo::tree::Subcommands::Info { treeish, extended } => prepare_and_run(
286+ "repository-tree-info",
287+ verbose,
288+ progress,
289+ progress_keep_open,
290+ None,
291+ move |_progress, out, err| {
292+ core::repository::tree::info(
293+ repository.into(),
294+ treeish.as_deref(),
295+ extended,
296+ format,
297+ out,
298+ err,
299+ )
300+ },
301+ ),
302+ },
303+ repo::Subcommands::Verify {
304+ args:
305+ pack::VerifyOptions {
306+ statistics,
307+ algorithm,
308+ decode,
309+ re_encode,
310+ },
169311 } => prepare_and_run(
170− "repository-commit-describe",
312+ "repository-verify",
171313 verbose,
172314 progress,
173315 progress_keep_open,
174− None,
175− move |_progress, out, err| {
176− core::repository::commit::describe(
177− repository,
178− rev_spec.as_deref(),
316+ core::repository::verify::PROGRESS_RANGE,
317+ move |progress, out, _err| {
318+ core::repository::verify::integrity(
319+ repository.into(),
179320 out,
180− err,
181− core::repository::commit::describe::Options {
182− all_tags: !annotated_tags,
183− all_refs,
184− long_format: long,
185− first_parent,
186− statistics,
187− max_candidates,
188− always,
321+ progress,
322+ &should_interrupt,
323+ core::repository::verify::Context {
324+ output_statistics: statistics.then(|| format),
325+ algorithm,
326+ verify_mode: verify_mode(decode, re_encode),
327+ thread_limit,
189328 },
190329 )
191330 },
192331 ),
193− },
194− repo::Subcommands::Mailmap { cmd } => match cmd {
195− repo::mailmap::Subcommands::Entries => prepare_and_run(
196− "repository-mailmap-entries",
197− verbose,
198− progress,
199− progress_keep_open,
200− None,
201− move |_progress, out, err| core::repository::mailmap::entries(repository, format, out, err),
202− ),
203− },
204− repo::Subcommands::Odb { cmd } => match cmd {
205− repo::odb::Subcommands::Entries => prepare_and_run(
206− "repository-odb-entries",
207− verbose,
208− progress,
209− progress_keep_open,
210− None,
211− move |_progress, out, _err| core::repository::odb::entries(repository, format, out),
212− ),
213− repo::odb::Subcommands::Info => prepare_and_run(
214− "repository-odb-info",
215− verbose,
216− progress,
217− progress_keep_open,
218− None,
219− move |_progress, out, err| core::repository::odb::info(repository, format, out, err),
220− ),
221− },
222− repo::Subcommands::Tree { cmd } => match cmd {
223− repo::tree::Subcommands::Entries {
224− treeish,
225− recursive,
226− extended,
227− } => prepare_and_run(
228− "repository-tree-entries",
229− verbose,
230− progress,
231− progress_keep_open,
232− None,
233− move |_progress, out, _err| {
234− core::repository::tree::entries(
235− repository,
236− treeish.as_deref(),
237− recursive,
238− extended,
239− format,
240− out,
241− )
242− },
243− ),
244− repo::tree::Subcommands::Info { treeish, extended } => prepare_and_run(
245− "repository-tree-info",
246− verbose,
247− progress,
248− progress_keep_open,
249− None,
250− move |_progress, out, err| {
251− core::repository::tree::info(repository, treeish.as_deref(), extended, format, out, err)
252− },
253− ),
254− },
255− repo::Subcommands::Verify {
256− args:
257− pack::VerifyOptions {
258− statistics,
259− algorithm,
260− decode,
261− re_encode,
262− },
263− } => prepare_and_run(
264− "repository-verify",
265− verbose,
266− progress,
267− progress_keep_open,
268− core::repository::verify::PROGRESS_RANGE,
269− move |progress, out, _err| {
270− core::repository::verify::integrity(
271− repository,
272− out,
273− progress,
274− &should_interrupt,
275− core::repository::verify::Context {
276− output_statistics: statistics.then(|| format),
277− algorithm,
278− verify_mode: verify_mode(decode, re_encode),
279− thread_limit,
280− },
281− )
282− },
283− ),
284− },
332+ }
333+ }
285334 Subcommands::Pack(subcommands) => match subcommands {
286335 pack::Subcommands::Create {
287336 repository,
@@ -303,16 +352,7 @@ pub fn main() -> Result<()> { · 15 unmodified lines
303352 progress_keep_open,
304353 core::pack::create::PROGRESS_RANGE,
305354 move |progress, out, _err| {
306− let input = if has_tips {
307− None
308− } else {
309− if atty::is(atty::Stream::Stdin) {
310− anyhow::bail!(
311− "Refusing to read from standard input as no path is given, but it's a terminal."
312− )
313− }
314− Some(BufReader::new(stdin()))
315− };
355+ let input = if has_tips { None } else { stdin_or_bail()?.into() };
316356 let repository = repository.unwrap_or_else(|| PathBuf::from("."));
317357 let context = core::pack::create::Context {
318358 thread_limit,
@@ -379,7 +419,7 @@ pub fn main() -> Result<()> { · 60 unmodified lines
379419 directory,
380420 refs_directory,
381421 refs.into_iter().map(|r| r.into()).collect(),
382− git_features::progress::DoOrDiscard::from(progress),
422+ progress,
383423 core::pack::receive::Context {
384424 thread_limit,
385425 format,
@@ -568,7 +608,7 @@ pub fn main() -> Result<()> { · 182 unmodified lines
568608 core::remote::refs::list(
569609 protocol,
570610 &url,
571− git_features::progress::DoOrDiscard::from(progress),
611+ progress,
572612 core::remote::refs::Context {
573613 thread_limit,
574614 format,
@@ -603,6 +643,13 @@ pub fn main() -> Result<()> { · 28 unmodified lines
603643 Ok(())
604644 }
605645
646+fn stdin_or_bail() -> anyhow::Result<std::io::BufReader<std::io::Stdin>> {
647+ if atty::is(atty::Stream::Stdin) {
648+ anyhow::bail!("Refusing to read from standard input while a terminal is connected")
649+ }
650+ Ok(BufReader::new(stdin()))
651+}
652+
606653 fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode {
607654 match (decode, re_encode) {
608655 (true, false) => verify::Mode::HashCrc32Decode,
src/plumbing/options.rs+30−0
@@ -370,6 +370,36 @@ pub mod repo { · 369 unmodified lines
370370 #[clap(subcommand)]
371371 cmd: mailmap::Subcommands,
372372 },
373+ /// Interact with the exclude files like .gitignore.
374+ Exclude {
375+ #[clap(subcommand)]
376+ cmd: exclude::Subcommands,
377+ },
378+ }
379+
380+ pub mod exclude {
381+ use git_repository as git;
382+ use std::ffi::OsString;
383+
384+ #[derive(Debug, clap::Subcommand)]
385+ pub enum Subcommands {
386+ /// Check if path-specs are excluded and print the result similar to `git check-ignore`.
387+ Query {
388+ /// Show actual ignore patterns instead of un-excluding an entry.
389+ ///
390+ /// That way one can understand why an entry might not be excluded.
391+ #[clap(long, short = 'i')]
392+ show_ignore_patterns: bool,
393+ /// Additional patterns to use for exclusions. They have the highest priority.
394+ ///
395+ /// Useful for undoing previous patterns using the '!' prefix.
396+ #[clap(long, short = 'p')]
397+ patterns: Vec<OsString>,
398+ /// The git path specifications to check for exclusion, or unset to read from stdin one per line.
399+ #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))]
400+ pathspecs: Vec<git::path::Spec>,
401+ },
402+ }
373403 }
374404
375405 pub mod mailmap {
src/porcelain/main.rs+3−3
@@ -53,7 +53,7 @@ pub fn main() -> Result<()> { · 52 unmodified lines
5353 hours::estimate(
5454 &working_dir,
5555 &refname,
56− git_features::progress::DoOrDiscard::from(progress),
56+ progress,
5757 hours::Context {
5858 show_pii,
5959 omit_unify_identities,
@@ -75,7 +75,7 @@ pub fn main() -> Result<()> { · 15 unmodified lines
7575 organize::discover(
7676 root.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),
7777 out,
78− git_features::progress::DoOrDiscard::from(progress),
78+ progress,
7979 debug,
8080 )
8181 },
@@ -102,7 +102,7 @@ pub fn main() -> Result<()> { · 20 unmodified lines
102102 },
103103 repository_source.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),
104104 destination_directory.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),
105− git_features::progress::DoOrDiscard::from(progress),
105+ progress,
106106 )
107107 },
108108 )
src/shared.rs+45−2
@@ -52,19 +52,62 @@ pub mod pretty { · 51 unmodified lines
5252
5353 use crate::shared::ProgressRange;
5454
55+ #[cfg(feature = "small")]
56+ pub fn prepare_and_run<T>(
57+ name: &str,
58+ verbose: bool,
59+ progress: bool,
60+ #[cfg_attr(not(feature = "prodash-render-tui"), allow(unused_variables))] progress_keep_open: bool,
61+ range: impl Into<Option<ProgressRange>>,
62+ run: impl FnOnce(
63+ progress::DoOrDiscard<prodash::tree::Item>,
64+ &mut dyn std::io::Write,
65+ &mut dyn std::io::Write,
66+ ) -> Result<T>,
67+ ) -> Result<T> {
68+ crate::shared::init_env_logger();
69+
70+ match (verbose, progress) {
71+ (false, false) => {
72+ let stdout = stdout();
73+ let mut stdout_lock = stdout.lock();
74+ let stderr = stderr();
75+ let mut stderr_lock = stderr.lock();
76+ run(progress::DoOrDiscard::from(None), &mut stdout_lock, &mut stderr_lock)
77+ }
78+ (true, false) => {
79+ let progress = crate::shared::progress_tree();
80+ let sub_progress = progress.add_child(name);
81+
82+ use crate::shared::{self, STANDARD_RANGE};
83+ let handle = shared::setup_line_renderer_range(&progress, range.into().unwrap_or(STANDARD_RANGE));
84+
85+ let mut out = Vec::<u8>::new();
86+ let res = run(progress::DoOrDiscard::from(Some(sub_progress)), &mut out, &mut stderr());
87+ handle.shutdown_and_wait();
88+ std::io::Write::write_all(&mut stdout(), &out)?;
89+ res
90+ }
91+ #[cfg(not(feature = "prodash-render-tui"))]
92+ (true, true) | (false, true) => {
93+ unreachable!("BUG: This branch can't be run without a TUI built-in")
94+ }
95+ }
96+ }
97+
98+ #[cfg(not(feature = "small"))]
5599 pub fn prepare_and_run<T: Send + 'static>(
56100 name: &str,
57101 verbose: bool,
58102 progress: bool,
59103 #[cfg_attr(not(feature = "prodash-render-tui"), allow(unused_variables))] progress_keep_open: bool,
60− #[cfg_attr(not(feature = "prodash-render-line"), allow(unused_variables))] range: impl Into<Option<ProgressRange>>,
104+ range: impl Into<Option<ProgressRange>>,
61105 run: impl FnOnce(
62106 progress::DoOrDiscard<prodash::tree::Item>,
63107 &mut dyn std::io::Write,
64108 &mut dyn std::io::Write,
65109 ) -> Result<T>
66110 + Send
67− + std::panic::UnwindSafe
68111 + 'static,
69112 ) -> Result<T> {
70113 crate::shared::init_env_logger();
tests/tools/src/lib.rs+1−1
@@ -13,7 +13,7 @@ use once_cell::sync::Lazy; · 12 unmodified lines
1313 use parking_lot::Mutex;
1414 pub use tempfile;
1515
16−type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
16+pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
1717
1818 static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new()));
1919