diff --git a/Cargo.lock b/Cargo.lock index a01d5d418a4..feac5975ea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,12 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + [[package]] name = "bytes" version = "1.2.1" @@ -954,6 +960,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -1835,7 +1847,9 @@ dependencies = [ "git-repository", "git-transport", "gitoxide-core", + "owo-colors", "prodash", + "tabled", ] [[package]] @@ -2380,6 +2394,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "papergrid" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453cf71f2a37af495a1a124bf30d4d7469cfbea58e9f2479be9d222396a518a2" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "parking" version = "2.0.0" @@ -2926,6 +2957,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tabled" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b2f8c37d26d87d2252187b0a45ea3cbf42baca10377c7e7eaaa2800fa9bf97" +dependencies = [ + "papergrid", + "unicode-width", +] + [[package]] name = "tar" version = "0.4.38" diff --git a/Cargo.toml b/Cargo.toml index 80013bf8cfa..e00a3cae59e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,10 @@ env_logger = { version = "0.9.0", default-features = false } crosstermion = { version = "0.10.1", optional = true, default-features = false } futures-lite = { version = "1.12.0", optional = true, default-features = false, features = ["std"] } +# for progress +owo-colors = "3.5.0" +tabled = { version = "0.8.0", default-features = false } + document-features = { version = "0.2.0", optional = true } [profile.dev.package] diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 66b00b0f37a..2a11a4e4f26 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -18,6 +18,21 @@ * We `thiserror` generally. * Adhere to the [stability guide](https://github.com/Byron/gitoxide/blob/main/STABILITY.md) +## Configuration and overrides + +As a general rule, respect and implement all applicable [git-config](https://git-scm.com/docs/git-config) by default, but allow the +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, +forcing a choice, or more typically, as a side-lane where overrides can be done on demand. + +Note that it should be possible to obtain the current configuration for modification by the user for selective overrides, either +by calling methods or by obtaining a data structure that can be set as a whole using a `get -> modify -> set` cycle. + +Note that without any of that, one should document that with `config_snapshot_mut()` any of the relevant configuration can be +changed in memory before invoking a method in order to affect it. + +Parameters which are not available in git or specific to `gitoxide` or the needs of the caller can be passed as parameters or via +`Options` or `Context` structures as needed. + ## General * **async** diff --git a/README.md b/README.md index da00f4e2143..6b6b6ac72d0 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ Please see _'Development Status'_ for a listing of all crates and their capabili * Based on the [git-hours] algorithm. * See the [discussion][git-hours-discussion] for some performance data. * **the `gix` program** _(plumbing)_ - lower level commands for use in automation + * **progress** - provide an overview of what works and what doesn't from the perspective of the git configuration. + This is likely to change a lot over time depending on actual needs, but maybe useful for you to see + if particular git-configuration is picked up and where it deviates. * **config** - list the complete git configuration in human-readable form and optionally filter sections by name. * **exclude** * [x] **query** - check if path specs are excluded via gits exclusion rules like `.gitignore`. diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh index b4b1e6572dc..f10113c73ee 100755 --- a/etc/check-package-size.sh +++ b/etc/check-package-size.sh @@ -57,6 +57,6 @@ echo "in root: gitoxide CLI" (enter git-odb && indent cargo diet -n --package-size-limit 120KB) (enter git-protocol && indent cargo diet -n --package-size-limit 50KB) (enter git-packetline && indent cargo diet -n --package-size-limit 35KB) -(enter git-repository && indent cargo diet -n --package-size-limit 175KB) +(enter git-repository && indent cargo diet -n --package-size-limit 185KB) (enter git-transport && indent cargo diet -n --package-size-limit 55KB) (enter gitoxide-core && indent cargo diet -n --package-size-limit 90KB) diff --git a/git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs b/git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs index f9255841587..38a972ccb7e 100644 --- a/git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs +++ b/git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs @@ -285,6 +285,7 @@ value = branch-override-by-include deref: false, }), git_repository::lock::acquire::Fail::Immediately, + git_repository::lock::acquire::Fail::Immediately, )? .commit(repo.committer_or_default())?; diff --git a/git-pack/src/bundle/write/error.rs b/git-pack/src/bundle/write/error.rs index 002fd0170a7..2a967b75b21 100644 --- a/git-pack/src/bundle/write/error.rs +++ b/git-pack/src/bundle/write/error.rs @@ -2,7 +2,9 @@ use std::io; use git_tempfile::handle::Writable; +/// The error returned by [`Bundle::write_to_directory()`][crate::Bundle::write_to_directory()] #[derive(thiserror::Error, Debug)] +#[allow(missing_docs)] pub enum Error { #[error("An IO error occurred when reading the pack or creating a temporary file")] Io(#[from] io::Error), diff --git a/git-pack/src/bundle/write/mod.rs b/git-pack/src/bundle/write/mod.rs index 1abbd43f7fd..ecc18c4e4d2 100644 --- a/git-pack/src/bundle/write/mod.rs +++ b/git-pack/src/bundle/write/mod.rs @@ -10,7 +10,7 @@ use git_tempfile::{handle::Writable, AutoRemove, ContainingDirectory}; use crate::data; mod error; -use error::Error; +pub use error::Error; mod types; use types::{LockWriter, PassThrough}; @@ -228,7 +228,7 @@ impl crate::Bundle { Options { thread_limit, iteration_mode: _, - index_kind, + index_version: index_kind, object_hash, }: Options, data_file: Arc>>, diff --git a/git-pack/src/bundle/write/types.rs b/git-pack/src/bundle/write/types.rs index ade253302b7..bf4a22161bd 100644 --- a/git-pack/src/bundle/write/types.rs +++ b/git-pack/src/bundle/write/types.rs @@ -11,7 +11,7 @@ pub struct Options { /// Determine how much processing to spend on protecting against corruption or recovering from errors. pub iteration_mode: crate::data::input::Mode, /// The version of pack index to write, should be [`crate::index::Version::default()`] - pub index_kind: crate::index::Version, + pub index_version: crate::index::Version, /// The kind of hash to use when writing the bundle. pub object_hash: git_hash::Kind, } @@ -22,7 +22,7 @@ impl Default for Options { Options { thread_limit: None, iteration_mode: crate::data::input::Mode::Verify, - index_kind: Default::default(), + index_version: Default::default(), object_hash: Default::default(), } } diff --git a/git-pack/src/index/write/error.rs b/git-pack/src/index/write/error.rs index cd022aefa01..7195b859c43 100644 --- a/git-pack/src/index/write/error.rs +++ b/git-pack/src/index/write/error.rs @@ -14,7 +14,7 @@ pub enum Error { IteratorInvariantNoRefDelta, #[error("The iterator failed to set a trailing hash over all prior pack entries in the last provided entry")] IteratorInvariantTrailer, - #[error("Did not encounter a single base")] + #[error("Did not encounter a single base - refusing to write empty pack.")] IteratorInvariantBasesPresent, #[error("Only u32::MAX objects can be stored in a pack, found {0}")] IteratorInvariantTooManyObjects(usize), diff --git a/git-pack/src/index/write/mod.rs b/git-pack/src/index/write/mod.rs index 9ea8cfda3b5..9aaeea3a715 100644 --- a/git-pack/src/index/write/mod.rs +++ b/git-pack/src/index/write/mod.rs @@ -18,7 +18,7 @@ pub(crate) struct TreeEntry { #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct Outcome { /// The version of the verified index - pub index_kind: crate::index::Version, + pub index_version: crate::index::Version, /// The verified checksum of the verified index pub index_hash: git_hash::ObjectId, @@ -171,7 +171,7 @@ impl crate::index::File { modify_base(data, entry, bytes, kind.hash()); Ok::<_, Error>(()) }, - crate::cache::delta::traverse::Options { + traverse::Options { object_progress: root_progress.add_child("Resolving"), size_progress: root_progress.add_child("Decoding"), thread_limit, @@ -207,7 +207,7 @@ impl crate::index::File { progress::MessageLevel::Success, ); Ok(Outcome { - index_kind: kind, + index_version: kind, index_hash, data_hash: pack_hash, num_objects, @@ -215,12 +215,7 @@ impl crate::index::File { } } -fn modify_base( - entry: &mut crate::index::write::TreeEntry, - pack_entry: &crate::data::Entry, - decompressed: &[u8], - hash: git_hash::Kind, -) { +fn modify_base(entry: &mut TreeEntry, pack_entry: &crate::data::Entry, decompressed: &[u8], hash: git_hash::Kind) { fn compute_hash(kind: git_object::Kind, bytes: &[u8], object_hash: git_hash::Kind) -> git_hash::ObjectId { let mut hasher = git_features::hash::hasher(object_hash); hasher.update(&git_object::encode::loose_header(kind, bytes.len())); diff --git a/git-pack/tests/pack/bundle.rs b/git-pack/tests/pack/bundle.rs index bf60aa844a9..dc6faf0489c 100644 --- a/git-pack/tests/pack/bundle.rs +++ b/git-pack/tests/pack/bundle.rs @@ -90,7 +90,7 @@ mod write_to_directory { fn expected_outcome() -> Result> { Ok(pack::bundle::write::Outcome { index: pack::index::write::Outcome { - index_kind: pack::index::Version::V2, + index_version: pack::index::Version::V2, index_hash: git_hash::ObjectId::from_hex(b"544a7204a55f6e9cacccf8f6e191ea8f83575de3")?, data_hash: git_hash::ObjectId::from_hex(b"0f3ea84cd1bba10c2a03d736a460635082833e59")?, num_objects: 42, @@ -156,7 +156,7 @@ mod write_to_directory { pack::bundle::write::Options { thread_limit: None, iteration_mode: pack::data::input::Mode::Verify, - index_kind: pack::index::Version::V2, + index_version: pack::index::Version::V2, object_hash: git_hash::Kind::Sha1, }, ) diff --git a/git-pack/tests/pack/data/output/count_and_entries.rs b/git-pack/tests/pack/data/output/count_and_entries.rs index a5fca708dc7..d08325c3474 100644 --- a/git-pack/tests/pack/data/output/count_and_entries.rs +++ b/git-pack/tests/pack/data/output/count_and_entries.rs @@ -321,6 +321,21 @@ fn traversals() -> crate::Result { Ok(()) } +#[test] +fn empty_pack_is_not_allowed() { + assert_eq!( + write_and_verify( + db(DbKind::DeterministicGeneratedContent).unwrap(), + vec![], + hex_to_id("029d08823bd8a8eab510ad6ac75c823cfd3ed31e"), + None, + ) + .unwrap_err() + .to_string(), + "Did not encounter a single base - refusing to write empty pack." + ); +} + fn write_and_verify( db: git_odb::HandleArc, entries: Vec, diff --git a/git-pack/tests/pack/index.rs b/git-pack/tests/pack/index.rs index 56af3c3378d..4e3930d3674 100644 --- a/git-pack/tests/pack/index.rs +++ b/git-pack/tests/pack/index.rs @@ -205,7 +205,7 @@ mod version { outcome.num_objects, num_objects, "it wrote the entire iterator worth of entries" ); - assert_eq!(outcome.index_kind, desired_kind); + assert_eq!(outcome.index_version, desired_kind); assert_eq!( outcome.index_hash, git_hash::ObjectId::from(&expected[end_of_pack_hash..end_of_index_hash]) diff --git a/git-protocol/src/fetch/arguments/blocking_io.rs b/git-protocol/src/fetch/arguments/blocking_io.rs index dac2163942d..98ac93f75b2 100644 --- a/git-protocol/src/fetch/arguments/blocking_io.rs +++ b/git-protocol/src/fetch/arguments/blocking_io.rs @@ -5,7 +5,8 @@ use git_transport::{client, client::TransportV2Ext}; use crate::fetch::{Arguments, Command}; impl Arguments { - pub(crate) fn send<'a, T: client::Transport + 'a>( + /// Send fetch arguments to the server, and indicate this is the end of negotiations only if `add_done_argument` is present. + pub fn send<'a, T: client::Transport + 'a>( &mut self, transport: &'a mut T, add_done_argument: bool, diff --git a/git-protocol/src/fetch/arguments/mod.rs b/git-protocol/src/fetch/arguments/mod.rs index 0539c7eca34..6a0ebda438e 100644 --- a/git-protocol/src/fetch/arguments/mod.rs +++ b/git-protocol/src/fetch/arguments/mod.rs @@ -3,6 +3,7 @@ use std::fmt; use bstr::{BStr, BString, ByteVec}; /// The arguments passed to a server command. +#[derive(Debug)] pub struct Arguments { /// The active features/capabilities of the fetch invocation #[cfg(any(feature = "async-client", feature = "blocking-client"))] @@ -24,6 +25,13 @@ pub struct Arguments { } impl Arguments { + /// Return true if there is no argument at all. + /// + /// 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 + /// new for them. + pub fn is_empty(&self) -> bool { + self.args.is_empty() + } /// Return true if ref filters is supported. pub fn can_use_filter(&self) -> bool { self.filter @@ -125,8 +133,10 @@ impl Arguments { fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) { self.args.push(format!("{}{}", prefix, value).into()); } + /// Create a new instance to help setting up arguments to send to the server as part of a `fetch` operation + /// for which `features` are the available and configured features to use. #[cfg(any(feature = "async-client", feature = "blocking-client"))] - pub(crate) fn new(version: git_transport::Protocol, features: Vec) -> Self { + pub fn new(version: git_transport::Protocol, features: Vec) -> Self { use crate::fetch::Command; let has = |name: &str| features.iter().any(|f| f.0 == name); let filter = has("filter"); diff --git a/git-protocol/src/fetch/command.rs b/git-protocol/src/fetch/command.rs index c86062dd4cc..3efa2fddc0c 100644 --- a/git-protocol/src/fetch/command.rs +++ b/git-protocol/src/fetch/command.rs @@ -50,9 +50,12 @@ mod with_io { "filter ", // filter-spec // ref-in-want feature "want-ref ", // ref path + // sideband-all feature "sideband-all", // packfile-uris feature "packfile-uris ", // protocols + // wait-for-done feature + "wait-for-done", ], } } @@ -79,9 +82,14 @@ mod with_io { "no-done", "filter", ], - git_transport::Protocol::V2 => { - &["shallow", "filter", "ref-in-want", "sideband-all", "packfile-uris"] - } + git_transport::Protocol::V2 => &[ + "shallow", + "filter", + "ref-in-want", + "sideband-all", + "packfile-uris", + "wait-for-done", + ], }, } } @@ -104,7 +112,9 @@ mod with_io { } } - pub(crate) fn default_features( + /// Turns on all modern features for V1 and all supported features for V2, returning them as a vector of features. + /// Note that this is the basis for any fetch operation as these features fulfil basic requirements and reasonably up-to-date servers. + pub fn default_features( &self, version: git_transport::Protocol, server_capabilities: &Capabilities, diff --git a/git-protocol/src/fetch/handshake.rs b/git-protocol/src/fetch/handshake.rs index 78d71095339..46e313cc6d8 100644 --- a/git-protocol/src/fetch/handshake.rs +++ b/git-protocol/src/fetch/handshake.rs @@ -3,7 +3,7 @@ use git_transport::client::Capabilities; use crate::fetch::Ref; /// The result of the [`handshake()`][super::handshake()] function. -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct Outcome { /// The protocol version the server responded with. It might have downgraded the desired version. diff --git a/git-protocol/src/fetch/refs/function.rs b/git-protocol/src/fetch/refs/function.rs index 39f2d039294..e4d2ad7df95 100644 --- a/git-protocol/src/fetch/refs/function.rs +++ b/git-protocol/src/fetch/refs/function.rs @@ -10,13 +10,13 @@ use super::Error; use crate::fetch::{indicate_end_of_interaction, refs::from_v2_refs, Command, LsRefsAction, Ref}; /// Invoke an ls-refs command on `transport` (assuming `protocol_version` 2 or panic), which requires a prior handshake that yielded -/// server `capabilities`. `prepare_ls_refs(arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback. +/// server `capabilities`. `prepare_ls_refs(capabilities, arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback. #[maybe_async] pub async fn refs( mut transport: impl Transport, protocol_version: Protocol, capabilities: &Capabilities, - mut prepare_ls_refs: impl FnMut( + prepare_ls_refs: impl FnOnce( &Capabilities, &mut Vec, &mut Vec<(&str, Option<&str>)>, diff --git a/git-protocol/src/fetch/refs/mod.rs b/git-protocol/src/fetch/refs/mod.rs index ba3fafd2471..122df7f644b 100644 --- a/git-protocol/src/fetch/refs/mod.rs +++ b/git-protocol/src/fetch/refs/mod.rs @@ -50,7 +50,7 @@ pub mod parse { pub enum Ref { /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit Peeled { - /// The name at which the ref is located, like `refs/heads/main`. + /// The name at which the ref is located, like `refs/tags/1.0`. full_ref_name: BString, /// The hash of the tag the ref points to. tag: git_hash::ObjectId, diff --git a/git-protocol/src/fetch/response/mod.rs b/git-protocol/src/fetch/response/mod.rs index 37e0943ecb8..c9afce53470 100644 --- a/git-protocol/src/fetch/response/mod.rs +++ b/git-protocol/src/fetch/response/mod.rs @@ -152,7 +152,8 @@ impl Response { self.has_pack } - /// Return an error if the given `features` don't contain the required ones for the given `version` of the protocol. + /// Return an error if the given `features` don't contain the required ones (the ones this implementation needs) + /// for the given `version` of the protocol. /// /// Even though technically any set of features supported by the server could work, we only implement the ones that /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable. diff --git a/git-protocol/src/fetch/tests/arguments.rs b/git-protocol/src/fetch/tests/arguments.rs index 76c8bd0acaf..bc81f0d6f4b 100644 --- a/git-protocol/src/fetch/tests/arguments.rs +++ b/git-protocol/src/fetch/tests/arguments.rs @@ -46,6 +46,13 @@ mod impls { fn connection_persists_across_multiple_requests(&self) -> bool { self.stateful } + + fn configure( + &mut self, + config: &dyn std::any::Any, + ) -> Result<(), Box> { + self.inner.configure(config) + } } impl client::Transport for Transport { @@ -89,6 +96,13 @@ mod impls { fn connection_persists_across_multiple_requests(&self) -> bool { self.stateful } + + fn configure( + &mut self, + config: &dyn std::any::Any, + ) -> Result<(), Box> { + self.inner.configure(config) + } } #[async_trait(?Send)] diff --git a/git-protocol/src/remote_progress.rs b/git-protocol/src/remote_progress.rs index 884057bfbd9..75700c24044 100644 --- a/git-protocol/src/remote_progress.rs +++ b/git-protocol/src/remote_progress.rs @@ -54,7 +54,7 @@ impl<'a> RemoteProgress<'a> { progress.fail(progress_name(None, text)); } } else { - match Self::from_bytes(text) { + match RemoteProgress::from_bytes(text) { Some(RemoteProgress { action, percent: _, diff --git a/git-ref/src/fullname.rs b/git-ref/src/fullname.rs index 053b67ee77a..e8ed206a16f 100644 --- a/git-ref/src/fullname.rs +++ b/git-ref/src/fullname.rs @@ -38,6 +38,15 @@ impl TryFrom for FullName { } } +impl TryFrom<&BString> for FullName { + type Error = git_validate::refname::Error; + + fn try_from(value: &BString) -> Result { + git_validate::refname(value.as_ref())?; + Ok(FullName(value.clone())) + } +} + impl From for BString { fn from(name: FullName) -> Self { name.0 diff --git a/git-ref/src/store/file/transaction/prepare.rs b/git-ref/src/store/file/transaction/prepare.rs index bb402f2a120..2f38dad6ea3 100644 --- a/git-ref/src/store/file/transaction/prepare.rs +++ b/git-ref/src/store/file/transaction/prepare.rs @@ -176,7 +176,8 @@ impl<'s> Transaction<'s> { pub fn prepare( mut self, edits: impl IntoIterator, - lock_fail_mode: git_lock::acquire::Fail, + ref_files_lock_fail_mode: git_lock::acquire::Fail, + packed_refs_lock_fail_mode: git_lock::acquire::Fail, ) -> Result { assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times"); let store = self.store; @@ -267,7 +268,7 @@ impl<'s> Transaction<'s> { let packed_transaction: Option<_> = if maybe_updates_for_packed_refs.unwrap_or(0) > 0 { // We have to create a packed-ref even if it doesn't exist self.store - .packed_transaction(lock_fail_mode) + .packed_transaction(packed_refs_lock_fail_mode) .map_err(|err| match err { file::packed::transaction::Error::BufferOpen(err) => Error::from(err), file::packed::transaction::Error::TransactionLock(err) => { @@ -280,7 +281,10 @@ impl<'s> Transaction<'s> { // no packed-ref file exists anyway self.store .assure_packed_refs_uptodate()? - .map(|p| buffer_into_transaction(p, lock_fail_mode).map_err(Error::PackedTransactionAcquire)) + .map(|p| { + buffer_into_transaction(p, packed_refs_lock_fail_mode) + .map_err(Error::PackedTransactionAcquire) + }) .transpose()? }; if let Some(transaction) = packed_transaction { @@ -302,7 +306,7 @@ impl<'s> Transaction<'s> { let change = &mut updates[cid]; if let Err(err) = Self::lock_ref_and_apply_change( self.store, - lock_fail_mode, + ref_files_lock_fail_mode, self.packed_transaction.as_ref().and_then(|t| t.buffer()), change, ) { diff --git a/git-ref/src/target.rs b/git-ref/src/target.rs index 8e38d75a1fd..a45c2ddd3b5 100644 --- a/git-ref/src/target.rs +++ b/git-ref/src/target.rs @@ -19,7 +19,7 @@ impl<'a> TargetRef<'a> { TargetRef::Peeled(oid) => Some(oid), } } - /// Interpret this target as object id or panic if it is symbolic. + /// Interpret this target as object id or **panic** if it is symbolic. pub fn id(&self) -> &oid { match self { TargetRef::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"), diff --git a/git-ref/src/transaction/mod.rs b/git-ref/src/transaction/mod.rs index e4fa518e756..5eeec36c867 100644 --- a/git-ref/src/transaction/mod.rs +++ b/git-ref/src/transaction/mod.rs @@ -83,7 +83,15 @@ pub enum Change { } impl Change { - /// Return references to values that are in common between all variants. + /// Return references to values that are the new value after the change is applied, if this is an update. + pub fn new_value(&self) -> Option> { + match self { + Change::Update { new, .. } => new.to_ref().into(), + Change::Delete { .. } => None, + } + } + + /// Return references to values that are in common between all variants and denote the previous observed value. pub fn previous_value(&self) -> Option> { match self { // TODO: use or-patterns once MRV is larger than 1.52 (and this is supported) diff --git a/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs b/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs index 666f3517069..352cf53c4cf 100644 --- a/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs +++ b/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs @@ -44,6 +44,7 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref()); if *is_empty { @@ -83,6 +84,7 @@ fn reference_with_old_value_must_exist_when_creating_it() -> crate::Result { deref: false, }), Fail::Immediately, + Fail::Immediately, ); match res { @@ -114,6 +116,7 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu deref: false, }), Fail::Immediately, + Fail::Immediately, ); match res { 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 deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -180,6 +184,7 @@ fn the_existing_must_match_constraint_requires_existing_references_to_have_the_g deref: false, }), Fail::Immediately, + Fail::Immediately, ); match res { 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 deref: false, }), Fail::Immediately, + Fail::Immediately, ); match res { Err(transaction::prepare::Error::MustNotExist { full_name, actual, .. }) => { @@ -246,6 +252,7 @@ fn namespaced_updates_or_deletions_are_transparent_and_not_observable() -> crate }, ], Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -295,6 +302,7 @@ fn reference_with_must_exist_constraint_must_exist_already_with_any_value() -> c deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -340,6 +348,7 @@ fn reference_with_must_not_exist_constraint_may_exist_already_if_the_new_value_m deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -387,6 +396,7 @@ fn cancellation_after_preparation_leaves_no_change() -> crate::Result { deref: false, }), Fail::Immediately, + Fail::Immediately, )?; assert_eq!(std::fs::read_dir(dir.path())?.count(), 1, "the lock file was created"); @@ -424,6 +434,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; assert_eq!( @@ -475,6 +486,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { deref: true, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -568,6 +580,7 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -632,7 +645,8 @@ fn packed_refs_are_looked_up_when_checking_existing_values() -> crate::Result { name: "refs/heads/main".try_into()?, deref: false, }), - git_lock::acquire::Fail::Immediately, + Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -687,7 +701,8 @@ fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs( name: r.name, deref: false, }), - git_lock::acquire::Fail::Immediately, + Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -743,7 +758,7 @@ fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdates(Box::new(|_, _| { Ok(Some(git_object::Kind::Commit)) }))) - .prepare(edits, git_lock::acquire::Fail::Immediately)? + .prepare(edits, Fail::Immediately, Fail::Immediately)? .commit(committer().to_ref())?; assert_eq!( edits.len(), diff --git a/git-ref/tests/file/transaction/prepare_and_commit/delete.rs b/git-ref/tests/file/transaction/prepare_and_commit/delete.rs index 1eecd5fb7f8..026ee137677 100644 --- a/git-ref/tests/file/transaction/prepare_and_commit/delete.rs +++ b/git-ref/tests/file/transaction/prepare_and_commit/delete.rs @@ -28,6 +28,7 @@ fn delete_a_ref_which_is_gone_succeeds() -> crate::Result { deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; assert_eq!(edits.len(), 1); @@ -47,6 +48,7 @@ fn delete_a_ref_which_is_gone_but_must_exist_fails() -> crate::Result { deref: false, }), Fail::Immediately, + Fail::Immediately, ); match res { 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 { deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -117,6 +120,7 @@ fn delete_ref_with_incorrect_previous_value_fails() -> crate::Result { deref: true, }), Fail::Immediately, + Fail::Immediately, ); match res { @@ -151,6 +155,7 @@ fn delete_reflog_only_of_symbolic_no_deref() -> crate::Result { deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -185,6 +190,7 @@ fn delete_reflog_only_of_symbolic_with_deref() -> crate::Result { deref: true, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -218,6 +224,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Res deref: true, }), Fail::Immediately, + Fail::Immediately, ); match res { Err(err) => { @@ -248,6 +255,7 @@ fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> crate::R deref: true, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -284,6 +292,7 @@ fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> crate::Res deref: true, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -325,6 +334,7 @@ fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> crate::Res deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; assert_eq!(edits.len(), 1); @@ -360,6 +370,7 @@ fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_delete deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -393,6 +404,7 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ deref: false, }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; @@ -432,6 +444,7 @@ fn all_contained_references_deletes_the_packed_ref_file_too() -> crate::Result { } }), Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref())?; diff --git a/git-ref/tests/file/worktree.rs b/git-ref/tests/file/worktree.rs index 764fcfd687f..8b39578737a 100644 --- a/git-ref/tests/file/worktree.rs +++ b/git-ref/tests/file/worktree.rs @@ -191,6 +191,7 @@ mod read_only { } mod writable { + use git_lock::acquire::Fail; use std::convert::TryInto; use git_ref::{ @@ -258,7 +259,8 @@ mod writable { deref: false, }, ], - git_lock::acquire::Fail::Immediately, + Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref()) .expect("successful commit as even similar resolved names live in different base locations"); @@ -405,7 +407,8 @@ mod writable { deref: false, }, ], - git_lock::acquire::Fail::Immediately, + Fail::Immediately, + Fail::Immediately, ), Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. }) ), "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 { deref: false, }, ], - git_lock::acquire::Fail::Immediately, + Fail::Immediately, + Fail::Immediately, ), Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. }) )); @@ -467,7 +471,8 @@ mod writable { deref: false, }, ], - git_lock::acquire::Fail::Immediately, + Fail::Immediately, + Fail::Immediately, ), Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. }) ), "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 { deref: false, }, ], - git_lock::acquire::Fail::Immediately, + Fail::Immediately, + Fail::Immediately, )? .commit(committer().to_ref()) .expect("successful commit as even similar resolved names live in different base locations"); diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index a9654f02ad4..efd97abd3fe 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -1,4 +1,4 @@ -use bstr::BStr; +use bstr::{BStr, ByteSlice}; use crate::{ instruction::{Fetch, Push}, @@ -18,6 +18,11 @@ impl RefSpec { dst: self.dst.as_ref().map(|b| b.as_ref()), } } + + /// Return true if the spec stats with a `+` and thus forces setting the reference. + pub fn allow_non_fast_forward(&self) -> bool { + matches!(self.mode, Mode::Force) + } } mod impls { @@ -84,7 +89,7 @@ mod impls { } /// Access -impl RefSpecRef<'_> { +impl<'a> RefSpecRef<'a> { /// Return the left-hand side of the spec, typically the source. /// It takes many different forms so don't rely on this being a ref name. /// @@ -117,8 +122,19 @@ impl RefSpecRef<'_> { } } + /// Derive the prefix from the `source` side of this spec, if possible. + /// + /// This means it starts with `refs/`. Note that it won't contain more than two components, like `refs/heads/` + pub fn prefix(&self) -> Option<&BStr> { + let source = self.source()?; + let suffix = source.strip_prefix(b"refs/")?; + let slash_pos = suffix.find_byte(b'/')?; + let prefix = source[..="refs/".len() + slash_pos].as_bstr(); + (!prefix.contains(&b'*')).then(|| prefix) + } + /// Transform the state of the refspec into an instruction making clear what to do with it. - pub fn instruction(&self) -> Instruction<'_> { + pub fn instruction(&self) -> Instruction<'a> { match self.op { Operation::Fetch => match (self.mode, self.src, self.dst) { (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }), diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs index 5768dc5c1f9..db8e38b6006 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -6,4 +6,5 @@ mod impls; mod match_group; mod matching; mod parse; +mod spec; mod write; diff --git a/git-refspec/tests/spec/mod.rs b/git-refspec/tests/spec/mod.rs new file mode 100644 index 00000000000..ca8b9e26575 --- /dev/null +++ b/git-refspec/tests/spec/mod.rs @@ -0,0 +1,41 @@ +mod prefix { + use git_refspec::parse::Operation; + use git_refspec::RefSpec; + + #[test] + fn partial_refs_have_no_prefix() { + assert_eq!(parse("main").to_ref().prefix(), None); + } + + #[test] + fn short_absolute_refs_have_no_prefix() { + assert_eq!(parse("refs/short").to_ref().prefix(), None); + } + + #[test] + fn full_names_have_a_prefix() { + assert_eq!(parse("refs/heads/main").to_ref().prefix().unwrap(), "refs/heads/"); + assert_eq!(parse("refs/foo/bar").to_ref().prefix().unwrap(), "refs/foo/"); + assert_eq!( + parse("refs/heads/*:refs/remotes/origin/*").to_ref().prefix().unwrap(), + "refs/heads/" + ); + } + + #[test] + fn strange_glob_patterns_have_no_prefix() { + assert_eq!(parse("refs/*/main:refs/*/main").to_ref().prefix(), None); + } + + #[test] + fn object_names_have_no_prefix() { + assert_eq!( + parse("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").to_ref().prefix(), + None + ); + } + + fn parse(spec: &str) -> RefSpec { + git_refspec::parse(spec.into(), Operation::Fetch).unwrap().to_owned() + } +} diff --git a/git-repository/examples/init-repo-and-commit.rs b/git-repository/examples/init-repo-and-commit.rs index 796bd04b8ff..1b0817b57dc 100644 --- a/git-repository/examples/init-repo-and-commit.rs +++ b/git-repository/examples/init-repo-and-commit.rs @@ -13,49 +13,36 @@ fn main() -> anyhow::Result<()> { let git_dir = std::env::args_os() .nth(1) .context("First argument needs to be the directory to initialize the repository in")?; - let repo = git::init_bare(git_dir)?; + let mut repo = git::init_bare(git_dir)?; println!("Repo (bare): {:?}", repo.git_dir()); let mut tree = git::objs::Tree::empty(); - let empty_tree_id = repo.write_object(&tree)?; - - let author = git::actor::SignatureRef { - name: "Maria Sanchez".into(), - email: "maria@example.com".into(), - time: git_date::Time::now_local_or_utc(), - }; - let initial_commit_id = repo.commit( - "HEAD", - author, - author, - "initial commit", - empty_tree_id, - git::commit::NO_PARENT_IDS, - )?; - - println!("initial commit id with empty tree: {:?}", initial_commit_id); - - let blob_id = repo.write_blob("hello world")?.into(); - let entry = tree::Entry { - mode: tree::EntryMode::Blob, - oid: blob_id, - filename: "hello.txt".into(), - }; - - tree.entries.push(entry); - let hello_tree_id = repo.write_object(&tree)?; - - let blob_commit_id = repo.commit( - "HEAD", - author, - author, - "hello commit", - hello_tree_id, - [initial_commit_id], - )?; - - println!("commit id for 'hello world' blob: {:?}", blob_commit_id); + let empty_tree_id = repo.write_object(&tree)?.detach(); + + let mut config = repo.config_snapshot_mut(); + config.set_raw_value("author", None, "name", "Maria Sanchez")?; + config.set_raw_value("author", None, "email", "maria@example.com")?; + { + let repo = config.commit_auto_rollback()?; + let initial_commit_id = repo.commit("HEAD", "initial commit", empty_tree_id, git::commit::NO_PARENT_IDS)?; + + println!("initial commit id with empty tree: {:?}", initial_commit_id); + + let blob_id = repo.write_blob("hello world")?.into(); + let entry = tree::Entry { + mode: tree::EntryMode::Blob, + oid: blob_id, + filename: "hello.txt".into(), + }; + + tree.entries.push(entry); + let hello_tree_id = repo.write_object(&tree)?; + + let blob_commit_id = repo.commit("HEAD", "hello commit", hello_tree_id, [initial_commit_id])?; + + println!("commit id for 'hello world' blob: {:?}", blob_commit_id); + } Ok(()) } diff --git a/git-repository/src/config/cache/access.rs b/git-repository/src/config/cache/access.rs new file mode 100644 index 00000000000..2f9a630c07d --- /dev/null +++ b/git-repository/src/config/cache/access.rs @@ -0,0 +1,103 @@ +use crate::config::Cache; +use crate::{remote, repository::identity}; +use git_lock::acquire::Fail; +use std::convert::TryInto; +use std::path::PathBuf; +use std::time::Duration; + +/// Access +impl Cache { + pub(crate) fn personas(&self) -> &identity::Personas { + self.personas + .get_or_init(|| identity::Personas::from_config_and_env(&self.resolved, self.git_prefix)) + } + + pub(crate) fn url_rewrite(&self) -> &remote::url::Rewrite { + self.url_rewrite + .get_or_init(|| remote::url::Rewrite::from_config(&self.resolved, self.filter_config_section)) + } + + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] + pub(crate) fn url_scheme( + &self, + ) -> Result<&remote::url::SchemePermission, remote::url::scheme_permission::init::Error> { + self.url_scheme.get_or_try_init(|| { + remote::url::SchemePermission::from_config(&self.resolved, self.git_prefix, self.filter_config_section) + }) + } + + /// Returns (file-timeout, pack-refs timeout) + pub(crate) fn lock_timeout( + &self, + ) -> Result<(git_lock::acquire::Fail, git_lock::acquire::Fail), git_config::value::Error> { + enum Kind { + RefFiles, + RefPackFile, + } + let mut out: [git_lock::acquire::Fail; 2] = Default::default(); + + for (idx, kind) in [Kind::RefFiles, Kind::RefPackFile].iter().enumerate() { + let (key, default_ms) = match kind { + Kind::RefFiles => ("filesRefLockTimeout", 100), + Kind::RefPackFile => ("packedRefsTimeout", 1000), + }; + let mk_default = || Fail::AfterDurationWithBackoff(Duration::from_millis(default_ms)); + let mut fnp = self.filter_config_section; + + let lock_mode = match self.resolved.integer_filter("core", None, key, &mut fnp) { + Some(Ok(val)) if val < 0 => Fail::AfterDurationWithBackoff(Duration::from_secs(u64::MAX)), + Some(Ok(val)) if val == 0 => Fail::Immediately, + Some(Ok(val)) => Fail::AfterDurationWithBackoff(Duration::from_millis( + val.try_into().expect("i64 can be repsented by u64"), + )), + Some(Err(_)) if self.lenient_config => mk_default(), + Some(Err(err)) => return Err(err), + None => mk_default(), + }; + out[idx] = lock_mode; + } + Ok((out[0], out[1])) + } + + /// The path to the user-level excludes file to ignore certain files in the worktree. + pub(crate) fn excludes_file(&self) -> Result, git_config::path::interpolate::Error> { + let home = self.home_dir(); + let install_dir = crate::path::install_dir().ok(); + let ctx = crate::config::cache::interpolate_context(install_dir.as_deref(), home.as_deref()); + match self + .resolved + .path_filter("core", None, "excludesFile", &mut self.filter_config_section.clone()) + .map(|p| p.interpolate(ctx).map(|p| p.into_owned())) + .transpose() + { + Ok(f) => Ok(f), + Err(_err) if self.lenient_config => Ok(None), + Err(err) => Err(err), + } + } + + /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations. + pub fn xdg_config_path( + &self, + resource_file_name: &str, + ) -> Result, git_sec::permission::Error> { + std::env::var_os("XDG_CONFIG_HOME") + .map(|path| (path, &self.xdg_config_home_env)) + .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env))) + .and_then(|(base, permission)| { + let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name); + permission.check(resource).transpose() + }) + .transpose() + } + + /// Return the home directory if we are allowed to read it and if it is set in the environment. + /// + /// We never fail for here even if the permission is set to deny as we `git-config` will fail later + /// if it actually wants to use the home directory - we don't want to fail prematurely. + pub fn home_dir(&self) -> Option { + std::env::var_os("HOME") + .map(PathBuf::from) + .and_then(|path| self.home_env.check_opt(path)) + } +} diff --git a/git-repository/src/config/cache/init.rs b/git-repository/src/config/cache/init.rs index 4b5c87fd31a..01442d25d09 100644 --- a/git-repository/src/config/cache/init.rs +++ b/git-repository/src/config/cache/init.rs @@ -1,7 +1,5 @@ -use std::path::PathBuf; - use super::{interpolate_context, util, Error, StageOne}; -use crate::{config::Cache, repository, revision::spec::parse::ObjectKindHint}; +use crate::{config::Cache, repository}; /// Initialization impl Cache { @@ -17,7 +15,7 @@ impl Cache { }: StageOne, git_dir: &std::path::Path, branch_name: Option<&git_ref::FullNameRef>, - mut filter_config_section: fn(&git_config::file::Metadata) -> bool, + filter_config_section: fn(&git_config::file::Metadata) -> bool, git_install_dir: Option<&std::path::Path>, home: Option<&std::path::Path>, repository::permissions::Environment { @@ -113,36 +111,14 @@ impl Cache { globals }; - let excludes_file = match config - .path_filter("core", None, "excludesFile", &mut filter_config_section) - .map(|p| p.interpolate(options.includes.interpolate).map(|p| p.into_owned())) - .transpose() - { - Ok(f) => f, - Err(_err) if lenient_config => None, - Err(err) => return Err(err.into()), - }; - - let hex_len = match util::parse_core_abbrev(&config, object_hash) { - Ok(v) => v, - Err(_err) if lenient_config => None, - Err(err) => return Err(err), - }; + let hex_len = util::check_lenient(util::parse_core_abbrev(&config, object_hash), lenient_config)?; use util::config_bool; let reflog = util::query_refupdates(&config); let ignore_case = config_bool(&config, "core.ignoreCase", false, lenient_config)?; let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true, lenient_config)?; - let object_kind_hint = config.string("core", None, "disambiguate").and_then(|value| { - Some(match value.as_ref().as_ref() { - b"commit" => ObjectKindHint::Commit, - b"committish" => ObjectKindHint::Committish, - b"tree" => ObjectKindHint::Tree, - b"treeish" => ObjectKindHint::Treeish, - b"blob" => ObjectKindHint::Blob, - _ => return None, - }) - }); + let object_kind_hint = util::disambiguate_hint(&config); + // NOTE: When adding a new initial cache, consider adjusting `reread_values_and_clear_caches()` as well. Ok(Cache { resolved: config.into(), use_multi_pack_index, @@ -153,39 +129,41 @@ impl Cache { ignore_case, hex_len, filter_config_section, - excludes_file, xdg_config_home_env, home_env, + lenient_config, personas: Default::default(), url_rewrite: Default::default(), - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] url_scheme: Default::default(), git_prefix, }) } - /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations. - pub fn xdg_config_path( - &self, - resource_file_name: &str, - ) -> Result, git_sec::permission::Error> { - std::env::var_os("XDG_CONFIG_HOME") - .map(|path| (path, &self.xdg_config_home_env)) - .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env))) - .and_then(|(base, permission)| { - let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name); - permission.check(resource).transpose() - }) - .transpose() - } - - /// Return the home directory if we are allowed to read it and if it is set in the environment. + /// Call this with new `config` to update values and clear caches. Note that none of the values will be applied if a single + /// one is invalid. + /// However, those that are lazily read won't be re-evaluated right away and might thus pass now but fail later. /// - /// We never fail for here even if the permission is set to deny as we `git-config` will fail later - /// if it actually wants to use the home directory - we don't want to fail prematurely. - pub fn home_dir(&self) -> Option { - std::env::var_os("HOME") - .map(PathBuf::from) - .and_then(|path| self.home_env.check_opt(path)) + /// Note that we unconditionally re-read all values. + pub fn reread_values_and_clear_caches(&mut self, config: crate::Config) -> Result<(), Error> { + let hex_len = util::check_lenient(util::parse_core_abbrev(&config, self.object_hash), self.lenient_config)?; + + use util::config_bool; + let ignore_case = config_bool(&config, "core.ignoreCase", false, self.lenient_config)?; + let object_kind_hint = util::disambiguate_hint(&config); + + self.personas = Default::default(); + self.url_rewrite = Default::default(); + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] + { + self.url_scheme = Default::default(); + } + + self.resolved = config; + self.hex_len = hex_len; + self.ignore_case = ignore_case; + self.object_kind_hint = object_kind_hint; + + Ok(()) } } diff --git a/git-repository/src/config/cache/mod.rs b/git-repository/src/config/cache/mod.rs index eeae0c20426..ae42c218836 100644 --- a/git-repository/src/config/cache/mod.rs +++ b/git-repository/src/config/cache/mod.rs @@ -1,5 +1,4 @@ use super::{Cache, Error}; -use crate::{remote, repository::identity}; mod incubate; pub(crate) use incubate::StageOne; @@ -12,27 +11,7 @@ impl std::fmt::Debug for Cache { } } -/// Access -impl Cache { - pub(crate) fn personas(&self) -> &identity::Personas { - self.personas - .get_or_init(|| identity::Personas::from_config_and_env(&self.resolved, self.git_prefix)) - } - - pub(crate) fn url_rewrite(&self) -> &remote::url::Rewrite { - self.url_rewrite - .get_or_init(|| remote::url::Rewrite::from_config(&self.resolved, self.filter_config_section)) - } - - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] - pub(crate) fn url_scheme( - &self, - ) -> Result<&remote::url::SchemePermission, remote::url::scheme_permission::init::Error> { - self.url_scheme.get_or_try_init(|| { - remote::url::SchemePermission::from_config(&self.resolved, self.git_prefix, self.filter_config_section) - }) - } -} +mod access; mod util; pub(crate) use util::interpolate_context; diff --git a/git-repository/src/config/cache/util.rs b/git-repository/src/config/cache/util.rs index a440cf8d58e..1e0c96cbc86 100644 --- a/git-repository/src/config/cache/util.rs +++ b/git-repository/src/config/cache/util.rs @@ -2,6 +2,7 @@ use std::convert::TryFrom; use super::Error; use crate::bstr::ByteSlice; +use crate::revision::spec::parse::ObjectKindHint; pub(crate) fn interpolate_context<'a>( git_install_dir: Option<&'a std::path::Path>, @@ -54,6 +55,14 @@ pub(crate) fn query_refupdates(config: &git_config::File<'static>) -> Option(v: Result, E>, lenient: bool) -> Result, E> { + match v { + Ok(v) => Ok(v), + Err(_) if lenient => Ok(None), + Err(err) => Err(err), + } +} + pub(crate) fn parse_core_abbrev( config: &git_config::File<'static>, object_hash: git_hash::Kind, @@ -93,3 +102,16 @@ pub(crate) fn parse_core_abbrev( None => Ok(None), } } + +pub(crate) fn disambiguate_hint(config: &git_config::File<'static>) -> Option { + config.string("core", None, "disambiguate").and_then(|value| { + Some(match value.as_ref().as_ref() { + b"commit" => ObjectKindHint::Commit, + b"committish" => ObjectKindHint::Committish, + b"tree" => ObjectKindHint::Tree, + b"treeish" => ObjectKindHint::Treeish, + b"blob" => ObjectKindHint::Blob, + _ => return None, + }) + }) +} diff --git a/git-repository/src/config/mod.rs b/git-repository/src/config/mod.rs index ef3e30bc439..bdfe59a62b0 100644 --- a/git-repository/src/config/mod.rs +++ b/git-repository/src/config/mod.rs @@ -14,16 +14,26 @@ pub struct Snapshot<'repo> { pub(crate) repo: &'repo Repository, } -/// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped. +/// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped +/// as form of auto-commit. /// Note that the values will only affect this instance of the parent repository, and not other clones that may exist. /// /// Note that these values won't update even if the underlying file(s) change. -// TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk. +/// +/// Use [`forget()`][Self::forget()] to not apply any of the changes. +// TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk which should be the way +// to affect all instances of a repo, probably via `config_mut()` and `config_mut_at()`. pub struct SnapshotMut<'repo> { - pub(crate) repo: &'repo mut Repository, + pub(crate) repo: Option<&'repo mut Repository>, pub(crate) config: git_config::File<'static>, } +/// A utility structure created by [`SnapshotMut::commit_auto_rollback()`] that restores the previous configuration on drop. +pub struct CommitAutoRollback<'repo> { + pub(crate) repo: Option<&'repo mut Repository>, + pub(crate) prev_config: crate::Config, +} + pub(crate) mod section { pub fn is_trusted(meta: &git_config::file::Metadata) -> bool { meta.trust == git_sec::Trust::Full || meta.source.kind() != git_config::source::Kind::Repository @@ -54,7 +64,10 @@ pub enum Error { PathInterpolation(#[from] git_config::path::interpolate::Error), } -/// Utility type to keep pre-obtained configuration values. +/// Utility type to keep pre-obtained configuration values, only for those required during initial setup +/// and other basic operations that are common enough to warrant a permanent cache. +/// +/// All other values are obtained lazily using OnceCell. #[derive(Clone)] pub(crate) struct Cache { pub resolved: crate::Config, @@ -73,7 +86,7 @@ pub(crate) struct Cache { /// A lazily loaded rewrite list for remote urls pub url_rewrite: OnceCell, /// A lazily loaded mapping to know which url schemes to allow - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] pub url_scheme: OnceCell, /// The config section filter from the options used to initialize this instance. Keep these in sync! filter_config_section: fn(&git_config::file::Metadata) -> bool, @@ -81,8 +94,9 @@ pub(crate) struct Cache { pub object_kind_hint: Option, /// If true, we are on a case-insensitive file system. pub ignore_case: bool, - /// The path to the user-level excludes file to ignore certain files in the worktree. - pub excludes_file: Option, + /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient. + /// Also available in options! Keep in sync! + pub lenient_config: bool, /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable. xdg_config_home_env: git_sec::Permission, /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable. diff --git a/git-repository/src/config/snapshot/_impls.rs b/git-repository/src/config/snapshot/_impls.rs new file mode 100644 index 00000000000..91332ad1652 --- /dev/null +++ b/git-repository/src/config/snapshot/_impls.rs @@ -0,0 +1,62 @@ +use std::{ + fmt::{Debug, Formatter}, + ops::{Deref, DerefMut}, +}; + +use crate::config::{CommitAutoRollback, Snapshot, SnapshotMut}; + +impl Debug for Snapshot<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.repo.config.resolved.to_string()) + } +} + +impl Debug for CommitAutoRollback<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.repo.as_ref().expect("still present").config.resolved.to_string()) + } +} + +impl Debug for SnapshotMut<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.config.to_string()) + } +} + +impl Drop for SnapshotMut<'_> { + fn drop(&mut self) { + if let Some(repo) = self.repo.take() { + self.commit_inner(repo).ok(); + }; + } +} + +impl Drop for CommitAutoRollback<'_> { + fn drop(&mut self) { + if let Some(repo) = self.repo.take() { + self.rollback_inner(repo).ok(); + } + } +} + +impl Deref for SnapshotMut<'_> { + type Target = git_config::File<'static>; + + fn deref(&self) -> &Self::Target { + &self.config + } +} + +impl Deref for CommitAutoRollback<'_> { + type Target = crate::Repository; + + fn deref(&self) -> &Self::Target { + self.repo.as_ref().expect("always present") + } +} + +impl DerefMut for SnapshotMut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.config + } +} diff --git a/git-repository/src/config/snapshot/access.rs b/git-repository/src/config/snapshot/access.rs index c386436f29f..106f369ca9d 100644 --- a/git-repository/src/config/snapshot/access.rs +++ b/git-repository/src/config/snapshot/access.rs @@ -1,5 +1,7 @@ +use git_features::threading::OwnShared; use std::borrow::Cow; +use crate::config::{CommitAutoRollback, SnapshotMut}; use crate::{ bstr::BStr, config::{cache::interpolate_context, Snapshot}, @@ -90,3 +92,59 @@ impl<'repo> Snapshot<'repo> { &self.repo.config.resolved } } + +/// Utilities +impl<'repo> SnapshotMut<'repo> { + /// Apply all changes made to this instance. + /// + /// Note that this would also happen once this instance is dropped, but using this method may be more intuitive and won't squelch errors + /// in case the new configuration is partially invalid. + pub fn commit(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> { + let repo = self.repo.take().expect("always present here"); + self.commit_inner(repo) + } + + pub(crate) fn commit_inner( + &mut self, + repo: &'repo mut crate::Repository, + ) -> Result<&'repo mut crate::Repository, crate::config::Error> { + repo.config + .reread_values_and_clear_caches(std::mem::take(&mut self.config).into())?; + Ok(repo) + } + + /// Create a structure the temporarily commits the changes, but rolls them back when dropped. + pub fn commit_auto_rollback(mut self) -> Result, crate::config::Error> { + let repo = self.repo.take().expect("this only runs once on consumption"); + let prev_config = OwnShared::clone(&repo.config.resolved); + + Ok(CommitAutoRollback { + repo: self.commit_inner(repo)?.into(), + prev_config, + }) + } + + /// Don't apply any of the changes after consuming this instance, effectively forgetting them, returning the changed configuration. + pub fn forget(mut self) -> git_config::File<'static> { + self.repo.take(); + std::mem::take(&mut self.config) + } +} + +/// Utilities +impl<'repo> CommitAutoRollback<'repo> { + /// Rollback the changes previously applied and all values before the change. + pub fn rollback(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> { + let repo = self.repo.take().expect("still present, consumed only once"); + self.rollback_inner(repo) + } + + pub(crate) fn rollback_inner( + &mut self, + repo: &'repo mut crate::Repository, + ) -> Result<&'repo mut crate::Repository, crate::config::Error> { + repo.config + .reread_values_and_clear_caches(OwnShared::clone(&self.prev_config))?; + Ok(repo) + } +} diff --git a/git-repository/src/config/snapshot/apply_cli_overrides.rs b/git-repository/src/config/snapshot/apply_cli_overrides.rs index 7c775776e93..91aa1c54c8b 100644 --- a/git-repository/src/config/snapshot/apply_cli_overrides.rs +++ b/git-repository/src/config/snapshot/apply_cli_overrides.rs @@ -23,7 +23,10 @@ pub enum Error { impl SnapshotMut<'_> { /// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true` /// to the repository configuration, marked with [source CLI][git_config::Source::Cli]. - pub fn apply_cli_overrides(&mut self, values: impl IntoIterator>) -> Result<(), Error> { + pub fn apply_cli_overrides( + &mut self, + values: impl IntoIterator>, + ) -> Result<&mut Self, Error> { let mut file = git_config::File::new(git_config::file::Metadata::from(git_config::Source::Cli)); for key_value in values { let key_value = key_value.as_ref(); @@ -44,6 +47,6 @@ impl SnapshotMut<'_> { ); } self.config.append(file); - Ok(()) + Ok(self) } } diff --git a/git-repository/src/config/snapshot/mod.rs b/git-repository/src/config/snapshot/mod.rs index 6fc03e002e1..2f44ef851f9 100644 --- a/git-repository/src/config/snapshot/mod.rs +++ b/git-repository/src/config/snapshot/mod.rs @@ -1,3 +1,4 @@ +mod _impls; mod access; /// @@ -5,44 +6,3 @@ pub mod apply_cli_overrides; /// pub mod credential_helpers; - -mod _impls { - use std::{ - fmt::{Debug, Formatter}, - ops::{Deref, DerefMut}, - }; - - use crate::config::{Snapshot, SnapshotMut}; - - impl Debug for Snapshot<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.repo.config.resolved.to_string()) - } - } - - impl Debug for SnapshotMut<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.config.to_string()) - } - } - - impl Drop for SnapshotMut<'_> { - fn drop(&mut self) { - self.repo.config.resolved = std::mem::take(&mut self.config).into(); - } - } - - impl Deref for SnapshotMut<'_> { - type Target = git_config::File<'static>; - - fn deref(&self) -> &Self::Target { - &self.config - } - } - - impl DerefMut for SnapshotMut<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.config - } - } -} diff --git a/git-repository/src/head/mod.rs b/git-repository/src/head/mod.rs index 87ddd8ce310..349e8b93a6c 100644 --- a/git-repository/src/head/mod.rs +++ b/git-repository/src/head/mod.rs @@ -87,19 +87,22 @@ mod remote { /// Remote impl<'repo> Head<'repo> { /// Return the remote with which the currently checked our reference can be handled as configured by `branch..remote|pushRemote` - /// or fall back to the non-branch specific remote configuration. `None` is returned if the head is detached or unborn. + /// or fall back to the non-branch specific remote configuration. `None` is returned if the head is detached or unborn, so there is + /// no branch specific remote. /// /// This is equivalent to calling [`Reference::remote(…)`][crate::Reference::remote()] and /// [`Repository::remote_default_name()`][crate::Repository::remote_default_name()] in order. + /// + /// Combine it with [`find_default_remote()`][crate::Repository::find_default_remote()] as fallback to handle detached heads, + /// i.e. obtain a remote even in case of detached heads. pub fn into_remote( self, direction: remote::Direction, ) -> Option, remote::find::existing::Error>> { let repo = self.repo; - self.try_into_referent()?.remote(direction).or_else(|| { - repo.remote_default_name(direction) - .map(|name| repo.find_remote(name.as_ref())) - }) + self.try_into_referent()? + .remote(direction) + .or_else(|| repo.find_default_remote(direction)) } } } diff --git a/git-repository/src/open.rs b/git-repository/src/open.rs index 6ee8b0dd4ae..132ae8c8224 100644 --- a/git-repository/src/open.rs +++ b/git-repository/src/open.rs @@ -201,7 +201,7 @@ impl Options { /// If set, default is false, invalid configuration values will cause an error even if these can safely be defaulted. /// /// This is recommended for all applications that prefer correctness over usability. - /// `git` itself by defaults to strict configuration mode to let you know if configuration is incorrect. + /// `git` itself defaults to strict configuration mode, flagging incorrect configuration immediately. pub fn strict_config(mut self, toggle: bool) -> Self { self.lenient_config = !toggle; self diff --git a/git-repository/src/reference/edits.rs b/git-repository/src/reference/edits.rs index 3e39822af51..7f967b7f127 100644 --- a/git-repository/src/reference/edits.rs +++ b/git-repository/src/reference/edits.rs @@ -55,35 +55,20 @@ pub mod delete { use crate::Reference; - mod error { - /// The error returned by [`Reference::delete()`][super::Reference::delete()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - ReferenceEdit(#[from] crate::reference::edit::Error), - } - } - pub use error::Error; - use git_lock::acquire::Fail; - impl<'repo> Reference<'repo> { /// Delete this reference or fail if it was changed since last observed. /// Note that this instance remains available in memory but probably shouldn't be used anymore. - pub fn delete(&self) -> Result<(), Error> { - self.repo.edit_reference( - RefEdit { + pub fn delete(&self) -> Result<(), crate::reference::edit::Error> { + self.repo + .edit_reference(RefEdit { change: Change::Delete { expected: PreviousValue::MustExistAndMatch(self.inner.target.clone()), log: RefLog::AndReference, }, name: self.inner.name.clone(), deref: false, - }, - Fail::Immediately, - self.repo.committer_or_default(), - )?; - Ok(()) + }) + .map(|_| ()) } } } diff --git a/git-repository/src/reference/errors.rs b/git-repository/src/reference/errors.rs index 15358ff0010..9854f8851bf 100644 --- a/git-repository/src/reference/errors.rs +++ b/git-repository/src/reference/errors.rs @@ -11,6 +11,8 @@ pub mod edit { FileTransactionCommit(#[from] git_ref::file::transaction::commit::Error), #[error(transparent)] NameValidation(#[from] git_validate::reference::name::Error), + #[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")] + LockTimeoutConfiguration(#[from] git_config::value::Error), } } diff --git a/git-repository/src/reference/remote.rs b/git-repository/src/reference/remote.rs index c6a4f6193d5..79600bd5238 100644 --- a/git-repository/src/reference/remote.rs +++ b/git-repository/src/reference/remote.rs @@ -72,6 +72,8 @@ impl<'repo> Reference<'repo> { /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like /// /// - `branch..merge` to know which branch on the remote side corresponds to this one for merging when pulling. + /// + /// It also handles if the remote is a configured URL, which has no name. pub fn remote( &self, direction: remote::Direction, diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 38aa61cb0b7..55cc129c6dd 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -1,8 +1,6 @@ -use git_protocol::transport::client::Transport; - use crate::{remote::Connection, Progress, Remote}; +use git_protocol::transport::client::Transport; -#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] mod error { use crate::{bstr::BString, remote}; @@ -24,10 +22,9 @@ mod error { FileUrl(#[from] git_discover::is_git::Error), } } -#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] pub use error::Error; -/// Establishing connections to remote hosts +/// Establishing connections to remote hosts (without performing a git-handshake). impl<'repo> Remote<'repo> { /// Create a new connection using `transport` to communicate, with `progress` to indicate changes. /// @@ -40,7 +37,7 @@ impl<'repo> Remote<'repo> { { Connection { remote: self, - credentials: None, + authenticate: None, transport, progress, } @@ -50,6 +47,10 @@ impl<'repo> Remote<'repo> { /// /// Note that the `protocol.version` configuration key affects the transport protocol used to connect, /// with `2` being the default. + /// + /// The transport used for connection can be configured via `transport_mut().configure()` assuming the actually + /// used transport is well known. If that's not the case, the transport can be created by hand and passed to + /// [to_connection_with_transport()][Self::to_connection_with_transport()]. #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] #[git_protocol::maybe_async::maybe_async] pub async fn connect

( @@ -60,7 +61,18 @@ impl<'repo> Remote<'repo> { where P: Progress, { - use git_protocol::transport::Protocol; + let (url, version) = self.sanitized_url_and_version(direction)?; + let transport = git_protocol::transport::connect(url, version).await?; + Ok(self.to_connection_with_transport(transport, progress)) + } + + /// Produce the sanitized URL and protocol version to use as obtained by querying the repository configuration. + /// + /// This can be useful when using custom transports to allow additional configuration. + pub fn sanitized_url_and_version( + &self, + direction: crate::remote::Direction, + ) -> Result<(git_url::Url, git_protocol::transport::Protocol), Error> { fn sanitize(mut url: git_url::Url) -> Result { if url.scheme == git_url::Scheme::File { let mut dir = git_path::from_bstr(url.path.as_ref()); @@ -75,6 +87,7 @@ impl<'repo> Remote<'repo> { Ok(url) } + use git_protocol::transport::Protocol; let version = self .repo .config @@ -101,7 +114,6 @@ impl<'repo> Remote<'repo> { scheme: url.scheme, }); } - let transport = git_protocol::transport::connect(sanitize(url)?, version).await?; - Ok(self.to_connection_with_transport(transport, progress)) + Ok((sanitize(url)?, version)) } } diff --git a/git-repository/src/remote/connection/access.rs b/git-repository/src/remote/connection/access.rs new file mode 100644 index 00000000000..dbdbc9204ff --- /dev/null +++ b/git-repository/src/remote/connection/access.rs @@ -0,0 +1,48 @@ +use crate::{ + remote::{connection::AuthenticateFn, Connection}, + Remote, +}; + +/// Builder +impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { + /// Set a custom credentials callback to provide credentials if the remotes require authentication. + /// + /// Otherwise we will use the git configuration to perform the same task as the `git credential` helper program, + /// which is calling other helper programs in succession while resorting to a prompt to obtain credentials from the + /// user. + /// + /// A custom function may also be used to prevent accessing resources with authentication. + pub fn with_credentials( + mut self, + helper: impl FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a, + ) -> Self { + self.authenticate = Some(Box::new(helper)); + self + } +} + +/// Access +impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { + /// A utility to return a function that will use this repository's configuration to obtain credentials, similar to + /// what `git credential` is doing. + /// + /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the + /// default way of handling credentials, which they can call as fallback. + pub fn configured_credentials( + &self, + url: git_url::Url, + ) -> Result, crate::config::credential_helpers::Error> { + let (mut cascade, _action_with_normalized_url, prompt_opts) = + self.remote.repo.config_snapshot().credential_helpers(url)?; + Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as AuthenticateFn<'_>) + } + /// Return the underlying remote that instantiate this connection. + pub fn remote(&self) -> &Remote<'repo> { + self.remote + } + + /// Provide a mutable transport to allow configuring it with [`configure()`][git_protocol::transport::client::TransportWithoutIO::configure()] + pub fn transport_mut(&mut self) -> &mut T { + &mut self.transport + } +} diff --git a/git-repository/src/remote/connection/fetch/config.rs b/git-repository/src/remote/connection/fetch/config.rs new file mode 100644 index 00000000000..dadb5da2dd6 --- /dev/null +++ b/git-repository/src/remote/connection/fetch/config.rs @@ -0,0 +1,66 @@ +use super::Error; +use crate::Repository; +use std::convert::TryInto; + +pub fn index_threads(repo: &Repository) -> Result, Error> { + let lenient_config = repo.options.lenient_config; + let message = "The configured pack.threads is invalid. It must be 0 or greater, with 0 turning it off"; + Ok( + match repo + .config + .resolved + .integer_filter("pack", None, "threads", &mut repo.filter_config_section()) + .transpose() + { + Ok(Some(0)) => Some(1), + Ok(Some(n)) => match n.try_into() { + Ok(n) => Some(n), + Err(_) if lenient_config => None, + Err(_) => { + return Err(Error::Configuration { + message: "The value for pack.threads is out of range", + desired: n.into(), + source: None, + }) + } + }, + Ok(None) => None, + Err(_) if lenient_config => None, + Err(err) => { + return Err(Error::Configuration { + message, + desired: None, + source: err.into(), + }) + } + }, + ) +} + +pub fn pack_index_version(repo: &Repository) -> Result { + use git_pack::index::Version; + let lenient_config = repo.options.lenient_config; + let message = "The configured pack.indexVersion is invalid. It must be 1 or 2, with 2 being the default"; + Ok( + match repo.config.resolved.integer("pack", None, "indexVersion").transpose() { + Ok(Some(v)) if v == 1 => Version::V1, + Ok(Some(v)) if v == 2 => Version::V2, + Ok(None) => Version::V2, + Ok(Some(_)) | Err(_) if lenient_config => Version::V2, + Ok(Some(v)) => { + return Err(Error::Configuration { + message, + desired: v.into(), + source: None, + }) + } + Err(err) => { + return Err(Error::Configuration { + message, + desired: None, + source: err.into(), + }) + } + }, + ) +} diff --git a/git-repository/src/remote/connection/fetch/mod.rs b/git-repository/src/remote/connection/fetch/mod.rs new file mode 100644 index 00000000000..4cf38ba47ca --- /dev/null +++ b/git-repository/src/remote/connection/fetch/mod.rs @@ -0,0 +1,263 @@ +use crate::remote::fetch::{DryRun, RefMap}; +use crate::remote::{fetch, ref_map, Connection}; +use crate::Progress; +use git_odb::FindExt; +use git_protocol::transport::client::Transport; +use std::sync::atomic::AtomicBool; + +mod error { + /// The error returned by [`receive()`](super::Prepare::receive()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("{message}{}", desired.map(|n| format!(" (got {})", n)).unwrap_or_default())] + Configuration { + message: &'static str, + desired: Option, + source: Option, + }, + #[error(transparent)] + FetchResponse(#[from] git_protocol::fetch::response::Error), + #[error(transparent)] + Negotiate(#[from] super::negotiate::Error), + #[error(transparent)] + Client(#[from] git_protocol::transport::client::Error), + #[error(transparent)] + WritePack(#[from] git_pack::bundle::write::Error), + #[error(transparent)] + UpdateRefs(#[from] super::refs::update::Error), + } +} +pub use error::Error; + +/// The status of the repository after the fetch operation +#[derive(Debug, Clone)] +pub enum Status { + /// Nothing changed as the remote didn't have anything new. + NoChange, + /// There was at least one tip with a new object which we received. + Change { + /// Information collected while writing the pack and its index. + write_pack_bundle: git_pack::bundle::write::Outcome, + /// Information collected while updating references. + update_refs: refs::update::Outcome, + }, + /// A dry run was performed which leaves the local repository without any change + /// nor will a pack have been received. + DryRun { + /// Information about what updates to refs would have been done. + update_refs: refs::update::Outcome, + }, +} + +/// The outcome of receiving a pack via [`Prepare::receive()`]. +#[derive(Debug, Clone)] +pub struct Outcome<'spec> { + /// The result of the initial mapping of references, the prerequisite for any fetch. + pub ref_map: RefMap<'spec>, + /// The status of the operation to indicate what happened. + pub status: Status, +} + +/// +pub mod negotiate; + +impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P> +where + T: Transport, + P: Progress, +{ + /// Perform a handshake with the remote and obtain a ref-map with `options`, and from there one + /// Note that at this point, the `transport` should already be configured using the [`transport_mut()`][Self::transport_mut()] + /// method, as it will be consumed here. + /// + /// From there additional properties of the fetch can be adjusted to override the defaults that are configured via git-config. + /// + /// # Blocking Only + /// + /// Note that this implementation is currently limited to blocking mode as it relies on Drop semantics to close the connection + /// should the fetch not be performed. Furthermore, there the code doing the fetch is inherently blocking so there is no benefit. + /// It's best to unblock it by placing it into its own thread or offload it should usage in an async context be required. + pub fn prepare_fetch(mut self, options: ref_map::Options) -> Result, ref_map::Error> { + let ref_map = self.ref_map_inner(options)?; + Ok(Prepare { + con: Some(self), + ref_map, + dry_run: fetch::DryRun::No, + }) + } +} + +impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P> +where + T: Transport, + P: Progress, +{ + /// Receive the pack and perform the operation as configured by git via `git-config` or overridden by various builder methods. + /// 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))` + /// to inform about all the changes that were made. + /// + /// ### Negotiation + /// + /// "fetch.negotiationAlgorithm" describes algorithms `git` uses currently, with the default being `consecutive` and `skipping` being + /// experimented with. We currently implement something we could call 'naive' which works for now. + pub fn receive(mut self, should_interrupt: &AtomicBool) -> Result, Error> { + let mut con = self.con.take().expect("receive() can only be called once"); + + let handshake = &self.ref_map.handshake; + let protocol_version = handshake.server_protocol_version; + + let fetch = git_protocol::fetch::Command::Fetch; + let fetch_features = fetch.default_features(protocol_version, &handshake.capabilities); + + git_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?; + let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all"); + let mut arguments = git_protocol::fetch::Arguments::new(protocol_version, fetch_features); + let mut previous_response = None::; + let mut round = 1; + let progress = &mut con.progress; + let repo = con.remote.repo; + + let reader = 'negotiation: loop { + progress.step(); + progress.set_name(format!("negotiate (round {})", round)); + let is_done = match negotiate::one_round( + negotiate::Algorithm::Naive, + round, + repo, + &self.ref_map, + &mut arguments, + previous_response.as_ref(), + ) { + Ok(_) if arguments.is_empty() => { + git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok(); + return Ok(Outcome { + ref_map: std::mem::take(&mut self.ref_map), + status: Status::NoChange, + }); + } + Ok(is_done) => is_done, + Err(err) => { + git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok(); + return Err(err.into()); + } + }; + round += 1; + let mut reader = arguments.send(&mut con.transport, is_done)?; + if sideband_all { + setup_remote_progress(progress, &mut reader); + } + let response = git_protocol::fetch::Response::from_line_reader(protocol_version, &mut reader)?; + if response.has_pack() { + progress.step(); + progress.set_name("receiving pack"); + if !sideband_all { + setup_remote_progress(progress, &mut reader); + } + break 'negotiation reader; + } else { + previous_response = Some(response); + } + }; + + let options = git_pack::bundle::write::Options { + thread_limit: config::index_threads(repo)?, + index_version: config::pack_index_version(repo)?, + iteration_mode: git_pack::data::input::Mode::Verify, + object_hash: con.remote.repo.object_hash(), + }; + + let write_pack_bundle = if matches!(self.dry_run, fetch::DryRun::No) { + Some(git_pack::Bundle::write_to_directory( + reader, + Some(repo.objects.store_ref().path().join("pack")), + con.progress, + should_interrupt, + Some(Box::new({ + let repo = repo.clone(); + move |oid, buf| repo.objects.find(oid, buf).ok() + })), + options, + )?) + } else { + drop(reader); + None + }; + + if matches!(protocol_version, git_protocol::transport::Protocol::V2) { + git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok(); + } + + let update_refs = refs::update( + repo, + "fetch", + &self.ref_map.mappings, + con.remote.refspecs(crate::remote::Direction::Fetch), + self.dry_run, + )?; + + Ok(Outcome { + ref_map: std::mem::take(&mut self.ref_map), + status: match write_pack_bundle { + Some(write_pack_bundle) => Status::Change { + write_pack_bundle, + update_refs, + }, + None => Status::DryRun { update_refs }, + }, + }) + } +} + +fn setup_remote_progress( + progress: &mut impl Progress, + reader: &mut Box, +) { + use git_protocol::transport::client::ExtendedBufRead; + reader.set_progress_handler(Some(Box::new({ + let mut remote_progress = progress.add_child("remote"); + move |is_err: bool, data: &[u8]| { + git_protocol::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress) + } + }) as git_protocol::transport::client::HandleProgress)); +} + +mod config; +/// +#[path = "update_refs/mod.rs"] +pub mod refs; + +/// A structure to hold the result of the handshake with the remote and configure the upcoming fetch operation. +pub struct Prepare<'remote, 'repo, T, P> +where + T: Transport, +{ + con: Option>, + ref_map: RefMap<'remote>, + dry_run: fetch::DryRun, +} + +/// Builder +impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P> +where + T: Transport, +{ + /// If dry run is enabled, no change to the repository will be made. + /// + /// This works by not actually fetching the pack after negotiating it, nor will refs be updated. + pub fn with_dry_run(mut self, enabled: bool) -> Self { + self.dry_run = enabled.then(|| fetch::DryRun::Yes).unwrap_or(DryRun::No); + self + } +} + +impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P> +where + T: Transport, +{ + fn drop(&mut self) { + if let Some(mut con) = self.con.take() { + git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok(); + } + } +} diff --git a/git-repository/src/remote/connection/fetch/negotiate.rs b/git-repository/src/remote/connection/fetch/negotiate.rs new file mode 100644 index 00000000000..f020732b74d --- /dev/null +++ b/git-repository/src/remote/connection/fetch/negotiate.rs @@ -0,0 +1,60 @@ +/// The way the negotiation is performed +#[derive(Copy, Clone)] +pub(crate) enum Algorithm { + /// Our very own implementation that probably should be replaced by one of the known algorithms soon. + Naive, +} + +/// The error returned during negotiation. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("We were unable to figure out what objects the server should send after {rounds} round(s)")] + NegotiationFailed { rounds: usize }, +} + +/// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants. +/// If this is not the first round, the `previous_response` is set with the last recorded server response. +/// Returns `true` if the negotiation is done from our side so the server won't keep asking. +pub(crate) fn one_round( + algo: Algorithm, + round: usize, + repo: &crate::Repository, + ref_map: &crate::remote::fetch::RefMap<'_>, + arguments: &mut git_protocol::fetch::Arguments, + _previous_response: Option<&git_protocol::fetch::Response>, +) -> Result { + match algo { + Algorithm::Naive => { + assert_eq!(round, 1, "Naive always finishes after the first round, and claims."); + let mut has_missing_tracking_branch = false; + for mapping in &ref_map.mappings { + let have_id = mapping.local.as_ref().and_then(|name| { + repo.find_reference(name) + .ok() + .and_then(|r| r.target().try_id().map(ToOwned::to_owned)) + }); + match have_id { + Some(have_id) if mapping.remote.as_id() != have_id => { + arguments.want(mapping.remote.as_id()); + arguments.have(have_id); + } + Some(_) => {} + None => { + arguments.want(mapping.remote.as_id()); + has_missing_tracking_branch = true; + } + } + } + + if has_missing_tracking_branch { + if let Ok(Some(r)) = repo.head_ref() { + if let Some(id) = r.target().try_id() { + arguments.have(id); + } + } + } + Ok(true) + } + } +} diff --git a/git-repository/src/remote/connection/fetch/update_refs/mod.rs b/git-repository/src/remote/connection/fetch/update_refs/mod.rs new file mode 100644 index 00000000000..9cfbd71aaae --- /dev/null +++ b/git-repository/src/remote/connection/fetch/update_refs/mod.rs @@ -0,0 +1,149 @@ +use crate::remote::fetch; +use crate::remote::fetch::refs::update::Mode; +use crate::Repository; +use git_pack::Find; +use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}; +use git_ref::{Target, TargetRef}; +use std::collections::BTreeMap; +use std::convert::TryInto; +use std::path::PathBuf; + +/// +pub mod update; + +/// Information about the update of a single reference, corresponding the respective entry in [`RefMap::mappings`][crate::remote::fetch::RefMap::mappings]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Update { + /// The way the update was performed. + pub mode: update::Mode, + /// The index to the edit that was created from the corresponding mapping, or `None` if there was no local ref. + pub edit_index: Option, +} + +impl From for Update { + fn from(mode: Mode) -> Self { + Update { mode, edit_index: None } + } +} + +/// Update all refs as derived from `mappings` and produce an `Outcome` informing about all applied changes in detail, with each +/// [`update`][Update] corresponding to the [`fetch::Mapping`] of at the same index. +/// If `dry_run` is true, ref transactions won't actually be applied, but are assumed to work without error so the underlying +/// `repo` is not actually changed. Also it won't perform an 'object exists' check as these are likely not to exist as the pack +/// wasn't fetched either. +/// `action` is the prefix used for reflog entries, and is typically "fetch". +/// +/// It can be used to produce typical information that one is used to from `git fetch`. +pub(crate) fn update( + repo: &Repository, + action: &str, + mappings: &[fetch::Mapping], + refspecs: &[git_refspec::RefSpec], + dry_run: fetch::DryRun, +) -> Result { + let mut edits = Vec::new(); + let mut updates = Vec::new(); + + for fetch::Mapping { + remote, + local, + spec_index, + } in mappings + { + let remote_id = remote.as_id(); + if dry_run == fetch::DryRun::No && !repo.objects.contains(remote_id) { + updates.push(update::Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into()); + continue; + } + let checked_out_branches = worktree_branches(repo)?; + let (mode, edit_index) = match local { + Some(name) => { + let (mode, reflog_message, name) = match repo.try_find_reference(name)? { + Some(existing) => { + if let Some(wt_dir) = checked_out_branches.get(existing.name()) { + let mode = update::Mode::RejectedCurrentlyCheckedOut { + worktree_dir: wt_dir.to_owned(), + }; + updates.push(mode.into()); + continue; + } + match existing.target() { + TargetRef::Symbolic(_) => { + updates.push(update::Mode::RejectedSymbolic.into()); + continue; + } + TargetRef::Peeled(local_id) => { + let (mode, reflog_message) = if local_id == remote_id { + (update::Mode::NoChangeNeeded, "no update will be performed") + } else if refspecs[*spec_index].allow_non_fast_forward() { + let reflog_msg = match existing.name().category() { + Some(git_ref::Category::Tag) => "updating tag", + _ => "forced-update", + }; + (update::Mode::Forced, reflog_msg) + } else if let Some(git_ref::Category::Tag) = existing.name().category() { + updates.push(update::Mode::RejectedTagUpdate.into()); + continue; + } else { + todo!("check for fast-forward (is local an ancestor of remote?)") + }; + (mode, reflog_message, existing.name().to_owned()) + } + } + } + None => { + let name: git_ref::FullName = name.try_into()?; + let reflog_msg = match name.category() { + Some(git_ref::Category::Tag) => "storing tag", + Some(git_ref::Category::LocalBranch) => "storing head", + _ => "storing ref", + }; + (update::Mode::New, reflog_msg, name) + } + }; + let edit = RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: format!("{}: {}", action, reflog_message).into(), + }, + expected: PreviousValue::ExistingMustMatch(Target::Peeled(remote_id.into())), + new: Target::Peeled(remote_id.into()), + }, + name, + deref: false, + }; + let edit_index = edits.len(); + edits.push(edit); + (mode, Some(edit_index)) + } + None => (update::Mode::NoChangeNeeded, None), + }; + updates.push(Update { mode, edit_index }) + } + + let edits = match dry_run { + fetch::DryRun::No => repo.edit_references(edits)?, + fetch::DryRun::Yes => edits, + }; + + Ok(update::Outcome { edits, updates }) +} + +fn worktree_branches(repo: &Repository) -> Result, update::Error> { + let mut map = BTreeMap::new(); + if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) { + map.insert(head_ref.inner.name, wt_dir.to_owned()); + } + for proxy in repo.worktrees()? { + let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?; + if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) { + map.insert(head_ref.inner.name, wt_dir.to_owned()); + } + } + Ok(map) +} + +#[cfg(test)] +mod tests; diff --git a/git-repository/src/remote/connection/fetch/update_refs/tests.rs b/git-repository/src/remote/connection/fetch/update_refs/tests.rs new file mode 100644 index 00000000000..ddbba5f11f9 --- /dev/null +++ b/git-repository/src/remote/connection/fetch/update_refs/tests.rs @@ -0,0 +1,263 @@ +mod update { + use crate as git; + use git_testtools::{hex_to_id, Result}; + + fn base_repo_path() -> String { + git::path::realpath( + git_testtools::scripted_fixture_repo_read_only("make_remote_repos.sh") + .unwrap() + .join("base"), + ) + .unwrap() + .to_string_lossy() + .into_owned() + } + + fn repo(name: &str) -> git::Repository { + let dir = git_testtools::scripted_fixture_repo_read_only_with_args("make_fetch_repos.sh", [base_repo_path()]) + .unwrap(); + git::open_opts(dir.join(name), git::open::Options::isolated()).unwrap() + } + use crate::remote::fetch; + use git_ref::transaction::Change; + use git_ref::TargetRef; + + #[test] + fn various_valid_updates() { + let repo = repo("two-origins"); + // TODO: test reflog message (various cases if it's new) + for (spec, expected_mode, reflog_message, detail) in [ + ( + "refs/heads/main:refs/remotes/origin/main", + fetch::refs::update::Mode::NoChangeNeeded, + Some("no update will be performed"), + "these refs are en-par since the initial clone", + ), + ( + "refs/heads/main", + fetch::refs::update::Mode::NoChangeNeeded, + None, + "without local destination ref there is nothing to do for us, ever (except for FETCH_HEADs) later", + ), + ( + "refs/heads/main:refs/remotes/origin/new-main", + fetch::refs::update::Mode::New, + Some("storing ref"), + "the destination branch doesn't exist and needs to be created", + ), + ( + "refs/heads/main:refs/heads/feature", + fetch::refs::update::Mode::New, + Some("storing head"), + "reflog messages are specific to the type of branch stored, to some limited extend", + ), + ( + "refs/heads/main:refs/tags/new-tag", + fetch::refs::update::Mode::New, + Some("storing tag"), + "reflog messages are specific to the type of branch stored, to some limited extend", + ), + ( + "+refs/heads/main:refs/remotes/origin/new-main", + fetch::refs::update::Mode::New, + Some("storing ref"), + "just to validate that we really are in dry-run mode, or else this ref would be present now", + ), + ( + "+refs/heads/main:refs/remotes/origin/g", + fetch::refs::update::Mode::Forced, + Some("forced-update"), + "a forced non-fastforward (main goes backwards)", + ), + ( + "+refs/heads/main:refs/tags/b-tag", + fetch::refs::update::Mode::Forced, + Some("updating tag"), + "tags can only be forced", + ), + ( + "refs/heads/main:refs/tags/b-tag", + fetch::refs::update::Mode::RejectedTagUpdate, + None, + "otherwise a tag is always refusing itself to be overwritten (no-clobber)", + ), + ( + "+refs/remotes/origin/g:refs/heads/main", + fetch::refs::update::Mode::RejectedCurrentlyCheckedOut { + worktree_dir: repo.work_dir().expect("present").to_owned(), + }, + None, + "checked out branches cannot be written, as it requires a merge of sorts which isn't done here", + ), + ( + "ffffffffffffffffffffffffffffffffffffffff:refs/heads/invalid-source-object", + fetch::refs::update::Mode::RejectedSourceObjectNotFound { + id: hex_to_id("ffffffffffffffffffffffffffffffffffffffff"), + }, + None, + "checked out branches cannot be written, as it requires a merge of sorts which isn't done here", + ), + // ( // TODO: make fast-forwards work + // "refs/remotes/origin/g:refs/heads/not-currently-checked-out", + // fetch::refs::update::Mode::FastForward, + // true, + // "a fast-forward only fast-forward situation, all good", + // ), + ] { + let (mapping, specs) = mapping_from_spec(spec, &repo); + let out = fetch::refs::update( + &repo, + "action", + &mapping, + &specs, + reflog_message.map(|_| fetch::DryRun::Yes).unwrap_or(fetch::DryRun::No), + ) + .unwrap(); + + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: expected_mode.clone(), + edit_index: reflog_message.map(|_| 0), + }], + "{spec:?}: {detail}" + ); + assert_eq!(out.edits.len(), reflog_message.map(|_| 1).unwrap_or(0)); + if let Some(reflog_message) = reflog_message { + let edit = &out.edits[0]; + match &edit.change { + Change::Update { log, .. } => { + assert_eq!( + log.message, + format!("action: {}", reflog_message), + "{}: reflog messages are specific and we emulate git word for word", + spec + ); + } + _ => unreachable!("only updates"), + } + } + } + } + + #[test] + fn checked_out_branches_in_worktrees_are_rejected_with_additional_infromation() -> Result { + let root = git_path::realpath(git_testtools::scripted_fixture_repo_read_only_with_args( + "make_fetch_repos.sh", + [base_repo_path()], + )?)?; + let repo = root.join("worktree-root"); + let repo = git::open_opts(repo, git::open::Options::isolated())?; + for (branch, path_from_root) in [ + ("main", "worktree-root"), + ("wt-a-nested", "prev/wt-a-nested"), + ("wt-a", "wt-a"), + ("nested-wt-b", "wt-a/nested-wt-b"), + ("wt-c-locked", "wt-c-locked"), + ("wt-deleted", "wt-deleted"), + ] { + let spec = format!("refs/heads/main:refs/heads/{}", branch); + let (mappings, specs) = mapping_from_spec(&spec, &repo); + let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes)?; + + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut { + worktree_dir: root.join(path_from_root), + }, + edit_index: None, + }], + "{}: checked-out checks are done before checking if a change would actually be required (here it isn't)", spec + ); + assert_eq!(out.edits.len(), 0); + } + Ok(()) + } + + #[test] + fn symbolic_refs_are_never_written() { + let repo = repo("two-origins"); + let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/heads/symbolic", &repo); + let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap(); + + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::RejectedSymbolic, + edit_index: None, + }], + "this also protects from writing HEAD, which should in theory be impossible to get from a refspec as it normalizes partial ref names" + ); + assert_eq!(out.edits.len(), 0); + } + + #[test] + #[should_panic] + fn fast_forward_is_not_implemented_yet_but_should_be_denied() { + let repo = repo("two-origins"); + let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/g", &repo); + let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap(); + + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::RejectedNonFastForward, + edit_index: Some(0), + }] + ); + assert_eq!(out.edits.len(), 1); + } + + fn mapping_from_spec(spec: &str, repo: &git::Repository) -> (Vec, Vec) { + let spec = git_refspec::parse(spec.into(), git_refspec::parse::Operation::Fetch).unwrap(); + let group = git_refspec::MatchGroup::from_fetch_specs(Some(spec)); + let references = repo.references().unwrap(); + let references: Vec<_> = references.all().unwrap().map(|r| into_remote_ref(r.unwrap())).collect(); + let mappings = group + .match_remotes(references.iter().map(remote_ref_to_item)) + .mappings + .into_iter() + .map(|m| fetch::Mapping { + remote: m + .item_index + .map(|idx| fetch::Source::Ref(references[idx].clone())) + .unwrap_or_else(|| match m.lhs { + git_refspec::match_group::SourceRef::ObjectId(id) => fetch::Source::ObjectId(id), + _ => unreachable!("not a ref, must be id: {:?}", m), + }), + local: m.rhs.map(|r| r.into_owned()), + spec_index: m.spec_index, + }) + .collect(); + (mappings, vec![spec.to_owned()]) + } + + fn into_remote_ref(mut r: git::Reference<'_>) -> git_protocol::fetch::Ref { + let full_ref_name = r.name().as_bstr().into(); + match r.target() { + TargetRef::Peeled(id) => git_protocol::fetch::Ref::Direct { + full_ref_name, + object: id.into(), + }, + TargetRef::Symbolic(name) => { + let target = name.as_bstr().into(); + let id = r.peel_to_id_in_place().unwrap(); + git_protocol::fetch::Ref::Symbolic { + full_ref_name, + target, + object: id.detach(), + } + } + } + } + + fn remote_ref_to_item(r: &git_protocol::fetch::Ref) -> git_refspec::match_group::Item<'_> { + let (full_ref_name, target, object) = r.unpack(); + git_refspec::match_group::Item { + full_ref_name, + target, + object, + } + } +} diff --git a/git-repository/src/remote/connection/fetch/update_refs/update.rs b/git-repository/src/remote/connection/fetch/update_refs/update.rs new file mode 100644 index 00000000000..a0658f934be --- /dev/null +++ b/git-repository/src/remote/connection/fetch/update_refs/update.rs @@ -0,0 +1,92 @@ +use crate::remote::fetch; +use std::path::PathBuf; + +mod error { + /// The error returned when updating references. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + FindReference(#[from] crate::reference::find::Error), + #[error("A remote reference had a name that wasn't considered valid. Corrupt remote repo or insufficient checks on remote?")] + InvalidRefName(#[from] git_validate::refname::Error), + #[error("Failed to update references to their new position to match their remote locations")] + EditReferences(#[from] crate::reference::edit::Error), + #[error("Failed to read or iterate worktree dir")] + WorktreeListing(#[from] std::io::Error), + #[error("Could not open worktree repository")] + OpenWorktreeRepo(#[from] crate::open::Error), + } +} + +pub use error::Error; + +/// The outcome of the refs-update operation at the end of a fetch. +#[derive(Debug, Clone)] +pub struct Outcome { + /// All edits that were performed to update local refs. + pub edits: Vec, + /// Each update provides more information about what happened to the corresponding mapping. + /// Use [`iter_mapping_updates()`][Self::iter_mapping_updates()] to recombine the update information with ref-edits and their + /// mapping. + pub updates: Vec, +} + +/// Describe the way a ref was updated +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Mode { + /// No change was attempted as the remote ref didn't change compared to the current ref, or because no remote ref was specified + /// in the ref-spec. + NoChangeNeeded, + /// The old ref's commit was an ancestor of the new one, allowing for a fast-forward without a merge. + FastForward, + /// The ref was set to point to the new commit from the remote without taking into consideration its ancestry. + Forced, + /// A new ref has been created as there was none before. + New, + /// The object id to set the target reference to could not be found. + RejectedSourceObjectNotFound { + /// 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. + id: git_hash::ObjectId, + }, + /// Tags can never be overwritten (whether the new object would be a fast-forward or not, or unchanged), unless the refspec + /// specifies force. + RejectedTagUpdate, + /// The reference update would not have been a fast-forward, and force is not specified in the ref-spec. + RejectedNonFastForward, + /// The update of a local symbolic reference was rejected. + RejectedSymbolic, + /// The update was rejected because the branch is checked out in the given worktree_dir. + /// + /// Note that the check applies to any known worktree, whether it's present on disk or not. + RejectedCurrentlyCheckedOut { + /// The path to the worktree directory where the branch is checked out. + worktree_dir: PathBuf, + }, +} + +impl Outcome { + /// Produce an iterator over all information used to produce the this outcome, ref-update by ref-update, using the `mappings` + /// used when producing the ref update. + pub fn iter_mapping_updates<'a, 'b>( + &self, + mappings: &'a [fetch::Mapping], + refspecs: &'b [git_refspec::RefSpec], + ) -> impl Iterator< + Item = ( + &super::Update, + &'a fetch::Mapping, + &'b git_refspec::RefSpec, + Option<&git_ref::transaction::RefEdit>, + ), + > { + self.updates.iter().zip(mappings.iter()).map(move |(update, mapping)| { + ( + update, + mapping, + &refspecs[mapping.spec_index], + update.edit_index.and_then(|idx| self.edits.get(idx)), + ) + }) + } +} diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index 0ac97e17902..8c875f9259f 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -5,8 +5,8 @@ pub(crate) struct HandshakeWithRefs { refs: Vec, } -/// A function that performs a given credential action. -pub type CredentialsFn<'a> = Box git_credentials::protocol::Result + 'a>; +/// A function that performs a given credential action, trying to obtain credentials for an operation that needs it. +pub type AuthenticateFn<'a> = Box git_credentials::protocol::Result + 'a>; /// A type to represent an ongoing connection to a remote host, typically with the connection already established. /// @@ -14,56 +14,16 @@ pub type CredentialsFn<'a> = Box g /// much like a remote procedure call. pub struct Connection<'a, 'repo, T, P> { pub(crate) remote: &'a Remote<'repo>, - pub(crate) credentials: Option>, + pub(crate) authenticate: Option>, pub(crate) transport: T, pub(crate) progress: P, } -mod access { - use crate::{ - remote::{connection::CredentialsFn, Connection}, - Remote, - }; - - /// Builder - impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { - /// Set a custom credentials callback to provide credentials if the remotes require authentication. - /// - /// Otherwise we will use the git configuration to perform the same task as the `git credential` helper program, - /// which is calling other helper programs in succession while resorting to a prompt to obtain credentials from the - /// user. - /// - /// A custom function may also be used to prevent accessing resources with authentication. - pub fn with_credentials( - mut self, - helper: impl FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a, - ) -> Self { - self.credentials = Some(Box::new(helper)); - self - } - } - - /// Access - impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { - /// A utility to return a function that will use this repository's configuration to obtain credentials, similar to - /// what `git credential` is doing. - /// - /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the - /// default way of handling credentials, which they can call as fallback. - pub fn configured_credentials( - &self, - url: git_url::Url, - ) -> Result, crate::config::credential_helpers::Error> { - let (mut cascade, _action_with_normalized_url, prompt_opts) = - self.remote.repo.config_snapshot().credential_helpers(url)?; - Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as CredentialsFn<'_>) - } - /// Drop the transport and additional state to regain the original remote. - pub fn remote(&self) -> &Remote<'repo> { - self.remote - } - } -} +mod access; /// pub mod ref_map; + +/// +#[cfg(feature = "blocking-network-client")] +pub mod fetch; diff --git a/git-repository/src/remote/connection/ref_map.rs b/git-repository/src/remote/connection/ref_map.rs index 307976f4991..ed37f38a0fe 100644 --- a/git-repository/src/remote/connection/ref_map.rs +++ b/git-repository/src/remote/connection/ref_map.rs @@ -1,5 +1,7 @@ +use crate::bstr::{BString, ByteVec}; use git_features::progress::Progress; use git_protocol::transport::client::Transport; +use std::collections::HashSet; use crate::remote::{connection::HandshakeWithRefs, fetch, Connection, Direction}; @@ -19,6 +21,27 @@ pub enum Error { MappingValidation(#[from] git_refspec::match_group::validate::Error), } +/// For use in [`Connection::ref_map()`]. +#[derive(Debug, Clone)] +pub struct Options { + /// Use a two-component prefix derived from the ref-spec's source, like `refs/heads/` to let the server pre-filter refs + /// with great potential for savings in traffic and local CPU time. Defaults to `true`. + pub prefix_from_spec_as_filter_on_remote: bool, + /// Parameters in the form of `(name, optional value)` to add to the handshake. + /// + /// This is useful in case of custom servers. + pub handshake_parameters: Vec<(String, Option)>, +} + +impl Default for Options { + fn default() -> Self { + Options { + prefix_from_spec_as_filter_on_remote: true, + handshake_parameters: Default::default(), + } + } +} + impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P> where T: Transport, @@ -31,16 +54,31 @@ where /// with the local tracking branch of these tips (if available). /// /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository. + /// + /// # Consumption + /// + /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus it's consumed along with + /// the connection. #[git_protocol::maybe_async::maybe_async] - pub async fn ref_map(mut self) -> Result, Error> { - let res = self.ref_map_inner().await; - git_protocol::fetch::indicate_end_of_interaction(&mut self.transport).await?; + pub async fn ref_map(mut self, options: Options) -> Result, Error> { + let res = self.ref_map_inner(options).await; + git_protocol::fetch::indicate_end_of_interaction(&mut self.transport) + .await + .ok(); res } #[git_protocol::maybe_async::maybe_async] - async fn ref_map_inner(&mut self) -> Result, Error> { - let remote = self.fetch_refs().await?; + pub(crate) async fn ref_map_inner( + &mut self, + Options { + prefix_from_spec_as_filter_on_remote, + handshake_parameters, + }: Options, + ) -> Result, Error> { + let remote = self + .fetch_refs(prefix_from_spec_as_filter_on_remote, handshake_parameters) + .await?; let group = git_refspec::MatchGroup::from_fetch_specs(self.remote.fetch_specs.iter().map(|s| s.to_ref())); let (res, fixes) = group .match_remotes(remote.refs.iter().map(|r| { @@ -77,9 +115,13 @@ where }) } #[git_protocol::maybe_async::maybe_async] - async fn fetch_refs(&mut self) -> Result { + async fn fetch_refs( + &mut self, + filter_by_prefix: bool, + extra_parameters: Vec<(String, Option)>, + ) -> Result { let mut credentials_storage; - let authenticate = match self.credentials.as_mut() { + let authenticate = match self.authenticate.as_mut() { Some(f) => f, None => { let url = self @@ -95,15 +137,32 @@ where } }; let mut outcome = - git_protocol::fetch::handshake(&mut self.transport, authenticate, Vec::new(), &mut self.progress).await?; + git_protocol::fetch::handshake(&mut self.transport, authenticate, extra_parameters, &mut self.progress) + .await?; let refs = match outcome.refs.take() { Some(refs) => refs, None => { + let specs = &self.remote.fetch_specs; git_protocol::fetch::refs( &mut self.transport, outcome.server_protocol_version, &outcome.capabilities, - |_a, _b, _c| Ok(git_protocol::fetch::delegate::LsRefsAction::Continue), + |_capabilities, arguments, _features| { + if filter_by_prefix { + let mut seen = HashSet::new(); + for spec in specs { + let spec = spec.to_ref(); + if seen.insert(spec.instruction()) { + if let Some(prefix) = spec.prefix() { + let mut arg: BString = "ref-prefix ".into(); + arg.push_str(prefix); + arguments.push(arg) + } + } + } + } + Ok(git_protocol::fetch::delegate::LsRefsAction::Continue) + }, &mut self.progress, ) .await? diff --git a/git-repository/src/remote/fetch.rs b/git-repository/src/remote/fetch.rs new file mode 100644 index 00000000000..39410c6746a --- /dev/null +++ b/git-repository/src/remote/fetch.rs @@ -0,0 +1,59 @@ +use crate::bstr::BString; + +/// If `Yes`, don't really make changes but do as much as possible to get an idea of what would be done. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg(feature = "blocking-network-client")] +pub(crate) enum DryRun { + /// Enable dry-run mode and don't actually change the underlying repository in any way. + Yes, + /// Run the operation like normal, making changes to the underlying repository. + No, +} + +/// Information about the relationship between our refspecs, and remote references with their local counterparts. +#[derive(Default, Debug, Clone)] +pub struct RefMap<'spec> { + /// A mapping between a remote reference and a local tracking branch. + pub mappings: Vec, + /// Information about the fixes applied to the `mapping` due to validation and sanitization. + pub fixes: Vec>, + /// All refs advertised by the remote. + pub remote_refs: Vec, + /// Additional information provided by the server as part of the handshake. + /// + /// Note that the `refs` field is always `None` as the refs are placed in `remote_refs`. + pub handshake: git_protocol::fetch::handshake::Outcome, +} + +/// Either an object id that the remote has or the matched remote ref itself. +#[derive(Debug, Clone)] +pub enum Source { + /// An object id, as the matched ref-spec was an object id itself. + ObjectId(git_hash::ObjectId), + /// The remote reference that matched the ref-specs name. + Ref(git_protocol::fetch::Ref), +} + +impl Source { + /// Return either the direct object id we refer to or the direct target that a reference refers to. + pub fn as_id(&self) -> &git_hash::oid { + match self { + Source::ObjectId(id) => id, + Source::Ref(r) => r.unpack().1, + } + } +} + +/// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist. +#[derive(Debug, Clone)] +pub struct Mapping { + /// The reference on the remote side, along with information about the objects they point to as advertised by the server. + pub remote: Source, + /// The local tracking reference to update after fetching the object visible via `remote`. + pub local: Option, + /// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered. + pub spec_index: usize, +} + +#[cfg(feature = "blocking-network-client")] +pub use super::connection::fetch::{negotiate, refs, Error, Outcome, Prepare, Status}; diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index e1bed940331..661ed9e5910 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -27,44 +27,7 @@ pub mod init; /// #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] -pub mod fetch { - use crate::bstr::BString; - - /// Information about the relationship between our refspecs, and remote references with their local counterparts. - #[derive(Debug, Clone)] - pub struct RefMap<'spec> { - /// A mapping between a remote reference and a local tracking branch. - pub mappings: Vec, - /// Information about the fixes applied to the `mapping` due to validation and sanitization. - pub fixes: Vec>, - /// All refs advertised by the remote. - pub remote_refs: Vec, - /// Additional information provided by the server as part of the handshake. - /// - /// Note that the `refs` field is always `None` as the refs are placed in `remote_refs`. - pub handshake: git_protocol::fetch::handshake::Outcome, - } - - /// Either an object id that the remote has or the matched remote ref itself. - #[derive(Debug, Clone)] - pub enum Source { - /// An object id, as the matched ref-spec was an object id itself. - ObjectId(git_hash::ObjectId), - /// The remote reference that matched the ref-specs name. - Ref(git_protocol::fetch::Ref), - } - - /// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist. - #[derive(Debug, Clone)] - pub struct Mapping { - /// The reference on the remote side, along with information about the objects they point to as advertised by the server. - pub remote: Source, - /// The local tracking reference to update after fetching the object visible via `remote`. - pub local: Option, - /// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered. - pub spec_index: usize, - } -} +pub mod fetch; /// #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] diff --git a/git-repository/src/remote/url/mod.rs b/git-repository/src/remote/url/mod.rs index 66ec6c66094..7b881581298 100644 --- a/git-repository/src/remote/url/mod.rs +++ b/git-repository/src/remote/url/mod.rs @@ -1,7 +1,7 @@ mod rewrite; /// -#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] +#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] pub mod scheme_permission; pub(crate) use rewrite::Rewrite; -#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] +#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] pub(crate) use scheme_permission::SchemePermission; diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index 5f963c114e6..3a2de9298ea 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -9,10 +9,17 @@ impl crate::Repository { config::Snapshot { repo: self } } - /// Return a mutable snapshot of the configuration as seen upon opening the repository. + /// Return a mutable snapshot of the configuration as seen upon opening the repository, starting a transaction. + /// When the returned instance is dropped, it is applied in full, even if the reason for the drop is an error. + /// + /// Note that changes to the configuration are in-memory only and are observed only the this instance + /// of the [`Repository`][crate::Repository]. pub fn config_snapshot_mut(&mut self) -> config::SnapshotMut<'_> { let config = self.config.resolved.as_ref().clone(); - config::SnapshotMut { repo: self, config } + config::SnapshotMut { + repo: Some(self), + config, + } } /// The options used to open the repository. diff --git a/git-repository/src/repository/identity.rs b/git-repository/src/repository/identity.rs index 98759a13de2..18bba3061a9 100644 --- a/git-repository/src/repository/identity.rs +++ b/git-repository/src/repository/identity.rs @@ -4,7 +4,8 @@ use crate::bstr::BString; /// Identity handling. impl crate::Repository { - /// Return a crate-specific constant signature with [`Time`][git_actor::Time] set to now, + /// Return a crate-specific constant signature with [`Time`][git_actor::Time] set to now, or whatever + /// was overridden via `GIT_COMMITTER_TIME` or `GIT_AUTHOR_TIME` if these variables are allowed to be read, /// in a similar vein as the default that git chooses if there is nothing configured. /// /// This can be useful as fallback for an unset `committer` or `author`. @@ -16,7 +17,13 @@ impl crate::Repository { git_actor::SignatureRef { name: "gitoxide".into(), email: "gitoxide@localhost".into(), - time: git_date::Time::now_local_or_utc(), + time: { + let p = self.config.personas(); + p.committer + .time + .or(p.author.time) + .unwrap_or_else(git_date::Time::now_local_or_utc) + }, } } diff --git a/git-repository/src/repository/object.rs b/git-repository/src/repository/object.rs index bfef691fb16..26c1260e9be 100644 --- a/git-repository/src/repository/object.rs +++ b/git-repository/src/repository/object.rs @@ -125,8 +125,10 @@ impl crate::Repository { self.tag_reference(name, tag_id, constraint).map_err(Into::into) } - /// Create a new commit object with `author`, `committer` and `message` referring to `tree` with `parents`, and point `reference` + /// Create a new commit object with `message` referring to `tree` with `parents`, and point `reference` /// to it. The commit is written without message encoding field, which can be assumed to be UTF-8. + /// `author` and `committer` fields are pre-set from the configuration, which can be altered + /// [temporarily][crate::Repository::config_snapshot_mut()] before the call if required. /// /// `reference` will be created if it doesn't exist, and can be `"HEAD"` to automatically write-through to the symbolic reference /// 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 { pub fn commit( &self, reference: Name, - author: git_actor::SignatureRef<'_>, - committer: git_actor::SignatureRef<'_>, message: impl AsRef, tree: impl Into, parents: impl IntoIterator>, @@ -157,6 +157,8 @@ impl crate::Repository { // TODO: possibly use CommitRef to save a few allocations (but will have to allocate for object ids anyway. // This can be made vastly more efficient though if we wanted to, so we lie in the API let reference = reference.try_into()?; + let author = self.author_or_default(); + let committer = self.committer_or_default(); let commit = git_object::Commit { message: message.as_ref().into(), tree: tree.into(), @@ -168,36 +170,28 @@ impl crate::Repository { }; let commit_id = self.write_object(&commit)?; - self.edit_reference( - RefEdit { - change: Change::Update { - log: LogChange { - mode: RefLog::AndReference, - force_create_reflog: false, - message: crate::reference::log::message( - "commit", - commit.message.as_ref(), - commit.parents.len(), - ), - }, - expected: match commit.parents.first().map(|p| Target::Peeled(*p)) { - Some(previous) => { - if reference.as_bstr() == "HEAD" { - PreviousValue::MustExistAndMatch(previous) - } else { - PreviousValue::ExistingMustMatch(previous) - } + self.edit_reference(RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: crate::reference::log::message("commit", commit.message.as_ref(), commit.parents.len()), + }, + expected: match commit.parents.first().map(|p| Target::Peeled(*p)) { + Some(previous) => { + if reference.as_bstr() == "HEAD" { + PreviousValue::MustExistAndMatch(previous) + } else { + PreviousValue::ExistingMustMatch(previous) } - None => PreviousValue::MustNotExist, - }, - new: Target::Peeled(commit_id.inner), + } + None => PreviousValue::MustNotExist, }, - name: reference, - deref: true, + new: Target::Peeled(commit_id.inner), }, - git_lock::acquire::Fail::Immediately, - commit.committer.to_ref(), - )?; + name: reference, + deref: true, + })?; Ok(commit_id) } } diff --git a/git-repository/src/repository/reference.rs b/git-repository/src/repository/reference.rs index eab411da703..8b8cbbfc931 100644 --- a/git-repository/src/repository/reference.rs +++ b/git-repository/src/repository/reference.rs @@ -1,8 +1,6 @@ use std::convert::TryInto; -use git_actor as actor; use git_hash::ObjectId; -use git_lock as lock; use git_ref::{ transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, FullName, PartialNameRef, Target, @@ -10,8 +8,6 @@ use git_ref::{ use crate::{bstr::BString, ext::ReferenceExt, reference, Reference}; -const DEFAULT_LOCK_MODE: git_lock::acquire::Fail = git_lock::acquire::Fail::Immediately; - /// Obtain and alter references comfortably impl crate::Repository { /// 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 { constraint: PreviousValue, ) -> Result, reference::edit::Error> { let id = target.into(); - let mut edits = self.edit_reference( - RefEdit { - change: Change::Update { - log: Default::default(), - expected: constraint, - new: Target::Peeled(id), - }, - name: format!("refs/tags/{}", name.as_ref()).try_into()?, - deref: false, + let mut edits = self.edit_reference(RefEdit { + change: Change::Update { + log: Default::default(), + expected: constraint, + new: Target::Peeled(id), }, - DEFAULT_LOCK_MODE, - self.committer_or_default(), - )?; + name: format!("refs/tags/{}", name.as_ref()).try_into()?, + deref: false, + })?; assert_eq!(edits.len(), 1, "reference splits should ever happen"); let edit = edits.pop().expect("exactly one item"); Ok(Reference { @@ -95,23 +87,19 @@ impl crate::Repository { { let name = name.try_into().map_err(git_validate::reference::name::Error::from)?; let id = target.into(); - let mut edits = self.edit_reference( - RefEdit { - change: Change::Update { - log: LogChange { - mode: RefLog::AndReference, - force_create_reflog: false, - message: log_message.into(), - }, - expected: constraint, - new: Target::Peeled(id), + let mut edits = self.edit_reference(RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: log_message.into(), }, - name, - deref: false, + expected: constraint, + new: Target::Peeled(id), }, - DEFAULT_LOCK_MODE, - self.committer_or_default(), - )?; + name, + deref: false, + })?; assert_eq!( edits.len(), 1, @@ -126,34 +114,29 @@ impl crate::Repository { .attach(self)) } - /// Edit a single reference as described in `edit`, handle locks via `lock_mode` and write reference logs as `log_committer`. + /// Edit a single reference as described in `edit`, and write reference logs as `log_committer`. /// /// One or more `RefEdit`s are returned - symbolic reference splits can cause more edits to be performed. All edits have the previous /// reference values set to the ones encountered at rest after acquiring the respective reference's lock. - pub fn edit_reference( - &self, - edit: RefEdit, - lock_mode: lock::acquire::Fail, - log_committer: actor::SignatureRef<'_>, - ) -> Result, reference::edit::Error> { - self.edit_references(Some(edit), lock_mode, log_committer) + pub fn edit_reference(&self, edit: RefEdit) -> Result, reference::edit::Error> { + self.edit_references(Some(edit)) } - /// Edit one or more references as described by their `edits`, with `lock_mode` deciding on how to handle competing - /// transactions. `log_committer` is the name appearing in reference logs. + /// Edit one or more references as described by their `edits`. + /// Note that one can set the committer name for use in the ref-log by temporarily + /// [overriding the git-config][crate::Repository::config_snapshot_mut()]. /// /// Returns all reference edits, which might be more than where provided due the splitting of symbolic references, and /// whose previous (_old_) values are the ones seen on in storage after the reference was locked. pub fn edit_references( &self, edits: impl IntoIterator, - lock_mode: lock::acquire::Fail, - log_committer: actor::SignatureRef<'_>, ) -> Result, reference::edit::Error> { + let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout()?; self.refs .transaction() - .prepare(edits, lock_mode)? - .commit(log_committer) + .prepare(edits, file_lock_fail, packed_refs_lock_fail)? + .commit(self.committer_or_default()) .map_err(Into::into) } diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index e9d53e1ca45..c23fbb10b68 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -32,6 +32,17 @@ impl crate::Repository { .ok_or_else(|| find::existing::Error::NotFound { name: name.into() })??) } + /// Find the default remote as configured, or `None` if no such configuration could be found. + /// + /// See [remote_default_name()][Self::remote_default_name()] for more information on the `direction` parameter. + pub fn find_default_remote( + &self, + direction: remote::Direction, + ) -> Option, find::existing::Error>> { + self.remote_default_name(direction) + .map(|name| self.find_remote(name.as_ref())) + } + /// Find the remote with the given `name` or return `None` if it doesn't exist, for the purpose of fetching or pushing /// data to a remote. /// diff --git a/git-repository/src/repository/worktree.rs b/git-repository/src/repository/worktree.rs index dfe988b771f..afc572d2c34 100644 --- a/git-repository/src/repository/worktree.rs +++ b/git-repository/src/repository/worktree.rs @@ -25,21 +25,11 @@ impl crate::Repository { res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir)); Ok(res) } - - /// Iterate all _linked_ worktrees in sort order and resolve them, ignoring those without an accessible work tree, into repositories - /// whose [`worktree()`][crate::Repository::worktree()] is the worktree currently being iterated. - /// - /// Note that for convenience all io errors are squelched so if there is a chance for IO errors during - /// traversal of an owned directory, better use `list()` directly. The latter allows to resolve repositories - /// even if the worktree checkout isn't accessible. - pub fn worktree_repos(&self) -> ! { - todo!() - } } /// Interact with individual worktrees and their information. impl crate::Repository { - /// Return the repository owning the main worktree. + /// Return the repository owning the main worktree, typically from a linked worktree. /// /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree. /// Also note that the main repo might be bare. diff --git a/git-repository/src/worktree/mod.rs b/git-repository/src/worktree/mod.rs index 1213aa04cca..771036e1fb0 100644 --- a/git-repository/src/worktree/mod.rs +++ b/git-repository/src/worktree/mod.rs @@ -112,6 +112,8 @@ pub mod excludes { Io(#[from] std::io::Error), #[error(transparent)] EnvironmentPermission(#[from] git_sec::permission::Error), + #[error("The value for `core.excludesFile` could not be read from configuration")] + ExcludesFilePathInterpolation(#[from] git_config::path::interpolate::Error), } impl<'repo> crate::Worktree<'repo> { @@ -135,7 +137,7 @@ pub mod excludes { overrides.unwrap_or_default(), git_attributes::MatchGroup::::from_git_dir( repo.git_dir(), - match repo.config.excludes_file.as_ref() { + match repo.config.excludes_file()?.as_ref() { Some(user_path) => Some(user_path.to_owned()), None => repo.config.xdg_config_path("ignore")?, }, diff --git a/git-repository/tests/fixtures/generated-archives/.gitignore b/git-repository/tests/fixtures/generated-archives/.gitignore index 9250e9825f6..9f4f7db5774 100644 --- a/git-repository/tests/fixtures/generated-archives/.gitignore +++ b/git-repository/tests/fixtures/generated-archives/.gitignore @@ -1,3 +1,4 @@ /make_worktree_repo.tar.xz /make_remote_repos.tar.xz +/make_fetch_repos.tar.xz /make_core_worktree_repo.tar.xz diff --git a/git-repository/tests/fixtures/make_fetch_repos.sh b/git-repository/tests/fixtures/make_fetch_repos.sh new file mode 100644 index 00000000000..d26e3a46660 --- /dev/null +++ b/git-repository/tests/fixtures/make_fetch_repos.sh @@ -0,0 +1,30 @@ +set -eu -o pipefail + +git clone --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base + +git clone --shared base clone-as-base-with-changes +(cd clone-as-base-with-changes + touch new-file + git add new-file + git commit -m "add new-file" + git tag -m "new-file introduction" v1.0 +) + +git clone --shared base two-origins +(cd two-origins + git remote add changes-on-top-of-origin "$PWD/../clone-as-base-with-changes" + git branch "not-currently-checked-out" + git symbolic-ref refs/heads/symbolic refs/heads/main +) + +git clone --shared base worktree-root +( + cd worktree-root + + git worktree add ../wt-a + git worktree add ../prev/wt-a-nested + git worktree add ../wt-b + git worktree add ../wt-a/nested-wt-b + git worktree add --lock ../wt-c-locked + git worktree add ../wt-deleted && rm -Rf ../wt-deleted +) diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index 7331e48ae9f..2f05bac7822 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -274,3 +274,7 @@ git clone --shared base credential-helpers baseline "git://host.org" ) +git clone --shared base detached-head +(cd detached-head + git checkout @~1 +) diff --git a/git-repository/tests/head/mod.rs b/git-repository/tests/head/mod.rs index 5940a9c4871..c8e2fa6a940 100644 --- a/git-repository/tests/head/mod.rs +++ b/git-repository/tests/head/mod.rs @@ -1,4 +1,4 @@ -mod remote { +mod into_remote { use git_repository as git; use crate::remote; @@ -12,4 +12,14 @@ mod remote { ); Ok(()) } + + #[test] + fn detached_is_none() -> crate::Result { + let repo = remote::repo("detached-head"); + assert_eq!( + repo.head()?.into_remote(git::remote::Direction::Fetch).transpose()?, + None + ); + Ok(()) + } } diff --git a/git-repository/tests/remote/fetch.rs b/git-repository/tests/remote/fetch.rs new file mode 100644 index 00000000000..08f8248d45f --- /dev/null +++ b/git-repository/tests/remote/fetch.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "blocking-network-client")] +mod blocking_io { + use crate::remote; + use git_features::progress; + use git_repository as git; + use git_repository::remote::fetch; + use git_repository::remote::Direction::Fetch; + use git_testtools::hex_to_id; + use std::sync::atomic::AtomicBool; + + fn repo_rw(name: &str) -> (git::Repository, git_testtools::tempfile::TempDir) { + let dir = git_testtools::scripted_fixture_repo_writable_with_args( + "make_fetch_repos.sh", + [git::path::realpath(remote::repo_path("base")) + .unwrap() + .to_string_lossy()], + git_testtools::Creation::ExecuteScript, + ) + .unwrap(); + let repo = git::open_opts(dir.path().join(name), git::open::Options::isolated()).unwrap(); + (repo, dir) + } + + #[test] + fn fetch_pack() -> crate::Result { + for version in [ + None, + Some(git::protocol::transport::Protocol::V2), + Some(git::protocol::transport::Protocol::V1), + ] { + let (mut repo, _tmp) = repo_rw("two-origins"); + if let Some(version) = version { + repo.config_snapshot_mut().set_raw_value( + "protocol", + None, + "version", + (version as u8).to_string().as_str(), + )?; + } + + // No updates + { + let remote = repo.find_remote("origin")?; + { + remote + .connect(Fetch, progress::Discard)? + .prepare_fetch(Default::default())?; + // early drops are fine and won't block. + } + let outcome = remote + .connect(Fetch, progress::Discard)? + .prepare_fetch(Default::default())? + .receive(&AtomicBool::default())?; + assert!(matches!(outcome.status, git::remote::fetch::Status::NoChange)); + } + + // Some updates to be fetched + for dry_run in [true, false] { + let remote = repo.find_remote("changes-on-top-of-origin")?; + let outcome: git::remote::fetch::Outcome = remote + .connect(Fetch, progress::Discard)? + .prepare_fetch(Default::default())? + .with_dry_run(dry_run) + .receive(&AtomicBool::default())?; + let refs = match outcome.status { + fetch::Status::Change { + write_pack_bundle, + update_refs, + } => { + assert_eq!(write_pack_bundle.pack_kind, git::odb::pack::data::Version::V2); + assert_eq!(write_pack_bundle.object_hash, repo.object_hash()); + 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."); + assert_eq!( + write_pack_bundle.index.index_version, + git::odb::pack::index::Version::V2 + ); + assert_eq!( + write_pack_bundle.index.index_hash, + hex_to_id("c75114f60ab2c9389916f3de1082bbaa47491e3b") + ); + assert!(write_pack_bundle.data_path.map_or(false, |f| f.is_file())); + assert!(write_pack_bundle.index_path.map_or(false, |f| f.is_file())); + + update_refs + } + fetch::Status::DryRun { update_refs } => update_refs, + fetch::Status::NoChange => unreachable!("we firmly expect changes here"), + }; + + assert_eq!( + refs.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::New, + edit_index: Some(0), + }] + ); + for (_update, mapping, _spec, edit) in + refs.iter_mapping_updates(&outcome.ref_map.mappings, remote.refspecs(Fetch)) + { + let edit = edit.expect("refedit present even if it's a no-op"); + if dry_run { + assert_eq!( + edit.change.new_value().expect("no deletions").id(), + mapping.remote.as_id() + ); + assert!( + repo.try_find_reference(edit.name.as_ref())?.is_none(), + "no ref created in dry-run mode" + ); + } else { + let r = repo.find_reference(edit.name.as_ref()).unwrap(); + assert_eq!( + r.id(), + *mapping.remote.as_id(), + "local reference should point to remote id" + ); + } + } + } + } + Ok(()) + } +} diff --git a/git-repository/tests/remote/mod.rs b/git-repository/tests/remote/mod.rs index c418f7ed926..ea4b5c03e75 100644 --- a/git-repository/tests/remote/mod.rs +++ b/git-repository/tests/remote/mod.rs @@ -17,4 +17,5 @@ pub(crate) fn cow_str(s: &str) -> Cow { } mod connect; -mod list_refs; +mod fetch; +mod ref_map; diff --git a/git-repository/tests/remote/list_refs.rs b/git-repository/tests/remote/ref_map.rs similarity index 89% rename from git-repository/tests/remote/list_refs.rs rename to git-repository/tests/remote/ref_map.rs index 67f84673678..dc1194bec41 100644 --- a/git-repository/tests/remote/list_refs.rs +++ b/git-repository/tests/remote/ref_map.rs @@ -24,8 +24,7 @@ mod blocking_io { } let remote = repo.find_remote("origin")?; - let connection = remote.connect(Fetch, progress::Discard)?; - let map = connection.ref_map()?; + let map = remote.connect(Fetch, progress::Discard)?.ref_map(Default::default())?; assert_eq!( map.remote_refs.len(), 14, @@ -36,7 +35,7 @@ mod blocking_io { assert_eq!( map.mappings.len(), 11, - "mappings are only a sub-set of all remotes due to refspec matching" + "mappings are only a sub-set of all remotes due to refspec matching, tags are filtered out." ); } Ok(()) diff --git a/git-repository/tests/repository/config/config_snapshot/mod.rs b/git-repository/tests/repository/config/config_snapshot/mod.rs index 8eb326a843b..98e1053fe61 100644 --- a/git-repository/tests/repository/config/config_snapshot/mod.rs +++ b/git-repository/tests/repository/config/config_snapshot/mod.rs @@ -1,5 +1,49 @@ use crate::named_repo; +#[test] +fn commit_auto_rollback() -> crate::Result { + let mut repo: git_repository::Repository = named_repo("make_basic_repo.sh")?; + assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3"); + + { + let mut config = repo.config_snapshot_mut(); + config.set_raw_value("core", None, "abbrev", "4")?; + let repo = config.commit_auto_rollback()?; + assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189"); + } + + assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3"); + + let repo = { + let mut config = repo.config_snapshot_mut(); + config.set_raw_value("core", None, "abbrev", "4")?; + let repo = config.commit_auto_rollback()?; + assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189"); + repo.rollback()? + }; + assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3"); + + Ok(()) +} + +#[test] +fn snapshot_mut_commit_and_forget() -> crate::Result { + let mut repo: git_repository::Repository = named_repo("make_basic_repo.sh")?; + let repo = { + let mut repo = repo.config_snapshot_mut(); + repo.set_raw_value("core", None, "abbrev", "4")?; + repo.commit()? + }; + assert_eq!(repo.config_snapshot().integer("core.abbrev").expect("set"), 4); + { + let mut repo = repo.config_snapshot_mut(); + repo.set_raw_value("core", None, "abbrev", "8")?; + repo.forget(); + } + assert_eq!(repo.config_snapshot().integer("core.abbrev"), Some(4)); + Ok(()) +} + #[test] fn values_are_set_in_memory_only() { let mut repo = named_repo("make_config_repo.sh").unwrap(); diff --git a/git-repository/tests/repository/mod.rs b/git-repository/tests/repository/mod.rs index 06ae361398e..89c00f76a20 100644 --- a/git-repository/tests/repository/mod.rs +++ b/git-repository/tests/repository/mod.rs @@ -10,7 +10,7 @@ mod worktree; #[test] fn size_in_memory() { - let expected = [760, 800]; + let expected = [728, 744, 784]; let actual_size = std::mem::size_of::(); assert!( expected.contains(&actual_size), diff --git a/git-repository/tests/repository/object.rs b/git-repository/tests/repository/object.rs index 27d05821f1b..fffc17c867d 100644 --- a/git-repository/tests/repository/object.rs +++ b/git-repository/tests/repository/object.rs @@ -147,23 +147,17 @@ mod tag { } mod commit { + use crate::{freeze_time, restricted_and_git}; use git_repository as git; use git_testtools::hex_to_id; + #[test] fn parent_in_initial_commit_causes_failure() { let tmp = tempfile::tempdir().unwrap(); let repo = git::init(&tmp).unwrap(); let empty_tree_id = repo.write_object(&git::objs::Tree::empty()).unwrap().detach(); - let author = git::actor::Signature::empty(); let err = repo - .commit( - "HEAD", - author.to_ref(), - author.to_ref(), - "initial", - empty_tree_id, - [empty_tree_id], - ) + .commit("HEAD", "initial", empty_tree_id, [empty_tree_id]) .unwrap_err(); assert_eq!( err.to_string(), @@ -173,22 +167,16 @@ mod commit { } #[test] + #[serial_test::serial] fn single_line_initial_commit_empty_tree_ref_nonexisting() -> crate::Result { + let _env = freeze_time(); let tmp = tempfile::tempdir()?; - let repo = git::init(&tmp)?; + let repo = git::open_opts(git::init(&tmp)?.path(), restricted_and_git())?; let empty_tree_id = repo.write_object(&git::objs::Tree::empty())?; - let author = git::actor::Signature::empty(); - let commit_id = repo.commit( - "HEAD", - author.to_ref(), - author.to_ref(), - "initial", - empty_tree_id, - git::commit::NO_PARENT_IDS, - )?; + let commit_id = repo.commit("HEAD", "initial", empty_tree_id, git::commit::NO_PARENT_IDS)?; assert_eq!( commit_id, - hex_to_id("302ea5640358f98ba23cda66c1e664a6f274643f"), + hex_to_id("3a774843723a713a8d361b4d4d98ad4092ef05bd"), "the commit id is stable" ); @@ -207,8 +195,10 @@ mod commit { } #[test] + #[serial_test::serial] fn multi_line_commit_message_uses_first_line_in_ref_log_ref_nonexisting() -> crate::Result { - let (repo, _keep) = crate::basic_rw_repo()?; + let _env = freeze_time(); + let (repo, _keep) = crate::repo_rw_opts("make_basic_repo.sh", restricted_and_git())?; let parent = repo.find_reference("HEAD")?.peel_to_id_in_place()?; let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id().expect("tree to be set"); assert_eq!( @@ -221,18 +211,10 @@ mod commit { empty_tree_id, "try and non-try work the same" ); - let author = git::actor::Signature::empty(); - let first_commit_id = repo.commit( - "HEAD", - author.to_ref(), - author.to_ref(), - "hello there \r\n\nthe body", - empty_tree_id, - Some(parent), - )?; + let first_commit_id = repo.commit("HEAD", "hello there \r\n\nthe body", empty_tree_id, Some(parent))?; assert_eq!( first_commit_id, - hex_to_id("1ff7decccf76bfa15bfdb0b66bac0c9144b4b083"), + hex_to_id("e7c7273539cfc1a52802fa9d61aa578f6ccebcb4"), "the commit id is stable" ); @@ -254,8 +236,6 @@ mod commit { let second_commit_id = repo.commit( "refs/heads/new-branch", - author.to_ref(), - author.to_ref(), "committing into a new branch creates it", empty_tree_id, Some(first_commit_id), @@ -263,7 +243,7 @@ mod commit { assert_eq!( second_commit_id, - hex_to_id("b0d041ade77e51d31c79c7147fb769336ccc77b1"), + hex_to_id("e1412f169e0812eb260601bdab3854ca0f1a7b33"), "the second commit id is stable" ); diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index d144b6b5fd2..d029b7074ec 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -271,3 +271,22 @@ mod find_remote { .to_string() } } + +mod find_default_remote { + use crate::remote; + use git_repository as git; + + #[test] + fn works_on_detached_heads() -> crate::Result { + let repo = remote::repo("detached-head"); + assert_eq!( + repo.find_default_remote(git::remote::Direction::Fetch) + .transpose()? + .expect("present") + .name() + .expect("always named"), + "origin" + ); + Ok(()) + } +} diff --git a/git-repository/tests/util/mod.rs b/git-repository/tests/util/mod.rs index e2ed60a4f80..e51af863ddb 100644 --- a/git-repository/tests/util/mod.rs +++ b/git-repository/tests/util/mod.rs @@ -2,6 +2,16 @@ use git_repository::{open, Repository, ThreadSafeRepository}; pub type Result = std::result::Result>; +pub fn freeze_time() -> git_testtools::Env<'static> { + let frozen_time = "1979-02-26 18:30:00"; + git_testtools::Env::new() + .unset("GIT_AUTHOR_NAME") + .unset("GIT_AUTHOR_EMAIL") + .set("GIT_AUTHOR_DATE", frozen_time) + .unset("GIT_COMMITTER_NAME") + .unset("GIT_COMMITTER_EMAIL") + .set("GIT_COMMITTER_DATE", frozen_time) +} pub fn repo(name: &str) -> Result { let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?; Ok(ThreadSafeRepository::open_opts(repo_path, restricted())?) @@ -16,15 +26,25 @@ pub fn restricted() -> open::Options { open::Options::isolated() } +pub fn restricted_and_git() -> open::Options { + let mut opts = open::Options::isolated(); + opts.permissions.env.git_prefix = git_sec::Permission::Allow; + opts +} + pub fn repo_rw(name: &str) -> Result<(Repository, tempfile::TempDir)> { + repo_rw_opts(name, restricted()) +} + +pub fn repo_rw_opts(name: &str, opts: git_repository::open::Options) -> Result<(Repository, tempfile::TempDir)> { let repo_path = git_testtools::scripted_fixture_repo_writable(name)?; Ok(( ThreadSafeRepository::discover_opts( repo_path.path(), Default::default(), git_sec::trust::Mapping { - full: restricted(), - reduced: restricted(), + full: opts.clone(), + reduced: opts, }, )? .to_thread_local(), diff --git a/git-transport/src/client/blocking_io/connect.rs b/git-transport/src/client/blocking_io/connect.rs index dc4704da7a4..d42cdb67326 100644 --- a/git-transport/src/client/blocking_io/connect.rs +++ b/git-transport/src/client/blocking_io/connect.rs @@ -66,10 +66,10 @@ pub(crate) mod function { #[cfg(not(feature = "http-client-curl"))] git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), #[cfg(feature = "http-client-curl")] - git_url::Scheme::Https | git_url::Scheme::Http => Box::new( - crate::client::http::connect(&url.to_bstring().to_string(), desired_version) - .map_err(|e| Box::new(e) as Box)?, - ), + git_url::Scheme::Https | git_url::Scheme::Http => Box::new(crate::client::http::connect( + &url.to_bstring().to_string(), + desired_version, + )), }) } } diff --git a/git-transport/src/client/blocking_io/file.rs b/git-transport/src/client/blocking_io/file.rs index 0e23075acd2..2ea33b27bda 100644 --- a/git-transport/src/client/blocking_io/file.rs +++ b/git-transport/src/client/blocking_io/file.rs @@ -1,3 +1,5 @@ +use std::any::Any; +use std::error::Error; use std::process::{self, Command, Stdio}; use bstr::{BString, ByteSlice}; @@ -101,6 +103,10 @@ impl client::TransportWithoutIO for SpawnProcessOnDemand { fn connection_persists_across_multiple_requests(&self) -> bool { true } + + fn configure(&mut self, _config: &dyn Any) -> Result<(), Box> { + Ok(()) + } } impl client::Transport for SpawnProcessOnDemand { diff --git a/git-transport/src/client/blocking_io/http/curl/mod.rs b/git-transport/src/client/blocking_io/http/curl/mod.rs index 06085bf5d68..360cbb28ac9 100644 --- a/git-transport/src/client/blocking_io/http/curl/mod.rs +++ b/git-transport/src/client/blocking_io/http/curl/mod.rs @@ -1,3 +1,4 @@ +use std::error::Error; use std::{ sync::mpsc::{Receiver, SyncSender}, thread, @@ -100,4 +101,8 @@ impl crate::client::http::Http for Curl { ) -> Result, http::Error> { self.make_request(url, headers, true) } + + fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box> { + Ok(()) + } } diff --git a/git-transport/src/client/blocking_io/http/mod.rs b/git-transport/src/client/blocking_io/http/mod.rs index a1c55b72424..b821dec9a6e 100644 --- a/git-transport/src/client/blocking_io/http/mod.rs +++ b/git-transport/src/client/blocking_io/http/mod.rs @@ -1,6 +1,6 @@ +use std::any::Any; use std::{ borrow::Cow, - convert::Infallible, io::{BufRead, Read}, }; @@ -169,6 +169,10 @@ impl client::TransportWithoutIO for Transport { fn connection_persists_across_multiple_requests(&self) -> bool { false } + + fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { + self.http.configure(config) + } } impl client::Transport for Transport { @@ -297,12 +301,12 @@ impl ExtendedBufRead for HeadersThenBody(http: H, url: &str, desired_version: Protocol) -> Result, Infallible> { - Ok(Transport::new_http(http, url, desired_version)) +pub fn connect_http(http: H, url: &str, desired_version: Protocol) -> Transport { + Transport::new_http(http, url, desired_version) } /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol. #[cfg(feature = "http-client-curl")] -pub fn connect(url: &str, desired_version: Protocol) -> Result, Infallible> { - Ok(Transport::new(url, desired_version)) +pub fn connect(url: &str, desired_version: Protocol) -> Transport { + Transport::new(url, desired_version) } diff --git a/git-transport/src/client/blocking_io/http/traits.rs b/git-transport/src/client/blocking_io/http/traits.rs index be39502dab3..d9103658385 100644 --- a/git-transport/src/client/blocking_io/http/traits.rs +++ b/git-transport/src/client/blocking_io/http/traits.rs @@ -67,4 +67,12 @@ pub trait Http { url: &str, headers: impl IntoIterator>, ) -> Result, Error>; + + /// Pass `config` which can deserialize in the implementation's configuration, as documented separately. + /// + /// The caller must know how that `config` data looks like for the intended implementation. + fn configure( + &mut self, + config: &dyn std::any::Any, + ) -> Result<(), Box>; } diff --git a/git-transport/src/client/capabilities.rs b/git-transport/src/client/capabilities.rs index f6e1efb1513..241bf11b3f7 100644 --- a/git-transport/src/client/capabilities.rs +++ b/git-transport/src/client/capabilities.rs @@ -23,7 +23,7 @@ pub enum Error { } /// A structure to represent multiple [capabilities][Capability] or features supported by the server. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct Capabilities { data: BString, diff --git a/git-transport/src/client/git/async_io.rs b/git-transport/src/client/git/async_io.rs index 5df5a648f70..bd5b9a14d16 100644 --- a/git-transport/src/client/git/async_io.rs +++ b/git-transport/src/client/git/async_io.rs @@ -3,6 +3,7 @@ use bstr::BString; use futures_io::{AsyncRead, AsyncWrite}; use futures_lite::AsyncWriteExt; use git_packetline::PacketLineRef; +use std::error::Error; use crate::{ client::{self, capabilities, git, Capabilities, SetServiceResponse}, @@ -50,6 +51,10 @@ where fn connection_persists_across_multiple_requests(&self) -> bool { true } + + fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box> { + Ok(()) + } } #[async_trait(?Send)] diff --git a/git-transport/src/client/git/blocking_io.rs b/git-transport/src/client/git/blocking_io.rs index 240f00c9903..97b950dcacf 100644 --- a/git-transport/src/client/git/blocking_io.rs +++ b/git-transport/src/client/git/blocking_io.rs @@ -1,3 +1,5 @@ +use std::any::Any; +use std::error::Error; use std::io::Write; use bstr::BString; @@ -52,6 +54,10 @@ where fn connection_persists_across_multiple_requests(&self) -> bool { true } + + fn configure(&mut self, _config: &dyn Any) -> Result<(), Box> { + Ok(()) + } } impl client::Transport for git::Connection diff --git a/git-transport/src/client/traits.rs b/git-transport/src/client/traits.rs index f57b2fe2d09..2e34bf03ac7 100644 --- a/git-transport/src/client/traits.rs +++ b/git-transport/src/client/traits.rs @@ -1,3 +1,4 @@ +use std::any::Any; use std::ops::{Deref, DerefMut}; #[cfg(any(feature = "blocking-client", feature = "async-client"))] @@ -46,6 +47,11 @@ pub trait TransportWithoutIO { /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated /// to the server for most graceful termination of the connection. fn connection_persists_across_multiple_requests(&self) -> bool; + + /// Pass `config` can be cast and interpreted by the implementation, as documented separately. + /// + /// The caller must know how that `config` data looks like for the intended implementation. + fn configure(&mut self, config: &dyn Any) -> Result<(), Box>; } // Would be nice if the box implementation could auto-forward to all implemented traits. @@ -70,6 +76,10 @@ impl TransportWithoutIO for Box { fn connection_persists_across_multiple_requests(&self) -> bool { self.deref().connection_persists_across_multiple_requests() } + + fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { + self.deref_mut().configure(config) + } } impl TransportWithoutIO for &mut T { @@ -93,4 +103,8 @@ impl TransportWithoutIO for &mut T { fn connection_persists_across_multiple_requests(&self) -> bool { self.deref().connection_persists_across_multiple_requests() } + + fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { + self.deref_mut().configure(config) + } } diff --git a/git-transport/tests/client/blocking_io/http/mock.rs b/git-transport/tests/client/blocking_io/http/mock.rs index 49c74ee5e1a..f11f1884bb2 100644 --- a/git-transport/tests/client/blocking_io/http/mock.rs +++ b/git-transport/tests/client/blocking_io/http/mock.rs @@ -104,7 +104,7 @@ pub fn serve_and_connect( &server.addr.port(), path ); - let client = git_transport::client::http::connect(&url, version)?; + let client = git_transport::client::http::connect(&url, version); assert_eq!(url, client.to_url()); Ok((server, client)) } diff --git a/gitoxide-core/src/pack/index.rs b/gitoxide-core/src/pack/index.rs index a41e4e68746..b346039424e 100644 --- a/gitoxide-core/src/pack/index.rs +++ b/gitoxide-core/src/pack/index.rs @@ -85,7 +85,7 @@ pub fn from_pack( let options = pack::bundle::write::Options { thread_limit: ctx.thread_limit, iteration_mode: ctx.iteration_mode.into(), - index_kind: pack::index::Version::default(), + index_version: pack::index::Version::default(), object_hash: ctx.object_hash, }; let out = ctx.out; diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 673196d48ee..93217d2c923 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -246,7 +246,7 @@ pub use self::async_io::receive; #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct JsonBundleWriteOutcome { - pub index_kind: pack::index::Version, + pub index_version: pack::index::Version, pub index_hash: String, pub data_hash: String, @@ -256,7 +256,7 @@ pub struct JsonBundleWriteOutcome { impl From for JsonBundleWriteOutcome { fn from(v: pack::index::write::Outcome) -> Self { JsonBundleWriteOutcome { - index_kind: v.index_kind, + index_version: v.index_version, num_objects: v.num_objects, data_hash: v.data_hash.to_string(), index_hash: v.index_hash.to_string(), @@ -340,7 +340,7 @@ fn receive_pack_blocking( ) -> io::Result<()> { let options = pack::bundle::write::Options { thread_limit: ctx.thread_limit, - index_kind: pack::index::Version::V2, + index_version: pack::index::Version::V2, iteration_mode: pack::data::input::Mode::Verify, object_hash: ctx.object_hash, }; diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs index 7325656cc31..39ee2ee0abc 100644 --- a/gitoxide-core/src/repository/remote.rs +++ b/gitoxide-core/src/repository/remote.rs @@ -73,7 +73,10 @@ mod refs_impl { let map = remote .connect(git::remote::Direction::Fetch, progress) .await? - .ref_map() + .ref_map(git::remote::ref_map::Options { + prefix_from_spec_as_filter_on_remote: !matches!(kind, refs::Kind::Remote), + ..Default::default() + }) .await?; if handshake_info { @@ -164,6 +167,15 @@ mod refs_impl { } } } + if map.remote_refs.len() - map.mappings.len() != 0 { + writeln!( + err, + "server sent {} tips, {} were filtered due to {} refspec(s).", + map.remote_refs.len(), + map.remote_refs.len() - map.mappings.len(), + refspecs.len() + )?; + } Ok(()) } diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 5c587d488dd..cdebe387dab 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -14,8 +14,9 @@ use gitoxide_core as core; use gitoxide_core::pack::verify; use crate::{ - plumbing::options::{ - commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands, + plumbing::{ + options::{commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands}, + show_progress, }, shared::pretty::prepare_and_run, }; @@ -111,6 +112,7 @@ pub fn main() -> Result<()> { })?; match cmd { + Subcommands::Progress => show_progress(), Subcommands::Credential(cmd) => core::repository::credential( repository(Mode::StrictWithGitInstallConfig)?, match cmd { diff --git a/src/plumbing/mod.rs b/src/plumbing/mod.rs index 2e32346bdd9..d2fab42bb0e 100644 --- a/src/plumbing/mod.rs +++ b/src/plumbing/mod.rs @@ -1,4 +1,8 @@ mod main; pub use main::main; +#[path = "progress.rs"] +mod progress_impl; +pub use progress_impl::show_progress; + mod options; diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs deleted file mode 100644 index 663773cc745..00000000000 --- a/src/plumbing/options.rs +++ /dev/null @@ -1,677 +0,0 @@ -use std::path::PathBuf; - -use git_repository as git; -use git_repository::bstr::BString; -use gitoxide_core as core; - -#[derive(Debug, clap::Parser)] -#[clap(name = "gix-plumbing", about = "The git underworld", version = clap::crate_version!())] -#[clap(subcommand_required = true)] -#[clap(arg_required_else_help = true)] -pub struct Args { - /// The repository to access. - #[clap(short = 'r', long, default_value = ".")] - pub repository: PathBuf, - - /// Add these values to the configuration in the form of `key=value` or `key`. - /// - /// For example, if `key` is `core.abbrev`, set configuration like `[core] abbrev = key`, - /// or `remote.origin.url = foo` to set `[remote "origin"] url = foo`. - #[clap(long, short = 'c', parse(try_from_os_str = git::env::os_str_to_bstring))] - pub config: Vec, - - #[clap(long, short = 't')] - /// The amount of threads to use for some operations. - /// - /// If unset, or the value is 0, there is no limit and all logical cores can be used. - pub threads: Option, - - /// Display verbose messages and progress information - #[clap(long, short = 'v')] - pub verbose: bool, - - /// Bring up a terminal user interface displaying progress visually - #[cfg(feature = "prodash-render-tui")] - #[clap(long, conflicts_with("verbose"))] - pub progress: bool, - - /// The progress TUI will stay up even though the work is already completed. - /// - /// Use this to be able to read progress messages or additional information visible in the TUI log pane. - #[cfg(feature = "prodash-render-tui")] - #[clap(long, conflicts_with("verbose"), requires("progress"))] - pub progress_keep_open: bool, - - /// Determine the format to use when outputting statistics. - #[clap( - long, - short = 'f', - default_value = "human", - possible_values(core::OutputFormat::variants()) - )] - pub format: core::OutputFormat, - - /// The object format to assume when reading files that don't inherently know about it, or when writing files. - #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))] - pub object_hash: git_repository::hash::Kind, - - #[clap(subcommand)] - pub cmd: Subcommands, -} - -#[derive(Debug, clap::Subcommand)] -pub enum Subcommands { - /// Interact with the object database. - #[clap(subcommand)] - Odb(odb::Subcommands), - /// Interact with tree objects. - #[clap(subcommand)] - Tree(tree::Subcommands), - /// Interact with commit objects. - #[clap(subcommand)] - Commit(commit::Subcommands), - /// Verify the integrity of the entire repository - Verify { - #[clap(flatten)] - args: free::pack::VerifyOptions, - }, - /// Query and obtain information about revisions. - #[clap(subcommand)] - Revision(revision::Subcommands), - /// A program just like `git credential`. - #[clap(subcommand)] - Credential(credential::Subcommands), - /// Interact with the mailmap. - #[clap(subcommand)] - Mailmap(mailmap::Subcommands), - /// Interact with the remote hosts. - Remote(remote::Platform), - /// Interact with the exclude files like .gitignore. - #[clap(subcommand)] - Exclude(exclude::Subcommands), - Config(config::Platform), - /// Subcommands that need no git repository to run. - #[clap(subcommand)] - Free(free::Subcommands), -} - -pub mod config { - /// Print all entries in a configuration file or access other sub-commands - #[derive(Debug, clap::Parser)] - #[clap(subcommand_required(false))] - pub struct Platform { - /// The filter terms to limit the output to matching sections and subsections only. - /// - /// Typical filters are `branch` or `remote.origin` or `remote.or*` - git-style globs are supported - /// and comparisons are case-insensitive. - pub filter: Vec, - } -} - -pub mod remote { - use git_repository as git; - - #[derive(Debug, clap::Parser)] - pub struct Platform { - /// The name of the remote to connect to. - /// - /// If unset, the current branch will determine the remote. - #[clap(long, short = 'n')] - pub name: Option, - - /// Connect directly to the given URL, forgoing any configuration from the repository. - #[clap(long, short = 'u', conflicts_with("name"), parse(try_from_os_str = std::convert::TryFrom::try_from))] - pub url: Option, - - /// Output additional typically information provided by the server as part of the connection handshake. - #[clap(long)] - pub handshake_info: bool, - - /// Subcommands - #[clap(subcommand)] - pub cmd: Subcommands, - } - - #[derive(Debug, clap::Subcommand)] - #[clap(visible_alias = "remotes")] - pub enum Subcommands { - /// Print all references available on the remote. - #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] - Refs, - /// Print all references available on the remote as filtered through ref-specs. - #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] - RefMap { - /// Override the built-in and configured ref-specs with one or more of the given ones. - #[clap(parse(try_from_os_str = git::env::os_str_to_bstring))] - ref_spec: Vec, - }, - } -} - -pub mod mailmap { - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Print all entries in configured mailmaps, inform about errors as well. - Entries, - } -} - -pub mod odb { - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Print all object names. - Entries, - /// Provide general information about the object database. - Info, - } -} - -pub mod tree { - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Print entries in a given tree - Entries { - /// Traverse the entire tree and its subtrees respectively, not only this tree. - #[clap(long, short = 'r')] - recursive: bool, - - /// Provide files size as well. This is expensive as the object is decoded entirely. - #[clap(long, short = 'e')] - extended: bool, - - /// The tree to traverse, or the tree at `HEAD` if unspecified. - treeish: Option, - }, - /// Provide information about a tree. - Info { - /// Provide files size as well. This is expensive as the object is decoded entirely. - #[clap(long, short = 'e')] - extended: bool, - /// The tree to traverse, or the tree at `HEAD` if unspecified. - treeish: Option, - }, - } -} - -pub mod commit { - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. - Describe { - /// Use annotated tag references only, not all tags. - #[clap(long, short = 't', conflicts_with("all-refs"))] - annotated_tags: bool, - - /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches. - #[clap(long, short = 'a', conflicts_with("annotated-tags"))] - all_refs: bool, - - /// Only follow the first parent when traversing the commit graph. - #[clap(long, short = 'f')] - first_parent: bool, - - /// Always display the long format, even if that would not be necessary as the id is located directly on a reference. - #[clap(long, short = 'l')] - long: bool, - - /// Consider only the given `n` candidates. This can take longer, but potentially produces more accurate results. - #[clap(long, short = 'c', default_value = "10")] - max_candidates: usize, - - /// Print information on stderr to inform about performance statistics - #[clap(long, short = 's')] - statistics: bool, - - #[clap(long)] - /// If there was no way to describe the commit, fallback to using the abbreviated input revision. - always: bool, - - /// A specification of the revision to use, or the current `HEAD` if unset. - rev_spec: Option, - }, - } -} - -pub mod credential { - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Get the credentials fed for `url=` via STDIN. - #[clap(visible_alias = "get")] - Fill, - /// Approve the information piped via STDIN as obtained with last call to `fill` - #[clap(visible_alias = "store")] - Approve, - /// Try to resolve the given revspec and print the object names. - #[clap(visible_alias = "erase")] - Reject, - } -} - -pub mod revision { - #[derive(Debug, clap::Subcommand)] - #[clap(visible_alias = "rev", visible_alias = "r")] - pub enum Subcommands { - /// List all commits reachable from the given rev-spec. - #[clap(visible_alias = "l")] - List { spec: std::ffi::OsString }, - /// Provide the revision specification like `@~1` to explain. - #[clap(visible_alias = "e")] - Explain { spec: std::ffi::OsString }, - /// Try to resolve the given revspec and print the object names. - #[clap(visible_alias = "query", visible_alias = "parse", visible_alias = "p")] - Resolve { - /// Instead of resolving a rev-spec, explain what would be done for the first spec. - /// - /// Equivalent to the `explain` subcommand. - #[clap(short = 'e', long)] - explain: bool, - /// Show the first resulting object similar to how `git cat-file` would, but don't show the resolved spec. - #[clap(short = 'c', long, conflicts_with = "explain")] - cat_file: bool, - /// rev-specs like `@`, `@~1` or `HEAD^2`. - #[clap(required = true)] - specs: Vec, - }, - /// Return the names and hashes of all previously checked-out branches. - #[clap(visible_alias = "prev")] - PreviousBranches, - } -} - -/// -pub mod free { - #[derive(Debug, clap::Subcommand)] - #[clap(visible_alias = "no-repo")] - pub enum Subcommands { - /// Subcommands for interacting with commit-graphs - #[clap(subcommand)] - CommitGraph(commitgraph::Subcommands), - /// Subcommands for interacting with mailmaps - Mailmap { - #[clap(flatten)] - cmd: mailmap::Platform, - }, - /// Subcommands for interacting with pack files and indices - #[clap(subcommand)] - Pack(pack::Subcommands), - /// Subcommands for interacting with a worktree index, typically at .git/index - Index(index::Platform), - } - - /// - pub mod commitgraph { - use std::path::PathBuf; - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Verify the integrity of a commit graph - Verify { - /// The path to '.git/objects/info/', '.git/objects/info/commit-graphs/', or '.git/objects/info/commit-graph' to validate. - path: PathBuf, - /// output statistical information about the pack - #[clap(long, short = 's')] - statistics: bool, - }, - } - } - - pub mod index { - use std::path::PathBuf; - - #[derive(Debug, clap::Parser)] - pub struct Platform { - /// The object format to assume when reading files that don't inherently know about it, or when writing files. - #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))] - pub object_hash: git_repository::hash::Kind, - - /// The path to the index file. - #[clap(short = 'i', long, default_value = ".git/index")] - pub index_path: PathBuf, - - /// Subcommands - #[clap(subcommand)] - pub cmd: Subcommands, - } - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Validate constraints and assumptions of an index along with its integrity. - Verify, - /// Print all entries to standard output - Entries, - /// Print information about the index structure - Info { - /// Do not extract specific extension information to gain only a superficial idea of the index's composition. - #[clap(long)] - no_details: bool, - }, - /// Checkout the index into a directory with exclusive write access, similar to what would happen during clone. - CheckoutExclusive { - /// The path to `.git` repository from which objects can be obtained to write the actual files referenced - /// in the index. Use this measure the impact on extracting objects on overall performance. - #[clap(long, short = 'r')] - repository: Option, - /// Ignore errors and keep checking out as many files as possible, and report all errors at the end of the operation. - #[clap(long, short = 'k')] - keep_going: bool, - /// Enable to query the object database yet write only empty files. This is useful to measure the overhead of ODB query - /// compared to writing the bytes to disk. - #[clap(long, short = 'e', requires = "repository")] - empty_files: bool, - /// The directory into which to write all index entries. - directory: PathBuf, - }, - } - } - - /// - pub mod pack { - use std::{ffi::OsString, path::PathBuf}; - - use gitoxide_core as core; - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Subcommands for interacting with pack indices (.idx) - #[clap(subcommand)] - Index(index::Subcommands), - /// Subcommands for interacting with multi-pack indices (named "multi-pack-index") - MultiIndex(multi_index::Platform), - /// Create a new pack with a set of objects. - Create { - #[clap(long, short = 'r')] - /// the directory containing the '.git' repository from which objects should be read. - repository: Option, - - #[clap(long, short = 'e', possible_values(core::pack::create::ObjectExpansion::variants()))] - /// the way objects are expanded. They differ in costs. - /// - /// Possible values are "none" and "tree-traversal". Default is "none". - expansion: Option, - - #[clap(long, default_value_t = 3, requires = "nondeterministic-count")] - /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting - /// to the globally configured threads. - /// - /// Use it to have different trade-offs between counting performance and cost in terms of CPU, as the scaling - /// here is everything but linear. The effectiveness of each core seems to be no more than 30%. - counting_threads: usize, - - #[clap(long)] - /// if set, the counting phase may be accelerated using multithreading. - /// - /// On the flip side, however, one will loose deterministic counting results which affects the - /// way the resulting pack is structured. - nondeterministic_count: bool, - - #[clap(long, short = 's')] - /// If set statistical information will be presented to inform about pack creation details. - /// It's a form of instrumentation for developers to help improve pack generation. - statistics: bool, - - #[clap(long)] - /// The size in megabytes for a cache to speed up pack access for packs with long delta chains. - /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size. - /// - /// If unset, no cache will be used. - pack_cache_size_mb: Option, - - #[clap(long)] - /// The size in megabytes for a cache to speed up accessing entire objects, bypassing object database access when hit. - /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size. - /// - /// This cache type is currently only effective when using the 'diff-tree' object expansion. - /// - /// If unset, no cache will be used. - object_cache_size_mb: Option, - - #[clap(long)] - /// if set, delta-objects whose base object wouldn't be in the pack will not be recompressed as base object, but instead - /// refer to its base object using its object id. - /// - /// This allows for smaller packs but requires the receiver of the pack to resolve these ids before storing the pack. - /// Packs produced with this option enabled are only valid in transit, but not at rest. - thin: bool, - - /// The directory into which to write the pack file. - #[clap(long, short = 'o')] - output_directory: Option, - - /// The tips from which to start the commit graph iteration, either as fully qualified commit hashes - /// or as branch names. - /// - /// If empty, we expect to read objects on stdin and default to 'none' as expansion mode. - /// Otherwise the expansion mode is 'tree-traversal' by default. - tips: Vec, - }, - /// Use the git-protocol to receive a pack, emulating a clone. - #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] - Receive { - /// The protocol version to use. Valid values are 1 and 2 - #[clap(long, short = 'p')] - protocol: Option, - - /// the directory into which to write references. Existing files will be overwritten. - /// - /// Note that the directory will be created if needed. - #[clap(long, short = 'd')] - refs_directory: Option, - - /// The URLs or path from which to receive the pack. - /// - /// See here for a list of supported URLs: - url: String, - - /// If set once or more times, these references will be fetched instead of all advertised ones. - /// - /// Note that this requires a reasonably modern git server. - #[clap(long = "reference", short = 'r')] - refs: Vec, - - /// The directory into which to write the received pack and index. - /// - /// If unset, they will be discarded. - directory: Option, - }, - /// Dissolve a pack into its loose objects. - /// - /// Note that this effectively removes delta compression for an average compression of 2x, creating one file per object in the process. - /// Thus this should only be done to dissolve small packs after a fetch. - Explode { - #[clap(long)] - /// Read written objects back and assert they match their source. Fail the operation otherwise. - /// - /// Only relevant if an object directory is set. - verify: bool, - - /// delete the pack and index file after the operation is successful - #[clap(long)] - delete_pack: bool, - - /// The amount of checks to run - #[clap( - long, - short = 'c', - default_value = "all", - possible_values(core::pack::explode::SafetyCheck::variants()) - )] - check: core::pack::explode::SafetyCheck, - - /// Compress bytes even when using the sink, i.e. no object directory is specified - /// - /// This helps to determine overhead related to compression. If unset, the sink will - /// only create hashes from bytes, which is usually limited by the speed at which input - /// can be obtained. - #[clap(long)] - sink_compress: bool, - - /// The '.pack' or '.idx' file to explode into loose objects - pack_path: PathBuf, - - /// The path into which all objects should be written. Commonly '.git/objects' - object_path: Option, - }, - /// Verify the integrity of a pack, index or multi-index file - Verify { - #[clap(flatten)] - args: VerifyOptions, - - /// The '.pack', '.idx' or 'multi-pack-index' file to validate. - path: PathBuf, - }, - } - - #[derive(Debug, clap::Parser)] - pub struct VerifyOptions { - /// output statistical information - #[clap(long, short = 's')] - pub statistics: bool, - /// The algorithm used to verify packs. They differ in costs. - #[clap( - long, - short = 'a', - default_value = "less-time", - possible_values(core::pack::verify::Algorithm::variants()) - )] - pub algorithm: core::pack::verify::Algorithm, - - #[clap(long, conflicts_with("re-encode"))] - /// Decode and parse tags, commits and trees to validate their correctness beyond hashing correctly. - /// - /// Malformed objects should not usually occur, but could be injected on purpose or accident. - /// This will reduce overall performance. - pub decode: bool, - - #[clap(long)] - /// Decode and parse tags, commits and trees to validate their correctness, and re-encode them. - /// - /// This flag is primarily to test the implementation of encoding, and requires to decode the object first. - /// Encoding an object after decoding it should yield exactly the same bytes. - /// This will reduce overall performance even more, as re-encoding requires to transform zero-copy objects into - /// owned objects, causing plenty of allocation to occur. - pub re_encode: bool, - } - - /// - pub mod multi_index { - use std::path::PathBuf; - - #[derive(Debug, clap::Parser)] - pub struct Platform { - /// The path to the index file. - #[clap(short = 'i', long, default_value = ".git/objects/pack/multi-pack-index")] - pub multi_index_path: PathBuf, - - /// Subcommands - #[clap(subcommand)] - pub cmd: Subcommands, - } - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Display all entries of a multi-index: - Entries, - /// Print general information about a multi-index file - Info, - /// Verify a multi-index quickly without inspecting objects themselves - Verify, - /// Create a multi-pack index from one or more pack index files, overwriting possibloy existing files. - Create { - /// Paths to the pack index files to read (with .idx extension). - /// - /// Note for the multi-index to be useful, it should be side-by-side with the supplied `.idx` files. - #[clap(required = true)] - index_paths: Vec, - }, - } - } - - /// - pub mod index { - use std::path::PathBuf; - - use gitoxide_core as core; - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// create a pack index from a pack data file. - Create { - /// Specify how to iterate the pack, defaults to 'verify' - /// - /// Valid values are - /// - /// **as-is** do not do anything and expect the pack file to be valid as per the trailing hash, - /// **verify** the input ourselves and validate that it matches with the hash provided in the pack, - /// **restore** hash the input ourselves and ignore failing entries, instead finish the pack with the hash we computed - /// to keep as many objects as possible. - #[clap( - long, - short = 'i', - default_value = "verify", - possible_values(core::pack::index::IterationMode::variants()) - )] - iteration_mode: core::pack::index::IterationMode, - - /// Path to the pack file to read (with .pack extension). - /// - /// If unset, the pack file is expected on stdin. - #[clap(long, short = 'p')] - pack_path: Option, - - /// The folder into which to place the pack and the generated index file - /// - /// If unset, only informational output will be provided to standard output. - directory: Option, - }, - } - } - } - - /// - pub mod mailmap { - use std::path::PathBuf; - - #[derive(Debug, clap::Parser)] - pub struct Platform { - /// The path to the mailmap file. - #[clap(short = 'p', long, default_value = ".mailmap")] - pub path: PathBuf, - - /// Subcommands - #[clap(subcommand)] - pub cmd: Subcommands, - } - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Parse all entries in the mailmap and report malformed lines or collisions. - Verify, - } - } -} - -pub mod exclude { - use std::ffi::OsString; - - use git_repository as git; - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// Check if path-specs are excluded and print the result similar to `git check-ignore`. - Query { - /// Show actual ignore patterns instead of un-excluding an entry. - /// - /// That way one can understand why an entry might not be excluded. - #[clap(long, short = 'i')] - show_ignore_patterns: bool, - /// Additional patterns to use for exclusions. They have the highest priority. - /// - /// Useful for undoing previous patterns using the '!' prefix. - #[clap(long, short = 'p')] - patterns: Vec, - /// The git path specifications to check for exclusion, or unset to read from stdin one per line. - #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))] - pathspecs: Vec, - }, - } -} diff --git a/src/plumbing/options/free.rs b/src/plumbing/options/free.rs new file mode 100644 index 00000000000..7e8e25c83ed --- /dev/null +++ b/src/plumbing/options/free.rs @@ -0,0 +1,368 @@ +#[derive(Debug, clap::Subcommand)] +#[clap(visible_alias = "no-repo")] +pub enum Subcommands { + /// Subcommands for interacting with commit-graphs + #[clap(subcommand)] + CommitGraph(commitgraph::Subcommands), + /// Subcommands for interacting with mailmaps + Mailmap { + #[clap(flatten)] + cmd: mailmap::Platform, + }, + /// Subcommands for interacting with pack files and indices + #[clap(subcommand)] + Pack(pack::Subcommands), + /// Subcommands for interacting with a worktree index, typically at .git/index + Index(index::Platform), +} + +/// +pub mod commitgraph { + use std::path::PathBuf; + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Verify the integrity of a commit graph + Verify { + /// The path to '.git/objects/info/', '.git/objects/info/commit-graphs/', or '.git/objects/info/commit-graph' to validate. + path: PathBuf, + /// output statistical information about the pack + #[clap(long, short = 's')] + statistics: bool, + }, + } +} + +pub mod index { + use std::path::PathBuf; + + #[derive(Debug, clap::Parser)] + pub struct Platform { + /// The object format to assume when reading files that don't inherently know about it, or when writing files. + #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))] + pub object_hash: git_repository::hash::Kind, + + /// The path to the index file. + #[clap(short = 'i', long, default_value = ".git/index")] + pub index_path: PathBuf, + + /// Subcommands + #[clap(subcommand)] + pub cmd: Subcommands, + } + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Validate constraints and assumptions of an index along with its integrity. + Verify, + /// Print all entries to standard output + Entries, + /// Print information about the index structure + Info { + /// Do not extract specific extension information to gain only a superficial idea of the index's composition. + #[clap(long)] + no_details: bool, + }, + /// Checkout the index into a directory with exclusive write access, similar to what would happen during clone. + CheckoutExclusive { + /// The path to `.git` repository from which objects can be obtained to write the actual files referenced + /// in the index. Use this measure the impact on extracting objects on overall performance. + #[clap(long, short = 'r')] + repository: Option, + /// Ignore errors and keep checking out as many files as possible, and report all errors at the end of the operation. + #[clap(long, short = 'k')] + keep_going: bool, + /// Enable to query the object database yet write only empty files. This is useful to measure the overhead of ODB query + /// compared to writing the bytes to disk. + #[clap(long, short = 'e', requires = "repository")] + empty_files: bool, + /// The directory into which to write all index entries. + directory: PathBuf, + }, + } +} + +/// +pub mod pack { + use std::{ffi::OsString, path::PathBuf}; + + use gitoxide_core as core; + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Subcommands for interacting with pack indices (.idx) + #[clap(subcommand)] + Index(index::Subcommands), + /// Subcommands for interacting with multi-pack indices (named "multi-pack-index") + MultiIndex(multi_index::Platform), + /// Create a new pack with a set of objects. + Create { + #[clap(long, short = 'r')] + /// the directory containing the '.git' repository from which objects should be read. + repository: Option, + + #[clap(long, short = 'e', possible_values(core::pack::create::ObjectExpansion::variants()))] + /// the way objects are expanded. They differ in costs. + /// + /// Possible values are "none" and "tree-traversal". Default is "none". + expansion: Option, + + #[clap(long, default_value_t = 3, requires = "nondeterministic-count")] + /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting + /// to the globally configured threads. + /// + /// Use it to have different trade-offs between counting performance and cost in terms of CPU, as the scaling + /// here is everything but linear. The effectiveness of each core seems to be no more than 30%. + counting_threads: usize, + + #[clap(long)] + /// if set, the counting phase may be accelerated using multithreading. + /// + /// On the flip side, however, one will loose deterministic counting results which affects the + /// way the resulting pack is structured. + nondeterministic_count: bool, + + #[clap(long, short = 's')] + /// If set statistical information will be presented to inform about pack creation details. + /// It's a form of instrumentation for developers to help improve pack generation. + statistics: bool, + + #[clap(long)] + /// The size in megabytes for a cache to speed up pack access for packs with long delta chains. + /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size. + /// + /// If unset, no cache will be used. + pack_cache_size_mb: Option, + + #[clap(long)] + /// The size in megabytes for a cache to speed up accessing entire objects, bypassing object database access when hit. + /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size. + /// + /// This cache type is currently only effective when using the 'diff-tree' object expansion. + /// + /// If unset, no cache will be used. + object_cache_size_mb: Option, + + #[clap(long)] + /// if set, delta-objects whose base object wouldn't be in the pack will not be recompressed as base object, but instead + /// refer to its base object using its object id. + /// + /// This allows for smaller packs but requires the receiver of the pack to resolve these ids before storing the pack. + /// Packs produced with this option enabled are only valid in transit, but not at rest. + thin: bool, + + /// The directory into which to write the pack file. + #[clap(long, short = 'o')] + output_directory: Option, + + /// The tips from which to start the commit graph iteration, either as fully qualified commit hashes + /// or as branch names. + /// + /// If empty, we expect to read objects on stdin and default to 'none' as expansion mode. + /// Otherwise the expansion mode is 'tree-traversal' by default. + tips: Vec, + }, + /// Use the git-protocol to receive a pack, emulating a clone. + #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] + Receive { + /// The protocol version to use. Valid values are 1 and 2 + #[clap(long, short = 'p')] + protocol: Option, + + /// the directory into which to write references. Existing files will be overwritten. + /// + /// Note that the directory will be created if needed. + #[clap(long, short = 'd')] + refs_directory: Option, + + /// The URLs or path from which to receive the pack. + /// + /// See here for a list of supported URLs: + url: String, + + /// If set once or more times, these references will be fetched instead of all advertised ones. + /// + /// Note that this requires a reasonably modern git server. + #[clap(long = "reference", short = 'r')] + refs: Vec, + + /// The directory into which to write the received pack and index. + /// + /// If unset, they will be discarded. + directory: Option, + }, + /// Dissolve a pack into its loose objects. + /// + /// Note that this effectively removes delta compression for an average compression of 2x, creating one file per object in the process. + /// Thus this should only be done to dissolve small packs after a fetch. + Explode { + #[clap(long)] + /// Read written objects back and assert they match their source. Fail the operation otherwise. + /// + /// Only relevant if an object directory is set. + verify: bool, + + /// delete the pack and index file after the operation is successful + #[clap(long)] + delete_pack: bool, + + /// The amount of checks to run + #[clap( + long, + short = 'c', + default_value = "all", + possible_values(core::pack::explode::SafetyCheck::variants()) + )] + check: core::pack::explode::SafetyCheck, + + /// Compress bytes even when using the sink, i.e. no object directory is specified + /// + /// This helps to determine overhead related to compression. If unset, the sink will + /// only create hashes from bytes, which is usually limited by the speed at which input + /// can be obtained. + #[clap(long)] + sink_compress: bool, + + /// The '.pack' or '.idx' file to explode into loose objects + pack_path: PathBuf, + + /// The path into which all objects should be written. Commonly '.git/objects' + object_path: Option, + }, + /// Verify the integrity of a pack, index or multi-index file + Verify { + #[clap(flatten)] + args: VerifyOptions, + + /// The '.pack', '.idx' or 'multi-pack-index' file to validate. + path: PathBuf, + }, + } + + #[derive(Debug, clap::Parser)] + pub struct VerifyOptions { + /// output statistical information + #[clap(long, short = 's')] + pub statistics: bool, + /// The algorithm used to verify packs. They differ in costs. + #[clap( + long, + short = 'a', + default_value = "less-time", + possible_values(core::pack::verify::Algorithm::variants()) + )] + pub algorithm: core::pack::verify::Algorithm, + + #[clap(long, conflicts_with("re-encode"))] + /// Decode and parse tags, commits and trees to validate their correctness beyond hashing correctly. + /// + /// Malformed objects should not usually occur, but could be injected on purpose or accident. + /// This will reduce overall performance. + pub decode: bool, + + #[clap(long)] + /// Decode and parse tags, commits and trees to validate their correctness, and re-encode them. + /// + /// This flag is primarily to test the implementation of encoding, and requires to decode the object first. + /// Encoding an object after decoding it should yield exactly the same bytes. + /// This will reduce overall performance even more, as re-encoding requires to transform zero-copy objects into + /// owned objects, causing plenty of allocation to occur. + pub re_encode: bool, + } + + /// + pub mod multi_index { + use std::path::PathBuf; + + #[derive(Debug, clap::Parser)] + pub struct Platform { + /// The path to the index file. + #[clap(short = 'i', long, default_value = ".git/objects/pack/multi-pack-index")] + pub multi_index_path: PathBuf, + + /// Subcommands + #[clap(subcommand)] + pub cmd: Subcommands, + } + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Display all entries of a multi-index: + Entries, + /// Print general information about a multi-index file + Info, + /// Verify a multi-index quickly without inspecting objects themselves + Verify, + /// Create a multi-pack index from one or more pack index files, overwriting possibloy existing files. + Create { + /// Paths to the pack index files to read (with .idx extension). + /// + /// Note for the multi-index to be useful, it should be side-by-side with the supplied `.idx` files. + #[clap(required = true)] + index_paths: Vec, + }, + } + } + + /// + pub mod index { + use std::path::PathBuf; + + use gitoxide_core as core; + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// create a pack index from a pack data file. + Create { + /// Specify how to iterate the pack, defaults to 'verify' + /// + /// Valid values are + /// + /// **as-is** do not do anything and expect the pack file to be valid as per the trailing hash, + /// **verify** the input ourselves and validate that it matches with the hash provided in the pack, + /// **restore** hash the input ourselves and ignore failing entries, instead finish the pack with the hash we computed + /// to keep as many objects as possible. + #[clap( + long, + short = 'i', + default_value = "verify", + possible_values(core::pack::index::IterationMode::variants()) + )] + iteration_mode: core::pack::index::IterationMode, + + /// Path to the pack file to read (with .pack extension). + /// + /// If unset, the pack file is expected on stdin. + #[clap(long, short = 'p')] + pack_path: Option, + + /// The folder into which to place the pack and the generated index file + /// + /// If unset, only informational output will be provided to standard output. + directory: Option, + }, + } + } +} + +/// +pub mod mailmap { + use std::path::PathBuf; + + #[derive(Debug, clap::Parser)] + pub struct Platform { + /// The path to the mailmap file. + #[clap(short = 'p', long, default_value = ".mailmap")] + pub path: PathBuf, + + /// Subcommands + #[clap(subcommand)] + pub cmd: Subcommands, + } + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Parse all entries in the mailmap and report malformed lines or collisions. + Verify, + } +} diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs new file mode 100644 index 00000000000..b7c3ea58db8 --- /dev/null +++ b/src/plumbing/options/mod.rs @@ -0,0 +1,310 @@ +use std::path::PathBuf; + +use git_repository as git; +use git_repository::bstr::BString; +use gitoxide_core as core; + +#[derive(Debug, clap::Parser)] +#[clap(name = "gix-plumbing", about = "The git underworld", version = clap::crate_version!())] +#[clap(subcommand_required = true)] +#[clap(arg_required_else_help = true)] +pub struct Args { + /// The repository to access. + #[clap(short = 'r', long, default_value = ".")] + pub repository: PathBuf, + + /// Add these values to the configuration in the form of `key=value` or `key`. + /// + /// For example, if `key` is `core.abbrev`, set configuration like `[core] abbrev = key`, + /// or `remote.origin.url = foo` to set `[remote "origin"] url = foo`. + #[clap(long, short = 'c', parse(try_from_os_str = git::env::os_str_to_bstring))] + pub config: Vec, + + #[clap(long, short = 't')] + /// The amount of threads to use for some operations. + /// + /// If unset, or the value is 0, there is no limit and all logical cores can be used. + pub threads: Option, + + /// Display verbose messages and progress information + #[clap(long, short = 'v')] + pub verbose: bool, + + /// Bring up a terminal user interface displaying progress visually + #[cfg(feature = "prodash-render-tui")] + #[clap(long, conflicts_with("verbose"))] + pub progress: bool, + + /// The progress TUI will stay up even though the work is already completed. + /// + /// Use this to be able to read progress messages or additional information visible in the TUI log pane. + #[cfg(feature = "prodash-render-tui")] + #[clap(long, conflicts_with("verbose"), requires("progress"))] + pub progress_keep_open: bool, + + /// Determine the format to use when outputting statistics. + #[clap( + long, + short = 'f', + default_value = "human", + possible_values(core::OutputFormat::variants()) + )] + pub format: core::OutputFormat, + + /// The object format to assume when reading files that don't inherently know about it, or when writing files. + #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))] + pub object_hash: git_repository::hash::Kind, + + #[clap(subcommand)] + pub cmd: Subcommands, +} + +#[derive(Debug, clap::Subcommand)] +pub enum Subcommands { + /// Interact with the object database. + #[clap(subcommand)] + Odb(odb::Subcommands), + /// Interact with tree objects. + #[clap(subcommand)] + Tree(tree::Subcommands), + /// Interact with commit objects. + #[clap(subcommand)] + Commit(commit::Subcommands), + /// Verify the integrity of the entire repository + Verify { + #[clap(flatten)] + args: free::pack::VerifyOptions, + }, + /// Query and obtain information about revisions. + #[clap(subcommand)] + Revision(revision::Subcommands), + /// A program just like `git credential`. + #[clap(subcommand)] + Credential(credential::Subcommands), + /// Interact with the mailmap. + #[clap(subcommand)] + Mailmap(mailmap::Subcommands), + /// Interact with the remote hosts. + Remote(remote::Platform), + /// Interact with the exclude files like .gitignore. + #[clap(subcommand)] + Exclude(exclude::Subcommands), + /// Display overall progress of the gitoxide project as seen from the perspective of git-config. + Progress, + Config(config::Platform), + /// Subcommands that need no git repository to run. + #[clap(subcommand)] + Free(free::Subcommands), +} + +pub mod config { + /// Print all entries in a configuration file or access other sub-commands + #[derive(Debug, clap::Parser)] + #[clap(subcommand_required(false))] + pub struct Platform { + /// The filter terms to limit the output to matching sections and subsections only. + /// + /// Typical filters are `branch` or `remote.origin` or `remote.or*` - git-style globs are supported + /// and comparisons are case-insensitive. + pub filter: Vec, + } +} + +pub mod remote { + use git_repository as git; + + #[derive(Debug, clap::Parser)] + pub struct Platform { + /// The name of the remote to connect to. + /// + /// If unset, the current branch will determine the remote. + #[clap(long, short = 'n')] + pub name: Option, + + /// Connect directly to the given URL, forgoing any configuration from the repository. + #[clap(long, short = 'u', conflicts_with("name"), parse(try_from_os_str = std::convert::TryFrom::try_from))] + pub url: Option, + + /// Output additional typically information provided by the server as part of the connection handshake. + #[clap(long)] + pub handshake_info: bool, + + /// Subcommands + #[clap(subcommand)] + pub cmd: Subcommands, + } + + #[derive(Debug, clap::Subcommand)] + #[clap(visible_alias = "remotes")] + pub enum Subcommands { + /// Print all references available on the remote. + #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] + Refs, + /// Print all references available on the remote as filtered through ref-specs. + #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] + RefMap { + /// Override the built-in and configured ref-specs with one or more of the given ones. + #[clap(parse(try_from_os_str = git::env::os_str_to_bstring))] + ref_spec: Vec, + }, + } +} + +pub mod mailmap { + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Print all entries in configured mailmaps, inform about errors as well. + Entries, + } +} + +pub mod odb { + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Print all object names. + Entries, + /// Provide general information about the object database. + Info, + } +} + +pub mod tree { + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Print entries in a given tree + Entries { + /// Traverse the entire tree and its subtrees respectively, not only this tree. + #[clap(long, short = 'r')] + recursive: bool, + + /// Provide files size as well. This is expensive as the object is decoded entirely. + #[clap(long, short = 'e')] + extended: bool, + + /// The tree to traverse, or the tree at `HEAD` if unspecified. + treeish: Option, + }, + /// Provide information about a tree. + Info { + /// Provide files size as well. This is expensive as the object is decoded entirely. + #[clap(long, short = 'e')] + extended: bool, + /// The tree to traverse, or the tree at `HEAD` if unspecified. + treeish: Option, + }, + } +} + +pub mod commit { + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. + Describe { + /// Use annotated tag references only, not all tags. + #[clap(long, short = 't', conflicts_with("all-refs"))] + annotated_tags: bool, + + /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches. + #[clap(long, short = 'a', conflicts_with("annotated-tags"))] + all_refs: bool, + + /// Only follow the first parent when traversing the commit graph. + #[clap(long, short = 'f')] + first_parent: bool, + + /// Always display the long format, even if that would not be necessary as the id is located directly on a reference. + #[clap(long, short = 'l')] + long: bool, + + /// Consider only the given `n` candidates. This can take longer, but potentially produces more accurate results. + #[clap(long, short = 'c', default_value = "10")] + max_candidates: usize, + + /// Print information on stderr to inform about performance statistics + #[clap(long, short = 's')] + statistics: bool, + + #[clap(long)] + /// If there was no way to describe the commit, fallback to using the abbreviated input revision. + always: bool, + + /// A specification of the revision to use, or the current `HEAD` if unset. + rev_spec: Option, + }, + } +} + +pub mod credential { + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Get the credentials fed for `url=` via STDIN. + #[clap(visible_alias = "get")] + Fill, + /// Approve the information piped via STDIN as obtained with last call to `fill` + #[clap(visible_alias = "store")] + Approve, + /// Try to resolve the given revspec and print the object names. + #[clap(visible_alias = "erase")] + Reject, + } +} + +pub mod revision { + #[derive(Debug, clap::Subcommand)] + #[clap(visible_alias = "rev", visible_alias = "r")] + pub enum Subcommands { + /// List all commits reachable from the given rev-spec. + #[clap(visible_alias = "l")] + List { spec: std::ffi::OsString }, + /// Provide the revision specification like `@~1` to explain. + #[clap(visible_alias = "e")] + Explain { spec: std::ffi::OsString }, + /// Try to resolve the given revspec and print the object names. + #[clap(visible_alias = "query", visible_alias = "parse", visible_alias = "p")] + Resolve { + /// Instead of resolving a rev-spec, explain what would be done for the first spec. + /// + /// Equivalent to the `explain` subcommand. + #[clap(short = 'e', long)] + explain: bool, + /// Show the first resulting object similar to how `git cat-file` would, but don't show the resolved spec. + #[clap(short = 'c', long, conflicts_with = "explain")] + cat_file: bool, + /// rev-specs like `@`, `@~1` or `HEAD^2`. + #[clap(required = true)] + specs: Vec, + }, + /// Return the names and hashes of all previously checked-out branches. + #[clap(visible_alias = "prev")] + PreviousBranches, + } +} + +pub mod exclude { + use std::ffi::OsString; + + use git_repository as git; + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Check if path-specs are excluded and print the result similar to `git check-ignore`. + Query { + /// Show actual ignore patterns instead of un-excluding an entry. + /// + /// That way one can understand why an entry might not be excluded. + #[clap(long, short = 'i')] + show_ignore_patterns: bool, + /// Additional patterns to use for exclusions. They have the highest priority. + /// + /// Useful for undoing previous patterns using the '!' prefix. + #[clap(long, short = 'p')] + patterns: Vec, + /// The git path specifications to check for exclusion, or unset to read from stdin one per line. + #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))] + pathspecs: Vec, + }, + } +} + +/// +pub mod free; diff --git a/src/plumbing/progress.rs b/src/plumbing/progress.rs new file mode 100644 index 00000000000..5032f23dc9a --- /dev/null +++ b/src/plumbing/progress.rs @@ -0,0 +1,305 @@ +use crosstermion::crossterm::style::Stylize; +use owo_colors::OwoColorize; +use std::fmt::{Display, Formatter}; +use tabled::{Style, TableIteratorExt, Tabled}; + +#[derive(Clone)] +enum Usage { + NotApplicable, + Planned { + note: Option<&'static str>, + }, + InModule { + name: &'static str, + deviation: Option<&'static str>, + }, + /// Needs analysis + Puzzled, +} +use Usage::*; + +impl Display for Usage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Puzzled => f.write_str("❓")?, + NotApplicable => f.write_str("not applicable")?, + Planned { note } => { + write!(f, "{}", "planned".blink())?; + if let Some(note) = note { + write!(f, " ℹ {} ℹ", note.bright_white())?; + } + } + InModule { name, deviation } => { + write!(f, "mod {name}")?; + if let Some(deviation) = deviation { + write!(f, "{}", format!(" ❗️{deviation}❗️").bright_white())? + } + } + } + Ok(()) + } +} + +impl Usage { + pub fn icon(&self) -> &'static str { + match self { + Puzzled => "?", + NotApplicable => "❌", + Planned { .. } => "🕒", + InModule { deviation, .. } => deviation.is_some().then(|| "👌️").unwrap_or("✅"), + } + } +} + +#[derive(Clone)] +struct Record { + config: &'static str, + usage: Usage, +} + +impl Tabled for Record { + const LENGTH: usize = 3; + + fn fields(&self) -> Vec { + let mut tokens = self.config.split('.'); + let mut buf = vec![tokens.next().expect("present").bold().to_string()]; + buf.extend(tokens.map(ToOwned::to_owned)); + + vec![self.usage.icon().into(), buf.join("."), self.usage.to_string()] + } + + fn headers() -> Vec { + vec![] + } +} + +static GIT_CONFIG: &[Record] = &[ + Record { + config: "core.bare", + usage: InModule { + name: "config::cache", + deviation: None, + }, + }, + Record { + config: "core.excludesFile", + usage: InModule { + name: "config::cache", + deviation: None, + }, + }, + Record { + config: "core.abbrev", + usage: InModule { + name: "config::cache", + deviation: None, + }, + }, + Record { + config: "core.ignoreCase", + usage: InModule { + name: "config::cache", + deviation: None, + }, + }, + Record { + config: "core.multiPackIndex", + usage: InModule { + name: "config::cache", + deviation: None, + }, + }, + Record { + config: "core.disambiguate", + usage: InModule { + name: "config::cache", + deviation: None, + }, + }, + Record { + config: "core.filesRefLockTimeout", + usage: InModule {name: "config::cache::access", deviation: None}, + }, + Record { + config: "core.packedRefsTimeout", + usage: InModule {name: "config::cache::access", deviation: None}, + }, + Record { + config: "core.logAllRefUpdates", + usage: InModule { + name: "config::cache", + deviation: None, + }, + }, + Record { + config: "core.repositoryFormatVersion", + usage: InModule { + name: "config::cache::incubate", + deviation: None, + }, + }, + Record { + config: "extensions.objectFormat", + usage: InModule { + name: "config::cache::incubate", + deviation: Some( + "Support for SHA256 is prepared but not fully implemented yet. For now we abort when encountered.", + ), + }, + }, + Record { + config: "committer.name", + usage: InModule { + name: "repository::identity", + deviation: None, + }, + }, + Record { + config: "committer.email", + usage: InModule { + name: "repository::identity", + deviation: None, + }, + }, + Record { + config: "author.name", + usage: InModule { + name: "repository::identity", + deviation: None, + }, + }, + Record { + config: "author.email", + usage: InModule { + name: "repository::identity", + deviation: None, + }, + }, + Record { + config: "user.name", + usage: InModule { + name: "repository::identity", + deviation: Some("defaults to 'gitoxide'"), + }, + }, + Record { + config: "user.email", + usage: InModule { + name: "repository::identity", + deviation: Some("defaults to 'gitoxide@localhost'"), + }, + }, + Record { + config: "fetch.recurseSubmodules", + usage: Planned { + note: Some("Seems useful for cargo as well"), + }, + }, + Record { + config: "fetch.fsckObjects", + usage: Puzzled, + }, + Record { + config: "fetch.fsck.", + usage: Puzzled, + }, + Record { + config: "fetch.fsck.skipList", + usage: Puzzled, + }, + Record { + config: "fetch.unpackLimit", + usage: Planned { note: None }, + }, + Record { + config: "fetch.prune", + usage: Planned { note: None }, + }, + Record { + config: "fetch.pruneTags", + usage: Planned { note: None }, + }, + Record { + config: "fetch.writeCommitGraph", + usage: Planned { note: None }, + }, + Record { + config: "fetch.parallel", + usage: Planned { note: None }, + }, + Record { + config: "fetch.showForcedUpdates", + usage: NotApplicable, + }, + Record { + config: "fetch.output", + usage: NotApplicable, + }, + Record { + config: "fetch.negotiationAlgorithm", + usage: Planned { + note: Some("Implements our own 'naive' algorithm, only"), + }, + }, + Record { + config: "pack.threads", + usage: InModule { + name: "remote::connection::fetch", + deviation: Some("if unset, it uses all threads as opposed to just 1"), + }, + }, + Record { + config: "pack.indexVersion", + usage: InModule { + name: "remote::connection::fetch", + deviation: None, + }, + }, + Record { + config: "protocol.allow", + usage: InModule { + name: "remote::url::scheme_permission", + deviation: None, + }, + }, + Record { + config: "protocol..allow", + usage: InModule { + name: "remote::url::scheme_permission", + deviation: None, + }, + }, + Record { + config: "remotes.", + usage: Planned { + note: Some("useful for multi-remote fetches as part of the standard API, maybe just `group(name) -> Option>`"), + }, + }, + Record { + config: "url..insteadOf", + usage: InModule { + name: "remote::url::rewrite", + deviation: None, + }, + }, + Record { + config: "url..pushInsteadOf", + usage: InModule { + name: "remote::url::rewrite", + deviation: None, + }, + }, +]; + +/// A programmatic way to record and display progress. +pub fn show_progress() -> anyhow::Result<()> { + let sorted = { + let mut v: Vec<_> = GIT_CONFIG.into(); + v.sort_by_key(|r| r.config); + v + }; + + println!("{}", sorted.table().with(Style::blank())); + println!("\nTotal records: {}", GIT_CONFIG.len()); + Ok(()) +} diff --git a/tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success b/tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success index fef8d1982c5..d0a2c67ba71 100644 --- a/tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success +++ b/tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success @@ -1,6 +1,6 @@ { "index": { - "index_kind": "V2", + "index_version": "V2", "index_hash": { "Sha1": [ 86, diff --git a/tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success b/tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success index e3254bf57d6..9fa80d16c06 100644 --- a/tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success +++ b/tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success @@ -1,6 +1,6 @@ { "index": { - "index_kind": "V2", + "index_version": "V2", "index_hash": { "Sha1": [ 44, diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json index 590a10265e2..ab2bb31f86b 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json +++ b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json @@ -1,6 +1,6 @@ { "index": { - "index_kind": "V2", + "index_version": "V2", "index_hash": "c787de2aafb897417ca8167baeb146eabd18bc5f", "data_hash": "346574b7331dc3a1724da218d622c6e1b6c66a57", "num_objects": 9 diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index 3cc40128c4c..9a7ee410db9 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -535,6 +535,14 @@ impl<'a> Env<'a> { self.altered_vars.push((var, prev)); self } + + /// Set `var` to `value`. + pub fn unset(mut self, var: &'a str) -> Self { + let prev = std::env::var_os(var); + std::env::remove_var(var); + self.altered_vars.push((var, prev)); + self + } } impl<'a> Drop for Env<'a> {