Unified · Split
| @@ -297,6 +297,12 @@ dependencies = [ · 296 unmodified lines | ||
| 297 | 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 | 306 | [[package]] |
| 301 | 307 | name = "bytes" |
| 302 | 308 | version = "1.2.1" |
| @@ -954,6 +960,12 @@ dependencies = [ · 651 unmodified lines | ||
| 954 | 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 | 969 | [[package]] |
| 958 | 970 | name = "form_urlencoded" |
| 959 | 971 | version = "1.1.0" |
| @@ -1835,7 +1847,9 @@ dependencies = [ · 875 unmodified lines | ||
| 1835 | 1847 | "git-repository", |
| 1836 | 1848 | "git-transport", |
| 1837 | 1849 | "gitoxide-core", |
| 1850 | + "owo-colors", | |
| 1838 | 1851 | "prodash", |
| 1852 | + "tabled", | |
| 1839 | 1853 | ] |
| 1840 | 1854 | |
| 1841 | 1855 | [[package]] |
| @@ -2380,6 +2394,23 @@ dependencies = [ · 538 unmodified lines | ||
| 2380 | 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 | 2414 | [[package]] |
| 2384 | 2415 | name = "parking" |
| 2385 | 2416 | version = "2.0.0" |
| @@ -2926,6 +2957,16 @@ dependencies = [ · 540 unmodified lines | ||
| 2926 | 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 | 2970 | [[package]] |
| 2930 | 2971 | name = "tar" |
| 2931 | 2972 | version = "0.4.38" |
| @@ -92,6 +92,10 @@ env_logger = { version = "0.9.0", default-features = false } · 91 unmodified lines | ||
| 92 | 92 | crosstermion = { version = "0.10.1", optional = true, default-features = false } |
| 93 | 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 | 99 | document-features = { version = "0.2.0", optional = true } |
| 96 | 100 | |
| 97 | 101 | [profile.dev.package] |
| @@ -18,6 +18,21 @@ · 17 unmodified lines | ||
| 18 | 18 | * We `thiserror` generally. |
| 19 | 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 | 36 | ## General |
| 22 | 37 | |
| 23 | 38 | * **async** |
| @@ -27,6 +27,9 @@ Please see _'Development Status'_ for a listing of all crates and their capabili · 26 unmodified lines | ||
| 27 | 27 | * Based on the [git-hours] algorithm. |
| 28 | 28 | * See the [discussion][git-hours-discussion] for some performance data. |
| 29 | 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 | 33 | * **config** - list the complete git configuration in human-readable form and optionally filter sections by name. |
| 31 | 34 | * **exclude** |
| 32 | 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 | 57 | (enter git-odb && indent cargo diet -n --package-size-limit 120KB) |
| 58 | 58 | (enter git-protocol && indent cargo diet -n --package-size-limit 50KB) |
| 59 | 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 | 61 | (enter git-transport && indent cargo diet -n --package-size-limit 55KB) |
| 62 | 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 | 285 | deref: false, |
| 286 | 286 | }), |
| 287 | 287 | git_repository::lock::acquire::Fail::Immediately, |
| 288 | + git_repository::lock::acquire::Fail::Immediately, | |
| 288 | 289 | )? |
| 289 | 290 | .commit(repo.committer_or_default())?; |
| 290 | 291 | |
| @@ -2,7 +2,9 @@ use std::io; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | use git_tempfile::handle::Writable; |
| 4 | 4 | |
| 5 | +/// The error returned by [`Bundle::write_to_directory()`][crate::Bundle::write_to_directory()] | |
| 5 | 6 | #[derive(thiserror::Error, Debug)] |
| 7 | +#[allow(missing_docs)] | |
| 6 | 8 | pub enum Error { |
| 7 | 9 | #[error("An IO error occurred when reading the pack or creating a temporary file")] |
| 8 | 10 | Io(#[from] io::Error), |
| @@ -10,7 +10,7 @@ use git_tempfile::{handle::Writable, AutoRemove, ContainingDirectory}; · 9 unmodified lines | ||
| 10 | 10 | use crate::data; |
| 11 | 11 | |
| 12 | 12 | mod error; |
| 13 | −use error::Error; | |
| 13 | +pub use error::Error; | |
| 14 | 14 | |
| 15 | 15 | mod types; |
| 16 | 16 | use types::{LockWriter, PassThrough}; |
| @@ -228,7 +228,7 @@ impl crate::Bundle { · 211 unmodified lines | ||
| 228 | 228 | Options { |
| 229 | 229 | thread_limit, |
| 230 | 230 | iteration_mode: _, |
| 231 | − index_kind, | |
| 231 | + index_version: index_kind, | |
| 232 | 232 | object_hash, |
| 233 | 233 | }: Options, |
| 234 | 234 | data_file: Arc<parking_lot::Mutex<git_tempfile::Handle<Writable>>>, |
| @@ -11,7 +11,7 @@ pub struct Options { · 10 unmodified lines | ||
| 11 | 11 | /// Determine how much processing to spend on protecting against corruption or recovering from errors. |
| 12 | 12 | pub iteration_mode: crate::data::input::Mode, |
| 13 | 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 | 15 | /// The kind of hash to use when writing the bundle. |
| 16 | 16 | pub object_hash: git_hash::Kind, |
| 17 | 17 | } |
| @@ -22,7 +22,7 @@ impl Default for Options { · 4 unmodified lines | ||
| 22 | 22 | Options { |
| 23 | 23 | thread_limit: None, |
| 24 | 24 | iteration_mode: crate::data::input::Mode::Verify, |
| 25 | − index_kind: Default::default(), | |
| 25 | + index_version: Default::default(), | |
| 26 | 26 | object_hash: Default::default(), |
| 27 | 27 | } |
| 28 | 28 | } |
| @@ -14,7 +14,7 @@ pub enum Error { · 13 unmodified lines | ||
| 14 | 14 | IteratorInvariantNoRefDelta, |
| 15 | 15 | #[error("The iterator failed to set a trailing hash over all prior pack entries in the last provided entry")] |
| 16 | 16 | IteratorInvariantTrailer, |
| 17 | − #[error("Did not encounter a single base")] | |
| 17 | + #[error("Did not encounter a single base - refusing to write empty pack.")] | |
| 18 | 18 | IteratorInvariantBasesPresent, |
| 19 | 19 | #[error("Only u32::MAX objects can be stored in a pack, found {0}")] |
| 20 | 20 | IteratorInvariantTooManyObjects(usize), |
| @@ -18,7 +18,7 @@ pub(crate) struct TreeEntry { · 17 unmodified lines | ||
| 18 | 18 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| 19 | 19 | pub struct Outcome { |
| 20 | 20 | /// The version of the verified index |
| 21 | − pub index_kind: crate::index::Version, | |
| 21 | + pub index_version: crate::index::Version, | |
| 22 | 22 | /// The verified checksum of the verified index |
| 23 | 23 | pub index_hash: git_hash::ObjectId, |
| 24 | 24 | |
| @@ -171,7 +171,7 @@ impl crate::index::File { · 146 unmodified lines | ||
| 171 | 171 | modify_base(data, entry, bytes, kind.hash()); |
| 172 | 172 | Ok::<_, Error>(()) |
| 173 | 173 | }, |
| 174 | − crate::cache::delta::traverse::Options { | |
| 174 | + traverse::Options { | |
| 175 | 175 | object_progress: root_progress.add_child("Resolving"), |
| 176 | 176 | size_progress: root_progress.add_child("Decoding"), |
| 177 | 177 | thread_limit, |
| @@ -207,7 +207,7 @@ impl crate::index::File { · 29 unmodified lines | ||
| 207 | 207 | progress::MessageLevel::Success, |
| 208 | 208 | ); |
| 209 | 209 | Ok(Outcome { |
| 210 | − index_kind: kind, | |
| 210 | + index_version: kind, | |
| 211 | 211 | index_hash, |
| 212 | 212 | data_hash: pack_hash, |
| 213 | 213 | num_objects, |
| @@ -215,12 +215,7 @@ impl crate::index::File { · 1 unmodified line | ||
| 215 | 215 | } |
| 216 | 216 | } |
| 217 | 217 | |
| 218 | −fn modify_base( | |
| 219 | − entry: &mut crate::index::write::TreeEntry, | |
| 220 | − pack_entry: &crate::data::Entry, | |
| 221 | − decompressed: &[u8], | |
| 222 | − hash: git_hash::Kind, | |
| 223 | −) { | |
| 218 | +fn modify_base(entry: &mut TreeEntry, pack_entry: &crate::data::Entry, decompressed: &[u8], hash: git_hash::Kind) { | |
| 224 | 219 | fn compute_hash(kind: git_object::Kind, bytes: &[u8], object_hash: git_hash::Kind) -> git_hash::ObjectId { |
| 225 | 220 | let mut hasher = git_features::hash::hasher(object_hash); |
| 226 | 221 | hasher.update(&git_object::encode::loose_header(kind, bytes.len())); |
| @@ -90,7 +90,7 @@ mod write_to_directory { · 89 unmodified lines | ||
| 90 | 90 | fn expected_outcome() -> Result<pack::bundle::write::Outcome, Box<dyn std::error::Error>> { |
| 91 | 91 | Ok(pack::bundle::write::Outcome { |
| 92 | 92 | index: pack::index::write::Outcome { |
| 93 | − index_kind: pack::index::Version::V2, | |
| 93 | + index_version: pack::index::Version::V2, | |
| 94 | 94 | index_hash: git_hash::ObjectId::from_hex(b"544a7204a55f6e9cacccf8f6e191ea8f83575de3")?, |
| 95 | 95 | data_hash: git_hash::ObjectId::from_hex(b"0f3ea84cd1bba10c2a03d736a460635082833e59")?, |
| 96 | 96 | num_objects: 42, |
| @@ -156,7 +156,7 @@ mod write_to_directory { · 59 unmodified lines | ||
| 156 | 156 | pack::bundle::write::Options { |
| 157 | 157 | thread_limit: None, |
| 158 | 158 | iteration_mode: pack::data::input::Mode::Verify, |
| 159 | − index_kind: pack::index::Version::V2, | |
| 159 | + index_version: pack::index::Version::V2, | |
| 160 | 160 | object_hash: git_hash::Kind::Sha1, |
| 161 | 161 | }, |
| 162 | 162 | ) |
| @@ -321,6 +321,21 @@ fn traversals() -> crate::Result { · 320 unmodified lines | ||
| 321 | 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 | 339 | fn write_and_verify( |
| 325 | 340 | db: git_odb::HandleArc, |
| 326 | 341 | entries: Vec<output::Entry>, |
| @@ -205,7 +205,7 @@ mod version { · 204 unmodified lines | ||
| 205 | 205 | outcome.num_objects, num_objects, |
| 206 | 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 | 209 | assert_eq!( |
| 210 | 210 | outcome.index_hash, |
| 211 | 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 | 5 | use crate::fetch::{Arguments, Command}; |
| 6 | 6 | |
| 7 | 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 | 10 | &mut self, |
| 10 | 11 | transport: &'a mut T, |
| 11 | 12 | add_done_argument: bool, |
| @@ -3,6 +3,7 @@ use std::fmt; · 2 unmodified lines | ||
| 3 | 3 | use bstr::{BStr, BString, ByteVec}; |
| 4 | 4 | |
| 5 | 5 | /// The arguments passed to a server command. |
| 6 | +#[derive(Debug)] | |
| 6 | 7 | pub struct Arguments { |
| 7 | 8 | /// The active features/capabilities of the fetch invocation |
| 8 | 9 | #[cfg(any(feature = "async-client", feature = "blocking-client"))] |
| @@ -24,6 +25,13 @@ pub struct Arguments { · 15 unmodified lines | ||
| 24 | 25 | } |
| 25 | 26 | |
| 26 | 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 | 35 | /// Return true if ref filters is supported. |
| 28 | 36 | pub fn can_use_filter(&self) -> bool { |
| 29 | 37 | self.filter |
| @@ -125,8 +133,10 @@ impl Arguments { · 95 unmodified lines | ||
| 125 | 133 | fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) { |
| 126 | 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 | 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 | 140 | use crate::fetch::Command; |
| 131 | 141 | let has = |name: &str| features.iter().any(|f| f.0 == name); |
| 132 | 142 | let filter = has("filter"); |
| @@ -50,9 +50,12 @@ mod with_io { · 49 unmodified lines | ||
| 50 | 50 | "filter ", // filter-spec |
| 51 | 51 | // ref-in-want feature |
| 52 | 52 | "want-ref ", // ref path |
| 53 | + // sideband-all feature | |
| 53 | 54 | "sideband-all", |
| 54 | 55 | // packfile-uris feature |
| 55 | 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 | 82 | "no-done", |
| 80 | 83 | "filter", |
| 81 | 84 | ], |
| 82 | − git_transport::Protocol::V2 => { | |
| 83 | − &["shallow", "filter", "ref-in-want", "sideband-all", "packfile-uris"] | |
| 84 | − } | |
| 85 | + git_transport::Protocol::V2 => &[ | |
| 86 | + "shallow", | |
| 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 | 118 | &self, |
| 109 | 119 | version: git_transport::Protocol, |
| 110 | 120 | server_capabilities: &Capabilities, |
| @@ -3,7 +3,7 @@ use git_transport::client::Capabilities; · 2 unmodified lines | ||
| 3 | 3 | use crate::fetch::Ref; |
| 4 | 4 | |
| 5 | 5 | /// The result of the [`handshake()`][super::handshake()] function. |
| 6 | −#[derive(Debug, Clone)] | |
| 6 | +#[derive(Default, Debug, Clone)] | |
| 7 | 7 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| 8 | 8 | pub struct Outcome { |
| 9 | 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 | 10 | use crate::fetch::{indicate_end_of_interaction, refs::from_v2_refs, Command, LsRefsAction, Ref}; |
| 11 | 11 | |
| 12 | 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 | 14 | #[maybe_async] |
| 15 | 15 | pub async fn refs( |
| 16 | 16 | mut transport: impl Transport, |
| 17 | 17 | protocol_version: Protocol, |
| 18 | 18 | capabilities: &Capabilities, |
| 19 | − mut prepare_ls_refs: impl FnMut( | |
| 19 | + prepare_ls_refs: impl FnOnce( | |
| 20 | 20 | &Capabilities, |
| 21 | 21 | &mut Vec<BString>, |
| 22 | 22 | &mut Vec<(&str, Option<&str>)>, |
| @@ -50,7 +50,7 @@ pub mod parse { · 49 unmodified lines | ||
| 50 | 50 | pub enum Ref { |
| 51 | 51 | /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit |
| 52 | 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 | 54 | full_ref_name: BString, |
| 55 | 55 | /// The hash of the tag the ref points to. |
| 56 | 56 | tag: git_hash::ObjectId, |
| @@ -152,7 +152,8 @@ impl Response { · 151 unmodified lines | ||
| 152 | 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 | 158 | /// Even though technically any set of features supported by the server could work, we only implement the ones that |
| 158 | 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 | 46 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 47 | 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 | 58 | impl<T: client::Transport> client::Transport for Transport<T> { |
| @@ -89,6 +96,13 @@ mod impls { · 37 unmodified lines | ||
| 89 | 96 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 90 | 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 | 108 | #[async_trait(?Send)] |
| @@ -54,7 +54,7 @@ impl<'a> RemoteProgress<'a> { · 53 unmodified lines | ||
| 54 | 54 | progress.fail(progress_name(None, text)); |
| 55 | 55 | } |
| 56 | 56 | } else { |
| 57 | − match Self::from_bytes(text) { | |
| 57 | + match RemoteProgress::from_bytes(text) { | |
| 58 | 58 | Some(RemoteProgress { |
| 59 | 59 | action, |
| 60 | 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 | 50 | impl From<FullName> for BString { |
| 42 | 51 | fn from(name: FullName) -> Self { |
| 43 | 52 | name.0 |
| @@ -176,7 +176,8 @@ impl<'s> Transaction<'s> { · 175 unmodified lines | ||
| 176 | 176 | pub fn prepare( |
| 177 | 177 | mut self, |
| 178 | 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 | 181 | ) -> Result<Self, Error> { |
| 181 | 182 | assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times"); |
| 182 | 183 | let store = self.store; |
| @@ -267,7 +268,7 @@ impl<'s> Transaction<'s> { · 84 unmodified lines | ||
| 267 | 268 | let packed_transaction: Option<_> = if maybe_updates_for_packed_refs.unwrap_or(0) > 0 { |
| 268 | 269 | // We have to create a packed-ref even if it doesn't exist |
| 269 | 270 | self.store |
| 270 | − .packed_transaction(lock_fail_mode) | |
| 271 | + .packed_transaction(packed_refs_lock_fail_mode) | |
| 271 | 272 | .map_err(|err| match err { |
| 272 | 273 | file::packed::transaction::Error::BufferOpen(err) => Error::from(err), |
| 273 | 274 | file::packed::transaction::Error::TransactionLock(err) => { |
| @@ -280,7 +281,10 @@ impl<'s> Transaction<'s> { · 6 unmodified lines | ||
| 280 | 281 | // no packed-ref file exists anyway |
| 281 | 282 | self.store |
| 282 | 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 | 288 | .transpose()? |
| 285 | 289 | }; |
| 286 | 290 | if let Some(transaction) = packed_transaction { |
| @@ -302,7 +306,7 @@ impl<'s> Transaction<'s> { · 15 unmodified lines | ||
| 302 | 306 | let change = &mut updates[cid]; |
| 303 | 307 | if let Err(err) = Self::lock_ref_and_apply_change( |
| 304 | 308 | self.store, |
| 305 | − lock_fail_mode, | |
| 309 | + ref_files_lock_fail_mode, | |
| 306 | 310 | self.packed_transaction.as_ref().and_then(|t| t.buffer()), |
| 307 | 311 | change, |
| 308 | 312 | ) { |
| @@ -19,7 +19,7 @@ impl<'a> TargetRef<'a> { · 18 unmodified lines | ||
| 19 | 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 | 23 | pub fn id(&self) -> &oid { |
| 24 | 24 | match self { |
| 25 | 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 | 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 | 95 | pub fn previous_value(&self) -> Option<crate::TargetRef<'_>> { |
| 88 | 96 | match self { |
| 89 | 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 | 44 | deref: false, |
| 45 | 45 | }), |
| 46 | 46 | Fail::Immediately, |
| 47 | + Fail::Immediately, | |
| 47 | 48 | )? |
| 48 | 49 | .commit(committer().to_ref()); |
| 49 | 50 | if *is_empty { |
| @@ -83,6 +84,7 @@ fn reference_with_old_value_must_exist_when_creating_it() -> crate::Result { · 33 unmodified lines | ||
| 83 | 84 | deref: false, |
| 84 | 85 | }), |
| 85 | 86 | Fail::Immediately, |
| 87 | + Fail::Immediately, | |
| 86 | 88 | ); |
| 87 | 89 | |
| 88 | 90 | match res { |
| @@ -114,6 +116,7 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu · 25 unmodified lines | ||
| 114 | 116 | deref: false, |
| 115 | 117 | }), |
| 116 | 118 | Fail::Immediately, |
| 119 | + Fail::Immediately, | |
| 117 | 120 | ); |
| 118 | 121 | match res { |
| 119 | 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 | 145 | deref: false, |
| 143 | 146 | }), |
| 144 | 147 | Fail::Immediately, |
| 148 | + Fail::Immediately, | |
| 145 | 149 | )? |
| 146 | 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 | 184 | deref: false, |
| 181 | 185 | }), |
| 182 | 186 | Fail::Immediately, |
| 187 | + Fail::Immediately, | |
| 183 | 188 | ); |
| 184 | 189 | match res { |
| 185 | 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 | 213 | deref: false, |
| 209 | 214 | }), |
| 210 | 215 | Fail::Immediately, |
| 216 | + Fail::Immediately, | |
| 211 | 217 | ); |
| 212 | 218 | match res { |
| 213 | 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 | 254 | Fail::Immediately, |
| 255 | + Fail::Immediately, | |
| 249 | 256 | )? |
| 250 | 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 | 302 | deref: false, |
| 296 | 303 | }), |
| 297 | 304 | Fail::Immediately, |
| 305 | + Fail::Immediately, | |
| 298 | 306 | )? |
| 299 | 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 | 348 | deref: false, |
| 341 | 349 | }), |
| 342 | 350 | Fail::Immediately, |
| 351 | + Fail::Immediately, | |
| 343 | 352 | )? |
| 344 | 353 | .commit(committer().to_ref())?; |
| 345 | 354 | |
| @@ -387,6 +396,7 @@ fn cancellation_after_preparation_leaves_no_change() -> crate::Result { · 41 unmodified lines | ||
| 387 | 396 | deref: false, |
| 388 | 397 | }), |
| 389 | 398 | Fail::Immediately, |
| 399 | + Fail::Immediately, | |
| 390 | 400 | )?; |
| 391 | 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 | 434 | deref: false, |
| 425 | 435 | }), |
| 426 | 436 | Fail::Immediately, |
| 437 | + Fail::Immediately, | |
| 427 | 438 | )? |
| 428 | 439 | .commit(committer().to_ref())?; |
| 429 | 440 | assert_eq!( |
| @@ -475,6 +486,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { · 45 unmodified lines | ||
| 475 | 486 | deref: true, |
| 476 | 487 | }), |
| 477 | 488 | Fail::Immediately, |
| 489 | + Fail::Immediately, | |
| 478 | 490 | )? |
| 479 | 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 | 580 | deref: false, |
| 569 | 581 | }), |
| 570 | 582 | Fail::Immediately, |
| 583 | + Fail::Immediately, | |
| 571 | 584 | )? |
| 572 | 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 | 645 | name: "refs/heads/main".try_into()?, |
| 633 | 646 | deref: false, |
| 634 | 647 | }), |
| 635 | − git_lock::acquire::Fail::Immediately, | |
| 648 | + Fail::Immediately, | |
| 649 | + Fail::Immediately, | |
| 636 | 650 | )? |
| 637 | 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 | 701 | name: r.name, |
| 688 | 702 | deref: false, |
| 689 | 703 | }), |
| 690 | − git_lock::acquire::Fail::Immediately, | |
| 704 | + Fail::Immediately, | |
| 705 | + Fail::Immediately, | |
| 691 | 706 | )? |
| 692 | 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 | 758 | .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdates(Box::new(|_, _| { |
| 744 | 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 | 762 | .commit(committer().to_ref())?; |
| 748 | 763 | assert_eq!( |
| 749 | 764 | edits.len(), |
| @@ -28,6 +28,7 @@ fn delete_a_ref_which_is_gone_succeeds() -> crate::Result { · 27 unmodified lines | ||
| 28 | 28 | deref: false, |
| 29 | 29 | }), |
| 30 | 30 | Fail::Immediately, |
| 31 | + Fail::Immediately, | |
| 31 | 32 | )? |
| 32 | 33 | .commit(committer().to_ref())?; |
| 33 | 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 | 48 | deref: false, |
| 48 | 49 | }), |
| 49 | 50 | Fail::Immediately, |
| 51 | + Fail::Immediately, | |
| 50 | 52 | ); |
| 51 | 53 | match res { |
| 52 | 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 | 79 | deref: false, |
| 78 | 80 | }), |
| 79 | 81 | Fail::Immediately, |
| 82 | + Fail::Immediately, | |
| 80 | 83 | )? |
| 81 | 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 | 120 | deref: true, |
| 118 | 121 | }), |
| 119 | 122 | Fail::Immediately, |
| 123 | + Fail::Immediately, | |
| 120 | 124 | ); |
| 121 | 125 | |
| 122 | 126 | match res { |
| @@ -151,6 +155,7 @@ fn delete_reflog_only_of_symbolic_no_deref() -> crate::Result { · 28 unmodified lines | ||
| 151 | 155 | deref: false, |
| 152 | 156 | }), |
| 153 | 157 | Fail::Immediately, |
| 158 | + Fail::Immediately, | |
| 154 | 159 | )? |
| 155 | 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 | 190 | deref: true, |
| 186 | 191 | }), |
| 187 | 192 | Fail::Immediately, |
| 193 | + Fail::Immediately, | |
| 188 | 194 | )? |
| 189 | 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 | 224 | deref: true, |
| 219 | 225 | }), |
| 220 | 226 | Fail::Immediately, |
| 227 | + Fail::Immediately, | |
| 221 | 228 | ); |
| 222 | 229 | match res { |
| 223 | 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 | 255 | deref: true, |
| 249 | 256 | }), |
| 250 | 257 | Fail::Immediately, |
| 258 | + Fail::Immediately, | |
| 251 | 259 | )? |
| 252 | 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 | 292 | deref: true, |
| 285 | 293 | }), |
| 286 | 294 | Fail::Immediately, |
| 295 | + Fail::Immediately, | |
| 287 | 296 | )? |
| 288 | 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 | 334 | deref: false, |
| 326 | 335 | }), |
| 327 | 336 | Fail::Immediately, |
| 337 | + Fail::Immediately, | |
| 328 | 338 | )? |
| 329 | 339 | .commit(committer().to_ref())?; |
| 330 | 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 | 370 | deref: false, |
| 361 | 371 | }), |
| 362 | 372 | Fail::Immediately, |
| 373 | + Fail::Immediately, | |
| 363 | 374 | )? |
| 364 | 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 | 404 | deref: false, |
| 394 | 405 | }), |
| 395 | 406 | Fail::Immediately, |
| 407 | + Fail::Immediately, | |
| 396 | 408 | )? |
| 397 | 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 | 446 | Fail::Immediately, |
| 447 | + Fail::Immediately, | |
| 435 | 448 | )? |
| 436 | 449 | .commit(committer().to_ref())?; |
| 437 | 450 | |
| @@ -191,6 +191,7 @@ mod read_only { · 190 unmodified lines | ||
| 191 | 191 | } |
| 192 | 192 | |
| 193 | 193 | mod writable { |
| 194 | + use git_lock::acquire::Fail; | |
| 194 | 195 | use std::convert::TryInto; |
| 195 | 196 | |
| 196 | 197 | use git_ref::{ |
| @@ -258,7 +259,8 @@ mod writable { · 61 unmodified lines | ||
| 258 | 259 | deref: false, |
| 259 | 260 | }, |
| 260 | 261 | ], |
| 261 | − git_lock::acquire::Fail::Immediately, | |
| 262 | + Fail::Immediately, | |
| 263 | + Fail::Immediately, | |
| 262 | 264 | )? |
| 263 | 265 | .commit(committer().to_ref()) |
| 264 | 266 | .expect("successful commit as even similar resolved names live in different base locations"); |
| @@ -405,7 +407,8 @@ mod writable { · 140 unmodified lines | ||
| 405 | 407 | deref: false, |
| 406 | 408 | }, |
| 407 | 409 | ], |
| 408 | − git_lock::acquire::Fail::Immediately, | |
| 410 | + Fail::Immediately, | |
| 411 | + Fail::Immediately, | |
| 409 | 412 | ), |
| 410 | 413 | Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. }) |
| 411 | 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 | 427 | deref: false, |
| 425 | 428 | }, |
| 426 | 429 | ], |
| 427 | − git_lock::acquire::Fail::Immediately, | |
| 430 | + Fail::Immediately, | |
| 431 | + Fail::Immediately, | |
| 428 | 432 | ), |
| 429 | 433 | Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. }) |
| 430 | 434 | )); |
| @@ -467,7 +471,8 @@ mod writable { · 36 unmodified lines | ||
| 467 | 471 | deref: false, |
| 468 | 472 | }, |
| 469 | 473 | ], |
| 470 | − git_lock::acquire::Fail::Immediately, | |
| 474 | + Fail::Immediately, | |
| 475 | + Fail::Immediately, | |
| 471 | 476 | ), |
| 472 | 477 | Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. }) |
| 473 | 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 | 514 | deref: false, |
| 510 | 515 | }, |
| 511 | 516 | ], |
| 512 | − git_lock::acquire::Fail::Immediately, | |
| 517 | + Fail::Immediately, | |
| 518 | + Fail::Immediately, | |
| 513 | 519 | )? |
| 514 | 520 | .commit(committer().to_ref()) |
| 515 | 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 | 3 | use crate::{ |
| 4 | 4 | instruction::{Fetch, Push}, |
| @@ -18,6 +18,11 @@ impl RefSpec { · 13 unmodified lines | ||
| 18 | 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 | 28 | mod impls { |
| @@ -84,7 +89,7 @@ mod impls { · 60 unmodified lines | ||
| 84 | 89 | } |
| 85 | 90 | |
| 86 | 91 | /// Access |
| 87 | −impl RefSpecRef<'_> { | |
| 92 | +impl<'a> RefSpecRef<'a> { | |
| 88 | 93 | /// Return the left-hand side of the spec, typically the source. |
| 89 | 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 | 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 | 138 | match self.op { |
| 123 | 139 | Operation::Fetch => match (self.mode, self.src, self.dst) { |
| 124 | 140 | (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }), |
| @@ -6,4 +6,5 @@ mod impls; · 5 unmodified lines | ||
| 6 | 6 | mod match_group; |
| 7 | 7 | mod matching; |
| 8 | 8 | mod parse; |
| 9 | +mod spec; | |
| 9 | 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 | 13 | let git_dir = std::env::args_os() |
| 14 | 14 | .nth(1) |
| 15 | 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 | 18 | println!("Repo (bare): {:?}", repo.git_dir()); |
| 19 | 19 | |
| 20 | 20 | let mut tree = git::objs::Tree::empty(); |
| 21 | − let empty_tree_id = repo.write_object(&tree)?; | |
| 22 | − | |
| 23 | − let author = git::actor::SignatureRef { | |
| 24 | − name: "Maria Sanchez".into(), | |
| 25 | − email: "maria@example.com".into(), | |
| 26 | − time: git_date::Time::now_local_or_utc(), | |
| 27 | − }; | |
| 28 | − let initial_commit_id = repo.commit( | |
| 29 | − "HEAD", | |
| 30 | − author, | |
| 31 | − author, | |
| 32 | − "initial commit", | |
| 33 | − empty_tree_id, | |
| 34 | − git::commit::NO_PARENT_IDS, | |
| 35 | − )?; | |
| 36 | − | |
| 37 | − println!("initial commit id with empty tree: {:?}", initial_commit_id); | |
| 38 | − | |
| 39 | − let blob_id = repo.write_blob("hello world")?.into(); | |
| 40 | − let entry = tree::Entry { | |
| 41 | − mode: tree::EntryMode::Blob, | |
| 42 | − oid: blob_id, | |
| 43 | − filename: "hello.txt".into(), | |
| 44 | − }; | |
| 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); | |
| 21 | + let empty_tree_id = repo.write_object(&tree)?.detach(); | |
| 22 | + | |
| 23 | + let mut config = repo.config_snapshot_mut(); | |
| 24 | + config.set_raw_value("author", None, "name", "Maria Sanchez")?; | |
| 25 | + config.set_raw_value("author", None, "email", "maria@example.com")?; | |
| 26 | + { | |
| 27 | + let repo = config.commit_auto_rollback()?; | |
| 28 | + let initial_commit_id = repo.commit("HEAD", "initial commit", empty_tree_id, git::commit::NO_PARENT_IDS)?; | |
| 29 | + | |
| 30 | + println!("initial commit id with empty tree: {:?}", initial_commit_id); | |
| 31 | + | |
| 32 | + let blob_id = repo.write_blob("hello world")?.into(); | |
| 33 | + let entry = tree::Entry { | |
| 34 | + mode: tree::EntryMode::Blob, | |
| 35 | + oid: blob_id, | |
| 36 | + filename: "hello.txt".into(), | |
| 37 | + }; | |
| 38 | + | |
| 39 | + tree.entries.push(entry); | |
| 40 | + let hello_tree_id = repo.write_object(&tree)?; | |
| 41 | + | |
| 42 | + let blob_commit_id = repo.commit("HEAD", "hello commit", hello_tree_id, [initial_commit_id])?; | |
| 43 | + | |
| 44 | + println!("commit id for 'hello world' blob: {:?}", blob_commit_id); | |
| 45 | + } | |
| 59 | 46 | |
| 60 | 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 | 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 | 4 | /// Initialization |
| 7 | 5 | impl Cache { |
| @@ -17,7 +15,7 @@ impl Cache { · 9 unmodified lines | ||
| 17 | 15 | }: StageOne, |
| 18 | 16 | git_dir: &std::path::Path, |
| 19 | 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 | 19 | git_install_dir: Option<&std::path::Path>, |
| 22 | 20 | home: Option<&std::path::Path>, |
| 23 | 21 | repository::permissions::Environment { |
| @@ -113,36 +111,14 @@ impl Cache { · 89 unmodified lines | ||
| 113 | 111 | globals |
| 114 | 112 | }; |
| 115 | 113 | |
| 116 | − let excludes_file = match 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 | − }; | |
| 114 | + let hex_len = util::check_lenient(util::parse_core_abbrev(&config, object_hash), lenient_config)?; | |
| 131 | 115 | |
| 132 | 116 | use util::config_bool; |
| 133 | 117 | let reflog = util::query_refupdates(&config); |
| 134 | 118 | let ignore_case = config_bool(&config, "core.ignoreCase", false, lenient_config)?; |
| 135 | 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| { | |
| 137 | − Some(match value.as_ref().as_ref() { | |
| 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 | − }); | |
| 120 | + let object_kind_hint = util::disambiguate_hint(&config); | |
| 121 | + // NOTE: When adding a new initial cache, consider adjusting `reread_values_and_clear_caches()` as well. | |
| 146 | 122 | Ok(Cache { |
| 147 | 123 | resolved: config.into(), |
| 148 | 124 | use_multi_pack_index, |
| @@ -153,39 +129,41 @@ impl Cache { · 4 unmodified lines | ||
| 153 | 129 | ignore_case, |
| 154 | 130 | hex_len, |
| 155 | 131 | filter_config_section, |
| 156 | − excludes_file, | |
| 157 | 132 | xdg_config_home_env, |
| 158 | 133 | home_env, |
| 134 | + lenient_config, | |
| 159 | 135 | personas: Default::default(), |
| 160 | 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 | 138 | url_scheme: Default::default(), |
| 163 | 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. | |
| 168 | − pub fn xdg_config_path( | |
| 169 | − &self, | |
| 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. | |
| 143 | + /// Call this with new `config` to update values and clear caches. Note that none of the values will be applied if a single | |
| 144 | + /// one is invalid. | |
| 145 | + /// However, those that are lazily read won't be re-evaluated right away and might thus pass now but fail later. | |
| 183 | 146 | /// |
| 184 | − /// We never fail for here even if the permission is set to deny as we `git-config` will fail later | |
| 185 | − /// if it actually wants to use the home directory - we don't want to fail prematurely. | |
| 186 | − pub fn home_dir(&self) -> Option<PathBuf> { | |
| 187 | − std::env::var_os("HOME") | |
| 188 | − .map(PathBuf::from) | |
| 189 | − .and_then(|path| self.home_env.check_opt(path)) | |
| 147 | + /// Note that we unconditionally re-read all values. | |
| 148 | + pub fn reread_values_and_clear_caches(&mut self, config: crate::Config) -> Result<(), Error> { | |
| 149 | + let hex_len = util::check_lenient(util::parse_core_abbrev(&config, self.object_hash), self.lenient_config)?; | |
| 150 | + | |
| 151 | + use util::config_bool; | |
| 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 | 1 | use super::{Cache, Error}; |
| 2 | −use crate::{remote, repository::identity}; | |
| 3 | 2 | |
| 4 | 3 | mod incubate; |
| 5 | 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 | |
| 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 | −} | |
| 14 | +mod access; | |
| 36 | 15 | |
| 37 | 16 | mod util; |
| 38 | 17 | pub(crate) use util::interpolate_context; |
| @@ -2,6 +2,7 @@ use std::convert::TryFrom; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | use super::Error; |
| 4 | 4 | use crate::bstr::ByteSlice; |
| 5 | +use crate::revision::spec::parse::ObjectKindHint; | |
| 5 | 6 | |
| 6 | 7 | pub(crate) fn interpolate_context<'a>( |
| 7 | 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 | 66 | pub(crate) fn parse_core_abbrev( |
| 58 | 67 | config: &git_config::File<'static>, |
| 59 | 68 | object_hash: git_hash::Kind, |
| @@ -93,3 +102,16 @@ pub(crate) fn parse_core_abbrev( · 33 unmodified lines | ||
| 93 | 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 | 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 | 19 | /// Note that the values will only affect this instance of the parent repository, and not other clones that may exist. |
| 19 | 20 | /// |
| 20 | 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 | 26 | pub struct SnapshotMut<'repo> { |
| 23 | − pub(crate) repo: &'repo mut Repository, | |
| 27 | + pub(crate) repo: Option<&'repo mut Repository>, | |
| 24 | 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 | 37 | pub(crate) mod section { |
| 28 | 38 | pub fn is_trusted(meta: &git_config::file::Metadata) -> bool { |
| 29 | 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 | 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 | 71 | #[derive(Clone)] |
| 59 | 72 | pub(crate) struct Cache { |
| 60 | 73 | pub resolved: crate::Config, |
| @@ -73,7 +86,7 @@ pub(crate) struct Cache { · 12 unmodified lines | ||
| 73 | 86 | /// A lazily loaded rewrite list for remote urls |
| 74 | 87 | pub url_rewrite: OnceCell<remote::url::Rewrite>, |
| 75 | 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 | 90 | pub url_scheme: OnceCell<remote::url::SchemePermission>, |
| 78 | 91 | /// The config section filter from the options used to initialize this instance. Keep these in sync! |
| 79 | 92 | filter_config_section: fn(&git_config::file::Metadata) -> bool, |
| @@ -81,8 +94,9 @@ pub(crate) struct Cache { · 1 unmodified line | ||
| 81 | 94 | pub object_kind_hint: Option<spec::parse::ObjectKindHint>, |
| 82 | 95 | /// If true, we are on a case-insensitive file system. |
| 83 | 96 | pub ignore_case: bool, |
| 84 | − /// The path to the user-level excludes file to ignore certain files in the worktree. | |
| 85 | − pub excludes_file: Option<std::path::PathBuf>, | |
| 97 | + /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient. | |
| 98 | + /// Also available in options! Keep in sync! | |
| 99 | + pub lenient_config: bool, | |
| 86 | 100 | /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable. |
| 87 | 101 | xdg_config_home_env: git_sec::Permission, |
| 88 | 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 | 2 | use std::borrow::Cow; |
| 2 | 3 | |
| 4 | +use crate::config::{CommitAutoRollback, SnapshotMut}; | |
| 3 | 5 | use crate::{ |
| 4 | 6 | bstr::BStr, |
| 5 | 7 | config::{cache::interpolate_context, Snapshot}, |
| @@ -90,3 +92,59 @@ impl<'repo> Snapshot<'repo> { · 84 unmodified lines | ||
| 90 | 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 | 23 | impl SnapshotMut<'_> { |
| 24 | 24 | /// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true` |
| 25 | 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 | 30 | let mut file = git_config::File::new(git_config::file::Metadata::from(git_config::Source::Cli)); |
| 28 | 31 | for key_value in values { |
| 29 | 32 | let key_value = key_value.as_ref(); |
| @@ -44,6 +47,6 @@ impl SnapshotMut<'_> { · 14 unmodified lines | ||
| 44 | 47 | ); |
| 45 | 48 | } |
| 46 | 49 | self.config.append(file); |
| 47 | − Ok(()) | |
| 50 | + Ok(self) | |
| 48 | 51 | } |
| 49 | 52 | } |
| @@ -1,3 +1,4 @@ | ||
| 1 | +mod _impls; | |
| 1 | 2 | mod access; |
| 2 | 3 | |
| 3 | 4 | /// |
| @@ -5,44 +6,3 @@ pub mod apply_cli_overrides; · 1 unmodified line | ||
| 5 | 6 | |
| 6 | 7 | /// |
| 7 | 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 | 87 | /// Remote |
| 88 | 88 | impl<'repo> Head<'repo> { |
| 89 | 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 | 93 | /// This is equivalent to calling [`Reference::remote(…)`][crate::Reference::remote()] and |
| 93 | 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 | 98 | pub fn into_remote( |
| 95 | 99 | self, |
| 96 | 100 | direction: remote::Direction, |
| 97 | 101 | ) -> Option<Result<Remote<'repo>, remote::find::existing::Error>> { |
| 98 | 102 | let repo = self.repo; |
| 99 | − self.try_into_referent()?.remote(direction).or_else(|| { | |
| 100 | − repo.remote_default_name(direction) | |
| 101 | − .map(|name| repo.find_remote(name.as_ref())) | |
| 102 | − }) | |
| 103 | + self.try_into_referent()? | |
| 104 | + .remote(direction) | |
| 105 | + .or_else(|| repo.find_default_remote(direction)) | |
| 103 | 106 | } |
| 104 | 107 | } |
| 105 | 108 | } |
| @@ -201,7 +201,7 @@ impl Options { · 200 unmodified lines | ||
| 201 | 201 | /// If set, default is false, invalid configuration values will cause an error even if these can safely be defaulted. |
| 202 | 202 | /// |
| 203 | 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 | 205 | pub fn strict_config(mut self, toggle: bool) -> Self { |
| 206 | 206 | self.lenient_config = !toggle; |
| 207 | 207 | self |
| @@ -55,35 +55,20 @@ pub mod delete { · 54 unmodified lines | ||
| 55 | 55 | |
| 56 | 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 | 58 | impl<'repo> Reference<'repo> { |
| 71 | 59 | /// Delete this reference or fail if it was changed since last observed. |
| 72 | 60 | /// Note that this instance remains available in memory but probably shouldn't be used anymore. |
| 73 | − pub fn delete(&self) -> Result<(), Error> { | |
| 74 | − self.repo.edit_reference( | |
| 75 | − RefEdit { | |
| 61 | + pub fn delete(&self) -> Result<(), crate::reference::edit::Error> { | |
| 62 | + self.repo | |
| 63 | + .edit_reference(RefEdit { | |
| 76 | 64 | change: Change::Delete { |
| 77 | 65 | expected: PreviousValue::MustExistAndMatch(self.inner.target.clone()), |
| 78 | 66 | log: RefLog::AndReference, |
| 79 | 67 | }, |
| 80 | 68 | name: self.inner.name.clone(), |
| 81 | 69 | deref: false, |
| 82 | − }, | |
| 83 | − Fail::Immediately, | |
| 84 | − self.repo.committer_or_default(), | |
| 85 | − )?; | |
| 86 | − Ok(()) | |
| 70 | + }) | |
| 71 | + .map(|_| ()) | |
| 87 | 72 | } |
| 88 | 73 | } |
| 89 | 74 | } |
| @@ -11,6 +11,8 @@ pub mod edit { · 10 unmodified lines | ||
| 11 | 11 | FileTransactionCommit(#[from] git_ref::file::transaction::commit::Error), |
| 12 | 12 | #[error(transparent)] |
| 13 | 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 | 72 | /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like |
| 73 | 73 | /// |
| 74 | 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 | 77 | pub fn remote( |
| 76 | 78 | &self, |
| 77 | 79 | direction: remote::Direction, |
| @@ -1,8 +1,6 @@ | ||
| 1 | −use git_protocol::transport::client::Transport; | |
| 2 | − | |
| 3 | 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 | 4 | mod error { |
| 7 | 5 | use crate::{bstr::BString, remote}; |
| 8 | 6 | |
| @@ -24,10 +22,9 @@ mod error { · 15 unmodified lines | ||
| 24 | 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 | 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 | 28 | impl<'repo> Remote<'repo> { |
| 32 | 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 | 38 | Connection { |
| 42 | 39 | remote: self, |
| 43 | − credentials: None, | |
| 40 | + authenticate: None, | |
| 44 | 41 | transport, |
| 45 | 42 | progress, |
| 46 | 43 | } |
| @@ -50,6 +47,10 @@ impl<'repo> Remote<'repo> { · 3 unmodified lines | ||
| 50 | 47 | /// |
| 51 | 48 | /// Note that the `protocol.version` configuration key affects the transport protocol used to connect, |
| 52 | 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 | 54 | #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] |
| 54 | 55 | #[git_protocol::maybe_async::maybe_async] |
| 55 | 56 | pub async fn connect<P>( |
| @@ -60,7 +61,18 @@ impl<'repo> Remote<'repo> { · 4 unmodified lines | ||
| 60 | 61 | where |
| 61 | 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 | 76 | fn sanitize(mut url: git_url::Url) -> Result<git_url::Url, Error> { |
| 65 | 77 | if url.scheme == git_url::Scheme::File { |
| 66 | 78 | let mut dir = git_path::from_bstr(url.path.as_ref()); |
| @@ -75,6 +87,7 @@ impl<'repo> Remote<'repo> { · 8 unmodified lines | ||
| 75 | 87 | Ok(url) |
| 76 | 88 | } |
| 77 | 89 | |
| 90 | + use git_protocol::transport::Protocol; | |
| 78 | 91 | let version = self |
| 79 | 92 | .repo |
| 80 | 93 | .config |
| @@ -101,7 +114,6 @@ impl<'repo> Remote<'repo> { · 20 unmodified lines | ||
| 101 | 114 | scheme: url.scheme, |
| 102 | 115 | }); |
| 103 | 116 | } |
| 104 | − let transport = git_protocol::transport::connect(sanitize(url)?, version).await?; | |
| 105 | − Ok(self.to_connection_with_transport(transport, progress)) | |
| 117 | + Ok((sanitize(url)?, version)) | |
| 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 | 5 | refs: Vec<git_protocol::fetch::Ref>, |
| 6 | 6 | } |
| 7 | 7 | |
| 8 | −/// A function that performs a given credential action. | |
| 9 | −pub type CredentialsFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a>; | |
| 8 | +/// A function that performs a given credential action, trying to obtain credentials for an operation that needs it. | |
| 9 | +pub type AuthenticateFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a>; | |
| 10 | 10 | |
| 11 | 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 | 14 | /// much like a remote procedure call. |
| 15 | 15 | pub struct Connection<'a, 'repo, T, P> { |
| 16 | 16 | pub(crate) remote: &'a Remote<'repo>, |
| 17 | − pub(crate) credentials: Option<CredentialsFn<'a>>, | |
| 17 | + pub(crate) authenticate: Option<AuthenticateFn<'a>>, | |
| 18 | 18 | pub(crate) transport: T, |
| 19 | 19 | pub(crate) progress: P, |
| 20 | 20 | } |
| 21 | 21 | |
| 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 | −} | |
| 22 | +mod access; | |
| 67 | 23 | |
| 68 | 24 | /// |
| 69 | 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 | 2 | use git_features::progress::Progress; |
| 2 | 3 | use git_protocol::transport::client::Transport; |
| 4 | +use std::collections::HashSet; | |
| 3 | 5 | |
| 4 | 6 | use crate::remote::{connection::HandshakeWithRefs, fetch, Connection, Direction}; |
| 5 | 7 | |
| @@ -19,6 +21,27 @@ pub enum Error { · 13 unmodified lines | ||
| 19 | 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 | 45 | impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P> |
| 23 | 46 | where |
| 24 | 47 | T: Transport, |
| @@ -31,16 +54,31 @@ where · 6 unmodified lines | ||
| 31 | 54 | /// with the local tracking branch of these tips (if available). |
| 32 | 55 | /// |
| 33 | 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 | 62 | #[git_protocol::maybe_async::maybe_async] |
| 35 | − pub async fn ref_map(mut self) -> Result<fetch::RefMap<'remote>, Error> { | |
| 36 | − let res = self.ref_map_inner().await; | |
| 37 | − git_protocol::fetch::indicate_end_of_interaction(&mut self.transport).await?; | |
| 63 | + pub async fn ref_map(mut self, options: Options) -> Result<fetch::RefMap<'remote>, Error> { | |
| 64 | + let res = self.ref_map_inner(options).await; | |
| 65 | + git_protocol::fetch::indicate_end_of_interaction(&mut self.transport) | |
| 66 | + .await | |
| 67 | + .ok(); | |
| 38 | 68 | res |
| 39 | 69 | } |
| 40 | 70 | |
| 41 | 71 | #[git_protocol::maybe_async::maybe_async] |
| 42 | − async fn ref_map_inner(&mut self) -> Result<fetch::RefMap<'remote>, Error> { | |
| 43 | − let remote = self.fetch_refs().await?; | |
| 72 | + pub(crate) async fn ref_map_inner( | |
| 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 | 82 | let group = git_refspec::MatchGroup::from_fetch_specs(self.remote.fetch_specs.iter().map(|s| s.to_ref())); |
| 45 | 83 | let (res, fixes) = group |
| 46 | 84 | .match_remotes(remote.refs.iter().map(|r| { |
| @@ -77,9 +115,13 @@ where · 30 unmodified lines | ||
| 77 | 115 | }) |
| 78 | 116 | } |
| 79 | 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 | 123 | let mut credentials_storage; |
| 82 | − let authenticate = match self.credentials.as_mut() { | |
| 124 | + let authenticate = match self.authenticate.as_mut() { | |
| 83 | 125 | Some(f) => f, |
| 84 | 126 | None => { |
| 85 | 127 | let url = self |
| @@ -95,15 +137,32 @@ where · 9 unmodified lines | ||
| 95 | 137 | } |
| 96 | 138 | }; |
| 97 | 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 | 142 | let refs = match outcome.refs.take() { |
| 100 | 143 | Some(refs) => refs, |
| 101 | 144 | None => { |
| 145 | + let specs = &self.remote.fetch_specs; | |
| 102 | 146 | git_protocol::fetch::refs( |
| 103 | 147 | &mut self.transport, |
| 104 | 148 | outcome.server_protocol_version, |
| 105 | 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 | 166 | &mut self.progress, |
| 108 | 167 | ) |
| 109 | 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 | 29 | #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] |
| 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 | −} | |
| 30 | +pub mod fetch; | |
| 68 | 31 | |
| 69 | 32 | /// |
| 70 | 33 | #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] |
| @@ -1,7 +1,7 @@ | ||
| 1 | 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 | 4 | pub mod scheme_permission; |
| 5 | 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 | 7 | pub(crate) use scheme_permission::SchemePermission; |
| @@ -9,10 +9,17 @@ impl crate::Repository { · 8 unmodified lines | ||
| 9 | 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 | 17 | pub fn config_snapshot_mut(&mut self) -> config::SnapshotMut<'_> { |
| 14 | 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 | 25 | /// The options used to open the repository. |
| @@ -4,7 +4,8 @@ use crate::bstr::BString; · 3 unmodified lines | ||
| 4 | 4 | |
| 5 | 5 | /// Identity handling. |
| 6 | 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 | 9 | /// in a similar vein as the default that git chooses if there is nothing configured. |
| 9 | 10 | /// |
| 10 | 11 | /// This can be useful as fallback for an unset `committer` or `author`. |
| @@ -16,7 +17,13 @@ impl crate::Repository { · 5 unmodified lines | ||
| 16 | 17 | git_actor::SignatureRef { |
| 17 | 18 | name: "gitoxide".into(), |
| 18 | 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 | 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 | 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 | 133 | /// `reference` will be created if it doesn't exist, and can be `"HEAD"` to automatically write-through to the symbolic reference |
| 132 | 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 | 141 | pub fn commit<Name, E>( |
| 140 | 142 | &self, |
| 141 | 143 | reference: Name, |
| 142 | − author: git_actor::SignatureRef<'_>, | |
| 143 | − committer: git_actor::SignatureRef<'_>, | |
| 144 | 144 | message: impl AsRef<str>, |
| 145 | 145 | tree: impl Into<ObjectId>, |
| 146 | 146 | parents: impl IntoIterator<Item = impl Into<ObjectId>>, |
| @@ -157,6 +157,8 @@ impl crate::Repository { · 10 unmodified lines | ||
| 157 | 157 | // TODO: possibly use CommitRef to save a few allocations (but will have to allocate for object ids anyway. |
| 158 | 158 | // This can be made vastly more efficient though if we wanted to, so we lie in the API |
| 159 | 159 | let reference = reference.try_into()?; |
| 160 | + let author = self.author_or_default(); | |
| 161 | + let committer = self.committer_or_default(); | |
| 160 | 162 | let commit = git_object::Commit { |
| 161 | 163 | message: message.as_ref().into(), |
| 162 | 164 | tree: tree.into(), |
| @@ -168,36 +170,28 @@ impl crate::Repository { · 5 unmodified lines | ||
| 168 | 170 | }; |
| 169 | 171 | |
| 170 | 172 | let commit_id = self.write_object(&commit)?; |
| 171 | − self.edit_reference( | |
| 172 | − RefEdit { | |
| 173 | − change: Change::Update { | |
| 174 | − log: LogChange { | |
| 175 | − mode: RefLog::AndReference, | |
| 176 | − force_create_reflog: false, | |
| 177 | − message: crate::reference::log::message( | |
| 178 | − "commit", | |
| 179 | − commit.message.as_ref(), | |
| 180 | − commit.parents.len(), | |
| 181 | − ), | |
| 182 | − }, | |
| 183 | − expected: match commit.parents.first().map(|p| Target::Peeled(*p)) { | |
| 184 | − Some(previous) => { | |
| 185 | − if reference.as_bstr() == "HEAD" { | |
| 186 | − PreviousValue::MustExistAndMatch(previous) | |
| 187 | − } else { | |
| 188 | − PreviousValue::ExistingMustMatch(previous) | |
| 189 | − } | |
| 173 | + self.edit_reference(RefEdit { | |
| 174 | + change: Change::Update { | |
| 175 | + log: LogChange { | |
| 176 | + mode: RefLog::AndReference, | |
| 177 | + force_create_reflog: false, | |
| 178 | + message: crate::reference::log::message("commit", commit.message.as_ref(), commit.parents.len()), | |
| 179 | + }, | |
| 180 | + expected: match commit.parents.first().map(|p| Target::Peeled(*p)) { | |
| 181 | + Some(previous) => { | |
| 182 | + if reference.as_bstr() == "HEAD" { | |
| 183 | + PreviousValue::MustExistAndMatch(previous) | |
| 184 | + } else { | |
| 185 | + PreviousValue::ExistingMustMatch(previous) | |
| 190 | 186 | } |
| 191 | − None => PreviousValue::MustNotExist, | |
| 192 | − }, | |
| 193 | − new: Target::Peeled(commit_id.inner), | |
| 187 | + } | |
| 188 | + None => PreviousValue::MustNotExist, | |
| 194 | 189 | }, |
| 195 | − name: reference, | |
| 196 | − deref: true, | |
| 190 | + new: Target::Peeled(commit_id.inner), | |
| 197 | 191 | }, |
| 198 | − git_lock::acquire::Fail::Immediately, | |
| 199 | − commit.committer.to_ref(), | |
| 200 | − )?; | |
| 192 | + name: reference, | |
| 193 | + deref: true, | |
| 194 | + })?; | |
| 201 | 195 | Ok(commit_id) |
| 202 | 196 | } |
| 203 | 197 | } |
| @@ -1,8 +1,6 @@ | ||
| 1 | 1 | use std::convert::TryInto; |
| 2 | 2 | |
| 3 | −use git_actor as actor; | |
| 4 | 3 | use git_hash::ObjectId; |
| 5 | −use git_lock as lock; | |
| 6 | 4 | use git_ref::{ |
| 7 | 5 | transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, |
| 8 | 6 | FullName, PartialNameRef, Target, |
| @@ -10,8 +8,6 @@ use git_ref::{ · 1 unmodified line | ||
| 10 | 8 | |
| 11 | 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 | 11 | /// Obtain and alter references comfortably |
| 16 | 12 | impl crate::Repository { |
| 17 | 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 | 21 | constraint: PreviousValue, |
| 26 | 22 | ) -> Result<Reference<'_>, reference::edit::Error> { |
| 27 | 23 | let id = target.into(); |
| 28 | − let mut edits = self.edit_reference( | |
| 29 | − RefEdit { | |
| 30 | − change: Change::Update { | |
| 31 | − log: Default::default(), | |
| 32 | − expected: constraint, | |
| 33 | − new: Target::Peeled(id), | |
| 34 | − }, | |
| 35 | − name: format!("refs/tags/{}", name.as_ref()).try_into()?, | |
| 36 | − deref: false, | |
| 24 | + let mut edits = self.edit_reference(RefEdit { | |
| 25 | + change: Change::Update { | |
| 26 | + log: Default::default(), | |
| 27 | + expected: constraint, | |
| 28 | + new: Target::Peeled(id), | |
| 37 | 29 | }, |
| 38 | − DEFAULT_LOCK_MODE, | |
| 39 | − self.committer_or_default(), | |
| 40 | − )?; | |
| 30 | + name: format!("refs/tags/{}", name.as_ref()).try_into()?, | |
| 31 | + deref: false, | |
| 32 | + })?; | |
| 41 | 33 | assert_eq!(edits.len(), 1, "reference splits should ever happen"); |
| 42 | 34 | let edit = edits.pop().expect("exactly one item"); |
| 43 | 35 | Ok(Reference { |
| @@ -95,23 +87,19 @@ impl crate::Repository { · 51 unmodified lines | ||
| 95 | 87 | { |
| 96 | 88 | let name = name.try_into().map_err(git_validate::reference::name::Error::from)?; |
| 97 | 89 | let id = target.into(); |
| 98 | − let mut edits = self.edit_reference( | |
| 99 | − RefEdit { | |
| 100 | − change: Change::Update { | |
| 101 | − log: LogChange { | |
| 102 | − mode: RefLog::AndReference, | |
| 103 | − force_create_reflog: false, | |
| 104 | − message: log_message.into(), | |
| 105 | − }, | |
| 106 | − expected: constraint, | |
| 107 | − new: Target::Peeled(id), | |
| 90 | + let mut edits = self.edit_reference(RefEdit { | |
| 91 | + change: Change::Update { | |
| 92 | + log: LogChange { | |
| 93 | + mode: RefLog::AndReference, | |
| 94 | + force_create_reflog: false, | |
| 95 | + message: log_message.into(), | |
| 108 | 96 | }, |
| 109 | − name, | |
| 110 | − deref: false, | |
| 97 | + expected: constraint, | |
| 98 | + new: Target::Peeled(id), | |
| 111 | 99 | }, |
| 112 | − DEFAULT_LOCK_MODE, | |
| 113 | − self.committer_or_default(), | |
| 114 | − )?; | |
| 100 | + name, | |
| 101 | + deref: false, | |
| 102 | + })?; | |
| 115 | 103 | assert_eq!( |
| 116 | 104 | edits.len(), |
| 117 | 105 | 1, |
| @@ -126,34 +114,29 @@ impl crate::Repository { · 8 unmodified lines | ||
| 126 | 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 | 119 | /// One or more `RefEdit`s are returned - symbolic reference splits can cause more edits to be performed. All edits have the previous |
| 132 | 120 | /// reference values set to the ones encountered at rest after acquiring the respective reference's lock. |
| 133 | − pub fn edit_reference( | |
| 134 | − &self, | |
| 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) | |
| 121 | + pub fn edit_reference(&self, edit: RefEdit) -> Result<Vec<RefEdit>, reference::edit::Error> { | |
| 122 | + self.edit_references(Some(edit)) | |
| 140 | 123 | } |
| 141 | 124 | |
| 142 | − /// Edit one or more references as described by their `edits`, with `lock_mode` deciding on how to handle competing | |
| 143 | − /// transactions. `log_committer` is the name appearing in reference logs. | |
| 125 | + /// Edit one or more references as described by their `edits`. | |
| 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 | 129 | /// Returns all reference edits, which might be more than where provided due the splitting of symbolic references, and |
| 146 | 130 | /// whose previous (_old_) values are the ones seen on in storage after the reference was locked. |
| 147 | 131 | pub fn edit_references( |
| 148 | 132 | &self, |
| 149 | 133 | edits: impl IntoIterator<Item = RefEdit>, |
| 150 | − lock_mode: lock::acquire::Fail, | |
| 151 | − log_committer: actor::SignatureRef<'_>, | |
| 152 | 134 | ) -> Result<Vec<RefEdit>, reference::edit::Error> { |
| 135 | + let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout()?; | |
| 153 | 136 | self.refs |
| 154 | 137 | .transaction() |
| 155 | − .prepare(edits, lock_mode)? | |
| 156 | − .commit(log_committer) | |
| 138 | + .prepare(edits, file_lock_fail, packed_refs_lock_fail)? | |
| 139 | + .commit(self.committer_or_default()) | |
| 157 | 140 | .map_err(Into::into) |
| 158 | 141 | } |
| 159 | 142 | |
| @@ -32,6 +32,17 @@ impl crate::Repository { · 31 unmodified lines | ||
| 32 | 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 | 46 | /// Find the remote with the given `name` or return `None` if it doesn't exist, for the purpose of fetching or pushing |
| 36 | 47 | /// data to a remote. |
| 37 | 48 | /// |
| @@ -25,21 +25,11 @@ impl crate::Repository { · 24 unmodified lines | ||
| 25 | 25 | res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir)); |
| 26 | 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 | 30 | /// Interact with individual worktrees and their information. |
| 41 | 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 | 34 | /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree. |
| 45 | 35 | /// Also note that the main repo might be bare. |
| @@ -112,6 +112,8 @@ pub mod excludes { · 111 unmodified lines | ||
| 112 | 112 | Io(#[from] std::io::Error), |
| 113 | 113 | #[error(transparent)] |
| 114 | 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 | 119 | impl<'repo> crate::Worktree<'repo> { |
| @@ -135,7 +137,7 @@ pub mod excludes { · 17 unmodified lines | ||
| 135 | 137 | overrides.unwrap_or_default(), |
| 136 | 138 | git_attributes::MatchGroup::<git_attributes::Ignore>::from_git_dir( |
| 137 | 139 | repo.git_dir(), |
| 138 | − match repo.config.excludes_file.as_ref() { | |
| 140 | + match repo.config.excludes_file()?.as_ref() { | |
| 139 | 141 | Some(user_path) => Some(user_path.to_owned()), |
| 140 | 142 | None => repo.config.xdg_config_path("ignore")?, |
| 141 | 143 | }, |
| @@ -1,3 +1,4 @@ | ||
| 1 | 1 | /make_worktree_repo.tar.xz |
| 2 | 2 | /make_remote_repos.tar.xz |
| 3 | +/make_fetch_repos.tar.xz | |
| 3 | 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 | 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 | 2 | use git_repository as git; |
| 3 | 3 | |
| 4 | 4 | use crate::remote; |
| @@ -12,4 +12,14 @@ mod remote { · 7 unmodified lines | ||
| 12 | 12 | ); |
| 13 | 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 | 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 | 26 | let remote = repo.find_remote("origin")?; |
| 27 | − let connection = remote.connect(Fetch, progress::Discard)?; | |
| 28 | − let map = connection.ref_map()?; | |
| 27 | + let map = remote.connect(Fetch, progress::Discard)?.ref_map(Default::default())?; | |
| 29 | 28 | assert_eq!( |
| 30 | 29 | map.remote_refs.len(), |
| 31 | 30 | 14, |
| @@ -36,7 +35,7 @@ mod blocking_io { · 4 unmodified lines | ||
| 36 | 35 | assert_eq!( |
| 37 | 36 | map.mappings.len(), |
| 38 | 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 | 41 | Ok(()) |
| @@ -1,5 +1,49 @@ | ||
| 1 | 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 | 47 | #[test] |
| 4 | 48 | fn values_are_set_in_memory_only() { |
| 5 | 49 | let mut repo = named_repo("make_config_repo.sh").unwrap(); |
| @@ -10,7 +10,7 @@ mod worktree; · 9 unmodified lines | ||
| 10 | 10 | |
| 11 | 11 | #[test] |
| 12 | 12 | fn size_in_memory() { |
| 13 | − let expected = [760, 800]; | |
| 13 | + let expected = [728, 744, 784]; | |
| 14 | 14 | let actual_size = std::mem::size_of::<Repository>(); |
| 15 | 15 | assert!( |
| 16 | 16 | expected.contains(&actual_size), |
| @@ -147,23 +147,17 @@ mod tag { · 146 unmodified lines | ||
| 147 | 147 | } |
| 148 | 148 | |
| 149 | 149 | mod commit { |
| 150 | + use crate::{freeze_time, restricted_and_git}; | |
| 150 | 151 | use git_repository as git; |
| 151 | 152 | use git_testtools::hex_to_id; |
| 153 | + | |
| 152 | 154 | #[test] |
| 153 | 155 | fn parent_in_initial_commit_causes_failure() { |
| 154 | 156 | let tmp = tempfile::tempdir().unwrap(); |
| 155 | 157 | let repo = git::init(&tmp).unwrap(); |
| 156 | 158 | let empty_tree_id = repo.write_object(&git::objs::Tree::empty()).unwrap().detach(); |
| 157 | − let author = git::actor::Signature::empty(); | |
| 158 | 159 | let err = repo |
| 159 | − .commit( | |
| 160 | − "HEAD", | |
| 161 | − author.to_ref(), | |
| 162 | − author.to_ref(), | |
| 163 | − "initial", | |
| 164 | − empty_tree_id, | |
| 165 | − [empty_tree_id], | |
| 166 | − ) | |
| 160 | + .commit("HEAD", "initial", empty_tree_id, [empty_tree_id]) | |
| 167 | 161 | .unwrap_err(); |
| 168 | 162 | assert_eq!( |
| 169 | 163 | err.to_string(), |
| @@ -173,22 +167,16 @@ mod commit { · 3 unmodified lines | ||
| 173 | 167 | } |
| 174 | 168 | |
| 175 | 169 | #[test] |
| 170 | + #[serial_test::serial] | |
| 176 | 171 | fn single_line_initial_commit_empty_tree_ref_nonexisting() -> crate::Result { |
| 172 | + let _env = freeze_time(); | |
| 177 | 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 | 175 | let empty_tree_id = repo.write_object(&git::objs::Tree::empty())?; |
| 180 | − let author = git::actor::Signature::empty(); | |
| 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 | − )?; | |
| 176 | + let commit_id = repo.commit("HEAD", "initial", empty_tree_id, git::commit::NO_PARENT_IDS)?; | |
| 189 | 177 | assert_eq!( |
| 190 | 178 | commit_id, |
| 191 | − hex_to_id("302ea5640358f98ba23cda66c1e664a6f274643f"), | |
| 179 | + hex_to_id("3a774843723a713a8d361b4d4d98ad4092ef05bd"), | |
| 192 | 180 | "the commit id is stable" |
| 193 | 181 | ); |
| 194 | 182 | |
| @@ -207,8 +195,10 @@ mod commit { · 12 unmodified lines | ||
| 207 | 195 | } |
| 208 | 196 | |
| 209 | 197 | #[test] |
| 198 | + #[serial_test::serial] | |
| 210 | 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 | 202 | let parent = repo.find_reference("HEAD")?.peel_to_id_in_place()?; |
| 213 | 203 | let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id().expect("tree to be set"); |
| 214 | 204 | assert_eq!( |
| @@ -221,18 +211,10 @@ mod commit { · 6 unmodified lines | ||
| 221 | 211 | empty_tree_id, |
| 222 | 212 | "try and non-try work the same" |
| 223 | 213 | ); |
| 224 | − let author = git::actor::Signature::empty(); | |
| 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 | − )?; | |
| 214 | + let first_commit_id = repo.commit("HEAD", "hello there \r\n\nthe body", empty_tree_id, Some(parent))?; | |
| 233 | 215 | assert_eq!( |
| 234 | 216 | first_commit_id, |
| 235 | − hex_to_id("1ff7decccf76bfa15bfdb0b66bac0c9144b4b083"), | |
| 217 | + hex_to_id("e7c7273539cfc1a52802fa9d61aa578f6ccebcb4"), | |
| 236 | 218 | "the commit id is stable" |
| 237 | 219 | ); |
| 238 | 220 | |
| @@ -254,8 +236,6 @@ mod commit { · 15 unmodified lines | ||
| 254 | 236 | |
| 255 | 237 | let second_commit_id = repo.commit( |
| 256 | 238 | "refs/heads/new-branch", |
| 257 | − author.to_ref(), | |
| 258 | − author.to_ref(), | |
| 259 | 239 | "committing into a new branch creates it", |
| 260 | 240 | empty_tree_id, |
| 261 | 241 | Some(first_commit_id), |
| @@ -263,7 +243,7 @@ mod commit { · 1 unmodified line | ||
| 263 | 243 | |
| 264 | 244 | assert_eq!( |
| 265 | 245 | second_commit_id, |
| 266 | − hex_to_id("b0d041ade77e51d31c79c7147fb769336ccc77b1"), | |
| 246 | + hex_to_id("e1412f169e0812eb260601bdab3854ca0f1a7b33"), | |
| 267 | 247 | "the second commit id is stable" |
| 268 | 248 | ); |
| 269 | 249 | |
| @@ -271,3 +271,22 @@ mod find_remote { · 270 unmodified lines | ||
| 271 | 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 | 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 | 15 | pub fn repo(name: &str) -> Result<ThreadSafeRepository> { |
| 6 | 16 | let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?; |
| 7 | 17 | Ok(ThreadSafeRepository::open_opts(repo_path, restricted())?) |
| @@ -16,15 +26,25 @@ pub fn restricted() -> open::Options { · 8 unmodified lines | ||
| 16 | 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 | 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 | 40 | let repo_path = git_testtools::scripted_fixture_repo_writable(name)?; |
| 21 | 41 | Ok(( |
| 22 | 42 | ThreadSafeRepository::discover_opts( |
| 23 | 43 | repo_path.path(), |
| 24 | 44 | Default::default(), |
| 25 | 45 | git_sec::trust::Mapping { |
| 26 | − full: restricted(), | |
| 27 | − reduced: restricted(), | |
| 46 | + full: opts.clone(), | |
| 47 | + reduced: opts, | |
| 28 | 48 | }, |
| 29 | 49 | )? |
| 30 | 50 | .to_thread_local(), |
| @@ -66,10 +66,10 @@ pub(crate) mod function { · 65 unmodified lines | ||
| 66 | 66 | #[cfg(not(feature = "http-client-curl"))] |
| 67 | 67 | git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), |
| 68 | 68 | #[cfg(feature = "http-client-curl")] |
| 69 | − git_url::Scheme::Https | git_url::Scheme::Http => Box::new( | |
| 70 | − crate::client::http::connect(&url.to_bstring().to_string(), desired_version) | |
| 71 | − .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?, | |
| 72 | − ), | |
| 69 | + git_url::Scheme::Https | git_url::Scheme::Http => Box::new(crate::client::http::connect( | |
| 70 | + &url.to_bstring().to_string(), | |
| 71 | + desired_version, | |
| 72 | + )), | |
| 73 | 73 | }) |
| 74 | 74 | } |
| 75 | 75 | } |
| @@ -1,3 +1,5 @@ | ||
| 1 | +use std::any::Any; | |
| 2 | +use std::error::Error; | |
| 1 | 3 | use std::process::{self, Command, Stdio}; |
| 2 | 4 | |
| 3 | 5 | use bstr::{BString, ByteSlice}; |
| @@ -101,6 +103,10 @@ impl client::TransportWithoutIO for SpawnProcessOnDemand { · 97 unmodified lines | ||
| 101 | 103 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 102 | 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 | 112 | impl client::Transport for SpawnProcessOnDemand { |
| @@ -1,3 +1,4 @@ | ||
| 1 | +use std::error::Error; | |
| 1 | 2 | use std::{ |
| 2 | 3 | sync::mpsc::{Receiver, SyncSender}, |
| 3 | 4 | thread, |
| @@ -100,4 +101,8 @@ impl crate::client::http::Http for Curl { · 96 unmodified lines | ||
| 100 | 101 | ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> { |
| 101 | 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 | 2 | use std::{ |
| 2 | 3 | borrow::Cow, |
| 3 | − convert::Infallible, | |
| 4 | 4 | io::{BufRead, Read}, |
| 5 | 5 | }; |
| 6 | 6 | |
| @@ -169,6 +169,10 @@ impl<H: Http> client::TransportWithoutIO for Transport<H> { · 162 unmodified lines | ||
| 169 | 169 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 170 | 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 | 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 | 302 | /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation. |
| 299 | 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> { | |
| 301 | − Ok(Transport::new_http(http, url, desired_version)) | |
| 304 | +pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Transport<H> { | |
| 305 | + Transport::new_http(http, url, desired_version) | |
| 302 | 306 | } |
| 303 | 307 | |
| 304 | 308 | /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol. |
| 305 | 309 | #[cfg(feature = "http-client-curl")] |
| 306 | −pub fn connect(url: &str, desired_version: Protocol) -> Result<Transport<Impl>, Infallible> { | |
| 307 | − Ok(Transport::new(url, desired_version)) | |
| 310 | +pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> { | |
| 311 | + Transport::new(url, desired_version) | |
| 308 | 312 | } |
| @@ -67,4 +67,12 @@ pub trait Http { · 66 unmodified lines | ||
| 67 | 67 | url: &str, |
| 68 | 68 | headers: impl IntoIterator<Item = impl AsRef<str>>, |
| 69 | 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 | 25 | /// A structure to represent multiple [capabilities][Capability] or features supported by the server. |
| 26 | −#[derive(Debug, Clone)] | |
| 26 | +#[derive(Debug, Clone, Default)] | |
| 27 | 27 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| 28 | 28 | pub struct Capabilities { |
| 29 | 29 | data: BString, |
| @@ -3,6 +3,7 @@ use bstr::BString; · 2 unmodified lines | ||
| 3 | 3 | use futures_io::{AsyncRead, AsyncWrite}; |
| 4 | 4 | use futures_lite::AsyncWriteExt; |
| 5 | 5 | use git_packetline::PacketLineRef; |
| 6 | +use std::error::Error; | |
| 6 | 7 | |
| 7 | 8 | use crate::{ |
| 8 | 9 | client::{self, capabilities, git, Capabilities, SetServiceResponse}, |
| @@ -50,6 +51,10 @@ where · 41 unmodified lines | ||
| 50 | 51 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 51 | 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 | 60 | #[async_trait(?Send)] |
| @@ -1,3 +1,5 @@ | ||
| 1 | +use std::any::Any; | |
| 2 | +use std::error::Error; | |
| 1 | 3 | use std::io::Write; |
| 2 | 4 | |
| 3 | 5 | use bstr::BString; |
| @@ -52,6 +54,10 @@ where · 48 unmodified lines | ||
| 52 | 54 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 53 | 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 | 63 | impl<R, W> client::Transport for git::Connection<R, W> |
| @@ -1,3 +1,4 @@ | ||
| 1 | +use std::any::Any; | |
| 1 | 2 | use std::ops::{Deref, DerefMut}; |
| 2 | 3 | |
| 3 | 4 | #[cfg(any(feature = "blocking-client", feature = "async-client"))] |
| @@ -46,6 +47,11 @@ pub trait TransportWithoutIO { · 42 unmodified lines | ||
| 46 | 47 | /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated |
| 47 | 48 | /// to the server for most graceful termination of the connection. |
| 48 | 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 | 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 | 76 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 71 | 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 | 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 | 103 | fn connection_persists_across_multiple_requests(&self) -> bool { |
| 94 | 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 | 104 | &server.addr.port(), |
| 105 | 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 | 108 | assert_eq!(url, client.to_url()); |
| 109 | 109 | Ok((server, client)) |
| 110 | 110 | } |
| @@ -85,7 +85,7 @@ pub fn from_pack( · 84 unmodified lines | ||
| 85 | 85 | let options = pack::bundle::write::Options { |
| 86 | 86 | thread_limit: ctx.thread_limit, |
| 87 | 87 | iteration_mode: ctx.iteration_mode.into(), |
| 88 | − index_kind: pack::index::Version::default(), | |
| 88 | + index_version: pack::index::Version::default(), | |
| 89 | 89 | object_hash: ctx.object_hash, |
| 90 | 90 | }; |
| 91 | 91 | let out = ctx.out; |
| @@ -246,7 +246,7 @@ pub use self::async_io::receive; · 245 unmodified lines | ||
| 246 | 246 | |
| 247 | 247 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| 248 | 248 | pub struct JsonBundleWriteOutcome { |
| 249 | − pub index_kind: pack::index::Version, | |
| 249 | + pub index_version: pack::index::Version, | |
| 250 | 250 | pub index_hash: String, |
| 251 | 251 | |
| 252 | 252 | pub data_hash: String, |
| @@ -256,7 +256,7 @@ pub struct JsonBundleWriteOutcome { · 3 unmodified lines | ||
| 256 | 256 | impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome { |
| 257 | 257 | fn from(v: pack::index::write::Outcome) -> Self { |
| 258 | 258 | JsonBundleWriteOutcome { |
| 259 | − index_kind: v.index_kind, | |
| 259 | + index_version: v.index_version, | |
| 260 | 260 | num_objects: v.num_objects, |
| 261 | 261 | data_hash: v.data_hash.to_string(), |
| 262 | 262 | index_hash: v.index_hash.to_string(), |
| @@ -340,7 +340,7 @@ fn receive_pack_blocking<W: io::Write>( · 77 unmodified lines | ||
| 340 | 340 | ) -> io::Result<()> { |
| 341 | 341 | let options = pack::bundle::write::Options { |
| 342 | 342 | thread_limit: ctx.thread_limit, |
| 343 | − index_kind: pack::index::Version::V2, | |
| 343 | + index_version: pack::index::Version::V2, | |
| 344 | 344 | iteration_mode: pack::data::input::Mode::Verify, |
| 345 | 345 | object_hash: ctx.object_hash, |
| 346 | 346 | }; |
| @@ -73,7 +73,10 @@ mod refs_impl { · 72 unmodified lines | ||
| 73 | 73 | let map = remote |
| 74 | 74 | .connect(git::remote::Direction::Fetch, progress) |
| 75 | 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 | 80 | .await?; |
| 78 | 81 | |
| 79 | 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 | 179 | Ok(()) |
| 168 | 180 | } |
| 169 | 181 | |
| @@ -14,8 +14,9 @@ use gitoxide_core as core; · 13 unmodified lines | ||
| 14 | 14 | use gitoxide_core::pack::verify; |
| 15 | 15 | |
| 16 | 16 | use crate::{ |
| 17 | − plumbing::options::{ | |
| 18 | − commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands, | |
| 17 | + plumbing::{ | |
| 18 | + options::{commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands}, | |
| 19 | + show_progress, | |
| 19 | 20 | }, |
| 20 | 21 | shared::pretty::prepare_and_run, |
| 21 | 22 | }; |
| @@ -111,6 +112,7 @@ pub fn main() -> Result<()> { · 89 unmodified lines | ||
| 111 | 112 | })?; |
| 112 | 113 | |
| 113 | 114 | match cmd { |
| 115 | + Subcommands::Progress => show_progress(), | |
| 114 | 116 | Subcommands::Credential(cmd) => core::repository::credential( |
| 115 | 117 | repository(Mode::StrictWithGitInstallConfig)?, |
| 116 | 118 | match cmd { |
| @@ -1,4 +1,8 @@ | ||
| 1 | 1 | mod main; |
| 2 | 2 | pub use main::main; |
| 3 | 3 | |
| 4 | +#[path = "progress.rs"] | |
| 5 | +mod progress_impl; | |
| 6 | +pub use progress_impl::show_progress; | |
| 7 | + | |
| 4 | 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 | 2 | "index": { |
| 3 | − "index_kind": "V2", | |
| 3 | + "index_version": "V2", | |
| 4 | 4 | "index_hash": { |
| 5 | 5 | "Sha1": [ |
| 6 | 6 | 86, |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "index": { |
| 3 | − "index_kind": "V2", | |
| 3 | + "index_version": "V2", | |
| 4 | 4 | "index_hash": { |
| 5 | 5 | "Sha1": [ |
| 6 | 6 | 44, |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "index": { |
| 3 | − "index_kind": "V2", | |
| 3 | + "index_version": "V2", | |
| 4 | 4 | "index_hash": "c787de2aafb897417ca8167baeb146eabd18bc5f", |
| 5 | 5 | "data_hash": "346574b7331dc3a1724da218d622c6e1b6c66a57", |
| 6 | 6 | "num_objects": 9 |
| @@ -535,6 +535,14 @@ impl<'a> Env<'a> { · 534 unmodified lines | ||
| 535 | 535 | self.altered_vars.push((var, prev)); |
| 536 | 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 | 548 | impl<'a> Drop for Env<'a> { |