Unified · Split
| @@ -201,9 +201,9 @@ dependencies = [ · 200 unmodified lines | ||
| 201 | 201 | |
| 202 | 202 | [[package]] |
| 203 | 203 | name = "atoi" |
| 204 | −version = "1.0.0" | |
| 204 | +version = "0.4.0" | |
| 205 | 205 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 206 | −checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" | |
| 206 | +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" | |
| 207 | 207 | dependencies = [ |
| 208 | 208 | "num-traits", |
| 209 | 209 | ] |
| @@ -503,6 +503,15 @@ version = "0.1.1" · 293 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,7 +1085,10 @@ name = "git-attributes" · 567 unmodified lines | ||
| 1076 | 1085 | version = "0.1.0" |
| 1077 | 1086 | dependencies = [ |
| 1078 | 1087 | "bstr", |
| 1088 | + "compact_str", | |
| 1089 | + "git-features", | |
| 1079 | 1090 | "git-glob", |
| 1091 | + "git-path", | |
| 1080 | 1092 | "git-quote", |
| 1081 | 1093 | "git-testtools", |
| 1082 | 1094 | "quick-error", |
| @@ -1121,15 +1133,16 @@ dependencies = [ · 38 unmodified lines | ||
| 1121 | 1133 | "criterion", |
| 1122 | 1134 | "dirs", |
| 1123 | 1135 | "git-features", |
| 1136 | + "git-path", | |
| 1124 | 1137 | "git-sec", |
| 1125 | 1138 | "memchr", |
| 1126 | 1139 | "nom", |
| 1127 | 1140 | "pwd", |
| 1128 | − "quick-error", | |
| 1129 | 1141 | "serde", |
| 1130 | 1142 | "serde_derive", |
| 1131 | 1143 | "serial_test", |
| 1132 | 1144 | "tempfile", |
| 1145 | + "thiserror", | |
| 1133 | 1146 | "unicode-bom", |
| 1134 | 1147 | ] |
| 1135 | 1148 | |
| @@ -1194,6 +1207,10 @@ dependencies = [ · 58 unmodified lines | ||
| 1194 | 1207 | "walkdir", |
| 1195 | 1208 | ] |
| 1196 | 1209 | |
| 1210 | +[[package]] | |
| 1211 | +name = "git-filter" | |
| 1212 | +version = "0.0.0" | |
| 1213 | + | |
| 1197 | 1214 | [[package]] |
| 1198 | 1215 | name = "git-glob" |
| 1199 | 1216 | version = "0.2.0" |
| @@ -1295,6 +1312,7 @@ dependencies = [ · 95 unmodified lines | ||
| 1295 | 1312 | "git-hash", |
| 1296 | 1313 | "git-object", |
| 1297 | 1314 | "git-pack", |
| 1315 | + "git-path", | |
| 1298 | 1316 | "git-quote", |
| 1299 | 1317 | "git-testtools", |
| 1300 | 1318 | "parking_lot 0.12.0", |
| @@ -1320,6 +1338,7 @@ dependencies = [ · 19 unmodified lines | ||
| 1320 | 1338 | "git-hash", |
| 1321 | 1339 | "git-object", |
| 1322 | 1340 | "git-odb", |
| 1341 | + "git-path", | |
| 1323 | 1342 | "git-tempfile", |
| 1324 | 1343 | "git-testtools", |
| 1325 | 1344 | "git-traverse", |
| @@ -1351,6 +1370,13 @@ dependencies = [ · 25 unmodified lines | ||
| 1351 | 1370 | "serde", |
| 1352 | 1371 | ] |
| 1353 | 1372 | |
| 1373 | +[[package]] | |
| 1374 | +name = "git-path" | |
| 1375 | +version = "0.1.0" | |
| 1376 | +dependencies = [ | |
| 1377 | + "bstr", | |
| 1378 | +] | |
| 1379 | + | |
| 1354 | 1380 | [[package]] |
| 1355 | 1381 | name = "git-pathspec" |
| 1356 | 1382 | version = "0.0.0" |
| @@ -1401,6 +1427,7 @@ dependencies = [ · 44 unmodified lines | ||
| 1401 | 1427 | "git-lock", |
| 1402 | 1428 | "git-object", |
| 1403 | 1429 | "git-odb", |
| 1430 | + "git-path", | |
| 1404 | 1431 | "git-tempfile", |
| 1405 | 1432 | "git-testtools", |
| 1406 | 1433 | "git-validate", |
| @@ -1420,6 +1447,7 @@ dependencies = [ · 13 unmodified lines | ||
| 1420 | 1447 | "clru", |
| 1421 | 1448 | "document-features", |
| 1422 | 1449 | "git-actor", |
| 1450 | + "git-attributes", | |
| 1423 | 1451 | "git-config", |
| 1424 | 1452 | "git-credentials", |
| 1425 | 1453 | "git-diff", |
| @@ -1432,6 +1460,7 @@ dependencies = [ · 6 unmodified lines | ||
| 1432 | 1460 | "git-object", |
| 1433 | 1461 | "git-odb", |
| 1434 | 1462 | "git-pack", |
| 1463 | + "git-path", | |
| 1435 | 1464 | "git-protocol", |
| 1436 | 1465 | "git-ref", |
| 1437 | 1466 | "git-revision", |
| @@ -1443,6 +1472,7 @@ dependencies = [ · 5 unmodified lines | ||
| 1443 | 1472 | "git-url", |
| 1444 | 1473 | "git-validate", |
| 1445 | 1474 | "git-worktree", |
| 1475 | + "is_ci", | |
| 1446 | 1476 | "log", |
| 1447 | 1477 | "signal-hook", |
| 1448 | 1478 | "tempfile", |
| @@ -1471,9 +1501,14 @@ dependencies = [ · 22 unmodified lines | ||
| 1471 | 1501 | "libc", |
| 1472 | 1502 | "serde", |
| 1473 | 1503 | "tempfile", |
| 1504 | + "thiserror", | |
| 1474 | 1505 | "windows", |
| 1475 | 1506 | ] |
| 1476 | 1507 | |
| 1508 | +[[package]] | |
| 1509 | +name = "git-sequencer" | |
| 1510 | +version = "0.0.0" | |
| 1511 | + | |
| 1477 | 1512 | [[package]] |
| 1478 | 1513 | name = "git-submodule" |
| 1479 | 1514 | version = "0.0.0" |
| @@ -1561,6 +1596,7 @@ version = "0.4.0" · 81 unmodified lines | ||
| 1561 | 1596 | dependencies = [ |
| 1562 | 1597 | "bstr", |
| 1563 | 1598 | "git-features", |
| 1599 | + "git-path", | |
| 1564 | 1600 | "home", |
| 1565 | 1601 | "quick-error", |
| 1566 | 1602 | "serde", |
| @@ -1582,11 +1618,14 @@ version = "0.1.0" · 15 unmodified lines | ||
| 1582 | 1618 | dependencies = [ |
| 1583 | 1619 | "bstr", |
| 1584 | 1620 | "document-features", |
| 1621 | + "git-attributes", | |
| 1585 | 1622 | "git-features", |
| 1623 | + "git-glob", | |
| 1586 | 1624 | "git-hash", |
| 1587 | 1625 | "git-index", |
| 1588 | 1626 | "git-object", |
| 1589 | 1627 | "git-odb", |
| 1628 | + "git-path", | |
| 1590 | 1629 | "git-testtools", |
| 1591 | 1630 | "io-close", |
| 1592 | 1631 | "serde", |
| @@ -146,9 +146,11 @@ members = [ · 145 unmodified lines | ||
| 146 | 146 | "git-packetline", |
| 147 | 147 | "git-mailmap", |
| 148 | 148 | "git-note", |
| 149 | + "git-filter", | |
| 149 | 150 | "git-sec", |
| 150 | 151 | "git-lfs", |
| 151 | 152 | "git-rebase", |
| 153 | + "git-sequencer", | |
| 152 | 154 | "git-submodule", |
| 153 | 155 | "git-transport", |
| 154 | 156 | "git-credentials", |
| @@ -159,6 +161,7 @@ members = [ · 4 unmodified lines | ||
| 159 | 161 | "git-lock", |
| 160 | 162 | "git-attributes", |
| 161 | 163 | "git-pathspec", |
| 164 | + "git-path", | |
| 162 | 165 | "git-repository", |
| 163 | 166 | "gitoxide-core", |
| 164 | 167 | "git-tui", |
| @@ -168,6 +168,12 @@ by humans. · 167 unmodified lines | ||
| 168 | 168 | Utilities to aid in keeping the project fresh and in sync can be found in the `Maintenance` section of the `makefile`. Run `make` to |
| 169 | 169 | get an overview. |
| 170 | 170 | |
| 171 | +## Reviewing PRs | |
| 172 | + | |
| 173 | +- be sure to clone locally and run tests with `GITOXIDE_TEST_IGNORE_ARCHIVES=1` to assure new fixture scripts (if there are any) are validated | |
| 174 | + on _MacOS_ and _Windows_. Note that linux doesn't need to be tested that way as CI on linux ignores them by merit of not checking them out | |
| 175 | + via `git-lfs`. | |
| 176 | + | |
| 171 | 177 | ## Creating a release |
| 172 | 178 | |
| 173 | 179 | Run `make publish-all` to publish all crates in leaf-first order using `cargo release` based on the currently set version. |
| @@ -65,7 +65,7 @@ clippy: ## Run cargo clippy on all crates · 64 unmodified lines | ||
| 65 | 65 | cargo clippy --all --no-default-features --features lean-async --tests |
| 66 | 66 | |
| 67 | 67 | check-msrv: ## run cargo msrv to validate the current msrv requirements, similar to what CI does |
| 68 | − cd git-repository && cargo msrv verify | |
| 68 | + cd git-repository && cargo +1.54.0 check --package git-repository --no-default-features --features async-network-client,unstable,local-time-support,max-performance | |
| 69 | 69 | |
| 70 | 70 | check-win: ## see that windows compiles, provided the x86_64-pc-windows-msvc target and cargo-xwin are present. |
| 71 | 71 | cargo xwin build --target x86_64-pc-windows-msvc --no-default-features --features small |
| @@ -286,7 +286,7 @@ bench-git-config: · 214 unmodified lines | ||
| 286 | 286 | check-msrv-on-ci: ## Check the minimal support rust version for currently installed Rust version |
| 287 | 287 | rustc --version |
| 288 | 288 | cargo check --package git-repository |
| 289 | − cargo check --package git-repository --no-default-features --features async-network-client | |
| 289 | + cargo check --package git-repository --no-default-features --features async-network-client,unstable,local-time-support,max-performance | |
| 290 | 290 | |
| 291 | 291 | ##@ Maintenance |
| 292 | 292 | |
| @@ -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 |
| @@ -110,6 +112,9 @@ Crates that seem feature complete and need to see some more use before they can · 56 unmodified lines | ||
| 110 | 112 | * [git-traverse](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-traverse) |
| 111 | 113 | * [git-config](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-config) |
| 112 | 114 | * [git-features](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-features) |
| 115 | + * [git-credentials](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-credentials) | |
| 116 | + * [git-sec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-sec) | |
| 117 | + * [git-quote](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-quote) | |
| 113 | 118 | * [git-ref](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-ref) |
| 114 | 119 | * [git-repository](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-repository) |
| 115 | 120 | * `gitoxide-core` |
| @@ -119,14 +124,14 @@ Crates that seem feature complete and need to see some more use before they can · 3 unmodified lines | ||
| 119 | 124 | * [git-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-bitmap) |
| 120 | 125 | * [git-revision](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-revision) |
| 121 | 126 | * [git-attributes](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-attributes) |
| 122 | − * [git-quote](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-quote) | |
| 127 | + * [git-path](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-path) | |
| 123 | 128 | * **idea** |
| 124 | − * [git-credentials](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-credentials) | |
| 125 | − * [git-sec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-sec) | |
| 126 | 129 | * [git-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-note) |
| 130 | + * [git-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-filter) | |
| 127 | 131 | * [git-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-date) |
| 128 | 132 | * [git-lfs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-lfs) |
| 129 | 133 | * [git-rebase](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-rebase) |
| 134 | + * [git-sequencer](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-sequencer) | |
| 130 | 135 | * [git-pathspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-pathspec) |
| 131 | 136 | * [git-submodule](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-submodule) |
| 132 | 137 | * [git-tui](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-tui) |
| @@ -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 |
| @@ -236,6 +243,14 @@ A mechanism to associate metadata with any object, and keep revisions of it usin · 9 unmodified lines | ||
| 236 | 243 | |
| 237 | 244 | ### git-credentials |
| 238 | 245 | * [x] launch git credentials helpers with a given action |
| 246 | + | |
| 247 | +### git-filter | |
| 248 | + | |
| 249 | +Provide base-implementations for dealing with smudge and clean filters as well as filter processes, facilitating their development. | |
| 250 | + | |
| 251 | +* [ ] clean filter base | |
| 252 | +* [ ] smudge filter base | |
| 253 | +* [ ] filter process base | |
| 239 | 254 | |
| 240 | 255 | ### git-sec |
| 241 | 256 | |
| @@ -245,6 +260,10 @@ Provides a trust model to share across gitoxide crates. It helps configuring how · 3 unmodified lines | ||
| 245 | 260 | * [ ] obtain rebase status |
| 246 | 261 | * [ ] drive a rebase operation |
| 247 | 262 | |
| 263 | +### git-sequencer | |
| 264 | + | |
| 265 | +Handle human-aided operations which cannot be completed in one command invocation. | |
| 266 | + | |
| 248 | 267 | ### git-lfs |
| 249 | 268 | |
| 250 | 269 | Implement git large file support using the process protocol and make it flexible enough to handle a variety of cases. |
| @@ -417,7 +436,8 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. · 166 unmodified lines | ||
| 417 | 436 | * **refs** |
| 418 | 437 | * [ ] run transaction hooks and handle special repository states like quarantine |
| 419 | 438 | * [ ] support for different backends like `files` and `reftable` |
| 420 | − * [ ] worktrees | |
| 439 | + * **worktrees** | |
| 440 | + * [ ] open a repository with worktrees | |
| 421 | 441 | * [ ] remotes with push and pull |
| 422 | 442 | * [x] mailmap |
| 423 | 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) |
| @@ -30,6 +31,8 @@ echo "in root: gitoxide CLI" · 5 unmodified lines | ||
| 30 | 31 | (enter git-config && indent cargo diet -n --package-size-limit 70KB) |
| 31 | 32 | (enter git-hash && indent cargo diet -n --package-size-limit 15KB) |
| 32 | 33 | (enter git-chunk && indent cargo diet -n --package-size-limit 10KB) |
| 34 | +(enter git-rebase && indent cargo diet -n --package-size-limit 5KB) | |
| 35 | +(enter git-sequencer && indent cargo diet -n --package-size-limit 5KB) | |
| 33 | 36 | (enter git-features && indent cargo diet -n --package-size-limit 50KB) |
| 34 | 37 | (enter git-ref && indent cargo diet -n --package-size-limit 50KB) |
| 35 | 38 | (enter git-diff && indent cargo diet -n --package-size-limit 10KB) |
| @@ -37,6 +40,8 @@ echo "in root: gitoxide CLI" · 1 unmodified line | ||
| 37 | 40 | (enter git-url && indent cargo diet -n --package-size-limit 15KB) |
| 38 | 41 | (enter git-validate && indent cargo diet -n --package-size-limit 5KB) |
| 39 | 42 | (enter git-date && indent cargo diet -n --package-size-limit 5KB) |
| 43 | +(enter git-filter && indent cargo diet -n --package-size-limit 5KB) | |
| 44 | +(enter git-lfs && indent cargo diet -n --package-size-limit 5KB) | |
| 40 | 45 | (enter git-note && indent cargo diet -n --package-size-limit 5KB) |
| 41 | 46 | (enter git-sec && indent cargo diet -n --package-size-limit 5KB) |
| 42 | 47 | (enter git-tix && indent cargo diet -n --package-size-limit 5KB) |
| @@ -47,6 +52,6 @@ echo "in root: gitoxide CLI" · 4 unmodified lines | ||
| 47 | 52 | (enter git-odb && indent cargo diet -n --package-size-limit 120KB) |
| 48 | 53 | (enter git-protocol && indent cargo diet -n --package-size-limit 50KB) |
| 49 | 54 | (enter git-packetline && indent cargo diet -n --package-size-limit 35KB) |
| 50 | −(enter git-repository && indent cargo diet -n --package-size-limit 90KB) | |
| 55 | +(enter git-repository && indent cargo diet -n --package-size-limit 100KB) | |
| 51 | 56 | (enter git-transport && indent cargo diet -n --package-size-limit 50KB) |
| 52 | 57 | (enter gitoxide-core && indent cargo diet -n --package-size-limit 70KB) |
| @@ -6,17 +6,20 @@ license = "MIT/Apache-2.0" · 5 unmodified lines | ||
| 6 | 6 | description = "A WIP crate of the gitoxide project dealing .gitattributes files" |
| 7 | 7 | authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] |
| 8 | 8 | edition = "2018" |
| 9 | +include = ["src/**/*", "CHANGELOG.md"] | |
| 9 | 10 | |
| 10 | 11 | [lib] |
| 11 | 12 | doctest = false |
| 12 | 13 | |
| 13 | 14 | [features] |
| 14 | 15 | ## Data structures implement `serde::Serialize` and `serde::Deserialize`. |
| 15 | −serde1 = ["serde", "bstr/serde1", "git-glob/serde1"] | |
| 16 | +serde1 = ["serde", "bstr/serde1", "git-glob/serde1", "compact_str/serde"] | |
| 16 | 17 | |
| 17 | 18 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
| 18 | 19 | |
| 19 | 20 | [dependencies] |
| 21 | +git-features = { version = "^0.20.0", path = "../git-features" } | |
| 22 | +git-path = { version = "^0.1.0", path = "../git-path" } | |
| 20 | 23 | git-quote = { version = "^0.2.0", path = "../git-quote" } |
| 21 | 24 | git-glob = { version = "^0.2.0", path = "../git-glob" } |
| 22 | 25 | |
| @@ -24,6 +27,7 @@ bstr = { version = "0.2.13", default-features = false, features = ["std"]} · 1 unmodified line | ||
| 24 | 27 | unicode-bom = "1.1.4" |
| 25 | 28 | quick-error = "2.0.0" |
| 26 | 29 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} |
| 30 | +compact_str = "0.3.2" | |
| 27 | 31 | |
| 28 | 32 | [dev-dependencies] |
| 29 | 33 | git-testtools = { path = "../tests/tools"} |
| @@ -1,11 +1,18 @@ | ||
| 1 | 1 | #![forbid(unsafe_code)] |
| 2 | 2 | #![deny(rust_2018_idioms)] |
| 3 | 3 | |
| 4 | −use bstr::BStr; | |
| 4 | +use std::path::PathBuf; | |
| 5 | 5 | |
| 6 | +use bstr::{BStr, BString}; | |
| 7 | +use compact_str::CompactStr; | |
| 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,6 +25,76 @@ pub enum State<'a> { · 6 unmodified lines | ||
| 18 | 25 | Unspecified, |
| 19 | 26 | } |
| 20 | 27 | |
| 28 | +/// The state an attribute can be in, owning the value. | |
| 29 | +/// | |
| 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. | |
| 46 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 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> { | |
| 60 | + /// A list of pattern lists, each representing a patterns from a file or specified by hand, in the order they were | |
| 61 | + /// specified in. | |
| 62 | + /// | |
| 63 | + /// During matching, this order is reversed. | |
| 64 | + pub patterns: Vec<PatternList<T>>, | |
| 65 | +} | |
| 66 | + | |
| 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)`_). | |
| 75 | + /// | |
| 76 | + /// During matching, this order is reversed. | |
| 77 | + pub patterns: Vec<PatternMapping<T::Value>>, | |
| 78 | + | |
| 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>, | |
| 82 | + | |
| 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 | +} | |
| 87 | + | |
| 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, | |
| 93 | +} | |
| 94 | + | |
| 95 | +mod match_group; | |
| 96 | +pub use match_group::{Attributes, Ignore, Match, Pattern}; | |
| 97 | + | |
| 21 | 98 | pub mod parse; |
| 22 | 99 | |
| 23 | 100 | pub fn parse(buf: &[u8]) -> parse::Lines<'_> { |
| @@ -0,0 +1,358 @@ | ||
| 1 | +use std::{ | |
| 2 | + ffi::OsString, | |
| 3 | + io::Read, | |
| 4 | + path::{Path, PathBuf}, | |
| 5 | +}; | |
| 6 | + | |
| 7 | +use bstr::{BStr, BString, ByteSlice, ByteVec}; | |
| 8 | + | |
| 9 | +use crate::{Assignment, MatchGroup, PatternList, PatternMapping, State, StateRef}; | |
| 10 | + | |
| 11 | +impl<'a> From<StateRef<'a>> for State { | |
| 12 | + fn from(s: StateRef<'a>) -> Self { | |
| 13 | + match s { | |
| 14 | + StateRef::Value(v) => State::Value(v.to_str().expect("no illformed unicode").into()), | |
| 15 | + StateRef::Set => State::Set, | |
| 16 | + StateRef::Unset => State::Unset, | |
| 17 | + StateRef::Unspecified => State::Unspecified, | |
| 18 | + } | |
| 19 | + } | |
| 20 | +} | |
| 21 | + | |
| 22 | +fn attrs_to_assignments<'a>( | |
| 23 | + attrs: impl Iterator<Item = Result<(&'a BStr, StateRef<'a>), crate::parse::Error>>, | |
| 24 | +) -> Result<Vec<Assignment>, crate::parse::Error> { | |
| 25 | + attrs | |
| 26 | + .map(|res| { | |
| 27 | + res.map(|(name, state)| Assignment { | |
| 28 | + name: name.to_str().expect("no illformed unicode").into(), | |
| 29 | + state: state.into(), | |
| 30 | + }) | |
| 31 | + }) | |
| 32 | + .collect() | |
| 33 | +} | |
| 34 | + | |
| 35 | +/// A marker trait to identify the type of a description. | |
| 36 | +pub trait Pattern: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Default { | |
| 37 | + /// The value associated with a pattern. | |
| 38 | + type Value: PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Clone; | |
| 39 | + | |
| 40 | + /// Parse all patterns in `bytes` line by line, ignoring lines with errors, and collect them. | |
| 41 | + fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>>; | |
| 42 | + | |
| 43 | + fn use_pattern(pattern: &git_glob::Pattern) -> bool; | |
| 44 | +} | |
| 45 | + | |
| 46 | +/// Identify ignore patterns. | |
| 47 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] | |
| 48 | +pub struct Ignore; | |
| 49 | + | |
| 50 | +impl Pattern for Ignore { | |
| 51 | + type Value = (); | |
| 52 | + | |
| 53 | + fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>> { | |
| 54 | + crate::parse::ignore(bytes) | |
| 55 | + .map(|(pattern, line_number)| PatternMapping { | |
| 56 | + pattern, | |
| 57 | + value: (), | |
| 58 | + sequence_number: line_number, | |
| 59 | + }) | |
| 60 | + .collect() | |
| 61 | + } | |
| 62 | + | |
| 63 | + fn use_pattern(_pattern: &git_glob::Pattern) -> bool { | |
| 64 | + true | |
| 65 | + } | |
| 66 | +} | |
| 67 | + | |
| 68 | +/// A value of an attribute pattern, which is either a macro definition or | |
| 69 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 70 | +pub enum Value { | |
| 71 | + MacroAttributes(Vec<Assignment>), | |
| 72 | + Attributes(Vec<Assignment>), | |
| 73 | +} | |
| 74 | + | |
| 75 | +/// Identify patterns with attributes. | |
| 76 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] | |
| 77 | +pub struct Attributes; | |
| 78 | + | |
| 79 | +impl Pattern for Attributes { | |
| 80 | + type Value = Value; | |
| 81 | + | |
| 82 | + fn bytes_to_patterns(bytes: &[u8]) -> Vec<PatternMapping<Self::Value>> { | |
| 83 | + crate::parse(bytes) | |
| 84 | + .filter_map(Result::ok) | |
| 85 | + .filter_map(|(pattern_kind, attrs, line_number)| { | |
| 86 | + let (pattern, value) = match pattern_kind { | |
| 87 | + crate::parse::Kind::Macro(macro_name) => ( | |
| 88 | + git_glob::Pattern { | |
| 89 | + text: macro_name, | |
| 90 | + mode: git_glob::pattern::Mode::all(), | |
| 91 | + first_wildcard_pos: None, | |
| 92 | + }, | |
| 93 | + Value::MacroAttributes(attrs_to_assignments(attrs).ok()?), | |
| 94 | + ), | |
| 95 | + crate::parse::Kind::Pattern(p) => ( | |
| 96 | + (!p.is_negative()).then(|| p)?, | |
| 97 | + Value::Attributes(attrs_to_assignments(attrs).ok()?), | |
| 98 | + ), | |
| 99 | + }; | |
| 100 | + PatternMapping { | |
| 101 | + pattern, | |
| 102 | + value, | |
| 103 | + sequence_number: line_number, | |
| 104 | + } | |
| 105 | + .into() | |
| 106 | + }) | |
| 107 | + .collect() | |
| 108 | + } | |
| 109 | + | |
| 110 | + fn use_pattern(pattern: &git_glob::Pattern) -> bool { | |
| 111 | + pattern.mode != git_glob::pattern::Mode::all() | |
| 112 | + } | |
| 113 | +} | |
| 114 | + | |
| 115 | +/// Describes a matching value within a [`MatchGroup`]. | |
| 116 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 117 | +pub struct Match<'a, T> { | |
| 118 | + pub pattern: &'a git_glob::Pattern, | |
| 119 | + /// The value associated with the pattern. | |
| 120 | + pub value: &'a T, | |
| 121 | + /// The path to the source from which the pattern was loaded, or `None` if it was specified by other means. | |
| 122 | + pub source: Option<&'a Path>, | |
| 123 | + /// The line at which the pattern was found in its `source` file, or the occurrence in which it was provided. | |
| 124 | + pub sequence_number: usize, | |
| 125 | +} | |
| 126 | + | |
| 127 | +impl<T> MatchGroup<T> | |
| 128 | +where | |
| 129 | + T: Pattern, | |
| 130 | +{ | |
| 131 | + /// Match `relative_path`, a path relative to the repository containing all patterns, and return the first match if available. | |
| 132 | + // TODO: better docs | |
| 133 | + pub fn pattern_matching_relative_path<'a>( | |
| 134 | + &self, | |
| 135 | + relative_path: impl Into<&'a BStr>, | |
| 136 | + is_dir: Option<bool>, | |
| 137 | + case: git_glob::pattern::Case, | |
| 138 | + ) -> Option<Match<'_, T::Value>> { | |
| 139 | + let relative_path = relative_path.into(); | |
| 140 | + let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); | |
| 141 | + self.patterns | |
| 142 | + .iter() | |
| 143 | + .rev() | |
| 144 | + .find_map(|pl| pl.pattern_matching_relative_path(relative_path, basename_pos, is_dir, case)) | |
| 145 | + } | |
| 146 | +} | |
| 147 | + | |
| 148 | +impl MatchGroup<Ignore> { | |
| 149 | + /// Given `git_dir`, a `.git` repository, load ignore patterns from `info/exclude` and from `excludes_file` if it | |
| 150 | + /// is provided. | |
| 151 | + /// Note that it's not considered an error if the provided `excludes_file` does not exist. | |
| 152 | + pub fn from_git_dir( | |
| 153 | + git_dir: impl AsRef<Path>, | |
| 154 | + excludes_file: Option<PathBuf>, | |
| 155 | + buf: &mut Vec<u8>, | |
| 156 | + ) -> std::io::Result<Self> { | |
| 157 | + let mut group = Self::default(); | |
| 158 | + | |
| 159 | + let follow_symlinks = true; | |
| 160 | + // order matters! More important ones first. | |
| 161 | + group.patterns.extend( | |
| 162 | + excludes_file | |
| 163 | + .map(|file| PatternList::<Ignore>::from_file(file, None, follow_symlinks, buf)) | |
| 164 | + .transpose()? | |
| 165 | + .flatten(), | |
| 166 | + ); | |
| 167 | + group.patterns.extend(PatternList::<Ignore>::from_file( | |
| 168 | + git_dir.as_ref().join("info").join("exclude"), | |
| 169 | + None, | |
| 170 | + follow_symlinks, | |
| 171 | + buf, | |
| 172 | + )?); | |
| 173 | + Ok(group) | |
| 174 | + } | |
| 175 | + | |
| 176 | + /// See [PatternList::<Ignore>::from_overrides()] for details. | |
| 177 | + pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self { | |
| 178 | + MatchGroup { | |
| 179 | + patterns: vec![PatternList::<Ignore>::from_overrides(patterns)], | |
| 180 | + } | |
| 181 | + } | |
| 182 | + | |
| 183 | + /// 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. | |
| 184 | + /// Returns true if the file was added, or false if it didn't exist. | |
| 185 | + pub fn add_patterns_file( | |
| 186 | + &mut self, | |
| 187 | + source: impl Into<PathBuf>, | |
| 188 | + follow_symlinks: bool, | |
| 189 | + root: Option<&Path>, | |
| 190 | + buf: &mut Vec<u8>, | |
| 191 | + ) -> std::io::Result<bool> { | |
| 192 | + let previous_len = self.patterns.len(); | |
| 193 | + self.patterns.extend(PatternList::<Ignore>::from_file( | |
| 194 | + source.into(), | |
| 195 | + root, | |
| 196 | + follow_symlinks, | |
| 197 | + buf, | |
| 198 | + )?); | |
| 199 | + Ok(self.patterns.len() != previous_len) | |
| 200 | + } | |
| 201 | + | |
| 202 | + pub fn add_patterns_buffer(&mut self, bytes: &[u8], source: impl Into<PathBuf>, root: Option<&Path>) { | |
| 203 | + self.patterns | |
| 204 | + .push(PatternList::<Ignore>::from_bytes(bytes, source.into(), root)); | |
| 205 | + } | |
| 206 | +} | |
| 207 | + | |
| 208 | +fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec<u8>) -> std::io::Result<bool> { | |
| 209 | + buf.clear(); | |
| 210 | + let file = if follow_symlinks { | |
| 211 | + std::fs::File::open(path) | |
| 212 | + } else { | |
| 213 | + git_features::fs::open_options_no_follow().read(true).open(path) | |
| 214 | + }; | |
| 215 | + Ok(match file { | |
| 216 | + Ok(mut file) => { | |
| 217 | + file.read_to_end(buf)?; | |
| 218 | + true | |
| 219 | + } | |
| 220 | + Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, | |
| 221 | + Err(err) => return Err(err), | |
| 222 | + }) | |
| 223 | +} | |
| 224 | + | |
| 225 | +impl<T> PatternList<T> | |
| 226 | +where | |
| 227 | + T: Pattern, | |
| 228 | +{ | |
| 229 | + /// `source` is the location of the `bytes` which represent a list of patterns line by line. | |
| 230 | + pub fn from_bytes(bytes: &[u8], source: impl Into<PathBuf>, root: Option<&Path>) -> Self { | |
| 231 | + let source = source.into(); | |
| 232 | + let patterns = T::bytes_to_patterns(bytes); | |
| 233 | + | |
| 234 | + let base = root | |
| 235 | + .and_then(|root| source.parent().expect("file").strip_prefix(root).ok()) | |
| 236 | + .and_then(|base| { | |
| 237 | + (!base.as_os_str().is_empty()).then(|| { | |
| 238 | + let mut base: BString = | |
| 239 | + git_path::to_unix_separators_on_windows(git_path::into_bstr(base)).into_owned(); | |
| 240 | + | |
| 241 | + base.push_byte(b'/'); | |
| 242 | + base | |
| 243 | + }) | |
| 244 | + }); | |
| 245 | + PatternList { | |
| 246 | + patterns, | |
| 247 | + source: Some(source), | |
| 248 | + base, | |
| 249 | + } | |
| 250 | + } | |
| 251 | + pub fn from_file( | |
| 252 | + source: impl Into<PathBuf>, | |
| 253 | + root: Option<&Path>, | |
| 254 | + follow_symlinks: bool, | |
| 255 | + buf: &mut Vec<u8>, | |
| 256 | + ) -> std::io::Result<Option<Self>> { | |
| 257 | + let source = source.into(); | |
| 258 | + Ok(read_in_full_ignore_missing(&source, follow_symlinks, buf)?.then(|| Self::from_bytes(buf, source, root))) | |
| 259 | + } | |
| 260 | +} | |
| 261 | + | |
| 262 | +impl<T> PatternList<T> | |
| 263 | +where | |
| 264 | + T: Pattern, | |
| 265 | +{ | |
| 266 | + pub fn pattern_matching_relative_path( | |
| 267 | + &self, | |
| 268 | + relative_path: &BStr, | |
| 269 | + basename_pos: Option<usize>, | |
| 270 | + is_dir: Option<bool>, | |
| 271 | + case: git_glob::pattern::Case, | |
| 272 | + ) -> Option<Match<'_, T::Value>> { | |
| 273 | + let (relative_path, basename_start_pos) = | |
| 274 | + self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?; | |
| 275 | + self.patterns | |
| 276 | + .iter() | |
| 277 | + .rev() | |
| 278 | + .filter(|pm| T::use_pattern(&pm.pattern)) | |
| 279 | + .find_map( | |
| 280 | + |PatternMapping { | |
| 281 | + pattern, | |
| 282 | + value, | |
| 283 | + sequence_number, | |
| 284 | + }| { | |
| 285 | + pattern | |
| 286 | + .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | |
| 287 | + .then(|| Match { | |
| 288 | + pattern, | |
| 289 | + value, | |
| 290 | + source: self.source.as_deref(), | |
| 291 | + sequence_number: *sequence_number, | |
| 292 | + }) | |
| 293 | + }, | |
| 294 | + ) | |
| 295 | + } | |
| 296 | + | |
| 297 | + pub fn pattern_idx_matching_relative_path( | |
| 298 | + &self, | |
| 299 | + relative_path: &BStr, | |
| 300 | + basename_pos: Option<usize>, | |
| 301 | + is_dir: Option<bool>, | |
| 302 | + case: git_glob::pattern::Case, | |
| 303 | + ) -> Option<usize> { | |
| 304 | + let (relative_path, basename_start_pos) = | |
| 305 | + self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?; | |
| 306 | + self.patterns | |
| 307 | + .iter() | |
| 308 | + .enumerate() | |
| 309 | + .rev() | |
| 310 | + .filter(|(_, pm)| T::use_pattern(&pm.pattern)) | |
| 311 | + .find_map(|(idx, pm)| { | |
| 312 | + pm.pattern | |
| 313 | + .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) | |
| 314 | + .then(|| idx) | |
| 315 | + }) | |
| 316 | + } | |
| 317 | + | |
| 318 | + fn strip_base_handle_recompute_basename_pos<'a>( | |
| 319 | + &self, | |
| 320 | + relative_path: &'a BStr, | |
| 321 | + basename_pos: Option<usize>, | |
| 322 | + ) -> Option<(&'a BStr, Option<usize>)> { | |
| 323 | + match self.base.as_deref() { | |
| 324 | + Some(base) => ( | |
| 325 | + relative_path.strip_prefix(base.as_slice())?.as_bstr(), | |
| 326 | + basename_pos.and_then(|pos| { | |
| 327 | + let pos = pos - base.len(); | |
| 328 | + (pos != 0).then(|| pos) | |
| 329 | + }), | |
| 330 | + ), | |
| 331 | + None => (relative_path, basename_pos), | |
| 332 | + } | |
| 333 | + .into() | |
| 334 | + } | |
| 335 | +} | |
| 336 | + | |
| 337 | +impl PatternList<Ignore> { | |
| 338 | + /// Parse a list of patterns, using slashes as path separators | |
| 339 | + pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self { | |
| 340 | + PatternList { | |
| 341 | + patterns: patterns | |
| 342 | + .into_iter() | |
| 343 | + .map(Into::into) | |
| 344 | + .enumerate() | |
| 345 | + .filter_map(|(seq_id, pattern)| { | |
| 346 | + let pattern = git_path::try_into_bstr(PathBuf::from(pattern)).ok()?; | |
| 347 | + git_glob::parse(pattern.as_ref()).map(|p| PatternMapping { | |
| 348 | + pattern: p, | |
| 349 | + value: (), | |
| 350 | + sequence_number: seq_id, | |
| 351 | + }) | |
| 352 | + }) | |
| 353 | + .collect(), | |
| 354 | + source: None, | |
| 355 | + base: None, | |
| 356 | + } | |
| 357 | + } | |
| 358 | +} | |
| @@ -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 +1,3 @@ | ||
| 1 | +pub use git_testtools::Result; | |
| 2 | +mod match_group; | |
| 1 | 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 | +) | |
| @@ -0,0 +1,118 @@ | ||
| 1 | +mod ignore { | |
| 2 | + use std::io::Read; | |
| 3 | + | |
| 4 | + use bstr::{BStr, ByteSlice}; | |
| 5 | + use git_attributes::{Ignore, Match, MatchGroup}; | |
| 6 | + use git_glob::pattern::Case; | |
| 7 | + | |
| 8 | + struct Expectations<'a> { | |
| 9 | + lines: bstr::Lines<'a>, | |
| 10 | + } | |
| 11 | + | |
| 12 | + impl<'a> Iterator for Expectations<'a> { | |
| 13 | + type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); | |
| 14 | + | |
| 15 | + fn next(&mut self) -> Option<Self::Item> { | |
| 16 | + let line = self.lines.next()?; | |
| 17 | + let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); | |
| 18 | + let value = value[1..].as_bstr(); | |
| 19 | + | |
| 20 | + let source_and_line = if left == b"::" { | |
| 21 | + None | |
| 22 | + } else { | |
| 23 | + let mut tokens = left.split(|b| *b == b':'); | |
| 24 | + let source = tokens.next().unwrap().as_bstr(); | |
| 25 | + let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); | |
| 26 | + let pattern = tokens.next().unwrap().as_bstr(); | |
| 27 | + Some((source, line_number, pattern)) | |
| 28 | + }; | |
| 29 | + Some((value, source_and_line)) | |
| 30 | + } | |
| 31 | + } | |
| 32 | + | |
| 33 | + #[test] | |
| 34 | + fn from_git_dir() -> crate::Result { | |
| 35 | + let dir = git_testtools::scripted_fixture_repo_read_only("make_global_and_external_and_dir_ignores.sh")?; | |
| 36 | + let repo_dir = dir.join("repo"); | |
| 37 | + let git_dir = repo_dir.join(".git"); | |
| 38 | + let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | |
| 39 | + let mut buf = Vec::new(); | |
| 40 | + let mut group = MatchGroup::from_git_dir(git_dir, Some(dir.join("user.exclude")), &mut buf)?; | |
| 41 | + | |
| 42 | + assert!( | |
| 43 | + !group.add_patterns_file("not-a-file", false, None, &mut buf)?, | |
| 44 | + "missing files are no problem and cause a negative response" | |
| 45 | + ); | |
| 46 | + assert!( | |
| 47 | + group.add_patterns_file(repo_dir.join(".gitignore"), true, repo_dir.as_path().into(), &mut buf)?, | |
| 48 | + "existing files return true" | |
| 49 | + ); | |
| 50 | + | |
| 51 | + buf.clear(); | |
| 52 | + let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); | |
| 53 | + std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?; | |
| 54 | + group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); | |
| 55 | + | |
| 56 | + for (path, source_and_line) in (Expectations { | |
| 57 | + lines: baseline.lines(), | |
| 58 | + }) { | |
| 59 | + let actual = group.pattern_matching_relative_path( | |
| 60 | + path, | |
| 61 | + repo_dir | |
| 62 | + .join(path.to_str_lossy().as_ref()) | |
| 63 | + .metadata() | |
| 64 | + .ok() | |
| 65 | + .map(|m| m.is_dir()), | |
| 66 | + Case::Sensitive, | |
| 67 | + ); | |
| 68 | + match (actual, source_and_line) { | |
| 69 | + ( | |
| 70 | + Some(Match { | |
| 71 | + sequence_number, | |
| 72 | + pattern: _, | |
| 73 | + source, | |
| 74 | + value: _, | |
| 75 | + }), | |
| 76 | + Some((expected_source, line, _expected_pattern)), | |
| 77 | + ) => { | |
| 78 | + assert_eq!(sequence_number, line, "our counting should match the one used in git"); | |
| 79 | + assert_eq!( | |
| 80 | + source.map(|p| p.canonicalize().unwrap()), | |
| 81 | + Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) | |
| 82 | + ); | |
| 83 | + } | |
| 84 | + (None, None) => {} | |
| 85 | + (actual, expected) => panic!("actual {:?} should match {:?} with path '{}'", actual, expected, path), | |
| 86 | + } | |
| 87 | + } | |
| 88 | + Ok(()) | |
| 89 | + } | |
| 90 | + | |
| 91 | + #[test] | |
| 92 | + fn from_overrides() { | |
| 93 | + let input = ["simple", "pattern/"]; | |
| 94 | + let group = git_attributes::MatchGroup::<Ignore>::from_overrides(input); | |
| 95 | + assert_eq!( | |
| 96 | + group.pattern_matching_relative_path("Simple", None, git_glob::pattern::Case::Fold), | |
| 97 | + Some(pattern_to_match(&git_glob::parse("simple").unwrap(), 0)) | |
| 98 | + ); | |
| 99 | + assert_eq!( | |
| 100 | + group.pattern_matching_relative_path("pattern", Some(true), git_glob::pattern::Case::Sensitive), | |
| 101 | + Some(pattern_to_match(&git_glob::parse("pattern/").unwrap(), 1)) | |
| 102 | + ); | |
| 103 | + assert_eq!(group.patterns.len(), 1); | |
| 104 | + assert_eq!( | |
| 105 | + git_attributes::PatternList::<Ignore>::from_overrides(input), | |
| 106 | + group.patterns.into_iter().next().unwrap() | |
| 107 | + ); | |
| 108 | + } | |
| 109 | + | |
| 110 | + fn pattern_to_match(pattern: &git_glob::Pattern, sequence_number: usize) -> Match<'_, ()> { | |
| 111 | + Match { | |
| 112 | + pattern, | |
| 113 | + value: &(), | |
| 114 | + source: None, | |
| 115 | + sequence_number, | |
| 116 | + } | |
| 117 | + } | |
| 118 | +} | |
| @@ -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 | |
| @@ -249,22 +249,22 @@ fn trailing_whitespace_in_attributes_is_ignored() { · 243 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 { |
| @@ -272,7 +272,6 @@ fn pattern(name: &str, flags: git_glob::pattern::Mode, first_wildcard_pos: Optio · 1 unmodified line | ||
| 272 | 272 | text: name.into(), |
| 273 | 273 | mode: flags, |
| 274 | 274 | first_wildcard_pos, |
| 275 | − base_path: None, | |
| 276 | 275 | }) |
| 277 | 276 | } |
| 278 | 277 | |
| @@ -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(()) |
| @@ -1,7 +1,9 @@ | ||
| 1 | −use crate::values::b; | |
| 2 | −use git_config::values::{Boolean, TrueVariant}; | |
| 3 | 1 | use std::convert::TryFrom; |
| 4 | 2 | |
| 3 | +use git_config::values::{Boolean, TrueVariant}; | |
| 4 | + | |
| 5 | +use crate::values::b; | |
| 6 | + | |
| 5 | 7 | #[test] |
| 6 | 8 | fn from_str_false() { |
| 7 | 9 | assert_eq!(Boolean::try_from(b("no")), Ok(Boolean::False("no".into()))); |
| @@ -1,6 +1,7 @@ | ||
| 1 | −use git_config::values::ColorAttribute; | |
| 2 | 1 | use std::str::FromStr; |
| 3 | 2 | |
| 3 | +use git_config::values::ColorAttribute; | |
| 4 | + | |
| 4 | 5 | #[test] |
| 5 | 6 | fn non_inverted() { |
| 6 | 7 | assert_eq!(ColorAttribute::from_str("bold"), Ok(ColorAttribute::Bold)); |
| @@ -1,6 +1,7 @@ | ||
| 1 | −use git_config::values::ColorValue; | |
| 2 | 1 | use std::str::FromStr; |
| 3 | 2 | |
| 3 | +use git_config::values::ColorValue; | |
| 4 | + | |
| 4 | 5 | #[test] |
| 5 | 6 | fn non_bright() { |
| 6 | 7 | assert_eq!(ColorValue::from_str("normal"), Ok(ColorValue::Normal)); |
| @@ -1,7 +1,9 @@ | ||
| 1 | −use crate::values::b; | |
| 2 | −use git_config::values::{Integer, IntegerSuffix}; | |
| 3 | 1 | use std::convert::TryFrom; |
| 4 | 2 | |
| 3 | +use git_config::values::{Integer, IntegerSuffix}; | |
| 4 | + | |
| 5 | +use crate::values::b; | |
| 6 | + | |
| 5 | 7 | #[test] |
| 6 | 8 | fn from_str_no_suffix() { |
| 7 | 9 | assert_eq!(Integer::try_from(b("1")).unwrap(), Integer { value: 1, suffix: None }); |
| @@ -1,6 +1,7 @@ | ||
| 1 | −use git_config::values::normalize_str; | |
| 2 | 1 | use std::borrow::Cow; |
| 3 | 2 | |
| 3 | +use git_config::values::normalize_str; | |
| 4 | + | |
| 4 | 5 | #[test] |
| 5 | 6 | fn not_modified_is_borrowed() { |
| 6 | 7 | assert_eq!(normalize_str("hello world"), Cow::Borrowed(b"hello world")); |
| @@ -1,8 +1,9 @@ | ||
| 1 | −use crate::values::b; | |
| 2 | −use git_config::values::path::interpolate; | |
| 3 | −use git_config::values::Path; | |
| 4 | 1 | use std::borrow::Cow; |
| 5 | 2 | |
| 3 | +use git_config::values::{path::interpolate, Path}; | |
| 4 | + | |
| 5 | +use crate::values::b; | |
| 6 | + | |
| 6 | 7 | #[test] |
| 7 | 8 | fn no_interpolation_for_paths_without_tilde_or_prefix() { |
| 8 | 9 | let path = &b"/foo/bar"[..]; |
| @@ -181,9 +181,10 @@ mod tests { · 180 unmodified lines | ||
| 181 | 181 | type Result = std::result::Result<(), Box<dyn std::error::Error>>; |
| 182 | 182 | |
| 183 | 183 | mod encode_message { |
| 184 | − use super::*; | |
| 185 | 184 | use bstr::ByteSlice; |
| 186 | 185 | |
| 186 | + use super::*; | |
| 187 | + | |
| 187 | 188 | #[test] |
| 188 | 189 | fn from_url() -> super::Result { |
| 189 | 190 | let mut out = Vec::new(); |
| @@ -193,9 +194,10 @@ mod tests { · 3 unmodified lines | ||
| 193 | 194 | } |
| 194 | 195 | |
| 195 | 196 | mod invalid { |
| 196 | − use super::*; | |
| 197 | 197 | use std::io; |
| 198 | 198 | |
| 199 | + use super::*; | |
| 200 | + | |
| 199 | 201 | #[test] |
| 200 | 202 | fn contains_null() { |
| 201 | 203 | assert_eq!( |
| @@ -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 | −} | |
| @@ -0,0 +1,29 @@ | ||
| 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.0.0 (2022-04-19) | |
| 9 | + | |
| 10 | +An empty crate without any content to reserve the name for the gitoxide project. | |
| 11 | + | |
| 12 | +### Commit Statistics | |
| 13 | + | |
| 14 | +<csr-read-only-do-not-edit/> | |
| 15 | + | |
| 16 | + - 1 commit contributed to the release. | |
| 17 | + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 18 | + - 1 unique issue was worked on: [#301](https://github.com/Byron/gitoxide/issues/301) | |
| 19 | + | |
| 20 | +### Commit Details | |
| 21 | + | |
| 22 | +<csr-read-only-do-not-edit/> | |
| 23 | + | |
| 24 | +<details><summary>view details</summary> | |
| 25 | + | |
| 26 | + * **[#301](https://github.com/Byron/gitoxide/issues/301)** | |
| 27 | + - add git-filter crate for name generation ([`5a3c628`](https://github.com/Byron/gitoxide/commit/5a3c628c757a7eb4ecfc5fd7265fa36c8362797b)) | |
| 28 | +</details> | |
| 29 | + | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +[package] | |
| 2 | +name = "git-filter" | |
| 3 | +version = "0.0.0" | |
| 4 | +repository = "https://github.com/Byron/gitoxide" | |
| 5 | +license = "MIT/Apache-2.0" | |
| 6 | +description = "A WIP crate of the gitoxide project implementing git filters" | |
| 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] | |
| @@ -0,0 +1 @@ | ||
| 1 | +#![forbid(unsafe_code, rust_2018_idioms)] | |
| @@ -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 { |
| @@ -16,9 +16,6 @@ pub struct Pattern { · 3 unmodified lines | ||
| 16 | 16 | pub mode: pattern::Mode, |
| 17 | 17 | /// The position in `text` with the first wildcard character, or `None` if there is no wildcard at all. |
| 18 | 18 | pub first_wildcard_pos: Option<usize>, |
| 19 | − /// The relative base at which this pattern resides, with trailing slash, using slashes as path separator. | |
| 20 | − /// If `None`, the pattern is considered to be at the root of the repository. | |
| 21 | − pub base_path: Option<BString>, | |
| 22 | 19 | } |
| 23 | 20 | |
| 24 | 21 | /// |
| @@ -31,6 +28,8 @@ pub use wildmatch::function::wildmatch; · 6 unmodified lines | ||
| 31 | 28 | mod parse; |
| 32 | 29 | |
| 33 | 30 | /// Create a [`Pattern`] by parsing `text` or return `None` if `text` is empty. |
| 34 | −pub fn parse(text: &[u8]) -> Option<Pattern> { | |
| 35 | − 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()) | |
| 36 | 35 | } |
| @@ -28,19 +28,21 @@ pub fn pattern(mut pat: &[u8]) -> Option<(BString, pattern::Mode, Option<usize>) · 27 unmodified lines | ||
| 28 | 28 | mode |= Mode::ABSOLUTE; |
| 29 | 29 | pat = &pat[1..]; |
| 30 | 30 | } |
| 31 | − let mut line = truncate_non_escaped_trailing_spaces(pat); | |
| 32 | − if line.last() == Some(&b'/') { | |
| 31 | + let mut pat = truncate_non_escaped_trailing_spaces(pat); | |
| 32 | + if pat.last() == Some(&b'/') { | |
| 33 | 33 | mode |= Mode::MUST_BE_DIR; |
| 34 | − line.pop(); | |
| 34 | + pat.pop(); | |
| 35 | 35 | } |
| 36 | − if !line.contains(&b'/') { | |
| 36 | + | |
| 37 | + if !pat.contains(&b'/') { | |
| 37 | 38 | mode |= Mode::NO_SUB_DIR; |
| 38 | 39 | } |
| 39 | − let pos_of_first_wildcard = first_wildcard_pos(&line); | |
| 40 | − if line.first() == Some(&b'*') && first_wildcard_pos(&line[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 | − Some((line, mode, pos_of_first_wildcard)) | |
| 43 | + | |
| 44 | + let pos_of_first_wildcard = first_wildcard_pos(&pat); | |
| 45 | + Some((pat, mode, pos_of_first_wildcard)) | |
| 44 | 46 | } |
| 45 | 47 | |
| 46 | 48 | fn first_wildcard_pos(pat: &[u8]) -> Option<usize> { |
| @@ -1,5 +1,7 @@ | ||
| 1 | +use std::fmt; | |
| 2 | + | |
| 1 | 3 | use bitflags::bitflags; |
| 2 | −use bstr::{BStr, BString, ByteSlice}; | |
| 4 | +use bstr::{BStr, ByteSlice}; | |
| 3 | 5 | |
| 4 | 6 | use crate::{pattern, wildmatch, Pattern}; |
| 5 | 7 | |
| @@ -36,6 +38,12 @@ pub enum Case { · 30 unmodified lines | ||
| 36 | 38 | Fold, |
| 37 | 39 | } |
| 38 | 40 | |
| 41 | +impl Default for Case { | |
| 42 | + fn default() -> Self { | |
| 43 | + Case::Sensitive | |
| 44 | + } | |
| 45 | +} | |
| 46 | + | |
| 39 | 47 | impl Pattern { |
| 40 | 48 | /// Parse the given `text` as pattern, or return `None` if `text` was empty. |
| 41 | 49 | pub fn from_bytes(text: &[u8]) -> Option<Self> { |
| @@ -43,7 +51,6 @@ impl Pattern { · 1 unmodified line | ||
| 43 | 51 | text, |
| 44 | 52 | mode, |
| 45 | 53 | first_wildcard_pos, |
| 46 | − base_path: None, | |
| 47 | 54 | }) |
| 48 | 55 | } |
| 49 | 56 | |
| @@ -52,35 +59,21 @@ impl Pattern { · 2 unmodified lines | ||
| 52 | 59 | self.mode.contains(Mode::NEGATIVE) |
| 53 | 60 | } |
| 54 | 61 | |
| 55 | − /// Set the base path of the pattern. | |
| 56 | − /// Must be a slash-separated relative path with a trailing slash. | |
| 57 | − /// | |
| 58 | − /// Use this upon creation of the pattern when the source file is known. | |
| 59 | − pub fn with_base(mut self, path: impl Into<BString>) -> Self { | |
| 60 | − let path = path.into(); | |
| 61 | − debug_assert!(path.ends_with(b"/"), "base must end with a trailing slash"); | |
| 62 | − debug_assert!(!path.starts_with(b"/"), "base must be relative"); | |
| 63 | − self.base_path = Some(path); | |
| 64 | − self | |
| 65 | − } | |
| 66 | − | |
| 67 | 62 | /// Match the given `path` which takes slashes (and only slashes) literally, and is relative to the repository root. |
| 68 | − /// Note that `path` is assumed to be relative to the repository, and that our [`base_path`][Self::base_path] | |
| 69 | − /// is assumed to contain `path`. | |
| 63 | + /// Note that `path` is assumed to be relative to the repository. | |
| 70 | 64 | /// |
| 71 | 65 | /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play. |
| 72 | 66 | /// `basename_start_pos` is the index at which the `path`'s basename starts. |
| 73 | 67 | /// |
| 74 | 68 | /// Lastly, `case` folding can be configured as well. |
| 75 | − /// | |
| 76 | − /// Note that this method uses shortcuts to accelerate simple patterns. | |
| 77 | 69 | pub fn matches_repo_relative_path<'a>( |
| 78 | 70 | &self, |
| 79 | 71 | path: impl Into<&'a BStr>, |
| 80 | 72 | basename_start_pos: Option<usize>, |
| 81 | − is_dir: bool, | |
| 73 | + is_dir: Option<bool>, | |
| 82 | 74 | case: Case, |
| 83 | 75 | ) -> bool { |
| 76 | + let is_dir = is_dir.unwrap_or(false); | |
| 84 | 77 | if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) { |
| 85 | 78 | return false; |
| 86 | 79 | } |
| @@ -96,32 +89,12 @@ impl Pattern { · 9 unmodified lines | ||
| 96 | 89 | path.rfind_byte(b'/').map(|p| p + 1), |
| 97 | 90 | "BUG: invalid cached basename_start_pos provided" |
| 98 | 91 | ); |
| 99 | − debug_assert!( | |
| 100 | − self.base_path | |
| 101 | − .as_ref() | |
| 102 | − .map(|base| path.starts_with(base)) | |
| 103 | − .unwrap_or(true), | |
| 104 | − "repo-relative paths must be pre-filtered to match our base." | |
| 105 | − ); | |
| 92 | + debug_assert!(!path.starts_with(b"/"), "input path must be relative"); | |
| 106 | 93 | |
| 107 | − if self.mode.contains(pattern::Mode::NO_SUB_DIR) { | |
| 108 | − let basename = if self.mode.contains(pattern::Mode::ABSOLUTE) { | |
| 109 | − self.base_path | |
| 110 | − .as_ref() | |
| 111 | − .and_then(|base| path.strip_prefix(base.as_slice()).map(|b| b.as_bstr())) | |
| 112 | − .unwrap_or(path) | |
| 113 | − } else { | |
| 114 | − &path[basename_start_pos.unwrap_or_default()..] | |
| 115 | − }; | |
| 94 | + if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) { | |
| 95 | + let basename = &path[basename_start_pos.unwrap_or_default()..]; | |
| 116 | 96 | self.matches(basename, flags) |
| 117 | 97 | } else { |
| 118 | − let path = match self.base_path.as_ref() { | |
| 119 | − Some(base) => match path.strip_prefix(base.as_slice()) { | |
| 120 | − Some(path) => path.as_bstr(), | |
| 121 | − None => return false, | |
| 122 | − }, | |
| 123 | − None => path, | |
| 124 | − }; | |
| 125 | 98 | self.matches(path, flags) |
| 126 | 99 | } |
| 127 | 100 | } |
| @@ -132,7 +105,7 @@ impl Pattern { · 4 unmodified lines | ||
| 132 | 105 | /// strings with cases ignored as well. Note that the case folding performed here is ASCII only. |
| 133 | 106 | /// |
| 134 | 107 | /// Note that this method uses some shortcuts to accelerate simple patterns. |
| 135 | − pub fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool { | |
| 108 | + fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool { | |
| 136 | 109 | let value = value.into(); |
| 137 | 110 | match self.first_wildcard_pos { |
| 138 | 111 | // "*literal" case, overrides starts-with |
| @@ -171,3 +144,19 @@ impl Pattern { · 32 unmodified lines | ||
| 171 | 144 | } |
| 172 | 145 | } |
| 173 | 146 | } |
| 147 | + | |
| 148 | +impl fmt::Display for Pattern { | |
| 149 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| 150 | + if self.mode.contains(Mode::NEGATIVE) { | |
| 151 | + "!".fmt(f)?; | |
| 152 | + } | |
| 153 | + if self.mode.contains(Mode::ABSOLUTE) { | |
| 154 | + "/".fmt(f)?; | |
| 155 | + } | |
| 156 | + self.text.fmt(f)?; | |
| 157 | + if self.mode.contains(Mode::MUST_BE_DIR) { | |
| 158 | + "/".fmt(f)?; | |
| 159 | + } | |
| 160 | + Ok(()) | |
| 161 | + } | |
| 162 | +} | |
| @@ -113,7 +113,8 @@ pub(crate) mod function { · 112 unmodified lines | ||
| 113 | 113 | }; |
| 114 | 114 | } |
| 115 | 115 | Some((next_p_idx, next_p_ch)) => { |
| 116 | − (p_idx, p_ch) = (next_p_idx, next_p_ch); | |
| 116 | + p_idx = next_p_idx; | |
| 117 | + p_ch = next_p_ch; | |
| 117 | 118 | if !match_slash && p_ch == SLASH { |
| 118 | 119 | match text[t_idx..].find_byte(SLASH) { |
| 119 | 120 | Some(distance_to_slash) => { |
| @@ -141,8 +142,11 @@ pub(crate) mod function { · 21 unmodified lines | ||
| 141 | 142 | if (!match_slash && t_ch == SLASH) || t_ch == p_ch { |
| 142 | 143 | break; |
| 143 | 144 | } |
| 144 | − (t_idx, t_ch) = match t.next() { | |
| 145 | − Some(t) => (t.0, t.1), | |
| 145 | + match t.next() { | |
| 146 | + Some(t) => { | |
| 147 | + t_idx = t.0; | |
| 148 | + t_ch = t.1; | |
| 149 | + } | |
| 146 | 150 | None => break, |
| 147 | 151 | }; |
| 148 | 152 | } |
| @@ -158,15 +162,21 @@ pub(crate) mod function { · 9 unmodified lines | ||
| 158 | 162 | } else if !match_slash && t_ch == SLASH { |
| 159 | 163 | return AbortToStarStar; |
| 160 | 164 | } |
| 161 | − (t_idx, t_ch) = match t.next() { | |
| 162 | − Some(t) => (t.0, t.1), | |
| 165 | + match t.next() { | |
| 166 | + Some(t) => { | |
| 167 | + t_idx = t.0; | |
| 168 | + t_ch = t.1; | |
| 169 | + } | |
| 163 | 170 | None => break AbortAll, |
| 164 | 171 | }; |
| 165 | 172 | }; |
| 166 | 173 | } |
| 167 | 174 | BRACKET_OPEN => { |
| 168 | − (p_idx, p_ch) = match p.next() { | |
| 169 | − Some(t) => t, | |
| 175 | + match p.next() { | |
| 176 | + Some(t) => { | |
| 177 | + p_idx = t.0; | |
| 178 | + p_ch = t.1; | |
| 179 | + } | |
| 170 | 180 | None => return AbortAll, |
| 171 | 181 | }; |
| 172 | 182 | |
| @@ -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,304 +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 basename_matches_from_end() { | |
| 125 | − let pat = &pat("foo"); | |
| 126 | − assert!(match_file(pat, "FoO", Case::Fold)); | |
| 127 | − assert!(!match_file(pat, "FoOo", Case::Fold)); | |
| 128 | − assert!(!match_file(pat, "Foo", Case::Sensitive)); | |
| 129 | − assert!(match_file(pat, "foo", Case::Sensitive)); | |
| 130 | − assert!(!match_file(pat, "Foo", Case::Sensitive)); | |
| 131 | − assert!(!match_file(pat, "barfoo", Case::Sensitive)); | |
| 132 | −} | |
| 133 | − | |
| 134 | −#[test] | |
| 135 | −#[should_panic] | |
| 136 | −fn base_path_must_match_or_panic_occours_in_debug_mode() { | |
| 137 | − let pat = pat("foo").with_base("base/"); | |
| 138 | − match_file(&pat, "other/FoO", Case::Fold); | |
| 139 | −} | |
| 140 | − | |
| 141 | −#[test] | |
| 142 | −fn absolute_basename_matches_only_from_beginning() { | |
| 143 | − let mut pattern = pat("/foo"); | |
| 144 | − let pat = &pattern; | |
| 145 | − assert!(match_file(pat, "FoO", Case::Fold)); | |
| 146 | − assert!(!match_file(pat, "bar/Foo", Case::Fold)); | |
| 147 | − assert!(match_file(pat, "foo", Case::Sensitive)); | |
| 148 | − assert!(!match_file(pat, "Foo", Case::Sensitive)); | |
| 149 | − assert!(!match_file(pat, "bar/foo", Case::Sensitive)); | |
| 150 | − | |
| 151 | − pattern = pattern.with_base("base/"); | |
| 152 | − let pat = &pattern; | |
| 153 | − assert!(match_file(pat, "base/FoO", Case::Fold)); | |
| 154 | − assert!(!match_file(pat, "base/bar/Foo", Case::Fold)); | |
| 155 | − assert!(match_file(pat, "base/foo", Case::Sensitive)); | |
| 156 | − assert!(!match_file(pat, "base/Foo", Case::Sensitive)); | |
| 157 | − assert!(!match_file(pat, "base/bar/foo", Case::Sensitive)); | |
| 158 | −} | |
| 159 | − | |
| 160 | −#[test] | |
| 161 | −fn absolute_path_matches_only_from_beginning() { | |
| 162 | − let mut pattern = pat("/bar/foo"); | |
| 163 | − let pat = &pattern; | |
| 164 | − assert!(!match_file(pat, "FoO", Case::Fold)); | |
| 165 | − assert!(match_file(pat, "bar/Foo", Case::Fold)); | |
| 166 | − assert!(!match_file(pat, "foo", Case::Sensitive)); | |
| 167 | − assert!(match_file(pat, "bar/foo", Case::Sensitive)); | |
| 168 | − assert!(!match_file(pat, "bar/Foo", Case::Sensitive)); | |
| 169 | − | |
| 170 | − pattern = pattern.with_base("base/"); | |
| 171 | − let pat = &pattern; | |
| 172 | − assert!(!match_file(pat, "base/FoO", Case::Fold)); | |
| 173 | − assert!(match_file(pat, "base/bar/Foo", Case::Fold)); | |
| 174 | − assert!(!match_file(pat, "base/foo", Case::Sensitive)); | |
| 175 | − assert!(match_file(pat, "base/bar/foo", Case::Sensitive)); | |
| 176 | − assert!(!match_file(pat, "base/bar/Foo", Case::Sensitive)); | |
| 177 | −} | |
| 178 | − | |
| 179 | −#[test] | |
| 180 | −fn absolute_path_with_recursive_glob_detects_mismatches_quickly() { | |
| 181 | − let mut pattern = pat("/bar/foo/**"); | |
| 182 | − let pat = &pattern; | |
| 183 | − assert!(!match_file(pat, "FoO", Case::Fold)); | |
| 184 | − assert!(!match_file(pat, "bar/Fooo", Case::Fold)); | |
| 185 | − assert!(!match_file(pat, "baz/bar/Foo", Case::Fold)); | |
| 186 | − | |
| 187 | − pattern = pattern.with_base("base/"); | |
| 188 | − let pat = &pattern; | |
| 189 | − assert!(!match_file(pat, "base/FoO", Case::Fold)); | |
| 190 | − assert!(!match_file(pat, "base/bar/Fooo", Case::Fold)); | |
| 191 | − assert!(!match_file(pat, "base/baz/bar/foo", Case::Sensitive)); | |
| 192 | −} | |
| 193 | − | |
| 194 | −#[test] | |
| 195 | −fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() { | |
| 196 | − let mut pattern = pat("/bar/foo/**"); | |
| 197 | − let pat = &pattern; | |
| 198 | − assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive)); | |
| 199 | − assert!(match_file(pat, "bar/Foo/match", Case::Fold)); | |
| 200 | − | |
| 201 | − pattern = pattern.with_base("base/"); | |
| 202 | − let pat = &pattern; | |
| 203 | − assert!(!match_file(pat, "base/bar/Foo/match", Case::Sensitive)); | |
| 204 | − assert!(match_file(pat, "base/bar/Foo/match", Case::Fold)); | |
| 205 | −} | |
| 206 | − | |
| 207 | −#[test] | |
| 208 | −fn relative_path_does_not_match_from_end() { | |
| 209 | − let pattern = &pat("bar/foo"); | |
| 210 | − assert!(!match_file(pattern, "FoO", Case::Fold)); | |
| 211 | − assert!(match_file(pattern, "bar/Foo", Case::Fold)); | |
| 212 | − assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold)); | |
| 213 | − assert!(!match_file(pattern, "foo", Case::Sensitive)); | |
| 214 | − assert!(match_file(pattern, "bar/foo", Case::Sensitive)); | |
| 215 | − assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive)); | |
| 216 | − assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive)); | |
| 217 | −} | |
| 218 | − | |
| 219 | −#[test] | |
| 220 | −fn basename_glob_and_literal_is_ends_with() { | |
| 221 | − let pattern = &pat("*foo"); | |
| 222 | − assert!(match_file(pattern, "FoO", Case::Fold)); | |
| 223 | − assert!(match_file(pattern, "BarFoO", Case::Fold)); | |
| 224 | − assert!(!match_file(pattern, "BarFoOo", Case::Fold)); | |
| 225 | − assert!(!match_file(pattern, "Foo", Case::Sensitive)); | |
| 226 | − assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); | |
| 227 | − assert!(match_file(pattern, "barfoo", Case::Sensitive)); | |
| 228 | − assert!(!match_file(pattern, "barfooo", Case::Sensitive)); | |
| 229 | − | |
| 230 | − assert!(match_file(pattern, "bar/foo", Case::Sensitive)); | |
| 231 | − assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive)); | |
| 232 | −} | |
| 233 | − | |
| 234 | −#[test] | |
| 235 | −fn special_cases_from_corpus() { | |
| 236 | − let pattern = &pat("foo*bar"); | |
| 237 | − assert!( | |
| 238 | − !match_file(pattern, "foo/baz/bar", Case::Sensitive), | |
| 239 | − "asterisk does not match path separators" | |
| 240 | − ); | |
| 241 | − let pattern = &pat("*some/path/to/hello.txt"); | |
| 242 | − assert!( | |
| 243 | − !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive), | |
| 244 | − "asterisk doesn't match path separators" | |
| 245 | − ); | |
| 246 | − | |
| 247 | − let pattern = &pat("/*foo.txt"); | |
| 248 | − assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive)); | |
| 249 | − assert!( | |
| 250 | − !match_file(pattern, "hello/foo.txt", Case::Sensitive), | |
| 251 | − "absolute single asterisk doesn't match paths" | |
| 252 | − ); | |
| 253 | −} | |
| 254 | − | |
| 255 | −#[test] | |
| 256 | −fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() { | |
| 257 | − let pattern = &pat("/*foo"); | |
| 258 | − | |
| 259 | − assert!(match_file(pattern, "FoO", Case::Fold)); | |
| 260 | − assert!(match_file(pattern, "BarFoO", Case::Fold)); | |
| 261 | − assert!(!match_file(pattern, "BarFoOo", Case::Fold)); | |
| 262 | − assert!(!match_file(pattern, "Foo", Case::Sensitive)); | |
| 263 | − assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); | |
| 264 | − assert!(match_file(pattern, "barfoo", Case::Sensitive)); | |
| 265 | − assert!(!match_file(pattern, "barfooo", Case::Sensitive)); | |
| 266 | −} | |
| 267 | − | |
| 268 | −#[test] | |
| 269 | −fn absolute_basename_glob_and_literal_is_glob_in_paths() { | |
| 270 | − let pattern = &pat("/*foo"); | |
| 271 | − | |
| 272 | − assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /"); | |
| 273 | − assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive)); | |
| 274 | −} | |
| 275 | − | |
| 276 | −#[test] | |
| 277 | −fn negated_patterns_are_handled_by_caller() { | |
| 278 | − let pattern = &pat("!foo"); | |
| 279 | − assert!( | |
| 280 | − match_file(pattern, "foo", Case::Sensitive), | |
| 281 | − "negative patterns match like any other" | |
| 282 | − ); | |
| 283 | − assert!( | |
| 284 | − pattern.is_negative(), | |
| 285 | − "the caller checks for the negative flag and acts accordingly" | |
| 286 | − ); | |
| 287 | −} | |
| 288 | − | |
| 289 | −fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern { | |
| 290 | − git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works") | |
| 291 | −} | |
| 292 | − | |
| 293 | −fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool { | |
| 294 | − match_path(pattern, path, false, case) | |
| 295 | −} | |
| 296 | − | |
| 297 | −fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: bool, case: Case) -> bool { | |
| 298 | − let path = path.into(); | |
| 299 | − pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case) | |
| 300 | −} | |
| 301 | − | |
| 302 | −fn basename_start_pos(value: &BStr) -> Option<usize> { | |
| 303 | − value.rfind_byte(b'/').map(|pos| pos + 1) | |
| 304 | −} | |
| @@ -1,5 +1,4 @@ | ||
| 1 | −use git_glob::pattern::Mode; | |
| 2 | −use git_glob::Pattern; | |
| 1 | +use git_glob::{pattern::Mode, Pattern}; | |
| 3 | 2 | |
| 4 | 3 | #[test] |
| 5 | 4 | fn mark_ends_with_pattern_specifically() { |
| @@ -32,7 +31,6 @@ fn pat(pattern: &str, mode: Mode, first_glob_char_pos: Option<usize>) -> Option< · 26 unmodified lines | ||
| 32 | 31 | text: pattern.into(), |
| 33 | 32 | mode, |
| 34 | 33 | first_wildcard_pos: first_glob_char_pos, |
| 35 | − base_path: None, | |
| 36 | 34 | }) |
| 37 | 35 | } |
| 38 | 36 | |
| @@ -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,18 @@ | ||
| 1 | +use git_glob::{pattern::Mode, Pattern}; | |
| 2 | + | |
| 3 | +#[test] | |
| 4 | +fn display() { | |
| 5 | + fn pat(text: &str, mode: Mode) -> String { | |
| 6 | + Pattern { | |
| 7 | + text: text.into(), | |
| 8 | + mode, | |
| 9 | + first_wildcard_pos: None, | |
| 10 | + } | |
| 11 | + .to_string() | |
| 12 | + } | |
| 13 | + assert_eq!(pat("a", Mode::ABSOLUTE), "/a"); | |
| 14 | + assert_eq!(pat("a", Mode::MUST_BE_DIR), "a/"); | |
| 15 | + assert_eq!(pat("a", Mode::NEGATIVE), "!a"); | |
| 16 | + assert_eq!(pat("a", Mode::ABSOLUTE | Mode::NEGATIVE | Mode::MUST_BE_DIR), "!/a/"); | |
| 17 | +} | |
| 18 | +mod matching; | |
| @@ -1,7 +1,10 @@ | ||
| 1 | −use git_glob::pattern::Case; | |
| 2 | −use git_glob::{wildmatch, Pattern}; | |
| 3 | −use std::fmt::{Debug, Display, Formatter}; | |
| 4 | −use std::panic::catch_unwind; | |
| 1 | +use std::{ | |
| 2 | + fmt::{Debug, Display, Formatter}, | |
| 3 | + panic::catch_unwind, | |
| 4 | +}; | |
| 5 | + | |
| 6 | +use bstr::ByteSlice; | |
| 7 | +use git_glob::{pattern::Case, wildmatch, Pattern}; | |
| 5 | 8 | |
| 6 | 9 | #[test] |
| 7 | 10 | fn corpus() { |
| @@ -220,6 +223,7 @@ fn corpus() { · 212 unmodified lines | ||
| 220 | 223 | } |
| 221 | 224 | } |
| 222 | 225 | |
| 226 | + dbg!(&failures); | |
| 223 | 227 | assert_eq!(failures.len(), 0); |
| 224 | 228 | assert_eq!(at_least_one_panic, 0, "not a single panic in any invocation"); |
| 225 | 229 | |
| @@ -247,8 +251,10 @@ fn multi_match(pattern_text: &str, text: &str) -> (Pattern, MultiMatch) { · 21 unmodified lines | ||
| 247 | 251 | let pattern = git_glob::Pattern::from_bytes(pattern_text.as_bytes()).expect("valid (enough) pattern"); |
| 248 | 252 | let actual_path_match: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Sensitive)).into(); |
| 249 | 253 | let actual_path_imatch: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Fold)).into(); |
| 250 | − let actual_glob_match: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::empty())).into(); | |
| 251 | − 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(); | |
| 252 | 258 | let actual = MultiMatch { |
| 253 | 259 | path_match: actual_path_match, |
| 254 | 260 | path_imatch: actual_path_imatch, |
| @@ -361,7 +367,7 @@ impl Display for MatchResult { · 106 unmodified lines | ||
| 361 | 367 | } |
| 362 | 368 | |
| 363 | 369 | fn match_file_path(pattern: &git_glob::Pattern, path: &str, case: Case) -> bool { |
| 364 | − 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) | |
| 365 | 371 | } |
| 366 | 372 | fn basename_of(path: &str) -> Option<usize> { |
| 367 | 373 | path.rfind('/').map(|pos| pos + 1) |
| @@ -44,7 +44,7 @@ bstr = { version = "0.2.13", default-features = false } · 43 unmodified lines | ||
| 44 | 44 | |
| 45 | 45 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } |
| 46 | 46 | smallvec = "1.7.0" |
| 47 | −atoi = "1.0.0" | |
| 47 | +atoi = "0.4.0" # TODO: use 1.0 once edition 2021 is supported in MSRV | |
| 48 | 48 | bitflags = "1.3.2" |
| 49 | 49 | |
| 50 | 50 | document-features = { version = "0.2.0", optional = true } |
| @@ -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, |
| @@ -17,9 +17,8 @@ · 16 unmodified lines | ||
| 17 | 17 | |
| 18 | 18 | use std::path::PathBuf; |
| 19 | 19 | |
| 20 | −use git_tempfile::handle::{Closed, Writable}; | |
| 21 | − | |
| 22 | 20 | pub use git_tempfile as tempfile; |
| 21 | +use git_tempfile::handle::{Closed, Writable}; | |
| 23 | 22 | |
| 24 | 23 | const DOT_LOCK_SUFFIX: &str = ".lock"; |
| 25 | 24 | |
| @@ -77,7 +77,6 @@ impl<'a> CommitRefIter<'a> { · 76 unmodified lines | ||
| 77 | 77 | } |
| 78 | 78 | |
| 79 | 79 | /// Returns the committer signature if there is no decoding error. |
| 80 | − /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not. | |
| 81 | 80 | pub fn committer(mut self) -> Result<git_actor::SignatureRef<'a>, crate::decode::Error> { |
| 82 | 81 | self.find_map(|t| match t { |
| 83 | 82 | Ok(Token::Committer { signature }) => Some(Ok(signature)), |
| @@ -90,7 +89,6 @@ impl<'a> CommitRefIter<'a> { · 6 unmodified lines | ||
| 90 | 89 | /// Returns the author signature if there is no decoding error. |
| 91 | 90 | /// |
| 92 | 91 | /// It may contain white space surrounding it, and is exactly as parsed. |
| 93 | − /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not. | |
| 94 | 92 | pub fn author(mut self) -> Result<git_actor::SignatureRef<'a>, crate::decode::Error> { |
| 95 | 93 | self.find_map(|t| match t { |
| 96 | 94 | Ok(Token::Author { signature }) => Some(Ok(signature)), |
| @@ -104,7 +102,6 @@ impl<'a> CommitRefIter<'a> { · 7 unmodified lines | ||
| 104 | 102 | /// |
| 105 | 103 | /// It may contain white space surrounding it, and is exactly as |
| 106 | 104 | // parsed. |
| 107 | − /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not. | |
| 108 | 105 | pub fn message(mut self) -> Result<&'a BStr, crate::decode::Error> { |
| 109 | 106 | self.find_map(|t| match t { |
| 110 | 107 | Ok(Token::Message(msg)) => Some(Ok(msg)), |
| @@ -148,6 +148,7 @@ pub struct TagRef<'a> { · 147 unmodified lines | ||
| 148 | 148 | |
| 149 | 149 | /// Like [`TagRef`], but as `Iterator` to support entirely allocation free parsing. |
| 150 | 150 | /// It's particularly useful to dereference only the target chain. |
| 151 | +#[derive(Copy, Clone)] | |
| 151 | 152 | pub struct TagRefIter<'a> { |
| 152 | 153 | data: &'a [u8], |
| 153 | 154 | state: tag::ref_iter::State, |
| @@ -9,6 +9,7 @@ use nom::{ · 8 unmodified lines | ||
| 9 | 9 | |
| 10 | 10 | use crate::{bstr::ByteSlice, parse, parse::NL, tag::decode, Kind, TagRefIter}; |
| 11 | 11 | |
| 12 | +#[derive(Copy, Clone)] | |
| 12 | 13 | pub(crate) enum State { |
| 13 | 14 | Target, |
| 14 | 15 | TargetKind, |
| @@ -39,10 +40,21 @@ impl<'a> TagRefIter<'a> { · 24 unmodified lines | ||
| 39 | 40 | /// Errors are coerced into options, hiding whether there was an error or not. The caller should assume an error if they |
| 40 | 41 | /// call the method as intended. Such a squelched error cannot be recovered unless the objects data is retrieved and parsed again. |
| 41 | 42 | /// `next()`. |
| 42 | − pub fn target_id(&mut self) -> Result<ObjectId, crate::decode::Error> { | |
| 43 | + pub fn target_id(mut self) -> Result<ObjectId, crate::decode::Error> { | |
| 43 | 44 | let token = self.next().ok_or_else(missing_field)??; |
| 44 | 45 | Token::into_id(token).ok_or_else(missing_field) |
| 45 | 46 | } |
| 47 | + | |
| 48 | + /// Returns the taggers signature if there is no decoding error, and if this field exists. | |
| 49 | + /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not. | |
| 50 | + pub fn tagger(mut self) -> Result<Option<git_actor::SignatureRef<'a>>, crate::decode::Error> { | |
| 51 | + self.find_map(|t| match t { | |
| 52 | + Ok(Token::Tagger(signature)) => Some(Ok(signature)), | |
| 53 | + Err(err) => Some(Err(err)), | |
| 54 | + _ => None, | |
| 55 | + }) | |
| 56 | + .ok_or_else(missing_field)? | |
| 57 | + } | |
| 46 | 58 | } |
| 47 | 59 | |
| 48 | 60 | fn missing_field() -> crate::decode::Error { |
| @@ -26,17 +26,21 @@ mod iter { · 25 unmodified lines | ||
| 26 | 26 | |
| 27 | 27 | #[test] |
| 28 | 28 | fn empty() -> crate::Result { |
| 29 | + let tag = fixture_bytes("tag", "empty.txt"); | |
| 30 | + let tag_iter = TagRefIter::from_bytes(&tag); | |
| 31 | + let target_id = hex_to_id("01dd4e2a978a9f5bd773dae6da7aa4a5ac1cdbbc"); | |
| 32 | + let tagger = Some(signature(1592381636)); | |
| 29 | 33 | assert_eq!( |
| 30 | − TagRefIter::from_bytes(&fixture_bytes("tag", "empty.txt")).collect::<Result<Vec<_>, _>>()?, | |
| 34 | + tag_iter.collect::<Result<Vec<_>, _>>()?, | |
| 31 | 35 | vec![ |
| 32 | − Token::Target { | |
| 33 | − id: hex_to_id("01dd4e2a978a9f5bd773dae6da7aa4a5ac1cdbbc") | |
| 34 | − }, | |
| 36 | + Token::Target { id: target_id }, | |
| 35 | 37 | Token::TargetKind(Kind::Commit), |
| 36 | 38 | Token::Name(b"empty".as_bstr()), |
| 37 | − Token::Tagger(Some(signature(1592381636))), | |
| 39 | + Token::Tagger(tagger), | |
| 38 | 40 | ] |
| 39 | 41 | ); |
| 42 | + assert_eq!(tag_iter.target_id()?, target_id); | |
| 43 | + assert_eq!(tag_iter.tagger()?, tagger); | |
| 40 | 44 | Ok(()) |
| 41 | 45 | } |
| 42 | 46 | |
| @@ -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,198 @@ | ||
| 1 | +use std::{ | |
| 2 | + borrow::Cow, | |
| 3 | + ffi::OsStr, | |
| 4 | + path::{Path, PathBuf}, | |
| 5 | +}; | |
| 6 | + | |
| 7 | +use bstr::{BStr, BString}; | |
| 8 | + | |
| 9 | +#[derive(Debug)] | |
| 10 | +/// The error type returned by [`into_bstr()`] and others may suffer from failed conversions from or to bytes. | |
| 11 | +pub struct Utf8Error; | |
| 12 | + | |
| 13 | +impl std::fmt::Display for Utf8Error { | |
| 14 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 15 | + f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input") | |
| 16 | + } | |
| 17 | +} | |
| 18 | + | |
| 19 | +impl std::error::Error for Utf8Error {} | |
| 20 | + | |
| 21 | +/// Like [`into_bstr()`], but takes `OsStr` as input for a lossless, but fallible, conversion. | |
| 22 | +pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, Utf8Error> { | |
| 23 | + let path = try_into_bstr(Cow::Borrowed(path.as_ref()))?; | |
| 24 | + match path { | |
| 25 | + Cow::Borrowed(path) => Ok(path), | |
| 26 | + Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"), | |
| 27 | + } | |
| 28 | +} | |
| 29 | + | |
| 30 | +/// Convert the given path either into its raw bytes on unix or its UTF8 encoded counterpart on windows. | |
| 31 | +/// | |
| 32 | +/// On windows, if the source Path contains ill-formed, lone surrogates, the UTF-8 conversion will fail | |
| 33 | +/// causing `Utf8Error` to be returned. | |
| 34 | +pub fn try_into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, BStr>, Utf8Error> { | |
| 35 | + let path = path.into(); | |
| 36 | + let path_str = match path { | |
| 37 | + Cow::Owned(path) => Cow::Owned({ | |
| 38 | + #[cfg(unix)] | |
| 39 | + let p: BString = { | |
| 40 | + use std::os::unix::ffi::OsStringExt; | |
| 41 | + path.into_os_string().into_vec().into() | |
| 42 | + }; | |
| 43 | + #[cfg(not(unix))] | |
| 44 | + let p: BString = path.into_os_string().into_string().map_err(|_| Utf8Error)?.into(); | |
| 45 | + p | |
| 46 | + }), | |
| 47 | + Cow::Borrowed(path) => Cow::Borrowed({ | |
| 48 | + #[cfg(unix)] | |
| 49 | + let p: &BStr = { | |
| 50 | + use std::os::unix::ffi::OsStrExt; | |
| 51 | + path.as_os_str().as_bytes().into() | |
| 52 | + }; | |
| 53 | + #[cfg(not(unix))] | |
| 54 | + let p: &BStr = path.to_str().ok_or(Utf8Error)?.as_bytes().into(); | |
| 55 | + p | |
| 56 | + }), | |
| 57 | + }; | |
| 58 | + Ok(path_str) | |
| 59 | +} | |
| 60 | + | |
| 61 | +/// Similar to [`try_into_bstr()`] but **panics** if malformed surrogates are encountered on windows. | |
| 62 | +pub fn into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> { | |
| 63 | + try_into_bstr(path).expect("prefix path doesn't contain ill-formed UTF-8") | |
| 64 | +} | |
| 65 | + | |
| 66 | +/// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on unix. | |
| 67 | +/// | |
| 68 | +/// On windows, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. There are some potential | |
| 69 | +/// git versions and windows installation which produce mal-formed UTF-16 if certain emojies are in the path. It's as rare as | |
| 70 | +/// it sounds, but possible. | |
| 71 | +pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> { | |
| 72 | + #[cfg(unix)] | |
| 73 | + let p = { | |
| 74 | + use std::os::unix::ffi::OsStrExt; | |
| 75 | + OsStr::from_bytes(input).as_ref() | |
| 76 | + }; | |
| 77 | + #[cfg(not(unix))] | |
| 78 | + let p = Path::new(std::str::from_utf8(input).map_err(|_| Utf8Error)?); | |
| 79 | + Ok(p) | |
| 80 | +} | |
| 81 | + | |
| 82 | +/// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. | |
| 83 | +pub fn try_from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Result<Cow<'a, Path>, Utf8Error> { | |
| 84 | + let input = input.into(); | |
| 85 | + match input { | |
| 86 | + Cow::Borrowed(input) => try_from_byte_slice(input).map(Cow::Borrowed), | |
| 87 | + Cow::Owned(input) => try_from_bstring(input).map(Cow::Owned), | |
| 88 | + } | |
| 89 | +} | |
| 90 | + | |
| 91 | +/// Similar to [`try_from_bstr()`], but **panics** if malformed surrogates are encountered on windows. | |
| 92 | +pub fn from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Cow<'a, Path> { | |
| 93 | + try_from_bstr(input).expect("prefix path doesn't contain ill-formed UTF-8") | |
| 94 | +} | |
| 95 | + | |
| 96 | +/// Similar to [`try_from_bstr()`], but takes and produces owned data. | |
| 97 | +pub fn try_from_bstring(input: impl Into<BString>) -> Result<PathBuf, Utf8Error> { | |
| 98 | + let input = input.into(); | |
| 99 | + #[cfg(unix)] | |
| 100 | + let p = { | |
| 101 | + use std::os::unix::ffi::OsStringExt; | |
| 102 | + std::ffi::OsString::from_vec(input.into()).into() | |
| 103 | + }; | |
| 104 | + #[cfg(not(unix))] | |
| 105 | + let p = { | |
| 106 | + use bstr::ByteVec; | |
| 107 | + PathBuf::from( | |
| 108 | + { | |
| 109 | + let v: Vec<_> = input.into(); | |
| 110 | + v | |
| 111 | + } | |
| 112 | + .into_string() | |
| 113 | + .map_err(|_| Utf8Error)?, | |
| 114 | + ) | |
| 115 | + }; | |
| 116 | + Ok(p) | |
| 117 | +} | |
| 118 | + | |
| 119 | +/// Similar to [`try_from_bstring()`], but will panic if there is ill-formed UTF-8 in the `input`. | |
| 120 | +pub fn from_bstring(input: impl Into<BString>) -> PathBuf { | |
| 121 | + try_from_bstring(input).expect("well-formed UTF-8 on windows") | |
| 122 | +} | |
| 123 | + | |
| 124 | +/// Similar to [`try_from_byte_slice()`], but will panic if there is ill-formed UTF-8 in the `input`. | |
| 125 | +pub fn from_byte_slice(input: &[u8]) -> &Path { | |
| 126 | + try_from_byte_slice(input).expect("well-formed UTF-8 on windows") | |
| 127 | +} | |
| 128 | + | |
| 129 | +fn replace<'a>(path: impl Into<Cow<'a, BStr>>, find: u8, replace: u8) -> Cow<'a, BStr> { | |
| 130 | + let path = path.into(); | |
| 131 | + match path { | |
| 132 | + Cow::Owned(mut path) => { | |
| 133 | + for b in path.iter_mut().filter(|b| **b == find) { | |
| 134 | + *b = replace; | |
| 135 | + } | |
| 136 | + path.into() | |
| 137 | + } | |
| 138 | + Cow::Borrowed(path) => { | |
| 139 | + if !path.contains(&find) { | |
| 140 | + return path.into(); | |
| 141 | + } | |
| 142 | + let mut path = path.to_owned(); | |
| 143 | + for b in path.iter_mut().filter(|b| **b == find) { | |
| 144 | + *b = replace; | |
| 145 | + } | |
| 146 | + path.into() | |
| 147 | + } | |
| 148 | + } | |
| 149 | +} | |
| 150 | + | |
| 151 | +/// Assures the given bytes use the native path separator. | |
| 152 | +pub fn to_native_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | |
| 153 | + #[cfg(not(windows))] | |
| 154 | + let p = to_unix_separators(path); | |
| 155 | + #[cfg(windows)] | |
| 156 | + let p = to_windows_separators(path); | |
| 157 | + p | |
| 158 | +} | |
| 159 | + | |
| 160 | +/// Convert paths with slashes to backslashes on windows and do nothing on unix, but **panics** if malformed surrogates are encountered on windows. | |
| 161 | +pub fn to_native_path_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, std::path::Path> { | |
| 162 | + #[cfg(not(windows))] | |
| 163 | + { | |
| 164 | + crate::from_bstr(path) | |
| 165 | + } | |
| 166 | + #[cfg(windows)] | |
| 167 | + { | |
| 168 | + crate::from_bstr(to_windows_separators(path)) | |
| 169 | + } | |
| 170 | +} | |
| 171 | + | |
| 172 | +/// Replaces windows path separators with slashes, but only do so on windows. | |
| 173 | +pub fn to_unix_separators_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | |
| 174 | + #[cfg(windows)] | |
| 175 | + { | |
| 176 | + replace(path, b'\\', b'/') | |
| 177 | + } | |
| 178 | + #[cfg(not(windows))] | |
| 179 | + { | |
| 180 | + path.into() | |
| 181 | + } | |
| 182 | +} | |
| 183 | + | |
| 184 | +/// Replaces windows path separators with slashes, unconditionally. | |
| 185 | +/// | |
| 186 | +/// **Note** Do not use these and prefer the conditional versions of this method. | |
| 187 | +// TODO: use https://lib.rs/crates/path-slash to handle escapes | |
| 188 | +pub fn to_unix_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | |
| 189 | + replace(path, b'\\', b'/') | |
| 190 | +} | |
| 191 | + | |
| 192 | +/// Find backslashes and replace them with slashes, which typically resembles a unix path, unconditionally. | |
| 193 | +/// | |
| 194 | +/// **Note** Do not use these and prefer the conditional versions of this method. | |
| 195 | +// TODO: use https://lib.rs/crates/path-slash to handle escapes | |
| 196 | +pub fn to_windows_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> { | |
| 197 | + replace(path, b'/', b'\\') | |
| 198 | +} | |
| @@ -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,53 @@ | ||
| 1 | +use std::ffi::OsStr; | |
| 2 | + | |
| 3 | +use bstr::{BStr, ByteSlice, ByteVec}; | |
| 4 | + | |
| 5 | +use crate::Spec; | |
| 6 | + | |
| 7 | +impl std::convert::TryFrom<&OsStr> for Spec { | |
| 8 | + type Error = crate::Utf8Error; | |
| 9 | + | |
| 10 | + fn try_from(value: &OsStr) -> Result<Self, Self::Error> { | |
| 11 | + crate::os_str_into_bstr(value).map(|value| { | |
| 12 | + assert_valid_hack(value); | |
| 13 | + Spec(value.into()) | |
| 14 | + }) | |
| 15 | + } | |
| 16 | +} | |
| 17 | + | |
| 18 | +fn assert_valid_hack(input: &BStr) { | |
| 19 | + assert!(!input.contains_str(b"/../")); | |
| 20 | + assert!(!input.contains_str(b"/./")); | |
| 21 | + assert!(!input.starts_with_str(b"../")); | |
| 22 | + assert!(!input.starts_with_str(b"./")); | |
| 23 | + assert!(!input.starts_with_str(b"/")); | |
| 24 | +} | |
| 25 | + | |
| 26 | +impl Spec { | |
| 27 | + /// Parse `input` into a `Spec` or `None` if it could not be parsed | |
| 28 | + // TODO: tests, actual implementation probably via `git-pathspec` to make use of the crate after all. | |
| 29 | + pub fn from_bytes(input: &BStr) -> Option<Self> { | |
| 30 | + assert_valid_hack(input); | |
| 31 | + Spec(input.into()).into() | |
| 32 | + } | |
| 33 | + /// Return all paths described by this path spec, using slashes on all platforms. | |
| 34 | + pub fn items(&self) -> impl Iterator<Item = &BStr> { | |
| 35 | + std::iter::once(self.0.as_bstr()) | |
| 36 | + } | |
| 37 | + /// Adjust this path specification according to the given `prefix`, which may be empty to indicate we are the at work-tree root. | |
| 38 | + // TODO: this is a hack, needs test and time to do according to spec. This is just a minimum version to have -something-. | |
| 39 | + pub fn apply_prefix(&mut self, prefix: &std::path::Path) -> &Self { | |
| 40 | + // many more things we can't handle. `Path` never ends with trailing path separator. | |
| 41 | + let prefix = crate::into_bstr(prefix); | |
| 42 | + if !prefix.is_empty() { | |
| 43 | + let mut prefix = crate::to_unix_separators_on_windows(prefix); | |
| 44 | + { | |
| 45 | + let path = prefix.to_mut(); | |
| 46 | + path.push_byte(b'/'); | |
| 47 | + path.extend_from_slice(&self.0); | |
| 48 | + } | |
| 49 | + self.0 = prefix.into_owned(); | |
| 50 | + } | |
| 51 | + self | |
| 52 | + } | |
| 53 | +} | |
| @@ -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 | /// |
| @@ -1,3 +1,3 @@ | ||
| 1 | 1 | version https://git-lfs.github.com/spec/v1 |
| 2 | −oid sha256:e7fe369e2c7a87e2688c6d0bf3bcdbe3e31754266f39f53a390f85551ba8258e | |
| 2 | +oid sha256:75458d7d4cc29c1cab7a97e47578339dff84baf164fc3a9bcddc6f844482bab2 | |
| 3 | 3 | size 10848 |
| @@ -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 } |
| @@ -97,6 +99,7 @@ unicode-normalization = { version = "0.1.19", default-features = false } · 12 unmodified lines | ||
| 97 | 99 | |
| 98 | 100 | [dev-dependencies] |
| 99 | 101 | git-testtools = { path = "../tests/tools" } |
| 102 | +is_ci = "1.1.1" | |
| 100 | 103 | anyhow = "1" |
| 101 | 104 | tempfile = "3.2.0" |
| 102 | 105 | |
| @@ -55,6 +55,7 @@ pub mod describe { · 54 unmodified lines | ||
| 55 | 55 | } |
| 56 | 56 | |
| 57 | 57 | /// A selector to choose what kind of references should contribute to names. |
| 58 | + #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] | |
| 58 | 59 | pub enum SelectRef { |
| 59 | 60 | /// Only use annotated tags for names. |
| 60 | 61 | AnnotatedTags, |
| @@ -70,33 +71,60 @@ pub mod describe { · 9 unmodified lines | ||
| 70 | 71 | repo: &Repository, |
| 71 | 72 | ) -> Result<git_revision::hash_hasher::HashedMap<ObjectId, Cow<'static, BStr>>, Error> { |
| 72 | 73 | let platform = repo.references()?; |
| 73 | − let into_tuple = | |
| 74 | − |r: crate::Reference<'_>| (r.inner.target.into_id(), Cow::from(r.inner.name.shorten().to_owned())); | |
| 75 | 74 | |
| 76 | 75 | Ok(match self { |
| 77 | − SelectRef::AllTags => platform | |
| 78 | − .tags()? | |
| 79 | − .peeled() | |
| 76 | + SelectRef::AllTags | SelectRef::AllRefs => { | |
| 77 | + let mut refs: Vec<_> = match self { | |
| 78 | + SelectRef::AllRefs => platform.all()?, | |
| 79 | + SelectRef::AllTags => platform.tags()?, | |
| 80 | + _ => unreachable!(), | |
| 81 | + } | |
| 80 | 82 | .filter_map(Result::ok) |
| 81 | − .map(into_tuple) | |
| 82 | − .collect(), | |
| 83 | − SelectRef::AllRefs => platform | |
| 84 | − .all()? | |
| 85 | − .peeled() | |
| 86 | − .filter_map(Result::ok) | |
| 87 | − .map(into_tuple) | |
| 88 | − .collect(), | |
| 89 | − SelectRef::AnnotatedTags => platform | |
| 90 | − .tags()? | |
| 91 | − .filter_map(Result::ok) | |
| 92 | − .filter_map(|r: crate::Reference<'_>| { | |
| 93 | − // TODO: we assume direct refs for tags, which is the common case, but it doesn't have to be | |
| 94 | − // so rather follow symrefs till the first object and then peel tags after the first object was found. | |
| 95 | − let tag = r.try_id()?.object().ok()?.try_into_tag().ok()?; | |
| 96 | − let commit_id = tag.target_id().ok()?.object().ok()?.try_into_commit().ok()?.id; | |
| 97 | − Some((commit_id, r.name().shorten().to_owned().into())) | |
| 83 | + .filter_map(|mut r: crate::Reference<'_>| { | |
| 84 | + let target_id = r.target().try_id().map(ToOwned::to_owned); | |
| 85 | + let peeled_id = r.peel_to_id_in_place().ok()?; | |
| 86 | + let (prio, tag_time) = match target_id { | |
| 87 | + Some(target_id) if peeled_id != *target_id => { | |
| 88 | + let tag = repo.find_object(target_id).ok()?.try_into_tag().ok()?; | |
| 89 | + (1, tag.tagger().ok()??.time.seconds_since_unix_epoch) | |
| 90 | + } | |
| 91 | + _ => (0, 0), | |
| 92 | + }; | |
| 93 | + ( | |
| 94 | + peeled_id.inner, | |
| 95 | + prio, | |
| 96 | + tag_time, | |
| 97 | + Cow::from(r.inner.name.shorten().to_owned()), | |
| 98 | + ) | |
| 99 | + .into() | |
| 98 | 100 | }) |
| 99 | − .collect(), | |
| 101 | + .collect(); | |
| 102 | + refs.sort_by(|a, b| a.2.cmp(&b.2).then_with(|| a.1.cmp(&b.1))); // by time ascending, then by priority. Older entries overwrite newer ones. | |
| 103 | + refs.into_iter().map(|(a, _, _, b)| (a, b)).collect() | |
| 104 | + } | |
| 105 | + SelectRef::AnnotatedTags => { | |
| 106 | + let mut peeled_commits_and_tag_date: Vec<_> = platform | |
| 107 | + .tags()? | |
| 108 | + .filter_map(Result::ok) | |
| 109 | + .filter_map(|r: crate::Reference<'_>| { | |
| 110 | + // TODO: we assume direct refs for tags, which is the common case, but it doesn't have to be | |
| 111 | + // so rather follow symrefs till the first object and then peel tags after the first object was found. | |
| 112 | + let tag = r.try_id()?.object().ok()?.try_into_tag().ok()?; | |
| 113 | + let tag_time = tag | |
| 114 | + .tagger() | |
| 115 | + .ok() | |
| 116 | + .and_then(|s| s.map(|s| s.time.seconds_since_unix_epoch)) | |
| 117 | + .unwrap_or(0); | |
| 118 | + let commit_id = tag.target_id().ok()?.object().ok()?.try_into_commit().ok()?.id; | |
| 119 | + Some((commit_id, tag_time, r.name().shorten().to_owned().into())) | |
| 120 | + }) | |
| 121 | + .collect(); | |
| 122 | + peeled_commits_and_tag_date.sort_by(|a, b| a.1.cmp(&b.1)); // by time, ascending, causing older names to overwrite newer ones. | |
| 123 | + peeled_commits_and_tag_date | |
| 124 | + .into_iter() | |
| 125 | + .map(|(a, _, c)| (a, c)) | |
| 126 | + .collect() | |
| 127 | + } | |
| 100 | 128 | }) |
| 101 | 129 | } |
| 102 | 130 | } |
| @@ -146,7 +174,7 @@ pub mod describe { · 43 unmodified lines | ||
| 146 | 174 | /// if one was found. |
| 147 | 175 | /// |
| 148 | 176 | /// Note that there will always be `Some(format)` |
| 149 | − pub fn try_format(self) -> Result<Option<git_revision::describe::Format<'static>>, Error> { | |
| 177 | + pub fn try_format(&self) -> Result<Option<git_revision::describe::Format<'static>>, Error> { | |
| 150 | 178 | self.try_resolve()?.map(|r| r.format()).transpose() |
| 151 | 179 | } |
| 152 | 180 | |
| @@ -154,7 +182,7 @@ pub mod describe { · 1 unmodified line | ||
| 154 | 182 | /// if one was found. |
| 155 | 183 | /// |
| 156 | 184 | /// The outcome provides additional information, but leaves the caller with the burden |
| 157 | − pub fn try_resolve(self) -> Result<Option<crate::commit::describe::Resolution<'repo>>, Error> { | |
| 185 | + pub fn try_resolve(&self) -> Result<Option<crate::commit::describe::Resolution<'repo>>, Error> { | |
| 158 | 186 | // TODO: dirty suffix with respective dirty-detection |
| 159 | 187 | let outcome = git_revision::describe( |
| 160 | 188 | &self.id, |
| @@ -180,7 +208,7 @@ pub mod describe { · 19 unmodified lines | ||
| 180 | 208 | } |
| 181 | 209 | |
| 182 | 210 | /// Like [`try_format()`][Platform::try_format()], but turns `id_as_fallback()` on to always produce a format. |
| 183 | − pub fn format(mut self) -> Result<git_revision::describe::Format<'static>, Error> { | |
| 211 | + pub fn format(&mut self) -> Result<git_revision::describe::Format<'static>, Error> { | |
| 184 | 212 | self.id_as_fallback = true; |
| 185 | 213 | Ok(self.try_format()?.expect("BUG: fallback must always produce a format")) |
| 186 | 214 | } |
| @@ -1,4 +1,4 @@ | ||
| 1 | −use crate::bstr::BString; | |
| 1 | +use crate::{bstr::BString, permission::EnvVarResourcePermission}; | |
| 2 | 2 | |
| 3 | 3 | #[derive(Debug, thiserror::Error)] |
| 4 | 4 | pub enum Error { |
| @@ -10,85 +10,110 @@ pub enum Error { · 5 unmodified lines | ||
| 10 | 10 | EmptyValue { key: &'static str }, |
| 11 | 11 | #[error("Invalid value for 'core.abbrev' = '{}'. It must be between 4 and {}", .value, .max)] |
| 12 | 12 | CoreAbbrev { value: BString, max: u8 }, |
| 13 | + #[error("Value '{}' at key '{}' could not be decoded as boolean", .value, .key)] | |
| 14 | + DecodeBoolean { key: String, value: BString }, | |
| 15 | + #[error(transparent)] | |
| 16 | + PathInterpolation(#[from] git_config::values::path::interpolate::Error), | |
| 13 | 17 | } |
| 14 | 18 | |
| 15 | 19 | /// Utility type to keep pre-obtained configuration values. |
| 16 | 20 | #[derive(Debug, Clone)] |
| 17 | 21 | pub(crate) struct Cache { |
| 18 | 22 | // TODO: remove this once resolved is used without a feature dependency |
| 19 | − #[cfg_attr(not(feature = "git-mailmap"), allow(dead_code))] | |
| 23 | + #[cfg_attr(not(any(feature = "git-mailmap", feature = "git-index")), allow(dead_code))] | |
| 20 | 24 | pub resolved: crate::Config, |
| 21 | 25 | /// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count. |
| 22 | 26 | pub hex_len: Option<usize>, |
| 23 | − /// true if the repository is designated as 'bare', without work tree | |
| 27 | + /// true if the repository is designated as 'bare', without work tree. | |
| 24 | 28 | pub is_bare: bool, |
| 25 | − /// The type of hash to use | |
| 29 | + /// The type of hash to use. | |
| 26 | 30 | pub object_hash: git_hash::Kind, |
| 27 | 31 | /// If true, multi-pack indices, whether present or not, may be used by the object database. |
| 28 | 32 | pub use_multi_pack_index: bool, |
| 33 | + /// If true, we are on a case-insensitive file system. | |
| 34 | + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] | |
| 35 | + pub ignore_case: bool, | |
| 36 | + /// The path to the user-level excludes file to ignore certain files in the worktree. | |
| 37 | + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] | |
| 38 | + pub excludes_file: Option<std::path::PathBuf>, | |
| 39 | + /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable. | |
| 40 | + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] | |
| 41 | + xdg_config_home_env: EnvVarResourcePermission, | |
| 42 | + /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable. | |
| 43 | + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] | |
| 44 | + home_env: EnvVarResourcePermission, | |
| 29 | 45 | // TODO: make core.precomposeUnicode available as well. |
| 30 | 46 | } |
| 31 | 47 | |
| 32 | 48 | mod cache { |
| 33 | − use std::{borrow::Cow, convert::TryFrom}; | |
| 49 | + use std::{convert::TryFrom, path::PathBuf}; | |
| 34 | 50 | |
| 35 | 51 | use git_config::{ |
| 36 | 52 | file::GitConfig, |
| 37 | − values, | |
| 38 | 53 | values::{Boolean, Integer}, |
| 39 | 54 | }; |
| 40 | 55 | |
| 41 | 56 | use super::{Cache, Error}; |
| 42 | − use crate::bstr::ByteSlice; | |
| 57 | + use crate::{bstr::ByteSlice, permission::EnvVarResourcePermission}; | |
| 43 | 58 | |
| 44 | 59 | impl Cache { |
| 45 | − pub fn new(git_dir: &std::path::Path) -> Result<Self, Error> { | |
| 60 | + pub fn new( | |
| 61 | + git_dir: &std::path::Path, | |
| 62 | + xdg_config_home_env: EnvVarResourcePermission, | |
| 63 | + home_env: EnvVarResourcePermission, | |
| 64 | + git_install_dir: Option<&std::path::Path>, | |
| 65 | + ) -> Result<Self, Error> { | |
| 46 | 66 | 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); | |
| 67 | + | |
| 68 | + let is_bare = config_bool(&config, "core.bare", false)?; | |
| 69 | + let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true)?; | |
| 70 | + let ignore_case = config_bool(&config, "core.ignorecase", false)?; | |
| 71 | + let excludes_file = config | |
| 72 | + .path("core", None, "excludesFile") | |
| 73 | + .map(|p| p.interpolate(git_install_dir).map(|p| p.into_owned())) | |
| 74 | + .transpose()?; | |
| 49 | 75 | let repo_format_version = config |
| 50 | 76 | .value::<Integer>("core", None, "repositoryFormatVersion") |
| 51 | 77 | .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 { | |
| 78 | + let object_hash = (repo_format_version != 1) | |
| 79 | + .then(|| Ok(git_hash::Kind::Sha1)) | |
| 80 | + .or_else(|| { | |
| 81 | + config | |
| 82 | + .raw_value("extensions", None, "objectFormat") | |
| 83 | + .ok() | |
| 84 | + .map(|format| match format.as_ref() { | |
| 85 | + b"sha1" => Ok(git_hash::Kind::Sha1), | |
| 86 | + _ => Err(Error::UnsupportedObjectFormat { | |
| 58 | 87 | name: format.to_vec().into(), |
| 59 | − }) | |
| 60 | − } | |
| 61 | − } | |
| 62 | − } else { | |
| 63 | − git_hash::Kind::Sha1 | |
| 64 | − } | |
| 65 | − } else { | |
| 66 | − git_hash::Kind::Sha1 | |
| 67 | − }; | |
| 88 | + }), | |
| 89 | + }) | |
| 90 | + }) | |
| 91 | + .transpose()? | |
| 92 | + .unwrap_or(git_hash::Kind::Sha1); | |
| 68 | 93 | |
| 69 | 94 | 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() { | |
| 95 | + if let Some(hex_len_str) = config.string("core", None, "abbrev") { | |
| 96 | + if hex_len_str.trim().is_empty() { | |
| 72 | 97 | return Err(Error::EmptyValue { key: "core.abbrev" }); |
| 73 | 98 | } |
| 74 | − if hex_len_str.value.as_ref() != "auto" { | |
| 75 | − let value_bytes = hex_len_str.value.as_ref().as_ref(); | |
| 99 | + if hex_len_str.as_ref() != "auto" { | |
| 100 | + let value_bytes = hex_len_str.as_ref().as_ref(); | |
| 76 | 101 | if let Ok(Boolean::False(_)) = Boolean::try_from(value_bytes) { |
| 77 | 102 | hex_len = object_hash.len_in_hex().into(); |
| 78 | 103 | } else { |
| 79 | 104 | let value = Integer::try_from(value_bytes) |
| 80 | 105 | .map_err(|_| Error::CoreAbbrev { |
| 81 | − value: hex_len_str.value.clone().into_owned(), | |
| 106 | + value: hex_len_str.clone().into_owned(), | |
| 82 | 107 | max: object_hash.len_in_hex() as u8, |
| 83 | 108 | })? |
| 84 | 109 | .to_decimal() |
| 85 | 110 | .ok_or_else(|| Error::CoreAbbrev { |
| 86 | − value: hex_len_str.value.clone().into_owned(), | |
| 111 | + value: hex_len_str.clone().into_owned(), | |
| 87 | 112 | max: object_hash.len_in_hex() as u8, |
| 88 | 113 | })?; |
| 89 | 114 | if value < 4 || value as usize > object_hash.len_in_hex() { |
| 90 | 115 | return Err(Error::CoreAbbrev { |
| 91 | − value: hex_len_str.value.clone().into_owned(), | |
| 116 | + value: hex_len_str.clone().into_owned(), | |
| 92 | 117 | max: object_hash.len_in_hex() as u8, |
| 93 | 118 | }); |
| 94 | 119 | } |
| @@ -102,15 +127,39 @@ mod cache { · 7 unmodified lines | ||
| 102 | 127 | use_multi_pack_index, |
| 103 | 128 | object_hash, |
| 104 | 129 | is_bare, |
| 130 | + ignore_case, | |
| 105 | 131 | hex_len, |
| 132 | + excludes_file, | |
| 133 | + xdg_config_home_env, | |
| 134 | + home_env, | |
| 106 | 135 | }) |
| 107 | 136 | } |
| 137 | + | |
| 138 | + /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations. | |
| 139 | + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] | |
| 140 | + pub fn xdg_config_path( | |
| 141 | + &self, | |
| 142 | + resource_file_name: &str, | |
| 143 | + ) -> Result<Option<PathBuf>, git_sec::permission::Error<PathBuf, git_sec::Permission>> { | |
| 144 | + std::env::var_os("XDG_CONFIG_HOME") | |
| 145 | + .map(|path| (path, &self.xdg_config_home_env)) | |
| 146 | + .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env))) | |
| 147 | + .and_then(|(base, permission)| { | |
| 148 | + let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name); | |
| 149 | + permission.check(resource).transpose() | |
| 150 | + }) | |
| 151 | + .transpose() | |
| 152 | + } | |
| 108 | 153 | } |
| 109 | 154 | |
| 110 | − fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> bool { | |
| 155 | + fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> Result<bool, Error> { | |
| 111 | 156 | let (section, key) = key.split_once('.').expect("valid section.key format"); |
| 112 | 157 | config |
| 113 | − .value::<Boolean<'_>>(section, None, key) | |
| 114 | − .map_or(default, |b| b.to_bool()) | |
| 158 | + .boolean(section, None, key) | |
| 159 | + .unwrap_or(Ok(default)) | |
| 160 | + .map_err(|err| Error::DecodeBoolean { | |
| 161 | + value: err.input, | |
| 162 | + key: key.into(), | |
| 163 | + }) | |
| 115 | 164 | } |
| 116 | 165 | } |
| @@ -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,15 @@ 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, Access}; | |
| 248 | + | |
| 249 | + /// A permission to control access to the resource pointed to an environment variable. | |
| 250 | + pub type EnvVarResourcePermission = Access<Resource, git_sec::Permission>; | |
| 251 | +} | |
| 252 | +pub use repository::permissions::Permissions; | |
| 253 | + | |
| 242 | 254 | /// |
| 243 | 255 | pub mod open; |
| 244 | 256 | |
| @@ -268,6 +280,9 @@ pub mod mailmap { · 23 unmodified lines | ||
| 268 | 280 | } |
| 269 | 281 | } |
| 270 | 282 | |
| 283 | +/// | |
| 284 | +pub mod worktree; | |
| 285 | + | |
| 271 | 286 | /// |
| 272 | 287 | pub mod rev_parse { |
| 273 | 288 | /// The error returned by [`crate::Repository::rev_parse()`]. |
| @@ -283,9 +298,10 @@ pub mod rev_parse { · 9 unmodified lines | ||
| 283 | 298 | |
| 284 | 299 | /// |
| 285 | 300 | pub mod init { |
| 286 | − use crate::ThreadSafeRepository; | |
| 287 | 301 | use std::path::Path; |
| 288 | 302 | |
| 303 | + use crate::ThreadSafeRepository; | |
| 304 | + | |
| 289 | 305 | /// The error returned by [`crate::init()`]. |
| 290 | 306 | #[derive(Debug, thiserror::Error)] |
| 291 | 307 | #[allow(missing_docs)] |
| @@ -321,40 +337,35 @@ pub mod state { · 29 unmodified lines | ||
| 321 | 337 | #[derive(Debug, PartialEq)] |
| 322 | 338 | pub enum InProgress { |
| 323 | 339 | /// A mailbox is being applied. |
| 324 | − // TODO: test | |
| 325 | 340 | ApplyMailbox, |
| 326 | 341 | /// A rebase is happening while a mailbox is being applied. |
| 327 | 342 | // TODO: test |
| 328 | 343 | ApplyMailboxRebase, |
| 329 | 344 | /// A git bisect operation has not yet been concluded. |
| 330 | − // TODO: test | |
| 331 | 345 | Bisect, |
| 332 | 346 | /// A cherry pick operation. |
| 333 | 347 | CherryPick, |
| 334 | 348 | /// A cherry pick with multiple commits pending. |
| 335 | − // TODO: test | |
| 336 | 349 | CherryPickSequence, |
| 337 | 350 | /// A merge operation. |
| 338 | − // TODO: test | |
| 339 | 351 | Merge, |
| 340 | 352 | /// A rebase operation. |
| 341 | − // TODO: test | |
| 342 | 353 | Rebase, |
| 343 | 354 | /// An interactive rebase operation. |
| 344 | 355 | RebaseInteractive, |
| 345 | 356 | /// A revert operation. |
| 346 | 357 | Revert, |
| 347 | 358 | /// A revert operation with multiple commits pending. |
| 348 | − // TODO: test | |
| 349 | 359 | RevertSequence, |
| 350 | 360 | } |
| 351 | 361 | } |
| 352 | 362 | |
| 353 | 363 | /// |
| 354 | 364 | pub mod discover { |
| 355 | − use crate::{path, ThreadSafeRepository}; | |
| 356 | 365 | use std::path::Path; |
| 357 | 366 | |
| 367 | + use crate::{path, ThreadSafeRepository}; | |
| 368 | + | |
| 358 | 369 | /// The error returned by [`crate::discover()`]. |
| 359 | 370 | #[derive(Debug, thiserror::Error)] |
| 360 | 371 | #[allow(missing_docs)] |
| @@ -1,10 +1,15 @@ | ||
| 1 | 1 | use crate::{ext::ObjectIdExt, Tag}; |
| 2 | 2 | |
| 3 | 3 | impl<'repo> Tag<'repo> { |
| 4 | − /// Decode this tag and return the id of its target. | |
| 4 | + /// Decode this tag partially and return the id of its target. | |
| 5 | 5 | pub fn target_id(&self) -> Result<crate::Id<'repo>, git_object::decode::Error> { |
| 6 | 6 | git_object::TagRefIter::from_bytes(&self.data) |
| 7 | 7 | .target_id() |
| 8 | 8 | .map(|id| id.attach(self.repo)) |
| 9 | 9 | } |
| 10 | + | |
| 11 | + /// Decode this tag partially and return the tagger, if the field exists. | |
| 12 | + pub fn tagger(&self) -> Result<Option<git_actor::SignatureRef<'_>>, git_object::decode::Error> { | |
| 13 | + git_object::TagRefIter::from_bytes(&self.data).tagger() | |
| 14 | + } | |
| 10 | 15 | } |
| @@ -1,9 +1,10 @@ | ||
| 1 | 1 | use std::path::PathBuf; |
| 2 | 2 | |
| 3 | −use crate::Permissions; | |
| 4 | 3 | use git_features::threading::OwnShared; |
| 5 | 4 | use git_sec::Trust; |
| 6 | 5 | |
| 6 | +use crate::Permissions; | |
| 7 | + | |
| 7 | 8 | /// A way to configure the usage of replacement objects, see `git replace`. |
| 8 | 9 | pub enum ReplacementObjects { |
| 9 | 10 | /// Allow replacement objects and configure the ref prefix the standard environment variable `GIT_REPLACE_REF_BASE`, |
| @@ -153,10 +154,15 @@ impl crate::ThreadSafeRepository { · 143 unmodified lines | ||
| 153 | 154 | Options { |
| 154 | 155 | object_store_slots, |
| 155 | 156 | replacement_objects, |
| 156 | − permissions, | |
| 157 | + permissions: | |
| 158 | + Permissions { | |
| 159 | + git_dir: git_dir_perm, | |
| 160 | + xdg_config_home, | |
| 161 | + home, | |
| 162 | + }, | |
| 157 | 163 | }: Options, |
| 158 | 164 | ) -> Result<Self, Error> { |
| 159 | − if *permissions.git_dir != git_sec::ReadWrite::all() { | |
| 165 | + if *git_dir_perm != git_sec::ReadWrite::all() { | |
| 160 | 166 | // TODO: respect `save.directory`, which needs more support from git-config to do properly. |
| 161 | 167 | return Err(Error::UnsafeGitDir { path: git_dir }); |
| 162 | 168 | } |
| @@ -164,7 +170,12 @@ impl crate::ThreadSafeRepository { · 1 unmodified line | ||
| 164 | 170 | // This would be something read in later as have to first check for extensions. Also this means |
| 165 | 171 | // that each worktree, even if accessible through this instance, has to come in its own Repository instance |
| 166 | 172 | // as it may have its own configuration. That's fine actually. |
| 167 | − let config = crate::config::Cache::new(&git_dir)?; | |
| 173 | + let config = crate::config::Cache::new( | |
| 174 | + &git_dir, | |
| 175 | + xdg_config_home, | |
| 176 | + home, | |
| 177 | + crate::path::install_dir().ok().as_deref(), | |
| 178 | + )?; | |
| 168 | 179 | match worktree_dir { |
| 169 | 180 | None if !config.is_bare => { |
| 170 | 181 | 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, |
| @@ -35,13 +41,14 @@ impl Default for Options { · 21 unmodified lines | ||
| 35 | 41 | } |
| 36 | 42 | |
| 37 | 43 | pub(crate) mod function { |
| 38 | − use super::{Error, Options}; | |
| 39 | − use git_sec::Trust; | |
| 40 | 44 | use std::{ |
| 41 | 45 | borrow::Cow, |
| 42 | 46 | path::{Component, Path}, |
| 43 | 47 | }; |
| 44 | 48 | |
| 49 | + use git_sec::Trust; | |
| 50 | + | |
| 51 | + use super::{Error, Options}; | |
| 45 | 52 | use crate::path; |
| 46 | 53 | |
| 47 | 54 | /// Find the location of the git repository directly in `directory` or in any of its parent directories and provide |
| @@ -57,23 +64,20 @@ pub(crate) mod function { · 9 unmodified lines | ||
| 57 | 64 | // us the parent directory. (`Path::parent` just strips off the last |
| 58 | 65 | // path component, which means it will not do what you expect when |
| 59 | 66 | // 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 | − }); | |
| 67 | + let directory = directory.as_ref(); | |
| 68 | + let dir = maybe_canonicalize(directory).map_err(|_| Error::InaccessibleDirectory { path: directory.into() })?; | |
| 69 | + let is_canonicalized = dir.as_ref() != directory; | |
| 70 | + if !dir.is_dir() { | |
| 71 | + return Err(Error::InaccessibleDirectory { path: dir.into_owned() }); | |
| 67 | 72 | } |
| 68 | 73 | |
| 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 | − }; | |
| 74 | + let filter_by_trust = |x: &std::path::Path| -> Result<Option<git_sec::Trust>, Error> { | |
| 75 | + let trust = | |
| 76 | + git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?; | |
| 77 | + Ok((trust >= required_trust).then(|| (trust))) | |
| 78 | + }; | |
| 75 | 79 | |
| 76 | − let mut cursor = directory.clone(); | |
| 80 | + let mut cursor = dir.clone(); | |
| 77 | 81 | 'outer: loop { |
| 78 | 82 | for append_dot_git in &[false, true] { |
| 79 | 83 | if *append_dot_git { |
| @@ -83,11 +87,36 @@ pub(crate) mod function { · 3 unmodified lines | ||
| 83 | 87 | } |
| 84 | 88 | } |
| 85 | 89 | if let Ok(kind) = path::is::git(&cursor) { |
| 86 | − match filter_by_trust(&cursor, kind)? { | |
| 87 | − Some(res) => break 'outer Ok(res), | |
| 90 | + match filter_by_trust(&cursor)? { | |
| 91 | + Some(trust) => { | |
| 92 | + // TODO: test this more | |
| 93 | + let path = if is_canonicalized { | |
| 94 | + match std::env::current_dir() { | |
| 95 | + Ok(cwd) => cwd | |
| 96 | + .strip_prefix(&cursor.parent().expect(".git appended")) | |
| 97 | + .ok() | |
| 98 | + .and_then(|p| { | |
| 99 | + let short_path_components = p.components().count(); | |
| 100 | + (short_path_components < cursor.components().count()).then(|| { | |
| 101 | + std::iter::repeat("..") | |
| 102 | + .take(short_path_components) | |
| 103 | + .chain(Some(".git")) | |
| 104 | + .collect() | |
| 105 | + }) | |
| 106 | + }) | |
| 107 | + .unwrap_or_else(|| cursor.into_owned()), | |
| 108 | + Err(_) => cursor.into_owned(), | |
| 109 | + } | |
| 110 | + } else { | |
| 111 | + cursor.into_owned() | |
| 112 | + }; | |
| 113 | + break 'outer Ok((crate::Path::from_dot_git_dir(path, kind), trust)); | |
| 114 | + } | |
| 88 | 115 | None => { |
| 89 | − break 'outer Err(Error::NoGitRepository { | |
| 90 | − path: directory.into_owned(), | |
| 116 | + break 'outer Err(Error::NoTrustedGitRepository { | |
| 117 | + path: dir.into_owned(), | |
| 118 | + candidate: cursor.into_owned(), | |
| 119 | + required: required_trust, | |
| 91 | 120 | }) |
| 92 | 121 | } |
| 93 | 122 | } |
| @@ -100,11 +129,7 @@ pub(crate) mod function { · 6 unmodified lines | ||
| 100 | 129 | } |
| 101 | 130 | match cursor.parent() { |
| 102 | 131 | Some(parent) => cursor = parent.to_owned().into(), |
| 103 | − None => { | |
| 104 | − break Err(Error::NoGitRepository { | |
| 105 | − path: directory.into_owned(), | |
| 106 | − }) | |
| 107 | − } | |
| 132 | + None => break Err(Error::NoGitRepository { path: dir.into_owned() }), | |
| 108 | 133 | } |
| 109 | 134 | } |
| 110 | 135 | } |
| @@ -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 |
| @@ -1,5 +1,8 @@ | ||
| 1 | 1 | use std::path::PathBuf; |
| 2 | 2 | |
| 3 | +#[cfg(all(feature = "unstable", feature = "git-path"))] | |
| 4 | +pub use git_path::*; | |
| 5 | + | |
| 3 | 6 | use crate::{Kind, Path}; |
| 4 | 7 | |
| 5 | 8 | /// |
| @@ -24,7 +27,11 @@ impl Path { · 18 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 git_sec::{permission::Resource, Access, Trust}; | |
| 2 | + | |
| 3 | +use crate::permission::EnvVarResourcePermission; | |
| 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 | +} | |
| @@ -200,6 +200,11 @@ impl crate::Repository { · 199 unmodified lines | ||
| 200 | 200 | .map_err(Into::into) |
| 201 | 201 | } |
| 202 | 202 | |
| 203 | + /// Return the name to the symbolic reference `HEAD` points to, or `None` if the head is detached. | |
| 204 | + pub fn head_name(&self) -> Result<Option<git_ref::FullName>, crate::reference::find::existing::Error> { | |
| 205 | + Ok(self.head()?.referent_name().map(|n| n.to_owned())) | |
| 206 | + } | |
| 207 | + | |
| 203 | 208 | /// Return the commit object the `HEAD` reference currently points to after peeling it fully. |
| 204 | 209 | /// |
| 205 | 210 | /// Note that this may fail for various reasons, most notably because the repository |
| @@ -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) |
| @@ -3,7 +3,9 @@ use crate::state; · 2 unmodified lines | ||
| 3 | 3 | impl crate::Repository { |
| 4 | 4 | /// Returns the status of an in progress operation on a repository or [`None`] |
| 5 | 5 | /// if no operation is currently in progress. |
| 6 | − pub fn in_progress_operation(&self) -> Option<state::InProgress> { | |
| 6 | + /// | |
| 7 | + /// Note to be confused with the repositories 'status'. | |
| 8 | + pub fn state(&self) -> Option<state::InProgress> { | |
| 7 | 9 | let git_dir = self.path(); |
| 8 | 10 | |
| 9 | 11 | // This is modeled on the logic from wt_status_get_state in git's wt-status.c and |
| @@ -20,7 +22,7 @@ impl crate::Repository { · 10 unmodified lines | ||
| 20 | 22 | } else if git_dir.join("rebase-merge").is_dir() { |
| 21 | 23 | Some(state::InProgress::Rebase) |
| 22 | 24 | } else if git_dir.join("CHERRY_PICK_HEAD").is_file() { |
| 23 | − if git_dir.join("todo").is_file() { | |
| 25 | + if git_dir.join("sequencer/todo").is_file() { | |
| 24 | 26 | Some(state::InProgress::CherryPickSequence) |
| 25 | 27 | } else { |
| 26 | 28 | Some(state::InProgress::CherryPick) |
| @@ -30,7 +32,7 @@ impl crate::Repository { · 3 unmodified lines | ||
| 30 | 32 | } else if git_dir.join("BISECT_LOG").is_file() { |
| 31 | 33 | Some(state::InProgress::Bisect) |
| 32 | 34 | } else if git_dir.join("REVERT_HEAD").is_file() { |
| 33 | − if git_dir.join("todo").is_file() { | |
| 35 | + if git_dir.join("sequencer/todo").is_file() { | |
| 34 | 36 | Some(state::InProgress::RevertSequence) |
| 35 | 37 | } else { |
| 36 | 38 | Some(state::InProgress::Revert) |
| @@ -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,130 @@ | ||
| 1 | +#[cfg(all(feature = "unstable", feature = "git-worktree"))] | |
| 2 | +pub use git_worktree::*; | |
| 3 | + | |
| 4 | +use crate::Repository; | |
| 5 | + | |
| 6 | +/// | |
| 7 | +#[cfg(feature = "git-index")] | |
| 8 | +pub mod open_index { | |
| 9 | + use crate::bstr::BString; | |
| 10 | + | |
| 11 | + /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. | |
| 12 | + #[derive(Debug, thiserror::Error)] | |
| 13 | + #[allow(missing_docs)] | |
| 14 | + pub enum Error { | |
| 15 | + #[error("Could not interpret value '{}' as 'index.threads'", .value)] | |
| 16 | + ConfigIndexThreads { | |
| 17 | + value: BString, | |
| 18 | + #[source] | |
| 19 | + err: git_config::value::parse::Error, | |
| 20 | + }, | |
| 21 | + #[error(transparent)] | |
| 22 | + IndexFile(#[from] git_index::file::init::Error), | |
| 23 | + } | |
| 24 | +} | |
| 25 | + | |
| 26 | +/// | |
| 27 | +#[cfg(feature = "git-index")] | |
| 28 | +pub mod excludes { | |
| 29 | + use std::path::PathBuf; | |
| 30 | + | |
| 31 | + /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. | |
| 32 | + #[derive(Debug, thiserror::Error)] | |
| 33 | + #[allow(missing_docs)] | |
| 34 | + pub enum Error { | |
| 35 | + #[error("Could not read repository exclude.")] | |
| 36 | + Io(#[from] std::io::Error), | |
| 37 | + #[error(transparent)] | |
| 38 | + EnvironmentPermission(#[from] git_sec::permission::Error<PathBuf, git_sec::Permission>), | |
| 39 | + } | |
| 40 | +} | |
| 41 | + | |
| 42 | +/// A structure to make the API more stuctured. | |
| 43 | +pub struct Platform<'repo> { | |
| 44 | + pub(crate) parent: &'repo Repository, | |
| 45 | +} | |
| 46 | + | |
| 47 | +/// Access | |
| 48 | +impl<'repo> crate::Worktree<'repo> { | |
| 49 | + /// Returns the root of the worktree under which all checked out files are located. | |
| 50 | + pub fn root(&self) -> &std::path::Path { | |
| 51 | + self.path | |
| 52 | + } | |
| 53 | +} | |
| 54 | + | |
| 55 | +impl<'repo> crate::Worktree<'repo> { | |
| 56 | + /// Configure a file-system cache checking if files below the repository are excluded. | |
| 57 | + /// | |
| 58 | + /// This takes into consideration all the usual repository configuration. | |
| 59 | + // TODO: test | |
| 60 | + #[cfg(feature = "git-index")] | |
| 61 | + pub fn excludes<'a>( | |
| 62 | + &self, | |
| 63 | + index: &'a git_index::State, | |
| 64 | + overrides: Option<git_attributes::MatchGroup<git_attributes::Ignore>>, | |
| 65 | + ) -> Result<git_worktree::fs::Cache<'a>, excludes::Error> { | |
| 66 | + let repo = self.parent; | |
| 67 | + let case = repo | |
| 68 | + .config | |
| 69 | + .ignore_case | |
| 70 | + .then(|| git_glob::pattern::Case::Fold) | |
| 71 | + .unwrap_or_default(); | |
| 72 | + let mut buf = Vec::with_capacity(512); | |
| 73 | + let state = git_worktree::fs::cache::State::IgnoreStack(git_worktree::fs::cache::state::Ignore::new( | |
| 74 | + overrides.unwrap_or_default(), | |
| 75 | + git_attributes::MatchGroup::<git_attributes::Ignore>::from_git_dir( | |
| 76 | + repo.git_dir(), | |
| 77 | + match repo.config.excludes_file.as_ref() { | |
| 78 | + Some(user_path) => Some(user_path.to_owned()), | |
| 79 | + None => repo.config.xdg_config_path("ignore")?, | |
| 80 | + }, | |
| 81 | + &mut buf, | |
| 82 | + )?, | |
| 83 | + None, | |
| 84 | + case, | |
| 85 | + )); | |
| 86 | + let attribute_list = state.build_attribute_list(index, index.path_backing(), case); | |
| 87 | + Ok(git_worktree::fs::Cache::new( | |
| 88 | + self.path, | |
| 89 | + state, | |
| 90 | + case, | |
| 91 | + buf, | |
| 92 | + attribute_list, | |
| 93 | + )) | |
| 94 | + } | |
| 95 | + | |
| 96 | + // pub fn | |
| 97 | + /// Open a new copy of the index file and decode it entirely. | |
| 98 | + /// | |
| 99 | + /// It will use the `index.threads` configuration key to learn how many threads to use. | |
| 100 | + // TODO: test | |
| 101 | + #[cfg(feature = "git-index")] | |
| 102 | + pub fn open_index(&self) -> Result<git_index::File, crate::worktree::open_index::Error> { | |
| 103 | + use std::convert::{TryFrom, TryInto}; | |
| 104 | + let repo = self.parent; | |
| 105 | + let thread_limit = repo | |
| 106 | + .config | |
| 107 | + .resolved | |
| 108 | + .boolean("index", None, "threads") | |
| 109 | + .map(|res| { | |
| 110 | + res.map(|value| if value { 0usize } else { 1 }).or_else(|err| { | |
| 111 | + git_config::values::Integer::try_from(err.input.as_ref()) | |
| 112 | + .map_err(|err| crate::worktree::open_index::Error::ConfigIndexThreads { | |
| 113 | + value: err.input.clone(), | |
| 114 | + err, | |
| 115 | + }) | |
| 116 | + .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1)) | |
| 117 | + }) | |
| 118 | + }) | |
| 119 | + .transpose()?; | |
| 120 | + git_index::File::at( | |
| 121 | + repo.git_dir().join("index"), | |
| 122 | + git_index::decode::Options { | |
| 123 | + object_hash: repo.object_hash(), | |
| 124 | + thread_limit, | |
| 125 | + min_extension_block_in_bytes_for_threading: 0, | |
| 126 | + }, | |
| 127 | + ) | |
| 128 | + .map_err(Into::into) | |
| 129 | + } | |
| 130 | +} | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +mod describe { | |
| 2 | + use git_repository::commit::describe::SelectRef::{AllRefs, AllTags, AnnotatedTags}; | |
| 3 | + | |
| 4 | + use crate::named_repo; | |
| 5 | + | |
| 6 | + #[test] | |
| 7 | + fn tags_are_sorted_by_date_and_lexigraphically() { | |
| 8 | + let repo = named_repo("make_commit_describe_multiple_tags.sh").unwrap(); | |
| 9 | + let mut describe = repo.head_commit().unwrap().describe(); | |
| 10 | + for filter in &[AnnotatedTags, AllTags, AllRefs] { | |
| 11 | + describe = describe.names(*filter); | |
| 12 | + assert_eq!(describe.format().unwrap().to_string(), "v2", "{:?}", filter); | |
| 13 | + } | |
| 14 | + } | |
| 15 | +} | |
| @@ -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()?, |
| @@ -0,0 +1,3 @@ | ||
| 1 | +version https://git-lfs.github.com/spec/v1 | |
| 2 | +oid sha256:87c31f0b601dae0e2b7d863be659b80507b21527d6788a50253181c4f3d5f054 | |
| 3 | +size 11360 | |
| @@ -0,0 +1,3 @@ | ||
| 1 | +version https://git-lfs.github.com/spec/v1 | |
| 2 | +oid sha256:6e38aff8e6054660ffa274ff00ad8398601949576d887e0cc462256f16e966bb | |
| 3 | +size 10200 | |
| @@ -0,0 +1,3 @@ | ||
| 1 | +version https://git-lfs.github.com/spec/v1 | |
| 2 | +oid sha256:4c3187949760fe07a5abc1a5b7f303ba060bedaed1bfad29535b93df72ca453c | |
| 3 | +size 11336 | |
| @@ -0,0 +1,3 @@ | ||
| 1 | +version https://git-lfs.github.com/spec/v1 | |
| 2 | +oid sha256:22fd60100187fc3f4180b31d3ae1d8c5506a65c3ee059b4bbc3a40009fb23f60 | |
| 3 | +size 10408 | |
| @@ -0,0 +1,3 @@ | ||
| 1 | +version https://git-lfs.github.com/spec/v1 | |
| 2 | +oid sha256:a61b3a205b9254579ea67390a9b3da43ab9d26b6576555da566ba69ea07aaa3b | |
| 3 | +size 10948 | |
| @@ -1,3 +1,3 @@ | ||
| 1 | 1 | version https://git-lfs.github.com/spec/v1 |
| 2 | −oid sha256:4b76a63be1b90ff164633c074caf3ce3ccfd73ed6f587a7a02e7c039af065519 | |
| 2 | +oid sha256:b44df1ace634ca3f4e4c05911d4bf949777b6037ba3fbb25f4a139d5048d33d0 | |
| 3 | 3 | size 10440 |
| @@ -0,0 +1,3 @@ | ||
| 1 | +version https://git-lfs.github.com/spec/v1 | |
| 2 | +oid sha256:60a892477189310fa935d43dbaa73d92b3079cbf86023e83262db6000b7556c0 | |
| 3 | +size 11224 | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init -q | |
| 5 | + | |
| 6 | +echo file.main > file | |
| 7 | +git add file | |
| 8 | +git commit -m file.main file | |
| 9 | + | |
| 10 | +git checkout -b other-branch | |
| 11 | +echo file.other-branch > file | |
| 12 | +git commit -m file.other-branch file | |
| 13 | +# Create an mbox formatted patch and save the path | |
| 14 | +patch_path=$(git format-patch main) | |
| 15 | + | |
| 16 | +git checkout main | |
| 17 | +# Create a conflict | |
| 18 | +echo file.main.update > file | |
| 19 | +git commit -m file.main.update file | |
| 20 | + | |
| 21 | +# This will fail due to the merge conflict and leave us in a 'apply mbox in progress' state | |
| 22 | +git am "$patch_path" || true | |
| @@ -0,0 +1,14 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init -q | |
| 5 | + | |
| 6 | +touch f1 f2 | |
| 7 | + | |
| 8 | +git add f1 | |
| 9 | +git commit -m f1 f1 | |
| 10 | + | |
| 11 | +git add f2 | |
| 12 | +git commit -m f2 f2 | |
| 13 | + | |
| 14 | +git bisect start | |
| @@ -0,0 +1,24 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init -q | |
| 5 | + | |
| 6 | +touch f1 f2 f3 | |
| 7 | + | |
| 8 | +git add f1 | |
| 9 | +git commit -m f1 f1 | |
| 10 | + | |
| 11 | +git checkout -b other-branch | |
| 12 | +echo f2.other-branch > f2 | |
| 13 | +git add f2 | |
| 14 | +git commit -m f2.other-branch f2 | |
| 15 | +git add f3 | |
| 16 | +git commit -m f3 f3 | |
| 17 | + | |
| 18 | +git checkout main | |
| 19 | +echo f2.main > f2 | |
| 20 | +git add f2 | |
| 21 | +git commit -m f2.main f2 | |
| 22 | + | |
| 23 | +# This should fail and leave us in a cherry-pick + sequencer state | |
| 24 | +git cherry-pick other-branch~2..other-branch || true | |
| @@ -0,0 +1,11 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init -q | |
| 5 | +git commit --allow-empty -q -m c1 | |
| 6 | +git commit --allow-empty -q -m c2 | |
| 7 | + | |
| 8 | +git tag v0 -m "tag object 0" "HEAD~1" | |
| 9 | +git tag v1 -m "tag object 1" | |
| 10 | +git tag v1.5 | |
| 11 | +GIT_COMMITTER_DATE="2022-01-02 00:00:00 +0000" git tag v2 -m "tag object 2" | |
| @@ -0,0 +1,19 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init -q | |
| 5 | + | |
| 6 | +echo file.main > file | |
| 7 | +git add file | |
| 8 | +git commit -m file.main file | |
| 9 | + | |
| 10 | +git checkout -b other-branch | |
| 11 | +echo file.other-branch > file | |
| 12 | +git add file | |
| 13 | +git commit -m file.other-branch file | |
| 14 | + | |
| 15 | +git checkout main | |
| 16 | +echo file.main changed > file | |
| 17 | +git commit -m file.main\ changed file | |
| 18 | + | |
| 19 | +git merge other-branch || true | |
| @@ -3,11 +3,11 @@ set -eu -o pipefail · 2 unmodified lines | ||
| 3 | 3 | |
| 4 | 4 | git init -q |
| 5 | 5 | |
| 6 | −touch 1 2 3 | |
| 7 | −git add 1 | |
| 8 | −git commit -m 1 1 | |
| 9 | −git add 2 | |
| 10 | −git commit -m 2 2 | |
| 11 | −git add 3 | |
| 12 | −git commit -m 3 3 | |
| 6 | +touch f1 f2 f3 | |
| 7 | +git add f1 | |
| 8 | +git commit -m f1 f1 | |
| 9 | +git add f2 | |
| 10 | +git commit -m f2 f2 | |
| 11 | +git add f3 | |
| 12 | +git commit -m f3 f3 | |
| 13 | 13 | git revert --no-commit HEAD~1 |
| @@ -0,0 +1,20 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init -q | |
| 5 | + | |
| 6 | +echo 1.0 > 1 | |
| 7 | +git add 1 | |
| 8 | +git commit -m 1.0 1 | |
| 9 | + | |
| 10 | +echo 1.1 > 1 | |
| 11 | +git commit -m 1.1 1 | |
| 12 | + | |
| 13 | +echo 1.2 > 1 | |
| 14 | +git commit -m 1.2 1 | |
| 15 | +touch 2 | |
| 16 | +git add 2 | |
| 17 | +git commit -m 2 2 | |
| 18 | + | |
| 19 | +# This should fail and leave us in a revert + sequencer state | |
| 20 | +git revert --no-commit HEAD HEAD~2 || true | |
| @@ -28,6 +28,7 @@ fn basic_rw_repo() -> crate::Result<(Repository, tempfile::TempDir)> { · 27 unmodified lines | ||
| 28 | 28 | repo_rw("make_basic_repo.sh") |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | +mod commit; | |
| 31 | 32 | mod discover; |
| 32 | 33 | mod id; |
| 33 | 34 | mod init; |
| @@ -1,15 +1,52 @@ | ||
| 1 | −use crate::{named_repo, Result}; | |
| 2 | 1 | use git_repository as git; |
| 3 | 2 | |
| 3 | +use crate::{named_repo, Result}; | |
| 4 | + | |
| 5 | +#[test] | |
| 6 | +fn apply_mailbox() -> Result { | |
| 7 | + let repo = named_repo("make_am_repo.sh")?; | |
| 8 | + | |
| 9 | + assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); | |
| 10 | + assert_eq!(repo.state(), Some(git::state::InProgress::ApplyMailbox)); | |
| 11 | + Ok(()) | |
| 12 | +} | |
| 13 | + | |
| 14 | +#[test] | |
| 15 | +fn bisect() -> Result { | |
| 16 | + let repo = named_repo("make_bisect_repo.sh")?; | |
| 17 | + | |
| 18 | + assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); | |
| 19 | + assert_eq!(repo.state(), Some(git::state::InProgress::Bisect)); | |
| 20 | + | |
| 21 | + Ok(()) | |
| 22 | +} | |
| 23 | + | |
| 4 | 24 | #[test] |
| 5 | 25 | fn cherry_pick() -> Result { |
| 6 | 26 | let repo = named_repo("make_cherry_pick_repo.sh")?; |
| 7 | 27 | |
| 8 | − let head = repo.head()?; | |
| 9 | − let head_name = head.referent_name().expect("no detached head").shorten(); | |
| 10 | − assert_eq!(head_name, "main"); | |
| 28 | + assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); | |
| 29 | + assert_eq!(repo.state(), Some(git::state::InProgress::CherryPick)); | |
| 30 | + Ok(()) | |
| 31 | +} | |
| 32 | + | |
| 33 | +#[test] | |
| 34 | +fn cherry_pick_sequence() -> Result { | |
| 35 | + let repo = named_repo("make_cherry_pick_sequence_repo.sh")?; | |
| 36 | + | |
| 37 | + assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); | |
| 38 | + assert_eq!(repo.state(), Some(git::state::InProgress::CherryPickSequence)); | |
| 39 | + | |
| 40 | + Ok(()) | |
| 41 | +} | |
| 42 | + | |
| 43 | +#[test] | |
| 44 | +fn merge() -> Result { | |
| 45 | + let repo = named_repo("make_merge_repo.sh")?; | |
| 46 | + | |
| 47 | + assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); | |
| 48 | + assert_eq!(repo.state(), Some(git::state::InProgress::Merge)); | |
| 11 | 49 | |
| 12 | − assert_eq!(repo.in_progress_operation(), Some(git::state::InProgress::CherryPick)); | |
| 13 | 50 | Ok(()) |
| 14 | 51 | } |
| 15 | 52 | |
| @@ -17,12 +54,8 @@ fn cherry_pick() -> Result { · 1 unmodified line | ||
| 17 | 54 | fn rebase_interactive() -> Result { |
| 18 | 55 | let repo = named_repo("make_rebase_i_repo.sh")?; |
| 19 | 56 | |
| 20 | − let head = repo.head()?; | |
| 21 | − assert!(head.is_detached()); | |
| 22 | − assert_eq!( | |
| 23 | − repo.in_progress_operation(), | |
| 24 | − Some(git::state::InProgress::RebaseInteractive) | |
| 25 | − ); | |
| 57 | + assert!(repo.head()?.is_detached()); | |
| 58 | + assert_eq!(repo.state(), Some(git::state::InProgress::RebaseInteractive)); | |
| 26 | 59 | |
| 27 | 60 | Ok(()) |
| 28 | 61 | } |
| @@ -31,11 +64,18 @@ fn rebase_interactive() -> Result { · 2 unmodified lines | ||
| 31 | 64 | fn revert() -> Result { |
| 32 | 65 | let repo = named_repo("make_revert_repo.sh")?; |
| 33 | 66 | |
| 34 | − let head = repo.head()?; | |
| 35 | − let head_name = head.referent_name().expect("no detached head").shorten(); | |
| 36 | − assert_eq!(head_name, "main"); | |
| 67 | + assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); | |
| 68 | + assert_eq!(repo.state(), Some(git::state::InProgress::Revert)); | |
| 69 | + | |
| 70 | + Ok(()) | |
| 71 | +} | |
| 72 | + | |
| 73 | +#[test] | |
| 74 | +fn revert_sequence() -> Result { | |
| 75 | + let repo = named_repo("make_revert_sequence_repo.sh")?; | |
| 37 | 76 | |
| 38 | − assert_eq!(repo.in_progress_operation(), Some(git::state::InProgress::Revert)); | |
| 77 | + assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); | |
| 78 | + assert_eq!(repo.state(), Some(git::state::InProgress::RevertSequence)); | |
| 39 | 79 | |
| 40 | 80 | Ok(()) |
| 41 | 81 | } |
| @@ -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" |
| @@ -49,48 +49,85 @@ mod impl_ { · 48 unmodified lines | ||
| 49 | 49 | |
| 50 | 50 | pub fn is_path_owned_by_current_user(path: impl AsRef<Path>) -> std::io::Result<bool> { |
| 51 | 51 | use windows::{ |
| 52 | − core::PCWSTR, | |
| 52 | + core::{Error, PCWSTR}, | |
| 53 | 53 | Win32::{ |
| 54 | − Foundation::{BOOL, HANDLE, PSID}, | |
| 54 | + Foundation::{HANDLE, PSID}, | |
| 55 | 55 | Security::{ |
| 56 | 56 | Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT}, |
| 57 | − CheckTokenMembershipEx, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, | |
| 57 | + EqualSid, GetTokenInformation, TokenOwner, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, | |
| 58 | + TOKEN_OWNER, TOKEN_QUERY, | |
| 59 | + }, | |
| 60 | + System::{ | |
| 61 | + Memory::LocalFree, | |
| 62 | + Threading::{GetCurrentProcess, OpenProcessToken}, | |
| 58 | 63 | }, |
| 59 | − System::Memory::LocalFree, | |
| 60 | 64 | }, |
| 61 | 65 | }; |
| 62 | 66 | |
| 63 | 67 | let mut err_msg = None; |
| 64 | 68 | let mut is_owned = false; |
| 69 | + let path = path.as_ref(); | |
| 65 | 70 | |
| 66 | 71 | #[allow(unsafe_code)] |
| 67 | 72 | unsafe { |
| 68 | − let mut psid = PSID::default(); | |
| 73 | + let mut folder_owner = PSID::default(); | |
| 69 | 74 | let mut pdescriptor = PSECURITY_DESCRIPTOR::default(); |
| 70 | − let wpath = to_wide_path(&path); | |
| 71 | − | |
| 72 | 75 | let result = GetNamedSecurityInfoW( |
| 73 | − PCWSTR(wpath.as_ptr()), | |
| 76 | + PCWSTR(to_wide_path(path).as_ptr()), | |
| 74 | 77 | SE_FILE_OBJECT, |
| 75 | 78 | OWNER_SECURITY_INFORMATION, |
| 76 | − &mut psid, | |
| 79 | + &mut folder_owner, | |
| 77 | 80 | std::ptr::null_mut(), |
| 78 | 81 | std::ptr::null_mut(), |
| 79 | 82 | std::ptr::null_mut(), |
| 80 | 83 | &mut pdescriptor, |
| 81 | 84 | ); |
| 82 | 85 | |
| 86 | + // Workaround for https://github.com/microsoft/win32metadata/issues/884 | |
| 83 | 87 | if result.is_ok() { |
| 84 | − let mut is_member = BOOL(0); | |
| 85 | − if CheckTokenMembershipEx(HANDLE::default(), psid, 0, &mut is_member).as_bool() { | |
| 86 | − is_owned = is_member.as_bool(); | |
| 88 | + let mut token = HANDLE::default(); | |
| 89 | + OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).ok()?; | |
| 90 | + | |
| 91 | + let mut buffer_size = 0; | |
| 92 | + let mut buffer = Vec::<u8>::new(); | |
| 93 | + GetTokenInformation(token, TokenOwner, std::ptr::null_mut(), 0, &mut buffer_size); | |
| 94 | + if buffer_size != 0 { | |
| 95 | + buffer.resize(buffer_size as usize, 0); | |
| 96 | + if GetTokenInformation( | |
| 97 | + token, | |
| 98 | + TokenOwner, | |
| 99 | + buffer.as_mut_ptr() as _, | |
| 100 | + buffer_size, | |
| 101 | + &mut buffer_size, | |
| 102 | + ) | |
| 103 | + .as_bool() | |
| 104 | + { | |
| 105 | + let token_owner = buffer.as_ptr() as *const TOKEN_OWNER; | |
| 106 | + let token_owner = (*token_owner).Owner; | |
| 107 | + | |
| 108 | + is_owned = EqualSid(folder_owner, token_owner).as_bool(); | |
| 109 | + } else { | |
| 110 | + err_msg = format!( | |
| 111 | + "Couldn't get actual token information for current process with err: {}", | |
| 112 | + Error::from_win32() | |
| 113 | + ) | |
| 114 | + .into(); | |
| 115 | + } | |
| 87 | 116 | } else { |
| 88 | − err_msg = String::from("Could not check token membership").into(); | |
| 117 | + err_msg = format!( | |
| 118 | + "Couldn't get token information size info for current process with err: {}", | |
| 119 | + Error::from_win32() | |
| 120 | + ) | |
| 121 | + .into(); | |
| 89 | 122 | } |
| 90 | 123 | } else { |
| 91 | − err_msg = format!("Could not get security information for path with err: {:?}", result).into(); | |
| 124 | + err_msg = format!( | |
| 125 | + "Couldn't get security information for path '{}' with err {}", | |
| 126 | + path.display(), | |
| 127 | + Error::from_win32() | |
| 128 | + ) | |
| 129 | + .into(); | |
| 92 | 130 | } |
| 93 | − | |
| 94 | 131 | LocalFree(pdescriptor.0 as isize); |
| 95 | 132 | } |
| 96 | 133 | |
| @@ -1,8 +1,11 @@ | ||
| 1 | 1 | #![deny(unsafe_code, rust_2018_idioms, missing_docs)] |
| 2 | 2 | //! A shared trust model for `gitoxide` crates. |
| 3 | 3 | |
| 4 | −use std::marker::PhantomData; | |
| 5 | −use std::ops::Deref; | |
| 4 | +use std::{ | |
| 5 | + fmt::{Debug, Display, Formatter}, | |
| 6 | + marker::PhantomData, | |
| 7 | + ops::Deref, | |
| 8 | +}; | |
| 6 | 9 | |
| 7 | 10 | /// A way to specify how 'safe' we feel about a resource, typically about a git repository. |
| 8 | 11 | #[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] |
| @@ -74,20 +77,24 @@ pub mod trust { · 65 unmodified lines | ||
| 74 | 77 | |
| 75 | 78 | /// |
| 76 | 79 | pub mod permission { |
| 80 | + use std::fmt::{Debug, Display}; | |
| 81 | + | |
| 77 | 82 | use crate::Access; |
| 78 | 83 | |
| 79 | 84 | /// A marker trait to signal tags for permissions. |
| 80 | − pub trait Tag {} | |
| 85 | + pub trait Tag: Debug + Clone {} | |
| 81 | 86 | |
| 82 | 87 | /// A tag indicating that a permission is applying to the contents of a configuration file. |
| 88 | + #[derive(Debug, Clone)] | |
| 83 | 89 | pub struct Config; |
| 84 | 90 | impl Tag for Config {} |
| 85 | 91 | |
| 86 | 92 | /// A tag indicating that a permission is applying to the resource itself. |
| 93 | + #[derive(Debug, Clone)] | |
| 87 | 94 | pub struct Resource; |
| 88 | 95 | impl Tag for Resource {} |
| 89 | 96 | |
| 90 | − impl<P> Access<Config, P> { | |
| 97 | + impl<P: Debug + Display + Clone> Access<Config, P> { | |
| 91 | 98 | /// Create a permission for values contained in git configuration files. |
| 92 | 99 | /// |
| 93 | 100 | /// This applies permissions to values contained inside of these files. |
| @@ -99,7 +106,7 @@ pub mod permission { · 5 unmodified lines | ||
| 99 | 106 | } |
| 100 | 107 | } |
| 101 | 108 | |
| 102 | − impl<P> Access<Resource, P> { | |
| 109 | + impl<P: Debug + Display + Clone> Access<Resource, P> { | |
| 103 | 110 | /// Create a permission a file or directory itself. |
| 104 | 111 | /// |
| 105 | 112 | /// This applies permissions to a configuration file itself and whether it can be used at all, or to a directory |
| @@ -111,6 +118,20 @@ pub mod permission { · 5 unmodified lines | ||
| 111 | 118 | } |
| 112 | 119 | } |
| 113 | 120 | } |
| 121 | + | |
| 122 | + /// An error to use if an operation cannot proceed due to insufficient permissions. | |
| 123 | + /// | |
| 124 | + /// It's up to the implementation to decide which permission is required for an operation, and which one | |
| 125 | + /// causes errors. | |
| 126 | + #[cfg(feature = "thiserror")] | |
| 127 | + #[derive(Debug, thiserror::Error)] | |
| 128 | + #[error("Not allowed to handle resource {:?}: permission {}", .resource, .permission)] | |
| 129 | + pub struct Error<R: Debug, P: Debug + Display> { | |
| 130 | + /// The resource which cannot be used. | |
| 131 | + pub resource: R, | |
| 132 | + /// The permission causing it to be disallowed. | |
| 133 | + pub permission: P, | |
| 134 | + } | |
| 114 | 135 | } |
| 115 | 136 | |
| 116 | 137 | /// Allow, deny or forbid using a resource or performing an action. |
| @@ -125,6 +146,36 @@ pub enum Permission { · 8 unmodified lines | ||
| 125 | 146 | Allow, |
| 126 | 147 | } |
| 127 | 148 | |
| 149 | +impl Permission { | |
| 150 | + /// Check this permissions and produce a reply to indicate if the `resource` can be used and in which way. | |
| 151 | + /// | |
| 152 | + /// Only if this permission is set to `Allow` will the resource be usable. | |
| 153 | + #[cfg(feature = "thiserror")] | |
| 154 | + pub fn check<R: Debug>(&self, resource: R) -> Result<Option<R>, permission::Error<R, Self>> { | |
| 155 | + match self { | |
| 156 | + Permission::Allow => Ok(Some(resource)), | |
| 157 | + Permission::Deny => Ok(None), | |
| 158 | + Permission::Forbid => Err(permission::Error { | |
| 159 | + resource, | |
| 160 | + permission: *self, | |
| 161 | + }), | |
| 162 | + } | |
| 163 | + } | |
| 164 | +} | |
| 165 | + | |
| 166 | +impl Display for Permission { | |
| 167 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| 168 | + Display::fmt( | |
| 169 | + match self { | |
| 170 | + Permission::Allow => "allowed", | |
| 171 | + Permission::Deny => "denied", | |
| 172 | + Permission::Forbid => "forbidden", | |
| 173 | + }, | |
| 174 | + f, | |
| 175 | + ) | |
| 176 | + } | |
| 177 | +} | |
| 178 | + | |
| 128 | 179 | bitflags::bitflags! { |
| 129 | 180 | /// Whether something can be read or written. |
| 130 | 181 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| @@ -136,14 +187,27 @@ bitflags::bitflags! { · 5 unmodified lines | ||
| 136 | 187 | } |
| 137 | 188 | } |
| 138 | 189 | |
| 190 | +impl Display for ReadWrite { | |
| 191 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| 192 | + Debug::fmt(self, f) | |
| 193 | + } | |
| 194 | +} | |
| 195 | + | |
| 139 | 196 | /// A container to define tagged access permissions, rendering the permission read-only. |
| 140 | −pub struct Access<T: permission::Tag, P> { | |
| 197 | +#[derive(Debug, Clone)] | |
| 198 | +pub struct Access<T: permission::Tag, P: Debug + Display + Clone> { | |
| 141 | 199 | /// The access permission itself. |
| 142 | 200 | permission: P, |
| 143 | 201 | _data: PhantomData<T>, |
| 144 | 202 | } |
| 145 | 203 | |
| 146 | −impl<T: permission::Tag, P> Deref for Access<T, P> { | |
| 204 | +impl<T: permission::Tag, P: Debug + Display + Clone> Display for Access<T, P> { | |
| 205 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| 206 | + Display::fmt(&self.permission, f) | |
| 207 | + } | |
| 208 | +} | |
| 209 | + | |
| 210 | +impl<T: permission::Tag, P: Debug + Display + Clone> Deref for Access<T, P> { | |
| 147 | 211 | type Target = P; |
| 148 | 212 | |
| 149 | 213 | fn deref(&self) -> &Self::Target { |
| @@ -0,0 +1,29 @@ | ||
| 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.0.0 (2022-04-21) | |
| 9 | + | |
| 10 | +An empty crate without any content to reserve the name for the gitoxide project. | |
| 11 | + | |
| 12 | +### Commit Statistics | |
| 13 | + | |
| 14 | +<csr-read-only-do-not-edit/> | |
| 15 | + | |
| 16 | + - 1 commit contributed to the release. | |
| 17 | + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 18 | + - 1 unique issue was worked on: [#396](https://github.com/Byron/gitoxide/issues/396) | |
| 19 | + | |
| 20 | +### Commit Details | |
| 21 | + | |
| 22 | +<csr-read-only-do-not-edit/> | |
| 23 | + | |
| 24 | +<details><summary>view details</summary> | |
| 25 | + | |
| 26 | + * **[#396](https://github.com/Byron/gitoxide/issues/396)** | |
| 27 | + - Add git-sequencer crate ([`f8160d0`](https://github.com/Byron/gitoxide/commit/f8160d0d0caa6e616a6d0efed346722d093ad60a)) | |
| 28 | +</details> | |
| 29 | + | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +[package] | |
| 2 | +name = "git-sequencer" | |
| 3 | +version = "0.0.0" | |
| 4 | +repository = "https://github.com/Byron/gitoxide" | |
| 5 | +license = "MIT/Apache-2.0" | |
| 6 | +description = "A WIP crate of the gitoxide project handling sequences of human-aided operations" | |
| 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] | |
| @@ -0,0 +1 @@ | ||
| 1 | +#![forbid(unsafe_code, rust_2018_idioms)] | |
| @@ -26,9 +26,8 @@ pub mod capabilities; · 25 unmodified lines | ||
| 26 | 26 | pub use capabilities::Capabilities; |
| 27 | 27 | |
| 28 | 28 | mod non_io_types; |
| 29 | −pub use non_io_types::{Error, MessageKind, WriteMode}; | |
| 30 | − | |
| 31 | 29 | pub use git_sec::identity::Account; |
| 30 | +pub use non_io_types::{Error, MessageKind, WriteMode}; | |
| 32 | 31 | |
| 33 | 32 | /// |
| 34 | 33 | #[cfg(any(feature = "blocking-client", feature = "async-client"))] |
| @@ -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"]} |
| @@ -0,0 +1,140 @@ | ||
| 1 | +use std::path::{Path, PathBuf}; | |
| 2 | + | |
| 3 | +use bstr::{BStr, ByteSlice}; | |
| 4 | +use git_hash::oid; | |
| 5 | + | |
| 6 | +use super::Cache; | |
| 7 | +use crate::{fs, fs::PathOidMapping}; | |
| 8 | + | |
| 9 | +#[derive(Clone)] | |
| 10 | +pub enum State { | |
| 11 | + /// Useful for checkout where directories need creation, but we need to access attributes as well. | |
| 12 | + CreateDirectoryAndAttributesStack { | |
| 13 | + /// If there is a symlink or a file in our path, try to unlink it before creating the directory. | |
| 14 | + unlink_on_collision: bool, | |
| 15 | + | |
| 16 | + /// just for testing | |
| 17 | + #[cfg(debug_assertions)] | |
| 18 | + test_mkdir_calls: usize, | |
| 19 | + /// State to handle attribute information | |
| 20 | + attributes: state::Attributes, | |
| 21 | + }, | |
| 22 | + /// Used when adding files, requiring access to both attributes and ignore information, for example during add operations. | |
| 23 | + AttributesAndIgnoreStack { | |
| 24 | + /// State to handle attribute information | |
| 25 | + attributes: state::Attributes, | |
| 26 | + /// State to handle exclusion information | |
| 27 | + ignore: state::Ignore, | |
| 28 | + }, | |
| 29 | + /// Used when providing worktree status information. | |
| 30 | + IgnoreStack(state::Ignore), | |
| 31 | +} | |
| 32 | + | |
| 33 | +#[cfg(debug_assertions)] | |
| 34 | +impl<'paths> Cache<'paths> { | |
| 35 | + pub fn set_case(&mut self, case: git_glob::pattern::Case) { | |
| 36 | + self.case = case; | |
| 37 | + } | |
| 38 | + pub fn num_mkdir_calls(&self) -> usize { | |
| 39 | + match self.state { | |
| 40 | + State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } => test_mkdir_calls, | |
| 41 | + _ => 0, | |
| 42 | + } | |
| 43 | + } | |
| 44 | + | |
| 45 | + pub fn reset_mkdir_calls(&mut self) { | |
| 46 | + if let State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } = &mut self.state { | |
| 47 | + *test_mkdir_calls = 0; | |
| 48 | + } | |
| 49 | + } | |
| 50 | + | |
| 51 | + pub fn unlink_on_collision(&mut self, value: bool) { | |
| 52 | + if let State::CreateDirectoryAndAttributesStack { | |
| 53 | + unlink_on_collision, .. | |
| 54 | + } = &mut self.state | |
| 55 | + { | |
| 56 | + *unlink_on_collision = value; | |
| 57 | + } | |
| 58 | + } | |
| 59 | +} | |
| 60 | + | |
| 61 | +#[must_use] | |
| 62 | +pub struct Platform<'a, 'paths> { | |
| 63 | + parent: &'a Cache<'paths>, | |
| 64 | + is_dir: Option<bool>, | |
| 65 | +} | |
| 66 | + | |
| 67 | +impl<'paths> Cache<'paths> { | |
| 68 | + /// Create a new instance with `worktree_root` being the base for all future paths we handle, assuming it to be valid which includes | |
| 69 | + /// symbolic links to be included in it as well. | |
| 70 | + /// The `case` configures attribute and exclusion query case sensitivity. | |
| 71 | + pub fn new( | |
| 72 | + worktree_root: impl Into<PathBuf>, | |
| 73 | + state: State, | |
| 74 | + case: git_glob::pattern::Case, | |
| 75 | + buf: Vec<u8>, | |
| 76 | + attribute_files_in_index: Vec<PathOidMapping<'paths>>, | |
| 77 | + ) -> Self { | |
| 78 | + let root = worktree_root.into(); | |
| 79 | + Cache { | |
| 80 | + stack: fs::Stack::new(root), | |
| 81 | + state, | |
| 82 | + case, | |
| 83 | + buf, | |
| 84 | + attribute_files_in_index, | |
| 85 | + } | |
| 86 | + } | |
| 87 | + | |
| 88 | + /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories | |
| 89 | + /// unless `is_dir` is known (`Some(…)`) then `relative` points to a directory itself in which case the entire resulting | |
| 90 | + /// path is created as directory. If it's not known it is assumed to be a file. | |
| 91 | + /// | |
| 92 | + /// Provide access to cached information for that `relative` entry via the platform returned. | |
| 93 | + pub fn at_path<Find, E>( | |
| 94 | + &mut self, | |
| 95 | + relative: impl AsRef<Path>, | |
| 96 | + is_dir: Option<bool>, | |
| 97 | + find: Find, | |
| 98 | + ) -> std::io::Result<Platform<'_, 'paths>> | |
| 99 | + where | |
| 100 | + Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>, | |
| 101 | + E: std::error::Error + Send + Sync + 'static, | |
| 102 | + { | |
| 103 | + let mut delegate = platform::StackDelegate { | |
| 104 | + state: &mut self.state, | |
| 105 | + buf: &mut self.buf, | |
| 106 | + is_dir: is_dir.unwrap_or(false), | |
| 107 | + attribute_files_in_index: &self.attribute_files_in_index, | |
| 108 | + find, | |
| 109 | + }; | |
| 110 | + self.stack.make_relative_path_current(relative, &mut delegate)?; | |
| 111 | + Ok(Platform { parent: self, is_dir }) | |
| 112 | + } | |
| 113 | + | |
| 114 | + /// **Panics** on illformed UTF8 in `relative` | |
| 115 | + // TODO: more docs | |
| 116 | + pub fn at_entry<'r, Find, E>( | |
| 117 | + &mut self, | |
| 118 | + relative: impl Into<&'r BStr>, | |
| 119 | + is_dir: Option<bool>, | |
| 120 | + find: Find, | |
| 121 | + ) -> std::io::Result<Platform<'_, 'paths>> | |
| 122 | + where | |
| 123 | + Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>, | |
| 124 | + E: std::error::Error + Send + Sync + 'static, | |
| 125 | + { | |
| 126 | + let relative = relative.into(); | |
| 127 | + let relative_path = git_path::from_bstr(relative); | |
| 128 | + | |
| 129 | + self.at_path( | |
| 130 | + relative_path, | |
| 131 | + is_dir.or_else(|| relative.ends_with_str("/").then(|| true)), | |
| 132 | + // is_dir, | |
| 133 | + find, | |
| 134 | + ) | |
| 135 | + } | |
| 136 | +} | |
| 137 | + | |
| 138 | +mod platform; | |
| 139 | +/// | |
| 140 | +pub mod state; | |
| @@ -0,0 +1,171 @@ | ||
| 1 | +use std::path::Path; | |
| 2 | + | |
| 3 | +use bstr::ByteSlice; | |
| 4 | +use git_hash::oid; | |
| 5 | + | |
| 6 | +use crate::{ | |
| 7 | + fs, | |
| 8 | + fs::{ | |
| 9 | + cache::{Platform, State}, | |
| 10 | + PathOidMapping, | |
| 11 | + }, | |
| 12 | +}; | |
| 13 | + | |
| 14 | +impl<'a, 'paths> Platform<'a, 'paths> { | |
| 15 | + /// The full path to `relative` will be returned for use on the file system. | |
| 16 | + pub fn path(&self) -> &'a Path { | |
| 17 | + self.parent.stack.current() | |
| 18 | + } | |
| 19 | + | |
| 20 | + /// See if the currently set entry is excluded as per exclude and git-ignore files. | |
| 21 | + /// | |
| 22 | + /// # Panics | |
| 23 | + /// | |
| 24 | + /// If the cache was configured without exclude patterns. | |
| 25 | + pub fn is_excluded(&self) -> bool { | |
| 26 | + self.matching_exclude_pattern() | |
| 27 | + .map_or(false, |m| m.pattern.is_negative()) | |
| 28 | + } | |
| 29 | + | |
| 30 | + /// Check all exclude patterns to see if the currently set path matches any of them. | |
| 31 | + /// | |
| 32 | + /// Note that this pattern might be negated, and means this path in included. | |
| 33 | + /// | |
| 34 | + /// # Panics | |
| 35 | + /// | |
| 36 | + /// If the cache was configured without exclude patterns. | |
| 37 | + pub fn matching_exclude_pattern(&self) -> Option<git_attributes::Match<'_, ()>> { | |
| 38 | + let ignore = self.parent.state.ignore_or_panic(); | |
| 39 | + let relative_path = | |
| 40 | + git_path::to_unix_separators_on_windows(git_path::into_bstr(self.parent.stack.current_relative.as_path())); | |
| 41 | + ignore.matching_exclude_pattern(relative_path.as_bstr(), self.is_dir, self.parent.case) | |
| 42 | + } | |
| 43 | +} | |
| 44 | + | |
| 45 | +impl<'a, 'paths> std::fmt::Debug for Platform<'a, 'paths> { | |
| 46 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 47 | + std::fmt::Debug::fmt(&self.path(), f) | |
| 48 | + } | |
| 49 | +} | |
| 50 | + | |
| 51 | +pub struct StackDelegate<'a, 'paths, Find> { | |
| 52 | + pub state: &'a mut State, | |
| 53 | + pub buf: &'a mut Vec<u8>, | |
| 54 | + pub is_dir: bool, | |
| 55 | + pub attribute_files_in_index: &'a Vec<PathOidMapping<'paths>>, | |
| 56 | + pub find: Find, | |
| 57 | +} | |
| 58 | + | |
| 59 | +impl<'a, 'paths, Find, E> fs::stack::Delegate for StackDelegate<'a, 'paths, Find> | |
| 60 | +where | |
| 61 | + Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<git_object::BlobRef<'b>, E>, | |
| 62 | + E: std::error::Error + Send + Sync + 'static, | |
| 63 | +{ | |
| 64 | + fn push_directory(&mut self, stack: &fs::Stack) -> std::io::Result<()> { | |
| 65 | + match &mut self.state { | |
| 66 | + State::CreateDirectoryAndAttributesStack { attributes: _, .. } => { | |
| 67 | + // TODO: attributes | |
| 68 | + } | |
| 69 | + State::AttributesAndIgnoreStack { ignore, attributes: _ } => { | |
| 70 | + // TODO: attributes | |
| 71 | + ignore.push_directory( | |
| 72 | + &stack.root, | |
| 73 | + &stack.current, | |
| 74 | + self.buf, | |
| 75 | + self.attribute_files_in_index, | |
| 76 | + &mut self.find, | |
| 77 | + )? | |
| 78 | + } | |
| 79 | + State::IgnoreStack(ignore) => ignore.push_directory( | |
| 80 | + &stack.root, | |
| 81 | + &stack.current, | |
| 82 | + self.buf, | |
| 83 | + self.attribute_files_in_index, | |
| 84 | + &mut self.find, | |
| 85 | + )?, | |
| 86 | + } | |
| 87 | + Ok(()) | |
| 88 | + } | |
| 89 | + | |
| 90 | + fn push(&mut self, is_last_component: bool, stack: &fs::Stack) -> std::io::Result<()> { | |
| 91 | + match &mut self.state { | |
| 92 | + State::CreateDirectoryAndAttributesStack { | |
| 93 | + #[cfg(debug_assertions)] | |
| 94 | + test_mkdir_calls, | |
| 95 | + unlink_on_collision, | |
| 96 | + attributes: _, | |
| 97 | + } => { | |
| 98 | + #[cfg(debug_assertions)] | |
| 99 | + { | |
| 100 | + create_leading_directory( | |
| 101 | + is_last_component, | |
| 102 | + stack, | |
| 103 | + self.is_dir, | |
| 104 | + test_mkdir_calls, | |
| 105 | + *unlink_on_collision, | |
| 106 | + )? | |
| 107 | + } | |
| 108 | + #[cfg(not(debug_assertions))] | |
| 109 | + { | |
| 110 | + create_leading_directory(is_last_component, stack, self.is_dir, *unlink_on_collision)? | |
| 111 | + } | |
| 112 | + } | |
| 113 | + State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {} | |
| 114 | + } | |
| 115 | + Ok(()) | |
| 116 | + } | |
| 117 | + | |
| 118 | + fn pop_directory(&mut self) { | |
| 119 | + match &mut self.state { | |
| 120 | + State::CreateDirectoryAndAttributesStack { attributes: _, .. } => { | |
| 121 | + // TODO: attributes | |
| 122 | + } | |
| 123 | + State::AttributesAndIgnoreStack { attributes: _, ignore } => { | |
| 124 | + // TODO: attributes | |
| 125 | + ignore.pop_directory(); | |
| 126 | + } | |
| 127 | + State::IgnoreStack(ignore) => { | |
| 128 | + ignore.pop_directory(); | |
| 129 | + } | |
| 130 | + } | |
| 131 | + } | |
| 132 | +} | |
| 133 | + | |
| 134 | +fn create_leading_directory( | |
| 135 | + is_last_component: bool, | |
| 136 | + stack: &fs::Stack, | |
| 137 | + is_dir: bool, | |
| 138 | + #[cfg(debug_assertions)] mkdir_calls: &mut usize, | |
| 139 | + unlink_on_collision: bool, | |
| 140 | +) -> std::io::Result<()> { | |
| 141 | + if is_last_component && !is_dir { | |
| 142 | + return Ok(()); | |
| 143 | + } | |
| 144 | + #[cfg(debug_assertions)] | |
| 145 | + { | |
| 146 | + *mkdir_calls += 1; | |
| 147 | + } | |
| 148 | + match std::fs::create_dir(stack.current()) { | |
| 149 | + Ok(()) => Ok(()), | |
| 150 | + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | |
| 151 | + let meta = stack.current().symlink_metadata()?; | |
| 152 | + if meta.is_dir() { | |
| 153 | + Ok(()) | |
| 154 | + } else if unlink_on_collision { | |
| 155 | + if meta.file_type().is_symlink() { | |
| 156 | + crate::os::remove_symlink(stack.current())?; | |
| 157 | + } else { | |
| 158 | + std::fs::remove_file(stack.current())?; | |
| 159 | + } | |
| 160 | + #[cfg(debug_assertions)] | |
| 161 | + { | |
| 162 | + *mkdir_calls += 1; | |
| 163 | + } | |
| 164 | + std::fs::create_dir(stack.current()) | |
| 165 | + } else { | |
| 166 | + Err(err) | |
| 167 | + } | |
| 168 | + } | |
| 169 | + Err(err) => Err(err), | |
| 170 | + } | |
| 171 | +} | |
| @@ -0,0 +1,288 @@ | ||
| 1 | +use std::path::Path; | |
| 2 | + | |
| 3 | +use bstr::{BStr, BString, ByteSlice}; | |
| 4 | +use git_glob::pattern::Case; | |
| 5 | +use git_hash::oid; | |
| 6 | + | |
| 7 | +use crate::fs::{cache::State, PathOidMapping}; | |
| 8 | + | |
| 9 | +type AttributeMatchGroup = git_attributes::MatchGroup<git_attributes::Attributes>; | |
| 10 | +type IgnoreMatchGroup = git_attributes::MatchGroup<git_attributes::Ignore>; | |
| 11 | + | |
| 12 | +/// State related to attributes associated with files in the repository. | |
| 13 | +#[derive(Default, Clone)] | |
| 14 | +#[allow(unused)] | |
| 15 | +pub struct Attributes { | |
| 16 | + /// Attribute patterns that match the currently set directory (in the stack). | |
| 17 | + pub stack: AttributeMatchGroup, | |
| 18 | + /// Attribute patterns which aren't tied to the repository root, hence are global. They are consulted last. | |
| 19 | + pub globals: AttributeMatchGroup, | |
| 20 | +} | |
| 21 | + | |
| 22 | +/// State related to the exclusion of files. | |
| 23 | +#[derive(Default, Clone)] | |
| 24 | +#[allow(unused)] | |
| 25 | +pub struct Ignore { | |
| 26 | + /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to | |
| 27 | + /// be consulted. | |
| 28 | + overrides: IgnoreMatchGroup, | |
| 29 | + /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. | |
| 30 | + stack: IgnoreMatchGroup, | |
| 31 | + /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. | |
| 32 | + globals: IgnoreMatchGroup, | |
| 33 | + /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the | |
| 34 | + /// currently set directory had a pattern matched. Note that this one could be negated. | |
| 35 | + /// (index into match groups, index into list of pattern lists, index into pattern list) | |
| 36 | + matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>, | |
| 37 | + /// The name of the file to look for in directories. | |
| 38 | + exclude_file_name_for_directories: BString, | |
| 39 | + /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine | |
| 40 | + /// to know if an entire path can be ignored as a parent directory is ignored. | |
| 41 | + case: Case, | |
| 42 | +} | |
| 43 | + | |
| 44 | +impl Ignore { | |
| 45 | + /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory | |
| 46 | + /// ignore files within the repository, defaults to`.gitignore`. | |
| 47 | + // TODO: more docs | |
| 48 | + pub fn new( | |
| 49 | + overrides: IgnoreMatchGroup, | |
| 50 | + globals: IgnoreMatchGroup, | |
| 51 | + exclude_file_name_for_directories: Option<&BStr>, | |
| 52 | + case: Case, | |
| 53 | + ) -> Self { | |
| 54 | + Ignore { | |
| 55 | + case, | |
| 56 | + overrides, | |
| 57 | + globals, | |
| 58 | + stack: Default::default(), | |
| 59 | + matched_directory_patterns_stack: Vec::with_capacity(6), | |
| 60 | + exclude_file_name_for_directories: exclude_file_name_for_directories | |
| 61 | + .map(ToOwned::to_owned) | |
| 62 | + .unwrap_or_else(|| ".gitignore".into()), | |
| 63 | + } | |
| 64 | + } | |
| 65 | +} | |
| 66 | + | |
| 67 | +impl Ignore { | |
| 68 | + pub(crate) fn pop_directory(&mut self) { | |
| 69 | + self.matched_directory_patterns_stack.pop().expect("something to pop"); | |
| 70 | + self.stack.patterns.pop().expect("something to pop"); | |
| 71 | + } | |
| 72 | + /// The match groups from lowest priority to highest. | |
| 73 | + pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] { | |
| 74 | + [&self.globals, &self.stack, &self.overrides] | |
| 75 | + } | |
| 76 | + | |
| 77 | + pub(crate) fn matching_exclude_pattern( | |
| 78 | + &self, | |
| 79 | + relative_path: &BStr, | |
| 80 | + is_dir: Option<bool>, | |
| 81 | + case: Case, | |
| 82 | + ) -> Option<git_attributes::Match<'_, ()>> { | |
| 83 | + let groups = self.match_groups(); | |
| 84 | + if let Some((source, mapping)) = self | |
| 85 | + .matched_directory_patterns_stack | |
| 86 | + .iter() | |
| 87 | + .rev() | |
| 88 | + .filter_map(|v| *v) | |
| 89 | + .map(|(gidx, plidx, pidx)| { | |
| 90 | + let list = &groups[gidx].patterns[plidx]; | |
| 91 | + (list.source.as_deref(), &list.patterns[pidx]) | |
| 92 | + }) | |
| 93 | + .next() | |
| 94 | + { | |
| 95 | + if !mapping.pattern.is_negative() { | |
| 96 | + return git_attributes::Match { | |
| 97 | + pattern: &mapping.pattern, | |
| 98 | + value: &mapping.value, | |
| 99 | + sequence_number: mapping.sequence_number, | |
| 100 | + source, | |
| 101 | + } | |
| 102 | + .into(); | |
| 103 | + } | |
| 104 | + } | |
| 105 | + groups | |
| 106 | + .iter() | |
| 107 | + .rev() | |
| 108 | + .find_map(|group| group.pattern_matching_relative_path(relative_path.as_ref(), is_dir, case)) | |
| 109 | + } | |
| 110 | + | |
| 111 | + /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded. | |
| 112 | + /// It returns a triple-index into our data structure from which a match can be reconstructed. | |
| 113 | + pub(crate) fn matching_exclude_pattern_no_dir( | |
| 114 | + &self, | |
| 115 | + relative_path: &BStr, | |
| 116 | + is_dir: Option<bool>, | |
| 117 | + case: Case, | |
| 118 | + ) -> Option<(usize, usize, usize)> { | |
| 119 | + let groups = self.match_groups(); | |
| 120 | + groups.iter().enumerate().rev().find_map(|(gidx, group)| { | |
| 121 | + let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); | |
| 122 | + group | |
| 123 | + .patterns | |
| 124 | + .iter() | |
| 125 | + .enumerate() | |
| 126 | + .rev() | |
| 127 | + .find_map(|(plidx, pl)| { | |
| 128 | + pl.pattern_idx_matching_relative_path(relative_path, basename_pos, is_dir, case) | |
| 129 | + .map(|idx| (plidx, idx)) | |
| 130 | + }) | |
| 131 | + .map(|(plidx, pidx)| (gidx, plidx, pidx)) | |
| 132 | + }) | |
| 133 | + } | |
| 134 | + | |
| 135 | + pub(crate) fn push_directory<Find, E>( | |
| 136 | + &mut self, | |
| 137 | + root: &Path, | |
| 138 | + dir: &Path, | |
| 139 | + buf: &mut Vec<u8>, | |
| 140 | + attribute_files_in_index: &[PathOidMapping<'_>], | |
| 141 | + mut find: Find, | |
| 142 | + ) -> std::io::Result<()> | |
| 143 | + where | |
| 144 | + Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<git_object::BlobRef<'b>, E>, | |
| 145 | + E: std::error::Error + Send + Sync + 'static, | |
| 146 | + { | |
| 147 | + let rela_dir = dir.strip_prefix(root).expect("dir in root"); | |
| 148 | + self.matched_directory_patterns_stack | |
| 149 | + .push(self.matching_exclude_pattern_no_dir(git_path::into_bstr(rela_dir).as_ref(), Some(true), self.case)); | |
| 150 | + | |
| 151 | + let ignore_path_relative = rela_dir.join(".gitignore"); | |
| 152 | + let ignore_path_relative = git_path::to_unix_separators_on_windows(git_path::into_bstr(ignore_path_relative)); | |
| 153 | + let ignore_file_in_index = | |
| 154 | + attribute_files_in_index.binary_search_by(|t| t.0.cmp(ignore_path_relative.as_ref())); | |
| 155 | + let follow_symlinks = ignore_file_in_index.is_err(); | |
| 156 | + if !self | |
| 157 | + .stack | |
| 158 | + .add_patterns_file(dir.join(".gitignore"), follow_symlinks, Some(root), buf)? | |
| 159 | + { | |
| 160 | + match ignore_file_in_index { | |
| 161 | + Ok(idx) => { | |
| 162 | + let ignore_blob = find(&attribute_files_in_index[idx].1, buf) | |
| 163 | + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; | |
| 164 | + let ignore_path = git_path::from_bstring(ignore_path_relative.into_owned()); | |
| 165 | + self.stack | |
| 166 | + .add_patterns_buffer(ignore_blob.data, ignore_path, Some(root)); | |
| 167 | + } | |
| 168 | + Err(_) => { | |
| 169 | + // Need one stack level per component so push and pop matches. | |
| 170 | + self.stack.patterns.push(Default::default()) | |
| 171 | + } | |
| 172 | + } | |
| 173 | + } | |
| 174 | + Ok(()) | |
| 175 | + } | |
| 176 | +} | |
| 177 | + | |
| 178 | +impl Attributes { | |
| 179 | + pub fn new(globals: AttributeMatchGroup) -> Self { | |
| 180 | + Attributes { | |
| 181 | + globals, | |
| 182 | + stack: Default::default(), | |
| 183 | + } | |
| 184 | + } | |
| 185 | +} | |
| 186 | + | |
| 187 | +impl From<AttributeMatchGroup> for Attributes { | |
| 188 | + fn from(group: AttributeMatchGroup) -> Self { | |
| 189 | + Attributes::new(group) | |
| 190 | + } | |
| 191 | +} | |
| 192 | + | |
| 193 | +impl State { | |
| 194 | + /// Configure a state to be suitable for checking out files. | |
| 195 | + pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { | |
| 196 | + State::CreateDirectoryAndAttributesStack { | |
| 197 | + unlink_on_collision, | |
| 198 | + #[cfg(debug_assertions)] | |
| 199 | + test_mkdir_calls: 0, | |
| 200 | + attributes, | |
| 201 | + } | |
| 202 | + } | |
| 203 | + | |
| 204 | + /// Configure a state for adding files. | |
| 205 | + pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self { | |
| 206 | + State::AttributesAndIgnoreStack { attributes, ignore } | |
| 207 | + } | |
| 208 | + | |
| 209 | + /// Configure a state for status retrieval. | |
| 210 | + pub fn for_status(ignore: Ignore) -> Self { | |
| 211 | + State::IgnoreStack(ignore) | |
| 212 | + } | |
| 213 | +} | |
| 214 | + | |
| 215 | +impl State { | |
| 216 | + /// Returns a vec of tuples of relative index paths along with the best usable OID for either ignore, attribute files or both. | |
| 217 | + /// | |
| 218 | + /// - ignores entries which aren't blobs | |
| 219 | + /// - ignores ignore entries which are not skip-worktree | |
| 220 | + /// - within merges, picks 'our' stage both for ignore and attribute files. | |
| 221 | + pub fn build_attribute_list<'paths>( | |
| 222 | + &self, | |
| 223 | + index: &git_index::State, | |
| 224 | + paths: &'paths git_index::PathStorage, | |
| 225 | + case: Case, | |
| 226 | + ) -> Vec<PathOidMapping<'paths>> { | |
| 227 | + let a1_backing; | |
| 228 | + let a2_backing; | |
| 229 | + let names = match self { | |
| 230 | + State::IgnoreStack(v) => { | |
| 231 | + a1_backing = [(v.exclude_file_name_for_directories.as_bytes().as_bstr(), true)]; | |
| 232 | + a1_backing.as_ref() | |
| 233 | + } | |
| 234 | + State::AttributesAndIgnoreStack { ignore, .. } => { | |
| 235 | + a2_backing = [ | |
| 236 | + (ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), true), | |
| 237 | + (".gitattributes".into(), false), | |
| 238 | + ]; | |
| 239 | + a2_backing.as_ref() | |
| 240 | + } | |
| 241 | + State::CreateDirectoryAndAttributesStack { .. } => { | |
| 242 | + a1_backing = [(".gitattributes".into(), true)]; | |
| 243 | + a1_backing.as_ref() | |
| 244 | + } | |
| 245 | + }; | |
| 246 | + | |
| 247 | + index | |
| 248 | + .entries() | |
| 249 | + .iter() | |
| 250 | + .filter_map(move |entry| { | |
| 251 | + let path = entry.path_in(paths); | |
| 252 | + | |
| 253 | + // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then | |
| 254 | + // there won't be a stage 0. | |
| 255 | + if entry.mode == git_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) { | |
| 256 | + let basename = path | |
| 257 | + .rfind_byte(b'/') | |
| 258 | + .map(|pos| path[pos + 1..].as_bstr()) | |
| 259 | + .unwrap_or(path); | |
| 260 | + let is_ignore = names.iter().find_map(|t| { | |
| 261 | + match case { | |
| 262 | + Case::Sensitive => basename == t.0, | |
| 263 | + Case::Fold => basename.eq_ignore_ascii_case(t.0), | |
| 264 | + } | |
| 265 | + .then(|| t.1) | |
| 266 | + })?; | |
| 267 | + // See https://github.com/git/git/blob/master/dir.c#L912:L912 | |
| 268 | + if is_ignore && !entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) { | |
| 269 | + return None; | |
| 270 | + } | |
| 271 | + Some((path, entry.id)) | |
| 272 | + } else { | |
| 273 | + None | |
| 274 | + } | |
| 275 | + }) | |
| 276 | + .collect() | |
| 277 | + } | |
| 278 | + | |
| 279 | + pub(crate) fn ignore_or_panic(&self) -> &Ignore { | |
| 280 | + match self { | |
| 281 | + State::IgnoreStack(v) => v, | |
| 282 | + State::AttributesAndIgnoreStack { ignore, .. } => ignore, | |
| 283 | + State::CreateDirectoryAndAttributesStack { .. } => { | |
| 284 | + unreachable!("BUG: must not try to check excludes without it being setup") | |
| 285 | + } | |
| 286 | + } | |
| 287 | + } | |
| 288 | +} | |
| @@ -1,24 +1,41 @@ | ||
| 1 | 1 | use std::path::Path; |
| 2 | 2 | |
| 3 | −/// Common knowledge about the worktree that is needed across most interactions with the work tree | |
| 4 | −#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 5 | −#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | |
| 6 | −pub struct Capabilities { | |
| 7 | − /// If true, the filesystem will store paths as decomposed unicode, i.e. `ä` becomes `"a\u{308}"`, which means that | |
| 8 | − /// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally | |
| 9 | − /// using it. This also applies to input received from the command-line, so callers may have to be aware of this and | |
| 10 | − /// perform conversions accordingly. | |
| 11 | − /// If false, no conversions will be performed. | |
| 12 | − pub precompose_unicode: bool, | |
| 13 | − /// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`. | |
| 14 | − /// This is also called case-folding. | |
| 15 | − pub ignore_case: bool, | |
| 16 | − /// If true, we assume the the executable bit is honored as part of the files mode. If false, we assume the file system | |
| 17 | − /// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on. | |
| 18 | − pub executable_bit: bool, | |
| 19 | − /// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked | |
| 20 | − /// out as files which contain the link as text. | |
| 21 | − pub symlink: bool, | |
| 3 | +use crate::fs::Capabilities; | |
| 4 | + | |
| 5 | +#[cfg(windows)] | |
| 6 | +impl Default for Capabilities { | |
| 7 | + fn default() -> Self { | |
| 8 | + Capabilities { | |
| 9 | + precompose_unicode: false, | |
| 10 | + ignore_case: true, | |
| 11 | + executable_bit: false, | |
| 12 | + symlink: false, | |
| 13 | + } | |
| 14 | + } | |
| 15 | +} | |
| 16 | + | |
| 17 | +#[cfg(target_os = "macos")] | |
| 18 | +impl Default for Capabilities { | |
| 19 | + fn default() -> Self { | |
| 20 | + Capabilities { | |
| 21 | + precompose_unicode: true, | |
| 22 | + ignore_case: true, | |
| 23 | + executable_bit: true, | |
| 24 | + symlink: true, | |
| 25 | + } | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +#[cfg(all(unix, not(target_os = "macos")))] | |
| 30 | +impl Default for Capabilities { | |
| 31 | + fn default() -> Self { | |
| 32 | + Capabilities { | |
| 33 | + precompose_unicode: false, | |
| 34 | + ignore_case: false, | |
| 35 | + executable_bit: true, | |
| 36 | + symlink: true, | |
| 37 | + } | |
| 38 | + } | |
| 22 | 39 | } |
| 23 | 40 | |
| 24 | 41 | impl Capabilities { |
| @@ -95,7 +112,7 @@ impl Capabilities { · 70 unmodified lines | ||
| 95 | 112 | return Ok(false); |
| 96 | 113 | } |
| 97 | 114 | |
| 98 | − let res = std::fs::symlink_metadata(&link_path).map(|m| m.is_symlink()); | |
| 115 | + let res = std::fs::symlink_metadata(&link_path).map(|m| m.file_type().is_symlink()); | |
| 99 | 116 | |
| 100 | 117 | let cleanup = crate::os::remove_symlink(&link_path).or_else(|_| std::fs::remove_file(&link_path)); |
| 101 | 118 | std::fs::remove_file(&src_path).and(cleanup)?; |
| @@ -103,39 +120,3 @@ impl Capabilities { · 1 unmodified line | ||
| 103 | 120 | res |
| 104 | 121 | } |
| 105 | 122 | } |
| 106 | − | |
| 107 | −#[cfg(windows)] | |
| 108 | −impl Default for Capabilities { | |
| 109 | − fn default() -> Self { | |
| 110 | − Capabilities { | |
| 111 | − precompose_unicode: false, | |
| 112 | − ignore_case: true, | |
| 113 | − executable_bit: false, | |
| 114 | − symlink: false, | |
| 115 | − } | |
| 116 | − } | |
| 117 | −} | |
| 118 | − | |
| 119 | −#[cfg(target_os = "macos")] | |
| 120 | −impl Default for Capabilities { | |
| 121 | − fn default() -> Self { | |
| 122 | − Capabilities { | |
| 123 | − precompose_unicode: true, | |
| 124 | − ignore_case: true, | |
| 125 | − executable_bit: true, | |
| 126 | − symlink: true, | |
| 127 | − } | |
| 128 | − } | |
| 129 | −} | |
| 130 | − | |
| 131 | −#[cfg(all(unix, not(target_os = "macos")))] | |
| 132 | −impl Default for Capabilities { | |
| 133 | − fn default() -> Self { | |
| 134 | − Capabilities { | |
| 135 | − precompose_unicode: false, | |
| 136 | − ignore_case: false, | |
| 137 | − executable_bit: true, | |
| 138 | − symlink: true, | |
| 139 | − } | |
| 140 | − } | |
| 141 | −} | |
| @@ -0,0 +1,80 @@ | ||
| 1 | +use std::path::PathBuf; | |
| 2 | + | |
| 3 | +use bstr::BStr; | |
| 4 | + | |
| 5 | +/// Common knowledge about the worktree that is needed across most interactions with the work tree | |
| 6 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 7 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | |
| 8 | +pub struct Capabilities { | |
| 9 | + /// If true, the filesystem will store paths as decomposed unicode, i.e. `ä` becomes `"a\u{308}"`, which means that | |
| 10 | + /// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally | |
| 11 | + /// using it. This also applies to input received from the command-line, so callers may have to be aware of this and | |
| 12 | + /// perform conversions accordingly. | |
| 13 | + /// If false, no conversions will be performed. | |
| 14 | + pub precompose_unicode: bool, | |
| 15 | + /// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`. | |
| 16 | + /// This is also called case-folding. | |
| 17 | + pub ignore_case: bool, | |
| 18 | + /// If true, we assume the the executable bit is honored as part of the files mode. If false, we assume the file system | |
| 19 | + /// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on. | |
| 20 | + pub executable_bit: bool, | |
| 21 | + /// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked | |
| 22 | + /// out as files which contain the link as text. | |
| 23 | + pub symlink: bool, | |
| 24 | +} | |
| 25 | + | |
| 26 | +#[derive(Clone)] | |
| 27 | +pub struct Stack { | |
| 28 | + /// The prefix/root for all paths we handle. | |
| 29 | + root: PathBuf, | |
| 30 | + /// the most recent known cached that we know is valid. | |
| 31 | + current: PathBuf, | |
| 32 | + /// The relative portion of `valid` that was added previously. | |
| 33 | + current_relative: PathBuf, | |
| 34 | + /// The amount of path components of 'current' beyond the roots components. | |
| 35 | + valid_components: usize, | |
| 36 | + /// If set, we assume the `current` element is a directory to affect calls to `(push|pop)_directory()`. | |
| 37 | + current_is_directory: bool, | |
| 38 | +} | |
| 39 | + | |
| 40 | +/// A cache for efficiently executing operations on directories and files which are encountered in sorted order. | |
| 41 | +/// That way, these operations can be re-used for subsequent invocations in the same directory. | |
| 42 | +/// | |
| 43 | +/// This cache can be configured to create directories efficiently, read git-ignore files and git-attribute files, | |
| 44 | +/// in any combination. | |
| 45 | +/// | |
| 46 | +/// A cache for directory creation to reduce the amount of stat calls when creating | |
| 47 | +/// directories safely, that is without following symlinks that might be on the way. | |
| 48 | +/// | |
| 49 | +/// As a special case, it offers a 'prefix' which (by itself) is assumed to exist and may contain symlinks. | |
| 50 | +/// Everything past that prefix boundary must not contain a symlink. We do this by allowing any input path. | |
| 51 | +/// | |
| 52 | +/// Another added benefit is its ability to store the path of full path of the entry to which leading directories | |
| 53 | +/// are to be created to avoid allocating memory. | |
| 54 | +/// | |
| 55 | +/// For this to work, it remembers the last 'good' path to a directory and assumes that all components of it | |
| 56 | +/// are still valid, too. | |
| 57 | +/// As directories are created, the cache will be adjusted to reflect the latest seen directory. | |
| 58 | +/// | |
| 59 | +/// The caching is only useful if consecutive calls to create a directory are using a sorted list of entries. | |
| 60 | +#[derive(Clone)] | |
| 61 | +pub struct Cache<'paths> { | |
| 62 | + stack: Stack, | |
| 63 | + /// tells us what to do as we change paths. | |
| 64 | + state: cache::State, | |
| 65 | + /// A buffer used when reading attribute or ignore files or their respective objects from the object database. | |
| 66 | + buf: Vec<u8>, | |
| 67 | + /// If case folding should happen when looking up attributes or exclusions. | |
| 68 | + case: git_glob::pattern::Case, | |
| 69 | + /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions. | |
| 70 | + attribute_files_in_index: Vec<PathOidMapping<'paths>>, | |
| 71 | +} | |
| 72 | + | |
| 73 | +pub(crate) type PathOidMapping<'paths> = (&'paths BStr, git_hash::ObjectId); | |
| 74 | + | |
| 75 | +/// | |
| 76 | +pub mod cache; | |
| 77 | +/// | |
| 78 | +pub mod stack; | |
| 79 | + | |
| 80 | +mod capabilities; | |
| @@ -0,0 +1,107 @@ | ||
| 1 | +use std::path::{Path, PathBuf}; | |
| 2 | + | |
| 3 | +use crate::fs::Stack; | |
| 4 | + | |
| 5 | +impl Stack { | |
| 6 | + pub fn root(&self) -> &Path { | |
| 7 | + &self.root | |
| 8 | + } | |
| 9 | + | |
| 10 | + pub fn current(&self) -> &Path { | |
| 11 | + &self.current | |
| 12 | + } | |
| 13 | + | |
| 14 | + pub fn current_relative(&self) -> &Path { | |
| 15 | + &self.current_relative | |
| 16 | + } | |
| 17 | +} | |
| 18 | + | |
| 19 | +pub trait Delegate { | |
| 20 | + fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>; | |
| 21 | + fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>; | |
| 22 | + fn pop_directory(&mut self); | |
| 23 | +} | |
| 24 | + | |
| 25 | +impl Stack { | |
| 26 | + /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes | |
| 27 | + /// symbolic links to be included in it as well. | |
| 28 | + pub fn new(root: impl Into<PathBuf>) -> Self { | |
| 29 | + let root = root.into(); | |
| 30 | + Stack { | |
| 31 | + current: root.clone(), | |
| 32 | + current_relative: PathBuf::with_capacity(128), | |
| 33 | + valid_components: 0, | |
| 34 | + root, | |
| 35 | + current_is_directory: true, | |
| 36 | + } | |
| 37 | + } | |
| 38 | + | |
| 39 | + /// Set the current stack to point to the `relative` path and call `push_comp()` each time a new path component is popped | |
| 40 | + /// along with the stacks state for inspection to perform an operation that produces some data. | |
| 41 | + /// | |
| 42 | + /// The full path to `relative` will be returned along with the data returned by push_comp. | |
| 43 | + /// Note that this only works correctly for the delegate's `push_directory()` and `pop_directory()` methods if | |
| 44 | + /// `relative` paths are terminal, so point to their designated file or directory. | |
| 45 | + pub fn make_relative_path_current( | |
| 46 | + &mut self, | |
| 47 | + relative: impl AsRef<Path>, | |
| 48 | + delegate: &mut impl Delegate, | |
| 49 | + ) -> std::io::Result<()> { | |
| 50 | + let relative = relative.as_ref(); | |
| 51 | + debug_assert!( | |
| 52 | + relative.is_relative(), | |
| 53 | + "only index paths are handled correctly here, must be relative" | |
| 54 | + ); | |
| 55 | + debug_assert!(!relative.to_string_lossy().is_empty(), "empty paths are not allowed"); | |
| 56 | + | |
| 57 | + if self.valid_components == 0 { | |
| 58 | + delegate.push_directory(self)?; | |
| 59 | + } | |
| 60 | + | |
| 61 | + let mut components = relative.components().peekable(); | |
| 62 | + let mut existing_components = self.current_relative.components(); | |
| 63 | + let mut matching_components = 0; | |
| 64 | + while let (Some(existing_comp), Some(new_comp)) = (existing_components.next(), components.peek()) { | |
| 65 | + if existing_comp == *new_comp { | |
| 66 | + components.next(); | |
| 67 | + matching_components += 1; | |
| 68 | + } else { | |
| 69 | + break; | |
| 70 | + } | |
| 71 | + } | |
| 72 | + | |
| 73 | + for _ in 0..self.valid_components - matching_components { | |
| 74 | + self.current.pop(); | |
| 75 | + self.current_relative.pop(); | |
| 76 | + if self.current_is_directory { | |
| 77 | + delegate.pop_directory(); | |
| 78 | + } | |
| 79 | + self.current_is_directory = true; | |
| 80 | + } | |
| 81 | + self.valid_components = matching_components; | |
| 82 | + | |
| 83 | + if !self.current_is_directory && components.peek().is_some() { | |
| 84 | + delegate.push_directory(self)?; | |
| 85 | + } | |
| 86 | + | |
| 87 | + while let Some(comp) = components.next() { | |
| 88 | + let is_last_component = components.peek().is_none(); | |
| 89 | + self.current_is_directory = !is_last_component; | |
| 90 | + self.current.push(comp); | |
| 91 | + self.current_relative.push(comp); | |
| 92 | + self.valid_components += 1; | |
| 93 | + let res = delegate.push(is_last_component, self); | |
| 94 | + if self.current_is_directory { | |
| 95 | + delegate.push_directory(self)?; | |
| 96 | + } | |
| 97 | + | |
| 98 | + if let Err(err) = res { | |
| 99 | + self.current.pop(); | |
| 100 | + self.current_relative.pop(); | |
| 101 | + self.valid_components -= 1; | |
| 102 | + return Err(err); | |
| 103 | + } | |
| 104 | + } | |
| 105 | + Ok(()) | |
| 106 | + } | |
| 107 | +} | |
| @@ -1,146 +1,5 @@ | ||
| 1 | −use std::path::PathBuf; | |
| 2 | − | |
| 3 | 1 | use bstr::BString; |
| 4 | − | |
| 5 | −/// A cache for directory creation to reduce the amount of stat calls when creating | |
| 6 | −/// directories safely, that is without following symlinks that might be on the way. | |
| 7 | −/// | |
| 8 | −/// As a special case, it offers a 'prefix' which (by itself) is assumed to exist and may contain symlinks. | |
| 9 | −/// Everything past that prefix boundary must not contain a symlink. We do this by allowing any input path. | |
| 10 | −/// | |
| 11 | −/// Another added benefit is its ability to store the path of full path of the entry to which leading directories | |
| 12 | −/// are to be created to avoid allocating memory. | |
| 13 | −/// | |
| 14 | −/// For this to work, it remembers the last 'good' path to a directory and assumes that all components of it | |
| 15 | −/// are still valid, too. | |
| 16 | −/// As directories are created, the cache will be adjusted to reflect the latest seen directory. | |
| 17 | −/// | |
| 18 | −/// The caching is only useful if consecutive calls to create a directory are using a sorted list of entries. | |
| 19 | −#[allow(unused)] | |
| 20 | −pub struct PathCache { | |
| 21 | − /// The prefix/root for all paths we handle. | |
| 22 | − root: PathBuf, | |
| 23 | − /// the most recent known cached that we know is valid. | |
| 24 | − valid: PathBuf, | |
| 25 | − /// The relative portion of `valid` that was added previously. | |
| 26 | − valid_relative: PathBuf, | |
| 27 | − /// The amount of path components of 'valid' beyond the roots components. If `root` has 2, and this is 2, `valid` has 4 components. | |
| 28 | − valid_components: usize, | |
| 29 | − | |
| 30 | − /// If there is a symlink or a file in our path, try to unlink it before creating the directory. | |
| 31 | − pub unlink_on_collision: bool, | |
| 32 | − | |
| 33 | − /// just for testing | |
| 34 | − #[cfg(debug_assertions)] | |
| 35 | − pub test_mkdir_calls: usize, | |
| 36 | −} | |
| 37 | − | |
| 38 | −mod cache { | |
| 39 | − use std::path::{Path, PathBuf}; | |
| 40 | − | |
| 41 | − use super::PathCache; | |
| 42 | − use crate::os; | |
| 43 | − | |
| 44 | − impl PathCache { | |
| 45 | − /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes | |
| 46 | − /// symbolic links to be included in it as well. | |
| 47 | − pub fn new(root: impl Into<PathBuf>) -> Self { | |
| 48 | − let root = root.into(); | |
| 49 | − PathCache { | |
| 50 | − valid: root.clone(), | |
| 51 | − valid_relative: PathBuf::with_capacity(128), | |
| 52 | − valid_components: 0, | |
| 53 | − root, | |
| 54 | − #[cfg(debug_assertions)] | |
| 55 | − test_mkdir_calls: 0, | |
| 56 | − unlink_on_collision: false, | |
| 57 | − } | |
| 58 | − } | |
| 59 | − | |
| 60 | − /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories | |
| 61 | − /// unless `mode` indicates `relative` points to a directory itself in which case the entire resulting path is created as directory. | |
| 62 | − /// | |
| 63 | − /// The full path to `relative` will be returned for use on the file system. | |
| 64 | − pub fn append_relative_path_assure_leading_dir( | |
| 65 | − &mut self, | |
| 66 | − relative: impl AsRef<Path>, | |
| 67 | − mode: git_index::entry::Mode, | |
| 68 | − ) -> std::io::Result<&Path> { | |
| 69 | − let relative = relative.as_ref(); | |
| 70 | − debug_assert!( | |
| 71 | − relative.is_relative(), | |
| 72 | − "only index paths are handled correctly here, must be relative" | |
| 73 | − ); | |
| 74 | − | |
| 75 | − let mut components = relative.components().peekable(); | |
| 76 | − let mut existing_components = self.valid_relative.components(); | |
| 77 | − let mut matching_components = 0; | |
| 78 | − while let (Some(existing_comp), Some(new_comp)) = (existing_components.next(), components.peek()) { | |
| 79 | − if existing_comp == *new_comp { | |
| 80 | − components.next(); | |
| 81 | − matching_components += 1; | |
| 82 | − } else { | |
| 83 | − break; | |
| 84 | − } | |
| 85 | − } | |
| 86 | − | |
| 87 | − for _ in 0..self.valid_components - matching_components { | |
| 88 | − self.valid.pop(); | |
| 89 | − self.valid_relative.pop(); | |
| 90 | − } | |
| 91 | − | |
| 92 | − self.valid_components = matching_components; | |
| 93 | − | |
| 94 | − let target_is_dir = mode == git_index::entry::Mode::COMMIT || mode == git_index::entry::Mode::DIR; | |
| 95 | − while let Some(comp) = components.next() { | |
| 96 | − self.valid.push(comp); | |
| 97 | − self.valid_relative.push(comp); | |
| 98 | − self.valid_components += 1; | |
| 99 | − let res = (|| { | |
| 100 | − if components.peek().is_some() || target_is_dir { | |
| 101 | − #[cfg(debug_assertions)] | |
| 102 | − { | |
| 103 | − self.test_mkdir_calls += 1; | |
| 104 | − } | |
| 105 | − match std::fs::create_dir(&self.valid) { | |
| 106 | − Ok(()) => {} | |
| 107 | − Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | |
| 108 | − let meta = self.valid.symlink_metadata()?; | |
| 109 | − if !meta.is_dir() { | |
| 110 | − if self.unlink_on_collision { | |
| 111 | − if meta.is_symlink() { | |
| 112 | − os::remove_symlink(&self.valid)?; | |
| 113 | − } else { | |
| 114 | − std::fs::remove_file(&self.valid)?; | |
| 115 | − } | |
| 116 | − #[cfg(debug_assertions)] | |
| 117 | − { | |
| 118 | − self.test_mkdir_calls += 1; | |
| 119 | − } | |
| 120 | − std::fs::create_dir(&self.valid)?; | |
| 121 | − } else { | |
| 122 | − return Err(err); | |
| 123 | − } | |
| 124 | − } | |
| 125 | − } | |
| 126 | − Err(err) => return Err(err), | |
| 127 | − } | |
| 128 | − } | |
| 129 | − Ok(()) | |
| 130 | − })(); | |
| 131 | − | |
| 132 | − if let Err(err) = res { | |
| 133 | − self.valid.pop(); | |
| 134 | − self.valid_relative.pop(); | |
| 135 | − self.valid_components -= 1; | |
| 136 | − return Err(err); | |
| 137 | − } | |
| 138 | − } | |
| 139 | − | |
| 140 | − Ok(&self.valid) | |
| 141 | − } | |
| 142 | − } | |
| 143 | −} | |
| 2 | +use git_attributes::Attributes; | |
| 144 | 3 | |
| 145 | 4 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| 146 | 5 | pub struct Collision { |
| @@ -166,7 +25,7 @@ pub struct Outcome { · 19 unmodified lines | ||
| 166 | 25 | pub errors: Vec<ErrorRecord>, |
| 167 | 26 | } |
| 168 | 27 | |
| 169 | −#[derive(Clone, Copy)] | |
| 28 | +#[derive(Clone)] | |
| 170 | 29 | pub struct Options { |
| 171 | 30 | /// capabilities of the file system |
| 172 | 31 | pub fs: crate::fs::Capabilities, |
| @@ -198,6 +57,8 @@ pub struct Options { · 25 unmodified lines | ||
| 198 | 57 | /// |
| 199 | 58 | /// Default true. |
| 200 | 59 | pub check_stat: bool, |
| 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>, | |
| 201 | 62 | } |
| 202 | 63 | |
| 203 | 64 | impl Default for Options { |
| @@ -210,6 +71,7 @@ impl Default for Options { · 6 unmodified lines | ||
| 210 | 71 | trust_ctime: true, |
| 211 | 72 | check_stat: true, |
| 212 | 73 | overwrite_existing: false, |
| 74 | + attribute_globals: Default::default(), | |
| 213 | 75 | } |
| 214 | 76 | } |
| 215 | 77 | } |
| @@ -5,14 +5,19 @@ use git_hash::oid; · 4 unmodified lines | ||
| 5 | 5 | use git_index::Entry; |
| 6 | 6 | use io_close::Close; |
| 7 | 7 | |
| 8 | −use crate::{index, index::checkout::PathCache, os}; | |
| 8 | +use crate::{fs, index, os}; | |
| 9 | + | |
| 10 | +pub struct Context<'a, 'paths, Find> { | |
| 11 | + pub find: &'a mut Find, | |
| 12 | + pub path_cache: &'a mut fs::Cache<'paths>, | |
| 13 | + pub buf: &'a mut Vec<u8>, | |
| 14 | +} | |
| 9 | 15 | |
| 10 | 16 | #[cfg_attr(not(unix), allow(unused_variables))] |
| 11 | 17 | pub fn checkout<Find, E>( |
| 12 | 18 | entry: &mut Entry, |
| 13 | 19 | entry_path: &BStr, |
| 14 | − find: &mut Find, | |
| 15 | − cache: &mut PathCache, | |
| 20 | + Context { find, path_cache, buf }: Context<'_, '_, Find>, | |
| 16 | 21 | index::checkout::Options { |
| 17 | 22 | fs: crate::fs::Capabilities { |
| 18 | 23 | symlink, |
| @@ -23,17 +28,16 @@ pub fn checkout<Find, E>( · 4 unmodified lines | ||
| 23 | 28 | overwrite_existing, |
| 24 | 29 | .. |
| 25 | 30 | }: index::checkout::Options, |
| 26 | − buf: &mut Vec<u8>, | |
| 27 | 31 | ) -> Result<usize, index::checkout::Error<E>> |
| 28 | 32 | where |
| 29 | 33 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>, |
| 30 | 34 | E: std::error::Error + Send + Sync + 'static, |
| 31 | 35 | { |
| 32 | − let dest_relative = | |
| 33 | − git_features::path::from_byte_slice(entry_path).map_err(|_| index::checkout::Error::IllformedUtf8 { | |
| 34 | − path: entry_path.to_owned(), | |
| 35 | − })?; | |
| 36 | − let dest = cache.append_relative_path_assure_leading_dir(dest_relative, entry.mode)?; | |
| 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(); | |
| 37 | 41 | |
| 38 | 42 | let object_size = match entry.mode { |
| 39 | 43 | git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => { |
| @@ -77,7 +81,7 @@ where · 37 unmodified lines | ||
| 77 | 81 | oid: entry.id, |
| 78 | 82 | path: dest.to_path_buf(), |
| 79 | 83 | })?; |
| 80 | − let symlink_destination = git_features::path::from_byte_slice(obj.data) | |
| 84 | + let symlink_destination = git_path::try_from_byte_slice(obj.data) | |
| 81 | 85 | .map_err(|_| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?; |
| 82 | 86 | |
| 83 | 87 | if symlink { |
| @@ -127,7 +131,7 @@ fn try_write_or_unlink<T>( · 43 unmodified lines | ||
| 127 | 131 | fn try_unlink_path_recursively(path: &Path, path_meta: &std::fs::Metadata) -> std::io::Result<()> { |
| 128 | 132 | if path_meta.is_dir() { |
| 129 | 133 | std::fs::remove_dir_all(path) |
| 130 | − } else if path_meta.is_symlink() { | |
| 134 | + } else if path_meta.file_type().is_symlink() { | |
| 131 | 135 | os::remove_symlink(path) |
| 132 | 136 | } else { |
| 133 | 137 | std::fs::remove_file(path) |
| @@ -142,7 +146,7 @@ fn debug_assert_dest_is_no_symlink(_path: &Path) {} · 8 unmodified lines | ||
| 142 | 146 | fn debug_assert_dest_is_no_symlink(path: &Path) { |
| 143 | 147 | if let Ok(meta) = path.metadata() { |
| 144 | 148 | debug_assert!( |
| 145 | − !meta.is_symlink(), | |
| 149 | + !meta.file_type().is_symlink(), | |
| 146 | 150 | "BUG: should not ever allow to overwrite/write-into the target of a symbolic link: {}", |
| 147 | 151 | path.display() |
| 148 | 152 | ); |
| @@ -3,12 +3,15 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; · 2 unmodified lines | ||
| 3 | 3 | use git_features::{interrupt, parallel::in_parallel, progress, progress::Progress}; |
| 4 | 4 | use git_hash::oid; |
| 5 | 5 | |
| 6 | −use crate::index::checkout::PathCache; | |
| 6 | +use crate::fs; | |
| 7 | 7 | |
| 8 | 8 | pub mod checkout; |
| 9 | 9 | pub(crate) mod entry; |
| 10 | 10 | |
| 11 | 11 | /// Note that interruption still produce an `Ok(…)` value, so the caller should look at `should_interrupt` to communicate the outcome. |
| 12 | +/// `dir` is the directory into which to checkout the `index`. | |
| 13 | +/// `git_dir` is the `.git` directory for reading additional per-repository configuration files. | |
| 14 | +#[allow(clippy::too_many_arguments)] | |
| 12 | 15 | pub fn checkout<Find, E>( |
| 13 | 16 | index: &mut git_index::State, |
| 14 | 17 | dir: impl Into<std::path::PathBuf>, |
| @@ -18,23 +21,36 @@ pub fn checkout<Find, E>( · 3 unmodified lines | ||
| 18 | 21 | should_interrupt: &AtomicBool, |
| 19 | 22 | options: checkout::Options, |
| 20 | 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>> | |
| 21 | 44 | where |
| 22 | 45 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E> + Send + Clone, |
| 23 | 46 | E: std::error::Error + Send + Sync + 'static, |
| 24 | 47 | { |
| 25 | 48 | let num_files = AtomicUsize::default(); |
| 26 | 49 | let dir = dir.into(); |
| 27 | − | |
| 28 | − let mut ctx = chunk::Context { | |
| 29 | − buf: Vec::new(), | |
| 30 | − path_cache: { | |
| 31 | − let mut cache = PathCache::new(dir.clone()); | |
| 32 | − cache.unlink_on_collision = options.overwrite_existing; | |
| 33 | − cache | |
| 34 | − }, | |
| 35 | − find: find.clone(), | |
| 36 | − options, | |
| 37 | − 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 | |
| 38 | 54 | }; |
| 39 | 55 | let (chunk_size, thread_limit, num_threads) = git_features::parallel::optimize_chunk_size_and_thread_limit( |
| 40 | 56 | 100, |
| @@ -43,15 +59,26 @@ where · 2 unmodified lines | ||
| 43 | 59 | None, |
| 44 | 60 | ); |
| 45 | 61 | |
| 46 | − 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 | + | |
| 47 | 72 | let chunk::Outcome { |
| 48 | 73 | mut collisions, |
| 49 | 74 | mut errors, |
| 50 | 75 | mut bytes_written, |
| 51 | 76 | delayed, |
| 52 | 77 | } = if num_threads == 1 { |
| 78 | + let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt); | |
| 53 | 79 | chunk::process(entries_with_paths, files, bytes, &mut ctx)? |
| 54 | 80 | } else { |
| 81 | + let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt); | |
| 55 | 82 | in_parallel( |
| 56 | 83 | git_features::iter::Chunks { |
| 57 | 84 | inner: entries_with_paths, |
| @@ -59,24 +86,8 @@ where · 1 unmodified line | ||
| 59 | 86 | }, |
| 60 | 87 | thread_limit, |
| 61 | 88 | { |
| 62 | − let num_files = &num_files; | |
| 63 | − move |_| { | |
| 64 | − ( | |
| 65 | − progress::Discard, | |
| 66 | − progress::Discard, | |
| 67 | − chunk::Context { | |
| 68 | − find: find.clone(), | |
| 69 | − path_cache: { | |
| 70 | − let mut cache = PathCache::new(dir.clone()); | |
| 71 | − cache.unlink_on_collision = options.overwrite_existing; | |
| 72 | − cache | |
| 73 | − }, | |
| 74 | − buf: Vec::new(), | |
| 75 | − options, | |
| 76 | − num_files, | |
| 77 | − }, | |
| 78 | − ) | |
| 79 | − } | |
| 89 | + let ctx = ctx.clone(); | |
| 90 | + move |_| (progress::Discard, progress::Discard, ctx.clone()) | |
| 80 | 91 | }, |
| 81 | 92 | |chunk, (files, bytes, ctx)| chunk::process(chunk.into_iter(), files, bytes, ctx), |
| 82 | 93 | chunk::Reduce { |
| @@ -102,7 +113,7 @@ where · 19 unmodified lines | ||
| 102 | 113 | } |
| 103 | 114 | |
| 104 | 115 | Ok(checkout::Outcome { |
| 105 | − files_updated: ctx.num_files.load(Ordering::Relaxed), | |
| 116 | + files_updated: num_files.load(Ordering::Relaxed), | |
| 106 | 117 | collisions, |
| 107 | 118 | errors, |
| 108 | 119 | bytes_written, |
| @@ -117,8 +128,8 @@ mod chunk { · 8 unmodified lines | ||
| 117 | 128 | use git_hash::oid; |
| 118 | 129 | |
| 119 | 130 | use crate::{ |
| 120 | − index, | |
| 121 | − index::{checkout, checkout::PathCache, entry}, | |
| 131 | + fs, index, | |
| 132 | + index::{checkout, entry}, | |
| 122 | 133 | os, |
| 123 | 134 | }; |
| 124 | 135 | |
| @@ -185,9 +196,10 @@ mod chunk { · 60 unmodified lines | ||
| 185 | 196 | pub bytes_written: u64, |
| 186 | 197 | } |
| 187 | 198 | |
| 188 | − pub struct Context<'a, Find> { | |
| 199 | + #[derive(Clone)] | |
| 200 | + pub struct Context<'a, 'paths, Find: Clone> { | |
| 189 | 201 | pub find: Find, |
| 190 | − pub path_cache: PathCache, | |
| 202 | + pub path_cache: fs::Cache<'paths>, | |
| 191 | 203 | pub buf: Vec<u8>, |
| 192 | 204 | pub options: checkout::Options, |
| 193 | 205 | /// We keep these shared so that there is the chance for printing numbers that aren't looking like |
| @@ -199,10 +211,10 @@ mod chunk { · 5 unmodified lines | ||
| 199 | 211 | entries_with_paths: impl Iterator<Item = (&'entry mut git_index::Entry, &'entry BStr)>, |
| 200 | 212 | files: &mut impl Progress, |
| 201 | 213 | bytes: &mut impl Progress, |
| 202 | − ctx: &mut Context<'_, Find>, | |
| 214 | + ctx: &mut Context<'_, '_, Find>, | |
| 203 | 215 | ) -> Result<Outcome<'entry>, checkout::Error<E>> |
| 204 | 216 | where |
| 205 | − 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, | |
| 206 | 218 | E: std::error::Error + Send + Sync + 'static, |
| 207 | 219 | { |
| 208 | 220 | let mut delayed = Vec::new(); |
| @@ -254,13 +266,18 @@ mod chunk { · 45 unmodified lines | ||
| 254 | 266 | buf, |
| 255 | 267 | options, |
| 256 | 268 | num_files, |
| 257 | − }: &mut Context<'_, Find>, | |
| 269 | + }: &mut Context<'_, '_, Find>, | |
| 258 | 270 | ) -> Result<usize, checkout::Error<E>> |
| 259 | 271 | where |
| 260 | − 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, | |
| 261 | 273 | E: std::error::Error + Send + Sync + 'static, |
| 262 | 274 | { |
| 263 | − let res = entry::checkout(entry, entry_path, find, path_cache, *options, buf); | |
| 275 | + let res = entry::checkout( | |
| 276 | + entry, | |
| 277 | + entry_path, | |
| 278 | + entry::Context { find, path_cache, buf }, | |
| 279 | + options.clone(), | |
| 280 | + ); | |
| 264 | 281 | files.inc(); |
| 265 | 282 | num_files.fetch_add(1, Ordering::SeqCst); |
| 266 | 283 | match res { |
| @@ -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 | +) | |
| @@ -0,0 +1,18 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init -q | |
| 5 | + | |
| 6 | +cat <<EOF >.git/info/exclude | |
| 7 | +# a sample .git/info/exclude | |
| 8 | +file-anywhere | |
| 9 | +/file-from-top | |
| 10 | + | |
| 11 | +dir-anywhere/ | |
| 12 | +/dir-from-top | |
| 13 | + | |
| 14 | +subdir-anywhere/file | |
| 15 | +subdir-anywhere/dir/ | |
| 16 | +EOF | |
| 17 | + | |
| 18 | +git commit --allow-empty -m "init" | |
| @@ -1,2 +1,4 @@ | ||
| 1 | +extern crate core; | |
| 2 | + | |
| 1 | 3 | mod worktree; |
| 2 | 4 | use worktree::*; |
| @@ -0,0 +1,228 @@ | ||
| 1 | +mod create_directory { | |
| 2 | + use std::path::Path; | |
| 3 | + | |
| 4 | + use git_worktree::fs; | |
| 5 | + use tempfile::{tempdir, TempDir}; | |
| 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 | + | |
| 14 | + #[test] | |
| 15 | + fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate::Result { | |
| 16 | + let dir = tempdir()?; | |
| 17 | + let mut cache = fs::Cache::new( | |
| 18 | + dir.path().join("non-existing-root"), | |
| 19 | + fs::cache::State::for_checkout(false, Default::default()), | |
| 20 | + Default::default(), | |
| 21 | + Vec::new(), | |
| 22 | + Default::default(), | |
| 23 | + ); | |
| 24 | + assert_eq!(cache.num_mkdir_calls(), 0); | |
| 25 | + | |
| 26 | + let path = cache.at_path("hello", Some(false), panic_on_find)?.path(); | |
| 27 | + assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); | |
| 28 | + assert_eq!(cache.num_mkdir_calls(), 0); | |
| 29 | + Ok(()) | |
| 30 | + } | |
| 31 | + | |
| 32 | + #[test] | |
| 33 | + fn directory_paths_are_created_in_full() { | |
| 34 | + let (mut cache, _tmp) = new_cache(); | |
| 35 | + | |
| 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), | |
| 42 | + ] { | |
| 43 | + let path = cache | |
| 44 | + .at_path(Path::new("dir").join(name), *is_dir, panic_on_find) | |
| 45 | + .unwrap() | |
| 46 | + .path(); | |
| 47 | + assert!(path.parent().unwrap().is_dir(), "dir exists"); | |
| 48 | + } | |
| 49 | + | |
| 50 | + assert_eq!(cache.num_mkdir_calls(), 3); | |
| 51 | + } | |
| 52 | + | |
| 53 | + #[test] | |
| 54 | + fn existing_directories_are_fine() -> crate::Result { | |
| 55 | + let (mut cache, tmp) = new_cache(); | |
| 56 | + std::fs::create_dir(tmp.path().join("dir"))?; | |
| 57 | + | |
| 58 | + let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path(); | |
| 59 | + assert!(path.parent().unwrap().is_dir(), "directory is still present"); | |
| 60 | + assert!(!path.exists(), "it won't create the file"); | |
| 61 | + assert_eq!(cache.num_mkdir_calls(), 1); | |
| 62 | + Ok(()) | |
| 63 | + } | |
| 64 | + | |
| 65 | + #[test] | |
| 66 | + fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::Result { | |
| 67 | + let (mut cache, tmp) = new_cache(); | |
| 68 | + let forbidden = tmp.path().join("forbidden"); | |
| 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"), &[])?; | |
| 72 | + | |
| 73 | + for dirname in &["file-in-dir", "link-to-dir"] { | |
| 74 | + cache.unlink_on_collision(false); | |
| 75 | + let relative_path = format!("{}/file", dirname); | |
| 76 | + assert_eq!( | |
| 77 | + cache | |
| 78 | + .at_path(&relative_path, Some(false), panic_on_find) | |
| 79 | + .unwrap_err() | |
| 80 | + .kind(), | |
| 81 | + std::io::ErrorKind::AlreadyExists | |
| 82 | + ); | |
| 83 | + } | |
| 84 | + assert_eq!( | |
| 85 | + cache.num_mkdir_calls(), | |
| 86 | + 2, | |
| 87 | + "it tries to create each directory once, but it's a file" | |
| 88 | + ); | |
| 89 | + cache.reset_mkdir_calls(); | |
| 90 | + for dirname in &["link-to-dir", "file-in-dir"] { | |
| 91 | + cache.unlink_on_collision(true); | |
| 92 | + let relative_path = format!("{}/file", dirname); | |
| 93 | + let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path(); | |
| 94 | + assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); | |
| 95 | + assert!(!path.exists()); | |
| 96 | + } | |
| 97 | + assert_eq!( | |
| 98 | + cache.num_mkdir_calls(), | |
| 99 | + 4, | |
| 100 | + "like before, but it unlinks what's there and tries again" | |
| 101 | + ); | |
| 102 | + Ok(()) | |
| 103 | + } | |
| 104 | + | |
| 105 | + fn new_cache() -> (fs::Cache<'static>, TempDir) { | |
| 106 | + let dir = tempdir().unwrap(); | |
| 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 | + ); | |
| 114 | + (cache, dir) | |
| 115 | + } | |
| 116 | +} | |
| 117 | + | |
| 118 | +#[allow(unused)] | |
| 119 | +mod ignore_and_attributes { | |
| 120 | + use std::path::Path; | |
| 121 | + | |
| 122 | + use bstr::{BStr, ByteSlice}; | |
| 123 | + use git_glob::pattern::Case; | |
| 124 | + use git_index::entry::Mode; | |
| 125 | + use git_odb::{pack::bundle::write::Options, FindExt}; | |
| 126 | + use git_testtools::hex_to_id; | |
| 127 | + use git_worktree::fs; | |
| 128 | + use tempfile::{tempdir, TempDir}; | |
| 129 | + | |
| 130 | + struct IgnoreExpectations<'a> { | |
| 131 | + lines: bstr::Lines<'a>, | |
| 132 | + } | |
| 133 | + | |
| 134 | + impl<'a> Iterator for IgnoreExpectations<'a> { | |
| 135 | + type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); | |
| 136 | + | |
| 137 | + fn next(&mut self) -> Option<Self::Item> { | |
| 138 | + let line = self.lines.next()?; | |
| 139 | + let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); | |
| 140 | + let value = value[1..].as_bstr(); | |
| 141 | + | |
| 142 | + let source_and_line = if left == b"::" { | |
| 143 | + None | |
| 144 | + } else { | |
| 145 | + let mut tokens = left.split(|b| *b == b':'); | |
| 146 | + let source = tokens.next().unwrap().as_bstr(); | |
| 147 | + let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); | |
| 148 | + let pattern = tokens.next().unwrap().as_bstr(); | |
| 149 | + Some((source, line_number, pattern)) | |
| 150 | + }; | |
| 151 | + Some((value, source_and_line)) | |
| 152 | + } | |
| 153 | + } | |
| 154 | + | |
| 155 | + #[test] | |
| 156 | + fn check_against_baseline() -> crate::Result { | |
| 157 | + let dir = git_testtools::scripted_fixture_repo_read_only("make_ignore_and_attributes_setup.sh")?; | |
| 158 | + let worktree_dir = dir.join("repo"); | |
| 159 | + let git_dir = worktree_dir.join(".git"); | |
| 160 | + let mut buf = Vec::new(); | |
| 161 | + let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | |
| 162 | + let user_exclude_path = dir.join("user.exclude"); | |
| 163 | + assert!(user_exclude_path.is_file()); | |
| 164 | + | |
| 165 | + let mut index = git_index::File::at(git_dir.join("index"), Default::default())?; | |
| 166 | + let odb = git_odb::at(git_dir.join("objects"))?; | |
| 167 | + let case = git_glob::pattern::Case::Sensitive; | |
| 168 | + let state = git_worktree::fs::cache::State::for_add( | |
| 169 | + Default::default(), // TODO: attribute tests | |
| 170 | + git_worktree::fs::cache::state::Ignore::new( | |
| 171 | + git_attributes::MatchGroup::from_overrides(vec!["!force-include"]), | |
| 172 | + git_attributes::MatchGroup::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, | |
| 173 | + None, | |
| 174 | + case, | |
| 175 | + ), | |
| 176 | + ); | |
| 177 | + let paths_storage = index.take_path_backing(); | |
| 178 | + let attribute_files_in_index = state.build_attribute_list(&index.state, &paths_storage, case); | |
| 179 | + assert_eq!( | |
| 180 | + attribute_files_in_index, | |
| 181 | + vec![( | |
| 182 | + "other-dir-with-ignore/.gitignore".as_bytes().as_bstr(), | |
| 183 | + hex_to_id("5c7e0ed672d3d31d83a3df61f13cc8f7b22d5bfd") | |
| 184 | + )] | |
| 185 | + ); | |
| 186 | + let mut cache = fs::Cache::new(&worktree_dir, state, case, buf, attribute_files_in_index); | |
| 187 | + | |
| 188 | + for (relative_entry, source_and_line) in (IgnoreExpectations { | |
| 189 | + lines: baseline.lines(), | |
| 190 | + }) { | |
| 191 | + let relative_path = git_path::from_byte_slice(relative_entry); | |
| 192 | + let is_dir = worktree_dir.join(&relative_path).metadata().ok().map(|m| m.is_dir()); | |
| 193 | + | |
| 194 | + let platform = cache.at_entry(relative_entry, is_dir, |oid, buf| odb.find_blob(oid, buf))?; | |
| 195 | + | |
| 196 | + let match_ = platform.matching_exclude_pattern(); | |
| 197 | + let is_excluded = platform.is_excluded(); | |
| 198 | + match (match_, source_and_line) { | |
| 199 | + (None, None) => { | |
| 200 | + assert!(!is_excluded); | |
| 201 | + } | |
| 202 | + (Some(m), Some((source_file, line, pattern))) => { | |
| 203 | + assert_eq!(m.pattern.to_string(), pattern); | |
| 204 | + assert_eq!(m.sequence_number, line); | |
| 205 | + // Paths read from the index are relative to the repo, and they don't exist locally due tot skip-worktree | |
| 206 | + if m.source.map_or(false, |p| p.exists()) { | |
| 207 | + assert_eq!( | |
| 208 | + m.source.map(|p| p.canonicalize().unwrap()), | |
| 209 | + Some(worktree_dir.join(source_file.to_str_lossy().as_ref()).canonicalize()?) | |
| 210 | + ); | |
| 211 | + } | |
| 212 | + } | |
| 213 | + (actual, expected) => { | |
| 214 | + panic!( | |
| 215 | + "actual {:?} didn't match {:?} at '{}'", | |
| 216 | + actual, expected, relative_entry | |
| 217 | + ); | |
| 218 | + } | |
| 219 | + } | |
| 220 | + } | |
| 221 | + | |
| 222 | + cache.set_case(Case::Fold); | |
| 223 | + let platform = cache.at_entry("User-file-ANYWHERE", Some(false), |oid, buf| odb.find_blob(oid, buf))?; | |
| 224 | + let m = platform.matching_exclude_pattern().expect("match"); | |
| 225 | + assert_eq!(m.pattern.text, "user-file-anywhere"); | |
| 226 | + Ok(()) | |
| 227 | + } | |
| 228 | +} | |
| @@ -17,3 +17,6 @@ fn from_probing_cwd() { · 16 unmodified lines | ||
| 17 | 17 | entries |
| 18 | 18 | ); |
| 19 | 19 | } |
| 20 | + | |
| 21 | +mod cache; | |
| 22 | +mod stack; | |
| @@ -0,0 +1,145 @@ | ||
| 1 | +use std::path::{Path, PathBuf}; | |
| 2 | + | |
| 3 | +use git_worktree::fs::Stack; | |
| 4 | + | |
| 5 | +#[derive(Debug, Default, Eq, PartialEq)] | |
| 6 | +struct Record { | |
| 7 | + push_dir: usize, | |
| 8 | + dirs: Vec<PathBuf>, | |
| 9 | + push: usize, | |
| 10 | +} | |
| 11 | + | |
| 12 | +impl git_worktree::fs::stack::Delegate for Record { | |
| 13 | + fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { | |
| 14 | + self.push_dir += 1; | |
| 15 | + self.dirs.push(stack.current().into()); | |
| 16 | + Ok(()) | |
| 17 | + } | |
| 18 | + | |
| 19 | + fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> { | |
| 20 | + self.push += 1; | |
| 21 | + Ok(()) | |
| 22 | + } | |
| 23 | + | |
| 24 | + fn pop_directory(&mut self) { | |
| 25 | + self.dirs.pop(); | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +#[test] | |
| 30 | +fn delegate_calls_are_consistent() -> crate::Result { | |
| 31 | + let root = PathBuf::from("."); | |
| 32 | + let mut s = Stack::new(&root); | |
| 33 | + | |
| 34 | + assert_eq!(s.current(), root); | |
| 35 | + assert_eq!(s.current_relative(), Path::new("")); | |
| 36 | + | |
| 37 | + let mut r = Record::default(); | |
| 38 | + s.make_relative_path_current("a/b", &mut r)?; | |
| 39 | + let mut dirs = vec![root.clone(), root.join("a")]; | |
| 40 | + assert_eq!( | |
| 41 | + r, | |
| 42 | + Record { | |
| 43 | + push_dir: 2, | |
| 44 | + dirs: dirs.clone(), | |
| 45 | + push: 2, | |
| 46 | + } | |
| 47 | + ); | |
| 48 | + | |
| 49 | + s.make_relative_path_current("a/b2", &mut r)?; | |
| 50 | + assert_eq!( | |
| 51 | + r, | |
| 52 | + Record { | |
| 53 | + push_dir: 2, | |
| 54 | + dirs: dirs.clone(), | |
| 55 | + push: 3, | |
| 56 | + } | |
| 57 | + ); | |
| 58 | + | |
| 59 | + s.make_relative_path_current("c/d/e", &mut r)?; | |
| 60 | + dirs.pop(); | |
| 61 | + dirs.extend([root.join("c"), root.join("c").join("d")]); | |
| 62 | + assert_eq!( | |
| 63 | + r, | |
| 64 | + Record { | |
| 65 | + push_dir: 4, | |
| 66 | + dirs: dirs.clone(), | |
| 67 | + push: 6, | |
| 68 | + } | |
| 69 | + ); | |
| 70 | + | |
| 71 | + dirs.push(root.join("c").join("d").join("x")); | |
| 72 | + s.make_relative_path_current("c/d/x/z", &mut r)?; | |
| 73 | + assert_eq!( | |
| 74 | + r, | |
| 75 | + Record { | |
| 76 | + push_dir: 5, | |
| 77 | + dirs: dirs.clone(), | |
| 78 | + push: 8, | |
| 79 | + } | |
| 80 | + ); | |
| 81 | + | |
| 82 | + dirs.drain(dirs.len() - 3..).count(); | |
| 83 | + s.make_relative_path_current("f", &mut r)?; | |
| 84 | + assert_eq!(s.current_relative(), Path::new("f")); | |
| 85 | + assert_eq!( | |
| 86 | + r, | |
| 87 | + Record { | |
| 88 | + push_dir: 5, | |
| 89 | + dirs: dirs.clone(), | |
| 90 | + push: 9, | |
| 91 | + } | |
| 92 | + ); | |
| 93 | + | |
| 94 | + dirs.push(root.join("x")); | |
| 95 | + s.make_relative_path_current("x/z", &mut r)?; | |
| 96 | + assert_eq!( | |
| 97 | + r, | |
| 98 | + Record { | |
| 99 | + push_dir: 6, | |
| 100 | + dirs: dirs.clone(), | |
| 101 | + push: 11, | |
| 102 | + } | |
| 103 | + ); | |
| 104 | + | |
| 105 | + dirs.push(root.join("x").join("z")); | |
| 106 | + s.make_relative_path_current("x/z/a", &mut r)?; | |
| 107 | + assert_eq!( | |
| 108 | + r, | |
| 109 | + Record { | |
| 110 | + push_dir: 7, | |
| 111 | + dirs: dirs.clone(), | |
| 112 | + push: 12, | |
| 113 | + } | |
| 114 | + ); | |
| 115 | + | |
| 116 | + dirs.push(root.join("x").join("z").join("a")); | |
| 117 | + dirs.push(root.join("x").join("z").join("a").join("b")); | |
| 118 | + s.make_relative_path_current("x/z/a/b/c", &mut r)?; | |
| 119 | + assert_eq!( | |
| 120 | + r, | |
| 121 | + Record { | |
| 122 | + push_dir: 9, | |
| 123 | + dirs: dirs.clone(), | |
| 124 | + push: 14, | |
| 125 | + } | |
| 126 | + ); | |
| 127 | + | |
| 128 | + dirs.drain(dirs.len() - 2..).count(); | |
| 129 | + s.make_relative_path_current("x/z", &mut r)?; | |
| 130 | + assert_eq!( | |
| 131 | + r, | |
| 132 | + Record { | |
| 133 | + push_dir: 9, | |
| 134 | + dirs: dirs.clone(), | |
| 135 | + push: 14, | |
| 136 | + } | |
| 137 | + ); | |
| 138 | + assert_eq!( | |
| 139 | + dirs.last(), | |
| 140 | + Some(&PathBuf::from("./x/z")), | |
| 141 | + "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." | |
| 142 | + ); | |
| 143 | + | |
| 144 | + Ok(()) | |
| 145 | +} | |
| @@ -1,102 +1,3 @@ | ||
| 1 | −mod cache { | |
| 2 | − use std::path::Path; | |
| 3 | − | |
| 4 | − use git_index::entry::Mode; | |
| 5 | − use git_worktree::index::checkout::PathCache; | |
| 6 | − use tempfile::{tempdir, TempDir}; | |
| 7 | − | |
| 8 | − #[test] | |
| 9 | − fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() { | |
| 10 | − let dir = tempdir().unwrap(); | |
| 11 | − let mut cache = PathCache::new(dir.path().join("non-existing-root")); | |
| 12 | − assert_eq!(cache.test_mkdir_calls, 0); | |
| 13 | − | |
| 14 | − let path = cache | |
| 15 | − .append_relative_path_assure_leading_dir("hello", Mode::FILE) | |
| 16 | − .unwrap(); | |
| 17 | − assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); | |
| 18 | − assert_eq!(cache.test_mkdir_calls, 0); | |
| 19 | − } | |
| 20 | − | |
| 21 | − #[test] | |
| 22 | − fn directory_paths_are_created_in_full() { | |
| 23 | − let (mut cache, _tmp) = new_cache(); | |
| 24 | − | |
| 25 | − for (name, mode) in &[ | |
| 26 | − ("dir", Mode::DIR), | |
| 27 | − ("submodule", Mode::COMMIT), | |
| 28 | − ("file", Mode::FILE), | |
| 29 | − ("exe", Mode::FILE_EXECUTABLE), | |
| 30 | − ("link", Mode::SYMLINK), | |
| 31 | − ] { | |
| 32 | − let path = cache | |
| 33 | − .append_relative_path_assure_leading_dir(Path::new("dir").join(name), *mode) | |
| 34 | − .unwrap(); | |
| 35 | − assert!(path.parent().unwrap().is_dir(), "dir exists"); | |
| 36 | − } | |
| 37 | − | |
| 38 | − assert_eq!(cache.test_mkdir_calls, 3); | |
| 39 | − } | |
| 40 | − | |
| 41 | − #[test] | |
| 42 | − fn existing_directories_are_fine() { | |
| 43 | − let (mut cache, tmp) = new_cache(); | |
| 44 | − std::fs::create_dir(tmp.path().join("dir")).unwrap(); | |
| 45 | − | |
| 46 | − let path = cache | |
| 47 | − .append_relative_path_assure_leading_dir("dir/file", Mode::FILE) | |
| 48 | − .unwrap(); | |
| 49 | − assert!(path.parent().unwrap().is_dir(), "directory is still present"); | |
| 50 | − assert!(!path.exists(), "it won't create the file"); | |
| 51 | − assert_eq!(cache.test_mkdir_calls, 1); | |
| 52 | − } | |
| 53 | − | |
| 54 | − #[test] | |
| 55 | − fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() { | |
| 56 | − let (mut cache, tmp) = new_cache(); | |
| 57 | − 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(); | |
| 61 | − | |
| 62 | − for dirname in &["file-in-dir", "link-to-dir"] { | |
| 63 | − cache.unlink_on_collision = false; | |
| 64 | − let relative_path = format!("{}/file", dirname); | |
| 65 | − assert_eq!( | |
| 66 | − cache | |
| 67 | − .append_relative_path_assure_leading_dir(&relative_path, Mode::FILE) | |
| 68 | − .unwrap_err() | |
| 69 | − .kind(), | |
| 70 | − std::io::ErrorKind::AlreadyExists | |
| 71 | − ); | |
| 72 | − } | |
| 73 | − assert_eq!( | |
| 74 | − cache.test_mkdir_calls, 2, | |
| 75 | − "it tries to create each directory once, but it's a file" | |
| 76 | − ); | |
| 77 | − cache.test_mkdir_calls = 0; | |
| 78 | − for dirname in &["link-to-dir", "file-in-dir"] { | |
| 79 | − cache.unlink_on_collision = true; | |
| 80 | − let relative_path = format!("{}/file", dirname); | |
| 81 | − let path = cache | |
| 82 | − .append_relative_path_assure_leading_dir(&relative_path, Mode::FILE) | |
| 83 | − .unwrap(); | |
| 84 | − assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); | |
| 85 | − assert!(!path.exists()); | |
| 86 | − } | |
| 87 | − assert_eq!( | |
| 88 | − cache.test_mkdir_calls, 4, | |
| 89 | − "like before, but it unlinks what's there and tries again" | |
| 90 | − ); | |
| 91 | − } | |
| 92 | − | |
| 93 | − fn new_cache() -> (PathCache, TempDir) { | |
| 94 | − let dir = tempdir().unwrap(); | |
| 95 | − let cache = PathCache::new(dir.path()); | |
| 96 | − (cache, dir) | |
| 97 | − } | |
| 98 | −} | |
| 99 | − | |
| 100 | 1 | #[cfg(unix)] |
| 101 | 2 | use std::os::unix::prelude::MetadataExt; |
| 102 | 3 | use std::{ |
| @@ -120,7 +21,7 @@ fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden( · 17 unmodified lines | ||
| 120 | 21 | // without overwrite mode, everything is safe. |
| 121 | 22 | opts.overwrite_existing = false; |
| 122 | 23 | let (source_tree, destination, _index, outcome) = |
| 123 | − checkout_index_in_tmp_dir(opts, "make_dangerous_symlink").unwrap(); | |
| 24 | + checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap(); | |
| 124 | 25 | |
| 125 | 26 | let source_files = dir_structure(&source_tree); |
| 126 | 27 | let worktree_files = dir_structure(&destination); |
| @@ -161,7 +62,7 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { · 34 unmodified lines | ||
| 161 | 62 | // with overwrite mode |
| 162 | 63 | opts.overwrite_existing = true; |
| 163 | 64 | let (source_tree, destination, _index, outcome) = |
| 164 | − checkout_index_in_tmp_dir(opts, "make_dangerous_symlink").unwrap(); | |
| 65 | + checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap(); | |
| 165 | 66 | |
| 166 | 67 | let source_files = dir_structure(&source_tree); |
| 167 | 68 | let worktree_files = dir_structure(&destination); |
| @@ -190,7 +91,7 @@ fn overwriting_files_and_lone_directories_works() { · 22 unmodified lines | ||
| 190 | 91 | opts.overwrite_existing = true; |
| 191 | 92 | opts.destination_is_initially_empty = false; |
| 192 | 93 | let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( |
| 193 | − opts, | |
| 94 | + opts.clone(), | |
| 194 | 95 | "make_mixed_without_submodules", |
| 195 | 96 | |_| true, |
| 196 | 97 | |d| { |
| @@ -249,7 +150,8 @@ fn overwriting_files_and_lone_directories_works() { · 52 unmodified lines | ||
| 249 | 150 | fn symlinks_become_files_if_disabled() -> crate::Result { |
| 250 | 151 | let mut opts = opts_from_probe(); |
| 251 | 152 | opts.fs.symlink = false; |
| 252 | − let (source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?; | |
| 153 | + let (source_tree, destination, _index, outcome) = | |
| 154 | + checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?; | |
| 253 | 155 | |
| 254 | 156 | assert_equality(&source_tree, &destination, opts.fs.symlink)?; |
| 255 | 157 | assert!(outcome.collisions.is_empty()); |
| @@ -262,7 +164,7 @@ fn allow_or_disallow_symlinks() -> crate::Result { · 6 unmodified lines | ||
| 262 | 164 | for allowed in &[false, true] { |
| 263 | 165 | opts.fs.symlink = *allowed; |
| 264 | 166 | let (source_tree, destination, _index, outcome) = |
| 265 | − checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?; | |
| 167 | + checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?; | |
| 266 | 168 | |
| 267 | 169 | assert_equality(&source_tree, &destination, opts.fs.symlink)?; |
| 268 | 170 | assert!(outcome.collisions.is_empty()); |
| @@ -338,7 +240,7 @@ fn no_case_related_collisions_on_case_sensitive_filesystem() { · 69 unmodified lines | ||
| 338 | 240 | return; |
| 339 | 241 | } |
| 340 | 242 | let (source_tree, destination, index, outcome) = |
| 341 | − checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap(); | |
| 243 | + checkout_index_in_tmp_dir(opts.clone(), "make_ignorecase_collisions").unwrap(); | |
| 342 | 244 | |
| 343 | 245 | assert!(outcome.collisions.is_empty()); |
| 344 | 246 | let num_files = assert_equality(&source_tree, &destination, opts.fs.symlink).unwrap(); |
| @@ -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,70 @@ | ||
| 1 | +use std::io; | |
| 2 | + | |
| 3 | +use anyhow::{bail, Context}; | |
| 4 | +use git_repository as git; | |
| 5 | +use git_repository::prelude::FindExt; | |
| 6 | + | |
| 7 | +use crate::OutputFormat; | |
| 8 | + | |
| 9 | +pub mod query { | |
| 10 | + use std::ffi::OsString; | |
| 11 | + | |
| 12 | + use crate::OutputFormat; | |
| 13 | + | |
| 14 | + pub struct Options { | |
| 15 | + pub format: OutputFormat, | |
| 16 | + pub overrides: Vec<OsString>, | |
| 17 | + pub show_ignore_patterns: bool, | |
| 18 | + } | |
| 19 | +} | |
| 20 | + | |
| 21 | +pub fn query( | |
| 22 | + repo: git::Repository, | |
| 23 | + pathspecs: impl Iterator<Item = git::path::Spec>, | |
| 24 | + mut out: impl io::Write, | |
| 25 | + query::Options { | |
| 26 | + overrides, | |
| 27 | + format, | |
| 28 | + show_ignore_patterns, | |
| 29 | + }: query::Options, | |
| 30 | +) -> anyhow::Result<()> { | |
| 31 | + if format != OutputFormat::Human { | |
| 32 | + bail!("JSON output isn't implemented yet"); | |
| 33 | + } | |
| 34 | + | |
| 35 | + let worktree = repo | |
| 36 | + .worktree() | |
| 37 | + .current() | |
| 38 | + .with_context(|| "Cannot check excludes without a current worktree")?; | |
| 39 | + let index = worktree.open_index()?; | |
| 40 | + let mut cache = worktree.excludes( | |
| 41 | + &index.state, | |
| 42 | + Some(git::attrs::MatchGroup::<git::attrs::Ignore>::from_overrides(overrides)), | |
| 43 | + )?; | |
| 44 | + | |
| 45 | + let prefix = repo.prefix().expect("worktree - we have an index by now")?; | |
| 46 | + | |
| 47 | + for mut spec in pathspecs { | |
| 48 | + for path in spec.apply_prefix(&prefix).items() { | |
| 49 | + // TODO: what about paths that end in /? Pathspec might handle it, it's definitely something git considers | |
| 50 | + // even if the directory doesn't exist. Seems to work as long as these are kept in the spec. | |
| 51 | + let is_dir = git::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); | |
| 52 | + let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; | |
| 53 | + let match_ = entry | |
| 54 | + .matching_exclude_pattern() | |
| 55 | + .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then(|| m)); | |
| 56 | + match match_ { | |
| 57 | + Some(m) => writeln!( | |
| 58 | + out, | |
| 59 | + "{}:{}:{}\t{}", | |
| 60 | + m.source.map(|p| p.to_string_lossy()).unwrap_or_default(), | |
| 61 | + m.sequence_number, | |
| 62 | + m.pattern, | |
| 63 | + path | |
| 64 | + )?, | |
| 65 | + None => writeln!(out, "::\t{}", path)?, | |
| 66 | + } | |
| 67 | + } | |
| 68 | + } | |
| 69 | + Ok(()) | |
| 70 | +} | |
| @@ -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,37 @@ 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 std::ffi::OsString; | |
| 382 | + | |
| 383 | + use git_repository as git; | |
| 384 | + | |
| 385 | + #[derive(Debug, clap::Subcommand)] | |
| 386 | + pub enum Subcommands { | |
| 387 | + /// Check if path-specs are excluded and print the result similar to `git check-ignore`. | |
| 388 | + Query { | |
| 389 | + /// Show actual ignore patterns instead of un-excluding an entry. | |
| 390 | + /// | |
| 391 | + /// That way one can understand why an entry might not be excluded. | |
| 392 | + #[clap(long, short = 'i')] | |
| 393 | + show_ignore_patterns: bool, | |
| 394 | + /// Additional patterns to use for exclusions. They have the highest priority. | |
| 395 | + /// | |
| 396 | + /// Useful for undoing previous patterns using the '!' prefix. | |
| 397 | + #[clap(long, short = 'p')] | |
| 398 | + patterns: Vec<OsString>, | |
| 399 | + /// The git path specifications to check for exclusion, or unset to read from stdin one per line. | |
| 400 | + #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))] | |
| 401 | + pathspecs: Vec<git::path::Spec>, | |
| 402 | + }, | |
| 403 | + } | |
| 373 | 404 | } |
| 374 | 405 | |
| 375 | 406 | 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(); |
| @@ -1,8 +1,8 @@ | ||
| 1 | −use std::io::Read; | |
| 2 | −use std::time::Duration; | |
| 3 | 1 | use std::{ |
| 4 | 2 | collections::BTreeMap, |
| 3 | + io::Read, | |
| 5 | 4 | path::{Path, PathBuf}, |
| 5 | + time::Duration, | |
| 6 | 6 | }; |
| 7 | 7 | |
| 8 | 8 | pub use bstr; |
| @@ -13,7 +13,7 @@ use once_cell::sync::Lazy; · 4 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 | |
| @@ -124,10 +124,14 @@ pub fn scripted_fixture_repo_read_only_with_args( · 104 unmodified lines | ||
| 124 | 124 | |
| 125 | 125 | let _marker = git_lock::Marker::acquire_to_hold_resource( |
| 126 | 126 | script_basename, |
| 127 | − git_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(5)), | |
| 127 | + git_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(if cfg!(windows) { 3 * 60 } else { 30 })), | |
| 128 | 128 | None, |
| 129 | 129 | )?; |
| 130 | − if !script_result_directory.is_dir() { | |
| 130 | + let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_"); | |
| 131 | + if !script_result_directory.is_dir() || failure_marker.is_file() { | |
| 132 | + if failure_marker.is_file() { | |
| 133 | + std::fs::remove_dir_all(&script_result_directory)?; | |
| 134 | + } | |
| 131 | 135 | std::fs::create_dir_all(&script_result_directory)?; |
| 132 | 136 | match extract_archive(&archive_file_path, &script_result_directory, script_identity) { |
| 133 | 137 | Ok((archive_id, platform)) => { |
| @@ -168,19 +172,31 @@ pub fn scripted_fixture_repo_read_only_with_args( · 34 unmodified lines | ||
| 168 | 172 | .env("GIT_CONFIG_KEY_1", "init.defaultBranch") |
| 169 | 173 | .env("GIT_CONFIG_VALUE_1", "main") |
| 170 | 174 | .output()?; |
| 175 | + if !output.status.success() { | |
| 176 | + write_failure_marker(&failure_marker); | |
| 177 | + } | |
| 171 | 178 | assert!( |
| 172 | 179 | output.status.success(), |
| 173 | 180 | "repo script failed: stdout: {}\nstderr: {}", |
| 174 | 181 | output.stdout.as_bstr(), |
| 175 | 182 | output.stderr.as_bstr() |
| 176 | 183 | ); |
| 177 | − create_archive_if_not_on_ci(&script_result_directory, &archive_file_path, script_identity)?; | |
| 184 | + create_archive_if_not_on_ci(&script_result_directory, &archive_file_path, script_identity).map_err( | |
| 185 | + |err| { | |
| 186 | + write_failure_marker(&failure_marker); | |
| 187 | + err | |
| 188 | + }, | |
| 189 | + )?; | |
| 178 | 190 | } |
| 179 | 191 | } |
| 180 | 192 | } |
| 181 | 193 | Ok(script_result_directory) |
| 182 | 194 | } |
| 183 | 195 | |
| 196 | +fn write_failure_marker(failure_marker: &Path) { | |
| 197 | + std::fs::write(failure_marker, &[]).ok(); | |
| 198 | +} | |
| 199 | + | |
| 184 | 200 | /// The `script_identity` will be baked into the soon to be created `archive` as it identitifies the script |
| 185 | 201 | /// that created the contents of `source_dir`. |
| 186 | 202 | fn create_archive_if_not_on_ci(source_dir: &Path, archive: &Path, script_identity: u32) -> std::io::Result<()> { |
| @@ -208,7 +224,10 @@ fn create_archive_if_not_on_ci(source_dir: &Path, archive: &Path, script_identit · 21 unmodified lines | ||
| 208 | 224 | std::io::copy(&mut &*buf, &mut xz_write)?; |
| 209 | 225 | xz_write.finish()?.close() |
| 210 | 226 | })(); |
| 227 | + #[cfg(not(windows))] | |
| 211 | 228 | std::fs::remove_dir_all(meta_dir)?; |
| 229 | + #[cfg(windows)] | |
| 230 | + std::fs::remove_dir_all(meta_dir).ok(); // it really can't delete these directories for some reason (even after 10 seconds) | |
| 212 | 231 | res |
| 213 | 232 | } |
| 214 | 233 | |
| @@ -237,9 +256,22 @@ fn extract_archive( · 22 unmodified lines | ||
| 237 | 256 | destination_dir: &Path, |
| 238 | 257 | required_script_identity: u32, |
| 239 | 258 | ) -> std::io::Result<(u32, Option<String>)> { |
| 240 | − let mut archive_buf = Vec::<u8>::new(); | |
| 241 | − let mut decoder = xz2::bufread::XzDecoder::new(std::io::BufReader::new(std::fs::File::open(archive)?)); | |
| 242 | − std::io::copy(&mut decoder, &mut archive_buf)?; | |
| 259 | + let archive_buf: Vec<u8> = { | |
| 260 | + let mut buf = Vec::new(); | |
| 261 | + let input_archive = std::fs::File::open(archive)?; | |
| 262 | + if std::env::var_os("GITOXIDE_TEST_IGNORE_ARCHIVES").is_some() { | |
| 263 | + return Err(std::io::Error::new( | |
| 264 | + std::io::ErrorKind::Other, | |
| 265 | + format!( | |
| 266 | + "Ignoring archive at '{}' as GITOXIDE_TEST_IGNORE_ARCHIVES is set.", | |
| 267 | + archive.display() | |
| 268 | + ), | |
| 269 | + )); | |
| 270 | + } | |
| 271 | + let mut decoder = xz2::bufread::XzDecoder::new(std::io::BufReader::new(input_archive)); | |
| 272 | + std::io::copy(&mut decoder, &mut buf)?; | |
| 273 | + buf | |
| 274 | + }; | |
| 243 | 275 | |
| 244 | 276 | let mut entry_buf = Vec::<u8>::new(); |
| 245 | 277 | let (archive_identity, platform): (u32, _) = tar::Archive::new(std::io::Cursor::new(&mut &*archive_buf)) |