Unified · Split
| @@ -297,6 +297,12 @@ dependencies = [ · 296 unmodified lines | |||
| 297 | "utf8-width", | 297 | "utf8-width", |
| 298 | ] | 298 | ] |
| 299 | 299 | ||
| 300 | [[package]] | ||
| 301 | name = "bytecount" | ||
| 302 | version = "0.6.3" | ||
| 303 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 304 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" | ||
| 305 | |||
| 300 | [[package]] | 306 | [[package]] |
| 301 | name = "bytes" | 307 | name = "bytes" |
| 302 | version = "1.2.1" | 308 | version = "1.2.1" |
| @@ -954,6 +960,12 @@ dependencies = [ · 651 unmodified lines | |||
| 954 | "miniz_oxide", | 960 | "miniz_oxide", |
| 955 | ] | 961 | ] |
| 956 | 962 | ||
| 963 | [[package]] | ||
| 964 | name = "fnv" | ||
| 965 | version = "1.0.7" | ||
| 966 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 967 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | ||
| 968 | |||
| 957 | [[package]] | 969 | [[package]] |
| 958 | name = "form_urlencoded" | 970 | name = "form_urlencoded" |
| 959 | version = "1.1.0" | 971 | version = "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]] | ||
| 2398 | name = "owo-colors" | ||
| 2399 | version = "3.5.0" | ||
| 2400 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2401 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" | ||
| 2402 | |||
| 2403 | [[package]] | ||
| 2404 | name = "papergrid" | ||
| 2405 | version = "0.5.1" | ||
| 2406 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2407 | checksum = "453cf71f2a37af495a1a124bf30d4d7469cfbea58e9f2479be9d222396a518a2" | ||
| 2408 | dependencies = [ | ||
| 2409 | "bytecount", | ||
| 2410 | "fnv", | ||
| 2411 | "unicode-width", | ||
| 2412 | ] | ||
| 2413 | |||
| 2383 | [[package]] | 2414 | [[package]] |
| 2384 | name = "parking" | 2415 | name = "parking" |
| 2385 | version = "2.0.0" | 2416 | version = "2.0.0" |
| @@ -2926,6 +2957,16 @@ dependencies = [ · 540 unmodified lines | |||
| 2926 | "unicode-ident", | 2957 | "unicode-ident", |
| 2927 | ] | 2958 | ] |
| 2928 | 2959 | ||
| 2960 | [[package]] | ||
| 2961 | name = "tabled" | ||
| 2962 | version = "0.8.0" | ||
| 2963 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2964 | checksum = "e5b2f8c37d26d87d2252187b0a45ea3cbf42baca10377c7e7eaaa2800fa9bf97" | ||
| 2965 | dependencies = [ | ||
| 2966 | "papergrid", | ||
| 2967 | "unicode-width", | ||
| 2968 | ] | ||
| 2969 | |||
| 2929 | [[package]] | 2970 | [[package]] |
| 2930 | name = "tar" | 2971 | name = "tar" |
| 2931 | version = "0.4.38" | 2972 | version = "0.4.38" |
| @@ -92,6 +92,10 @@ env_logger = { version = "0.9.0", default-features = false } · 91 unmodified lines | |||
| 92 | crosstermion = { version = "0.10.1", optional = true, default-features = false } | 92 | crosstermion = { version = "0.10.1", optional = true, default-features = false } |
| 93 | futures-lite = { version = "1.12.0", optional = true, default-features = false, features = ["std"] } | 93 | futures-lite = { version = "1.12.0", optional = true, default-features = false, features = ["std"] } |
| 94 | 94 | ||
| 95 | # for progress | ||
| 96 | owo-colors = "3.5.0" | ||
| 97 | tabled = { version = "0.8.0", default-features = false } | ||
| 98 | |||
| 95 | document-features = { version = "0.2.0", optional = true } | 99 | document-features = { version = "0.2.0", optional = true } |
| 96 | 100 | ||
| 97 | [profile.dev.package] | 101 | [profile.dev.package] |
| @@ -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 | |||
| 23 | As a general rule, respect and implement all applicable [git-config](https://git-scm.com/docs/git-config) by default, but allow the | ||
| 24 | caller to set overrides. How overrides work depends on the goals of the particular API so it can be done on the main call path, | ||
| 25 | forcing a choice, or more typically, as a side-lane where overrides can be done on demand. | ||
| 26 | |||
| 27 | Note that it should be possible to obtain the current configuration for modification by the user for selective overrides, either | ||
| 28 | by calling methods or by obtaining a data structure that can be set as a whole using a `get -> modify -> set` cycle. | ||
| 29 | |||
| 30 | Note that without any of that, one should document that with `config_snapshot_mut()` any of the relevant configuration can be | ||
| 31 | changed in memory before invoking a method in order to affect it. | ||
| 32 | |||
| 33 | Parameters 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 | ## General | 36 | ## General |
| 22 | 37 | ||
| 23 | * **async** | 38 | * **async** |
| @@ -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 automation | 29 | * **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`. |
| @@ -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) |
| @@ -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 | ||
| @@ -2,7 +2,9 @@ use std::io; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | use git_tempfile::handle::Writable; | 3 | use 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)] | ||
| 6 | pub enum Error { | 8 | pub 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), |
| @@ -10,7 +10,7 @@ use git_tempfile::{handle::Writable, AutoRemove, ContainingDirectory}; · 9 unmodified lines | |||
| 10 | use crate::data; | 10 | use crate::data; |
| 11 | 11 | ||
| 12 | mod error; | 12 | mod error; |
| 13 | use error::Error; | 13 | pub use error::Error; |
| 14 | 14 | ||
| 15 | mod types; | 15 | mod types; |
| 16 | use types::{LockWriter, PassThrough}; | 16 | use 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>>>, |
| @@ -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 | } |
| @@ -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), |
| @@ -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))] |
| 19 | pub struct Outcome { | 19 | pub struct Outcome { |
| 20 | /// The version of the verified index | 20 | /// 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 index | 22 | /// 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 | ||
| 218 | fn modify_base( | 218 | fn 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())); |
| @@ -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 | ) |
| @@ -321,6 +321,21 @@ fn traversals() -> crate::Result { · 320 unmodified lines | |||
| 321 | Ok(()) | 321 | Ok(()) |
| 322 | } | 322 | } |
| 323 | 323 | ||
| 324 | #[test] | ||
| 325 | fn 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 | |||
| 324 | fn write_and_verify( | 339 | fn write_and_verify( |
| 325 | db: git_odb::HandleArc, | 340 | db: git_odb::HandleArc, |
| 326 | entries: Vec<output::Entry>, | 341 | entries: Vec<output::Entry>, |
| @@ -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]) |
| @@ -5,7 +5,8 @@ use git_transport::{client, client::TransportV2Ext}; · 4 unmodified lines | |||
| 5 | use crate::fetch::{Arguments, Command}; | 5 | use crate::fetch::{Arguments, Command}; |
| 6 | 6 | ||
| 7 | impl Arguments { | 7 | impl 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, |
| @@ -3,6 +3,7 @@ use std::fmt; · 2 unmodified lines | |||
| 3 | use bstr::{BStr, BString, ByteVec}; | 3 | use 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)] | ||
| 6 | pub struct Arguments { | 7 | pub struct Arguments { |
| 7 | /// The active features/capabilities of the fetch invocation | 8 | /// 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 | ||
| 26 | impl Arguments { | 27 | impl 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.filter | 37 | 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"); |
| @@ -50,9 +50,12 @@ mod with_io { · 49 unmodified lines | |||
| 50 | "filter ", // filter-spec | 50 | "filter ", // filter-spec |
| 51 | // ref-in-want feature | 51 | // ref-in-want feature |
| 52 | "want-ref ", // ref path | 52 | "want-ref ", // ref path |
| 53 | // sideband-all feature | ||
| 53 | "sideband-all", | 54 | "sideband-all", |
| 54 | // packfile-uris feature | 55 | // packfile-uris feature |
| 55 | "packfile-uris ", // protocols | 56 | "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, |
| @@ -3,7 +3,7 @@ use git_transport::client::Capabilities; · 2 unmodified lines | |||
| 3 | use crate::fetch::Ref; | 3 | use 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))] |
| 8 | pub struct Outcome { | 8 | pub 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. |
| @@ -10,13 +10,13 @@ use super::Error; · 9 unmodified lines | |||
| 10 | use crate::fetch::{indicate_end_of_interaction, refs::from_v2_refs, Command, LsRefsAction, Ref}; | 10 | use 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 yielded | 12 | /// 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] |
| 15 | pub async fn refs( | 15 | pub 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>)>, |
| @@ -50,7 +50,7 @@ pub mod parse { · 49 unmodified lines | |||
| 50 | pub enum Ref { | 50 | pub enum Ref { |
| 51 | /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit | 51 | /// 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, |
| @@ -152,7 +152,8 @@ impl Response { · 151 unmodified lines | |||
| 152 | self.has_pack | 152 | 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 that | 158 | /// 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. |
| @@ -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.stateful | 47 | 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.stateful | 97 | 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)] |
| @@ -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: _, |
| @@ -38,6 +38,15 @@ impl TryFrom<BString> for FullName { · 37 unmodified lines | |||
| 38 | } | 38 | } |
| 39 | } | 39 | } |
| 40 | 40 | ||
| 41 | impl 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 | |||
| 41 | impl From<FullName> for BString { | 50 | impl From<FullName> for BString { |
| 42 | fn from(name: FullName) -> Self { | 51 | fn from(name: FullName) -> Self { |
| 43 | name.0 | 52 | name.0 |
| @@ -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 exist | 269 | // We have to create a packed-ref even if it doesn't exist |
| 269 | self.store | 270 | 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 anyway | 281 | // no packed-ref file exists anyway |
| 281 | self.store | 282 | 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 | ) { |
| @@ -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"), |
| @@ -83,7 +83,15 @@ pub enum Change { · 82 unmodified lines | |||
| 83 | } | 83 | } |
| 84 | 84 | ||
| 85 | impl Change { | 85 | impl 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) |
| @@ -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(), |
| @@ -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 | ||
| @@ -191,6 +191,7 @@ mod read_only { · 190 unmodified lines | |||
| 191 | } | 191 | } |
| 192 | 192 | ||
| 193 | mod writable { | 193 | mod 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"); |
| @@ -1,4 +1,4 @@ | |||
| 1 | use bstr::BStr; | 1 | use bstr::{BStr, ByteSlice}; |
| 2 | 2 | ||
| 3 | use crate::{ | 3 | use 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 | ||
| 23 | mod impls { | 28 | mod impls { |
| @@ -84,7 +89,7 @@ mod impls { · 60 unmodified lines | |||
| 84 | } | 89 | } |
| 85 | 90 | ||
| 86 | /// Access | 91 | /// Access |
| 87 | impl RefSpecRef<'_> { | 92 | impl<'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 }), |
| @@ -6,4 +6,5 @@ mod impls; · 5 unmodified lines | |||
| 6 | mod match_group; | 6 | mod match_group; |
| 7 | mod matching; | 7 | mod matching; |
| 8 | mod parse; | 8 | mod parse; |
| 9 | mod spec; | ||
| 9 | mod write; | 10 | mod write; |
| @@ -0,0 +1,41 @@ | |||
| 1 | mod 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 | } | ||
| @@ -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 | } |
| @@ -0,0 +1,103 @@ | |||
| 1 | use crate::config::Cache; | ||
| 2 | use crate::{remote, repository::identity}; | ||
| 3 | use git_lock::acquire::Fail; | ||
| 4 | use std::convert::TryInto; | ||
| 5 | use std::path::PathBuf; | ||
| 6 | use std::time::Duration; | ||
| 7 | |||
| 8 | /// Access | ||
| 9 | impl 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 | } | ||
| @@ -1,7 +1,5 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use super::{interpolate_context, util, Error, StageOne}; | 1 | use super::{interpolate_context, util, Error, StageOne}; |
| 4 | use crate::{config::Cache, repository, revision::spec::parse::ObjectKindHint}; | 2 | use crate::{config::Cache, repository}; |
| 5 | 3 | ||
| 6 | /// Initialization | 4 | /// Initialization |
| 7 | impl Cache { | 5 | impl 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 | globals | 111 | globals |
| 114 | }; | 112 | }; |
| 115 | 113 | ||
| 116 | let excludes_file = match config | 114 | 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 later | 147 | /// 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 | } |
| @@ -1,5 +1,4 @@ | |||
| 1 | use super::{Cache, Error}; | 1 | use super::{Cache, Error}; |
| 2 | use crate::{remote, repository::identity}; | ||
| 3 | 2 | ||
| 4 | mod incubate; | 3 | mod incubate; |
| 5 | pub(crate) use incubate::StageOne; | 4 | pub(crate) use incubate::StageOne; |
| @@ -12,27 +11,7 @@ impl std::fmt::Debug for Cache { · 6 unmodified lines | |||
| 12 | } | 11 | } |
| 13 | } | 12 | } |
| 14 | 13 | ||
| 15 | /// Access | 14 | mod access; |
| 16 | impl 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 | ||
| 37 | mod util; | 16 | mod util; |
| 38 | pub(crate) use util::interpolate_context; | 17 | pub(crate) use util::interpolate_context; |
| @@ -2,6 +2,7 @@ use std::convert::TryFrom; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | use super::Error; | 3 | use super::Error; |
| 4 | use crate::bstr::ByteSlice; | 4 | use crate::bstr::ByteSlice; |
| 5 | use crate::revision::spec::parse::ObjectKindHint; | ||
| 5 | 6 | ||
| 6 | pub(crate) fn interpolate_context<'a>( | 7 | pub(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 | ||
| 58 | pub(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 | |||
| 57 | pub(crate) fn parse_core_abbrev( | 66 | pub(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 | |||
| 106 | pub(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 | } | ||
| @@ -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()`. | ||
| 22 | pub struct SnapshotMut<'repo> { | 26 | pub 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. | ||
| 32 | pub struct CommitAutoRollback<'repo> { | ||
| 33 | pub(crate) repo: Option<&'repo mut Repository>, | ||
| 34 | pub(crate) prev_config: crate::Config, | ||
| 35 | } | ||
| 36 | |||
| 27 | pub(crate) mod section { | 37 | pub(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::Repository | 39 | 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)] |
| 59 | pub(crate) struct Cache { | 72 | pub(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 urls | 86 | /// 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 allow | 88 | /// 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. |
| @@ -0,0 +1,62 @@ | |||
| 1 | use std::{ | ||
| 2 | fmt::{Debug, Formatter}, | ||
| 3 | ops::{Deref, DerefMut}, | ||
| 4 | }; | ||
| 5 | |||
| 6 | use crate::config::{CommitAutoRollback, Snapshot, SnapshotMut}; | ||
| 7 | |||
| 8 | impl 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 | |||
| 14 | impl 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 | |||
| 20 | impl Debug for SnapshotMut<'_> { | ||
| 21 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| 22 | f.write_str(&self.config.to_string()) | ||
| 23 | } | ||
| 24 | } | ||
| 25 | |||
| 26 | impl 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 | |||
| 34 | impl 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 | |||
| 42 | impl Deref for SnapshotMut<'_> { | ||
| 43 | type Target = git_config::File<'static>; | ||
| 44 | |||
| 45 | fn deref(&self) -> &Self::Target { | ||
| 46 | &self.config | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | impl 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 | |||
| 58 | impl DerefMut for SnapshotMut<'_> { | ||
| 59 | fn deref_mut(&mut self) -> &mut Self::Target { | ||
| 60 | &mut self.config | ||
| 61 | } | ||
| 62 | } | ||
| @@ -1,5 +1,7 @@ | |||
| 1 | use git_features::threading::OwnShared; | ||
| 1 | use std::borrow::Cow; | 2 | use std::borrow::Cow; |
| 2 | 3 | ||
| 4 | use crate::config::{CommitAutoRollback, SnapshotMut}; | ||
| 3 | use crate::{ | 5 | use 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.resolved | 92 | &self.repo.config.resolved |
| 91 | } | 93 | } |
| 92 | } | 94 | } |
| 95 | |||
| 96 | /// Utilities | ||
| 97 | impl<'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 | ||
| 135 | impl<'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 | } | ||
| @@ -23,7 +23,10 @@ pub enum Error { · 22 unmodified lines | |||
| 23 | impl SnapshotMut<'_> { | 23 | impl 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 | } |
| @@ -1,3 +1,4 @@ | |||
| 1 | mod _impls; | ||
| 1 | mod access; | 2 | mod access; |
| 2 | 3 | ||
| 3 | /// | 4 | /// |
| @@ -5,44 +6,3 @@ pub mod apply_cli_overrides; · 1 unmodified line | |||
| 5 | 6 | ||
| 6 | /// | 7 | /// |
| 7 | pub mod credential_helpers; | 8 | pub mod credential_helpers; |
| 8 | |||
| 9 | mod _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 | } | ||
| @@ -87,19 +87,22 @@ mod remote { · 86 unmodified lines | |||
| 87 | /// Remote | 87 | /// 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()] and | 93 | /// 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 | } |
| @@ -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 | self | 207 | self |
| @@ -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 | } |
| @@ -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 | ||
| @@ -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 like | 72 | /// 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, |
| @@ -1,8 +1,6 @@ | |||
| 1 | use git_protocol::transport::client::Transport; | ||
| 2 | |||
| 3 | use crate::{remote::Connection, Progress, Remote}; | 1 | use crate::{remote::Connection, Progress, Remote}; |
| 2 | use git_protocol::transport::client::Transport; | ||
| 4 | 3 | ||
| 5 | #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] | ||
| 6 | mod error { | 4 | mod 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"))] | ||
| 28 | pub use error::Error; | 25 | pub use error::Error; |
| 29 | 26 | ||
| 30 | /// Establishing connections to remote hosts | 27 | /// Establishing connections to remote hosts (without performing a git-handshake). |
| 31 | impl<'repo> Remote<'repo> { | 28 | impl<'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 | where | 61 | 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 = self | 91 | let version = self |
| 79 | .repo | 92 | .repo |
| 80 | .config | 93 | .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 | } |
| @@ -0,0 +1,48 @@ | |||
| 1 | use crate::{ | ||
| 2 | remote::{connection::AuthenticateFn, Connection}, | ||
| 3 | Remote, | ||
| 4 | }; | ||
| 5 | |||
| 6 | /// Builder | ||
| 7 | impl<'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 | ||
| 25 | impl<'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 | } | ||
| @@ -0,0 +1,66 @@ | |||
| 1 | use super::Error; | ||
| 2 | use crate::Repository; | ||
| 3 | use std::convert::TryInto; | ||
| 4 | |||
| 5 | pub 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 | |||
| 40 | pub 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 | } | ||
| @@ -0,0 +1,263 @@ | |||
| 1 | use crate::remote::fetch::{DryRun, RefMap}; | ||
| 2 | use crate::remote::{fetch, ref_map, Connection}; | ||
| 3 | use crate::Progress; | ||
| 4 | use git_odb::FindExt; | ||
| 5 | use git_protocol::transport::client::Transport; | ||
| 6 | use std::sync::atomic::AtomicBool; | ||
| 7 | |||
| 8 | mod 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 | } | ||
| 31 | pub use error::Error; | ||
| 32 | |||
| 33 | /// The status of the repository after the fetch operation | ||
| 34 | #[derive(Debug, Clone)] | ||
| 35 | pub 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)] | ||
| 55 | pub 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 | /// | ||
| 63 | pub mod negotiate; | ||
| 64 | |||
| 65 | impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P> | ||
| 66 | where | ||
| 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 | |||
| 91 | impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P> | ||
| 92 | where | ||
| 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 | |||
| 212 | fn 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 | |||
| 225 | mod config; | ||
| 226 | /// | ||
| 227 | #[path = "update_refs/mod.rs"] | ||
| 228 | pub mod refs; | ||
| 229 | |||
| 230 | /// A structure to hold the result of the handshake with the remote and configure the upcoming fetch operation. | ||
| 231 | pub struct Prepare<'remote, 'repo, T, P> | ||
| 232 | where | ||
| 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 | ||
| 241 | impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P> | ||
| 242 | where | ||
| 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 | |||
| 254 | impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P> | ||
| 255 | where | ||
| 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 | } | ||
| @@ -0,0 +1,60 @@ | |||
| 1 | /// The way the negotiation is performed | ||
| 2 | #[derive(Copy, Clone)] | ||
| 3 | pub(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)] | ||
| 11 | pub 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. | ||
| 19 | pub(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 | } | ||
| @@ -0,0 +1,149 @@ | |||
| 1 | use crate::remote::fetch; | ||
| 2 | use crate::remote::fetch::refs::update::Mode; | ||
| 3 | use crate::Repository; | ||
| 4 | use git_pack::Find; | ||
| 5 | use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}; | ||
| 6 | use git_ref::{Target, TargetRef}; | ||
| 7 | use std::collections::BTreeMap; | ||
| 8 | use std::convert::TryInto; | ||
| 9 | use std::path::PathBuf; | ||
| 10 | |||
| 11 | /// | ||
| 12 | pub 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)] | ||
| 16 | pub 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 | |||
| 23 | impl 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`. | ||
| 37 | pub(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 | |||
| 134 | fn 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)] | ||
| 149 | mod tests; | ||
| @@ -0,0 +1,263 @@ | |||
| 1 | mod 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 | } | ||
| @@ -0,0 +1,92 @@ | |||
| 1 | use crate::remote::fetch; | ||
| 2 | use std::path::PathBuf; | ||
| 3 | |||
| 4 | mod 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 | |||
| 22 | pub use error::Error; | ||
| 23 | |||
| 24 | /// The outcome of the refs-update operation at the end of a fetch. | ||
| 25 | #[derive(Debug, Clone)] | ||
| 26 | pub 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)] | ||
| 37 | pub 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 | |||
| 68 | impl 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 | } | ||
| @@ -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. |
| 9 | pub type CredentialsFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a>; | 9 | pub 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. |
| 15 | pub struct Connection<'a, 'repo, T, P> { | 15 | pub 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 | ||
| 22 | mod access { | 22 | mod 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 | /// |
| 69 | pub mod ref_map; | 25 | pub mod ref_map; |
| 26 | |||
| 27 | /// | ||
| 28 | #[cfg(feature = "blocking-network-client")] | ||
| 29 | pub mod fetch; | ||
| @@ -1,5 +1,7 @@ | |||
| 1 | use crate::bstr::{BString, ByteVec}; | ||
| 1 | use git_features::progress::Progress; | 2 | use git_features::progress::Progress; |
| 2 | use git_protocol::transport::client::Transport; | 3 | use git_protocol::transport::client::Transport; |
| 4 | use std::collections::HashSet; | ||
| 3 | 5 | ||
| 4 | use crate::remote::{connection::HandshakeWithRefs, fetch, Connection, Direction}; | 6 | use 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)] | ||
| 26 | pub 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 | |||
| 36 | impl 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 | |||
| 22 | impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P> | 45 | impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P> |
| 23 | where | 46 | where |
| 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 | res | 68 | 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) = group | 83 | 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 = self | 127 | 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? |
| @@ -0,0 +1,59 @@ | |||
| 1 | use 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")] | ||
| 6 | pub(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)] | ||
| 15 | pub 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)] | ||
| 30 | pub 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 | |||
| 37 | impl 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)] | ||
| 49 | pub 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")] | ||
| 59 | pub use super::connection::fetch::{negotiate, refs, Error, Outcome, Prepare, Status}; | ||
| @@ -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"))] |
| 30 | pub mod fetch { | 30 | pub 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"))] |
| @@ -1,7 +1,7 @@ | |||
| 1 | mod rewrite; | 1 | mod 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"))] |
| 4 | pub mod scheme_permission; | 4 | pub mod scheme_permission; |
| 5 | pub(crate) use rewrite::Rewrite; | 5 | pub(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"))] |
| 7 | pub(crate) use scheme_permission::SchemePermission; | 7 | pub(crate) use scheme_permission::SchemePermission; |
| @@ -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. |
| @@ -4,7 +4,8 @@ use crate::bstr::BString; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | /// Identity handling. | 5 | /// Identity handling. |
| 6 | impl crate::Repository { | 6 | impl 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 | ||
| @@ -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 reference | 133 | /// `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 detached | 134 | /// 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 API | 158 | // 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 | } |
| @@ -1,8 +1,6 @@ | |||
| 1 | use std::convert::TryInto; | 1 | use std::convert::TryInto; |
| 2 | 2 | ||
| 3 | use git_actor as actor; | ||
| 4 | use git_hash::ObjectId; | 3 | use git_hash::ObjectId; |
| 5 | use git_lock as lock; | ||
| 6 | use git_ref::{ | 4 | use 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 | ||
| 11 | use crate::{bstr::BString, ext::ReferenceExt, reference, Reference}; | 9 | use crate::{bstr::BString, ext::ReferenceExt, reference, Reference}; |
| 12 | 10 | ||
| 13 | const DEFAULT_LOCK_MODE: git_lock::acquire::Fail = git_lock::acquire::Fail::Immediately; | ||
| 14 | |||
| 15 | /// Obtain and alter references comfortably | 11 | /// Obtain and alter references comfortably |
| 16 | impl crate::Repository { | 12 | impl 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 previous | 119 | /// 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 competing | 125 | /// 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, and | 129 | /// 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.refs | 136 | 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 | ||
| @@ -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 pushing | 46 | /// 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 | /// |
| @@ -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. |
| 41 | impl crate::Repository { | 31 | impl 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. |
| @@ -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 | }, |
| @@ -1,3 +1,4 @@ | |||
| 1 | /make_worktree_repo.tar.xz | 1 | /make_worktree_repo.tar.xz |
| 2 | /make_remote_repos.tar.xz | 2 | /make_remote_repos.tar.xz |
| 3 | /make_fetch_repos.tar.xz | ||
| 3 | /make_core_worktree_repo.tar.xz | 4 | /make_core_worktree_repo.tar.xz |
| @@ -0,0 +1,30 @@ | |||
| 1 | set -eu -o pipefail | ||
| 2 | |||
| 3 | git clone --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base | ||
| 4 | |||
| 5 | git 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 | |||
| 13 | git 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 | |||
| 20 | git 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 | ) | ||
| @@ -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 | ||
| 277 | git clone --shared base detached-head | ||
| 278 | (cd detached-head | ||
| 279 | git checkout @~1 | ||
| 280 | ) | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | mod remote { | 1 | mod 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 | } |
| @@ -0,0 +1,123 @@ | |||
| 1 | #[cfg(feature = "blocking-network-client")] | ||
| 2 | mod 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 | } | ||
| @@ -17,4 +17,5 @@ pub(crate) fn cow_str(s: &str) -> Cow<str> { · 16 unmodified lines | |||
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | mod connect; | 19 | mod connect; |
| 20 | mod list_refs; | 20 | mod fetch; |
| 21 | mod ref_map; | ||
| @@ -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(()) |
| @@ -1,5 +1,49 @@ | |||
| 1 | use crate::named_repo; | 1 | use crate::named_repo; |
| 2 | 2 | ||
| 3 | #[test] | ||
| 4 | fn 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] | ||
| 30 | fn 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] |
| 4 | fn values_are_set_in_memory_only() { | 48 | fn 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(); |
| @@ -10,7 +10,7 @@ mod worktree; · 9 unmodified lines | |||
| 10 | 10 | ||
| 11 | #[test] | 11 | #[test] |
| 12 | fn size_in_memory() { | 12 | fn 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), |
| @@ -147,23 +147,17 @@ mod tag { · 146 unmodified lines | |||
| 147 | } | 147 | } |
| 148 | 148 | ||
| 149 | mod commit { | 149 | mod 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 = repo | 159 | 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 | ||
| @@ -271,3 +271,22 @@ mod find_remote { · 270 unmodified lines | |||
| 271 | .to_string() | 271 | .to_string() |
| 272 | } | 272 | } |
| 273 | } | 273 | } |
| 274 | |||
| 275 | mod 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 | } | ||
| @@ -2,6 +2,16 @@ use git_repository::{open, Repository, ThreadSafeRepository}; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; | 3 | pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; |
| 4 | 4 | ||
| 5 | pub 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 | } | ||
| 5 | pub fn repo(name: &str) -> Result<ThreadSafeRepository> { | 15 | pub 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 | ||
| 29 | pub 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 | |||
| 19 | pub fn repo_rw(name: &str) -> Result<(Repository, tempfile::TempDir)> { | 35 | pub fn repo_rw(name: &str) -> Result<(Repository, tempfile::TempDir)> { |
| 36 | repo_rw_opts(name, restricted()) | ||
| 37 | } | ||
| 38 | |||
| 39 | pub 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(), |
| @@ -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 | } |
| @@ -1,3 +1,5 @@ | |||
| 1 | use std::any::Any; | ||
| 2 | use std::error::Error; | ||
| 1 | use std::process::{self, Command, Stdio}; | 3 | use std::process::{self, Command, Stdio}; |
| 2 | 4 | ||
| 3 | use bstr::{BString, ByteSlice}; | 5 | use 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 | true | 104 | 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 | ||
| 106 | impl client::Transport for SpawnProcessOnDemand { | 112 | impl client::Transport for SpawnProcessOnDemand { |
| @@ -1,3 +1,4 @@ | |||
| 1 | use std::error::Error; | ||
| 1 | use std::{ | 2 | use 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 | } |
| @@ -1,6 +1,6 @@ | |||
| 1 | use std::any::Any; | ||
| 1 | use std::{ | 2 | use 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 | false | 170 | 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 | ||
| 174 | impl<H: Http> client::Transport for Transport<H> { | 178 | impl<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")))] |
| 300 | pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Result<Transport<H>, Infallible> { | 304 | pub 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")] |
| 306 | pub fn connect(url: &str, desired_version: Protocol) -> Result<Transport<Impl>, Infallible> { | 310 | pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> { |
| 307 | Ok(Transport::new(url, desired_version)) | 311 | Transport::new(url, desired_version) |
| 308 | } | 312 | } |
| @@ -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 | } |
| @@ -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))] |
| 28 | pub struct Capabilities { | 28 | pub struct Capabilities { |
| 29 | data: BString, | 29 | data: BString, |
| @@ -3,6 +3,7 @@ use bstr::BString; · 2 unmodified lines | |||
| 3 | use futures_io::{AsyncRead, AsyncWrite}; | 3 | use futures_io::{AsyncRead, AsyncWrite}; |
| 4 | use futures_lite::AsyncWriteExt; | 4 | use futures_lite::AsyncWriteExt; |
| 5 | use git_packetline::PacketLineRef; | 5 | use git_packetline::PacketLineRef; |
| 6 | use std::error::Error; | ||
| 6 | 7 | ||
| 7 | use crate::{ | 8 | use 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 | true | 52 | 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)] |
| @@ -1,3 +1,5 @@ | |||
| 1 | use std::any::Any; | ||
| 2 | use std::error::Error; | ||
| 1 | use std::io::Write; | 3 | use std::io::Write; |
| 2 | 4 | ||
| 3 | use bstr::BString; | 5 | use 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 | true | 55 | 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 | ||
| 57 | impl<R, W> client::Transport for git::Connection<R, W> | 63 | impl<R, W> client::Transport for git::Connection<R, W> |
| @@ -1,3 +1,4 @@ | |||
| 1 | use std::any::Any; | ||
| 1 | use std::ops::{Deref, DerefMut}; | 2 | use 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 indicated | 47 | /// 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 | ||
| 75 | impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T { | 85 | impl<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 | } |
| @@ -104,7 +104,7 @@ pub fn serve_and_connect( · 103 unmodified lines | |||
| 104 | &server.addr.port(), | 104 | &server.addr.port(), |
| 105 | path | 105 | 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 | } |
| @@ -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; |
| @@ -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))] |
| 248 | pub struct JsonBundleWriteOutcome { | 248 | pub 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 | |||
| 256 | impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome { | 256 | impl 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 | }; |
| @@ -73,7 +73,10 @@ mod refs_impl { · 72 unmodified lines | |||
| 73 | let map = remote | 73 | 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 | ||
| @@ -14,8 +14,9 @@ use gitoxide_core as core; · 13 unmodified lines | |||
| 14 | use gitoxide_core::pack::verify; | 14 | use gitoxide_core::pack::verify; |
| 15 | 15 | ||
| 16 | use crate::{ | 16 | use 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 { |
| @@ -1,4 +1,8 @@ | |||
| 1 | mod main; | 1 | mod main; |
| 2 | pub use main::main; | 2 | pub use main::main; |
| 3 | 3 | ||
| 4 | #[path = "progress.rs"] | ||
| 5 | mod progress_impl; | ||
| 6 | pub use progress_impl::show_progress; | ||
| 7 | |||
| 4 | mod options; | 8 | mod options; |
| @@ -1,677 +0,0 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use git_repository as git; | ||
| 4 | use git_repository::bstr::BString; | ||
| 5 | use 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)] | ||
| 11 | pub 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)] | ||
| 63 | pub 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 | |||
| 98 | pub 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 | |||
| 111 | pub 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 | |||
| 151 | pub 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 | |||
| 159 | pub 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 | |||
| 169 | pub 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 | |||
| 196 | pub 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 | |||
| 235 | pub 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 | |||
| 250 | pub 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 | /// | ||
| 282 | pub 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 | |||
| 653 | pub 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 | } | ||
| @@ -0,0 +1,368 @@ | |||
| 1 | #[derive(Debug, clap::Subcommand)] | ||
| 2 | #[clap(visible_alias = "no-repo")] | ||
| 3 | pub 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 | /// | ||
| 20 | pub 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 | |||
| 36 | pub 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 | /// | ||
| 86 | pub 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 | /// | ||
| 349 | pub 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 | } | ||
| @@ -0,0 +1,310 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use git_repository as git; | ||
| 4 | use git_repository::bstr::BString; | ||
| 5 | use 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)] | ||
| 11 | pub 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)] | ||
| 63 | pub 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 | |||
| 100 | pub 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 | |||
| 113 | pub 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 | |||
| 153 | pub 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 | |||
| 161 | pub 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 | |||
| 171 | pub 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 | |||
| 198 | pub 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 | |||
| 237 | pub 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 | |||
| 252 | pub 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 | |||
| 283 | pub 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 | /// | ||
| 310 | pub mod free; | ||
| @@ -0,0 +1,305 @@ | |||
| 1 | use crosstermion::crossterm::style::Stylize; | ||
| 2 | use owo_colors::OwoColorize; | ||
| 3 | use std::fmt::{Display, Formatter}; | ||
| 4 | use tabled::{Style, TableIteratorExt, Tabled}; | ||
| 5 | |||
| 6 | #[derive(Clone)] | ||
| 7 | enum 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 | } | ||
| 19 | use Usage::*; | ||
| 20 | |||
| 21 | impl 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 | |||
| 43 | impl 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)] | ||
| 55 | struct Record { | ||
| 56 | config: &'static str, | ||
| 57 | usage: Usage, | ||
| 58 | } | ||
| 59 | |||
| 60 | impl 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 | |||
| 76 | static 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. | ||
| 295 | pub 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 | } | ||
| @@ -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, |
| @@ -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, |
| @@ -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": 9 | 6 | "num_objects": 9 |
| @@ -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 | self | 536 | 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 | ||
| 540 | impl<'a> Drop for Env<'a> { | 548 | impl<'a> Drop for Env<'a> { |