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