From 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 14:41:41 +0200 Subject: Merging upstream version 1.70.0+dfsg2. Signed-off-by: Daniel Baumann --- vendor/gix/src/repository/cache.rs | 30 ++ vendor/gix/src/repository/config/mod.rs | 191 ++++++++++++ vendor/gix/src/repository/config/transport.rs | 425 ++++++++++++++++++++++++++ vendor/gix/src/repository/identity.rs | 175 +++++++++++ vendor/gix/src/repository/impls.rs | 73 +++++ vendor/gix/src/repository/init.rs | 55 ++++ vendor/gix/src/repository/location.rs | 86 ++++++ vendor/gix/src/repository/mod.rs | 36 +++ vendor/gix/src/repository/object.rs | 214 +++++++++++++ vendor/gix/src/repository/permissions.rs | 168 ++++++++++ vendor/gix/src/repository/reference.rs | 243 +++++++++++++++ vendor/gix/src/repository/remote.rs | 199 ++++++++++++ vendor/gix/src/repository/revision.rs | 42 +++ vendor/gix/src/repository/snapshots.rs | 109 +++++++ vendor/gix/src/repository/state.rs | 44 +++ vendor/gix/src/repository/thread_safe.rs | 66 ++++ vendor/gix/src/repository/worktree.rs | 119 ++++++++ 17 files changed, 2275 insertions(+) create mode 100644 vendor/gix/src/repository/cache.rs create mode 100644 vendor/gix/src/repository/config/mod.rs create mode 100644 vendor/gix/src/repository/config/transport.rs create mode 100644 vendor/gix/src/repository/identity.rs create mode 100644 vendor/gix/src/repository/impls.rs create mode 100644 vendor/gix/src/repository/init.rs create mode 100644 vendor/gix/src/repository/location.rs create mode 100644 vendor/gix/src/repository/mod.rs create mode 100644 vendor/gix/src/repository/object.rs create mode 100644 vendor/gix/src/repository/permissions.rs create mode 100644 vendor/gix/src/repository/reference.rs create mode 100644 vendor/gix/src/repository/remote.rs create mode 100644 vendor/gix/src/repository/revision.rs create mode 100644 vendor/gix/src/repository/snapshots.rs create mode 100644 vendor/gix/src/repository/state.rs create mode 100644 vendor/gix/src/repository/thread_safe.rs create mode 100644 vendor/gix/src/repository/worktree.rs (limited to 'vendor/gix/src/repository') diff --git a/vendor/gix/src/repository/cache.rs b/vendor/gix/src/repository/cache.rs new file mode 100644 index 000000000..7dcd844e6 --- /dev/null +++ b/vendor/gix/src/repository/cache.rs @@ -0,0 +1,30 @@ +/// Configure how caches are used to speed up various git repository operations +impl crate::Repository { + /// Sets the amount of space used at most for caching most recently accessed fully decoded objects, to `Some(bytes)`, + /// or `None` to deactivate it entirely. + /// + /// Note that it is unset by default but can be enabled once there is time for performance optimization. + /// Well-chosen cache sizes can improve performance particularly if objects are accessed multiple times in a row. + /// The cache is configured to grow gradually. + /// + /// Note that a cache on application level should be considered as well as the best object access is not doing one. + pub fn object_cache_size(&mut self, bytes: impl Into>) { + let bytes = bytes.into(); + match bytes { + Some(bytes) if bytes == 0 => self.objects.unset_object_cache(), + Some(bytes) => self + .objects + .set_object_cache(move || Box::new(crate::object::cache::MemoryCappedHashmap::new(bytes))), + None => self.objects.unset_object_cache(), + } + } + + /// Set an object cache of size `bytes` if none is set. + /// + /// Use this method to avoid overwriting any existing value while assuring better performance in case no value is set. + pub fn object_cache_size_if_unset(&mut self, bytes: usize) { + if !self.objects.has_object_cache() { + self.object_cache_size(bytes) + } + } +} diff --git a/vendor/gix/src/repository/config/mod.rs b/vendor/gix/src/repository/config/mod.rs new file mode 100644 index 000000000..92b2618cc --- /dev/null +++ b/vendor/gix/src/repository/config/mod.rs @@ -0,0 +1,191 @@ +use std::collections::BTreeSet; + +use crate::{bstr::ByteSlice, config}; + +/// General Configuration +impl crate::Repository { + /// Return a snapshot of the configuration as seen upon opening the repository. + pub fn config_snapshot(&self) -> config::Snapshot<'_> { + config::Snapshot { repo: self } + } + + /// 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: Some(self), + config, + } + } + + /// The options used to open the repository. + pub fn open_options(&self) -> &crate::open::Options { + &self.options + } + + /// Obtain options for use when connecting via `ssh`. + #[cfg(feature = "blocking-network-client")] + pub fn ssh_connect_options( + &self, + ) -> Result { + use crate::config::{ + cache::util::ApplyLeniency, + tree::{gitoxide, Core, Ssh}, + }; + + let config = &self.config.resolved; + let mut trusted = self.filter_config_section(); + let mut fallback_active = false; + let ssh_command = config + .string_filter("core", None, Core::SSH_COMMAND.name, &mut trusted) + .or_else(|| { + fallback_active = true; + config.string_filter( + "gitoxide", + Some("ssh".into()), + gitoxide::Ssh::COMMAND_WITHOUT_SHELL_FALLBACK.name, + &mut trusted, + ) + }) + .map(|cmd| gix_path::from_bstr(cmd).into_owned().into()); + let opts = gix_protocol::transport::client::ssh::connect::Options { + disallow_shell: fallback_active, + command: ssh_command, + kind: config + .string_filter_by_key("ssh.variant", &mut trusted) + .and_then(|variant| Ssh::VARIANT.try_into_variant(variant).transpose()) + .transpose() + .with_leniency(self.options.lenient_config)?, + }; + Ok(opts) + } + + /// The kind of object hash the repository is configured to use. + pub fn object_hash(&self) -> gix_hash::Kind { + self.config.object_hash + } +} + +#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] +mod transport; + +mod remote { + use std::{borrow::Cow, collections::BTreeSet}; + + use crate::{bstr::ByteSlice, remote}; + + impl crate::Repository { + /// Returns a sorted list unique of symbolic names of remotes that + /// we deem [trustworthy][crate::open::Options::filter_config_section()]. + // TODO: Use `remote::Name` here + pub fn remote_names(&self) -> BTreeSet<&str> { + self.subsection_names_of("remote") + } + + /// Obtain the branch-independent name for a remote for use in the given `direction`, or `None` if it could not be determined. + /// + /// For _fetching_, use the only configured remote, or default to `origin` if it exists. + /// For _pushing_, use the `remote.pushDefault` trusted configuration key, or fall back to the rules for _fetching_. + /// + /// # Notes + /// + /// It's up to the caller to determine what to do if the current `head` is unborn or detached. + // TODO: use remote::Name here + pub fn remote_default_name(&self, direction: remote::Direction) -> Option> { + let name = (direction == remote::Direction::Push) + .then(|| { + self.config + .resolved + .string_filter("remote", None, "pushDefault", &mut self.filter_config_section()) + .and_then(|s| match s { + Cow::Borrowed(s) => s.to_str().ok().map(Cow::Borrowed), + Cow::Owned(s) => s.to_str().ok().map(|s| Cow::Owned(s.into())), + }) + }) + .flatten(); + name.or_else(|| { + let names = self.remote_names(); + match names.len() { + 0 => None, + 1 => names.iter().next().copied().map(Cow::Borrowed), + _more_than_one => names.get("origin").copied().map(Cow::Borrowed), + } + }) + } + } +} + +mod branch { + use std::{borrow::Cow, collections::BTreeSet, convert::TryInto}; + + use gix_ref::FullNameRef; + use gix_validate::reference::name::Error as ValidateNameError; + + use crate::bstr::BStr; + + impl crate::Repository { + /// Return a set of unique short branch names for which custom configuration exists in the configuration, + /// if we deem them [trustworthy][crate::open::Options::filter_config_section()]. + pub fn branch_names(&self) -> BTreeSet<&str> { + self.subsection_names_of("branch") + } + + /// Returns the validated reference on the remote associated with the given `short_branch_name`, + /// always `main` instead of `refs/heads/main`. + /// + /// The returned reference is the one we track on the remote side for merging and pushing. + /// Returns `None` if the remote reference was not found. + /// May return an error if the reference is invalid. + pub fn branch_remote_ref<'a>( + &self, + short_branch_name: impl Into<&'a BStr>, + ) -> Option, ValidateNameError>> { + self.config + .resolved + .string("branch", Some(short_branch_name.into()), "merge") + .map(crate::config::tree::branch::Merge::try_into_fullrefname) + } + + /// Returns the unvalidated name of the remote associated with the given `short_branch_name`, + /// typically `main` instead of `refs/heads/main`. + /// In some cases, the returned name will be an URL. + /// Returns `None` if the remote was not found or if the name contained illformed UTF-8. + /// + /// See also [Reference::remote_name()][crate::Reference::remote_name()] for a more typesafe version + /// to be used when a `Reference` is available. + pub fn branch_remote_name<'a>( + &self, + short_branch_name: impl Into<&'a BStr>, + ) -> Option> { + self.config + .resolved + .string("branch", Some(short_branch_name.into()), "remote") + .and_then(|name| name.try_into().ok()) + } + } +} + +impl crate::Repository { + pub(crate) fn filter_config_section(&self) -> fn(&gix_config::file::Metadata) -> bool { + self.options + .filter_config_section + .unwrap_or(config::section::is_trusted) + } + + fn subsection_names_of<'a>(&'a self, header_name: &'a str) -> BTreeSet<&'a str> { + self.config + .resolved + .sections_by_name(header_name) + .map(|it| { + let filter = self.filter_config_section(); + it.filter(move |s| filter(s.meta())) + .filter_map(|section| section.header().subsection_name().and_then(|b| b.to_str().ok())) + .collect() + }) + .unwrap_or_default() + } +} diff --git a/vendor/gix/src/repository/config/transport.rs b/vendor/gix/src/repository/config/transport.rs new file mode 100644 index 000000000..dcfbc0bf6 --- /dev/null +++ b/vendor/gix/src/repository/config/transport.rs @@ -0,0 +1,425 @@ +#![allow(clippy::result_large_err)] +use std::any::Any; + +use crate::bstr::BStr; + +impl crate::Repository { + /// Produce configuration suitable for `url`, as differentiated by its protocol/scheme, to be passed to a transport instance via + /// [configure()][gix_transport::client::TransportWithoutIO::configure()] (via `&**config` to pass the contained `Any` and not the `Box`). + /// `None` is returned if there is no known configuration. If `remote_name` is not `None`, the remote's name may contribute to + /// configuration overrides, typically for the HTTP transport. + /// + /// Note that the caller may cast the instance themselves to modify it before passing it on. + /// + /// For transports that support proxy authentication, the + /// [default authentication method](crate::config::Snapshot::credential_helpers()) will be used with the url of the proxy + /// if it contains a user name. + #[cfg_attr( + not(any( + feature = "blocking-http-transport-reqwest", + feature = "blocking-http-transport-curl" + )), + allow(unused_variables) + )] + pub fn transport_options<'a>( + &self, + url: impl Into<&'a BStr>, + remote_name: Option<&BStr>, + ) -> Result>, crate::config::transport::Error> { + let url = gix_url::parse(url.into())?; + use gix_url::Scheme::*; + + match &url.scheme { + Http | Https => { + #[cfg(not(any( + feature = "blocking-http-transport-reqwest", + feature = "blocking-http-transport-curl" + )))] + { + Ok(None) + } + #[cfg(any( + feature = "blocking-http-transport-reqwest", + feature = "blocking-http-transport-curl" + ))] + { + use std::{ + borrow::Cow, + sync::{Arc, Mutex}, + }; + + use gix_transport::client::{ + http, + http::options::{ProxyAuthMethod, SslVersion, SslVersionRangeInclusive}, + }; + + use crate::{ + config, + config::{ + cache::util::ApplyLeniency, + tree::{gitoxide, Key, Remote}, + }, + }; + fn try_cow_to_string( + v: Cow<'_, BStr>, + lenient: bool, + key_str: impl Into>, + key: &'static config::tree::keys::String, + ) -> Result, config::transport::Error> { + key.try_into_string(v) + .map_err(|err| config::transport::Error::IllformedUtf8 { + source: err, + key: key_str.into(), + }) + .map(Some) + .with_leniency(lenient) + } + + fn cow_bstr(v: &str) -> Cow<'_, BStr> { + Cow::Borrowed(v.into()) + } + + fn proxy_auth_method( + value_and_key: Option<( + Cow<'_, BStr>, + Cow<'static, BStr>, + &'static config::tree::http::ProxyAuthMethod, + )>, + ) -> Result { + let value = value_and_key + .map(|(method, key, key_type)| { + key_type.try_into_proxy_auth_method(method).map_err(|err| { + config::transport::http::Error::InvalidProxyAuthMethod { source: err, key } + }) + }) + .transpose()? + .unwrap_or_default(); + Ok(value) + } + + fn ssl_version( + config: &gix_config::File<'static>, + key_str: &'static str, + key: &'static config::tree::http::SslVersion, + mut filter: fn(&gix_config::file::Metadata) -> bool, + lenient: bool, + ) -> Result, config::transport::Error> { + debug_assert_eq!( + key_str, + key.logical_name(), + "BUG: hardcoded and generated key names must match" + ); + config + .string_filter_by_key(key_str, &mut filter) + .filter(|v| !v.is_empty()) + .map(|v| { + key.try_into_ssl_version(v) + .map_err(crate::config::transport::http::Error::from) + }) + .transpose() + .with_leniency(lenient) + .map_err(Into::into) + } + + fn proxy( + value: Option<(Cow<'_, BStr>, Cow<'static, BStr>, &'static config::tree::keys::String)>, + lenient: bool, + ) -> Result, config::transport::Error> { + Ok(value + .and_then(|(v, k, key)| try_cow_to_string(v, lenient, k.clone(), key).transpose()) + .transpose()? + .map(|mut proxy| { + if !proxy.trim().is_empty() && !proxy.contains("://") { + proxy.insert_str(0, "http://"); + proxy + } else { + proxy + } + })) + } + + let mut opts = http::Options::default(); + let config = &self.config.resolved; + let mut trusted_only = self.filter_config_section(); + let lenient = self.config.lenient_config; + opts.extra_headers = { + let key = "http.extraHeader"; + debug_assert_eq!(key, &config::tree::Http::EXTRA_HEADER.logical_name()); + config + .strings_filter_by_key(key, &mut trusted_only) + .map(|values| config::tree::Http::EXTRA_HEADER.try_into_extra_header(values)) + .transpose() + .map_err(|err| config::transport::Error::IllformedUtf8 { + source: err, + key: Cow::Borrowed(key.into()), + })? + .unwrap_or_default() + }; + + opts.follow_redirects = { + let key = "http.followRedirects"; + + config::tree::Http::FOLLOW_REDIRECTS + .try_into_follow_redirects( + config.string_filter_by_key(key, &mut trusted_only).unwrap_or_default(), + || { + config + .boolean_filter_by_key(key, &mut trusted_only) + .transpose() + .with_leniency(lenient) + }, + ) + .map_err(config::transport::http::Error::InvalidFollowRedirects)? + }; + + opts.low_speed_time_seconds = config + .integer_filter_by_key("http.lowSpeedTime", &mut trusted_only) + .map(|value| config::tree::Http::LOW_SPEED_TIME.try_into_u64(value)) + .transpose() + .with_leniency(lenient) + .map_err(config::transport::http::Error::from)? + .unwrap_or_default(); + opts.low_speed_limit_bytes_per_second = config + .integer_filter_by_key("http.lowSpeedLimit", &mut trusted_only) + .map(|value| config::tree::Http::LOW_SPEED_LIMIT.try_into_u32(value)) + .transpose() + .with_leniency(lenient) + .map_err(config::transport::http::Error::from)? + .unwrap_or_default(); + opts.proxy = proxy( + remote_name + .and_then(|name| { + config + .string_filter("remote", Some(name), Remote::PROXY.name, &mut trusted_only) + .map(|v| (v, Cow::Owned(format!("remote.{name}.proxy").into()), &Remote::PROXY)) + }) + .or_else(|| { + let key = "http.proxy"; + debug_assert_eq!(key, config::tree::Http::PROXY.logical_name()); + let http_proxy = config + .string_filter_by_key(key, &mut trusted_only) + .map(|v| (v, cow_bstr(key), &config::tree::Http::PROXY)) + .or_else(|| { + let key = "gitoxide.http.proxy"; + debug_assert_eq!(key, gitoxide::Http::PROXY.logical_name()); + config + .string_filter_by_key(key, &mut trusted_only) + .map(|v| (v, cow_bstr(key), &gitoxide::Http::PROXY)) + }); + if url.scheme == Https { + http_proxy.or_else(|| { + let key = "gitoxide.https.proxy"; + debug_assert_eq!(key, gitoxide::Https::PROXY.logical_name()); + config + .string_filter_by_key(key, &mut trusted_only) + .map(|v| (v, cow_bstr(key), &gitoxide::Https::PROXY)) + }) + } else { + http_proxy + } + }) + .or_else(|| { + let key = "gitoxide.http.allProxy"; + debug_assert_eq!(key, gitoxide::Http::ALL_PROXY.logical_name()); + config + .string_filter_by_key(key, &mut trusted_only) + .map(|v| (v, cow_bstr(key), &gitoxide::Http::ALL_PROXY)) + }), + lenient, + )?; + { + let key = "gitoxide.http.noProxy"; + debug_assert_eq!(key, gitoxide::Http::NO_PROXY.logical_name()); + opts.no_proxy = config + .string_filter_by_key(key, &mut trusted_only) + .and_then(|v| { + try_cow_to_string(v, lenient, Cow::Borrowed(key.into()), &gitoxide::Http::NO_PROXY) + .transpose() + }) + .transpose()?; + } + opts.proxy_auth_method = proxy_auth_method({ + let key = "gitoxide.http.proxyAuthMethod"; + debug_assert_eq!(key, gitoxide::Http::PROXY_AUTH_METHOD.logical_name()); + config + .string_filter_by_key(key, &mut trusted_only) + .map(|v| (v, Cow::Borrowed(key.into()), &gitoxide::Http::PROXY_AUTH_METHOD)) + .or_else(|| { + remote_name + .and_then(|name| { + config + .string_filter("remote", Some(name), "proxyAuthMethod", &mut trusted_only) + .map(|v| { + ( + v, + Cow::Owned(format!("remote.{name}.proxyAuthMethod").into()), + &Remote::PROXY_AUTH_METHOD, + ) + }) + }) + .or_else(|| { + let key = "http.proxyAuthMethod"; + debug_assert_eq!(key, config::tree::Http::PROXY_AUTH_METHOD.logical_name()); + config.string_filter_by_key(key, &mut trusted_only).map(|v| { + (v, Cow::Borrowed(key.into()), &config::tree::Http::PROXY_AUTH_METHOD) + }) + }) + }) + })?; + opts.proxy_authenticate = opts + .proxy + .as_deref() + .filter(|url| !url.is_empty()) + .map(|url| gix_url::parse(url.into())) + .transpose()? + .filter(|url| url.user().is_some()) + .map(|url| -> Result<_, config::transport::http::Error> { + let (mut cascade, action_with_normalized_url, prompt_opts) = + self.config_snapshot().credential_helpers(url)?; + Ok(( + action_with_normalized_url, + Arc::new(Mutex::new(move |action| cascade.invoke(action, prompt_opts.clone()))) + as Arc>, + )) + }) + .transpose()?; + opts.connect_timeout = { + let key = "gitoxide.http.connectTimeout"; + config + .integer_filter_by_key(key, &mut trusted_only) + .map(|v| { + debug_assert_eq!(key, gitoxide::Http::CONNECT_TIMEOUT.logical_name()); + gitoxide::Http::CONNECT_TIMEOUT + .try_into_duration(v) + .map_err(crate::config::transport::http::Error::from) + }) + .transpose() + .with_leniency(lenient)? + }; + { + let key = "http.userAgent"; + opts.user_agent = config + .string_filter_by_key(key, &mut trusted_only) + .and_then(|v| { + try_cow_to_string( + v, + lenient, + Cow::Borrowed(key.into()), + &config::tree::Http::USER_AGENT, + ) + .transpose() + }) + .transpose()? + .or_else(|| Some(crate::env::agent().into())); + } + + { + let key = "http.version"; + opts.http_version = config + .string_filter_by_key(key, &mut trusted_only) + .map(|v| { + config::tree::Http::VERSION + .try_into_http_version(v) + .map_err(config::transport::http::Error::InvalidHttpVersion) + }) + .transpose()?; + } + + { + opts.verbose = config + .boolean_filter( + "gitoxide", + Some("http".into()), + gitoxide::Http::VERBOSE.name, + &mut trusted_only, + ) + .and_then(Result::ok) + .unwrap_or_default(); + } + + let may_use_cainfo = { + let key = "http.schannelUseSSLCAInfo"; + config + .boolean_filter_by_key(key, &mut trusted_only) + .map(|value| config::tree::Http::SCHANNEL_USE_SSL_CA_INFO.enrich_error(value)) + .transpose() + .with_leniency(lenient) + .map_err(config::transport::http::Error::from)? + .unwrap_or(true) + }; + + if may_use_cainfo { + let key = "http.sslCAInfo"; + debug_assert_eq!(key, config::tree::Http::SSL_CA_INFO.logical_name()); + opts.ssl_ca_info = config + .path_filter_by_key(key, &mut trusted_only) + .map(|p| { + use crate::config::cache::interpolate_context; + p.interpolate(interpolate_context( + self.install_dir().ok().as_deref(), + self.config.home_dir().as_deref(), + )) + .map(|cow| cow.into_owned()) + }) + .transpose() + .with_leniency(lenient) + .map_err(|err| config::transport::Error::InterpolatePath { source: err, key })?; + } + + { + opts.ssl_version = ssl_version( + config, + "http.sslVersion", + &config::tree::Http::SSL_VERSION, + trusted_only, + lenient, + )? + .map(|v| SslVersionRangeInclusive { min: v, max: v }); + let min_max = ssl_version( + config, + "gitoxide.http.sslVersionMin", + &gitoxide::Http::SSL_VERSION_MIN, + trusted_only, + lenient, + ) + .and_then(|min| { + ssl_version( + config, + "gitoxide.http.sslVersionMax", + &gitoxide::Http::SSL_VERSION_MAX, + trusted_only, + lenient, + ) + .map(|max| min.and_then(|min| max.map(|max| (min, max)))) + })?; + if let Some((min, max)) = min_max { + let v = opts.ssl_version.get_or_insert(SslVersionRangeInclusive { + min: SslVersion::TlsV1_3, + max: SslVersion::TlsV1_3, + }); + v.min = min; + v.max = max; + } + } + + #[cfg(feature = "blocking-http-transport-curl")] + { + let key = "http.schannelCheckRevoke"; + let schannel_check_revoke = config + .boolean_filter_by_key(key, &mut trusted_only) + .map(|value| config::tree::Http::SCHANNEL_CHECK_REVOKE.enrich_error(value)) + .transpose() + .with_leniency(lenient) + .map_err(config::transport::http::Error::from)?; + let backend = gix_protocol::transport::client::http::curl::Options { schannel_check_revoke }; + opts.backend = + Some(Arc::new(Mutex::new(backend)) as Arc>); + } + + Ok(Some(Box::new(opts))) + } + } + File | Git | Ssh | Ext(_) => Ok(None), + } + } +} diff --git a/vendor/gix/src/repository/identity.rs b/vendor/gix/src/repository/identity.rs new file mode 100644 index 000000000..61a4b4a98 --- /dev/null +++ b/vendor/gix/src/repository/identity.rs @@ -0,0 +1,175 @@ +use std::time::SystemTime; + +use crate::{ + bstr::BString, + config, + config::tree::{gitoxide, keys, Author, Committer, Key, User}, +}; + +/// Identity handling. +/// +/// # Deviation +/// +/// There is no notion of a default user like in git, and instead failing to provide a user +/// is fatal. That way, we enforce correctness and force application developers to take care +/// of this issue which can be done in various ways, for instance by setting +/// `gitoxide.committer.nameFallback` and similar. +impl crate::Repository { + /// Return the committer as configured by this repository, which is determined by… + /// + /// * …the git configuration `committer.name|email`… + /// * …the `GIT_COMMITTER_(NAME|EMAIL|DATE)` environment variables… + /// * …the configuration for `user.name|email` as fallback… + /// + /// …and in that order, or `None` if no committer name or email was configured, or `Some(Err(…))` + /// if the committer date could not be parsed. + /// + /// # Note + /// + /// The values are cached when the repository is instantiated. + pub fn committer(&self) -> Option, config::time::Error>> { + let p = self.config.personas(); + + Ok(gix_actor::SignatureRef { + name: p.committer.name.as_ref().or(p.user.name.as_ref()).map(|v| v.as_ref())?, + email: p + .committer + .email + .as_ref() + .or(p.user.email.as_ref()) + .map(|v| v.as_ref())?, + time: match extract_time_or_default(p.committer.time.as_ref(), &gitoxide::Commit::COMMITTER_DATE) { + Ok(t) => t, + Err(err) => return Some(Err(err)), + }, + }) + .into() + } + + /// Return the author as configured by this repository, which is determined by… + /// + /// * …the git configuration `author.name|email`… + /// * …the `GIT_AUTHOR_(NAME|EMAIL|DATE)` environment variables… + /// * …the configuration for `user.name|email` as fallback… + /// + /// …and in that order, or `None` if there was nothing configured. + /// + /// # Note + /// + /// The values are cached when the repository is instantiated. + pub fn author(&self) -> Option, config::time::Error>> { + let p = self.config.personas(); + + Ok(gix_actor::SignatureRef { + name: p.author.name.as_ref().or(p.user.name.as_ref()).map(|v| v.as_ref())?, + email: p.author.email.as_ref().or(p.user.email.as_ref()).map(|v| v.as_ref())?, + time: match extract_time_or_default(p.author.time.as_ref(), &gitoxide::Commit::AUTHOR_DATE) { + Ok(t) => t, + Err(err) => return Some(Err(err)), + }, + }) + .into() + } +} + +fn extract_time_or_default( + time: Option<&Result>, + config_key: &'static keys::Time, +) -> Result { + match time { + Some(Ok(t)) => Ok(*t), + None => Ok(gix_date::Time::now_local_or_utc()), + Some(Err(err)) => Err(config::time::Error::from(config_key).with_source(err.clone())), + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Entity { + pub name: Option, + pub email: Option, + /// A time parsed from an environment variable, handling potential errors is delayed. + pub time: Option>, +} + +#[derive(Debug, Clone)] +pub(crate) struct Personas { + user: Entity, + committer: Entity, + author: Entity, +} + +impl Personas { + pub fn from_config_and_env(config: &gix_config::File<'_>) -> Self { + fn entity_in_section( + config: &gix_config::File<'_>, + name_key: &keys::Any, + email_key: &keys::Any, + fallback: Option<(&keys::Any, &keys::Any)>, + ) -> (Option, Option) { + let fallback = fallback.and_then(|(name_key, email_key)| { + debug_assert_eq!(name_key.section.name(), email_key.section.name()); + config + .section("gitoxide", Some(name_key.section.name().into())) + .ok() + .map(|section| (section, name_key, email_key)) + }); + ( + config + .string(name_key.section.name(), None, name_key.name) + .or_else(|| fallback.as_ref().and_then(|(s, name_key, _)| s.value(name_key.name))) + .map(|v| v.into_owned()), + config + .string(email_key.section.name(), None, email_key.name) + .or_else(|| fallback.as_ref().and_then(|(s, _, email_key)| s.value(email_key.name))) + .map(|v| v.into_owned()), + ) + } + let now = SystemTime::now(); + let parse_date = |key: &str, date: &keys::Time| -> Option> { + debug_assert_eq!( + key, + date.logical_name(), + "BUG: drift of expected name and actual name of the key (we hardcode it to save an allocation)" + ); + config + .string_by_key(key) + .map(|time| date.try_into_time(time, now.into())) + }; + + let fallback = ( + &gitoxide::Committer::NAME_FALLBACK, + &gitoxide::Committer::EMAIL_FALLBACK, + ); + let (committer_name, committer_email) = + entity_in_section(config, &Committer::NAME, &Committer::EMAIL, Some(fallback)); + let fallback = (&gitoxide::Author::NAME_FALLBACK, &gitoxide::Author::EMAIL_FALLBACK); + let (author_name, author_email) = entity_in_section(config, &Author::NAME, &Author::EMAIL, Some(fallback)); + let (user_name, mut user_email) = entity_in_section(config, &User::NAME, &User::EMAIL, None); + + let committer_date = parse_date("gitoxide.commit.committerDate", &gitoxide::Commit::COMMITTER_DATE); + let author_date = parse_date("gitoxide.commit.authorDate", &gitoxide::Commit::AUTHOR_DATE); + + user_email = user_email.or_else(|| { + config + .string_by_key(gitoxide::User::EMAIL_FALLBACK.logical_name().as_str()) + .map(|v| v.into_owned()) + }); + Personas { + user: Entity { + name: user_name, + email: user_email, + time: None, + }, + committer: Entity { + name: committer_name, + email: committer_email, + time: committer_date, + }, + author: Entity { + name: author_name, + email: author_email, + time: author_date, + }, + } + } +} diff --git a/vendor/gix/src/repository/impls.rs b/vendor/gix/src/repository/impls.rs new file mode 100644 index 000000000..6cf2b2e9b --- /dev/null +++ b/vendor/gix/src/repository/impls.rs @@ -0,0 +1,73 @@ +impl Clone for crate::Repository { + fn clone(&self) -> Self { + crate::Repository::from_refs_and_objects( + self.refs.clone(), + self.objects.clone(), + self.work_tree.clone(), + self.common_dir.clone(), + self.config.clone(), + self.options.clone(), + self.index.clone(), + ) + } +} + +impl std::fmt::Debug for crate::Repository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Repository") + .field("kind", &self.kind()) + .field("git_dir", &self.git_dir()) + .field("work_dir", &self.work_dir()) + .finish() + } +} + +impl PartialEq for crate::Repository { + fn eq(&self, other: &crate::Repository) -> bool { + self.git_dir().canonicalize().ok() == other.git_dir().canonicalize().ok() + && self.work_tree.as_deref().and_then(|wt| wt.canonicalize().ok()) + == other.work_tree.as_deref().and_then(|wt| wt.canonicalize().ok()) + } +} + +impl From<&crate::ThreadSafeRepository> for crate::Repository { + fn from(repo: &crate::ThreadSafeRepository) -> Self { + crate::Repository::from_refs_and_objects( + repo.refs.clone(), + repo.objects.to_handle().into(), + repo.work_tree.clone(), + repo.common_dir.clone(), + repo.config.clone(), + repo.linked_worktree_options.clone(), + repo.index.clone(), + ) + } +} + +impl From for crate::Repository { + fn from(repo: crate::ThreadSafeRepository) -> Self { + crate::Repository::from_refs_and_objects( + repo.refs, + repo.objects.to_handle().into(), + repo.work_tree, + repo.common_dir, + repo.config, + repo.linked_worktree_options, + repo.index, + ) + } +} + +impl From for crate::ThreadSafeRepository { + fn from(r: crate::Repository) -> Self { + crate::ThreadSafeRepository { + refs: r.refs, + objects: r.objects.into_inner().store(), + work_tree: r.work_tree, + common_dir: r.common_dir, + config: r.config, + linked_worktree_options: r.options, + index: r.index, + } + } +} diff --git a/vendor/gix/src/repository/init.rs b/vendor/gix/src/repository/init.rs new file mode 100644 index 000000000..ae6a42c3b --- /dev/null +++ b/vendor/gix/src/repository/init.rs @@ -0,0 +1,55 @@ +use std::cell::RefCell; + +impl crate::Repository { + pub(crate) fn from_refs_and_objects( + refs: crate::RefStore, + objects: crate::OdbHandle, + work_tree: Option, + common_dir: Option, + config: crate::config::Cache, + linked_worktree_options: crate::open::Options, + index: crate::worktree::IndexStorage, + ) -> Self { + let objects = setup_objects(objects, &config); + crate::Repository { + bufs: RefCell::new(Vec::with_capacity(4)), + work_tree, + common_dir, + objects, + refs, + config, + options: linked_worktree_options, + index, + } + } + + /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data. + pub fn into_sync(self) -> crate::ThreadSafeRepository { + self.into() + } +} + +#[cfg_attr(not(feature = "max-performance-safe"), allow(unused_variables, unused_mut))] +fn setup_objects(mut objects: crate::OdbHandle, config: &crate::config::Cache) -> crate::OdbHandle { + #[cfg(feature = "max-performance-safe")] + { + match config.pack_cache_bytes { + None => objects.set_pack_cache(|| Box::>::default()), + Some(0) => objects.unset_pack_cache(), + Some(bytes) => objects.set_pack_cache(move || -> Box { + Box::new(gix_pack::cache::lru::MemoryCappedHashmap::new(bytes)) + }), + }; + if config.object_cache_bytes == 0 { + objects.unset_object_cache(); + } else { + let bytes = config.object_cache_bytes; + objects.set_object_cache(move || Box::new(gix_pack::cache::object::MemoryCappedHashmap::new(bytes))); + } + objects + } + #[cfg(not(feature = "max-performance-safe"))] + { + objects + } +} diff --git a/vendor/gix/src/repository/location.rs b/vendor/gix/src/repository/location.rs new file mode 100644 index 000000000..0bb8ea253 --- /dev/null +++ b/vendor/gix/src/repository/location.rs @@ -0,0 +1,86 @@ +use std::path::PathBuf; + +use gix_path::realpath::MAX_SYMLINKS; + +impl crate::Repository { + /// Return the path to the repository itself, containing objects, references, configuration, and more. + /// + /// Synonymous to [`path()`][crate::Repository::path()]. + pub fn git_dir(&self) -> &std::path::Path { + self.refs.git_dir() + } + + /// The trust we place in the git-dir, with lower amounts of trust causing access to configuration to be limited. + pub fn git_dir_trust(&self) -> gix_sec::Trust { + self.options.git_dir_trust.expect("definitely set by now") + } + + /// Returns the main git repository if this is a repository on a linked work-tree, or the `git_dir` itself. + pub fn common_dir(&self) -> &std::path::Path { + self.common_dir.as_deref().unwrap_or_else(|| self.git_dir()) + } + + /// Return the path to the worktree index file, which may or may not exist. + pub fn index_path(&self) -> PathBuf { + self.git_dir().join("index") + } + + /// The path to the `.git` directory itself, or equivalent if this is a bare repository. + pub fn path(&self) -> &std::path::Path { + self.git_dir() + } + + /// Return the work tree containing all checked out files, if there is one. + pub fn work_dir(&self) -> Option<&std::path::Path> { + self.work_tree.as_deref() + } + + // TODO: tests, respect precomposeUnicode + /// The directory of the binary path of the current process. + pub fn install_dir(&self) -> std::io::Result { + crate::path::install_dir() + } + + /// Returns the relative path which is the components between the working tree and the current working dir (CWD). + /// Note that there may be `None` if there is no work tree, even though the `PathBuf` will be empty + /// if the CWD is at the root of the work tree. + // TODO: tests, details - there is a lot about environment variables to change things around. + pub fn prefix(&self) -> Option> { + self.work_tree.as_ref().map(|root| { + std::env::current_dir().and_then(|cwd| { + gix_path::realpath_opts(root, &cwd, MAX_SYMLINKS) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) + .and_then(|root| { + cwd.strip_prefix(&root) + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "CWD '{}' isn't within the work tree '{}'", + cwd.display(), + root.display() + ), + ) + }) + .map(ToOwned::to_owned) + }) + }) + }) + } + + /// Return the kind of repository, either bare or one with a work tree. + pub fn kind(&self) -> crate::Kind { + match self.worktree() { + Some(wt) => { + if gix_discover::is_submodule_git_dir(self.git_dir()) { + crate::Kind::Submodule + } else { + crate::Kind::WorkTree { + is_linked: !wt.is_main(), + } + } + } + None => crate::Kind::Bare, + } + } +} diff --git a/vendor/gix/src/repository/mod.rs b/vendor/gix/src/repository/mod.rs new file mode 100644 index 000000000..31199e22d --- /dev/null +++ b/vendor/gix/src/repository/mod.rs @@ -0,0 +1,36 @@ +//! + +/// Internal +impl crate::Repository { + #[inline] + pub(crate) fn free_buf(&self) -> Vec { + self.bufs.borrow_mut().pop().unwrap_or_default() + } + + /// This method is commonly called from the destructor of objects that previously claimed an entry + /// in the free-list with `free_buf()`. + /// They are welcome to take out the data themselves, for instance when the object is detached, to avoid + /// it to be reclaimed. + #[inline] + pub(crate) fn reuse_buffer(&self, data: &mut Vec) { + if data.capacity() > 0 { + self.bufs.borrow_mut().push(std::mem::take(data)); + } + } +} + +mod cache; +mod config; +pub(crate) mod identity; +mod impls; +mod init; +mod location; +mod object; +pub(crate) mod permissions; +mod reference; +mod remote; +mod revision; +mod snapshots; +mod state; +mod thread_safe; +mod worktree; diff --git a/vendor/gix/src/repository/object.rs b/vendor/gix/src/repository/object.rs new file mode 100644 index 000000000..bda1a54c3 --- /dev/null +++ b/vendor/gix/src/repository/object.rs @@ -0,0 +1,214 @@ +#![allow(clippy::result_large_err)] +use std::convert::TryInto; + +use gix_hash::ObjectId; +use gix_odb::{Find, FindExt, Write}; +use gix_ref::{ + transaction::{LogChange, PreviousValue, RefLog}, + FullName, +}; + +use crate::{commit, ext::ObjectIdExt, object, tag, Id, Object, Reference, Tree}; + +/// Methods related to object creation. +impl crate::Repository { + /// Find the object with `id` in the object database or return an error if it could not be found. + /// + /// There are various legitimate reasons for an object to not be present, which is why + /// [`try_find_object(…)`][crate::Repository::try_find_object()] might be preferable instead. + /// + /// # Performance Note + /// + /// In order to get the kind of the object, is must be fully decoded from storage if it is packed with deltas. + /// Loose object could be partially decoded, even though that's not implemented. + pub fn find_object(&self, id: impl Into) -> Result, object::find::existing::Error> { + let id = id.into(); + if id == gix_hash::ObjectId::empty_tree(self.object_hash()) { + return Ok(Object { + id, + kind: gix_object::Kind::Tree, + data: Vec::new(), + repo: self, + }); + } + let mut buf = self.free_buf(); + let kind = self.objects.find(id, &mut buf)?.kind; + Ok(Object::from_data(id, kind, buf, self)) + } + + /// Try to find the object with `id` or return `None` it it wasn't found. + pub fn try_find_object(&self, id: impl Into) -> Result>, object::find::Error> { + let id = id.into(); + if id == gix_hash::ObjectId::empty_tree(self.object_hash()) { + return Ok(Some(Object { + id, + kind: gix_object::Kind::Tree, + data: Vec::new(), + repo: self, + })); + } + + let mut buf = self.free_buf(); + match self.objects.try_find(id, &mut buf)? { + Some(obj) => { + let kind = obj.kind; + Ok(Some(Object::from_data(id, kind, buf, self))) + } + None => Ok(None), + } + } + + /// Write the given object into the object database and return its object id. + pub fn write_object(&self, object: impl gix_object::WriteTo) -> Result, object::write::Error> { + self.objects + .write(object) + .map(|oid| oid.attach(self)) + .map_err(Into::into) + } + + /// Write a blob from the given `bytes`. + pub fn write_blob(&self, bytes: impl AsRef<[u8]>) -> Result, object::write::Error> { + self.objects + .write_buf(gix_object::Kind::Blob, bytes.as_ref()) + .map(|oid| oid.attach(self)) + } + + /// Write a blob from the given `Read` implementation. + pub fn write_blob_stream( + &self, + mut bytes: impl std::io::Read + std::io::Seek, + ) -> Result, object::write::Error> { + let current = bytes.stream_position()?; + let len = bytes.seek(std::io::SeekFrom::End(0))? - current; + bytes.seek(std::io::SeekFrom::Start(current))?; + + self.objects + .write_stream(gix_object::Kind::Blob, len, bytes) + .map(|oid| oid.attach(self)) + } + + /// Create a tag reference named `name` (without `refs/tags/` prefix) pointing to a newly created tag object + /// which in turn points to `target` and return the newly created reference. + /// + /// It will be created with `constraint` which is most commonly to [only create it][PreviousValue::MustNotExist] + /// or to [force overwriting a possibly existing tag](PreviousValue::Any). + pub fn tag( + &self, + name: impl AsRef, + target: impl AsRef, + target_kind: gix_object::Kind, + tagger: Option>, + message: impl AsRef, + constraint: PreviousValue, + ) -> Result, tag::Error> { + let tag = gix_object::Tag { + target: target.as_ref().into(), + target_kind, + name: name.as_ref().into(), + tagger: tagger.map(|t| t.to_owned()), + message: message.as_ref().into(), + pgp_signature: None, + }; + let tag_id = self.write_object(&tag)?; + self.tag_reference(name, tag_id, constraint).map_err(Into::into) + } + + /// Similar to [`commit(…)`][crate::Repository::commit()], but allows to create the commit with `committer` and `author` specified. + /// + /// This forces setting the commit time and author time by hand. Note that typically, committer and author are the same. + pub fn commit_as<'a, 'c, Name, E>( + &self, + committer: impl Into>, + author: impl Into>, + reference: Name, + message: impl AsRef, + tree: impl Into, + parents: impl IntoIterator>, + ) -> Result, commit::Error> + where + Name: TryInto, + commit::Error: From, + { + use gix_ref::{ + transaction::{Change, RefEdit}, + Target, + }; + + // 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 commit = gix_object::Commit { + message: message.as_ref().into(), + tree: tree.into(), + author: author.into().to_owned(), + committer: committer.into().to_owned(), + encoding: None, + parents: parents.into_iter().map(|id| id.into()).collect(), + extra_headers: Default::default(), + }; + + 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) + } + } + None => PreviousValue::MustNotExist, + }, + new: Target::Peeled(commit_id.inner), + }, + name: reference, + deref: true, + })?; + Ok(commit_id) + } + + /// 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 + /// already. The reflog will be written as canonical git would do, like ` (): `. + /// + /// The first parent id in `parents` is expected to be the current target of `reference` and the operation will fail if it is not. + /// If there is no parent, the `reference` is expected to not exist yet. + /// + /// The method fails immediately if a `reference` lock can't be acquired. + pub fn commit( + &self, + reference: Name, + message: impl AsRef, + tree: impl Into, + parents: impl IntoIterator>, + ) -> Result, commit::Error> + where + Name: TryInto, + commit::Error: From, + { + let author = self.author().ok_or(commit::Error::AuthorMissing)??; + let committer = self.committer().ok_or(commit::Error::CommitterMissing)??; + self.commit_as(committer, author, reference, message, tree, parents) + } + + /// Return an empty tree object, suitable for [getting changes](crate::Tree::changes()). + /// + /// Note that it is special and doesn't physically exist in the object database even though it can be returned. + /// This means that this object can be used in an uninitialized, empty repository which would report to have no objects at all. + pub fn empty_tree(&self) -> Tree<'_> { + self.find_object(gix_hash::ObjectId::empty_tree(self.object_hash())) + .expect("always present") + .into_tree() + } +} diff --git a/vendor/gix/src/repository/permissions.rs b/vendor/gix/src/repository/permissions.rs new file mode 100644 index 000000000..88b61b739 --- /dev/null +++ b/vendor/gix/src/repository/permissions.rs @@ -0,0 +1,168 @@ +use gix_sec::Trust; + +/// Permissions associated with various resources of a git repository +#[derive(Debug, Clone)] +pub struct Permissions { + /// Permissions related to the environment + pub env: Environment, + /// Permissions related to the handling of git configuration. + pub config: Config, +} + +/// Configure from which sources git configuration may be loaded. +/// +/// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness. +#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] +pub struct Config { + /// The git binary may come with configuration as part of its configuration, and if this is true (default false) + /// we will load the configuration of the git binary, if present and not a duplicate of the ones below. + /// + /// It's disable by default as it involves executing the git binary once per execution of the application. + pub git_binary: bool, + /// Whether to use the system configuration. + /// This is defined as `$(prefix)/etc/gitconfig` on unix. + pub system: bool, + /// Whether to use the git application configuration. + /// + /// A platform defined location for where a user's git application configuration should be located. + /// If `$XDG_CONFIG_HOME` is not set or empty, `$HOME/.config/git/config` will be used + /// on unix. + pub git: bool, + /// Whether to use the user configuration. + /// This is usually `~/.gitconfig` on unix. + pub user: bool, + /// Whether to use the configuration from environment variables. + pub env: bool, + /// Whether to follow include files are encountered in loaded configuration, + /// via `include` and `includeIf` sections. + pub includes: bool, +} + +impl Config { + /// Allow everything which usually relates to a fully trusted environment + pub fn all() -> Self { + Config { + git_binary: false, + system: true, + git: true, + user: true, + env: true, + includes: true, + } + } +} + +impl Default for Config { + fn default() -> Self { + Self::all() + } +} + +/// Permissions related to the usage of environment variables +#[derive(Debug, Clone)] +pub struct Environment { + /// Control whether resources pointed to by `XDG_CONFIG_HOME` can be used when looking up common configuration values. + /// + /// Note that [`gix_sec::Permission::Forbid`] will cause the operation to abort if a resource is set via the XDG config environment. + pub xdg_config_home: gix_sec::Permission, + /// Control the way resources pointed to by the home directory (similar to `xdg_config_home`) may be used. + pub home: gix_sec::Permission, + /// Control if environment variables to configure the HTTP transport, like `http_proxy` may be used. + /// + /// Note that http-transport related environment variables prefixed with `GIT_` may also be included here + /// if they match this category like `GIT_HTTP_USER_AGENT`. + pub http_transport: gix_sec::Permission, + /// Control if the `EMAIL` environment variables may be read. + /// + /// Note that identity related environment variables prefixed with `GIT_` may also be included here + /// if they match this category. + pub identity: gix_sec::Permission, + /// Control if environment variables related to the object database are handled. This includes features and performance + /// options alike. + pub objects: gix_sec::Permission, + /// Control if resources pointed to by `GIT_*` prefixed environment variables can be used, **but only** if they + /// are not contained in any other category. This is a catch-all section. + pub git_prefix: gix_sec::Permission, + /// Control if resources pointed to by `SSH_*` prefixed environment variables can be used (like `SSH_ASKPASS`) + pub ssh_prefix: gix_sec::Permission, +} + +impl Environment { + /// Allow access to the entire environment. + pub fn all() -> Self { + let allow = gix_sec::Permission::Allow; + Environment { + xdg_config_home: allow, + home: allow, + git_prefix: allow, + ssh_prefix: allow, + http_transport: allow, + identity: allow, + objects: allow, + } + } +} + +impl Permissions { + /// Return permissions that will not include configuration files not owned by the current user, + /// but trust system and global configuration files along with those which are owned by the current user. + /// + /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using + /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`. + pub fn secure() -> Self { + Permissions { + env: Environment::all(), + config: Config::all(), + } + } + + /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically + /// does with owned repositories. + pub fn all() -> Self { + Permissions { + env: Environment::all(), + config: Config::all(), + } + } + + /// Don't read any but the local git configuration and deny reading any environment variables. + pub fn isolated() -> Self { + Permissions { + config: Config { + git_binary: false, + system: false, + git: false, + user: false, + env: false, + includes: false, + }, + env: { + let deny = gix_sec::Permission::Deny; + Environment { + xdg_config_home: deny, + home: deny, + ssh_prefix: deny, + git_prefix: deny, + http_transport: deny, + identity: deny, + objects: deny, + } + }, + } + } +} + +impl gix_sec::trust::DefaultForLevel for Permissions { + fn default_for_level(level: Trust) -> Self { + match level { + Trust::Full => Permissions::all(), + Trust::Reduced => Permissions::secure(), + } + } +} + +impl Default for Permissions { + fn default() -> Self { + Permissions::secure() + } +} diff --git a/vendor/gix/src/repository/reference.rs b/vendor/gix/src/repository/reference.rs new file mode 100644 index 000000000..e5a8aadcb --- /dev/null +++ b/vendor/gix/src/repository/reference.rs @@ -0,0 +1,243 @@ +use std::convert::TryInto; + +use gix_hash::ObjectId; +use gix_ref::{ + transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, + FullName, PartialNameRef, Target, +}; + +use crate::{bstr::BString, ext::ReferenceExt, reference, Reference}; + +/// 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. + /// + /// It will be created with `constraint` which is most commonly to [only create it][PreviousValue::MustNotExist] + /// or to [force overwriting a possibly existing tag](PreviousValue::Any). + pub fn tag_reference( + &self, + name: impl AsRef, + target: impl Into, + 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, + })?; + assert_eq!(edits.len(), 1, "reference splits should ever happen"); + let edit = edits.pop().expect("exactly one item"); + Ok(Reference { + inner: gix_ref::Reference { + name: edit.name, + target: id.into(), + peeled: None, + }, + repo: self, + }) + } + + /// Returns the currently set namespace for references, or `None` if it is not set. + /// + /// Namespaces allow to partition references, and is configured per `Easy`. + pub fn namespace(&self) -> Option<&gix_ref::Namespace> { + self.refs.namespace.as_ref() + } + + /// Remove the currently set reference namespace and return it, affecting only this `Easy`. + pub fn clear_namespace(&mut self) -> Option { + self.refs.namespace.take() + } + + /// Set the reference namespace to the given value, like `"foo"` or `"foo/bar"`. + /// + /// Note that this value is shared across all `Easy…` instances as the value is stored in the shared `Repository`. + pub fn set_namespace<'a, Name, E>( + &mut self, + namespace: Name, + ) -> Result, gix_validate::refname::Error> + where + Name: TryInto<&'a PartialNameRef, Error = E>, + gix_validate::refname::Error: From, + { + let namespace = gix_ref::namespace::expand(namespace)?; + Ok(self.refs.namespace.replace(namespace)) + } + + // TODO: more tests or usage + /// Create a new reference with `name`, like `refs/heads/branch`, pointing to `target`, adhering to `constraint` + /// during creation and writing `log_message` into the reflog. Note that a ref-log will be written even if `log_message` is empty. + /// + /// The newly created Reference is returned. + pub fn reference( + &self, + name: Name, + target: impl Into, + constraint: PreviousValue, + log_message: impl Into, + ) -> Result, reference::edit::Error> + where + Name: TryInto, + gix_validate::reference::name::Error: From, + { + let name = name.try_into().map_err(gix_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), + }, + name, + deref: false, + })?; + assert_eq!( + edits.len(), + 1, + "only one reference can be created, splits aren't possible" + ); + + Ok(gix_ref::Reference { + name: edits.pop().expect("exactly one edit").name, + target: Target::Peeled(id), + peeled: None, + } + .attach(self)) + } + + /// 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) -> Result, reference::edit::Error> { + self.edit_references(Some(edit)) + } + + /// 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 gix-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, + ) -> Result, reference::edit::Error> { + let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout()?; + self.refs + .transaction() + .prepare(edits, file_lock_fail, packed_refs_lock_fail)? + .commit(self.committer().transpose()?) + .map_err(Into::into) + } + + /// Return the repository head, an abstraction to help dealing with the `HEAD` reference. + /// + /// The `HEAD` reference can be in various states, for more information, the documentation of [`Head`][crate::Head]. + pub fn head(&self) -> Result, reference::find::existing::Error> { + let head = self.find_reference("HEAD")?; + Ok(match head.inner.target { + Target::Symbolic(branch) => match self.find_reference(&branch) { + Ok(r) => crate::head::Kind::Symbolic(r.detach()), + Err(reference::find::existing::Error::NotFound) => crate::head::Kind::Unborn(branch), + Err(err) => return Err(err), + }, + Target::Peeled(target) => crate::head::Kind::Detached { + target, + peeled: head.inner.peeled, + }, + } + .attach(self)) + } + + /// Resolve the `HEAD` reference, follow and peel its target and obtain its object id. + /// + /// Note that this may fail for various reasons, most notably because the repository + /// is freshly initialized and doesn't have any commits yet. + /// + /// Also note that the returned id is likely to point to a commit, but could also + /// point to a tree or blob. It won't, however, point to a tag as these are always peeled. + pub fn head_id(&self) -> Result, reference::head_id::Error> { + let mut head = self.head()?; + head.peel_to_id_in_place() + .ok_or_else(|| reference::head_id::Error::Unborn { + name: head.referent_name().expect("unborn").to_owned(), + })? + .map_err(Into::into) + } + + /// Return the name to the symbolic reference `HEAD` points to, or `None` if the head is detached. + /// + /// The difference to [`head_ref()`][Self::head_ref()] is that the latter requires the reference to exist, + /// whereas here we merely return a the name of the possibly unborn reference. + pub fn head_name(&self) -> Result, reference::find::existing::Error> { + Ok(self.head()?.referent_name().map(|n| n.to_owned())) + } + + /// Return the reference that `HEAD` points to, or `None` if the head is detached or unborn. + pub fn head_ref(&self) -> Result>, reference::find::existing::Error> { + Ok(self.head()?.try_into_referent()) + } + + /// Return the commit object the `HEAD` reference currently points to after peeling it fully. + /// + /// Note that this may fail for various reasons, most notably because the repository + /// is freshly initialized and doesn't have any commits yet. It could also fail if the + /// head does not point to a commit. + pub fn head_commit(&self) -> Result, reference::head_commit::Error> { + Ok(self.head()?.peel_to_commit_in_place()?) + } + + /// Find the reference with the given partial or full `name`, like `main`, `HEAD`, `heads/branch` or `origin/other`, + /// or return an error if it wasn't found. + /// + /// Consider [`try_find_reference(…)`][crate::Repository::try_find_reference()] if the reference might not exist + /// without that being considered an error. + pub fn find_reference<'a, Name, E>(&self, name: Name) -> Result, reference::find::existing::Error> + where + Name: TryInto<&'a PartialNameRef, Error = E>, + gix_ref::file::find::Error: From, + { + self.try_find_reference(name)? + .ok_or(reference::find::existing::Error::NotFound) + } + + /// Return a platform for iterating references. + /// + /// Common kinds of iteration are [all][crate::reference::iter::Platform::all()] or [prefixed][crate::reference::iter::Platform::prefixed()] + /// references. + pub fn references(&self) -> Result, reference::iter::Error> { + Ok(reference::iter::Platform { + platform: self.refs.iter()?, + repo: self, + }) + } + + /// Try to find the reference named `name`, like `main`, `heads/branch`, `HEAD` or `origin/other`, and return it. + /// + /// Otherwise return `None` if the reference wasn't found. + /// If the reference is expected to exist, use [`find_reference()`][crate::Repository::find_reference()]. + pub fn try_find_reference<'a, Name, E>(&self, name: Name) -> Result>, reference::find::Error> + where + Name: TryInto<&'a PartialNameRef, Error = E>, + gix_ref::file::find::Error: From, + { + let state = self; + match state.refs.try_find(name) { + Ok(r) => match r { + Some(r) => Ok(Some(Reference::from_ref(r, self))), + None => Ok(None), + }, + Err(err) => Err(err.into()), + } + } +} diff --git a/vendor/gix/src/repository/remote.rs b/vendor/gix/src/repository/remote.rs new file mode 100644 index 000000000..e3f210899 --- /dev/null +++ b/vendor/gix/src/repository/remote.rs @@ -0,0 +1,199 @@ +#![allow(clippy::result_large_err)] +use std::convert::TryInto; + +use crate::{bstr::BStr, config, remote, remote::find, Remote}; + +impl crate::Repository { + /// Create a new remote available at the given `url`. + /// + /// It's configured to fetch included tags by default, similar to git. + /// See [`with_fetch_tags(…)`][Remote::with_fetch_tags()] for a way to change it. + pub fn remote_at(&self, url: Url) -> Result, remote::init::Error> + where + Url: TryInto, + gix_url::parse::Error: From, + { + Remote::from_fetch_url(url, true, self) + } + + /// Create a new remote available at the given `url` similarly to [`remote_at()`][crate::Repository::remote_at()], + /// but don't rewrite the url according to rewrite rules. + /// This eliminates a failure mode in case the rewritten URL is faulty, allowing to selectively [apply rewrite + /// rules][Remote::rewrite_urls()] later and do so non-destructively. + pub fn remote_at_without_url_rewrite(&self, url: Url) -> Result, remote::init::Error> + where + Url: TryInto, + gix_url::parse::Error: From, + { + Remote::from_fetch_url(url, false, self) + } + + /// Find the remote with the given `name_or_url` or report an error, similar to [`try_find_remote(…)`][Self::try_find_remote()]. + /// + /// Note that we will obtain remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()]. + pub fn find_remote<'a>(&self, name_or_url: impl Into<&'a BStr>) -> Result, find::existing::Error> { + let name_or_url = name_or_url.into(); + Ok(self + .try_find_remote(name_or_url) + .ok_or_else(|| find::existing::Error::NotFound { + name: name_or_url.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_url` or return `None` if it doesn't exist, for the purpose of fetching or pushing + /// data to a remote. + /// + /// There are various error kinds related to partial information or incorrectly formatted URLs or ref-specs. + /// Also note that the created `Remote` may have neither fetch nor push ref-specs set at all. + /// + /// Note that ref-specs are de-duplicated right away which may change their order. This doesn't affect matching in any way + /// as negations/excludes are applied after includes. + /// + /// We will only include information if we deem it [trustworthy][crate::open::Options::filter_config_section()]. + pub fn try_find_remote<'a>(&self, name_or_url: impl Into<&'a BStr>) -> Option, find::Error>> { + self.try_find_remote_inner(name_or_url, true) + } + + /// Similar to [try_find_remote()][Self::try_find_remote()], but removes a failure mode if rewritten URLs turn out to be invalid + /// as it skips rewriting them. + /// Use this in conjunction with [`Remote::rewrite_urls()`] to non-destructively apply the rules and keep the failed urls unchanged. + pub fn try_find_remote_without_url_rewrite<'a>( + &self, + name_or_url: impl Into<&'a BStr>, + ) -> Option, find::Error>> { + self.try_find_remote_inner(name_or_url, false) + } + + fn try_find_remote_inner<'a>( + &self, + name_or_url: impl Into<&'a BStr>, + rewrite_urls: bool, + ) -> Option, find::Error>> { + fn config_spec( + specs: Vec>, + name_or_url: &BStr, + key: &'static config::tree::keys::Any, + op: gix_refspec::parse::Operation, + ) -> Result, find::Error> { + let kind = key.name; + specs + .into_iter() + .map(|spec| { + key.try_into_refspec(spec, op).map_err(|err| find::Error::RefSpec { + remote_name: name_or_url.into(), + kind, + source: err, + }) + }) + .collect::, _>>() + .map(|mut specs| { + specs.sort(); + specs.dedup(); + specs + }) + } + + let mut filter = self.filter_config_section(); + let name_or_url = name_or_url.into(); + let mut config_url = |key: &'static config::tree::keys::Url, kind: &'static str| { + self.config + .resolved + .string_filter("remote", Some(name_or_url), key.name, &mut filter) + .map(|url| { + key.try_into_url(url).map_err(|err| find::Error::Url { + kind, + remote_name: name_or_url.into(), + source: err, + }) + }) + }; + let url = config_url(&config::tree::Remote::URL, "fetch"); + let push_url = config_url(&config::tree::Remote::PUSH_URL, "push"); + let config = &self.config.resolved; + + let fetch_specs = config + .strings_filter("remote", Some(name_or_url), "fetch", &mut filter) + .map(|specs| { + config_spec( + specs, + name_or_url, + &config::tree::Remote::FETCH, + gix_refspec::parse::Operation::Fetch, + ) + }); + let push_specs = config + .strings_filter("remote", Some(name_or_url), "push", &mut filter) + .map(|specs| { + config_spec( + specs, + name_or_url, + &config::tree::Remote::PUSH, + gix_refspec::parse::Operation::Push, + ) + }); + let fetch_tags = config + .string_filter("remote", Some(name_or_url), "tagOpt", &mut filter) + .map(|value| { + config::tree::Remote::TAG_OPT + .try_into_tag_opt(value) + .map_err(Into::into) + }); + let fetch_tags = match fetch_tags { + Some(Ok(v)) => v, + Some(Err(err)) => return Some(Err(err)), + None => Default::default(), + }; + + match (url, fetch_specs, push_url, push_specs) { + (None, None, None, None) => None, + (None, _, None, _) => Some(Err(find::Error::UrlMissing)), + (url, fetch_specs, push_url, push_specs) => { + let url = match url { + Some(Ok(v)) => Some(v), + Some(Err(err)) => return Some(Err(err)), + None => None, + }; + let push_url = match push_url { + Some(Ok(v)) => Some(v), + Some(Err(err)) => return Some(Err(err)), + None => None, + }; + let fetch_specs = match fetch_specs { + Some(Ok(v)) => v, + Some(Err(err)) => return Some(Err(err)), + None => Vec::new(), + }; + let push_specs = match push_specs { + Some(Ok(v)) => v, + Some(Err(err)) => return Some(Err(err)), + None => Vec::new(), + }; + + Some( + Remote::from_preparsed_config( + Some(name_or_url.to_owned()), + url, + push_url, + fetch_specs, + push_specs, + rewrite_urls, + fetch_tags, + self, + ) + .map_err(Into::into), + ) + } + } + } +} diff --git a/vendor/gix/src/repository/revision.rs b/vendor/gix/src/repository/revision.rs new file mode 100644 index 000000000..3018c2be8 --- /dev/null +++ b/vendor/gix/src/repository/revision.rs @@ -0,0 +1,42 @@ +use crate::{bstr::BStr, revision, Id}; + +/// Methods for resolving revisions by spec or working with the commit graph. +impl crate::Repository { + /// Parse a revision specification and turn it into the object(s) it describes, similar to `git rev-parse`. + /// + /// # Deviation + /// + /// - `@` actually stands for `HEAD`, whereas `git` resolves it to the object pointed to by `HEAD` without making the + /// `HEAD` ref available for lookups. + pub fn rev_parse<'a>(&self, spec: impl Into<&'a BStr>) -> Result, revision::spec::parse::Error> { + revision::Spec::from_bstr( + spec, + self, + revision::spec::parse::Options { + object_kind_hint: self.config.object_kind_hint, + ..Default::default() + }, + ) + } + + /// Parse a revision specification and return single object id as represented by this instance. + pub fn rev_parse_single<'repo, 'a>( + &'repo self, + spec: impl Into<&'a BStr>, + ) -> Result, revision::spec::parse::single::Error> { + let spec = spec.into(); + self.rev_parse(spec)? + .single() + .ok_or(revision::spec::parse::single::Error::RangedRev { spec: spec.into() }) + } + + /// Create the baseline for a revision walk by initializing it with the `tips` to start iterating on. + /// + /// It can be configured further before starting the actual walk. + pub fn rev_walk( + &self, + tips: impl IntoIterator>, + ) -> revision::walk::Platform<'_> { + revision::walk::Platform::new(tips, self) + } +} diff --git a/vendor/gix/src/repository/snapshots.rs b/vendor/gix/src/repository/snapshots.rs new file mode 100644 index 000000000..6933dc9c6 --- /dev/null +++ b/vendor/gix/src/repository/snapshots.rs @@ -0,0 +1,109 @@ +impl crate::Repository { + // TODO: tests + /// Similar to [`open_mailmap_into()`][crate::Repository::open_mailmap_into()], but ignores all errors and returns at worst + /// an empty mailmap, e.g. if there is no mailmap or if there were errors loading them. + /// + /// This represents typical usage within git, which also works with what's there without considering a populated mailmap + /// a reason to abort an operation, considering it optional. + pub fn open_mailmap(&self) -> gix_mailmap::Snapshot { + let mut out = gix_mailmap::Snapshot::default(); + self.open_mailmap_into(&mut out).ok(); + out + } + + // TODO: tests + /// Try to merge mailmaps from the following locations into `target`: + /// + /// - read the `.mailmap` file without following symlinks from the working tree, if present + /// - OR read `HEAD:.mailmap` if this repository is bare (i.e. has no working tree), if the `mailmap.blob` is not set. + /// - read the mailmap as configured in `mailmap.blob`, if set. + /// - read the file as configured by `mailmap.file`, following symlinks, if set. + /// + /// Only the first error will be reported, and as many source mailmaps will be merged into `target` as possible. + /// Parsing errors will be ignored. + pub fn open_mailmap_into(&self, target: &mut gix_mailmap::Snapshot) -> Result<(), crate::mailmap::load::Error> { + let mut err = None::; + let mut buf = Vec::new(); + let mut blob_id = self + .config + .resolved + .raw_value("mailmap", None, "blob") + .ok() + .and_then(|spec| { + // TODO: actually resolve this as spec (once we can do that) + gix_hash::ObjectId::from_hex(spec.as_ref()) + .map_err(|e| err.get_or_insert(e.into())) + .ok() + }); + match self.work_dir() { + None => { + // TODO: replace with ref-spec `HEAD:.mailmap` for less verbose way of getting the blob id + blob_id = blob_id.or_else(|| { + self.head().ok().and_then(|mut head| { + let commit = head.peel_to_commit_in_place().ok()?; + let tree = commit.tree().ok()?; + tree.lookup_entry(Some(".mailmap")).ok()?.map(|e| e.object_id()) + }) + }); + } + Some(root) => { + if let Ok(mut file) = gix_features::fs::open_options_no_follow() + .read(true) + .open(root.join(".mailmap")) + .map_err(|e| { + if e.kind() != std::io::ErrorKind::NotFound { + err.get_or_insert(e.into()); + } + }) + { + buf.clear(); + std::io::copy(&mut file, &mut buf) + .map_err(|e| err.get_or_insert(e.into())) + .ok(); + target.merge(gix_mailmap::parse_ignore_errors(&buf)); + } + } + } + + if let Some(blob) = blob_id.and_then(|id| self.find_object(id).map_err(|e| err.get_or_insert(e.into())).ok()) { + target.merge(gix_mailmap::parse_ignore_errors(&blob.data)); + } + + let configured_path = self + .config + .resolved + .value::>("mailmap", None, "file") + .ok() + .and_then(|path| { + let install_dir = self.install_dir().ok()?; + let home = self.config.home_dir(); + match path.interpolate(gix_config::path::interpolate::Context { + git_install_dir: Some(install_dir.as_path()), + home_dir: home.as_deref(), + home_for_user: if self.options.git_dir_trust.expect("trust is set") == gix_sec::Trust::Full { + Some(gix_config::path::interpolate::home_for_user) + } else { + None + }, + }) { + Ok(path) => Some(path), + Err(e) => { + err.get_or_insert(e.into()); + None + } + } + }); + + if let Some(mut file) = + configured_path.and_then(|path| std::fs::File::open(path).map_err(|e| err.get_or_insert(e.into())).ok()) + { + buf.clear(); + std::io::copy(&mut file, &mut buf) + .map_err(|e| err.get_or_insert(e.into())) + .ok(); + target.merge(gix_mailmap::parse_ignore_errors(&buf)); + } + + err.map(Err).unwrap_or(Ok(())) + } +} diff --git a/vendor/gix/src/repository/state.rs b/vendor/gix/src/repository/state.rs new file mode 100644 index 000000000..4034fe349 --- /dev/null +++ b/vendor/gix/src/repository/state.rs @@ -0,0 +1,44 @@ +use crate::state; + +impl crate::Repository { + /// Returns the status of an in progress operation on a repository or [`None`] + /// if no operation is currently in progress. + /// + /// Note to be confused with the repositories 'status'. + pub fn state(&self) -> Option { + let git_dir = self.path(); + + // This is modeled on the logic from wt_status_get_state in git's wt-status.c and + // ps1 from gix-prompt.sh. + + if git_dir.join("rebase-apply/applying").is_file() { + Some(state::InProgress::ApplyMailbox) + } else if git_dir.join("rebase-apply/rebasing").is_file() { + Some(state::InProgress::Rebase) + } else if git_dir.join("rebase-apply").is_dir() { + Some(state::InProgress::ApplyMailboxRebase) + } else if git_dir.join("rebase-merge/interactive").is_file() { + Some(state::InProgress::RebaseInteractive) + } else if git_dir.join("rebase-merge").is_dir() { + Some(state::InProgress::Rebase) + } else if git_dir.join("CHERRY_PICK_HEAD").is_file() { + if git_dir.join("sequencer/todo").is_file() { + Some(state::InProgress::CherryPickSequence) + } else { + Some(state::InProgress::CherryPick) + } + } else if git_dir.join("MERGE_HEAD").is_file() { + Some(state::InProgress::Merge) + } else if git_dir.join("BISECT_LOG").is_file() { + Some(state::InProgress::Bisect) + } else if git_dir.join("REVERT_HEAD").is_file() { + if git_dir.join("sequencer/todo").is_file() { + Some(state::InProgress::RevertSequence) + } else { + Some(state::InProgress::Revert) + } + } else { + None + } + } +} diff --git a/vendor/gix/src/repository/thread_safe.rs b/vendor/gix/src/repository/thread_safe.rs new file mode 100644 index 000000000..7c89aee60 --- /dev/null +++ b/vendor/gix/src/repository/thread_safe.rs @@ -0,0 +1,66 @@ +mod access { + use crate::Kind; + + impl crate::ThreadSafeRepository { + /// Return the kind of repository, either bare or one with a work tree. + pub fn kind(&self) -> Kind { + match self.work_tree { + Some(_) => Kind::WorkTree { + is_linked: crate::worktree::id(self.git_dir(), self.common_dir.is_some()).is_some(), + }, + None => Kind::Bare, + } + } + + /// Add thread-local state to an easy-to-use thread-local repository for the most convenient API. + pub fn to_thread_local(&self) -> crate::Repository { + self.into() + } + } +} + +mod location { + + impl crate::ThreadSafeRepository { + /// The path to the `.git` directory itself, or equivalent if this is a bare repository. + pub fn path(&self) -> &std::path::Path { + self.git_dir() + } + + /// Return the path to the repository itself, containing objects, references, configuration, and more. + /// + /// Synonymous to [`path()`][crate::ThreadSafeRepository::path()]. + pub fn git_dir(&self) -> &std::path::Path { + self.refs.git_dir() + } + + /// Return the path to the working directory if this is not a bare repository. + pub fn work_dir(&self) -> Option<&std::path::Path> { + self.work_tree.as_deref() + } + + /// Return the path to the directory containing all objects. + pub fn objects_dir(&self) -> &std::path::Path { + self.objects.path() + } + } +} + +mod impls { + impl std::fmt::Debug for crate::ThreadSafeRepository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Repository(git = '{}', working_tree: {:?}", + self.git_dir().display(), + self.work_tree + ) + } + } + + impl PartialEq for crate::ThreadSafeRepository { + fn eq(&self, other: &crate::ThreadSafeRepository) -> bool { + self.git_dir() == other.git_dir() && self.work_tree == other.work_tree + } + } +} diff --git a/vendor/gix/src/repository/worktree.rs b/vendor/gix/src/repository/worktree.rs new file mode 100644 index 000000000..2de31bc86 --- /dev/null +++ b/vendor/gix/src/repository/worktree.rs @@ -0,0 +1,119 @@ +use crate::{worktree, Worktree}; + +/// Worktree iteration +impl crate::Repository { + /// Return a list of all _linked_ worktrees sorted by private git dir path as a lightweight proxy. + /// + /// Note that these need additional processing to become usable, but provide a first glimpse a typical worktree information. + pub fn worktrees(&self) -> std::io::Result>> { + let mut res = Vec::new(); + let iter = match std::fs::read_dir(self.common_dir().join("worktrees")) { + Ok(iter) => iter, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(res), + Err(err) => return Err(err), + }; + for entry in iter { + let entry = entry?; + let worktree_git_dir = entry.path(); + if worktree_git_dir.join("gitdir").is_file() { + res.push(worktree::Proxy { + parent: self, + git_dir: worktree_git_dir, + }) + } + } + res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir)); + Ok(res) + } +} + +/// Interact with individual worktrees and their information. +impl crate::Repository { + /// 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. + #[allow(clippy::result_large_err)] + pub fn main_repo(&self) -> Result { + crate::ThreadSafeRepository::open_opts(self.common_dir(), self.options.clone()).map(Into::into) + } + + /// Return the currently set worktree if there is one, acting as platform providing a validated worktree base path. + /// + /// Note that there would be `None` if this repository is `bare` and the parent [`Repository`][crate::Repository] was instantiated without + /// registered worktree in the current working dir. + pub fn worktree(&self) -> Option> { + self.work_dir().map(|path| Worktree { parent: self, path }) + } + + /// Return true if this repository is bare, and has no main work tree. + /// + /// This is not to be confused with the [`worktree()`][crate::Repository::worktree()] worktree, which may exists if this instance + /// was opened in a worktree that was created separately. + pub fn is_bare(&self) -> bool { + self.config.is_bare && self.work_dir().is_none() + } + + /// Open a new copy of the index file and decode it entirely. + /// + /// It will use the `index.threads` configuration key to learn how many threads to use. + /// Note that it may fail if there is no index. + // TODO: test + pub fn open_index(&self) -> Result { + let thread_limit = self + .config + .resolved + .boolean("index", None, "threads") + .map(|res| { + res.map(|value| usize::from(!value)).or_else(|err| { + gix_config::Integer::try_from(err.input.as_ref()) + .map_err(|err| worktree::open_index::Error::ConfigIndexThreads { + value: err.input.clone(), + err, + }) + .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1)) + }) + }) + .transpose()?; + gix_index::File::at( + self.index_path(), + self.object_hash(), + gix_index::decode::Options { + thread_limit, + min_extension_block_in_bytes_for_threading: 0, + expected_checksum: None, + }, + ) + .map_err(Into::into) + } + + /// Return a shared worktree index which is updated automatically if the in-memory snapshot has become stale as the underlying file + /// on disk has changed. + /// + /// The index file is shared across all clones of this repository. + pub fn index(&self) -> Result { + self.index + .recent_snapshot( + || self.index_path().metadata().and_then(|m| m.modified()).ok(), + || { + self.open_index().map(Some).or_else(|err| match err { + worktree::open_index::Error::IndexFile(gix_index::file::init::Error::Io(err)) + if err.kind() == std::io::ErrorKind::NotFound => + { + Ok(None) + } + err => Err(err), + }) + }, + ) + .and_then(|opt| match opt { + Some(index) => Ok(index), + None => Err(worktree::open_index::Error::IndexFile( + gix_index::file::init::Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Could not find index file at {:?} for opening.", self.index_path()), + )), + )), + }) + } +} -- cgit v1.2.3