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
503source = "registry+https://github.com/rust-lang/crates.io-index"503source = "registry+https://github.com/rust-lang/crates.io-index"
504checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35"504checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35"
505 505
506[[package]]
507name = "compact_str"
508version = "0.3.2"
509source = "registry+https://github.com/rust-lang/crates.io-index"
510checksum = "d0e60dedcb8b23cedf6f23ee35ecf5c7889961e99f26f79ab196aaf4a8b48608"
511dependencies = [
512 "serde",
513]
514
506[[package]]515[[package]]
507name = "concurrent-queue"516name = "concurrent-queue"
508version = "1.2.2"517version = "1.2.2"
@@ -1076,8 +1085,10 @@ name = "git-attributes" · 567 unmodified lines
1076version = "0.1.0"1085version = "0.1.0"
1077dependencies = [1086dependencies = [
1078 "bstr",1087 "bstr",
1088 "compact_str",
1079 "git-features",1089 "git-features",
1080 "git-glob",1090 "git-glob",
1091 "git-path",
1081 "git-quote",1092 "git-quote",
1082 "git-testtools",1093 "git-testtools",
1083 "quick-error",1094 "quick-error",
@@ -1122,15 +1133,16 @@ dependencies = [ · 38 unmodified lines
1122 "criterion",1133 "criterion",
1123 "dirs",1134 "dirs",
1124 "git-features",1135 "git-features",
1136 "git-path",
1125 "git-sec",1137 "git-sec",
1126 "memchr",1138 "memchr",
1127 "nom",1139 "nom",
1128 "pwd",1140 "pwd",
1129 "quick-error",
1130 "serde",1141 "serde",
1131 "serde_derive",1142 "serde_derive",
1132 "serial_test",1143 "serial_test",
1133 "tempfile",1144 "tempfile",
1145 "thiserror",
1134 "unicode-bom",1146 "unicode-bom",
1135]1147]
1136 1148
@@ -1300,6 +1312,7 @@ dependencies = [ · 163 unmodified lines
1300 "git-hash",1312 "git-hash",
1301 "git-object",1313 "git-object",
1302 "git-pack",1314 "git-pack",
1315 "git-path",
1303 "git-quote",1316 "git-quote",
1304 "git-testtools",1317 "git-testtools",
1305 "parking_lot 0.12.0",1318 "parking_lot 0.12.0",
@@ -1325,6 +1338,7 @@ dependencies = [ · 19 unmodified lines
1325 "git-hash",1338 "git-hash",
1326 "git-object",1339 "git-object",
1327 "git-odb",1340 "git-odb",
1341 "git-path",
1328 "git-tempfile",1342 "git-tempfile",
1329 "git-testtools",1343 "git-testtools",
1330 "git-traverse",1344 "git-traverse",
@@ -1356,6 +1370,13 @@ dependencies = [ · 25 unmodified lines
1356 "serde",1370 "serde",
1357]1371]
1358 1372
1373[[package]]
1374name = "git-path"
1375version = "0.1.0"
1376dependencies = [
1377 "bstr",
1378]
1379
1359[[package]]1380[[package]]
1360name = "git-pathspec"1381name = "git-pathspec"
1361version = "0.0.0"1382version = "0.0.0"
@@ -1406,6 +1427,7 @@ dependencies = [ · 44 unmodified lines
1406 "git-lock",1427 "git-lock",
1407 "git-object",1428 "git-object",
1408 "git-odb",1429 "git-odb",
1430 "git-path",
1409 "git-tempfile",1431 "git-tempfile",
1410 "git-testtools",1432 "git-testtools",
1411 "git-validate",1433 "git-validate",
@@ -1425,6 +1447,7 @@ dependencies = [ · 13 unmodified lines
1425 "clru",1447 "clru",
1426 "document-features",1448 "document-features",
1427 "git-actor",1449 "git-actor",
1450 "git-attributes",
1428 "git-config",1451 "git-config",
1429 "git-credentials",1452 "git-credentials",
1430 "git-diff",1453 "git-diff",
@@ -1437,6 +1460,7 @@ dependencies = [ · 6 unmodified lines
1437 "git-object",1460 "git-object",
1438 "git-odb",1461 "git-odb",
1439 "git-pack",1462 "git-pack",
1463 "git-path",
1440 "git-protocol",1464 "git-protocol",
1441 "git-ref",1465 "git-ref",
1442 "git-revision",1466 "git-revision",
@@ -1448,6 +1472,7 @@ dependencies = [ · 5 unmodified lines
1448 "git-url",1472 "git-url",
1449 "git-validate",1473 "git-validate",
1450 "git-worktree",1474 "git-worktree",
1475 "is_ci",
1451 "log",1476 "log",
1452 "signal-hook",1477 "signal-hook",
1453 "tempfile",1478 "tempfile",
@@ -1477,6 +1502,7 @@ dependencies = [ · 23 unmodified lines
1477 "libc",1502 "libc",
1478 "serde",1503 "serde",
1479 "tempfile",1504 "tempfile",
1505 "thiserror",
1480 "windows",1506 "windows",
1481]1507]
1482 1508
@@ -1571,6 +1597,7 @@ version = "0.4.0" · 88 unmodified lines
1571dependencies = [1597dependencies = [
1572 "bstr",1598 "bstr",
1573 "git-features",1599 "git-features",
1600 "git-path",
1574 "home",1601 "home",
1575 "quick-error",1602 "quick-error",
1576 "serde",1603 "serde",
@@ -1592,11 +1619,14 @@ version = "0.1.0" · 15 unmodified lines
1592dependencies = [1619dependencies = [
1593 "bstr",1620 "bstr",
1594 "document-features",1621 "document-features",
1622 "git-attributes",
1595 "git-features",1623 "git-features",
1624 "git-glob",
1596 "git-hash",1625 "git-hash",
1597 "git-index",1626 "git-index",
1598 "git-object",1627 "git-object",
1599 "git-odb",1628 "git-odb",
1629 "git-path",
1600 "git-testtools",1630 "git-testtools",
1601 "io-close",1631 "io-close",
1602 "serde",1632 "serde",
Cargo.toml+1−0
@@ -161,6 +161,7 @@ members = [ · 160 unmodified lines
161 "git-lock",161 "git-lock",
162 "git-attributes",162 "git-attributes",
163 "git-pathspec",163 "git-pathspec",
164 "git-path",
164 "git-repository",165 "git-repository",
165 "gitoxide-core",166 "gitoxide-core",
166 "git-tui",167 "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
48 * **mailmap**48 * **mailmap**
49 * [x] **verify** - check entries of a mailmap file for parse errors and display them49 * [x] **verify** - check entries of a mailmap file for parse errors and display them
50 * **repository**50 * **repository**
51 * **exclude**
52 * [x] **query** - check if path specs are excluded via gits exclusion rules like `.gitignore`.
51 * **verify** - validate a whole repository, for now only the object database.53 * **verify** - validate a whole repository, for now only the object database.
52 * **commit**54 * **commit**
53 * [x] **describe** - identify a commit by its closest tag in its past55 * [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
122 * [git-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-bitmap)124 * [git-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-bitmap)
123 * [git-revision](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-revision)125 * [git-revision](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-revision)
124 * [git-attributes](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-attributes)126 * [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)
125* **idea**128* **idea**
126 * [git-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-note)129 * [git-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-note)
127 * [git-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-filter)130 * [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
221* [x] parsing221* [x] parsing
222* [x] lookup and mapping of author names222* [x] lookup and mapping of author names
223 223
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
224### git-pathspec231### git-pathspec
225* [ ] parse232* [ ] parse
226* [ ] check for match233* [ ] check for match
@@ -429,7 +436,8 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. · 202 unmodified lines
429 * **refs**436 * **refs**
430 * [ ] run transaction hooks and handle special repository states like quarantine437 * [ ] run transaction hooks and handle special repository states like quarantine
431 * [ ] support for different backends like `files` and `reftable`438 * [ ] support for different backends like `files` and `reftable`
432 * [ ] worktrees439 * **worktrees**
440 * [ ] open a repository with worktrees
433 * [ ] remotes with push and pull441 * [ ] remotes with push and pull
434 * [x] mailmap 442 * [x] mailmap
435 * [x] object replacements (`git replace`)443 * [x] object replacements (`git replace`)
etc/check-package-size.sh+2−1
@@ -19,6 +19,7 @@ echo "in root: gitoxide CLI" · 18 unmodified lines
19(enter cargo-smart-release && indent cargo diet -n --package-size-limit 90KB)19(enter cargo-smart-release && indent cargo diet -n --package-size-limit 90KB)
20(enter git-actor && indent cargo diet -n --package-size-limit 5KB)20(enter git-actor && indent cargo diet -n --package-size-limit 5KB)
21(enter git-pathspec && indent cargo diet -n --package-size-limit 5KB)21(enter git-pathspec && indent cargo diet -n --package-size-limit 5KB)
22(enter git-path && indent cargo diet -n --package-size-limit 10KB)
22(enter git-attributes && indent cargo diet -n --package-size-limit 10KB)23(enter git-attributes && indent cargo diet -n --package-size-limit 10KB)
23(enter git-index && indent cargo diet -n --package-size-limit 30KB)24(enter git-index && indent cargo diet -n --package-size-limit 30KB)
24(enter git-worktree && indent cargo diet -n --package-size-limit 25KB)25(enter git-worktree && indent cargo diet -n --package-size-limit 25KB)
@@ -51,6 +52,6 @@ echo "in root: gitoxide CLI" · 26 unmodified lines
51(enter git-odb && indent cargo diet -n --package-size-limit 120KB)52(enter git-odb && indent cargo diet -n --package-size-limit 120KB)
52(enter git-protocol && indent cargo diet -n --package-size-limit 50KB)53(enter git-protocol && indent cargo diet -n --package-size-limit 50KB)
53(enter git-packetline && indent cargo diet -n --package-size-limit 35KB)54(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)
55(enter git-transport && indent cargo diet -n --package-size-limit 50KB)56(enter git-transport && indent cargo diet -n --package-size-limit 50KB)
56(enter gitoxide-core && indent cargo diet -n --package-size-limit 70KB)57(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
13 13
14[features]14[features]
15## Data structures implement `serde::Serialize` and `serde::Deserialize`.15## Data structures implement `serde::Serialize` and `serde::Deserialize`.
16serde1 = ["serde", "bstr/serde1", "git-glob/serde1"]16serde1 = ["serde", "bstr/serde1", "git-glob/serde1", "compact_str/serde"]
17 17
18# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html18# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
19 19
20[dependencies]20[dependencies]
21git-features = { version = "^0.20.0", path = "../git-features" }21git-features = { version = "^0.20.0", path = "../git-features" }
22git-path = { version = "^0.1.0", path = "../git-path" }
22git-quote = { version = "^0.2.0", path = "../git-quote" }23git-quote = { version = "^0.2.0", path = "../git-quote" }
23git-glob = { version = "^0.2.0", path = "../git-glob" }24git-glob = { version = "^0.2.0", path = "../git-glob" }
24 25
@@ -26,6 +27,7 @@ bstr = { version = "0.2.13", default-features = false, features = ["std"]} · 1 unmodified line
26unicode-bom = "1.1.4"27unicode-bom = "1.1.4"
27quick-error = "2.0.0"28quick-error = "2.0.0"
28serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}29serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
30compact_str = "0.3.2"
29 31
30[dev-dependencies]32[dev-dependencies]
31git-testtools = { path = "../tests/tools"}33git-testtools = { path = "../tests/tools"}
git-attributes/src/lib.rs+61−66
@@ -2,10 +2,17 @@ · 1 unmodified line
2#![deny(rust_2018_idioms)]2#![deny(rust_2018_idioms)]
3 3
4use bstr::{BStr, BString};4use bstr::{BStr, BString};
5use compact_str::CompactStr;
6use std::path::PathBuf;
5 7
8pub 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.
6#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]13#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
7#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]14#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
8pub enum State<'a> {15pub enum StateRef<'a> {
9 /// The attribute is listed, or has the special value 'true'16 /// The attribute is listed, or has the special value 'true'
10 Set,17 Set,
11 /// The attribute has the special value 'false', or was prefixed with a `-` sign.18 /// The attribute has the special value 'false', or was prefixed with a `-` sign.
@@ -18,11 +25,38 @@ pub enum State<'a> { · 6 unmodified lines
18 Unspecified,25 Unspecified,
19}26}
20 27
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.
22///29///
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))]
33pub 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.
24#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]46#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
25pub struct MatchGroup<T: match_group::Tag> {47#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
48pub 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)]
59pub struct MatchGroup<T: match_group::Pattern = Attributes> {
26 /// A list of pattern lists, each representing a patterns from a file or specified by hand, in the order they were60 /// A list of pattern lists, each representing a patterns from a file or specified by hand, in the order they were
27 /// specified in.61 /// specified in.
28 ///62 ///
@@ -30,75 +64,36 @@ pub struct MatchGroup<T: match_group::Tag> { · 1 unmodified line
30 pub patterns: Vec<PatternList<T>>,64 pub patterns: Vec<PatternList<T>>,
31}65}
32 66
33/// A list of patterns with an optional names, for matching against it.67/// A list of patterns which optionally know where they were loaded from and what their base is.
34#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]68///
35pub struct PatternList<T: match_group::Tag> {69/// Knowing their base which is relative to a source directory, it will ignore all path to match against
36 /// Patterns and their associated data in the order they were loaded in or specified.70/// that don't also start with said base.
71#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)]
72pub 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)`_).
37 ///75 ///
38 /// During matching, this order is reversed.76 /// During matching, this order is reversed.
39 pub patterns: Vec<(git_glob::Pattern, T::Value)>,77 pub patterns: Vec<PatternMapping<T::Value>>,
40 78
41 /// The path at which the patterns are located in a format suitable for matches, or `None` if the patterns79 /// The path from which the patterns were read, or `None` if the patterns
42 /// are relative to the worktree root.80 /// don't originate in a file on disk.
43 base: Option<BString>,81 pub source: Option<PathBuf>,
44}
45
46mod 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 }
63 82
64 /// Identify patterns with attributes.83 /// The parent directory of source, or `None` if the patterns are _global_ to match against the repository root.
65 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]84 /// It's processed to contain slashes only and to end with a trailing slash, and is relative to the repository root.
66 pub struct Attributes;85 pub base: Option<BString>,
67 impl Tag for Attributes {86}
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 }
80 87
81 impl PatternList<Ignore> {88#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
82 /// Parse a list of patterns, using slashes as path separators89pub struct PatternMapping<T> {
83 pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {90 pub pattern: git_glob::Pattern,
84 PatternList {91 pub value: T,
85 patterns: patterns92 pub sequence_number: usize,
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 }
97}93}
98pub use match_group::{Attributes, Ignore, Tag};
99 94
100pub type Files = MatchGroup<Attributes>;95mod match_group;
101pub type IgnoreFiles = MatchGroup<Ignore>;96pub use match_group::{Attributes, Ignore, Match, Pattern};
102 97
103pub mod parse;98pub mod parse;
104 99
git-attributes/src/match_group.rs+354−0
@@ -0,0 +1,354 @@
1use crate::{Assignment, MatchGroup, PatternList, PatternMapping, State, StateRef};
2use bstr::{BStr, BString, ByteSlice, ByteVec};
3use std::ffi::OsString;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6
7impl<'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
18fn 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.
32pub 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)]
44pub struct Ignore;
45
46impl 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)]
66pub 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)]
73pub struct Attributes;
74
75impl 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)]
113pub 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
123impl<T> MatchGroup<T>
124where
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
144impl 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
204fn 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
221impl<T> PatternList<T>
222where
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
258impl<T> PatternList<T>
259where
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
333impl 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
56 }56 }
57 }57 }
58 58
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> {
60 let mut tokens = attr.splitn(2, |b| *b == b'=');60 let mut tokens = attr.splitn(2, |b| *b == b'=');
61 let attr = tokens.next().expect("attr itself").as_bstr();61 let attr = tokens.next().expect("attr itself").as_bstr();
62 let possibly_value = tokens.next();62 let possibly_value = tokens.next();
63 let (attr, state) = if attr.first() == Some(&b'-') {63 let (attr, state) = if attr.first() == Some(&b'-') {
64 (&attr[1..], crate::State::Unset)64 (&attr[1..], crate::StateRef::Unset)
65 } else if attr.first() == Some(&b'!') {65 } else if attr.first() == Some(&b'!') {
66 (&attr[1..], crate::State::Unspecified)66 (&attr[1..], crate::StateRef::Unspecified)
67 } else {67 } else {
68 (68 (
69 attr,69 attr,
70 possibly_value70 possibly_value
71 .map(|v| crate::State::Value(v.as_bstr()))71 .map(|v| crate::StateRef::Value(v.as_bstr()))
72 .unwrap_or(crate::State::Set),72 .unwrap_or(crate::StateRef::Set),
73 )73 )
74 };74 };
75 Ok((check_attr(attr, self.line_no)?, state))75 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
95}95}
96 96
97impl<'a> Iterator for Iter<'a> {97impl<'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>;
99 99
100 fn next(&mut self) -> Option<Self::Item> {100 fn next(&mut self) -> Option<Self::Item> {
101 let attr = self.attrs.next().filter(|a| !a.is_empty())?;101 let attr = self.attrs.next().filter(|a| !a.is_empty())?;
git-attributes/tests/attributes.rs+1−0
@@ -1,2 +1,3 @@
1pub use git_testtools::Result;
1mod match_group;2mod match_group;
2mod parse;3mod parse;
git-attributes/tests/fixtures/make_global_and_external_and_dir_ignores.sh+77−0
@@ -0,0 +1,77 @@
1#!/bin/bash
2set -eu -o pipefail
3
4cat <<EOF >user.exclude
5# a custom exclude configured per user
6user-file-anywhere
7/user-file-from-top
8
9user-dir-anywhere/
10/user-dir-from-top
11
12user-subdir/file
13**/user-subdir-anywhere/file
14EOF
15
16mkdir 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
23file-anywhere
24/file-from-top
25
26dir-anywhere/
27/dir-from-top
28
29subdir/file
30**/subdir-anywhere/file
31EOF
32
33 cat <<EOF >.gitignore
34# a sample .gitignore
35top-level-local-file-anywhere
36EOF
37
38 mkdir dir-with-ignore
39 cat <<EOF >dir-with-ignore/.gitignore
40# a sample .gitignore
41sub-level-local-file-anywhere
42EOF
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 || :
51user-file-anywhere
52dir/user-file-anywhere
53user-file-from-top
54no-match/user-file-from-top
55user-dir-anywhere
56user-dir-from-top
57no-match/user-dir-from-top
58user-subdir/file
59subdir/user-subdir-anywhere/file
60file-anywhere
61dir/file-anywhere
62file-from-top
63no-match/file-from-top
64dir-anywhere
65dir/dir-anywhere
66dir-from-top
67no-match/dir-from-top
68subdir/file
69subdir/subdir-anywhere/file
70top-level-local-file-anywhere
71dir/top-level-local-file-anywhere
72no-match/sub-level-local-file-anywhere
73dir-with-ignore/sub-level-local-file-anywhere
74dir-with-ignore/sub-dir/sub-level-local-file-anywhere
75EOF
76
77)
git-attributes/tests/match_group/mod.rs+108−5
@@ -1,14 +1,117 @@
1mod ignore {1mod 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 }
3 89
4 #[test]90 #[test]
5 fn init_from_overrides() {91 fn from_overrides() {
6 let input = ["simple", "pattern/"];92 let input = ["simple", "pattern/"];
7 let patterns = git_attributes::MatchGroup::<Ignore>::from_overrides(input).patterns;93 let group = git_attributes::MatchGroup::<Ignore>::from_overrides(input);
8 assert_eq!(patterns.len(), 1);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);
9 assert_eq!(103 assert_eq!(
10 git_attributes::PatternList::<Ignore>::from_overrides(input),104 git_attributes::PatternList::<Ignore>::from_overrides(input),
11 patterns.into_iter().next().unwrap()105 group.patterns.into_iter().next().unwrap()
12 );106 );
13 }107 }
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 }
14}117}
git-attributes/tests/parse/attribute.rs+11−11
@@ -1,5 +1,5 @@
1use bstr::{BStr, ByteSlice};1use bstr::{BStr, ByteSlice};
2use git_attributes::{parse, State};2use git_attributes::{parse, StateRef};
3use git_glob::pattern::Mode;3use git_glob::pattern::Mode;
4use git_testtools::fixture_bytes;4use git_testtools::fixture_bytes;
5 5
@@ -30,7 +30,7 @@ fn line_numbers_are_counted_correctly() { · 24 unmodified lines
30 (pattern(r"!foo.html", Mode::NO_SUB_DIR, None), vec![set("x")], 8),30 (pattern(r"!foo.html", Mode::NO_SUB_DIR, None), vec![set("x")], 8),
31 (pattern(r"#a/path", Mode::empty(), None), vec![unset("a")], 10),31 (pattern(r"#a/path", Mode::empty(), None), vec![unset("a")], 10),
32 (32 (
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)),
34 vec![unspecified("b")],34 vec![unspecified("b")],
35 1135 11
36 ),36 ),
@@ -249,22 +249,22 @@ fn trailing_whitespace_in_attributes_is_ignored() { · 212 unmodified lines
249 );249 );
250}250}
251 251
252type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::State<'a>)>, usize);252type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::StateRef<'a>)>, usize);
253 253
254fn set(attr: &str) -> (&BStr, State) {254fn set(attr: &str) -> (&BStr, StateRef) {
255 (attr.as_bytes().as_bstr(), State::Set)255 (attr.as_bytes().as_bstr(), StateRef::Set)
256}256}
257 257
258fn unset(attr: &str) -> (&BStr, State) {258fn unset(attr: &str) -> (&BStr, StateRef) {
259 (attr.as_bytes().as_bstr(), State::Unset)259 (attr.as_bytes().as_bstr(), StateRef::Unset)
260}260}
261 261
262fn unspecified(attr: &str) -> (&BStr, State) {262fn unspecified(attr: &str) -> (&BStr, StateRef) {
263 (attr.as_bytes().as_bstr(), State::Unspecified)263 (attr.as_bytes().as_bstr(), StateRef::Unspecified)
264}264}
265 265
266fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, State<'b>) {266fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, StateRef<'b>) {
267 (attr.as_bytes().as_bstr(), State::Value(value.as_bytes().as_bstr()))267 (attr.as_bytes().as_bstr(), StateRef::Value(value.as_bytes().as_bstr()))
268}268}
269 269
270fn pattern(name: &str, flags: git_glob::pattern::Mode, first_wildcard_pos: Option<usize>) -> parse::Kind {270fn 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
20 ("*.[oa]".into(), Mode::NO_SUB_DIR, 2),20 ("*.[oa]".into(), Mode::NO_SUB_DIR, 2),
21 ("*.html".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH, 5),21 ("*.html".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH, 5),
22 ("foo.html".into(), Mode::NO_SUB_DIR | Mode::NEGATIVE, 8),22 ("foo.html".into(), Mode::NO_SUB_DIR | Mode::NEGATIVE, 8),
23 ("/*".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH | Mode::ABSOLUTE, 11),23 ("*".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH | Mode::ABSOLUTE, 11),
24 ("/foo".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR | Mode::ABSOLUTE, 12),24 ("foo".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR | Mode::ABSOLUTE, 12),
25 ("/foo/*".into(), Mode::ABSOLUTE, 13),25 ("foo/*".into(), Mode::ABSOLUTE, 13),
26 ("/foo/bar".into(), Mode::ABSOLUTE | Mode::NEGATIVE, 14)26 ("foo/bar".into(), Mode::ABSOLUTE | Mode::NEGATIVE, 14)
27 ]27 ]
28 );28 );
29}29}
git-config/Cargo.toml+2−1
@@ -15,6 +15,7 @@ include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] · 14 unmodified lines
15 15
16[dependencies]16[dependencies]
17git-features = { version = "^0.20.0", path = "../git-features"}17git-features = { version = "^0.20.0", path = "../git-features"}
18git-path = { version = "^0.1.0", path = "../git-path" }
18git-sec = { version = "^0.1.0", path = "../git-sec" }19git-sec = { version = "^0.1.0", path = "../git-sec" }
19 20
20dirs = "4"21dirs = "4"
@@ -22,7 +23,7 @@ nom = { version = "7", default_features = false, features = [ "std" ] } · 1 unmodified line
22memchr = "2"23memchr = "2"
23serde_crate = { version = "1", package = "serde", optional = true }24serde_crate = { version = "1", package = "serde", optional = true }
24pwd = "1.3.1"25pwd = "1.3.1"
25quick-error = "2.0.0"26thiserror = "1.0.26"
26unicode-bom = "1.1.4"27unicode-bom = "1.1.4"
27bstr = { version = "0.2.13", default-features = false, features = ["std"] }28bstr = { version = "0.2.13", default-features = false, features = ["std"] }
28 29
git-config/src/file/error.rs+0−35
@@ -1,35 +0,0 @@
1use std::{error::Error, fmt::Display};
2
3use 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)]
9pub 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
21impl 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
35impl Error for GitConfigError<'_> {}
git-config/src/file/git_config.rs+191−171
@@ -1,3 +1,4 @@
1use bstr::BStr;
1use std::{2use std::{
2 borrow::Cow,3 borrow::Cow,
3 collections::{HashMap, VecDeque},4 collections::{HashMap, VecDeque},
@@ -8,17 +9,16 @@ use std::{ · 4 unmodified lines
8 9
9use crate::{10use crate::{
10 file::{11 file::{
11 error::GitConfigError,
12 section::{MutableSection, SectionBody},12 section::{MutableSection, SectionBody},
13 value::{EntryData, MutableMultiValue, MutableValue},13 value::{EntryData, MutableMultiValue, MutableValue},
14 Index, Size,14 Index, Size,
15 },15 },
16 parser,16 lookup, parser,
17 parser::{17 parser::{
18 parse_from_bytes, parse_from_path, parse_from_str, Error, Event, Key, ParsedSectionHeader, Parser,18 parse_from_bytes, parse_from_path, parse_from_str, Error, Event, Key, ParsedSectionHeader, Parser,
19 SectionHeaderName,19 SectionHeaderName,
20 },20 },
21 values,21 value, values,
22};22};
23 23
24/// The section ID is a monotonically increasing ID used to refer to sections.24/// 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
61///61///
62/// `git` is flexible enough to allow users to set a key multiple times in62/// `git` is flexible enough to allow users to set a key multiple times in
63/// any number of identically named sections. When this is the case, the key63/// 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 the64/// is known as a "multivar". In this case, `raw_value` follows the
65/// "last one wins" approach that `git-config` internally uses for multivar65/// "last one wins" approach that `git-config` internally uses for multivar
66/// resolution.66/// resolution.
67///67///
@@ -78,7 +78,7 @@ pub(super) enum LookupTreeNode<'a> { · 10 unmodified lines
78/// e = f g h78/// e = f g h
79/// ```79/// ```
80///80///
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`])
82/// key `a` with the above config will fetch `d` or replace `d`, since the last82/// key `a` with the above config will fetch `d` or replace `d`, since the last
83/// valid config key/value pair is `a = d`:83/// valid config key/value pair is `a = d`:
84///84///
@@ -87,14 +87,14 @@ pub(super) enum LookupTreeNode<'a> { · 2 unmodified lines
87/// # use std::borrow::Cow;87/// # use std::borrow::Cow;
88/// # use std::convert::TryFrom;88/// # use std::convert::TryFrom;
89/// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();89/// # 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()));
91/// ```91/// ```
92///92///
93/// Consider the `multi` variants of the methods instead, if you want to work93/// Consider the `multi` variants of the methods instead, if you want to work
94/// with all values instead.94/// with all values instead.
95///95///
96/// [`ResolvedGitConfig`]: crate::file::ResolvedGitConfig96/// [`ResolvedGitConfig`]: crate::file::ResolvedGitConfig
97/// [`get_raw_value`]: Self::get_raw_value97/// [`raw_value`]: Self::raw_value
98#[derive(PartialEq, Eq, Clone, Debug, Default)]98#[derive(PartialEq, Eq, Clone, Debug, Default)]
99pub struct GitConfig<'event> {99pub struct GitConfig<'event> {
100 /// The list of events that occur before an actual section. Since a100 /// The list of events that occur before an actual section. Since a
@@ -121,32 +121,20 @@ pub struct GitConfig<'event> { · 20 unmodified lines
121pub mod from_paths {121pub mod from_paths {
122 use std::borrow::Cow;122 use std::borrow::Cow;
123 123
124 use quick_error::quick_error;
125
126 use crate::{parser, values::path::interpolate};124 use crate::{parser, values::path::interpolate};
127 125
128 quick_error! {126 /// The error returned by [`GitConfig::from_paths()`][super::GitConfig::from_paths()] and [`GitConfig::from_env_paths()`][super::GitConfig::from_env_paths()].
129 #[derive(Debug)]127 #[derive(Debug, thiserror::Error)]
130 /// The error returned by [`GitConfig::from_paths()`][super::GitConfig::from_paths()] and [`GitConfig::from_env_paths()`][super::GitConfig::from_env_paths()].128 #[allow(missing_docs)]
131 #[allow(missing_docs)]129 pub enum Error {
132 pub enum Error {130 #[error(transparent)]
133 ParserOrIoError(err: parser::ParserOrIoError<'static>) {131 ParserOrIoError(#[from] parser::ParserOrIoError<'static>),
134 display("Could not read config")132 #[error(transparent)]
135 source(err)133 Interpolate(#[from] interpolate::Error),
136 from()134 #[error("The maximum allowed length {} of the file include chain built by following nested includes is exceeded", .max_depth)]
137 }135 IncludeDepthExceeded { max_depth: u8 },
138 Interpolate(err: interpolate::Error) {136 #[error("Include paths from environment variables must not be relative")]
139 display("Could not interpolate path")137 MissingConfigPath,
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 }
150 }138 }
151 139
152 /// Options when loading git config using [`GitConfig::from_paths()`][super::GitConfig::from_paths()].140 /// Options when loading git config using [`GitConfig::from_paths()`][super::GitConfig::from_paths()].
@@ -175,45 +163,30 @@ pub mod from_paths { · 22 unmodified lines
175}163}
176 164
177pub mod from_env {165pub mod from_env {
178 use quick_error::quick_error;
179
180 use super::from_paths;166 use super::from_paths;
181 use crate::values::path::interpolate;167 use crate::values::path::interpolate;
182 168
183 quick_error! {169 /// Represents the errors that may occur when calling [`GitConfig::from_env`][crate::file::GitConfig::from_env()].
184 #[derive(Debug)]170 #[derive(Debug, thiserror::Error)]
185 /// Represents the errors that may occur when calling [`GitConfig::from_env`][crate::file::GitConfig::from_env()].171 #[allow(missing_docs)]
186 #[allow(missing_docs)]172 pub enum Error {
187 pub enum Error {173 #[error("GIT_CONFIG_COUNT was not a positive integer: {}", .input)]
188 ParseError (err: String) {174 ParseError { input: String },
189 display("GIT_CONFIG_COUNT was not a positive integer: {}", err)175 #[error("GIT_CONFIG_KEY_{} was not set", .key_id)]
190 }176 InvalidKeyId { key_id: usize },
191 InvalidKeyId (key_id: usize) {177 #[error("GIT_CONFIG_KEY_{} was set to an invalid value: {}", .key_id, .key_val)]
192 display("GIT_CONFIG_KEY_{} was not set.", key_id)178 InvalidKeyValue { key_id: usize, key_val: String },
193 }179 #[error("GIT_CONFIG_VALUE_{} was not set", .value_id)]
194 InvalidKeyValue (key_id: usize, key_val: String) {180 InvalidValueId { value_id: usize },
195 display("GIT_CONFIG_KEY_{} was set to an invalid value: {}", key_id, key_val)181 #[error(transparent)]
196 }182 PathInterpolationError(#[from] interpolate::Error),
197 InvalidValueId (value_id: usize) {183 #[error(transparent)]
198 display("GIT_CONFIG_VALUE_{} was not set.", value_id)184 FromPathsError(#[from] from_paths::Error),
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 }
211 }185 }
212}186}
213 187
214impl<'event> GitConfig<'event> {188impl<'event> GitConfig<'event> {
215 /// Constructs an empty `git-config` file.189 /// Constructs an empty `git-config` file.
216 #[inline]
217 #[must_use]190 #[must_use]
218 pub fn new() -> Self {191 pub fn new() -> Self {
219 Self::default()192 Self::default()
@@ -225,7 +198,6 @@ impl<'event> GitConfig<'event> { · 5 unmodified lines
225 ///198 ///
226 /// Returns an error if there was an IO error or if the file wasn't a valid199 /// Returns an error if there was an IO error or if the file wasn't a valid
227 /// git-config file.200 /// git-config file.
228 #[inline]
229 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {201 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {
230 parse_from_path(path).map(Self::from)202 parse_from_path(path).map(Self::from)
231 }203 }
@@ -239,7 +211,6 @@ impl<'event> GitConfig<'event> { · 7 unmodified lines
239 /// git-config file.211 /// git-config file.
240 ///212 ///
241 /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-FILES213 /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-FILES
242 #[inline]
243 pub fn from_paths(paths: Vec<PathBuf>, options: &from_paths::Options) -> Result<Self, from_paths::Error> {214 pub fn from_paths(paths: Vec<PathBuf>, options: &from_paths::Options) -> Result<Self, from_paths::Error> {
244 let mut target = Self::new();215 let mut target = Self::new();
245 for path in paths {216 for path in paths {
@@ -375,14 +346,16 @@ impl<'event> GitConfig<'event> { · 129 unmodified lines
375 pub fn from_env(options: &from_paths::Options) -> Result<Option<Self>, from_env::Error> {346 pub fn from_env(options: &from_paths::Options) -> Result<Option<Self>, from_env::Error> {
376 use std::env;347 use std::env;
377 let count: usize = match env::var("GIT_CONFIG_COUNT") {348 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 })?,
379 Err(_) => return Ok(None),350 Err(_) => return Ok(None),
380 };351 };
381 352
382 let mut config = Self::new();353 let mut config = Self::new();
383 for i in 0..count {354 for i in 0..count {
384 let key = env::var(format!("GIT_CONFIG_KEY_{}", i)).map_err(|_| from_env::Error::InvalidKeyId(i))?;355 let key =
385 let value = env::var(format!("GIT_CONFIG_VALUE_{}", i)).map_err(|_| from_env::Error::InvalidValueId(i))?;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 })?;
386 if let Some((section_name, maybe_subsection)) = key.split_once('.') {359 if let Some((section_name, maybe_subsection)) = key.split_once('.') {
387 let (subsection, key) = if let Some((subsection, key)) = maybe_subsection.rsplit_once('.') {360 let (subsection, key) = if let Some((subsection, key)) = maybe_subsection.rsplit_once('.') {
388 (Some(subsection), key)361 (Some(subsection), key)
@@ -406,7 +379,10 @@ impl<'event> GitConfig<'event> { · 17 unmodified lines
406 Cow::Owned(value.into_bytes()),379 Cow::Owned(value.into_bytes()),
407 );380 );
408 } else {381 } 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 });
410 }386 }
411 }387 }
412 388
@@ -432,7 +408,7 @@ impl<'event> GitConfig<'event> { · 19 unmodified lines
432 /// # Examples408 /// # Examples
433 ///409 ///
434 /// ```410 /// ```
435 /// # use git_config::file::{GitConfig, GitConfigError};411 /// # use git_config::file::{GitConfig};
436 /// # use git_config::values::{Integer, Boolean};412 /// # use git_config::values::{Integer, Boolean};
437 /// # use std::borrow::Cow;413 /// # use std::borrow::Cow;
438 /// # use std::convert::TryFrom;414 /// # use std::convert::TryFrom;
@@ -457,15 +433,69 @@ impl<'event> GitConfig<'event> { · 18 unmodified lines
457 ///433 ///
458 /// [`values`]: crate::values434 /// [`values`]: crate::values
459 /// [`TryFrom`]: std::convert::TryFrom435 /// [`TryFrom`]: std::convert::TryFrom
460 #[inline]436 pub fn value<T: TryFrom<Cow<'event, [u8]>>>(
461 pub fn value<'lookup, T: TryFrom<Cow<'event, [u8]>>>(
462 &'event self,437 &'event self,
463 section_name: &'lookup str,438 section_name: &str,
464 subsection_name: Option<&'lookup str>,439 subsection_name: Option<&str>,
465 key: &'lookup str,440 key: &str,
466 ) -> Result<T, GitConfigError<'lookup>> {441 ) -> Result<T, lookup::Error<T::Error>> {
467 T::try_from(self.get_raw_value(section_name, subsection_name, key)?)442 T::try_from(self.raw_value(section_name, subsection_name, key)?).map_err(lookup::Error::FailedConversion)
468 .map_err(|_| GitConfigError::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()))
469 }499 }
470 500
471 /// Returns all interpreted values given a section, an optional subsection501 /// Returns all interpreted values given a section, an optional subsection
@@ -481,7 +511,7 @@ impl<'event> GitConfig<'event> { · 9 unmodified lines
481 /// # Examples511 /// # Examples
482 ///512 ///
483 /// ```513 /// ```
484 /// # use git_config::file::{GitConfig, GitConfigError};514 /// # use git_config::file::{GitConfig};
485 /// # use git_config::values::{Integer, Bytes, Boolean, TrueVariant};515 /// # use git_config::values::{Integer, Bytes, Boolean, TrueVariant};
486 /// # use std::borrow::Cow;516 /// # use std::borrow::Cow;
487 /// # use std::convert::TryFrom;517 /// # use std::convert::TryFrom;
@@ -507,7 +537,7 @@ impl<'event> GitConfig<'event> { · 19 unmodified lines
507 /// // ... or explicitly declare the type to avoid the turbofish537 /// // ... or explicitly declare the type to avoid the turbofish
508 /// let c_value: Vec<Bytes> = git_config.multi_value("core", None, "c")?;538 /// let c_value: Vec<Bytes> = git_config.multi_value("core", None, "c")?;
509 /// assert_eq!(c_value, vec![Bytes { value: Cow::Borrowed(b"g") }]);539 /// assert_eq!(c_value, vec![Bytes { value: Cow::Borrowed(b"g") }]);
510 /// # Ok::<(), GitConfigError>(())540 /// # Ok::<(), Box<dyn std::error::Error>>(())
511 /// ```541 /// ```
512 ///542 ///
513 /// # Errors543 /// # Errors
@@ -518,18 +548,17 @@ impl<'event> GitConfig<'event> { · 4 unmodified lines
518 ///548 ///
519 /// [`values`]: crate::values549 /// [`values`]: crate::values
520 /// [`TryFrom`]: std::convert::TryFrom550 /// [`TryFrom`]: std::convert::TryFrom
521 #[inline]
522 pub fn multi_value<'lookup, T: TryFrom<Cow<'event, [u8]>>>(551 pub fn multi_value<'lookup, T: TryFrom<Cow<'event, [u8]>>>(
523 &'event self,552 &'event self,
524 section_name: &'lookup str,553 section_name: &'lookup str,
525 subsection_name: Option<&'lookup str>,554 subsection_name: Option<&'lookup str>,
526 key: &'lookup str,555 key: &'lookup str,
527 ) -> Result<Vec<T>, GitConfigError<'lookup>> {556 ) -> Result<Vec<T>, lookup::Error<T::Error>> {
528 self.get_raw_multi_value(section_name, subsection_name, key)?557 self.raw_multi_value(section_name, subsection_name, key)?
529 .into_iter()558 .into_iter()
530 .map(T::try_from)559 .map(T::try_from)
531 .collect::<Result<Vec<_>, _>>()560 .collect::<Result<Vec<_>, _>>()
532 .map_err(|_| GitConfigError::FailedConversion)561 .map_err(lookup::Error::FailedConversion)
533 }562 }
534 563
535 /// Returns an immutable section reference.564 /// Returns an immutable section reference.
@@ -542,15 +571,10 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
542 &mut self,571 &mut self,
543 section_name: &'lookup str,572 section_name: &'lookup str,
544 subsection_name: Option<&'lookup str>,573 subsection_name: Option<&'lookup str>,
545 ) -> Result<&SectionBody<'event>, GitConfigError<'lookup>> {574 ) -> Result<&SectionBody<'event>, lookup::existing::Error> {
546 let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;575 let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
547 let id = section_ids576 let id = section_ids.last().expect("BUG: Section lookup vec was empty");
548 .last()577 Ok(self.sections.get(id).expect("BUG: Section did not have id from lookup"))
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"))
554 }578 }
555 579
556 /// Returns an mutable section reference.580 /// Returns an mutable section reference.
@@ -563,14 +587,14 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
563 &mut self,587 &mut self,
564 section_name: &'lookup str,588 section_name: &'lookup str,
565 subsection_name: Option<&'lookup str>,589 subsection_name: Option<&'lookup str>,
566 ) -> Result<MutableSection<'_, 'event>, GitConfigError<'lookup>> {590 ) -> Result<MutableSection<'_, 'event>, lookup::existing::Error> {
567 let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;591 let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
568 let id = section_ids592 let id = section_ids.last().expect("BUG: Section lookup vec was empty");
569 .last()593 Ok(MutableSection::new(
570 .expect("Section lookup vec was empty, internal invariant violated");594 self.sections
571 Ok(MutableSection::new(self.sections.get_mut(id).expect(595 .get_mut(id)
572 "Section did not have id from lookup, internal invariant violated",596 .expect("BUG: Section did not have id from lookup"),
573 )))597 ))
574 }598 }
575 599
576 /// Gets all sections that match the provided name, ignoring any subsections.600 /// Gets all sections that match the provided name, ignoring any subsections.
@@ -591,7 +615,7 @@ impl<'event> GitConfig<'event> { · 14 unmodified lines
591 /// Calling this method will yield all sections:615 /// Calling this method will yield all sections:
592 ///616 ///
593 /// ```617 /// ```
594 /// # use git_config::file::{GitConfig, GitConfigError};618 /// # use git_config::file::{GitConfig};
595 /// # use git_config::values::{Integer, Boolean, TrueVariant};619 /// # use git_config::values::{Integer, Boolean, TrueVariant};
596 /// # use std::borrow::Cow;620 /// # use std::borrow::Cow;
597 /// # use std::convert::TryFrom;621 /// # use std::convert::TryFrom;
@@ -608,7 +632,7 @@ impl<'event> GitConfig<'event> { · 10 unmodified lines
608 /// ```632 /// ```
609 #[must_use]633 #[must_use]
610 pub fn sections_by_name<'lookup>(&self, section_name: &'lookup str) -> Vec<&SectionBody<'event>> {634 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)
612 .unwrap_or_default()636 .unwrap_or_default()
613 .into_iter()637 .into_iter()
614 .map(|id| {638 .map(|id| {
@@ -635,7 +659,7 @@ impl<'event> GitConfig<'event> { · 20 unmodified lines
635 /// Calling this method will yield all section bodies and their header:659 /// Calling this method will yield all section bodies and their header:
636 ///660 ///
637 /// ```rust661 /// ```rust
638 /// use git_config::file::{GitConfig, GitConfigError};662 /// use git_config::file::{GitConfig};
639 /// use git_config::parser::Key;663 /// use git_config::parser::Key;
640 /// use std::borrow::Cow;664 /// use std::borrow::Cow;
641 /// use std::convert::TryFrom;665 /// use std::convert::TryFrom;
@@ -669,7 +693,7 @@ impl<'event> GitConfig<'event> { · 27 unmodified lines
669 &self,693 &self,
670 section_name: &'lookup str,694 section_name: &'lookup str,
671 ) -> Vec<(&ParsedSectionHeader<'event>, &SectionBody<'event>)> {695 ) -> Vec<(&ParsedSectionHeader<'event>, &SectionBody<'event>)> {
672 self.get_section_ids_by_name(section_name)696 self.section_ids_by_name(section_name)
673 .unwrap_or_default()697 .unwrap_or_default()
674 .into_iter()698 .into_iter()
675 .map(|id| {699 .map(|id| {
@@ -694,7 +718,7 @@ impl<'event> GitConfig<'event> { · 18 unmodified lines
694 /// Creating a new empty section:718 /// Creating a new empty section:
695 ///719 ///
696 /// ```720 /// ```
697 /// # use git_config::file::{GitConfig, GitConfigError};721 /// # use git_config::file::{GitConfig};
698 /// # use std::convert::TryFrom;722 /// # use std::convert::TryFrom;
699 /// let mut git_config = GitConfig::new();723 /// let mut git_config = GitConfig::new();
700 /// let _section = git_config.new_section("hello", Some("world".into()));724 /// let _section = git_config.new_section("hello", Some("world".into()));
@@ -704,7 +728,7 @@ impl<'event> GitConfig<'event> { · 3 unmodified lines
704 /// Creating a new empty section and adding values to it:728 /// Creating a new empty section and adding values to it:
705 ///729 ///
706 /// ```730 /// ```
707 /// # use git_config::file::{GitConfig, GitConfigError};731 /// # use git_config::file::{GitConfig};
708 /// # use std::convert::TryFrom;732 /// # use std::convert::TryFrom;
709 /// let mut git_config = GitConfig::new();733 /// let mut git_config = GitConfig::new();
710 /// let mut section = git_config.new_section("hello", Some("world".into()));734 /// let mut section = git_config.new_section("hello", Some("world".into()));
@@ -732,7 +756,7 @@ impl<'event> GitConfig<'event> { · 21 unmodified lines
732 /// Creating and removing a section:756 /// Creating and removing a section:
733 ///757 ///
734 /// ```758 /// ```
735 /// # use git_config::file::{GitConfig, GitConfigError};759 /// # use git_config::file::{GitConfig};
736 /// # use std::convert::TryFrom;760 /// # use std::convert::TryFrom;
737 /// let mut git_config = GitConfig::try_from(761 /// let mut git_config = GitConfig::try_from(
738 /// r#"[hello "world"]762 /// r#"[hello "world"]
@@ -746,7 +770,7 @@ impl<'event> GitConfig<'event> { · 7 unmodified lines
746 /// Precedence example for removing sections with the same name:770 /// Precedence example for removing sections with the same name:
747 ///771 ///
748 /// ```772 /// ```
749 /// # use git_config::file::{GitConfig, GitConfigError};773 /// # use git_config::file::{GitConfig};
750 /// # use std::convert::TryFrom;774 /// # use std::convert::TryFrom;
751 /// let mut git_config = GitConfig::try_from(775 /// let mut git_config = GitConfig::try_from(
752 /// r#"[hello "world"]776 /// r#"[hello "world"]
@@ -764,7 +788,7 @@ impl<'event> GitConfig<'event> { · 11 unmodified lines
764 subsection_name: impl Into<Option<&'lookup str>>,788 subsection_name: impl Into<Option<&'lookup str>>,
765 ) -> Option<SectionBody> {789 ) -> Option<SectionBody> {
766 let id = self790 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())
768 .ok()?792 .ok()?
769 .pop()?;793 .pop()?;
770 self.section_order.remove(794 self.section_order.remove(
@@ -817,8 +841,8 @@ impl<'event> GitConfig<'event> { · 46 unmodified lines
817 subsection_name: impl Into<Option<&'lookup str>>,841 subsection_name: impl Into<Option<&'lookup str>>,
818 new_section_name: impl Into<SectionHeaderName<'event>>,842 new_section_name: impl Into<SectionHeaderName<'event>>,
819 new_subsection_name: impl Into<Option<Cow<'event, str>>>,843 new_subsection_name: impl Into<Option<Cow<'event, str>>>,
820 ) -> Result<(), GitConfigError<'lookup>> {844 ) -> Result<(), lookup::existing::Error> {
821 let id = self.get_section_ids_by_name_and_subname(section_name, subsection_name.into())?;845 let id = self.section_ids_by_name_and_subname(section_name, subsection_name.into())?;
822 let id = id846 let id = id
823 .last()847 .last()
824 .expect("list of sections were empty, which violates invariant");848 .expect("list of sections were empty, which violates invariant");
@@ -855,25 +879,25 @@ impl<'event> GitConfig<'event> { · 30 unmodified lines
855 /// Returns an uninterpreted value given a section, an optional subsection879 /// Returns an uninterpreted value given a section, an optional subsection
856 /// and key.880 /// and key.
857 ///881 ///
858 /// Consider [`Self::get_raw_multi_value`] if you want to get all values of882 /// Consider [`Self::raw_multi_value`] if you want to get all values of
859 /// a multivar instead.883 /// a multivar instead.
860 ///884 ///
861 /// # Errors885 /// # Errors
862 ///886 ///
863 /// This function will return an error if the key is not in the requested887 /// This function will return an error if the key is not in the requested
864 /// section and subsection, or if the section and subsection do not exist.888 /// 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>(
866 &self,890 &self,
867 section_name: &'lookup str,891 section_name: &'lookup str,
868 subsection_name: Option<&'lookup str>,892 subsection_name: Option<&'lookup str>,
869 key: &'lookup str,893 key: &'lookup str,
870 ) -> Result<Cow<'_, [u8]>, GitConfigError<'lookup>> {894 ) -> Result<Cow<'_, [u8]>, lookup::existing::Error> {
871 // Note: cannot wrap around the raw_multi_value method because we need895 // Note: cannot wrap around the raw_multi_value method because we need
872 // to guarantee that the highest section id is used (so that we follow896 // to guarantee that the highest section id is used (so that we follow
873 // the "last one wins" resolution strategy by `git-config`).897 // the "last one wins" resolution strategy by `git-config`).
874 let key = Key(key.into());898 let key = Key(key.into());
875 for section_id in self899 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)?
877 .iter()901 .iter()
878 .rev()902 .rev()
879 {903 {
@@ -887,26 +911,26 @@ impl<'event> GitConfig<'event> { · 7 unmodified lines
887 }911 }
888 }912 }
889 913
890 Err(GitConfigError::KeyDoesNotExist)914 Err(lookup::existing::Error::KeyMissing)
891 }915 }
892 916
893 /// Returns a mutable reference to an uninterpreted value given a section,917 /// Returns a mutable reference to an uninterpreted value given a section,
894 /// an optional subsection and key.918 /// an optional subsection and key.
895 ///919 ///
896 /// Consider [`Self::get_raw_multi_value_mut`] if you want to get mutable920 /// Consider [`Self::raw_multi_value_mut`] if you want to get mutable
897 /// references to all values of a multivar instead.921 /// references to all values of a multivar instead.
898 ///922 ///
899 /// # Errors923 /// # Errors
900 ///924 ///
901 /// This function will return an error if the key is not in the requested925 /// This function will return an error if the key is not in the requested
902 /// section and subsection, or if the section and subsection do not exist.926 /// 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>(
904 &mut self,928 &mut self,
905 section_name: &'lookup str,929 section_name: &'lookup str,
906 subsection_name: Option<&'lookup str>,930 subsection_name: Option<&'lookup str>,
907 key: &'lookup str,931 key: &'lookup str,
908 ) -> Result<MutableValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> {932 ) -> Result<MutableValue<'_, 'lookup, 'event>, lookup::existing::Error> {
909 let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;933 let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
910 let key = Key(key.into());934 let key = Key(key.into());
911 935
912 for section_id in section_ids.iter().rev() {936 for section_id in section_ids.iter().rev() {
@@ -955,7 +979,7 @@ impl<'event> GitConfig<'event> { · 42 unmodified lines
955 ));979 ));
956 }980 }
957 981
958 Err(GitConfigError::KeyDoesNotExist)982 Err(lookup::existing::Error::KeyMissing)
959 }983 }
960 984
961 /// Returns all uninterpreted values given a section, an optional subsection985 /// Returns all uninterpreted values given a section, an optional subsection
@@ -981,16 +1005,16 @@ impl<'event> GitConfig<'event> { · 19 unmodified lines
981 /// # use std::convert::TryFrom;1005 /// # use std::convert::TryFrom;
982 /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();1006 /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
983 /// assert_eq!(1007 /// assert_eq!(
984 /// git_config.get_raw_multi_value("core", None, "a"),1008 /// git_config.raw_multi_value("core", None, "a").unwrap(),
985 /// Ok(vec![1009 /// vec![
986 /// Cow::<[u8]>::Borrowed(b"b"),1010 /// Cow::<[u8]>::Borrowed(b"b"),
987 /// Cow::<[u8]>::Borrowed(b"c"),1011 /// Cow::<[u8]>::Borrowed(b"c"),
988 /// Cow::<[u8]>::Borrowed(b"d"),1012 /// Cow::<[u8]>::Borrowed(b"d"),
989 /// ]),1013 /// ],
990 /// );1014 /// );
991 /// ```1015 /// ```
992 ///1016 ///
993 /// Consider [`Self::get_raw_value`] if you want to get the resolved single1017 /// Consider [`Self::raw_value`] if you want to get the resolved single
994 /// value for a given key, if your key does not support multi-valued values.1018 /// value for a given key, if your key does not support multi-valued values.
995 ///1019 ///
996 /// # Errors1020 /// # Errors
@@ -998,14 +1022,14 @@ impl<'event> GitConfig<'event> { · 1 unmodified line
998 /// This function will return an error if the key is not in any requested1022 /// This function will return an error if the key is not in any requested
999 /// section and subsection, or if no instance of the section and subsections1023 /// section and subsection, or if no instance of the section and subsections
1000 /// exist.1024 /// exist.
1001 pub fn get_raw_multi_value<'lookup>(1025 pub fn raw_multi_value(
1002 &self,1026 &self,
1003 section_name: &'lookup str,1027 section_name: &str,
1004 subsection_name: Option<&'lookup str>,1028 subsection_name: Option<&str>,
1005 key: &'lookup str,1029 key: &str,
1006 ) -> Result<Vec<Cow<'_, [u8]>>, GitConfigError<'lookup>> {1030 ) -> Result<Vec<Cow<'_, [u8]>>, lookup::existing::Error> {
1007 let mut values = vec![];1031 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)? {
1009 values.extend(1033 values.extend(
1010 self.sections1034 self.sections
1011 .get(&section_id)1035 .get(&section_id)
@@ -1017,7 +1041,7 @@ impl<'event> GitConfig<'event> { · 5 unmodified lines
1017 }1041 }
1018 1042
1019 if values.is_empty() {1043 if values.is_empty() {
1020 Err(GitConfigError::KeyDoesNotExist)1044 Err(lookup::existing::Error::KeyMissing)
1021 } else {1045 } else {
1022 Ok(values)1046 Ok(values)
1023 }1047 }
@@ -1041,12 +1065,12 @@ impl<'event> GitConfig<'event> { · 17 unmodified lines
1041 /// Attempting to get all values of `a` yields the following:1065 /// Attempting to get all values of `a` yields the following:
1042 ///1066 ///
1043 /// ```1067 /// ```
1044 /// # use git_config::file::{GitConfig, GitConfigError};1068 /// # use git_config::file::{GitConfig};
1045 /// # use std::borrow::Cow;1069 /// # use std::borrow::Cow;
1046 /// # use std::convert::TryFrom;1070 /// # use std::convert::TryFrom;
1047 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();1071 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
1048 /// assert_eq!(1072 /// assert_eq!(
1049 /// git_config.get_raw_multi_value("core", None, "a")?,1073 /// git_config.raw_multi_value("core", None, "a")?,
1050 /// vec![1074 /// vec![
1051 /// Cow::Borrowed(b"b"),1075 /// Cow::Borrowed(b"b"),
1052 /// Cow::Borrowed(b"c"),1076 /// Cow::Borrowed(b"c"),
@@ -1054,20 +1078,20 @@ impl<'event> GitConfig<'event> { · 1 unmodified line
1054 /// ]1078 /// ]
1055 /// );1079 /// );
1056 ///1080 ///
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");
1058 ///1082 ///
1059 /// assert_eq!(1083 /// assert_eq!(
1060 /// git_config.get_raw_multi_value("core", None, "a")?,1084 /// git_config.raw_multi_value("core", None, "a")?,
1061 /// vec![1085 /// vec![
1062 /// Cow::Borrowed(b"g"),1086 /// Cow::Borrowed(b"g"),
1063 /// Cow::Borrowed(b"g"),1087 /// Cow::Borrowed(b"g"),
1064 /// Cow::Borrowed(b"g")1088 /// Cow::Borrowed(b"g")
1065 /// ],1089 /// ],
1066 /// );1090 /// );
1067 /// # Ok::<(), GitConfigError>(())1091 /// # Ok::<(), git_config::lookup::existing::Error>(())
1068 /// ```1092 /// ```
1069 ///1093 ///
1070 /// Consider [`Self::get_raw_value`] if you want to get the resolved single1094 /// Consider [`Self::raw_value`] if you want to get the resolved single
1071 /// value for a given key, if your key does not support multi-valued values.1095 /// value for a given key, if your key does not support multi-valued values.
1072 ///1096 ///
1073 /// Note that this operation is relatively expensive, requiring a full1097 /// Note that this operation is relatively expensive, requiring a full
@@ -1078,13 +1102,13 @@ impl<'event> GitConfig<'event> { · 4 unmodified lines
1078 /// This function will return an error if the key is not in any requested1102 /// This function will return an error if the key is not in any requested
1079 /// section and subsection, or if no instance of the section and subsections1103 /// section and subsection, or if no instance of the section and subsections
1080 /// exist.1104 /// exist.
1081 pub fn get_raw_multi_value_mut<'lookup>(1105 pub fn raw_multi_value_mut<'lookup>(
1082 &mut self,1106 &mut self,
1083 section_name: &'lookup str,1107 section_name: &'lookup str,
1084 subsection_name: Option<&'lookup str>,1108 subsection_name: Option<&'lookup str>,
1085 key: &'lookup str,1109 key: &'lookup str,
1086 ) -> Result<MutableMultiValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> {1110 ) -> Result<MutableMultiValue<'_, 'lookup, 'event>, lookup::existing::Error> {
1087 let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;1111 let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?;
1088 let key = Key(key.into());1112 let key = Key(key.into());
1089 1113
1090 let mut offsets = HashMap::new();1114 let mut offsets = HashMap::new();
@@ -1125,7 +1149,7 @@ impl<'event> GitConfig<'event> { · 34 unmodified lines
1125 entries.sort();1149 entries.sort();
1126 1150
1127 if entries.is_empty() {1151 if entries.is_empty() {
1128 Err(GitConfigError::KeyDoesNotExist)1152 Err(lookup::existing::Error::KeyMissing)
1129 } else {1153 } else {
1130 Ok(MutableMultiValue::new(&mut self.sections, key, entries, offsets))1154 Ok(MutableMultiValue::new(&mut self.sections, key, entries, offsets))
1131 }1155 }
@@ -1148,13 +1172,13 @@ impl<'event> GitConfig<'event> { · 16 unmodified lines
1148 /// Setting a new value to the key `core.a` will yield the following:1172 /// Setting a new value to the key `core.a` will yield the following:
1149 ///1173 ///
1150 /// ```1174 /// ```
1151 /// # use git_config::file::{GitConfig, GitConfigError};1175 /// # use git_config::file::{GitConfig};
1152 /// # use std::borrow::Cow;1176 /// # use std::borrow::Cow;
1153 /// # use std::convert::TryFrom;1177 /// # use std::convert::TryFrom;
1154 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();1178 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();
1155 /// git_config.set_raw_value("core", None, "a", vec![b'e'])?;1179 /// 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"));1180 /// assert_eq!(git_config.raw_value("core", None, "a")?, Cow::Borrowed(b"e"));
1157 /// # Ok::<(), GitConfigError>(())1181 /// # Ok::<(), Box<dyn std::error::Error>>(())
1158 /// ```1182 /// ```
1159 ///1183 ///
1160 /// # Errors1184 /// # Errors
@@ -1166,8 +1190,8 @@ impl<'event> GitConfig<'event> { · 5 unmodified lines
1166 subsection_name: Option<&'lookup str>,1190 subsection_name: Option<&'lookup str>,
1167 key: &'lookup str,1191 key: &'lookup str,
1168 new_value: Vec<u8>,1192 new_value: Vec<u8>,
1169 ) -> Result<(), GitConfigError<'lookup>> {1193 ) -> Result<(), lookup::existing::Error> {
1170 self.get_raw_value_mut(section_name, subsection_name, key)1194 self.raw_value_mut(section_name, subsection_name, key)
1171 .map(|mut entry| entry.set_bytes(new_value))1195 .map(|mut entry| entry.set_bytes(new_value))
1172 }1196 }
1173 1197
@@ -1180,7 +1204,7 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
1180 ///1204 ///
1181 /// **Note**: Mutation order is _not_ guaranteed and is non-deterministic.1205 /// **Note**: Mutation order is _not_ guaranteed and is non-deterministic.
1182 /// If you need finer control over which values of the multivar are set,1206 /// 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 iterate1207 /// consider using [`raw_multi_value_mut`], which will let you iterate
1184 /// and check over the values instead. This is best used as a convenience1208 /// and check over the values instead. This is best used as a convenience
1185 /// function for setting multivars whose values should be treated as an1209 /// function for setting multivars whose values should be treated as an
1186 /// unordered set.1210 /// unordered set.
@@ -1200,7 +1224,7 @@ impl<'event> GitConfig<'event> { · 13 unmodified lines
1200 /// Setting an equal number of values:1224 /// Setting an equal number of values:
1201 ///1225 ///
1202 /// ```1226 /// ```
1203 /// # use git_config::file::{GitConfig, GitConfigError};1227 /// # use git_config::file::{GitConfig};
1204 /// # use std::borrow::Cow;1228 /// # use std::borrow::Cow;
1205 /// # use std::convert::TryFrom;1229 /// # use std::convert::TryFrom;
1206 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();1230 /// # 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
1210 /// Cow::Borrowed(b"z"),1234 /// Cow::Borrowed(b"z"),
1211 /// ];1235 /// ];
1212 /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?;1236 /// 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")?;
1214 /// assert!(fetched_config.contains(&Cow::Borrowed(b"x")));1238 /// assert!(fetched_config.contains(&Cow::Borrowed(b"x")));
1215 /// assert!(fetched_config.contains(&Cow::Borrowed(b"y")));1239 /// assert!(fetched_config.contains(&Cow::Borrowed(b"y")));
1216 /// assert!(fetched_config.contains(&Cow::Borrowed(b"z")));1240 /// assert!(fetched_config.contains(&Cow::Borrowed(b"z")));
1217 /// # Ok::<(), GitConfigError>(())1241 /// # Ok::<(), git_config::lookup::existing::Error>(())
1218 /// ```1242 /// ```
1219 ///1243 ///
1220 /// Setting less than the number of present values sets the first ones found:1244 /// Setting less than the number of present values sets the first ones found:
1221 ///1245 ///
1222 /// ```1246 /// ```
1223 /// # use git_config::file::{GitConfig, GitConfigError};1247 /// # use git_config::file::{GitConfig};
1224 /// # use std::borrow::Cow;1248 /// # use std::borrow::Cow;
1225 /// # use std::convert::TryFrom;1249 /// # use std::convert::TryFrom;
1226 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();1250 /// # 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
1229 /// Cow::Borrowed(b"y"),1253 /// Cow::Borrowed(b"y"),
1230 /// ];1254 /// ];
1231 /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?;1255 /// 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")?;
1233 /// assert!(fetched_config.contains(&Cow::Borrowed(b"x")));1257 /// assert!(fetched_config.contains(&Cow::Borrowed(b"x")));
1234 /// assert!(fetched_config.contains(&Cow::Borrowed(b"y")));1258 /// assert!(fetched_config.contains(&Cow::Borrowed(b"y")));
1235 /// # Ok::<(), GitConfigError>(())1259 /// # Ok::<(), git_config::lookup::existing::Error>(())
1236 /// ```1260 /// ```
1237 ///1261 ///
1238 /// Setting more than the number of present values discards the rest:1262 /// Setting more than the number of present values discards the rest:
1239 ///1263 ///
1240 /// ```1264 /// ```
1241 /// # use git_config::file::{GitConfig, GitConfigError};1265 /// # use git_config::file::{GitConfig};
1242 /// # use std::borrow::Cow;1266 /// # use std::borrow::Cow;
1243 /// # use std::convert::TryFrom;1267 /// # use std::convert::TryFrom;
1244 /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();1268 /// # 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
1249 /// Cow::Borrowed(b"discarded"),1273 /// Cow::Borrowed(b"discarded"),
1250 /// ];1274 /// ];
1251 /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?;1275 /// 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")));1276 /// assert!(!git_config.raw_multi_value("core", None, "a")?.contains(&Cow::Borrowed(b"discarded")));
1253 /// # Ok::<(), GitConfigError>(())1277 /// # Ok::<(), git_config::lookup::existing::Error>(())
1254 /// ```1278 /// ```
1255 ///1279 ///
1256 /// # Errors1280 /// # Errors
1257 ///1281 ///
1258 /// This errors if any lookup input (section, subsection, and key value) fails.1282 /// This errors if any lookup input (section, subsection, and key value) fails.
1259 ///1283 ///
1260 /// [`get_raw_multi_value_mut`]: Self::get_raw_multi_value_mut1284 /// [`raw_multi_value_mut`]: Self::raw_multi_value_mut
1261 pub fn set_raw_multi_value<'lookup>(1285 pub fn set_raw_multi_value<'lookup>(
1262 &mut self,1286 &mut self,
1263 section_name: &'lookup str,1287 section_name: &'lookup str,
1264 subsection_name: Option<&'lookup str>,1288 subsection_name: Option<&'lookup str>,
1265 key: &'lookup str,1289 key: &'lookup str,
1266 new_values: impl Iterator<Item = Cow<'event, [u8]>>,1290 new_values: impl Iterator<Item = Cow<'event, [u8]>>,
1267 ) -> Result<(), GitConfigError<'lookup>> {1291 ) -> Result<(), lookup::existing::Error> {
1268 self.get_raw_multi_value_mut(section_name, subsection_name, key)1292 self.raw_multi_value_mut(section_name, subsection_name, key)
1269 .map(|mut v| v.set_values(new_values))1293 .map(|mut v| v.set_values(new_values))
1270 }1294 }
1271}1295}
@@ -1321,16 +1345,16 @@ impl<'event> GitConfig<'event> { · 49 unmodified lines
1321 }1345 }
1322 1346
1323 /// Returns the mapping between section and subsection name to section ids.1347 /// 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>(
1325 &self,1349 &self,
1326 section_name: impl Into<SectionHeaderName<'lookup>>,1350 section_name: impl Into<SectionHeaderName<'lookup>>,
1327 subsection_name: Option<&'lookup str>,1351 subsection_name: Option<&'lookup str>,
1328 ) -> Result<Vec<SectionId>, GitConfigError<'lookup>> {1352 ) -> Result<Vec<SectionId>, lookup::existing::Error> {
1329 let section_name = section_name.into();1353 let section_name = section_name.into();
1330 let section_ids = self1354 let section_ids = self
1331 .section_lookup_tree1355 .section_lookup_tree
1332 .get(&section_name)1356 .get(&section_name)
1333 .ok_or(GitConfigError::SectionDoesNotExist(section_name))?;1357 .ok_or(lookup::existing::Error::SectionMissing)?;
1334 let mut maybe_ids = None;1358 let mut maybe_ids = None;
1335 // Don't simplify if and matches here -- the for loop currently needs1359 // Don't simplify if and matches here -- the for loop currently needs
1336 // `n + 1` checks, while the if and matches will result in the for loop1360 // `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
1352 }1376 }
1353 maybe_ids1377 maybe_ids
1354 .map(Vec::to_owned)1378 .map(Vec::to_owned)
1355 .ok_or(GitConfigError::SubSectionDoesNotExist(subsection_name))1379 .ok_or(lookup::existing::Error::SubSectionMissing)
1356 }1380 }
1357 1381
1358 fn get_section_ids_by_name<'lookup>(1382 fn section_ids_by_name<'lookup>(
1359 &self,1383 &self,
1360 section_name: impl Into<SectionHeaderName<'lookup>>,1384 section_name: impl Into<SectionHeaderName<'lookup>>,
1361 ) -> Result<Vec<SectionId>, GitConfigError<'lookup>> {1385 ) -> Result<Vec<SectionId>, lookup::existing::Error> {
1362 let section_name = section_name.into();1386 let section_name = section_name.into();
1363 self.section_lookup_tree1387 self.section_lookup_tree
1364 .get(&section_name)1388 .get(&section_name)
@@ -1371,7 +1395,7 @@ impl<'event> GitConfig<'event> { · 6 unmodified lines
1371 })1395 })
1372 .collect()1396 .collect()
1373 })1397 })
1374 .ok_or(GitConfigError::SectionDoesNotExist(section_name))1398 .ok_or(lookup::existing::Error::SectionMissing)
1375 }1399 }
1376}1400}
1377 1401
@@ -1382,7 +1406,6 @@ impl<'a> TryFrom<&'a str> for GitConfig<'a> { · 4 unmodified lines
1382 /// [`GitConfig`]. See [`parse_from_str`] for more information.1406 /// [`GitConfig`]. See [`parse_from_str`] for more information.
1383 ///1407 ///
1384 /// [`parse_from_str`]: crate::parser::parse_from_str1408 /// [`parse_from_str`]: crate::parser::parse_from_str
1385 #[inline]
1386 fn try_from(s: &'a str) -> Result<GitConfig<'a>, Self::Error> {1409 fn try_from(s: &'a str) -> Result<GitConfig<'a>, Self::Error> {
1387 parse_from_str(s).map(Self::from)1410 parse_from_str(s).map(Self::from)
1388 }1411 }
@@ -1395,7 +1418,6 @@ impl<'a> TryFrom<&'a [u8]> for GitConfig<'a> { · 6 unmodified lines
1395 //// a [`GitConfig`]. See [`parse_from_bytes`] for more information.1418 //// a [`GitConfig`]. See [`parse_from_bytes`] for more information.
1396 ///1419 ///
1397 /// [`parse_from_bytes`]: crate::parser::parse_from_bytes1420 /// [`parse_from_bytes`]: crate::parser::parse_from_bytes
1398 #[inline]
1399 fn try_from(value: &'a [u8]) -> Result<GitConfig<'a>, Self::Error> {1421 fn try_from(value: &'a [u8]) -> Result<GitConfig<'a>, Self::Error> {
1400 parse_from_bytes(value).map(GitConfig::from)1422 parse_from_bytes(value).map(GitConfig::from)
1401 }1423 }
@@ -1408,7 +1430,6 @@ impl<'a> TryFrom<&'a Vec<u8>> for GitConfig<'a> { · 6 unmodified lines
1408 //// a [`GitConfig`]. See [`parse_from_bytes`] for more information.1430 //// a [`GitConfig`]. See [`parse_from_bytes`] for more information.
1409 ///1431 ///
1410 /// [`parse_from_bytes`]: crate::parser::parse_from_bytes1432 /// [`parse_from_bytes`]: crate::parser::parse_from_bytes
1411 #[inline]
1412 fn try_from(value: &'a Vec<u8>) -> Result<GitConfig<'a>, Self::Error> {1433 fn try_from(value: &'a Vec<u8>) -> Result<GitConfig<'a>, Self::Error> {
1413 parse_from_bytes(value).map(GitConfig::from)1434 parse_from_bytes(value).map(GitConfig::from)
1414 }1435 }
@@ -1458,7 +1479,6 @@ impl<'a> From<Parser<'a>> for GitConfig<'a> { · 43 unmodified lines
1458}1479}
1459 1480
1460impl From<GitConfig<'_>> for Vec<u8> {1481impl From<GitConfig<'_>> for Vec<u8> {
1461 #[inline]
1462 fn from(c: GitConfig) -> Self {1482 fn from(c: GitConfig) -> Self {
1463 c.into()1483 c.into()
1464 }1484 }
git-config/src/file/mod.rs+0−2
@@ -1,6 +1,5 @@
1//! This module provides a high level wrapper around a single `git-config` file.1//! This module provides a high level wrapper around a single `git-config` file.
2 2
3mod error;
4mod git_config;3mod git_config;
5mod resolved;4mod resolved;
6mod section;5mod section;
@@ -8,7 +7,6 @@ mod value; · 1 unmodified line
8 7
9use std::ops::{Add, AddAssign};8use std::ops::{Add, AddAssign};
10 9
11pub use error::*;
12pub use resolved::*;10pub use resolved::*;
13pub use section::*;11pub use section::*;
14pub use value::*;12pub use value::*;
git-config/src/file/resolved.rs+0−3
@@ -31,7 +31,6 @@ impl ResolvedGitConfig<'static> { · 30 unmodified lines
31 ///31 ///
32 /// This returns an error if an IO error occurs, or if the file is not a32 /// This returns an error if an IO error occurs, or if the file is not a
33 /// valid `git-config` file.33 /// valid `git-config` file.
34 #[inline]
35 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {34 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {
36 GitConfig::open(path.as_ref()).map(Self::from)35 GitConfig::open(path.as_ref()).map(Self::from)
37 }36 }
@@ -107,14 +106,12 @@ fn resolve_sections<'key, 'data>( · 69 unmodified lines
107impl TryFrom<&Path> for ResolvedGitConfig<'static> {106impl TryFrom<&Path> for ResolvedGitConfig<'static> {
108 type Error = parser::ParserOrIoError<'static>;107 type Error = parser::ParserOrIoError<'static>;
109 108
110 #[inline]
111 fn try_from(path: &Path) -> Result<Self, Self::Error> {109 fn try_from(path: &Path) -> Result<Self, Self::Error> {
112 Self::open(path)110 Self::open(path)
113 }111 }
114}112}
115 113
116impl<'data> From<GitConfig<'data>> for ResolvedGitConfig<'data> {114impl<'data> From<GitConfig<'data>> for ResolvedGitConfig<'data> {
117 #[inline]
118 fn from(config: GitConfig<'data>) -> Self {115 fn from(config: GitConfig<'data>) -> Self {
119 Self::from_config(config)116 Self::from_config(config)
120 }117 }
git-config/src/file/section.rs+13−27
@@ -7,7 +7,8 @@ use std::{ · 6 unmodified lines
7};7};
8 8
9use crate::{9use crate::{
10 file::{error::GitConfigError, Index},10 file::Index,
11 lookup,
11 parser::{Event, Key},12 parser::{Event, Key},
12 values::{normalize_cow, normalize_vec},13 values::{normalize_cow, normalize_vec},
13};14};
@@ -72,7 +73,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 58 unmodified lines
72 /// Returns the previous value if it replaced a value, or None if it adds73 /// Returns the previous value if it replaced a value, or None if it adds
73 /// the value.74 /// the value.
74 pub fn set(&mut self, key: Key<'event>, value: Cow<'event, [u8]>) -> Option<Cow<'event, [u8]>> {75 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);
76 if range.is_empty() {77 if range.is_empty() {
77 self.push(key, value);78 self.push(key, value);
78 return None;79 return None;
@@ -85,7 +86,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 6 unmodified lines
85 86
86 /// Removes the latest value by key and returns it, if it exists.87 /// Removes the latest value by key and returns it, if it exists.
87 pub fn remove(&mut self, key: &Key<'event>) -> Option<Cow<'event, [u8]>> {88 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);
89 if range.is_empty() {90 if range.is_empty() {
90 return None;91 return None;
91 }92 }
@@ -113,14 +114,12 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 21 unmodified lines
113 114
114 /// Adds a new line event. Note that you don't need to call this unless115 /// Adds a new line event. Note that you don't need to call this unless
115 /// you've disabled implicit newlines.116 /// you've disabled implicit newlines.
116 #[inline]
117 pub fn push_newline(&mut self) {117 pub fn push_newline(&mut self) {
118 self.section.0.push(Event::Newline("\n".into()));118 self.section.0.push(Event::Newline("\n".into()));
119 }119 }
120 120
121 /// Enables or disables automatically adding newline events after adding121 /// Enables or disables automatically adding newline events after adding
122 /// a value. This is enabled by default.122 /// a value. This is enabled by default.
123 #[inline]
124 pub fn implicit_newline(&mut self, on: bool) {123 pub fn implicit_newline(&mut self, on: bool) {
125 self.implicit_newline = on;124 self.implicit_newline = on;
126 }125 }
@@ -128,14 +127,12 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 1 unmodified line
128 /// Sets the number of spaces before the start of a key value. By default,127 /// Sets the number of spaces before the start of a key value. By default,
129 /// this is set to two. Set to 0 to disable adding whitespace before a key128 /// this is set to two. Set to 0 to disable adding whitespace before a key
130 /// value.129 /// value.
131 #[inline]
132 pub fn set_whitespace(&mut self, num: usize) {130 pub fn set_whitespace(&mut self, num: usize) {
133 self.whitespace = num;131 self.whitespace = num;
134 }132 }
135 133
136 /// Returns the number of whitespace this section will insert before the134 /// Returns the number of whitespace this section will insert before the
137 /// beginning of a key.135 /// beginning of a key.
138 #[inline]
139 #[must_use]136 #[must_use]
140 pub const fn whitespace(&self) -> usize {137 pub const fn whitespace(&self) -> usize {
141 self.whitespace138 self.whitespace
@@ -144,7 +141,6 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 2 unmodified lines
144 141
145// Internal methods that may require exact indices for faster operations.142// Internal methods that may require exact indices for faster operations.
146impl<'borrow, 'event> MutableSection<'borrow, 'event> {143impl<'borrow, 'event> MutableSection<'borrow, 'event> {
147 #[inline]
148 pub(super) fn new(section: &'borrow mut SectionBody<'event>) -> Self {144 pub(super) fn new(section: &'borrow mut SectionBody<'event>) -> Self {
149 Self {145 Self {
150 section,146 section,
@@ -158,7 +154,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 7 unmodified lines
158 key: &Key<'key>,154 key: &Key<'key>,
159 start: Index,155 start: Index,
160 end: Index,156 end: Index,
161 ) -> Result<Cow<'_, [u8]>, GitConfigError<'key>> {157 ) -> Result<Cow<'_, [u8]>, lookup::existing::Error> {
162 let mut found_key = false;158 let mut found_key = false;
163 let mut latest_value = None;159 let mut latest_value = None;
164 let mut partial_value = None;160 let mut partial_value = None;
@@ -188,10 +184,9 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 23 unmodified lines
188 latest_value184 latest_value
189 .map(normalize_cow)185 .map(normalize_cow)
190 .or_else(|| partial_value.map(normalize_vec))186 .or_else(|| partial_value.map(normalize_vec))
191 .ok_or(GitConfigError::KeyDoesNotExist)187 .ok_or(lookup::existing::Error::KeyMissing)
192 }188 }
193 189
194 #[inline]
195 pub(super) fn delete(&mut self, start: Index, end: Index) {190 pub(super) fn delete(&mut self, start: Index, end: Index) {
196 self.section.0.drain(start.0..=end.0);191 self.section.0.drain(start.0..=end.0);
197 }192 }
@@ -206,7 +201,6 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { · 8 unmodified lines
206impl<'event> Deref for MutableSection<'_, 'event> {201impl<'event> Deref for MutableSection<'_, 'event> {
207 type Target = SectionBody<'event>;202 type Target = SectionBody<'event>;
208 203
209 #[inline]
210 fn deref(&self) -> &Self::Target {204 fn deref(&self) -> &Self::Target {
211 self.section205 self.section
212 }206 }
@@ -227,7 +221,6 @@ impl<'event> SectionBody<'event> { · 14 unmodified lines
227 }221 }
228 222
229 /// Constructs a new empty section body.223 /// Constructs a new empty section body.
230 #[inline]
231 pub(super) fn new() -> Self {224 pub(super) fn new() -> Self {
232 Self::default()225 Self::default()
233 }226 }
@@ -240,7 +233,7 @@ impl<'event> SectionBody<'event> { · 6 unmodified lines
240 #[allow(clippy::missing_panics_doc)]233 #[allow(clippy::missing_panics_doc)]
241 #[must_use]234 #[must_use]
242 pub fn value(&self, key: &Key) -> Option<Cow<'event, [u8]>> {235 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);
244 if range.is_empty() {237 if range.is_empty() {
245 return None;238 return None;
246 }239 }
@@ -276,10 +269,9 @@ impl<'event> SectionBody<'event> { · 29 unmodified lines
276 /// # Errors269 /// # Errors
277 ///270 ///
278 /// Returns an error if the key was not found, or if the conversion failed.271 /// Returns an error if the key was not found, or if the conversion failed.
279 #[inline]272 pub fn value_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<T, lookup::Error<T::Error>> {
280 pub fn value_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<T, GitConfigError<'event>> {273 T::try_from(self.value(key).ok_or(lookup::existing::Error::KeyMissing)?)
281 T::try_from(self.value(key).ok_or(GitConfigError::KeyDoesNotExist)?)274 .map_err(lookup::Error::FailedConversion)
282 .map_err(|_| GitConfigError::FailedConversion)
283 }275 }
284 276
285 /// Retrieves all values that have the provided key name. This may return277 /// Retrieves all values that have the provided key name. This may return
@@ -325,17 +317,15 @@ impl<'event> SectionBody<'event> { · 39 unmodified lines
325 /// # Errors317 /// # Errors
326 ///318 ///
327 /// Returns an error if the conversion failed.319 /// Returns an error if the conversion failed.
328 #[inline]320 pub fn values_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<Vec<T>, lookup::Error<T::Error>> {
329 pub fn values_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<Vec<T>, GitConfigError<'event>> {
330 self.values(key)321 self.values(key)
331 .into_iter()322 .into_iter()
332 .map(T::try_from)323 .map(T::try_from)
333 .collect::<Result<Vec<T>, _>>()324 .collect::<Result<Vec<T>, _>>()
334 .map_err(|_| GitConfigError::FailedConversion)325 .map_err(lookup::Error::FailedConversion)
335 }326 }
336 327
337 /// Returns an iterator visiting all keys in order.328 /// Returns an iterator visiting all keys in order.
338 #[inline]
339 pub fn keys(&self) -> impl Iterator<Item = &Key<'event>> {329 pub fn keys(&self) -> impl Iterator<Item = &Key<'event>> {
340 self.0330 self.0
341 .iter()331 .iter()
@@ -353,14 +343,12 @@ impl<'event> SectionBody<'event> { · 11 unmodified lines
353 }343 }
354 344
355 /// Returns the number of entries in the section.345 /// Returns the number of entries in the section.
356 #[inline]
357 #[must_use]346 #[must_use]
358 pub fn len(&self) -> usize {347 pub fn len(&self) -> usize {
359 self.0.iter().filter(|e| matches!(e, Event::Key(_))).count()348 self.0.iter().filter(|e| matches!(e, Event::Key(_))).count()
360 }349 }
361 350
362 /// Returns if the section is empty.351 /// Returns if the section is empty.
363 #[inline]
364 #[must_use]352 #[must_use]
365 pub fn is_empty(&self) -> bool {353 pub fn is_empty(&self) -> bool {
366 self.0.is_empty()354 self.0.is_empty()
@@ -368,7 +356,7 @@ impl<'event> SectionBody<'event> { · 1 unmodified line
368 356
369 /// Returns the the range containing the value events for the section.357 /// Returns the the range containing the value events for the section.
370 /// If the value is not found, then this returns an empty range.358 /// 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> {
372 let mut values_start = 0;360 let mut values_start = 0;
373 // value end needs to be offset by one so that the last value's index361 // value end needs to be offset by one so that the last value's index
374 // is included in the range362 // is included in the range
@@ -406,7 +394,6 @@ impl<'event> IntoIterator for SectionBody<'event> { · 31 unmodified lines
406 394
407 type IntoIter = SectionBodyIter<'event>;395 type IntoIter = SectionBodyIter<'event>;
408 396
409 #[inline]
410 fn into_iter(self) -> Self::IntoIter {397 fn into_iter(self) -> Self::IntoIter {
411 SectionBodyIter(self.0.into())398 SectionBodyIter(self.0.into())
412 }399 }
@@ -448,7 +435,6 @@ impl<'event> Iterator for SectionBodyIter<'event> { · 35 unmodified lines
448impl FusedIterator for SectionBodyIter<'_> {}435impl FusedIterator for SectionBodyIter<'_> {}
449 436
450impl<'event> From<Vec<Event<'event>>> for SectionBody<'event> {437impl<'event> From<Vec<Event<'event>>> for SectionBody<'event> {
451 #[inline]
452 fn from(e: Vec<Event<'event>>) -> Self {438 fn from(e: Vec<Event<'event>>) -> Self {
453 Self(e)439 Self(e)
454 }440 }
git-config/src/file/value.rs+9−21
@@ -6,11 +6,11 @@ use std::{ · 5 unmodified lines
6 6
7use crate::{7use crate::{
8 file::{8 file::{
9 error::GitConfigError,
10 git_config::SectionId,9 git_config::SectionId,
11 section::{MutableSection, SectionBody},10 section::{MutableSection, SectionBody},
12 Index, Size,11 Index, Size,
13 },12 },
13 lookup,
14 parser::{Event, Key},14 parser::{Event, Key},
15 values::{normalize_bytes, normalize_vec},15 values::{normalize_bytes, normalize_vec},
16};16};
@@ -54,15 +54,13 @@ impl<'borrow, 'lookup, 'event> MutableValue<'borrow, 'lookup, 'event> { · 37 unmodified lines
54 /// # Errors54 /// # Errors
55 ///55 ///
56 /// Returns an error if the lookup failed.56 /// Returns an error if the lookup failed.
57 #[inline]57 pub fn get(&self) -> Result<Cow<'_, [u8]>, lookup::existing::Error> {
58 pub fn get(&self) -> Result<Cow<'_, [u8]>, GitConfigError> {
59 self.section.get(&self.key, self.index, self.index + self.size)58 self.section.get(&self.key, self.index, self.index + self.size)
60 }59 }
61 60
62 /// Update the value to the provided one. This modifies the value such that61 /// Update the value to the provided one. This modifies the value such that
63 /// the Value event(s) are replaced with a single new event containing the62 /// the Value event(s) are replaced with a single new event containing the
64 /// new value.63 /// new value.
65 #[inline]
66 pub fn set_string(&mut self, input: String) {64 pub fn set_string(&mut self, input: String) {
67 self.set_bytes(input.into_bytes());65 self.set_bytes(input.into_bytes());
68 }66 }
@@ -149,7 +147,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 80 unmodified lines
149 /// # Errors147 /// # Errors
150 ///148 ///
151 /// Returns an error if the lookup failed.149 /// 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> {
153 let mut found_key = false;151 let mut found_key = false;
154 let mut values = vec![];152 let mut values = vec![];
155 let mut partial_value = None;153 let mut partial_value = None;
@@ -160,7 +158,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 4 unmodified lines
160 offset_index,158 offset_index,
161 } in &self.indices_and_sizes159 } in &self.indices_and_sizes
162 {160 {
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);
164 for event in &self162 for event in &self
165 .section163 .section
166 .get(section_id)164 .get(section_id)
@@ -190,14 +188,13 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 23 unmodified lines
190 }188 }
191 189
192 if values.is_empty() {190 if values.is_empty() {
193 return Err(GitConfigError::KeyDoesNotExist);191 return Err(lookup::existing::Error::KeyMissing);
194 }192 }
195 193
196 Ok(values)194 Ok(values)
197 }195 }
198 196
199 /// Returns the size of values the multivar has.197 /// Returns the size of values the multivar has.
200 #[inline]
201 #[must_use]198 #[must_use]
202 pub fn len(&self) -> usize {199 pub fn len(&self) -> usize {
203 self.indices_and_sizes.len()200 self.indices_and_sizes.len()
@@ -205,7 +202,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 1 unmodified line
205 202
206 /// Returns if the multivar has any values. This might occur if the value203 /// Returns if the multivar has any values. This might occur if the value
207 /// was deleted but not set with a new value.204 /// was deleted but not set with a new value.
208 #[inline]
209 #[must_use]205 #[must_use]
210 pub fn is_empty(&self) -> bool {206 pub fn is_empty(&self) -> bool {
211 self.indices_and_sizes.is_empty()207 self.indices_and_sizes.is_empty()
@@ -216,7 +212,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 4 unmodified lines
216 /// # Safety212 /// # Safety
217 ///213 ///
218 /// This will panic if the index is out of range.214 /// This will panic if the index is out of range.
219 #[inline]
220 pub fn set_string(&mut self, index: usize, input: String) {215 pub fn set_string(&mut self, index: usize, input: String) {
221 self.set_bytes(index, input.into_bytes());216 self.set_bytes(index, input.into_bytes());
222 }217 }
@@ -226,7 +221,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 3 unmodified lines
226 /// # Safety221 /// # Safety
227 ///222 ///
228 /// This will panic if the index is out of range.223 /// This will panic if the index is out of range.
229 #[inline]
230 pub fn set_bytes(&mut self, index: usize, input: Vec<u8>) {224 pub fn set_bytes(&mut self, index: usize, input: Vec<u8>) {
231 self.set_value(index, Cow::Owned(input));225 self.set_value(index, Cow::Owned(input));
232 }226 }
@@ -260,7 +254,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 27 unmodified lines
260 /// remaining values are ignored.254 /// remaining values are ignored.
261 ///255 ///
262 /// [`zip`]: std::iter::Iterator::zip256 /// [`zip`]: std::iter::Iterator::zip
263 #[inline]
264 pub fn set_values<'a: 'event>(&mut self, input: impl Iterator<Item = Cow<'a, [u8]>>) {257 pub fn set_values<'a: 'event>(&mut self, input: impl Iterator<Item = Cow<'a, [u8]>>) {
265 for (258 for (
266 EntryData {259 EntryData {
@@ -285,14 +278,12 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 18 unmodified lines
285 278
286 /// Sets all values in this multivar to the provided one by copying the279 /// Sets all values in this multivar to the provided one by copying the
287 /// input for all values.280 /// input for all values.
288 #[inline]
289 pub fn set_str_all(&mut self, input: &str) {281 pub fn set_str_all(&mut self, input: &str) {
290 self.set_owned_values_all(input.as_bytes());282 self.set_owned_values_all(input.as_bytes());
291 }283 }
292 284
293 /// Sets all values in this multivar to the provided one by copying the285 /// Sets all values in this multivar to the provided one by copying the
294 /// input bytes for all values.286 /// input bytes for all values.
295 #[inline]
296 pub fn set_owned_values_all(&mut self, input: &[u8]) {287 pub fn set_owned_values_all(&mut self, input: &[u8]) {
297 for EntryData {288 for EntryData {
298 section_id,289 section_id,
@@ -319,7 +310,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 20 unmodified lines
319 /// need for a more ergonomic interface.310 /// need for a more ergonomic interface.
320 ///311 ///
321 /// [`GitConfig`]: super::GitConfig312 /// [`GitConfig`]: super::GitConfig
322 #[inline]
323 pub fn set_values_all<'a: 'event>(&mut self, input: &'a [u8]) {313 pub fn set_values_all<'a: 'event>(&mut self, input: &'a [u8]) {
324 for EntryData {314 for EntryData {
325 section_id,315 section_id,
@@ -347,7 +337,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 21 unmodified lines
347 offset_index: usize,337 offset_index: usize,
348 input: Cow<'a, [u8]>,338 input: Cow<'a, [u8]>,
349 ) {339 ) {
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);
351 section.as_mut().drain(offset..offset + size);341 section.as_mut().drain(offset..offset + size);
352 342
353 MutableMultiValue::set_offset(offsets, section_id, offset_index, 3);343 MutableMultiValue::set_offset(offsets, section_id, offset_index, 3);
@@ -369,7 +359,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 15 unmodified lines
369 section_id,359 section_id,
370 offset_index,360 offset_index,
371 } = &self.indices_and_sizes[index];361 } = &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);
373 if size > 0 {363 if size > 0 {
374 self.section364 self.section
375 .get_mut(section_id)365 .get_mut(section_id)
@@ -390,7 +380,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 14 unmodified lines
390 offset_index,380 offset_index,
391 } in &self.indices_and_sizes381 } in &self.indices_and_sizes
392 {382 {
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);
394 if size > 0 {384 if size > 0 {
395 self.section385 self.section
396 .get_mut(section_id)386 .get_mut(section_id)
@@ -405,8 +395,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 8 unmodified lines
405 395
406 // SectionId is the same size as a reference, which means it's just as396 // SectionId is the same size as a reference, which means it's just as
407 // efficient passing in a value instead of a reference.397 // efficient passing in a value instead of a reference.
408 #[inline]398 fn index_and_size(
409 fn get_index_and_size(
410 offsets: &'lookup HashMap<SectionId, Vec<usize>>,399 offsets: &'lookup HashMap<SectionId, Vec<usize>>,
411 section_id: SectionId,400 section_id: SectionId,
412 offset_index: usize,401 offset_index: usize,
@@ -424,7 +413,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { · 11 unmodified lines
424 //413 //
425 // SectionId is the same size as a reference, which means it's just as414 // SectionId is the same size as a reference, which means it's just as
426 // efficient passing in a value instead of a reference.415 // efficient passing in a value instead of a reference.
427 #[inline]
428 fn set_offset(416 fn set_offset(
429 offsets: &mut HashMap<SectionId, Vec<usize>>,417 offsets: &mut HashMap<SectionId, Vec<usize>>,
430 section_id: SectionId,418 section_id: SectionId,
git-config/src/fs.rs+6−8
@@ -7,7 +7,8 @@ use std::{ · 6 unmodified lines
7 path::{Path, PathBuf},7 path::{Path, PathBuf},
8};8};
9 9
10use crate::file::{from_paths, GitConfig, GitConfigError};10use crate::file::{from_paths, GitConfig};
11use crate::lookup;
11 12
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]13#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
13pub enum ConfigSource {14pub enum ConfigSource {
@@ -40,7 +41,6 @@ pub struct ConfigBuilder { · 26 unmodified lines
40 41
41impl ConfigBuilder {42impl ConfigBuilder {
42 /// Constructs a new builder that finds the default location43 /// Constructs a new builder that finds the default location
43 #[inline]
44 #[must_use]44 #[must_use]
45 pub fn new() -> Self {45 pub fn new() -> Self {
46 Self {46 Self {
@@ -146,7 +146,6 @@ pub struct Config<'config> { · 99 unmodified lines
146}146}
147 147
148impl<'config> Config<'config> {148impl<'config> Config<'config> {
149 #[inline]
150 #[must_use]149 #[must_use]
151 pub fn value<T: TryFrom<Cow<'config, [u8]>>>(150 pub fn value<T: TryFrom<Cow<'config, [u8]>>>(
152 &'config self,151 &'config self,
@@ -177,13 +176,12 @@ impl<'config> Config<'config> { · 24 unmodified lines
177 None176 None
178 }177 }
179 178
180 #[inline]
181 pub fn try_value<'lookup, T: TryFrom<Cow<'config, [u8]>>>(179 pub fn try_value<'lookup, T: TryFrom<Cow<'config, [u8]>>>(
182 &'config self,180 &'config self,
183 section_name: &'lookup str,181 section_name: &'lookup str,
184 subsection_name: Option<&'lookup str>,182 subsection_name: Option<&'lookup str>,
185 key: &'lookup str,183 key: &'lookup str,
186 ) -> Result<Option<T>, GitConfigError<'lookup>> {184 ) -> Result<Option<T>, lookup::Error<T::Error>> {
187 self.try_value_with_source(section_name, subsection_name, key)185 self.try_value_with_source(section_name, subsection_name, key)
188 .map(|res| res.map(|(value, _)| value))186 .map(|res| res.map(|(value, _)| value))
189 }187 }
@@ -197,7 +195,7 @@ impl<'config> Config<'config> { · 7 unmodified lines
197 section_name: &'lookup str,195 section_name: &'lookup str,
198 subsection_name: Option<&'lookup str>,196 subsection_name: Option<&'lookup str>,
199 key: &'lookup str,197 key: &'lookup str,
200 ) -> Result<Option<(T, ConfigSource)>, GitConfigError<'lookup>> {198 ) -> Result<Option<(T, ConfigSource)>, lookup::Error<T::Error>> {
201 let mapping = self.mapping();199 let mapping = self.mapping();
202 200
203 for (conf, source) in mapping.iter() {201 for (conf, source) in mapping.iter() {
@@ -227,7 +225,7 @@ impl<'config> Config<'config> { · 23 unmodified lines
227 /// Retrieves the underlying [`GitConfig`] object, if one was found during225 /// Retrieves the underlying [`GitConfig`] object, if one was found during
228 /// initialization.226 /// initialization.
229 #[must_use]227 #[must_use]
230 pub fn get_config(&self, source: ConfigSource) -> Option<&GitConfig<'config>> {228 pub fn config(&self, source: ConfigSource) -> Option<&GitConfig<'config>> {
231 match source {229 match source {
232 ConfigSource::System => self.system_conf.as_ref(),230 ConfigSource::System => self.system_conf.as_ref(),
233 ConfigSource::Global => self.global_conf.as_ref(),231 ConfigSource::Global => self.global_conf.as_ref(),
@@ -241,7 +239,7 @@ impl<'config> Config<'config> { · 7 unmodified lines
241 /// Retrieves the underlying [`GitConfig`] object as a mutable reference,239 /// Retrieves the underlying [`GitConfig`] object as a mutable reference,
242 /// if one was found during initialization.240 /// if one was found during initialization.
243 #[must_use]241 #[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>> {
245 match source {243 match source {
246 ConfigSource::System => self.system_conf.as_mut(),244 ConfigSource::System => self.system_conf.as_mut(),
247 ConfigSource::Global => self.global_conf.as_mut(),245 ConfigSource::Global => self.global_conf.as_mut(),
git-config/src/lib.rs+57−0
@@ -54,10 +54,67 @@ · 53 unmodified lines
54#[cfg(feature = "serde")]54#[cfg(feature = "serde")]
55extern crate serde_crate as serde;55extern crate serde_crate as serde;
56 56
57pub 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
57pub mod file;82pub mod file;
58pub mod fs;83pub mod fs;
59pub mod parser;84pub mod parser;
60pub mod values;85pub mod values;
86/// The future home of the `values` module (TODO).
87pub 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}
61 118
62mod permissions {119mod permissions {
63 use crate::Permissions;120 use crate::Permissions;
git-config/src/parser.rs+0−36
@@ -75,7 +75,6 @@ impl Event<'_> { · 74 unmodified lines
75 /// Generates a byte representation of the value. This should be used when75 /// Generates a byte representation of the value. This should be used when
76 /// non-UTF-8 sequences are present or a UTF-8 representation can't be76 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
77 /// guaranteed.77 /// guaranteed.
78 #[inline]
79 #[must_use]78 #[must_use]
80 pub fn to_vec(&self) -> Vec<u8> {79 pub fn to_vec(&self) -> Vec<u8> {
81 self.into()80 self.into()
@@ -95,7 +94,6 @@ impl Event<'_> { · 13 unmodified lines
95 /// not.94 /// not.
96 ///95 ///
97 /// [`clone`]: Self::clone96 /// [`clone`]: Self::clone
98 #[inline]
99 #[must_use]97 #[must_use]
100 pub fn to_owned(&self) -> Event<'static> {98 pub fn to_owned(&self) -> Event<'static> {
101 match self {99 match self {
@@ -116,7 +114,6 @@ impl Display for Event<'_> { · 14 unmodified lines
116 /// Note that this is a best-effort attempt at printing an `Event`. If114 /// Note that this is a best-effort attempt at printing an `Event`. If
117 /// there are non UTF-8 values in your config, this will _NOT_ render115 /// there are non UTF-8 values in your config, this will _NOT_ render
118 /// as read.116 /// as read.
119 #[inline]
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {118 match self {
122 Self::Value(e) | Self::ValueNotDone(e) | Self::ValueDone(e) => match std::str::from_utf8(e) {119 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
133}130}
134 131
135impl From<Event<'_>> for Vec<u8> {132impl From<Event<'_>> for Vec<u8> {
136 #[inline]
137 fn from(event: Event) -> Self {133 fn from(event: Event) -> Self {
138 event.into()134 event.into()
139 }135 }
140}136}
141 137
142impl From<&Event<'_>> for Vec<u8> {138impl From<&Event<'_>> for Vec<u8> {
143 #[inline]
144 fn from(event: &Event) -> Self {139 fn from(event: &Event) -> Self {
145 match event {140 match event {
146 Event::Value(e) | Event::ValueNotDone(e) | Event::ValueDone(e) => e.to_vec(),141 Event::Value(e) | Event::ValueNotDone(e) | Event::ValueDone(e) => e.to_vec(),
@@ -177,7 +172,6 @@ impl ParsedSection<'_> { · 30 unmodified lines
177 /// not.172 /// not.
178 ///173 ///
179 /// [`clone`]: Self::clone174 /// [`clone`]: Self::clone
180 #[inline]
181 #[must_use]175 #[must_use]
182 pub fn to_owned(&self) -> ParsedSection<'static> {176 pub fn to_owned(&self) -> ParsedSection<'static> {
183 ParsedSection {177 ParsedSection {
@@ -218,7 +212,6 @@ macro_rules! generate_case_insensitive { · 34 unmodified lines
218 /// while `clone` does not.212 /// while `clone` does not.
219 ///213 ///
220 /// [`clone`]: Self::clone214 /// [`clone`]: Self::clone
221 #[inline]
222 #[must_use]215 #[must_use]
223 pub fn to_owned(&self) -> $name<'static> {216 pub fn to_owned(&self) -> $name<'static> {
224 $name(Cow::Owned(self.0.clone().into_owned()))217 $name(Cow::Owned(self.0.clone().into_owned()))
@@ -226,21 +219,18 @@ macro_rules! generate_case_insensitive { · 1 unmodified line
226 }219 }
227 220
228 impl PartialEq for $name<'_> {221 impl PartialEq for $name<'_> {
229 #[inline]
230 fn eq(&self, other: &Self) -> bool {222 fn eq(&self, other: &Self) -> bool {
231 self.0.eq_ignore_ascii_case(&other.0)223 self.0.eq_ignore_ascii_case(&other.0)
232 }224 }
233 }225 }
234 226
235 impl Display for $name<'_> {227 impl Display for $name<'_> {
236 #[inline]
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 self.0.fmt(f)229 self.0.fmt(f)
239 }230 }
240 }231 }
241 232
242 impl PartialOrd for $name<'_> {233 impl PartialOrd for $name<'_> {
243 #[inline]
244 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {234 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
245 self.0235 self.0
246 .to_ascii_lowercase()236 .to_ascii_lowercase()
@@ -249,21 +239,18 @@ macro_rules! generate_case_insensitive { · 2 unmodified lines
249 }239 }
250 240
251 impl std::hash::Hash for $name<'_> {241 impl std::hash::Hash for $name<'_> {
252 #[inline]
253 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {242 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
254 self.0.to_ascii_lowercase().hash(state)243 self.0.to_ascii_lowercase().hash(state)
255 }244 }
256 }245 }
257 246
258 impl<'a> From<&'a str> for $name<'a> {247 impl<'a> From<&'a str> for $name<'a> {
259 #[inline]
260 fn from(s: &'a str) -> Self {248 fn from(s: &'a str) -> Self {
261 Self(Cow::Borrowed(s))249 Self(Cow::Borrowed(s))
262 }250 }
263 }251 }
264 252
265 impl<'a> From<Cow<'a, str>> for $name<'a> {253 impl<'a> From<Cow<'a, str>> for $name<'a> {
266 #[inline]
267 fn from(s: Cow<'a, str>) -> Self {254 fn from(s: Cow<'a, str>) -> Self {
268 Self(s)255 Self(s)
269 }256 }
@@ -272,7 +259,6 @@ macro_rules! generate_case_insensitive { · 2 unmodified lines
272 impl<'a> std::ops::Deref for $name<'a> {259 impl<'a> std::ops::Deref for $name<'a> {
273 type Target = $cow_inner_type;260 type Target = $cow_inner_type;
274 261
275 #[inline]
276 fn deref(&self) -> &Self::Target {262 fn deref(&self) -> &Self::Target {
277 &self.0263 &self.0
278 }264 }
@@ -316,7 +302,6 @@ impl ParsedSectionHeader<'_> { · 37 unmodified lines
316 /// non-UTF-8 sequences are present or a UTF-8 representation can't be302 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
317 /// guaranteed.303 /// guaranteed.
318 #[must_use]304 #[must_use]
319 #[inline]
320 pub fn to_vec(&self) -> Vec<u8> {305 pub fn to_vec(&self) -> Vec<u8> {
321 self.into()306 self.into()
322 }307 }
@@ -335,7 +320,6 @@ impl ParsedSectionHeader<'_> { · 12 unmodified lines
335 /// not.320 /// not.
336 ///321 ///
337 /// [`clone`]: Self::clone322 /// [`clone`]: Self::clone
338 #[inline]
339 #[must_use]323 #[must_use]
340 pub fn to_owned(&self) -> ParsedSectionHeader<'static> {324 pub fn to_owned(&self) -> ParsedSectionHeader<'static> {
341 ParsedSectionHeader {325 ParsedSectionHeader {
@@ -366,21 +350,18 @@ impl Display for ParsedSectionHeader<'_> { · 24 unmodified lines
366}350}
367 351
368impl From<ParsedSectionHeader<'_>> for Vec<u8> {352impl From<ParsedSectionHeader<'_>> for Vec<u8> {
369 #[inline]
370 fn from(header: ParsedSectionHeader) -> Self {353 fn from(header: ParsedSectionHeader) -> Self {
371 header.into()354 header.into()
372 }355 }
373}356}
374 357
375impl From<&ParsedSectionHeader<'_>> for Vec<u8> {358impl From<&ParsedSectionHeader<'_>> for Vec<u8> {
376 #[inline]
377 fn from(header: &ParsedSectionHeader) -> Self {359 fn from(header: &ParsedSectionHeader) -> Self {
378 header.to_string().into_bytes()360 header.to_string().into_bytes()
379 }361 }
380}362}
381 363
382impl<'a> From<ParsedSectionHeader<'a>> for Event<'a> {364impl<'a> From<ParsedSectionHeader<'a>> for Event<'a> {
383 #[inline]
384 fn from(header: ParsedSectionHeader) -> Event {365 fn from(header: ParsedSectionHeader) -> Event {
385 Event::SectionHeader(header)366 Event::SectionHeader(header)
386 }367 }
@@ -410,7 +391,6 @@ impl ParsedComment<'_> { · 23 unmodified lines
410 /// not.391 /// not.
411 ///392 ///
412 /// [`clone`]: Self::clone393 /// [`clone`]: Self::clone
413 #[inline]
414 #[must_use]394 #[must_use]
415 pub fn to_owned(&self) -> ParsedComment<'static> {395 pub fn to_owned(&self) -> ParsedComment<'static> {
416 ParsedComment {396 ParsedComment {
@@ -435,14 +415,12 @@ impl Display for ParsedComment<'_> { · 18 unmodified lines
435}415}
436 416
437impl From<ParsedComment<'_>> for Vec<u8> {417impl From<ParsedComment<'_>> for Vec<u8> {
438 #[inline]
439 fn from(c: ParsedComment) -> Self {418 fn from(c: ParsedComment) -> Self {
440 c.into()419 c.into()
441 }420 }
442}421}
443 422
444impl From<&ParsedComment<'_>> for Vec<u8> {423impl From<&ParsedComment<'_>> for Vec<u8> {
445 #[inline]
446 fn from(c: &ParsedComment) -> Self {424 fn from(c: &ParsedComment) -> Self {
447 let mut values = vec![c.comment_tag as u8];425 let mut values = vec![c.comment_tag as u8];
448 values.extend(c.comment.iter());426 values.extend(c.comment.iter());
@@ -463,14 +441,12 @@ pub struct Error<'a> { · 14 unmodified lines
463impl Error<'_> {441impl Error<'_> {
464 /// The one-indexed line number where the error occurred. This is determined442 /// The one-indexed line number where the error occurred. This is determined
465 /// by the number of newlines that were successfully parsed.443 /// by the number of newlines that were successfully parsed.
466 #[inline]
467 #[must_use]444 #[must_use]
468 pub const fn line_number(&self) -> usize {445 pub const fn line_number(&self) -> usize {
469 self.line_number + 1446 self.line_number + 1
470 }447 }
471 448
472 /// The remaining data that was left unparsed.449 /// The remaining data that was left unparsed.
473 #[inline]
474 #[must_use]450 #[must_use]
475 pub fn remaining_data(&self) -> &[u8] {451 pub fn remaining_data(&self) -> &[u8] {
476 &self.parsed_until452 &self.parsed_until
@@ -490,7 +466,6 @@ impl Error<'_> { · 13 unmodified lines
490 /// not.466 /// not.
491 ///467 ///
492 /// [`clone`]: std::clone::Clone::clone468 /// [`clone`]: std::clone::Clone::clone
493 #[inline]
494 #[must_use]469 #[must_use]
495 pub fn to_owned(&self) -> Error<'static> {470 pub fn to_owned(&self) -> Error<'static> {
496 Error {471 Error {
@@ -567,7 +542,6 @@ impl ParserOrIoError<'_> { · 70 unmodified lines
567}542}
568 543
569impl Display for ParserOrIoError<'_> {544impl Display for ParserOrIoError<'_> {
570 #[inline]
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {545 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 match self {546 match self {
573 ParserOrIoError::Parser(e) => e.fmt(f),547 ParserOrIoError::Parser(e) => e.fmt(f),
@@ -577,7 +551,6 @@ impl Display for ParserOrIoError<'_> { · 3 unmodified lines
577}551}
578 552
579impl From<std::io::Error> for ParserOrIoError<'_> {553impl From<std::io::Error> for ParserOrIoError<'_> {
580 #[inline]
581 fn from(e: std::io::Error) -> Self {554 fn from(e: std::io::Error) -> Self {
582 Self::Io(e)555 Self::Io(e)
583 }556 }
@@ -594,7 +567,6 @@ enum ParserNode { · 10 unmodified lines
594}567}
595 568
596impl Display for ParserNode {569impl Display for ParserNode {
597 #[inline]
598 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
599 match self {571 match self {
600 Self::SectionHeader => write!(f, "section header"),572 Self::SectionHeader => write!(f, "section header"),
@@ -822,7 +794,6 @@ impl<'a> Parser<'a> { · 221 unmodified lines
822 /// a section) from the parser. Consider [`Parser::take_frontmatter`] if794 /// a section) from the parser. Consider [`Parser::take_frontmatter`] if
823 /// you need an owned copy only once. If that function was called, then this795 /// you need an owned copy only once. If that function was called, then this
824 /// will always return an empty slice.796 /// will always return an empty slice.
825 #[inline]
826 #[must_use]797 #[must_use]
827 pub fn frontmatter(&self) -> &[Event<'a>] {798 pub fn frontmatter(&self) -> &[Event<'a>] {
828 &self.frontmatter799 &self.frontmatter
@@ -832,7 +803,6 @@ impl<'a> Parser<'a> { · 3 unmodified lines
832 /// a section) from the parser. Subsequent calls will return an empty vec.803 /// a section) from the parser. Subsequent calls will return an empty vec.
833 /// Consider [`Parser::frontmatter`] if you only need a reference to the804 /// Consider [`Parser::frontmatter`] if you only need a reference to the
834 /// frontmatter805 /// frontmatter
835 #[inline]
836 pub fn take_frontmatter(&mut self) -> Vec<Event<'a>> {806 pub fn take_frontmatter(&mut self) -> Vec<Event<'a>> {
837 std::mem::take(&mut self.frontmatter)807 std::mem::take(&mut self.frontmatter)
838 }808 }
@@ -840,7 +810,6 @@ impl<'a> Parser<'a> { · 1 unmodified line
840 /// Returns the parsed sections from the parser. Consider810 /// Returns the parsed sections from the parser. Consider
841 /// [`Parser::take_sections`] if you need an owned copy only once. If that811 /// [`Parser::take_sections`] if you need an owned copy only once. If that
842 /// function was called, then this will always return an empty slice.812 /// function was called, then this will always return an empty slice.
843 #[inline]
844 #[must_use]813 #[must_use]
845 pub fn sections(&self) -> &[ParsedSection<'a>] {814 pub fn sections(&self) -> &[ParsedSection<'a>] {
846 &self.sections815 &self.sections
@@ -849,7 +818,6 @@ impl<'a> Parser<'a> { · 2 unmodified lines
849 /// Takes the parsed sections from the parser. Subsequent calls will return818 /// Takes the parsed sections from the parser. Subsequent calls will return
850 /// an empty vec. Consider [`Parser::sections`] if you only need a reference819 /// an empty vec. Consider [`Parser::sections`] if you only need a reference
851 /// to the comments.820 /// to the comments.
852 #[inline]
853 pub fn take_sections(&mut self) -> Vec<ParsedSection<'a>> {821 pub fn take_sections(&mut self) -> Vec<ParsedSection<'a>> {
854 let mut to_return = vec![];822 let mut to_return = vec![];
855 std::mem::swap(&mut self.sections, &mut to_return);823 std::mem::swap(&mut self.sections, &mut to_return);
@@ -857,7 +825,6 @@ impl<'a> Parser<'a> { · 1 unmodified line
857 }825 }
858 826
859 /// Consumes the parser to produce a Vec of Events.827 /// Consumes the parser to produce a Vec of Events.
860 #[inline]
861 #[must_use]828 #[must_use]
862 pub fn into_vec(self) -> Vec<Event<'a>> {829 pub fn into_vec(self) -> Vec<Event<'a>> {
863 self.into_iter().collect()830 self.into_iter().collect()
@@ -878,7 +845,6 @@ impl<'a> Parser<'a> { · 14 unmodified lines
878impl<'a> TryFrom<&'a str> for Parser<'a> {845impl<'a> TryFrom<&'a str> for Parser<'a> {
879 type Error = Error<'a>;846 type Error = Error<'a>;
880 847
881 #[inline]
882 fn try_from(value: &'a str) -> Result<Self, Self::Error> {848 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
883 parse_from_str(value)849 parse_from_str(value)
884 }850 }
@@ -887,7 +853,6 @@ impl<'a> TryFrom<&'a str> for Parser<'a> { · 2 unmodified lines
887impl<'a> TryFrom<&'a [u8]> for Parser<'a> {853impl<'a> TryFrom<&'a [u8]> for Parser<'a> {
888 type Error = Error<'a>;854 type Error = Error<'a>;
889 855
890 #[inline]
891 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {856 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
892 parse_from_bytes(value)857 parse_from_bytes(value)
893 }858 }
@@ -925,7 +890,6 @@ pub fn parse_from_path<P: AsRef<Path>>(path: P) -> Result<Parser<'static>, Parse · 31 unmodified lines
925/// Returns an error if the string provided is not a valid `git-config`.890/// Returns an error if the string provided is not a valid `git-config`.
926/// This generally is due to either invalid names or if there's extraneous891/// This generally is due to either invalid names or if there's extraneous
927/// data succeeding valid `git-config` data.892/// data succeeding valid `git-config` data.
928#[inline]
929pub fn parse_from_str(input: &str) -> Result<Parser, Error> {893pub fn parse_from_str(input: &str) -> Result<Parser, Error> {
930 parse_from_bytes(input.as_bytes())894 parse_from_bytes(input.as_bytes())
931}895}
git-config/src/values.rs+101−148
@@ -2,8 +2,8 @@ · 1 unmodified line
2 2
3use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr};3use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr};
4 4
5use bstr::BStr;5use crate::value;
6use quick_error::quick_error;6use bstr::{BStr, BString};
7#[cfg(feature = "serde")]7#[cfg(feature = "serde")]
8use serde::{Serialize, Serializer};8use serde::{Serialize, Serializer};
9 9
@@ -120,21 +120,18 @@ pub fn normalize_cow(input: Cow<'_, [u8]>) -> Cow<'_, [u8]> { · 110 unmodified lines
120}120}
121 121
122/// `&[u8]` variant of [`normalize_cow`].122/// `&[u8]` variant of [`normalize_cow`].
123#[inline]
124#[must_use]123#[must_use]
125pub fn normalize_bytes(input: &[u8]) -> Cow<'_, [u8]> {124pub fn normalize_bytes(input: &[u8]) -> Cow<'_, [u8]> {
126 normalize_cow(Cow::Borrowed(input))125 normalize_cow(Cow::Borrowed(input))
127}126}
128 127
129/// `Vec[u8]` variant of [`normalize_cow`].128/// `Vec[u8]` variant of [`normalize_cow`].
130#[inline]
131#[must_use]129#[must_use]
132pub fn normalize_vec(input: Vec<u8>) -> Cow<'static, [u8]> {130pub fn normalize_vec(input: Vec<u8>) -> Cow<'static, [u8]> {
133 normalize_cow(Cow::Owned(input))131 normalize_cow(Cow::Owned(input))
134}132}
135 133
136/// [`str`] variant of [`normalize_cow`].134/// [`str`] variant of [`normalize_cow`].
137#[inline]
138#[must_use]135#[must_use]
139pub fn normalize_str(input: &str) -> Cow<'_, [u8]> {136pub fn normalize_str(input: &str) -> Cow<'_, [u8]> {
140 normalize_bytes(input.as_bytes())137 normalize_bytes(input.as_bytes())
@@ -149,7 +146,6 @@ pub struct Bytes<'a> { · 8 unmodified lines
149}146}
150 147
151impl<'a> From<&'a [u8]> for Bytes<'a> {148impl<'a> From<&'a [u8]> for Bytes<'a> {
152 #[inline]
153 fn from(s: &'a [u8]) -> Self {149 fn from(s: &'a [u8]) -> Self {
154 Self {150 Self {
155 value: Cow::Borrowed(s),151 value: Cow::Borrowed(s),
@@ -164,7 +160,6 @@ impl From<Vec<u8>> for Bytes<'_> { · 8 unmodified lines
164}160}
165 161
166impl<'a> From<Cow<'a, [u8]>> for Bytes<'a> {162impl<'a> From<Cow<'a, [u8]>> for Bytes<'a> {
167 #[inline]
168 fn from(c: Cow<'a, [u8]>) -> Self {163 fn from(c: Cow<'a, [u8]>) -> Self {
169 match c {164 match c {
170 Cow::Borrowed(c) => Self::from(c),165 Cow::Borrowed(c) => Self::from(c),
@@ -181,7 +176,6 @@ pub struct String<'a> { · 10 unmodified lines
181}176}
182 177
183impl<'a> From<Cow<'a, [u8]>> for String<'a> {178impl<'a> From<Cow<'a, [u8]>> for String<'a> {
184 #[inline]
185 fn from(c: Cow<'a, [u8]>) -> Self {179 fn from(c: Cow<'a, [u8]>) -> Self {
186 String {180 String {
187 value: match c {181 value: match c {
@@ -198,38 +192,28 @@ pub mod path { · 10 unmodified lines
198 192
199 #[cfg(not(any(target_os = "android", target_os = "windows")))]193 #[cfg(not(any(target_os = "android", target_os = "windows")))]
200 use pwd::Passwd;194 use pwd::Passwd;
201 use quick_error::ResultExt;
202 195
203 use crate::values::Path;196 use crate::values::Path;
204 197
205 pub mod interpolate {198 pub mod interpolate {
206 use quick_error::quick_error;199 /// The error returned by [`Path::interpolate()`][crate::values::Path::interpolate()].
207 200 #[derive(Debug, thiserror::Error)]
208 quick_error! {201 #[allow(missing_docs)]
209 #[derive(Debug)]202 pub enum Error {
210 /// The error returned by [`Path::interpolate()`].203 #[error("{} is missing", .what)]
211 #[allow(missing_docs)]204 Missing { what: &'static str },
212 pub enum Error {205 #[error("Ill-formed UTF-8 in {}", .what)]
213 Missing { what: &'static str } {206 Utf8Conversion {
214 display("{} is missing", what)207 what: &'static str,
215 }208 #[source]
216 Utf8Conversion(what: &'static str, err: git_features::path::Utf8Error) {209 err: git_path::Utf8Error,
217 display("Ill-formed UTF-8 in {}", what)210 },
218 context(what: &'static str, err: git_features::path::Utf8Error) -> (what, err)211 #[error("Ill-formed UTF-8 in username")]
219 source(err)212 UsernameConversion(#[from] std::str::Utf8Error),
220 }213 #[error("User home info missing")]
221 UsernameConversion(err: std::str::Utf8Error) {214 PwdFileQuery,
222 display("Ill-formed UTF-8 in username")215 #[error("User interpolation is not available on this platform")]
223 source(err)216 UserInterpolationUnsupported,
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 }
233 }217 }
234 }218 }
235 219
@@ -261,17 +245,25 @@ pub mod path { · 25 unmodified lines
261 })?;245 })?;
262 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());246 let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
263 let path_without_trailing_slash =247 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 })?;
265 Ok(git_install_dir.join(path_without_trailing_slash).into())254 Ok(git_install_dir.join(path_without_trailing_slash).into())
266 } else if self.starts_with(USER_HOME) {255 } else if self.starts_with(USER_HOME) {
267 let home_path = dirs::home_dir().ok_or(interpolate::Error::Missing { what: "home dir" })?;256 let home_path = dirs::home_dir().ok_or(interpolate::Error::Missing { what: "home dir" })?;
268 let (_prefix, val) = self.split_at(USER_HOME.len());257 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 })?;
270 Ok(home_path.join(val).into())262 Ok(home_path.join(val).into())
271 } else if self.starts_with(b"~") && self.contains(&b'/') {263 } else if self.starts_with(b"~") && self.contains(&b'/') {
272 self.interpolate_user()264 self.interpolate_user()
273 } else {265 } else {
274 Ok(git_features::path::from_bytes(self.value).context("unexpanded path")?)266 Ok(git_path::from_bstr(self.value))
275 }267 }
276 }268 }
277 269
@@ -293,8 +285,13 @@ pub mod path { · 15 unmodified lines
293 .map_err(|_| interpolate::Error::PwdFileQuery)?285 .map_err(|_| interpolate::Error::PwdFileQuery)?
294 .ok_or(interpolate::Error::Missing { what: "pwd user info" })?286 .ok_or(interpolate::Error::Missing { what: "pwd user info" })?
295 .dir;287 .dir;
296 let path_past_user_prefix = git_features::path::from_byte_slice(&path_with_leading_slash["/".len()..])288 let path_past_user_prefix =
297 .context("path past ~user/")?;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 })?;
298 Ok(std::path::PathBuf::from(home).join(path_past_user_prefix).into())295 Ok(std::path::PathBuf::from(home).join(path_past_user_prefix).into())
299 }296 }
300 }297 }
@@ -306,11 +303,11 @@ pub mod path { · 5 unmodified lines
306#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]303#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
307pub struct Path<'a> {304pub struct Path<'a> {
308 /// The path string, un-interpolated305 /// The path string, un-interpolated
309 pub value: Cow<'a, [u8]>,306 pub value: Cow<'a, BStr>,
310}307}
311 308
312impl<'a> std::ops::Deref for Path<'a> {309impl<'a> std::ops::Deref for Path<'a> {
313 type Target = [u8];310 type Target = BStr;
314 311
315 fn deref(&self) -> &Self::Target {312 fn deref(&self) -> &Self::Target {
316 self.value.as_ref()313 self.value.as_ref()
@@ -323,10 +320,20 @@ impl<'a> AsRef<[u8]> for Path<'a> { · 6 unmodified lines
323 }320 }
324}321}
325 322
323impl<'a> AsRef<BStr> for Path<'a> {
324 fn as_ref(&self) -> &BStr {
325 self.value.as_ref()
326 }
327}
328
326impl<'a> From<Cow<'a, [u8]>> for Path<'a> {329impl<'a> From<Cow<'a, [u8]>> for Path<'a> {
327 #[inline]
328 fn from(value: Cow<'a, [u8]>) -> Self {330 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 }
330 }337 }
331}338}
332 339
@@ -354,7 +361,6 @@ impl Boolean<'_> { · 21 unmodified lines
354 /// Generates a byte representation of the value. This should be used when361 /// Generates a byte representation of the value. This should be used when
355 /// non-UTF-8 sequences are present or a UTF-8 representation can't be362 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
356 /// guaranteed.363 /// guaranteed.
357 #[inline]
358 #[must_use]364 #[must_use]
359 pub fn to_vec(&self) -> Vec<u8> {365 pub fn to_vec(&self) -> Vec<u8> {
360 self.into()366 self.into()
@@ -363,26 +369,21 @@ impl Boolean<'_> { · 2 unmodified lines
363 /// Generates a byte representation of the value. This should be used when369 /// Generates a byte representation of the value. This should be used when
364 /// non-UTF-8 sequences are present or a UTF-8 representation can't be370 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
365 /// guaranteed.371 /// guaranteed.
366 #[inline]
367 #[must_use]372 #[must_use]
368 pub fn as_bytes(&self) -> &[u8] {373 pub fn as_bytes(&self) -> &[u8] {
369 self.into()374 self.into()
370 }375 }
371}376}
372 377
373quick_error! {378fn bool_err(input: impl Into<BString>) -> value::parse::Error {
374 #[derive(Debug, PartialEq)]379 value::parse::Error::new(
375 /// The error returned when creating `Boolean` from byte string.380 "Booleans need to be 'no', 'off', 'false', 'zero' or 'yes', 'on', 'true', 'one'",
376 #[allow(missing_docs)]381 input,
377 pub enum BooleanError {382 )
378 InvalidFormat {
379 display("Invalid argument format")
380 }
381 }
382}383}
383 384
384impl<'a> TryFrom<&'a [u8]> for Boolean<'a> {385impl<'a> TryFrom<&'a [u8]> for Boolean<'a> {
385 type Error = BooleanError;386 type Error = value::parse::Error;
386 387
387 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {388 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
388 if let Ok(v) = TrueVariant::try_from(value) {389 if let Ok(v) = TrueVariant::try_from(value) {
@@ -400,12 +401,12 @@ impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { · 11 unmodified lines
400 ));401 ));
401 }402 }
402 403
403 Err(BooleanError::InvalidFormat)404 Err(bool_err(value))
404 }405 }
405}406}
406 407
407impl TryFrom<Vec<u8>> for Boolean<'_> {408impl TryFrom<Vec<u8>> for Boolean<'_> {
408 type Error = BooleanError;409 type Error = value::parse::Error;
409 410
410 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {411 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
411 if value.eq_ignore_ascii_case(b"no")412 if value.eq_ignore_ascii_case(b"no")
@@ -424,7 +425,7 @@ impl TryFrom<Vec<u8>> for Boolean<'_> { · 12 unmodified lines
424}425}
425 426
426impl<'a> TryFrom<Cow<'a, [u8]>> for Boolean<'a> {427impl<'a> TryFrom<Cow<'a, [u8]>> for Boolean<'a> {
427 type Error = BooleanError;428 type Error = value::parse::Error;
428 fn try_from(c: Cow<'a, [u8]>) -> Result<Self, Self::Error> {429 fn try_from(c: Cow<'a, [u8]>) -> Result<Self, Self::Error> {
429 match c {430 match c {
430 Cow::Borrowed(c) => Self::try_from(c),431 Cow::Borrowed(c) => Self::try_from(c),
@@ -434,7 +435,6 @@ impl<'a> TryFrom<Cow<'a, [u8]>> for Boolean<'a> { · 3 unmodified lines
434}435}
435 436
436impl Display for Boolean<'_> {437impl Display for Boolean<'_> {
437 #[inline]
438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439 match self {439 match self {
440 Boolean::True(v) => v.fmt(f),440 Boolean::True(v) => v.fmt(f),
@@ -444,7 +444,6 @@ impl Display for Boolean<'_> { · 3 unmodified lines
444}444}
445 445
446impl From<Boolean<'_>> for bool {446impl From<Boolean<'_>> for bool {
447 #[inline]
448 fn from(b: Boolean) -> Self {447 fn from(b: Boolean) -> Self {
449 match b {448 match b {
450 Boolean::True(_) => true,449 Boolean::True(_) => true,
@@ -454,7 +453,6 @@ impl From<Boolean<'_>> for bool { · 3 unmodified lines
454}453}
455 454
456impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] {455impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] {
457 #[inline]
458 fn from(b: &'b Boolean) -> Self {456 fn from(b: &'b Boolean) -> Self {
459 match b {457 match b {
460 Boolean::True(t) => t.into(),458 Boolean::True(t) => t.into(),
@@ -464,14 +462,12 @@ impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] { · 3 unmodified lines
464}462}
465 463
466impl From<Boolean<'_>> for Vec<u8> {464impl From<Boolean<'_>> for Vec<u8> {
467 #[inline]
468 fn from(b: Boolean) -> Self {465 fn from(b: Boolean) -> Self {
469 b.into()466 b.into()
470 }467 }
471}468}
472 469
473impl From<&Boolean<'_>> for Vec<u8> {470impl From<&Boolean<'_>> for Vec<u8> {
474 #[inline]
475 fn from(b: &Boolean) -> Self {471 fn from(b: &Boolean) -> Self {
476 b.to_string().into_bytes()472 b.to_string().into_bytes()
477 }473 }
@@ -502,7 +498,7 @@ pub enum TrueVariant<'a> { · 24 unmodified lines
502}498}
503 499
504impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> {500impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> {
505 type Error = BooleanError;501 type Error = value::parse::Error;
506 502
507 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {503 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
508 if value.eq_ignore_ascii_case(b"yes")504 if value.eq_ignore_ascii_case(b"yes")
@@ -516,13 +512,13 @@ impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> { · 7 unmodified lines
516 } else if value.is_empty() {512 } else if value.is_empty() {
517 Ok(Self::Implicit)513 Ok(Self::Implicit)
518 } else {514 } else {
519 Err(BooleanError::InvalidFormat)515 Err(bool_err(value))
520 }516 }
521 }517 }
522}518}
523 519
524impl TryFrom<Vec<u8>> for TrueVariant<'_> {520impl TryFrom<Vec<u8>> for TrueVariant<'_> {
525 type Error = BooleanError;521 type Error = value::parse::Error;
526 522
527 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {523 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
528 if value.eq_ignore_ascii_case(b"yes")524 if value.eq_ignore_ascii_case(b"yes")
@@ -536,7 +532,7 @@ impl TryFrom<Vec<u8>> for TrueVariant<'_> { · 7 unmodified lines
536 } else if value.is_empty() {532 } else if value.is_empty() {
537 Ok(Self::Implicit)533 Ok(Self::Implicit)
538 } else {534 } else {
539 Err(BooleanError::InvalidFormat)535 Err(bool_err(value))
540 }536 }
541 }537 }
542}538}
@@ -552,7 +548,6 @@ impl Display for TrueVariant<'_> { · 9 unmodified lines
552}548}
553 549
554impl<'a, 'b: 'a> From<&'b TrueVariant<'a>> for &'a [u8] {550impl<'a, 'b: 'a> From<&'b TrueVariant<'a>> for &'a [u8] {
555 #[inline]
556 fn from(t: &'b TrueVariant<'a>) -> Self {551 fn from(t: &'b TrueVariant<'a>) -> Self {
557 match t {552 match t {
558 TrueVariant::Explicit(e) => e.as_bytes(),553 TrueVariant::Explicit(e) => e.as_bytes(),
@@ -641,31 +636,18 @@ impl Serialize for Integer { · 82 unmodified lines
641 }636 }
642}637}
643 638
644quick_error! {639fn int_err(input: impl Into<BString>) -> value::parse::Error {
645 #[derive(Debug)]640 value::parse::Error::new(
646 /// The error returned when creating `Integer` from byte string.641 "Intgers needs to be positive or negative numbers which may have a suffix like 1k, or 50G",
647 #[allow(missing_docs)]642 input,
648 pub enum IntegerError {643 )
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 }
661}644}
662 645
663impl TryFrom<&[u8]> for Integer {646impl TryFrom<&[u8]> for Integer {
664 type Error = IntegerError;647 type Error = value::parse::Error;
665 648
666 #[inline]
667 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {649 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))?;
669 if let Ok(value) = s.parse() {651 if let Ok(value) = s.parse() {
670 return Ok(Self { value, suffix: None });652 return Ok(Self { value, suffix: None });
671 }653 }
@@ -673,7 +655,7 @@ impl TryFrom<&[u8]> for Integer { · 1 unmodified line
673 // Assume we have a prefix at this point.655 // Assume we have a prefix at this point.
674 656
675 if s.len() <= 1 {657 if s.len() <= 1 {
676 return Err(IntegerError::InvalidFormat);658 return Err(int_err(s));
677 }659 }
678 660
679 let (number, suffix) = s.split_at(s.len() - 1);661 let (number, suffix) = s.split_at(s.len() - 1);
@@ -683,24 +665,22 @@ impl TryFrom<&[u8]> for Integer { · 3 unmodified lines
683 suffix: Some(suffix),665 suffix: Some(suffix),
684 })666 })
685 } else {667 } else {
686 Err(IntegerError::InvalidFormat)668 Err(int_err(s))
687 }669 }
688 }670 }
689}671}
690 672
691impl TryFrom<Vec<u8>> for Integer {673impl TryFrom<Vec<u8>> for Integer {
692 type Error = IntegerError;674 type Error = value::parse::Error;
693 675
694 #[inline]
695 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {676 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
696 Self::try_from(value.as_ref())677 Self::try_from(value.as_ref())
697 }678 }
698}679}
699 680
700impl TryFrom<Cow<'_, [u8]>> for Integer {681impl TryFrom<Cow<'_, [u8]>> for Integer {
701 type Error = IntegerError;682 type Error = value::parse::Error;
702 683
703 #[inline]
704 fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {684 fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {
705 match c {685 match c {
706 Cow::Borrowed(c) => Self::try_from(c),686 Cow::Borrowed(c) => Self::try_from(c),
@@ -710,14 +690,12 @@ impl TryFrom<Cow<'_, [u8]>> for Integer { · 3 unmodified lines
710}690}
711 691
712impl From<Integer> for Vec<u8> {692impl From<Integer> for Vec<u8> {
713 #[inline]
714 fn from(i: Integer) -> Self {693 fn from(i: Integer) -> Self {
715 i.into()694 i.into()
716 }695 }
717}696}
718 697
719impl From<&Integer> for Vec<u8> {698impl From<&Integer> for Vec<u8> {
720 #[inline]
721 fn from(i: &Integer) -> Self {699 fn from(i: &Integer) -> Self {
722 i.to_string().into_bytes()700 i.to_string().into_bytes()
723 }701 }
@@ -736,7 +714,6 @@ pub enum IntegerSuffix { · 12 unmodified lines
736 714
737impl IntegerSuffix {715impl IntegerSuffix {
738 /// Returns the number of bits that the suffix shifts left by.716 /// Returns the number of bits that the suffix shifts left by.
739 #[inline]
740 #[must_use]717 #[must_use]
741 pub const fn bitwise_offset(self) -> usize {718 pub const fn bitwise_offset(self) -> usize {
742 match self {719 match self {
@@ -748,7 +725,6 @@ impl IntegerSuffix { · 5 unmodified lines
748}725}
749 726
750impl Display for IntegerSuffix {727impl Display for IntegerSuffix {
751 #[inline]
752 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {728 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
753 match self {729 match self {
754 Self::Kibi => write!(f, "k"),730 Self::Kibi => write!(f, "k"),
@@ -773,32 +749,29 @@ impl Serialize for IntegerSuffix { · 18 unmodified lines
773}749}
774 750
775impl FromStr for IntegerSuffix {751impl FromStr for IntegerSuffix {
776 type Err = IntegerError;752 type Err = ();
777 753
778 #[inline]
779 fn from_str(s: &str) -> Result<Self, Self::Err> {754 fn from_str(s: &str) -> Result<Self, Self::Err> {
780 match s {755 match s {
781 "k" | "K" => Ok(Self::Kibi),756 "k" | "K" => Ok(Self::Kibi),
782 "m" | "M" => Ok(Self::Mebi),757 "m" | "M" => Ok(Self::Mebi),
783 "g" | "G" => Ok(Self::Gibi),758 "g" | "G" => Ok(Self::Gibi),
784 _ => Err(IntegerError::InvalidSuffix),759 _ => Err(()),
785 }760 }
786 }761 }
787}762}
788 763
789impl TryFrom<&[u8]> for IntegerSuffix {764impl TryFrom<&[u8]> for IntegerSuffix {
790 type Error = IntegerError;765 type Error = ();
791 766
792 #[inline]
793 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {767 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(|_| ())?)
795 }769 }
796}770}
797 771
798impl TryFrom<Vec<u8>> for IntegerSuffix {772impl TryFrom<Vec<u8>> for IntegerSuffix {
799 type Error = IntegerError;773 type Error = ();
800 774
801 #[inline]
802 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {775 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
803 Self::try_from(value.as_ref())776 Self::try_from(value.as_ref())
804 }777 }
@@ -825,7 +798,6 @@ impl Color { · 20 unmodified lines
825 /// Generates a byte representation of the value. This should be used when798 /// Generates a byte representation of the value. This should be used when
826 /// non-UTF-8 sequences are present or a UTF-8 representation can't be799 /// non-UTF-8 sequences are present or a UTF-8 representation can't be
827 /// guaranteed.800 /// guaranteed.
828 #[inline]
829 #[must_use]801 #[must_use]
830 pub fn to_vec(&self) -> Vec<u8> {802 pub fn to_vec(&self) -> Vec<u8> {
831 self.into()803 self.into()
@@ -861,31 +833,18 @@ impl Serialize for Color { · 29 unmodified lines
861 }833 }
862}834}
863 835
864quick_error! {836fn color_err(input: impl Into<BString>) -> value::parse::Error {
865 #[derive(Debug, PartialEq)]837 value::parse::Error::new(
866 ///838 "Colors are specific color values and their attributes, like 'brightred', or 'blue'",
867 #[allow(missing_docs)]839 input,
868 pub enum ColorError {840 )
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 }
881}841}
882 842
883impl TryFrom<&[u8]> for Color {843impl TryFrom<&[u8]> for Color {
884 type Error = ColorError;844 type Error = value::parse::Error;
885 845
886 #[inline]
887 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {846 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))?;
889 enum ColorItem {848 enum ColorItem {
890 Value(ColorValue),849 Value(ColorValue),
891 Attr(ColorAttribute),850 Attr(ColorAttribute),
@@ -913,12 +872,12 @@ impl TryFrom<&[u8]> for Color { · 21 unmodified lines
913 } else if new_self.background.is_none() {872 } else if new_self.background.is_none() {
914 new_self.background = Some(v);873 new_self.background = Some(v);
915 } else {874 } else {
916 return Err(ColorError::InvalidColorItem);875 return Err(color_err(s));
917 }876 }
918 }877 }
919 ColorItem::Attr(a) => new_self.attributes.push(a),878 ColorItem::Attr(a) => new_self.attributes.push(a),
920 },879 },
921 Err(_) => return Err(ColorError::InvalidColorItem),880 Err(_) => return Err(color_err(s)),
922 }881 }
923 }882 }
924 883
@@ -927,18 +886,16 @@ impl TryFrom<&[u8]> for Color { · 2 unmodified lines
927}886}
928 887
929impl TryFrom<Vec<u8>> for Color {888impl TryFrom<Vec<u8>> for Color {
930 type Error = ColorError;889 type Error = value::parse::Error;
931 890
932 #[inline]
933 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {891 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
934 Self::try_from(value.as_ref())892 Self::try_from(value.as_ref())
935 }893 }
936}894}
937 895
938impl TryFrom<Cow<'_, [u8]>> for Color {896impl TryFrom<Cow<'_, [u8]>> for Color {
939 type Error = ColorError;897 type Error = value::parse::Error;
940 898
941 #[inline]
942 fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {899 fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {
943 match c {900 match c {
944 Cow::Borrowed(c) => Self::try_from(c),901 Cow::Borrowed(c) => Self::try_from(c),
@@ -948,14 +905,12 @@ impl TryFrom<Cow<'_, [u8]>> for Color { · 3 unmodified lines
948}905}
949 906
950impl From<Color> for Vec<u8> {907impl From<Color> for Vec<u8> {
951 #[inline]
952 fn from(c: Color) -> Self {908 fn from(c: Color) -> Self {
953 c.into()909 c.into()
954 }910 }
955}911}
956 912
957impl From<&Color> for Vec<u8> {913impl From<&Color> for Vec<u8> {
958 #[inline]
959 fn from(c: &Color) -> Self {914 fn from(c: &Color) -> Self {
960 c.to_string().into_bytes()915 c.to_string().into_bytes()
961 }916 }
@@ -1026,7 +981,7 @@ impl Serialize for ColorValue { · 64 unmodified lines
1026}981}
1027 982
1028impl FromStr for ColorValue {983impl FromStr for ColorValue {
1029 type Err = ColorError;984 type Err = value::parse::Error;
1030 985
1031 fn from_str(s: &str) -> Result<Self, Self::Err> {986 fn from_str(s: &str) -> Result<Self, Self::Err> {
1032 let mut s = s;987 let mut s = s;
@@ -1039,7 +994,7 @@ impl FromStr for ColorValue { · 6 unmodified lines
1039 994
1040 match s {995 match s {
1041 "normal" if !bright => return Ok(Self::Normal),996 "normal" if !bright => return Ok(Self::Normal),
1042 "normal" if bright => return Err(ColorError::InvalidFormat),997 "normal" if bright => return Err(color_err(s)),
1043 "black" if !bright => return Ok(Self::Black),998 "black" if !bright => return Ok(Self::Black),
1044 "black" if bright => return Ok(Self::BrightBlack),999 "black" if bright => return Ok(Self::BrightBlack),
1045 "red" if !bright => return Ok(Self::Red),1000 "red" if !bright => return Ok(Self::Red),
@@ -1077,16 +1032,15 @@ impl FromStr for ColorValue { · 31 unmodified lines
1077 }1032 }
1078 }1033 }
1079 1034
1080 Err(ColorError::InvalidFormat)1035 Err(color_err(s))
1081 }1036 }
1082}1037}
1083 1038
1084impl TryFrom<&[u8]> for ColorValue {1039impl TryFrom<&[u8]> for ColorValue {
1085 type Error = ColorError;1040 type Error = value::parse::Error;
1086 1041
1087 #[inline]
1088 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {1042 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))?)
1090 }1044 }
1091}1045}
1092 1046
@@ -1161,7 +1115,7 @@ impl Serialize for ColorAttribute { · 68 unmodified lines
1161}1115}
1162 1116
1163impl FromStr for ColorAttribute {1117impl FromStr for ColorAttribute {
1164 type Err = ColorError;1118 type Err = value::parse::Error;
1165 1119
1166 fn from_str(s: &str) -> Result<Self, Self::Err> {1120 fn from_str(s: &str) -> Result<Self, Self::Err> {
1167 let inverted = s.starts_with("no");1121 let inverted = s.starts_with("no");
@@ -1190,16 +1144,15 @@ impl FromStr for ColorAttribute { · 22 unmodified lines
1190 "italic" if inverted => Ok(Self::NoItalic),1144 "italic" if inverted => Ok(Self::NoItalic),
1191 "strike" if !inverted => Ok(Self::Strike),1145 "strike" if !inverted => Ok(Self::Strike),
1192 "strike" if inverted => Ok(Self::NoStrike),1146 "strike" if inverted => Ok(Self::NoStrike),
1193 _ => Err(ColorError::InvalidFormat),1147 _ => Err(color_err(parsed)),
1194 }1148 }
1195 }1149 }
1196}1150}
1197 1151
1198impl TryFrom<&[u8]> for ColorAttribute {1152impl TryFrom<&[u8]> for ColorAttribute {
1199 type Error = ColorError;1153 type Error = value::parse::Error;
1200 1154
1201 #[inline]
1202 fn try_from(s: &[u8]) -> Result<Self, Self::Error> {1155 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))?)
1204 }1157 }
1205}1158}
git-config/tests/git_config/mod.rs+139−108
@@ -19,7 +19,7 @@ mod mutable_value { · 18 unmodified lines
19 fn value_is_correct() {19 fn value_is_correct() {
20 let mut git_config = init_config();20 let mut git_config = init_config();
21 21
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();
23 assert_eq!(&*value.get().unwrap(), b"b100");23 assert_eq!(&*value.get().unwrap(), b"b100");
24 }24 }
25 25
@@ -27,7 +27,7 @@ mod mutable_value { · 1 unmodified line
27 fn set_string_cleanly_updates() {27 fn set_string_cleanly_updates() {
28 let mut git_config = init_config();28 let mut git_config = init_config();
29 29
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();
31 value.set_string("hello world".to_string());31 value.set_string("hello world".to_string());
32 assert_eq!(32 assert_eq!(
33 git_config.to_string(),33 git_config.to_string(),
@@ -38,7 +38,7 @@ mod mutable_value { · 4 unmodified lines
38 e=f"#,38 e=f"#,
39 );39 );
40 40
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();
42 value.set_string(String::new());42 value.set_string(String::new());
43 assert_eq!(43 assert_eq!(
44 git_config.to_string(),44 git_config.to_string(),
@@ -54,7 +54,7 @@ mod mutable_value { · 9 unmodified lines
54 fn delete_value() {54 fn delete_value() {
55 let mut git_config = init_config();55 let mut git_config = init_config();
56 56
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();
58 value.delete();58 value.delete();
59 assert_eq!(59 assert_eq!(
60 git_config.to_string(),60 git_config.to_string(),
@@ -63,7 +63,7 @@ mod mutable_value { · 2 unmodified lines
63 e=f",63 e=f",
64 );64 );
65 65
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();
67 value.delete();67 value.delete();
68 assert_eq!(68 assert_eq!(
69 git_config.to_string(),69 git_config.to_string(),
@@ -75,7 +75,7 @@ mod mutable_value { · 5 unmodified lines
75 fn get_value_after_deleted() {75 fn get_value_after_deleted() {
76 let mut git_config = init_config();76 let mut git_config = init_config();
77 77
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();
79 value.delete();79 value.delete();
80 assert!(value.get().is_err());80 assert!(value.get().is_err());
81 }81 }
@@ -84,7 +84,7 @@ mod mutable_value { · 2 unmodified lines
84 fn set_string_after_deleted() {84 fn set_string_after_deleted() {
85 let mut git_config = init_config();85 let mut git_config = init_config();
86 86
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();
88 value.delete();88 value.delete();
89 value.set_string("hello world".to_string());89 value.set_string("hello world".to_string());
90 assert_eq!(90 assert_eq!(
@@ -101,7 +101,7 @@ mod mutable_value { · 10 unmodified lines
101 fn subsequent_delete_calls_are_noop() {101 fn subsequent_delete_calls_are_noop() {
102 let mut git_config = init_config();102 let mut git_config = init_config();
103 103
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();
105 for _ in 0..10 {105 for _ in 0..10 {
106 value.delete();106 value.delete();
107 }107 }
@@ -124,7 +124,7 @@ b · 16 unmodified lines
124 e=f"#,124 e=f"#,
125 )125 )
126 .unwrap();126 .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();
128 assert_eq!(&*value.get().unwrap(), b"b100b");128 assert_eq!(&*value.get().unwrap(), b"b100b");
129 value.delete();129 value.delete();
130 assert_eq!(130 assert_eq!(
@@ -157,7 +157,7 @@ mod mutable_multi_value { · 26 unmodified lines
157 fn value_is_correct() {157 fn value_is_correct() {
158 let mut git_config = init_config();158 let mut git_config = init_config();
159 159
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();
161 assert_eq!(161 assert_eq!(
162 &*value.get().unwrap(),162 &*value.get().unwrap(),
163 vec![163 vec![
@@ -171,17 +171,14 @@ mod mutable_multi_value { · 7 unmodified lines
171 #[test]171 #[test]
172 fn non_empty_sizes_are_correct() {172 fn non_empty_sizes_are_correct() {
173 let mut git_config = init_config();173 let mut git_config = init_config();
174 assert_eq!(git_config.get_raw_multi_value_mut("core", None, "a").unwrap().len(), 3);174 assert_eq!(git_config.raw_multi_value_mut("core", None, "a").unwrap().len(), 3);
175 assert!(!git_config175 assert!(!git_config.raw_multi_value_mut("core", None, "a").unwrap().is_empty());
176 .get_raw_multi_value_mut("core", None, "a")
177 .unwrap()
178 .is_empty());
179 }176 }
180 177
181 #[test]178 #[test]
182 fn set_value_at_start() {179 fn set_value_at_start() {
183 let mut git_config = init_config();180 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();
185 values.set_string(0, "Hello".to_string());182 values.set_string(0, "Hello".to_string());
186 assert_eq!(183 assert_eq!(
187 git_config.to_string(),184 git_config.to_string(),
@@ -196,7 +193,7 @@ mod mutable_multi_value { · 8 unmodified lines
196 #[test]193 #[test]
197 fn set_value_at_end() {194 fn set_value_at_end() {
198 let mut git_config = init_config();195 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();
200 values.set_string(2, "Hello".to_string());197 values.set_string(2, "Hello".to_string());
201 assert_eq!(198 assert_eq!(
202 git_config.to_string(),199 git_config.to_string(),
@@ -211,7 +208,7 @@ mod mutable_multi_value { · 8 unmodified lines
211 #[test]208 #[test]
212 fn set_values_all() {209 fn set_values_all() {
213 let mut git_config = init_config();210 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();
215 values.set_owned_values_all(b"Hello");212 values.set_owned_values_all(b"Hello");
216 assert_eq!(213 assert_eq!(
217 git_config.to_string(),214 git_config.to_string(),
@@ -226,7 +223,7 @@ mod mutable_multi_value { · 8 unmodified lines
226 #[test]223 #[test]
227 fn delete() {224 fn delete() {
228 let mut git_config = init_config();225 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();
230 values.delete(0);227 values.delete(0);
231 assert_eq!(228 assert_eq!(
232 git_config.to_string(),229 git_config.to_string(),
@@ -239,7 +236,7 @@ mod mutable_multi_value { · 6 unmodified lines
239 #[test]236 #[test]
240 fn delete_all() {237 fn delete_all() {
241 let mut git_config = init_config();238 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();
243 values.delete_all();240 values.delete_all();
244 assert!(values.get().is_err());241 assert!(values.get().is_err());
245 assert_eq!(242 assert_eq!(
@@ -261,7 +258,7 @@ b · 15 unmodified lines
261a"#,258a"#,
262 )259 )
263 .unwrap();260 .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();
265 262
266 assert_eq!(263 assert_eq!(
267 &*values.get().unwrap(),264 &*values.get().unwrap(),
@@ -320,8 +317,8 @@ mod from_paths_tests { · 52 unmodified lines
320 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();317 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();
321 318
322 assert_eq!(319 assert_eq!(
323 config.get_raw_value("core", None, "boolean"),320 config.raw_value("core", None, "boolean").unwrap(),
324 Ok(Cow::<[u8]>::Borrowed(b"true"))321 Cow::<[u8]>::Borrowed(b"true")
325 );322 );
326 323
327 assert_eq!(config.len(), 1);324 assert_eq!(config.len(), 1);
@@ -390,26 +387,26 @@ mod from_paths_tests { · 62 unmodified lines
390 let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap();387 let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap();
391 388
392 assert_eq!(389 assert_eq!(
393 config.get_raw_value("core", None, "c"),390 config.raw_value("core", None, "c").unwrap(),
394 Ok(Cow::<[u8]>::Borrowed(b"12"))391 Cow::<[u8]>::Borrowed(b"12")
395 );392 );
396 assert_eq!(393 assert_eq!(
397 config.get_raw_value("core", None, "d"),394 config.raw_value("core", None, "d").unwrap(),
398 Ok(Cow::<[u8]>::Borrowed(b"41"))395 Cow::<[u8]>::Borrowed(b"41")
399 );396 );
400 assert_eq!(397 assert_eq!(
401 config.get_raw_value("http", None, "sslVerify"),398 config.raw_value("http", None, "sslVerify").unwrap(),
402 Ok(Cow::<[u8]>::Borrowed(b"false"))399 Cow::<[u8]>::Borrowed(b"false")
403 );400 );
404 401
405 assert_eq!(402 assert_eq!(
406 config.get_raw_value("diff", None, "renames"),403 config.raw_value("diff", None, "renames").unwrap(),
407 Ok(Cow::<[u8]>::Borrowed(b"true"))404 Cow::<[u8]>::Borrowed(b"true")
408 );405 );
409 406
410 assert_eq!(407 assert_eq!(
411 config.get_raw_value("core", None, "a"),408 config.raw_value("core", None, "a").unwrap(),
412 Ok(Cow::<[u8]>::Borrowed(b"false"))409 Cow::<[u8]>::Borrowed(b"false")
413 );410 );
414 }411 }
415 412
@@ -452,7 +449,7 @@ mod from_paths_tests { · 36 unmodified lines
452 let options = from_paths::Options::default();449 let options = from_paths::Options::default();
453 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();450 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();
454 assert_eq!(451 assert_eq!(
455 config.get_raw_multi_value("core", None, "i").unwrap(),452 config.raw_multi_value("core", None, "i").unwrap(),
456 vec![453 vec![
457 Cow::Borrowed(b"0"),454 Cow::Borrowed(b"0"),
458 Cow::Borrowed(b"1"),455 Cow::Borrowed(b"1"),
@@ -470,12 +467,18 @@ mod from_paths_tests { · 11 unmodified lines
470 ..Default::default()467 ..Default::default()
471 };468 };
472 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();469 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 );
474 474
475 // with default max_allowed_depth of 10 and 4 levels of includes, last level is read475 // with default max_allowed_depth of 10 and 4 levels of includes, last level is read
476 let options = from_paths::Options::default();476 let options = from_paths::Options::default();
477 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();477 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 );
479 482
480 // with max_allowed_depth of 5, the base and 4 levels of includes, last level is read483 // with max_allowed_depth of 5, the base and 4 levels of includes, last level is read
481 let options = from_paths::Options {484 let options = from_paths::Options {
@@ -483,7 +486,10 @@ mod from_paths_tests { · 1 unmodified line
483 ..Default::default()486 ..Default::default()
484 };487 };
485 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();488 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 );
487 493
488 // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned494 // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned
489 let options = from_paths::Options {495 let options = from_paths::Options {
@@ -503,7 +509,10 @@ mod from_paths_tests { · 13 unmodified lines
503 ..Default::default()509 ..Default::default()
504 };510 };
505 let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap();511 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 );
507 516
508 // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned517 // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned
509 let options = from_paths::Options {518 let options = from_paths::Options {
@@ -552,8 +561,8 @@ mod from_paths_tests { · 42 unmodified lines
552 561
553 let config = GitConfig::from_paths(vec![a_path], &Default::default()).unwrap();562 let config = GitConfig::from_paths(vec![a_path], &Default::default()).unwrap();
554 assert_eq!(563 assert_eq!(
555 config.get_raw_value("core", None, "b"),564 config.raw_value("core", None, "b").unwrap(),
556 Ok(Cow::<[u8]>::Borrowed(b"false"))565 Cow::<[u8]>::Borrowed(b"false")
557 );566 );
558 }567 }
559 568
@@ -607,7 +616,7 @@ mod from_paths_tests { · 47 unmodified lines
607 };616 };
608 let config = GitConfig::from_paths(vec![a_path], &options).unwrap();617 let config = GitConfig::from_paths(vec![a_path], &options).unwrap();
609 assert_eq!(618 assert_eq!(
610 config.get_raw_multi_value("core", None, "b").unwrap(),619 config.raw_multi_value("core", None, "b").unwrap(),
611 vec![620 vec![
612 Cow::Borrowed(b"0"),621 Cow::Borrowed(b"0"),
613 Cow::Borrowed(b"1"),622 Cow::Borrowed(b"1"),
@@ -662,16 +671,19 @@ mod from_paths_tests { · 48 unmodified lines
662 671
663 let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap();672 let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap();
664 673
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 );
666 678
667 assert_eq!(679 assert_eq!(
668 config.get_raw_value("core", None, "b"),680 config.raw_value("core", None, "b").unwrap(),
669 Ok(Cow::<[u8]>::Borrowed(b"true"))681 Cow::<[u8]>::Borrowed(b"true")
670 );682 );
671 683
672 assert_eq!(684 assert_eq!(
673 config.get_raw_value("core", None, "a"),685 config.raw_value("core", None, "a").unwrap(),
674 Ok(Cow::<[u8]>::Borrowed(b"false"))686 Cow::<[u8]>::Borrowed(b"false")
675 );687 );
676 }688 }
677 689
@@ -695,18 +707,18 @@ mod from_paths_tests { · 17 unmodified lines
695 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();707 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();
696 708
697 assert_eq!(709 assert_eq!(
698 config.get_raw_value("core", None, "a"),710 config.raw_value("core", None, "a").unwrap(),
699 Ok(Cow::<[u8]>::Borrowed(b"false"))711 Cow::<[u8]>::Borrowed(b"false")
700 );712 );
701 713
702 assert_eq!(714 assert_eq!(
703 config.get_raw_value("core", None, "b"),715 config.raw_value("core", None, "b").unwrap(),
704 Ok(Cow::<[u8]>::Borrowed(b"true"))716 Cow::<[u8]>::Borrowed(b"true")
705 );717 );
706 718
707 assert_eq!(719 assert_eq!(
708 config.get_raw_value("core", None, "c"),720 config.raw_value("core", None, "c").unwrap(),
709 Ok(Cow::<[u8]>::Borrowed(b"true"))721 Cow::<[u8]>::Borrowed(b"true")
710 );722 );
711 723
712 assert_eq!(config.len(), 4);724 assert_eq!(config.len(), 4);
@@ -735,12 +747,12 @@ mod from_paths_tests { · 22 unmodified lines
735 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();747 let config = GitConfig::from_paths(paths, &Default::default()).unwrap();
736 748
737 assert_eq!(749 assert_eq!(
738 config.get_raw_multi_value("core", None, "key").unwrap(),750 config.raw_multi_value("core", None, "key").unwrap(),
739 vec![Cow::Borrowed(b"a"), Cow::Borrowed(b"b"), Cow::Borrowed(b"c")]751 vec![Cow::Borrowed(b"a"), Cow::Borrowed(b"b"), Cow::Borrowed(b"c")]
740 );752 );
741 753
742 assert_eq!(754 assert_eq!(
743 config.get_raw_multi_value("include", None, "path").unwrap(),755 config.raw_multi_value("include", None, "path").unwrap(),
744 vec![Cow::Borrowed(b"d_path"), Cow::Borrowed(b"e_path")]756 vec![Cow::Borrowed(b"d_path"), Cow::Borrowed(b"e_path")]
745 );757 );
746 758
@@ -802,7 +814,7 @@ mod from_env_tests { · 55 unmodified lines
802 fn parse_error_with_invalid_count() {814 fn parse_error_with_invalid_count() {
803 let _env = Env::new().set("GIT_CONFIG_COUNT", "invalid");815 let _env = Env::new().set("GIT_CONFIG_COUNT", "invalid");
804 let err = GitConfig::from_env(&Options::default()).unwrap_err();816 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 { .. }));
806 }818 }
807 819
808 #[test]820 #[test]
@@ -815,8 +827,8 @@ mod from_env_tests { · 6 unmodified lines
815 827
816 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();828 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();
817 assert_eq!(829 assert_eq!(
818 config.get_raw_value("core", None, "key"),830 config.raw_value("core", None, "key").unwrap(),
819 Ok(Cow::<[u8]>::Borrowed(b"value"))831 Cow::<[u8]>::Borrowed(b"value")
820 );832 );
821 833
822 assert_eq!(config.len(), 1);834 assert_eq!(config.len(), 1);
@@ -836,9 +848,18 @@ mod from_env_tests { · 13 unmodified lines
836 848
837 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();849 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();
838 850
839 assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"a")));851 assert_eq!(
840 assert_eq!(config.get_raw_value("core", None, "b"), Ok(Cow::<[u8]>::Borrowed(b"b")));852 config.raw_value("core", None, "a").unwrap(),
841 assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"c")));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 );
842 assert_eq!(config.len(), 3);863 assert_eq!(config.len(), 3);
843 }864 }
844 865
@@ -881,8 +902,8 @@ mod from_env_tests { · 36 unmodified lines
881 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();902 let config = GitConfig::from_env(&Options::default()).unwrap().unwrap();
882 903
883 assert_eq!(904 assert_eq!(
884 config.get_raw_value("core", None, "key"),905 config.raw_value("core", None, "key").unwrap(),
885 Ok(Cow::<[u8]>::Borrowed(b"changed"))906 Cow::<[u8]>::Borrowed(b"changed")
886 );907 );
887 assert_eq!(config.len(), 5);908 assert_eq!(config.len(), 5);
888 }909 }
@@ -892,64 +913,76 @@ mod from_env_tests { · 3 unmodified lines
892mod get_raw_value {913mod get_raw_value {
893 use std::{borrow::Cow, convert::TryFrom};914 use std::{borrow::Cow, convert::TryFrom};
894 915
895 use git_config::{916 use git_config::{file::GitConfig, lookup};
896 file::{GitConfig, GitConfigError},
897 parser::SectionHeaderName,
898 };
899 917
900 #[test]918 #[test]
901 fn single_section() {919 fn single_section() {
902 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();920 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")));921 assert_eq!(
904 assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"d")));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 );
905 }929 }
906 930
907 #[test]931 #[test]
908 fn last_one_wins_respected_in_section() {932 fn last_one_wins_respected_in_section() {
909 let config = GitConfig::try_from("[core]\na=b\na=d").unwrap();933 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 );
911 }938 }
912 939
913 #[test]940 #[test]
914 fn last_one_wins_respected_across_section() {941 fn last_one_wins_respected_across_section() {
915 let config = GitConfig::try_from("[core]\na=b\n[core]\na=d").unwrap();942 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 );
917 }947 }
918 948
919 #[test]949 #[test]
920 fn section_not_found() {950 fn section_not_found() {
921 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();951 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
922 assert_eq!(952 assert!(matches!(
923 config.get_raw_value("foo", None, "a"),953 config.raw_value("foo", None, "a"),
924 Err(GitConfigError::SectionDoesNotExist(SectionHeaderName("foo".into())))954 Err(lookup::existing::Error::SectionMissing)
925 );955 ));
926 }956 }
927 957
928 #[test]958 #[test]
929 fn subsection_not_found() {959 fn subsection_not_found() {
930 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();960 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
931 assert_eq!(961 assert!(matches!(
932 config.get_raw_value("core", Some("a"), "a"),962 config.raw_value("core", Some("a"), "a"),
933 Err(GitConfigError::SubSectionDoesNotExist(Some("a")))963 Err(lookup::existing::Error::SubSectionMissing)
934 );964 ));
935 }965 }
936 966
937 #[test]967 #[test]
938 fn key_not_found() {968 fn key_not_found() {
939 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();969 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
940 assert_eq!(970 assert!(matches!(
941 config.get_raw_value("core", None, "aaaaaa"),971 config.raw_value("core", None, "aaaaaa"),
942 Err(GitConfigError::KeyDoesNotExist)972 Err(lookup::existing::Error::KeyMissing)
943 );973 ));
944 }974 }
945 975
946 #[test]976 #[test]
947 fn subsection_must_be_respected() {977 fn subsection_must_be_respected() {
948 let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap();978 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")));
950 assert_eq!(979 assert_eq!(
951 config.get_raw_value("core", Some("a"), "a"),980 config.raw_value("core", None, "a").unwrap(),
952 Ok(Cow::<[u8]>::Borrowed(b"c"))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")
953 );986 );
954 }987 }
955}988}
@@ -1008,17 +1041,15 @@ mod get_value { · 52 unmodified lines
1008mod get_raw_multi_value {1041mod get_raw_multi_value {
1009 use std::{borrow::Cow, convert::TryFrom};1042 use std::{borrow::Cow, convert::TryFrom};
1010 1043
1011 use git_config::{1044 use git_config::file::GitConfig;
1012 file::{GitConfig, GitConfigError},1045 use git_config::lookup;
1013 parser::SectionHeaderName,
1014 };
1015 1046
1016 #[test]1047 #[test]
1017 fn single_value_is_identical_to_single_value_query() {1048 fn single_value_is_identical_to_single_value_query() {
1018 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();1049 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
1019 assert_eq!(1050 assert_eq!(
1020 vec![config.get_raw_value("core", None, "a").unwrap()],1051 vec![config.raw_value("core", None, "a").unwrap()],
1021 config.get_raw_multi_value("core", None, "a").unwrap()1052 config.raw_multi_value("core", None, "a").unwrap()
1022 );1053 );
1023 }1054 }
1024 1055
@@ -1026,7 +1057,7 @@ mod get_raw_multi_value { · 1 unmodified line
1026 fn multi_value_in_section() {1057 fn multi_value_in_section() {
1027 let config = GitConfig::try_from("[core]\na=b\na=c").unwrap();1058 let config = GitConfig::try_from("[core]\na=b\na=c").unwrap();
1028 assert_eq!(1059 assert_eq!(
1029 config.get_raw_multi_value("core", None, "a").unwrap(),1060 config.raw_multi_value("core", None, "a").unwrap(),
1030 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c")]1061 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c")]
1031 );1062 );
1032 }1063 }
@@ -1035,7 +1066,7 @@ mod get_raw_multi_value { · 2 unmodified lines
1035 fn multi_value_across_sections() {1066 fn multi_value_across_sections() {
1036 let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d").unwrap();1067 let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d").unwrap();
1037 assert_eq!(1068 assert_eq!(
1038 config.get_raw_multi_value("core", None, "a").unwrap(),1069 config.raw_multi_value("core", None, "a").unwrap(),
1039 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")]1070 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")]
1040 );1071 );
1041 }1072 }
@@ -1043,39 +1074,39 @@ mod get_raw_multi_value { · 1 unmodified line
1043 #[test]1074 #[test]
1044 fn section_not_found() {1075 fn section_not_found() {
1045 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();1076 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
1046 assert_eq!(1077 assert!(matches!(
1047 config.get_raw_multi_value("foo", None, "a"),1078 config.raw_multi_value("foo", None, "a"),
1048 Err(GitConfigError::SectionDoesNotExist(SectionHeaderName("foo".into())))1079 Err(lookup::existing::Error::SectionMissing)
1049 );1080 ));
1050 }1081 }
1051 1082
1052 #[test]1083 #[test]
1053 fn subsection_not_found() {1084 fn subsection_not_found() {
1054 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();1085 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
1055 assert_eq!(1086 assert!(matches!(
1056 config.get_raw_multi_value("core", Some("a"), "a"),1087 config.raw_multi_value("core", Some("a"), "a"),
1057 Err(GitConfigError::SubSectionDoesNotExist(Some("a")))1088 Err(lookup::existing::Error::SubSectionMissing)
1058 );1089 ));
1059 }1090 }
1060 1091
1061 #[test]1092 #[test]
1062 fn key_not_found() {1093 fn key_not_found() {
1063 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();1094 let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
1064 assert_eq!(1095 assert!(matches!(
1065 config.get_raw_multi_value("core", None, "aaaaaa"),1096 config.raw_multi_value("core", None, "aaaaaa"),
1066 Err(GitConfigError::KeyDoesNotExist)1097 Err(lookup::existing::Error::KeyMissing)
1067 );1098 ));
1068 }1099 }
1069 1100
1070 #[test]1101 #[test]
1071 fn subsection_must_be_respected() {1102 fn subsection_must_be_respected() {
1072 let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap();1103 let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap();
1073 assert_eq!(1104 assert_eq!(
1074 config.get_raw_multi_value("core", None, "a").unwrap(),1105 config.raw_multi_value("core", None, "a").unwrap(),
1075 vec![Cow::Borrowed(b"b")]1106 vec![Cow::Borrowed(b"b")]
1076 );1107 );
1077 assert_eq!(1108 assert_eq!(
1078 config.get_raw_multi_value("core", Some("a"), "a").unwrap(),1109 config.raw_multi_value("core", Some("a"), "a").unwrap(),
1079 vec![Cow::Borrowed(b"c")]1110 vec![Cow::Borrowed(b"c")]
1080 );1111 );
1081 }1112 }
@@ -1084,7 +1115,7 @@ mod get_raw_multi_value { · 2 unmodified lines
1084 fn non_relevant_subsection_is_ignored() {1115 fn non_relevant_subsection_is_ignored() {
1085 let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d\n[core]g=g").unwrap();1116 let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d\n[core]g=g").unwrap();
1086 assert_eq!(1117 assert_eq!(
1087 config.get_raw_multi_value("core", None, "a").unwrap(),1118 config.raw_multi_value("core", None, "a").unwrap(),
1088 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")]1119 vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")]
1089 );1120 );
1090 }1121 }
git-config/tests/value/mod.rs+22−7
@@ -23,11 +23,20 @@ fn get_value_for_all_provided_values() -> crate::Result { · 22 unmodified lines
23 file.value::<Boolean>("core", None, "bool-explicit")?,23 file.value::<Boolean>("core", None, "bool-explicit")?,
24 Boolean::False(Cow::Borrowed("false"))24 Boolean::False(Cow::Borrowed("false"))
25 );25 );
26 assert!(!file.boolean("core", None, "bool-explicit").expect("exists")?);
26 27
27 assert_eq!(28 assert_eq!(
28 file.value::<Boolean>("core", None, "bool-implicit")?,29 file.value::<Boolean>("core", None, "bool-implicit")?,
29 Boolean::True(TrueVariant::Implicit)30 Boolean::True(TrueVariant::Implicit)
30 );31 );
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);
31 40
32 assert_eq!(41 assert_eq!(
33 file.value::<Integer>("core", None, "integer-no-prefix")?,42 file.value::<Integer>("core", None, "integer-no-prefix")?,
@@ -69,15 +78,22 @@ fn get_value_for_all_provided_values() -> crate::Result { · 35 unmodified lines
69 }78 }
70 );79 );
71 80
81 assert_eq!(
82 file.string("core", None, "other").expect("present").as_ref(),
83 "hello world"
84 );
85
72 let actual = file.value::<git_config::values::Path>("core", None, "location")?;86 let actual = file.value::<git_config::values::Path>("core", None, "location")?;
73 assert_eq!(87 assert_eq!(
74 &*actual,88 &*actual, "~/tmp",
75 "~/tmp".as_bytes(),
76 "no interpolation occurs when querying a path due to lack of context"89 "no interpolation occurs when querying a path due to lack of context"
77 );90 );
78 let expected = PathBuf::from(format!("{}/tmp", dirs::home_dir().expect("empty home dir").display()));91 let expected = PathBuf::from(format!("{}/tmp", dirs::home_dir().expect("empty home dir").display()));
79 assert_eq!(actual.interpolate(None).unwrap(), expected);92 assert_eq!(actual.interpolate(None).unwrap(), expected);
80 93
94 let actual = file.path("core", None, "location").expect("present");
95 assert_eq!(&*actual, "~/tmp",);
96
81 Ok(())97 Ok(())
82}98}
83 99
@@ -113,10 +129,9 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { · 29 unmodified lines
113fn section_names_are_case_insensitive() -> crate::Result {129fn section_names_are_case_insensitive() -> crate::Result {
114 let config = "[core] bool-implicit";130 let config = "[core] bool-implicit";
115 let file = GitConfig::try_from(config)?;131 let file = GitConfig::try_from(config)?;
116 assert!(file.value::<Boolean>("core", None, "bool-implicit").is_ok());
117 assert_eq!(132 assert_eq!(
118 file.value::<Boolean>("core", None, "bool-implicit"),133 file.value::<Boolean>("core", None, "bool-implicit").unwrap(),
119 file.value::<Boolean>("CORE", None, "bool-implicit")134 file.value::<Boolean>("CORE", None, "bool-implicit").unwrap()
120 );135 );
121 136
122 Ok(())137 Ok(())
@@ -130,8 +145,8 @@ fn value_names_are_case_insensitive() -> crate::Result { · 7 unmodified lines
130 let file = GitConfig::try_from(config)?;145 let file = GitConfig::try_from(config)?;
131 assert_eq!(file.multi_value::<Boolean>("core", None, "a")?.len(), 2);146 assert_eq!(file.multi_value::<Boolean>("core", None, "a")?.len(), 2);
132 assert_eq!(147 assert_eq!(
133 file.value::<Boolean>("core", None, "a"),148 file.value::<Boolean>("core", None, "a").unwrap(),
134 file.value::<Boolean>("core", None, "A")149 file.value::<Boolean>("core", None, "A").unwrap()
135 );150 );
136 151
137 Ok(())152 Ok(())
git-features/Cargo.toml+0−10
@@ -81,11 +81,6 @@ name = "pipe" · 80 unmodified lines
81path = "tests/pipe.rs"81path = "tests/pipe.rs"
82required-features = ["io-pipe"]82required-features = ["io-pipe"]
83 83
84[[test]]
85name = "path"
86path = "tests/path.rs"
87required-features = ["bstr"]
88
89[dependencies]84[dependencies]
90#! ### Optional Dependencies85#! ### Optional Dependencies
91 86
@@ -122,11 +117,6 @@ quick-error = { version = "2.0.0", optional = true } · 30 unmodified lines
122## make the `time` module available with access to the local time as configured by the system.117## make the `time` module available with access to the local time as configured by the system.
123time = { version = "0.3.2", optional = true, default-features = false, features = ["local-offset"] }118time = { version = "0.3.2", optional = true, default-features = false, features = ["local-offset"] }
124 119
125
126# path
127## make bstr utilities available in the `path` modules, which itself is gated by the `path` feature.
128bstr = { version = "0.2.17", optional = true, default-features = false, features = ["std"] }
129
130document-features = { version = "0.2.0", optional = true }120document-features = { version = "0.2.0", optional = true }
131 121
132[target.'cfg(unix)'.dependencies]122[target.'cfg(unix)'.dependencies]
git-features/src/lib.rs+0−1
@@ -24,7 +24,6 @@ pub mod interrupt; · 23 unmodified lines
24#[cfg(feature = "io-pipe")]24#[cfg(feature = "io-pipe")]
25pub mod io;25pub mod io;
26pub mod parallel;26pub mod parallel;
27pub mod path;
28#[cfg(feature = "progress")]27#[cfg(feature = "progress")]
29pub mod progress;28pub mod progress;
30pub mod threading;29pub 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
42use 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.
50pub struct Utf8Error;
51
52impl 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
58impl std::error::Error for Utf8Error {}
59
60/// Like [`into_bytes()`], but takes `OsStr` as input for a lossless, but fallible, conversion.
61pub 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.
73pub 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.
101pub 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.
110pub 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`.
122pub 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")]
132pub 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.
141pub 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`.
154pub 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`.
159pub 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.
164pub 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 @@
1mod 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
4 4
5use bstr::BString;5use bstr::BString;
6 6
7/// A glob pattern at a particular base path.7/// A glob pattern optimized for matching paths relative to a root directory.
8///8///
9/// This closely models how patterns appear in a directory hierarchy of include or attribute files.9/// For normal globbing, use [`wildmatch()`] instead.
10#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]10#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
11#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]11#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
12pub struct Pattern {12pub struct Pattern {
@@ -28,6 +28,8 @@ pub use wildmatch::function::wildmatch; · 15 unmodified lines
28mod parse;28mod parse;
29 29
30/// Create a [`Pattern`] by parsing `text` or return `None` if `text` is empty.30/// Create a [`Pattern`] by parsing `text` or return `None` if `text` is empty.
31pub fn parse(text: &[u8]) -> Option<Pattern> {31///
32 Pattern::from_bytes(text)32/// Note that
33pub fn parse(text: impl AsRef<[u8]>) -> Option<Pattern> {
34 Pattern::from_bytes(text.as_ref())
33}35}
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
26 }26 }
27 if pat.first() == Some(&b'/') {27 if pat.first() == Some(&b'/') {
28 mode |= Mode::ABSOLUTE;28 mode |= Mode::ABSOLUTE;
29 pat = &pat[1..];
29 }30 }
30 let mut pat = truncate_non_escaped_trailing_spaces(pat);31 let mut pat = truncate_non_escaped_trailing_spaces(pat);
31 if pat.last() == Some(&b'/') {32 if pat.last() == Some(&b'/') {
@@ -33,11 +34,10 @@ pub fn pattern(mut pat: &[u8]) -> Option<(BString, pattern::Mode, Option<usize>) · 1 unmodified line
33 pat.pop();34 pat.pop();
34 }35 }
35 36
36 let relative_pattern = mode.contains(Mode::ABSOLUTE).then(|| &pat[1..]).unwrap_or(&pat);37 if !pat.contains(&b'/') {
37 if !relative_pattern.contains(&b'/') {
38 mode |= Mode::NO_SUB_DIR;38 mode |= Mode::NO_SUB_DIR;
39 }39 }
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() {
41 mode |= Mode::ENDS_WITH;41 mode |= Mode::ENDS_WITH;
42 }42 }
43 43
git-glob/src/pattern.rs+37−34
@@ -1,5 +1,6 @@
1use bitflags::bitflags;1use bitflags::bitflags;
2use bstr::{BStr, ByteSlice};2use bstr::{BStr, ByteSlice};
3use std::fmt;
3 4
4use crate::{pattern, wildmatch, Pattern};5use crate::{pattern, wildmatch, Pattern};
5 6
@@ -36,6 +37,12 @@ pub enum Case { · 30 unmodified lines
36 Fold,37 Fold,
37}38}
38 39
40impl Default for Case {
41 fn default() -> Self {
42 Case::Sensitive
43 }
44}
45
39impl Pattern {46impl Pattern {
40 /// Parse the given `text` as pattern, or return `None` if `text` was empty.47 /// Parse the given `text` as pattern, or return `None` if `text` was empty.
41 pub fn from_bytes(text: &[u8]) -> Option<Self> {48 pub fn from_bytes(text: &[u8]) -> Option<Self> {
@@ -58,15 +65,14 @@ impl Pattern { · 16 unmodified lines
58 /// `basename_start_pos` is the index at which the `path`'s basename starts.65 /// `basename_start_pos` is the index at which the `path`'s basename starts.
59 ///66 ///
60 /// Lastly, `case` folding can be configured as well.67 /// Lastly, `case` folding can be configured as well.
61 ///
62 /// Note that this method uses shortcuts to accelerate simple patterns.
63 pub fn matches_repo_relative_path<'a>(68 pub fn matches_repo_relative_path<'a>(
64 &self,69 &self,
65 path: impl Into<&'a BStr>,70 path: impl Into<&'a BStr>,
66 basename_start_pos: Option<usize>,71 basename_start_pos: Option<usize>,
67 is_dir: bool,72 is_dir: Option<bool>,
68 case: Case,73 case: Case,
69 ) -> bool {74 ) -> bool {
75 let is_dir = is_dir.unwrap_or(false);
70 if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {76 if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {
71 return false;77 return false;
72 }78 }
@@ -84,20 +90,11 @@ impl Pattern { · 11 unmodified lines
84 );90 );
85 debug_assert!(!path.starts_with(b"/"), "input path must be relative");91 debug_assert!(!path.starts_with(b"/"), "input path must be relative");
86 92
87 let (text, first_wildcard_pos) = self93 if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) {
88 .mode94 let basename = &path[basename_start_pos.unwrap_or_default()..];
89 .contains(pattern::Mode::ABSOLUTE)95 self.matches(basename, flags)
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)
99 } else {96 } else {
100 self.matches_inner(text, first_wildcard_pos, path, flags)97 self.matches(path, flags)
101 }98 }
102 }99 }
103 100
@@ -107,22 +104,12 @@ impl Pattern { · 3 unmodified lines
107 /// strings with cases ignored as well. Note that the case folding performed here is ASCII only.104 /// strings with cases ignored as well. Note that the case folding performed here is ASCII only.
108 ///105 ///
109 /// Note that this method uses some shortcuts to accelerate simple patterns.106 /// 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 {107 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 {
121 let value = value.into();108 let value = value.into();
122 match first_wildcard_pos {109 match self.first_wildcard_pos {
123 // "*literal" case, overrides starts-with110 // "*literal" case, overrides starts-with
124 Some(pos) if self.mode.contains(pattern::Mode::ENDS_WITH) && !value.contains(&b'/') => {111 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..];
126 if mode.contains(wildmatch::Mode::IGNORE_CASE) {113 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
127 value114 value
128 .len()115 .len()
@@ -137,22 +124,38 @@ impl Pattern { · 8 unmodified lines
137 if mode.contains(wildmatch::Mode::IGNORE_CASE) {124 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
138 if !value125 if !value
139 .get(..pos)126 .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]))
141 {128 {
142 return false;129 return false;
143 }130 }
144 } else if !value.starts_with(&text[..pos]) {131 } else if !value.starts_with(&self.text[..pos]) {
145 return false;132 return false;
146 }133 }
147 crate::wildmatch(text.as_bstr(), value, mode)134 crate::wildmatch(self.text.as_bstr(), value, mode)
148 }135 }
149 None => {136 None => {
150 if mode.contains(wildmatch::Mode::IGNORE_CASE) {137 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
151 text.eq_ignore_ascii_case(value)138 self.text.eq_ignore_ascii_case(value)
152 } else {139 } else {
153 text == value140 self.text == value
154 }141 }
155 }142 }
156 }143 }
157 }144 }
158}145}
146
147impl 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 @@
1version https://git-lfs.github.com/spec/v11version https://git-lfs.github.com/spec/v1
2oid sha256:cdade0fe7f3df3ac737130a101b077c82f9de1dcb3d59d5964e5ffd920405e8d2oid sha256:6bda59c1591dfd35f09cad5d09e2950b2a6ff885d9f7900535144a5275ab51c3
3size 103843size 10428
git-glob/tests/fixtures/make_baseline.sh+5−2
@@ -10,8 +10,10 @@ while read -r pattern value; do · 9 unmodified lines
10 echo "$pattern" > .gitignore10 echo "$pattern" > .gitignore
11 echo "$value" | git check-ignore -vn --stdin 2>&1 || :11 echo "$value" | git check-ignore -vn --stdin 2>&1 || :
12done <<EOF >git-baseline.nmatch12done <<EOF >git-baseline.nmatch
13*/\ XXX/\13/*foo bam/barfoo/baz/bam
14*/\\ XXX/\14/*foo bar/bam/barfoo/baz/bam
15foo foobaz
16*/\' XXX/\'
15/*foo bar/foo17/*foo bar/foo
16/*foo bar/bazfoo18/*foo bar/bazfoo
17foo*bar foo/baz/bar19foo*bar foo/baz/bar
@@ -71,6 +73,7 @@ while read -r pattern value; do · 53 unmodified lines
71 echo "$pattern" > .gitignore73 echo "$pattern" > .gitignore
72 echo "$value" | git check-ignore -vn --stdin 2>&1 || :74 echo "$value" | git check-ignore -vn --stdin 2>&1 || :
73done <<EOF >git-baseline.match75done <<EOF >git-baseline.match
76*/' XXX/'
74\a a77\a a
75\\\[a-z] \a78\\\[a-z] \a
76\\\? \a79\\\? \a
git-glob/tests/glob.rs+1−1
@@ -1,3 +1,3 @@
1mod matching;
2mod parse;1mod parse;
2mod pattern;
3mod wildmatch;3mod wildmatch;
git-glob/tests/matching/mod.rs+0−279
@@ -1,280 +1 @@
1use std::collections::BTreeSet;
2 1
3use bstr::{BStr, ByteSlice};
4use git_glob::{pattern, pattern::Case};
5
6#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)]
7pub 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
14pub struct Baseline<'a> {
15 inner: bstr::Lines<'a>,
16}
17
18impl<'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
36impl<'a> Baseline<'a> {
37 fn new(input: &'a [u8]) -> Self {
38 Baseline {
39 inner: input.as_bstr().lines(),
40 }
41 }
42}
43
44#[test]
45fn 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]
104fn 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]
124fn 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]
138fn 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]
149fn 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]
159fn 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]
169fn 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]
177fn 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]
184fn 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]
196fn 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]
211fn 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]
232fn 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]
245fn 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]
253fn 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
265fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern {
266 git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works")
267}
268
269fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool {
270 match_path(pattern, path, false, case)
271}
272
273fn 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
278fn 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
77fn leading_slashes_mark_patterns_as_absolute() {77fn leading_slashes_mark_patterns_as_absolute() {
78 assert_eq!(78 assert_eq!(
79 git_glob::parse(br"/absolute"),79 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)
81 );81 );
82 82
83 assert_eq!(83 assert_eq!(
84 git_glob::parse(br"/absolute/path"),84 git_glob::parse(br"/absolute/path"),
85 pat("/absolute/path", Mode::ABSOLUTE, None)85 pat("absolute/path", Mode::ABSOLUTE, None)
86 );86 );
87}87}
88 88
git-glob/tests/pattern/matching.rs+326−0
@@ -0,0 +1,326 @@
1use std::collections::BTreeSet;
2
3use bstr::{BStr, ByteSlice};
4use git_glob::{pattern, pattern::Case};
5
6#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)]
7pub 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
14pub struct Baseline<'a> {
15 inner: bstr::Lines<'a>,
16}
17
18impl<'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
36impl<'a> Baseline<'a> {
37 fn new(input: &'a [u8]) -> Self {
38 Baseline {
39 inner: input.as_bstr().lines(),
40 }
41 }
42}
43
44#[test]
45fn 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]
99fn 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]
119fn 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]
136fn 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]
147fn 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]
157fn 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]
167fn 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]
175fn 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]
182fn 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]
196fn 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]
211fn 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]
232fn 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]
245fn 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]
253fn 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]
265fn 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]
274fn 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]
291fn 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
311fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern {
312 git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works")
313}
314
315fn 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
319fn 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
324fn 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 @@
1use git_glob::pattern::Mode;
2use git_glob::Pattern;
3
4#[test]
5fn 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}
19mod matching;
git-glob/tests/wildmatch/mod.rs+7−3
@@ -1,3 +1,4 @@
1use bstr::ByteSlice;
1use std::{2use std::{
2 fmt::{Debug, Display, Formatter},3 fmt::{Debug, Display, Formatter},
3 panic::catch_unwind,4 panic::catch_unwind,
@@ -222,6 +223,7 @@ fn corpus() { · 218 unmodified lines
222 }223 }
223 }224 }
224 225
226 dbg!(&failures);
225 assert_eq!(failures.len(), 0);227 assert_eq!(failures.len(), 0);
226 assert_eq!(at_least_one_panic, 0, "not a single panic in any invocation");228 assert_eq!(at_least_one_panic, 0, "not a single panic in any invocation");
227 229
@@ -249,8 +251,10 @@ fn multi_match(pattern_text: &str, text: &str) -> (Pattern, MultiMatch) { · 21 unmodified lines
249 let pattern = git_glob::Pattern::from_bytes(pattern_text.as_bytes()).expect("valid (enough) pattern");251 let pattern = git_glob::Pattern::from_bytes(pattern_text.as_bytes()).expect("valid (enough) pattern");
250 let actual_path_match: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Sensitive)).into();252 let actual_path_match: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Sensitive)).into();
251 let actual_path_imatch: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Fold)).into();253 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();254 let actual_glob_match: MatchResult =
253 let actual_glob_imatch: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::IGNORE_CASE)).into();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();
254 let actual = MultiMatch {258 let actual = MultiMatch {
255 path_match: actual_path_match,259 path_match: actual_path_match,
256 path_imatch: actual_path_imatch,260 path_imatch: actual_path_imatch,
@@ -363,7 +367,7 @@ impl Display for MatchResult { · 106 unmodified lines
363}367}
364 368
365fn match_file_path(pattern: &git_glob::Pattern, path: &str, case: Case) -> bool {369fn 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)
367}371}
368fn basename_of(path: &str) -> Option<usize> {372fn basename_of(path: &str) -> Option<usize> {
369 path.rfind('/').map(|pos| pos + 1)373 path.rfind('/').map(|pos| pos + 1)
git-index/src/access.rs+39−1
@@ -1,6 +1,6 @@
1use bstr::{BStr, ByteSlice};1use bstr::{BStr, ByteSlice};
2 2
3use crate::{extension, Entry, State, Version};3use crate::{extension, Entry, PathStorage, State, Version};
4 4
5impl State {5impl State {
6 pub fn version(&self) -> Version {6 pub fn version(&self) -> Version {
@@ -10,6 +10,35 @@ impl State { · 3 unmodified lines
10 pub fn entries(&self) -> &[Entry] {10 pub fn entries(&self) -> &[Entry] {
11 &self.entries11 &self.entries
12 }12 }
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 }
13 pub fn entries_mut(&mut self) -> &mut [Entry] {42 pub fn entries_mut(&mut self) -> &mut [Entry] {
14 &mut self.entries43 &mut self.entries
15 }44 }
@@ -20,6 +49,15 @@ impl State { · 4 unmodified lines
20 (e, path)49 (e, path)
21 })50 })
22 }51 }
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 }
23 pub fn tree(&self) -> Option<&extension::Tree> {61 pub fn tree(&self) -> Option<&extension::Tree> {
24 self.tree.as_ref()62 self.tree.as_ref()
25 }63 }
git-index/src/decode/mod.rs+2−1
@@ -41,12 +41,13 @@ use crate::util::read_u32; · 40 unmodified lines
41pub struct Options {41pub struct Options {
42 pub object_hash: git_hash::Kind,42 pub object_hash: git_hash::Kind,
43 /// 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 threads43 /// 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.
45 ///45 ///
46 /// This applies to loading extensions in parallel to entries if the common EOIE extension is available.46 /// This applies to loading extensions in parallel to entries if the common EOIE extension is available.
47 /// It also allows to use multiple threads for loading entries if the IEOT extension is present.47 /// It also allows to use multiple threads for loading entries if the IEOT extension is present.
48 pub thread_limit: Option<usize>,48 pub thread_limit: Option<usize>,
49 /// The minimum size in bytes to load extensions in their own thread, assuming there is enough `num_threads` available.49 /// 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.
50 pub min_extension_block_in_bytes_for_threading: usize,51 pub min_extension_block_in_bytes_for_threading: usize,
51}52}
52 53
git-index/src/entry.rs+4−0
@@ -143,6 +143,10 @@ mod access { · 142 unmodified lines
143 (&state.path_backing[self.path.clone()]).as_bstr()143 (&state.path_backing[self.path.clone()]).as_bstr()
144 }144 }
145 145
146 pub fn path_in<'backing>(&self, backing: &'backing crate::PathStorage) -> &'backing BStr {
147 (backing[self.path.clone()]).as_bstr()
148 }
149
146 pub fn stage(&self) -> u32 {150 pub fn stage(&self) -> u32 {
147 self.flags.stage()151 self.flags.stage()
148 }152 }
git-index/src/lib.rs+4−1
@@ -51,6 +51,9 @@ pub struct File { · 50 unmodified lines
51 pub checksum: git_hash::ObjectId,51 pub checksum: git_hash::ObjectId,
52}52}
53 53
54/// The type to use and store paths to all entries.
55pub type PathStorage = Vec<u8>;
56
54/// An in-memory cache of a fully parsed git index file.57/// An in-memory cache of a fully parsed git index file.
55///58///
56/// As opposed to a snapshot, it's meant to be altered and eventually be written back to disk or converted into a tree.59/// 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
65 version: Version,68 version: Version,
66 entries: Vec<Entry>,69 entries: Vec<Entry>,
67 /// A memory area keeping all index paths, in full length, independently of the index version.70 /// A memory area keeping all index paths, in full length, independently of the index version.
68 path_backing: Vec<u8>,71 path_backing: PathStorage,
69 /// True if one entry in the index has a special marker mode72 /// True if one entry in the index has a special marker mode
70 #[allow(dead_code)]73 #[allow(dead_code)]
71 is_sparse: bool,74 is_sparse: bool,
git-odb/Cargo.toml+2−1
@@ -30,7 +30,8 @@ required-features = [] · 29 unmodified lines
30all-features = true30all-features = true
31 31
32[dependencies]32[dependencies]
33git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32", "bstr"] }33git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32" ] }
34git-path = { version = "^0.1.0", path = "../git-path" }
34git-hash = { version = "^0.9.3", path = "../git-hash" }35git-hash = { version = "^0.9.3", path = "../git-hash" }
35git-quote = { version = "^0.2.0", path = "../git-quote" }36git-quote = { version = "^0.2.0", path = "../git-quote" }
36git-object = { version = "^0.18.0", path = "../git-object" }37git-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
20 continue;20 continue;
21 }21 }
22 out.push(22 out.push(
23 git_features::path::from_bstr(if line.starts_with(b"\"") {23 git_path::try_from_bstr(if line.starts_with(b"\"") {
24 git_quote::ansi_c::undo(line)?.024 git_quote::ansi_c::undo(line)?.0
25 } else {25 } else {
26 Cow::Borrowed(line)26 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
39[dependencies]39[dependencies]
40git-features = { version = "^0.20.0", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] }40git-features = { version = "^0.20.0", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] }
41git-hash = { version = "^0.9.3", path = "../git-hash" }41git-hash = { version = "^0.9.3", path = "../git-hash" }
42git-path = { version = "^0.1.0", path = "../git-path" }
42git-chunk = { version = "^0.3.0", path = "../git-chunk" }43git-chunk = { version = "^0.3.0", path = "../git-chunk" }
43git-object = { version = "^0.18.0", path = "../git-object" }44git-object = { version = "^0.18.0", path = "../git-object" }
44git-traverse = { version = "^0.14.0", path = "../git-traverse" }45git-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
34 let null_byte_pos = chunk.find_byte(b'\0').ok_or(decode::Error::MissingNullByte)?;34 let null_byte_pos = chunk.find_byte(b'\0').ok_or(decode::Error::MissingNullByte)?;
35 35
36 let path = &chunk[..null_byte_pos];36 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)
38 .map_err(|_| decode::Error::PathEncoding {38 .map_err(|_| decode::Error::PathEncoding {
39 path: BString::from(path),39 path: BString::from(path),
40 })?40 })?
git-path/CHANGELOG.md+42−0
@@ -0,0 +1,42 @@
1# Changelog
2
3All notable changes to this project will be documented in this file.
4
5The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6and 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
41An 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]
2name = "git-path"
3version = "0.1.0"
4repository = "https://github.com/Byron/gitoxide"
5license = "MIT/Apache-2.0"
6description = "A WIP crate of the gitoxide project dealing paths and their conversions"
7authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
8edition = "2018"
9
10[lib]
11doctest = false
12
13# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14
15[dependencies]
16bstr = { version = "0.2.17", default-features = false, features = ["std"] }
git-path/src/convert.rs+197−0
@@ -0,0 +1,197 @@
1use bstr::{BStr, BString};
2use 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.
10pub struct Utf8Error;
11
12impl 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
18impl std::error::Error for Utf8Error {}
19
20/// Like [`into_bstr()`], but takes `OsStr` as input for a lossless, but fallible, conversion.
21pub 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.
33pub 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.
61pub 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.
70pub 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`.
82pub 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.
91pub 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.
96pub 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`.
119pub 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`.
124pub fn from_byte_slice(input: &[u8]) -> &Path {
125 try_from_byte_slice(input).expect("well-formed UTF-8 on windows")
126}
127
128fn 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.
151pub 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.
160pub 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.
172pub 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
187pub 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
195pub 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)]
47pub struct Spec(bstr::BString);
48
49mod convert;
50mod spec;
51
52pub use convert::*;
git-path/src/spec.rs+51−0
@@ -0,0 +1,51 @@
1use crate::Spec;
2use bstr::{BStr, ByteSlice, ByteVec};
3use std::ffi::OsStr;
4
5impl 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
16fn 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
24impl 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 @@
1mod 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
25# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html25# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
26 26
27[dependencies]27[dependencies]
28git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir", "bstr"]}28git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir"]}
29git-path = { version = "^0.1.0", path = "../git-path" }
29git-hash = { version = "^0.9.3", path = "../git-hash" }30git-hash = { version = "^0.9.3", path = "../git-hash" }
30git-object = { version = "^0.18.0", path = "../git-object" }31git-object = { version = "^0.18.0", path = "../git-object" }
31git-validate = { version ="^0.5.3", path = "../git-validate" }32git-validate = { version ="^0.5.3", path = "../git-validate" }
git-ref/src/fullname.rs+1−1
@@ -75,7 +75,7 @@ impl FullName { · 74 unmodified lines
75 75
76 /// Convert this name into the relative path, lossily, identifying the reference location relative to a repository76 /// Convert this name into the relative path, lossily, identifying the reference location relative to a repository
77 pub fn to_path(&self) -> &Path {77 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)
79 }79 }
80 80
81 /// Dissolve this instance and return the buffer.81 /// Dissolve this instance and return the buffer.
git-ref/src/name.rs+3−3
@@ -26,7 +26,7 @@ impl Category { · 25 unmodified lines
26impl<'a> FullNameRef<'a> {26impl<'a> FullNameRef<'a> {
27 /// Convert this name into the relative path identifying the reference location.27 /// Convert this name into the relative path identifying the reference location.
28 pub fn to_path(self) -> &'a Path {28 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)
30 }30 }
31 31
32 /// Return ourselves as byte string which is a valid refname32 /// Return ourselves as byte string which is a valid refname
@@ -79,7 +79,7 @@ impl<'a> PartialNameRef<'a> { · 46 unmodified lines
79 /// Convert this name into the relative path possibly identifying the reference location.79 /// Convert this name into the relative path possibly identifying the reference location.
80 /// Note that it may be only a partial path though.80 /// Note that it may be only a partial path though.
81 pub fn to_partial_path(&'a self) -> &'a Path {81 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())
83 }83 }
84 84
85 /// Provide the name as binary string which is known to be a valid partial ref name.85 /// 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
122 type Error = Error;122 type Error = Error;
123 123
124 fn try_from(v: &'a OsStr) -> Result<Self, Self::Error> {124 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)
126 .map_err(|_| Error::Tag(git_validate::tag::name::Error::InvalidByte("<unknown encoding>".into())))?;126 .map_err(|_| Error::Tag(git_validate::tag::name::Error::InvalidByte("<unknown encoding>".into())))?;
127 Ok(PartialNameRef(127 Ok(PartialNameRef(
128 git_validate::reference::name_partial(v.as_bstr())?.into(),128 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
18 }18 }
19 /// Return ourselves as a path for use within the filesystem.19 /// Return ourselves as a path for use within the filesystem.
20 pub fn to_path(&self) -> &Path {20 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)
22 }22 }
23 /// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration.23 /// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration.
24 pub fn into_namespaced_prefix(mut self, prefix: impl AsRef<Path>) -> PathBuf {24 pub fn into_namespaced_prefix(mut self, prefix: impl AsRef<Path>) -> PathBuf {
25 self.025 self.0.push_str(git_path::into_bstr(prefix.as_ref()).as_ref());
26 .push_str(git_features::path::into_bytes_or_panic_on_windows(prefix.as_ref()));26 git_path::to_native_path_on_windows(self.0.clone()).into_owned()
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 )
34 }27 }
35}28}
36 29
git-ref/src/store/file/find.rs+1−1
@@ -124,7 +124,7 @@ impl file::Store { · 123 unmodified lines
124 };124 };
125 let relative_path = base.join(inbetween).join(relative_path);125 let relative_path = base.join(inbetween).join(relative_path);
126 126
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));
128 let contents = match self128 let contents = match self
129 .ref_contents(&path_to_open)129 .ref_contents(&path_to_open)
130 .map_err(|err| Error::ReadFileContents {130 .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
49 .as_deref()49 .as_deref()
50 .and_then(|prefix| full_path.file_name().map(|name| (prefix, name)))50 .and_then(|prefix| full_path.file_name().map(|name| (prefix, name)))
51 {51 {
52 match git_features::path::os_str_into_bytes(name) {52 match git_path::os_str_into_bstr(name) {
53 Ok(name) => {53 Ok(name) => {
54 if !name.starts_with(prefix) {54 if !name.starts_with(prefix) {
55 continue;55 continue;
@@ -61,17 +61,16 @@ impl Iterator for SortedLoosePaths { · 5 unmodified lines
61 let full_name = full_path61 let full_name = full_path
62 .strip_prefix(&self.base)62 .strip_prefix(&self.base)
63 .expect("prefix-stripping cannot fail as prefix is our root");63 .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) {
65 Ok(name) => {65 Ok(name) => {
66 #[cfg(windows)]66 let name = git_path::to_unix_separators_on_windows(name);
67 let name = git_features::path::convert::to_unix_separators(name);
68 name.into_owned()67 name.into_owned()
69 }68 }
70 Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways?69 Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways?
71 };70 };
72 71
73 if git_validate::reference::name_partial(full_name.as_bstr()).is_ok() {72 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);
75 return Some(Ok((full_path, name)));74 return Some(Ok((full_path, name)));
76 } else {75 } else {
77 continue;76 continue;
@@ -201,8 +200,8 @@ impl file::Store { · 123 unmodified lines
201 base.file_name()200 base.file_name()
202 .map(ToOwned::to_owned)201 .map(ToOwned::to_owned)
203 .map(|p| {202 .map(|p| {
204 git_features::path::into_bytes(PathBuf::from(p))203 git_path::try_into_bstr(PathBuf::from(p))
205 .map(|p| BString::from(p.into_owned()))204 .map(|p| p.into_owned())
206 .map_err(|_| {205 .map_err(|_| {
207 std::io::Error::new(206 std::io::Error::new(
208 std::io::ErrorKind::InvalidInput,207 std::io::ErrorKind::InvalidInput,
git-ref/src/store/file/mod.rs+2−6
@@ -53,12 +53,8 @@ pub struct Transaction<'s> { · 52 unmodified lines
53}53}
54 54
55pub(in crate::store_impl::file) fn path_to_name<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> {55pub(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());56 let path = git_path::into_bstr(path.into());
57 57 git_path::to_unix_separators_on_windows(path)
58 #[cfg(windows)]
59 let path = git_features::path::convert::to_unix_separators(path);
60
61 git_features::path::convert::into_bstr(path)
62}58}
63 59
64///60///
git-repository/Cargo.toml+6−3
@@ -41,7 +41,7 @@ one-stop-shop = [ "local", "local-time-support" ] · 40 unmodified lines
41#! ### Other41#! ### Other
42 42
43## Data structures implement `serde::Serialize` and `serde::Deserialize`.43## Data structures implement `serde::Serialize` and `serde::Deserialize`.
44serde1 = ["git-pack/serde1", "git-object/serde1", "git-protocol/serde1", "git-transport/serde1", "git-ref/serde1", "git-odb/serde1", "git-index/serde1", "git-mailmap/serde1"]44serde1 = ["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"]
45## Activate other features that maximize performance, like usage of threads, `zlib-ng` and access to caching in object databases.45## Activate other features that maximize performance, like usage of threads, `zlib-ng` and access to caching in object databases.
46## **Note** that46## **Note** that
47max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git-pack/pack-cache-lru-static", "git-pack/pack-cache-lru-dynamic"]47max-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
49local-time-support = ["git-actor/local-time-support"]49local-time-support = ["git-actor/local-time-support"]
50## Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible.50## Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible.
51## Doing so is less stable than the stability tier 1 that `git-repository` is a member of.51## Doing so is less stable than the stability tier 1 that `git-repository` is a member of.
52unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials"]52unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials", "git-path", "git-attributes"]
53## Print debugging information about usage of object database caches, useful for tuning cache sizes.53## Print debugging information about usage of object database caches, useful for tuning cache sizes.
54cache-efficiency-debug = ["git-features/cache-efficiency-debug"]54cache-efficiency-debug = ["git-features/cache-efficiency-debug"]
55 55
@@ -60,7 +60,7 @@ git-ref = { version = "^0.12.1", path = "../git-ref" } · 4 unmodified lines
60git-tempfile = { version = "^2.0.0", path = "../git-tempfile" }60git-tempfile = { version = "^2.0.0", path = "../git-tempfile" }
61git-lock = { version = "^2.0.0", path = "../git-lock" }61git-lock = { version = "^2.0.0", path = "../git-lock" }
62git-validate = { version ="^0.5.3", path = "../git-validate" }62git-validate = { version ="^0.5.3", path = "../git-validate" }
63git-sec = { version = "^0.1.0", path = "../git-sec" }63git-sec = { version = "^0.1.0", path = "../git-sec", features = ["thiserror"] }
64 64
65git-config = { version = "^0.2.1", path = "../git-config" }65git-config = { version = "^0.2.1", path = "../git-config" }
66git-odb = { version = "^0.28.0", path = "../git-odb" }66git-odb = { version = "^0.28.0", path = "../git-odb" }
@@ -70,6 +70,7 @@ git-actor = { version = "^0.9.0", path = "../git-actor" } · 3 unmodified lines
70git-pack = { version = "^0.18.0", path = "../git-pack", features = ["object-cache-dynamic"] }70git-pack = { version = "^0.18.0", path = "../git-pack", features = ["object-cache-dynamic"] }
71git-revision = { version = "^0.1.0", path = "../git-revision" }71git-revision = { version = "^0.1.0", path = "../git-revision" }
72 72
73git-path = { version = "^0.1.0", path = "../git-path", optional = true }
73git-url = { version = "^0.4.0", path = "../git-url", optional = true }74git-url = { version = "^0.4.0", path = "../git-url", optional = true }
74git-traverse = { version = "^0.14.0", path = "../git-traverse" }75git-traverse = { version = "^0.14.0", path = "../git-traverse" }
75git-protocol = { version = "^0.15.0", path = "../git-protocol", optional = true }76git-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
79git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] }80git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] }
80 81
81# unstable only82# unstable only
83git-attributes = { version = "^0.1.0", path = "../git-attributes", optional = true }
82git-glob = { version = "^0.2.0", path = "../git-glob", optional = true }84git-glob = { version = "^0.2.0", path = "../git-glob", optional = true }
83git-credentials = { version = "^0.1.0", path = "../git-credentials", optional = true }85git-credentials = { version = "^0.1.0", path = "../git-credentials", optional = true }
84git-index = { version = "^0.2.0", path = "../git-index", optional = true }86git-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
98 100
99[dev-dependencies]101[dev-dependencies]
100git-testtools = { path = "../tests/tools" }102git-testtools = { path = "../tests/tools" }
103is_ci = "1.1.1"
101anyhow = "1"104anyhow = "1"
102tempfile = "3.2.0"105tempfile = "3.2.0"
103 106
git-repository/src/config.rs+85−33
@@ -1,4 +1,5 @@
1use crate::bstr::BString;1use crate::bstr::BString;
2use crate::permission::EnvVarResourcePermission;
2 3
3#[derive(Debug, thiserror::Error)]4#[derive(Debug, thiserror::Error)]
4pub enum Error {5pub enum Error {
@@ -10,85 +11,112 @@ pub enum Error { · 5 unmodified lines
10 EmptyValue { key: &'static str },11 EmptyValue { key: &'static str },
11 #[error("Invalid value for 'core.abbrev' = '{}'. It must be between 4 and {}", .value, .max)]12 #[error("Invalid value for 'core.abbrev' = '{}'. It must be between 4 and {}", .value, .max)]
12 CoreAbbrev { value: BString, max: u8 },13 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),
13}18}
14 19
15/// Utility type to keep pre-obtained configuration values.20/// Utility type to keep pre-obtained configuration values.
16#[derive(Debug, Clone)]21#[derive(Debug, Clone)]
17pub(crate) struct Cache {22pub(crate) struct Cache {
18 // TODO: remove this once resolved is used without a feature dependency23 // 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))]
20 pub resolved: crate::Config,25 pub resolved: crate::Config,
21 /// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count.26 /// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count.
22 pub hex_len: Option<usize>,27 pub hex_len: Option<usize>,
23 /// true if the repository is designated as 'bare', without work tree28 /// true if the repository is designated as 'bare', without work tree.
24 pub is_bare: bool,29 pub is_bare: bool,
25 /// The type of hash to use30 /// The type of hash to use.
26 pub object_hash: git_hash::Kind,31 pub object_hash: git_hash::Kind,
27 /// If true, multi-pack indices, whether present or not, may be used by the object database.32 /// If true, multi-pack indices, whether present or not, may be used by the object database.
28 pub use_multi_pack_index: bool,33 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,
29 // TODO: make core.precomposeUnicode available as well.46 // TODO: make core.precomposeUnicode available as well.
30}47}
31 48
32mod cache {49mod cache {
33 use std::{borrow::Cow, convert::TryFrom};50 use std::convert::TryFrom;
51 use std::path::PathBuf;
34 52
35 use git_config::{53 use git_config::{
36 file::GitConfig,54 file::GitConfig,
37 values,
38 values::{Boolean, Integer},55 values::{Boolean, Integer},
39 };56 };
40 57
41 use super::{Cache, Error};58 use super::{Cache, Error};
42 use crate::bstr::ByteSlice;59 use crate::bstr::ByteSlice;
60 use crate::permission::EnvVarResourcePermission;
43 61
44 impl Cache {62 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> {
46 let config = GitConfig::open(git_dir.join("config"))?;69 let config = GitConfig::open(git_dir.join("config"))?;
47 let is_bare = config_bool(&config, "core.bare", false);70
48 let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true);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()?;
49 let repo_format_version = config78 let repo_format_version = config
50 .value::<Integer>("core", None, "repositoryFormatVersion")79 .value::<Integer>("core", None, "repositoryFormatVersion")
51 .map_or(0, |v| v.value);80 .map_or(0, |v| v.value);
52 let object_hash = if repo_format_version == 1 {81 let object_hash = (repo_format_version != 1)
53 if let Ok(format) = config.value::<Cow<'_, [u8]>>("extensions", None, "objectFormat") {82 .then(|| Ok(git_hash::Kind::Sha1))
54 match format.as_ref() {83 .or_else(|| {
55 b"sha1" => git_hash::Kind::Sha1,84 config
56 _ => {85 .raw_value("extensions", None, "objectFormat")
57 return Err(Error::UnsupportedObjectFormat {86 .ok()
87 .map(|format| match format.as_ref() {
88 b"sha1" => Ok(git_hash::Kind::Sha1),
89 _ => Err(Error::UnsupportedObjectFormat {
58 name: format.to_vec().into(),90 name: format.to_vec().into(),
59 })91 }),
60 }92 })
61 }93 })
62 } else {94 .transpose()?
63 git_hash::Kind::Sha195 .unwrap_or(git_hash::Kind::Sha1);
64 }
65 } else {
66 git_hash::Kind::Sha1
67 };
68 96
69 let mut hex_len = None;97 let mut hex_len = None;
70 if let Ok(hex_len_str) = config.value::<values::String<'_>>("core", None, "abbrev") {98 if let Some(hex_len_str) = config.string("core", None, "abbrev") {
71 if hex_len_str.value.trim().is_empty() {99 if hex_len_str.trim().is_empty() {
72 return Err(Error::EmptyValue { key: "core.abbrev" });100 return Err(Error::EmptyValue { key: "core.abbrev" });
73 }101 }
74 if hex_len_str.value.as_ref() != "auto" {102 if hex_len_str.as_ref() != "auto" {
75 let value_bytes = hex_len_str.value.as_ref().as_ref();103 let value_bytes = hex_len_str.as_ref().as_ref();
76 if let Ok(Boolean::False(_)) = Boolean::try_from(value_bytes) {104 if let Ok(Boolean::False(_)) = Boolean::try_from(value_bytes) {
77 hex_len = object_hash.len_in_hex().into();105 hex_len = object_hash.len_in_hex().into();
78 } else {106 } else {
79 let value = Integer::try_from(value_bytes)107 let value = Integer::try_from(value_bytes)
80 .map_err(|_| Error::CoreAbbrev {108 .map_err(|_| Error::CoreAbbrev {
81 value: hex_len_str.value.clone().into_owned(),109 value: hex_len_str.clone().into_owned(),
82 max: object_hash.len_in_hex() as u8,110 max: object_hash.len_in_hex() as u8,
83 })?111 })?
84 .to_decimal()112 .to_decimal()
85 .ok_or_else(|| Error::CoreAbbrev {113 .ok_or_else(|| Error::CoreAbbrev {
86 value: hex_len_str.value.clone().into_owned(),114 value: hex_len_str.clone().into_owned(),
87 max: object_hash.len_in_hex() as u8,115 max: object_hash.len_in_hex() as u8,
88 })?;116 })?;
89 if value < 4 || value as usize > object_hash.len_in_hex() {117 if value < 4 || value as usize > object_hash.len_in_hex() {
90 return Err(Error::CoreAbbrev {118 return Err(Error::CoreAbbrev {
91 value: hex_len_str.value.clone().into_owned(),119 value: hex_len_str.clone().into_owned(),
92 max: object_hash.len_in_hex() as u8,120 max: object_hash.len_in_hex() as u8,
93 });121 });
94 }122 }
@@ -102,15 +130,39 @@ mod cache { · 7 unmodified lines
102 use_multi_pack_index,130 use_multi_pack_index,
103 object_hash,131 object_hash,
104 is_bare,132 is_bare,
133 ignore_case,
105 hex_len,134 hex_len,
135 excludes_file,
136 xdg_config_home_env,
137 home_env,
106 })138 })
107 }139 }
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 }
108 }156 }
109 157
110 fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> bool {158 fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> Result<bool, Error> {
111 let (section, key) = key.split_once('.').expect("valid section.key format");159 let (section, key) = key.split_once('.').expect("valid section.key format");
112 config160 config
113 .value::<Boolean<'_>>(section, None, key)161 .boolean(section, None, key)
114 .map_or(default, |b| b.to_bool())162 .unwrap_or(Ok(default))
163 .map_err(|err| Error::DecodeBoolean {
164 value: err.input,
165 key: key.into(),
166 })
115 }167 }
116}168}
git-repository/src/lib.rs+20−4
@@ -83,12 +83,14 @@ · 82 unmodified lines
83//! even if this crate doesn't, hence breaking downstream.83//! even if this crate doesn't, hence breaking downstream.
84//!84//!
85//! `git_repository::`85//! `git_repository::`
86//! * [`attrs`]
86//! * [`hash`]87//! * [`hash`]
87//! * [`url`]88//! * [`url`]
88//! * [`actor`]89//! * [`actor`]
89//! * [`bstr`][bstr]90//! * [`bstr`][bstr]
90//! * [`index`]91//! * [`index`]
91//! * [`glob`]92//! * [`glob`]
93//! * [`path`]
92//! * [`credentials`]94//! * [`credentials`]
93//! * [`sec`]95//! * [`sec`]
94//! * [`worktree`]96//! * [`worktree`]
@@ -124,6 +126,8 @@ use std::path::PathBuf; · 29 unmodified lines
124// This also means that their major version changes affect our major version, but that's alright as we directly expose their126// This also means that their major version changes affect our major version, but that's alright as we directly expose their
125// APIs/instances anyway.127// APIs/instances anyway.
126pub use git_actor as actor;128pub use git_actor as actor;
129#[cfg(all(feature = "unstable", feature = "git-attributes"))]
130pub use git_attributes as attrs;
127#[cfg(all(feature = "unstable", feature = "git-credentials"))]131#[cfg(all(feature = "unstable", feature = "git-credentials"))]
128pub use git_credentials as credentials;132pub use git_credentials as credentials;
129#[cfg(all(feature = "unstable", feature = "git-diff"))]133#[cfg(all(feature = "unstable", feature = "git-diff"))]
@@ -156,8 +160,6 @@ pub use git_url as url; · 26 unmodified lines
156#[doc(inline)]160#[doc(inline)]
157#[cfg(all(feature = "unstable", feature = "git-url"))]161#[cfg(all(feature = "unstable", feature = "git-url"))]
158pub use git_url::Url;162pub use git_url::Url;
159#[cfg(all(feature = "unstable", feature = "git-worktree"))]
160pub use git_worktree as worktree;
161pub use hash::{oid, ObjectId};163pub use hash::{oid, ObjectId};
162 164
163pub mod interrupt;165pub mod interrupt;
@@ -192,7 +194,9 @@ pub enum Path { · 28 unmodified lines
192 194
193///195///
194mod types;196mod types;
195pub use types::{Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree};197pub use types::{
198 Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree, Worktree,
199};
196 200
197pub mod commit;201pub mod commit;
198pub mod head;202pub mod head;
@@ -200,7 +204,6 @@ pub mod id; · 1 unmodified line
200pub mod object;204pub mod object;
201pub mod reference;205pub mod reference;
202mod repository;206mod repository;
203pub use repository::{permissions, permissions::Permissions};
204pub mod tag;207pub mod tag;
205 208
206/// The kind of `Repository`209/// The kind of `Repository`
@@ -239,6 +242,16 @@ pub fn open(directory: impl Into<std::path::PathBuf>) -> Result<crate::Repositor · 32 unmodified lines
239 ThreadSafeRepository::open(directory).map(Into::into)242 ThreadSafeRepository::open(directory).map(Into::into)
240}243}
241 244
245///
246pub 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}
253pub use repository::permissions::Permissions;
254
242///255///
243pub mod open;256pub mod open;
244 257
@@ -268,6 +281,9 @@ pub mod mailmap { · 23 unmodified lines
268 }281 }
269}282}
270 283
284///
285pub mod worktree;
286
271///287///
272pub mod rev_parse {288pub mod rev_parse {
273 /// The error returned by [`crate::Repository::rev_parse()`].289 /// 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
153 Options {153 Options {
154 object_store_slots,154 object_store_slots,
155 replacement_objects,155 replacement_objects,
156 permissions,156 permissions:
157 Permissions {
158 git_dir: git_dir_perm,
159 xdg_config_home,
160 home,
161 },
157 }: Options,162 }: Options,
158 ) -> Result<Self, Error> {163 ) -> Result<Self, Error> {
159 if *permissions.git_dir != git_sec::ReadWrite::all() {164 if *git_dir_perm != git_sec::ReadWrite::all() {
160 // TODO: respect `save.directory`, which needs more support from git-config to do properly.165 // TODO: respect `save.directory`, which needs more support from git-config to do properly.
161 return Err(Error::UnsafeGitDir { path: git_dir });166 return Err(Error::UnsafeGitDir { path: git_dir });
162 }167 }
@@ -164,7 +169,12 @@ impl crate::ThreadSafeRepository { · 1 unmodified line
164 // This would be something read in later as have to first check for extensions. Also this means169 // This would be something read in later as have to first check for extensions. Also this means
165 // that each worktree, even if accessible through this instance, has to come in its own Repository instance170 // that each worktree, even if accessible through this instance, has to come in its own Repository instance
166 // as it may have its own configuration. That's fine actually.171 // 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 )?;
168 match worktree_dir {178 match worktree_dir {
169 None if !config.is_bare => {179 None if !config.is_bare => {
170 worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned());180 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
8 InaccessibleDirectory { path: PathBuf },8 InaccessibleDirectory { path: PathBuf },
9 #[error("Could find a git repository in '{}' or in any of its parents", .path.display())]9 #[error("Could find a git repository in '{}' or in any of its parents", .path.display())]
10 NoGitRepository { path: PathBuf },10 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 },
11 #[error("Could not determine trust level for path '{}'.", .path.display())]17 #[error("Could not determine trust level for path '{}'.", .path.display())]
12 CheckTrust {18 CheckTrust {
13 path: PathBuf,19 path: PathBuf,
@@ -57,23 +63,20 @@ pub(crate) mod function { · 43 unmodified lines
57 // us the parent directory. (`Path::parent` just strips off the last63 // us the parent directory. (`Path::parent` just strips off the last
58 // path component, which means it will not do what you expect when64 // path component, which means it will not do what you expect when
59 // working with paths paths that contain '..'.)65 // working with paths paths that contain '..'.)
60 let directory = maybe_canonicalize(directory.as_ref()).map_err(|_| Error::InaccessibleDirectory {66 let directory = directory.as_ref();
61 path: directory.as_ref().into(),67 let dir = maybe_canonicalize(directory).map_err(|_| Error::InaccessibleDirectory { path: directory.into() })?;
62 })?;68 let is_canonicalized = dir.as_ref() != directory;
63 if !directory.is_dir() {69 if !dir.is_dir() {
64 return Err(Error::InaccessibleDirectory {70 return Err(Error::InaccessibleDirectory { path: dir.into_owned() });
65 path: directory.into_owned(),
66 });
67 }71 }
68 72
69 let filter_by_trust =73 let filter_by_trust = |x: &std::path::Path| -> Result<Option<git_sec::Trust>, Error> {
70 |x: &std::path::Path, kind: crate::path::Kind| -> Result<Option<(crate::Path, git_sec::Trust)>, Error> {74 let trust =
71 let trust =75 git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?;
72 git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?;76 Ok((trust >= required_trust).then(|| (trust)))
73 Ok((trust >= required_trust).then(|| (crate::Path::from_dot_git_dir(x, kind), trust)))77 };
74 };
75 78
76 let mut cursor = directory.clone();79 let mut cursor = dir.clone();
77 'outer: loop {80 'outer: loop {
78 for append_dot_git in &[false, true] {81 for append_dot_git in &[false, true] {
79 if *append_dot_git {82 if *append_dot_git {
@@ -83,11 +86,36 @@ pub(crate) mod function { · 3 unmodified lines
83 }86 }
84 }87 }
85 if let Ok(kind) = path::is::git(&cursor) {88 if let Ok(kind) = path::is::git(&cursor) {
86 match filter_by_trust(&cursor, kind)? {89 match filter_by_trust(&cursor)? {
87 Some(res) => break 'outer Ok(res),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 }
88 None => {114 None => {
89 break 'outer Err(Error::NoGitRepository {115 break 'outer Err(Error::NoTrustedGitRepository {
90 path: directory.into_owned(),116 path: dir.into_owned(),
117 candidate: cursor.into_owned(),
118 required: required_trust,
91 })119 })
92 }120 }
93 }121 }
@@ -100,11 +128,7 @@ pub(crate) mod function { · 6 unmodified lines
100 }128 }
101 match cursor.parent() {129 match cursor.parent() {
102 Some(parent) => cursor = parent.to_owned().into(),130 Some(parent) => cursor = parent.to_owned().into(),
103 None => {131 None => break Err(Error::NoGitRepository { path: dir.into_owned() }),
104 break Err(Error::NoGitRepository {
105 path: directory.into_owned(),
106 })
107 }
108 }132 }
109 }133 }
110 }134 }
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
28/// What constitutes a valid git repository, and what's yet to be implemented, returning the guessed repository kind28/// What constitutes a valid git repository, and what's yet to be implemented, returning the guessed repository kind
29/// purely based on the presence of files. Note that the git-config ultimately decides what's bare.29/// purely based on the presence of files. Note that the git-config ultimately decides what's bare.
30///30///
31/// * [ ] git files
31/// * [x] a valid head32/// * [x] a valid head
32/// * [ ] git common directory33/// * [ ] git common directory
33/// * [ ] respect GIT_COMMON_DIR34/// * [ ] respect GIT_COMMON_DIR
git-repository/src/path/mod.rs+16−1
@@ -2,6 +2,9 @@ use std::path::PathBuf; · 1 unmodified line
2 2
3use crate::{Kind, Path};3use crate::{Kind, Path};
4 4
5#[cfg(all(feature = "unstable", feature = "git-path"))]
6pub use git_path::*;
7
5///8///
6pub mod create;9pub mod create;
7///10///
@@ -24,7 +27,11 @@ impl Path { · 16 unmodified lines
24 pub fn from_dot_git_dir(dir: impl Into<PathBuf>, kind: Kind) -> Self {27 pub fn from_dot_git_dir(dir: impl Into<PathBuf>, kind: Kind) -> Self {
25 let dir = dir.into();28 let dir = dir.into();
26 match kind {29 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 }),
28 Kind::Bare => Path::Repository(dir),35 Kind::Bare => Path::Repository(dir),
29 }36 }
30 }37 }
@@ -44,3 +51,11 @@ impl Path { · 13 unmodified lines
44 }51 }
45 }52 }
46}53}
54
55pub(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 @@
1use std::cell::RefCell;
2
3impl 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
12 // TODO: tests, respect precomposeUnicode12 // TODO: tests, respect precomposeUnicode
13 /// The directory of the binary path of the current process.13 /// The directory of the binary path of the current process.
14 pub fn install_dir(&self) -> std::io::Result<std::path::PathBuf> {14 pub fn install_dir(&self) -> std::io::Result<std::path::PathBuf> {
15 std::env::current_exe().and_then(|exe| {15 crate::path::install_dir()
16 exe.parent()16 }
17 .map(ToOwned::to_owned)17
18 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no parent for current executable"))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 })
19 })40 })
20 }41 }
21 42
git-repository/src/repository/mod.rs+4−90
@@ -39,98 +39,12 @@ impl crate::Repository { · 38 unmodified lines
39 }39 }
40}40}
41 41
42/// Various permissions for parts of git repositories.42mod worktree;
43pub 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 }
83 43
84 impl git_sec::trust::DefaultForLevel for Permissions {44/// Various permissions for parts of git repositories.
85 fn default_for_level(level: Trust) -> Self {45pub(crate) mod permissions;
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
100mod 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 }
127 46
128 /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data.47mod init;
129 pub fn into_sync(self) -> crate::ThreadSafeRepository {
130 self.into()
131 }
132 }
133}
134 48
135mod location;49mod location;
136 50
git-repository/src/repository/permissions.rs+67−0
@@ -0,0 +1,67 @@
1use crate::permission::EnvVarResourcePermission;
2use git_sec::permission::Resource;
3use git_sec::{Access, Trust};
4
5/// Permissions associated with various resources of a git repository
6pub 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
19impl 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
54impl 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
63impl 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
48 let mut blob_id = self48 let mut blob_id = self
49 .config49 .config
50 .resolved50 .resolved
51 .get_raw_value("mailmap", None, "blob")51 .raw_value("mailmap", None, "blob")
52 .ok()52 .ok()
53 .and_then(|spec| {53 .and_then(|spec| {
54 // TODO: actually resolve this as spec (once we can do that)54 // TODO: actually resolve this as spec (once we can do that)
git-repository/src/repository/worktree.rs+21−0
@@ -0,0 +1,21 @@
1use crate::{worktree, Worktree};
2
3impl 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
10impl<'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
4 4
5use crate::head;5use crate::head;
6 6
7/// A worktree checkout containing the files of the repository in consumable form.
8pub 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
7/// The head reference, as created from looking at `.git/HEAD`, able to represent all of its possible states.16/// The head reference, as created from looking at `.git/HEAD`, able to represent all of its possible states.
8///17///
9/// Note that like [`Reference`], this type's data is snapshot of persisted state on disk.18/// Note that like [`Reference`], this type's data is snapshot of persisted state on disk.
10pub struct Head<'repo> {19pub struct Head<'repo> {
11 /// One of various possible states for the HEAD reference20 /// One of various possible states for the HEAD reference
12 pub kind: head::Kind,21 pub kind: head::Kind,
13 pub(crate) repo: &'repo crate::Repository,22 pub(crate) repo: &'repo Repository,
14}23}
15 24
16/// An [ObjectId] with access to a repository.25/// An [ObjectId] with access to a repository.
@@ -18,7 +27,7 @@ pub struct Head<'repo> { · 1 unmodified line
18pub struct Id<'r> {27pub struct Id<'r> {
19 /// The actual object id28 /// The actual object id
20 pub(crate) inner: ObjectId,29 pub(crate) inner: ObjectId,
21 pub(crate) repo: &'r crate::Repository,30 pub(crate) repo: &'r Repository,
22}31}
23 32
24/// A decoded object with a reference to its owning repository.33/// A decoded object with a reference to its owning repository.
@@ -29,7 +38,7 @@ pub struct Object<'repo> { · 4 unmodified lines
29 pub kind: git_object::Kind,38 pub kind: git_object::Kind,
30 /// The fully decoded object data39 /// The fully decoded object data
31 pub data: Vec<u8>,40 pub data: Vec<u8>,
32 pub(crate) repo: &'repo crate::Repository,41 pub(crate) repo: &'repo Repository,
33}42}
34 43
35impl<'a> Drop for Object<'a> {44impl<'a> Drop for Object<'a> {
@@ -44,7 +53,7 @@ pub struct Tree<'repo> { · 8 unmodified lines
44 pub id: ObjectId,53 pub id: ObjectId,
45 /// The fully decoded tree data54 /// The fully decoded tree data
46 pub data: Vec<u8>,55 pub data: Vec<u8>,
47 pub(crate) repo: &'repo crate::Repository,56 pub(crate) repo: &'repo Repository,
48}57}
49 58
50impl<'a> Drop for Tree<'a> {59impl<'a> Drop for Tree<'a> {
@@ -59,7 +68,7 @@ pub struct Tag<'repo> { · 8 unmodified lines
59 pub id: ObjectId,68 pub id: ObjectId,
60 /// The fully decoded tag data69 /// The fully decoded tag data
61 pub data: Vec<u8>,70 pub data: Vec<u8>,
62 pub(crate) repo: &'repo crate::Repository,71 pub(crate) repo: &'repo Repository,
63}72}
64 73
65impl<'a> Drop for Tag<'a> {74impl<'a> Drop for Tag<'a> {
@@ -74,7 +83,7 @@ pub struct Commit<'repo> { · 8 unmodified lines
74 pub id: ObjectId,83 pub id: ObjectId,
75 /// The fully decoded commit data84 /// The fully decoded commit data
76 pub data: Vec<u8>,85 pub data: Vec<u8>,
77 pub(crate) repo: &'repo crate::Repository,86 pub(crate) repo: &'repo Repository,
78}87}
79 88
80impl<'a> Drop for Commit<'a> {89impl<'a> Drop for Commit<'a> {
@@ -102,7 +111,7 @@ pub struct DetachedObject { · 21 unmodified lines
102pub struct Reference<'r> {111pub struct Reference<'r> {
103 /// The actual reference data112 /// The actual reference data
104 pub inner: git_ref::Reference,113 pub inner: git_ref::Reference,
105 pub(crate) repo: &'r crate::Repository,114 pub(crate) repo: &'r Repository,
106}115}
107 116
108/// A thread-local handle to interact with a repository from a single thread.117/// A thread-local handle to interact with a repository from a single thread.
@@ -127,7 +136,7 @@ pub struct Repository { · 18 unmodified lines
127/// An instance with access to everything a git repository entails, best imagined as container implementing `Sync + Send` for _most_136/// An instance with access to everything a git repository entails, best imagined as container implementing `Sync + Send` for _most_
128/// for system resources required to interact with a `git` repository which are loaded in once the instance is created.137/// for system resources required to interact with a `git` repository which are loaded in once the instance is created.
129///138///
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].
131///140///
132/// Note that this type purposefully isn't very useful until it is converted into a thread-local repository with `to_thread_local()`,141/// Note that this type purposefully isn't very useful until it is converted into a thread-local repository with `to_thread_local()`,
133/// it's merely meant to be able to exist in a `Sync` context.142/// it's merely meant to be able to exist in a `Sync` context.
git-repository/src/worktree.rs+129−0
@@ -0,0 +1,129 @@
1use crate::Repository;
2#[cfg(all(feature = "unstable", feature = "git-worktree"))]
3pub use git_worktree::*;
4
5///
6#[cfg(feature = "git-index")]
7pub 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")]
27pub 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.
42pub struct Platform<'repo> {
43 pub(crate) parent: &'repo Repository,
44}
45
46/// Access
47impl<'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
54impl<'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 @@
1mod existing {1mod existing {
2 use std::path::{Component, PathBuf};2 use std::path::PathBuf;
3 3
4 use git_repository::Kind;4 use git_repository::Kind;
5 5
@@ -71,17 +71,20 @@ mod existing { · 65 unmodified lines
71 // up far enough. (This tests that `discover::existing` canonicalizes paths before71 // up far enough. (This tests that `discover::existing` canonicalizes paths before
72 // exploring ancestors.)72 // exploring ancestors.)
73 let working_dir = repo_path()?;73 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/../../../../../..");
75 let (path, trust) = git_repository::path::discover(&dir)?;75 let (path, trust) = git_repository::path::discover(&dir)?;
76 assert_eq!(path.kind(), Kind::WorkTree);76 assert_eq!(path.kind(), Kind::WorkTree);
77 assert_eq!(77 if !(cfg!(windows) && is_ci::cached()) {
78 path.as_ref()78 // On CI on windows we get a cursor like this with a question mark so our prefix check won't work.
79 .components()79 // We recover, but that means this assertion will fail.
80 .filter(|c| matches!(c, Component::ParentDir | Component::CurDir))80 // &cursor = "\\\\?\\D:\\a\\gitoxide\\gitoxide\\.git"
81 .count(),81 // &cwd = "D:\\a\\gitoxide\\gitoxide\\git-repository"
82 0,82 assert_eq!(
83 "there are no relative path components anymore"83 path.as_ref(),
84 );84 std::path::Path::new(".."),
85 "there is only the minimal amount of relative path components to see this worktree"
86 );
87 }
85 assert_ne!(88 assert_ne!(
86 path.as_ref().canonicalize()?,89 path.as_ref().canonicalize()?,
87 working_dir.canonicalize()?,90 working_dir.canonicalize()?,
git-sec/Cargo.toml+1−0
@@ -19,6 +19,7 @@ serde1 = [ "serde" ] · 18 unmodified lines
19[dependencies]19[dependencies]
20serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] }20serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] }
21bitflags = "1.3.2"21bitflags = "1.3.2"
22thiserror = { version = "1.0.26", optional = true }
22 23
23[target.'cfg(not(windows))'.dependencies]24[target.'cfg(not(windows))'.dependencies]
24libc = "0.2.123"25libc = "0.2.123"
git-sec/src/lib.rs+66−5
@@ -1,6 +1,7 @@
1#![deny(unsafe_code, rust_2018_idioms, missing_docs)]1#![deny(unsafe_code, rust_2018_idioms, missing_docs)]
2//! A shared trust model for `gitoxide` crates.2//! A shared trust model for `gitoxide` crates.
3 3
4use std::fmt::{Debug, Display, Formatter};
4use std::marker::PhantomData;5use std::marker::PhantomData;
5use std::ops::Deref;6use std::ops::Deref;
6 7
@@ -75,19 +76,22 @@ pub mod trust { · 68 unmodified lines
75///76///
76pub mod permission {77pub mod permission {
77 use crate::Access;78 use crate::Access;
79 use std::fmt::{Debug, Display};
78 80
79 /// A marker trait to signal tags for permissions.81 /// A marker trait to signal tags for permissions.
80 pub trait Tag {}82 pub trait Tag: Debug + Clone {}
81 83
82 /// A tag indicating that a permission is applying to the contents of a configuration file.84 /// A tag indicating that a permission is applying to the contents of a configuration file.
85 #[derive(Debug, Clone)]
83 pub struct Config;86 pub struct Config;
84 impl Tag for Config {}87 impl Tag for Config {}
85 88
86 /// A tag indicating that a permission is applying to the resource itself.89 /// A tag indicating that a permission is applying to the resource itself.
90 #[derive(Debug, Clone)]
87 pub struct Resource;91 pub struct Resource;
88 impl Tag for Resource {}92 impl Tag for Resource {}
89 93
90 impl<P> Access<Config, P> {94 impl<P: Debug + Display + Clone> Access<Config, P> {
91 /// Create a permission for values contained in git configuration files.95 /// Create a permission for values contained in git configuration files.
92 ///96 ///
93 /// This applies permissions to values contained inside of these files.97 /// This applies permissions to values contained inside of these files.
@@ -99,7 +103,7 @@ pub mod permission { · 5 unmodified lines
99 }103 }
100 }104 }
101 105
102 impl<P> Access<Resource, P> {106 impl<P: Debug + Display + Clone> Access<Resource, P> {
103 /// Create a permission a file or directory itself.107 /// Create a permission a file or directory itself.
104 ///108 ///
105 /// This applies permissions to a configuration file itself and whether it can be used at all, or to a directory109 /// 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
111 }115 }
112 }116 }
113 }117 }
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 }
114}132}
115 133
116/// Allow, deny or forbid using a resource or performing an action.134/// Allow, deny or forbid using a resource or performing an action.
@@ -125,6 +143,36 @@ pub enum Permission { · 8 unmodified lines
125 Allow,143 Allow,
126}144}
127 145
146impl 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
163impl 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
128bitflags::bitflags! {176bitflags::bitflags! {
129 /// Whether something can be read or written.177 /// Whether something can be read or written.
130 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]178 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
@@ -136,14 +184,27 @@ bitflags::bitflags! { · 5 unmodified lines
136 }184 }
137}185}
138 186
187impl Display for ReadWrite {
188 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
189 Debug::fmt(self, f)
190 }
191}
192
139/// A container to define tagged access permissions, rendering the permission read-only.193/// A container to define tagged access permissions, rendering the permission read-only.
140pub struct Access<T: permission::Tag, P> {194#[derive(Debug, Clone)]
195pub struct Access<T: permission::Tag, P: Debug + Display + Clone> {
141 /// The access permission itself.196 /// The access permission itself.
142 permission: P,197 permission: P,
143 _data: PhantomData<T>,198 _data: PhantomData<T>,
144}199}
145 200
146impl<T: permission::Tag, P> Deref for Access<T, P> {201impl<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
207impl<T: permission::Tag, P: Debug + Display + Clone> Deref for Access<T, P> {
147 type Target = P;208 type Target = P;
148 209
149 fn deref(&self) -> &Self::Target {210 fn deref(&self) -> &Self::Target {
git-url/Cargo.toml+1−0
@@ -20,6 +20,7 @@ serde1 = ["serde", "bstr/serde1"] · 19 unmodified lines
20[dependencies]20[dependencies]
21serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]}21serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]}
22git-features = { version = "^0.20.0", path = "../git-features" }22git-features = { version = "^0.20.0", path = "../git-features" }
23git-path = { version = "^0.1.0", path = "../git-path" }
23quick-error = "2.0.0"24quick-error = "2.0.0"
24url = "2.1.1"25url = "2.1.1"
25bstr = { version = "0.2.13", default-features = false, features = ["std"] }26bstr = { 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
106 fn make_relative(path: &Path) -> PathBuf {106 fn make_relative(path: &Path) -> PathBuf {
107 path.components().skip(1).collect()107 path.components().skip(1).collect()
108 }108 }
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() })?;
110 Ok(match user {110 Ok(match user {
111 Some(user) => home_for_user(user)111 Some(user) => home_for_user(user)
112 .ok_or_else(|| Error::MissingHome(user.to_owned().into()))?112 .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
33git-index = { version = "^0.2.0", path = "../git-index" }33git-index = { version = "^0.2.0", path = "../git-index" }
34git-hash = { version = "^0.9.3", path = "../git-hash" }34git-hash = { version = "^0.9.3", path = "../git-hash" }
35git-object = { version = "^0.18.0", path = "../git-object" }35git-object = { version = "^0.18.0", path = "../git-object" }
36git-glob = { version = "^0.2.0", path = "../git-glob" }
37git-path = { version = "^0.1.0", path = "../git-path" }
38git-attributes = { version = "^0.1.0", path = "../git-attributes" }
36git-features = { version = "^0.20.0", path = "../git-features" }39git-features = { version = "^0.20.0", path = "../git-features" }
37 40
38serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}41serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
git-worktree/src/fs/cache.rs+0−185
@@ -1,185 +0,0 @@
1use super::Cache;
2use crate::fs::Stack;
3use crate::{fs, os};
4use std::path::{Path, PathBuf};
5
6#[derive(Clone)]
7pub 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
28impl 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)]
49impl 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
73pub struct Platform<'a> {
74 parent: &'a Cache,
75}
76
77impl<'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
84impl<'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
90impl Cache {
91 fn assure_init(&mut self) -> std::io::Result<()> {
92 Ok(())
93 }
94}
95
96impl 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
147fn 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 @@
1use super::Cache;
2use crate::fs;
3use crate::fs::PathOidMapping;
4use bstr::{BStr, ByteSlice};
5use git_hash::oid;
6use std::path::{Path, PathBuf};
7
8#[derive(Clone)]
9pub 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)]
33impl<'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]
61pub struct Platform<'a, 'paths> {
62 parent: &'a Cache<'paths>,
63 is_dir: Option<bool>,
64}
65
66impl<'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
137mod platform;
138///
139pub mod state;
git-worktree/src/fs/cache/platform.rs+165−0
@@ -0,0 +1,165 @@
1use crate::fs;
2use crate::fs::cache::{Platform, State};
3use crate::fs::PathOidMapping;
4use bstr::ByteSlice;
5use git_hash::oid;
6use std::path::Path;
7
8impl<'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
39impl<'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
45pub 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
53impl<'a, 'paths, Find, E> fs::stack::Delegate for StackDelegate<'a, 'paths, Find>
54where
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
128fn 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 @@
1use crate::fs::cache::State;
2use crate::fs::PathOidMapping;
3use bstr::{BStr, BString, ByteSlice};
4use git_glob::pattern::Case;
5use git_hash::oid;
6use std::path::Path;
7
8type AttributeMatchGroup = git_attributes::MatchGroup<git_attributes::Attributes>;
9type 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)]
14pub 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)]
24pub 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
43impl 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
66impl 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
177impl Attributes {
178 pub fn new(globals: AttributeMatchGroup) -> Self {
179 Attributes {
180 globals,
181 stack: Default::default(),
182 }
183 }
184}
185
186impl From<AttributeMatchGroup> for Attributes {
187 fn from(group: AttributeMatchGroup) -> Self {
188 Attributes::new(group)
189 }
190}
191
192impl 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
214impl 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 @@
1use bstr::BStr;
1use std::path::PathBuf;2use std::path::PathBuf;
2 3
3/// Common knowledge about the worktree that is needed across most interactions with the work tree4/// 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
21 pub symlink: bool,22 pub symlink: bool,
22}23}
23 24
25#[derive(Clone)]
24pub struct Stack {26pub struct Stack {
25 /// The prefix/root for all paths we handle.27 /// The prefix/root for all paths we handle.
26 root: PathBuf,28 root: PathBuf,
@@ -30,6 +32,8 @@ pub struct Stack { · 3 unmodified lines
30 current_relative: PathBuf,32 current_relative: PathBuf,
31 /// The amount of path components of 'current' beyond the roots components.33 /// The amount of path components of 'current' beyond the roots components.
32 valid_components: usize,34 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,
33}37}
34 38
35/// A cache for efficiently executing operations on directories and files which are encountered in sorted order.39/// 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
52/// As directories are created, the cache will be adjusted to reflect the latest seen directory.56/// As directories are created, the cache will be adjusted to reflect the latest seen directory.
53///57///
54/// The caching is only useful if consecutive calls to create a directory are using a sorted list of entries.58/// The caching is only useful if consecutive calls to create a directory are using a sorted list of entries.
55#[allow(unused)]59#[derive(Clone)]
56pub struct Cache {60pub struct Cache<'paths> {
57 stack: Stack,61 stack: Stack,
58 /// tells us what to do as we change paths.62 /// 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>>,
60}70}
61 71
72pub(crate) type PathOidMapping<'paths> = (&'paths BStr, git_hash::ObjectId);
73
62///74///
63pub mod cache;75pub mod cache;
64///76///
git-worktree/src/fs/stack.rs+29−5
@@ -15,6 +15,12 @@ impl Stack { · 14 unmodified lines
15 }15 }
16}16}
17 17
18pub 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
18impl Stack {24impl Stack {
19 /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes25 /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes
20 /// symbolic links to be included in it as well.26 /// symbolic links to be included in it as well.
@@ -25,6 +31,7 @@ impl Stack { · 4 unmodified lines
25 current_relative: PathBuf::with_capacity(128),31 current_relative: PathBuf::with_capacity(128),
26 valid_components: 0,32 valid_components: 0,
27 root,33 root,
34 current_is_directory: true,
28 }35 }
29 }36 }
30 37
@@ -32,17 +39,23 @@ impl Stack { · 1 unmodified line
32 /// along with the stacks state for inspection to perform an operation that produces some data.39 /// along with the stacks state for inspection to perform an operation that produces some data.
33 ///40 ///
34 /// The full path to `relative` will be returned along with the data returned by push_comp.41 /// 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.
35 pub fn make_relative_path_current(44 pub fn make_relative_path_current(
36 &mut self,45 &mut self,
37 relative: impl AsRef<Path>,46 relative: impl AsRef<Path>,
38 mut push_comp: impl FnMut(&mut std::iter::Peekable<std::path::Components<'_>>, &Self) -> std::io::Result<()>,47 delegate: &mut impl Delegate,
39 mut pop_comp: impl FnMut(&Self),
40 ) -> std::io::Result<()> {48 ) -> std::io::Result<()> {
41 let relative = relative.as_ref();49 let relative = relative.as_ref();
42 debug_assert!(50 debug_assert!(
43 relative.is_relative(),51 relative.is_relative(),
44 "only index paths are handled correctly here, must be relative"52 "only index paths are handled correctly here, must be relative"
45 );53 );
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 }
46 59
47 let mut components = relative.components().peekable();60 let mut components = relative.components().peekable();
48 let mut existing_components = self.current_relative.components();61 let mut existing_components = self.current_relative.components();
@@ -59,21 +72,32 @@ impl Stack { · 10 unmodified lines
59 for _ in 0..self.valid_components - matching_components {72 for _ in 0..self.valid_components - matching_components {
60 self.current.pop();73 self.current.pop();
61 self.current_relative.pop();74 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;
63 }79 }
64 self.valid_components = matching_components;80 self.valid_components = matching_components;
65 81
82 if !self.current_is_directory && components.peek().is_some() {
83 delegate.push_directory(self)?;
84 }
85
66 while let Some(comp) = components.next() {86 while let Some(comp) = components.next() {
87 let is_last_component = components.peek().is_none();
88 self.current_is_directory = !is_last_component;
67 self.current.push(comp);89 self.current.push(comp);
68 self.current_relative.push(comp);90 self.current_relative.push(comp);
69 self.valid_components += 1;91 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 }
71 96
72 if let Err(err) = res {97 if let Err(err) = res {
73 self.current.pop();98 self.current.pop();
74 self.current_relative.pop();99 self.current_relative.pop();
75 self.valid_components -= 1;100 self.valid_components -= 1;
76 pop_comp(&*self);
77 return Err(err);101 return Err(err);
78 }102 }
79 }103 }
git-worktree/src/index/checkout.rs+4−4
@@ -1,5 +1,5 @@
1use bstr::BString;1use bstr::BString;
2use std::path::PathBuf;2use git_attributes::Attributes;
3 3
4#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]4#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
5pub struct Collision {5pub struct Collision {
@@ -57,8 +57,8 @@ pub struct Options { · 51 unmodified lines
57 ///57 ///
58 /// Default true.58 /// Default true.
59 pub check_stat: bool,59 pub check_stat: bool,
60 /// The location of the per-user attributes file. It must exist if it is set, causing failure otherwise.60 /// A group of attribute patterns that are applied globally, i.e. aren't rooted within the repository itself.
61 pub attributes_file: Option<PathBuf>,61 pub attribute_globals: git_attributes::MatchGroup<Attributes>,
62}62}
63 63
64impl Default for Options {64impl Default for Options {
@@ -71,7 +71,7 @@ impl Default for Options { · 6 unmodified lines
71 trust_ctime: true,71 trust_ctime: true,
72 check_stat: true,72 check_stat: true,
73 overwrite_existing: false,73 overwrite_existing: false,
74 attributes_file: None,74 attribute_globals: Default::default(),
75 }75 }
76 }76 }
77}77}
git-worktree/src/index/entry.rs+9−9
@@ -7,9 +7,9 @@ use io_close::Close; · 6 unmodified lines
7 7
8use crate::{fs, index, os};8use crate::{fs, index, os};
9 9
10pub struct Context<'a, Find> {10pub struct Context<'a, 'paths, Find> {
11 pub find: &'a mut Find,11 pub find: &'a mut Find,
12 pub path_cache: &'a mut fs::Cache,12 pub path_cache: &'a mut fs::Cache<'paths>,
13 pub buf: &'a mut Vec<u8>,13 pub buf: &'a mut Vec<u8>,
14}14}
15 15
@@ -17,7 +17,7 @@ pub struct Context<'a, Find> { · 1 unmodified line
17pub fn checkout<Find, E>(17pub fn checkout<Find, E>(
18 entry: &mut Entry,18 entry: &mut Entry,
19 entry_path: &BStr,19 entry_path: &BStr,
20 Context { find, path_cache, buf }: Context<'_, Find>,20 Context { find, path_cache, buf }: Context<'_, '_, Find>,
21 index::checkout::Options {21 index::checkout::Options {
22 fs: crate::fs::Capabilities {22 fs: crate::fs::Capabilities {
23 symlink,23 symlink,
@@ -33,11 +33,11 @@ where · 9 unmodified lines
33 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,33 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
34 E: std::error::Error + Send + Sync + 'static,34 E: std::error::Error + Send + Sync + 'static,
35{35{
36 let dest_relative =36 let dest_relative = git_path::try_from_bstr(entry_path).map_err(|_| index::checkout::Error::IllformedUtf8 {
37 git_features::path::from_byte_slice(entry_path).map_err(|_| index::checkout::Error::IllformedUtf8 {37 path: entry_path.to_owned(),
38 path: entry_path.to_owned(),38 })?;
39 })?;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_entry(dest_relative, entry.mode)?.leading_dir();40 let dest = path_cache.at_path(dest_relative, is_dir, &mut *find)?.path();
41 41
42 let object_size = match entry.mode {42 let object_size = match entry.mode {
43 git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => {43 git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => {
@@ -81,7 +81,7 @@ where · 37 unmodified lines
81 oid: entry.id,81 oid: entry.id,
82 path: dest.to_path_buf(),82 path: dest.to_path_buf(),
83 })?;83 })?;
84 let symlink_destination = git_features::path::from_byte_slice(obj.data)84 let symlink_destination = git_path::try_from_byte_slice(obj.data)
85 .map_err(|_| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?;85 .map_err(|_| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?;
86 86
87 if symlink {87 if symlink {
git-worktree/src/index/mod.rs+46−35
@@ -21,22 +21,36 @@ pub fn checkout<Find, E>( · 20 unmodified lines
21 should_interrupt: &AtomicBool,21 should_interrupt: &AtomicBool,
22 options: checkout::Options,22 options: checkout::Options,
23) -> Result<checkout::Outcome, checkout::Error<E>>23) -> Result<checkout::Outcome, checkout::Error<E>>
24where
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)]
34fn 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>>
24where44where
25 Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E> + Send + Clone,45 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,46 E: std::error::Error + Send + Sync + 'static,
27{47{
28 let num_files = AtomicUsize::default();48 let num_files = AtomicUsize::default();
29 let dir = dir.into();49 let dir = dir.into();
30 50 let case = if options.fs.ignore_case {
31 let mut ctx = chunk::Context {51 git_glob::pattern::Case::Fold
32 buf: Vec::new(),52 } else {
33 path_cache: fs::Cache::new(53 git_glob::pattern::Case::Sensitive
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,
40 };54 };
41 let (chunk_size, thread_limit, num_threads) = git_features::parallel::optimize_chunk_size_and_thread_limit(55 let (chunk_size, thread_limit, num_threads) = git_features::parallel::optimize_chunk_size_and_thread_limit(
42 100,56 100,
@@ -45,15 +59,26 @@ where · 2 unmodified lines
45 None,59 None,
46 );60 );
47 61
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
49 let chunk::Outcome {72 let chunk::Outcome {
50 mut collisions,73 mut collisions,
51 mut errors,74 mut errors,
52 mut bytes_written,75 mut bytes_written,
53 delayed,76 delayed,
54 } = if num_threads == 1 {77 } = if num_threads == 1 {
78 let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
55 chunk::process(entries_with_paths, files, bytes, &mut ctx)?79 chunk::process(entries_with_paths, files, bytes, &mut ctx)?
56 } else {80 } else {
81 let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
57 in_parallel(82 in_parallel(
58 git_features::iter::Chunks {83 git_features::iter::Chunks {
59 inner: entries_with_paths,84 inner: entries_with_paths,
@@ -61,23 +86,8 @@ where · 1 unmodified line
61 },86 },
62 thread_limit,87 thread_limit,
63 {88 {
64 let num_files = &num_files;89 let ctx = ctx.clone();
65 move |_| {90 move |_| (progress::Discard, progress::Discard, ctx.clone())
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 }
81 },91 },
82 |chunk, (files, bytes, ctx)| chunk::process(chunk.into_iter(), files, bytes, ctx),92 |chunk, (files, bytes, ctx)| chunk::process(chunk.into_iter(), files, bytes, ctx),
83 chunk::Reduce {93 chunk::Reduce {
@@ -103,7 +113,7 @@ where · 19 unmodified lines
103 }113 }
104 114
105 Ok(checkout::Outcome {115 Ok(checkout::Outcome {
106 files_updated: ctx.num_files.load(Ordering::Relaxed),116 files_updated: num_files.load(Ordering::Relaxed),
107 collisions,117 collisions,
108 errors,118 errors,
109 bytes_written,119 bytes_written,
@@ -186,9 +196,10 @@ mod chunk { · 76 unmodified lines
186 pub bytes_written: u64,196 pub bytes_written: u64,
187 }197 }
188 198
189 pub struct Context<'a, Find> {199 #[derive(Clone)]
200 pub struct Context<'a, 'paths, Find: Clone> {
190 pub find: Find,201 pub find: Find,
191 pub path_cache: fs::Cache,202 pub path_cache: fs::Cache<'paths>,
192 pub buf: Vec<u8>,203 pub buf: Vec<u8>,
193 pub options: checkout::Options,204 pub options: checkout::Options,
194 /// We keep these shared so that there is the chance for printing numbers that aren't looking like205 /// 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
200 entries_with_paths: impl Iterator<Item = (&'entry mut git_index::Entry, &'entry BStr)>,211 entries_with_paths: impl Iterator<Item = (&'entry mut git_index::Entry, &'entry BStr)>,
201 files: &mut impl Progress,212 files: &mut impl Progress,
202 bytes: &mut impl Progress,213 bytes: &mut impl Progress,
203 ctx: &mut Context<'_, Find>,214 ctx: &mut Context<'_, '_, Find>,
204 ) -> Result<Outcome<'entry>, checkout::Error<E>>215 ) -> Result<Outcome<'entry>, checkout::Error<E>>
205 where216 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,
207 E: std::error::Error + Send + Sync + 'static,218 E: std::error::Error + Send + Sync + 'static,
208 {219 {
209 let mut delayed = Vec::new();220 let mut delayed = Vec::new();
@@ -255,10 +266,10 @@ mod chunk { · 45 unmodified lines
255 buf,266 buf,
256 options,267 options,
257 num_files,268 num_files,
258 }: &mut Context<'_, Find>,269 }: &mut Context<'_, '_, Find>,
259 ) -> Result<usize, checkout::Error<E>>270 ) -> Result<usize, checkout::Error<E>>
260 where271 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,
262 E: std::error::Error + Send + Sync + 'static,273 E: std::error::Error + Send + Sync + 'static,
263 {274 {
264 let res = entry::checkout(275 let res = entry::checkout(
git-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz+3−0
@@ -0,0 +1,3 @@
1version https://git-lfs.github.com/spec/v1
2oid sha256:ae297f2c067e2cc7b0c8eabfff721ff775a7d8266ffef979bde2458de2ab03c9
3size 10944
git-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh+112−0
@@ -0,0 +1,112 @@
1#!/bin/bash
2set -eu -o pipefail
3
4cat <<EOF >user.exclude
5# a custom exclude configured per user
6user-file-anywhere
7/user-file-from-top
8
9user-dir-anywhere/
10/user-dir-from-top
11
12user-subdir/file
13**/user-subdir-anywhere/file
14EOF
15
16mkdir 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
23file-anywhere
24/file-from-top
25
26dir-anywhere/
27/dir-from-top
28
29subdir/file
30**/subdir-anywhere/file
31EOF
32
33 cat <<EOF >.gitignore
34# a sample .gitignore
35top-level-local-file-anywhere
36EOF
37
38 mkdir dir-with-ignore
39 cat <<EOF >dir-with-ignore/.gitignore
40# a sample .gitignore
41sub-level-local-file-anywhere
42sub-level-dir-anywhere/
43!/negated
44/negated-dir/
45!/negated-dir/
46EOF
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
56other-sub-level-local-file-anywhere
57other-sub-level-dir-anywhere/
58EOF
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 || :
65dir-with-ignore/sub-level-dir-anywhere/
66dir-with-ignore/foo/sub-level-dir-anywhere/
67dir-with-ignore/sub-level-dir-anywhere
68user-file-anywhere
69dir/user-file-anywhere
70user-file-from-top
71no-match/user-file-from-top
72user-dir-anywhere
73user-dir-from-top
74no-match/user-dir-from-top
75user-subdir/file
76subdir/user-subdir-anywhere/file
77user-dir-anywhere/hello
78dir/user-dir-anywhere/hello
79file-anywhere
80dir/file-anywhere
81file-from-top
82no-match/file-from-top
83dir-anywhere
84dir/dir-anywhere
85dir-from-top
86no-match/dir-from-top
87subdir/file
88subdir/subdir-anywhere/file
89top-level-local-file-anywhere
90dir/top-level-local-file-anywhere
91no-match/sub-level-local-file-anywhere
92dir-with-ignore/sub-level-local-file-anywhere
93dir-with-ignore/sub-dir/sub-level-local-file-anywhere
94other-dir-with-ignore/other-sub-level-local-file-anywhere
95other-dir-with-ignore/sub-level-local-file-anywhere
96other-dir-with-ignore/sub-dir/other-sub-level-local-file-anywhere
97other-dir-with-ignore/no-match/sub-level-local-file-anywhere
98non-existing/dir-anywhere
99dir-anywhere/hello
100dir/dir-anywhere/hello
101no-match/sub-level-dir-anywhere/hello
102no-match/other-sub-level-dir-anywhere/hello
103dir-with-ignore/sub-level-dir-anywhere/hello
104dir-with-ignore/sub-level-dir-anywhere/
105other-dir-with-ignore/sub-level-dir-anywhere/hello
106other-dir-with-ignore/other-sub-level-dir-anywhere/hello
107other-dir-with-ignore/other-sub-level-dir-anywhere/
108dir-with-ignore/negated
109dir-with-ignore/negated-dir/hello
110EOF
111
112)
git-worktree/tests/worktree-multi-threaded.rs+2−0
@@ -1,2 +1,4 @@
1extern crate core;
2
1mod worktree;3mod worktree;
2use worktree::*;4use worktree::*;
git-worktree/tests/worktree/fs/cache.rs+148−29
@@ -1,39 +1,49 @@
1mod create_directory {1mod create_directory {
2 use std::path::Path;2 use std::path::Path;
3 3
4 use git_index::entry::Mode;
5 use git_worktree::fs;4 use git_worktree::fs;
6 use tempfile::{tempdir, TempDir};5 use tempfile::{tempdir, TempDir};
7 6
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
8 #[test]14 #[test]
9 fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() {15 fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate::Result {
10 let dir = tempdir().unwrap();16 let dir = tempdir()?;
11 let mut cache = fs::Cache::new(17 let mut cache = fs::Cache::new(
12 dir.path().join("non-existing-root"),18 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(),
14 );23 );
15 assert_eq!(cache.num_mkdir_calls(), 0);24 assert_eq!(cache.num_mkdir_calls(), 0);
16 25
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();
18 assert!(!path.parent().unwrap().exists(), "prefix itself is never created");27 assert!(!path.parent().unwrap().exists(), "prefix itself is never created");
19 assert_eq!(cache.num_mkdir_calls(), 0);28 assert_eq!(cache.num_mkdir_calls(), 0);
29 Ok(())
20 }30 }
21 31
22 #[test]32 #[test]
23 fn directory_paths_are_created_in_full() {33 fn directory_paths_are_created_in_full() {
24 let (mut cache, _tmp) = new_cache();34 let (mut cache, _tmp) = new_cache();
25 35
26 for (name, mode) in &[36 for (name, is_dir) in &[
27 ("dir", Mode::DIR),37 ("dir", Some(true)),
28 ("submodule", Mode::COMMIT),38 ("submodule", Some(true)),
29 ("file", Mode::FILE),39 ("file", Some(false)),
30 ("exe", Mode::FILE_EXECUTABLE),40 ("exe", Some(false)),
31 ("link", Mode::SYMLINK),41 ("link", None),
32 ] {42 ] {
33 let path = cache43 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)
35 .unwrap()45 .unwrap()
36 .leading_dir();46 .path();
37 assert!(path.parent().unwrap().is_dir(), "dir exists");47 assert!(path.parent().unwrap().is_dir(), "dir exists");
38 }48 }
39 49
@@ -41,29 +51,33 @@ mod create_directory { · 1 unmodified line
41 }51 }
42 52
43 #[test]53 #[test]
44 fn existing_directories_are_fine() {54 fn existing_directories_are_fine() -> crate::Result {
45 let (mut cache, tmp) = new_cache();55 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"))?;
47 57
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();
49 assert!(path.parent().unwrap().is_dir(), "directory is still present");59 assert!(path.parent().unwrap().is_dir(), "directory is still present");
50 assert!(!path.exists(), "it won't create the file");60 assert!(!path.exists(), "it won't create the file");
51 assert_eq!(cache.num_mkdir_calls(), 1);61 assert_eq!(cache.num_mkdir_calls(), 1);
62 Ok(())
52 }63 }
53 64
54 #[test]65 #[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 {
56 let (mut cache, tmp) = new_cache();67 let (mut cache, tmp) = new_cache();
57 let forbidden = tmp.path().join("forbidden");68 let forbidden = tmp.path().join("forbidden");
58 std::fs::create_dir(&forbidden).unwrap();69 std::fs::create_dir(&forbidden)?;
59 symlink::symlink_dir(&forbidden, tmp.path().join("link-to-dir")).unwrap();70 symlink::symlink_dir(&forbidden, tmp.path().join("link-to-dir"))?;
60 std::fs::write(tmp.path().join("file-in-dir"), &[]).unwrap();71 std::fs::write(tmp.path().join("file-in-dir"), &[])?;
61 72
62 for dirname in &["file-in-dir", "link-to-dir"] {73 for dirname in &["file-in-dir", "link-to-dir"] {
63 cache.unlink_on_collision(false);74 cache.unlink_on_collision(false);
64 let relative_path = format!("{}/file", dirname);75 let relative_path = format!("{}/file", dirname);
65 assert_eq!(76 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(),
67 std::io::ErrorKind::AlreadyExists81 std::io::ErrorKind::AlreadyExists
68 );82 );
69 }83 }
@@ -76,7 +90,7 @@ mod create_directory { · 6 unmodified lines
76 for dirname in &["link-to-dir", "file-in-dir"] {90 for dirname in &["link-to-dir", "file-in-dir"] {
77 cache.unlink_on_collision(true);91 cache.unlink_on_collision(true);
78 let relative_path = format!("{}/file", dirname);92 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();
80 assert!(path.parent().unwrap().is_dir(), "directory was forcefully created");94 assert!(path.parent().unwrap().is_dir(), "directory was forcefully created");
81 assert!(!path.exists());95 assert!(!path.exists());
82 }96 }
@@ -85,26 +99,131 @@ mod create_directory { · 2 unmodified lines
85 4,99 4,
86 "like before, but it unlinks what's there and tries again"100 "like before, but it unlinks what's there and tries again"
87 );101 );
102 Ok(())
88 }103 }
89 104
90 fn new_cache() -> (fs::Cache, TempDir) {105 fn new_cache() -> (fs::Cache<'static>, TempDir) {
91 let dir = tempdir().unwrap();106 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 );
93 (cache, dir)114 (cache, dir)
94 }115 }
95}116}
96 117
97#[allow(unused)]118#[allow(unused)]
98mod ignore_only {119mod ignore_and_attributes {
120 use bstr::{BStr, ByteSlice};
99 use std::path::Path;121 use std::path::Path;
100 122
123 use git_glob::pattern::Case;
101 use git_index::entry::Mode;124 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;
102 use git_worktree::fs;128 use git_worktree::fs;
103 use tempfile::{tempdir, TempDir};129 use tempfile::{tempdir, TempDir};
104 130
105 fn new_cache() -> fs::Cache {131 struct IgnoreExpectations<'a> {
106 let dir = git_testtools::scripted_fixture_repo_read_only("make_ignore_setup.sh").unwrap();132 lines: bstr::Lines<'a>,
107 let cache = fs::Cache::new(dir, todo!()); // TODO: also test initialization133 }
108 cache134
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(())
109 }228 }
110}229}
git-worktree/tests/worktree/fs/mod.rs+1−0
@@ -19,3 +19,4 @@ fn from_probing_cwd() { · 18 unmodified lines
19}19}
20 20
21mod cache;21mod cache;
22mod stack;
git-worktree/tests/worktree/fs/stack/mod.rs+144−0
@@ -0,0 +1,144 @@
1use git_worktree::fs::Stack;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Default, Eq, PartialEq)]
5struct Record {
6 push_dir: usize,
7 dirs: Vec<PathBuf>,
8 push: usize,
9}
10
11impl 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]
29fn 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
257 progress.fail(format!(257 progress.fail(format!(
258 "Error when handling directory {:?}: {}",258 "Error when handling directory {:?}: {}",
259 path_to_move.display(),259 path_to_move.display(),
260 err.to_string()260 err
261 ));261 ));
262 num_errors += 1;262 num_errors += 1;
263 }263 }
gitoxide-core/src/pack/receive.rs+2−1
@@ -4,6 +4,7 @@ use std::{ · 3 unmodified lines
4 sync::{atomic::AtomicBool, Arc},4 sync::{atomic::AtomicBool, Arc},
5};5};
6 6
7pub use git_repository as git;
7use git_repository::{8use git_repository::{
8 hash::ObjectId,9 hash::ObjectId,
9 objs::bstr::{BString, ByteSlice},10 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
304fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> {305fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> {
305 let assure_dir_exists = |path: &BString| {306 let assure_dir_exists = |path: &BString| {
306 assert!(!path.starts_with_str("/"), "no ref start with a /, they are relative");307 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));
308 std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path)309 std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path)
309 };310 };
310 for r in refs {311 for r in refs {
gitoxide-core/src/repository/commit.rs+1−4
@@ -1,10 +1,8 @@
1use std::path::PathBuf;
2
3use anyhow::{Context, Result};1use anyhow::{Context, Result};
4use git_repository as git;2use git_repository as git;
5 3
6pub fn describe(4pub fn describe(
7 repo: impl Into<PathBuf>,5 repo: git::Repository,
8 rev_spec: Option<&str>,6 rev_spec: Option<&str>,
9 mut out: impl std::io::Write,7 mut out: impl std::io::Write,
10 mut err: impl std::io::Write,8 mut err: impl std::io::Write,
@@ -18,7 +16,6 @@ pub fn describe( · 7 unmodified lines
18 long_format,16 long_format,
19 }: describe::Options,17 }: describe::Options,
20) -> Result<()> {18) -> Result<()> {
21 let repo = git::open(repo)?.apply_environment();
22 let commit = match rev_spec {19 let commit = match rev_spec {
23 Some(spec) => repo.rev_parse(spec)?.object()?.try_into_commit()?,20 Some(spec) => repo.rev_parse(spec)?.object()?.try_into_commit()?,
24 None => repo.head_commit()?,21 None => repo.head_commit()?,
gitoxide-core/src/repository/exclude.rs+68−0
@@ -0,0 +1,68 @@
1use anyhow::{bail, Context};
2use std::io;
3
4use crate::OutputFormat;
5use git_repository as git;
6use git_repository::prelude::FindExt;
7
8pub 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
19pub 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 @@
1use std::{io, path::PathBuf};1use std::io;
2 2
3use git_repository as git;3use git_repository as git;
4#[cfg(feature = "serde1")]4#[cfg(feature = "serde1")]
@@ -29,7 +29,7 @@ impl<'a> From<Entry<'a>> for JsonEntry { · 24 unmodified lines
29}29}
30 30
31pub fn entries(31pub fn entries(
32 repository: PathBuf,32 repo: git::Repository,
33 format: OutputFormat,33 format: OutputFormat,
34 #[cfg_attr(not(feature = "serde1"), allow(unused_variables))] out: impl io::Write,34 #[cfg_attr(not(feature = "serde1"), allow(unused_variables))] out: impl io::Write,
35 mut err: impl io::Write,35 mut err: impl io::Write,
@@ -38,7 +38,6 @@ pub fn entries( · 2 unmodified lines
38 writeln!(err, "Defaulting to JSON as human format isn't implemented").ok();38 writeln!(err, "Defaulting to JSON as human format isn't implemented").ok();
39 }39 }
40 40
41 let repo = git::open(repository)?.apply_environment();
42 let mut mailmap = git::mailmap::Snapshot::default();41 let mut mailmap = git::mailmap::Snapshot::default();
43 if let Err(e) = repo.load_mailmap_into(&mut mailmap) {42 if let Err(e) = repo.load_mailmap_into(&mut mailmap) {
44 writeln!(err, "Error while loading mailmap, the first error is: {}", e).ok();43 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
16pub mod odb;16pub mod odb;
17 17
18pub mod mailmap;18pub mod mailmap;
19
20pub mod exclude;
gitoxide-core/src/repository/odb.rs+3−6
@@ -1,4 +1,4 @@
1use std::{io, path::PathBuf};1use std::io;
2 2
3use anyhow::bail;3use anyhow::bail;
4use git_repository as git;4use git_repository as git;
@@ -22,7 +22,7 @@ mod info { · 17 unmodified lines
22 22
23#[cfg_attr(not(feature = "serde1"), allow(unused_variables))]23#[cfg_attr(not(feature = "serde1"), allow(unused_variables))]
24pub fn info(24pub fn info(
25 repository: PathBuf,25 repo: git::Repository,
26 format: OutputFormat,26 format: OutputFormat,
27 out: impl io::Write,27 out: impl io::Write,
28 mut err: impl io::Write,28 mut err: impl io::Write,
@@ -31,7 +31,6 @@ pub fn info( · 2 unmodified lines
31 writeln!(err, "Only JSON is implemented - using that instead")?;31 writeln!(err, "Only JSON is implemented - using that instead")?;
32 }32 }
33 33
34 let repo = git::open(repository)?.apply_environment();
35 let store = repo.objects.store_ref();34 let store = repo.objects.store_ref();
36 let stats = info::Statistics {35 let stats = info::Statistics {
37 path: store.path().into(),36 path: store.path().into(),
@@ -49,13 +48,11 @@ pub fn info( · 11 unmodified lines
49 Ok(())48 Ok(())
50}49}
51 50
52pub fn entries(repository: PathBuf, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> {51pub fn entries(repo: git::Repository, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> {
53 if format != OutputFormat::Human {52 if format != OutputFormat::Human {
54 bail!("Only human output format is supported at the moment");53 bail!("Only human output format is supported at the moment");
55 }54 }
56 55
57 let repo = git::open(repository)?.apply_environment();
58
59 for object in repo.objects.iter()? {56 for object in repo.objects.iter()? {
60 let object = object?;57 let object = object?;
61 writeln!(out, "{}", object)?;58 writeln!(out, "{}", object)?;
gitoxide-core/src/repository/tree.rs+3−5
@@ -1,4 +1,4 @@
1use std::{borrow::Cow, io, path::PathBuf};1use std::{borrow::Cow, io};
2 2
3use anyhow::bail;3use anyhow::bail;
4use git_repository as git;4use git_repository as git;
@@ -117,7 +117,7 @@ mod entries { · 112 unmodified lines
117 117
118#[cfg_attr(not(feature = "serde1"), allow(unused_variables))]118#[cfg_attr(not(feature = "serde1"), allow(unused_variables))]
119pub fn info(119pub fn info(
120 repository: PathBuf,120 repo: git::Repository,
121 treeish: Option<&str>,121 treeish: Option<&str>,
122 extended: bool,122 extended: bool,
123 format: OutputFormat,123 format: OutputFormat,
@@ -128,7 +128,6 @@ pub fn info( · 4 unmodified lines
128 writeln!(err, "Only JSON is implemented - using that instead")?;128 writeln!(err, "Only JSON is implemented - using that instead")?;
129 }129 }
130 130
131 let repo = git::open(repository)?.apply_environment();
132 let tree = treeish_to_tree(treeish, &repo)?;131 let tree = treeish_to_tree(treeish, &repo)?;
133 132
134 let mut delegate = entries::Traverse::new(extended.then(|| &repo), None);133 let mut delegate = entries::Traverse::new(extended.then(|| &repo), None);
@@ -144,7 +143,7 @@ pub fn info( · 9 unmodified lines
144}143}
145 144
146pub fn entries(145pub fn entries(
147 repository: PathBuf,146 repo: git::Repository,
148 treeish: Option<&str>,147 treeish: Option<&str>,
149 recursive: bool,148 recursive: bool,
150 extended: bool,149 extended: bool,
@@ -155,7 +154,6 @@ pub fn entries( · 4 unmodified lines
155 bail!("Only human output format is supported at the moment");154 bail!("Only human output format is supported at the moment");
156 }155 }
157 156
158 let repo = git::open(repository)?.apply_environment();
159 let tree = treeish_to_tree(treeish, &repo)?;157 let tree = treeish_to_tree(treeish, &repo)?;
160 158
161 if recursive {159 if recursive {
gitoxide-core/src/repository/verify.rs+2−3
@@ -1,4 +1,4 @@
1use std::{path::PathBuf, sync::atomic::AtomicBool};1use std::sync::atomic::AtomicBool;
2 2
3use git_repository as git;3use git_repository as git;
4use git_repository::Progress;4use git_repository::Progress;
@@ -20,7 +20,7 @@ pub struct Context { · 15 unmodified lines
20pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;20pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
21 21
22pub fn integrity(22pub fn integrity(
23 repo: PathBuf,23 repo: git::Repository,
24 mut out: impl std::io::Write,24 mut out: impl std::io::Write,
25 progress: impl Progress,25 progress: impl Progress,
26 should_interrupt: &AtomicBool,26 should_interrupt: &AtomicBool,
@@ -31,7 +31,6 @@ pub fn integrity( · 4 unmodified lines
31 algorithm,31 algorithm,
32 }: Context,32 }: Context,
33) -> anyhow::Result<()> {33) -> anyhow::Result<()> {
34 let repo = git_repository::open(repo)?;
35 #[cfg_attr(not(feature = "serde1"), allow(unused))]34 #[cfg_attr(not(feature = "serde1"), allow(unused))]
36 let mut outcome = repo.objects.store_ref().verify_integrity(35 let mut outcome = repo.objects.store_ref().verify_integrity(
37 progress,36 progress,
src/plumbing/main.rs+177−130
@@ -9,6 +9,7 @@ use std::{ · 8 unmodified lines
9 9
10use anyhow::Result;10use anyhow::Result;
11use clap::Parser;11use clap::Parser;
12use git_repository::bstr::io::BufReadExt;
12use gitoxide_core as core;13use gitoxide_core as core;
13use gitoxide_core::pack::verify;14use gitoxide_core::pack::verify;
14 15
@@ -155,133 +156,181 @@ pub fn main() -> Result<()> { · 140 unmodified lines
155 },156 },
156 ),157 ),
157 },158 },
158 Subcommands::Repository(repo::Platform { repository, cmd }) => match cmd {159 Subcommands::Repository(repo::Platform { repository, cmd }) => {
159 repo::Subcommands::Commit { cmd } => match cmd {160 use git_repository as git;
160 repo::commit::Subcommands::Describe {161 let repository = git::ThreadSafeRepository::discover(repository)?;
161 annotated_tags,162 match cmd {
162 all_refs,163 repo::Subcommands::Commit { cmd } => match cmd {
163 first_parent,164 repo::commit::Subcommands::Describe {
164 always,165 annotated_tags,
165 long,166 all_refs,
166 statistics,167 first_parent,
167 max_candidates,168 always,
168 rev_spec,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 },
169 } => prepare_and_run(311 } => prepare_and_run(
170 "repository-commit-describe",312 "repository-verify",
171 verbose,313 verbose,
172 progress,314 progress,
173 progress_keep_open,315 progress_keep_open,
174 None,316 core::repository::verify::PROGRESS_RANGE,
175 move |_progress, out, err| {317 move |progress, out, _err| {
176 core::repository::commit::describe(318 core::repository::verify::integrity(
177 repository,319 repository.into(),
178 rev_spec.as_deref(),
179 out,320 out,
180 err,321 progress,
181 core::repository::commit::describe::Options {322 &should_interrupt,
182 all_tags: !annotated_tags,323 core::repository::verify::Context {
183 all_refs,324 output_statistics: statistics.then(|| format),
184 long_format: long,325 algorithm,
185 first_parent,326 verify_mode: verify_mode(decode, re_encode),
186 statistics,327 thread_limit,
187 max_candidates,
188 always,
189 },328 },
190 )329 )
191 },330 },
192 ),331 ),
193 },332 }
194 repo::Subcommands::Mailmap { cmd } => match cmd {333 }
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 },
285 Subcommands::Pack(subcommands) => match subcommands {334 Subcommands::Pack(subcommands) => match subcommands {
286 pack::Subcommands::Create {335 pack::Subcommands::Create {
287 repository,336 repository,
@@ -303,16 +352,7 @@ pub fn main() -> Result<()> { · 15 unmodified lines
303 progress_keep_open,352 progress_keep_open,
304 core::pack::create::PROGRESS_RANGE,353 core::pack::create::PROGRESS_RANGE,
305 move |progress, out, _err| {354 move |progress, out, _err| {
306 let input = if has_tips {355 let input = if has_tips { None } else { stdin_or_bail()?.into() };
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 };
316 let repository = repository.unwrap_or_else(|| PathBuf::from("."));356 let repository = repository.unwrap_or_else(|| PathBuf::from("."));
317 let context = core::pack::create::Context {357 let context = core::pack::create::Context {
318 thread_limit,358 thread_limit,
@@ -379,7 +419,7 @@ pub fn main() -> Result<()> { · 60 unmodified lines
379 directory,419 directory,
380 refs_directory,420 refs_directory,
381 refs.into_iter().map(|r| r.into()).collect(),421 refs.into_iter().map(|r| r.into()).collect(),
382 git_features::progress::DoOrDiscard::from(progress),422 progress,
383 core::pack::receive::Context {423 core::pack::receive::Context {
384 thread_limit,424 thread_limit,
385 format,425 format,
@@ -568,7 +608,7 @@ pub fn main() -> Result<()> { · 182 unmodified lines
568 core::remote::refs::list(608 core::remote::refs::list(
569 protocol,609 protocol,
570 &url,610 &url,
571 git_features::progress::DoOrDiscard::from(progress),611 progress,
572 core::remote::refs::Context {612 core::remote::refs::Context {
573 thread_limit,613 thread_limit,
574 format,614 format,
@@ -603,6 +643,13 @@ pub fn main() -> Result<()> { · 28 unmodified lines
603 Ok(())643 Ok(())
604}644}
605 645
646fn 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
606fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode {653fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode {
607 match (decode, re_encode) {654 match (decode, re_encode) {
608 (true, false) => verify::Mode::HashCrc32Decode,655 (true, false) => verify::Mode::HashCrc32Decode,
src/plumbing/options.rs+30−0
@@ -370,6 +370,36 @@ pub mod repo { · 369 unmodified lines
370 #[clap(subcommand)]370 #[clap(subcommand)]
371 cmd: mailmap::Subcommands,371 cmd: mailmap::Subcommands,
372 },372 },
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 }
373 }403 }
374 404
375 pub mod mailmap {405 pub mod mailmap {
src/porcelain/main.rs+3−3
@@ -53,7 +53,7 @@ pub fn main() -> Result<()> { · 52 unmodified lines
53 hours::estimate(53 hours::estimate(
54 &working_dir,54 &working_dir,
55 &refname,55 &refname,
56 git_features::progress::DoOrDiscard::from(progress),56 progress,
57 hours::Context {57 hours::Context {
58 show_pii,58 show_pii,
59 omit_unify_identities,59 omit_unify_identities,
@@ -75,7 +75,7 @@ pub fn main() -> Result<()> { · 15 unmodified lines
75 organize::discover(75 organize::discover(
76 root.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),76 root.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),
77 out,77 out,
78 git_features::progress::DoOrDiscard::from(progress),78 progress,
79 debug,79 debug,
80 )80 )
81 },81 },
@@ -102,7 +102,7 @@ pub fn main() -> Result<()> { · 20 unmodified lines
102 },102 },
103 repository_source.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),103 repository_source.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),
104 destination_directory.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),104 destination_directory.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()),
105 git_features::progress::DoOrDiscard::from(progress),105 progress,
106 )106 )
107 },107 },
108 )108 )
src/shared.rs+45−2
@@ -52,19 +52,62 @@ pub mod pretty { · 51 unmodified lines
52 52
53 use crate::shared::ProgressRange;53 use crate::shared::ProgressRange;
54 54
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"))]
55 pub fn prepare_and_run<T: Send + 'static>(99 pub fn prepare_and_run<T: Send + 'static>(
56 name: &str,100 name: &str,
57 verbose: bool,101 verbose: bool,
58 progress: bool,102 progress: bool,
59 #[cfg_attr(not(feature = "prodash-render-tui"), allow(unused_variables))] progress_keep_open: bool,103 #[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>>,
61 run: impl FnOnce(105 run: impl FnOnce(
62 progress::DoOrDiscard<prodash::tree::Item>,106 progress::DoOrDiscard<prodash::tree::Item>,
63 &mut dyn std::io::Write,107 &mut dyn std::io::Write,
64 &mut dyn std::io::Write,108 &mut dyn std::io::Write,
65 ) -> Result<T>109 ) -> Result<T>
66 + Send110 + Send
67 + std::panic::UnwindSafe
68 + 'static,111 + 'static,
69 ) -> Result<T> {112 ) -> Result<T> {
70 crate::shared::init_env_logger();113 crate::shared::init_env_logger();
tests/tools/src/lib.rs+1−1
@@ -13,7 +13,7 @@ use once_cell::sync::Lazy; · 12 unmodified lines
13use parking_lot::Mutex;13use parking_lot::Mutex;
14pub use tempfile;14pub use tempfile;
15 15
16type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;16pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
17 17
18static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new()));18static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new()));
19 19