History

Merge branch 'fetch-pack'

Sebastian Thiel committed 4 years ago

Commit 3c494002 parents a0dd96b ef9fa98103 files · +3156 −1174Browse files

Unified · Split

103 files
  1. Cargo.lock +41 −0
  2. Cargo.toml +4 −0
  3. DEVELOPMENT.md +15 −0
  4. README.md +3 −0
  5. etc/check-package-size.sh +1 −1
  6. git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs +1 −0
  7. git-pack/src/bundle/write/error.rs +2 −0
  8. git-pack/src/bundle/write/mod.rs +2 −2
  9. git-pack/src/bundle/write/types.rs +2 −2
  10. git-pack/src/index/write/error.rs +1 −1
  11. git-pack/src/index/write/mod.rs +4 −9
  12. git-pack/tests/pack/bundle.rs +2 −2
  13. git-pack/tests/pack/data/output/count_and_entries.rs +15 −0
  14. git-pack/tests/pack/index.rs +1 −1
  15. git-protocol/src/fetch/arguments/blocking_io.rs +2 −1
  16. git-protocol/src/fetch/arguments/mod.rs +11 −1
  17. git-protocol/src/fetch/command.rs +14 −4
  18. git-protocol/src/fetch/handshake.rs +1 −1
  19. git-protocol/src/fetch/refs/function.rs +2 −2
  20. git-protocol/src/fetch/refs/mod.rs +1 −1
  21. git-protocol/src/fetch/response/mod.rs +2 −1
  22. git-protocol/src/fetch/tests/arguments.rs +14 −0
  23. git-protocol/src/remote_progress.rs +1 −1
  24. git-ref/src/fullname.rs +9 −0
  25. git-ref/src/store/file/transaction/prepare.rs +8 −4
  26. git-ref/src/target.rs +1 −1
  27. git-ref/src/transaction/mod.rs +9 −1
  28. git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs +18 −3
  29. git-ref/tests/file/transaction/prepare_and_commit/delete.rs +13 −0
  30. git-ref/tests/file/worktree.rs +11 −5
  31. git-refspec/src/spec.rs +19 −3
  32. git-refspec/tests/refspec.rs +1 −0
  33. git-refspec/tests/spec/mod.rs +41 −0
  34. git-repository/examples/init-repo-and-commit.rs +26 −39
  35. git-repository/src/config/cache/access.rs +103 −0
  36. git-repository/src/config/cache/init.rs +31 −53
  37. git-repository/src/config/cache/mod.rs +1 −22
  38. git-repository/src/config/cache/util.rs +22 −0
  39. git-repository/src/config/mod.rs +21 −7
  40. git-repository/src/config/snapshot/_impls.rs +62 −0
  41. git-repository/src/config/snapshot/access.rs +58 −0
  42. git-repository/src/config/snapshot/apply_cli_overrides.rs +5 −2
  43. git-repository/src/config/snapshot/mod.rs +1 −41
  44. git-repository/src/head/mod.rs +8 −5
  45. git-repository/src/open.rs +1 −1
  46. git-repository/src/reference/edits.rs +5 −20
  47. git-repository/src/reference/errors.rs +2 −0
  48. git-repository/src/reference/remote.rs +2 −0
  49. git-repository/src/remote/connect.rs +21 −9
  50. git-repository/src/remote/connection/access.rs +48 −0
  51. git-repository/src/remote/connection/fetch/config.rs +66 −0
  52. git-repository/src/remote/connection/fetch/mod.rs +263 −0
  53. git-repository/src/remote/connection/fetch/negotiate.rs +60 −0
  54. git-repository/src/remote/connection/fetch/update_refs/mod.rs +149 −0
  55. git-repository/src/remote/connection/fetch/update_refs/tests.rs +263 −0
  56. git-repository/src/remote/connection/fetch/update_refs/update.rs +92 −0
  57. git-repository/src/remote/connection/mod.rs +8 −48
  58. git-repository/src/remote/connection/ref_map.rs +68 −9
  59. git-repository/src/remote/fetch.rs +59 −0
  60. git-repository/src/remote/mod.rs +1 −38
  61. git-repository/src/remote/url/mod.rs +2 −2
  62. git-repository/src/repository/config.rs +9 −2
  63. git-repository/src/repository/identity.rs +9 −2
  64. git-repository/src/repository/object.rs +24 −30
  65. git-repository/src/repository/reference.rs +28 −45
  66. git-repository/src/repository/remote.rs +11 −0
  67. git-repository/src/repository/worktree.rs +1 −11
  68. git-repository/src/worktree/mod.rs +3 −1
  69. git-repository/tests/fixtures/generated-archives/.gitignore +1 −0
  70. git-repository/tests/fixtures/make_fetch_repos.sh +30 −0
  71. git-repository/tests/fixtures/make_remote_repos.sh +4 −0
  72. git-repository/tests/head/mod.rs +11 −1
  73. git-repository/tests/remote/fetch.rs +123 −0
  74. git-repository/tests/remote/mod.rs +2 −1
  75. git-repository/tests/remote/ref_map.rs +2 −3
  76. git-repository/tests/repository/config/config_snapshot/mod.rs +44 −0
  77. git-repository/tests/repository/mod.rs +1 −1
  78. git-repository/tests/repository/object.rs +14 −34
  79. git-repository/tests/repository/remote.rs +19 −0
  80. git-repository/tests/util/mod.rs +22 −2
  81. git-transport/src/client/blocking_io/connect.rs +4 −4
  82. git-transport/src/client/blocking_io/file.rs +6 −0
  83. git-transport/src/client/blocking_io/http/curl/mod.rs +5 −0
  84. git-transport/src/client/blocking_io/http/mod.rs +9 −5
  85. git-transport/src/client/blocking_io/http/traits.rs +8 −0
  86. git-transport/src/client/capabilities.rs +1 −1
  87. git-transport/src/client/git/async_io.rs +5 −0
  88. git-transport/src/client/git/blocking_io.rs +6 −0
  89. git-transport/src/client/traits.rs +14 −0
  90. git-transport/tests/client/blocking_io/http/mock.rs +1 −1
  91. gitoxide-core/src/pack/index.rs +1 −1
  92. gitoxide-core/src/pack/receive.rs +3 −3
  93. gitoxide-core/src/repository/remote.rs +13 −1
  94. src/plumbing/main.rs +4 −2
  95. src/plumbing/mod.rs +4 −0
  96. src/plumbing/options.rs +0 −677
  97. src/plumbing/options/free.rs +368 −0
  98. src/plumbing/options/mod.rs +310 −0
  99. src/plumbing/progress.rs +305 −0
  100. tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success +1 −1
  101. tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success +1 −1
  102. tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json +1 −1
  103. tests/tools/src/lib.rs +8 −0
Cargo.lock+41−0
@@ -297,6 +297,12 @@ dependencies = [ · 296 unmodified lines
297 "utf8-width",297 "utf8-width",
298]298]
299 299
300[[package]]
301name = "bytecount"
302version = "0.6.3"
303source = "registry+https://github.com/rust-lang/crates.io-index"
304checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
305
300[[package]]306[[package]]
301name = "bytes"307name = "bytes"
302version = "1.2.1"308version = "1.2.1"
@@ -954,6 +960,12 @@ dependencies = [ · 651 unmodified lines
954 "miniz_oxide",960 "miniz_oxide",
955]961]
956 962
963[[package]]
964name = "fnv"
965version = "1.0.7"
966source = "registry+https://github.com/rust-lang/crates.io-index"
967checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
968
957[[package]]969[[package]]
958name = "form_urlencoded"970name = "form_urlencoded"
959version = "1.1.0"971version = "1.1.0"
@@ -1835,7 +1847,9 @@ dependencies = [ · 875 unmodified lines
1835 "git-repository",1847 "git-repository",
1836 "git-transport",1848 "git-transport",
1837 "gitoxide-core",1849 "gitoxide-core",
1850 "owo-colors",
1838 "prodash",1851 "prodash",
1852 "tabled",
1839]1853]
1840 1854
1841[[package]]1855[[package]]
@@ -2380,6 +2394,23 @@ dependencies = [ · 538 unmodified lines
2380 "winapi",2394 "winapi",
2381]2395]
2382 2396
2397[[package]]
2398name = "owo-colors"
2399version = "3.5.0"
2400source = "registry+https://github.com/rust-lang/crates.io-index"
2401checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
2402
2403[[package]]
2404name = "papergrid"
2405version = "0.5.1"
2406source = "registry+https://github.com/rust-lang/crates.io-index"
2407checksum = "453cf71f2a37af495a1a124bf30d4d7469cfbea58e9f2479be9d222396a518a2"
2408dependencies = [
2409 "bytecount",
2410 "fnv",
2411 "unicode-width",
2412]
2413
2383[[package]]2414[[package]]
2384name = "parking"2415name = "parking"
2385version = "2.0.0"2416version = "2.0.0"
@@ -2926,6 +2957,16 @@ dependencies = [ · 540 unmodified lines
2926 "unicode-ident",2957 "unicode-ident",
2927]2958]
2928 2959
2960[[package]]
2961name = "tabled"
2962version = "0.8.0"
2963source = "registry+https://github.com/rust-lang/crates.io-index"
2964checksum = "e5b2f8c37d26d87d2252187b0a45ea3cbf42baca10377c7e7eaaa2800fa9bf97"
2965dependencies = [
2966 "papergrid",
2967 "unicode-width",
2968]
2969
2929[[package]]2970[[package]]
2930name = "tar"2971name = "tar"
2931version = "0.4.38"2972version = "0.4.38"
Cargo.toml+4−0
@@ -92,6 +92,10 @@ env_logger = { version = "0.9.0", default-features = false } · 91 unmodified lines
92crosstermion = { version = "0.10.1", optional = true, default-features = false }92crosstermion = { version = "0.10.1", optional = true, default-features = false }
93futures-lite = { version = "1.12.0", optional = true, default-features = false, features = ["std"] }93futures-lite = { version = "1.12.0", optional = true, default-features = false, features = ["std"] }
94 94
95# for progress
96owo-colors = "3.5.0"
97tabled = { version = "0.8.0", default-features = false }
98
95document-features = { version = "0.2.0", optional = true }99document-features = { version = "0.2.0", optional = true }
96 100
97[profile.dev.package]101[profile.dev.package]
DEVELOPMENT.md+15−0
@@ -18,6 +18,21 @@ · 17 unmodified lines
18 * We `thiserror` generally.18 * We `thiserror` generally.
19 * Adhere to the [stability guide](https://github.com/Byron/gitoxide/blob/main/STABILITY.md)19 * Adhere to the [stability guide](https://github.com/Byron/gitoxide/blob/main/STABILITY.md)
20 20
21## Configuration and overrides
22
23As a general rule, respect and implement all applicable [git-config](https://git-scm.com/docs/git-config) by default, but allow the
24caller to set overrides. How overrides work depends on the goals of the particular API so it can be done on the main call path,
25forcing a choice, or more typically, as a side-lane where overrides can be done on demand.
26
27Note that it should be possible to obtain the current configuration for modification by the user for selective overrides, either
28by calling methods or by obtaining a data structure that can be set as a whole using a `get -> modify -> set` cycle.
29
30Note that without any of that, one should document that with `config_snapshot_mut()` any of the relevant configuration can be
31changed in memory before invoking a method in order to affect it.
32
33Parameters which are not available in git or specific to `gitoxide` or the needs of the caller can be passed as parameters or via
34`Options` or `Context` structures as needed.
35
21## General36## General
22 37
23* **async**38* **async**
README.md+3−0
@@ -27,6 +27,9 @@ Please see _'Development Status'_ for a listing of all crates and their capabili · 26 unmodified lines
27 * Based on the [git-hours] algorithm.27 * Based on the [git-hours] algorithm.
28 * See the [discussion][git-hours-discussion] for some performance data.28 * See the [discussion][git-hours-discussion] for some performance data.
29 * **the `gix` program** _(plumbing)_ - lower level commands for use in automation29 * **the `gix` program** _(plumbing)_ - lower level commands for use in automation
30 * **progress** - provide an overview of what works and what doesn't from the perspective of the git configuration.
31 This is likely to change a lot over time depending on actual needs, but maybe useful for you to see
32 if particular git-configuration is picked up and where it deviates.
30 * **config** - list the complete git configuration in human-readable form and optionally filter sections by name.33 * **config** - list the complete git configuration in human-readable form and optionally filter sections by name.
31 * **exclude**34 * **exclude**
32 * [x] **query** - check if path specs are excluded via gits exclusion rules like `.gitignore`.35 * [x] **query** - check if path specs are excluded via gits exclusion rules like `.gitignore`.
etc/check-package-size.sh+1−1
@@ -57,6 +57,6 @@ echo "in root: gitoxide CLI" · 56 unmodified lines
57(enter git-odb && indent cargo diet -n --package-size-limit 120KB)57(enter git-odb && indent cargo diet -n --package-size-limit 120KB)
58(enter git-protocol && indent cargo diet -n --package-size-limit 50KB)58(enter git-protocol && indent cargo diet -n --package-size-limit 50KB)
59(enter git-packetline && indent cargo diet -n --package-size-limit 35KB)59(enter git-packetline && indent cargo diet -n --package-size-limit 35KB)
60(enter git-repository && indent cargo diet -n --package-size-limit 175KB)60(enter git-repository && indent cargo diet -n --package-size-limit 185KB)
61(enter git-transport && indent cargo diet -n --package-size-limit 55KB)61(enter git-transport && indent cargo diet -n --package-size-limit 55KB)
62(enter gitoxide-core && indent cargo diet -n --package-size-limit 90KB)62(enter gitoxide-core && indent cargo diet -n --package-size-limit 90KB)
git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs+1−0
@@ -285,6 +285,7 @@ value = branch-override-by-include · 284 unmodified lines
285 deref: false,285 deref: false,
286 }),286 }),
287 git_repository::lock::acquire::Fail::Immediately,287 git_repository::lock::acquire::Fail::Immediately,
288 git_repository::lock::acquire::Fail::Immediately,
288 )?289 )?
289 .commit(repo.committer_or_default())?;290 .commit(repo.committer_or_default())?;
290 291
git-pack/src/bundle/write/error.rs+2−0
@@ -2,7 +2,9 @@ use std::io; · 1 unmodified line
2 2
3use git_tempfile::handle::Writable;3use git_tempfile::handle::Writable;
4 4
5/// The error returned by [`Bundle::write_to_directory()`][crate::Bundle::write_to_directory()]
5#[derive(thiserror::Error, Debug)]6#[derive(thiserror::Error, Debug)]
7#[allow(missing_docs)]
6pub enum Error {8pub enum Error {
7 #[error("An IO error occurred when reading the pack or creating a temporary file")]9 #[error("An IO error occurred when reading the pack or creating a temporary file")]
8 Io(#[from] io::Error),10 Io(#[from] io::Error),
git-pack/src/bundle/write/mod.rs+2−2
@@ -10,7 +10,7 @@ use git_tempfile::{handle::Writable, AutoRemove, ContainingDirectory}; · 9 unmodified lines
10use crate::data;10use crate::data;
11 11
12mod error;12mod error;
13use error::Error;13pub use error::Error;
14 14
15mod types;15mod types;
16use types::{LockWriter, PassThrough};16use types::{LockWriter, PassThrough};
@@ -228,7 +228,7 @@ impl crate::Bundle { · 211 unmodified lines
228 Options {228 Options {
229 thread_limit,229 thread_limit,
230 iteration_mode: _,230 iteration_mode: _,
231 index_kind,231 index_version: index_kind,
232 object_hash,232 object_hash,
233 }: Options,233 }: Options,
234 data_file: Arc<parking_lot::Mutex<git_tempfile::Handle<Writable>>>,234 data_file: Arc<parking_lot::Mutex<git_tempfile::Handle<Writable>>>,
git-pack/src/bundle/write/types.rs+2−2
@@ -11,7 +11,7 @@ pub struct Options { · 10 unmodified lines
11 /// Determine how much processing to spend on protecting against corruption or recovering from errors.11 /// Determine how much processing to spend on protecting against corruption or recovering from errors.
12 pub iteration_mode: crate::data::input::Mode,12 pub iteration_mode: crate::data::input::Mode,
13 /// The version of pack index to write, should be [`crate::index::Version::default()`]13 /// The version of pack index to write, should be [`crate::index::Version::default()`]
14 pub index_kind: crate::index::Version,14 pub index_version: crate::index::Version,
15 /// The kind of hash to use when writing the bundle.15 /// The kind of hash to use when writing the bundle.
16 pub object_hash: git_hash::Kind,16 pub object_hash: git_hash::Kind,
17}17}
@@ -22,7 +22,7 @@ impl Default for Options { · 4 unmodified lines
22 Options {22 Options {
23 thread_limit: None,23 thread_limit: None,
24 iteration_mode: crate::data::input::Mode::Verify,24 iteration_mode: crate::data::input::Mode::Verify,
25 index_kind: Default::default(),25 index_version: Default::default(),
26 object_hash: Default::default(),26 object_hash: Default::default(),
27 }27 }
28 }28 }
git-pack/src/index/write/error.rs+1−1
@@ -14,7 +14,7 @@ pub enum Error { · 13 unmodified lines
14 IteratorInvariantNoRefDelta,14 IteratorInvariantNoRefDelta,
15 #[error("The iterator failed to set a trailing hash over all prior pack entries in the last provided entry")]15 #[error("The iterator failed to set a trailing hash over all prior pack entries in the last provided entry")]
16 IteratorInvariantTrailer,16 IteratorInvariantTrailer,
17 #[error("Did not encounter a single base")]17 #[error("Did not encounter a single base - refusing to write empty pack.")]
18 IteratorInvariantBasesPresent,18 IteratorInvariantBasesPresent,
19 #[error("Only u32::MAX objects can be stored in a pack, found {0}")]19 #[error("Only u32::MAX objects can be stored in a pack, found {0}")]
20 IteratorInvariantTooManyObjects(usize),20 IteratorInvariantTooManyObjects(usize),
git-pack/src/index/write/mod.rs+4−9
@@ -18,7 +18,7 @@ pub(crate) struct TreeEntry { · 17 unmodified lines
18#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]18#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
19pub struct Outcome {19pub struct Outcome {
20 /// The version of the verified index20 /// The version of the verified index
21 pub index_kind: crate::index::Version,21 pub index_version: crate::index::Version,
22 /// The verified checksum of the verified index22 /// The verified checksum of the verified index
23 pub index_hash: git_hash::ObjectId,23 pub index_hash: git_hash::ObjectId,
24 24
@@ -171,7 +171,7 @@ impl crate::index::File { · 146 unmodified lines
171 modify_base(data, entry, bytes, kind.hash());171 modify_base(data, entry, bytes, kind.hash());
172 Ok::<_, Error>(())172 Ok::<_, Error>(())
173 },173 },
174 crate::cache::delta::traverse::Options {174 traverse::Options {
175 object_progress: root_progress.add_child("Resolving"),175 object_progress: root_progress.add_child("Resolving"),
176 size_progress: root_progress.add_child("Decoding"),176 size_progress: root_progress.add_child("Decoding"),
177 thread_limit,177 thread_limit,
@@ -207,7 +207,7 @@ impl crate::index::File { · 29 unmodified lines
207 progress::MessageLevel::Success,207 progress::MessageLevel::Success,
208 );208 );
209 Ok(Outcome {209 Ok(Outcome {
210 index_kind: kind,210 index_version: kind,
211 index_hash,211 index_hash,
212 data_hash: pack_hash,212 data_hash: pack_hash,
213 num_objects,213 num_objects,
@@ -215,12 +215,7 @@ impl crate::index::File { · 1 unmodified line
215 }215 }
216}216}
217 217
218fn modify_base(218fn modify_base(entry: &mut TreeEntry, pack_entry: &crate::data::Entry, decompressed: &[u8], hash: git_hash::Kind) {
219 entry: &mut crate::index::write::TreeEntry,
220 pack_entry: &crate::data::Entry,
221 decompressed: &[u8],
222 hash: git_hash::Kind,
223) {
224 fn compute_hash(kind: git_object::Kind, bytes: &[u8], object_hash: git_hash::Kind) -> git_hash::ObjectId {219 fn compute_hash(kind: git_object::Kind, bytes: &[u8], object_hash: git_hash::Kind) -> git_hash::ObjectId {
225 let mut hasher = git_features::hash::hasher(object_hash);220 let mut hasher = git_features::hash::hasher(object_hash);
226 hasher.update(&git_object::encode::loose_header(kind, bytes.len()));221 hasher.update(&git_object::encode::loose_header(kind, bytes.len()));
git-pack/tests/pack/bundle.rs+2−2
@@ -90,7 +90,7 @@ mod write_to_directory { · 89 unmodified lines
90 fn expected_outcome() -> Result<pack::bundle::write::Outcome, Box<dyn std::error::Error>> {90 fn expected_outcome() -> Result<pack::bundle::write::Outcome, Box<dyn std::error::Error>> {
91 Ok(pack::bundle::write::Outcome {91 Ok(pack::bundle::write::Outcome {
92 index: pack::index::write::Outcome {92 index: pack::index::write::Outcome {
93 index_kind: pack::index::Version::V2,93 index_version: pack::index::Version::V2,
94 index_hash: git_hash::ObjectId::from_hex(b"544a7204a55f6e9cacccf8f6e191ea8f83575de3")?,94 index_hash: git_hash::ObjectId::from_hex(b"544a7204a55f6e9cacccf8f6e191ea8f83575de3")?,
95 data_hash: git_hash::ObjectId::from_hex(b"0f3ea84cd1bba10c2a03d736a460635082833e59")?,95 data_hash: git_hash::ObjectId::from_hex(b"0f3ea84cd1bba10c2a03d736a460635082833e59")?,
96 num_objects: 42,96 num_objects: 42,
@@ -156,7 +156,7 @@ mod write_to_directory { · 59 unmodified lines
156 pack::bundle::write::Options {156 pack::bundle::write::Options {
157 thread_limit: None,157 thread_limit: None,
158 iteration_mode: pack::data::input::Mode::Verify,158 iteration_mode: pack::data::input::Mode::Verify,
159 index_kind: pack::index::Version::V2,159 index_version: pack::index::Version::V2,
160 object_hash: git_hash::Kind::Sha1,160 object_hash: git_hash::Kind::Sha1,
161 },161 },
162 )162 )
git-pack/tests/pack/data/output/count_and_entries.rs+15−0
@@ -321,6 +321,21 @@ fn traversals() -> crate::Result { · 320 unmodified lines
321 Ok(())321 Ok(())
322}322}
323 323
324#[test]
325fn empty_pack_is_not_allowed() {
326 assert_eq!(
327 write_and_verify(
328 db(DbKind::DeterministicGeneratedContent).unwrap(),
329 vec![],
330 hex_to_id("029d08823bd8a8eab510ad6ac75c823cfd3ed31e"),
331 None,
332 )
333 .unwrap_err()
334 .to_string(),
335 "Did not encounter a single base - refusing to write empty pack."
336 );
337}
338
324fn write_and_verify(339fn write_and_verify(
325 db: git_odb::HandleArc,340 db: git_odb::HandleArc,
326 entries: Vec<output::Entry>,341 entries: Vec<output::Entry>,
git-pack/tests/pack/index.rs+1−1
@@ -205,7 +205,7 @@ mod version { · 204 unmodified lines
205 outcome.num_objects, num_objects,205 outcome.num_objects, num_objects,
206 "it wrote the entire iterator worth of entries"206 "it wrote the entire iterator worth of entries"
207 );207 );
208 assert_eq!(outcome.index_kind, desired_kind);208 assert_eq!(outcome.index_version, desired_kind);
209 assert_eq!(209 assert_eq!(
210 outcome.index_hash,210 outcome.index_hash,
211 git_hash::ObjectId::from(&expected[end_of_pack_hash..end_of_index_hash])211 git_hash::ObjectId::from(&expected[end_of_pack_hash..end_of_index_hash])
git-protocol/src/fetch/arguments/blocking_io.rs+2−1
@@ -5,7 +5,8 @@ use git_transport::{client, client::TransportV2Ext}; · 4 unmodified lines
5use crate::fetch::{Arguments, Command};5use crate::fetch::{Arguments, Command};
6 6
7impl Arguments {7impl Arguments {
8 pub(crate) fn send<'a, T: client::Transport + 'a>(8 /// Send fetch arguments to the server, and indicate this is the end of negotiations only if `add_done_argument` is present.
9 pub fn send<'a, T: client::Transport + 'a>(
9 &mut self,10 &mut self,
10 transport: &'a mut T,11 transport: &'a mut T,
11 add_done_argument: bool,12 add_done_argument: bool,
git-protocol/src/fetch/arguments/mod.rs+11−1
@@ -3,6 +3,7 @@ use std::fmt; · 2 unmodified lines
3use bstr::{BStr, BString, ByteVec};3use bstr::{BStr, BString, ByteVec};
4 4
5/// The arguments passed to a server command.5/// The arguments passed to a server command.
6#[derive(Debug)]
6pub struct Arguments {7pub struct Arguments {
7 /// The active features/capabilities of the fetch invocation8 /// The active features/capabilities of the fetch invocation
8 #[cfg(any(feature = "async-client", feature = "blocking-client"))]9 #[cfg(any(feature = "async-client", feature = "blocking-client"))]
@@ -24,6 +25,13 @@ pub struct Arguments { · 15 unmodified lines
24}25}
25 26
26impl Arguments {27impl Arguments {
28 /// Return true if there is no argument at all.
29 ///
30 /// This can happen if callers assure that they won't add 'wants' if their 'have' is the same, i.e. if the remote has nothing
31 /// new for them.
32 pub fn is_empty(&self) -> bool {
33 self.args.is_empty()
34 }
27 /// Return true if ref filters is supported.35 /// Return true if ref filters is supported.
28 pub fn can_use_filter(&self) -> bool {36 pub fn can_use_filter(&self) -> bool {
29 self.filter37 self.filter
@@ -125,8 +133,10 @@ impl Arguments { · 95 unmodified lines
125 fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) {133 fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) {
126 self.args.push(format!("{}{}", prefix, value).into());134 self.args.push(format!("{}{}", prefix, value).into());
127 }135 }
136 /// Create a new instance to help setting up arguments to send to the server as part of a `fetch` operation
137 /// for which `features` are the available and configured features to use.
128 #[cfg(any(feature = "async-client", feature = "blocking-client"))]138 #[cfg(any(feature = "async-client", feature = "blocking-client"))]
129 pub(crate) fn new(version: git_transport::Protocol, features: Vec<crate::fetch::command::Feature>) -> Self {139 pub fn new(version: git_transport::Protocol, features: Vec<crate::fetch::command::Feature>) -> Self {
130 use crate::fetch::Command;140 use crate::fetch::Command;
131 let has = |name: &str| features.iter().any(|f| f.0 == name);141 let has = |name: &str| features.iter().any(|f| f.0 == name);
132 let filter = has("filter");142 let filter = has("filter");
git-protocol/src/fetch/command.rs+14−4
@@ -50,9 +50,12 @@ mod with_io { · 49 unmodified lines
50 "filter ", // filter-spec50 "filter ", // filter-spec
51 // ref-in-want feature51 // ref-in-want feature
52 "want-ref ", // ref path52 "want-ref ", // ref path
53 // sideband-all feature
53 "sideband-all",54 "sideband-all",
54 // packfile-uris feature55 // packfile-uris feature
55 "packfile-uris ", // protocols56 "packfile-uris ", // protocols
57 // wait-for-done feature
58 "wait-for-done",
56 ],59 ],
57 }60 }
58 }61 }
@@ -79,9 +82,14 @@ mod with_io { · 20 unmodified lines
79 "no-done",82 "no-done",
80 "filter",83 "filter",
81 ],84 ],
82 git_transport::Protocol::V2 => {85 git_transport::Protocol::V2 => &[
83 &["shallow", "filter", "ref-in-want", "sideband-all", "packfile-uris"]86 "shallow",
84 }87 "filter",
88 "ref-in-want",
89 "sideband-all",
90 "packfile-uris",
91 "wait-for-done",
92 ],
85 },93 },
86 }94 }
87 }95 }
@@ -104,7 +112,9 @@ mod with_io { · 16 unmodified lines
104 }112 }
105 }113 }
106 114
107 pub(crate) fn default_features(115 /// Turns on all modern features for V1 and all supported features for V2, returning them as a vector of features.
116 /// Note that this is the basis for any fetch operation as these features fulfil basic requirements and reasonably up-to-date servers.
117 pub fn default_features(
108 &self,118 &self,
109 version: git_transport::Protocol,119 version: git_transport::Protocol,
110 server_capabilities: &Capabilities,120 server_capabilities: &Capabilities,
git-protocol/src/fetch/handshake.rs+1−1
@@ -3,7 +3,7 @@ use git_transport::client::Capabilities; · 2 unmodified lines
3use crate::fetch::Ref;3use crate::fetch::Ref;
4 4
5/// The result of the [`handshake()`][super::handshake()] function.5/// The result of the [`handshake()`][super::handshake()] function.
6#[derive(Debug, Clone)]6#[derive(Default, Debug, Clone)]
7#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]7#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
8pub struct Outcome {8pub struct Outcome {
9 /// The protocol version the server responded with. It might have downgraded the desired version.9 /// The protocol version the server responded with. It might have downgraded the desired version.
git-protocol/src/fetch/refs/function.rs+2−2
@@ -10,13 +10,13 @@ use super::Error; · 9 unmodified lines
10use crate::fetch::{indicate_end_of_interaction, refs::from_v2_refs, Command, LsRefsAction, Ref};10use crate::fetch::{indicate_end_of_interaction, refs::from_v2_refs, Command, LsRefsAction, Ref};
11 11
12/// Invoke an ls-refs command on `transport` (assuming `protocol_version` 2 or panic), which requires a prior handshake that yielded12/// Invoke an ls-refs command on `transport` (assuming `protocol_version` 2 or panic), which requires a prior handshake that yielded
13/// server `capabilities`. `prepare_ls_refs(arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback.13/// server `capabilities`. `prepare_ls_refs(capabilities, arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback.
14#[maybe_async]14#[maybe_async]
15pub async fn refs(15pub async fn refs(
16 mut transport: impl Transport,16 mut transport: impl Transport,
17 protocol_version: Protocol,17 protocol_version: Protocol,
18 capabilities: &Capabilities,18 capabilities: &Capabilities,
19 mut prepare_ls_refs: impl FnMut(19 prepare_ls_refs: impl FnOnce(
20 &Capabilities,20 &Capabilities,
21 &mut Vec<BString>,21 &mut Vec<BString>,
22 &mut Vec<(&str, Option<&str>)>,22 &mut Vec<(&str, Option<&str>)>,
git-protocol/src/fetch/refs/mod.rs+1−1
@@ -50,7 +50,7 @@ pub mod parse { · 49 unmodified lines
50pub enum Ref {50pub enum Ref {
51 /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit51 /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
52 Peeled {52 Peeled {
53 /// The name at which the ref is located, like `refs/heads/main`.53 /// The name at which the ref is located, like `refs/tags/1.0`.
54 full_ref_name: BString,54 full_ref_name: BString,
55 /// The hash of the tag the ref points to.55 /// The hash of the tag the ref points to.
56 tag: git_hash::ObjectId,56 tag: git_hash::ObjectId,
git-protocol/src/fetch/response/mod.rs+2−1
@@ -152,7 +152,8 @@ impl Response { · 151 unmodified lines
152 self.has_pack152 self.has_pack
153 }153 }
154 154
155 /// Return an error if the given `features` don't contain the required ones for the given `version` of the protocol.155 /// Return an error if the given `features` don't contain the required ones (the ones this implementation needs)
156 /// for the given `version` of the protocol.
156 ///157 ///
157 /// Even though technically any set of features supported by the server could work, we only implement the ones that158 /// Even though technically any set of features supported by the server could work, we only implement the ones that
158 /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable.159 /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable.
git-protocol/src/fetch/tests/arguments.rs+14−0
@@ -46,6 +46,13 @@ mod impls { · 45 unmodified lines
46 fn connection_persists_across_multiple_requests(&self) -> bool {46 fn connection_persists_across_multiple_requests(&self) -> bool {
47 self.stateful47 self.stateful
48 }48 }
49
50 fn configure(
51 &mut self,
52 config: &dyn std::any::Any,
53 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
54 self.inner.configure(config)
55 }
49 }56 }
50 57
51 impl<T: client::Transport> client::Transport for Transport<T> {58 impl<T: client::Transport> client::Transport for Transport<T> {
@@ -89,6 +96,13 @@ mod impls { · 37 unmodified lines
89 fn connection_persists_across_multiple_requests(&self) -> bool {96 fn connection_persists_across_multiple_requests(&self) -> bool {
90 self.stateful97 self.stateful
91 }98 }
99
100 fn configure(
101 &mut self,
102 config: &dyn std::any::Any,
103 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
104 self.inner.configure(config)
105 }
92 }106 }
93 107
94 #[async_trait(?Send)]108 #[async_trait(?Send)]
git-protocol/src/remote_progress.rs+1−1
@@ -54,7 +54,7 @@ impl<'a> RemoteProgress<'a> { · 53 unmodified lines
54 progress.fail(progress_name(None, text));54 progress.fail(progress_name(None, text));
55 }55 }
56 } else {56 } else {
57 match Self::from_bytes(text) {57 match RemoteProgress::from_bytes(text) {
58 Some(RemoteProgress {58 Some(RemoteProgress {
59 action,59 action,
60 percent: _,60 percent: _,
git-ref/src/fullname.rs+9−0
@@ -38,6 +38,15 @@ impl TryFrom<BString> for FullName { · 37 unmodified lines
38 }38 }
39}39}
40 40
41impl TryFrom<&BString> for FullName {
42 type Error = git_validate::refname::Error;
43
44 fn try_from(value: &BString) -> Result<Self, Self::Error> {
45 git_validate::refname(value.as_ref())?;
46 Ok(FullName(value.clone()))
47 }
48}
49
41impl From<FullName> for BString {50impl From<FullName> for BString {
42 fn from(name: FullName) -> Self {51 fn from(name: FullName) -> Self {
43 name.052 name.0
git-ref/src/store/file/transaction/prepare.rs+8−4
@@ -176,7 +176,8 @@ impl<'s> Transaction<'s> { · 175 unmodified lines
176 pub fn prepare(176 pub fn prepare(
177 mut self,177 mut self,
178 edits: impl IntoIterator<Item = RefEdit>,178 edits: impl IntoIterator<Item = RefEdit>,
179 lock_fail_mode: git_lock::acquire::Fail,179 ref_files_lock_fail_mode: git_lock::acquire::Fail,
180 packed_refs_lock_fail_mode: git_lock::acquire::Fail,
180 ) -> Result<Self, Error> {181 ) -> Result<Self, Error> {
181 assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times");182 assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times");
182 let store = self.store;183 let store = self.store;
@@ -267,7 +268,7 @@ impl<'s> Transaction<'s> { · 84 unmodified lines
267 let packed_transaction: Option<_> = if maybe_updates_for_packed_refs.unwrap_or(0) > 0 {268 let packed_transaction: Option<_> = if maybe_updates_for_packed_refs.unwrap_or(0) > 0 {
268 // We have to create a packed-ref even if it doesn't exist269 // We have to create a packed-ref even if it doesn't exist
269 self.store270 self.store
270 .packed_transaction(lock_fail_mode)271 .packed_transaction(packed_refs_lock_fail_mode)
271 .map_err(|err| match err {272 .map_err(|err| match err {
272 file::packed::transaction::Error::BufferOpen(err) => Error::from(err),273 file::packed::transaction::Error::BufferOpen(err) => Error::from(err),
273 file::packed::transaction::Error::TransactionLock(err) => {274 file::packed::transaction::Error::TransactionLock(err) => {
@@ -280,7 +281,10 @@ impl<'s> Transaction<'s> { · 6 unmodified lines
280 // no packed-ref file exists anyway281 // no packed-ref file exists anyway
281 self.store282 self.store
282 .assure_packed_refs_uptodate()?283 .assure_packed_refs_uptodate()?
283 .map(|p| buffer_into_transaction(p, lock_fail_mode).map_err(Error::PackedTransactionAcquire))284 .map(|p| {
285 buffer_into_transaction(p, packed_refs_lock_fail_mode)
286 .map_err(Error::PackedTransactionAcquire)
287 })
284 .transpose()?288 .transpose()?
285 };289 };
286 if let Some(transaction) = packed_transaction {290 if let Some(transaction) = packed_transaction {
@@ -302,7 +306,7 @@ impl<'s> Transaction<'s> { · 15 unmodified lines
302 let change = &mut updates[cid];306 let change = &mut updates[cid];
303 if let Err(err) = Self::lock_ref_and_apply_change(307 if let Err(err) = Self::lock_ref_and_apply_change(
304 self.store,308 self.store,
305 lock_fail_mode,309 ref_files_lock_fail_mode,
306 self.packed_transaction.as_ref().and_then(|t| t.buffer()),310 self.packed_transaction.as_ref().and_then(|t| t.buffer()),
307 change,311 change,
308 ) {312 ) {
git-ref/src/target.rs+1−1
@@ -19,7 +19,7 @@ impl<'a> TargetRef<'a> { · 18 unmodified lines
19 TargetRef::Peeled(oid) => Some(oid),19 TargetRef::Peeled(oid) => Some(oid),
20 }20 }
21 }21 }
22 /// Interpret this target as object id or panic if it is symbolic.22 /// Interpret this target as object id or **panic** if it is symbolic.
23 pub fn id(&self) -> &oid {23 pub fn id(&self) -> &oid {
24 match self {24 match self {
25 TargetRef::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"),25 TargetRef::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"),
git-ref/src/transaction/mod.rs+9−1
@@ -83,7 +83,15 @@ pub enum Change { · 82 unmodified lines
83}83}
84 84
85impl Change {85impl Change {
86 /// Return references to values that are in common between all variants.86 /// Return references to values that are the new value after the change is applied, if this is an update.
87 pub fn new_value(&self) -> Option<crate::TargetRef<'_>> {
88 match self {
89 Change::Update { new, .. } => new.to_ref().into(),
90 Change::Delete { .. } => None,
91 }
92 }
93
94 /// Return references to values that are in common between all variants and denote the previous observed value.
87 pub fn previous_value(&self) -> Option<crate::TargetRef<'_>> {95 pub fn previous_value(&self) -> Option<crate::TargetRef<'_>> {
88 match self {96 match self {
89 // TODO: use or-patterns once MRV is larger than 1.52 (and this is supported)97 // TODO: use or-patterns once MRV is larger than 1.52 (and this is supported)
git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs+18−3
@@ -44,6 +44,7 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca · 43 unmodified lines
44 deref: false,44 deref: false,
45 }),45 }),
46 Fail::Immediately,46 Fail::Immediately,
47 Fail::Immediately,
47 )?48 )?
48 .commit(committer().to_ref());49 .commit(committer().to_ref());
49 if *is_empty {50 if *is_empty {
@@ -83,6 +84,7 @@ fn reference_with_old_value_must_exist_when_creating_it() -> crate::Result { · 33 unmodified lines
83 deref: false,84 deref: false,
84 }),85 }),
85 Fail::Immediately,86 Fail::Immediately,
87 Fail::Immediately,
86 );88 );
87 89
88 match res {90 match res {
@@ -114,6 +116,7 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu · 25 unmodified lines
114 deref: false,116 deref: false,
115 }),117 }),
116 Fail::Immediately,118 Fail::Immediately,
119 Fail::Immediately,
117 );120 );
118 match res {121 match res {
119 Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => {122 Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => {
@@ -142,6 +145,7 @@ fn the_existing_must_match_constraint_allow_non_existing_references_to_be_create · 22 unmodified lines
142 deref: false,145 deref: false,
143 }),146 }),
144 Fail::Immediately,147 Fail::Immediately,
148 Fail::Immediately,
145 )?149 )?
146 .commit(committer().to_ref())?;150 .commit(committer().to_ref())?;
147 151
@@ -180,6 +184,7 @@ fn the_existing_must_match_constraint_requires_existing_references_to_have_the_g · 32 unmodified lines
180 deref: false,184 deref: false,
181 }),185 }),
182 Fail::Immediately,186 Fail::Immediately,
187 Fail::Immediately,
183 );188 );
184 match res {189 match res {
185 Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => {190 Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => {
@@ -208,6 +213,7 @@ fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_alrea · 22 unmodified lines
208 deref: false,213 deref: false,
209 }),214 }),
210 Fail::Immediately,215 Fail::Immediately,
216 Fail::Immediately,
211 );217 );
212 match res {218 match res {
213 Err(transaction::prepare::Error::MustNotExist { full_name, actual, .. }) => {219 Err(transaction::prepare::Error::MustNotExist { full_name, actual, .. }) => {
@@ -246,6 +252,7 @@ fn namespaced_updates_or_deletions_are_transparent_and_not_observable() -> crate · 32 unmodified lines
246 },252 },
247 ],253 ],
248 Fail::Immediately,254 Fail::Immediately,
255 Fail::Immediately,
249 )?256 )?
250 .commit(committer().to_ref())?;257 .commit(committer().to_ref())?;
251 258
@@ -295,6 +302,7 @@ fn reference_with_must_exist_constraint_must_exist_already_with_any_value() -> c · 43 unmodified lines
295 deref: false,302 deref: false,
296 }),303 }),
297 Fail::Immediately,304 Fail::Immediately,
305 Fail::Immediately,
298 )?306 )?
299 .commit(committer().to_ref())?;307 .commit(committer().to_ref())?;
300 308
@@ -340,6 +348,7 @@ fn reference_with_must_not_exist_constraint_may_exist_already_if_the_new_value_m · 39 unmodified lines
340 deref: false,348 deref: false,
341 }),349 }),
342 Fail::Immediately,350 Fail::Immediately,
351 Fail::Immediately,
343 )?352 )?
344 .commit(committer().to_ref())?;353 .commit(committer().to_ref())?;
345 354
@@ -387,6 +396,7 @@ fn cancellation_after_preparation_leaves_no_change() -> crate::Result { · 41 unmodified lines
387 deref: false,396 deref: false,
388 }),397 }),
389 Fail::Immediately,398 Fail::Immediately,
399 Fail::Immediately,
390 )?;400 )?;
391 assert_eq!(std::fs::read_dir(dir.path())?.count(), 1, "the lock file was created");401 assert_eq!(std::fs::read_dir(dir.path())?.count(), 1, "the lock file was created");
392 402
@@ -424,6 +434,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { · 31 unmodified lines
424 deref: false,434 deref: false,
425 }),435 }),
426 Fail::Immediately,436 Fail::Immediately,
437 Fail::Immediately,
427 )?438 )?
428 .commit(committer().to_ref())?;439 .commit(committer().to_ref())?;
429 assert_eq!(440 assert_eq!(
@@ -475,6 +486,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { · 45 unmodified lines
475 deref: true,486 deref: true,
476 }),487 }),
477 Fail::Immediately,488 Fail::Immediately,
489 Fail::Immediately,
478 )?490 )?
479 .commit(committer().to_ref())?;491 .commit(committer().to_ref())?;
480 492
@@ -568,6 +580,7 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho · 87 unmodified lines
568 deref: false,580 deref: false,
569 }),581 }),
570 Fail::Immediately,582 Fail::Immediately,
583 Fail::Immediately,
571 )?584 )?
572 .commit(committer().to_ref())?;585 .commit(committer().to_ref())?;
573 586
@@ -632,7 +645,8 @@ fn packed_refs_are_looked_up_when_checking_existing_values() -> crate::Result { · 58 unmodified lines
632 name: "refs/heads/main".try_into()?,645 name: "refs/heads/main".try_into()?,
633 deref: false,646 deref: false,
634 }),647 }),
635 git_lock::acquire::Fail::Immediately,648 Fail::Immediately,
649 Fail::Immediately,
636 )?650 )?
637 .commit(committer().to_ref())?;651 .commit(committer().to_ref())?;
638 652
@@ -687,7 +701,8 @@ fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs( · 48 unmodified lines
687 name: r.name,701 name: r.name,
688 deref: false,702 deref: false,
689 }),703 }),
690 git_lock::acquire::Fail::Immediately,704 Fail::Immediately,
705 Fail::Immediately,
691 )?706 )?
692 .commit(committer().to_ref())?;707 .commit(committer().to_ref())?;
693 708
@@ -743,7 +758,7 @@ fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() · 49 unmodified lines
743 .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdates(Box::new(|_, _| {758 .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdates(Box::new(|_, _| {
744 Ok(Some(git_object::Kind::Commit))759 Ok(Some(git_object::Kind::Commit))
745 })))760 })))
746 .prepare(edits, git_lock::acquire::Fail::Immediately)?761 .prepare(edits, Fail::Immediately, Fail::Immediately)?
747 .commit(committer().to_ref())?;762 .commit(committer().to_ref())?;
748 assert_eq!(763 assert_eq!(
749 edits.len(),764 edits.len(),
git-ref/tests/file/transaction/prepare_and_commit/delete.rs+13−0
@@ -28,6 +28,7 @@ fn delete_a_ref_which_is_gone_succeeds() -> crate::Result { · 27 unmodified lines
28 deref: false,28 deref: false,
29 }),29 }),
30 Fail::Immediately,30 Fail::Immediately,
31 Fail::Immediately,
31 )?32 )?
32 .commit(committer().to_ref())?;33 .commit(committer().to_ref())?;
33 assert_eq!(edits.len(), 1);34 assert_eq!(edits.len(), 1);
@@ -47,6 +48,7 @@ fn delete_a_ref_which_is_gone_but_must_exist_fails() -> crate::Result { · 13 unmodified lines
47 deref: false,48 deref: false,
48 }),49 }),
49 Fail::Immediately,50 Fail::Immediately,
51 Fail::Immediately,
50 );52 );
51 match res {53 match res {
52 Ok(_) => unreachable!("must exist, but it doesn't actually exist"),54 Ok(_) => unreachable!("must exist, but it doesn't actually exist"),
@@ -77,6 +79,7 @@ fn delete_ref_and_reflog_on_symbolic_no_deref() -> crate::Result { · 24 unmodified lines
77 deref: false,79 deref: false,
78 }),80 }),
79 Fail::Immediately,81 Fail::Immediately,
82 Fail::Immediately,
80 )?83 )?
81 .commit(committer().to_ref())?;84 .commit(committer().to_ref())?;
82 85
@@ -117,6 +120,7 @@ fn delete_ref_with_incorrect_previous_value_fails() -> crate::Result { · 34 unmodified lines
117 deref: true,120 deref: true,
118 }),121 }),
119 Fail::Immediately,122 Fail::Immediately,
123 Fail::Immediately,
120 );124 );
121 125
122 match res {126 match res {
@@ -151,6 +155,7 @@ fn delete_reflog_only_of_symbolic_no_deref() -> crate::Result { · 28 unmodified lines
151 deref: false,155 deref: false,
152 }),156 }),
153 Fail::Immediately,157 Fail::Immediately,
158 Fail::Immediately,
154 )?159 )?
155 .commit(committer().to_ref())?;160 .commit(committer().to_ref())?;
156 161
@@ -185,6 +190,7 @@ fn delete_reflog_only_of_symbolic_with_deref() -> crate::Result { · 28 unmodified lines
185 deref: true,190 deref: true,
186 }),191 }),
187 Fail::Immediately,192 Fail::Immediately,
193 Fail::Immediately,
188 )?194 )?
189 .commit(committer().to_ref())?;195 .commit(committer().to_ref())?;
190 196
@@ -218,6 +224,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Res · 27 unmodified lines
218 deref: true,224 deref: true,
219 }),225 }),
220 Fail::Immediately,226 Fail::Immediately,
227 Fail::Immediately,
221 );228 );
222 match res {229 match res {
223 Err(err) => {230 Err(err) => {
@@ -248,6 +255,7 @@ fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> crate::R · 24 unmodified lines
248 deref: true,255 deref: true,
249 }),256 }),
250 Fail::Immediately,257 Fail::Immediately,
258 Fail::Immediately,
251 )?259 )?
252 .commit(committer().to_ref())?;260 .commit(committer().to_ref())?;
253 261
@@ -284,6 +292,7 @@ fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> crate::Res · 30 unmodified lines
284 deref: true,292 deref: true,
285 }),293 }),
286 Fail::Immediately,294 Fail::Immediately,
295 Fail::Immediately,
287 )?296 )?
288 .commit(committer().to_ref())?;297 .commit(committer().to_ref())?;
289 298
@@ -325,6 +334,7 @@ fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> crate::Res · 35 unmodified lines
325 deref: false,334 deref: false,
326 }),335 }),
327 Fail::Immediately,336 Fail::Immediately,
337 Fail::Immediately,
328 )?338 )?
329 .commit(committer().to_ref())?;339 .commit(committer().to_ref())?;
330 assert_eq!(edits.len(), 1);340 assert_eq!(edits.len(), 1);
@@ -360,6 +370,7 @@ fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_delete · 29 unmodified lines
360 deref: false,370 deref: false,
361 }),371 }),
362 Fail::Immediately,372 Fail::Immediately,
373 Fail::Immediately,
363 )?374 )?
364 .commit(committer().to_ref())?;375 .commit(committer().to_ref())?;
365 376
@@ -393,6 +404,7 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ · 27 unmodified lines
393 deref: false,404 deref: false,
394 }),405 }),
395 Fail::Immediately,406 Fail::Immediately,
407 Fail::Immediately,
396 )?408 )?
397 .commit(committer().to_ref())?;409 .commit(committer().to_ref())?;
398 410
@@ -432,6 +444,7 @@ fn all_contained_references_deletes_the_packed_ref_file_too() -> crate::Result { · 33 unmodified lines
432 }444 }
433 }),445 }),
434 Fail::Immediately,446 Fail::Immediately,
447 Fail::Immediately,
435 )?448 )?
436 .commit(committer().to_ref())?;449 .commit(committer().to_ref())?;
437 450
git-ref/tests/file/worktree.rs+11−5
@@ -191,6 +191,7 @@ mod read_only { · 190 unmodified lines
191}191}
192 192
193mod writable {193mod writable {
194 use git_lock::acquire::Fail;
194 use std::convert::TryInto;195 use std::convert::TryInto;
195 196
196 use git_ref::{197 use git_ref::{
@@ -258,7 +259,8 @@ mod writable { · 61 unmodified lines
258 deref: false,259 deref: false,
259 },260 },
260 ],261 ],
261 git_lock::acquire::Fail::Immediately,262 Fail::Immediately,
263 Fail::Immediately,
262 )?264 )?
263 .commit(committer().to_ref())265 .commit(committer().to_ref())
264 .expect("successful commit as even similar resolved names live in different base locations");266 .expect("successful commit as even similar resolved names live in different base locations");
@@ -405,7 +407,8 @@ mod writable { · 140 unmodified lines
405 deref: false,407 deref: false,
406 },408 },
407 ],409 ],
408 git_lock::acquire::Fail::Immediately,410 Fail::Immediately,
411 Fail::Immediately,
409 ),412 ),
410 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })413 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })
411 ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)");414 ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)");
@@ -424,7 +427,8 @@ mod writable { · 12 unmodified lines
424 deref: false,427 deref: false,
425 },428 },
426 ],429 ],
427 git_lock::acquire::Fail::Immediately,430 Fail::Immediately,
431 Fail::Immediately,
428 ),432 ),
429 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })433 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })
430 ));434 ));
@@ -467,7 +471,8 @@ mod writable { · 36 unmodified lines
467 deref: false,471 deref: false,
468 },472 },
469 ],473 ],
470 git_lock::acquire::Fail::Immediately,474 Fail::Immediately,
475 Fail::Immediately,
471 ),476 ),
472 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })477 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })
473 ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)");478 ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)");
@@ -509,7 +514,8 @@ mod writable { · 35 unmodified lines
509 deref: false,514 deref: false,
510 },515 },
511 ],516 ],
512 git_lock::acquire::Fail::Immediately,517 Fail::Immediately,
518 Fail::Immediately,
513 )?519 )?
514 .commit(committer().to_ref())520 .commit(committer().to_ref())
515 .expect("successful commit as even similar resolved names live in different base locations");521 .expect("successful commit as even similar resolved names live in different base locations");
git-refspec/src/spec.rs+19−3
@@ -1,4 +1,4 @@
1use bstr::BStr;1use bstr::{BStr, ByteSlice};
2 2
3use crate::{3use crate::{
4 instruction::{Fetch, Push},4 instruction::{Fetch, Push},
@@ -18,6 +18,11 @@ impl RefSpec { · 13 unmodified lines
18 dst: self.dst.as_ref().map(|b| b.as_ref()),18 dst: self.dst.as_ref().map(|b| b.as_ref()),
19 }19 }
20 }20 }
21
22 /// Return true if the spec stats with a `+` and thus forces setting the reference.
23 pub fn allow_non_fast_forward(&self) -> bool {
24 matches!(self.mode, Mode::Force)
25 }
21}26}
22 27
23mod impls {28mod impls {
@@ -84,7 +89,7 @@ mod impls { · 60 unmodified lines
84}89}
85 90
86/// Access91/// Access
87impl RefSpecRef<'_> {92impl<'a> RefSpecRef<'a> {
88 /// Return the left-hand side of the spec, typically the source.93 /// Return the left-hand side of the spec, typically the source.
89 /// It takes many different forms so don't rely on this being a ref name.94 /// It takes many different forms so don't rely on this being a ref name.
90 ///95 ///
@@ -117,8 +122,19 @@ impl RefSpecRef<'_> { · 26 unmodified lines
117 }122 }
118 }123 }
119 124
125 /// Derive the prefix from the `source` side of this spec, if possible.
126 ///
127 /// This means it starts with `refs/`. Note that it won't contain more than two components, like `refs/heads/`
128 pub fn prefix(&self) -> Option<&BStr> {
129 let source = self.source()?;
130 let suffix = source.strip_prefix(b"refs/")?;
131 let slash_pos = suffix.find_byte(b'/')?;
132 let prefix = source[..="refs/".len() + slash_pos].as_bstr();
133 (!prefix.contains(&b'*')).then(|| prefix)
134 }
135
120 /// Transform the state of the refspec into an instruction making clear what to do with it.136 /// Transform the state of the refspec into an instruction making clear what to do with it.
121 pub fn instruction(&self) -> Instruction<'_> {137 pub fn instruction(&self) -> Instruction<'a> {
122 match self.op {138 match self.op {
123 Operation::Fetch => match (self.mode, self.src, self.dst) {139 Operation::Fetch => match (self.mode, self.src, self.dst) {
124 (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }),140 (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }),
git-refspec/tests/refspec.rs+1−0
@@ -6,4 +6,5 @@ mod impls; · 5 unmodified lines
6mod match_group;6mod match_group;
7mod matching;7mod matching;
8mod parse;8mod parse;
9mod spec;
9mod write;10mod write;
git-refspec/tests/spec/mod.rs+41−0
@@ -0,0 +1,41 @@
1mod prefix {
2 use git_refspec::parse::Operation;
3 use git_refspec::RefSpec;
4
5 #[test]
6 fn partial_refs_have_no_prefix() {
7 assert_eq!(parse("main").to_ref().prefix(), None);
8 }
9
10 #[test]
11 fn short_absolute_refs_have_no_prefix() {
12 assert_eq!(parse("refs/short").to_ref().prefix(), None);
13 }
14
15 #[test]
16 fn full_names_have_a_prefix() {
17 assert_eq!(parse("refs/heads/main").to_ref().prefix().unwrap(), "refs/heads/");
18 assert_eq!(parse("refs/foo/bar").to_ref().prefix().unwrap(), "refs/foo/");
19 assert_eq!(
20 parse("refs/heads/*:refs/remotes/origin/*").to_ref().prefix().unwrap(),
21 "refs/heads/"
22 );
23 }
24
25 #[test]
26 fn strange_glob_patterns_have_no_prefix() {
27 assert_eq!(parse("refs/*/main:refs/*/main").to_ref().prefix(), None);
28 }
29
30 #[test]
31 fn object_names_have_no_prefix() {
32 assert_eq!(
33 parse("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").to_ref().prefix(),
34 None
35 );
36 }
37
38 fn parse(spec: &str) -> RefSpec {
39 git_refspec::parse(spec.into(), Operation::Fetch).unwrap().to_owned()
40 }
41}
git-repository/examples/init-repo-and-commit.rs+26−39
@@ -13,49 +13,36 @@ fn main() -> anyhow::Result<()> { · 12 unmodified lines
13 let git_dir = std::env::args_os()13 let git_dir = std::env::args_os()
14 .nth(1)14 .nth(1)
15 .context("First argument needs to be the directory to initialize the repository in")?;15 .context("First argument needs to be the directory to initialize the repository in")?;
16 let repo = git::init_bare(git_dir)?;16 let mut repo = git::init_bare(git_dir)?;
17 17
18 println!("Repo (bare): {:?}", repo.git_dir());18 println!("Repo (bare): {:?}", repo.git_dir());
19 19
20 let mut tree = git::objs::Tree::empty();20 let mut tree = git::objs::Tree::empty();
21 let empty_tree_id = repo.write_object(&tree)?;21 let empty_tree_id = repo.write_object(&tree)?.detach();
22 22
23 let author = git::actor::SignatureRef {23 let mut config = repo.config_snapshot_mut();
24 name: "Maria Sanchez".into(),24 config.set_raw_value("author", None, "name", "Maria Sanchez")?;
25 email: "maria@example.com".into(),25 config.set_raw_value("author", None, "email", "maria@example.com")?;
26 time: git_date::Time::now_local_or_utc(),26 {
27 };27 let repo = config.commit_auto_rollback()?;
28 let initial_commit_id = repo.commit(28 let initial_commit_id = repo.commit("HEAD", "initial commit", empty_tree_id, git::commit::NO_PARENT_IDS)?;
29 "HEAD",29
30 author,30 println!("initial commit id with empty tree: {:?}", initial_commit_id);
31 author,31
32 "initial commit",32 let blob_id = repo.write_blob("hello world")?.into();
33 empty_tree_id,33 let entry = tree::Entry {
34 git::commit::NO_PARENT_IDS,34 mode: tree::EntryMode::Blob,
35 )?;35 oid: blob_id,
36 36 filename: "hello.txt".into(),
37 println!("initial commit id with empty tree: {:?}", initial_commit_id);37 };
38 38
39 let blob_id = repo.write_blob("hello world")?.into();39 tree.entries.push(entry);
40 let entry = tree::Entry {40 let hello_tree_id = repo.write_object(&tree)?;
41 mode: tree::EntryMode::Blob,41
42 oid: blob_id,42 let blob_commit_id = repo.commit("HEAD", "hello commit", hello_tree_id, [initial_commit_id])?;
43 filename: "hello.txt".into(),43
44 };44 println!("commit id for 'hello world' blob: {:?}", blob_commit_id);
45 45 }
46 tree.entries.push(entry);
47 let hello_tree_id = repo.write_object(&tree)?;
48
49 let blob_commit_id = repo.commit(
50 "HEAD",
51 author,
52 author,
53 "hello commit",
54 hello_tree_id,
55 [initial_commit_id],
56 )?;
57
58 println!("commit id for 'hello world' blob: {:?}", blob_commit_id);
59 46
60 Ok(())47 Ok(())
61}48}
git-repository/src/config/cache/access.rs+103−0
@@ -0,0 +1,103 @@
1use crate::config::Cache;
2use crate::{remote, repository::identity};
3use git_lock::acquire::Fail;
4use std::convert::TryInto;
5use std::path::PathBuf;
6use std::time::Duration;
7
8/// Access
9impl Cache {
10 pub(crate) fn personas(&self) -> &identity::Personas {
11 self.personas
12 .get_or_init(|| identity::Personas::from_config_and_env(&self.resolved, self.git_prefix))
13 }
14
15 pub(crate) fn url_rewrite(&self) -> &remote::url::Rewrite {
16 self.url_rewrite
17 .get_or_init(|| remote::url::Rewrite::from_config(&self.resolved, self.filter_config_section))
18 }
19
20 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
21 pub(crate) fn url_scheme(
22 &self,
23 ) -> Result<&remote::url::SchemePermission, remote::url::scheme_permission::init::Error> {
24 self.url_scheme.get_or_try_init(|| {
25 remote::url::SchemePermission::from_config(&self.resolved, self.git_prefix, self.filter_config_section)
26 })
27 }
28
29 /// Returns (file-timeout, pack-refs timeout)
30 pub(crate) fn lock_timeout(
31 &self,
32 ) -> Result<(git_lock::acquire::Fail, git_lock::acquire::Fail), git_config::value::Error> {
33 enum Kind {
34 RefFiles,
35 RefPackFile,
36 }
37 let mut out: [git_lock::acquire::Fail; 2] = Default::default();
38
39 for (idx, kind) in [Kind::RefFiles, Kind::RefPackFile].iter().enumerate() {
40 let (key, default_ms) = match kind {
41 Kind::RefFiles => ("filesRefLockTimeout", 100),
42 Kind::RefPackFile => ("packedRefsTimeout", 1000),
43 };
44 let mk_default = || Fail::AfterDurationWithBackoff(Duration::from_millis(default_ms));
45 let mut fnp = self.filter_config_section;
46
47 let lock_mode = match self.resolved.integer_filter("core", None, key, &mut fnp) {
48 Some(Ok(val)) if val < 0 => Fail::AfterDurationWithBackoff(Duration::from_secs(u64::MAX)),
49 Some(Ok(val)) if val == 0 => Fail::Immediately,
50 Some(Ok(val)) => Fail::AfterDurationWithBackoff(Duration::from_millis(
51 val.try_into().expect("i64 can be repsented by u64"),
52 )),
53 Some(Err(_)) if self.lenient_config => mk_default(),
54 Some(Err(err)) => return Err(err),
55 None => mk_default(),
56 };
57 out[idx] = lock_mode;
58 }
59 Ok((out[0], out[1]))
60 }
61
62 /// The path to the user-level excludes file to ignore certain files in the worktree.
63 pub(crate) fn excludes_file(&self) -> Result<Option<PathBuf>, git_config::path::interpolate::Error> {
64 let home = self.home_dir();
65 let install_dir = crate::path::install_dir().ok();
66 let ctx = crate::config::cache::interpolate_context(install_dir.as_deref(), home.as_deref());
67 match self
68 .resolved
69 .path_filter("core", None, "excludesFile", &mut self.filter_config_section.clone())
70 .map(|p| p.interpolate(ctx).map(|p| p.into_owned()))
71 .transpose()
72 {
73 Ok(f) => Ok(f),
74 Err(_err) if self.lenient_config => Ok(None),
75 Err(err) => Err(err),
76 }
77 }
78
79 /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations.
80 pub fn xdg_config_path(
81 &self,
82 resource_file_name: &str,
83 ) -> Result<Option<PathBuf>, git_sec::permission::Error<PathBuf>> {
84 std::env::var_os("XDG_CONFIG_HOME")
85 .map(|path| (path, &self.xdg_config_home_env))
86 .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env)))
87 .and_then(|(base, permission)| {
88 let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name);
89 permission.check(resource).transpose()
90 })
91 .transpose()
92 }
93
94 /// Return the home directory if we are allowed to read it and if it is set in the environment.
95 ///
96 /// We never fail for here even if the permission is set to deny as we `git-config` will fail later
97 /// if it actually wants to use the home directory - we don't want to fail prematurely.
98 pub fn home_dir(&self) -> Option<PathBuf> {
99 std::env::var_os("HOME")
100 .map(PathBuf::from)
101 .and_then(|path| self.home_env.check_opt(path))
102 }
103}
git-repository/src/config/cache/init.rs+31−53
@@ -1,7 +1,5 @@
1use std::path::PathBuf;
2
3use super::{interpolate_context, util, Error, StageOne};1use super::{interpolate_context, util, Error, StageOne};
4use crate::{config::Cache, repository, revision::spec::parse::ObjectKindHint};2use crate::{config::Cache, repository};
5 3
6/// Initialization4/// Initialization
7impl Cache {5impl Cache {
@@ -17,7 +15,7 @@ impl Cache { · 9 unmodified lines
17 }: StageOne,15 }: StageOne,
18 git_dir: &std::path::Path,16 git_dir: &std::path::Path,
19 branch_name: Option<&git_ref::FullNameRef>,17 branch_name: Option<&git_ref::FullNameRef>,
20 mut filter_config_section: fn(&git_config::file::Metadata) -> bool,18 filter_config_section: fn(&git_config::file::Metadata) -> bool,
21 git_install_dir: Option<&std::path::Path>,19 git_install_dir: Option<&std::path::Path>,
22 home: Option<&std::path::Path>,20 home: Option<&std::path::Path>,
23 repository::permissions::Environment {21 repository::permissions::Environment {
@@ -113,36 +111,14 @@ impl Cache { · 89 unmodified lines
113 globals111 globals
114 };112 };
115 113
116 let excludes_file = match config114 let hex_len = util::check_lenient(util::parse_core_abbrev(&config, object_hash), lenient_config)?;
117 .path_filter("core", None, "excludesFile", &mut filter_config_section)
118 .map(|p| p.interpolate(options.includes.interpolate).map(|p| p.into_owned()))
119 .transpose()
120 {
121 Ok(f) => f,
122 Err(_err) if lenient_config => None,
123 Err(err) => return Err(err.into()),
124 };
125
126 let hex_len = match util::parse_core_abbrev(&config, object_hash) {
127 Ok(v) => v,
128 Err(_err) if lenient_config => None,
129 Err(err) => return Err(err),
130 };
131 115
132 use util::config_bool;116 use util::config_bool;
133 let reflog = util::query_refupdates(&config);117 let reflog = util::query_refupdates(&config);
134 let ignore_case = config_bool(&config, "core.ignoreCase", false, lenient_config)?;118 let ignore_case = config_bool(&config, "core.ignoreCase", false, lenient_config)?;
135 let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true, lenient_config)?;119 let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true, lenient_config)?;
136 let object_kind_hint = config.string("core", None, "disambiguate").and_then(|value| {120 let object_kind_hint = util::disambiguate_hint(&config);
137 Some(match value.as_ref().as_ref() {121 // NOTE: When adding a new initial cache, consider adjusting `reread_values_and_clear_caches()` as well.
138 b"commit" => ObjectKindHint::Commit,
139 b"committish" => ObjectKindHint::Committish,
140 b"tree" => ObjectKindHint::Tree,
141 b"treeish" => ObjectKindHint::Treeish,
142 b"blob" => ObjectKindHint::Blob,
143 _ => return None,
144 })
145 });
146 Ok(Cache {122 Ok(Cache {
147 resolved: config.into(),123 resolved: config.into(),
148 use_multi_pack_index,124 use_multi_pack_index,
@@ -153,39 +129,41 @@ impl Cache { · 4 unmodified lines
153 ignore_case,129 ignore_case,
154 hex_len,130 hex_len,
155 filter_config_section,131 filter_config_section,
156 excludes_file,
157 xdg_config_home_env,132 xdg_config_home_env,
158 home_env,133 home_env,
134 lenient_config,
159 personas: Default::default(),135 personas: Default::default(),
160 url_rewrite: Default::default(),136 url_rewrite: Default::default(),
161 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]137 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
162 url_scheme: Default::default(),138 url_scheme: Default::default(),
163 git_prefix,139 git_prefix,
164 })140 })
165 }141 }
166 142
167 /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations.143 /// Call this with new `config` to update values and clear caches. Note that none of the values will be applied if a single
168 pub fn xdg_config_path(144 /// one is invalid.
169 &self,145 /// However, those that are lazily read won't be re-evaluated right away and might thus pass now but fail later.
170 resource_file_name: &str,
171 ) -> Result<Option<PathBuf>, git_sec::permission::Error<PathBuf>> {
172 std::env::var_os("XDG_CONFIG_HOME")
173 .map(|path| (path, &self.xdg_config_home_env))
174 .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env)))
175 .and_then(|(base, permission)| {
176 let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name);
177 permission.check(resource).transpose()
178 })
179 .transpose()
180 }
181
182 /// Return the home directory if we are allowed to read it and if it is set in the environment.
183 ///146 ///
184 /// We never fail for here even if the permission is set to deny as we `git-config` will fail later147 /// Note that we unconditionally re-read all values.
185 /// if it actually wants to use the home directory - we don't want to fail prematurely.148 pub fn reread_values_and_clear_caches(&mut self, config: crate::Config) -> Result<(), Error> {
186 pub fn home_dir(&self) -> Option<PathBuf> {149 let hex_len = util::check_lenient(util::parse_core_abbrev(&config, self.object_hash), self.lenient_config)?;
187 std::env::var_os("HOME")150
188 .map(PathBuf::from)151 use util::config_bool;
189 .and_then(|path| self.home_env.check_opt(path))152 let ignore_case = config_bool(&config, "core.ignoreCase", false, self.lenient_config)?;
153 let object_kind_hint = util::disambiguate_hint(&config);
154
155 self.personas = Default::default();
156 self.url_rewrite = Default::default();
157 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
158 {
159 self.url_scheme = Default::default();
160 }
161
162 self.resolved = config;
163 self.hex_len = hex_len;
164 self.ignore_case = ignore_case;
165 self.object_kind_hint = object_kind_hint;
166
167 Ok(())
190 }168 }
191}169}
git-repository/src/config/cache/mod.rs+1−22
@@ -1,5 +1,4 @@
1use super::{Cache, Error};1use super::{Cache, Error};
2use crate::{remote, repository::identity};
3 2
4mod incubate;3mod incubate;
5pub(crate) use incubate::StageOne;4pub(crate) use incubate::StageOne;
@@ -12,27 +11,7 @@ impl std::fmt::Debug for Cache { · 6 unmodified lines
12 }11 }
13}12}
14 13
15/// Access14mod access;
16impl Cache {
17 pub(crate) fn personas(&self) -> &identity::Personas {
18 self.personas
19 .get_or_init(|| identity::Personas::from_config_and_env(&self.resolved, self.git_prefix))
20 }
21
22 pub(crate) fn url_rewrite(&self) -> &remote::url::Rewrite {
23 self.url_rewrite
24 .get_or_init(|| remote::url::Rewrite::from_config(&self.resolved, self.filter_config_section))
25 }
26
27 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
28 pub(crate) fn url_scheme(
29 &self,
30 ) -> Result<&remote::url::SchemePermission, remote::url::scheme_permission::init::Error> {
31 self.url_scheme.get_or_try_init(|| {
32 remote::url::SchemePermission::from_config(&self.resolved, self.git_prefix, self.filter_config_section)
33 })
34 }
35}
36 15
37mod util;16mod util;
38pub(crate) use util::interpolate_context;17pub(crate) use util::interpolate_context;
git-repository/src/config/cache/util.rs+22−0
@@ -2,6 +2,7 @@ use std::convert::TryFrom; · 1 unmodified line
2 2
3use super::Error;3use super::Error;
4use crate::bstr::ByteSlice;4use crate::bstr::ByteSlice;
5use crate::revision::spec::parse::ObjectKindHint;
5 6
6pub(crate) fn interpolate_context<'a>(7pub(crate) fn interpolate_context<'a>(
7 git_install_dir: Option<&'a std::path::Path>,8 git_install_dir: Option<&'a std::path::Path>,
@@ -54,6 +55,14 @@ pub(crate) fn query_refupdates(config: &git_config::File<'static>) -> Option<git · 46 unmodified lines
54 })55 })
55}56}
56 57
58pub(crate) fn check_lenient<T, E>(v: Result<Option<T>, E>, lenient: bool) -> Result<Option<T>, E> {
59 match v {
60 Ok(v) => Ok(v),
61 Err(_) if lenient => Ok(None),
62 Err(err) => Err(err),
63 }
64}
65
57pub(crate) fn parse_core_abbrev(66pub(crate) fn parse_core_abbrev(
58 config: &git_config::File<'static>,67 config: &git_config::File<'static>,
59 object_hash: git_hash::Kind,68 object_hash: git_hash::Kind,
@@ -93,3 +102,16 @@ pub(crate) fn parse_core_abbrev( · 33 unmodified lines
93 None => Ok(None),102 None => Ok(None),
94 }103 }
95}104}
105
106pub(crate) fn disambiguate_hint(config: &git_config::File<'static>) -> Option<ObjectKindHint> {
107 config.string("core", None, "disambiguate").and_then(|value| {
108 Some(match value.as_ref().as_ref() {
109 b"commit" => ObjectKindHint::Commit,
110 b"committish" => ObjectKindHint::Committish,
111 b"tree" => ObjectKindHint::Tree,
112 b"treeish" => ObjectKindHint::Treeish,
113 b"blob" => ObjectKindHint::Blob,
114 _ => return None,
115 })
116 })
117}
git-repository/src/config/mod.rs+21−7
@@ -14,16 +14,26 @@ pub struct Snapshot<'repo> { · 13 unmodified lines
14 pub(crate) repo: &'repo Repository,14 pub(crate) repo: &'repo Repository,
15}15}
16 16
17/// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped.17/// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped
18/// as form of auto-commit.
18/// Note that the values will only affect this instance of the parent repository, and not other clones that may exist.19/// Note that the values will only affect this instance of the parent repository, and not other clones that may exist.
19///20///
20/// Note that these values won't update even if the underlying file(s) change.21/// Note that these values won't update even if the underlying file(s) change.
21// TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk.22///
23/// Use [`forget()`][Self::forget()] to not apply any of the changes.
24// TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk which should be the way
25// to affect all instances of a repo, probably via `config_mut()` and `config_mut_at()`.
22pub struct SnapshotMut<'repo> {26pub struct SnapshotMut<'repo> {
23 pub(crate) repo: &'repo mut Repository,27 pub(crate) repo: Option<&'repo mut Repository>,
24 pub(crate) config: git_config::File<'static>,28 pub(crate) config: git_config::File<'static>,
25}29}
26 30
31/// A utility structure created by [`SnapshotMut::commit_auto_rollback()`] that restores the previous configuration on drop.
32pub struct CommitAutoRollback<'repo> {
33 pub(crate) repo: Option<&'repo mut Repository>,
34 pub(crate) prev_config: crate::Config,
35}
36
27pub(crate) mod section {37pub(crate) mod section {
28 pub fn is_trusted(meta: &git_config::file::Metadata) -> bool {38 pub fn is_trusted(meta: &git_config::file::Metadata) -> bool {
29 meta.trust == git_sec::Trust::Full || meta.source.kind() != git_config::source::Kind::Repository39 meta.trust == git_sec::Trust::Full || meta.source.kind() != git_config::source::Kind::Repository
@@ -54,7 +64,10 @@ pub enum Error { · 24 unmodified lines
54 PathInterpolation(#[from] git_config::path::interpolate::Error),64 PathInterpolation(#[from] git_config::path::interpolate::Error),
55}65}
56 66
57/// Utility type to keep pre-obtained configuration values.67/// Utility type to keep pre-obtained configuration values, only for those required during initial setup
68/// and other basic operations that are common enough to warrant a permanent cache.
69///
70/// All other values are obtained lazily using OnceCell.
58#[derive(Clone)]71#[derive(Clone)]
59pub(crate) struct Cache {72pub(crate) struct Cache {
60 pub resolved: crate::Config,73 pub resolved: crate::Config,
@@ -73,7 +86,7 @@ pub(crate) struct Cache { · 12 unmodified lines
73 /// A lazily loaded rewrite list for remote urls86 /// A lazily loaded rewrite list for remote urls
74 pub url_rewrite: OnceCell<remote::url::Rewrite>,87 pub url_rewrite: OnceCell<remote::url::Rewrite>,
75 /// A lazily loaded mapping to know which url schemes to allow88 /// A lazily loaded mapping to know which url schemes to allow
76 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]89 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
77 pub url_scheme: OnceCell<remote::url::SchemePermission>,90 pub url_scheme: OnceCell<remote::url::SchemePermission>,
78 /// The config section filter from the options used to initialize this instance. Keep these in sync!91 /// The config section filter from the options used to initialize this instance. Keep these in sync!
79 filter_config_section: fn(&git_config::file::Metadata) -> bool,92 filter_config_section: fn(&git_config::file::Metadata) -> bool,
@@ -81,8 +94,9 @@ pub(crate) struct Cache { · 1 unmodified line
81 pub object_kind_hint: Option<spec::parse::ObjectKindHint>,94 pub object_kind_hint: Option<spec::parse::ObjectKindHint>,
82 /// If true, we are on a case-insensitive file system.95 /// If true, we are on a case-insensitive file system.
83 pub ignore_case: bool,96 pub ignore_case: bool,
84 /// The path to the user-level excludes file to ignore certain files in the worktree.97 /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient.
85 pub excludes_file: Option<std::path::PathBuf>,98 /// Also available in options! Keep in sync!
99 pub lenient_config: bool,
86 /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable.100 /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable.
87 xdg_config_home_env: git_sec::Permission,101 xdg_config_home_env: git_sec::Permission,
88 /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable.102 /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable.
git-repository/src/config/snapshot/_impls.rs+62−0
@@ -0,0 +1,62 @@
1use std::{
2 fmt::{Debug, Formatter},
3 ops::{Deref, DerefMut},
4};
5
6use crate::config::{CommitAutoRollback, Snapshot, SnapshotMut};
7
8impl Debug for Snapshot<'_> {
9 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
10 f.write_str(&self.repo.config.resolved.to_string())
11 }
12}
13
14impl Debug for CommitAutoRollback<'_> {
15 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
16 f.write_str(&self.repo.as_ref().expect("still present").config.resolved.to_string())
17 }
18}
19
20impl Debug for SnapshotMut<'_> {
21 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
22 f.write_str(&self.config.to_string())
23 }
24}
25
26impl Drop for SnapshotMut<'_> {
27 fn drop(&mut self) {
28 if let Some(repo) = self.repo.take() {
29 self.commit_inner(repo).ok();
30 };
31 }
32}
33
34impl Drop for CommitAutoRollback<'_> {
35 fn drop(&mut self) {
36 if let Some(repo) = self.repo.take() {
37 self.rollback_inner(repo).ok();
38 }
39 }
40}
41
42impl Deref for SnapshotMut<'_> {
43 type Target = git_config::File<'static>;
44
45 fn deref(&self) -> &Self::Target {
46 &self.config
47 }
48}
49
50impl Deref for CommitAutoRollback<'_> {
51 type Target = crate::Repository;
52
53 fn deref(&self) -> &Self::Target {
54 self.repo.as_ref().expect("always present")
55 }
56}
57
58impl DerefMut for SnapshotMut<'_> {
59 fn deref_mut(&mut self) -> &mut Self::Target {
60 &mut self.config
61 }
62}
git-repository/src/config/snapshot/access.rs+58−0
@@ -1,5 +1,7 @@
1use git_features::threading::OwnShared;
1use std::borrow::Cow;2use std::borrow::Cow;
2 3
4use crate::config::{CommitAutoRollback, SnapshotMut};
3use crate::{5use crate::{
4 bstr::BStr,6 bstr::BStr,
5 config::{cache::interpolate_context, Snapshot},7 config::{cache::interpolate_context, Snapshot},
@@ -90,3 +92,59 @@ impl<'repo> Snapshot<'repo> { · 84 unmodified lines
90 &self.repo.config.resolved92 &self.repo.config.resolved
91 }93 }
92}94}
95
96/// Utilities
97impl<'repo> SnapshotMut<'repo> {
98 /// Apply all changes made to this instance.
99 ///
100 /// Note that this would also happen once this instance is dropped, but using this method may be more intuitive and won't squelch errors
101 /// in case the new configuration is partially invalid.
102 pub fn commit(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> {
103 let repo = self.repo.take().expect("always present here");
104 self.commit_inner(repo)
105 }
106
107 pub(crate) fn commit_inner(
108 &mut self,
109 repo: &'repo mut crate::Repository,
110 ) -> Result<&'repo mut crate::Repository, crate::config::Error> {
111 repo.config
112 .reread_values_and_clear_caches(std::mem::take(&mut self.config).into())?;
113 Ok(repo)
114 }
115
116 /// Create a structure the temporarily commits the changes, but rolls them back when dropped.
117 pub fn commit_auto_rollback(mut self) -> Result<CommitAutoRollback<'repo>, crate::config::Error> {
118 let repo = self.repo.take().expect("this only runs once on consumption");
119 let prev_config = OwnShared::clone(&repo.config.resolved);
120
121 Ok(CommitAutoRollback {
122 repo: self.commit_inner(repo)?.into(),
123 prev_config,
124 })
125 }
126
127 /// Don't apply any of the changes after consuming this instance, effectively forgetting them, returning the changed configuration.
128 pub fn forget(mut self) -> git_config::File<'static> {
129 self.repo.take();
130 std::mem::take(&mut self.config)
131 }
132}
133
134/// Utilities
135impl<'repo> CommitAutoRollback<'repo> {
136 /// Rollback the changes previously applied and all values before the change.
137 pub fn rollback(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> {
138 let repo = self.repo.take().expect("still present, consumed only once");
139 self.rollback_inner(repo)
140 }
141
142 pub(crate) fn rollback_inner(
143 &mut self,
144 repo: &'repo mut crate::Repository,
145 ) -> Result<&'repo mut crate::Repository, crate::config::Error> {
146 repo.config
147 .reread_values_and_clear_caches(OwnShared::clone(&self.prev_config))?;
148 Ok(repo)
149 }
150}
git-repository/src/config/snapshot/apply_cli_overrides.rs+5−2
@@ -23,7 +23,10 @@ pub enum Error { · 22 unmodified lines
23impl SnapshotMut<'_> {23impl SnapshotMut<'_> {
24 /// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true`24 /// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true`
25 /// to the repository configuration, marked with [source CLI][git_config::Source::Cli].25 /// to the repository configuration, marked with [source CLI][git_config::Source::Cli].
26 pub fn apply_cli_overrides(&mut self, values: impl IntoIterator<Item = impl AsRef<BStr>>) -> Result<(), Error> {26 pub fn apply_cli_overrides(
27 &mut self,
28 values: impl IntoIterator<Item = impl AsRef<BStr>>,
29 ) -> Result<&mut Self, Error> {
27 let mut file = git_config::File::new(git_config::file::Metadata::from(git_config::Source::Cli));30 let mut file = git_config::File::new(git_config::file::Metadata::from(git_config::Source::Cli));
28 for key_value in values {31 for key_value in values {
29 let key_value = key_value.as_ref();32 let key_value = key_value.as_ref();
@@ -44,6 +47,6 @@ impl SnapshotMut<'_> { · 14 unmodified lines
44 );47 );
45 }48 }
46 self.config.append(file);49 self.config.append(file);
47 Ok(())50 Ok(self)
48 }51 }
49}52}
git-repository/src/config/snapshot/mod.rs+1−41
@@ -1,3 +1,4 @@
1mod _impls;
1mod access;2mod access;
2 3
3///4///
@@ -5,44 +6,3 @@ pub mod apply_cli_overrides; · 1 unmodified line
5 6
6///7///
7pub mod credential_helpers;8pub mod credential_helpers;
8
9mod _impls {
10 use std::{
11 fmt::{Debug, Formatter},
12 ops::{Deref, DerefMut},
13 };
14
15 use crate::config::{Snapshot, SnapshotMut};
16
17 impl Debug for Snapshot<'_> {
18 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
19 f.write_str(&self.repo.config.resolved.to_string())
20 }
21 }
22
23 impl Debug for SnapshotMut<'_> {
24 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25 f.write_str(&self.config.to_string())
26 }
27 }
28
29 impl Drop for SnapshotMut<'_> {
30 fn drop(&mut self) {
31 self.repo.config.resolved = std::mem::take(&mut self.config).into();
32 }
33 }
34
35 impl Deref for SnapshotMut<'_> {
36 type Target = git_config::File<'static>;
37
38 fn deref(&self) -> &Self::Target {
39 &self.config
40 }
41 }
42
43 impl DerefMut for SnapshotMut<'_> {
44 fn deref_mut(&mut self) -> &mut Self::Target {
45 &mut self.config
46 }
47 }
48}
git-repository/src/head/mod.rs+8−5
@@ -87,19 +87,22 @@ mod remote { · 86 unmodified lines
87 /// Remote87 /// Remote
88 impl<'repo> Head<'repo> {88 impl<'repo> Head<'repo> {
89 /// Return the remote with which the currently checked our reference can be handled as configured by `branch.<name>.remote|pushRemote`89 /// Return the remote with which the currently checked our reference can be handled as configured by `branch.<name>.remote|pushRemote`
90 /// or fall back to the non-branch specific remote configuration. `None` is returned if the head is detached or unborn.90 /// or fall back to the non-branch specific remote configuration. `None` is returned if the head is detached or unborn, so there is
91 /// no branch specific remote.
91 ///92 ///
92 /// This is equivalent to calling [`Reference::remote(…)`][crate::Reference::remote()] and93 /// This is equivalent to calling [`Reference::remote(…)`][crate::Reference::remote()] and
93 /// [`Repository::remote_default_name()`][crate::Repository::remote_default_name()] in order.94 /// [`Repository::remote_default_name()`][crate::Repository::remote_default_name()] in order.
95 ///
96 /// Combine it with [`find_default_remote()`][crate::Repository::find_default_remote()] as fallback to handle detached heads,
97 /// i.e. obtain a remote even in case of detached heads.
94 pub fn into_remote(98 pub fn into_remote(
95 self,99 self,
96 direction: remote::Direction,100 direction: remote::Direction,
97 ) -> Option<Result<Remote<'repo>, remote::find::existing::Error>> {101 ) -> Option<Result<Remote<'repo>, remote::find::existing::Error>> {
98 let repo = self.repo;102 let repo = self.repo;
99 self.try_into_referent()?.remote(direction).or_else(|| {103 self.try_into_referent()?
100 repo.remote_default_name(direction)104 .remote(direction)
101 .map(|name| repo.find_remote(name.as_ref()))105 .or_else(|| repo.find_default_remote(direction))
102 })
103 }106 }
104 }107 }
105}108}
git-repository/src/open.rs+1−1
@@ -201,7 +201,7 @@ impl Options { · 200 unmodified lines
201 /// If set, default is false, invalid configuration values will cause an error even if these can safely be defaulted.201 /// If set, default is false, invalid configuration values will cause an error even if these can safely be defaulted.
202 ///202 ///
203 /// This is recommended for all applications that prefer correctness over usability.203 /// This is recommended for all applications that prefer correctness over usability.
204 /// `git` itself by defaults to strict configuration mode to let you know if configuration is incorrect.204 /// `git` itself defaults to strict configuration mode, flagging incorrect configuration immediately.
205 pub fn strict_config(mut self, toggle: bool) -> Self {205 pub fn strict_config(mut self, toggle: bool) -> Self {
206 self.lenient_config = !toggle;206 self.lenient_config = !toggle;
207 self207 self
git-repository/src/reference/edits.rs+5−20
@@ -55,35 +55,20 @@ pub mod delete { · 54 unmodified lines
55 55
56 use crate::Reference;56 use crate::Reference;
57 57
58 mod error {
59 /// The error returned by [`Reference::delete()`][super::Reference::delete()].
60 #[derive(Debug, thiserror::Error)]
61 #[allow(missing_docs)]
62 pub enum Error {
63 #[error(transparent)]
64 ReferenceEdit(#[from] crate::reference::edit::Error),
65 }
66 }
67 pub use error::Error;
68 use git_lock::acquire::Fail;
69
70 impl<'repo> Reference<'repo> {58 impl<'repo> Reference<'repo> {
71 /// Delete this reference or fail if it was changed since last observed.59 /// Delete this reference or fail if it was changed since last observed.
72 /// Note that this instance remains available in memory but probably shouldn't be used anymore.60 /// Note that this instance remains available in memory but probably shouldn't be used anymore.
73 pub fn delete(&self) -> Result<(), Error> {61 pub fn delete(&self) -> Result<(), crate::reference::edit::Error> {
74 self.repo.edit_reference(62 self.repo
75 RefEdit {63 .edit_reference(RefEdit {
76 change: Change::Delete {64 change: Change::Delete {
77 expected: PreviousValue::MustExistAndMatch(self.inner.target.clone()),65 expected: PreviousValue::MustExistAndMatch(self.inner.target.clone()),
78 log: RefLog::AndReference,66 log: RefLog::AndReference,
79 },67 },
80 name: self.inner.name.clone(),68 name: self.inner.name.clone(),
81 deref: false,69 deref: false,
82 },70 })
83 Fail::Immediately,71 .map(|_| ())
84 self.repo.committer_or_default(),
85 )?;
86 Ok(())
87 }72 }
88 }73 }
89}74}
git-repository/src/reference/errors.rs+2−0
@@ -11,6 +11,8 @@ pub mod edit { · 10 unmodified lines
11 FileTransactionCommit(#[from] git_ref::file::transaction::commit::Error),11 FileTransactionCommit(#[from] git_ref::file::transaction::commit::Error),
12 #[error(transparent)]12 #[error(transparent)]
13 NameValidation(#[from] git_validate::reference::name::Error),13 NameValidation(#[from] git_validate::reference::name::Error),
14 #[error("Could not interpret core.filesRefLockTimeout or core.packedRefsTimeout, it must be the number in milliseconds to wait for locks or negative to wait forever")]
15 LockTimeoutConfiguration(#[from] git_config::value::Error),
14 }16 }
15}17}
16 18
git-repository/src/reference/remote.rs+2−0
@@ -72,6 +72,8 @@ impl<'repo> Reference<'repo> { · 71 unmodified lines
72 /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like72 /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like
73 ///73 ///
74 /// - `branch.<name>.merge` to know which branch on the remote side corresponds to this one for merging when pulling.74 /// - `branch.<name>.merge` to know which branch on the remote side corresponds to this one for merging when pulling.
75 ///
76 /// It also handles if the remote is a configured URL, which has no name.
75 pub fn remote(77 pub fn remote(
76 &self,78 &self,
77 direction: remote::Direction,79 direction: remote::Direction,
git-repository/src/remote/connect.rs+21−9
@@ -1,8 +1,6 @@
1use git_protocol::transport::client::Transport;
2
3use crate::{remote::Connection, Progress, Remote};1use crate::{remote::Connection, Progress, Remote};
2use git_protocol::transport::client::Transport;
4 3
5#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
6mod error {4mod error {
7 use crate::{bstr::BString, remote};5 use crate::{bstr::BString, remote};
8 6
@@ -24,10 +22,9 @@ mod error { · 15 unmodified lines
24 FileUrl(#[from] git_discover::is_git::Error),22 FileUrl(#[from] git_discover::is_git::Error),
25 }23 }
26}24}
27#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
28pub use error::Error;25pub use error::Error;
29 26
30/// Establishing connections to remote hosts27/// Establishing connections to remote hosts (without performing a git-handshake).
31impl<'repo> Remote<'repo> {28impl<'repo> Remote<'repo> {
32 /// Create a new connection using `transport` to communicate, with `progress` to indicate changes.29 /// Create a new connection using `transport` to communicate, with `progress` to indicate changes.
33 ///30 ///
@@ -40,7 +37,7 @@ impl<'repo> Remote<'repo> { · 6 unmodified lines
40 {37 {
41 Connection {38 Connection {
42 remote: self,39 remote: self,
43 credentials: None,40 authenticate: None,
44 transport,41 transport,
45 progress,42 progress,
46 }43 }
@@ -50,6 +47,10 @@ impl<'repo> Remote<'repo> { · 3 unmodified lines
50 ///47 ///
51 /// Note that the `protocol.version` configuration key affects the transport protocol used to connect,48 /// Note that the `protocol.version` configuration key affects the transport protocol used to connect,
52 /// with `2` being the default.49 /// with `2` being the default.
50 ///
51 /// The transport used for connection can be configured via `transport_mut().configure()` assuming the actually
52 /// used transport is well known. If that's not the case, the transport can be created by hand and passed to
53 /// [to_connection_with_transport()][Self::to_connection_with_transport()].
53 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]54 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
54 #[git_protocol::maybe_async::maybe_async]55 #[git_protocol::maybe_async::maybe_async]
55 pub async fn connect<P>(56 pub async fn connect<P>(
@@ -60,7 +61,18 @@ impl<'repo> Remote<'repo> { · 4 unmodified lines
60 where61 where
61 P: Progress,62 P: Progress,
62 {63 {
63 use git_protocol::transport::Protocol;64 let (url, version) = self.sanitized_url_and_version(direction)?;
65 let transport = git_protocol::transport::connect(url, version).await?;
66 Ok(self.to_connection_with_transport(transport, progress))
67 }
68
69 /// Produce the sanitized URL and protocol version to use as obtained by querying the repository configuration.
70 ///
71 /// This can be useful when using custom transports to allow additional configuration.
72 pub fn sanitized_url_and_version(
73 &self,
74 direction: crate::remote::Direction,
75 ) -> Result<(git_url::Url, git_protocol::transport::Protocol), Error> {
64 fn sanitize(mut url: git_url::Url) -> Result<git_url::Url, Error> {76 fn sanitize(mut url: git_url::Url) -> Result<git_url::Url, Error> {
65 if url.scheme == git_url::Scheme::File {77 if url.scheme == git_url::Scheme::File {
66 let mut dir = git_path::from_bstr(url.path.as_ref());78 let mut dir = git_path::from_bstr(url.path.as_ref());
@@ -75,6 +87,7 @@ impl<'repo> Remote<'repo> { · 8 unmodified lines
75 Ok(url)87 Ok(url)
76 }88 }
77 89
90 use git_protocol::transport::Protocol;
78 let version = self91 let version = self
79 .repo92 .repo
80 .config93 .config
@@ -101,7 +114,6 @@ impl<'repo> Remote<'repo> { · 20 unmodified lines
101 scheme: url.scheme,114 scheme: url.scheme,
102 });115 });
103 }116 }
104 let transport = git_protocol::transport::connect(sanitize(url)?, version).await?;117 Ok((sanitize(url)?, version))
105 Ok(self.to_connection_with_transport(transport, progress))
106 }118 }
107}119}
git-repository/src/remote/connection/access.rs+48−0
@@ -0,0 +1,48 @@
1use crate::{
2 remote::{connection::AuthenticateFn, Connection},
3 Remote,
4};
5
6/// Builder
7impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
8 /// Set a custom credentials callback to provide credentials if the remotes require authentication.
9 ///
10 /// Otherwise we will use the git configuration to perform the same task as the `git credential` helper program,
11 /// which is calling other helper programs in succession while resorting to a prompt to obtain credentials from the
12 /// user.
13 ///
14 /// A custom function may also be used to prevent accessing resources with authentication.
15 pub fn with_credentials(
16 mut self,
17 helper: impl FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a,
18 ) -> Self {
19 self.authenticate = Some(Box::new(helper));
20 self
21 }
22}
23
24/// Access
25impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
26 /// A utility to return a function that will use this repository's configuration to obtain credentials, similar to
27 /// what `git credential` is doing.
28 ///
29 /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the
30 /// default way of handling credentials, which they can call as fallback.
31 pub fn configured_credentials(
32 &self,
33 url: git_url::Url,
34 ) -> Result<AuthenticateFn<'a>, crate::config::credential_helpers::Error> {
35 let (mut cascade, _action_with_normalized_url, prompt_opts) =
36 self.remote.repo.config_snapshot().credential_helpers(url)?;
37 Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as AuthenticateFn<'_>)
38 }
39 /// Return the underlying remote that instantiate this connection.
40 pub fn remote(&self) -> &Remote<'repo> {
41 self.remote
42 }
43
44 /// Provide a mutable transport to allow configuring it with [`configure()`][git_protocol::transport::client::TransportWithoutIO::configure()]
45 pub fn transport_mut(&mut self) -> &mut T {
46 &mut self.transport
47 }
48}
git-repository/src/remote/connection/fetch/config.rs+66−0
@@ -0,0 +1,66 @@
1use super::Error;
2use crate::Repository;
3use std::convert::TryInto;
4
5pub fn index_threads(repo: &Repository) -> Result<Option<usize>, Error> {
6 let lenient_config = repo.options.lenient_config;
7 let message = "The configured pack.threads is invalid. It must be 0 or greater, with 0 turning it off";
8 Ok(
9 match repo
10 .config
11 .resolved
12 .integer_filter("pack", None, "threads", &mut repo.filter_config_section())
13 .transpose()
14 {
15 Ok(Some(0)) => Some(1),
16 Ok(Some(n)) => match n.try_into() {
17 Ok(n) => Some(n),
18 Err(_) if lenient_config => None,
19 Err(_) => {
20 return Err(Error::Configuration {
21 message: "The value for pack.threads is out of range",
22 desired: n.into(),
23 source: None,
24 })
25 }
26 },
27 Ok(None) => None,
28 Err(_) if lenient_config => None,
29 Err(err) => {
30 return Err(Error::Configuration {
31 message,
32 desired: None,
33 source: err.into(),
34 })
35 }
36 },
37 )
38}
39
40pub fn pack_index_version(repo: &Repository) -> Result<git_pack::index::Version, Error> {
41 use git_pack::index::Version;
42 let lenient_config = repo.options.lenient_config;
43 let message = "The configured pack.indexVersion is invalid. It must be 1 or 2, with 2 being the default";
44 Ok(
45 match repo.config.resolved.integer("pack", None, "indexVersion").transpose() {
46 Ok(Some(v)) if v == 1 => Version::V1,
47 Ok(Some(v)) if v == 2 => Version::V2,
48 Ok(None) => Version::V2,
49 Ok(Some(_)) | Err(_) if lenient_config => Version::V2,
50 Ok(Some(v)) => {
51 return Err(Error::Configuration {
52 message,
53 desired: v.into(),
54 source: None,
55 })
56 }
57 Err(err) => {
58 return Err(Error::Configuration {
59 message,
60 desired: None,
61 source: err.into(),
62 })
63 }
64 },
65 )
66}
git-repository/src/remote/connection/fetch/mod.rs+263−0
@@ -0,0 +1,263 @@
1use crate::remote::fetch::{DryRun, RefMap};
2use crate::remote::{fetch, ref_map, Connection};
3use crate::Progress;
4use git_odb::FindExt;
5use git_protocol::transport::client::Transport;
6use std::sync::atomic::AtomicBool;
7
8mod error {
9 /// The error returned by [`receive()`](super::Prepare::receive()).
10 #[derive(Debug, thiserror::Error)]
11 #[allow(missing_docs)]
12 pub enum Error {
13 #[error("{message}{}", desired.map(|n| format!(" (got {})", n)).unwrap_or_default())]
14 Configuration {
15 message: &'static str,
16 desired: Option<i64>,
17 source: Option<git_config::value::Error>,
18 },
19 #[error(transparent)]
20 FetchResponse(#[from] git_protocol::fetch::response::Error),
21 #[error(transparent)]
22 Negotiate(#[from] super::negotiate::Error),
23 #[error(transparent)]
24 Client(#[from] git_protocol::transport::client::Error),
25 #[error(transparent)]
26 WritePack(#[from] git_pack::bundle::write::Error),
27 #[error(transparent)]
28 UpdateRefs(#[from] super::refs::update::Error),
29 }
30}
31pub use error::Error;
32
33/// The status of the repository after the fetch operation
34#[derive(Debug, Clone)]
35pub enum Status {
36 /// Nothing changed as the remote didn't have anything new.
37 NoChange,
38 /// There was at least one tip with a new object which we received.
39 Change {
40 /// Information collected while writing the pack and its index.
41 write_pack_bundle: git_pack::bundle::write::Outcome,
42 /// Information collected while updating references.
43 update_refs: refs::update::Outcome,
44 },
45 /// A dry run was performed which leaves the local repository without any change
46 /// nor will a pack have been received.
47 DryRun {
48 /// Information about what updates to refs would have been done.
49 update_refs: refs::update::Outcome,
50 },
51}
52
53/// The outcome of receiving a pack via [`Prepare::receive()`].
54#[derive(Debug, Clone)]
55pub struct Outcome<'spec> {
56 /// The result of the initial mapping of references, the prerequisite for any fetch.
57 pub ref_map: RefMap<'spec>,
58 /// The status of the operation to indicate what happened.
59 pub status: Status,
60}
61
62///
63pub mod negotiate;
64
65impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P>
66where
67 T: Transport,
68 P: Progress,
69{
70 /// Perform a handshake with the remote and obtain a ref-map with `options`, and from there one
71 /// Note that at this point, the `transport` should already be configured using the [`transport_mut()`][Self::transport_mut()]
72 /// method, as it will be consumed here.
73 ///
74 /// From there additional properties of the fetch can be adjusted to override the defaults that are configured via git-config.
75 ///
76 /// # Blocking Only
77 ///
78 /// Note that this implementation is currently limited to blocking mode as it relies on Drop semantics to close the connection
79 /// should the fetch not be performed. Furthermore, there the code doing the fetch is inherently blocking so there is no benefit.
80 /// It's best to unblock it by placing it into its own thread or offload it should usage in an async context be required.
81 pub fn prepare_fetch(mut self, options: ref_map::Options) -> Result<Prepare<'remote, 'repo, T, P>, ref_map::Error> {
82 let ref_map = self.ref_map_inner(options)?;
83 Ok(Prepare {
84 con: Some(self),
85 ref_map,
86 dry_run: fetch::DryRun::No,
87 })
88 }
89}
90
91impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>
92where
93 T: Transport,
94 P: Progress,
95{
96 /// Receive the pack and perform the operation as configured by git via `git-config` or overridden by various builder methods.
97 /// Return `Ok(None)` if there was nothing to do because all remote refs are at the same state as they are locally, or `Ok(Some(outcome))`
98 /// to inform about all the changes that were made.
99 ///
100 /// ### Negotiation
101 ///
102 /// "fetch.negotiationAlgorithm" describes algorithms `git` uses currently, with the default being `consecutive` and `skipping` being
103 /// experimented with. We currently implement something we could call 'naive' which works for now.
104 pub fn receive(mut self, should_interrupt: &AtomicBool) -> Result<Outcome<'remote>, Error> {
105 let mut con = self.con.take().expect("receive() can only be called once");
106
107 let handshake = &self.ref_map.handshake;
108 let protocol_version = handshake.server_protocol_version;
109
110 let fetch = git_protocol::fetch::Command::Fetch;
111 let fetch_features = fetch.default_features(protocol_version, &handshake.capabilities);
112
113 git_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?;
114 let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
115 let mut arguments = git_protocol::fetch::Arguments::new(protocol_version, fetch_features);
116 let mut previous_response = None::<git_protocol::fetch::Response>;
117 let mut round = 1;
118 let progress = &mut con.progress;
119 let repo = con.remote.repo;
120
121 let reader = 'negotiation: loop {
122 progress.step();
123 progress.set_name(format!("negotiate (round {})", round));
124 let is_done = match negotiate::one_round(
125 negotiate::Algorithm::Naive,
126 round,
127 repo,
128 &self.ref_map,
129 &mut arguments,
130 previous_response.as_ref(),
131 ) {
132 Ok(_) if arguments.is_empty() => {
133 git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
134 return Ok(Outcome {
135 ref_map: std::mem::take(&mut self.ref_map),
136 status: Status::NoChange,
137 });
138 }
139 Ok(is_done) => is_done,
140 Err(err) => {
141 git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
142 return Err(err.into());
143 }
144 };
145 round += 1;
146 let mut reader = arguments.send(&mut con.transport, is_done)?;
147 if sideband_all {
148 setup_remote_progress(progress, &mut reader);
149 }
150 let response = git_protocol::fetch::Response::from_line_reader(protocol_version, &mut reader)?;
151 if response.has_pack() {
152 progress.step();
153 progress.set_name("receiving pack");
154 if !sideband_all {
155 setup_remote_progress(progress, &mut reader);
156 }
157 break 'negotiation reader;
158 } else {
159 previous_response = Some(response);
160 }
161 };
162
163 let options = git_pack::bundle::write::Options {
164 thread_limit: config::index_threads(repo)?,
165 index_version: config::pack_index_version(repo)?,
166 iteration_mode: git_pack::data::input::Mode::Verify,
167 object_hash: con.remote.repo.object_hash(),
168 };
169
170 let write_pack_bundle = if matches!(self.dry_run, fetch::DryRun::No) {
171 Some(git_pack::Bundle::write_to_directory(
172 reader,
173 Some(repo.objects.store_ref().path().join("pack")),
174 con.progress,
175 should_interrupt,
176 Some(Box::new({
177 let repo = repo.clone();
178 move |oid, buf| repo.objects.find(oid, buf).ok()
179 })),
180 options,
181 )?)
182 } else {
183 drop(reader);
184 None
185 };
186
187 if matches!(protocol_version, git_protocol::transport::Protocol::V2) {
188 git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
189 }
190
191 let update_refs = refs::update(
192 repo,
193 "fetch",
194 &self.ref_map.mappings,
195 con.remote.refspecs(crate::remote::Direction::Fetch),
196 self.dry_run,
197 )?;
198
199 Ok(Outcome {
200 ref_map: std::mem::take(&mut self.ref_map),
201 status: match write_pack_bundle {
202 Some(write_pack_bundle) => Status::Change {
203 write_pack_bundle,
204 update_refs,
205 },
206 None => Status::DryRun { update_refs },
207 },
208 })
209 }
210}
211
212fn setup_remote_progress(
213 progress: &mut impl Progress,
214 reader: &mut Box<dyn git_protocol::transport::client::ExtendedBufRead + Unpin + '_>,
215) {
216 use git_protocol::transport::client::ExtendedBufRead;
217 reader.set_progress_handler(Some(Box::new({
218 let mut remote_progress = progress.add_child("remote");
219 move |is_err: bool, data: &[u8]| {
220 git_protocol::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress)
221 }
222 }) as git_protocol::transport::client::HandleProgress));
223}
224
225mod config;
226///
227#[path = "update_refs/mod.rs"]
228pub mod refs;
229
230/// A structure to hold the result of the handshake with the remote and configure the upcoming fetch operation.
231pub struct Prepare<'remote, 'repo, T, P>
232where
233 T: Transport,
234{
235 con: Option<Connection<'remote, 'repo, T, P>>,
236 ref_map: RefMap<'remote>,
237 dry_run: fetch::DryRun,
238}
239
240/// Builder
241impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>
242where
243 T: Transport,
244{
245 /// If dry run is enabled, no change to the repository will be made.
246 ///
247 /// This works by not actually fetching the pack after negotiating it, nor will refs be updated.
248 pub fn with_dry_run(mut self, enabled: bool) -> Self {
249 self.dry_run = enabled.then(|| fetch::DryRun::Yes).unwrap_or(DryRun::No);
250 self
251 }
252}
253
254impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P>
255where
256 T: Transport,
257{
258 fn drop(&mut self) {
259 if let Some(mut con) = self.con.take() {
260 git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
261 }
262 }
263}
git-repository/src/remote/connection/fetch/negotiate.rs+60−0
@@ -0,0 +1,60 @@
1/// The way the negotiation is performed
2#[derive(Copy, Clone)]
3pub(crate) enum Algorithm {
4 /// Our very own implementation that probably should be replaced by one of the known algorithms soon.
5 Naive,
6}
7
8/// The error returned during negotiation.
9#[derive(Debug, thiserror::Error)]
10#[allow(missing_docs)]
11pub enum Error {
12 #[error("We were unable to figure out what objects the server should send after {rounds} round(s)")]
13 NegotiationFailed { rounds: usize },
14}
15
16/// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants.
17/// If this is not the first round, the `previous_response` is set with the last recorded server response.
18/// Returns `true` if the negotiation is done from our side so the server won't keep asking.
19pub(crate) fn one_round(
20 algo: Algorithm,
21 round: usize,
22 repo: &crate::Repository,
23 ref_map: &crate::remote::fetch::RefMap<'_>,
24 arguments: &mut git_protocol::fetch::Arguments,
25 _previous_response: Option<&git_protocol::fetch::Response>,
26) -> Result<bool, Error> {
27 match algo {
28 Algorithm::Naive => {
29 assert_eq!(round, 1, "Naive always finishes after the first round, and claims.");
30 let mut has_missing_tracking_branch = false;
31 for mapping in &ref_map.mappings {
32 let have_id = mapping.local.as_ref().and_then(|name| {
33 repo.find_reference(name)
34 .ok()
35 .and_then(|r| r.target().try_id().map(ToOwned::to_owned))
36 });
37 match have_id {
38 Some(have_id) if mapping.remote.as_id() != have_id => {
39 arguments.want(mapping.remote.as_id());
40 arguments.have(have_id);
41 }
42 Some(_) => {}
43 None => {
44 arguments.want(mapping.remote.as_id());
45 has_missing_tracking_branch = true;
46 }
47 }
48 }
49
50 if has_missing_tracking_branch {
51 if let Ok(Some(r)) = repo.head_ref() {
52 if let Some(id) = r.target().try_id() {
53 arguments.have(id);
54 }
55 }
56 }
57 Ok(true)
58 }
59 }
60}
git-repository/src/remote/connection/fetch/update_refs/mod.rs+149−0
@@ -0,0 +1,149 @@
1use crate::remote::fetch;
2use crate::remote::fetch::refs::update::Mode;
3use crate::Repository;
4use git_pack::Find;
5use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog};
6use git_ref::{Target, TargetRef};
7use std::collections::BTreeMap;
8use std::convert::TryInto;
9use std::path::PathBuf;
10
11///
12pub mod update;
13
14/// Information about the update of a single reference, corresponding the respective entry in [`RefMap::mappings`][crate::remote::fetch::RefMap::mappings].
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Update {
17 /// The way the update was performed.
18 pub mode: update::Mode,
19 /// The index to the edit that was created from the corresponding mapping, or `None` if there was no local ref.
20 pub edit_index: Option<usize>,
21}
22
23impl From<update::Mode> for Update {
24 fn from(mode: Mode) -> Self {
25 Update { mode, edit_index: None }
26 }
27}
28
29/// Update all refs as derived from `mappings` and produce an `Outcome` informing about all applied changes in detail, with each
30/// [`update`][Update] corresponding to the [`fetch::Mapping`] of at the same index.
31/// If `dry_run` is true, ref transactions won't actually be applied, but are assumed to work without error so the underlying
32/// `repo` is not actually changed. Also it won't perform an 'object exists' check as these are likely not to exist as the pack
33/// wasn't fetched either.
34/// `action` is the prefix used for reflog entries, and is typically "fetch".
35///
36/// It can be used to produce typical information that one is used to from `git fetch`.
37pub(crate) fn update(
38 repo: &Repository,
39 action: &str,
40 mappings: &[fetch::Mapping],
41 refspecs: &[git_refspec::RefSpec],
42 dry_run: fetch::DryRun,
43) -> Result<update::Outcome, update::Error> {
44 let mut edits = Vec::new();
45 let mut updates = Vec::new();
46
47 for fetch::Mapping {
48 remote,
49 local,
50 spec_index,
51 } in mappings
52 {
53 let remote_id = remote.as_id();
54 if dry_run == fetch::DryRun::No && !repo.objects.contains(remote_id) {
55 updates.push(update::Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into());
56 continue;
57 }
58 let checked_out_branches = worktree_branches(repo)?;
59 let (mode, edit_index) = match local {
60 Some(name) => {
61 let (mode, reflog_message, name) = match repo.try_find_reference(name)? {
62 Some(existing) => {
63 if let Some(wt_dir) = checked_out_branches.get(existing.name()) {
64 let mode = update::Mode::RejectedCurrentlyCheckedOut {
65 worktree_dir: wt_dir.to_owned(),
66 };
67 updates.push(mode.into());
68 continue;
69 }
70 match existing.target() {
71 TargetRef::Symbolic(_) => {
72 updates.push(update::Mode::RejectedSymbolic.into());
73 continue;
74 }
75 TargetRef::Peeled(local_id) => {
76 let (mode, reflog_message) = if local_id == remote_id {
77 (update::Mode::NoChangeNeeded, "no update will be performed")
78 } else if refspecs[*spec_index].allow_non_fast_forward() {
79 let reflog_msg = match existing.name().category() {
80 Some(git_ref::Category::Tag) => "updating tag",
81 _ => "forced-update",
82 };
83 (update::Mode::Forced, reflog_msg)
84 } else if let Some(git_ref::Category::Tag) = existing.name().category() {
85 updates.push(update::Mode::RejectedTagUpdate.into());
86 continue;
87 } else {
88 todo!("check for fast-forward (is local an ancestor of remote?)")
89 };
90 (mode, reflog_message, existing.name().to_owned())
91 }
92 }
93 }
94 None => {
95 let name: git_ref::FullName = name.try_into()?;
96 let reflog_msg = match name.category() {
97 Some(git_ref::Category::Tag) => "storing tag",
98 Some(git_ref::Category::LocalBranch) => "storing head",
99 _ => "storing ref",
100 };
101 (update::Mode::New, reflog_msg, name)
102 }
103 };
104 let edit = RefEdit {
105 change: Change::Update {
106 log: LogChange {
107 mode: RefLog::AndReference,
108 force_create_reflog: false,
109 message: format!("{}: {}", action, reflog_message).into(),
110 },
111 expected: PreviousValue::ExistingMustMatch(Target::Peeled(remote_id.into())),
112 new: Target::Peeled(remote_id.into()),
113 },
114 name,
115 deref: false,
116 };
117 let edit_index = edits.len();
118 edits.push(edit);
119 (mode, Some(edit_index))
120 }
121 None => (update::Mode::NoChangeNeeded, None),
122 };
123 updates.push(Update { mode, edit_index })
124 }
125
126 let edits = match dry_run {
127 fetch::DryRun::No => repo.edit_references(edits)?,
128 fetch::DryRun::Yes => edits,
129 };
130
131 Ok(update::Outcome { edits, updates })
132}
133
134fn worktree_branches(repo: &Repository) -> Result<BTreeMap<git_ref::FullName, PathBuf>, update::Error> {
135 let mut map = BTreeMap::new();
136 if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) {
137 map.insert(head_ref.inner.name, wt_dir.to_owned());
138 }
139 for proxy in repo.worktrees()? {
140 let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?;
141 if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) {
142 map.insert(head_ref.inner.name, wt_dir.to_owned());
143 }
144 }
145 Ok(map)
146}
147
148#[cfg(test)]
149mod tests;
git-repository/src/remote/connection/fetch/update_refs/tests.rs+263−0
@@ -0,0 +1,263 @@
1mod update {
2 use crate as git;
3 use git_testtools::{hex_to_id, Result};
4
5 fn base_repo_path() -> String {
6 git::path::realpath(
7 git_testtools::scripted_fixture_repo_read_only("make_remote_repos.sh")
8 .unwrap()
9 .join("base"),
10 )
11 .unwrap()
12 .to_string_lossy()
13 .into_owned()
14 }
15
16 fn repo(name: &str) -> git::Repository {
17 let dir = git_testtools::scripted_fixture_repo_read_only_with_args("make_fetch_repos.sh", [base_repo_path()])
18 .unwrap();
19 git::open_opts(dir.join(name), git::open::Options::isolated()).unwrap()
20 }
21 use crate::remote::fetch;
22 use git_ref::transaction::Change;
23 use git_ref::TargetRef;
24
25 #[test]
26 fn various_valid_updates() {
27 let repo = repo("two-origins");
28 // TODO: test reflog message (various cases if it's new)
29 for (spec, expected_mode, reflog_message, detail) in [
30 (
31 "refs/heads/main:refs/remotes/origin/main",
32 fetch::refs::update::Mode::NoChangeNeeded,
33 Some("no update will be performed"),
34 "these refs are en-par since the initial clone",
35 ),
36 (
37 "refs/heads/main",
38 fetch::refs::update::Mode::NoChangeNeeded,
39 None,
40 "without local destination ref there is nothing to do for us, ever (except for FETCH_HEADs) later",
41 ),
42 (
43 "refs/heads/main:refs/remotes/origin/new-main",
44 fetch::refs::update::Mode::New,
45 Some("storing ref"),
46 "the destination branch doesn't exist and needs to be created",
47 ),
48 (
49 "refs/heads/main:refs/heads/feature",
50 fetch::refs::update::Mode::New,
51 Some("storing head"),
52 "reflog messages are specific to the type of branch stored, to some limited extend",
53 ),
54 (
55 "refs/heads/main:refs/tags/new-tag",
56 fetch::refs::update::Mode::New,
57 Some("storing tag"),
58 "reflog messages are specific to the type of branch stored, to some limited extend",
59 ),
60 (
61 "+refs/heads/main:refs/remotes/origin/new-main",
62 fetch::refs::update::Mode::New,
63 Some("storing ref"),
64 "just to validate that we really are in dry-run mode, or else this ref would be present now",
65 ),
66 (
67 "+refs/heads/main:refs/remotes/origin/g",
68 fetch::refs::update::Mode::Forced,
69 Some("forced-update"),
70 "a forced non-fastforward (main goes backwards)",
71 ),
72 (
73 "+refs/heads/main:refs/tags/b-tag",
74 fetch::refs::update::Mode::Forced,
75 Some("updating tag"),
76 "tags can only be forced",
77 ),
78 (
79 "refs/heads/main:refs/tags/b-tag",
80 fetch::refs::update::Mode::RejectedTagUpdate,
81 None,
82 "otherwise a tag is always refusing itself to be overwritten (no-clobber)",
83 ),
84 (
85 "+refs/remotes/origin/g:refs/heads/main",
86 fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {
87 worktree_dir: repo.work_dir().expect("present").to_owned(),
88 },
89 None,
90 "checked out branches cannot be written, as it requires a merge of sorts which isn't done here",
91 ),
92 (
93 "ffffffffffffffffffffffffffffffffffffffff:refs/heads/invalid-source-object",
94 fetch::refs::update::Mode::RejectedSourceObjectNotFound {
95 id: hex_to_id("ffffffffffffffffffffffffffffffffffffffff"),
96 },
97 None,
98 "checked out branches cannot be written, as it requires a merge of sorts which isn't done here",
99 ),
100 // ( // TODO: make fast-forwards work
101 // "refs/remotes/origin/g:refs/heads/not-currently-checked-out",
102 // fetch::refs::update::Mode::FastForward,
103 // true,
104 // "a fast-forward only fast-forward situation, all good",
105 // ),
106 ] {
107 let (mapping, specs) = mapping_from_spec(spec, &repo);
108 let out = fetch::refs::update(
109 &repo,
110 "action",
111 &mapping,
112 &specs,
113 reflog_message.map(|_| fetch::DryRun::Yes).unwrap_or(fetch::DryRun::No),
114 )
115 .unwrap();
116
117 assert_eq!(
118 out.updates,
119 vec![fetch::refs::Update {
120 mode: expected_mode.clone(),
121 edit_index: reflog_message.map(|_| 0),
122 }],
123 "{spec:?}: {detail}"
124 );
125 assert_eq!(out.edits.len(), reflog_message.map(|_| 1).unwrap_or(0));
126 if let Some(reflog_message) = reflog_message {
127 let edit = &out.edits[0];
128 match &edit.change {
129 Change::Update { log, .. } => {
130 assert_eq!(
131 log.message,
132 format!("action: {}", reflog_message),
133 "{}: reflog messages are specific and we emulate git word for word",
134 spec
135 );
136 }
137 _ => unreachable!("only updates"),
138 }
139 }
140 }
141 }
142
143 #[test]
144 fn checked_out_branches_in_worktrees_are_rejected_with_additional_infromation() -> Result {
145 let root = git_path::realpath(git_testtools::scripted_fixture_repo_read_only_with_args(
146 "make_fetch_repos.sh",
147 [base_repo_path()],
148 )?)?;
149 let repo = root.join("worktree-root");
150 let repo = git::open_opts(repo, git::open::Options::isolated())?;
151 for (branch, path_from_root) in [
152 ("main", "worktree-root"),
153 ("wt-a-nested", "prev/wt-a-nested"),
154 ("wt-a", "wt-a"),
155 ("nested-wt-b", "wt-a/nested-wt-b"),
156 ("wt-c-locked", "wt-c-locked"),
157 ("wt-deleted", "wt-deleted"),
158 ] {
159 let spec = format!("refs/heads/main:refs/heads/{}", branch);
160 let (mappings, specs) = mapping_from_spec(&spec, &repo);
161 let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes)?;
162
163 assert_eq!(
164 out.updates,
165 vec![fetch::refs::Update {
166 mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {
167 worktree_dir: root.join(path_from_root),
168 },
169 edit_index: None,
170 }],
171 "{}: checked-out checks are done before checking if a change would actually be required (here it isn't)", spec
172 );
173 assert_eq!(out.edits.len(), 0);
174 }
175 Ok(())
176 }
177
178 #[test]
179 fn symbolic_refs_are_never_written() {
180 let repo = repo("two-origins");
181 let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/heads/symbolic", &repo);
182 let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap();
183
184 assert_eq!(
185 out.updates,
186 vec![fetch::refs::Update {
187 mode: fetch::refs::update::Mode::RejectedSymbolic,
188 edit_index: None,
189 }],
190 "this also protects from writing HEAD, which should in theory be impossible to get from a refspec as it normalizes partial ref names"
191 );
192 assert_eq!(out.edits.len(), 0);
193 }
194
195 #[test]
196 #[should_panic]
197 fn fast_forward_is_not_implemented_yet_but_should_be_denied() {
198 let repo = repo("two-origins");
199 let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/g", &repo);
200 let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap();
201
202 assert_eq!(
203 out.updates,
204 vec![fetch::refs::Update {
205 mode: fetch::refs::update::Mode::RejectedNonFastForward,
206 edit_index: Some(0),
207 }]
208 );
209 assert_eq!(out.edits.len(), 1);
210 }
211
212 fn mapping_from_spec(spec: &str, repo: &git::Repository) -> (Vec<fetch::Mapping>, Vec<git::refspec::RefSpec>) {
213 let spec = git_refspec::parse(spec.into(), git_refspec::parse::Operation::Fetch).unwrap();
214 let group = git_refspec::MatchGroup::from_fetch_specs(Some(spec));
215 let references = repo.references().unwrap();
216 let references: Vec<_> = references.all().unwrap().map(|r| into_remote_ref(r.unwrap())).collect();
217 let mappings = group
218 .match_remotes(references.iter().map(remote_ref_to_item))
219 .mappings
220 .into_iter()
221 .map(|m| fetch::Mapping {
222 remote: m
223 .item_index
224 .map(|idx| fetch::Source::Ref(references[idx].clone()))
225 .unwrap_or_else(|| match m.lhs {
226 git_refspec::match_group::SourceRef::ObjectId(id) => fetch::Source::ObjectId(id),
227 _ => unreachable!("not a ref, must be id: {:?}", m),
228 }),
229 local: m.rhs.map(|r| r.into_owned()),
230 spec_index: m.spec_index,
231 })
232 .collect();
233 (mappings, vec![spec.to_owned()])
234 }
235
236 fn into_remote_ref(mut r: git::Reference<'_>) -> git_protocol::fetch::Ref {
237 let full_ref_name = r.name().as_bstr().into();
238 match r.target() {
239 TargetRef::Peeled(id) => git_protocol::fetch::Ref::Direct {
240 full_ref_name,
241 object: id.into(),
242 },
243 TargetRef::Symbolic(name) => {
244 let target = name.as_bstr().into();
245 let id = r.peel_to_id_in_place().unwrap();
246 git_protocol::fetch::Ref::Symbolic {
247 full_ref_name,
248 target,
249 object: id.detach(),
250 }
251 }
252 }
253 }
254
255 fn remote_ref_to_item(r: &git_protocol::fetch::Ref) -> git_refspec::match_group::Item<'_> {
256 let (full_ref_name, target, object) = r.unpack();
257 git_refspec::match_group::Item {
258 full_ref_name,
259 target,
260 object,
261 }
262 }
263}
git-repository/src/remote/connection/fetch/update_refs/update.rs+92−0
@@ -0,0 +1,92 @@
1use crate::remote::fetch;
2use std::path::PathBuf;
3
4mod error {
5 /// The error returned when updating references.
6 #[derive(Debug, thiserror::Error)]
7 #[allow(missing_docs)]
8 pub enum Error {
9 #[error(transparent)]
10 FindReference(#[from] crate::reference::find::Error),
11 #[error("A remote reference had a name that wasn't considered valid. Corrupt remote repo or insufficient checks on remote?")]
12 InvalidRefName(#[from] git_validate::refname::Error),
13 #[error("Failed to update references to their new position to match their remote locations")]
14 EditReferences(#[from] crate::reference::edit::Error),
15 #[error("Failed to read or iterate worktree dir")]
16 WorktreeListing(#[from] std::io::Error),
17 #[error("Could not open worktree repository")]
18 OpenWorktreeRepo(#[from] crate::open::Error),
19 }
20}
21
22pub use error::Error;
23
24/// The outcome of the refs-update operation at the end of a fetch.
25#[derive(Debug, Clone)]
26pub struct Outcome {
27 /// All edits that were performed to update local refs.
28 pub edits: Vec<git_ref::transaction::RefEdit>,
29 /// Each update provides more information about what happened to the corresponding mapping.
30 /// Use [`iter_mapping_updates()`][Self::iter_mapping_updates()] to recombine the update information with ref-edits and their
31 /// mapping.
32 pub updates: Vec<super::Update>,
33}
34
35/// Describe the way a ref was updated
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum Mode {
38 /// No change was attempted as the remote ref didn't change compared to the current ref, or because no remote ref was specified
39 /// in the ref-spec.
40 NoChangeNeeded,
41 /// The old ref's commit was an ancestor of the new one, allowing for a fast-forward without a merge.
42 FastForward,
43 /// The ref was set to point to the new commit from the remote without taking into consideration its ancestry.
44 Forced,
45 /// A new ref has been created as there was none before.
46 New,
47 /// The object id to set the target reference to could not be found.
48 RejectedSourceObjectNotFound {
49 /// The id of the object that didn't exist in the object database, even though it should since it should be part of the pack.
50 id: git_hash::ObjectId,
51 },
52 /// Tags can never be overwritten (whether the new object would be a fast-forward or not, or unchanged), unless the refspec
53 /// specifies force.
54 RejectedTagUpdate,
55 /// The reference update would not have been a fast-forward, and force is not specified in the ref-spec.
56 RejectedNonFastForward,
57 /// The update of a local symbolic reference was rejected.
58 RejectedSymbolic,
59 /// The update was rejected because the branch is checked out in the given worktree_dir.
60 ///
61 /// Note that the check applies to any known worktree, whether it's present on disk or not.
62 RejectedCurrentlyCheckedOut {
63 /// The path to the worktree directory where the branch is checked out.
64 worktree_dir: PathBuf,
65 },
66}
67
68impl Outcome {
69 /// Produce an iterator over all information used to produce the this outcome, ref-update by ref-update, using the `mappings`
70 /// used when producing the ref update.
71 pub fn iter_mapping_updates<'a, 'b>(
72 &self,
73 mappings: &'a [fetch::Mapping],
74 refspecs: &'b [git_refspec::RefSpec],
75 ) -> impl Iterator<
76 Item = (
77 &super::Update,
78 &'a fetch::Mapping,
79 &'b git_refspec::RefSpec,
80 Option<&git_ref::transaction::RefEdit>,
81 ),
82 > {
83 self.updates.iter().zip(mappings.iter()).map(move |(update, mapping)| {
84 (
85 update,
86 mapping,
87 &refspecs[mapping.spec_index],
88 update.edit_index.and_then(|idx| self.edits.get(idx)),
89 )
90 })
91 }
92}
git-repository/src/remote/connection/mod.rs+8−48
@@ -5,8 +5,8 @@ pub(crate) struct HandshakeWithRefs { · 4 unmodified lines
5 refs: Vec<git_protocol::fetch::Ref>,5 refs: Vec<git_protocol::fetch::Ref>,
6}6}
7 7
8/// A function that performs a given credential action.8/// A function that performs a given credential action, trying to obtain credentials for an operation that needs it.
9pub type CredentialsFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a>;9pub type AuthenticateFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a>;
10 10
11/// A type to represent an ongoing connection to a remote host, typically with the connection already established.11/// A type to represent an ongoing connection to a remote host, typically with the connection already established.
12///12///
@@ -14,56 +14,16 @@ pub type CredentialsFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> g · 1 unmodified line
14/// much like a remote procedure call.14/// much like a remote procedure call.
15pub struct Connection<'a, 'repo, T, P> {15pub struct Connection<'a, 'repo, T, P> {
16 pub(crate) remote: &'a Remote<'repo>,16 pub(crate) remote: &'a Remote<'repo>,
17 pub(crate) credentials: Option<CredentialsFn<'a>>,17 pub(crate) authenticate: Option<AuthenticateFn<'a>>,
18 pub(crate) transport: T,18 pub(crate) transport: T,
19 pub(crate) progress: P,19 pub(crate) progress: P,
20}20}
21 21
22mod access {22mod access;
23 use crate::{
24 remote::{connection::CredentialsFn, Connection},
25 Remote,
26 };
27
28 /// Builder
29 impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
30 /// Set a custom credentials callback to provide credentials if the remotes require authentication.
31 ///
32 /// Otherwise we will use the git configuration to perform the same task as the `git credential` helper program,
33 /// which is calling other helper programs in succession while resorting to a prompt to obtain credentials from the
34 /// user.
35 ///
36 /// A custom function may also be used to prevent accessing resources with authentication.
37 pub fn with_credentials(
38 mut self,
39 helper: impl FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a,
40 ) -> Self {
41 self.credentials = Some(Box::new(helper));
42 self
43 }
44 }
45
46 /// Access
47 impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
48 /// A utility to return a function that will use this repository's configuration to obtain credentials, similar to
49 /// what `git credential` is doing.
50 ///
51 /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the
52 /// default way of handling credentials, which they can call as fallback.
53 pub fn configured_credentials(
54 &self,
55 url: git_url::Url,
56 ) -> Result<CredentialsFn<'a>, crate::config::credential_helpers::Error> {
57 let (mut cascade, _action_with_normalized_url, prompt_opts) =
58 self.remote.repo.config_snapshot().credential_helpers(url)?;
59 Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as CredentialsFn<'_>)
60 }
61 /// Drop the transport and additional state to regain the original remote.
62 pub fn remote(&self) -> &Remote<'repo> {
63 self.remote
64 }
65 }
66}
67 23
68///24///
69pub mod ref_map;25pub mod ref_map;
26
27///
28#[cfg(feature = "blocking-network-client")]
29pub mod fetch;
git-repository/src/remote/connection/ref_map.rs+68−9
@@ -1,5 +1,7 @@
1use crate::bstr::{BString, ByteVec};
1use git_features::progress::Progress;2use git_features::progress::Progress;
2use git_protocol::transport::client::Transport;3use git_protocol::transport::client::Transport;
4use std::collections::HashSet;
3 5
4use crate::remote::{connection::HandshakeWithRefs, fetch, Connection, Direction};6use crate::remote::{connection::HandshakeWithRefs, fetch, Connection, Direction};
5 7
@@ -19,6 +21,27 @@ pub enum Error { · 13 unmodified lines
19 MappingValidation(#[from] git_refspec::match_group::validate::Error),21 MappingValidation(#[from] git_refspec::match_group::validate::Error),
20}22}
21 23
24/// For use in [`Connection::ref_map()`].
25#[derive(Debug, Clone)]
26pub struct Options {
27 /// Use a two-component prefix derived from the ref-spec's source, like `refs/heads/` to let the server pre-filter refs
28 /// with great potential for savings in traffic and local CPU time. Defaults to `true`.
29 pub prefix_from_spec_as_filter_on_remote: bool,
30 /// Parameters in the form of `(name, optional value)` to add to the handshake.
31 ///
32 /// This is useful in case of custom servers.
33 pub handshake_parameters: Vec<(String, Option<String>)>,
34}
35
36impl Default for Options {
37 fn default() -> Self {
38 Options {
39 prefix_from_spec_as_filter_on_remote: true,
40 handshake_parameters: Default::default(),
41 }
42 }
43}
44
22impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P>45impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P>
23where46where
24 T: Transport,47 T: Transport,
@@ -31,16 +54,31 @@ where · 6 unmodified lines
31 /// with the local tracking branch of these tips (if available).54 /// with the local tracking branch of these tips (if available).
32 ///55 ///
33 /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository.56 /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository.
57 ///
58 /// # Consumption
59 ///
60 /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus it's consumed along with
61 /// the connection.
34 #[git_protocol::maybe_async::maybe_async]62 #[git_protocol::maybe_async::maybe_async]
35 pub async fn ref_map(mut self) -> Result<fetch::RefMap<'remote>, Error> {63 pub async fn ref_map(mut self, options: Options) -> Result<fetch::RefMap<'remote>, Error> {
36 let res = self.ref_map_inner().await;64 let res = self.ref_map_inner(options).await;
37 git_protocol::fetch::indicate_end_of_interaction(&mut self.transport).await?;65 git_protocol::fetch::indicate_end_of_interaction(&mut self.transport)
66 .await
67 .ok();
38 res68 res
39 }69 }
40 70
41 #[git_protocol::maybe_async::maybe_async]71 #[git_protocol::maybe_async::maybe_async]
42 async fn ref_map_inner(&mut self) -> Result<fetch::RefMap<'remote>, Error> {72 pub(crate) async fn ref_map_inner(
43 let remote = self.fetch_refs().await?;73 &mut self,
74 Options {
75 prefix_from_spec_as_filter_on_remote,
76 handshake_parameters,
77 }: Options,
78 ) -> Result<fetch::RefMap<'remote>, Error> {
79 let remote = self
80 .fetch_refs(prefix_from_spec_as_filter_on_remote, handshake_parameters)
81 .await?;
44 let group = git_refspec::MatchGroup::from_fetch_specs(self.remote.fetch_specs.iter().map(|s| s.to_ref()));82 let group = git_refspec::MatchGroup::from_fetch_specs(self.remote.fetch_specs.iter().map(|s| s.to_ref()));
45 let (res, fixes) = group83 let (res, fixes) = group
46 .match_remotes(remote.refs.iter().map(|r| {84 .match_remotes(remote.refs.iter().map(|r| {
@@ -77,9 +115,13 @@ where · 30 unmodified lines
77 })115 })
78 }116 }
79 #[git_protocol::maybe_async::maybe_async]117 #[git_protocol::maybe_async::maybe_async]
80 async fn fetch_refs(&mut self) -> Result<HandshakeWithRefs, Error> {118 async fn fetch_refs(
119 &mut self,
120 filter_by_prefix: bool,
121 extra_parameters: Vec<(String, Option<String>)>,
122 ) -> Result<HandshakeWithRefs, Error> {
81 let mut credentials_storage;123 let mut credentials_storage;
82 let authenticate = match self.credentials.as_mut() {124 let authenticate = match self.authenticate.as_mut() {
83 Some(f) => f,125 Some(f) => f,
84 None => {126 None => {
85 let url = self127 let url = self
@@ -95,15 +137,32 @@ where · 9 unmodified lines
95 }137 }
96 };138 };
97 let mut outcome =139 let mut outcome =
98 git_protocol::fetch::handshake(&mut self.transport, authenticate, Vec::new(), &mut self.progress).await?;140 git_protocol::fetch::handshake(&mut self.transport, authenticate, extra_parameters, &mut self.progress)
141 .await?;
99 let refs = match outcome.refs.take() {142 let refs = match outcome.refs.take() {
100 Some(refs) => refs,143 Some(refs) => refs,
101 None => {144 None => {
145 let specs = &self.remote.fetch_specs;
102 git_protocol::fetch::refs(146 git_protocol::fetch::refs(
103 &mut self.transport,147 &mut self.transport,
104 outcome.server_protocol_version,148 outcome.server_protocol_version,
105 &outcome.capabilities,149 &outcome.capabilities,
106 |_a, _b, _c| Ok(git_protocol::fetch::delegate::LsRefsAction::Continue),150 |_capabilities, arguments, _features| {
151 if filter_by_prefix {
152 let mut seen = HashSet::new();
153 for spec in specs {
154 let spec = spec.to_ref();
155 if seen.insert(spec.instruction()) {
156 if let Some(prefix) = spec.prefix() {
157 let mut arg: BString = "ref-prefix ".into();
158 arg.push_str(prefix);
159 arguments.push(arg)
160 }
161 }
162 }
163 }
164 Ok(git_protocol::fetch::delegate::LsRefsAction::Continue)
165 },
107 &mut self.progress,166 &mut self.progress,
108 )167 )
109 .await?168 .await?
git-repository/src/remote/fetch.rs+59−0
@@ -0,0 +1,59 @@
1use crate::bstr::BString;
2
3/// If `Yes`, don't really make changes but do as much as possible to get an idea of what would be done.
4#[derive(Debug, Copy, Clone, PartialEq, Eq)]
5#[cfg(feature = "blocking-network-client")]
6pub(crate) enum DryRun {
7 /// Enable dry-run mode and don't actually change the underlying repository in any way.
8 Yes,
9 /// Run the operation like normal, making changes to the underlying repository.
10 No,
11}
12
13/// Information about the relationship between our refspecs, and remote references with their local counterparts.
14#[derive(Default, Debug, Clone)]
15pub struct RefMap<'spec> {
16 /// A mapping between a remote reference and a local tracking branch.
17 pub mappings: Vec<Mapping>,
18 /// Information about the fixes applied to the `mapping` due to validation and sanitization.
19 pub fixes: Vec<git_refspec::match_group::validate::Fix<'spec>>,
20 /// All refs advertised by the remote.
21 pub remote_refs: Vec<git_protocol::fetch::Ref>,
22 /// Additional information provided by the server as part of the handshake.
23 ///
24 /// Note that the `refs` field is always `None` as the refs are placed in `remote_refs`.
25 pub handshake: git_protocol::fetch::handshake::Outcome,
26}
27
28/// Either an object id that the remote has or the matched remote ref itself.
29#[derive(Debug, Clone)]
30pub enum Source {
31 /// An object id, as the matched ref-spec was an object id itself.
32 ObjectId(git_hash::ObjectId),
33 /// The remote reference that matched the ref-specs name.
34 Ref(git_protocol::fetch::Ref),
35}
36
37impl Source {
38 /// Return either the direct object id we refer to or the direct target that a reference refers to.
39 pub fn as_id(&self) -> &git_hash::oid {
40 match self {
41 Source::ObjectId(id) => id,
42 Source::Ref(r) => r.unpack().1,
43 }
44 }
45}
46
47/// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist.
48#[derive(Debug, Clone)]
49pub struct Mapping {
50 /// The reference on the remote side, along with information about the objects they point to as advertised by the server.
51 pub remote: Source,
52 /// The local tracking reference to update after fetching the object visible via `remote`.
53 pub local: Option<BString>,
54 /// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered.
55 pub spec_index: usize,
56}
57
58#[cfg(feature = "blocking-network-client")]
59pub use super::connection::fetch::{negotiate, refs, Error, Outcome, Prepare, Status};
git-repository/src/remote/mod.rs+1−38
@@ -27,44 +27,7 @@ pub mod init; · 26 unmodified lines
27 27
28///28///
29#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]29#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]
30pub mod fetch {30pub mod fetch;
31 use crate::bstr::BString;
32
33 /// Information about the relationship between our refspecs, and remote references with their local counterparts.
34 #[derive(Debug, Clone)]
35 pub struct RefMap<'spec> {
36 /// A mapping between a remote reference and a local tracking branch.
37 pub mappings: Vec<Mapping>,
38 /// Information about the fixes applied to the `mapping` due to validation and sanitization.
39 pub fixes: Vec<git_refspec::match_group::validate::Fix<'spec>>,
40 /// All refs advertised by the remote.
41 pub remote_refs: Vec<git_protocol::fetch::Ref>,
42 /// Additional information provided by the server as part of the handshake.
43 ///
44 /// Note that the `refs` field is always `None` as the refs are placed in `remote_refs`.
45 pub handshake: git_protocol::fetch::handshake::Outcome,
46 }
47
48 /// Either an object id that the remote has or the matched remote ref itself.
49 #[derive(Debug, Clone)]
50 pub enum Source {
51 /// An object id, as the matched ref-spec was an object id itself.
52 ObjectId(git_hash::ObjectId),
53 /// The remote reference that matched the ref-specs name.
54 Ref(git_protocol::fetch::Ref),
55 }
56
57 /// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist.
58 #[derive(Debug, Clone)]
59 pub struct Mapping {
60 /// The reference on the remote side, along with information about the objects they point to as advertised by the server.
61 pub remote: Source,
62 /// The local tracking reference to update after fetching the object visible via `remote`.
63 pub local: Option<BString>,
64 /// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered.
65 pub spec_index: usize,
66 }
67}
68 31
69///32///
70#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]33#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]
git-repository/src/remote/url/mod.rs+2−2
@@ -1,7 +1,7 @@
1mod rewrite;1mod rewrite;
2///2///
3#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]3#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
4pub mod scheme_permission;4pub mod scheme_permission;
5pub(crate) use rewrite::Rewrite;5pub(crate) use rewrite::Rewrite;
6#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]6#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
7pub(crate) use scheme_permission::SchemePermission;7pub(crate) use scheme_permission::SchemePermission;
git-repository/src/repository/config.rs+9−2
@@ -9,10 +9,17 @@ impl crate::Repository { · 8 unmodified lines
9 config::Snapshot { repo: self }9 config::Snapshot { repo: self }
10 }10 }
11 11
12 /// Return a mutable snapshot of the configuration as seen upon opening the repository.12 /// Return a mutable snapshot of the configuration as seen upon opening the repository, starting a transaction.
13 /// When the returned instance is dropped, it is applied in full, even if the reason for the drop is an error.
14 ///
15 /// Note that changes to the configuration are in-memory only and are observed only the this instance
16 /// of the [`Repository`][crate::Repository].
13 pub fn config_snapshot_mut(&mut self) -> config::SnapshotMut<'_> {17 pub fn config_snapshot_mut(&mut self) -> config::SnapshotMut<'_> {
14 let config = self.config.resolved.as_ref().clone();18 let config = self.config.resolved.as_ref().clone();
15 config::SnapshotMut { repo: self, config }19 config::SnapshotMut {
20 repo: Some(self),
21 config,
22 }
16 }23 }
17 24
18 /// The options used to open the repository.25 /// The options used to open the repository.
git-repository/src/repository/identity.rs+9−2
@@ -4,7 +4,8 @@ use crate::bstr::BString; · 3 unmodified lines
4 4
5/// Identity handling.5/// Identity handling.
6impl crate::Repository {6impl crate::Repository {
7 /// Return a crate-specific constant signature with [`Time`][git_actor::Time] set to now,7 /// Return a crate-specific constant signature with [`Time`][git_actor::Time] set to now, or whatever
8 /// was overridden via `GIT_COMMITTER_TIME` or `GIT_AUTHOR_TIME` if these variables are allowed to be read,
8 /// in a similar vein as the default that git chooses if there is nothing configured.9 /// in a similar vein as the default that git chooses if there is nothing configured.
9 ///10 ///
10 /// This can be useful as fallback for an unset `committer` or `author`.11 /// This can be useful as fallback for an unset `committer` or `author`.
@@ -16,7 +17,13 @@ impl crate::Repository { · 5 unmodified lines
16 git_actor::SignatureRef {17 git_actor::SignatureRef {
17 name: "gitoxide".into(),18 name: "gitoxide".into(),
18 email: "gitoxide@localhost".into(),19 email: "gitoxide@localhost".into(),
19 time: git_date::Time::now_local_or_utc(),20 time: {
21 let p = self.config.personas();
22 p.committer
23 .time
24 .or(p.author.time)
25 .unwrap_or_else(git_date::Time::now_local_or_utc)
26 },
20 }27 }
21 }28 }
22 29
git-repository/src/repository/object.rs+24−30
@@ -125,8 +125,10 @@ impl crate::Repository { · 124 unmodified lines
125 self.tag_reference(name, tag_id, constraint).map_err(Into::into)125 self.tag_reference(name, tag_id, constraint).map_err(Into::into)
126 }126 }
127 127
128 /// Create a new commit object with `author`, `committer` and `message` referring to `tree` with `parents`, and point `reference`128 /// Create a new commit object with `message` referring to `tree` with `parents`, and point `reference`
129 /// to it. The commit is written without message encoding field, which can be assumed to be UTF-8.129 /// to it. The commit is written without message encoding field, which can be assumed to be UTF-8.
130 /// `author` and `committer` fields are pre-set from the configuration, which can be altered
131 /// [temporarily][crate::Repository::config_snapshot_mut()] before the call if required.
130 ///132 ///
131 /// `reference` will be created if it doesn't exist, and can be `"HEAD"` to automatically write-through to the symbolic reference133 /// `reference` will be created if it doesn't exist, and can be `"HEAD"` to automatically write-through to the symbolic reference
132 /// that `HEAD` points to if it is not detached. For this reason, detached head states cannot be created unless the `HEAD` is detached134 /// that `HEAD` points to if it is not detached. For this reason, detached head states cannot be created unless the `HEAD` is detached
@@ -139,8 +141,6 @@ impl crate::Repository { · 6 unmodified lines
139 pub fn commit<Name, E>(141 pub fn commit<Name, E>(
140 &self,142 &self,
141 reference: Name,143 reference: Name,
142 author: git_actor::SignatureRef<'_>,
143 committer: git_actor::SignatureRef<'_>,
144 message: impl AsRef<str>,144 message: impl AsRef<str>,
145 tree: impl Into<ObjectId>,145 tree: impl Into<ObjectId>,
146 parents: impl IntoIterator<Item = impl Into<ObjectId>>,146 parents: impl IntoIterator<Item = impl Into<ObjectId>>,
@@ -157,6 +157,8 @@ impl crate::Repository { · 10 unmodified lines
157 // TODO: possibly use CommitRef to save a few allocations (but will have to allocate for object ids anyway.157 // TODO: possibly use CommitRef to save a few allocations (but will have to allocate for object ids anyway.
158 // This can be made vastly more efficient though if we wanted to, so we lie in the API158 // This can be made vastly more efficient though if we wanted to, so we lie in the API
159 let reference = reference.try_into()?;159 let reference = reference.try_into()?;
160 let author = self.author_or_default();
161 let committer = self.committer_or_default();
160 let commit = git_object::Commit {162 let commit = git_object::Commit {
161 message: message.as_ref().into(),163 message: message.as_ref().into(),
162 tree: tree.into(),164 tree: tree.into(),
@@ -168,36 +170,28 @@ impl crate::Repository { · 5 unmodified lines
168 };170 };
169 171
170 let commit_id = self.write_object(&commit)?;172 let commit_id = self.write_object(&commit)?;
171 self.edit_reference(173 self.edit_reference(RefEdit {
172 RefEdit {174 change: Change::Update {
173 change: Change::Update {175 log: LogChange {
174 log: LogChange {176 mode: RefLog::AndReference,
175 mode: RefLog::AndReference,177 force_create_reflog: false,
176 force_create_reflog: false,178 message: crate::reference::log::message("commit", commit.message.as_ref(), commit.parents.len()),
177 message: crate::reference::log::message(179 },
178 "commit",180 expected: match commit.parents.first().map(|p| Target::Peeled(*p)) {
179 commit.message.as_ref(),181 Some(previous) => {
180 commit.parents.len(),182 if reference.as_bstr() == "HEAD" {
181 ),183 PreviousValue::MustExistAndMatch(previous)
182 },184 } else {
183 expected: match commit.parents.first().map(|p| Target::Peeled(*p)) {185 PreviousValue::ExistingMustMatch(previous)
184 Some(previous) => {
185 if reference.as_bstr() == "HEAD" {
186 PreviousValue::MustExistAndMatch(previous)
187 } else {
188 PreviousValue::ExistingMustMatch(previous)
189 }
190 }186 }
191 None => PreviousValue::MustNotExist,187 }
192 },188 None => PreviousValue::MustNotExist,
193 new: Target::Peeled(commit_id.inner),
194 },189 },
195 name: reference,190 new: Target::Peeled(commit_id.inner),
196 deref: true,
197 },191 },
198 git_lock::acquire::Fail::Immediately,192 name: reference,
199 commit.committer.to_ref(),193 deref: true,
200 )?;194 })?;
201 Ok(commit_id)195 Ok(commit_id)
202 }196 }
203}197}
git-repository/src/repository/reference.rs+28−45
@@ -1,8 +1,6 @@
1use std::convert::TryInto;1use std::convert::TryInto;
2 2
3use git_actor as actor;
4use git_hash::ObjectId;3use git_hash::ObjectId;
5use git_lock as lock;
6use git_ref::{4use git_ref::{
7 transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},5 transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},
8 FullName, PartialNameRef, Target,6 FullName, PartialNameRef, Target,
@@ -10,8 +8,6 @@ use git_ref::{ · 1 unmodified line
10 8
11use crate::{bstr::BString, ext::ReferenceExt, reference, Reference};9use crate::{bstr::BString, ext::ReferenceExt, reference, Reference};
12 10
13const DEFAULT_LOCK_MODE: git_lock::acquire::Fail = git_lock::acquire::Fail::Immediately;
14
15/// Obtain and alter references comfortably11/// Obtain and alter references comfortably
16impl crate::Repository {12impl crate::Repository {
17 /// Create a lightweight tag with given `name` (and without `refs/tags/` prefix) pointing to the given `target`, and return it as reference.13 /// Create a lightweight tag with given `name` (and without `refs/tags/` prefix) pointing to the given `target`, and return it as reference.
@@ -25,19 +21,15 @@ impl crate::Repository { · 7 unmodified lines
25 constraint: PreviousValue,21 constraint: PreviousValue,
26 ) -> Result<Reference<'_>, reference::edit::Error> {22 ) -> Result<Reference<'_>, reference::edit::Error> {
27 let id = target.into();23 let id = target.into();
28 let mut edits = self.edit_reference(24 let mut edits = self.edit_reference(RefEdit {
29 RefEdit {25 change: Change::Update {
30 change: Change::Update {26 log: Default::default(),
31 log: Default::default(),27 expected: constraint,
32 expected: constraint,28 new: Target::Peeled(id),
33 new: Target::Peeled(id),
34 },
35 name: format!("refs/tags/{}", name.as_ref()).try_into()?,
36 deref: false,
37 },29 },
38 DEFAULT_LOCK_MODE,30 name: format!("refs/tags/{}", name.as_ref()).try_into()?,
39 self.committer_or_default(),31 deref: false,
40 )?;32 })?;
41 assert_eq!(edits.len(), 1, "reference splits should ever happen");33 assert_eq!(edits.len(), 1, "reference splits should ever happen");
42 let edit = edits.pop().expect("exactly one item");34 let edit = edits.pop().expect("exactly one item");
43 Ok(Reference {35 Ok(Reference {
@@ -95,23 +87,19 @@ impl crate::Repository { · 51 unmodified lines
95 {87 {
96 let name = name.try_into().map_err(git_validate::reference::name::Error::from)?;88 let name = name.try_into().map_err(git_validate::reference::name::Error::from)?;
97 let id = target.into();89 let id = target.into();
98 let mut edits = self.edit_reference(90 let mut edits = self.edit_reference(RefEdit {
99 RefEdit {91 change: Change::Update {
100 change: Change::Update {92 log: LogChange {
101 log: LogChange {93 mode: RefLog::AndReference,
102 mode: RefLog::AndReference,94 force_create_reflog: false,
103 force_create_reflog: false,95 message: log_message.into(),
104 message: log_message.into(),
105 },
106 expected: constraint,
107 new: Target::Peeled(id),
108 },96 },
109 name,97 expected: constraint,
110 deref: false,98 new: Target::Peeled(id),
111 },99 },
112 DEFAULT_LOCK_MODE,100 name,
113 self.committer_or_default(),101 deref: false,
114 )?;102 })?;
115 assert_eq!(103 assert_eq!(
116 edits.len(),104 edits.len(),
117 1,105 1,
@@ -126,34 +114,29 @@ impl crate::Repository { · 8 unmodified lines
126 .attach(self))114 .attach(self))
127 }115 }
128 116
129 /// Edit a single reference as described in `edit`, handle locks via `lock_mode` and write reference logs as `log_committer`.117 /// Edit a single reference as described in `edit`, and write reference logs as `log_committer`.
130 ///118 ///
131 /// One or more `RefEdit`s are returned - symbolic reference splits can cause more edits to be performed. All edits have the previous119 /// One or more `RefEdit`s are returned - symbolic reference splits can cause more edits to be performed. All edits have the previous
132 /// reference values set to the ones encountered at rest after acquiring the respective reference's lock.120 /// reference values set to the ones encountered at rest after acquiring the respective reference's lock.
133 pub fn edit_reference(121 pub fn edit_reference(&self, edit: RefEdit) -> Result<Vec<RefEdit>, reference::edit::Error> {
134 &self,122 self.edit_references(Some(edit))
135 edit: RefEdit,
136 lock_mode: lock::acquire::Fail,
137 log_committer: actor::SignatureRef<'_>,
138 ) -> Result<Vec<RefEdit>, reference::edit::Error> {
139 self.edit_references(Some(edit), lock_mode, log_committer)
140 }123 }
141 124
142 /// Edit one or more references as described by their `edits`, with `lock_mode` deciding on how to handle competing125 /// Edit one or more references as described by their `edits`.
143 /// transactions. `log_committer` is the name appearing in reference logs.126 /// Note that one can set the committer name for use in the ref-log by temporarily
127 /// [overriding the git-config][crate::Repository::config_snapshot_mut()].
144 ///128 ///
145 /// Returns all reference edits, which might be more than where provided due the splitting of symbolic references, and129 /// Returns all reference edits, which might be more than where provided due the splitting of symbolic references, and
146 /// whose previous (_old_) values are the ones seen on in storage after the reference was locked.130 /// whose previous (_old_) values are the ones seen on in storage after the reference was locked.
147 pub fn edit_references(131 pub fn edit_references(
148 &self,132 &self,
149 edits: impl IntoIterator<Item = RefEdit>,133 edits: impl IntoIterator<Item = RefEdit>,
150 lock_mode: lock::acquire::Fail,
151 log_committer: actor::SignatureRef<'_>,
152 ) -> Result<Vec<RefEdit>, reference::edit::Error> {134 ) -> Result<Vec<RefEdit>, reference::edit::Error> {
135 let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout()?;
153 self.refs136 self.refs
154 .transaction()137 .transaction()
155 .prepare(edits, lock_mode)?138 .prepare(edits, file_lock_fail, packed_refs_lock_fail)?
156 .commit(log_committer)139 .commit(self.committer_or_default())
157 .map_err(Into::into)140 .map_err(Into::into)
158 }141 }
159 142
git-repository/src/repository/remote.rs+11−0
@@ -32,6 +32,17 @@ impl crate::Repository { · 31 unmodified lines
32 .ok_or_else(|| find::existing::Error::NotFound { name: name.into() })??)32 .ok_or_else(|| find::existing::Error::NotFound { name: name.into() })??)
33 }33 }
34 34
35 /// Find the default remote as configured, or `None` if no such configuration could be found.
36 ///
37 /// See [remote_default_name()][Self::remote_default_name()] for more information on the `direction` parameter.
38 pub fn find_default_remote(
39 &self,
40 direction: remote::Direction,
41 ) -> Option<Result<Remote<'_>, find::existing::Error>> {
42 self.remote_default_name(direction)
43 .map(|name| self.find_remote(name.as_ref()))
44 }
45
35 /// Find the remote with the given `name` or return `None` if it doesn't exist, for the purpose of fetching or pushing46 /// Find the remote with the given `name` or return `None` if it doesn't exist, for the purpose of fetching or pushing
36 /// data to a remote.47 /// data to a remote.
37 ///48 ///
git-repository/src/repository/worktree.rs+1−11
@@ -25,21 +25,11 @@ impl crate::Repository { · 24 unmodified lines
25 res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir));25 res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir));
26 Ok(res)26 Ok(res)
27 }27 }
28
29 /// Iterate all _linked_ worktrees in sort order and resolve them, ignoring those without an accessible work tree, into repositories
30 /// whose [`worktree()`][crate::Repository::worktree()] is the worktree currently being iterated.
31 ///
32 /// Note that for convenience all io errors are squelched so if there is a chance for IO errors during
33 /// traversal of an owned directory, better use `list()` directly. The latter allows to resolve repositories
34 /// even if the worktree checkout isn't accessible.
35 pub fn worktree_repos(&self) -> ! {
36 todo!()
37 }
38}28}
39 29
40/// Interact with individual worktrees and their information.30/// Interact with individual worktrees and their information.
41impl crate::Repository {31impl crate::Repository {
42 /// Return the repository owning the main worktree.32 /// Return the repository owning the main worktree, typically from a linked worktree.
43 ///33 ///
44 /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree.34 /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree.
45 /// Also note that the main repo might be bare.35 /// Also note that the main repo might be bare.
git-repository/src/worktree/mod.rs+3−1
@@ -112,6 +112,8 @@ pub mod excludes { · 111 unmodified lines
112 Io(#[from] std::io::Error),112 Io(#[from] std::io::Error),
113 #[error(transparent)]113 #[error(transparent)]
114 EnvironmentPermission(#[from] git_sec::permission::Error<PathBuf>),114 EnvironmentPermission(#[from] git_sec::permission::Error<PathBuf>),
115 #[error("The value for `core.excludesFile` could not be read from configuration")]
116 ExcludesFilePathInterpolation(#[from] git_config::path::interpolate::Error),
115 }117 }
116 118
117 impl<'repo> crate::Worktree<'repo> {119 impl<'repo> crate::Worktree<'repo> {
@@ -135,7 +137,7 @@ pub mod excludes { · 17 unmodified lines
135 overrides.unwrap_or_default(),137 overrides.unwrap_or_default(),
136 git_attributes::MatchGroup::<git_attributes::Ignore>::from_git_dir(138 git_attributes::MatchGroup::<git_attributes::Ignore>::from_git_dir(
137 repo.git_dir(),139 repo.git_dir(),
138 match repo.config.excludes_file.as_ref() {140 match repo.config.excludes_file()?.as_ref() {
139 Some(user_path) => Some(user_path.to_owned()),141 Some(user_path) => Some(user_path.to_owned()),
140 None => repo.config.xdg_config_path("ignore")?,142 None => repo.config.xdg_config_path("ignore")?,
141 },143 },
git-repository/tests/fixtures/generated-archives/.gitignore+1−0
@@ -1,3 +1,4 @@
1/make_worktree_repo.tar.xz1/make_worktree_repo.tar.xz
2/make_remote_repos.tar.xz2/make_remote_repos.tar.xz
3/make_fetch_repos.tar.xz
3/make_core_worktree_repo.tar.xz4/make_core_worktree_repo.tar.xz
git-repository/tests/fixtures/make_fetch_repos.sh+30−0
@@ -0,0 +1,30 @@
1set -eu -o pipefail
2
3git clone --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base
4
5git clone --shared base clone-as-base-with-changes
6(cd clone-as-base-with-changes
7 touch new-file
8 git add new-file
9 git commit -m "add new-file"
10 git tag -m "new-file introduction" v1.0
11)
12
13git clone --shared base two-origins
14(cd two-origins
15 git remote add changes-on-top-of-origin "$PWD/../clone-as-base-with-changes"
16 git branch "not-currently-checked-out"
17 git symbolic-ref refs/heads/symbolic refs/heads/main
18)
19
20git clone --shared base worktree-root
21(
22 cd worktree-root
23
24 git worktree add ../wt-a
25 git worktree add ../prev/wt-a-nested
26 git worktree add ../wt-b
27 git worktree add ../wt-a/nested-wt-b
28 git worktree add --lock ../wt-c-locked
29 git worktree add ../wt-deleted && rm -Rf ../wt-deleted
30)
git-repository/tests/fixtures/make_remote_repos.sh+4−0
@@ -274,3 +274,7 @@ git clone --shared base credential-helpers · 273 unmodified lines
274 baseline "git://host.org"274 baseline "git://host.org"
275)275)
276 276
277git clone --shared base detached-head
278(cd detached-head
279 git checkout @~1
280)
git-repository/tests/head/mod.rs+11−1
@@ -1,4 +1,4 @@
1mod remote {1mod into_remote {
2 use git_repository as git;2 use git_repository as git;
3 3
4 use crate::remote;4 use crate::remote;
@@ -12,4 +12,14 @@ mod remote { · 7 unmodified lines
12 );12 );
13 Ok(())13 Ok(())
14 }14 }
15
16 #[test]
17 fn detached_is_none() -> crate::Result {
18 let repo = remote::repo("detached-head");
19 assert_eq!(
20 repo.head()?.into_remote(git::remote::Direction::Fetch).transpose()?,
21 None
22 );
23 Ok(())
24 }
15}25}
git-repository/tests/remote/fetch.rs+123−0
@@ -0,0 +1,123 @@
1#[cfg(feature = "blocking-network-client")]
2mod blocking_io {
3 use crate::remote;
4 use git_features::progress;
5 use git_repository as git;
6 use git_repository::remote::fetch;
7 use git_repository::remote::Direction::Fetch;
8 use git_testtools::hex_to_id;
9 use std::sync::atomic::AtomicBool;
10
11 fn repo_rw(name: &str) -> (git::Repository, git_testtools::tempfile::TempDir) {
12 let dir = git_testtools::scripted_fixture_repo_writable_with_args(
13 "make_fetch_repos.sh",
14 [git::path::realpath(remote::repo_path("base"))
15 .unwrap()
16 .to_string_lossy()],
17 git_testtools::Creation::ExecuteScript,
18 )
19 .unwrap();
20 let repo = git::open_opts(dir.path().join(name), git::open::Options::isolated()).unwrap();
21 (repo, dir)
22 }
23
24 #[test]
25 fn fetch_pack() -> crate::Result {
26 for version in [
27 None,
28 Some(git::protocol::transport::Protocol::V2),
29 Some(git::protocol::transport::Protocol::V1),
30 ] {
31 let (mut repo, _tmp) = repo_rw("two-origins");
32 if let Some(version) = version {
33 repo.config_snapshot_mut().set_raw_value(
34 "protocol",
35 None,
36 "version",
37 (version as u8).to_string().as_str(),
38 )?;
39 }
40
41 // No updates
42 {
43 let remote = repo.find_remote("origin")?;
44 {
45 remote
46 .connect(Fetch, progress::Discard)?
47 .prepare_fetch(Default::default())?;
48 // early drops are fine and won't block.
49 }
50 let outcome = remote
51 .connect(Fetch, progress::Discard)?
52 .prepare_fetch(Default::default())?
53 .receive(&AtomicBool::default())?;
54 assert!(matches!(outcome.status, git::remote::fetch::Status::NoChange));
55 }
56
57 // Some updates to be fetched
58 for dry_run in [true, false] {
59 let remote = repo.find_remote("changes-on-top-of-origin")?;
60 let outcome: git::remote::fetch::Outcome = remote
61 .connect(Fetch, progress::Discard)?
62 .prepare_fetch(Default::default())?
63 .with_dry_run(dry_run)
64 .receive(&AtomicBool::default())?;
65 let refs = match outcome.status {
66 fetch::Status::Change {
67 write_pack_bundle,
68 update_refs,
69 } => {
70 assert_eq!(write_pack_bundle.pack_kind, git::odb::pack::data::Version::V2);
71 assert_eq!(write_pack_bundle.object_hash, repo.object_hash());
72 assert_eq!(write_pack_bundle.index.num_objects, 3, "this value is 4 when git does it with 'consecutive' negotiation style, but could be 33 if completely naive.");
73 assert_eq!(
74 write_pack_bundle.index.index_version,
75 git::odb::pack::index::Version::V2
76 );
77 assert_eq!(
78 write_pack_bundle.index.index_hash,
79 hex_to_id("c75114f60ab2c9389916f3de1082bbaa47491e3b")
80 );
81 assert!(write_pack_bundle.data_path.map_or(false, |f| f.is_file()));
82 assert!(write_pack_bundle.index_path.map_or(false, |f| f.is_file()));
83
84 update_refs
85 }
86 fetch::Status::DryRun { update_refs } => update_refs,
87 fetch::Status::NoChange => unreachable!("we firmly expect changes here"),
88 };
89
90 assert_eq!(
91 refs.updates,
92 vec![fetch::refs::Update {
93 mode: fetch::refs::update::Mode::New,
94 edit_index: Some(0),
95 }]
96 );
97 for (_update, mapping, _spec, edit) in
98 refs.iter_mapping_updates(&outcome.ref_map.mappings, remote.refspecs(Fetch))
99 {
100 let edit = edit.expect("refedit present even if it's a no-op");
101 if dry_run {
102 assert_eq!(
103 edit.change.new_value().expect("no deletions").id(),
104 mapping.remote.as_id()
105 );
106 assert!(
107 repo.try_find_reference(edit.name.as_ref())?.is_none(),
108 "no ref created in dry-run mode"
109 );
110 } else {
111 let r = repo.find_reference(edit.name.as_ref()).unwrap();
112 assert_eq!(
113 r.id(),
114 *mapping.remote.as_id(),
115 "local reference should point to remote id"
116 );
117 }
118 }
119 }
120 }
121 Ok(())
122 }
123}
git-repository/tests/remote/mod.rs+2−1
@@ -17,4 +17,5 @@ pub(crate) fn cow_str(s: &str) -> Cow<str> { · 16 unmodified lines
17}17}
18 18
19mod connect;19mod connect;
20mod list_refs;20mod fetch;
21mod ref_map;
git-repository/tests/remote/ref_map.rsrenamed from git-repository/tests/remote/list_refs.rs+2−3
@@ -24,8 +24,7 @@ mod blocking_io { · 23 unmodified lines
24 }24 }
25 25
26 let remote = repo.find_remote("origin")?;26 let remote = repo.find_remote("origin")?;
27 let connection = remote.connect(Fetch, progress::Discard)?;27 let map = remote.connect(Fetch, progress::Discard)?.ref_map(Default::default())?;
28 let map = connection.ref_map()?;
29 assert_eq!(28 assert_eq!(
30 map.remote_refs.len(),29 map.remote_refs.len(),
31 14,30 14,
@@ -36,7 +35,7 @@ mod blocking_io { · 4 unmodified lines
36 assert_eq!(35 assert_eq!(
37 map.mappings.len(),36 map.mappings.len(),
38 11,37 11,
39 "mappings are only a sub-set of all remotes due to refspec matching"38 "mappings are only a sub-set of all remotes due to refspec matching, tags are filtered out."
40 );39 );
41 }40 }
42 Ok(())41 Ok(())
git-repository/tests/repository/config/config_snapshot/mod.rs+44−0
@@ -1,5 +1,49 @@
1use crate::named_repo;1use crate::named_repo;
2 2
3#[test]
4fn commit_auto_rollback() -> crate::Result {
5 let mut repo: git_repository::Repository = named_repo("make_basic_repo.sh")?;
6 assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3");
7
8 {
9 let mut config = repo.config_snapshot_mut();
10 config.set_raw_value("core", None, "abbrev", "4")?;
11 let repo = config.commit_auto_rollback()?;
12 assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189");
13 }
14
15 assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3");
16
17 let repo = {
18 let mut config = repo.config_snapshot_mut();
19 config.set_raw_value("core", None, "abbrev", "4")?;
20 let repo = config.commit_auto_rollback()?;
21 assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189");
22 repo.rollback()?
23 };
24 assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3");
25
26 Ok(())
27}
28
29#[test]
30fn snapshot_mut_commit_and_forget() -> crate::Result {
31 let mut repo: git_repository::Repository = named_repo("make_basic_repo.sh")?;
32 let repo = {
33 let mut repo = repo.config_snapshot_mut();
34 repo.set_raw_value("core", None, "abbrev", "4")?;
35 repo.commit()?
36 };
37 assert_eq!(repo.config_snapshot().integer("core.abbrev").expect("set"), 4);
38 {
39 let mut repo = repo.config_snapshot_mut();
40 repo.set_raw_value("core", None, "abbrev", "8")?;
41 repo.forget();
42 }
43 assert_eq!(repo.config_snapshot().integer("core.abbrev"), Some(4));
44 Ok(())
45}
46
3#[test]47#[test]
4fn values_are_set_in_memory_only() {48fn values_are_set_in_memory_only() {
5 let mut repo = named_repo("make_config_repo.sh").unwrap();49 let mut repo = named_repo("make_config_repo.sh").unwrap();
git-repository/tests/repository/mod.rs+1−1
@@ -10,7 +10,7 @@ mod worktree; · 9 unmodified lines
10 10
11#[test]11#[test]
12fn size_in_memory() {12fn size_in_memory() {
13 let expected = [760, 800];13 let expected = [728, 744, 784];
14 let actual_size = std::mem::size_of::<Repository>();14 let actual_size = std::mem::size_of::<Repository>();
15 assert!(15 assert!(
16 expected.contains(&actual_size),16 expected.contains(&actual_size),
git-repository/tests/repository/object.rs+14−34
@@ -147,23 +147,17 @@ mod tag { · 146 unmodified lines
147}147}
148 148
149mod commit {149mod commit {
150 use crate::{freeze_time, restricted_and_git};
150 use git_repository as git;151 use git_repository as git;
151 use git_testtools::hex_to_id;152 use git_testtools::hex_to_id;
153
152 #[test]154 #[test]
153 fn parent_in_initial_commit_causes_failure() {155 fn parent_in_initial_commit_causes_failure() {
154 let tmp = tempfile::tempdir().unwrap();156 let tmp = tempfile::tempdir().unwrap();
155 let repo = git::init(&tmp).unwrap();157 let repo = git::init(&tmp).unwrap();
156 let empty_tree_id = repo.write_object(&git::objs::Tree::empty()).unwrap().detach();158 let empty_tree_id = repo.write_object(&git::objs::Tree::empty()).unwrap().detach();
157 let author = git::actor::Signature::empty();
158 let err = repo159 let err = repo
159 .commit(160 .commit("HEAD", "initial", empty_tree_id, [empty_tree_id])
160 "HEAD",
161 author.to_ref(),
162 author.to_ref(),
163 "initial",
164 empty_tree_id,
165 [empty_tree_id],
166 )
167 .unwrap_err();161 .unwrap_err();
168 assert_eq!(162 assert_eq!(
169 err.to_string(),163 err.to_string(),
@@ -173,22 +167,16 @@ mod commit { · 3 unmodified lines
173 }167 }
174 168
175 #[test]169 #[test]
170 #[serial_test::serial]
176 fn single_line_initial_commit_empty_tree_ref_nonexisting() -> crate::Result {171 fn single_line_initial_commit_empty_tree_ref_nonexisting() -> crate::Result {
172 let _env = freeze_time();
177 let tmp = tempfile::tempdir()?;173 let tmp = tempfile::tempdir()?;
178 let repo = git::init(&tmp)?;174 let repo = git::open_opts(git::init(&tmp)?.path(), restricted_and_git())?;
179 let empty_tree_id = repo.write_object(&git::objs::Tree::empty())?;175 let empty_tree_id = repo.write_object(&git::objs::Tree::empty())?;
180 let author = git::actor::Signature::empty();176 let commit_id = repo.commit("HEAD", "initial", empty_tree_id, git::commit::NO_PARENT_IDS)?;
181 let commit_id = repo.commit(
182 "HEAD",
183 author.to_ref(),
184 author.to_ref(),
185 "initial",
186 empty_tree_id,
187 git::commit::NO_PARENT_IDS,
188 )?;
189 assert_eq!(177 assert_eq!(
190 commit_id,178 commit_id,
191 hex_to_id("302ea5640358f98ba23cda66c1e664a6f274643f"),179 hex_to_id("3a774843723a713a8d361b4d4d98ad4092ef05bd"),
192 "the commit id is stable"180 "the commit id is stable"
193 );181 );
194 182
@@ -207,8 +195,10 @@ mod commit { · 12 unmodified lines
207 }195 }
208 196
209 #[test]197 #[test]
198 #[serial_test::serial]
210 fn multi_line_commit_message_uses_first_line_in_ref_log_ref_nonexisting() -> crate::Result {199 fn multi_line_commit_message_uses_first_line_in_ref_log_ref_nonexisting() -> crate::Result {
211 let (repo, _keep) = crate::basic_rw_repo()?;200 let _env = freeze_time();
201 let (repo, _keep) = crate::repo_rw_opts("make_basic_repo.sh", restricted_and_git())?;
212 let parent = repo.find_reference("HEAD")?.peel_to_id_in_place()?;202 let parent = repo.find_reference("HEAD")?.peel_to_id_in_place()?;
213 let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id().expect("tree to be set");203 let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id().expect("tree to be set");
214 assert_eq!(204 assert_eq!(
@@ -221,18 +211,10 @@ mod commit { · 6 unmodified lines
221 empty_tree_id,211 empty_tree_id,
222 "try and non-try work the same"212 "try and non-try work the same"
223 );213 );
224 let author = git::actor::Signature::empty();214 let first_commit_id = repo.commit("HEAD", "hello there \r\n\nthe body", empty_tree_id, Some(parent))?;
225 let first_commit_id = repo.commit(
226 "HEAD",
227 author.to_ref(),
228 author.to_ref(),
229 "hello there \r\n\nthe body",
230 empty_tree_id,
231 Some(parent),
232 )?;
233 assert_eq!(215 assert_eq!(
234 first_commit_id,216 first_commit_id,
235 hex_to_id("1ff7decccf76bfa15bfdb0b66bac0c9144b4b083"),217 hex_to_id("e7c7273539cfc1a52802fa9d61aa578f6ccebcb4"),
236 "the commit id is stable"218 "the commit id is stable"
237 );219 );
238 220
@@ -254,8 +236,6 @@ mod commit { · 15 unmodified lines
254 236
255 let second_commit_id = repo.commit(237 let second_commit_id = repo.commit(
256 "refs/heads/new-branch",238 "refs/heads/new-branch",
257 author.to_ref(),
258 author.to_ref(),
259 "committing into a new branch creates it",239 "committing into a new branch creates it",
260 empty_tree_id,240 empty_tree_id,
261 Some(first_commit_id),241 Some(first_commit_id),
@@ -263,7 +243,7 @@ mod commit { · 1 unmodified line
263 243
264 assert_eq!(244 assert_eq!(
265 second_commit_id,245 second_commit_id,
266 hex_to_id("b0d041ade77e51d31c79c7147fb769336ccc77b1"),246 hex_to_id("e1412f169e0812eb260601bdab3854ca0f1a7b33"),
267 "the second commit id is stable"247 "the second commit id is stable"
268 );248 );
269 249
git-repository/tests/repository/remote.rs+19−0
@@ -271,3 +271,22 @@ mod find_remote { · 270 unmodified lines
271 .to_string()271 .to_string()
272 }272 }
273}273}
274
275mod find_default_remote {
276 use crate::remote;
277 use git_repository as git;
278
279 #[test]
280 fn works_on_detached_heads() -> crate::Result {
281 let repo = remote::repo("detached-head");
282 assert_eq!(
283 repo.find_default_remote(git::remote::Direction::Fetch)
284 .transpose()?
285 .expect("present")
286 .name()
287 .expect("always named"),
288 "origin"
289 );
290 Ok(())
291 }
292}
git-repository/tests/util/mod.rs+22−2
@@ -2,6 +2,16 @@ use git_repository::{open, Repository, ThreadSafeRepository}; · 1 unmodified line
2 2
3pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;3pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
4 4
5pub fn freeze_time() -> git_testtools::Env<'static> {
6 let frozen_time = "1979-02-26 18:30:00";
7 git_testtools::Env::new()
8 .unset("GIT_AUTHOR_NAME")
9 .unset("GIT_AUTHOR_EMAIL")
10 .set("GIT_AUTHOR_DATE", frozen_time)
11 .unset("GIT_COMMITTER_NAME")
12 .unset("GIT_COMMITTER_EMAIL")
13 .set("GIT_COMMITTER_DATE", frozen_time)
14}
5pub fn repo(name: &str) -> Result<ThreadSafeRepository> {15pub fn repo(name: &str) -> Result<ThreadSafeRepository> {
6 let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?;16 let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?;
7 Ok(ThreadSafeRepository::open_opts(repo_path, restricted())?)17 Ok(ThreadSafeRepository::open_opts(repo_path, restricted())?)
@@ -16,15 +26,25 @@ pub fn restricted() -> open::Options { · 8 unmodified lines
16 open::Options::isolated()26 open::Options::isolated()
17}27}
18 28
29pub fn restricted_and_git() -> open::Options {
30 let mut opts = open::Options::isolated();
31 opts.permissions.env.git_prefix = git_sec::Permission::Allow;
32 opts
33}
34
19pub fn repo_rw(name: &str) -> Result<(Repository, tempfile::TempDir)> {35pub fn repo_rw(name: &str) -> Result<(Repository, tempfile::TempDir)> {
36 repo_rw_opts(name, restricted())
37}
38
39pub fn repo_rw_opts(name: &str, opts: git_repository::open::Options) -> Result<(Repository, tempfile::TempDir)> {
20 let repo_path = git_testtools::scripted_fixture_repo_writable(name)?;40 let repo_path = git_testtools::scripted_fixture_repo_writable(name)?;
21 Ok((41 Ok((
22 ThreadSafeRepository::discover_opts(42 ThreadSafeRepository::discover_opts(
23 repo_path.path(),43 repo_path.path(),
24 Default::default(),44 Default::default(),
25 git_sec::trust::Mapping {45 git_sec::trust::Mapping {
26 full: restricted(),46 full: opts.clone(),
27 reduced: restricted(),47 reduced: opts,
28 },48 },
29 )?49 )?
30 .to_thread_local(),50 .to_thread_local(),
git-transport/src/client/blocking_io/connect.rs+4−4
@@ -66,10 +66,10 @@ pub(crate) mod function { · 65 unmodified lines
66 #[cfg(not(feature = "http-client-curl"))]66 #[cfg(not(feature = "http-client-curl"))]
67 git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)),67 git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)),
68 #[cfg(feature = "http-client-curl")]68 #[cfg(feature = "http-client-curl")]
69 git_url::Scheme::Https | git_url::Scheme::Http => Box::new(69 git_url::Scheme::Https | git_url::Scheme::Http => Box::new(crate::client::http::connect(
70 crate::client::http::connect(&url.to_bstring().to_string(), desired_version)70 &url.to_bstring().to_string(),
71 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?,71 desired_version,
72 ),72 )),
73 })73 })
74 }74 }
75}75}
git-transport/src/client/blocking_io/file.rs+6−0
@@ -1,3 +1,5 @@
1use std::any::Any;
2use std::error::Error;
1use std::process::{self, Command, Stdio};3use std::process::{self, Command, Stdio};
2 4
3use bstr::{BString, ByteSlice};5use bstr::{BString, ByteSlice};
@@ -101,6 +103,10 @@ impl client::TransportWithoutIO for SpawnProcessOnDemand { · 97 unmodified lines
101 fn connection_persists_across_multiple_requests(&self) -> bool {103 fn connection_persists_across_multiple_requests(&self) -> bool {
102 true104 true
103 }105 }
106
107 fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
108 Ok(())
109 }
104}110}
105 111
106impl client::Transport for SpawnProcessOnDemand {112impl client::Transport for SpawnProcessOnDemand {
git-transport/src/client/blocking_io/http/curl/mod.rs+5−0
@@ -1,3 +1,4 @@
1use std::error::Error;
1use std::{2use std::{
2 sync::mpsc::{Receiver, SyncSender},3 sync::mpsc::{Receiver, SyncSender},
3 thread,4 thread,
@@ -100,4 +101,8 @@ impl crate::client::http::Http for Curl { · 96 unmodified lines
100 ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {101 ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {
101 self.make_request(url, headers, true)102 self.make_request(url, headers, true)
102 }103 }
104
105 fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
106 Ok(())
107 }
103}108}
git-transport/src/client/blocking_io/http/mod.rs+9−5
@@ -1,6 +1,6 @@
1use std::any::Any;
1use std::{2use std::{
2 borrow::Cow,3 borrow::Cow,
3 convert::Infallible,
4 io::{BufRead, Read},4 io::{BufRead, Read},
5};5};
6 6
@@ -169,6 +169,10 @@ impl<H: Http> client::TransportWithoutIO for Transport<H> { · 162 unmodified lines
169 fn connection_persists_across_multiple_requests(&self) -> bool {169 fn connection_persists_across_multiple_requests(&self) -> bool {
170 false170 false
171 }171 }
172
173 fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
174 self.http.configure(config)
175 }
172}176}
173 177
174impl<H: Http> client::Transport for Transport<H> {178impl<H: Http> client::Transport for Transport<H> {
@@ -297,12 +301,12 @@ impl<H: Http, B: ExtendedBufRead + Unpin> ExtendedBufRead for HeadersThenBody<H, · 122 unmodified lines
297 301
298/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation.302/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation.
299#[cfg(all(feature = "http-client", not(feature = "http-client-curl")))]303#[cfg(all(feature = "http-client", not(feature = "http-client-curl")))]
300pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Result<Transport<H>, Infallible> {304pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Transport<H> {
301 Ok(Transport::new_http(http, url, desired_version))305 Transport::new_http(http, url, desired_version)
302}306}
303 307
304/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol.308/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol.
305#[cfg(feature = "http-client-curl")]309#[cfg(feature = "http-client-curl")]
306pub fn connect(url: &str, desired_version: Protocol) -> Result<Transport<Impl>, Infallible> {310pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> {
307 Ok(Transport::new(url, desired_version))311 Transport::new(url, desired_version)
308}312}
git-transport/src/client/blocking_io/http/traits.rs+8−0
@@ -67,4 +67,12 @@ pub trait Http { · 66 unmodified lines
67 url: &str,67 url: &str,
68 headers: impl IntoIterator<Item = impl AsRef<str>>,68 headers: impl IntoIterator<Item = impl AsRef<str>>,
69 ) -> Result<PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, Error>;69 ) -> Result<PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, Error>;
70
71 /// Pass `config` which can deserialize in the implementation's configuration, as documented separately.
72 ///
73 /// The caller must know how that `config` data looks like for the intended implementation.
74 fn configure(
75 &mut self,
76 config: &dyn std::any::Any,
77 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
70}78}
git-transport/src/client/capabilities.rs+1−1
@@ -23,7 +23,7 @@ pub enum Error { · 22 unmodified lines
23}23}
24 24
25/// A structure to represent multiple [capabilities][Capability] or features supported by the server.25/// A structure to represent multiple [capabilities][Capability] or features supported by the server.
26#[derive(Debug, Clone)]26#[derive(Debug, Clone, Default)]
27#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]27#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
28pub struct Capabilities {28pub struct Capabilities {
29 data: BString,29 data: BString,
git-transport/src/client/git/async_io.rs+5−0
@@ -3,6 +3,7 @@ use bstr::BString; · 2 unmodified lines
3use futures_io::{AsyncRead, AsyncWrite};3use futures_io::{AsyncRead, AsyncWrite};
4use futures_lite::AsyncWriteExt;4use futures_lite::AsyncWriteExt;
5use git_packetline::PacketLineRef;5use git_packetline::PacketLineRef;
6use std::error::Error;
6 7
7use crate::{8use crate::{
8 client::{self, capabilities, git, Capabilities, SetServiceResponse},9 client::{self, capabilities, git, Capabilities, SetServiceResponse},
@@ -50,6 +51,10 @@ where · 41 unmodified lines
50 fn connection_persists_across_multiple_requests(&self) -> bool {51 fn connection_persists_across_multiple_requests(&self) -> bool {
51 true52 true
52 }53 }
54
55 fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
56 Ok(())
57 }
53}58}
54 59
55#[async_trait(?Send)]60#[async_trait(?Send)]
git-transport/src/client/git/blocking_io.rs+6−0
@@ -1,3 +1,5 @@
1use std::any::Any;
2use std::error::Error;
1use std::io::Write;3use std::io::Write;
2 4
3use bstr::BString;5use bstr::BString;
@@ -52,6 +54,10 @@ where · 48 unmodified lines
52 fn connection_persists_across_multiple_requests(&self) -> bool {54 fn connection_persists_across_multiple_requests(&self) -> bool {
53 true55 true
54 }56 }
57
58 fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
59 Ok(())
60 }
55}61}
56 62
57impl<R, W> client::Transport for git::Connection<R, W>63impl<R, W> client::Transport for git::Connection<R, W>
git-transport/src/client/traits.rs+14−0
@@ -1,3 +1,4 @@
1use std::any::Any;
1use std::ops::{Deref, DerefMut};2use std::ops::{Deref, DerefMut};
2 3
3#[cfg(any(feature = "blocking-client", feature = "async-client"))]4#[cfg(any(feature = "blocking-client", feature = "async-client"))]
@@ -46,6 +47,11 @@ pub trait TransportWithoutIO { · 42 unmodified lines
46 /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated47 /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated
47 /// to the server for most graceful termination of the connection.48 /// to the server for most graceful termination of the connection.
48 fn connection_persists_across_multiple_requests(&self) -> bool;49 fn connection_persists_across_multiple_requests(&self) -> bool;
50
51 /// Pass `config` can be cast and interpreted by the implementation, as documented separately.
52 ///
53 /// The caller must know how that `config` data looks like for the intended implementation.
54 fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
49}55}
50 56
51// Would be nice if the box implementation could auto-forward to all implemented traits.57// Would be nice if the box implementation could auto-forward to all implemented traits.
@@ -70,6 +76,10 @@ impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for Box<T> { · 18 unmodified lines
70 fn connection_persists_across_multiple_requests(&self) -> bool {76 fn connection_persists_across_multiple_requests(&self) -> bool {
71 self.deref().connection_persists_across_multiple_requests()77 self.deref().connection_persists_across_multiple_requests()
72 }78 }
79
80 fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
81 self.deref_mut().configure(config)
82 }
73}83}
74 84
75impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T {85impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T {
@@ -93,4 +103,8 @@ impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T { · 17 unmodified lines
93 fn connection_persists_across_multiple_requests(&self) -> bool {103 fn connection_persists_across_multiple_requests(&self) -> bool {
94 self.deref().connection_persists_across_multiple_requests()104 self.deref().connection_persists_across_multiple_requests()
95 }105 }
106
107 fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
108 self.deref_mut().configure(config)
109 }
96}110}
git-transport/tests/client/blocking_io/http/mock.rs+1−1
@@ -104,7 +104,7 @@ pub fn serve_and_connect( · 103 unmodified lines
104 &server.addr.port(),104 &server.addr.port(),
105 path105 path
106 );106 );
107 let client = git_transport::client::http::connect(&url, version)?;107 let client = git_transport::client::http::connect(&url, version);
108 assert_eq!(url, client.to_url());108 assert_eq!(url, client.to_url());
109 Ok((server, client))109 Ok((server, client))
110}110}
gitoxide-core/src/pack/index.rs+1−1
@@ -85,7 +85,7 @@ pub fn from_pack( · 84 unmodified lines
85 let options = pack::bundle::write::Options {85 let options = pack::bundle::write::Options {
86 thread_limit: ctx.thread_limit,86 thread_limit: ctx.thread_limit,
87 iteration_mode: ctx.iteration_mode.into(),87 iteration_mode: ctx.iteration_mode.into(),
88 index_kind: pack::index::Version::default(),88 index_version: pack::index::Version::default(),
89 object_hash: ctx.object_hash,89 object_hash: ctx.object_hash,
90 };90 };
91 let out = ctx.out;91 let out = ctx.out;
gitoxide-core/src/pack/receive.rs+3−3
@@ -246,7 +246,7 @@ pub use self::async_io::receive; · 245 unmodified lines
246 246
247#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]247#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
248pub struct JsonBundleWriteOutcome {248pub struct JsonBundleWriteOutcome {
249 pub index_kind: pack::index::Version,249 pub index_version: pack::index::Version,
250 pub index_hash: String,250 pub index_hash: String,
251 251
252 pub data_hash: String,252 pub data_hash: String,
@@ -256,7 +256,7 @@ pub struct JsonBundleWriteOutcome { · 3 unmodified lines
256impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome {256impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome {
257 fn from(v: pack::index::write::Outcome) -> Self {257 fn from(v: pack::index::write::Outcome) -> Self {
258 JsonBundleWriteOutcome {258 JsonBundleWriteOutcome {
259 index_kind: v.index_kind,259 index_version: v.index_version,
260 num_objects: v.num_objects,260 num_objects: v.num_objects,
261 data_hash: v.data_hash.to_string(),261 data_hash: v.data_hash.to_string(),
262 index_hash: v.index_hash.to_string(),262 index_hash: v.index_hash.to_string(),
@@ -340,7 +340,7 @@ fn receive_pack_blocking<W: io::Write>( · 77 unmodified lines
340) -> io::Result<()> {340) -> io::Result<()> {
341 let options = pack::bundle::write::Options {341 let options = pack::bundle::write::Options {
342 thread_limit: ctx.thread_limit,342 thread_limit: ctx.thread_limit,
343 index_kind: pack::index::Version::V2,343 index_version: pack::index::Version::V2,
344 iteration_mode: pack::data::input::Mode::Verify,344 iteration_mode: pack::data::input::Mode::Verify,
345 object_hash: ctx.object_hash,345 object_hash: ctx.object_hash,
346 };346 };
gitoxide-core/src/repository/remote.rs+13−1
@@ -73,7 +73,10 @@ mod refs_impl { · 72 unmodified lines
73 let map = remote73 let map = remote
74 .connect(git::remote::Direction::Fetch, progress)74 .connect(git::remote::Direction::Fetch, progress)
75 .await?75 .await?
76 .ref_map()76 .ref_map(git::remote::ref_map::Options {
77 prefix_from_spec_as_filter_on_remote: !matches!(kind, refs::Kind::Remote),
78 ..Default::default()
79 })
77 .await?;80 .await?;
78 81
79 if handshake_info {82 if handshake_info {
@@ -164,6 +167,15 @@ mod refs_impl { · 84 unmodified lines
164 }167 }
165 }168 }
166 }169 }
170 if map.remote_refs.len() - map.mappings.len() != 0 {
171 writeln!(
172 err,
173 "server sent {} tips, {} were filtered due to {} refspec(s).",
174 map.remote_refs.len(),
175 map.remote_refs.len() - map.mappings.len(),
176 refspecs.len()
177 )?;
178 }
167 Ok(())179 Ok(())
168 }180 }
169 181
src/plumbing/main.rs+4−2
@@ -14,8 +14,9 @@ use gitoxide_core as core; · 13 unmodified lines
14use gitoxide_core::pack::verify;14use gitoxide_core::pack::verify;
15 15
16use crate::{16use crate::{
17 plumbing::options::{17 plumbing::{
18 commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands,18 options::{commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands},
19 show_progress,
19 },20 },
20 shared::pretty::prepare_and_run,21 shared::pretty::prepare_and_run,
21};22};
@@ -111,6 +112,7 @@ pub fn main() -> Result<()> { · 89 unmodified lines
111 })?;112 })?;
112 113
113 match cmd {114 match cmd {
115 Subcommands::Progress => show_progress(),
114 Subcommands::Credential(cmd) => core::repository::credential(116 Subcommands::Credential(cmd) => core::repository::credential(
115 repository(Mode::StrictWithGitInstallConfig)?,117 repository(Mode::StrictWithGitInstallConfig)?,
116 match cmd {118 match cmd {
src/plumbing/mod.rs+4−0
@@ -1,4 +1,8 @@
1mod main;1mod main;
2pub use main::main;2pub use main::main;
3 3
4#[path = "progress.rs"]
5mod progress_impl;
6pub use progress_impl::show_progress;
7
4mod options;8mod options;
src/plumbing/options.rs+0−677
@@ -1,677 +0,0 @@
1use std::path::PathBuf;
2
3use git_repository as git;
4use git_repository::bstr::BString;
5use gitoxide_core as core;
6
7#[derive(Debug, clap::Parser)]
8#[clap(name = "gix-plumbing", about = "The git underworld", version = clap::crate_version!())]
9#[clap(subcommand_required = true)]
10#[clap(arg_required_else_help = true)]
11pub struct Args {
12 /// The repository to access.
13 #[clap(short = 'r', long, default_value = ".")]
14 pub repository: PathBuf,
15
16 /// Add these values to the configuration in the form of `key=value` or `key`.
17 ///
18 /// For example, if `key` is `core.abbrev`, set configuration like `[core] abbrev = key`,
19 /// or `remote.origin.url = foo` to set `[remote "origin"] url = foo`.
20 #[clap(long, short = 'c', parse(try_from_os_str = git::env::os_str_to_bstring))]
21 pub config: Vec<BString>,
22
23 #[clap(long, short = 't')]
24 /// The amount of threads to use for some operations.
25 ///
26 /// If unset, or the value is 0, there is no limit and all logical cores can be used.
27 pub threads: Option<usize>,
28
29 /// Display verbose messages and progress information
30 #[clap(long, short = 'v')]
31 pub verbose: bool,
32
33 /// Bring up a terminal user interface displaying progress visually
34 #[cfg(feature = "prodash-render-tui")]
35 #[clap(long, conflicts_with("verbose"))]
36 pub progress: bool,
37
38 /// The progress TUI will stay up even though the work is already completed.
39 ///
40 /// Use this to be able to read progress messages or additional information visible in the TUI log pane.
41 #[cfg(feature = "prodash-render-tui")]
42 #[clap(long, conflicts_with("verbose"), requires("progress"))]
43 pub progress_keep_open: bool,
44
45 /// Determine the format to use when outputting statistics.
46 #[clap(
47 long,
48 short = 'f',
49 default_value = "human",
50 possible_values(core::OutputFormat::variants())
51 )]
52 pub format: core::OutputFormat,
53
54 /// The object format to assume when reading files that don't inherently know about it, or when writing files.
55 #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
56 pub object_hash: git_repository::hash::Kind,
57
58 #[clap(subcommand)]
59 pub cmd: Subcommands,
60}
61
62#[derive(Debug, clap::Subcommand)]
63pub enum Subcommands {
64 /// Interact with the object database.
65 #[clap(subcommand)]
66 Odb(odb::Subcommands),
67 /// Interact with tree objects.
68 #[clap(subcommand)]
69 Tree(tree::Subcommands),
70 /// Interact with commit objects.
71 #[clap(subcommand)]
72 Commit(commit::Subcommands),
73 /// Verify the integrity of the entire repository
74 Verify {
75 #[clap(flatten)]
76 args: free::pack::VerifyOptions,
77 },
78 /// Query and obtain information about revisions.
79 #[clap(subcommand)]
80 Revision(revision::Subcommands),
81 /// A program just like `git credential`.
82 #[clap(subcommand)]
83 Credential(credential::Subcommands),
84 /// Interact with the mailmap.
85 #[clap(subcommand)]
86 Mailmap(mailmap::Subcommands),
87 /// Interact with the remote hosts.
88 Remote(remote::Platform),
89 /// Interact with the exclude files like .gitignore.
90 #[clap(subcommand)]
91 Exclude(exclude::Subcommands),
92 Config(config::Platform),
93 /// Subcommands that need no git repository to run.
94 #[clap(subcommand)]
95 Free(free::Subcommands),
96}
97
98pub mod config {
99 /// Print all entries in a configuration file or access other sub-commands
100 #[derive(Debug, clap::Parser)]
101 #[clap(subcommand_required(false))]
102 pub struct Platform {
103 /// The filter terms to limit the output to matching sections and subsections only.
104 ///
105 /// Typical filters are `branch` or `remote.origin` or `remote.or*` - git-style globs are supported
106 /// and comparisons are case-insensitive.
107 pub filter: Vec<String>,
108 }
109}
110
111pub mod remote {
112 use git_repository as git;
113
114 #[derive(Debug, clap::Parser)]
115 pub struct Platform {
116 /// The name of the remote to connect to.
117 ///
118 /// If unset, the current branch will determine the remote.
119 #[clap(long, short = 'n')]
120 pub name: Option<String>,
121
122 /// Connect directly to the given URL, forgoing any configuration from the repository.
123 #[clap(long, short = 'u', conflicts_with("name"), parse(try_from_os_str = std::convert::TryFrom::try_from))]
124 pub url: Option<git::Url>,
125
126 /// Output additional typically information provided by the server as part of the connection handshake.
127 #[clap(long)]
128 pub handshake_info: bool,
129
130 /// Subcommands
131 #[clap(subcommand)]
132 pub cmd: Subcommands,
133 }
134
135 #[derive(Debug, clap::Subcommand)]
136 #[clap(visible_alias = "remotes")]
137 pub enum Subcommands {
138 /// Print all references available on the remote.
139 #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
140 Refs,
141 /// Print all references available on the remote as filtered through ref-specs.
142 #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
143 RefMap {
144 /// Override the built-in and configured ref-specs with one or more of the given ones.
145 #[clap(parse(try_from_os_str = git::env::os_str_to_bstring))]
146 ref_spec: Vec<git_repository::bstr::BString>,
147 },
148 }
149}
150
151pub mod mailmap {
152 #[derive(Debug, clap::Subcommand)]
153 pub enum Subcommands {
154 /// Print all entries in configured mailmaps, inform about errors as well.
155 Entries,
156 }
157}
158
159pub mod odb {
160 #[derive(Debug, clap::Subcommand)]
161 pub enum Subcommands {
162 /// Print all object names.
163 Entries,
164 /// Provide general information about the object database.
165 Info,
166 }
167}
168
169pub mod tree {
170 #[derive(Debug, clap::Subcommand)]
171 pub enum Subcommands {
172 /// Print entries in a given tree
173 Entries {
174 /// Traverse the entire tree and its subtrees respectively, not only this tree.
175 #[clap(long, short = 'r')]
176 recursive: bool,
177
178 /// Provide files size as well. This is expensive as the object is decoded entirely.
179 #[clap(long, short = 'e')]
180 extended: bool,
181
182 /// The tree to traverse, or the tree at `HEAD` if unspecified.
183 treeish: Option<String>,
184 },
185 /// Provide information about a tree.
186 Info {
187 /// Provide files size as well. This is expensive as the object is decoded entirely.
188 #[clap(long, short = 'e')]
189 extended: bool,
190 /// The tree to traverse, or the tree at `HEAD` if unspecified.
191 treeish: Option<String>,
192 },
193 }
194}
195
196pub mod commit {
197 #[derive(Debug, clap::Subcommand)]
198 pub enum Subcommands {
199 /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
200 Describe {
201 /// Use annotated tag references only, not all tags.
202 #[clap(long, short = 't', conflicts_with("all-refs"))]
203 annotated_tags: bool,
204
205 /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches.
206 #[clap(long, short = 'a', conflicts_with("annotated-tags"))]
207 all_refs: bool,
208
209 /// Only follow the first parent when traversing the commit graph.
210 #[clap(long, short = 'f')]
211 first_parent: bool,
212
213 /// Always display the long format, even if that would not be necessary as the id is located directly on a reference.
214 #[clap(long, short = 'l')]
215 long: bool,
216
217 /// Consider only the given `n` candidates. This can take longer, but potentially produces more accurate results.
218 #[clap(long, short = 'c', default_value = "10")]
219 max_candidates: usize,
220
221 /// Print information on stderr to inform about performance statistics
222 #[clap(long, short = 's')]
223 statistics: bool,
224
225 #[clap(long)]
226 /// If there was no way to describe the commit, fallback to using the abbreviated input revision.
227 always: bool,
228
229 /// A specification of the revision to use, or the current `HEAD` if unset.
230 rev_spec: Option<String>,
231 },
232 }
233}
234
235pub mod credential {
236 #[derive(Debug, clap::Subcommand)]
237 pub enum Subcommands {
238 /// Get the credentials fed for `url=<url>` via STDIN.
239 #[clap(visible_alias = "get")]
240 Fill,
241 /// Approve the information piped via STDIN as obtained with last call to `fill`
242 #[clap(visible_alias = "store")]
243 Approve,
244 /// Try to resolve the given revspec and print the object names.
245 #[clap(visible_alias = "erase")]
246 Reject,
247 }
248}
249
250pub mod revision {
251 #[derive(Debug, clap::Subcommand)]
252 #[clap(visible_alias = "rev", visible_alias = "r")]
253 pub enum Subcommands {
254 /// List all commits reachable from the given rev-spec.
255 #[clap(visible_alias = "l")]
256 List { spec: std::ffi::OsString },
257 /// Provide the revision specification like `@~1` to explain.
258 #[clap(visible_alias = "e")]
259 Explain { spec: std::ffi::OsString },
260 /// Try to resolve the given revspec and print the object names.
261 #[clap(visible_alias = "query", visible_alias = "parse", visible_alias = "p")]
262 Resolve {
263 /// Instead of resolving a rev-spec, explain what would be done for the first spec.
264 ///
265 /// Equivalent to the `explain` subcommand.
266 #[clap(short = 'e', long)]
267 explain: bool,
268 /// Show the first resulting object similar to how `git cat-file` would, but don't show the resolved spec.
269 #[clap(short = 'c', long, conflicts_with = "explain")]
270 cat_file: bool,
271 /// rev-specs like `@`, `@~1` or `HEAD^2`.
272 #[clap(required = true)]
273 specs: Vec<std::ffi::OsString>,
274 },
275 /// Return the names and hashes of all previously checked-out branches.
276 #[clap(visible_alias = "prev")]
277 PreviousBranches,
278 }
279}
280
281///
282pub mod free {
283 #[derive(Debug, clap::Subcommand)]
284 #[clap(visible_alias = "no-repo")]
285 pub enum Subcommands {
286 /// Subcommands for interacting with commit-graphs
287 #[clap(subcommand)]
288 CommitGraph(commitgraph::Subcommands),
289 /// Subcommands for interacting with mailmaps
290 Mailmap {
291 #[clap(flatten)]
292 cmd: mailmap::Platform,
293 },
294 /// Subcommands for interacting with pack files and indices
295 #[clap(subcommand)]
296 Pack(pack::Subcommands),
297 /// Subcommands for interacting with a worktree index, typically at .git/index
298 Index(index::Platform),
299 }
300
301 ///
302 pub mod commitgraph {
303 use std::path::PathBuf;
304
305 #[derive(Debug, clap::Subcommand)]
306 pub enum Subcommands {
307 /// Verify the integrity of a commit graph
308 Verify {
309 /// The path to '.git/objects/info/', '.git/objects/info/commit-graphs/', or '.git/objects/info/commit-graph' to validate.
310 path: PathBuf,
311 /// output statistical information about the pack
312 #[clap(long, short = 's')]
313 statistics: bool,
314 },
315 }
316 }
317
318 pub mod index {
319 use std::path::PathBuf;
320
321 #[derive(Debug, clap::Parser)]
322 pub struct Platform {
323 /// The object format to assume when reading files that don't inherently know about it, or when writing files.
324 #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
325 pub object_hash: git_repository::hash::Kind,
326
327 /// The path to the index file.
328 #[clap(short = 'i', long, default_value = ".git/index")]
329 pub index_path: PathBuf,
330
331 /// Subcommands
332 #[clap(subcommand)]
333 pub cmd: Subcommands,
334 }
335
336 #[derive(Debug, clap::Subcommand)]
337 pub enum Subcommands {
338 /// Validate constraints and assumptions of an index along with its integrity.
339 Verify,
340 /// Print all entries to standard output
341 Entries,
342 /// Print information about the index structure
343 Info {
344 /// Do not extract specific extension information to gain only a superficial idea of the index's composition.
345 #[clap(long)]
346 no_details: bool,
347 },
348 /// Checkout the index into a directory with exclusive write access, similar to what would happen during clone.
349 CheckoutExclusive {
350 /// The path to `.git` repository from which objects can be obtained to write the actual files referenced
351 /// in the index. Use this measure the impact on extracting objects on overall performance.
352 #[clap(long, short = 'r')]
353 repository: Option<PathBuf>,
354 /// Ignore errors and keep checking out as many files as possible, and report all errors at the end of the operation.
355 #[clap(long, short = 'k')]
356 keep_going: bool,
357 /// Enable to query the object database yet write only empty files. This is useful to measure the overhead of ODB query
358 /// compared to writing the bytes to disk.
359 #[clap(long, short = 'e', requires = "repository")]
360 empty_files: bool,
361 /// The directory into which to write all index entries.
362 directory: PathBuf,
363 },
364 }
365 }
366
367 ///
368 pub mod pack {
369 use std::{ffi::OsString, path::PathBuf};
370
371 use gitoxide_core as core;
372
373 #[derive(Debug, clap::Subcommand)]
374 pub enum Subcommands {
375 /// Subcommands for interacting with pack indices (.idx)
376 #[clap(subcommand)]
377 Index(index::Subcommands),
378 /// Subcommands for interacting with multi-pack indices (named "multi-pack-index")
379 MultiIndex(multi_index::Platform),
380 /// Create a new pack with a set of objects.
381 Create {
382 #[clap(long, short = 'r')]
383 /// the directory containing the '.git' repository from which objects should be read.
384 repository: Option<PathBuf>,
385
386 #[clap(long, short = 'e', possible_values(core::pack::create::ObjectExpansion::variants()))]
387 /// the way objects are expanded. They differ in costs.
388 ///
389 /// Possible values are "none" and "tree-traversal". Default is "none".
390 expansion: Option<core::pack::create::ObjectExpansion>,
391
392 #[clap(long, default_value_t = 3, requires = "nondeterministic-count")]
393 /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting
394 /// to the globally configured threads.
395 ///
396 /// Use it to have different trade-offs between counting performance and cost in terms of CPU, as the scaling
397 /// here is everything but linear. The effectiveness of each core seems to be no more than 30%.
398 counting_threads: usize,
399
400 #[clap(long)]
401 /// if set, the counting phase may be accelerated using multithreading.
402 ///
403 /// On the flip side, however, one will loose deterministic counting results which affects the
404 /// way the resulting pack is structured.
405 nondeterministic_count: bool,
406
407 #[clap(long, short = 's')]
408 /// If set statistical information will be presented to inform about pack creation details.
409 /// It's a form of instrumentation for developers to help improve pack generation.
410 statistics: bool,
411
412 #[clap(long)]
413 /// The size in megabytes for a cache to speed up pack access for packs with long delta chains.
414 /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
415 ///
416 /// If unset, no cache will be used.
417 pack_cache_size_mb: Option<usize>,
418
419 #[clap(long)]
420 /// The size in megabytes for a cache to speed up accessing entire objects, bypassing object database access when hit.
421 /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
422 ///
423 /// This cache type is currently only effective when using the 'diff-tree' object expansion.
424 ///
425 /// If unset, no cache will be used.
426 object_cache_size_mb: Option<usize>,
427
428 #[clap(long)]
429 /// if set, delta-objects whose base object wouldn't be in the pack will not be recompressed as base object, but instead
430 /// refer to its base object using its object id.
431 ///
432 /// This allows for smaller packs but requires the receiver of the pack to resolve these ids before storing the pack.
433 /// Packs produced with this option enabled are only valid in transit, but not at rest.
434 thin: bool,
435
436 /// The directory into which to write the pack file.
437 #[clap(long, short = 'o')]
438 output_directory: Option<PathBuf>,
439
440 /// The tips from which to start the commit graph iteration, either as fully qualified commit hashes
441 /// or as branch names.
442 ///
443 /// If empty, we expect to read objects on stdin and default to 'none' as expansion mode.
444 /// Otherwise the expansion mode is 'tree-traversal' by default.
445 tips: Vec<OsString>,
446 },
447 /// Use the git-protocol to receive a pack, emulating a clone.
448 #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
449 Receive {
450 /// The protocol version to use. Valid values are 1 and 2
451 #[clap(long, short = 'p')]
452 protocol: Option<core::net::Protocol>,
453
454 /// the directory into which to write references. Existing files will be overwritten.
455 ///
456 /// Note that the directory will be created if needed.
457 #[clap(long, short = 'd')]
458 refs_directory: Option<PathBuf>,
459
460 /// The URLs or path from which to receive the pack.
461 ///
462 /// See here for a list of supported URLs: <https://www.git-scm.com/docs/git-clone#_git_urls>
463 url: String,
464
465 /// If set once or more times, these references will be fetched instead of all advertised ones.
466 ///
467 /// Note that this requires a reasonably modern git server.
468 #[clap(long = "reference", short = 'r')]
469 refs: Vec<String>,
470
471 /// The directory into which to write the received pack and index.
472 ///
473 /// If unset, they will be discarded.
474 directory: Option<PathBuf>,
475 },
476 /// Dissolve a pack into its loose objects.
477 ///
478 /// Note that this effectively removes delta compression for an average compression of 2x, creating one file per object in the process.
479 /// Thus this should only be done to dissolve small packs after a fetch.
480 Explode {
481 #[clap(long)]
482 /// Read written objects back and assert they match their source. Fail the operation otherwise.
483 ///
484 /// Only relevant if an object directory is set.
485 verify: bool,
486
487 /// delete the pack and index file after the operation is successful
488 #[clap(long)]
489 delete_pack: bool,
490
491 /// The amount of checks to run
492 #[clap(
493 long,
494 short = 'c',
495 default_value = "all",
496 possible_values(core::pack::explode::SafetyCheck::variants())
497 )]
498 check: core::pack::explode::SafetyCheck,
499
500 /// Compress bytes even when using the sink, i.e. no object directory is specified
501 ///
502 /// This helps to determine overhead related to compression. If unset, the sink will
503 /// only create hashes from bytes, which is usually limited by the speed at which input
504 /// can be obtained.
505 #[clap(long)]
506 sink_compress: bool,
507
508 /// The '.pack' or '.idx' file to explode into loose objects
509 pack_path: PathBuf,
510
511 /// The path into which all objects should be written. Commonly '.git/objects'
512 object_path: Option<PathBuf>,
513 },
514 /// Verify the integrity of a pack, index or multi-index file
515 Verify {
516 #[clap(flatten)]
517 args: VerifyOptions,
518
519 /// The '.pack', '.idx' or 'multi-pack-index' file to validate.
520 path: PathBuf,
521 },
522 }
523
524 #[derive(Debug, clap::Parser)]
525 pub struct VerifyOptions {
526 /// output statistical information
527 #[clap(long, short = 's')]
528 pub statistics: bool,
529 /// The algorithm used to verify packs. They differ in costs.
530 #[clap(
531 long,
532 short = 'a',
533 default_value = "less-time",
534 possible_values(core::pack::verify::Algorithm::variants())
535 )]
536 pub algorithm: core::pack::verify::Algorithm,
537
538 #[clap(long, conflicts_with("re-encode"))]
539 /// Decode and parse tags, commits and trees to validate their correctness beyond hashing correctly.
540 ///
541 /// Malformed objects should not usually occur, but could be injected on purpose or accident.
542 /// This will reduce overall performance.
543 pub decode: bool,
544
545 #[clap(long)]
546 /// Decode and parse tags, commits and trees to validate their correctness, and re-encode them.
547 ///
548 /// This flag is primarily to test the implementation of encoding, and requires to decode the object first.
549 /// Encoding an object after decoding it should yield exactly the same bytes.
550 /// This will reduce overall performance even more, as re-encoding requires to transform zero-copy objects into
551 /// owned objects, causing plenty of allocation to occur.
552 pub re_encode: bool,
553 }
554
555 ///
556 pub mod multi_index {
557 use std::path::PathBuf;
558
559 #[derive(Debug, clap::Parser)]
560 pub struct Platform {
561 /// The path to the index file.
562 #[clap(short = 'i', long, default_value = ".git/objects/pack/multi-pack-index")]
563 pub multi_index_path: PathBuf,
564
565 /// Subcommands
566 #[clap(subcommand)]
567 pub cmd: Subcommands,
568 }
569
570 #[derive(Debug, clap::Subcommand)]
571 pub enum Subcommands {
572 /// Display all entries of a multi-index: <oid> <pack-id> <pack-offset>
573 Entries,
574 /// Print general information about a multi-index file
575 Info,
576 /// Verify a multi-index quickly without inspecting objects themselves
577 Verify,
578 /// Create a multi-pack index from one or more pack index files, overwriting possibloy existing files.
579 Create {
580 /// Paths to the pack index files to read (with .idx extension).
581 ///
582 /// Note for the multi-index to be useful, it should be side-by-side with the supplied `.idx` files.
583 #[clap(required = true)]
584 index_paths: Vec<PathBuf>,
585 },
586 }
587 }
588
589 ///
590 pub mod index {
591 use std::path::PathBuf;
592
593 use gitoxide_core as core;
594
595 #[derive(Debug, clap::Subcommand)]
596 pub enum Subcommands {
597 /// create a pack index from a pack data file.
598 Create {
599 /// Specify how to iterate the pack, defaults to 'verify'
600 ///
601 /// Valid values are
602 ///
603 /// **as-is** do not do anything and expect the pack file to be valid as per the trailing hash,
604 /// **verify** the input ourselves and validate that it matches with the hash provided in the pack,
605 /// **restore** hash the input ourselves and ignore failing entries, instead finish the pack with the hash we computed
606 /// to keep as many objects as possible.
607 #[clap(
608 long,
609 short = 'i',
610 default_value = "verify",
611 possible_values(core::pack::index::IterationMode::variants())
612 )]
613 iteration_mode: core::pack::index::IterationMode,
614
615 /// Path to the pack file to read (with .pack extension).
616 ///
617 /// If unset, the pack file is expected on stdin.
618 #[clap(long, short = 'p')]
619 pack_path: Option<PathBuf>,
620
621 /// The folder into which to place the pack and the generated index file
622 ///
623 /// If unset, only informational output will be provided to standard output.
624 directory: Option<PathBuf>,
625 },
626 }
627 }
628 }
629
630 ///
631 pub mod mailmap {
632 use std::path::PathBuf;
633
634 #[derive(Debug, clap::Parser)]
635 pub struct Platform {
636 /// The path to the mailmap file.
637 #[clap(short = 'p', long, default_value = ".mailmap")]
638 pub path: PathBuf,
639
640 /// Subcommands
641 #[clap(subcommand)]
642 pub cmd: Subcommands,
643 }
644
645 #[derive(Debug, clap::Subcommand)]
646 pub enum Subcommands {
647 /// Parse all entries in the mailmap and report malformed lines or collisions.
648 Verify,
649 }
650 }
651}
652
653pub mod exclude {
654 use std::ffi::OsString;
655
656 use git_repository as git;
657
658 #[derive(Debug, clap::Subcommand)]
659 pub enum Subcommands {
660 /// Check if path-specs are excluded and print the result similar to `git check-ignore`.
661 Query {
662 /// Show actual ignore patterns instead of un-excluding an entry.
663 ///
664 /// That way one can understand why an entry might not be excluded.
665 #[clap(long, short = 'i')]
666 show_ignore_patterns: bool,
667 /// Additional patterns to use for exclusions. They have the highest priority.
668 ///
669 /// Useful for undoing previous patterns using the '!' prefix.
670 #[clap(long, short = 'p')]
671 patterns: Vec<OsString>,
672 /// The git path specifications to check for exclusion, or unset to read from stdin one per line.
673 #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))]
674 pathspecs: Vec<git::path::Spec>,
675 },
676 }
677}
src/plumbing/options/free.rs+368−0
@@ -0,0 +1,368 @@
1#[derive(Debug, clap::Subcommand)]
2#[clap(visible_alias = "no-repo")]
3pub enum Subcommands {
4 /// Subcommands for interacting with commit-graphs
5 #[clap(subcommand)]
6 CommitGraph(commitgraph::Subcommands),
7 /// Subcommands for interacting with mailmaps
8 Mailmap {
9 #[clap(flatten)]
10 cmd: mailmap::Platform,
11 },
12 /// Subcommands for interacting with pack files and indices
13 #[clap(subcommand)]
14 Pack(pack::Subcommands),
15 /// Subcommands for interacting with a worktree index, typically at .git/index
16 Index(index::Platform),
17}
18
19///
20pub mod commitgraph {
21 use std::path::PathBuf;
22
23 #[derive(Debug, clap::Subcommand)]
24 pub enum Subcommands {
25 /// Verify the integrity of a commit graph
26 Verify {
27 /// The path to '.git/objects/info/', '.git/objects/info/commit-graphs/', or '.git/objects/info/commit-graph' to validate.
28 path: PathBuf,
29 /// output statistical information about the pack
30 #[clap(long, short = 's')]
31 statistics: bool,
32 },
33 }
34}
35
36pub mod index {
37 use std::path::PathBuf;
38
39 #[derive(Debug, clap::Parser)]
40 pub struct Platform {
41 /// The object format to assume when reading files that don't inherently know about it, or when writing files.
42 #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
43 pub object_hash: git_repository::hash::Kind,
44
45 /// The path to the index file.
46 #[clap(short = 'i', long, default_value = ".git/index")]
47 pub index_path: PathBuf,
48
49 /// Subcommands
50 #[clap(subcommand)]
51 pub cmd: Subcommands,
52 }
53
54 #[derive(Debug, clap::Subcommand)]
55 pub enum Subcommands {
56 /// Validate constraints and assumptions of an index along with its integrity.
57 Verify,
58 /// Print all entries to standard output
59 Entries,
60 /// Print information about the index structure
61 Info {
62 /// Do not extract specific extension information to gain only a superficial idea of the index's composition.
63 #[clap(long)]
64 no_details: bool,
65 },
66 /// Checkout the index into a directory with exclusive write access, similar to what would happen during clone.
67 CheckoutExclusive {
68 /// The path to `.git` repository from which objects can be obtained to write the actual files referenced
69 /// in the index. Use this measure the impact on extracting objects on overall performance.
70 #[clap(long, short = 'r')]
71 repository: Option<PathBuf>,
72 /// Ignore errors and keep checking out as many files as possible, and report all errors at the end of the operation.
73 #[clap(long, short = 'k')]
74 keep_going: bool,
75 /// Enable to query the object database yet write only empty files. This is useful to measure the overhead of ODB query
76 /// compared to writing the bytes to disk.
77 #[clap(long, short = 'e', requires = "repository")]
78 empty_files: bool,
79 /// The directory into which to write all index entries.
80 directory: PathBuf,
81 },
82 }
83}
84
85///
86pub mod pack {
87 use std::{ffi::OsString, path::PathBuf};
88
89 use gitoxide_core as core;
90
91 #[derive(Debug, clap::Subcommand)]
92 pub enum Subcommands {
93 /// Subcommands for interacting with pack indices (.idx)
94 #[clap(subcommand)]
95 Index(index::Subcommands),
96 /// Subcommands for interacting with multi-pack indices (named "multi-pack-index")
97 MultiIndex(multi_index::Platform),
98 /// Create a new pack with a set of objects.
99 Create {
100 #[clap(long, short = 'r')]
101 /// the directory containing the '.git' repository from which objects should be read.
102 repository: Option<PathBuf>,
103
104 #[clap(long, short = 'e', possible_values(core::pack::create::ObjectExpansion::variants()))]
105 /// the way objects are expanded. They differ in costs.
106 ///
107 /// Possible values are "none" and "tree-traversal". Default is "none".
108 expansion: Option<core::pack::create::ObjectExpansion>,
109
110 #[clap(long, default_value_t = 3, requires = "nondeterministic-count")]
111 /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting
112 /// to the globally configured threads.
113 ///
114 /// Use it to have different trade-offs between counting performance and cost in terms of CPU, as the scaling
115 /// here is everything but linear. The effectiveness of each core seems to be no more than 30%.
116 counting_threads: usize,
117
118 #[clap(long)]
119 /// if set, the counting phase may be accelerated using multithreading.
120 ///
121 /// On the flip side, however, one will loose deterministic counting results which affects the
122 /// way the resulting pack is structured.
123 nondeterministic_count: bool,
124
125 #[clap(long, short = 's')]
126 /// If set statistical information will be presented to inform about pack creation details.
127 /// It's a form of instrumentation for developers to help improve pack generation.
128 statistics: bool,
129
130 #[clap(long)]
131 /// The size in megabytes for a cache to speed up pack access for packs with long delta chains.
132 /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
133 ///
134 /// If unset, no cache will be used.
135 pack_cache_size_mb: Option<usize>,
136
137 #[clap(long)]
138 /// The size in megabytes for a cache to speed up accessing entire objects, bypassing object database access when hit.
139 /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
140 ///
141 /// This cache type is currently only effective when using the 'diff-tree' object expansion.
142 ///
143 /// If unset, no cache will be used.
144 object_cache_size_mb: Option<usize>,
145
146 #[clap(long)]
147 /// if set, delta-objects whose base object wouldn't be in the pack will not be recompressed as base object, but instead
148 /// refer to its base object using its object id.
149 ///
150 /// This allows for smaller packs but requires the receiver of the pack to resolve these ids before storing the pack.
151 /// Packs produced with this option enabled are only valid in transit, but not at rest.
152 thin: bool,
153
154 /// The directory into which to write the pack file.
155 #[clap(long, short = 'o')]
156 output_directory: Option<PathBuf>,
157
158 /// The tips from which to start the commit graph iteration, either as fully qualified commit hashes
159 /// or as branch names.
160 ///
161 /// If empty, we expect to read objects on stdin and default to 'none' as expansion mode.
162 /// Otherwise the expansion mode is 'tree-traversal' by default.
163 tips: Vec<OsString>,
164 },
165 /// Use the git-protocol to receive a pack, emulating a clone.
166 #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
167 Receive {
168 /// The protocol version to use. Valid values are 1 and 2
169 #[clap(long, short = 'p')]
170 protocol: Option<core::net::Protocol>,
171
172 /// the directory into which to write references. Existing files will be overwritten.
173 ///
174 /// Note that the directory will be created if needed.
175 #[clap(long, short = 'd')]
176 refs_directory: Option<PathBuf>,
177
178 /// The URLs or path from which to receive the pack.
179 ///
180 /// See here for a list of supported URLs: <https://www.git-scm.com/docs/git-clone#_git_urls>
181 url: String,
182
183 /// If set once or more times, these references will be fetched instead of all advertised ones.
184 ///
185 /// Note that this requires a reasonably modern git server.
186 #[clap(long = "reference", short = 'r')]
187 refs: Vec<String>,
188
189 /// The directory into which to write the received pack and index.
190 ///
191 /// If unset, they will be discarded.
192 directory: Option<PathBuf>,
193 },
194 /// Dissolve a pack into its loose objects.
195 ///
196 /// Note that this effectively removes delta compression for an average compression of 2x, creating one file per object in the process.
197 /// Thus this should only be done to dissolve small packs after a fetch.
198 Explode {
199 #[clap(long)]
200 /// Read written objects back and assert they match their source. Fail the operation otherwise.
201 ///
202 /// Only relevant if an object directory is set.
203 verify: bool,
204
205 /// delete the pack and index file after the operation is successful
206 #[clap(long)]
207 delete_pack: bool,
208
209 /// The amount of checks to run
210 #[clap(
211 long,
212 short = 'c',
213 default_value = "all",
214 possible_values(core::pack::explode::SafetyCheck::variants())
215 )]
216 check: core::pack::explode::SafetyCheck,
217
218 /// Compress bytes even when using the sink, i.e. no object directory is specified
219 ///
220 /// This helps to determine overhead related to compression. If unset, the sink will
221 /// only create hashes from bytes, which is usually limited by the speed at which input
222 /// can be obtained.
223 #[clap(long)]
224 sink_compress: bool,
225
226 /// The '.pack' or '.idx' file to explode into loose objects
227 pack_path: PathBuf,
228
229 /// The path into which all objects should be written. Commonly '.git/objects'
230 object_path: Option<PathBuf>,
231 },
232 /// Verify the integrity of a pack, index or multi-index file
233 Verify {
234 #[clap(flatten)]
235 args: VerifyOptions,
236
237 /// The '.pack', '.idx' or 'multi-pack-index' file to validate.
238 path: PathBuf,
239 },
240 }
241
242 #[derive(Debug, clap::Parser)]
243 pub struct VerifyOptions {
244 /// output statistical information
245 #[clap(long, short = 's')]
246 pub statistics: bool,
247 /// The algorithm used to verify packs. They differ in costs.
248 #[clap(
249 long,
250 short = 'a',
251 default_value = "less-time",
252 possible_values(core::pack::verify::Algorithm::variants())
253 )]
254 pub algorithm: core::pack::verify::Algorithm,
255
256 #[clap(long, conflicts_with("re-encode"))]
257 /// Decode and parse tags, commits and trees to validate their correctness beyond hashing correctly.
258 ///
259 /// Malformed objects should not usually occur, but could be injected on purpose or accident.
260 /// This will reduce overall performance.
261 pub decode: bool,
262
263 #[clap(long)]
264 /// Decode and parse tags, commits and trees to validate their correctness, and re-encode them.
265 ///
266 /// This flag is primarily to test the implementation of encoding, and requires to decode the object first.
267 /// Encoding an object after decoding it should yield exactly the same bytes.
268 /// This will reduce overall performance even more, as re-encoding requires to transform zero-copy objects into
269 /// owned objects, causing plenty of allocation to occur.
270 pub re_encode: bool,
271 }
272
273 ///
274 pub mod multi_index {
275 use std::path::PathBuf;
276
277 #[derive(Debug, clap::Parser)]
278 pub struct Platform {
279 /// The path to the index file.
280 #[clap(short = 'i', long, default_value = ".git/objects/pack/multi-pack-index")]
281 pub multi_index_path: PathBuf,
282
283 /// Subcommands
284 #[clap(subcommand)]
285 pub cmd: Subcommands,
286 }
287
288 #[derive(Debug, clap::Subcommand)]
289 pub enum Subcommands {
290 /// Display all entries of a multi-index: <oid> <pack-id> <pack-offset>
291 Entries,
292 /// Print general information about a multi-index file
293 Info,
294 /// Verify a multi-index quickly without inspecting objects themselves
295 Verify,
296 /// Create a multi-pack index from one or more pack index files, overwriting possibloy existing files.
297 Create {
298 /// Paths to the pack index files to read (with .idx extension).
299 ///
300 /// Note for the multi-index to be useful, it should be side-by-side with the supplied `.idx` files.
301 #[clap(required = true)]
302 index_paths: Vec<PathBuf>,
303 },
304 }
305 }
306
307 ///
308 pub mod index {
309 use std::path::PathBuf;
310
311 use gitoxide_core as core;
312
313 #[derive(Debug, clap::Subcommand)]
314 pub enum Subcommands {
315 /// create a pack index from a pack data file.
316 Create {
317 /// Specify how to iterate the pack, defaults to 'verify'
318 ///
319 /// Valid values are
320 ///
321 /// **as-is** do not do anything and expect the pack file to be valid as per the trailing hash,
322 /// **verify** the input ourselves and validate that it matches with the hash provided in the pack,
323 /// **restore** hash the input ourselves and ignore failing entries, instead finish the pack with the hash we computed
324 /// to keep as many objects as possible.
325 #[clap(
326 long,
327 short = 'i',
328 default_value = "verify",
329 possible_values(core::pack::index::IterationMode::variants())
330 )]
331 iteration_mode: core::pack::index::IterationMode,
332
333 /// Path to the pack file to read (with .pack extension).
334 ///
335 /// If unset, the pack file is expected on stdin.
336 #[clap(long, short = 'p')]
337 pack_path: Option<PathBuf>,
338
339 /// The folder into which to place the pack and the generated index file
340 ///
341 /// If unset, only informational output will be provided to standard output.
342 directory: Option<PathBuf>,
343 },
344 }
345 }
346}
347
348///
349pub mod mailmap {
350 use std::path::PathBuf;
351
352 #[derive(Debug, clap::Parser)]
353 pub struct Platform {
354 /// The path to the mailmap file.
355 #[clap(short = 'p', long, default_value = ".mailmap")]
356 pub path: PathBuf,
357
358 /// Subcommands
359 #[clap(subcommand)]
360 pub cmd: Subcommands,
361 }
362
363 #[derive(Debug, clap::Subcommand)]
364 pub enum Subcommands {
365 /// Parse all entries in the mailmap and report malformed lines or collisions.
366 Verify,
367 }
368}
src/plumbing/options/mod.rs+310−0
@@ -0,0 +1,310 @@
1use std::path::PathBuf;
2
3use git_repository as git;
4use git_repository::bstr::BString;
5use gitoxide_core as core;
6
7#[derive(Debug, clap::Parser)]
8#[clap(name = "gix-plumbing", about = "The git underworld", version = clap::crate_version!())]
9#[clap(subcommand_required = true)]
10#[clap(arg_required_else_help = true)]
11pub struct Args {
12 /// The repository to access.
13 #[clap(short = 'r', long, default_value = ".")]
14 pub repository: PathBuf,
15
16 /// Add these values to the configuration in the form of `key=value` or `key`.
17 ///
18 /// For example, if `key` is `core.abbrev`, set configuration like `[core] abbrev = key`,
19 /// or `remote.origin.url = foo` to set `[remote "origin"] url = foo`.
20 #[clap(long, short = 'c', parse(try_from_os_str = git::env::os_str_to_bstring))]
21 pub config: Vec<BString>,
22
23 #[clap(long, short = 't')]
24 /// The amount of threads to use for some operations.
25 ///
26 /// If unset, or the value is 0, there is no limit and all logical cores can be used.
27 pub threads: Option<usize>,
28
29 /// Display verbose messages and progress information
30 #[clap(long, short = 'v')]
31 pub verbose: bool,
32
33 /// Bring up a terminal user interface displaying progress visually
34 #[cfg(feature = "prodash-render-tui")]
35 #[clap(long, conflicts_with("verbose"))]
36 pub progress: bool,
37
38 /// The progress TUI will stay up even though the work is already completed.
39 ///
40 /// Use this to be able to read progress messages or additional information visible in the TUI log pane.
41 #[cfg(feature = "prodash-render-tui")]
42 #[clap(long, conflicts_with("verbose"), requires("progress"))]
43 pub progress_keep_open: bool,
44
45 /// Determine the format to use when outputting statistics.
46 #[clap(
47 long,
48 short = 'f',
49 default_value = "human",
50 possible_values(core::OutputFormat::variants())
51 )]
52 pub format: core::OutputFormat,
53
54 /// The object format to assume when reading files that don't inherently know about it, or when writing files.
55 #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
56 pub object_hash: git_repository::hash::Kind,
57
58 #[clap(subcommand)]
59 pub cmd: Subcommands,
60}
61
62#[derive(Debug, clap::Subcommand)]
63pub enum Subcommands {
64 /// Interact with the object database.
65 #[clap(subcommand)]
66 Odb(odb::Subcommands),
67 /// Interact with tree objects.
68 #[clap(subcommand)]
69 Tree(tree::Subcommands),
70 /// Interact with commit objects.
71 #[clap(subcommand)]
72 Commit(commit::Subcommands),
73 /// Verify the integrity of the entire repository
74 Verify {
75 #[clap(flatten)]
76 args: free::pack::VerifyOptions,
77 },
78 /// Query and obtain information about revisions.
79 #[clap(subcommand)]
80 Revision(revision::Subcommands),
81 /// A program just like `git credential`.
82 #[clap(subcommand)]
83 Credential(credential::Subcommands),
84 /// Interact with the mailmap.
85 #[clap(subcommand)]
86 Mailmap(mailmap::Subcommands),
87 /// Interact with the remote hosts.
88 Remote(remote::Platform),
89 /// Interact with the exclude files like .gitignore.
90 #[clap(subcommand)]
91 Exclude(exclude::Subcommands),
92 /// Display overall progress of the gitoxide project as seen from the perspective of git-config.
93 Progress,
94 Config(config::Platform),
95 /// Subcommands that need no git repository to run.
96 #[clap(subcommand)]
97 Free(free::Subcommands),
98}
99
100pub mod config {
101 /// Print all entries in a configuration file or access other sub-commands
102 #[derive(Debug, clap::Parser)]
103 #[clap(subcommand_required(false))]
104 pub struct Platform {
105 /// The filter terms to limit the output to matching sections and subsections only.
106 ///
107 /// Typical filters are `branch` or `remote.origin` or `remote.or*` - git-style globs are supported
108 /// and comparisons are case-insensitive.
109 pub filter: Vec<String>,
110 }
111}
112
113pub mod remote {
114 use git_repository as git;
115
116 #[derive(Debug, clap::Parser)]
117 pub struct Platform {
118 /// The name of the remote to connect to.
119 ///
120 /// If unset, the current branch will determine the remote.
121 #[clap(long, short = 'n')]
122 pub name: Option<String>,
123
124 /// Connect directly to the given URL, forgoing any configuration from the repository.
125 #[clap(long, short = 'u', conflicts_with("name"), parse(try_from_os_str = std::convert::TryFrom::try_from))]
126 pub url: Option<git::Url>,
127
128 /// Output additional typically information provided by the server as part of the connection handshake.
129 #[clap(long)]
130 pub handshake_info: bool,
131
132 /// Subcommands
133 #[clap(subcommand)]
134 pub cmd: Subcommands,
135 }
136
137 #[derive(Debug, clap::Subcommand)]
138 #[clap(visible_alias = "remotes")]
139 pub enum Subcommands {
140 /// Print all references available on the remote.
141 #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
142 Refs,
143 /// Print all references available on the remote as filtered through ref-specs.
144 #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
145 RefMap {
146 /// Override the built-in and configured ref-specs with one or more of the given ones.
147 #[clap(parse(try_from_os_str = git::env::os_str_to_bstring))]
148 ref_spec: Vec<git_repository::bstr::BString>,
149 },
150 }
151}
152
153pub mod mailmap {
154 #[derive(Debug, clap::Subcommand)]
155 pub enum Subcommands {
156 /// Print all entries in configured mailmaps, inform about errors as well.
157 Entries,
158 }
159}
160
161pub mod odb {
162 #[derive(Debug, clap::Subcommand)]
163 pub enum Subcommands {
164 /// Print all object names.
165 Entries,
166 /// Provide general information about the object database.
167 Info,
168 }
169}
170
171pub mod tree {
172 #[derive(Debug, clap::Subcommand)]
173 pub enum Subcommands {
174 /// Print entries in a given tree
175 Entries {
176 /// Traverse the entire tree and its subtrees respectively, not only this tree.
177 #[clap(long, short = 'r')]
178 recursive: bool,
179
180 /// Provide files size as well. This is expensive as the object is decoded entirely.
181 #[clap(long, short = 'e')]
182 extended: bool,
183
184 /// The tree to traverse, or the tree at `HEAD` if unspecified.
185 treeish: Option<String>,
186 },
187 /// Provide information about a tree.
188 Info {
189 /// Provide files size as well. This is expensive as the object is decoded entirely.
190 #[clap(long, short = 'e')]
191 extended: bool,
192 /// The tree to traverse, or the tree at `HEAD` if unspecified.
193 treeish: Option<String>,
194 },
195 }
196}
197
198pub mod commit {
199 #[derive(Debug, clap::Subcommand)]
200 pub enum Subcommands {
201 /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
202 Describe {
203 /// Use annotated tag references only, not all tags.
204 #[clap(long, short = 't', conflicts_with("all-refs"))]
205 annotated_tags: bool,
206
207 /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches.
208 #[clap(long, short = 'a', conflicts_with("annotated-tags"))]
209 all_refs: bool,
210
211 /// Only follow the first parent when traversing the commit graph.
212 #[clap(long, short = 'f')]
213 first_parent: bool,
214
215 /// Always display the long format, even if that would not be necessary as the id is located directly on a reference.
216 #[clap(long, short = 'l')]
217 long: bool,
218
219 /// Consider only the given `n` candidates. This can take longer, but potentially produces more accurate results.
220 #[clap(long, short = 'c', default_value = "10")]
221 max_candidates: usize,
222
223 /// Print information on stderr to inform about performance statistics
224 #[clap(long, short = 's')]
225 statistics: bool,
226
227 #[clap(long)]
228 /// If there was no way to describe the commit, fallback to using the abbreviated input revision.
229 always: bool,
230
231 /// A specification of the revision to use, or the current `HEAD` if unset.
232 rev_spec: Option<String>,
233 },
234 }
235}
236
237pub mod credential {
238 #[derive(Debug, clap::Subcommand)]
239 pub enum Subcommands {
240 /// Get the credentials fed for `url=<url>` via STDIN.
241 #[clap(visible_alias = "get")]
242 Fill,
243 /// Approve the information piped via STDIN as obtained with last call to `fill`
244 #[clap(visible_alias = "store")]
245 Approve,
246 /// Try to resolve the given revspec and print the object names.
247 #[clap(visible_alias = "erase")]
248 Reject,
249 }
250}
251
252pub mod revision {
253 #[derive(Debug, clap::Subcommand)]
254 #[clap(visible_alias = "rev", visible_alias = "r")]
255 pub enum Subcommands {
256 /// List all commits reachable from the given rev-spec.
257 #[clap(visible_alias = "l")]
258 List { spec: std::ffi::OsString },
259 /// Provide the revision specification like `@~1` to explain.
260 #[clap(visible_alias = "e")]
261 Explain { spec: std::ffi::OsString },
262 /// Try to resolve the given revspec and print the object names.
263 #[clap(visible_alias = "query", visible_alias = "parse", visible_alias = "p")]
264 Resolve {
265 /// Instead of resolving a rev-spec, explain what would be done for the first spec.
266 ///
267 /// Equivalent to the `explain` subcommand.
268 #[clap(short = 'e', long)]
269 explain: bool,
270 /// Show the first resulting object similar to how `git cat-file` would, but don't show the resolved spec.
271 #[clap(short = 'c', long, conflicts_with = "explain")]
272 cat_file: bool,
273 /// rev-specs like `@`, `@~1` or `HEAD^2`.
274 #[clap(required = true)]
275 specs: Vec<std::ffi::OsString>,
276 },
277 /// Return the names and hashes of all previously checked-out branches.
278 #[clap(visible_alias = "prev")]
279 PreviousBranches,
280 }
281}
282
283pub mod exclude {
284 use std::ffi::OsString;
285
286 use git_repository as git;
287
288 #[derive(Debug, clap::Subcommand)]
289 pub enum Subcommands {
290 /// Check if path-specs are excluded and print the result similar to `git check-ignore`.
291 Query {
292 /// Show actual ignore patterns instead of un-excluding an entry.
293 ///
294 /// That way one can understand why an entry might not be excluded.
295 #[clap(long, short = 'i')]
296 show_ignore_patterns: bool,
297 /// Additional patterns to use for exclusions. They have the highest priority.
298 ///
299 /// Useful for undoing previous patterns using the '!' prefix.
300 #[clap(long, short = 'p')]
301 patterns: Vec<OsString>,
302 /// The git path specifications to check for exclusion, or unset to read from stdin one per line.
303 #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))]
304 pathspecs: Vec<git::path::Spec>,
305 },
306 }
307}
308
309///
310pub mod free;
src/plumbing/progress.rs+305−0
@@ -0,0 +1,305 @@
1use crosstermion::crossterm::style::Stylize;
2use owo_colors::OwoColorize;
3use std::fmt::{Display, Formatter};
4use tabled::{Style, TableIteratorExt, Tabled};
5
6#[derive(Clone)]
7enum Usage {
8 NotApplicable,
9 Planned {
10 note: Option<&'static str>,
11 },
12 InModule {
13 name: &'static str,
14 deviation: Option<&'static str>,
15 },
16 /// Needs analysis
17 Puzzled,
18}
19use Usage::*;
20
21impl Display for Usage {
22 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Puzzled => f.write_str("❓")?,
25 NotApplicable => f.write_str("not applicable")?,
26 Planned { note } => {
27 write!(f, "{}", "planned".blink())?;
28 if let Some(note) = note {
29 write!(f, " ℹ {} ℹ", note.bright_white())?;
30 }
31 }
32 InModule { name, deviation } => {
33 write!(f, "mod {name}")?;
34 if let Some(deviation) = deviation {
35 write!(f, "{}", format!(" ❗️{deviation}❗️").bright_white())?
36 }
37 }
38 }
39 Ok(())
40 }
41}
42
43impl Usage {
44 pub fn icon(&self) -> &'static str {
45 match self {
46 Puzzled => "?",
47 NotApplicable => "❌",
48 Planned { .. } => "🕒",
49 InModule { deviation, .. } => deviation.is_some().then(|| "👌️").unwrap_or("✅"),
50 }
51 }
52}
53
54#[derive(Clone)]
55struct Record {
56 config: &'static str,
57 usage: Usage,
58}
59
60impl Tabled for Record {
61 const LENGTH: usize = 3;
62
63 fn fields(&self) -> Vec<String> {
64 let mut tokens = self.config.split('.');
65 let mut buf = vec![tokens.next().expect("present").bold().to_string()];
66 buf.extend(tokens.map(ToOwned::to_owned));
67
68 vec![self.usage.icon().into(), buf.join("."), self.usage.to_string()]
69 }
70
71 fn headers() -> Vec<String> {
72 vec![]
73 }
74}
75
76static GIT_CONFIG: &[Record] = &[
77 Record {
78 config: "core.bare",
79 usage: InModule {
80 name: "config::cache",
81 deviation: None,
82 },
83 },
84 Record {
85 config: "core.excludesFile",
86 usage: InModule {
87 name: "config::cache",
88 deviation: None,
89 },
90 },
91 Record {
92 config: "core.abbrev",
93 usage: InModule {
94 name: "config::cache",
95 deviation: None,
96 },
97 },
98 Record {
99 config: "core.ignoreCase",
100 usage: InModule {
101 name: "config::cache",
102 deviation: None,
103 },
104 },
105 Record {
106 config: "core.multiPackIndex",
107 usage: InModule {
108 name: "config::cache",
109 deviation: None,
110 },
111 },
112 Record {
113 config: "core.disambiguate",
114 usage: InModule {
115 name: "config::cache",
116 deviation: None,
117 },
118 },
119 Record {
120 config: "core.filesRefLockTimeout",
121 usage: InModule {name: "config::cache::access", deviation: None},
122 },
123 Record {
124 config: "core.packedRefsTimeout",
125 usage: InModule {name: "config::cache::access", deviation: None},
126 },
127 Record {
128 config: "core.logAllRefUpdates",
129 usage: InModule {
130 name: "config::cache",
131 deviation: None,
132 },
133 },
134 Record {
135 config: "core.repositoryFormatVersion",
136 usage: InModule {
137 name: "config::cache::incubate",
138 deviation: None,
139 },
140 },
141 Record {
142 config: "extensions.objectFormat",
143 usage: InModule {
144 name: "config::cache::incubate",
145 deviation: Some(
146 "Support for SHA256 is prepared but not fully implemented yet. For now we abort when encountered.",
147 ),
148 },
149 },
150 Record {
151 config: "committer.name",
152 usage: InModule {
153 name: "repository::identity",
154 deviation: None,
155 },
156 },
157 Record {
158 config: "committer.email",
159 usage: InModule {
160 name: "repository::identity",
161 deviation: None,
162 },
163 },
164 Record {
165 config: "author.name",
166 usage: InModule {
167 name: "repository::identity",
168 deviation: None,
169 },
170 },
171 Record {
172 config: "author.email",
173 usage: InModule {
174 name: "repository::identity",
175 deviation: None,
176 },
177 },
178 Record {
179 config: "user.name",
180 usage: InModule {
181 name: "repository::identity",
182 deviation: Some("defaults to 'gitoxide'"),
183 },
184 },
185 Record {
186 config: "user.email",
187 usage: InModule {
188 name: "repository::identity",
189 deviation: Some("defaults to 'gitoxide@localhost'"),
190 },
191 },
192 Record {
193 config: "fetch.recurseSubmodules",
194 usage: Planned {
195 note: Some("Seems useful for cargo as well"),
196 },
197 },
198 Record {
199 config: "fetch.fsckObjects",
200 usage: Puzzled,
201 },
202 Record {
203 config: "fetch.fsck.<msg-id>",
204 usage: Puzzled,
205 },
206 Record {
207 config: "fetch.fsck.skipList",
208 usage: Puzzled,
209 },
210 Record {
211 config: "fetch.unpackLimit",
212 usage: Planned { note: None },
213 },
214 Record {
215 config: "fetch.prune",
216 usage: Planned { note: None },
217 },
218 Record {
219 config: "fetch.pruneTags",
220 usage: Planned { note: None },
221 },
222 Record {
223 config: "fetch.writeCommitGraph",
224 usage: Planned { note: None },
225 },
226 Record {
227 config: "fetch.parallel",
228 usage: Planned { note: None },
229 },
230 Record {
231 config: "fetch.showForcedUpdates",
232 usage: NotApplicable,
233 },
234 Record {
235 config: "fetch.output",
236 usage: NotApplicable,
237 },
238 Record {
239 config: "fetch.negotiationAlgorithm",
240 usage: Planned {
241 note: Some("Implements our own 'naive' algorithm, only"),
242 },
243 },
244 Record {
245 config: "pack.threads",
246 usage: InModule {
247 name: "remote::connection::fetch",
248 deviation: Some("if unset, it uses all threads as opposed to just 1"),
249 },
250 },
251 Record {
252 config: "pack.indexVersion",
253 usage: InModule {
254 name: "remote::connection::fetch",
255 deviation: None,
256 },
257 },
258 Record {
259 config: "protocol.allow",
260 usage: InModule {
261 name: "remote::url::scheme_permission",
262 deviation: None,
263 },
264 },
265 Record {
266 config: "protocol.<name>.allow",
267 usage: InModule {
268 name: "remote::url::scheme_permission",
269 deviation: None,
270 },
271 },
272 Record {
273 config: "remotes.<group>",
274 usage: Planned {
275 note: Some("useful for multi-remote fetches as part of the standard API, maybe just `group(name) -> Option<Vec<Remote>>`"),
276 },
277 },
278 Record {
279 config: "url.<base>.insteadOf",
280 usage: InModule {
281 name: "remote::url::rewrite",
282 deviation: None,
283 },
284 },
285 Record {
286 config: "url.<base>.pushInsteadOf",
287 usage: InModule {
288 name: "remote::url::rewrite",
289 deviation: None,
290 },
291 },
292];
293
294/// A programmatic way to record and display progress.
295pub fn show_progress() -> anyhow::Result<()> {
296 let sorted = {
297 let mut v: Vec<_> = GIT_CONFIG.into();
298 v.sort_by_key(|r| r.config);
299 v
300 };
301
302 println!("{}", sorted.table().with(Style::blank()));
303 println!("\nTotal records: {}", GIT_CONFIG.len());
304 Ok(())
305}
tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success+1−1
@@ -1,6 +1,6 @@
1{1{
2 "index": {2 "index": {
3 "index_kind": "V2",3 "index_version": "V2",
4 "index_hash": {4 "index_hash": {
5 "Sha1": [5 "Sha1": [
6 86,6 86,
tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success+1−1
@@ -1,6 +1,6 @@
1{1{
2 "index": {2 "index": {
3 "index_kind": "V2",3 "index_version": "V2",
4 "index_hash": {4 "index_hash": {
5 "Sha1": [5 "Sha1": [
6 44,6 44,
tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json+1−1
@@ -1,6 +1,6 @@
1{1{
2 "index": {2 "index": {
3 "index_kind": "V2",3 "index_version": "V2",
4 "index_hash": "c787de2aafb897417ca8167baeb146eabd18bc5f",4 "index_hash": "c787de2aafb897417ca8167baeb146eabd18bc5f",
5 "data_hash": "346574b7331dc3a1724da218d622c6e1b6c66a57",5 "data_hash": "346574b7331dc3a1724da218d622c6e1b6c66a57",
6 "num_objects": 96 "num_objects": 9
tests/tools/src/lib.rs+8−0
@@ -535,6 +535,14 @@ impl<'a> Env<'a> { · 534 unmodified lines
535 self.altered_vars.push((var, prev));535 self.altered_vars.push((var, prev));
536 self536 self
537 }537 }
538
539 /// Set `var` to `value`.
540 pub fn unset(mut self, var: &'a str) -> Self {
541 let prev = std::env::var_os(var);
542 std::env::remove_var(var);
543 self.altered_vars.push((var, prev));
544 self
545 }
538}546}
539 547
540impl<'a> Drop for Env<'a> {548impl<'a> Drop for Env<'a> {