Unified · Split
| @@ -503,6 +503,15 @@ version = "0.1.1" · 502 unmodified lines | |||
| 503 | source = "registry+https://github.com/rust-lang/crates.io-index" | 503 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 504 | checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35" | 504 | checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35" |
| 505 | 505 | ||
| 506 | [[package]] | ||
| 507 | name = "compact_str" | ||
| 508 | version = "0.3.2" | ||
| 509 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 510 | checksum = "d0e60dedcb8b23cedf6f23ee35ecf5c7889961e99f26f79ab196aaf4a8b48608" | ||
| 511 | dependencies = [ | ||
| 512 | "serde", | ||
| 513 | ] | ||
| 514 | |||
| 506 | [[package]] | 515 | [[package]] |
| 507 | name = "concurrent-queue" | 516 | name = "concurrent-queue" |
| 508 | version = "1.2.2" | 517 | version = "1.2.2" |
| @@ -1076,8 +1085,10 @@ name = "git-attributes" · 567 unmodified lines | |||
| 1076 | version = "0.1.0" | 1085 | version = "0.1.0" |
| 1077 | dependencies = [ | 1086 | dependencies = [ |
| 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]] | ||
| 1374 | name = "git-path" | ||
| 1375 | version = "0.1.0" | ||
| 1376 | dependencies = [ | ||
| 1377 | "bstr", | ||
| 1378 | ] | ||
| 1379 | |||
| 1359 | [[package]] | 1380 | [[package]] |
| 1360 | name = "git-pathspec" | 1381 | name = "git-pathspec" |
| 1361 | version = "0.0.0" | 1382 | version = "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 | |||
| 1571 | dependencies = [ | 1597 | dependencies = [ |
| 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 | |||
| 1592 | dependencies = [ | 1619 | dependencies = [ |
| 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", |
| @@ -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", |
| @@ -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 them | 49 | * [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 past | 55 | * [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) |
| @@ -221,6 +221,13 @@ Check out the [performance discussion][git-traverse-performance] as well. · 220 unmodified lines | |||
| 221 | * [x] parsing | 221 | * [x] parsing |
| 222 | * [x] lookup and mapping of author names | 222 | * [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-pathspec | 231 | ### git-pathspec |
| 225 | * [ ] parse | 232 | * [ ] parse |
| 226 | * [ ] check for match | 233 | * [ ] 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 quarantine | 437 | * [ ] 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 | * [ ] worktrees | 439 | * **worktrees** |
| 440 | * [ ] open a repository with worktrees | ||
| 433 | * [ ] remotes with push and pull | 441 | * [ ] remotes with push and pull |
| 434 | * [x] mailmap | 442 | * [x] mailmap |
| 435 | * [x] object replacements (`git replace`) | 443 | * [x] object replacements (`git replace`) |
| @@ -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) |
| @@ -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`. |
| 16 | serde1 = ["serde", "bstr/serde1", "git-glob/serde1"] | 16 | serde1 = ["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.html | 18 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
| 19 | 19 | ||
| 20 | [dependencies] | 20 | [dependencies] |
| 21 | git-features = { version = "^0.20.0", path = "../git-features" } | 21 | git-features = { version = "^0.20.0", path = "../git-features" } |
| 22 | git-path = { version = "^0.1.0", path = "../git-path" } | ||
| 22 | git-quote = { version = "^0.2.0", path = "../git-quote" } | 23 | git-quote = { version = "^0.2.0", path = "../git-quote" } |
| 23 | git-glob = { version = "^0.2.0", path = "../git-glob" } | 24 | git-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 | |||
| 26 | unicode-bom = "1.1.4" | 27 | unicode-bom = "1.1.4" |
| 27 | quick-error = "2.0.0" | 28 | quick-error = "2.0.0" |
| 28 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} | 29 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} |
| 30 | compact_str = "0.3.2" | ||
| 29 | 31 | ||
| 30 | [dev-dependencies] | 32 | [dev-dependencies] |
| 31 | git-testtools = { path = "../tests/tools"} | 33 | git-testtools = { path = "../tests/tools"} |
| @@ -2,10 +2,17 @@ · 1 unmodified line | |||
| 2 | #![deny(rust_2018_idioms)] | 2 | #![deny(rust_2018_idioms)] |
| 3 | 3 | ||
| 4 | use bstr::{BStr, BString}; | 4 | use bstr::{BStr, BString}; |
| 5 | use compact_str::CompactStr; | ||
| 6 | use std::path::PathBuf; | ||
| 5 | 7 | ||
| 8 | pub use git_glob as glob; | ||
| 9 | |||
| 10 | /// The state an attribute can be in, referencing the value. | ||
| 11 | /// | ||
| 12 | /// Note that this doesn't contain the name. | ||
| 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))] |
| 8 | pub enum State<'a> { | 15 | pub 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))] | ||
| 33 | pub enum State { | ||
| 34 | /// The attribute is listed, or has the special value 'true' | ||
| 35 | Set, | ||
| 36 | /// The attribute has the special value 'false', or was prefixed with a `-` sign. | ||
| 37 | Unset, | ||
| 38 | /// The attribute is set to the given value, which followed the `=` sign. | ||
| 39 | /// Note that values can be empty. | ||
| 40 | Value(compact_str::CompactStr), | ||
| 41 | /// The attribute isn't mentioned with a given path or is explicitly set to `Unspecified` using the `!` sign. | ||
| 42 | Unspecified, | ||
| 43 | } | ||
| 44 | |||
| 45 | /// Name an attribute and describe it's assigned state. | ||
| 24 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | 46 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] |
| 25 | pub struct MatchGroup<T: match_group::Tag> { | 47 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| 48 | pub struct Assignment { | ||
| 49 | /// The name of the attribute. | ||
| 50 | pub name: CompactStr, | ||
| 51 | /// The state of the attribute. | ||
| 52 | pub state: State, | ||
| 53 | } | ||
| 54 | |||
| 55 | /// A grouping of lists of patterns while possibly keeping associated to their base path. | ||
| 56 | /// | ||
| 57 | /// Pattern lists with base path are queryable relative to that base, otherwise they are relative to the repository root. | ||
| 58 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] | ||
| 59 | pub struct MatchGroup<T: match_group::Pattern = Attributes> { | ||
| 26 | /// A list of pattern lists, each representing a patterns from a file or specified by hand, in the order they were | 60 | /// 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 | /// |
| 35 | pub 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)] | ||
| 72 | pub struct PatternList<T: match_group::Pattern> { | ||
| 73 | /// Patterns and their associated data in the order they were loaded in or specified, | ||
| 74 | /// the line number in its source file or its sequence number (_`(pattern, value, line_number)`_). | ||
| 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 patterns | 79 | /// 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 | |||
| 46 | mod match_group { | ||
| 47 | use crate::{MatchGroup, PatternList}; | ||
| 48 | use std::ffi::OsString; | ||
| 49 | use std::path::PathBuf; | ||
| 50 | |||
| 51 | /// A marker trait to identify the type of a description. | ||
| 52 | pub trait Tag: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd { | ||
| 53 | /// The value associated with a pattern. | ||
| 54 | type Value: PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Clone; | ||
| 55 | } | ||
| 56 | |||
| 57 | /// Identify ignore patterns. | ||
| 58 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 59 | pub struct Ignore; | ||
| 60 | impl Tag for Ignore { | ||
| 61 | type Value = (); | ||
| 62 | } | ||
| 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 separators | 89 | pub 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: patterns | 92 | 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 | } |
| 98 | pub use match_group::{Attributes, Ignore, Tag}; | ||
| 99 | 94 | ||
| 100 | pub type Files = MatchGroup<Attributes>; | 95 | mod match_group; |
| 101 | pub type IgnoreFiles = MatchGroup<Ignore>; | 96 | pub use match_group::{Attributes, Ignore, Match, Pattern}; |
| 102 | 97 | ||
| 103 | pub mod parse; | 98 | pub mod parse; |
| 104 | 99 | ||
| @@ -0,0 +1,354 @@ | |||
| 1 | use crate::{Assignment, MatchGroup, PatternList, PatternMapping, State, StateRef}; | ||
| 2 | use bstr::{BStr, BString, ByteSlice, ByteVec}; | ||
| 3 | use std::ffi::OsString; | ||
| 4 | use std::io::Read; | ||
| 5 | use std::path::{Path, PathBuf}; | ||
| 6 | |||
| 7 | impl<'a> From<StateRef<'a>> for State { | ||
| 8 | fn from(s: StateRef<'a>) -> Self { | ||
| 9 | match s { | ||
| 10 | StateRef::Value(v) => State::Value(v.to_str().expect("no illformed unicode").into()), | ||
| 11 | StateRef::Set => State::Set, | ||
| 12 | StateRef::Unset => State::Unset, | ||
| 13 | StateRef::Unspecified => State::Unspecified, | ||
| 14 | } | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 18 | fn attrs_to_assignments<'a>( | ||
| 19 | attrs: impl Iterator<Item = Result<(&'a BStr, StateRef<'a>), crate::parse::Error>>, | ||
| 20 | ) -> Result<Vec<Assignment>, crate::parse::Error> { | ||
| 21 | attrs | ||
| 22 | .map(|res| { | ||
| 23 | res.map(|(name, state)| Assignment { | ||
| 24 | name: name.to_str().expect("no illformed unicode").into(), | ||
| 25 | state: state.into(), | ||
| 26 | }) | ||
| 27 | }) | ||
| 28 | .collect() | ||
| 29 | } | ||
| 30 | |||
| 31 | /// A marker trait to identify the type of a description. | ||
| 32 | pub trait Pattern: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Default { | ||
| 33 | /// The value associated with a pattern. | ||
| 34 | type Value: PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Clone; | ||
| 35 | |||
| 36 | /// Parse all patterns in `bytes` line by line, ignoring lines with errors, and collect them. | ||
| 37 | fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>>; | ||
| 38 | |||
| 39 | fn use_pattern(pattern: &git_glob::Pattern) -> bool; | ||
| 40 | } | ||
| 41 | |||
| 42 | /// Identify ignore patterns. | ||
| 43 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] | ||
| 44 | pub struct Ignore; | ||
| 45 | |||
| 46 | impl Pattern for Ignore { | ||
| 47 | type Value = (); | ||
| 48 | |||
| 49 | fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>> { | ||
| 50 | crate::parse::ignore(bytes) | ||
| 51 | .map(|(pattern, line_number)| PatternMapping { | ||
| 52 | pattern, | ||
| 53 | value: (), | ||
| 54 | sequence_number: line_number, | ||
| 55 | }) | ||
| 56 | .collect() | ||
| 57 | } | ||
| 58 | |||
| 59 | fn use_pattern(_pattern: &git_glob::Pattern) -> bool { | ||
| 60 | true | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | /// A value of an attribute pattern, which is either a macro definition or | ||
| 65 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 66 | pub enum Value { | ||
| 67 | MacroAttributes(Vec<Assignment>), | ||
| 68 | Attributes(Vec<Assignment>), | ||
| 69 | } | ||
| 70 | |||
| 71 | /// Identify patterns with attributes. | ||
| 72 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] | ||
| 73 | pub struct Attributes; | ||
| 74 | |||
| 75 | impl Pattern for Attributes { | ||
| 76 | type Value = Value; | ||
| 77 | |||
| 78 | fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>> { | ||
| 79 | crate::parse(bytes) | ||
| 80 | .filter_map(Result::ok) | ||
| 81 | .filter_map(|(pattern_kind, attrs, line_number)| { | ||
| 82 | let (pattern, value) = match pattern_kind { | ||
| 83 | crate::parse::Kind::Macro(macro_name) => ( | ||
| 84 | git_glob::Pattern { | ||
| 85 | text: macro_name, | ||
| 86 | mode: git_glob::pattern::Mode::all(), | ||
| 87 | first_wildcard_pos: None, | ||
| 88 | }, | ||
| 89 | Value::MacroAttributes(attrs_to_assignments(attrs).ok()?), | ||
| 90 | ), | ||
| 91 | crate::parse::Kind::Pattern(p) => ( | ||
| 92 | (!p.is_negative()).then(|| p)?, | ||
| 93 | Value::Attributes(attrs_to_assignments(attrs).ok()?), | ||
| 94 | ), | ||
| 95 | }; | ||
| 96 | PatternMapping { | ||
| 97 | pattern, | ||
| 98 | value, | ||
| 99 | sequence_number: line_number, | ||
| 100 | } | ||
| 101 | .into() | ||
| 102 | }) | ||
| 103 | .collect() | ||
| 104 | } | ||
| 105 | |||
| 106 | fn use_pattern(pattern: &git_glob::Pattern) -> bool { | ||
| 107 | pattern.mode != git_glob::pattern::Mode::all() | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | /// Describes a matching value within a [`MatchGroup`]. | ||
| 112 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | ||
| 113 | pub struct Match<'a, T> { | ||
| 114 | pub pattern: &'a git_glob::Pattern, | ||
| 115 | /// The value associated with the pattern. | ||
| 116 | pub value: &'a T, | ||
| 117 | /// The path to the source from which the pattern was loaded, or `None` if it was specified by other means. | ||
| 118 | pub source: Option<&'a Path>, | ||
| 119 | /// The line at which the pattern was found in its `source` file, or the occurrence in which it was provided. | ||
| 120 | pub sequence_number: usize, | ||
| 121 | } | ||
| 122 | |||
| 123 | impl<T> MatchGroup<T> | ||
| 124 | where | ||
| 125 | T: Pattern, | ||
| 126 | { | ||
| 127 | /// Match `relative_path`, a path relative to the repository containing all patterns, and return the first match if available. | ||
| 128 | // TODO: better docs | ||
| 129 | pub fn pattern_matching_relative_path<'a>( | ||
| 130 | &self, | ||
| 131 | relative_path: impl Into<&'a BStr>, | ||
| 132 | is_dir: Option<bool>, | ||
| 133 | case: git_glob::pattern::Case, | ||
| 134 | ) -> Option<Match<'_, T::Value>> { | ||
| 135 | let relative_path = relative_path.into(); | ||
| 136 | let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); | ||
| 137 | self.patterns | ||
| 138 | .iter() | ||
| 139 | .rev() | ||
| 140 | .find_map(|pl| pl.pattern_matching_relative_path(relative_path, basename_pos, is_dir, case)) | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | impl MatchGroup<Ignore> { | ||
| 145 | /// Given `git_dir`, a `.git` repository, load ignore patterns from `info/exclude` and from `excludes_file` if it | ||
| 146 | /// is provided. | ||
| 147 | /// Note that it's not considered an error if the provided `excludes_file` does not exist. | ||
| 148 | pub fn from_git_dir( | ||
| 149 | git_dir: impl AsRef<Path>, | ||
| 150 | excludes_file: Option<PathBuf>, | ||
| 151 | buf: &mut Vec<u8>, | ||
| 152 | ) -> std::io::Result<Self> { | ||
| 153 | let mut group = Self::default(); | ||
| 154 | |||
| 155 | let follow_symlinks = true; | ||
| 156 | // order matters! More important ones first. | ||
| 157 | group.patterns.extend( | ||
| 158 | excludes_file | ||
| 159 | .map(|file| PatternList::<Ignore>::from_file(file, None, follow_symlinks, buf)) | ||
| 160 | .transpose()? | ||
| 161 | .flatten(), | ||
| 162 | ); | ||
| 163 | group.patterns.extend(PatternList::<Ignore>::from_file( | ||
| 164 | git_dir.as_ref().join("info").join("exclude"), | ||
| 165 | None, | ||
| 166 | follow_symlinks, | ||
| 167 | buf, | ||
| 168 | )?); | ||
| 169 | Ok(group) | ||
| 170 | } | ||
| 171 | |||
| 172 | /// See [PatternList::<Ignore>::from_overrides()] for details. | ||
| 173 | pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self { | ||
| 174 | MatchGroup { | ||
| 175 | patterns: vec![PatternList::<Ignore>::from_overrides(patterns)], | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | /// Add the given file at `source` if it exists, otherwise do nothing. If a `root` is provided, it's not considered a global file anymore. | ||
| 180 | /// Returns true if the file was added, or false if it didn't exist. | ||
| 181 | pub fn add_patterns_file( | ||
| 182 | &mut self, | ||
| 183 | source: impl Into<PathBuf>, | ||
| 184 | follow_symlinks: bool, | ||
| 185 | root: Option<&Path>, | ||
| 186 | buf: &mut Vec<u8>, | ||
| 187 | ) -> std::io::Result<bool> { | ||
| 188 | let previous_len = self.patterns.len(); | ||
| 189 | self.patterns.extend(PatternList::<Ignore>::from_file( | ||
| 190 | source.into(), | ||
| 191 | root, | ||
| 192 | follow_symlinks, | ||
| 193 | buf, | ||
| 194 | )?); | ||
| 195 | Ok(self.patterns.len() != previous_len) | ||
| 196 | } | ||
| 197 | |||
| 198 | pub fn add_patterns_buffer(&mut self, bytes: &[u8], source: impl Into<PathBuf>, root: Option<&Path>) { | ||
| 199 | self.patterns | ||
| 200 | .push(PatternList::<Ignore>::from_bytes(bytes, source.into(), root)); | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec<u8>) -> std::io::Result<bool> { | ||
| 205 | buf.clear(); | ||
| 206 | let file = if follow_symlinks { | ||
| 207 | std::fs::File::open(path) | ||
| 208 | } else { | ||
| 209 | git_features::fs::open_options_no_follow().read(true).open(path) | ||
| 210 | }; | ||
| 211 | Ok(match file { | ||
| 212 | Ok(mut file) => { | ||
| 213 | file.read_to_end(buf)?; | ||
| 214 | true | ||
| 215 | } | ||
| 216 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, | ||
| 217 | Err(err) => return Err(err), | ||
| 218 | }) | ||
| 219 | } | ||
| 220 | |||
| 221 | impl<T> PatternList<T> | ||
| 222 | where | ||
| 223 | T: Pattern, | ||
| 224 | { | ||
| 225 | /// `source` is the location of the `bytes` which represent a list of patterns line by line. | ||
| 226 | pub fn from_bytes(bytes: &[u8], source: impl Into<PathBuf>, root: Option<&Path>) -> Self { | ||
| 227 | let source = source.into(); | ||
| 228 | let patterns = T::bytes_to_patterns(bytes); | ||
| 229 | |||
| 230 | let base = root | ||
| 231 | .and_then(|root| source.parent().expect("file").strip_prefix(root).ok()) | ||
| 232 | .and_then(|base| { | ||
| 233 | (!base.as_os_str().is_empty()).then(|| { | ||
| 234 | let mut base: BString = | ||
| 235 | git_path::to_unix_separators_on_windows(git_path::into_bstr(base)).into_owned(); | ||
| 236 | |||
| 237 | base.push_byte(b'/'); | ||
| 238 | base | ||
| 239 | }) | ||
| 240 | }); | ||
| 241 | PatternList { | ||
| 242 | patterns, | ||
| 243 | source: Some(source), | ||
| 244 | base, | ||
| 245 | } | ||
| 246 | } | ||
| 247 | pub fn from_file( | ||
| 248 | source: impl Into<PathBuf>, | ||
| 249 | root: Option<&Path>, | ||
| 250 | follow_symlinks: bool, | ||
| 251 | buf: &mut Vec<u8>, | ||
| 252 | ) -> std::io::Result<Option<Self>> { | ||
| 253 | let source = source.into(); | ||
| 254 | Ok(read_in_full_ignore_missing(&source, follow_symlinks, buf)?.then(|| Self::from_bytes(buf, source, root))) | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | impl<T> PatternList<T> | ||
| 259 | where | ||
| 260 | T: Pattern, | ||
| 261 | { | ||
| 262 | pub fn pattern_matching_relative_path( | ||
| 263 | &self, | ||
| 264 | relative_path: &BStr, | ||
| 265 | basename_pos: Option<usize>, | ||
| 266 | is_dir: Option<bool>, | ||
| 267 | case: git_glob::pattern::Case, | ||
| 268 | ) -> Option<Match<'_, T::Value>> { | ||
| 269 | let (relative_path, basename_start_pos) = | ||
| 270 | self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?; | ||
| 271 | self.patterns | ||
| 272 | .iter() | ||
| 273 | .rev() | ||
| 274 | .filter(|pm| T::use_pattern(&pm.pattern)) | ||
| 275 | .find_map( | ||
| 276 | |PatternMapping { | ||
| 277 | pattern, | ||
| 278 | value, | ||
| 279 | sequence_number, | ||
| 280 | }| { | ||
| 281 | pattern | ||
| 282 | .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | ||
| 283 | .then(|| Match { | ||
| 284 | pattern, | ||
| 285 | value, | ||
| 286 | source: self.source.as_deref(), | ||
| 287 | sequence_number: *sequence_number, | ||
| 288 | }) | ||
| 289 | }, | ||
| 290 | ) | ||
| 291 | } | ||
| 292 | |||
| 293 | pub fn pattern_idx_matching_relative_path( | ||
| 294 | &self, | ||
| 295 | relative_path: &BStr, | ||
| 296 | basename_pos: Option<usize>, | ||
| 297 | is_dir: Option<bool>, | ||
| 298 | case: git_glob::pattern::Case, | ||
| 299 | ) -> Option<usize> { | ||
| 300 | let (relative_path, basename_start_pos) = | ||
| 301 | self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?; | ||
| 302 | self.patterns | ||
| 303 | .iter() | ||
| 304 | .enumerate() | ||
| 305 | .rev() | ||
| 306 | .filter(|(_, pm)| T::use_pattern(&pm.pattern)) | ||
| 307 | .find_map(|(idx, pm)| { | ||
| 308 | pm.pattern | ||
| 309 | .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | ||
| 310 | .then(|| idx) | ||
| 311 | }) | ||
| 312 | } | ||
| 313 | |||
| 314 | fn strip_base_handle_recompute_basename_pos<'a>( | ||
| 315 | &self, | ||
| 316 | relative_path: &'a BStr, | ||
| 317 | basename_pos: Option<usize>, | ||
| 318 | ) -> Option<(&'a BStr, Option<usize>)> { | ||
| 319 | match self.base.as_deref() { | ||
| 320 | Some(base) => ( | ||
| 321 | relative_path.strip_prefix(base.as_slice())?.as_bstr(), | ||
| 322 | basename_pos.and_then(|pos| { | ||
| 323 | let pos = pos - base.len(); | ||
| 324 | (pos != 0).then(|| pos) | ||
| 325 | }), | ||
| 326 | ), | ||
| 327 | None => (relative_path, basename_pos), | ||
| 328 | } | ||
| 329 | .into() | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | impl PatternList<Ignore> { | ||
| 334 | /// Parse a list of patterns, using slashes as path separators | ||
| 335 | pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self { | ||
| 336 | PatternList { | ||
| 337 | patterns: patterns | ||
| 338 | .into_iter() | ||
| 339 | .map(Into::into) | ||
| 340 | .enumerate() | ||
| 341 | .filter_map(|(seq_id, pattern)| { | ||
| 342 | let pattern = git_path::try_into_bstr(PathBuf::from(pattern)).ok()?; | ||
| 343 | git_glob::parse(pattern.as_ref()).map(|p| PatternMapping { | ||
| 344 | pattern: p, | ||
| 345 | value: (), | ||
| 346 | sequence_number: seq_id, | ||
| 347 | }) | ||
| 348 | }) | ||
| 349 | .collect(), | ||
| 350 | source: None, | ||
| 351 | base: None, | ||
| 352 | } | ||
| 353 | } | ||
| 354 | } | ||
| @@ -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_value | 70 | 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 | ||
| 97 | impl<'a> Iterator for Iter<'a> { | 97 | impl<'a> Iterator for Iter<'a> { |
| 98 | type Item = Result<(&'a BStr, crate::State<'a>), Error>; | 98 | type Item = Result<(&'a BStr, crate::StateRef<'a>), Error>; |
| 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())?; |
| @@ -1,2 +1,3 @@ | |||
| 1 | pub use git_testtools::Result; | ||
| 1 | mod match_group; | 2 | mod match_group; |
| 2 | mod parse; | 3 | mod parse; |
| @@ -0,0 +1,77 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | set -eu -o pipefail | ||
| 3 | |||
| 4 | cat <<EOF >user.exclude | ||
| 5 | # a custom exclude configured per user | ||
| 6 | user-file-anywhere | ||
| 7 | /user-file-from-top | ||
| 8 | |||
| 9 | user-dir-anywhere/ | ||
| 10 | /user-dir-from-top | ||
| 11 | |||
| 12 | user-subdir/file | ||
| 13 | **/user-subdir-anywhere/file | ||
| 14 | EOF | ||
| 15 | |||
| 16 | mkdir repo; | ||
| 17 | (cd repo | ||
| 18 | git init -q | ||
| 19 | git config core.excludesFile ../user.exclude | ||
| 20 | |||
| 21 | cat <<EOF >.git/info/exclude | ||
| 22 | # a sample .git/info/exclude | ||
| 23 | file-anywhere | ||
| 24 | /file-from-top | ||
| 25 | |||
| 26 | dir-anywhere/ | ||
| 27 | /dir-from-top | ||
| 28 | |||
| 29 | subdir/file | ||
| 30 | **/subdir-anywhere/file | ||
| 31 | EOF | ||
| 32 | |||
| 33 | cat <<EOF >.gitignore | ||
| 34 | # a sample .gitignore | ||
| 35 | top-level-local-file-anywhere | ||
| 36 | EOF | ||
| 37 | |||
| 38 | mkdir dir-with-ignore | ||
| 39 | cat <<EOF >dir-with-ignore/.gitignore | ||
| 40 | # a sample .gitignore | ||
| 41 | sub-level-local-file-anywhere | ||
| 42 | EOF | ||
| 43 | |||
| 44 | git add .gitignore dir-with-ignore | ||
| 45 | git commit --allow-empty -m "init" | ||
| 46 | |||
| 47 | mkdir user-dir-anywhere user-dir-from-top dir-anywhere dir-from-top | ||
| 48 | mkdir -p dir/user-dir-anywhere dir/dir-anywhere | ||
| 49 | |||
| 50 | git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || : | ||
| 51 | user-file-anywhere | ||
| 52 | dir/user-file-anywhere | ||
| 53 | user-file-from-top | ||
| 54 | no-match/user-file-from-top | ||
| 55 | user-dir-anywhere | ||
| 56 | user-dir-from-top | ||
| 57 | no-match/user-dir-from-top | ||
| 58 | user-subdir/file | ||
| 59 | subdir/user-subdir-anywhere/file | ||
| 60 | file-anywhere | ||
| 61 | dir/file-anywhere | ||
| 62 | file-from-top | ||
| 63 | no-match/file-from-top | ||
| 64 | dir-anywhere | ||
| 65 | dir/dir-anywhere | ||
| 66 | dir-from-top | ||
| 67 | no-match/dir-from-top | ||
| 68 | subdir/file | ||
| 69 | subdir/subdir-anywhere/file | ||
| 70 | top-level-local-file-anywhere | ||
| 71 | dir/top-level-local-file-anywhere | ||
| 72 | no-match/sub-level-local-file-anywhere | ||
| 73 | dir-with-ignore/sub-level-local-file-anywhere | ||
| 74 | dir-with-ignore/sub-dir/sub-level-local-file-anywhere | ||
| 75 | EOF | ||
| 76 | |||
| 77 | ) | ||
| @@ -1,14 +1,117 @@ | |||
| 1 | mod ignore { | 1 | mod ignore { |
| 2 | use git_attributes::Ignore; | 2 | use bstr::{BStr, ByteSlice}; |
| 3 | use git_attributes::{Ignore, Match, MatchGroup}; | ||
| 4 | use git_glob::pattern::Case; | ||
| 5 | use std::io::Read; | ||
| 6 | |||
| 7 | struct Expectations<'a> { | ||
| 8 | lines: bstr::Lines<'a>, | ||
| 9 | } | ||
| 10 | |||
| 11 | impl<'a> Iterator for Expectations<'a> { | ||
| 12 | type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); | ||
| 13 | |||
| 14 | fn next(&mut self) -> Option<Self::Item> { | ||
| 15 | let line = self.lines.next()?; | ||
| 16 | let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); | ||
| 17 | let value = value[1..].as_bstr(); | ||
| 18 | |||
| 19 | let source_and_line = if left == b"::" { | ||
| 20 | None | ||
| 21 | } else { | ||
| 22 | let mut tokens = left.split(|b| *b == b':'); | ||
| 23 | let source = tokens.next().unwrap().as_bstr(); | ||
| 24 | let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); | ||
| 25 | let pattern = tokens.next().unwrap().as_bstr(); | ||
| 26 | Some((source, line_number, pattern)) | ||
| 27 | }; | ||
| 28 | Some((value, source_and_line)) | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | #[test] | ||
| 33 | fn from_git_dir() -> crate::Result { | ||
| 34 | let dir = git_testtools::scripted_fixture_repo_read_only("make_global_and_external_and_dir_ignores.sh")?; | ||
| 35 | let repo_dir = dir.join("repo"); | ||
| 36 | let git_dir = repo_dir.join(".git"); | ||
| 37 | let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | ||
| 38 | let mut buf = Vec::new(); | ||
| 39 | let mut group = MatchGroup::from_git_dir(git_dir, Some(dir.join("user.exclude")), &mut buf)?; | ||
| 40 | |||
| 41 | assert!( | ||
| 42 | !group.add_patterns_file("not-a-file", false, None, &mut buf)?, | ||
| 43 | "missing files are no problem and cause a negative response" | ||
| 44 | ); | ||
| 45 | assert!( | ||
| 46 | group.add_patterns_file(repo_dir.join(".gitignore"), true, repo_dir.as_path().into(), &mut buf)?, | ||
| 47 | "existing files return true" | ||
| 48 | ); | ||
| 49 | |||
| 50 | buf.clear(); | ||
| 51 | let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); | ||
| 52 | std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?; | ||
| 53 | group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); | ||
| 54 | |||
| 55 | for (path, source_and_line) in (Expectations { | ||
| 56 | lines: baseline.lines(), | ||
| 57 | }) { | ||
| 58 | let actual = group.pattern_matching_relative_path( | ||
| 59 | path, | ||
| 60 | repo_dir | ||
| 61 | .join(path.to_str_lossy().as_ref()) | ||
| 62 | .metadata() | ||
| 63 | .ok() | ||
| 64 | .map(|m| m.is_dir()), | ||
| 65 | Case::Sensitive, | ||
| 66 | ); | ||
| 67 | match (actual, source_and_line) { | ||
| 68 | ( | ||
| 69 | Some(Match { | ||
| 70 | sequence_number, | ||
| 71 | pattern: _, | ||
| 72 | source, | ||
| 73 | value: _, | ||
| 74 | }), | ||
| 75 | Some((expected_source, line, _expected_pattern)), | ||
| 76 | ) => { | ||
| 77 | assert_eq!(sequence_number, line, "our counting should match the one used in git"); | ||
| 78 | assert_eq!( | ||
| 79 | source.map(|p| p.canonicalize().unwrap()), | ||
| 80 | Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) | ||
| 81 | ); | ||
| 82 | } | ||
| 83 | (None, None) => {} | ||
| 84 | (actual, expected) => panic!("actual {:?} should match {:?} with path '{}'", actual, expected, path), | ||
| 85 | } | ||
| 86 | } | ||
| 87 | Ok(()) | ||
| 88 | } | ||
| 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 | } |
| @@ -1,5 +1,5 @@ | |||
| 1 | use bstr::{BStr, ByteSlice}; | 1 | use bstr::{BStr, ByteSlice}; |
| 2 | use git_attributes::{parse, State}; | 2 | use git_attributes::{parse, StateRef}; |
| 3 | use git_glob::pattern::Mode; | 3 | use git_glob::pattern::Mode; |
| 4 | use git_testtools::fixture_bytes; | 4 | use 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 | 11 | 35 | 11 |
| 36 | ), | 36 | ), |
| @@ -249,22 +249,22 @@ fn trailing_whitespace_in_attributes_is_ignored() { · 212 unmodified lines | |||
| 249 | ); | 249 | ); |
| 250 | } | 250 | } |
| 251 | 251 | ||
| 252 | type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::State<'a>)>, usize); | 252 | type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::StateRef<'a>)>, usize); |
| 253 | 253 | ||
| 254 | fn set(attr: &str) -> (&BStr, State) { | 254 | fn 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 | ||
| 258 | fn unset(attr: &str) -> (&BStr, State) { | 258 | fn 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 | ||
| 262 | fn unspecified(attr: &str) -> (&BStr, State) { | 262 | fn 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 | ||
| 266 | fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, State<'b>) { | 266 | fn 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 | ||
| 270 | fn pattern(name: &str, flags: git_glob::pattern::Mode, first_wildcard_pos: Option<usize>) -> parse::Kind { | 270 | fn pattern(name: &str, flags: git_glob::pattern::Mode, first_wildcard_pos: Option<usize>) -> parse::Kind { |
| @@ -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 | } |
| @@ -15,6 +15,7 @@ include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] · 14 unmodified lines | |||
| 15 | 15 | ||
| 16 | [dependencies] | 16 | [dependencies] |
| 17 | git-features = { version = "^0.20.0", path = "../git-features"} | 17 | git-features = { version = "^0.20.0", path = "../git-features"} |
| 18 | git-path = { version = "^0.1.0", path = "../git-path" } | ||
| 18 | git-sec = { version = "^0.1.0", path = "../git-sec" } | 19 | git-sec = { version = "^0.1.0", path = "../git-sec" } |
| 19 | 20 | ||
| 20 | dirs = "4" | 21 | dirs = "4" |
| @@ -22,7 +23,7 @@ nom = { version = "7", default_features = false, features = [ "std" ] } · 1 unmodified line | |||
| 22 | memchr = "2" | 23 | memchr = "2" |
| 23 | serde_crate = { version = "1", package = "serde", optional = true } | 24 | serde_crate = { version = "1", package = "serde", optional = true } |
| 24 | pwd = "1.3.1" | 25 | pwd = "1.3.1" |
| 25 | quick-error = "2.0.0" | 26 | thiserror = "1.0.26" |
| 26 | unicode-bom = "1.1.4" | 27 | unicode-bom = "1.1.4" |
| 27 | bstr = { version = "0.2.13", default-features = false, features = ["std"] } | 28 | bstr = { version = "0.2.13", default-features = false, features = ["std"] } |
| 28 | 29 | ||
| @@ -1,35 +0,0 @@ | |||
| 1 | use std::{error::Error, fmt::Display}; | ||
| 2 | |||
| 3 | use crate::parser::SectionHeaderName; | ||
| 4 | // TODO Consider replacing with quick_error | ||
| 5 | /// All possible error types that may occur from interacting with | ||
| 6 | /// [`GitConfig`](super::GitConfig). | ||
| 7 | #[allow(clippy::module_name_repetitions)] | ||
| 8 | #[derive(PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Debug)] | ||
| 9 | pub enum GitConfigError<'a> { | ||
| 10 | /// The requested section does not exist. | ||
| 11 | SectionDoesNotExist(SectionHeaderName<'a>), | ||
| 12 | /// The requested subsection does not exist. | ||
| 13 | SubSectionDoesNotExist(Option<&'a str>), | ||
| 14 | /// The key does not exist in the requested section. | ||
| 15 | KeyDoesNotExist, | ||
| 16 | /// The conversion into the provided type for methods such as | ||
| 17 | /// [`GitConfig::value`](super::GitConfig::value) failed. | ||
| 18 | FailedConversion, | ||
| 19 | } | ||
| 20 | |||
| 21 | impl Display for GitConfigError<'_> { | ||
| 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 23 | match self { | ||
| 24 | Self::SectionDoesNotExist(s) => write!(f, "Section '{}' does not exist.", s), | ||
| 25 | Self::SubSectionDoesNotExist(s) => match s { | ||
| 26 | Some(s) => write!(f, "Subsection '{}' does not exist.", s), | ||
| 27 | None => write!(f, "Top level section does not exist."), | ||
| 28 | }, | ||
| 29 | Self::KeyDoesNotExist => write!(f, "The name for a value provided does not exist."), | ||
| 30 | Self::FailedConversion => write!(f, "Failed to convert to specified type."), | ||
| 31 | } | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | impl Error for GitConfigError<'_> {} | ||
| @@ -1,3 +1,4 @@ | |||
| 1 | use bstr::BStr; | ||
| 1 | use std::{ | 2 | use 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 | ||
| 9 | use crate::{ | 10 | use 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 in | 62 | /// `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 key | 63 | /// any number of identically named sections. When this is the case, the key |
| 64 | /// is known as a "multivar". In this case, `get_raw_value` follows the | 64 | /// is known as a "multivar". In this case, `raw_value` follows the |
| 65 | /// "last one wins" approach that `git-config` internally uses for multivar | 65 | /// "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 h | 78 | /// 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 last | 82 | /// 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 work | 93 | /// 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::ResolvedGitConfig | 96 | /// [`ResolvedGitConfig`]: crate::file::ResolvedGitConfig |
| 97 | /// [`get_raw_value`]: Self::get_raw_value | 97 | /// [`raw_value`]: Self::raw_value |
| 98 | #[derive(PartialEq, Eq, Clone, Debug, Default)] | 98 | #[derive(PartialEq, Eq, Clone, Debug, Default)] |
| 99 | pub struct GitConfig<'event> { | 99 | pub struct GitConfig<'event> { |
| 100 | /// The list of events that occur before an actual section. Since a | 100 | /// The list of events that occur before an actual section. Since a |
| @@ -121,32 +121,20 @@ pub struct GitConfig<'event> { · 20 unmodified lines | |||
| 121 | pub mod from_paths { | 121 | pub 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 | ||
| 177 | pub mod from_env { | 165 | pub 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 | ||
| 214 | impl<'event> GitConfig<'event> { | 188 | impl<'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 valid | 199 | /// 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-FILES | 213 | /// [`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 | /// # Examples | 408 | /// # 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::values | 434 | /// [`values`]: crate::values |
| 459 | /// [`TryFrom`]: std::convert::TryFrom | 435 | /// [`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 subsection | 501 | /// Returns all interpreted values given a section, an optional subsection |
| @@ -481,7 +511,7 @@ impl<'event> GitConfig<'event> { · 9 unmodified lines | |||
| 481 | /// # Examples | 511 | /// # 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 turbofish | 537 | /// // ... 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 | /// # Errors | 543 | /// # Errors |
| @@ -518,18 +548,17 @@ impl<'event> GitConfig<'event> { · 4 unmodified lines | |||
| 518 | /// | 548 | /// |
| 519 | /// [`values`]: crate::values | 549 | /// [`values`]: crate::values |
| 520 | /// [`TryFrom`]: std::convert::TryFrom | 550 | /// [`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_ids | 576 | 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_ids | 592 | 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 | /// ```rust | 661 | /// ```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 = self | 790 | 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 = id | 846 | 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 subsection | 879 | /// 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 of | 882 | /// Consider [`Self::raw_multi_value`] if you want to get all values of |
| 859 | /// a multivar instead. | 883 | /// a multivar instead. |
| 860 | /// | 884 | /// |
| 861 | /// # Errors | 885 | /// # Errors |
| 862 | /// | 886 | /// |
| 863 | /// This function will return an error if the key is not in the requested | 887 | /// 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 need | 895 | // 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 follow | 896 | // 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 self | 899 | 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 mutable | 920 | /// 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 | /// # Errors | 923 | /// # Errors |
| 900 | /// | 924 | /// |
| 901 | /// This function will return an error if the key is not in the requested | 925 | /// 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 subsection | 985 | /// 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 single | 1017 | /// 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 | /// # Errors | 1020 | /// # 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 requested | 1022 | /// 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 subsections | 1023 | /// 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.sections | 1034 | self.sections |
| 1011 | .get(§ion_id) | 1035 | .get(§ion_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 single | 1094 | /// 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 full | 1097 | /// 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 requested | 1102 | /// 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 subsections | 1103 | /// 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 | /// # Errors | 1184 | /// # 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 iterate | 1207 | /// consider using [`raw_multi_value_mut`], which will let you iterate |
| 1184 | /// and check over the values instead. This is best used as a convenience | 1208 | /// and check over the values instead. This is best used as a convenience |
| 1185 | /// function for setting multivars whose values should be treated as an | 1209 | /// 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 | /// # Errors | 1280 | /// # 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_mut | 1284 | /// [`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 = self | 1354 | let section_ids = self |
| 1331 | .section_lookup_tree | 1355 | .section_lookup_tree |
| 1332 | .get(§ion_name) | 1356 | .get(§ion_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 needs | 1359 | // 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 loop | 1360 | // `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_ids | 1377 | 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_tree | 1387 | self.section_lookup_tree |
| 1364 | .get(§ion_name) | 1388 | .get(§ion_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_str | 1408 | /// [`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_bytes | 1420 | /// [`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_bytes | 1432 | /// [`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 | ||
| 1460 | impl From<GitConfig<'_>> for Vec<u8> { | 1481 | impl 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 | } |
| @@ -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 | ||
| 3 | mod error; | ||
| 4 | mod git_config; | 3 | mod git_config; |
| 5 | mod resolved; | 4 | mod resolved; |
| 6 | mod section; | 5 | mod section; |
| @@ -8,7 +7,6 @@ mod value; · 1 unmodified line | |||
| 8 | 7 | ||
| 9 | use std::ops::{Add, AddAssign}; | 8 | use std::ops::{Add, AddAssign}; |
| 10 | 9 | ||
| 11 | pub use error::*; | ||
| 12 | pub use resolved::*; | 10 | pub use resolved::*; |
| 13 | pub use section::*; | 11 | pub use section::*; |
| 14 | pub use value::*; | 12 | pub use value::*; |
| @@ -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 a | 32 | /// 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 | |||
| 107 | impl TryFrom<&Path> for ResolvedGitConfig<'static> { | 106 | impl 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 | ||
| 116 | impl<'data> From<GitConfig<'data>> for ResolvedGitConfig<'data> { | 114 | impl<'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 | } |
| @@ -7,7 +7,8 @@ use std::{ · 6 unmodified lines | |||
| 7 | }; | 7 | }; |
| 8 | 8 | ||
| 9 | use crate::{ | 9 | use 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 adds | 73 | /// 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 unless | 115 | /// 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 adding | 121 | /// 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 key | 128 | /// 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 the | 134 | /// 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.whitespace | 138 | 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. |
| 146 | impl<'borrow, 'event> MutableSection<'borrow, 'event> { | 143 | impl<'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_value | 184 | 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 | |||
| 206 | impl<'event> Deref for MutableSection<'_, 'event> { | 201 | impl<'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.section | 205 | 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 | /// # Errors | 269 | /// # 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 return | 277 | /// Retrieves all values that have the provided key name. This may return |
| @@ -325,17 +317,15 @@ impl<'event> SectionBody<'event> { · 39 unmodified lines | |||
| 325 | /// # Errors | 317 | /// # 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.0 | 330 | 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 index | 361 | // value end needs to be offset by one so that the last value's index |
| 374 | // is included in the range | 362 | // 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 | |||
| 448 | impl FusedIterator for SectionBodyIter<'_> {} | 435 | impl FusedIterator for SectionBodyIter<'_> {} |
| 449 | 436 | ||
| 450 | impl<'event> From<Vec<Event<'event>>> for SectionBody<'event> { | 437 | impl<'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 | } |
| @@ -6,11 +6,11 @@ use std::{ · 5 unmodified lines | |||
| 6 | 6 | ||
| 7 | use crate::{ | 7 | use 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 | /// # Errors | 54 | /// # 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 that | 61 | /// 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 the | 62 | /// 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 | /// # Errors | 147 | /// # 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_sizes | 159 | } 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 &self | 162 | for event in &self |
| 165 | .section | 163 | .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 value | 203 | /// 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 | /// # Safety | 212 | /// # 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 | /// # Safety | 221 | /// # 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::zip | 256 | /// [`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 the | 279 | /// 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 the | 285 | /// 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::GitConfig | 312 | /// [`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.section | 364 | 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_sizes | 381 | } 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.section | 385 | 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 as | 396 | // 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 as | 414 | // 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, |
| @@ -7,7 +7,8 @@ use std::{ · 6 unmodified lines | |||
| 7 | path::{Path, PathBuf}, | 7 | path::{Path, PathBuf}, |
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | use crate::file::{from_paths, GitConfig, GitConfigError}; | 10 | use crate::file::{from_paths, GitConfig}; |
| 11 | use crate::lookup; | ||
| 11 | 12 | ||
| 12 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] | 13 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] |
| 13 | pub enum ConfigSource { | 14 | pub enum ConfigSource { |
| @@ -40,7 +41,6 @@ pub struct ConfigBuilder { · 26 unmodified lines | |||
| 40 | 41 | ||
| 41 | impl ConfigBuilder { | 42 | impl ConfigBuilder { |
| 42 | /// Constructs a new builder that finds the default location | 43 | /// 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 | ||
| 148 | impl<'config> Config<'config> { | 148 | impl<'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 | None | 176 | 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 during | 225 | /// 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(), |
| @@ -54,10 +54,67 @@ · 53 unmodified lines | |||
| 54 | #[cfg(feature = "serde")] | 54 | #[cfg(feature = "serde")] |
| 55 | extern crate serde_crate as serde; | 55 | extern crate serde_crate as serde; |
| 56 | 56 | ||
| 57 | pub mod lookup { | ||
| 58 | |||
| 59 | /// The error when looking up a value. | ||
| 60 | #[derive(Debug, thiserror::Error)] | ||
| 61 | pub enum Error<E> { | ||
| 62 | #[error(transparent)] | ||
| 63 | ValueMissing(#[from] crate::lookup::existing::Error), | ||
| 64 | #[error(transparent)] | ||
| 65 | FailedConversion(E), | ||
| 66 | } | ||
| 67 | |||
| 68 | pub mod existing { | ||
| 69 | /// The error when looking up a value that doesn't exist. | ||
| 70 | #[derive(Debug, thiserror::Error)] | ||
| 71 | pub enum Error { | ||
| 72 | #[error("The requested section does not exist")] | ||
| 73 | SectionMissing, | ||
| 74 | #[error("The requested subsection does not exist")] | ||
| 75 | SubSectionMissing, | ||
| 76 | #[error("The key does not exist in the requested section")] | ||
| 77 | KeyMissing, | ||
| 78 | } | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 57 | pub mod file; | 82 | pub mod file; |
| 58 | pub mod fs; | 83 | pub mod fs; |
| 59 | pub mod parser; | 84 | pub mod parser; |
| 60 | pub mod values; | 85 | pub mod values; |
| 86 | /// The future home of the `values` module (TODO). | ||
| 87 | pub mod value { | ||
| 88 | pub mod parse { | ||
| 89 | use bstr::BString; | ||
| 90 | |||
| 91 | /// The error returned when creating `Integer` from byte string. | ||
| 92 | #[derive(Debug, thiserror::Error, Eq, PartialEq)] | ||
| 93 | #[allow(missing_docs)] | ||
| 94 | #[error("Could not decode '{}': {}", .input, .message)] | ||
| 95 | pub struct Error { | ||
| 96 | pub message: &'static str, | ||
| 97 | pub input: BString, | ||
| 98 | #[source] | ||
| 99 | pub utf8_err: Option<std::str::Utf8Error>, | ||
| 100 | } | ||
| 101 | |||
| 102 | impl Error { | ||
| 103 | pub(crate) fn new(message: &'static str, input: impl Into<BString>) -> Self { | ||
| 104 | Error { | ||
| 105 | message, | ||
| 106 | input: input.into(), | ||
| 107 | utf8_err: None, | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | pub(crate) fn with_err(mut self, err: std::str::Utf8Error) -> Self { | ||
| 112 | self.utf8_err = Some(err); | ||
| 113 | self | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
| 61 | 118 | ||
| 62 | mod permissions { | 119 | mod permissions { |
| 63 | use crate::Permissions; | 120 | use crate::Permissions; |
| @@ -75,7 +75,6 @@ impl Event<'_> { · 74 unmodified lines | |||
| 75 | /// Generates a byte representation of the value. This should be used when | 75 | /// 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 be | 76 | /// 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::clone | 96 | /// [`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`. If | 114 | /// 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_ render | 115 | /// 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 | ||
| 135 | impl From<Event<'_>> for Vec<u8> { | 132 | impl 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 | ||
| 142 | impl From<&Event<'_>> for Vec<u8> { | 138 | impl 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::clone | 174 | /// [`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::clone | 214 | /// [`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.0 | 235 | 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.0 | 263 | &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 be | 302 | /// 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::clone | 322 | /// [`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 | ||
| 368 | impl From<ParsedSectionHeader<'_>> for Vec<u8> { | 352 | impl 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 | ||
| 375 | impl From<&ParsedSectionHeader<'_>> for Vec<u8> { | 358 | impl 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 | ||
| 382 | impl<'a> From<ParsedSectionHeader<'a>> for Event<'a> { | 364 | impl<'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::clone | 393 | /// [`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 | ||
| 437 | impl From<ParsedComment<'_>> for Vec<u8> { | 417 | impl 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 | ||
| 444 | impl From<&ParsedComment<'_>> for Vec<u8> { | 423 | impl 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 | |||
| 463 | impl Error<'_> { | 441 | impl Error<'_> { |
| 464 | /// The one-indexed line number where the error occurred. This is determined | 442 | /// 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 + 1 | 446 | 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_until | 452 | &self.parsed_until |
| @@ -490,7 +466,6 @@ impl Error<'_> { · 13 unmodified lines | |||
| 490 | /// not. | 466 | /// not. |
| 491 | /// | 467 | /// |
| 492 | /// [`clone`]: std::clone::Clone::clone | 468 | /// [`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 | ||
| 569 | impl Display for ParserOrIoError<'_> { | 544 | impl 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 | ||
| 579 | impl From<std::io::Error> for ParserOrIoError<'_> { | 553 | impl 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 | ||
| 596 | impl Display for ParserNode { | 569 | impl 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`] if | 794 | /// a section) from the parser. Consider [`Parser::take_frontmatter`] if |
| 823 | /// you need an owned copy only once. If that function was called, then this | 795 | /// 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.frontmatter | 799 | &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 the | 804 | /// Consider [`Parser::frontmatter`] if you only need a reference to the |
| 834 | /// frontmatter | 805 | /// 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. Consider | 810 | /// Returns the parsed sections from the parser. Consider |
| 841 | /// [`Parser::take_sections`] if you need an owned copy only once. If that | 811 | /// [`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.sections | 815 | &self.sections |
| @@ -849,7 +818,6 @@ impl<'a> Parser<'a> { · 2 unmodified lines | |||
| 849 | /// Takes the parsed sections from the parser. Subsequent calls will return | 818 | /// Takes the parsed sections from the parser. Subsequent calls will return |
| 850 | /// an empty vec. Consider [`Parser::sections`] if you only need a reference | 819 | /// 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 | |||
| 878 | impl<'a> TryFrom<&'a str> for Parser<'a> { | 845 | impl<'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 | |||
| 887 | impl<'a> TryFrom<&'a [u8]> for Parser<'a> { | 853 | impl<'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 extraneous | 891 | /// 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] | ||
| 929 | pub fn parse_from_str(input: &str) -> Result<Parser, Error> { | 893 | pub 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 | } |
| @@ -2,8 +2,8 @@ · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; | 3 | use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; |
| 4 | 4 | ||
| 5 | use bstr::BStr; | 5 | use crate::value; |
| 6 | use quick_error::quick_error; | 6 | use bstr::{BStr, BString}; |
| 7 | #[cfg(feature = "serde")] | 7 | #[cfg(feature = "serde")] |
| 8 | use serde::{Serialize, Serializer}; | 8 | use 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] |
| 125 | pub fn normalize_bytes(input: &[u8]) -> Cow<'_, [u8]> { | 124 | pub 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] |
| 132 | pub fn normalize_vec(input: Vec<u8>) -> Cow<'static, [u8]> { | 130 | pub 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] |
| 139 | pub fn normalize_str(input: &str) -> Cow<'_, [u8]> { | 136 | pub 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 | ||
| 151 | impl<'a> From<&'a [u8]> for Bytes<'a> { | 148 | impl<'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 | ||
| 166 | impl<'a> From<Cow<'a, [u8]>> for Bytes<'a> { | 162 | impl<'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 | ||
| 183 | impl<'a> From<Cow<'a, [u8]>> for String<'a> { | 178 | impl<'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)] |
| 307 | pub struct Path<'a> { | 304 | pub struct Path<'a> { |
| 308 | /// The path string, un-interpolated | 305 | /// The path string, un-interpolated |
| 309 | pub value: Cow<'a, [u8]>, | 306 | pub value: Cow<'a, BStr>, |
| 310 | } | 307 | } |
| 311 | 308 | ||
| 312 | impl<'a> std::ops::Deref for Path<'a> { | 309 | impl<'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 | ||
| 323 | impl<'a> AsRef<BStr> for Path<'a> { | ||
| 324 | fn as_ref(&self) -> &BStr { | ||
| 325 | self.value.as_ref() | ||
| 326 | } | ||
| 327 | } | ||
| 328 | |||
| 326 | impl<'a> From<Cow<'a, [u8]>> for Path<'a> { | 329 | impl<'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 when | 361 | /// 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 be | 362 | /// 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 when | 369 | /// 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 be | 370 | /// 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 | ||
| 373 | quick_error! { | 378 | fn 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 | ||
| 384 | impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { | 385 | impl<'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 | ||
| 407 | impl TryFrom<Vec<u8>> for Boolean<'_> { | 408 | impl 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 | ||
| 426 | impl<'a> TryFrom<Cow<'a, [u8]>> for Boolean<'a> { | 427 | impl<'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 | ||
| 436 | impl Display for Boolean<'_> { | 437 | impl 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 | ||
| 446 | impl From<Boolean<'_>> for bool { | 446 | impl 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 | ||
| 456 | impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] { | 455 | impl<'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 | ||
| 466 | impl From<Boolean<'_>> for Vec<u8> { | 464 | impl 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 | ||
| 473 | impl From<&Boolean<'_>> for Vec<u8> { | 470 | impl 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 | ||
| 504 | impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> { | 500 | impl<'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 | ||
| 524 | impl TryFrom<Vec<u8>> for TrueVariant<'_> { | 520 | impl 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 | ||
| 554 | impl<'a, 'b: 'a> From<&'b TrueVariant<'a>> for &'a [u8] { | 550 | impl<'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 | ||
| 644 | quick_error! { | 639 | fn 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 | ||
| 663 | impl TryFrom<&[u8]> for Integer { | 646 | impl 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 | ||
| 691 | impl TryFrom<Vec<u8>> for Integer { | 673 | impl 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 | ||
| 700 | impl TryFrom<Cow<'_, [u8]>> for Integer { | 681 | impl 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 | ||
| 712 | impl From<Integer> for Vec<u8> { | 692 | impl 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 | ||
| 719 | impl From<&Integer> for Vec<u8> { | 698 | impl 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 | ||
| 737 | impl IntegerSuffix { | 715 | impl 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 | ||
| 750 | impl Display for IntegerSuffix { | 727 | impl 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 | ||
| 775 | impl FromStr for IntegerSuffix { | 751 | impl 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 | ||
| 789 | impl TryFrom<&[u8]> for IntegerSuffix { | 764 | impl 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 | ||
| 798 | impl TryFrom<Vec<u8>> for IntegerSuffix { | 772 | impl 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 when | 798 | /// 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 be | 799 | /// 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 | ||
| 864 | quick_error! { | 836 | fn 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 | ||
| 883 | impl TryFrom<&[u8]> for Color { | 843 | impl 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 | ||
| 929 | impl TryFrom<Vec<u8>> for Color { | 888 | impl 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 | ||
| 938 | impl TryFrom<Cow<'_, [u8]>> for Color { | 896 | impl 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 | ||
| 950 | impl From<Color> for Vec<u8> { | 907 | impl 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 | ||
| 957 | impl From<&Color> for Vec<u8> { | 913 | impl 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 | ||
| 1028 | impl FromStr for ColorValue { | 983 | impl 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 | ||
| 1084 | impl TryFrom<&[u8]> for ColorValue { | 1039 | impl 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 | ||
| 1163 | impl FromStr for ColorAttribute { | 1117 | impl 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 | ||
| 1198 | impl TryFrom<&[u8]> for ColorAttribute { | 1152 | impl 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 | } |
| @@ -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_config | 175 | 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 | |||
| 261 | a"#, | 258 | a"#, |
| 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 read | 475 | // 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 read | 483 | // 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 returned | 494 | // 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 returned | 517 | // 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 | |||
| 892 | mod get_raw_value { | 913 | mod 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 | |||
| 1008 | mod get_raw_multi_value { | 1041 | mod 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 | } |
| @@ -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 | |||
| 113 | fn section_names_are_case_insensitive() -> crate::Result { | 129 | fn 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(()) |
| @@ -81,11 +81,6 @@ name = "pipe" · 80 unmodified lines | |||
| 81 | path = "tests/pipe.rs" | 81 | path = "tests/pipe.rs" |
| 82 | required-features = ["io-pipe"] | 82 | required-features = ["io-pipe"] |
| 83 | 83 | ||
| 84 | [[test]] | ||
| 85 | name = "path" | ||
| 86 | path = "tests/path.rs" | ||
| 87 | required-features = ["bstr"] | ||
| 88 | |||
| 89 | [dependencies] | 84 | [dependencies] |
| 90 | #! ### Optional Dependencies | 85 | #! ### 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. |
| 123 | time = { version = "0.3.2", optional = true, default-features = false, features = ["local-offset"] } | 118 | time = { 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. | ||
| 128 | bstr = { version = "0.2.17", optional = true, default-features = false, features = ["std"] } | ||
| 129 | |||
| 130 | document-features = { version = "0.2.0", optional = true } | 120 | document-features = { version = "0.2.0", optional = true } |
| 131 | 121 | ||
| 132 | [target.'cfg(unix)'.dependencies] | 122 | [target.'cfg(unix)'.dependencies] |
| @@ -24,7 +24,6 @@ pub mod interrupt; · 23 unmodified lines | |||
| 24 | #[cfg(feature = "io-pipe")] | 24 | #[cfg(feature = "io-pipe")] |
| 25 | pub mod io; | 25 | pub mod io; |
| 26 | pub mod parallel; | 26 | pub mod parallel; |
| 27 | pub mod path; | ||
| 28 | #[cfg(feature = "progress")] | 27 | #[cfg(feature = "progress")] |
| 29 | pub mod progress; | 28 | pub mod progress; |
| 30 | pub mod threading; | 29 | pub mod threading; |
| @@ -1,234 +0,0 @@ | |||
| 1 | //! ### Research | ||
| 2 | //! | ||
| 3 | //! * **windows** | ||
| 4 | //! - [`dirent.c`](https://github.com/git/git/blob/main/compat/win32/dirent.c#L31:L31) contains all implementation (seemingly) of opening directories and reading their entries, along with all path conversions (UTF-16 for windows). This is done on the fly so git can work with [in UTF-8](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). | ||
| 5 | //! - mingw [is used for the conversion](https://github.com/git/git/blob/main/compat/mingw.h#L579:L579) and it appears they handle surrogates during the conversion, maybe some sort of non-strict UTF-8 converter? Actually it uses [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) | ||
| 6 | //! under the hood which by now does fail if the UTF-8 would be invalid unicode, i.e. unicode pairs. | ||
| 7 | //! - `OsString` on windows already stores strings as WTF-8, which supports [surrogate pairs](https://unicodebook.readthedocs.io/unicode_encodings.html), | ||
| 8 | //! something that UTF-8 isn't allowed do it for security reasons, after all it's UTF-16 specific and exists only to extend | ||
| 9 | //! the encodable code-points. | ||
| 10 | //! - informative reading on [WTF-8](https://simonsapin.github.io/wtf-8/#motivation) which is the encoding used by Rust | ||
| 11 | //! internally that deals with surrogates and non-wellformed surrogates (those that aren't in pairs). | ||
| 12 | //! * **unix** | ||
| 13 | //! - It uses [opendir](https://man7.org/linux/man-pages/man3/opendir.3.html) and [readdir](https://man7.org/linux/man-pages/man3/readdir.3.html) | ||
| 14 | //! respectively. There is no encoding specified, except that these paths are null-terminated. | ||
| 15 | //! | ||
| 16 | //! ### Learnings | ||
| 17 | //! | ||
| 18 | //! Surrogate pairs are a way to extend the encodable value range in UTF-16 encodings, used primarily on windows and in Javascript. | ||
| 19 | //! For a long time these codepoints used for surrogates, always to be used in pairs, were not assigned, until…they were for rare | ||
| 20 | //! emojies and the likes. The unicode standard does not require surrogates to happen in pairs, even though by now unpaired surrogates | ||
| 21 | //! in UTF-16 are considered ill-formed, which aren't supposed to be converted to UTF-8 for example. | ||
| 22 | //! | ||
| 23 | //! This is the reason we have to deal with `to_string_lossy()`, it's _just_ for that quirk. | ||
| 24 | //! | ||
| 25 | //! This also means the only platform ever eligible to see conversion errors is windows, and there it's only older pre-vista | ||
| 26 | //! windows versions which incorrectly allow ill-formed UTF-16 strings. Newer versions don't perform such conversions anymore, for | ||
| 27 | //! example when going from UTF-16 to UTF-8, they will trigger an error. | ||
| 28 | //! | ||
| 29 | //! ### Conclusions | ||
| 30 | //! | ||
| 31 | //! Since [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) by now is | ||
| 32 | //! fixed (Vista onward) to produce valid UTF-8, lone surrogate codepoints will cause failure, which `git` | ||
| 33 | //! [doesn't care about](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). | ||
| 34 | //! | ||
| 35 | //! We will, though, which means from now on we can just convert to UTF-8 on windows and bubble up errors where necessary, | ||
| 36 | //! preventing potential mismatched surrogate pairs to ever be saved on disk by gitoxide. | ||
| 37 | //! | ||
| 38 | //! Even though the error only exists on older windows versions, we will represent it in the type system through fallible function calls. | ||
| 39 | //! Callers may `.expect()` on the result to indicate they don't wish to handle this special and rare case. Note that servers should not | ||
| 40 | //! ever get into a code-path which does panic though. | ||
| 41 | |||
| 42 | use std::{ | ||
| 43 | borrow::Cow, | ||
| 44 | ffi::OsStr, | ||
| 45 | path::{Path, PathBuf}, | ||
| 46 | }; | ||
| 47 | |||
| 48 | #[derive(Debug)] | ||
| 49 | /// The error type returned by [`into_bytes()`] and others may suffer from failed conversions from or to bytes. | ||
| 50 | pub struct Utf8Error; | ||
| 51 | |||
| 52 | impl std::fmt::Display for Utf8Error { | ||
| 53 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 54 | f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input") | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | impl std::error::Error for Utf8Error {} | ||
| 59 | |||
| 60 | /// Like [`into_bytes()`], but takes `OsStr` as input for a lossless, but fallible, conversion. | ||
| 61 | pub fn os_str_into_bytes(path: &OsStr) -> Result<&[u8], Utf8Error> { | ||
| 62 | let path = into_bytes(Cow::Borrowed(path.as_ref()))?; | ||
| 63 | match path { | ||
| 64 | Cow::Borrowed(path) => Ok(path), | ||
| 65 | Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"), | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | /// Convert the given path either into its raw bytes on unix or its UTF8 encoded counterpart on windows. | ||
| 70 | /// | ||
| 71 | /// On windows, if the source Path contains ill-formed, lone surrogates, the UTF-8 conversion will fail | ||
| 72 | /// causing `Utf8Error` to be returned. | ||
| 73 | pub fn into_bytes<'a>(path: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, [u8]>, Utf8Error> { | ||
| 74 | let path = path.into(); | ||
| 75 | let utf8_bytes = match path { | ||
| 76 | Cow::Owned(path) => Cow::Owned({ | ||
| 77 | #[cfg(unix)] | ||
| 78 | let p = { | ||
| 79 | use std::os::unix::ffi::OsStringExt; | ||
| 80 | path.into_os_string().into_vec() | ||
| 81 | }; | ||
| 82 | #[cfg(not(unix))] | ||
| 83 | let p: Vec<_> = path.into_os_string().into_string().map_err(|_| Utf8Error)?.into(); | ||
| 84 | p | ||
| 85 | }), | ||
| 86 | Cow::Borrowed(path) => Cow::Borrowed({ | ||
| 87 | #[cfg(unix)] | ||
| 88 | let p = { | ||
| 89 | use std::os::unix::ffi::OsStrExt; | ||
| 90 | path.as_os_str().as_bytes() | ||
| 91 | }; | ||
| 92 | #[cfg(not(unix))] | ||
| 93 | let p = path.to_str().ok_or(Utf8Error)?.as_bytes(); | ||
| 94 | p | ||
| 95 | }), | ||
| 96 | }; | ||
| 97 | Ok(utf8_bytes) | ||
| 98 | } | ||
| 99 | |||
| 100 | /// Similar to [`into_bytes()`] but panics if malformed surrogates are encountered on windows. | ||
| 101 | pub fn into_bytes_or_panic_on_windows<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, [u8]> { | ||
| 102 | into_bytes(path).expect("prefix path doesn't contain ill-formed UTF-8") | ||
| 103 | } | ||
| 104 | |||
| 105 | /// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on unix. | ||
| 106 | /// | ||
| 107 | /// On windows, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. There are some potential | ||
| 108 | /// git versions and windows installation which produce mal-formed UTF-16 if certain emojies are in the path. It's as rare as | ||
| 109 | /// it sounds, but possible. | ||
| 110 | pub fn from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> { | ||
| 111 | #[cfg(unix)] | ||
| 112 | let p = { | ||
| 113 | use std::os::unix::ffi::OsStrExt; | ||
| 114 | OsStr::from_bytes(input).as_ref() | ||
| 115 | }; | ||
| 116 | #[cfg(not(unix))] | ||
| 117 | let p = Path::new(std::str::from_utf8(input).map_err(|_| Utf8Error)?); | ||
| 118 | Ok(p) | ||
| 119 | } | ||
| 120 | |||
| 121 | /// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. | ||
| 122 | pub fn from_bytes<'a>(input: impl Into<Cow<'a, [u8]>>) -> Result<Cow<'a, Path>, Utf8Error> { | ||
| 123 | let input = input.into(); | ||
| 124 | match input { | ||
| 125 | Cow::Borrowed(input) => from_byte_slice(input).map(Cow::Borrowed), | ||
| 126 | Cow::Owned(input) => from_byte_vec(input).map(Cow::Owned), | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | /// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input` as bstr. | ||
| 131 | #[cfg(feature = "bstr")] | ||
| 132 | pub fn from_bstr<'a>(input: impl Into<Cow<'a, bstr::BStr>>) -> Result<Cow<'a, Path>, Utf8Error> { | ||
| 133 | let input = input.into(); | ||
| 134 | match input { | ||
| 135 | Cow::Borrowed(input) => from_byte_slice(input).map(Cow::Borrowed), | ||
| 136 | Cow::Owned(input) => from_byte_vec(input).map(Cow::Owned), | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | /// Similar to [`from_byte_slice()`], but takes and produces owned data. | ||
| 141 | pub fn from_byte_vec(input: impl Into<Vec<u8>>) -> Result<PathBuf, Utf8Error> { | ||
| 142 | let input = input.into(); | ||
| 143 | #[cfg(unix)] | ||
| 144 | let p = { | ||
| 145 | use std::os::unix::ffi::OsStringExt; | ||
| 146 | std::ffi::OsString::from_vec(input).into() | ||
| 147 | }; | ||
| 148 | #[cfg(not(unix))] | ||
| 149 | let p = PathBuf::from(String::from_utf8(input).map_err(|_| Utf8Error)?); | ||
| 150 | Ok(p) | ||
| 151 | } | ||
| 152 | |||
| 153 | /// Similar to [`from_byte_vec()`], but will panic if there is ill-formed UTF-8 in the `input`. | ||
| 154 | pub fn from_byte_vec_or_panic_on_windows(input: impl Into<Vec<u8>>) -> PathBuf { | ||
| 155 | from_byte_vec(input).expect("well-formed UTF-8 on windows") | ||
| 156 | } | ||
| 157 | |||
| 158 | /// Similar to [`from_byte_slice()`], but will panic if there is ill-formed UTF-8 in the `input`. | ||
| 159 | pub fn from_byte_slice_or_panic_on_windows(input: &[u8]) -> &Path { | ||
| 160 | from_byte_slice(input).expect("well-formed UTF-8 on windows") | ||
| 161 | } | ||
| 162 | |||
| 163 | /// Methods to handle paths as bytes and do conversions between them. | ||
| 164 | pub mod convert { | ||
| 165 | use std::borrow::Cow; | ||
| 166 | |||
| 167 | fn replace<'a>(path: impl Into<Cow<'a, [u8]>>, find: u8, replace: u8) -> Cow<'a, [u8]> { | ||
| 168 | let path = path.into(); | ||
| 169 | match path { | ||
| 170 | Cow::Owned(mut path) => { | ||
| 171 | for b in path.iter_mut().filter(|b| **b == find) { | ||
| 172 | *b = replace; | ||
| 173 | } | ||
| 174 | path.into() | ||
| 175 | } | ||
| 176 | Cow::Borrowed(path) => { | ||
| 177 | if !path.contains(&find) { | ||
| 178 | return path.into(); | ||
| 179 | } | ||
| 180 | let mut path = path.to_owned(); | ||
| 181 | for b in path.iter_mut().filter(|b| **b == find) { | ||
| 182 | *b = replace; | ||
| 183 | } | ||
| 184 | path.into() | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | /// Replaces windows path separators with slashes. | ||
| 190 | pub fn to_native_separators<'a>(path: impl Into<Cow<'a, [u8]>>) -> Cow<'a, [u8]> { | ||
| 191 | #[cfg(not(windows))] | ||
| 192 | let p = to_unix_separators(path); | ||
| 193 | #[cfg(windows)] | ||
| 194 | let p = to_windows_separators(path); | ||
| 195 | p | ||
| 196 | } | ||
| 197 | |||
| 198 | /// Convert paths with slashes to backslashes on windows and do nothing on unix. | ||
| 199 | pub fn to_windows_separators_on_windows_or_panic(path: &std::path::Path) -> Cow<'_, std::path::Path> { | ||
| 200 | #[cfg(not(windows))] | ||
| 201 | { | ||
| 202 | path.into() | ||
| 203 | } | ||
| 204 | #[cfg(windows)] | ||
| 205 | { | ||
| 206 | crate::path::from_byte_slice_or_panic_on_windows( | ||
| 207 | crate::path::convert::to_windows_separators(crate::path::into_bytes_or_panic_on_windows(path)).as_ref(), | ||
| 208 | ) | ||
| 209 | .to_owned() | ||
| 210 | .into() | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | /// Replaces windows path separators with slashes. | ||
| 215 | pub fn to_unix_separators<'a>(path: impl Into<Cow<'a, [u8]>>) -> Cow<'a, [u8]> { | ||
| 216 | replace(path, b'\\', b'/') | ||
| 217 | } | ||
| 218 | |||
| 219 | /// Find backslashes and replace them with slashes, which typically resembles a unix path. | ||
| 220 | /// | ||
| 221 | /// No other transformation is performed, the caller must check other invariants. | ||
| 222 | pub fn to_windows_separators<'a>(path: impl Into<Cow<'a, [u8]>>) -> Cow<'a, [u8]> { | ||
| 223 | replace(path, b'/', b'\\') | ||
| 224 | } | ||
| 225 | |||
| 226 | /// Obtain a `BStr` compatible `Cow` from one that is bytes. | ||
| 227 | #[cfg(feature = "bstr")] | ||
| 228 | pub fn into_bstr(path: Cow<'_, [u8]>) -> Cow<'_, bstr::BStr> { | ||
| 229 | match path { | ||
| 230 | Cow::Owned(p) => Cow::Owned(p.into()), | ||
| 231 | Cow::Borrowed(p) => Cow::Borrowed(p.into()), | ||
| 232 | } | ||
| 233 | } | ||
| 234 | } | ||
| @@ -1,30 +0,0 @@ | |||
| 1 | mod bytes { | ||
| 2 | use bstr::ByteSlice; | ||
| 3 | use git_features::path; | ||
| 4 | |||
| 5 | #[test] | ||
| 6 | fn assure_unix_separators() { | ||
| 7 | assert_eq!( | ||
| 8 | path::convert::to_unix_separators(b"no-backslash".as_ref()).as_bstr(), | ||
| 9 | "no-backslash" | ||
| 10 | ); | ||
| 11 | |||
| 12 | assert_eq!( | ||
| 13 | path::convert::to_unix_separators(b"\\a\\b\\\\".as_ref()).as_bstr(), | ||
| 14 | "/a/b//" | ||
| 15 | ); | ||
| 16 | } | ||
| 17 | |||
| 18 | #[test] | ||
| 19 | fn assure_windows_separators() { | ||
| 20 | assert_eq!( | ||
| 21 | path::convert::to_windows_separators(b"no-backslash".as_ref()).as_bstr(), | ||
| 22 | "no-backslash" | ||
| 23 | ); | ||
| 24 | |||
| 25 | assert_eq!( | ||
| 26 | path::convert::to_windows_separators(b"/a/b//".as_ref()).as_bstr(), | ||
| 27 | "\\a\\b\\\\" | ||
| 28 | ); | ||
| 29 | } | ||
| 30 | } | ||
| @@ -4,9 +4,9 @@ · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | use bstr::BString; | 5 | use 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))] |
| 12 | pub struct Pattern { | 12 | pub struct Pattern { |
| @@ -28,6 +28,8 @@ pub use wildmatch::function::wildmatch; · 15 unmodified lines | |||
| 28 | mod parse; | 28 | mod 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. |
| 31 | pub fn parse(text: &[u8]) -> Option<Pattern> { | 31 | /// |
| 32 | Pattern::from_bytes(text) | 32 | /// Note that |
| 33 | pub fn parse(text: impl AsRef<[u8]>) -> Option<Pattern> { | ||
| 34 | Pattern::from_bytes(text.as_ref()) | ||
| 33 | } | 35 | } |
| @@ -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 | ||
| @@ -1,5 +1,6 @@ | |||
| 1 | use bitflags::bitflags; | 1 | use bitflags::bitflags; |
| 2 | use bstr::{BStr, ByteSlice}; | 2 | use bstr::{BStr, ByteSlice}; |
| 3 | use std::fmt; | ||
| 3 | 4 | ||
| 4 | use crate::{pattern, wildmatch, Pattern}; | 5 | use crate::{pattern, wildmatch, Pattern}; |
| 5 | 6 | ||
| @@ -36,6 +37,12 @@ pub enum Case { · 30 unmodified lines | |||
| 36 | Fold, | 37 | Fold, |
| 37 | } | 38 | } |
| 38 | 39 | ||
| 40 | impl Default for Case { | ||
| 41 | fn default() -> Self { | ||
| 42 | Case::Sensitive | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 39 | impl Pattern { | 46 | impl 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) = self | 93 | if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) { |
| 88 | .mode | 94 | 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-with | 110 | // "*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 | value | 114 | 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 !value | 125 | 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 == value | 140 | self.text == value |
| 154 | } | 141 | } |
| 155 | } | 142 | } |
| 156 | } | 143 | } |
| 157 | } | 144 | } |
| 158 | } | 145 | } |
| 146 | |||
| 147 | impl fmt::Display for Pattern { | ||
| 148 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| 149 | if self.mode.contains(Mode::NEGATIVE) { | ||
| 150 | "!".fmt(f)?; | ||
| 151 | } | ||
| 152 | if self.mode.contains(Mode::ABSOLUTE) { | ||
| 153 | "/".fmt(f)?; | ||
| 154 | } | ||
| 155 | self.text.fmt(f)?; | ||
| 156 | if self.mode.contains(Mode::MUST_BE_DIR) { | ||
| 157 | "/".fmt(f)?; | ||
| 158 | } | ||
| 159 | Ok(()) | ||
| 160 | } | ||
| 161 | } | ||
| @@ -1,3 +1,3 @@ | |||
| 1 | version https://git-lfs.github.com/spec/v1 | 1 | version https://git-lfs.github.com/spec/v1 |
| 2 | oid sha256:cdade0fe7f3df3ac737130a101b077c82f9de1dcb3d59d5964e5ffd920405e8d | 2 | oid sha256:6bda59c1591dfd35f09cad5d09e2950b2a6ff885d9f7900535144a5275ab51c3 |
| 3 | size 10384 | 3 | size 10428 |
| @@ -10,8 +10,10 @@ while read -r pattern value; do · 9 unmodified lines | |||
| 10 | echo "$pattern" > .gitignore | 10 | echo "$pattern" > .gitignore |
| 11 | echo "$value" | git check-ignore -vn --stdin 2>&1 || : | 11 | echo "$value" | git check-ignore -vn --stdin 2>&1 || : |
| 12 | done <<EOF >git-baseline.nmatch | 12 | done <<EOF >git-baseline.nmatch |
| 13 | */\ XXX/\ | 13 | /*foo bam/barfoo/baz/bam |
| 14 | */\\ XXX/\ | 14 | /*foo bar/bam/barfoo/baz/bam |
| 15 | foo foobaz | ||
| 16 | */\' XXX/\' | ||
| 15 | /*foo bar/foo | 17 | /*foo bar/foo |
| 16 | /*foo bar/bazfoo | 18 | /*foo bar/bazfoo |
| 17 | foo*bar foo/baz/bar | 19 | foo*bar foo/baz/bar |
| @@ -71,6 +73,7 @@ while read -r pattern value; do · 53 unmodified lines | |||
| 71 | echo "$pattern" > .gitignore | 73 | echo "$pattern" > .gitignore |
| 72 | echo "$value" | git check-ignore -vn --stdin 2>&1 || : | 74 | echo "$value" | git check-ignore -vn --stdin 2>&1 || : |
| 73 | done <<EOF >git-baseline.match | 75 | done <<EOF >git-baseline.match |
| 76 | */' XXX/' | ||
| 74 | \a a | 77 | \a a |
| 75 | \\\[a-z] \a | 78 | \\\[a-z] \a |
| 76 | \\\? \a | 79 | \\\? \a |
| @@ -1,3 +1,3 @@ | |||
| 1 | mod matching; | ||
| 2 | mod parse; | 1 | mod parse; |
| 2 | mod pattern; | ||
| 3 | mod wildmatch; | 3 | mod wildmatch; |
| @@ -1,280 +1 @@ | |||
| 1 | use std::collections::BTreeSet; | ||
| 2 | 1 | ||
| 3 | use bstr::{BStr, ByteSlice}; | ||
| 4 | use git_glob::{pattern, pattern::Case}; | ||
| 5 | |||
| 6 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] | ||
| 7 | pub struct GitMatch<'a> { | ||
| 8 | pattern: &'a BStr, | ||
| 9 | value: &'a BStr, | ||
| 10 | /// True if git could match `value` with `pattern` | ||
| 11 | is_match: bool, | ||
| 12 | } | ||
| 13 | |||
| 14 | pub struct Baseline<'a> { | ||
| 15 | inner: bstr::Lines<'a>, | ||
| 16 | } | ||
| 17 | |||
| 18 | impl<'a> Iterator for Baseline<'a> { | ||
| 19 | type Item = GitMatch<'a>; | ||
| 20 | |||
| 21 | fn next(&mut self) -> Option<Self::Item> { | ||
| 22 | let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' '); | ||
| 23 | let pattern = tokens.next().expect("pattern").as_bstr(); | ||
| 24 | let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr(); | ||
| 25 | |||
| 26 | let git_match = self.inner.next()?; | ||
| 27 | let is_match = !git_match.starts_with(b"::\t"); | ||
| 28 | Some(GitMatch { | ||
| 29 | pattern, | ||
| 30 | value, | ||
| 31 | is_match, | ||
| 32 | }) | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | impl<'a> Baseline<'a> { | ||
| 37 | fn new(input: &'a [u8]) -> Self { | ||
| 38 | Baseline { | ||
| 39 | inner: input.as_bstr().lines(), | ||
| 40 | } | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | #[test] | ||
| 45 | fn compare_baseline_with_ours() { | ||
| 46 | let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); | ||
| 47 | let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0); | ||
| 48 | let mut mismatches = Vec::new(); | ||
| 49 | for (input_file, expected_matches, case) in &[ | ||
| 50 | ("git-baseline.match", true, pattern::Case::Sensitive), | ||
| 51 | ("git-baseline.nmatch", false, pattern::Case::Sensitive), | ||
| 52 | ("git-baseline.match-icase", true, pattern::Case::Fold), | ||
| 53 | ] { | ||
| 54 | let input = std::fs::read(dir.join(*input_file)).unwrap(); | ||
| 55 | let mut seen = BTreeSet::default(); | ||
| 56 | |||
| 57 | for m @ GitMatch { | ||
| 58 | pattern, | ||
| 59 | value, | ||
| 60 | is_match, | ||
| 61 | } in Baseline::new(&input) | ||
| 62 | { | ||
| 63 | total_matches += 1; | ||
| 64 | assert!(seen.insert(m), "duplicate match entry: {:?}", m); | ||
| 65 | assert_eq!( | ||
| 66 | is_match, *expected_matches, | ||
| 67 | "baseline for matches must be {} - check baseline and git version: {:?}", | ||
| 68 | expected_matches, m | ||
| 69 | ); | ||
| 70 | match std::panic::catch_unwind(|| { | ||
| 71 | let pattern = pat(pattern); | ||
| 72 | pattern.matches_repo_relative_path( | ||
| 73 | value, | ||
| 74 | basename_start_pos(value), | ||
| 75 | false, // TODO: does it make sense to pretend it is a dir and see what happens? | ||
| 76 | *case, | ||
| 77 | ) | ||
| 78 | }) { | ||
| 79 | Ok(actual_match) => { | ||
| 80 | if actual_match == is_match { | ||
| 81 | total_correct += 1; | ||
| 82 | } else { | ||
| 83 | mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches)); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | Err(_) => { | ||
| 87 | panics += 1; | ||
| 88 | continue; | ||
| 89 | } | ||
| 90 | }; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | dbg!(mismatches); | ||
| 95 | assert_eq!( | ||
| 96 | total_correct, | ||
| 97 | total_matches - panics, | ||
| 98 | "We perfectly agree with git here" | ||
| 99 | ); | ||
| 100 | assert_eq!(panics, 0); | ||
| 101 | } | ||
| 102 | |||
| 103 | #[test] | ||
| 104 | fn non_dirs_for_must_be_dir_patterns_are_ignored() { | ||
| 105 | let pattern = pat("hello/"); | ||
| 106 | |||
| 107 | assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR)); | ||
| 108 | assert_eq!( | ||
| 109 | pattern.text, "hello", | ||
| 110 | "a dir pattern doesn't actually end with the trailing slash" | ||
| 111 | ); | ||
| 112 | let path = "hello"; | ||
| 113 | assert!( | ||
| 114 | !pattern.matches_repo_relative_path(path, None, false /* is-dir */, Case::Sensitive), | ||
| 115 | "non-dirs never match a dir pattern" | ||
| 116 | ); | ||
| 117 | assert!( | ||
| 118 | pattern.matches_repo_relative_path(path, None, true /* is-dir */, Case::Sensitive), | ||
| 119 | "dirs can match a dir pattern with the normal rules" | ||
| 120 | ); | ||
| 121 | } | ||
| 122 | |||
| 123 | #[test] | ||
| 124 | fn matches_of_absolute_paths_work() { | ||
| 125 | let input = "/hello/git"; | ||
| 126 | let pat = pat(input); | ||
| 127 | assert!( | ||
| 128 | pat.matches(input, git_glob::wildmatch::Mode::empty()), | ||
| 129 | "patterns always match themselves" | ||
| 130 | ); | ||
| 131 | assert!( | ||
| 132 | pat.matches(input, git_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL), | ||
| 133 | "patterns always match themselves, path mode doesn't change that" | ||
| 134 | ); | ||
| 135 | } | ||
| 136 | |||
| 137 | #[test] | ||
| 138 | fn basename_matches_from_end() { | ||
| 139 | let pat = &pat("foo"); | ||
| 140 | assert!(match_file(pat, "FoO", Case::Fold)); | ||
| 141 | assert!(!match_file(pat, "FoOo", Case::Fold)); | ||
| 142 | assert!(!match_file(pat, "Foo", Case::Sensitive)); | ||
| 143 | assert!(match_file(pat, "foo", Case::Sensitive)); | ||
| 144 | assert!(!match_file(pat, "Foo", Case::Sensitive)); | ||
| 145 | assert!(!match_file(pat, "barfoo", Case::Sensitive)); | ||
| 146 | } | ||
| 147 | |||
| 148 | #[test] | ||
| 149 | fn absolute_basename_matches_only_from_beginning() { | ||
| 150 | let pat = &pat("/foo"); | ||
| 151 | assert!(match_file(pat, "FoO", Case::Fold)); | ||
| 152 | assert!(!match_file(pat, "bar/Foo", Case::Fold)); | ||
| 153 | assert!(match_file(pat, "foo", Case::Sensitive)); | ||
| 154 | assert!(!match_file(pat, "Foo", Case::Sensitive)); | ||
| 155 | assert!(!match_file(pat, "bar/foo", Case::Sensitive)); | ||
| 156 | } | ||
| 157 | |||
| 158 | #[test] | ||
| 159 | fn absolute_path_matches_only_from_beginning() { | ||
| 160 | let pat = &pat("/bar/foo"); | ||
| 161 | assert!(!match_file(pat, "FoO", Case::Fold)); | ||
| 162 | assert!(match_file(pat, "bar/Foo", Case::Fold)); | ||
| 163 | assert!(!match_file(pat, "foo", Case::Sensitive)); | ||
| 164 | assert!(match_file(pat, "bar/foo", Case::Sensitive)); | ||
| 165 | assert!(!match_file(pat, "bar/Foo", Case::Sensitive)); | ||
| 166 | } | ||
| 167 | |||
| 168 | #[test] | ||
| 169 | fn absolute_path_with_recursive_glob_detects_mismatches_quickly() { | ||
| 170 | let pat = &pat("/bar/foo/**"); | ||
| 171 | assert!(!match_file(pat, "FoO", Case::Fold)); | ||
| 172 | assert!(!match_file(pat, "bar/Fooo", Case::Fold)); | ||
| 173 | assert!(!match_file(pat, "baz/bar/Foo", Case::Fold)); | ||
| 174 | } | ||
| 175 | |||
| 176 | #[test] | ||
| 177 | fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() { | ||
| 178 | let pat = &pat("/bar/foo/**"); | ||
| 179 | assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive)); | ||
| 180 | assert!(match_file(pat, "bar/Foo/match", Case::Fold)); | ||
| 181 | } | ||
| 182 | |||
| 183 | #[test] | ||
| 184 | fn relative_path_does_not_match_from_end() { | ||
| 185 | let pattern = &pat("bar/foo"); | ||
| 186 | assert!(!match_file(pattern, "FoO", Case::Fold)); | ||
| 187 | assert!(match_file(pattern, "bar/Foo", Case::Fold)); | ||
| 188 | assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold)); | ||
| 189 | assert!(!match_file(pattern, "foo", Case::Sensitive)); | ||
| 190 | assert!(match_file(pattern, "bar/foo", Case::Sensitive)); | ||
| 191 | assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive)); | ||
| 192 | assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive)); | ||
| 193 | } | ||
| 194 | |||
| 195 | #[test] | ||
| 196 | fn basename_glob_and_literal_is_ends_with() { | ||
| 197 | let pattern = &pat("*foo"); | ||
| 198 | assert!(match_file(pattern, "FoO", Case::Fold)); | ||
| 199 | assert!(match_file(pattern, "BarFoO", Case::Fold)); | ||
| 200 | assert!(!match_file(pattern, "BarFoOo", Case::Fold)); | ||
| 201 | assert!(!match_file(pattern, "Foo", Case::Sensitive)); | ||
| 202 | assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); | ||
| 203 | assert!(match_file(pattern, "barfoo", Case::Sensitive)); | ||
| 204 | assert!(!match_file(pattern, "barfooo", Case::Sensitive)); | ||
| 205 | |||
| 206 | assert!(match_file(pattern, "bar/foo", Case::Sensitive)); | ||
| 207 | assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive)); | ||
| 208 | } | ||
| 209 | |||
| 210 | #[test] | ||
| 211 | fn special_cases_from_corpus() { | ||
| 212 | let pattern = &pat("foo*bar"); | ||
| 213 | assert!( | ||
| 214 | !match_file(pattern, "foo/baz/bar", Case::Sensitive), | ||
| 215 | "asterisk does not match path separators" | ||
| 216 | ); | ||
| 217 | let pattern = &pat("*some/path/to/hello.txt"); | ||
| 218 | assert!( | ||
| 219 | !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive), | ||
| 220 | "asterisk doesn't match path separators" | ||
| 221 | ); | ||
| 222 | |||
| 223 | let pattern = &pat("/*foo.txt"); | ||
| 224 | assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive)); | ||
| 225 | assert!( | ||
| 226 | !match_file(pattern, "hello/foo.txt", Case::Sensitive), | ||
| 227 | "absolute single asterisk doesn't match paths" | ||
| 228 | ); | ||
| 229 | } | ||
| 230 | |||
| 231 | #[test] | ||
| 232 | fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() { | ||
| 233 | let pattern = &pat("/*foo"); | ||
| 234 | |||
| 235 | assert!(match_file(pattern, "FoO", Case::Fold)); | ||
| 236 | assert!(match_file(pattern, "BarFoO", Case::Fold)); | ||
| 237 | assert!(!match_file(pattern, "BarFoOo", Case::Fold)); | ||
| 238 | assert!(!match_file(pattern, "Foo", Case::Sensitive)); | ||
| 239 | assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); | ||
| 240 | assert!(match_file(pattern, "barfoo", Case::Sensitive)); | ||
| 241 | assert!(!match_file(pattern, "barfooo", Case::Sensitive)); | ||
| 242 | } | ||
| 243 | |||
| 244 | #[test] | ||
| 245 | fn absolute_basename_glob_and_literal_is_glob_in_paths() { | ||
| 246 | let pattern = &pat("/*foo"); | ||
| 247 | |||
| 248 | assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /"); | ||
| 249 | assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive)); | ||
| 250 | } | ||
| 251 | |||
| 252 | #[test] | ||
| 253 | fn negated_patterns_are_handled_by_caller() { | ||
| 254 | let pattern = &pat("!foo"); | ||
| 255 | assert!( | ||
| 256 | match_file(pattern, "foo", Case::Sensitive), | ||
| 257 | "negative patterns match like any other" | ||
| 258 | ); | ||
| 259 | assert!( | ||
| 260 | pattern.is_negative(), | ||
| 261 | "the caller checks for the negative flag and acts accordingly" | ||
| 262 | ); | ||
| 263 | } | ||
| 264 | |||
| 265 | fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern { | ||
| 266 | git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works") | ||
| 267 | } | ||
| 268 | |||
| 269 | fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool { | ||
| 270 | match_path(pattern, path, false, case) | ||
| 271 | } | ||
| 272 | |||
| 273 | fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: bool, case: Case) -> bool { | ||
| 274 | let path = path.into(); | ||
| 275 | pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case) | ||
| 276 | } | ||
| 277 | |||
| 278 | fn basename_start_pos(value: &BStr) -> Option<usize> { | ||
| 279 | value.rfind_byte(b'/').map(|pos| pos + 1) | ||
| 280 | } | ||
| @@ -77,12 +77,12 @@ fn leading_exclamation_marks_can_be_escaped_with_backslash() { · 76 unmodified lines | |||
| 77 | fn leading_slashes_mark_patterns_as_absolute() { | 77 | fn 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 | ||
| @@ -0,0 +1,326 @@ | |||
| 1 | use std::collections::BTreeSet; | ||
| 2 | |||
| 3 | use bstr::{BStr, ByteSlice}; | ||
| 4 | use git_glob::{pattern, pattern::Case}; | ||
| 5 | |||
| 6 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] | ||
| 7 | pub struct GitMatch<'a> { | ||
| 8 | pattern: &'a BStr, | ||
| 9 | value: &'a BStr, | ||
| 10 | /// True if git could match `value` with `pattern` | ||
| 11 | is_match: bool, | ||
| 12 | } | ||
| 13 | |||
| 14 | pub struct Baseline<'a> { | ||
| 15 | inner: bstr::Lines<'a>, | ||
| 16 | } | ||
| 17 | |||
| 18 | impl<'a> Iterator for Baseline<'a> { | ||
| 19 | type Item = GitMatch<'a>; | ||
| 20 | |||
| 21 | fn next(&mut self) -> Option<Self::Item> { | ||
| 22 | let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' '); | ||
| 23 | let pattern = tokens.next().expect("pattern").as_bstr(); | ||
| 24 | let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr(); | ||
| 25 | |||
| 26 | let git_match = self.inner.next()?; | ||
| 27 | let is_match = !git_match.starts_with(b"::\t"); | ||
| 28 | Some(GitMatch { | ||
| 29 | pattern, | ||
| 30 | value, | ||
| 31 | is_match, | ||
| 32 | }) | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | impl<'a> Baseline<'a> { | ||
| 37 | fn new(input: &'a [u8]) -> Self { | ||
| 38 | Baseline { | ||
| 39 | inner: input.as_bstr().lines(), | ||
| 40 | } | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | #[test] | ||
| 45 | fn compare_baseline_with_ours() { | ||
| 46 | let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); | ||
| 47 | let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0); | ||
| 48 | let mut mismatches = Vec::new(); | ||
| 49 | for (input_file, expected_matches, case) in &[ | ||
| 50 | ("git-baseline.match", true, pattern::Case::Sensitive), | ||
| 51 | ("git-baseline.nmatch", false, pattern::Case::Sensitive), | ||
| 52 | ("git-baseline.match-icase", true, pattern::Case::Fold), | ||
| 53 | ] { | ||
| 54 | let input = std::fs::read(dir.join(*input_file)).unwrap(); | ||
| 55 | let mut seen = BTreeSet::default(); | ||
| 56 | |||
| 57 | for m @ GitMatch { | ||
| 58 | pattern, | ||
| 59 | value, | ||
| 60 | is_match, | ||
| 61 | } in Baseline::new(&input) | ||
| 62 | { | ||
| 63 | total_matches += 1; | ||
| 64 | assert!(seen.insert(m), "duplicate match entry: {:?}", m); | ||
| 65 | assert_eq!( | ||
| 66 | is_match, *expected_matches, | ||
| 67 | "baseline for matches must be {} - check baseline and git version: {:?}", | ||
| 68 | expected_matches, m | ||
| 69 | ); | ||
| 70 | match std::panic::catch_unwind(|| { | ||
| 71 | let pattern = pat(pattern); | ||
| 72 | pattern.matches_repo_relative_path(value, basename_start_pos(value), None, *case) | ||
| 73 | }) { | ||
| 74 | Ok(actual_match) => { | ||
| 75 | if actual_match == is_match { | ||
| 76 | total_correct += 1; | ||
| 77 | } else { | ||
| 78 | mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches)); | ||
| 79 | } | ||
| 80 | } | ||
| 81 | Err(_) => { | ||
| 82 | panics += 1; | ||
| 83 | continue; | ||
| 84 | } | ||
| 85 | }; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | dbg!(mismatches); | ||
| 90 | assert_eq!( | ||
| 91 | total_correct, | ||
| 92 | total_matches - panics, | ||
| 93 | "We perfectly agree with git here" | ||
| 94 | ); | ||
| 95 | assert_eq!(panics, 0); | ||
| 96 | } | ||
| 97 | |||
| 98 | #[test] | ||
| 99 | fn non_dirs_for_must_be_dir_patterns_are_ignored() { | ||
| 100 | let pattern = pat("hello/"); | ||
| 101 | |||
| 102 | assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR)); | ||
| 103 | assert_eq!( | ||
| 104 | pattern.text, "hello", | ||
| 105 | "a dir pattern doesn't actually end with the trailing slash" | ||
| 106 | ); | ||
| 107 | let path = "hello"; | ||
| 108 | assert!( | ||
| 109 | !pattern.matches_repo_relative_path(path, None, false.into() /* is-dir */, Case::Sensitive), | ||
| 110 | "non-dirs never match a dir pattern" | ||
| 111 | ); | ||
| 112 | assert!( | ||
| 113 | pattern.matches_repo_relative_path(path, None, true.into() /* is-dir */, Case::Sensitive), | ||
| 114 | "dirs can match a dir pattern with the normal rules" | ||
| 115 | ); | ||
| 116 | } | ||
| 117 | |||
| 118 | #[test] | ||
| 119 | fn matches_of_absolute_paths_work() { | ||
| 120 | let pattern = "/hello/git"; | ||
| 121 | assert!( | ||
| 122 | git_glob::wildmatch(pattern.into(), pattern.into(), git_glob::wildmatch::Mode::empty()), | ||
| 123 | "patterns always match themselves" | ||
| 124 | ); | ||
| 125 | assert!( | ||
| 126 | git_glob::wildmatch( | ||
| 127 | pattern.into(), | ||
| 128 | pattern.into(), | ||
| 129 | git_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | ||
| 130 | ), | ||
| 131 | "patterns always match themselves, path mode doesn't change that" | ||
| 132 | ); | ||
| 133 | } | ||
| 134 | |||
| 135 | #[test] | ||
| 136 | fn basename_matches_from_end() { | ||
| 137 | let pat = &pat("foo"); | ||
| 138 | assert!(match_file(pat, "FoO", Case::Fold)); | ||
| 139 | assert!(!match_file(pat, "FoOo", Case::Fold)); | ||
| 140 | assert!(!match_file(pat, "Foo", Case::Sensitive)); | ||
| 141 | assert!(match_file(pat, "foo", Case::Sensitive)); | ||
| 142 | assert!(!match_file(pat, "Foo", Case::Sensitive)); | ||
| 143 | assert!(!match_file(pat, "barfoo", Case::Sensitive)); | ||
| 144 | } | ||
| 145 | |||
| 146 | #[test] | ||
| 147 | fn absolute_basename_matches_only_from_beginning() { | ||
| 148 | let pat = &pat("/foo"); | ||
| 149 | assert!(match_file(pat, "FoO", Case::Fold)); | ||
| 150 | assert!(!match_file(pat, "bar/Foo", Case::Fold)); | ||
| 151 | assert!(match_file(pat, "foo", Case::Sensitive)); | ||
| 152 | assert!(!match_file(pat, "Foo", Case::Sensitive)); | ||
| 153 | assert!(!match_file(pat, "bar/foo", Case::Sensitive)); | ||
| 154 | } | ||
| 155 | |||
| 156 | #[test] | ||
| 157 | fn absolute_path_matches_only_from_beginning() { | ||
| 158 | let pat = &pat("/bar/foo"); | ||
| 159 | assert!(!match_file(pat, "FoO", Case::Fold)); | ||
| 160 | assert!(match_file(pat, "bar/Foo", Case::Fold)); | ||
| 161 | assert!(!match_file(pat, "foo", Case::Sensitive)); | ||
| 162 | assert!(match_file(pat, "bar/foo", Case::Sensitive)); | ||
| 163 | assert!(!match_file(pat, "bar/Foo", Case::Sensitive)); | ||
| 164 | } | ||
| 165 | |||
| 166 | #[test] | ||
| 167 | fn absolute_path_with_recursive_glob_detects_mismatches_quickly() { | ||
| 168 | let pat = &pat("/bar/foo/**"); | ||
| 169 | assert!(!match_file(pat, "FoO", Case::Fold)); | ||
| 170 | assert!(!match_file(pat, "bar/Fooo", Case::Fold)); | ||
| 171 | assert!(!match_file(pat, "baz/bar/Foo", Case::Fold)); | ||
| 172 | } | ||
| 173 | |||
| 174 | #[test] | ||
| 175 | fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() { | ||
| 176 | let pat = &pat("/bar/foo/**"); | ||
| 177 | assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive)); | ||
| 178 | assert!(match_file(pat, "bar/Foo/match", Case::Fold)); | ||
| 179 | } | ||
| 180 | |||
| 181 | #[test] | ||
| 182 | fn relative_path_does_not_match_from_end() { | ||
| 183 | for pattern in &["bar/foo", "/bar/foo"] { | ||
| 184 | let pattern = &pat(*pattern); | ||
| 185 | assert!(!match_file(pattern, "FoO", Case::Fold)); | ||
| 186 | assert!(match_file(pattern, "bar/Foo", Case::Fold)); | ||
| 187 | assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold)); | ||
| 188 | assert!(!match_file(pattern, "foo", Case::Sensitive)); | ||
| 189 | assert!(match_file(pattern, "bar/foo", Case::Sensitive)); | ||
| 190 | assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive)); | ||
| 191 | assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive)); | ||
| 192 | } | ||
| 193 | } | ||
| 194 | |||
| 195 | #[test] | ||
| 196 | fn basename_glob_and_literal_is_ends_with() { | ||
| 197 | let pattern = &pat("*foo"); | ||
| 198 | assert!(match_file(pattern, "FoO", Case::Fold)); | ||
| 199 | assert!(match_file(pattern, "BarFoO", Case::Fold)); | ||
| 200 | assert!(!match_file(pattern, "BarFoOo", Case::Fold)); | ||
| 201 | assert!(!match_file(pattern, "Foo", Case::Sensitive)); | ||
| 202 | assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); | ||
| 203 | assert!(match_file(pattern, "barfoo", Case::Sensitive)); | ||
| 204 | assert!(!match_file(pattern, "barfooo", Case::Sensitive)); | ||
| 205 | |||
| 206 | assert!(match_file(pattern, "bar/foo", Case::Sensitive)); | ||
| 207 | assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive)); | ||
| 208 | } | ||
| 209 | |||
| 210 | #[test] | ||
| 211 | fn special_cases_from_corpus() { | ||
| 212 | let pattern = &pat("foo*bar"); | ||
| 213 | assert!( | ||
| 214 | !match_file(pattern, "foo/baz/bar", Case::Sensitive), | ||
| 215 | "asterisk does not match path separators" | ||
| 216 | ); | ||
| 217 | let pattern = &pat("*some/path/to/hello.txt"); | ||
| 218 | assert!( | ||
| 219 | !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive), | ||
| 220 | "asterisk doesn't match path separators" | ||
| 221 | ); | ||
| 222 | |||
| 223 | let pattern = &pat("/*foo.txt"); | ||
| 224 | assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive)); | ||
| 225 | assert!( | ||
| 226 | !match_file(pattern, "hello/foo.txt", Case::Sensitive), | ||
| 227 | "absolute single asterisk doesn't match paths" | ||
| 228 | ); | ||
| 229 | } | ||
| 230 | |||
| 231 | #[test] | ||
| 232 | fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() { | ||
| 233 | let pattern = &pat("/*foo"); | ||
| 234 | |||
| 235 | assert!(match_file(pattern, "FoO", Case::Fold)); | ||
| 236 | assert!(match_file(pattern, "BarFoO", Case::Fold)); | ||
| 237 | assert!(!match_file(pattern, "BarFoOo", Case::Fold)); | ||
| 238 | assert!(!match_file(pattern, "Foo", Case::Sensitive)); | ||
| 239 | assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); | ||
| 240 | assert!(match_file(pattern, "barfoo", Case::Sensitive)); | ||
| 241 | assert!(!match_file(pattern, "barfooo", Case::Sensitive)); | ||
| 242 | } | ||
| 243 | |||
| 244 | #[test] | ||
| 245 | fn absolute_basename_glob_and_literal_is_glob_in_paths() { | ||
| 246 | let pattern = &pat("/*foo"); | ||
| 247 | |||
| 248 | assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /"); | ||
| 249 | assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive)); | ||
| 250 | } | ||
| 251 | |||
| 252 | #[test] | ||
| 253 | fn negated_patterns_are_handled_by_caller() { | ||
| 254 | let pattern = &pat("!foo"); | ||
| 255 | assert!( | ||
| 256 | match_file(pattern, "foo", Case::Sensitive), | ||
| 257 | "negative patterns match like any other" | ||
| 258 | ); | ||
| 259 | assert!( | ||
| 260 | pattern.is_negative(), | ||
| 261 | "the caller checks for the negative flag and acts accordingly" | ||
| 262 | ); | ||
| 263 | } | ||
| 264 | #[test] | ||
| 265 | fn names_do_not_automatically_match_entire_directories() { | ||
| 266 | // this feature is implemented with the directory stack. | ||
| 267 | let pattern = &pat("foo"); | ||
| 268 | assert!(!match_file(pattern, "foobar", Case::Sensitive)); | ||
| 269 | assert!(!match_file(pattern, "foo/bar", Case::Sensitive)); | ||
| 270 | assert!(!match_file(pattern, "foo/bar/baz", Case::Sensitive)); | ||
| 271 | } | ||
| 272 | |||
| 273 | #[test] | ||
| 274 | fn directory_patterns_do_not_match_files_within_a_directory_as_well_like_slash_star_star() { | ||
| 275 | // this feature is implemented with the directory stack, which excludes entire directories | ||
| 276 | let pattern = &pat("dir/"); | ||
| 277 | assert!(!match_path(pattern, "dir/file", None, Case::Sensitive)); | ||
| 278 | assert!(!match_path(pattern, "base/dir/file", None, Case::Sensitive)); | ||
| 279 | assert!(!match_path(pattern, "base/ndir/file", None, Case::Sensitive)); | ||
| 280 | assert!(!match_path(pattern, "Dir/File", None, Case::Fold)); | ||
| 281 | assert!(!match_path(pattern, "Base/Dir/File", None, Case::Fold)); | ||
| 282 | assert!(!match_path(pattern, "dir2/file", None, Case::Sensitive)); | ||
| 283 | |||
| 284 | let pattern = &pat("dir/sub-dir/"); | ||
| 285 | assert!(!match_path(pattern, "dir/sub-dir/file", None, Case::Sensitive)); | ||
| 286 | assert!(!match_path(pattern, "dir/Sub-dir/File", None, Case::Fold)); | ||
| 287 | assert!(!match_path(pattern, "dir/Sub-dir2/File", None, Case::Fold)); | ||
| 288 | } | ||
| 289 | |||
| 290 | #[test] | ||
| 291 | fn single_paths_match_anywhere() { | ||
| 292 | let pattern = &pat("target"); | ||
| 293 | assert!(match_file(pattern, "dir/target", Case::Sensitive)); | ||
| 294 | assert!(!match_file(pattern, "dir/atarget", Case::Sensitive)); | ||
| 295 | assert!(!match_file(pattern, "dir/targeta", Case::Sensitive)); | ||
| 296 | assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive)); | ||
| 297 | |||
| 298 | let pattern = &pat("target/"); | ||
| 299 | assert!(!match_file(pattern, "dir/target", Case::Sensitive)); | ||
| 300 | assert!( | ||
| 301 | !match_path(pattern, "dir/target", None, Case::Sensitive), | ||
| 302 | "it assumes unknown to not be a directory" | ||
| 303 | ); | ||
| 304 | assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive)); | ||
| 305 | assert!( | ||
| 306 | !match_path(pattern, "dir/target/", Some(true), Case::Sensitive), | ||
| 307 | "we need sanitized paths that don't have trailing slashes" | ||
| 308 | ); | ||
| 309 | } | ||
| 310 | |||
| 311 | fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern { | ||
| 312 | git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works") | ||
| 313 | } | ||
| 314 | |||
| 315 | fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool { | ||
| 316 | match_path(pattern, path, false.into(), case) | ||
| 317 | } | ||
| 318 | |||
| 319 | fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: Option<bool>, case: Case) -> bool { | ||
| 320 | let path = path.into(); | ||
| 321 | pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case) | ||
| 322 | } | ||
| 323 | |||
| 324 | fn basename_start_pos(value: &BStr) -> Option<usize> { | ||
| 325 | value.rfind_byte(b'/').map(|pos| pos + 1) | ||
| 326 | } | ||
| @@ -0,0 +1,19 @@ | |||
| 1 | use git_glob::pattern::Mode; | ||
| 2 | use git_glob::Pattern; | ||
| 3 | |||
| 4 | #[test] | ||
| 5 | fn display() { | ||
| 6 | fn pat(text: &str, mode: Mode) -> String { | ||
| 7 | Pattern { | ||
| 8 | text: text.into(), | ||
| 9 | mode, | ||
| 10 | first_wildcard_pos: None, | ||
| 11 | } | ||
| 12 | .to_string() | ||
| 13 | } | ||
| 14 | assert_eq!(pat("a", Mode::ABSOLUTE), "/a"); | ||
| 15 | assert_eq!(pat("a", Mode::MUST_BE_DIR), "a/"); | ||
| 16 | assert_eq!(pat("a", Mode::NEGATIVE), "!a"); | ||
| 17 | assert_eq!(pat("a", Mode::ABSOLUTE | Mode::NEGATIVE | Mode::MUST_BE_DIR), "!/a/"); | ||
| 18 | } | ||
| 19 | mod matching; | ||
| @@ -1,3 +1,4 @@ | |||
| 1 | use bstr::ByteSlice; | ||
| 1 | use std::{ | 2 | use 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 | ||
| 365 | fn match_file_path(pattern: &git_glob::Pattern, path: &str, case: Case) -> bool { | 369 | fn match_file_path(pattern: &git_glob::Pattern, path: &str, case: Case) -> bool { |
| 366 | pattern.matches_repo_relative_path(path, basename_of(path), false /* is_dir */, case) | 370 | pattern.matches_repo_relative_path(path, basename_of(path), false.into() /* is_dir */, case) |
| 367 | } | 371 | } |
| 368 | fn basename_of(path: &str) -> Option<usize> { | 372 | fn basename_of(path: &str) -> Option<usize> { |
| 369 | path.rfind('/').map(|pos| pos + 1) | 373 | path.rfind('/').map(|pos| pos + 1) |
| @@ -1,6 +1,6 @@ | |||
| 1 | use bstr::{BStr, ByteSlice}; | 1 | use bstr::{BStr, ByteSlice}; |
| 2 | 2 | ||
| 3 | use crate::{extension, Entry, State, Version}; | 3 | use crate::{extension, Entry, PathStorage, State, Version}; |
| 4 | 4 | ||
| 5 | impl State { | 5 | impl 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.entries | 11 | &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.entries | 43 | &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 | } |
| @@ -41,12 +41,13 @@ use crate::util::read_u32; · 40 unmodified lines | |||
| 41 | pub struct Options { | 41 | pub 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 threads | 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 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 | ||
| @@ -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 | } |
| @@ -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. | ||
| 55 | pub 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 mode | 72 | /// 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, |
| @@ -30,7 +30,8 @@ required-features = [] · 29 unmodified lines | |||
| 30 | all-features = true | 30 | all-features = true |
| 31 | 31 | ||
| 32 | [dependencies] | 32 | [dependencies] |
| 33 | git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32", "bstr"] } | 33 | git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32" ] } |
| 34 | git-path = { version = "^0.1.0", path = "../git-path" } | ||
| 34 | git-hash = { version = "^0.9.3", path = "../git-hash" } | 35 | git-hash = { version = "^0.9.3", path = "../git-hash" } |
| 35 | git-quote = { version = "^0.2.0", path = "../git-quote" } | 36 | git-quote = { version = "^0.2.0", path = "../git-quote" } |
| 36 | git-object = { version = "^0.18.0", path = "../git-object" } | 37 | git-object = { version = "^0.18.0", path = "../git-object" } |
| @@ -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)?.0 | 24 | git_quote::ansi_c::undo(line)?.0 |
| 25 | } else { | 25 | } else { |
| 26 | Cow::Borrowed(line) | 26 | Cow::Borrowed(line) |
| @@ -39,6 +39,7 @@ required-features = ["internal-testing-to-avoid-being-run-by-cargo-test-all"] · 38 unmodified lines | |||
| 39 | [dependencies] | 39 | [dependencies] |
| 40 | git-features = { version = "^0.20.0", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] } | 40 | git-features = { version = "^0.20.0", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] } |
| 41 | git-hash = { version = "^0.9.3", path = "../git-hash" } | 41 | git-hash = { version = "^0.9.3", path = "../git-hash" } |
| 42 | git-path = { version = "^0.1.0", path = "../git-path" } | ||
| 42 | git-chunk = { version = "^0.3.0", path = "../git-chunk" } | 43 | git-chunk = { version = "^0.3.0", path = "../git-chunk" } |
| 43 | git-object = { version = "^0.18.0", path = "../git-object" } | 44 | git-object = { version = "^0.18.0", path = "../git-object" } |
| 44 | git-traverse = { version = "^0.14.0", path = "../git-traverse" } | 45 | git-traverse = { version = "^0.14.0", path = "../git-traverse" } |
| @@ -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 | })? |
| @@ -0,0 +1,42 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | All notable changes to this project will be documented in this file. | ||
| 4 | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| 7 | |||
| 8 | ## 0.1.0 (2022-04-28) | ||
| 9 | |||
| 10 | ### Refactor (BREAKING) | ||
| 11 | |||
| 12 | - <csr-id-54801592488416ef2bb0f34c5061b62189c35c5e/> various name changes for more convenient API | ||
| 13 | |||
| 14 | ### Commit Statistics | ||
| 15 | |||
| 16 | <csr-read-only-do-not-edit/> | ||
| 17 | |||
| 18 | - 8 commits contributed to the release over the course of 1 calendar day. | ||
| 19 | - 1 commit where understood as [conventional](https://www.conventionalcommits.org). | ||
| 20 | - 1 unique issue was worked on: [#301](https://github.com/Byron/gitoxide/issues/301) | ||
| 21 | |||
| 22 | ### Commit Details | ||
| 23 | |||
| 24 | <csr-read-only-do-not-edit/> | ||
| 25 | |||
| 26 | <details><summary>view details</summary> | ||
| 27 | |||
| 28 | * **[#301](https://github.com/Byron/gitoxide/issues/301)** | ||
| 29 | - frame for `gix repo exclude query` ([`a331314`](https://github.com/Byron/gitoxide/commit/a331314758629a93ba036245a5dd03cf4109dc52)) | ||
| 30 | - refactor ([`21d4076`](https://github.com/Byron/gitoxide/commit/21d407638285b728d0c64fabf2abe0e1948e9bec)) | ||
| 31 | - The first indication that directory-based excludes work ([`e868acc`](https://github.com/Byron/gitoxide/commit/e868acce2e7c3e2501497bf630e3a54f349ad38e)) | ||
| 32 | - various name changes for more convenient API ([`5480159`](https://github.com/Byron/gitoxide/commit/54801592488416ef2bb0f34c5061b62189c35c5e)) | ||
| 33 | - Use bstr intead of [u8] ([`9380e99`](https://github.com/Byron/gitoxide/commit/9380e9990065897e318b040f49b3c9a6de8bebb1)) | ||
| 34 | - Use `git-path` crate instead of `git_features::path` ([`47e607d`](https://github.com/Byron/gitoxide/commit/47e607dc256a43a3411406c645eb7ff04239dd3a)) | ||
| 35 | - Copy all existing functions from git-features::path to git-path:: ([`725e198`](https://github.com/Byron/gitoxide/commit/725e1985dc521d01ff9e1e89b6468ef62fc09656)) | ||
| 36 | - add empty git-path crate ([`8d13f81`](https://github.com/Byron/gitoxide/commit/8d13f81068b4663d322002a9617d39b307b63469)) | ||
| 37 | </details> | ||
| 38 | |||
| 39 | ## 0.0.0 (2022-03-31) | ||
| 40 | |||
| 41 | An empty crate without any content to reserve the name for the gitoxide project. | ||
| 42 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | [package] | ||
| 2 | name = "git-path" | ||
| 3 | version = "0.1.0" | ||
| 4 | repository = "https://github.com/Byron/gitoxide" | ||
| 5 | license = "MIT/Apache-2.0" | ||
| 6 | description = "A WIP crate of the gitoxide project dealing paths and their conversions" | ||
| 7 | authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] | ||
| 8 | edition = "2018" | ||
| 9 | |||
| 10 | [lib] | ||
| 11 | doctest = false | ||
| 12 | |||
| 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
| 14 | |||
| 15 | [dependencies] | ||
| 16 | bstr = { version = "0.2.17", default-features = false, features = ["std"] } | ||
| @@ -0,0 +1,197 @@ | |||
| 1 | use bstr::{BStr, BString}; | ||
| 2 | use std::{ | ||
| 3 | borrow::Cow, | ||
| 4 | ffi::OsStr, | ||
| 5 | path::{Path, PathBuf}, | ||
| 6 | }; | ||
| 7 | |||
| 8 | #[derive(Debug)] | ||
| 9 | /// The error type returned by [`into_bstr()`] and others may suffer from failed conversions from or to bytes. | ||
| 10 | pub struct Utf8Error; | ||
| 11 | |||
| 12 | impl std::fmt::Display for Utf8Error { | ||
| 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 14 | f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input") | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 18 | impl std::error::Error for Utf8Error {} | ||
| 19 | |||
| 20 | /// Like [`into_bstr()`], but takes `OsStr` as input for a lossless, but fallible, conversion. | ||
| 21 | pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, Utf8Error> { | ||
| 22 | let path = try_into_bstr(Cow::Borrowed(path.as_ref()))?; | ||
| 23 | match path { | ||
| 24 | Cow::Borrowed(path) => Ok(path), | ||
| 25 | Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"), | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | /// Convert the given path either into its raw bytes on unix or its UTF8 encoded counterpart on windows. | ||
| 30 | /// | ||
| 31 | /// On windows, if the source Path contains ill-formed, lone surrogates, the UTF-8 conversion will fail | ||
| 32 | /// causing `Utf8Error` to be returned. | ||
| 33 | pub fn try_into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, BStr>, Utf8Error> { | ||
| 34 | let path = path.into(); | ||
| 35 | let path_str = match path { | ||
| 36 | Cow::Owned(path) => Cow::Owned({ | ||
| 37 | #[cfg(unix)] | ||
| 38 | let p: BString = { | ||
| 39 | use std::os::unix::ffi::OsStringExt; | ||
| 40 | path.into_os_string().into_vec().into() | ||
| 41 | }; | ||
| 42 | #[cfg(not(unix))] | ||
| 43 | let p: BString = path.into_os_string().into_string().map_err(|_| Utf8Error)?.into(); | ||
| 44 | p | ||
| 45 | }), | ||
| 46 | Cow::Borrowed(path) => Cow::Borrowed({ | ||
| 47 | #[cfg(unix)] | ||
| 48 | let p: &BStr = { | ||
| 49 | use std::os::unix::ffi::OsStrExt; | ||
| 50 | path.as_os_str().as_bytes().into() | ||
| 51 | }; | ||
| 52 | #[cfg(not(unix))] | ||
| 53 | let p: &BStr = path.to_str().ok_or(Utf8Error)?.as_bytes().into(); | ||
| 54 | p | ||
| 55 | }), | ||
| 56 | }; | ||
| 57 | Ok(path_str) | ||
| 58 | } | ||
| 59 | |||
| 60 | /// Similar to [`try_into_bstr()`] but **panics** if malformed surrogates are encountered on windows. | ||
| 61 | pub fn into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> { | ||
| 62 | try_into_bstr(path).expect("prefix path doesn't contain ill-formed UTF-8") | ||
| 63 | } | ||
| 64 | |||
| 65 | /// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on unix. | ||
| 66 | /// | ||
| 67 | /// On windows, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. There are some potential | ||
| 68 | /// git versions and windows installation which produce mal-formed UTF-16 if certain emojies are in the path. It's as rare as | ||
| 69 | /// it sounds, but possible. | ||
| 70 | pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> { | ||
| 71 | #[cfg(unix)] | ||
| 72 | let p = { | ||
| 73 | use std::os::unix::ffi::OsStrExt; | ||
| 74 | OsStr::from_bytes(input).as_ref() | ||
| 75 | }; | ||
| 76 | #[cfg(not(unix))] | ||
| 77 | let p = Path::new(std::str::from_utf8(input).map_err(|_| Utf8Error)?); | ||
| 78 | Ok(p) | ||
| 79 | } | ||
| 80 | |||
| 81 | /// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. | ||
| 82 | pub fn try_from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Result<Cow<'a, Path>, Utf8Error> { | ||
| 83 | let input = input.into(); | ||
| 84 | match input { | ||
| 85 | Cow::Borrowed(input) => try_from_byte_slice(input).map(Cow::Borrowed), | ||
| 86 | Cow::Owned(input) => try_from_bstring(input).map(Cow::Owned), | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | /// Similar to [`try_from_bstr()`], but **panics** if malformed surrogates are encountered on windows. | ||
| 91 | pub fn from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Cow<'a, Path> { | ||
| 92 | try_from_bstr(input).expect("prefix path doesn't contain ill-formed UTF-8") | ||
| 93 | } | ||
| 94 | |||
| 95 | /// Similar to [`try_from_bstr()`], but takes and produces owned data. | ||
| 96 | pub fn try_from_bstring(input: impl Into<BString>) -> Result<PathBuf, Utf8Error> { | ||
| 97 | let input = input.into(); | ||
| 98 | #[cfg(unix)] | ||
| 99 | let p = { | ||
| 100 | use std::os::unix::ffi::OsStringExt; | ||
| 101 | std::ffi::OsString::from_vec(input.into()).into() | ||
| 102 | }; | ||
| 103 | #[cfg(not(unix))] | ||
| 104 | let p = { | ||
| 105 | use bstr::ByteVec; | ||
| 106 | PathBuf::from( | ||
| 107 | { | ||
| 108 | let v: Vec<_> = input.into(); | ||
| 109 | v | ||
| 110 | } | ||
| 111 | .into_string() | ||
| 112 | .map_err(|_| Utf8Error)?, | ||
| 113 | ) | ||
| 114 | }; | ||
| 115 | Ok(p) | ||
| 116 | } | ||
| 117 | |||
| 118 | /// Similar to [`try_from_bstring()`], but will panic if there is ill-formed UTF-8 in the `input`. | ||
| 119 | pub fn from_bstring(input: impl Into<BString>) -> PathBuf { | ||
| 120 | try_from_bstring(input).expect("well-formed UTF-8 on windows") | ||
| 121 | } | ||
| 122 | |||
| 123 | /// Similar to [`try_from_byte_slice()`], but will panic if there is ill-formed UTF-8 in the `input`. | ||
| 124 | pub fn from_byte_slice(input: &[u8]) -> &Path { | ||
| 125 | try_from_byte_slice(input).expect("well-formed UTF-8 on windows") | ||
| 126 | } | ||
| 127 | |||
| 128 | fn replace<'a>(path: impl Into<Cow<'a, BStr>>, find: u8, replace: u8) -> Cow<'a, BStr> { | ||
| 129 | let path = path.into(); | ||
| 130 | match path { | ||
| 131 | Cow::Owned(mut path) => { | ||
| 132 | for b in path.iter_mut().filter(|b| **b == find) { | ||
| 133 | *b = replace; | ||
| 134 | } | ||
| 135 | path.into() | ||
| 136 | } | ||
| 137 | Cow::Borrowed(path) => { | ||
| 138 | if !path.contains(&find) { | ||
| 139 | return path.into(); | ||
| 140 | } | ||
| 141 | let mut path = path.to_owned(); | ||
| 142 | for b in path.iter_mut().filter(|b| **b == find) { | ||
| 143 | *b = replace; | ||
| 144 | } | ||
| 145 | path.into() | ||
| 146 | } | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | /// Assures the given bytes use the native path separator. | ||
| 151 | pub fn to_native_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | ||
| 152 | #[cfg(not(windows))] | ||
| 153 | let p = to_unix_separators(path); | ||
| 154 | #[cfg(windows)] | ||
| 155 | let p = to_windows_separators(path); | ||
| 156 | p | ||
| 157 | } | ||
| 158 | |||
| 159 | /// Convert paths with slashes to backslashes on windows and do nothing on unix, but **panics** if malformed surrogates are encountered on windows. | ||
| 160 | pub fn to_native_path_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, std::path::Path> { | ||
| 161 | #[cfg(not(windows))] | ||
| 162 | { | ||
| 163 | crate::from_bstr(path) | ||
| 164 | } | ||
| 165 | #[cfg(windows)] | ||
| 166 | { | ||
| 167 | crate::from_bstr(to_windows_separators(path)) | ||
| 168 | } | ||
| 169 | } | ||
| 170 | |||
| 171 | /// Replaces windows path separators with slashes, but only do so on windows. | ||
| 172 | pub fn to_unix_separators_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | ||
| 173 | #[cfg(windows)] | ||
| 174 | { | ||
| 175 | replace(path, b'\\', b'/') | ||
| 176 | } | ||
| 177 | #[cfg(not(windows))] | ||
| 178 | { | ||
| 179 | path.into() | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | /// Replaces windows path separators with slashes, unconditionally. | ||
| 184 | /// | ||
| 185 | /// **Note** Do not use these and prefer the conditional versions of this method. | ||
| 186 | // TODO: use https://lib.rs/crates/path-slash to handle escapes | ||
| 187 | pub fn to_unix_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | ||
| 188 | replace(path, b'\\', b'/') | ||
| 189 | } | ||
| 190 | |||
| 191 | /// Find backslashes and replace them with slashes, which typically resembles a unix path, unconditionally. | ||
| 192 | /// | ||
| 193 | /// **Note** Do not use these and prefer the conditional versions of this method. | ||
| 194 | // TODO: use https://lib.rs/crates/path-slash to handle escapes | ||
| 195 | pub fn to_windows_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | ||
| 196 | replace(path, b'/', b'\\') | ||
| 197 | } | ||
| @@ -0,0 +1,52 @@ | |||
| 1 | #![forbid(unsafe_code, rust_2018_idioms, missing_docs)] | ||
| 2 | //! ### Research | ||
| 3 | //! | ||
| 4 | //! * **windows** | ||
| 5 | //! - [`dirent.c`](https://github.com/git/git/blob/main/compat/win32/dirent.c#L31:L31) contains all implementation (seemingly) of opening directories and reading their entries, along with all path conversions (UTF-16 for windows). This is done on the fly so git can work with [in UTF-8](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). | ||
| 6 | //! - mingw [is used for the conversion](https://github.com/git/git/blob/main/compat/mingw.h#L579:L579) and it appears they handle surrogates during the conversion, maybe some sort of non-strict UTF-8 converter? Actually it uses [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) | ||
| 7 | //! under the hood which by now does fail if the UTF-8 would be invalid unicode, i.e. unicode pairs. | ||
| 8 | //! - `OsString` on windows already stores strings as WTF-8, which supports [surrogate pairs](https://unicodebook.readthedocs.io/unicode_encodings.html), | ||
| 9 | //! something that UTF-8 isn't allowed do it for security reasons, after all it's UTF-16 specific and exists only to extend | ||
| 10 | //! the encodable code-points. | ||
| 11 | //! - informative reading on [WTF-8](https://simonsapin.github.io/wtf-8/#motivation) which is the encoding used by Rust | ||
| 12 | //! internally that deals with surrogates and non-wellformed surrogates (those that aren't in pairs). | ||
| 13 | //! * **unix** | ||
| 14 | //! - It uses [opendir](https://man7.org/linux/man-pages/man3/opendir.3.html) and [readdir](https://man7.org/linux/man-pages/man3/readdir.3.html) | ||
| 15 | //! respectively. There is no encoding specified, except that these paths are null-terminated. | ||
| 16 | //! | ||
| 17 | //! ### Learnings | ||
| 18 | //! | ||
| 19 | //! Surrogate pairs are a way to extend the encodable value range in UTF-16 encodings, used primarily on windows and in Javascript. | ||
| 20 | //! For a long time these codepoints used for surrogates, always to be used in pairs, were not assigned, until…they were for rare | ||
| 21 | //! emojies and the likes. The unicode standard does not require surrogates to happen in pairs, even though by now unpaired surrogates | ||
| 22 | //! in UTF-16 are considered ill-formed, which aren't supposed to be converted to UTF-8 for example. | ||
| 23 | //! | ||
| 24 | //! This is the reason we have to deal with `to_string_lossy()`, it's _just_ for that quirk. | ||
| 25 | //! | ||
| 26 | //! This also means the only platform ever eligible to see conversion errors is windows, and there it's only older pre-vista | ||
| 27 | //! windows versions which incorrectly allow ill-formed UTF-16 strings. Newer versions don't perform such conversions anymore, for | ||
| 28 | //! example when going from UTF-16 to UTF-8, they will trigger an error. | ||
| 29 | //! | ||
| 30 | //! ### Conclusions | ||
| 31 | //! | ||
| 32 | //! Since [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) by now is | ||
| 33 | //! fixed (Vista onward) to produce valid UTF-8, lone surrogate codepoints will cause failure, which `git` | ||
| 34 | //! [doesn't care about](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). | ||
| 35 | //! | ||
| 36 | //! We will, though, which means from now on we can just convert to UTF-8 on windows and bubble up errors where necessary, | ||
| 37 | //! preventing potential mismatched surrogate pairs to ever be saved on disk by gitoxide. | ||
| 38 | //! | ||
| 39 | //! Even though the error only exists on older windows versions, we will represent it in the type system through fallible function calls. | ||
| 40 | //! Callers may `.expect()` on the result to indicate they don't wish to handle this special and rare case. Note that servers should not | ||
| 41 | //! ever get into a code-path which does panic though. | ||
| 42 | |||
| 43 | /// A dummy type to represent path specs and help finding all spots that take path specs once it is implemented. | ||
| 44 | |||
| 45 | /// A preliminary version of a path-spec based on glances of the code. | ||
| 46 | #[derive(Clone, Debug)] | ||
| 47 | pub struct Spec(bstr::BString); | ||
| 48 | |||
| 49 | mod convert; | ||
| 50 | mod spec; | ||
| 51 | |||
| 52 | pub use convert::*; | ||
| @@ -0,0 +1,51 @@ | |||
| 1 | use crate::Spec; | ||
| 2 | use bstr::{BStr, ByteSlice, ByteVec}; | ||
| 3 | use std::ffi::OsStr; | ||
| 4 | |||
| 5 | impl std::convert::TryFrom<&OsStr> for Spec { | ||
| 6 | type Error = crate::Utf8Error; | ||
| 7 | |||
| 8 | fn try_from(value: &OsStr) -> Result<Self, Self::Error> { | ||
| 9 | crate::os_str_into_bstr(value).map(|value| { | ||
| 10 | assert_valid_hack(value); | ||
| 11 | Spec(value.into()) | ||
| 12 | }) | ||
| 13 | } | ||
| 14 | } | ||
| 15 | |||
| 16 | fn assert_valid_hack(input: &BStr) { | ||
| 17 | assert!(!input.contains_str(b"/../")); | ||
| 18 | assert!(!input.contains_str(b"/./")); | ||
| 19 | assert!(!input.starts_with_str(b"../")); | ||
| 20 | assert!(!input.starts_with_str(b"./")); | ||
| 21 | assert!(!input.starts_with_str(b"/")); | ||
| 22 | } | ||
| 23 | |||
| 24 | impl Spec { | ||
| 25 | /// Parse `input` into a `Spec` or `None` if it could not be parsed | ||
| 26 | // TODO: tests, actual implementation probably via `git-pathspec` to make use of the crate after all. | ||
| 27 | pub fn from_bytes(input: &BStr) -> Option<Self> { | ||
| 28 | assert_valid_hack(input); | ||
| 29 | Spec(input.into()).into() | ||
| 30 | } | ||
| 31 | /// Return all paths described by this path spec, using slashes on all platforms. | ||
| 32 | pub fn items(&self) -> impl Iterator<Item = &BStr> { | ||
| 33 | std::iter::once(self.0.as_bstr()) | ||
| 34 | } | ||
| 35 | /// Adjust this path specification according to the given `prefix`, which may be empty to indicate we are the at work-tree root. | ||
| 36 | // TODO: this is a hack, needs test and time to do according to spec. This is just a minimum version to have -something-. | ||
| 37 | pub fn apply_prefix(&mut self, prefix: &std::path::Path) -> &Self { | ||
| 38 | // many more things we can't handle. `Path` never ends with trailing path separator. | ||
| 39 | let prefix = crate::into_bstr(prefix); | ||
| 40 | if !prefix.is_empty() { | ||
| 41 | let mut prefix = crate::to_unix_separators_on_windows(prefix); | ||
| 42 | { | ||
| 43 | let path = prefix.to_mut(); | ||
| 44 | path.push_byte(b'/'); | ||
| 45 | path.extend_from_slice(&self.0); | ||
| 46 | } | ||
| 47 | self.0 = prefix.into_owned(); | ||
| 48 | } | ||
| 49 | self | ||
| 50 | } | ||
| 51 | } | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | mod convert { | ||
| 2 | use bstr::ByteSlice; | ||
| 3 | use git_path::{to_unix_separators, to_windows_separators}; | ||
| 4 | |||
| 5 | #[test] | ||
| 6 | fn assure_unix_separators() { | ||
| 7 | assert_eq!(to_unix_separators(b"no-backslash".as_bstr()).as_bstr(), "no-backslash"); | ||
| 8 | |||
| 9 | assert_eq!(to_unix_separators(b"\\a\\b\\\\".as_bstr()).as_bstr(), "/a/b//"); | ||
| 10 | } | ||
| 11 | |||
| 12 | #[test] | ||
| 13 | fn assure_windows_separators() { | ||
| 14 | assert_eq!( | ||
| 15 | to_windows_separators(b"no-backslash".as_bstr()).as_bstr(), | ||
| 16 | "no-backslash" | ||
| 17 | ); | ||
| 18 | |||
| 19 | assert_eq!(to_windows_separators(b"/a/b//".as_bstr()).as_bstr(), "\\a\\b\\\\"); | ||
| 20 | } | ||
| 21 | } | ||
| @@ -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.html | 25 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
| 26 | 26 | ||
| 27 | [dependencies] | 27 | [dependencies] |
| 28 | git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir", "bstr"]} | 28 | git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir"]} |
| 29 | git-path = { version = "^0.1.0", path = "../git-path" } | ||
| 29 | git-hash = { version = "^0.9.3", path = "../git-hash" } | 30 | git-hash = { version = "^0.9.3", path = "../git-hash" } |
| 30 | git-object = { version = "^0.18.0", path = "../git-object" } | 31 | git-object = { version = "^0.18.0", path = "../git-object" } |
| 31 | git-validate = { version ="^0.5.3", path = "../git-validate" } | 32 | git-validate = { version ="^0.5.3", path = "../git-validate" } |
| @@ -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 repository | 76 | /// 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. |
| @@ -26,7 +26,7 @@ impl Category { · 25 unmodified lines | |||
| 26 | impl<'a> FullNameRef<'a> { | 26 | impl<'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 refname | 32 | /// 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(), |
| @@ -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.0 | 25 | 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 | ||
| @@ -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 self | 128 | 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 { |
| @@ -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_path | 61 | 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, |
| @@ -53,12 +53,8 @@ pub struct Transaction<'s> { · 52 unmodified lines | |||
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | pub(in crate::store_impl::file) fn path_to_name<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> { | 55 | pub(in crate::store_impl::file) fn path_to_name<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> { |
| 56 | let path = git_features::path::into_bytes_or_panic_on_windows(path.into()); | 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 | /// |
| @@ -41,7 +41,7 @@ one-stop-shop = [ "local", "local-time-support" ] · 40 unmodified lines | |||
| 41 | #! ### Other | 41 | #! ### Other |
| 42 | 42 | ||
| 43 | ## Data structures implement `serde::Serialize` and `serde::Deserialize`. | 43 | ## Data structures implement `serde::Serialize` and `serde::Deserialize`. |
| 44 | serde1 = ["git-pack/serde1", "git-object/serde1", "git-protocol/serde1", "git-transport/serde1", "git-ref/serde1", "git-odb/serde1", "git-index/serde1", "git-mailmap/serde1"] | 44 | serde1 = ["git-pack/serde1", "git-object/serde1", "git-protocol/serde1", "git-transport/serde1", "git-ref/serde1", "git-odb/serde1", "git-index/serde1", "git-mailmap/serde1", "git-attributes/serde1"] |
| 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** that | 46 | ## **Note** that |
| 47 | max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git-pack/pack-cache-lru-static", "git-pack/pack-cache-lru-dynamic"] | 47 | max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git-pack/pack-cache-lru-static", "git-pack/pack-cache-lru-dynamic"] |
| @@ -49,7 +49,7 @@ max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git- · 1 unmodified line | |||
| 49 | local-time-support = ["git-actor/local-time-support"] | 49 | local-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. |
| 52 | unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials"] | 52 | unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials", "git-path", "git-attributes"] |
| 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. |
| 54 | cache-efficiency-debug = ["git-features/cache-efficiency-debug"] | 54 | cache-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 | |||
| 60 | git-tempfile = { version = "^2.0.0", path = "../git-tempfile" } | 60 | git-tempfile = { version = "^2.0.0", path = "../git-tempfile" } |
| 61 | git-lock = { version = "^2.0.0", path = "../git-lock" } | 61 | git-lock = { version = "^2.0.0", path = "../git-lock" } |
| 62 | git-validate = { version ="^0.5.3", path = "../git-validate" } | 62 | git-validate = { version ="^0.5.3", path = "../git-validate" } |
| 63 | git-sec = { version = "^0.1.0", path = "../git-sec" } | 63 | git-sec = { version = "^0.1.0", path = "../git-sec", features = ["thiserror"] } |
| 64 | 64 | ||
| 65 | git-config = { version = "^0.2.1", path = "../git-config" } | 65 | git-config = { version = "^0.2.1", path = "../git-config" } |
| 66 | git-odb = { version = "^0.28.0", path = "../git-odb" } | 66 | git-odb = { version = "^0.28.0", path = "../git-odb" } |
| @@ -70,6 +70,7 @@ git-actor = { version = "^0.9.0", path = "../git-actor" } · 3 unmodified lines | |||
| 70 | git-pack = { version = "^0.18.0", path = "../git-pack", features = ["object-cache-dynamic"] } | 70 | git-pack = { version = "^0.18.0", path = "../git-pack", features = ["object-cache-dynamic"] } |
| 71 | git-revision = { version = "^0.1.0", path = "../git-revision" } | 71 | git-revision = { version = "^0.1.0", path = "../git-revision" } |
| 72 | 72 | ||
| 73 | git-path = { version = "^0.1.0", path = "../git-path", optional = true } | ||
| 73 | git-url = { version = "^0.4.0", path = "../git-url", optional = true } | 74 | git-url = { version = "^0.4.0", path = "../git-url", optional = true } |
| 74 | git-traverse = { version = "^0.14.0", path = "../git-traverse" } | 75 | git-traverse = { version = "^0.14.0", path = "../git-traverse" } |
| 75 | git-protocol = { version = "^0.15.0", path = "../git-protocol", optional = true } | 76 | git-protocol = { version = "^0.15.0", path = "../git-protocol", optional = true } |
| @@ -79,6 +80,7 @@ git-mailmap = { version = "^0.1.0", path = "../git-mailmap", optional = true } · 3 unmodified lines | |||
| 79 | git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] } | 80 | git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] } |
| 80 | 81 | ||
| 81 | # unstable only | 82 | # unstable only |
| 83 | git-attributes = { version = "^0.1.0", path = "../git-attributes", optional = true } | ||
| 82 | git-glob = { version = "^0.2.0", path = "../git-glob", optional = true } | 84 | git-glob = { version = "^0.2.0", path = "../git-glob", optional = true } |
| 83 | git-credentials = { version = "^0.1.0", path = "../git-credentials", optional = true } | 85 | git-credentials = { version = "^0.1.0", path = "../git-credentials", optional = true } |
| 84 | git-index = { version = "^0.2.0", path = "../git-index", optional = true } | 86 | git-index = { version = "^0.2.0", path = "../git-index", optional = true } |
| @@ -98,6 +100,7 @@ unicode-normalization = { version = "0.1.19", default-features = false } · 13 unmodified lines | |||
| 98 | 100 | ||
| 99 | [dev-dependencies] | 101 | [dev-dependencies] |
| 100 | git-testtools = { path = "../tests/tools" } | 102 | git-testtools = { path = "../tests/tools" } |
| 103 | is_ci = "1.1.1" | ||
| 101 | anyhow = "1" | 104 | anyhow = "1" |
| 102 | tempfile = "3.2.0" | 105 | tempfile = "3.2.0" |
| 103 | 106 | ||
| @@ -1,4 +1,5 @@ | |||
| 1 | use crate::bstr::BString; | 1 | use crate::bstr::BString; |
| 2 | use crate::permission::EnvVarResourcePermission; | ||
| 2 | 3 | ||
| 3 | #[derive(Debug, thiserror::Error)] | 4 | #[derive(Debug, thiserror::Error)] |
| 4 | pub enum Error { | 5 | pub 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)] |
| 17 | pub(crate) struct Cache { | 22 | pub(crate) struct Cache { |
| 18 | // TODO: remove this once resolved is used without a feature dependency | 23 | // 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 tree | 28 | /// 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 use | 30 | /// 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 | ||
| 32 | mod cache { | 49 | mod 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 = config | 78 | 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::Sha1 | 95 | .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 | config | 160 | 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 | } |
| @@ -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 their | 126 | // 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. |
| 126 | pub use git_actor as actor; | 128 | pub use git_actor as actor; |
| 129 | #[cfg(all(feature = "unstable", feature = "git-attributes"))] | ||
| 130 | pub use git_attributes as attrs; | ||
| 127 | #[cfg(all(feature = "unstable", feature = "git-credentials"))] | 131 | #[cfg(all(feature = "unstable", feature = "git-credentials"))] |
| 128 | pub use git_credentials as credentials; | 132 | pub 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"))] |
| 158 | pub use git_url::Url; | 162 | pub use git_url::Url; |
| 159 | #[cfg(all(feature = "unstable", feature = "git-worktree"))] | ||
| 160 | pub use git_worktree as worktree; | ||
| 161 | pub use hash::{oid, ObjectId}; | 163 | pub use hash::{oid, ObjectId}; |
| 162 | 164 | ||
| 163 | pub mod interrupt; | 165 | pub mod interrupt; |
| @@ -192,7 +194,9 @@ pub enum Path { · 28 unmodified lines | |||
| 192 | 194 | ||
| 193 | /// | 195 | /// |
| 194 | mod types; | 196 | mod types; |
| 195 | pub use types::{Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree}; | 197 | pub use types::{ |
| 198 | Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree, Worktree, | ||
| 199 | }; | ||
| 196 | 200 | ||
| 197 | pub mod commit; | 201 | pub mod commit; |
| 198 | pub mod head; | 202 | pub mod head; |
| @@ -200,7 +204,6 @@ pub mod id; · 1 unmodified line | |||
| 200 | pub mod object; | 204 | pub mod object; |
| 201 | pub mod reference; | 205 | pub mod reference; |
| 202 | mod repository; | 206 | mod repository; |
| 203 | pub use repository::{permissions, permissions::Permissions}; | ||
| 204 | pub mod tag; | 207 | pub 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 | /// | ||
| 246 | pub mod permission { | ||
| 247 | use git_sec::permission::Resource; | ||
| 248 | use git_sec::Access; | ||
| 249 | |||
| 250 | /// A permission to control access to the resource pointed to an environment variable. | ||
| 251 | pub type EnvVarResourcePermission = Access<Resource, git_sec::Permission>; | ||
| 252 | } | ||
| 253 | pub use repository::permissions::Permissions; | ||
| 254 | |||
| 242 | /// | 255 | /// |
| 243 | pub mod open; | 256 | pub mod open; |
| 244 | 257 | ||
| @@ -268,6 +281,9 @@ pub mod mailmap { · 23 unmodified lines | |||
| 268 | } | 281 | } |
| 269 | } | 282 | } |
| 270 | 283 | ||
| 284 | /// | ||
| 285 | pub mod worktree; | ||
| 286 | |||
| 271 | /// | 287 | /// |
| 272 | pub mod rev_parse { | 288 | pub mod rev_parse { |
| 273 | /// The error returned by [`crate::Repository::rev_parse()`]. | 289 | /// The error returned by [`crate::Repository::rev_parse()`]. |
| @@ -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 means | 169 | // 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 instance | 170 | // 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()); |
| @@ -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 last | 63 | // us the parent directory. (`Path::parent` just strips off the last |
| 58 | // path component, which means it will not do what you expect when | 64 | // 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 | } |
| @@ -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 kind | 28 | /// 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 head | 32 | /// * [x] a valid head |
| 32 | /// * [ ] git common directory | 33 | /// * [ ] git common directory |
| 33 | /// * [ ] respect GIT_COMMON_DIR | 34 | /// * [ ] respect GIT_COMMON_DIR |
| @@ -2,6 +2,9 @@ use std::path::PathBuf; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | use crate::{Kind, Path}; | 3 | use crate::{Kind, Path}; |
| 4 | 4 | ||
| 5 | #[cfg(all(feature = "unstable", feature = "git-path"))] | ||
| 6 | pub use git_path::*; | ||
| 7 | |||
| 5 | /// | 8 | /// |
| 6 | pub mod create; | 9 | pub 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 | |||
| 55 | pub(crate) fn install_dir() -> std::io::Result<std::path::PathBuf> { | ||
| 56 | std::env::current_exe().and_then(|exe| { | ||
| 57 | exe.parent() | ||
| 58 | .map(ToOwned::to_owned) | ||
| 59 | .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no parent for current executable")) | ||
| 60 | }) | ||
| 61 | } | ||
| @@ -0,0 +1,32 @@ | |||
| 1 | use std::cell::RefCell; | ||
| 2 | |||
| 3 | impl crate::Repository { | ||
| 4 | pub(crate) fn from_refs_and_objects( | ||
| 5 | refs: crate::RefStore, | ||
| 6 | objects: crate::OdbHandle, | ||
| 7 | work_tree: Option<std::path::PathBuf>, | ||
| 8 | config: crate::config::Cache, | ||
| 9 | ) -> Self { | ||
| 10 | crate::Repository { | ||
| 11 | bufs: RefCell::new(Vec::with_capacity(4)), | ||
| 12 | work_tree, | ||
| 13 | objects: { | ||
| 14 | #[cfg(feature = "max-performance")] | ||
| 15 | { | ||
| 16 | objects.with_pack_cache(|| Box::new(git_pack::cache::lru::StaticLinkedList::<64>::default())) | ||
| 17 | } | ||
| 18 | #[cfg(not(feature = "max-performance"))] | ||
| 19 | { | ||
| 20 | objects | ||
| 21 | } | ||
| 22 | }, | ||
| 23 | refs, | ||
| 24 | config, | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data. | ||
| 29 | pub fn into_sync(self) -> crate::ThreadSafeRepository { | ||
| 30 | self.into() | ||
| 31 | } | ||
| 32 | } | ||
| @@ -12,10 +12,31 @@ impl crate::Repository { · 11 unmodified lines | |||
| 12 | // TODO: tests, respect precomposeUnicode | 12 | // 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 | ||
| @@ -39,98 +39,12 @@ impl crate::Repository { · 38 unmodified lines | |||
| 39 | } | 39 | } |
| 40 | } | 40 | } |
| 41 | 41 | ||
| 42 | /// Various permissions for parts of git repositories. | 42 | mod worktree; |
| 43 | pub mod permissions { | ||
| 44 | use git_sec::permission::Resource; | ||
| 45 | use git_sec::{Access, Trust}; | ||
| 46 | |||
| 47 | /// Permissions associated with various resources of a git repository | ||
| 48 | pub struct Permissions { | ||
| 49 | /// Control how a git-dir can be used. | ||
| 50 | /// | ||
| 51 | /// Note that a repository won't be usable at all unless read and write permissions are given. | ||
| 52 | pub git_dir: Access<Resource, git_sec::ReadWrite>, | ||
| 53 | } | ||
| 54 | |||
| 55 | impl Permissions { | ||
| 56 | /// Return permissions similar to what git does when the repository isn't owned by the current user, | ||
| 57 | /// thus refusing all operations in it. | ||
| 58 | pub fn strict() -> Self { | ||
| 59 | Permissions { | ||
| 60 | git_dir: Access::resource(git_sec::ReadWrite::empty()), | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | /// Return permissions that will not include configuration files not owned by the current user, | ||
| 65 | /// but trust system and global configuration files along with those which are owned by the current user. | ||
| 66 | /// | ||
| 67 | /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using | ||
| 68 | /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`. | ||
| 69 | pub fn secure() -> Self { | ||
| 70 | Permissions { | ||
| 71 | git_dir: Access::resource(git_sec::ReadWrite::all()), | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically | ||
| 76 | /// does with owned repositories. | ||
| 77 | pub fn all() -> Self { | ||
| 78 | Permissions { | ||
| 79 | git_dir: Access::resource(git_sec::ReadWrite::all()), | ||
| 80 | } | ||
| 81 | } | ||
| 82 | } | ||
| 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 { | 45 | pub(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 | |||
| 100 | mod init { | ||
| 101 | use std::cell::RefCell; | ||
| 102 | |||
| 103 | impl crate::Repository { | ||
| 104 | pub(crate) fn from_refs_and_objects( | ||
| 105 | refs: crate::RefStore, | ||
| 106 | objects: crate::OdbHandle, | ||
| 107 | work_tree: Option<std::path::PathBuf>, | ||
| 108 | config: crate::config::Cache, | ||
| 109 | ) -> Self { | ||
| 110 | crate::Repository { | ||
| 111 | bufs: RefCell::new(Vec::with_capacity(4)), | ||
| 112 | work_tree, | ||
| 113 | objects: { | ||
| 114 | #[cfg(feature = "max-performance")] | ||
| 115 | { | ||
| 116 | objects.with_pack_cache(|| Box::new(git_pack::cache::lru::StaticLinkedList::<64>::default())) | ||
| 117 | } | ||
| 118 | #[cfg(not(feature = "max-performance"))] | ||
| 119 | { | ||
| 120 | objects | ||
| 121 | } | ||
| 122 | }, | ||
| 123 | refs, | ||
| 124 | config, | ||
| 125 | } | ||
| 126 | } | ||
| 127 | 46 | ||
| 128 | /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data. | 47 | mod init; |
| 129 | pub fn into_sync(self) -> crate::ThreadSafeRepository { | ||
| 130 | self.into() | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | 48 | ||
| 135 | mod location; | 49 | mod location; |
| 136 | 50 | ||
| @@ -0,0 +1,67 @@ | |||
| 1 | use crate::permission::EnvVarResourcePermission; | ||
| 2 | use git_sec::permission::Resource; | ||
| 3 | use git_sec::{Access, Trust}; | ||
| 4 | |||
| 5 | /// Permissions associated with various resources of a git repository | ||
| 6 | pub struct Permissions { | ||
| 7 | /// Control how a git-dir can be used. | ||
| 8 | /// | ||
| 9 | /// Note that a repository won't be usable at all unless read and write permissions are given. | ||
| 10 | pub git_dir: Access<Resource, git_sec::ReadWrite>, | ||
| 11 | /// Control whether resources pointed to by `XDG_CONFIG_HOME` can be used when looking up common configuration values. | ||
| 12 | /// | ||
| 13 | /// Note that [`git_sec::Permission::Forbid`] will cause the operation to abort if a resource is set via the XDG config environment. | ||
| 14 | pub xdg_config_home: EnvVarResourcePermission, | ||
| 15 | /// Control if resources pointed to by the | ||
| 16 | pub home: EnvVarResourcePermission, | ||
| 17 | } | ||
| 18 | |||
| 19 | impl Permissions { | ||
| 20 | /// Return permissions similar to what git does when the repository isn't owned by the current user, | ||
| 21 | /// thus refusing all operations in it. | ||
| 22 | pub fn strict() -> Self { | ||
| 23 | Permissions { | ||
| 24 | git_dir: Access::resource(git_sec::ReadWrite::empty()), | ||
| 25 | xdg_config_home: Access::resource(git_sec::Permission::Allow), | ||
| 26 | home: Access::resource(git_sec::Permission::Allow), | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | /// Return permissions that will not include configuration files not owned by the current user, | ||
| 31 | /// but trust system and global configuration files along with those which are owned by the current user. | ||
| 32 | /// | ||
| 33 | /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using | ||
| 34 | /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`. | ||
| 35 | pub fn secure() -> Self { | ||
| 36 | Permissions { | ||
| 37 | git_dir: Access::resource(git_sec::ReadWrite::all()), | ||
| 38 | xdg_config_home: Access::resource(git_sec::Permission::Allow), | ||
| 39 | home: Access::resource(git_sec::Permission::Allow), | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically | ||
| 44 | /// does with owned repositories. | ||
| 45 | pub fn all() -> Self { | ||
| 46 | Permissions { | ||
| 47 | git_dir: Access::resource(git_sec::ReadWrite::all()), | ||
| 48 | xdg_config_home: Access::resource(git_sec::Permission::Allow), | ||
| 49 | home: Access::resource(git_sec::Permission::Allow), | ||
| 50 | } | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | impl git_sec::trust::DefaultForLevel for Permissions { | ||
| 55 | fn default_for_level(level: Trust) -> Self { | ||
| 56 | match level { | ||
| 57 | Trust::Full => Permissions::all(), | ||
| 58 | Trust::Reduced => Permissions::secure(), | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | impl Default for Permissions { | ||
| 64 | fn default() -> Self { | ||
| 65 | Permissions::secure() | ||
| 66 | } | ||
| 67 | } | ||
| @@ -48,7 +48,7 @@ impl crate::Repository { · 47 unmodified lines | |||
| 48 | let mut blob_id = self | 48 | let mut blob_id = self |
| 49 | .config | 49 | .config |
| 50 | .resolved | 50 | .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) |
| @@ -0,0 +1,21 @@ | |||
| 1 | use crate::{worktree, Worktree}; | ||
| 2 | |||
| 3 | impl crate::Repository { | ||
| 4 | /// Return a platform for interacting with worktrees | ||
| 5 | pub fn worktree(&self) -> worktree::Platform<'_> { | ||
| 6 | worktree::Platform { parent: self } | ||
| 7 | } | ||
| 8 | } | ||
| 9 | |||
| 10 | impl<'repo> worktree::Platform<'repo> { | ||
| 11 | /// Return the currently set worktree if there is one. | ||
| 12 | /// | ||
| 13 | /// Note that there would be `None` if this repository is `bare` and the parent [`Repository`][crate::Repository] was instantiated without | ||
| 14 | /// registered worktree in the current working dir. | ||
| 15 | pub fn current(&self) -> Option<Worktree<'repo>> { | ||
| 16 | self.parent.work_dir().map(|path| Worktree { | ||
| 17 | parent: self.parent, | ||
| 18 | path, | ||
| 19 | }) | ||
| 20 | } | ||
| 21 | } | ||
| @@ -4,13 +4,22 @@ use git_hash::ObjectId; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | use crate::head; | 5 | use crate::head; |
| 6 | 6 | ||
| 7 | /// A worktree checkout containing the files of the repository in consumable form. | ||
| 8 | pub struct Worktree<'repo> { | ||
| 9 | #[cfg_attr(not(feature = "git-index"), allow(dead_code))] | ||
| 10 | pub(crate) parent: &'repo Repository, | ||
| 11 | /// The root path of the checkout. | ||
| 12 | #[allow(dead_code)] | ||
| 13 | pub(crate) path: &'repo std::path::Path, | ||
| 14 | } | ||
| 15 | |||
| 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. |
| 10 | pub struct Head<'repo> { | 19 | pub struct Head<'repo> { |
| 11 | /// One of various possible states for the HEAD reference | 20 | /// 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 | |||
| 18 | pub struct Id<'r> { | 27 | pub struct Id<'r> { |
| 19 | /// The actual object id | 28 | /// 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 data | 39 | /// 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 | ||
| 35 | impl<'a> Drop for Object<'a> { | 44 | impl<'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 data | 54 | /// 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 | ||
| 50 | impl<'a> Drop for Tree<'a> { | 59 | impl<'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 data | 69 | /// 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 | ||
| 65 | impl<'a> Drop for Tag<'a> { | 74 | impl<'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 data | 84 | /// 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 | ||
| 80 | impl<'a> Drop for Commit<'a> { | 89 | impl<'a> Drop for Commit<'a> { |
| @@ -102,7 +111,7 @@ pub struct DetachedObject { · 21 unmodified lines | |||
| 102 | pub struct Reference<'r> { | 111 | pub struct Reference<'r> { |
| 103 | /// The actual reference data | 112 | /// 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. |
| @@ -0,0 +1,129 @@ | |||
| 1 | use crate::Repository; | ||
| 2 | #[cfg(all(feature = "unstable", feature = "git-worktree"))] | ||
| 3 | pub use git_worktree::*; | ||
| 4 | |||
| 5 | /// | ||
| 6 | #[cfg(feature = "git-index")] | ||
| 7 | pub mod open_index { | ||
| 8 | use crate::bstr::BString; | ||
| 9 | |||
| 10 | /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. | ||
| 11 | #[derive(Debug, thiserror::Error)] | ||
| 12 | #[allow(missing_docs)] | ||
| 13 | pub enum Error { | ||
| 14 | #[error("Could not interpret value '{}' as 'index.threads'", .value)] | ||
| 15 | ConfigIndexThreads { | ||
| 16 | value: BString, | ||
| 17 | #[source] | ||
| 18 | err: git_config::value::parse::Error, | ||
| 19 | }, | ||
| 20 | #[error(transparent)] | ||
| 21 | IndexFile(#[from] git_index::file::init::Error), | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | /// | ||
| 26 | #[cfg(feature = "git-index")] | ||
| 27 | pub mod excludes { | ||
| 28 | use std::path::PathBuf; | ||
| 29 | |||
| 30 | /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. | ||
| 31 | #[derive(Debug, thiserror::Error)] | ||
| 32 | #[allow(missing_docs)] | ||
| 33 | pub enum Error { | ||
| 34 | #[error("Could not read repository exclude.")] | ||
| 35 | Io(#[from] std::io::Error), | ||
| 36 | #[error(transparent)] | ||
| 37 | EnvironmentPermission(#[from] git_sec::permission::Error<PathBuf, git_sec::Permission>), | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | /// A structure to make the API more stuctured. | ||
| 42 | pub struct Platform<'repo> { | ||
| 43 | pub(crate) parent: &'repo Repository, | ||
| 44 | } | ||
| 45 | |||
| 46 | /// Access | ||
| 47 | impl<'repo> crate::Worktree<'repo> { | ||
| 48 | /// Returns the root of the worktree under which all checked out files are located. | ||
| 49 | pub fn root(&self) -> &std::path::Path { | ||
| 50 | self.path | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | impl<'repo> crate::Worktree<'repo> { | ||
| 55 | /// Configure a file-system cache checking if files below the repository are excluded. | ||
| 56 | /// | ||
| 57 | /// This takes into consideration all the usual repository configuration. | ||
| 58 | // TODO: test | ||
| 59 | #[cfg(feature = "git-index")] | ||
| 60 | pub fn excludes<'a>( | ||
| 61 | &self, | ||
| 62 | index: &'a git_index::State, | ||
| 63 | overrides: Option<git_attributes::MatchGroup<git_attributes::Ignore>>, | ||
| 64 | ) -> Result<git_worktree::fs::Cache<'a>, excludes::Error> { | ||
| 65 | let repo = self.parent; | ||
| 66 | let case = repo | ||
| 67 | .config | ||
| 68 | .ignore_case | ||
| 69 | .then(|| git_glob::pattern::Case::Fold) | ||
| 70 | .unwrap_or_default(); | ||
| 71 | let mut buf = Vec::with_capacity(512); | ||
| 72 | let state = git_worktree::fs::cache::State::IgnoreStack(git_worktree::fs::cache::state::Ignore::new( | ||
| 73 | overrides.unwrap_or_default(), | ||
| 74 | git_attributes::MatchGroup::<git_attributes::Ignore>::from_git_dir( | ||
| 75 | repo.git_dir(), | ||
| 76 | match repo.config.excludes_file.as_ref() { | ||
| 77 | Some(user_path) => Some(user_path.to_owned()), | ||
| 78 | None => repo.config.xdg_config_path("ignore")?, | ||
| 79 | }, | ||
| 80 | &mut buf, | ||
| 81 | )?, | ||
| 82 | None, | ||
| 83 | case, | ||
| 84 | )); | ||
| 85 | let attribute_list = state.build_attribute_list(index, index.path_backing(), case); | ||
| 86 | Ok(git_worktree::fs::Cache::new( | ||
| 87 | self.path, | ||
| 88 | state, | ||
| 89 | case, | ||
| 90 | buf, | ||
| 91 | attribute_list, | ||
| 92 | )) | ||
| 93 | } | ||
| 94 | |||
| 95 | // pub fn | ||
| 96 | /// Open a new copy of the index file and decode it entirely. | ||
| 97 | /// | ||
| 98 | /// It will use the `index.threads` configuration key to learn how many threads to use. | ||
| 99 | // TODO: test | ||
| 100 | #[cfg(feature = "git-index")] | ||
| 101 | pub fn open_index(&self) -> Result<git_index::File, crate::worktree::open_index::Error> { | ||
| 102 | use std::convert::{TryFrom, TryInto}; | ||
| 103 | let repo = self.parent; | ||
| 104 | let thread_limit = repo | ||
| 105 | .config | ||
| 106 | .resolved | ||
| 107 | .boolean("index", None, "threads") | ||
| 108 | .map(|res| { | ||
| 109 | res.map(|value| if value { 0usize } else { 1 }).or_else(|err| { | ||
| 110 | git_config::values::Integer::try_from(err.input.as_ref()) | ||
| 111 | .map_err(|err| crate::worktree::open_index::Error::ConfigIndexThreads { | ||
| 112 | value: err.input.clone(), | ||
| 113 | err, | ||
| 114 | }) | ||
| 115 | .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1)) | ||
| 116 | }) | ||
| 117 | }) | ||
| 118 | .transpose()?; | ||
| 119 | git_index::File::at( | ||
| 120 | repo.git_dir().join("index"), | ||
| 121 | git_index::decode::Options { | ||
| 122 | object_hash: repo.object_hash(), | ||
| 123 | thread_limit, | ||
| 124 | min_extension_block_in_bytes_for_threading: 0, | ||
| 125 | }, | ||
| 126 | ) | ||
| 127 | .map_err(Into::into) | ||
| 128 | } | ||
| 129 | } | ||
| @@ -1,5 +1,5 @@ | |||
| 1 | mod existing { | 1 | mod 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 before | 71 | // 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()?, |
| @@ -19,6 +19,7 @@ serde1 = [ "serde" ] · 18 unmodified lines | |||
| 19 | [dependencies] | 19 | [dependencies] |
| 20 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } | 20 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } |
| 21 | bitflags = "1.3.2" | 21 | bitflags = "1.3.2" |
| 22 | thiserror = { version = "1.0.26", optional = true } | ||
| 22 | 23 | ||
| 23 | [target.'cfg(not(windows))'.dependencies] | 24 | [target.'cfg(not(windows))'.dependencies] |
| 24 | libc = "0.2.123" | 25 | libc = "0.2.123" |
| @@ -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 | ||
| 4 | use std::fmt::{Debug, Display, Formatter}; | ||
| 4 | use std::marker::PhantomData; | 5 | use std::marker::PhantomData; |
| 5 | use std::ops::Deref; | 6 | use std::ops::Deref; |
| 6 | 7 | ||
| @@ -75,19 +76,22 @@ pub mod trust { · 68 unmodified lines | |||
| 75 | /// | 76 | /// |
| 76 | pub mod permission { | 77 | pub 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 directory | 109 | /// 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 | ||
| 146 | impl Permission { | ||
| 147 | /// Check this permissions and produce a reply to indicate if the `resource` can be used and in which way. | ||
| 148 | /// | ||
| 149 | /// Only if this permission is set to `Allow` will the resource be usable. | ||
| 150 | #[cfg(feature = "thiserror")] | ||
| 151 | pub fn check<R: Debug>(&self, resource: R) -> Result<Option<R>, permission::Error<R, Self>> { | ||
| 152 | match self { | ||
| 153 | Permission::Allow => Ok(Some(resource)), | ||
| 154 | Permission::Deny => Ok(None), | ||
| 155 | Permission::Forbid => Err(permission::Error { | ||
| 156 | resource, | ||
| 157 | permission: *self, | ||
| 158 | }), | ||
| 159 | } | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | impl Display for Permission { | ||
| 164 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| 165 | Display::fmt( | ||
| 166 | match self { | ||
| 167 | Permission::Allow => "allowed", | ||
| 168 | Permission::Deny => "denied", | ||
| 169 | Permission::Forbid => "forbidden", | ||
| 170 | }, | ||
| 171 | f, | ||
| 172 | ) | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 128 | bitflags::bitflags! { | 176 | bitflags::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 | ||
| 187 | impl 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. |
| 140 | pub struct Access<T: permission::Tag, P> { | 194 | #[derive(Debug, Clone)] |
| 195 | pub 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 | ||
| 146 | impl<T: permission::Tag, P> Deref for Access<T, P> { | 201 | impl<T: permission::Tag, P: Debug + Display + Clone> Display for Access<T, P> { |
| 202 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| 203 | Display::fmt(&self.permission, f) | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | impl<T: permission::Tag, P: Debug + Display + Clone> Deref for Access<T, P> { | ||
| 147 | type Target = P; | 208 | type Target = P; |
| 148 | 209 | ||
| 149 | fn deref(&self) -> &Self::Target { | 210 | fn deref(&self) -> &Self::Target { |
| @@ -20,6 +20,7 @@ serde1 = ["serde", "bstr/serde1"] · 19 unmodified lines | |||
| 20 | [dependencies] | 20 | [dependencies] |
| 21 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} | 21 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} |
| 22 | git-features = { version = "^0.20.0", path = "../git-features" } | 22 | git-features = { version = "^0.20.0", path = "../git-features" } |
| 23 | git-path = { version = "^0.1.0", path = "../git-path" } | ||
| 23 | quick-error = "2.0.0" | 24 | quick-error = "2.0.0" |
| 24 | url = "2.1.1" | 25 | url = "2.1.1" |
| 25 | bstr = { version = "0.2.13", default-features = false, features = ["std"] } | 26 | bstr = { version = "0.2.13", default-features = false, features = ["std"] } |
| @@ -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()))? |
| @@ -33,6 +33,9 @@ internal-testing-to-avoid-being-run-by-cargo-test-all = [] · 32 unmodified lines | |||
| 33 | git-index = { version = "^0.2.0", path = "../git-index" } | 33 | git-index = { version = "^0.2.0", path = "../git-index" } |
| 34 | git-hash = { version = "^0.9.3", path = "../git-hash" } | 34 | git-hash = { version = "^0.9.3", path = "../git-hash" } |
| 35 | git-object = { version = "^0.18.0", path = "../git-object" } | 35 | git-object = { version = "^0.18.0", path = "../git-object" } |
| 36 | git-glob = { version = "^0.2.0", path = "../git-glob" } | ||
| 37 | git-path = { version = "^0.1.0", path = "../git-path" } | ||
| 38 | git-attributes = { version = "^0.1.0", path = "../git-attributes" } | ||
| 36 | git-features = { version = "^0.20.0", path = "../git-features" } | 39 | git-features = { version = "^0.20.0", path = "../git-features" } |
| 37 | 40 | ||
| 38 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} | 41 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} |
| @@ -1,185 +0,0 @@ | |||
| 1 | use super::Cache; | ||
| 2 | use crate::fs::Stack; | ||
| 3 | use crate::{fs, os}; | ||
| 4 | use std::path::{Path, PathBuf}; | ||
| 5 | |||
| 6 | #[derive(Clone)] | ||
| 7 | pub enum Mode { | ||
| 8 | /// Useful for checkout where directories need creation, but we need to access attributes as well. | ||
| 9 | CreateDirectoryAndProvideAttributes { | ||
| 10 | /// If there is a symlink or a file in our path, try to unlink it before creating the directory. | ||
| 11 | unlink_on_collision: bool, | ||
| 12 | |||
| 13 | /// just for testing | ||
| 14 | #[cfg(debug_assertions)] | ||
| 15 | test_mkdir_calls: usize, | ||
| 16 | /// An additional per-user attributes file, similar to `$GIT_DIR/info/attributes` | ||
| 17 | attributes_file: Option<PathBuf>, | ||
| 18 | }, | ||
| 19 | /// Used when adding files, requiring access to both attributes and ignore information. | ||
| 20 | ProvideAttributesAndIgnore { | ||
| 21 | /// An additional per-user excludes file, similar to `$GIT_DIR/info/exclude`. It's an error if it is set but can't be read/opened. | ||
| 22 | excludes_file: Option<PathBuf>, | ||
| 23 | /// An additional per-user attributes file, similar to `$GIT_DIR/info/attributes` | ||
| 24 | attributes_file: Option<PathBuf>, | ||
| 25 | }, | ||
| 26 | } | ||
| 27 | |||
| 28 | impl Mode { | ||
| 29 | /// Configure a mode to be suitable for checking out files. | ||
| 30 | pub fn checkout(unlink_on_collision: bool, attributes_file: Option<PathBuf>) -> Self { | ||
| 31 | Mode::CreateDirectoryAndProvideAttributes { | ||
| 32 | unlink_on_collision, | ||
| 33 | #[cfg(debug_assertions)] | ||
| 34 | test_mkdir_calls: 0, | ||
| 35 | attributes_file, | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | /// Configure a mode for adding files. | ||
| 40 | pub fn add(excludes_file: Option<PathBuf>, attributes_file: Option<PathBuf>) -> Self { | ||
| 41 | Mode::ProvideAttributesAndIgnore { | ||
| 42 | excludes_file, | ||
| 43 | attributes_file, | ||
| 44 | } | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | #[cfg(debug_assertions)] | ||
| 49 | impl Cache { | ||
| 50 | pub fn num_mkdir_calls(&self) -> usize { | ||
| 51 | match self.mode { | ||
| 52 | Mode::CreateDirectoryAndProvideAttributes { test_mkdir_calls, .. } => test_mkdir_calls, | ||
| 53 | _ => 0, | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | pub fn reset_mkdir_calls(&mut self) { | ||
| 58 | if let Mode::CreateDirectoryAndProvideAttributes { test_mkdir_calls, .. } = &mut self.mode { | ||
| 59 | *test_mkdir_calls = 0; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | pub fn unlink_on_collision(&mut self, value: bool) { | ||
| 64 | if let Mode::CreateDirectoryAndProvideAttributes { | ||
| 65 | unlink_on_collision, .. | ||
| 66 | } = &mut self.mode | ||
| 67 | { | ||
| 68 | *unlink_on_collision = value; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | pub struct Platform<'a> { | ||
| 74 | parent: &'a Cache, | ||
| 75 | } | ||
| 76 | |||
| 77 | impl<'a> Platform<'a> { | ||
| 78 | /// The full path to `relative` will be returned for use on the file system. | ||
| 79 | pub fn leading_dir(&self) -> &'a Path { | ||
| 80 | self.parent.stack.current() | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | impl<'a> std::fmt::Debug for Platform<'a> { | ||
| 85 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 86 | std::fmt::Debug::fmt(&self.leading_dir(), f) | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | impl Cache { | ||
| 91 | fn assure_init(&mut self) -> std::io::Result<()> { | ||
| 92 | Ok(()) | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | impl Cache { | ||
| 97 | /// Create a new instance with `worktree_root` being the base for all future paths we handle, assuming it to be valid which includes | ||
| 98 | /// symbolic links to be included in it as well. | ||
| 99 | pub fn new(worktree_root: impl Into<PathBuf>, mode: Mode) -> Self { | ||
| 100 | let root = worktree_root.into(); | ||
| 101 | Cache { | ||
| 102 | stack: fs::Stack::new(root), | ||
| 103 | mode, | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories | ||
| 108 | /// unless `mode` indicates `relative` points to a directory itself in which case the entire resulting path is created as directory. | ||
| 109 | /// | ||
| 110 | /// Provide access to cached information for that `relative` entry via the platform returned. | ||
| 111 | pub fn at_entry( | ||
| 112 | &mut self, | ||
| 113 | relative: impl AsRef<Path>, | ||
| 114 | mode: git_index::entry::Mode, | ||
| 115 | ) -> std::io::Result<Platform<'_>> { | ||
| 116 | self.assure_init()?; | ||
| 117 | let op_mode = &mut self.mode; | ||
| 118 | self.stack.make_relative_path_current( | ||
| 119 | relative, | ||
| 120 | |components, stack: &fs::Stack| { | ||
| 121 | match op_mode { | ||
| 122 | Mode::CreateDirectoryAndProvideAttributes { | ||
| 123 | #[cfg(debug_assertions)] | ||
| 124 | test_mkdir_calls, | ||
| 125 | unlink_on_collision, | ||
| 126 | attributes_file: _, | ||
| 127 | } => { | ||
| 128 | #[cfg(debug_assertions)] | ||
| 129 | { | ||
| 130 | create_leading_directory(components, stack, mode, test_mkdir_calls, *unlink_on_collision)? | ||
| 131 | } | ||
| 132 | #[cfg(not(debug_assertions))] | ||
| 133 | { | ||
| 134 | create_leading_directory(components, stack, mode, *unlink_on_collision)? | ||
| 135 | } | ||
| 136 | } | ||
| 137 | Mode::ProvideAttributesAndIgnore { .. } => todo!(), | ||
| 138 | } | ||
| 139 | Ok(()) | ||
| 140 | }, | ||
| 141 | |_| {}, | ||
| 142 | )?; | ||
| 143 | Ok(Platform { parent: self }) | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | fn create_leading_directory( | ||
| 148 | components: &mut std::iter::Peekable<std::path::Components<'_>>, | ||
| 149 | stack: &Stack, | ||
| 150 | mode: git_index::entry::Mode, | ||
| 151 | #[cfg(debug_assertions)] mkdir_calls: &mut usize, | ||
| 152 | unlink_on_collision: bool, | ||
| 153 | ) -> std::io::Result<()> { | ||
| 154 | let target_is_dir = mode == git_index::entry::Mode::COMMIT || mode == git_index::entry::Mode::DIR; | ||
| 155 | if !(components.peek().is_some() || target_is_dir) { | ||
| 156 | return Ok(()); | ||
| 157 | } | ||
| 158 | #[cfg(debug_assertions)] | ||
| 159 | { | ||
| 160 | *mkdir_calls += 1; | ||
| 161 | } | ||
| 162 | match std::fs::create_dir(stack.current()) { | ||
| 163 | Ok(()) => Ok(()), | ||
| 164 | Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | ||
| 165 | let meta = stack.current().symlink_metadata()?; | ||
| 166 | if meta.is_dir() { | ||
| 167 | Ok(()) | ||
| 168 | } else if unlink_on_collision { | ||
| 169 | if meta.file_type().is_symlink() { | ||
| 170 | os::remove_symlink(stack.current())?; | ||
| 171 | } else { | ||
| 172 | std::fs::remove_file(stack.current())?; | ||
| 173 | } | ||
| 174 | #[cfg(debug_assertions)] | ||
| 175 | { | ||
| 176 | *mkdir_calls += 1; | ||
| 177 | } | ||
| 178 | std::fs::create_dir(stack.current()) | ||
| 179 | } else { | ||
| 180 | Err(err) | ||
| 181 | } | ||
| 182 | } | ||
| 183 | Err(err) => Err(err), | ||
| 184 | } | ||
| 185 | } | ||
| @@ -0,0 +1,139 @@ | |||
| 1 | use super::Cache; | ||
| 2 | use crate::fs; | ||
| 3 | use crate::fs::PathOidMapping; | ||
| 4 | use bstr::{BStr, ByteSlice}; | ||
| 5 | use git_hash::oid; | ||
| 6 | use std::path::{Path, PathBuf}; | ||
| 7 | |||
| 8 | #[derive(Clone)] | ||
| 9 | pub enum State { | ||
| 10 | /// Useful for checkout where directories need creation, but we need to access attributes as well. | ||
| 11 | CreateDirectoryAndAttributesStack { | ||
| 12 | /// If there is a symlink or a file in our path, try to unlink it before creating the directory. | ||
| 13 | unlink_on_collision: bool, | ||
| 14 | |||
| 15 | /// just for testing | ||
| 16 | #[cfg(debug_assertions)] | ||
| 17 | test_mkdir_calls: usize, | ||
| 18 | /// State to handle attribute information | ||
| 19 | attributes: state::Attributes, | ||
| 20 | }, | ||
| 21 | /// Used when adding files, requiring access to both attributes and ignore information, for example during add operations. | ||
| 22 | AttributesAndIgnoreStack { | ||
| 23 | /// State to handle attribute information | ||
| 24 | attributes: state::Attributes, | ||
| 25 | /// State to handle exclusion information | ||
| 26 | ignore: state::Ignore, | ||
| 27 | }, | ||
| 28 | /// Used when providing worktree status information. | ||
| 29 | IgnoreStack(state::Ignore), | ||
| 30 | } | ||
| 31 | |||
| 32 | #[cfg(debug_assertions)] | ||
| 33 | impl<'paths> Cache<'paths> { | ||
| 34 | pub fn set_case(&mut self, case: git_glob::pattern::Case) { | ||
| 35 | self.case = case; | ||
| 36 | } | ||
| 37 | pub fn num_mkdir_calls(&self) -> usize { | ||
| 38 | match self.state { | ||
| 39 | State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } => test_mkdir_calls, | ||
| 40 | _ => 0, | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | pub fn reset_mkdir_calls(&mut self) { | ||
| 45 | if let State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } = &mut self.state { | ||
| 46 | *test_mkdir_calls = 0; | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | pub fn unlink_on_collision(&mut self, value: bool) { | ||
| 51 | if let State::CreateDirectoryAndAttributesStack { | ||
| 52 | unlink_on_collision, .. | ||
| 53 | } = &mut self.state | ||
| 54 | { | ||
| 55 | *unlink_on_collision = value; | ||
| 56 | } | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | #[must_use] | ||
| 61 | pub struct Platform<'a, 'paths> { | ||
| 62 | parent: &'a Cache<'paths>, | ||
| 63 | is_dir: Option<bool>, | ||
| 64 | } | ||
| 65 | |||
| 66 | impl<'paths> Cache<'paths> { | ||
| 67 | /// Create a new instance with `worktree_root` being the base for all future paths we handle, assuming it to be valid which includes | ||
| 68 | /// symbolic links to be included in it as well. | ||
| 69 | /// The `case` configures attribute and exclusion query case sensitivity. | ||
| 70 | pub fn new( | ||
| 71 | worktree_root: impl Into<PathBuf>, | ||
| 72 | state: State, | ||
| 73 | case: git_glob::pattern::Case, | ||
| 74 | buf: Vec<u8>, | ||
| 75 | attribute_files_in_index: Vec<PathOidMapping<'paths>>, | ||
| 76 | ) -> Self { | ||
| 77 | let root = worktree_root.into(); | ||
| 78 | Cache { | ||
| 79 | stack: fs::Stack::new(root), | ||
| 80 | state, | ||
| 81 | case, | ||
| 82 | buf, | ||
| 83 | attribute_files_in_index, | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories | ||
| 88 | /// unless `is_dir` is known (`Some(…)`) then `relative` points to a directory itself in which case the entire resulting | ||
| 89 | /// path is created as directory. If it's not known it is assumed to be a file. | ||
| 90 | /// | ||
| 91 | /// Provide access to cached information for that `relative` entry via the platform returned. | ||
| 92 | pub fn at_path<Find, E>( | ||
| 93 | &mut self, | ||
| 94 | relative: impl AsRef<Path>, | ||
| 95 | is_dir: Option<bool>, | ||
| 96 | find: Find, | ||
| 97 | ) -> std::io::Result<Platform<'_, 'paths>> | ||
| 98 | where | ||
| 99 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>, | ||
| 100 | E: std::error::Error + Send + Sync + 'static, | ||
| 101 | { | ||
| 102 | let mut delegate = platform::StackDelegate { | ||
| 103 | state: &mut self.state, | ||
| 104 | buf: &mut self.buf, | ||
| 105 | is_dir: is_dir.unwrap_or(false), | ||
| 106 | attribute_files_in_index: &self.attribute_files_in_index, | ||
| 107 | find, | ||
| 108 | }; | ||
| 109 | self.stack.make_relative_path_current(relative, &mut delegate)?; | ||
| 110 | Ok(Platform { parent: self, is_dir }) | ||
| 111 | } | ||
| 112 | |||
| 113 | /// **Panics** on illformed UTF8 in `relative` | ||
| 114 | // TODO: more docs | ||
| 115 | pub fn at_entry<'r, Find, E>( | ||
| 116 | &mut self, | ||
| 117 | relative: impl Into<&'r BStr>, | ||
| 118 | is_dir: Option<bool>, | ||
| 119 | find: Find, | ||
| 120 | ) -> std::io::Result<Platform<'_, 'paths>> | ||
| 121 | where | ||
| 122 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>, | ||
| 123 | E: std::error::Error + Send + Sync + 'static, | ||
| 124 | { | ||
| 125 | let relative = relative.into(); | ||
| 126 | let relative_path = git_path::from_bstr(relative); | ||
| 127 | |||
| 128 | self.at_path( | ||
| 129 | relative_path, | ||
| 130 | is_dir.or_else(|| relative.ends_with_str("/").then(|| true)), | ||
| 131 | // is_dir, | ||
| 132 | find, | ||
| 133 | ) | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | mod platform; | ||
| 138 | /// | ||
| 139 | pub mod state; | ||
| @@ -0,0 +1,165 @@ | |||
| 1 | use crate::fs; | ||
| 2 | use crate::fs::cache::{Platform, State}; | ||
| 3 | use crate::fs::PathOidMapping; | ||
| 4 | use bstr::ByteSlice; | ||
| 5 | use git_hash::oid; | ||
| 6 | use std::path::Path; | ||
| 7 | |||
| 8 | impl<'a, 'paths> Platform<'a, 'paths> { | ||
| 9 | /// The full path to `relative` will be returned for use on the file system. | ||
| 10 | pub fn path(&self) -> &'a Path { | ||
| 11 | self.parent.stack.current() | ||
| 12 | } | ||
| 13 | |||
| 14 | /// See if the currently set entry is excluded as per exclude and git-ignore files. | ||
| 15 | /// | ||
| 16 | /// # Panics | ||
| 17 | /// | ||
| 18 | /// If the cache was configured without exclude patterns. | ||
| 19 | pub fn is_excluded(&self) -> bool { | ||
| 20 | self.matching_exclude_pattern() | ||
| 21 | .map_or(false, |m| m.pattern.is_negative()) | ||
| 22 | } | ||
| 23 | |||
| 24 | /// Check all exclude patterns to see if the currently set path matches any of them. | ||
| 25 | /// | ||
| 26 | /// Note that this pattern might be negated, and means this path in included. | ||
| 27 | /// | ||
| 28 | /// # Panics | ||
| 29 | /// | ||
| 30 | /// If the cache was configured without exclude patterns. | ||
| 31 | pub fn matching_exclude_pattern(&self) -> Option<git_attributes::Match<'_, ()>> { | ||
| 32 | let ignore = self.parent.state.ignore_or_panic(); | ||
| 33 | let relative_path = | ||
| 34 | git_path::to_unix_separators_on_windows(git_path::into_bstr(self.parent.stack.current_relative.as_path())); | ||
| 35 | ignore.matching_exclude_pattern(relative_path.as_bstr(), self.is_dir, self.parent.case) | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | impl<'a, 'paths> std::fmt::Debug for Platform<'a, 'paths> { | ||
| 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 41 | std::fmt::Debug::fmt(&self.path(), f) | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | pub struct StackDelegate<'a, 'paths, Find> { | ||
| 46 | pub state: &'a mut State, | ||
| 47 | pub buf: &'a mut Vec<u8>, | ||
| 48 | pub is_dir: bool, | ||
| 49 | pub attribute_files_in_index: &'a Vec<PathOidMapping<'paths>>, | ||
| 50 | pub find: Find, | ||
| 51 | } | ||
| 52 | |||
| 53 | impl<'a, 'paths, Find, E> fs::stack::Delegate for StackDelegate<'a, 'paths, Find> | ||
| 54 | where | ||
| 55 | Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<git_object::BlobRef<'b>, E>, | ||
| 56 | E: std::error::Error + Send + Sync + 'static, | ||
| 57 | { | ||
| 58 | fn push_directory(&mut self, stack: &fs::Stack) -> std::io::Result<()> { | ||
| 59 | match &mut self.state { | ||
| 60 | State::CreateDirectoryAndAttributesStack { attributes: _, .. } => { | ||
| 61 | // TODO: attributes | ||
| 62 | } | ||
| 63 | State::AttributesAndIgnoreStack { ignore, attributes: _ } => { | ||
| 64 | // TODO: attributes | ||
| 65 | ignore.push_directory( | ||
| 66 | &stack.root, | ||
| 67 | &stack.current, | ||
| 68 | self.buf, | ||
| 69 | self.attribute_files_in_index, | ||
| 70 | &mut self.find, | ||
| 71 | )? | ||
| 72 | } | ||
| 73 | State::IgnoreStack(ignore) => ignore.push_directory( | ||
| 74 | &stack.root, | ||
| 75 | &stack.current, | ||
| 76 | self.buf, | ||
| 77 | self.attribute_files_in_index, | ||
| 78 | &mut self.find, | ||
| 79 | )?, | ||
| 80 | } | ||
| 81 | Ok(()) | ||
| 82 | } | ||
| 83 | |||
| 84 | fn push(&mut self, is_last_component: bool, stack: &fs::Stack) -> std::io::Result<()> { | ||
| 85 | match &mut self.state { | ||
| 86 | State::CreateDirectoryAndAttributesStack { | ||
| 87 | #[cfg(debug_assertions)] | ||
| 88 | test_mkdir_calls, | ||
| 89 | unlink_on_collision, | ||
| 90 | attributes: _, | ||
| 91 | } => { | ||
| 92 | #[cfg(debug_assertions)] | ||
| 93 | { | ||
| 94 | create_leading_directory( | ||
| 95 | is_last_component, | ||
| 96 | stack, | ||
| 97 | self.is_dir, | ||
| 98 | test_mkdir_calls, | ||
| 99 | *unlink_on_collision, | ||
| 100 | )? | ||
| 101 | } | ||
| 102 | #[cfg(not(debug_assertions))] | ||
| 103 | { | ||
| 104 | create_leading_directory(is_last_component, stack, self.is_dir, *unlink_on_collision)? | ||
| 105 | } | ||
| 106 | } | ||
| 107 | State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {} | ||
| 108 | } | ||
| 109 | Ok(()) | ||
| 110 | } | ||
| 111 | |||
| 112 | fn pop_directory(&mut self) { | ||
| 113 | match &mut self.state { | ||
| 114 | State::CreateDirectoryAndAttributesStack { attributes: _, .. } => { | ||
| 115 | // TODO: attributes | ||
| 116 | } | ||
| 117 | State::AttributesAndIgnoreStack { attributes: _, ignore } => { | ||
| 118 | // TODO: attributes | ||
| 119 | ignore.pop_directory(); | ||
| 120 | } | ||
| 121 | State::IgnoreStack(ignore) => { | ||
| 122 | ignore.pop_directory(); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | fn create_leading_directory( | ||
| 129 | is_last_component: bool, | ||
| 130 | stack: &fs::Stack, | ||
| 131 | is_dir: bool, | ||
| 132 | #[cfg(debug_assertions)] mkdir_calls: &mut usize, | ||
| 133 | unlink_on_collision: bool, | ||
| 134 | ) -> std::io::Result<()> { | ||
| 135 | if is_last_component && !is_dir { | ||
| 136 | return Ok(()); | ||
| 137 | } | ||
| 138 | #[cfg(debug_assertions)] | ||
| 139 | { | ||
| 140 | *mkdir_calls += 1; | ||
| 141 | } | ||
| 142 | match std::fs::create_dir(stack.current()) { | ||
| 143 | Ok(()) => Ok(()), | ||
| 144 | Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | ||
| 145 | let meta = stack.current().symlink_metadata()?; | ||
| 146 | if meta.is_dir() { | ||
| 147 | Ok(()) | ||
| 148 | } else if unlink_on_collision { | ||
| 149 | if meta.file_type().is_symlink() { | ||
| 150 | crate::os::remove_symlink(stack.current())?; | ||
| 151 | } else { | ||
| 152 | std::fs::remove_file(stack.current())?; | ||
| 153 | } | ||
| 154 | #[cfg(debug_assertions)] | ||
| 155 | { | ||
| 156 | *mkdir_calls += 1; | ||
| 157 | } | ||
| 158 | std::fs::create_dir(stack.current()) | ||
| 159 | } else { | ||
| 160 | Err(err) | ||
| 161 | } | ||
| 162 | } | ||
| 163 | Err(err) => Err(err), | ||
| 164 | } | ||
| 165 | } | ||
| @@ -0,0 +1,287 @@ | |||
| 1 | use crate::fs::cache::State; | ||
| 2 | use crate::fs::PathOidMapping; | ||
| 3 | use bstr::{BStr, BString, ByteSlice}; | ||
| 4 | use git_glob::pattern::Case; | ||
| 5 | use git_hash::oid; | ||
| 6 | use std::path::Path; | ||
| 7 | |||
| 8 | type AttributeMatchGroup = git_attributes::MatchGroup<git_attributes::Attributes>; | ||
| 9 | type IgnoreMatchGroup = git_attributes::MatchGroup<git_attributes::Ignore>; | ||
| 10 | |||
| 11 | /// State related to attributes associated with files in the repository. | ||
| 12 | #[derive(Default, Clone)] | ||
| 13 | #[allow(unused)] | ||
| 14 | pub struct Attributes { | ||
| 15 | /// Attribute patterns that match the currently set directory (in the stack). | ||
| 16 | pub stack: AttributeMatchGroup, | ||
| 17 | /// Attribute patterns which aren't tied to the repository root, hence are global. They are consulted last. | ||
| 18 | pub globals: AttributeMatchGroup, | ||
| 19 | } | ||
| 20 | |||
| 21 | /// State related to the exclusion of files. | ||
| 22 | #[derive(Default, Clone)] | ||
| 23 | #[allow(unused)] | ||
| 24 | pub struct Ignore { | ||
| 25 | /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to | ||
| 26 | /// be consulted. | ||
| 27 | overrides: IgnoreMatchGroup, | ||
| 28 | /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. | ||
| 29 | stack: IgnoreMatchGroup, | ||
| 30 | /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. | ||
| 31 | globals: IgnoreMatchGroup, | ||
| 32 | /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the | ||
| 33 | /// currently set directory had a pattern matched. Note that this one could be negated. | ||
| 34 | /// (index into match groups, index into list of pattern lists, index into pattern list) | ||
| 35 | matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>, | ||
| 36 | /// The name of the file to look for in directories. | ||
| 37 | exclude_file_name_for_directories: BString, | ||
| 38 | /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine | ||
| 39 | /// to know if an entire path can be ignored as a parent directory is ignored. | ||
| 40 | case: Case, | ||
| 41 | } | ||
| 42 | |||
| 43 | impl Ignore { | ||
| 44 | /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory | ||
| 45 | /// ignore files within the repository, defaults to`.gitignore`. | ||
| 46 | // TODO: more docs | ||
| 47 | pub fn new( | ||
| 48 | overrides: IgnoreMatchGroup, | ||
| 49 | globals: IgnoreMatchGroup, | ||
| 50 | exclude_file_name_for_directories: Option<&BStr>, | ||
| 51 | case: Case, | ||
| 52 | ) -> Self { | ||
| 53 | Ignore { | ||
| 54 | case, | ||
| 55 | overrides, | ||
| 56 | globals, | ||
| 57 | stack: Default::default(), | ||
| 58 | matched_directory_patterns_stack: Vec::with_capacity(6), | ||
| 59 | exclude_file_name_for_directories: exclude_file_name_for_directories | ||
| 60 | .map(ToOwned::to_owned) | ||
| 61 | .unwrap_or_else(|| ".gitignore".into()), | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | impl Ignore { | ||
| 67 | pub(crate) fn pop_directory(&mut self) { | ||
| 68 | self.matched_directory_patterns_stack.pop().expect("something to pop"); | ||
| 69 | self.stack.patterns.pop().expect("something to pop"); | ||
| 70 | } | ||
| 71 | /// The match groups from lowest priority to highest. | ||
| 72 | pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] { | ||
| 73 | [&self.globals, &self.stack, &self.overrides] | ||
| 74 | } | ||
| 75 | |||
| 76 | pub(crate) fn matching_exclude_pattern( | ||
| 77 | &self, | ||
| 78 | relative_path: &BStr, | ||
| 79 | is_dir: Option<bool>, | ||
| 80 | case: Case, | ||
| 81 | ) -> Option<git_attributes::Match<'_, ()>> { | ||
| 82 | let groups = self.match_groups(); | ||
| 83 | if let Some((source, mapping)) = self | ||
| 84 | .matched_directory_patterns_stack | ||
| 85 | .iter() | ||
| 86 | .rev() | ||
| 87 | .filter_map(|v| *v) | ||
| 88 | .map(|(gidx, plidx, pidx)| { | ||
| 89 | let list = &groups[gidx].patterns[plidx]; | ||
| 90 | (list.source.as_deref(), &list.patterns[pidx]) | ||
| 91 | }) | ||
| 92 | .next() | ||
| 93 | { | ||
| 94 | if !mapping.pattern.is_negative() { | ||
| 95 | return git_attributes::Match { | ||
| 96 | pattern: &mapping.pattern, | ||
| 97 | value: &mapping.value, | ||
| 98 | sequence_number: mapping.sequence_number, | ||
| 99 | source, | ||
| 100 | } | ||
| 101 | .into(); | ||
| 102 | } | ||
| 103 | } | ||
| 104 | groups | ||
| 105 | .iter() | ||
| 106 | .rev() | ||
| 107 | .find_map(|group| group.pattern_matching_relative_path(relative_path.as_ref(), is_dir, case)) | ||
| 108 | } | ||
| 109 | |||
| 110 | /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded. | ||
| 111 | /// It returns a triple-index into our data structure from which a match can be reconstructed. | ||
| 112 | pub(crate) fn matching_exclude_pattern_no_dir( | ||
| 113 | &self, | ||
| 114 | relative_path: &BStr, | ||
| 115 | is_dir: Option<bool>, | ||
| 116 | case: Case, | ||
| 117 | ) -> Option<(usize, usize, usize)> { | ||
| 118 | let groups = self.match_groups(); | ||
| 119 | groups.iter().enumerate().rev().find_map(|(gidx, group)| { | ||
| 120 | let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); | ||
| 121 | group | ||
| 122 | .patterns | ||
| 123 | .iter() | ||
| 124 | .enumerate() | ||
| 125 | .rev() | ||
| 126 | .find_map(|(plidx, pl)| { | ||
| 127 | pl.pattern_idx_matching_relative_path(relative_path, basename_pos, is_dir, case) | ||
| 128 | .map(|idx| (plidx, idx)) | ||
| 129 | }) | ||
| 130 | .map(|(plidx, pidx)| (gidx, plidx, pidx)) | ||
| 131 | }) | ||
| 132 | } | ||
| 133 | |||
| 134 | pub(crate) fn push_directory<Find, E>( | ||
| 135 | &mut self, | ||
| 136 | root: &Path, | ||
| 137 | dir: &Path, | ||
| 138 | buf: &mut Vec<u8>, | ||
| 139 | attribute_files_in_index: &[PathOidMapping<'_>], | ||
| 140 | mut find: Find, | ||
| 141 | ) -> std::io::Result<()> | ||
| 142 | where | ||
| 143 | Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<git_object::BlobRef<'b>, E>, | ||
| 144 | E: std::error::Error + Send + Sync + 'static, | ||
| 145 | { | ||
| 146 | let rela_dir = dir.strip_prefix(root).expect("dir in root"); | ||
| 147 | self.matched_directory_patterns_stack | ||
| 148 | .push(self.matching_exclude_pattern_no_dir(git_path::into_bstr(rela_dir).as_ref(), Some(true), self.case)); | ||
| 149 | |||
| 150 | let ignore_path_relative = rela_dir.join(".gitignore"); | ||
| 151 | let ignore_path_relative = git_path::to_unix_separators_on_windows(git_path::into_bstr(ignore_path_relative)); | ||
| 152 | let ignore_file_in_index = | ||
| 153 | attribute_files_in_index.binary_search_by(|t| t.0.cmp(ignore_path_relative.as_ref())); | ||
| 154 | let follow_symlinks = ignore_file_in_index.is_err(); | ||
| 155 | if !self | ||
| 156 | .stack | ||
| 157 | .add_patterns_file(dir.join(".gitignore"), follow_symlinks, Some(root), buf)? | ||
| 158 | { | ||
| 159 | match ignore_file_in_index { | ||
| 160 | Ok(idx) => { | ||
| 161 | let ignore_blob = find(&attribute_files_in_index[idx].1, buf) | ||
| 162 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; | ||
| 163 | let ignore_path = git_path::from_bstring(ignore_path_relative.into_owned()); | ||
| 164 | self.stack | ||
| 165 | .add_patterns_buffer(ignore_blob.data, ignore_path, Some(root)); | ||
| 166 | } | ||
| 167 | Err(_) => { | ||
| 168 | // Need one stack level per component so push and pop matches. | ||
| 169 | self.stack.patterns.push(Default::default()) | ||
| 170 | } | ||
| 171 | } | ||
| 172 | } | ||
| 173 | Ok(()) | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | impl Attributes { | ||
| 178 | pub fn new(globals: AttributeMatchGroup) -> Self { | ||
| 179 | Attributes { | ||
| 180 | globals, | ||
| 181 | stack: Default::default(), | ||
| 182 | } | ||
| 183 | } | ||
| 184 | } | ||
| 185 | |||
| 186 | impl From<AttributeMatchGroup> for Attributes { | ||
| 187 | fn from(group: AttributeMatchGroup) -> Self { | ||
| 188 | Attributes::new(group) | ||
| 189 | } | ||
| 190 | } | ||
| 191 | |||
| 192 | impl State { | ||
| 193 | /// Configure a state to be suitable for checking out files. | ||
| 194 | pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { | ||
| 195 | State::CreateDirectoryAndAttributesStack { | ||
| 196 | unlink_on_collision, | ||
| 197 | #[cfg(debug_assertions)] | ||
| 198 | test_mkdir_calls: 0, | ||
| 199 | attributes, | ||
| 200 | } | ||
| 201 | } | ||
| 202 | |||
| 203 | /// Configure a state for adding files. | ||
| 204 | pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self { | ||
| 205 | State::AttributesAndIgnoreStack { attributes, ignore } | ||
| 206 | } | ||
| 207 | |||
| 208 | /// Configure a state for status retrieval. | ||
| 209 | pub fn for_status(ignore: Ignore) -> Self { | ||
| 210 | State::IgnoreStack(ignore) | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | impl State { | ||
| 215 | /// Returns a vec of tuples of relative index paths along with the best usable OID for either ignore, attribute files or both. | ||
| 216 | /// | ||
| 217 | /// - ignores entries which aren't blobs | ||
| 218 | /// - ignores ignore entries which are not skip-worktree | ||
| 219 | /// - within merges, picks 'our' stage both for ignore and attribute files. | ||
| 220 | pub fn build_attribute_list<'paths>( | ||
| 221 | &self, | ||
| 222 | index: &git_index::State, | ||
| 223 | paths: &'paths git_index::PathStorage, | ||
| 224 | case: Case, | ||
| 225 | ) -> Vec<PathOidMapping<'paths>> { | ||
| 226 | let a1_backing; | ||
| 227 | let a2_backing; | ||
| 228 | let names = match self { | ||
| 229 | State::IgnoreStack(v) => { | ||
| 230 | a1_backing = [(v.exclude_file_name_for_directories.as_bytes().as_bstr(), true)]; | ||
| 231 | a1_backing.as_ref() | ||
| 232 | } | ||
| 233 | State::AttributesAndIgnoreStack { ignore, .. } => { | ||
| 234 | a2_backing = [ | ||
| 235 | (ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), true), | ||
| 236 | (".gitattributes".into(), false), | ||
| 237 | ]; | ||
| 238 | a2_backing.as_ref() | ||
| 239 | } | ||
| 240 | State::CreateDirectoryAndAttributesStack { .. } => { | ||
| 241 | a1_backing = [(".gitattributes".into(), true)]; | ||
| 242 | a1_backing.as_ref() | ||
| 243 | } | ||
| 244 | }; | ||
| 245 | |||
| 246 | index | ||
| 247 | .entries() | ||
| 248 | .iter() | ||
| 249 | .filter_map(move |entry| { | ||
| 250 | let path = entry.path_in(paths); | ||
| 251 | |||
| 252 | // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then | ||
| 253 | // there won't be a stage 0. | ||
| 254 | if entry.mode == git_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) { | ||
| 255 | let basename = path | ||
| 256 | .rfind_byte(b'/') | ||
| 257 | .map(|pos| path[pos + 1..].as_bstr()) | ||
| 258 | .unwrap_or(path); | ||
| 259 | let is_ignore = names.iter().find_map(|t| { | ||
| 260 | match case { | ||
| 261 | Case::Sensitive => basename == t.0, | ||
| 262 | Case::Fold => basename.eq_ignore_ascii_case(t.0), | ||
| 263 | } | ||
| 264 | .then(|| t.1) | ||
| 265 | })?; | ||
| 266 | // See https://github.com/git/git/blob/master/dir.c#L912:L912 | ||
| 267 | if is_ignore && !entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) { | ||
| 268 | return None; | ||
| 269 | } | ||
| 270 | Some((path, entry.id)) | ||
| 271 | } else { | ||
| 272 | None | ||
| 273 | } | ||
| 274 | }) | ||
| 275 | .collect() | ||
| 276 | } | ||
| 277 | |||
| 278 | pub(crate) fn ignore_or_panic(&self) -> &Ignore { | ||
| 279 | match self { | ||
| 280 | State::IgnoreStack(v) => v, | ||
| 281 | State::AttributesAndIgnoreStack { ignore, .. } => ignore, | ||
| 282 | State::CreateDirectoryAndAttributesStack { .. } => { | ||
| 283 | unreachable!("BUG: must not try to check excludes without it being setup") | ||
| 284 | } | ||
| 285 | } | ||
| 286 | } | ||
| 287 | } | ||
| @@ -1,3 +1,4 @@ | |||
| 1 | use bstr::BStr; | ||
| 1 | use std::path::PathBuf; | 2 | use std::path::PathBuf; |
| 2 | 3 | ||
| 3 | /// Common knowledge about the worktree that is needed across most interactions with the work tree | 4 | /// 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)] | ||
| 24 | pub struct Stack { | 26 | pub 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)] |
| 56 | pub struct Cache { | 60 | pub 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 | ||
| 72 | pub(crate) type PathOidMapping<'paths> = (&'paths BStr, git_hash::ObjectId); | ||
| 73 | |||
| 62 | /// | 74 | /// |
| 63 | pub mod cache; | 75 | pub mod cache; |
| 64 | /// | 76 | /// |
| @@ -15,6 +15,12 @@ impl Stack { · 14 unmodified lines | |||
| 15 | } | 15 | } |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | pub trait Delegate { | ||
| 19 | fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>; | ||
| 20 | fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>; | ||
| 21 | fn pop_directory(&mut self); | ||
| 22 | } | ||
| 23 | |||
| 18 | impl Stack { | 24 | impl Stack { |
| 19 | /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes | 25 | /// 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 | } |
| @@ -1,5 +1,5 @@ | |||
| 1 | use bstr::BString; | 1 | use bstr::BString; |
| 2 | use std::path::PathBuf; | 2 | use git_attributes::Attributes; |
| 3 | 3 | ||
| 4 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] | 4 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| 5 | pub struct Collision { | 5 | pub 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 | ||
| 64 | impl Default for Options { | 64 | impl 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 | } |
| @@ -7,9 +7,9 @@ use io_close::Close; · 6 unmodified lines | |||
| 7 | 7 | ||
| 8 | use crate::{fs, index, os}; | 8 | use crate::{fs, index, os}; |
| 9 | 9 | ||
| 10 | pub struct Context<'a, Find> { | 10 | pub 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 | |||
| 17 | pub fn checkout<Find, E>( | 17 | pub 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 { |
| @@ -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>> |
| 24 | where | ||
| 25 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E> + Send + Clone, | ||
| 26 | E: std::error::Error + Send + Sync + 'static, | ||
| 27 | { | ||
| 28 | let paths = index.take_path_backing(); | ||
| 29 | let res = checkout_inner(index, &paths, dir, find, files, bytes, should_interrupt, options); | ||
| 30 | index.return_path_backing(paths); | ||
| 31 | res | ||
| 32 | } | ||
| 33 | #[allow(clippy::too_many_arguments)] | ||
| 34 | fn checkout_inner<Find, E>( | ||
| 35 | index: &mut git_index::State, | ||
| 36 | paths: &git_index::PathStorage, | ||
| 37 | dir: impl Into<std::path::PathBuf>, | ||
| 38 | find: Find, | ||
| 39 | files: &mut impl Progress, | ||
| 40 | bytes: &mut impl Progress, | ||
| 41 | should_interrupt: &AtomicBool, | ||
| 42 | options: checkout::Options, | ||
| 43 | ) -> Result<checkout::Outcome, checkout::Error<E>> | ||
| 24 | where | 44 | where |
| 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 like | 205 | /// 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 | where | 216 | 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 | where | 271 | 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( |
| @@ -0,0 +1,3 @@ | |||
| 1 | version https://git-lfs.github.com/spec/v1 | ||
| 2 | oid sha256:ae297f2c067e2cc7b0c8eabfff721ff775a7d8266ffef979bde2458de2ab03c9 | ||
| 3 | size 10944 | ||
| @@ -0,0 +1,112 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | set -eu -o pipefail | ||
| 3 | |||
| 4 | cat <<EOF >user.exclude | ||
| 5 | # a custom exclude configured per user | ||
| 6 | user-file-anywhere | ||
| 7 | /user-file-from-top | ||
| 8 | |||
| 9 | user-dir-anywhere/ | ||
| 10 | /user-dir-from-top | ||
| 11 | |||
| 12 | user-subdir/file | ||
| 13 | **/user-subdir-anywhere/file | ||
| 14 | EOF | ||
| 15 | |||
| 16 | mkdir repo; | ||
| 17 | (cd repo | ||
| 18 | git init -q | ||
| 19 | git config core.excludesFile ../user.exclude | ||
| 20 | |||
| 21 | cat <<EOF >.git/info/exclude | ||
| 22 | # a sample .git/info/exclude | ||
| 23 | file-anywhere | ||
| 24 | /file-from-top | ||
| 25 | |||
| 26 | dir-anywhere/ | ||
| 27 | /dir-from-top | ||
| 28 | |||
| 29 | subdir/file | ||
| 30 | **/subdir-anywhere/file | ||
| 31 | EOF | ||
| 32 | |||
| 33 | cat <<EOF >.gitignore | ||
| 34 | # a sample .gitignore | ||
| 35 | top-level-local-file-anywhere | ||
| 36 | EOF | ||
| 37 | |||
| 38 | mkdir dir-with-ignore | ||
| 39 | cat <<EOF >dir-with-ignore/.gitignore | ||
| 40 | # a sample .gitignore | ||
| 41 | sub-level-local-file-anywhere | ||
| 42 | sub-level-dir-anywhere/ | ||
| 43 | !/negated | ||
| 44 | /negated-dir/ | ||
| 45 | !/negated-dir/ | ||
| 46 | EOF | ||
| 47 | |||
| 48 | git add .gitignore dir-with-ignore | ||
| 49 | git commit --allow-empty -m "init" | ||
| 50 | |||
| 51 | # just add this git-ignore file, so it's a new file that doesn't exist on disk. | ||
| 52 | mkdir other-dir-with-ignore | ||
| 53 | skip_worktree_ignore=other-dir-with-ignore/.gitignore | ||
| 54 | cat <<EOF >"$skip_worktree_ignore" | ||
| 55 | # a sample .gitignore | ||
| 56 | other-sub-level-local-file-anywhere | ||
| 57 | other-sub-level-dir-anywhere/ | ||
| 58 | EOF | ||
| 59 | git add $skip_worktree_ignore && git update-index --skip-worktree $skip_worktree_ignore && rm $skip_worktree_ignore | ||
| 60 | |||
| 61 | mkdir user-dir-anywhere user-dir-from-top dir-anywhere dir-from-top | ||
| 62 | mkdir -p dir/user-dir-anywhere dir/dir-anywhere | ||
| 63 | |||
| 64 | git check-ignore -vn --stdin 2>&1 <<EOF >git-check-ignore.baseline || : | ||
| 65 | dir-with-ignore/sub-level-dir-anywhere/ | ||
| 66 | dir-with-ignore/foo/sub-level-dir-anywhere/ | ||
| 67 | dir-with-ignore/sub-level-dir-anywhere | ||
| 68 | user-file-anywhere | ||
| 69 | dir/user-file-anywhere | ||
| 70 | user-file-from-top | ||
| 71 | no-match/user-file-from-top | ||
| 72 | user-dir-anywhere | ||
| 73 | user-dir-from-top | ||
| 74 | no-match/user-dir-from-top | ||
| 75 | user-subdir/file | ||
| 76 | subdir/user-subdir-anywhere/file | ||
| 77 | user-dir-anywhere/hello | ||
| 78 | dir/user-dir-anywhere/hello | ||
| 79 | file-anywhere | ||
| 80 | dir/file-anywhere | ||
| 81 | file-from-top | ||
| 82 | no-match/file-from-top | ||
| 83 | dir-anywhere | ||
| 84 | dir/dir-anywhere | ||
| 85 | dir-from-top | ||
| 86 | no-match/dir-from-top | ||
| 87 | subdir/file | ||
| 88 | subdir/subdir-anywhere/file | ||
| 89 | top-level-local-file-anywhere | ||
| 90 | dir/top-level-local-file-anywhere | ||
| 91 | no-match/sub-level-local-file-anywhere | ||
| 92 | dir-with-ignore/sub-level-local-file-anywhere | ||
| 93 | dir-with-ignore/sub-dir/sub-level-local-file-anywhere | ||
| 94 | other-dir-with-ignore/other-sub-level-local-file-anywhere | ||
| 95 | other-dir-with-ignore/sub-level-local-file-anywhere | ||
| 96 | other-dir-with-ignore/sub-dir/other-sub-level-local-file-anywhere | ||
| 97 | other-dir-with-ignore/no-match/sub-level-local-file-anywhere | ||
| 98 | non-existing/dir-anywhere | ||
| 99 | dir-anywhere/hello | ||
| 100 | dir/dir-anywhere/hello | ||
| 101 | no-match/sub-level-dir-anywhere/hello | ||
| 102 | no-match/other-sub-level-dir-anywhere/hello | ||
| 103 | dir-with-ignore/sub-level-dir-anywhere/hello | ||
| 104 | dir-with-ignore/sub-level-dir-anywhere/ | ||
| 105 | other-dir-with-ignore/sub-level-dir-anywhere/hello | ||
| 106 | other-dir-with-ignore/other-sub-level-dir-anywhere/hello | ||
| 107 | other-dir-with-ignore/other-sub-level-dir-anywhere/ | ||
| 108 | dir-with-ignore/negated | ||
| 109 | dir-with-ignore/negated-dir/hello | ||
| 110 | EOF | ||
| 111 | |||
| 112 | ) | ||
| @@ -1,2 +1,4 @@ | |||
| 1 | extern crate core; | ||
| 2 | |||
| 1 | mod worktree; | 3 | mod worktree; |
| 2 | use worktree::*; | 4 | use worktree::*; |
| @@ -1,39 +1,49 @@ | |||
| 1 | mod create_directory { | 1 | mod 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 = cache | 43 | 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::AlreadyExists | 81 | 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)] |
| 98 | mod ignore_only { | 119 | mod 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 initialization | 133 | } |
| 108 | cache | 134 | |
| 135 | impl<'a> Iterator for IgnoreExpectations<'a> { | ||
| 136 | type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); | ||
| 137 | |||
| 138 | fn next(&mut self) -> Option<Self::Item> { | ||
| 139 | let line = self.lines.next()?; | ||
| 140 | let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); | ||
| 141 | let value = value[1..].as_bstr(); | ||
| 142 | |||
| 143 | let source_and_line = if left == b"::" { | ||
| 144 | None | ||
| 145 | } else { | ||
| 146 | let mut tokens = left.split(|b| *b == b':'); | ||
| 147 | let source = tokens.next().unwrap().as_bstr(); | ||
| 148 | let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); | ||
| 149 | let pattern = tokens.next().unwrap().as_bstr(); | ||
| 150 | Some((source, line_number, pattern)) | ||
| 151 | }; | ||
| 152 | Some((value, source_and_line)) | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | #[test] | ||
| 157 | fn check_against_baseline() -> crate::Result { | ||
| 158 | let dir = git_testtools::scripted_fixture_repo_read_only("make_ignore_and_attributes_setup.sh")?; | ||
| 159 | let worktree_dir = dir.join("repo"); | ||
| 160 | let git_dir = worktree_dir.join(".git"); | ||
| 161 | let mut buf = Vec::new(); | ||
| 162 | let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | ||
| 163 | let user_exclude_path = dir.join("user.exclude"); | ||
| 164 | assert!(user_exclude_path.is_file()); | ||
| 165 | |||
| 166 | let mut index = git_index::File::at(git_dir.join("index"), Default::default())?; | ||
| 167 | let odb = git_odb::at(git_dir.join("objects"))?; | ||
| 168 | let case = git_glob::pattern::Case::Sensitive; | ||
| 169 | let state = git_worktree::fs::cache::State::for_add( | ||
| 170 | Default::default(), // TODO: attribute tests | ||
| 171 | git_worktree::fs::cache::state::Ignore::new( | ||
| 172 | git_attributes::MatchGroup::from_overrides(vec!["!force-include"]), | ||
| 173 | git_attributes::MatchGroup::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, | ||
| 174 | None, | ||
| 175 | case, | ||
| 176 | ), | ||
| 177 | ); | ||
| 178 | let paths_storage = index.take_path_backing(); | ||
| 179 | let attribute_files_in_index = state.build_attribute_list(&index.state, &paths_storage, case); | ||
| 180 | assert_eq!( | ||
| 181 | attribute_files_in_index, | ||
| 182 | vec![( | ||
| 183 | "other-dir-with-ignore/.gitignore".as_bytes().as_bstr(), | ||
| 184 | hex_to_id("5c7e0ed672d3d31d83a3df61f13cc8f7b22d5bfd") | ||
| 185 | )] | ||
| 186 | ); | ||
| 187 | let mut cache = fs::Cache::new(&worktree_dir, state, case, buf, attribute_files_in_index); | ||
| 188 | |||
| 189 | for (relative_entry, source_and_line) in (IgnoreExpectations { | ||
| 190 | lines: baseline.lines(), | ||
| 191 | }) { | ||
| 192 | let relative_path = git_path::from_byte_slice(relative_entry); | ||
| 193 | let is_dir = worktree_dir.join(&relative_path).metadata().ok().map(|m| m.is_dir()); | ||
| 194 | |||
| 195 | let platform = cache.at_entry(relative_entry, is_dir, |oid, buf| odb.find_blob(oid, buf))?; | ||
| 196 | |||
| 197 | let match_ = platform.matching_exclude_pattern(); | ||
| 198 | let is_excluded = platform.is_excluded(); | ||
| 199 | match (match_, source_and_line) { | ||
| 200 | (None, None) => { | ||
| 201 | assert!(!is_excluded); | ||
| 202 | } | ||
| 203 | (Some(m), Some((source_file, line, pattern))) => { | ||
| 204 | assert_eq!(m.pattern.to_string(), pattern); | ||
| 205 | assert_eq!(m.sequence_number, line); | ||
| 206 | // Paths read from the index are relative to the repo, and they don't exist locally due tot skip-worktree | ||
| 207 | if m.source.map_or(false, |p| p.exists()) { | ||
| 208 | assert_eq!( | ||
| 209 | m.source.map(|p| p.canonicalize().unwrap()), | ||
| 210 | Some(worktree_dir.join(source_file.to_str_lossy().as_ref()).canonicalize()?) | ||
| 211 | ); | ||
| 212 | } | ||
| 213 | } | ||
| 214 | (actual, expected) => { | ||
| 215 | panic!( | ||
| 216 | "actual {:?} didn't match {:?} at '{}'", | ||
| 217 | actual, expected, relative_entry | ||
| 218 | ); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | cache.set_case(Case::Fold); | ||
| 224 | let platform = cache.at_entry("User-file-ANYWHERE", Some(false), |oid, buf| odb.find_blob(oid, buf))?; | ||
| 225 | let m = platform.matching_exclude_pattern().expect("match"); | ||
| 226 | assert_eq!(m.pattern.text, "user-file-anywhere"); | ||
| 227 | Ok(()) | ||
| 109 | } | 228 | } |
| 110 | } | 229 | } |
| @@ -19,3 +19,4 @@ fn from_probing_cwd() { · 18 unmodified lines | |||
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | mod cache; | 21 | mod cache; |
| 22 | mod stack; | ||
| @@ -0,0 +1,144 @@ | |||
| 1 | use git_worktree::fs::Stack; | ||
| 2 | use std::path::{Path, PathBuf}; | ||
| 3 | |||
| 4 | #[derive(Debug, Default, Eq, PartialEq)] | ||
| 5 | struct Record { | ||
| 6 | push_dir: usize, | ||
| 7 | dirs: Vec<PathBuf>, | ||
| 8 | push: usize, | ||
| 9 | } | ||
| 10 | |||
| 11 | impl git_worktree::fs::stack::Delegate for Record { | ||
| 12 | fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { | ||
| 13 | self.push_dir += 1; | ||
| 14 | self.dirs.push(stack.current().into()); | ||
| 15 | Ok(()) | ||
| 16 | } | ||
| 17 | |||
| 18 | fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> { | ||
| 19 | self.push += 1; | ||
| 20 | Ok(()) | ||
| 21 | } | ||
| 22 | |||
| 23 | fn pop_directory(&mut self) { | ||
| 24 | self.dirs.pop(); | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | #[test] | ||
| 29 | fn delegate_calls_are_consistent() -> crate::Result { | ||
| 30 | let root = PathBuf::from("."); | ||
| 31 | let mut s = Stack::new(&root); | ||
| 32 | |||
| 33 | assert_eq!(s.current(), root); | ||
| 34 | assert_eq!(s.current_relative(), Path::new("")); | ||
| 35 | |||
| 36 | let mut r = Record::default(); | ||
| 37 | s.make_relative_path_current("a/b", &mut r)?; | ||
| 38 | let mut dirs = vec![root.clone(), root.join("a")]; | ||
| 39 | assert_eq!( | ||
| 40 | r, | ||
| 41 | Record { | ||
| 42 | push_dir: 2, | ||
| 43 | dirs: dirs.clone(), | ||
| 44 | push: 2, | ||
| 45 | } | ||
| 46 | ); | ||
| 47 | |||
| 48 | s.make_relative_path_current("a/b2", &mut r)?; | ||
| 49 | assert_eq!( | ||
| 50 | r, | ||
| 51 | Record { | ||
| 52 | push_dir: 2, | ||
| 53 | dirs: dirs.clone(), | ||
| 54 | push: 3, | ||
| 55 | } | ||
| 56 | ); | ||
| 57 | |||
| 58 | s.make_relative_path_current("c/d/e", &mut r)?; | ||
| 59 | dirs.pop(); | ||
| 60 | dirs.extend([root.join("c"), root.join("c").join("d")]); | ||
| 61 | assert_eq!( | ||
| 62 | r, | ||
| 63 | Record { | ||
| 64 | push_dir: 4, | ||
| 65 | dirs: dirs.clone(), | ||
| 66 | push: 6, | ||
| 67 | } | ||
| 68 | ); | ||
| 69 | |||
| 70 | dirs.push(root.join("c").join("d").join("x")); | ||
| 71 | s.make_relative_path_current("c/d/x/z", &mut r)?; | ||
| 72 | assert_eq!( | ||
| 73 | r, | ||
| 74 | Record { | ||
| 75 | push_dir: 5, | ||
| 76 | dirs: dirs.clone(), | ||
| 77 | push: 8, | ||
| 78 | } | ||
| 79 | ); | ||
| 80 | |||
| 81 | dirs.drain(dirs.len() - 3..).count(); | ||
| 82 | s.make_relative_path_current("f", &mut r)?; | ||
| 83 | assert_eq!(s.current_relative(), Path::new("f")); | ||
| 84 | assert_eq!( | ||
| 85 | r, | ||
| 86 | Record { | ||
| 87 | push_dir: 5, | ||
| 88 | dirs: dirs.clone(), | ||
| 89 | push: 9, | ||
| 90 | } | ||
| 91 | ); | ||
| 92 | |||
| 93 | dirs.push(root.join("x")); | ||
| 94 | s.make_relative_path_current("x/z", &mut r)?; | ||
| 95 | assert_eq!( | ||
| 96 | r, | ||
| 97 | Record { | ||
| 98 | push_dir: 6, | ||
| 99 | dirs: dirs.clone(), | ||
| 100 | push: 11, | ||
| 101 | } | ||
| 102 | ); | ||
| 103 | |||
| 104 | dirs.push(root.join("x").join("z")); | ||
| 105 | s.make_relative_path_current("x/z/a", &mut r)?; | ||
| 106 | assert_eq!( | ||
| 107 | r, | ||
| 108 | Record { | ||
| 109 | push_dir: 7, | ||
| 110 | dirs: dirs.clone(), | ||
| 111 | push: 12, | ||
| 112 | } | ||
| 113 | ); | ||
| 114 | |||
| 115 | dirs.push(root.join("x").join("z").join("a")); | ||
| 116 | dirs.push(root.join("x").join("z").join("a").join("b")); | ||
| 117 | s.make_relative_path_current("x/z/a/b/c", &mut r)?; | ||
| 118 | assert_eq!( | ||
| 119 | r, | ||
| 120 | Record { | ||
| 121 | push_dir: 9, | ||
| 122 | dirs: dirs.clone(), | ||
| 123 | push: 14, | ||
| 124 | } | ||
| 125 | ); | ||
| 126 | |||
| 127 | dirs.drain(dirs.len() - 2..).count(); | ||
| 128 | s.make_relative_path_current("x/z", &mut r)?; | ||
| 129 | assert_eq!( | ||
| 130 | r, | ||
| 131 | Record { | ||
| 132 | push_dir: 9, | ||
| 133 | dirs: dirs.clone(), | ||
| 134 | push: 14, | ||
| 135 | } | ||
| 136 | ); | ||
| 137 | assert_eq!( | ||
| 138 | dirs.last(), | ||
| 139 | Some(&PathBuf::from("./x/z")), | ||
| 140 | "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." | ||
| 141 | ); | ||
| 142 | |||
| 143 | Ok(()) | ||
| 144 | } | ||
| @@ -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 | } |
| @@ -4,6 +4,7 @@ use std::{ · 3 unmodified lines | |||
| 4 | sync::{atomic::AtomicBool, Arc}, | 4 | sync::{atomic::AtomicBool, Arc}, |
| 5 | }; | 5 | }; |
| 6 | 6 | ||
| 7 | pub use git_repository as git; | ||
| 7 | use git_repository::{ | 8 | use 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 | |||
| 304 | fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> { | 305 | fn 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 { |
| @@ -1,10 +1,8 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use anyhow::{Context, Result}; | 1 | use anyhow::{Context, Result}; |
| 4 | use git_repository as git; | 2 | use git_repository as git; |
| 5 | 3 | ||
| 6 | pub fn describe( | 4 | pub 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()?, |
| @@ -0,0 +1,68 @@ | |||
| 1 | use anyhow::{bail, Context}; | ||
| 2 | use std::io; | ||
| 3 | |||
| 4 | use crate::OutputFormat; | ||
| 5 | use git_repository as git; | ||
| 6 | use git_repository::prelude::FindExt; | ||
| 7 | |||
| 8 | pub mod query { | ||
| 9 | use crate::OutputFormat; | ||
| 10 | use std::ffi::OsString; | ||
| 11 | |||
| 12 | pub struct Options { | ||
| 13 | pub format: OutputFormat, | ||
| 14 | pub overrides: Vec<OsString>, | ||
| 15 | pub show_ignore_patterns: bool, | ||
| 16 | } | ||
| 17 | } | ||
| 18 | |||
| 19 | pub fn query( | ||
| 20 | repo: git::Repository, | ||
| 21 | pathspecs: impl Iterator<Item = git::path::Spec>, | ||
| 22 | mut out: impl io::Write, | ||
| 23 | query::Options { | ||
| 24 | overrides, | ||
| 25 | format, | ||
| 26 | show_ignore_patterns, | ||
| 27 | }: query::Options, | ||
| 28 | ) -> anyhow::Result<()> { | ||
| 29 | if format != OutputFormat::Human { | ||
| 30 | bail!("JSON output isn't implemented yet"); | ||
| 31 | } | ||
| 32 | |||
| 33 | let worktree = repo | ||
| 34 | .worktree() | ||
| 35 | .current() | ||
| 36 | .with_context(|| "Cannot check excludes without a current worktree")?; | ||
| 37 | let index = worktree.open_index()?; | ||
| 38 | let mut cache = worktree.excludes( | ||
| 39 | &index.state, | ||
| 40 | Some(git::attrs::MatchGroup::<git::attrs::Ignore>::from_overrides(overrides)), | ||
| 41 | )?; | ||
| 42 | |||
| 43 | let prefix = repo.prefix().expect("worktree - we have an index by now")?; | ||
| 44 | |||
| 45 | for mut spec in pathspecs { | ||
| 46 | for path in spec.apply_prefix(&prefix).items() { | ||
| 47 | // TODO: what about paths that end in /? Pathspec might handle it, it's definitely something git considers | ||
| 48 | // even if the directory doesn't exist. Seems to work as long as these are kept in the spec. | ||
| 49 | let is_dir = git::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); | ||
| 50 | let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; | ||
| 51 | let match_ = entry | ||
| 52 | .matching_exclude_pattern() | ||
| 53 | .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then(|| m)); | ||
| 54 | match match_ { | ||
| 55 | Some(m) => writeln!( | ||
| 56 | out, | ||
| 57 | "{}:{}:{}\t{}", | ||
| 58 | m.source.map(|p| p.to_string_lossy()).unwrap_or_default(), | ||
| 59 | m.sequence_number, | ||
| 60 | m.pattern, | ||
| 61 | path | ||
| 62 | )?, | ||
| 63 | None => writeln!(out, "::\t{}", path)?, | ||
| 64 | } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | Ok(()) | ||
| 68 | } | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | use std::{io, path::PathBuf}; | 1 | use std::io; |
| 2 | 2 | ||
| 3 | use git_repository as git; | 3 | use 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 | ||
| 31 | pub fn entries( | 31 | pub 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(); |
| @@ -16,3 +16,5 @@ pub mod verify; · 15 unmodified lines | |||
| 16 | pub mod odb; | 16 | pub mod odb; |
| 17 | 17 | ||
| 18 | pub mod mailmap; | 18 | pub mod mailmap; |
| 19 | |||
| 20 | pub mod exclude; | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | use std::{io, path::PathBuf}; | 1 | use std::io; |
| 2 | 2 | ||
| 3 | use anyhow::bail; | 3 | use anyhow::bail; |
| 4 | use git_repository as git; | 4 | use 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))] |
| 24 | pub fn info( | 24 | pub 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 | ||
| 52 | pub fn entries(repository: PathBuf, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> { | 51 | pub fn entries(repo: git::Repository, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> { |
| 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)?; |
| @@ -1,4 +1,4 @@ | |||
| 1 | use std::{borrow::Cow, io, path::PathBuf}; | 1 | use std::{borrow::Cow, io}; |
| 2 | 2 | ||
| 3 | use anyhow::bail; | 3 | use anyhow::bail; |
| 4 | use git_repository as git; | 4 | use 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))] |
| 119 | pub fn info( | 119 | pub 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 | ||
| 146 | pub fn entries( | 145 | pub 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 { |
| @@ -1,4 +1,4 @@ | |||
| 1 | use std::{path::PathBuf, sync::atomic::AtomicBool}; | 1 | use std::sync::atomic::AtomicBool; |
| 2 | 2 | ||
| 3 | use git_repository as git; | 3 | use git_repository as git; |
| 4 | use git_repository::Progress; | 4 | use git_repository::Progress; |
| @@ -20,7 +20,7 @@ pub struct Context { · 15 unmodified lines | |||
| 20 | pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3; | 20 | pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3; |
| 21 | 21 | ||
| 22 | pub fn integrity( | 22 | pub 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, |
| @@ -9,6 +9,7 @@ use std::{ · 8 unmodified lines | |||
| 9 | 9 | ||
| 10 | use anyhow::Result; | 10 | use anyhow::Result; |
| 11 | use clap::Parser; | 11 | use clap::Parser; |
| 12 | use git_repository::bstr::io::BufReadExt; | ||
| 12 | use gitoxide_core as core; | 13 | use gitoxide_core as core; |
| 13 | use gitoxide_core::pack::verify; | 14 | use 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 | ||
| 646 | fn stdin_or_bail() -> anyhow::Result<std::io::BufReader<std::io::Stdin>> { | ||
| 647 | if atty::is(atty::Stream::Stdin) { | ||
| 648 | anyhow::bail!("Refusing to read from standard input while a terminal is connected") | ||
| 649 | } | ||
| 650 | Ok(BufReader::new(stdin())) | ||
| 651 | } | ||
| 652 | |||
| 606 | fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode { | 653 | fn 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, |
| @@ -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 { |
| @@ -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 | ) |
| @@ -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 | + Send | 110 | + 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(); |
| @@ -13,7 +13,7 @@ use once_cell::sync::Lazy; · 12 unmodified lines | |||
| 13 | use parking_lot::Mutex; | 13 | use parking_lot::Mutex; |
| 14 | pub use tempfile; | 14 | pub use tempfile; |
| 15 | 15 | ||
| 16 | type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; | 16 | pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; |
| 17 | 17 | ||
| 18 | static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new())); | 18 | static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new())); |
| 19 | 19 | ||