diff --git a/Cargo.lock b/Cargo.lock index 5dd5cefad16..cf92d12399e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1188,6 +1188,7 @@ dependencies = [ "git-testtools", "memchr", "nom", + "once_cell", "serde", "serde_derive", "serial_test 0.7.0", diff --git a/README.md b/README.md index d064cc3d831..d2fc0167c0c 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Please see _'Development Status'_ for a listing of all crates and their capabili * [x] **previous-branches** - list all previously checked out branches, powered by the ref-log. * **remote** * [x] **refs** - list all references available on the remote based on the current remote configuration. + * [x] **ref-map** - show how remote references relate to their local tracking branches as mapped by refspecs. * **credential** * [x] **fill/approve/reject** - The same as `git credential`, but implemented in Rust, calling helpers only when from trusted configuration. * **free** - no git repository necessary @@ -142,6 +143,7 @@ is usable to some extend. * [git-revision](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-revision) * [git-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-command) * [git-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-prompt) + * [git-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-refspec) * `gitoxide-core` * **very early** _(possibly without any documentation and many rough edges)_ * [git-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-worktree) diff --git a/crate-status.md b/crate-status.md index 78f282293a5..cc79ffe9b2f 100644 --- a/crate-status.md +++ b/crate-status.md @@ -14,7 +14,6 @@ ### git-object * *decode (zero-copy)* borrowed objects * [x] commit - * [x] parse the title, body, and provide a title summary. * [ ] parse [trailers](https://git-scm.com/docs/git-interpret-trailers#_description) * [x] tree * encode owned objects diff --git a/git-config/Cargo.toml b/git-config/Cargo.toml index a5afbd107bf..685503dd7ec 100644 --- a/git-config/Cargo.toml +++ b/git-config/Cargo.toml @@ -29,6 +29,7 @@ unicode-bom = "1.1.4" bstr = { version = "1.0.1", default-features = false, features = ["std"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} smallvec = "1.9.0" +once_cell = "1.14.0" document-features = { version = "0.2.0", optional = true } diff --git a/git-config/src/source.rs b/git-config/src/source.rs index 1943e455679..7a21dc04a0a 100644 --- a/git-config/src/source.rs +++ b/git-config/src/source.rs @@ -9,6 +9,8 @@ use crate::Source; /// The category of a [`Source`], in order of ascending precedence. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] pub enum Kind { + /// A special configuration file that ships with the git installation, and is thus tied to the used git binary. + GitInstallation, /// A source shared for the entire system. System, /// Application specific configuration unique for each user of the `System`. @@ -23,7 +25,8 @@ impl Kind { /// Return a list of sources associated with this `Kind` of source, in order of ascending precedence. pub fn sources(self) -> &'static [Source] { let src = match self { - Kind::System => &[Source::System] as &[_], + Kind::GitInstallation => &[Source::GitInstallation] as &[_], + Kind::System => &[Source::System], Kind::Global => &[Source::Git, Source::User], Kind::Repository => &[Source::Local, Source::Worktree], Kind::Override => &[Source::Env, Source::Cli, Source::Api], @@ -41,6 +44,7 @@ impl Source { pub const fn kind(self) -> Kind { use Source::*; match self { + GitInstallation => Kind::GitInstallation, System => Kind::System, Git | User => Kind::Global, Local | Worktree => Kind::Repository, @@ -61,6 +65,7 @@ impl Source { pub fn storage_location(self, env_var: &mut dyn FnMut(&str) -> Option) -> Option> { use Source::*; match self { + GitInstallation => git::install_config_path().map(git_path::from_bstr), System => env_var("GIT_CONFIG_NO_SYSTEM") .is_none() .then(|| PathBuf::from(env_var("GIT_CONFIG_SYSTEM").unwrap_or_else(|| "/etc/gitconfig".into())).into()), @@ -99,3 +104,59 @@ impl Source { } } } + +/// Environment information involving the `git` program itself. +mod git { + use bstr::{BStr, BString, ByteSlice}; + use std::process::{Command, Stdio}; + + /// Returns the file that contains git configuration coming with the installation of the `git` file in the current `PATH`, or `None` + /// if no `git` executable was found or there were other errors during execution. + pub fn install_config_path() -> Option<&'static BStr> { + static PATH: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { + let mut cmd = Command::new(if cfg!(windows) { "git.exe" } else { "git" }); + cmd.args(["config", "-l", "--show-origin"]) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + first_file_from_config_with_origin(cmd.output().ok()?.stdout.as_slice().into()).map(ToOwned::to_owned) + }); + PATH.as_ref().map(|b| b.as_ref()) + } + + fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> { + let file = source.strip_prefix(b"file:")?; + let end_pos = file.find_byte(b'\t')?; + file[..end_pos].as_bstr().into() + } + + #[cfg(test)] + mod tests { + #[test] + fn first_file_from_config_with_origin() { + let macos = "file:/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig credential.helper=osxkeychain\nfile:/Users/byron/.gitconfig push.default=simple\n"; + let win_msys = + "file:C:/git-sdk-64/etc/gitconfig core.symlinks=false\r\nfile:C:/git-sdk-64/etc/gitconfig core.autocrlf=true"; + let win_cmd = "file:C:/Program Files/Git/etc/gitconfig diff.astextplain.textconv=astextplain\r\nfile:C:/Program Files/Git/etc/gitconfig filter.lfs.clean=git-lfs clean -- %f\r\n"; + let linux = "file:/home/parallels/.gitconfig core.excludesfile=~/.gitignore\n"; + let bogus = "something unexpected"; + let empty = ""; + + for (source, expected) in [ + ( + macos, + Some("/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig"), + ), + (win_msys, Some("C:/git-sdk-64/etc/gitconfig")), + (win_cmd, Some("C:/Program Files/Git/etc/gitconfig")), + (linux, Some("/home/parallels/.gitconfig")), + (bogus, None), + (empty, None), + ] { + assert_eq!( + super::first_file_from_config_with_origin(source.into()), + expected.map(Into::into) + ); + } + } + } +} diff --git a/git-config/src/types.rs b/git-config/src/types.rs index e96ea44813e..f5a75653159 100644 --- a/git-config/src/types.rs +++ b/git-config/src/types.rs @@ -15,6 +15,8 @@ use crate::{ /// their source. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] pub enum Source { + /// A special configuration file that ships with the git installation, and is thus tied to the used git binary. + GitInstallation, /// System-wide configuration path. This is defined as /// `$(prefix)/etc/gitconfig` (where prefix is the git-installation directory). System, diff --git a/git-protocol/src/fetch/handshake.rs b/git-protocol/src/fetch/handshake.rs index 292094cfa48..78d71095339 100644 --- a/git-protocol/src/fetch/handshake.rs +++ b/git-protocol/src/fetch/handshake.rs @@ -3,6 +3,8 @@ use git_transport::client::Capabilities; use crate::fetch::Ref; /// The result of the [`handshake()`][super::handshake()] function. +#[derive(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. pub server_protocol_version: git_transport::Protocol, diff --git a/git-protocol/src/fetch/refs/mod.rs b/git-protocol/src/fetch/refs/mod.rs index a847d6107a4..ba3fafd2471 100644 --- a/git-protocol/src/fetch/refs/mod.rs +++ b/git-protocol/src/fetch/refs/mod.rs @@ -1,4 +1,4 @@ -use bstr::BString; +use bstr::{BStr, BString}; mod error { use crate::fetch::refs::parse; @@ -50,8 +50,8 @@ 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 path at which the ref is located, like `/refs/heads/main`. - path: BString, + /// The name at which the ref is located, like `refs/heads/main`. + full_ref_name: BString, /// The hash of the tag the ref points to. tag: git_hash::ObjectId, /// The hash of the object the `tag` points to. @@ -59,15 +59,15 @@ pub enum Ref { }, /// A ref pointing to a commit object Direct { - /// The path at which the ref is located, like `/refs/heads/main`. - path: BString, + /// The name at which the ref is located, like `refs/heads/main`. + full_ref_name: BString, /// The hash of the object the ref points to. object: git_hash::ObjectId, }, /// A symbolic ref pointing to `target` ref, which in turn points to an `object` Symbolic { - /// The path at which the symbolic ref is located, like `/refs/heads/main`. - path: BString, + /// The name at which the symbolic ref is located, like `refs/heads/main`. + full_ref_name: BString, /// The path of the ref the symbolic ref points to, see issue [#205] for details /// /// [#205]: https://github.com/Byron/gitoxide/issues/205 @@ -78,13 +78,20 @@ pub enum Ref { } impl Ref { - /// Provide shared fields referring to the ref itself, namely `(path, object id)`. - /// In case of peeled refs, the tag object itself is returned as it is what the path refers to. - pub fn unpack(&self) -> (&BString, &git_hash::ObjectId) { + /// Provide shared fields referring to the ref itself, namely `(name, target, [peeled])`. + /// In case of peeled refs, the tag object itself is returned as it is what the ref directly refers to, and target of the tag is returned + /// as `peeled`. + pub fn unpack(&self) -> (&BStr, &git_hash::oid, Option<&git_hash::oid>) { match self { - Ref::Direct { path, object, .. } - | Ref::Peeled { path, tag: object, .. } // the tag acts as reference - | Ref::Symbolic { path, object, .. } => (path, object), + Ref::Direct { full_ref_name, object } + | Ref::Symbolic { + full_ref_name, object, .. + } => (full_ref_name.as_ref(), object, None), + Ref::Peeled { + full_ref_name, + tag: object, + object: peeled, + } => (full_ref_name.as_ref(), object, Some(peeled)), } } } diff --git a/git-protocol/src/fetch/refs/shared.rs b/git-protocol/src/fetch/refs/shared.rs index 46a8ad53620..87e0541d49a 100644 --- a/git-protocol/src/fetch/refs/shared.rs +++ b/git-protocol/src/fetch/refs/shared.rs @@ -9,14 +9,28 @@ impl From for Ref { path, target: Some(target), object, - } => Ref::Symbolic { path, target, object }, + } => Ref::Symbolic { + full_ref_name: path, + target, + object, + }, InternalRef::Symbolic { path, target: None, object, - } => Ref::Direct { path, object }, - InternalRef::Peeled { path, tag, object } => Ref::Peeled { path, tag, object }, - InternalRef::Direct { path, object } => Ref::Direct { path, object }, + } => Ref::Direct { + full_ref_name: path, + object, + }, + InternalRef::Peeled { path, tag, object } => Ref::Peeled { + full_ref_name: path, + tag, + object, + }, + InternalRef::Direct { path, object } => Ref::Direct { + full_ref_name: path, + object, + }, InternalRef::SymbolicForLookup { .. } => { unreachable!("this case should have been removed during processing") } @@ -170,17 +184,17 @@ pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { } match attribute { "peeled" => Ref::Peeled { - path: path.into(), + full_ref_name: path.into(), object: git_hash::ObjectId::from_hex(value.as_bytes())?, tag: id, }, "symref-target" => match value { "(null)" => Ref::Direct { - path: path.into(), + full_ref_name: path.into(), object: id, }, name => Ref::Symbolic { - path: path.into(), + full_ref_name: path.into(), object: id, target: name.into(), }, @@ -198,7 +212,7 @@ pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { } else { Ref::Direct { object: id, - path: path.into(), + full_ref_name: path.into(), } }) } diff --git a/git-protocol/src/fetch/tests/refs.rs b/git-protocol/src/fetch/tests/refs.rs index a07d75f325e..e6ca670ba59 100644 --- a/git-protocol/src/fetch/tests/refs.rs +++ b/git-protocol/src/fetch/tests/refs.rs @@ -19,25 +19,25 @@ async fn extract_references_from_v2_refs() { out, vec![ Ref::Symbolic { - path: "HEAD".into(), + full_ref_name: "HEAD".into(), target: "refs/heads/main".into(), object: oid("808e50d724f604f69ab93c6da2919c014667bedb") }, Ref::Direct { - path: "MISSING_NAMESPACE_TARGET".into(), + full_ref_name: "MISSING_NAMESPACE_TARGET".into(), object: oid("808e50d724f604f69ab93c6da2919c014667bedb") }, Ref::Direct { - path: "refs/heads/main".into(), + full_ref_name: "refs/heads/main".into(), object: oid("808e50d724f604f69ab93c6da2919c014667bedb") }, Ref::Peeled { - path: "refs/tags/foo".into(), + full_ref_name: "refs/tags/foo".into(), tag: oid("7fe1b98b39423b71e14217aa299a03b7c937d656"), object: oid("808e50d724f604f69ab93c6da2919c014667bedb") }, Ref::Direct { - path: "refs/tags/blaz".into(), + full_ref_name: "refs/tags/blaz".into(), object: oid("7fe1b98b39423b71e14217aa299a03b7c937d6ff") }, ] @@ -66,24 +66,24 @@ dce0ea858eef7ff61ad345cc5cdac62203fb3c10 refs/tags/git-commitgraph-v0.0.0 out, vec![ Ref::Symbolic { - path: "HEAD".into(), + full_ref_name: "HEAD".into(), target: "refs/heads/main".into(), object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944") }, Ref::Direct { - path: "MISSING_NAMESPACE_TARGET".into(), + full_ref_name: "MISSING_NAMESPACE_TARGET".into(), object: oid("21c9b7500cb144b3169a6537961ec2b9e865be81") }, Ref::Direct { - path: "refs/heads/main".into(), + full_ref_name: "refs/heads/main".into(), object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944") }, Ref::Direct { - path: "refs/pull/13/head".into(), + full_ref_name: "refs/pull/13/head".into(), object: oid("8e472f9ccc7d745927426cbb2d9d077de545aa4e") }, Ref::Peeled { - path: "refs/tags/git-commitgraph-v0.0.0".into(), + full_ref_name: "refs/tags/git-commitgraph-v0.0.0".into(), tag: oid("dce0ea858eef7ff61ad345cc5cdac62203fb3c10"), object: oid("21c9b7500cb144b3169a6537961ec2b9e865be81") }, diff --git a/git-protocol/tests/fetch/mod.rs b/git-protocol/tests/fetch/mod.rs index c5ff0e0da09..dfdcdbdf309 100644 --- a/git-protocol/tests/fetch/mod.rs +++ b/git-protocol/tests/fetch/mod.rs @@ -169,7 +169,7 @@ mod blocking_io { ) -> io::Result<()> { for wanted in response.wanted_refs() { self.wanted_refs.push(fetch::Ref::Direct { - path: wanted.path.clone(), + full_ref_name: wanted.path.clone(), object: wanted.id, }); } @@ -230,7 +230,7 @@ mod async_io { ) -> io::Result<()> { for wanted in response.wanted_refs() { self.wanted_refs.push(fetch::Ref::Direct { - path: wanted.path.clone(), + full_ref_name: wanted.path.clone(), object: wanted.id, }); } diff --git a/git-protocol/tests/fetch/v1.rs b/git-protocol/tests/fetch/v1.rs index 9088b9d3c99..2820ebab364 100644 --- a/git-protocol/tests/fetch/v1.rs +++ b/git-protocol/tests/fetch/v1.rs @@ -49,12 +49,12 @@ async fn ls_remote() -> crate::Result { delegate.refs, vec![ fetch::Ref::Symbolic { - path: "HEAD".into(), + full_ref_name: "HEAD".into(), object: oid("808e50d724f604f69ab93c6da2919c014667bedb"), target: "refs/heads/master".into() }, fetch::Ref::Direct { - path: "refs/heads/master".into(), + full_ref_name: "refs/heads/master".into(), object: oid("808e50d724f604f69ab93c6da2919c014667bedb") } ] diff --git a/git-protocol/tests/fetch/v2.rs b/git-protocol/tests/fetch/v2.rs index d4bf585447a..ba3bb1ca820 100644 --- a/git-protocol/tests/fetch/v2.rs +++ b/git-protocol/tests/fetch/v2.rs @@ -75,12 +75,12 @@ async fn ls_remote() -> crate::Result { delegate.refs, vec![ fetch::Ref::Symbolic { - path: "HEAD".into(), + full_ref_name: "HEAD".into(), object: oid("808e50d724f604f69ab93c6da2919c014667bedb"), target: "refs/heads/master".into() }, fetch::Ref::Direct { - path: "refs/heads/master".into(), + full_ref_name: "refs/heads/master".into(), object: oid("808e50d724f604f69ab93c6da2919c014667bedb") } ] @@ -167,7 +167,7 @@ async fn ref_in_want() -> crate::Result { assert_eq!( delegate.wanted_refs, vec![fetch::Ref::Direct { - path: "refs/heads/main".into(), + full_ref_name: "refs/heads/main".into(), object: oid("9e320b9180e0b5580af68fa3255b7f3d9ecd5af0"), }] ); diff --git a/git-ref/src/name.rs b/git-ref/src/name.rs index 4162a69fa31..edfe48e901a 100644 --- a/git-ref/src/name.rs +++ b/git-ref/src/name.rs @@ -168,6 +168,16 @@ mod impls { } } +impl<'a> convert::TryFrom<&'a BString> for &'a PartialNameRef { + type Error = Error; + + fn try_from(v: &'a BString) -> Result { + Ok(PartialNameRef::new_unchecked(git_validate::reference::name_partial( + v.as_ref(), + )?)) + } +} + impl<'a> convert::TryFrom<&'a BStr> for &'a PartialNameRef { type Error = Error; diff --git a/git-refspec/src/match_group/mod.rs b/git-refspec/src/match_group/mod.rs index 5b10ee32cc4..b8060bc74ef 100644 --- a/git-refspec/src/match_group/mod.rs +++ b/git-refspec/src/match_group/mod.rs @@ -88,7 +88,7 @@ impl<'a> MatchGroup<'a> { .matches_lhs(Item { full_ref_name: name, target: &null_id, - tag: None, + object: None, }) .0 } @@ -100,11 +100,6 @@ impl<'a> MatchGroup<'a> { mappings: out, } } - - /// Return the spec that produced the given `mapping`. - pub fn spec_by_mapping(&self, mapping: &Mapping<'_, '_>) -> RefSpecRef<'a> { - self.specs[mapping.spec_index] - } } fn calculate_hash(t: &T) -> u64 { diff --git a/git-refspec/src/match_group/types.rs b/git-refspec/src/match_group/types.rs index 61711252d9c..c664adc840d 100644 --- a/git-refspec/src/match_group/types.rs +++ b/git-refspec/src/match_group/types.rs @@ -4,9 +4,10 @@ use git_hash::oid; use std::borrow::Cow; /// A match group is able to match a list of ref specs in order while handling negation, conflicts and one to many mappings. -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct MatchGroup<'a> { - pub(crate) specs: Vec>, + /// The specs that take part in item matching. + pub specs: Vec>, } /// The outcome of any matching operation of a [`MatchGroup`]. @@ -25,10 +26,10 @@ pub struct Outcome<'spec, 'item> { pub struct Item<'a> { /// The full name of the references, like `refs/heads/main` pub full_ref_name: &'a BStr, - /// The peeled id it points to that we should match against. + /// The id that `full_ref_name` points to, which typically is a commit, but can also be a tag object (or anything else). pub target: &'a oid, - /// The tag object's id if this is a tag - pub tag: Option<&'a oid>, + /// The object an annotated tag is pointing to, if `target` is an annotated tag. + pub object: Option<&'a oid>, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -90,7 +91,7 @@ pub struct Mapping<'a, 'b> { /// The name of the local side for fetches or the remote one for pushes that corresponds to `lhs`, if available. pub rhs: Option>, /// The index of the matched ref-spec as seen from the match group. - pub(crate) spec_index: usize, + pub spec_index: usize, } impl std::hash::Hash for Mapping<'_, '_> { diff --git a/git-refspec/src/match_group/util.rs b/git-refspec/src/match_group/util.rs index 42a31c03acb..3aa73aaddfa 100644 --- a/git-refspec/src/match_group/util.rs +++ b/git-refspec/src/match_group/util.rs @@ -111,8 +111,8 @@ impl<'a> Needle<'a> { if *id == item.target { return Match::Normal; } - match item.tag { - Some(tag) if tag == *id => Match::Normal, + match item.object { + Some(object) if object == *id => Match::Normal, _ => Match::None, } } diff --git a/git-refspec/src/match_group/validate.rs b/git-refspec/src/match_group/validate.rs index 0443a47b3cf..4cd47b35b38 100644 --- a/git-refspec/src/match_group/validate.rs +++ b/git-refspec/src/match_group/validate.rs @@ -1,4 +1,5 @@ use crate::match_group::{Outcome, Source}; +use crate::RefSpecRef; use bstr::BString; use std::collections::BTreeMap; @@ -43,14 +44,14 @@ impl std::fmt::Display for Issue { } /// All possible fixes corrected while validating matched mappings. -#[derive(Debug, PartialEq, Eq)] -pub enum Fix { +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Fix<'a> { /// Removed a mapping that contained a partial destination entirely. MappingWithPartialDestinationRemoved { /// The destination ref name that was ignored. name: BString, /// The spec that defined the mapping - spec: BString, + spec: RefSpecRef<'a>, }, } @@ -86,7 +87,7 @@ impl<'spec, 'item> Outcome<'spec, 'item> { /// Return `(modified self, issues)` providing a fixed-up set of mappings in `self` with the fixed `issues` /// provided as part of it. /// Terminal issues are communicated using the [`Error`] type accordingly. - pub fn validated(mut self) -> Result<(Self, Vec), Error> { + pub fn validated(mut self) -> Result<(Self, Vec>), Error> { let mut sources_by_destinations = BTreeMap::new(); for (dst, (spec_index, src)) in self .mappings @@ -121,7 +122,7 @@ impl<'spec, 'item> Outcome<'spec, 'item> { } else { fixed.push(Fix::MappingWithPartialDestinationRemoved { name: dst.as_ref().to_owned(), - spec: group.specs[m.spec_index].to_bstring(), + spec: group.specs[m.spec_index], }); false } diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index fade345bb04..170d0b270bc 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -50,7 +50,7 @@ mod single { mod multiple { use crate::matching::baseline; use git_refspec::match_group::validate::Fix; - use git_refspec::parse::Error; + use git_refspec::parse::{Error, Operation}; #[test] fn fetch_only() { @@ -160,20 +160,21 @@ mod multiple { #[test] fn fetch_and_update_with_fixes() { let glob_spec = "refs/heads/f*:foo/f*"; + let glob_spec_ref = git_refspec::parse(glob_spec.into(), Operation::Fetch).unwrap(); baseline::agrees_and_applies_fixes( [glob_spec, "f1:f1"], [ Fix::MappingWithPartialDestinationRemoved { name: "foo/f1".into(), - spec: glob_spec.into(), + spec: glob_spec_ref, }, Fix::MappingWithPartialDestinationRemoved { name: "foo/f2".into(), - spec: glob_spec.into(), + spec: glob_spec_ref, }, Fix::MappingWithPartialDestinationRemoved { name: "foo/f3".into(), - spec: glob_spec.into(), + spec: glob_spec_ref, }, ], ["refs/heads/f1:refs/heads/f1"], diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index bf88bb68d63..a68ab3aa50f 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -18,8 +18,8 @@ pub mod baseline { pub struct Ref { pub name: BString, pub target: ObjectId, - /// Set if this is a tag, pointing to the tag object itself - pub tag: Option, + /// Set if `target` is an annotated tag, this being the object it points to. + pub object: Option, } impl Ref { @@ -27,7 +27,7 @@ pub mod baseline { git_refspec::match_group::Item { full_ref_name: self.name.borrow(), target: &self.target, - tag: self.tag.as_deref(), + object: self.object.as_deref(), } } } @@ -54,9 +54,9 @@ pub mod baseline { agrees_and_applies_fixes(specs, Vec::new(), expected) } - pub fn agrees_and_applies_fixes<'a, 'b>( + pub fn agrees_and_applies_fixes<'a, 'b, 'c>( specs: impl IntoIterator + Clone, - fixes: impl IntoIterator, + fixes: impl IntoIterator>, expected: impl IntoIterator, ) { check_fetch_remote( @@ -125,9 +125,14 @@ pub mod baseline { of_objects_with_destinations_are_written_into_given_local_branches(specs, expected) } - enum Mode { - Normal { validate_err: Option }, - Custom { expected: Vec, fixes: Vec }, + enum Mode<'a> { + Normal { + validate_err: Option, + }, + Custom { + expected: Vec, + fixes: Vec>, + }, } fn check_fetch_remote<'a>(specs: impl IntoIterator + Clone, mode: Mode) { @@ -216,13 +221,10 @@ pub mod baseline { out.push(Ref { name: name.into(), target, - tag: None, + object: None, }) } else { - let last = out.last_mut().unwrap(); - let tag = last.target; - last.target = target; - last.tag = Some(tag); + out.last_mut().unwrap().object = Some(target); } } Ok(out) diff --git a/git-repository/src/config/cache/incubate.rs b/git-repository/src/config/cache/incubate.rs index 24ff91bcd88..f8048a535ab 100644 --- a/git-repository/src/config/cache/incubate.rs +++ b/git-repository/src/config/cache/incubate.rs @@ -2,7 +2,6 @@ use super::{util, Error}; /// A utility to deal with the cyclic dependency between the ref store and the configuration. The ref-store needs the /// object hash kind, and the configuration needs the current branch name to resolve conditional includes with `onbranch`. -#[allow(dead_code)] pub(crate) struct StageOne { pub git_dir_config: git_config::File<'static>, pub buf: Vec, diff --git a/git-repository/src/config/cache/init.rs b/git-repository/src/config/cache/init.rs index 4e5ab68203c..4b5c87fd31a 100644 --- a/git-repository/src/config/cache/init.rs +++ b/git-repository/src/config/cache/init.rs @@ -55,42 +55,39 @@ impl Cache { let home_env = &home_env; let xdg_config_home_env = &xdg_config_home_env; let git_prefix = &git_prefix; - let mut install_path = use_installation.then(crate::env::git::install_config_path).flatten(); - let metas = [git_config::source::Kind::System, git_config::source::Kind::Global] - .iter() - .flat_map(|kind| kind.sources()) - .filter_map(|source| match install_path.take() { - Some(install_path) => ( - &git_config::Source::System, - git_path::from_bstr(install_path).into_owned(), - ) - .into(), - None => { - match source { - git_config::Source::System if !use_system => return None, - git_config::Source::Git if !use_git => return None, - git_config::Source::User if !use_user => return None, - _ => {} + let metas = [ + git_config::source::Kind::GitInstallation, + git_config::source::Kind::System, + git_config::source::Kind::Global, + ] + .iter() + .flat_map(|kind| kind.sources()) + .filter_map(|source| { + match source { + git_config::Source::GitInstallation if !use_installation => return None, + git_config::Source::System if !use_system => return None, + git_config::Source::Git if !use_git => return None, + git_config::Source::User if !use_user => return None, + _ => {} + } + source + .storage_location(&mut |name| { + match name { + git_ if git_.starts_with("GIT_") => Some(git_prefix), + "XDG_CONFIG_HOME" => Some(xdg_config_home_env), + "HOME" => Some(home_env), + _ => None, } - source - .storage_location(&mut |name| { - match name { - git_ if git_.starts_with("GIT_") => Some(git_prefix), - "XDG_CONFIG_HOME" => Some(xdg_config_home_env), - "HOME" => Some(home_env), - _ => None, - } - .and_then(|perm| std::env::var_os(name).and_then(|val| perm.check_opt(val))) - }) - .map(|p| (source, p.into_owned())) - } - }) - .map(|(source, path)| git_config::file::Metadata { - path: Some(path), - source: *source, - level: 0, - trust: git_sec::Trust::Full, - }); + .and_then(|perm| std::env::var_os(name).and_then(|val| perm.check_opt(val))) + }) + .map(|p| (source, p.into_owned())) + }) + .map(|(source, path)| git_config::file::Metadata { + path: Some(path), + source: *source, + level: 0, + trust: git_sec::Trust::Full, + }); let err_on_nonexisting_paths = false; let mut globals = git_config::File::from_paths_metadata_buf( diff --git a/git-repository/src/env.rs b/git-repository/src/env.rs index f2b95f1f5d8..148e197ef34 100644 --- a/git-repository/src/env.rs +++ b/git-repository/src/env.rs @@ -26,62 +26,3 @@ pub fn os_str_to_bstring(input: &OsStr) -> Result { .map(Into::into) .map_err(|_| input.to_string_lossy().into_owned()) } - -/// Environment information involving the `git` program itself. -pub mod git { - use std::process::{Command, Stdio}; - - use crate::bstr::{BStr, BString, ByteSlice}; - - /// Returns the file that contains git configuration coming with the installation of the `git` file in the current `PATH`, or `None` - /// if no `git` executable was found or there were other errors during execution. - pub fn install_config_path() -> Option<&'static BStr> { - static PATH: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { - let mut cmd = Command::new(if cfg!(windows) { "git.exe" } else { "git" }); - cmd.args(["config", "-l", "--show-origin"]) - .stdin(Stdio::null()) - .stderr(Stdio::null()); - first_file_from_config_with_origin(cmd.output().ok()?.stdout.as_slice().into()).map(ToOwned::to_owned) - }); - PATH.as_ref().map(|b| b.as_ref()) - } - - fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> { - let file = source.strip_prefix(b"file:")?; - let end_pos = file.find_byte(b'\t')?; - file[..end_pos].as_bstr().into() - } - - #[cfg(test)] - mod tests { - use crate::env::git; - - #[test] - fn first_file_from_config_with_origin() { - let macos = "file:/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig credential.helper=osxkeychain\nfile:/Users/byron/.gitconfig push.default=simple\n"; - let win_msys = - "file:C:/git-sdk-64/etc/gitconfig core.symlinks=false\r\nfile:C:/git-sdk-64/etc/gitconfig core.autocrlf=true"; - let win_cmd = "file:C:/Program Files/Git/etc/gitconfig diff.astextplain.textconv=astextplain\r\nfile:C:/Program Files/Git/etc/gitconfig filter.lfs.clean=git-lfs clean -- %f\r\n"; - let linux = "file:/home/parallels/.gitconfig core.excludesfile=~/.gitignore\n"; - let bogus = "something unexpected"; - let empty = ""; - - for (source, expected) in [ - ( - macos, - Some("/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig"), - ), - (win_msys, Some("C:/git-sdk-64/etc/gitconfig")), - (win_cmd, Some("C:/Program Files/Git/etc/gitconfig")), - (linux, Some("/home/parallels/.gitconfig")), - (bogus, None), - (empty, None), - ] { - assert_eq!( - git::first_file_from_config_with_origin(source.into()), - expected.map(Into::into) - ); - } - } - } -} diff --git a/git-repository/src/open.rs b/git-repository/src/open.rs index a3c7048082c..6ee8b0dd4ae 100644 --- a/git-repository/src/open.rs +++ b/git-repository/src/open.rs @@ -90,7 +90,6 @@ impl Default for Options { } #[derive(Default, Clone)] -#[allow(dead_code)] pub(crate) struct EnvironmentOverrides { /// An override of the worktree typically from the environment, and overrides even worktree dirs set as parameter. /// diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index 6d6e18aa45a..7a194f94531 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -1,5 +1,6 @@ use git_refspec::RefSpec; +use crate::bstr::BStr; use crate::{remote, Remote}; /// Access @@ -60,4 +61,36 @@ impl Remote<'_> { url_err.or(push_url_err).map(Err::<&mut Self, _>).transpose()?; Ok(self) } + + /// Replace all currently set refspecs, typically from configuration, with the given `specs` for `direction`, + /// or `None` if one of the input specs could not be parsed. + pub fn replace_refspecs( + &mut self, + specs: impl IntoIterator, + direction: remote::Direction, + ) -> Result<(), git_refspec::parse::Error> + where + Spec: AsRef, + { + use remote::Direction::*; + let specs: Vec<_> = specs + .into_iter() + .map(|spec| { + git_refspec::parse( + spec.as_ref(), + match direction { + Push => git_refspec::parse::Operation::Push, + Fetch => git_refspec::parse::Operation::Fetch, + }, + ) + .map(|url| url.to_owned()) + }) + .collect::>()?; + let dst = match direction { + Push => &mut self.push_specs, + Fetch => &mut self.fetch_specs, + }; + *dst = specs; + Ok(()) + } } diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs deleted file mode 100644 index f497ebdbfc7..00000000000 --- a/git-repository/src/remote/connection/list_refs.rs +++ /dev/null @@ -1,82 +0,0 @@ -use git_features::progress::Progress; -use git_protocol::transport::client::Transport; - -use crate::remote::{connection::HandshakeWithRefs, Connection, Direction}; - -mod error { - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - Handshake(#[from] git_protocol::fetch::handshake::Error), - #[error(transparent)] - ListRefs(#[from] git_protocol::fetch::refs::Error), - #[error(transparent)] - Transport(#[from] git_protocol::transport::client::Error), - #[error(transparent)] - ConfigureCredentials(#[from] crate::config::credential_helpers::Error), - } -} -pub use error::Error; - -impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> -where - T: Transport, - P: Progress, -{ - /// List all references on the remote. - /// - /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository. - #[git_protocol::maybe_async::maybe_async] - pub async fn list_refs(mut self) -> Result, Error> { - let res = self.fetch_refs().await?; - git_protocol::fetch::indicate_end_of_interaction(&mut self.transport).await?; - Ok(res.refs) - } - - #[git_protocol::maybe_async::maybe_async] - async fn fetch_refs(&mut self) -> Result { - let mut credentials_storage; - let authenticate = match self.credentials.as_mut() { - Some(f) => f, - None => { - let url = self - .remote - .url(Direction::Fetch) - .map(ToOwned::to_owned) - .unwrap_or_else(|| { - git_url::parse(self.transport.to_url().as_bytes().into()) - .expect("valid URL to be provided by transport") - }); - credentials_storage = self.configured_credentials(url)?; - &mut credentials_storage - } - }; - let mut outcome = - git_protocol::fetch::handshake(&mut self.transport, authenticate, Vec::new(), &mut self.progress).await?; - let refs = match outcome.refs.take() { - Some(refs) => refs, - None => { - git_protocol::fetch::refs( - &mut self.transport, - outcome.server_protocol_version, - &outcome.capabilities, - |_a, _b, _c| Ok(git_protocol::fetch::delegate::LsRefsAction::Continue), - &mut self.progress, - ) - .await? - } - }; - Ok(HandshakeWithRefs { outcome, refs }) - } - - /// List all references on the remote that have been filtered through our remote's [`refspecs`][crate::Remote::refspecs()] - /// for _fetching_ or _pushing_ depending on `direction`. - /// - /// This comes in the form of information of all matching tips on the remote and the object they point to, along with - /// 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. - pub fn list_refs_by_refspec(&mut self, _direction: Direction) -> ! { - todo!() - } -} diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index 8e3e4e449e0..0ac97e17902 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -1,11 +1,11 @@ use crate::Remote; pub(crate) struct HandshakeWithRefs { - #[allow(dead_code)] outcome: git_protocol::fetch::handshake::Outcome, refs: Vec, } +/// A function that performs a given credential action. pub type CredentialsFn<'a> = Box git_credentials::protocol::Result + 'a>; /// A type to represent an ongoing connection to a remote host, typically with the connection already established. @@ -65,4 +65,5 @@ mod access { } } -mod list_refs; +/// +pub mod ref_map; diff --git a/git-repository/src/remote/connection/ref_map.rs b/git-repository/src/remote/connection/ref_map.rs new file mode 100644 index 00000000000..307976f4991 --- /dev/null +++ b/git-repository/src/remote/connection/ref_map.rs @@ -0,0 +1,114 @@ +use git_features::progress::Progress; +use git_protocol::transport::client::Transport; + +use crate::remote::{connection::HandshakeWithRefs, fetch, Connection, Direction}; + +/// The error returned by [`Connection::ref_map()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Handshake(#[from] git_protocol::fetch::handshake::Error), + #[error(transparent)] + ListRefs(#[from] git_protocol::fetch::refs::Error), + #[error(transparent)] + Transport(#[from] git_protocol::transport::client::Error), + #[error(transparent)] + ConfigureCredentials(#[from] crate::config::credential_helpers::Error), + #[error(transparent)] + MappingValidation(#[from] git_refspec::match_group::validate::Error), +} + +impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P> +where + T: Transport, + P: Progress, +{ + /// List all references on the remote that have been filtered through our remote's [`refspecs`][crate::Remote::refspecs()] + /// for _fetching_. + /// + /// This comes in the form of all matching tips on the remote and the object they point to, along with + /// 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. + #[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?; + res + } + + #[git_protocol::maybe_async::maybe_async] + async fn ref_map_inner(&mut self) -> Result, Error> { + let remote = self.fetch_refs().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| { + let (full_ref_name, target, object) = r.unpack(); + git_refspec::match_group::Item { + full_ref_name, + target, + object, + } + })) + .validated()?; + let mappings = res.mappings; + let mappings = mappings + .into_iter() + .map(|m| fetch::Mapping { + remote: m + .item_index + .map(|idx| fetch::Source::Ref(remote.refs[idx].clone())) + .unwrap_or_else(|| { + fetch::Source::ObjectId(match m.lhs { + git_refspec::match_group::SourceRef::ObjectId(id) => id, + _ => unreachable!("no item index implies having an object id"), + }) + }), + local: m.rhs.map(|c| c.into_owned()), + spec_index: m.spec_index, + }) + .collect(); + Ok(fetch::RefMap { + mappings, + fixes, + remote_refs: remote.refs, + handshake: remote.outcome, + }) + } + #[git_protocol::maybe_async::maybe_async] + async fn fetch_refs(&mut self) -> Result { + let mut credentials_storage; + let authenticate = match self.credentials.as_mut() { + Some(f) => f, + None => { + let url = self + .remote + .url(Direction::Fetch) + .map(ToOwned::to_owned) + .unwrap_or_else(|| { + git_url::parse(self.transport.to_url().as_bytes().into()) + .expect("valid URL to be provided by transport") + }); + credentials_storage = self.configured_credentials(url)?; + &mut credentials_storage + } + }; + let mut outcome = + git_protocol::fetch::handshake(&mut self.transport, authenticate, Vec::new(), &mut self.progress).await?; + let refs = match outcome.refs.take() { + Some(refs) => refs, + None => { + git_protocol::fetch::refs( + &mut self.transport, + outcome.server_protocol_version, + &outcome.capabilities, + |_a, _b, _c| Ok(git_protocol::fetch::delegate::LsRefsAction::Continue), + &mut self.progress, + ) + .await? + } + }; + Ok(HandshakeWithRefs { outcome, refs }) + } +} diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 9fc9666c66a..e1bed940331 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -25,6 +25,47 @@ pub use errors::find; /// 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, + } +} + /// #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] pub mod connect; @@ -32,7 +73,7 @@ pub mod connect; #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] mod connection; #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] -pub use connection::Connection; +pub use connection::{ref_map, Connection}; mod access; pub(crate) mod url; diff --git a/git-repository/src/remote/url/scheme_permission.rs b/git-repository/src/remote/url/scheme_permission.rs index 4493d175dbf..db8d7aeefba 100644 --- a/git-repository/src/remote/url/scheme_permission.rs +++ b/git-repository/src/remote/url/scheme_permission.rs @@ -1,5 +1,3 @@ -#![allow(dead_code, unused_variables)] - use std::{borrow::Cow, collections::BTreeMap, convert::TryFrom}; use crate::bstr::{BStr, BString, ByteSlice}; diff --git a/git-repository/tests/remote/list_refs.rs b/git-repository/tests/remote/list_refs.rs index 01b85ca2fce..67f84673678 100644 --- a/git-repository/tests/remote/list_refs.rs +++ b/git-repository/tests/remote/list_refs.rs @@ -22,10 +22,22 @@ mod blocking_io { (version as u8).to_string().as_str(), )?; } + let remote = repo.find_remote("origin")?; let connection = remote.connect(Fetch, progress::Discard)?; - let refs = connection.list_refs()?; - assert_eq!(refs.len(), 14, "it gets all remote refs, independently of the refspec."); + let map = connection.ref_map()?; + assert_eq!( + map.remote_refs.len(), + 14, + "it gets all remote refs, independently of the refspec." + ); + + assert_eq!(map.fixes.len(), 0); + assert_eq!( + map.mappings.len(), + 11, + "mappings are only a sub-set of all remotes due to refspec matching" + ); } Ok(()) } diff --git a/git-transport/src/client/capabilities.rs b/git-transport/src/client/capabilities.rs index 3bda61ade18..f6e1efb1513 100644 --- a/git-transport/src/client/capabilities.rs +++ b/git-transport/src/client/capabilities.rs @@ -24,6 +24,7 @@ pub enum Error { /// A structure to represent multiple [capabilities][Capability] or features supported by the server. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct Capabilities { data: BString, value_sep: u8, diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 784eef5cf8b..673196d48ee 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -90,7 +90,7 @@ impl protocol::fetch::DelegateBlocking for CloneDelegate { ) -> io::Result { if self.wanted_refs.is_empty() { for r in refs { - let (path, id) = r.unpack(); + let (path, id, _) = r.unpack(); match self.ref_filter { Some(ref_prefixes) => { if ref_prefixes.iter().any(|prefix| path.starts_with_str(prefix)) { @@ -310,10 +310,20 @@ fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> { }; for r in refs { let (path, content) = match r { - Ref::Symbolic { path, target, .. } => (assure_dir_exists(path)?, format!("ref: {}", target)), - Ref::Peeled { path, tag: object, .. } | Ref::Direct { path, object } => { - (assure_dir_exists(path)?, object.to_string()) + Ref::Symbolic { + full_ref_name: path, + target, + .. + } => (assure_dir_exists(path)?, format!("ref: {}", target)), + Ref::Peeled { + full_ref_name: path, + tag: object, + .. } + | Ref::Direct { + full_ref_name: path, + object, + } => (assure_dir_exists(path)?, object.to_string()), }; std::fs::write(path, content.as_bytes())?; } diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs index 5b7ed841376..1d1cb4c2280 100644 --- a/gitoxide-core/src/repository/remote.rs +++ b/gitoxide-core/src/repository/remote.rs @@ -1,20 +1,29 @@ #[cfg(any(feature = "blocking-client", feature = "async-client"))] -mod net { +mod refs_impl { use anyhow::bail; use git_repository as git; use git_repository::protocol::fetch; + use git_repository::refspec::match_group::validate::Fix; + use git_repository::refspec::RefSpec; use crate::OutputFormat; pub mod refs { use crate::OutputFormat; + use git_repository::bstr::BString; pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=2; + pub enum Kind { + Remote, + Tracking { ref_specs: Vec }, + } + pub struct Context { pub format: OutputFormat, pub name: Option, pub url: Option, + pub handshake_info: bool, } pub(crate) use super::print; @@ -23,12 +32,19 @@ mod net { #[git::protocol::maybe_async::maybe_async] pub async fn refs_fn( repo: git::Repository, + kind: refs::Kind, mut progress: impl git::Progress, - out: impl std::io::Write, - refs::Context { format, name, url }: refs::Context, + mut out: impl std::io::Write, + err: impl std::io::Write, + refs::Context { + format, + name, + url, + handshake_info, + }: refs::Context, ) -> anyhow::Result<()> { use anyhow::Context; - let remote = match (name, url) { + let mut remote = match (name, url) { (Some(name), None) => repo.find_remote(&name)?, (None, None) => repo .head()? @@ -37,6 +53,14 @@ mod net { (None, Some(url)) => repo.remote_at(url)?, (Some(_), Some(_)) => bail!("Must not set both the remote name and the url - they are mutually exclusive"), }; + if let refs::Kind::Tracking { ref_specs, .. } = &kind { + if format != OutputFormat::Human { + bail!("JSON output isn't yet supported for listing ref-mappings."); + } + if !ref_specs.is_empty() { + remote.replace_refspecs(ref_specs.iter(), git::remote::Direction::Fetch)?; + } + } progress.info(format!( "Connecting to {:?}", remote @@ -44,19 +68,100 @@ mod net { .context("Remote didn't have a URL to connect to")? .to_bstring() )); - let refs = remote + let map = remote .connect(git::remote::Direction::Fetch, progress) .await? - .list_refs() + .ref_map() .await?; - match format { - OutputFormat::Human => drop(print(out, &refs)), - #[cfg(feature = "serde1")] - OutputFormat::Json => { - serde_json::to_writer_pretty(out, &refs.into_iter().map(JsonRef::from).collect::>())? + if handshake_info { + writeln!(out, "Handshake Information")?; + writeln!(out, "\t{:?}", map.handshake)?; + } + match kind { + refs::Kind::Tracking { .. } => { + print_refmap(&repo, remote.refspecs(git::remote::Direction::Fetch), map, out, err) } - }; + refs::Kind::Remote => { + match format { + OutputFormat::Human => drop(print(out, &map.remote_refs)), + #[cfg(feature = "serde1")] + OutputFormat::Json => serde_json::to_writer_pretty( + out, + &map.remote_refs.into_iter().map(JsonRef::from).collect::>(), + )?, + }; + Ok(()) + } + } + } + + fn print_refmap( + repo: &git::Repository, + refspecs: &[RefSpec], + mut map: git::remote::fetch::RefMap<'_>, + mut out: impl std::io::Write, + mut err: impl std::io::Write, + ) -> anyhow::Result<()> { + let mut last_spec_index = usize::MAX; + map.mappings.sort_by_key(|m| m.spec_index); + for mapping in &map.mappings { + if mapping.spec_index != last_spec_index { + last_spec_index = mapping.spec_index; + let spec = &refspecs[mapping.spec_index]; + spec.to_ref().write_to(&mut out)?; + writeln!(out)?; + } + + write!(out, "\t")?; + let target_id = match &mapping.remote { + git::remote::fetch::Source::ObjectId(id) => { + write!(out, "{}", id)?; + id + } + git::remote::fetch::Source::Ref(r) => print_ref(&mut out, r)?, + }; + match &mapping.local { + Some(local) => { + write!(out, " -> {local} ")?; + match repo.try_find_reference(local)? { + Some(tracking) => { + let msg = match tracking.try_id() { + Some(id) => (id.as_ref() == target_id) + .then(|| "[up-to-date]") + .unwrap_or("[changed]"), + None => "[skipped]", + }; + writeln!(out, "{msg}") + } + None => writeln!(out, "[new]"), + } + } + None => writeln!(out, " (fetch only)"), + }?; + } + if !map.fixes.is_empty() { + writeln!( + err, + "The following destination refs were removed as they didn't start with 'ref/'" + )?; + map.fixes.sort_by_key(|f| match f { + Fix::MappingWithPartialDestinationRemoved { spec, .. } => *spec, + }); + let mut prev_spec = None; + for fix in &map.fixes { + match fix { + Fix::MappingWithPartialDestinationRemoved { name, spec } => { + if prev_spec.map_or(true, |prev_spec| prev_spec != spec) { + prev_spec = spec.into(); + spec.write_to(&mut err)?; + writeln!(err)?; + } + writeln!(err, "\t{name}")?; + } + } + } + } Ok(()) } @@ -81,16 +186,27 @@ mod net { impl From for JsonRef { fn from(value: fetch::Ref) -> Self { match value { - fetch::Ref::Direct { path, object } => JsonRef::Direct { + fetch::Ref::Direct { + full_ref_name: path, + object, + } => JsonRef::Direct { path: path.to_string(), object: object.to_string(), }, - fetch::Ref::Symbolic { path, target, object } => JsonRef::Symbolic { + fetch::Ref::Symbolic { + full_ref_name: path, + target, + object, + } => JsonRef::Symbolic { path: path.to_string(), target: target.to_string(), object: object.to_string(), }, - fetch::Ref::Peeled { path, tag, object } => JsonRef::Peeled { + fetch::Ref::Peeled { + full_ref_name: path, + tag, + object, + } => JsonRef::Peeled { path: path.to_string(), tag: tag.to_string(), object: object.to_string(), @@ -99,20 +215,32 @@ mod net { } } + fn print_ref(mut out: impl std::io::Write, r: &fetch::Ref) -> std::io::Result<&git::hash::oid> { + match r { + fetch::Ref::Direct { + full_ref_name: path, + object, + } => write!(&mut out, "{} {}", object, path).map(|_| object.as_ref()), + fetch::Ref::Peeled { + full_ref_name: path, + tag, + object, + } => write!(&mut out, "{} {} object:{}", tag, path, object).map(|_| tag.as_ref()), + fetch::Ref::Symbolic { + full_ref_name: path, + target, + object, + } => write!(&mut out, "{} {} symref-target:{}", object, path, target).map(|_| object.as_ref()), + } + } + pub(crate) fn print(mut out: impl std::io::Write, refs: &[fetch::Ref]) -> std::io::Result<()> { for r in refs { - match r { - fetch::Ref::Direct { path, object } => writeln!(&mut out, "{} {}", object.to_hex(), path), - fetch::Ref::Peeled { path, object, tag } => { - writeln!(&mut out, "{} {} tag:{}", object.to_hex(), path, tag) - } - fetch::Ref::Symbolic { path, target, object } => { - writeln!(&mut out, "{} {} symref-target:{}", object.to_hex(), path, target) - } - }?; + print_ref(&mut out, r)?; + writeln!(out)?; } Ok(()) } } #[cfg(any(feature = "blocking-client", feature = "async-client"))] -pub use net::{refs, refs_fn as refs, JsonRef}; +pub use refs_impl::{refs, refs_fn as refs, JsonRef}; diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index baf5b6b9201..5c587d488dd 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -120,9 +120,26 @@ pub fn main() -> Result<()> { }, ), #[cfg_attr(feature = "small", allow(unused_variables))] - Subcommands::Remote(remote::Platform { name, url, cmd }) => match cmd { + Subcommands::Remote(remote::Platform { + name, + url, + cmd, + handshake_info, + }) => match cmd { #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] - remote::Subcommands::Refs => { + remote::Subcommands::Refs | remote::Subcommands::RefMap { .. } => { + let kind = match cmd { + remote::Subcommands::Refs => core::repository::remote::refs::Kind::Remote, + remote::Subcommands::RefMap { ref_spec } => { + core::repository::remote::refs::Kind::Tracking { ref_specs: ref_spec } + } + }; + let context = core::repository::remote::refs::Context { + name, + url, + format, + handshake_info, + }; #[cfg(feature = "gitoxide-core-blocking-client")] { prepare_and_run( @@ -131,12 +148,14 @@ pub fn main() -> Result<()> { progress, progress_keep_open, core::repository::remote::refs::PROGRESS_RANGE, - move |progress, out, _err| { + move |progress, out, err| { core::repository::remote::refs( repository(Mode::LenientWithGitInstallConfig)?, + kind, progress, out, - core::repository::remote::refs::Context { name, url, format }, + err, + context, ) }, ) @@ -149,10 +168,12 @@ pub fn main() -> Result<()> { Some(core::repository::remote::refs::PROGRESS_RANGE), ); futures_lite::future::block_on(core::repository::remote::refs( - repository(Mode::Lenient)?, + repository(Mode::LenientWithGitInstallConfig)?, + kind, progress, std::io::stdout(), - core::repository::remote::refs::Context { name, url, format }, + std::io::stderr(), + context, )) } } diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index 467cb4ec024..663773cc745 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -123,6 +123,10 @@ pub mod remote { #[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, @@ -131,9 +135,16 @@ pub mod remote { #[derive(Debug, clap::Subcommand)] #[clap(visible_alias = "remotes")] pub enum Subcommands { - /// Print all references available on the remote + /// 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, + }, } } diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output index 57c0617c306..b908a0b0987 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output +++ b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output @@ -4,5 +4,5 @@ pack: 346574b7331dc3a1724da218d622c6e1b6c66a57 3f72b39ad1600e6dac63430c15e0d875e9d3f9d6 HEAD symref-target:refs/heads/main ee3c97678e89db4eab7420b04aef51758359f152 refs/heads/dev 3f72b39ad1600e6dac63430c15e0d875e9d3f9d6 refs/heads/main -ee3c97678e89db4eab7420b04aef51758359f152 refs/tags/annotated tag:feae03400632392a7f38e5b2775f98a439f5eaf5 +feae03400632392a7f38e5b2775f98a439f5eaf5 refs/tags/annotated object:ee3c97678e89db4eab7420b04aef51758359f152 efa596d621559707b2d221f10490959b2decbc6c refs/tags/unannotated \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-with-output b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-with-output index 43ac53bfe11..6ea910a5bda 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-with-output +++ b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-with-output @@ -4,5 +4,5 @@ pack: 346574b7331dc3a1724da218d622c6e1b6c66a57 (out/346574b7331dc3a1724da218d622 3f72b39ad1600e6dac63430c15e0d875e9d3f9d6 HEAD symref-target:refs/heads/main ee3c97678e89db4eab7420b04aef51758359f152 refs/heads/dev 3f72b39ad1600e6dac63430c15e0d875e9d3f9d6 refs/heads/main -ee3c97678e89db4eab7420b04aef51758359f152 refs/tags/annotated tag:feae03400632392a7f38e5b2775f98a439f5eaf5 +feae03400632392a7f38e5b2775f98a439f5eaf5 refs/tags/annotated object:ee3c97678e89db4eab7420b04aef51758359f152 efa596d621559707b2d221f10490959b2decbc6c refs/tags/unannotated \ No newline at end of file diff --git a/tests/snapshots/plumbing/repository/remote/refs/file-v-any b/tests/snapshots/plumbing/repository/remote/refs/file-v-any index 1ff9b7937dc..3b4b200d937 100644 --- a/tests/snapshots/plumbing/repository/remote/refs/file-v-any +++ b/tests/snapshots/plumbing/repository/remote/refs/file-v-any @@ -1,5 +1,5 @@ 3f72b39ad1600e6dac63430c15e0d875e9d3f9d6 HEAD symref-target:refs/heads/main ee3c97678e89db4eab7420b04aef51758359f152 refs/heads/dev 3f72b39ad1600e6dac63430c15e0d875e9d3f9d6 refs/heads/main -ee3c97678e89db4eab7420b04aef51758359f152 refs/tags/annotated tag:feae03400632392a7f38e5b2775f98a439f5eaf5 +feae03400632392a7f38e5b2775f98a439f5eaf5 refs/tags/annotated object:ee3c97678e89db4eab7420b04aef51758359f152 efa596d621559707b2d221f10490959b2decbc6c refs/tags/unannotated \ No newline at end of file