From 0f97c44cf5f52fbd4431cddcbff188c791fe667e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Aug 2022 15:56:18 +0800 Subject: [PATCH 001/125] refactor (#450) Prepare to free up the remote module for actual remote handling --- git-repository/src/repository/config.rs | 33 ++++++++++++++++++++++++- git-repository/src/repository/remote.rs | 27 -------------------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index dd5cf205356..d05b33f837e 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -1,6 +1,6 @@ use crate::config; -/// Configuration +/// General Configuration impl crate::Repository { /// Return /// Return a snapshot of the configuration as seen upon opening the repository. @@ -18,3 +18,34 @@ impl crate::Repository { self.config.object_hash } } + +mod branch { + use std::{borrow::Cow, convert::TryInto}; + + use git_ref::FullNameRef; + use git_validate::reference::name::Error as ValidateNameError; + + use crate::bstr::BStr; + + impl crate::Repository { + /// Returns a reference to the remote associated with the given `short_branch_name`, typically `main` instead of `refs/heads/main`. + /// Returns `None` if the remote reference was not found. + /// May return an error if the reference is invalid. + pub fn remote_ref(&self, short_branch_name: &str) -> Option, ValidateNameError>> { + self.config + .resolved + .string("branch", Some(short_branch_name), "merge") + .map(|v| match v { + Cow::Borrowed(v) => v.try_into().map(Cow::Borrowed), + Cow::Owned(v) => v.try_into().map(Cow::Owned), + }) + } + + /// Returns the 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. + pub fn branch_remote_name(&self, short_branch_name: &str) -> Option> { + self.config.resolved.string("branch", Some(short_branch_name), "remote") + } + } +} diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 7d032d4e944..8b137891791 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -1,28 +1 @@ -use std::{borrow::Cow, convert::TryInto}; -use git_ref::FullNameRef; -use git_validate::reference::name::Error as ValidateNameError; - -use crate::bstr::BStr; - -impl crate::Repository { - /// Returns a reference to the remote associated with the given `short_branch_name`, typically `main` instead of `refs/heads/main`. - /// Returns `None` if the remote reference was not found. - /// May return an error if the reference is invalid. - pub fn remote_ref(&self, short_branch_name: &str) -> Option, ValidateNameError>> { - self.config - .resolved - .string("branch", Some(short_branch_name), "merge") - .map(|v| match v { - Cow::Borrowed(v) => v.try_into().map(Cow::Borrowed), - Cow::Owned(v) => v.try_into().map(Cow::Owned), - }) - } - - /// Returns the 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. - pub fn branch_remote_name(&self, short_branch_name: &str) -> Option> { - self.config.resolved.string("branch", Some(short_branch_name), "remote") - } -} From 4297f535562a5104dd6eba54c349fd020bfab8ad Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Aug 2022 16:44:35 +0800 Subject: [PATCH 002/125] update feature list for remotes (#450) --- crate-status.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crate-status.md b/crate-status.md index 0d9790685c2..1cae0741b3c 100644 --- a/crate-status.md +++ b/crate-status.md @@ -237,6 +237,8 @@ Check out the [performance discussion][git-traverse-performance] as well. ### git-refspec * [x] parse * [ ] matching of references and object names + * [ ] for fetch + * [ ] for push ### git-note @@ -459,9 +461,14 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. * **references** * [x] peel to end * [x] ref-log access - * [ ] clone from remote - * [ ] shallow - * [ ] execute hooks + * **remotes** + * [ ] clone + * [ ] shallow + * [ ] fetch + * [ ] push + * [ ] ls-refs + * [ ] list, find by name, create in memory. + * [ ] execute hooks * **refs** * [ ] run transaction hooks and handle special repository states like quarantine * [ ] support for different backends like `files` and `reftable` @@ -485,7 +492,6 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. * [x] read and interpolate trusted paths * [x] low-level API for more elaborate access to all details of `git-config` files * [ ] a way to make changes to individual configuration files - * [ ] remotes with push and pull * [x] mailmap * [x] object replacements (`git replace`) * [ ] configuration From bca9fe91c015633ed83e9e8ba248a16a0fdbddd6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 12:02:20 +0800 Subject: [PATCH 003/125] first sketch of method to access remote names (#450) Needs more tests to see it actually finds remotes with names. --- git-repository/src/config/snapshot.rs | 6 +---- git-repository/src/repository/config.rs | 29 +++++++++++++++++++++++ git-repository/tests/repository/config.rs | 8 ++++++- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/git-repository/src/config/snapshot.rs b/git-repository/src/config/snapshot.rs index 7bbcb31a88d..61caba594fa 100644 --- a/git-repository/src/config/snapshot.rs +++ b/git-repository/src/config/snapshot.rs @@ -75,11 +75,7 @@ impl<'repo> Snapshot<'repo> { key.section_name, key.subsection_name, key.value_name, - &mut self - .repo - .options - .filter_config_section - .unwrap_or(crate::config::section::is_trusted), + &mut self.repo.filter_config_section(), )?; let install_dir = self.repo.install_dir().ok(); diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index d05b33f837e..21f1863cebd 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -19,6 +19,27 @@ impl crate::Repository { } } +mod remote { + use crate::bstr::BStr; + + impl crate::Repository { + /// Returns an iterator over all symbolic names of remotes that we deem [trustworthy][crate::open::Options::filter_config_section()]. + pub fn remote_names(&self) -> impl Iterator + '_ { + self.config + .resolved + .sections_by_name("remote") + .map(|it| { + let filter = self.filter_config_section(); + Box::new( + it.filter(move |s| filter(s.meta())) + .filter_map(|section| section.header().subsection_name()), + ) as Box> + }) + .unwrap_or_else(|| Box::new(std::iter::empty())) + } + } +} + mod branch { use std::{borrow::Cow, convert::TryInto}; @@ -49,3 +70,11 @@ mod branch { } } } + +impl crate::Repository { + pub(crate) fn filter_config_section(&self) -> fn(&git_config::file::Metadata) -> bool { + self.options + .filter_config_section + .unwrap_or(config::section::is_trusted) + } +} diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index 1a3c1954c51..9ae1e4f3e19 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -7,9 +7,15 @@ use serial_test::serial; use crate::named_repo; +#[test] +fn remote_names() { + let repo = named_repo("make_basic_repo.sh").unwrap(); + assert_eq!(repo.remote_names().count(), 0, "there are no remotes"); +} + #[test] #[serial] -fn access_values() { +fn access_values_and_identity() { for trust in [git_sec::Trust::Full, git_sec::Trust::Reduced] { let repo = named_repo("make_config_repo.sh").unwrap(); let work_dir = repo.work_dir().expect("present").canonicalize().unwrap(); From 2b21ac5948623beadf8e89c3d0030886f3fdaeee Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 13:00:44 +0800 Subject: [PATCH 004/125] Add test to list remote names (#450) --- .../fixtures/generated-archives/.gitignore | 1 + .../tests/fixtures/make_remote_repos.sh | 93 +++++++++++++++++++ git-repository/tests/git-with-regex.rs | 1 + git-repository/tests/git.rs | 2 + git-repository/tests/remote/mod.rs | 7 ++ git-repository/tests/repository/config.rs | 8 +- 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 git-repository/tests/fixtures/make_remote_repos.sh create mode 100644 git-repository/tests/remote/mod.rs diff --git a/git-repository/tests/fixtures/generated-archives/.gitignore b/git-repository/tests/fixtures/generated-archives/.gitignore index 82067349dd4..e04be2ceea4 100644 --- a/git-repository/tests/fixtures/generated-archives/.gitignore +++ b/git-repository/tests/fixtures/generated-archives/.gitignore @@ -1 +1,2 @@ /make_worktree_repo.tar.xz +/make_remote_repos.tar.xz diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh new file mode 100644 index 00000000000..31008b76a31 --- /dev/null +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -0,0 +1,93 @@ +function tick () { + if test -z "${tick+set}" + then + tick=1112911993 + else + tick=$(($tick + 60)) + fi + GIT_COMMITTER_DATE="$tick -0700" + GIT_AUTHOR_DATE="$tick -0700" + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} + +GIT_AUTHOR_EMAIL=author@example.com +GIT_AUTHOR_NAME='A U Thor' +GIT_AUTHOR_DATE='1112354055 +0200' +TEST_COMMITTER_LOCALNAME=committer +TEST_COMMITTER_DOMAIN=example.com +GIT_COMMITTER_EMAIL=committer@example.com +GIT_COMMITTER_NAME='C O Mitter' +GIT_COMMITTER_DATE='1112354055 +0200' + +# runup to the correct count for ambigous commits +tick; tick; tick; tick; tick + +git init base +( + cd base + tick + + echo g > file + git add file && git commit -m $'G\n\n initial message' + git branch g + + tick + git checkout --orphan=h + echo h > file + git add file && git commit -m H + + tick + git checkout main + git merge h --allow-unrelated-histories || : + { echo g && echo h && echo d; } > file + git add file + git commit -m D + git branch d + + tick + git checkout --orphan=i + echo i > file + git add file && git commit -m I + git tag -m I-tag i-tag + + tick + git checkout --orphan=j + echo j > file + git add file && git commit -m J + + tick + git checkout i + git merge j --allow-unrelated-histories || : + { echo i && echo j && echo f; } > file + git add file + git commit -m F + git branch f + + tick + git checkout --orphan=e + echo e > file + git add file && git commit -m E + + tick + git checkout main + git merge e i --allow-unrelated-histories || : + { echo g && echo h && echo i && echo j && echo d && echo e && echo f && echo b; } > file + git add file && git commit -m B + git tag -m b-tag b-tag && git branch b + + tick + git checkout i + echo c >> file + git add file && git commit -m $'C\n\n message recent' + git branch c + git reset --hard i-tag + + tick + git checkout main + git merge c || : + { echo g && echo h && echo i && echo j && echo d && echo e && echo f && echo b && echo c && echo a; } > file + git add file && git commit -m A + git branch a +) + +git clone --shared base clone diff --git a/git-repository/tests/git-with-regex.rs b/git-repository/tests/git-with-regex.rs index 7a817b59fb1..98857c6649c 100644 --- a/git-repository/tests/git-with-regex.rs +++ b/git-repository/tests/git-with-regex.rs @@ -6,5 +6,6 @@ mod id; mod init; mod object; mod reference; +mod remote; mod repository; mod revision; diff --git a/git-repository/tests/git.rs b/git-repository/tests/git.rs index 00e91d969c1..434ea4f0c56 100644 --- a/git-repository/tests/git.rs +++ b/git-repository/tests/git.rs @@ -14,6 +14,8 @@ mod object; #[cfg(not(feature = "regex"))] mod reference; #[cfg(not(feature = "regex"))] +mod remote; +#[cfg(not(feature = "regex"))] mod repository; #[cfg(not(feature = "regex"))] mod revision; diff --git a/git-repository/tests/remote/mod.rs b/git-repository/tests/remote/mod.rs new file mode 100644 index 00000000000..2aca868bd15 --- /dev/null +++ b/git-repository/tests/remote/mod.rs @@ -0,0 +1,7 @@ +use git_repository as git; +use git_testtools::scripted_fixture_repo_read_only; + +pub(crate) fn repo(name: &str) -> git::Repository { + let dir = scripted_fixture_repo_read_only("make_remote_repos.sh").unwrap(); + git::open_opts(dir.join(name), git::open::Options::isolated()).unwrap() +} diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index 9ae1e4f3e19..3d3cbe07ca0 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -5,12 +5,18 @@ use git_sec::{Access, Permission}; use git_testtools::Env; use serial_test::serial; -use crate::named_repo; +use crate::{named_repo, remote}; #[test] fn remote_names() { let repo = named_repo("make_basic_repo.sh").unwrap(); assert_eq!(repo.remote_names().count(), 0, "there are no remotes"); + + let repo = remote::repo("clone"); + assert_eq!( + repo.remote_names().map(|name| name.to_string()).collect::>(), + vec![String::from("origin")] + ); } #[test] From 5ab81ece15ec802ad4328ce31304233bd25b2929 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 16:12:00 +0800 Subject: [PATCH 005/125] change!: rename `Repository::remote_ref()` to `::branch_remote_ref()` (#450) --- git-repository/src/repository/config.rs | 5 +++- git-repository/tests/repository/config.rs | 29 ++++++++++++++++++++++- git-repository/tests/repository/remote.rs | 23 ------------------ 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index 21f1863cebd..7b556555b41 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -52,7 +52,10 @@ mod branch { /// Returns a reference to the remote associated with the given `short_branch_name`, typically `main` instead of `refs/heads/main`. /// Returns `None` if the remote reference was not found. /// May return an error if the reference is invalid. - pub fn remote_ref(&self, short_branch_name: &str) -> Option, ValidateNameError>> { + pub fn branch_remote_ref( + &self, + short_branch_name: &str, + ) -> Option, ValidateNameError>> { self.config .resolved .string("branch", Some(short_branch_name), "merge") diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index 3d3cbe07ca0..2579da9856f 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::path::Path; use git_repository as git; @@ -9,7 +10,7 @@ use crate::{named_repo, remote}; #[test] fn remote_names() { - let repo = named_repo("make_basic_repo.sh").unwrap(); + let repo = remote::repo("base"); assert_eq!(repo.remote_names().count(), 0, "there are no remotes"); let repo = remote::repo("clone"); @@ -19,6 +20,32 @@ fn remote_names() { ); } +#[test] +fn branch_remote() -> Result { + let repo = named_repo("make_remote_repo.sh")?; + + assert_eq!( + repo.branch_remote_ref("main") + .expect("Remote Merge ref exists") + .expect("Remote Merge ref is valid") + .shorten(), + "main" + ); + assert_eq!( + repo.branch_remote_name("main").expect("Remote name exists").as_ref(), + "remote_repo" + ); + + assert!(repo + .branch_remote_ref("broken") + .expect("Remote Merge ref exists") + .is_err()); + assert!(repo.branch_remote_ref("missing").is_none()); + assert!(repo.branch_remote_name("broken").is_none()); + + Ok(()) +} + #[test] #[serial] fn access_values_and_identity() { diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 1d707f416ea..8b137891791 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -1,24 +1 @@ -use crate::{named_repo, Result}; -#[test] -fn simple() -> Result { - let repo = named_repo("make_remote_repo.sh")?; - - assert_eq!( - repo.remote_ref("main") - .expect("Remote Merge ref exists") - .expect("Remote Merge ref is valid") - .shorten(), - "main" - ); - assert_eq!( - repo.branch_remote_name("main").expect("Remote name exists").as_ref(), - "remote_repo" - ); - - assert!(repo.remote_ref("broken").expect("Remote Merge ref exists").is_err()); - assert!(repo.remote_ref("missing").is_none()); - assert!(repo.branch_remote_name("broken").is_none()); - - Ok(()) -} From 7ef35b2d67b74be8420b821d5a477bad56d2026b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 16:27:36 +0800 Subject: [PATCH 006/125] Assure remote-names are unique and we don't double-count sections. (#450) --- git-repository/src/repository/config.rs | 24 +++++++++++++++-------- git-repository/tests/repository/config.rs | 8 +++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index 7b556555b41..ad44f8cd207 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -20,22 +20,25 @@ impl crate::Repository { } mod remote { - use crate::bstr::BStr; + use crate::bstr::ByteSlice; + use std::collections::BTreeSet; impl crate::Repository { - /// Returns an iterator over all symbolic names of remotes that we deem [trustworthy][crate::open::Options::filter_config_section()]. - pub fn remote_names(&self) -> impl Iterator + '_ { + /// Returns a sorted list unique of symbolic names of remotes that + /// we deem [trustworthy][crate::open::Options::filter_config_section()]. + pub fn remote_names(&self) -> BTreeSet<&str> { self.config .resolved .sections_by_name("remote") .map(|it| { let filter = self.filter_config_section(); - Box::new( - it.filter(move |s| filter(s.meta())) - .filter_map(|section| section.header().subsection_name()), - ) as Box> + let set: BTreeSet<_> = it + .filter(move |s| filter(s.meta())) + .filter_map(|section| section.header().subsection_name().and_then(|b| b.to_str().ok())) + .collect(); + set.into() }) - .unwrap_or_else(|| Box::new(std::iter::empty())) + .unwrap_or_default() } } } @@ -49,6 +52,11 @@ mod branch { use crate::bstr::BStr; impl crate::Repository { + // /// Return a iterator of short branch names for which information custom configuration exists. + // pub fn branch_names(&self) -> impl Iterator + '_ { + // + // } + /// Returns a reference to the remote associated with the given `short_branch_name`, typically `main` instead of `refs/heads/main`. /// Returns `None` if the remote reference was not found. /// May return an error if the reference is invalid. diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index 2579da9856f..149f23cc465 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -1,4 +1,5 @@ use crate::Result; +use std::iter::FromIterator; use std::path::Path; use git_repository as git; @@ -11,13 +12,10 @@ use crate::{named_repo, remote}; #[test] fn remote_names() { let repo = remote::repo("base"); - assert_eq!(repo.remote_names().count(), 0, "there are no remotes"); + assert_eq!(repo.remote_names().len(), 0, "there are no remotes"); let repo = remote::repo("clone"); - assert_eq!( - repo.remote_names().map(|name| name.to_string()).collect::>(), - vec![String::from("origin")] - ); + assert_eq!(Vec::from_iter(repo.remote_names().into_iter()), vec!["origin"]); } #[test] From f47464f64f7c21500a24f563b25a8fc070c41778 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 16:36:14 +0800 Subject: [PATCH 007/125] feat: `Repository::branch_names()` to obtain branch names for which configuration exists. (#450) --- git-repository/src/repository/config.rs | 41 +++++++++++++---------- git-repository/tests/repository/config.rs | 4 ++- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index ad44f8cd207..ea5425da751 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -1,4 +1,6 @@ +use crate::bstr::ByteSlice; use crate::config; +use std::collections::BTreeSet; /// General Configuration impl crate::Repository { @@ -20,30 +22,19 @@ impl crate::Repository { } mod remote { - use crate::bstr::ByteSlice; use std::collections::BTreeSet; impl crate::Repository { /// Returns a sorted list unique of symbolic names of remotes that /// we deem [trustworthy][crate::open::Options::filter_config_section()]. pub fn remote_names(&self) -> BTreeSet<&str> { - self.config - .resolved - .sections_by_name("remote") - .map(|it| { - let filter = self.filter_config_section(); - let set: BTreeSet<_> = it - .filter(move |s| filter(s.meta())) - .filter_map(|section| section.header().subsection_name().and_then(|b| b.to_str().ok())) - .collect(); - set.into() - }) - .unwrap_or_default() + self.subsection_names_of("remote") } } } mod branch { + use std::collections::BTreeSet; use std::{borrow::Cow, convert::TryInto}; use git_ref::FullNameRef; @@ -52,10 +43,11 @@ mod branch { use crate::bstr::BStr; impl crate::Repository { - // /// Return a iterator of short branch names for which information custom configuration exists. - // pub fn branch_names(&self) -> impl Iterator + '_ { - // - // } + /// 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 a reference to the remote associated with the given `short_branch_name`, typically `main` instead of `refs/heads/main`. /// Returns `None` if the remote reference was not found. @@ -88,4 +80,19 @@ impl crate::Repository { .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(); + let set: BTreeSet<_> = it + .filter(move |s| filter(s.meta())) + .filter_map(|section| section.header().subsection_name().and_then(|b| b.to_str().ok())) + .collect(); + set.into() + }) + .unwrap_or_default() + } } diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index 149f23cc465..0156c1272b2 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -10,12 +10,14 @@ use serial_test::serial; use crate::{named_repo, remote}; #[test] -fn remote_names() { +fn remote_and_branch_names() { let repo = remote::repo("base"); assert_eq!(repo.remote_names().len(), 0, "there are no remotes"); + assert_eq!(repo.branch_names().len(), 0, "there are no configured branches"); let repo = remote::repo("clone"); assert_eq!(Vec::from_iter(repo.remote_names().into_iter()), vec!["origin"]); + assert_eq!(Vec::from_iter(repo.branch_names()), vec!["main"]); } #[test] From f392f26bec6069ac43ecd461b4f50e0def8b8972 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 19:13:03 +0800 Subject: [PATCH 008/125] feat: `Repository::remote_default_name()` to obtain the repo-wide remote for a a direction. (#450) --- git-repository/src/lib.rs | 3 ++ git-repository/src/remote.rs | 8 +++++ git-repository/src/repository/config.rs | 29 ++++++++++++++++ .../tests/fixtures/make_remote_repos.sh | 15 +++++++++ git-repository/tests/remote/mod.rs | 5 +++ git-repository/tests/repository/config.rs | 33 ++++++++++++++++++- 6 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 git-repository/src/remote.rs diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index db84a72fad9..8a1ff5360ce 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -306,6 +306,9 @@ pub mod worktree; pub mod revision; +/// +pub mod remote; + /// pub mod init { use std::path::Path; diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs new file mode 100644 index 00000000000..fc7e03ebab0 --- /dev/null +++ b/git-repository/src/remote.rs @@ -0,0 +1,8 @@ +/// The direction of an operation carried out (or to be carried out) through a remote. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub enum Direction { + /// Push local changes to the remote. + Push, + /// Fetch changes from the remote to the local repository. + Fetch, +} diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index ea5425da751..0c8d5f476e5 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -22,6 +22,9 @@ impl crate::Repository { } mod remote { + use crate::bstr::ByteSlice; + use crate::remote; + use std::borrow::Cow; use std::collections::BTreeSet; impl crate::Repository { @@ -30,6 +33,32 @@ mod remote { 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_. + 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), + } + }) + } } } diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index 31008b76a31..890b8b2eb8a 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -1,3 +1,5 @@ +set -eu -o pipefail + function tick () { if test -z "${tick+set}" then @@ -91,3 +93,16 @@ git init base ) git clone --shared base clone +( + cd clone + git remote add myself . +) + +git clone --shared base push-default +( + cd push-default + + git remote add myself . + git remote rename origin new-origin + git config remote.pushDefault myself +) diff --git a/git-repository/tests/remote/mod.rs b/git-repository/tests/remote/mod.rs index 2aca868bd15..032c3fd683d 100644 --- a/git-repository/tests/remote/mod.rs +++ b/git-repository/tests/remote/mod.rs @@ -1,7 +1,12 @@ use git_repository as git; use git_testtools::scripted_fixture_repo_read_only; +use std::borrow::Cow; pub(crate) fn repo(name: &str) -> git::Repository { let dir = scripted_fixture_repo_read_only("make_remote_repos.sh").unwrap(); git::open_opts(dir.join(name), git::open::Options::isolated()).unwrap() } + +pub(crate) fn cow_str(s: &str) -> Cow { + Cow::Borrowed(s) +} diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index 0156c1272b2..a3cf6003d55 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -7,6 +7,7 @@ use git_sec::{Access, Permission}; use git_testtools::Env; use serial_test::serial; +use crate::remote::cow_str; use crate::{named_repo, remote}; #[test] @@ -14,12 +15,42 @@ fn remote_and_branch_names() { let repo = remote::repo("base"); assert_eq!(repo.remote_names().len(), 0, "there are no remotes"); assert_eq!(repo.branch_names().len(), 0, "there are no configured branches"); + assert_eq!(repo.remote_default_name(git::remote::Direction::Fetch), None); + assert_eq!(repo.remote_default_name(git::remote::Direction::Push), None); let repo = remote::repo("clone"); - assert_eq!(Vec::from_iter(repo.remote_names().into_iter()), vec!["origin"]); + assert_eq!( + Vec::from_iter(repo.remote_names().into_iter()), + vec!["myself", "origin"] + ); + assert_eq!( + repo.remote_default_name(git::remote::Direction::Fetch), + Some(cow_str("origin")) + ); + assert_eq!( + repo.remote_default_name(git::remote::Direction::Push), + Some(cow_str("origin")) + ); assert_eq!(Vec::from_iter(repo.branch_names()), vec!["main"]); } +#[test] +fn remote_default_name() { + let repo = remote::repo("push-default"); + + assert_eq!( + repo.remote_default_name(git::remote::Direction::Push), + Some(cow_str("myself")), + "overridden via remote.pushDefault" + ); + + assert_eq!( + repo.remote_default_name(git::remote::Direction::Fetch), + None, + "none if name origin, and there are multiple" + ); +} + #[test] fn branch_remote() -> Result { let repo = named_repo("make_remote_repo.sh")?; From 94959472e1a40e79d7894ff732512ef03066d22b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 20:54:31 +0800 Subject: [PATCH 009/125] sketch Remote type for implementing find_remote() (#450) --- Cargo.lock | 1 + crate-status.md | 1 + git-repository/Cargo.toml | 1 + git-repository/src/lib.rs | 5 ++- git-repository/src/remote.rs | 38 +++++++++++++++++++++++ git-repository/src/repository/remote.rs | 14 +++++++++ git-repository/src/types.rs | 15 +++++++++ git-repository/tests/repository/remote.rs | 12 +++++++ 8 files changed, 86 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0d853e295ec..8249750f08e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1524,6 +1524,7 @@ dependencies = [ "git-path", "git-protocol", "git-ref", + "git-refspec", "git-revision", "git-sec", "git-tempfile", diff --git a/crate-status.md b/crate-status.md index 1cae0741b3c..199fc47c78d 100644 --- a/crate-status.md +++ b/crate-status.md @@ -468,6 +468,7 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. * [ ] push * [ ] ls-refs * [ ] list, find by name, create in memory. + * [ ] groups * [ ] execute hooks * **refs** * [ ] run transaction hooks and handle special repository states like quarantine diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index 753a8000552..c76cd0e0580 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -83,6 +83,7 @@ git-lock = { version = "^2.0.0", path = "../git-lock" } git-validate = { version = "^0.5.4", path = "../git-validate" } git-sec = { version = "^0.3.0", path = "../git-sec", features = ["thiserror"] } git-date = { version = "^0.0.2", path = "../git-date" } +git-refspec = { version = "^0.0.0", path = "../git-refspec" } git-config = { version = "^0.6.0", path = "../git-config" } git-odb = { version = "^0.31.0", path = "../git-odb" } diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index 8a1ff5360ce..b002da9529c 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -108,6 +108,7 @@ //! * [`traverse`] //! * [`diff`] //! * [`parallel`] +//! * [`refspec`] //! * [`Progress`] //! * [`progress`] //! * [`interrupt`] @@ -151,6 +152,8 @@ pub use git_odb as odb; #[cfg(all(feature = "unstable", feature = "git-protocol"))] pub use git_protocol as protocol; pub use git_ref as refs; +#[cfg(all(feature = "unstable"))] +pub use git_refspec as refspec; pub use git_sec as sec; #[cfg(feature = "unstable")] pub use git_tempfile as tempfile; @@ -187,7 +190,7 @@ pub(crate) type Config = OwnShared>; /// mod types; pub use types::{ - Commit, Head, Id, Object, ObjectDetached, Reference, Repository, Tag, ThreadSafeRepository, Tree, Worktree, + Commit, Head, Id, Object, ObjectDetached, Reference, Remote, Repository, Tag, ThreadSafeRepository, Tree, Worktree, }; pub mod commit; diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs index fc7e03ebab0..fde7e04b07b 100644 --- a/git-repository/src/remote.rs +++ b/git-repository/src/remote.rs @@ -1,3 +1,41 @@ +mod errors { + /// + pub mod find { + /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + RefSpec(#[from] git_refspec::parse::Error), + } + + /// + pub mod existing { + /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Find(#[from] super::Error), + #[error("The remote named {name:?} did not exist")] + NotFound { name: String }, + } + } + } +} +pub use errors::find; + +mod access { + use crate::Remote; + + impl Remote<'_> { + /// Return the name of this remote or `None` if it wasn't persisted to disk yet. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + } +} + /// The direction of an operation carried out (or to be carried out) through a remote. #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub enum Direction { diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 8b137891791..175144270d4 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -1 +1,15 @@ +use crate::{remote, Remote}; +impl crate::Repository { + /// Find the remote with the given `name` or report an error, similar to [`try_find_remote(…)`][Self::try_find_remote()]. + pub fn find_remote(&self, name: &str) -> Result, remote::find::existing::Error> { + Ok(self + .try_find_remote(name) + .ok_or_else(|| remote::find::existing::Error::NotFound { name: name.into() })??) + } + + /// Find the remote with the given `name` or return `None` if it doesn't exist. + pub fn try_find_remote(&self, _name: &str) -> Option, remote::find::Error>> { + todo!() + } +} diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index 0fce2f2a64a..f15c0082b58 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -163,3 +163,18 @@ pub struct ThreadSafeRepository { /// The index of this instances worktree. pub(crate) index: crate::worktree::IndexStorage, } + +/// A remote which represents a way to interact with hosts for remote clones of the parent repository. +#[allow(dead_code)] +pub struct Remote<'repo> { + /// The url of the host to talk to, after application of replacements. + pub(crate) url: git_url::Url, + /// Refspecs for use when fetching. + pub(crate) fetch_refspecs: Vec, + pub(crate) name: Option, + pub(crate) repo: &'repo Repository, + /// Delete local tracking branches that don't exist on the remote anymore. + pub(crate) prune: bool, + /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. + pub(crate) prune_tags: bool, +} diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 8b137891791..830e1f5a0c9 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -1 +1,13 @@ +use crate::remote; +#[test] +#[ignore] +fn find_remote() { + let repo = remote::repo("clone"); + let mut count = 0; + for name in repo.remote_names() { + count += 1; + assert_eq!(repo.find_remote(name).expect("no error").name(), Some(name)); + } + assert!(count > 0, "should have seen more than one commit") +} From c57cb6f14c8add07398107e25545a7bc699afe1a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 20:55:29 +0800 Subject: [PATCH 010/125] thanks clippy --- git-repository/src/repository/config.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index 0c8d5f476e5..6121cfb3969 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -116,11 +116,9 @@ impl crate::Repository { .sections_by_name(header_name) .map(|it| { let filter = self.filter_config_section(); - let set: BTreeSet<_> = it - .filter(move |s| filter(s.meta())) + it.filter(move |s| filter(s.meta())) .filter_map(|section| section.header().subsection_name().and_then(|b| b.to_str().ok())) - .collect(); - set.into() + .collect() }) .unwrap_or_default() } From 2f7960f55ead318cedded2b8041df31233f8a11b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 8 Aug 2022 21:20:29 +0800 Subject: [PATCH 011/125] make git-url public for good (#450) --- git-repository/Cargo.toml | 6 +++--- git-repository/src/lib.rs | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index c76cd0e0580..0287bf9cc68 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -40,8 +40,8 @@ blocking-http-transport = ["git-transport/http-client-curl"] #! ### Reducing dependencies #! The following toggles can be left disabled to save on dependencies. -## Provide additional non-networked functionality like `git-url` and `git-diff`. -local = [ "git-url", "git-diff" ] +## Provide additional non-networked functionality. +local = [ "git-diff" ] ## Turns on access to all stable features that are unrelated to networking. one-stop-shop = [ "local" ] @@ -94,7 +94,7 @@ git-pack = { version = "^0.21.0", path = "../git-pack", features = ["object-cach git-revision = { version = "^0.3.0", path = "../git-revision" } git-path = { version = "^0.4.0", path = "../git-path" } -git-url = { version = "^0.7.0", path = "../git-url", optional = true } +git-url = { version = "^0.7.0", path = "../git-url" } git-traverse = { version = "^0.16.0", path = "../git-traverse" } git-protocol = { version = "^0.18.0", path = "../git-protocol", optional = true } git-transport = { version = "^0.19.0", path = "../git-transport", optional = true } diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index b002da9529c..d6ec6f84cf2 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -159,10 +159,8 @@ pub use git_sec as sec; pub use git_tempfile as tempfile; #[cfg(feature = "unstable")] pub use git_traverse as traverse; -#[cfg(all(feature = "unstable", feature = "git-url"))] pub use git_url as url; #[doc(inline)] -#[cfg(all(feature = "unstable", feature = "git-url"))] pub use git_url::Url; pub use hash::{oid, ObjectId}; From 0e57aa24a96dfb94da02c78bbc03a0d3010c80c1 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 10:58:00 +0800 Subject: [PATCH 012/125] first sketch of finding remotes (#450) With many tests still missing. --- git-repository/src/remote.rs | 7 ++ git-repository/src/repository/remote.rs | 84 +++++++++++++++++++++-- git-repository/src/types.rs | 24 ++++--- git-repository/tests/repository/remote.rs | 8 ++- 4 files changed, 107 insertions(+), 16 deletions(-) diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs index fde7e04b07b..d04b35173bb 100644 --- a/git-repository/src/remote.rs +++ b/git-repository/src/remote.rs @@ -7,6 +7,13 @@ mod errors { pub enum Error { #[error(transparent)] RefSpec(#[from] git_refspec::parse::Error), + #[error("Neither 'url` nor 'pushUrl' fields were set in the remote's configuration.")] + UrlMissing, + #[error("The {kind} url couldn't be parsed")] + UrlInvalid { + kind: &'static str, + source: git_url::parse::Error, + }, } /// diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 175144270d4..45198bf7a34 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -1,15 +1,89 @@ -use crate::{remote, Remote}; +use crate::remote::find; +use crate::Remote; impl crate::Repository { /// Find the remote with the given `name` or report an error, similar to [`try_find_remote(…)`][Self::try_find_remote()]. - pub fn find_remote(&self, name: &str) -> Result, remote::find::existing::Error> { + /// + /// Note that we will include remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()]. + pub fn find_remote(&self, name: &str) -> Result, find::existing::Error> { Ok(self .try_find_remote(name) - .ok_or_else(|| remote::find::existing::Error::NotFound { name: name.into() })??) + .ok_or_else(|| find::existing::Error::NotFound { name: name.into() })??) } /// Find the remote with the given `name` or return `None` if it doesn't exist. - pub fn try_find_remote(&self, _name: &str) -> Option, remote::find::Error>> { - todo!() + /// Note that 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 we will include remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()]. + pub fn try_find_remote(&self, name: &str) -> Option, find::Error>> { + let mut filter = self.filter_config_section(); + let mut config_url = |field: &str, kind: &'static str| { + self.config + .resolved + .string_filter("remote", name.into(), field, &mut filter) + .map(|url| { + git_url::parse::parse(url.as_ref()).map_err(|err| find::Error::UrlInvalid { kind, source: err }) + }) + }; + let url = config_url("url", "fetch"); + let push_url = config_url("pushUrl", "push"); + + let mut config_spec = |op: git_refspec::parse::Operation| { + self.config + .resolved + .strings_filter( + "remote", + name.into(), + match op { + git_refspec::parse::Operation::Fetch => "fetch", + git_refspec::parse::Operation::Push => "push", + }, + &mut filter, + ) + .map(|specs| { + specs + .into_iter() + .map(|spec| git_refspec::parse(spec.as_ref(), op).map(|spec| spec.to_owned())) + .collect::, _>>() + }) + }; + let fetch_specs = config_spec(git_refspec::parse::Operation::Fetch); + let push_specs = config_spec(git_refspec::parse::Operation::Push); + + 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.into())), + None => Vec::new(), + }; + let push_specs = match push_specs { + Some(Ok(v)) => v, + Some(Err(err)) => return Some(Err(err.into())), + None => Vec::new(), + }; + Some(Ok(Remote { + name: name.to_owned().into(), + url, + push_url, + fetch_specs, + push_specs, + repo: self, + })) + } + } } } diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index f15c0082b58..250ccede45d 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -165,16 +165,22 @@ pub struct ThreadSafeRepository { } /// A remote which represents a way to interact with hosts for remote clones of the parent repository. -#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq)] pub struct Remote<'repo> { - /// The url of the host to talk to, after application of replacements. - pub(crate) url: git_url::Url, - /// Refspecs for use when fetching. - pub(crate) fetch_refspecs: Vec, + /// The remotes symbolic name, only present if persisted in git configuration files. pub(crate) name: Option, + /// The url of the host to talk to, after application of replacements. If it is unset, the `push_url` must be set. + /// and fetches aren't possible. + pub(crate) url: Option, + /// The url to use for pushing specifically. + pub(crate) push_url: Option, + /// Refspecs for use when fetching. + pub(crate) fetch_specs: Vec, + /// Refspecs for use when pushing. + pub(crate) push_specs: Vec, + // /// Delete local tracking branches that don't exist on the remote anymore. + // pub(crate) prune: bool, + // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. + // pub(crate) prune_tags: bool, pub(crate) repo: &'repo Repository, - /// Delete local tracking branches that don't exist on the remote anymore. - pub(crate) prune: bool, - /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. - pub(crate) prune_tags: bool, } diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 830e1f5a0c9..140e4276aa0 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -1,7 +1,7 @@ use crate::remote; +use git_repository as git; #[test] -#[ignore] fn find_remote() { let repo = remote::repo("clone"); let mut count = 0; @@ -9,5 +9,9 @@ fn find_remote() { count += 1; assert_eq!(repo.find_remote(name).expect("no error").name(), Some(name)); } - assert!(count > 0, "should have seen more than one commit") + assert!(count > 0, "should have seen more than one commit"); + assert!(matches!( + repo.find_remote("unknown").unwrap_err(), + git::remote::find::existing::Error::NotFound { .. } + )); } From 72545ddce8cb9e1399336526a3ffc8311fb1195a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 13:20:04 +0800 Subject: [PATCH 013/125] tests to validate typical remotes can be instantiated (#450) --- git-repository/src/remote.rs | 21 ++++++++++++- git-repository/src/repository/remote.rs | 8 +++-- git-repository/tests/repository/remote.rs | 37 +++++++++++++++++++++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs index d04b35173bb..ca204ea9996 100644 --- a/git-repository/src/remote.rs +++ b/git-repository/src/remote.rs @@ -33,13 +33,32 @@ mod errors { pub use errors::find; mod access { - use crate::Remote; + use crate::{remote, Remote}; + use git_refspec::RefSpec; impl Remote<'_> { /// Return the name of this remote or `None` if it wasn't persisted to disk yet. pub fn name(&self) -> Option<&str> { self.name.as_deref() } + + /// Return the set of ref-specs used for `direction`, which may be empty, in order of occurrence in the configuration. + pub fn refspecs(&self, direction: remote::Direction) -> &[RefSpec] { + match direction { + remote::Direction::Fetch => &self.fetch_specs, + remote::Direction::Push => &self.push_specs, + } + } + + /// Return the url used for the given `direction`. + /// For pushing, this is the `remote..pushUrl` or the `remote..url` used for fetching, and for fetching it's + /// the `remote..url`. + pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { + match direction { + remote::Direction::Fetch => self.url.as_ref(), + remote::Direction::Push => self.push_url.as_ref().or_else(|| self.url.as_ref()), + } + } } } diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 45198bf7a34..9983b588204 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -11,11 +11,13 @@ impl crate::Repository { .ok_or_else(|| find::existing::Error::NotFound { name: name.into() })??) } - /// Find the remote with the given `name` or return `None` if it doesn't exist. - /// Note that there are various error kinds related to partial information or incorrectly formatted URLs or ref-specs. + /// Find the remote with the given `name` or return `None` if it doesn't exist, for the purpose of fetching or pushing + /// data to a remote. + /// + /// 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 we will include remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()]. + /// We will only include information if we deem it [trustworthy][crate::open::Options::filter_config_section()]. pub fn try_find_remote(&self, name: &str) -> Option, find::Error>> { let mut filter = self.filter_config_section(); let mut config_url = |field: &str, kind: &'static str| { diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 140e4276aa0..485ba09bec1 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -1,13 +1,46 @@ use crate::remote; use git_repository as git; +use git_repository::remote::Direction; #[test] fn find_remote() { let repo = remote::repo("clone"); let mut count = 0; - for name in repo.remote_names() { + let base_dir = repo + .work_dir() + .unwrap() + .canonicalize() + .unwrap() + .parent() + .unwrap() + .join("base") + .display() + .to_string(); + let expected = [ + (".", "+refs/heads/*:refs/remotes/myself/*"), + (base_dir.as_str(), "+refs/heads/*:refs/remotes/origin/*"), + ]; + for (name, (url, refspec)) in repo.remote_names().into_iter().zip(expected) { count += 1; - assert_eq!(repo.find_remote(name).expect("no error").name(), Some(name)); + let remote = repo.find_remote(name).expect("no error"); + assert_eq!(remote.name(), Some(name)); + + let url = git::url::parse(url.as_bytes()).expect("valid"); + assert_eq!(remote.url(Direction::Fetch), Some(&url)); + + let refspec = git::refspec::parse(refspec.into(), git::refspec::parse::Operation::Fetch) + .expect("valid expectation") + .to_owned(); + assert_eq!( + remote.refspecs(Direction::Fetch), + &[refspec], + "default refspecs are set by git" + ); + assert_eq!( + remote.refspecs(Direction::Push), + &[], + "push-specs aren't configured by default" + ); } assert!(count > 0, "should have seen more than one commit"); assert!(matches!( From 214dd1694c7f29b250e515ab4128a303d6ffac97 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 13:45:48 +0800 Subject: [PATCH 014/125] valid push-url and push-specs as well (#450) --- .../tests/fixtures/make_remote_repos.sh | 7 ++ git-repository/tests/repository/remote.rs | 106 ++++++++++-------- 2 files changed, 69 insertions(+), 44 deletions(-) diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index 890b8b2eb8a..a67024a2e09 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -106,3 +106,10 @@ git clone --shared base push-default git remote rename origin new-origin git config remote.pushDefault myself ) + +git clone --shared base push-url +( + cd push-url + git config remote.origin.pushUrl . + git config remote.origin.push refs/tags/*:refs/tags/* +) diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 485ba09bec1..6c084ab68e5 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -1,50 +1,68 @@ -use crate::remote; -use git_repository as git; -use git_repository::remote::Direction; +mod find_remote { + use crate::remote; + use git_repository as git; + use git_repository::remote::Direction; + use git_repository::Repository; -#[test] -fn find_remote() { - let repo = remote::repo("clone"); - let mut count = 0; - let base_dir = repo - .work_dir() - .unwrap() - .canonicalize() - .unwrap() - .parent() - .unwrap() - .join("base") - .display() - .to_string(); - let expected = [ - (".", "+refs/heads/*:refs/remotes/myself/*"), - (base_dir.as_str(), "+refs/heads/*:refs/remotes/origin/*"), - ]; - for (name, (url, refspec)) in repo.remote_names().into_iter().zip(expected) { - count += 1; - let remote = repo.find_remote(name).expect("no error"); - assert_eq!(remote.name(), Some(name)); + #[test] + fn typical() { + let repo = remote::repo("clone"); + let mut count = 0; + let base_dir = base_dir(&repo); + let expected = [ + (".", "+refs/heads/*:refs/remotes/myself/*"), + (base_dir.as_str(), "+refs/heads/*:refs/remotes/origin/*"), + ]; + for (name, (url, refspec)) in repo.remote_names().into_iter().zip(expected) { + count += 1; + let remote = repo.find_remote(name).expect("no error"); + assert_eq!(remote.name(), Some(name)); - let url = git::url::parse(url.as_bytes()).expect("valid"); - assert_eq!(remote.url(Direction::Fetch), Some(&url)); + let url = git::url::parse(url.as_bytes()).expect("valid"); + assert_eq!(remote.url(Direction::Fetch), Some(&url)); - let refspec = git::refspec::parse(refspec.into(), git::refspec::parse::Operation::Fetch) - .expect("valid expectation") + let refspec = git::refspec::parse(refspec.into(), git::refspec::parse::Operation::Fetch) + .expect("valid expectation") + .to_owned(); + assert_eq!( + remote.refspecs(Direction::Fetch), + &[refspec], + "default refspecs are set by git" + ); + assert_eq!( + remote.refspecs(Direction::Push), + &[], + "push-specs aren't configured by default" + ); + } + assert!(count > 0, "should have seen more than one commit"); + assert!(matches!( + repo.find_remote("unknown").unwrap_err(), + git::remote::find::existing::Error::NotFound { .. } + )); + } + + #[test] + fn push_url_and_push_specs() { + let repo = remote::repo("push-url"); + let remote = repo.find_remote("origin").expect("present"); + assert_eq!(remote.url(Direction::Push).unwrap().path, "."); + assert_eq!(remote.url(Direction::Fetch).unwrap().path, base_dir(&repo)); + let spec = git::refspec::parse("refs/tags/*:refs/tags/*".into(), git::refspec::parse::Operation::Push) + .unwrap() .to_owned(); - assert_eq!( - remote.refspecs(Direction::Fetch), - &[refspec], - "default refspecs are set by git" - ); - assert_eq!( - remote.refspecs(Direction::Push), - &[], - "push-specs aren't configured by default" - ); + assert_eq!(remote.refspecs(Direction::Push), &[spec]) + } + + fn base_dir(repo: &Repository) -> String { + repo.work_dir() + .unwrap() + .canonicalize() + .unwrap() + .parent() + .unwrap() + .join("base") + .display() + .to_string() } - assert!(count > 0, "should have seen more than one commit"); - assert!(matches!( - repo.find_remote("unknown").unwrap_err(), - git::remote::find::existing::Error::NotFound { .. } - )); } From fcea9d1c48d84d30893d3e15272abd85a26bb4e2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 13:53:18 +0800 Subject: [PATCH 015/125] better error reporting for ref-spec parsing (#450) --- git-repository/src/remote.rs | 10 ++++++++-- git-repository/src/repository/remote.rs | 25 +++++++++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs index ca204ea9996..c0fb5340963 100644 --- a/git-repository/src/remote.rs +++ b/git-repository/src/remote.rs @@ -1,12 +1,18 @@ mod errors { /// pub mod find { + use crate::bstr::BString; + /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - RefSpec(#[from] git_refspec::parse::Error), + #[error("{spec:?} {kind} ref-spec failed to parse")] + RefSpec { + spec: BString, + kind: &'static str, + source: git_refspec::parse::Error, + }, #[error("Neither 'url` nor 'pushUrl' fields were set in the remote's configuration.")] UrlMissing, #[error("The {kind} url couldn't be parsed")] diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 9983b588204..d7c82cd6b55 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -1,4 +1,5 @@ use crate::remote::find; +use crate::remote::find::Error; use crate::Remote; impl crate::Repository { @@ -32,21 +33,25 @@ impl crate::Repository { let push_url = config_url("pushUrl", "push"); let mut config_spec = |op: git_refspec::parse::Operation| { + let kind = match op { + git_refspec::parse::Operation::Fetch => "fetch", + git_refspec::parse::Operation::Push => "push", + }; self.config .resolved - .strings_filter( - "remote", - name.into(), - match op { - git_refspec::parse::Operation::Fetch => "fetch", - git_refspec::parse::Operation::Push => "push", - }, - &mut filter, - ) + .strings_filter("remote", name.into(), kind, &mut filter) .map(|specs| { specs .into_iter() - .map(|spec| git_refspec::parse(spec.as_ref(), op).map(|spec| spec.to_owned())) + .map(|spec| { + git_refspec::parse(spec.as_ref(), op) + .map(|spec| spec.to_owned()) + .map_err(|err| Error::RefSpec { + spec: spec.into_owned(), + kind, + source: err, + }) + }) .collect::, _>>() }) }; From 4347a96df7742dd1b2b1e0d56543ba16612b7924 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 13:57:38 +0800 Subject: [PATCH 016/125] thanks clippy --- git-repository/src/lib.rs | 1 - git-repository/src/remote.rs | 2 +- git-repository/src/repository/remote.rs | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index d6ec6f84cf2..2004de20e8c 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -152,7 +152,6 @@ pub use git_odb as odb; #[cfg(all(feature = "unstable", feature = "git-protocol"))] pub use git_protocol as protocol; pub use git_ref as refs; -#[cfg(all(feature = "unstable"))] pub use git_refspec as refspec; pub use git_sec as sec; #[cfg(feature = "unstable")] diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs index c0fb5340963..d6eaa898ad5 100644 --- a/git-repository/src/remote.rs +++ b/git-repository/src/remote.rs @@ -62,7 +62,7 @@ mod access { pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { match direction { remote::Direction::Fetch => self.url.as_ref(), - remote::Direction::Push => self.push_url.as_ref().or_else(|| self.url.as_ref()), + remote::Direction::Push => self.push_url.as_ref().or(self.url.as_ref()), } } } diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index d7c82cd6b55..620b3f9f1c1 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -74,12 +74,12 @@ impl crate::Repository { }; let fetch_specs = match fetch_specs { Some(Ok(v)) => v, - Some(Err(err)) => return Some(Err(err.into())), + 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.into())), + Some(Err(err)) => return Some(Err(err)), None => Vec::new(), }; Some(Ok(Remote { From b4bf7d015a5d0d48bf7d0509d2fd930a1cb6f398 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 14:23:01 +0800 Subject: [PATCH 017/125] assure ref-specs handle equality, ordering and hashing according to their instruction (#450) That way, it's possible to treat dissimilar yet effectively equal specs like they are equal. --- git-refspec/src/instruction.rs | 4 ++-- git-refspec/src/lib.rs | 4 ++-- git-refspec/src/parse.rs | 2 +- git-refspec/src/spec.rs | 38 ++++++++++++++++++++++++++++++++++ git-refspec/src/types.rs | 4 ++-- git-refspec/tests/impls/mod.rs | 27 ++++++++++++++++++++++++ git-refspec/tests/refspec.rs | 1 + 7 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 git-refspec/tests/impls/mod.rs diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index ceceb9db79d..b2caa699f86 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -13,7 +13,7 @@ impl Instruction<'_> { /// Note that all sources can either be a ref-name, partial or full, or a rev-spec, unless specified otherwise, on the local side. /// Destinations can only be a partial or full ref names on the remote side. -#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] +#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Push<'a> { /// Push all local branches to the matching destination on the remote, which has to exist to be updated. AllMatchingBranches { @@ -39,7 +39,7 @@ pub enum Push<'a> { /// Any source can either be a ref name (full or partial) or a fully spelled out hex-sha for an object, on the remote side. /// /// Destinations can only be a partial or full ref-names on the local side. -#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] +#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Fetch<'a> { /// Fetch a ref or refs and write the result into the `FETCH_HEAD` without updating local branches. Only { diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index 5f25a43c497..df7c4ba1104 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -10,7 +10,7 @@ pub use parse::function::parse; pub mod instruction; /// A refspec with references to the memory it was parsed from. -#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] +#[derive(Ord, Eq, Copy, Clone, Debug)] pub struct RefSpecRef<'a> { mode: types::Mode, op: parse::Operation, @@ -19,7 +19,7 @@ pub struct RefSpecRef<'a> { } /// An owned refspec. -#[derive(PartialEq, Eq, Clone, Hash, Debug)] +#[derive(Ord, Eq, Clone, Debug)] pub struct RefSpec { mode: types::Mode, op: parse::Operation, diff --git a/git-refspec/src/parse.rs b/git-refspec/src/parse.rs index 496ef202bca..55939e11039 100644 --- a/git-refspec/src/parse.rs +++ b/git-refspec/src/parse.rs @@ -27,7 +27,7 @@ pub enum Error { } /// Define how the parsed refspec should be used. -#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] +#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Operation { /// The `src` side is local and the `dst` side is remote. Push, diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index 14fd2a090e2..b197a808cd8 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -16,12 +16,50 @@ impl RefSpec { mod impls { use crate::{RefSpec, RefSpecRef}; + use std::cmp::Ordering; + use std::hash::{Hash, Hasher}; impl From> for RefSpec { fn from(v: RefSpecRef<'_>) -> Self { v.to_owned() } } + + impl Hash for RefSpec { + fn hash(&self, state: &mut H) { + self.to_ref().hash(state) + } + } + + impl Hash for RefSpecRef<'_> { + fn hash(&self, state: &mut H) { + self.instruction().hash(state) + } + } + + impl PartialEq for RefSpec { + fn eq(&self, other: &Self) -> bool { + self.to_ref().eq(&other.to_ref()) + } + } + + impl PartialEq for RefSpecRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.instruction().eq(&other.instruction()) + } + } + + impl PartialOrd for RefSpecRef<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + self.instruction().partial_cmp(&other.instruction()) + } + } + + impl PartialOrd for RefSpec { + fn partial_cmp(&self, other: &Self) -> Option { + self.to_ref().partial_cmp(&other.to_ref()) + } + } } /// Access diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index f7c453a0a71..0a0e24e36cc 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -1,7 +1,7 @@ use crate::instruction; /// The way to interpret a refspec. -#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] +#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub(crate) enum Mode { /// Apply standard rules for refspecs which are including refs with specific rules related to allowing fast forwards of destinations. Normal, @@ -12,7 +12,7 @@ pub(crate) enum Mode { } /// Tells what to do and is derived from a [`RefSpec`][crate::RefSpecRef]. -#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] +#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Instruction<'a> { /// An instruction for pushing. Push(instruction::Push<'a>), diff --git a/git-refspec/tests/impls/mod.rs b/git-refspec/tests/impls/mod.rs new file mode 100644 index 00000000000..97f200ae86b --- /dev/null +++ b/git-refspec/tests/impls/mod.rs @@ -0,0 +1,27 @@ +use git_refspec::parse::Operation; +use git_refspec::RefSpec; +use std::collections::{BTreeSet, HashSet}; +use std::iter::FromIterator; + +fn pair() -> Vec { + let lhs = git_refspec::parse("refs/heads/foo".into(), Operation::Push).unwrap(); + let rhs = git_refspec::parse("refs/heads/foo:refs/heads/foo".into(), Operation::Push).unwrap(); + vec![lhs.to_owned(), rhs.to_owned()] +} + +#[test] +fn cmp() { + assert_eq!(BTreeSet::from_iter(pair()).len(), 1) +} + +#[test] +fn hash() { + let set: HashSet<_> = pair().into_iter().collect(); + assert_eq!(set.len(), 1) +} + +#[test] +fn eq() { + let specs = pair(); + assert_eq!(&specs[0], &specs[1]); +} diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs index 06f1a3c69d4..ee35817f145 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -1 +1,2 @@ +mod impls; mod parse; From 60780cc3a341e3de744f949c428f05e31dc8ffab Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 14:48:21 +0800 Subject: [PATCH 018/125] deduplicate refs when reading them (#450) --- git-repository/src/repository/remote.rs | 8 +++++ .../tests/fixtures/make_remote_repos.sh | 8 +++++ git-repository/tests/repository/remote.rs | 34 +++++++++++++++---- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 620b3f9f1c1..b3c2e0bee7e 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -18,6 +18,9 @@ impl crate::Repository { /// 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(&self, name: &str) -> Option, find::Error>> { let mut filter = self.filter_config_section(); @@ -53,6 +56,11 @@ impl crate::Repository { }) }) .collect::, _>>() + .map(|mut specs| { + specs.sort(); + specs.dedup(); + specs + }) }) }; let fetch_specs = config_spec(git_refspec::parse::Operation::Fetch); diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index a67024a2e09..52d07c82d72 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -113,3 +113,11 @@ git clone --shared base push-url git config remote.origin.pushUrl . git config remote.origin.push refs/tags/*:refs/tags/* ) + +git clone --shared base many-fetchspecs +( + cd many-fetchspecs + git config --add remote.origin.fetch @ + git config --add remote.origin.fetch refs/tags/*:refs/tags/* + git config --add remote.origin.fetch HEAD +) diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 6c084ab68e5..86c9558c852 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -21,12 +21,9 @@ mod find_remote { let url = git::url::parse(url.as_bytes()).expect("valid"); assert_eq!(remote.url(Direction::Fetch), Some(&url)); - let refspec = git::refspec::parse(refspec.into(), git::refspec::parse::Operation::Fetch) - .expect("valid expectation") - .to_owned(); assert_eq!( remote.refspecs(Direction::Fetch), - &[refspec], + &[fetchspec(refspec)], "default refspecs are set by git" ); assert_eq!( @@ -48,10 +45,33 @@ mod find_remote { let remote = repo.find_remote("origin").expect("present"); assert_eq!(remote.url(Direction::Push).unwrap().path, "."); assert_eq!(remote.url(Direction::Fetch).unwrap().path, base_dir(&repo)); - let spec = git::refspec::parse("refs/tags/*:refs/tags/*".into(), git::refspec::parse::Operation::Push) + assert_eq!(remote.refspecs(Direction::Push), &[pushspec("refs/tags/*:refs/tags/*")]) + } + + #[test] + fn many_fetchspecs() { + let repo = remote::repo("many-fetchspecs"); + let remote = repo.find_remote("origin").expect("present"); + assert_eq!( + remote.refspecs(Direction::Fetch), + &[ + fetchspec("HEAD"), + fetchspec("+refs/heads/*:refs/remotes/origin/*"), + fetchspec("refs/tags/*:refs/tags/*") + ] + ) + } + + fn fetchspec(spec: &str) -> git_refspec::RefSpec { + git::refspec::parse(spec.into(), git::refspec::parse::Operation::Fetch) + .unwrap() + .to_owned() + } + + fn pushspec(spec: &str) -> git_refspec::RefSpec { + git::refspec::parse(spec.into(), git::refspec::parse::Operation::Push) .unwrap() - .to_owned(); - assert_eq!(remote.refspecs(Direction::Push), &[spec]) + .to_owned() } fn base_dir(repo: &Repository) -> String { From b9e1cdbf19045816387d922abd9c886419ff6bf2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 14:50:30 +0800 Subject: [PATCH 019/125] thanks clippy --- git-refspec/src/lib.rs | 4 ++-- git-refspec/src/spec.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index df7c4ba1104..feac125c0d3 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -10,7 +10,7 @@ pub use parse::function::parse; pub mod instruction; /// A refspec with references to the memory it was parsed from. -#[derive(Ord, Eq, Copy, Clone, Debug)] +#[derive(Eq, Copy, Clone, Debug)] pub struct RefSpecRef<'a> { mode: types::Mode, op: parse::Operation, @@ -19,7 +19,7 @@ pub struct RefSpecRef<'a> { } /// An owned refspec. -#[derive(Ord, Eq, Clone, Debug)] +#[derive(Eq, Clone, Debug)] pub struct RefSpec { mode: types::Mode, op: parse::Operation, diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index b197a808cd8..f118b92a484 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -60,6 +60,18 @@ mod impls { self.to_ref().partial_cmp(&other.to_ref()) } } + + impl Ord for RefSpecRef<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.instruction().cmp(&other.instruction()) + } + } + + impl Ord for RefSpec { + fn cmp(&self, other: &Self) -> Ordering { + self.to_ref().cmp(&other.to_ref()) + } + } } /// Access From dc0186ef72812b6362b17e7a21ecf5014cd202c5 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 15:31:17 +0800 Subject: [PATCH 020/125] fix tests on windows (#450) --- git-repository/src/repository/config.rs | 5 ++++- git-repository/tests/repository/remote.rs | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index 6121cfb3969..af360655402 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -78,7 +78,10 @@ mod branch { self.subsection_names_of("branch") } - /// Returns a reference to the remote associated with the given `short_branch_name`, typically `main` instead of `refs/heads/main`. + /// Returns a reference to the remote associated with the given `short_branch_name`, + /// always `main` instead of `refs/heads/main`. + /// + /// The remote-ref 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( diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 86c9558c852..b45d37f1f4c 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -75,9 +75,7 @@ mod find_remote { } fn base_dir(repo: &Repository) -> String { - repo.work_dir() - .unwrap() - .canonicalize() + git::path::realpath(repo.work_dir().unwrap()) .unwrap() .parent() .unwrap() From b4f6bbd10f4aa6a8d7cd1e57a462514cbc0ebb94 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 16:21:51 +0800 Subject: [PATCH 021/125] Allow to use `git-path` at all times (#450) --- git-repository/src/path.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/git-repository/src/path.rs b/git-repository/src/path.rs index b78a3e3c3d1..418affa8c6a 100644 --- a/git-repository/src/path.rs +++ b/git-repository/src/path.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; -#[cfg(all(feature = "unstable"))] pub use git_path::*; pub(crate) fn install_dir() -> std::io::Result { From 0fbbe346571bdade15346fdf6978c3a360845d06 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 16:48:10 +0800 Subject: [PATCH 022/125] Fix windwos errors, hopefully (#450) --- git-repository/tests/repository/remote.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index b45d37f1f4c..a16c3066d9b 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -75,12 +75,14 @@ mod find_remote { } fn base_dir(repo: &Repository) -> String { - git::path::realpath(repo.work_dir().unwrap()) - .unwrap() - .parent() - .unwrap() - .join("base") - .display() - .to_string() + git_path::to_unix_separators_on_windows(git::path::into_bstr( + git::path::realpath(repo.work_dir().unwrap()) + .unwrap() + .parent() + .unwrap() + .join("base"), + )) + .into_owned() + .to_string() } } From 58aee3395c0a70d1659df99d2fe4953676dbe346 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 19:27:29 +0800 Subject: [PATCH 023/125] failing test for url rewrites (#450) --- crate-status.md | 1 + .../tests/fixtures/make_remote_repos.sh | 24 +++++++++++++++++++ git-repository/tests/repository/remote.rs | 24 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/crate-status.md b/crate-status.md index 199fc47c78d..a9942038da7 100644 --- a/crate-status.md +++ b/crate-status.md @@ -469,6 +469,7 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. * [ ] ls-refs * [ ] list, find by name, create in memory. * [ ] groups + * [ ] [remote and branch files](https://github.com/git/git/blob/master/remote.c#L300) * [ ] execute hooks * **refs** * [ ] run transaction hooks and handle special repository states like quarantine diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index 52d07c82d72..c0bbffd70de 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -121,3 +121,27 @@ git clone --shared base many-fetchspecs git config --add remote.origin.fetch refs/tags/*:refs/tags/* git config --add remote.origin.fetch HEAD ) + +git init --bare url-rewriting +( + cd url-rewriting + git remote add origin https://github.com/foobar/gitoxide + cat <> config + +[remote "origin"] + pushUrl = "file://dev/null" + +[url "ssh://"] + insteadOf = "https://" + pushInsteadOf = "file://" + +[url "https://github.com/byron/"] + insteadOf = https://github.com/foobar/ + pushInsteadOf = ssh://example.com/ +EOF + + { + git remote get-url origin + git remote get-url origin --push + } > baseline.git +) diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index a16c3066d9b..82285c21fea 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -1,8 +1,10 @@ mod find_remote { use crate::remote; + use git_object::bstr::BString; use git_repository as git; use git_repository::remote::Direction; use git_repository::Repository; + use std::io::BufRead; #[test] fn typical() { @@ -62,6 +64,28 @@ mod find_remote { ) } + #[test] + #[ignore] + fn instead_of_url_rewriting() -> crate::Result { + let repo = remote::repo("url-rewriting"); + + let baseline = std::fs::read(repo.git_dir().join("baseline.git"))?; + let mut baseline = baseline.lines().filter_map(Result::ok); + let expected_fetch_url: BString = baseline.next().expect("fetch").into(); + let expected_push_url: BString = baseline.next().expect("push").into(); + + let remote = repo.find_remote("origin")?; + assert_eq!( + remote.url(Direction::Fetch).expect("present").to_string(), + expected_fetch_url, + ); + assert_eq!( + remote.url(Direction::Push).expect("present").to_string(), + expected_push_url, + ); + Ok(()) + } + fn fetchspec(spec: &str) -> git_refspec::RefSpec { git::refspec::parse(spec.into(), git::refspec::parse::Operation::Fetch) .unwrap() From 5f707c7e99c70ab9683d55c396e8dc11e1d3b0ea Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 20:30:34 +0800 Subject: [PATCH 024/125] feat: Add `Url::to_bstring()` for lossless but fallible bstring conversion. (#450) --- git-url/src/lib.rs | 53 +++++++++++++++++++++++++++++++++++--- git-url/tests/parse/mod.rs | 2 +- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index f9dc123fff0..cb86c691d5c 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -33,20 +33,28 @@ pub enum Scheme { Ssh, Http, Https, + // TODO: replace this with custom formats, maybe, get an idea how to do that. Radicle, } -impl fmt::Display for Scheme { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl Scheme { + /// Return ourselves parseable name. + pub fn as_str(&self) -> &'static str { use Scheme::*; - f.write_str(match self { + match self { File => "file", Git => "git", Ssh => "ssh", Http => "http", Https => "https", Radicle => "rad", - }) + } + } +} + +impl fmt::Display for Scheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) } } @@ -81,6 +89,43 @@ impl Default for Url { } } +/// Serialization +impl Url { + /// Transform ourselves into a binary string, losslessly, or `None` if user and host is strangely configured. + pub fn to_bstring(&self) -> Option { + let mut buf = Vec::with_capacity( + (5 + 3) + + self.user.as_ref().map(|n| n.len()).unwrap_or_default() + + 1 + + self.host.as_ref().map(|h| h.len()).unwrap_or_default() + + self.port.map(|_| 5).unwrap_or_default() + + self.path.len(), + ); + buf.extend_from_slice(self.scheme.as_str().as_bytes()); + buf.extend_from_slice(b"://"); + match (&self.user, &self.host) { + (Some(user), Some(host)) => { + buf.extend_from_slice(user.as_bytes()); + buf.push(b'@'); + buf.extend_from_slice(host.as_bytes()); + } + (None, Some(host)) => { + buf.extend_from_slice(host.as_bytes()); + } + (None, None) => {} + _ => return None, + }; + if let Some(port) = &self.port { + use std::io::Write; + buf.push(b':'); + let mut numbers = [0u8; 5]; + write!(numbers.as_mut_slice(), "{}", port).expect("write succeeds as max number fits into buffer"); + buf.extend(numbers.iter().take_while(|b| **b != 0)); + } + buf.extend_from_slice(&self.path); + Some(buf.into()) + } +} impl fmt::Display for Url { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.scheme.fmt(f)?; diff --git a/git-url/tests/parse/mod.rs b/git-url/tests/parse/mod.rs index 646b8920cc9..b27daa33473 100644 --- a/git-url/tests/parse/mod.rs +++ b/git-url/tests/parse/mod.rs @@ -6,7 +6,7 @@ fn assert_url_and(url: &str, expected: git_url::Url) -> Result crate::Result { - assert_eq!(assert_url_and(url, expected)?.to_string(), url); + assert_eq!(assert_url_and(url, expected)?.to_bstring().expect("valid"), url); Ok(()) } From 833899dce120d26a2bbe04d07fc4c71455eb3afe Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 20:37:15 +0800 Subject: [PATCH 025/125] feat: `Url::write_to(out)` to write itself more flexibly. (#450) --- git-url/src/expand_path.rs | 1 + git-url/src/lib.rs | 53 ++++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/git-url/src/expand_path.rs b/git-url/src/expand_path.rs index 59cab95b36d..b654c3333dd 100644 --- a/git-url/src/expand_path.rs +++ b/git-url/src/expand_path.rs @@ -26,6 +26,7 @@ impl From for Option { quick_error! { /// The error used by [`parse()`], [`with()`] and [`expand_path()`]. #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { IllformedUtf8{path: BString} { display("UTF8 conversion on non-unix system failed for path: {}", path) diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index cb86c691d5c..1324ed234f6 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -5,7 +5,7 @@ feature = "document-features", cfg_attr(doc, doc = ::document_features::document_features!()) )] #![forbid(unsafe_code)] -#![deny(rust_2018_idioms)] +#![deny(rust_2018_idioms, missing_docs)] use std::{ convert::TryFrom, @@ -27,6 +27,7 @@ pub use expand_path::expand_path; /// A scheme for use in a [`Url`] #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +#[allow(missing_docs)] pub enum Scheme { File, Git, @@ -91,39 +92,41 @@ impl Default for Url { /// Serialization impl Url { - /// Transform ourselves into a binary string, losslessly, or `None` if user and host is strangely configured. - pub fn to_bstring(&self) -> Option { - let mut buf = Vec::with_capacity( - (5 + 3) - + self.user.as_ref().map(|n| n.len()).unwrap_or_default() - + 1 - + self.host.as_ref().map(|h| h.len()).unwrap_or_default() - + self.port.map(|_| 5).unwrap_or_default() - + self.path.len(), - ); - buf.extend_from_slice(self.scheme.as_str().as_bytes()); - buf.extend_from_slice(b"://"); + /// Write this URL losslessly to `out`, ready to be parsed again. + pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> { + out.write_all(self.scheme.as_str().as_bytes())?; + out.write_all(b"://")?; match (&self.user, &self.host) { (Some(user), Some(host)) => { - buf.extend_from_slice(user.as_bytes()); - buf.push(b'@'); - buf.extend_from_slice(host.as_bytes()); + out.write_all(user.as_bytes())?; + out.write_all(&[b'@'])?; + out.write_all(host.as_bytes())?; } (None, Some(host)) => { - buf.extend_from_slice(host.as_bytes()); + out.write_all(host.as_bytes())?; } (None, None) => {} - _ => return None, + _ => return Err(std::io::Error::new(std::io::ErrorKind::Other, "Malformed URL")), }; if let Some(port) = &self.port { - use std::io::Write; - buf.push(b':'); - let mut numbers = [0u8; 5]; - write!(numbers.as_mut_slice(), "{}", port).expect("write succeeds as max number fits into buffer"); - buf.extend(numbers.iter().take_while(|b| **b != 0)); + write!(&mut out, ":{}", port)?; } - buf.extend_from_slice(&self.path); - Some(buf.into()) + out.write_all(&self.path)?; + Ok(()) + } + + /// Transform ourselves into a binary string, losslessly, or fail if the URL is malformed due to host or user parts being incorrect. + pub fn to_bstring(&self) -> std::io::Result { + let mut buf = Vec::with_capacity( + (5 + 3) + + self.user.as_ref().map(|n| n.len()).unwrap_or_default() + + 1 + + self.host.as_ref().map(|h| h.len()).unwrap_or_default() + + self.port.map(|_| 5).unwrap_or_default() + + self.path.len(), + ); + self.write_to(&mut buf)?; + Ok(buf.into()) } } impl fmt::Display for Url { From 79ab4aeb8206a5f32735891336d7745e046bbea1 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 20:42:36 +0800 Subject: [PATCH 026/125] change!: remove `impl std::fmt::Display for Url` as it's not lossless. (#450) --- git-url/src/lib.rs | 21 ++------------------- git-url/tests/parse/file.rs | 21 ++++++++++++--------- git-url/tests/parse/ssh.rs | 8 ++++---- 3 files changed, 18 insertions(+), 32 deletions(-) diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index 1324ed234f6..a6e451ad3a7 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -9,10 +9,10 @@ cfg_attr(doc, doc = ::document_features::document_features!()) use std::{ convert::TryFrom, - fmt::{self, Write}, + fmt::{self}, }; -use bstr::{BStr, ByteSlice}; +use bstr::BStr; /// pub mod parse; @@ -129,23 +129,6 @@ impl Url { Ok(buf.into()) } } -impl fmt::Display for Url { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.scheme.fmt(f)?; - f.write_str("://")?; - match (&self.user, &self.host) { - (Some(user), Some(host)) => f.write_fmt(format_args!("{}@{}", user, host)), - (None, Some(host)) => f.write_str(host), - (None, None) => Ok(()), - _ => return Err(fmt::Error), - }?; - if let Some(port) = &self.port { - f.write_char(':')?; - f.write_fmt(format_args!("{}", port))?; - } - f.write_str(self.path.to_str_lossy().as_ref()) - } -} impl Url { /// Parse a URL from `bytes` diff --git a/git-url/tests/parse/file.rs b/git-url/tests/parse/file.rs index 1e9500878a7..bce754fe3fd 100644 --- a/git-url/tests/parse/file.rs +++ b/git-url/tests/parse/file.rs @@ -12,14 +12,14 @@ fn file_path_with_protocol() -> crate::Result { #[test] fn file_path_without_protocol() -> crate::Result { - let url = assert_url_and("/path/to/git", url(Scheme::File, None, None, None, b"/path/to/git"))?.to_string(); + let url = assert_url_and("/path/to/git", url(Scheme::File, None, None, None, b"/path/to/git"))?.to_bstring()?; assert_eq!(url, "file:///path/to/git"); Ok(()) } #[test] fn no_username_expansion_for_file_paths_without_protocol() -> crate::Result { - let url = assert_url_and("~/path/to/git", url(Scheme::File, None, None, None, b"~/path/to/git"))?.to_string(); + let url = assert_url_and("~/path/to/git", url(Scheme::File, None, None, None, b"~/path/to/git"))?.to_bstring()?; assert_eq!(url, "file://~/path/to/git"); Ok(()) } @@ -35,11 +35,13 @@ fn no_username_expansion_for_file_paths_with_protocol() -> crate::Result { fn non_utf8_file_path_without_protocol() -> crate::Result { let parsed = git_url::parse(b"/path/to\xff/git")?; assert_eq!(parsed, url(Scheme::File, None, None, None, b"/path/to\xff/git",)); + let url_lossless = parsed.to_bstring()?; assert_eq!( - parsed.to_string(), + url_lossless.to_string(), "file:///path/to�/git", - "non-unicode is made unicode safe" + "non-unicode is made unicode safe after conversion" ); + assert_eq!(url_lossless, &b"file:///path/to\xff/git"[..], "otherwise it's lossless"); Ok(()) } @@ -49,9 +51,9 @@ fn relative_file_path_without_protocol() -> crate::Result { "../../path/to/git", url(Scheme::File, None, None, None, b"../../path/to/git"), )? - .to_string(); + .to_bstring()?; assert_eq!(parsed, "file://../../path/to/git"); - let url = assert_url_and("path/to/git", url(Scheme::File, None, None, None, b"path/to/git"))?.to_string(); + let url = assert_url_and("path/to/git", url(Scheme::File, None, None, None, b"path/to/git"))?.to_bstring()?; assert_eq!(url, "file://path/to/git"); Ok(()) } @@ -62,7 +64,7 @@ fn interior_relative_file_path_without_protocol() -> crate::Result { "/abs/path/../../path/to/git", url(Scheme::File, None, None, None, b"/abs/path/../../path/to/git"), )? - .to_string(); + .to_bstring()?; assert_eq!(url, "file:///abs/path/../../path/to/git"); Ok(()) } @@ -74,7 +76,8 @@ mod windows { #[test] fn file_path_without_protocol() -> crate::Result { - let url = assert_url_and("x:/path/to/git", url(Scheme::File, None, None, None, b"x:/path/to/git"))?.to_string(); + let url = + assert_url_and("x:/path/to/git", url(Scheme::File, None, None, None, b"x:/path/to/git"))?.to_bstring()?; assert_eq!(url, "file://x:/path/to/git"); Ok(()) } @@ -85,7 +88,7 @@ mod windows { "x:\\path\\to\\git", url(Scheme::File, None, None, None, b"x:\\path\\to\\git"), )? - .to_string(); + .to_bstring()?; assert_eq!(url, "file://x:\\path\\to\\git"); Ok(()) } diff --git a/git-url/tests/parse/ssh.rs b/git-url/tests/parse/ssh.rs index 0b708255e92..21ad4f2fa87 100644 --- a/git-url/tests/parse/ssh.rs +++ b/git-url/tests/parse/ssh.rs @@ -53,7 +53,7 @@ fn scp_like_without_user() -> crate::Result { "host.xz:path/to/git", url(Scheme::Ssh, None, "host.xz", None, b"/path/to/git"), )? - .to_string(); + .to_bstring()?; assert_eq!(url, "ssh://host.xz/path/to/git"); Ok(()) } @@ -64,7 +64,7 @@ fn scp_like_without_user_and_username_expansion_without_username() -> crate::Res "host.xz:~/to/git", url(Scheme::Ssh, None, "host.xz", None, b"/~/to/git"), )? - .to_string(); + .to_bstring()?; assert_eq!(url, "ssh://host.xz/~/to/git"); Ok(()) } @@ -75,7 +75,7 @@ fn scp_like_without_user_and_username_expansion_with_username() -> crate::Result "host.xz:~byron/to/git", url(Scheme::Ssh, None, "host.xz", None, b"/~byron/to/git"), )? - .to_string(); + .to_bstring()?; assert_eq!(url, "ssh://host.xz/~byron/to/git"); Ok(()) } @@ -86,7 +86,7 @@ fn scp_like_with_user_and_relative_path_turns_into_absolute_path() -> crate::Res "user@host.xz:./relative", url(Scheme::Ssh, "user", "host.xz", None, b"/relative"), )? - .to_string(); + .to_bstring()?; assert_eq!(url, "ssh://user@host.xz/relative"); Ok(()) } From 76f76f533c5cc8e44fc20a05ee31c0c1a0122cc3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 21:09:36 +0800 Subject: [PATCH 027/125] support for instant url rewriting (trusted values), with option to use the originals. (#450) --- git-repository/src/remote.rs | 33 +++++++++- git-repository/src/repository/remote.rs | 77 ++++++++++++++++++++++- git-repository/src/types.rs | 6 ++ git-repository/tests/repository/remote.rs | 24 +++++-- 4 files changed, 130 insertions(+), 10 deletions(-) diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs index d6eaa898ad5..6a9d5cfaa2a 100644 --- a/git-repository/src/remote.rs +++ b/git-repository/src/remote.rs @@ -18,6 +18,13 @@ mod errors { #[error("The {kind} url couldn't be parsed")] UrlInvalid { kind: &'static str, + url: BString, + source: git_url::parse::Error, + }, + #[error("The rewritten {kind} url {rewritten_url:?} failed to parse")] + RewrittenUrlInvalid { + kind: &'static str, + rewritten_url: BString, source: git_url::parse::Error, }, } @@ -42,6 +49,16 @@ mod access { use crate::{remote, Remote}; use git_refspec::RefSpec; + /// Builder methods + impl Remote<'_> { + /// By default, `url..insteadOf|pushInsteadOf` configuration variables will be applied to rewrite fetch and push + /// urls unless `toggle` is `false`. + pub fn apply_url_aliases(mut self, toggle: bool) -> Self { + self.apply_url_aliases = toggle; + self + } + } + impl Remote<'_> { /// Return the name of this remote or `None` if it wasn't persisted to disk yet. pub fn name(&self) -> Option<&str> { @@ -56,13 +73,23 @@ mod access { } } - /// Return the url used for the given `direction`. + /// Return the url used for the given `direction` with rewrites from `url..insteadOf|pushInsteadOf` applied unless + /// [`apply_url_aliases(false)`][Self::apply_url_aliases()] was called before. /// For pushing, this is the `remote..pushUrl` or the `remote..url` used for fetching, and for fetching it's /// the `remote..url`. pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { match direction { - remote::Direction::Fetch => self.url.as_ref(), - remote::Direction::Push => self.push_url.as_ref().or(self.url.as_ref()), + remote::Direction::Fetch => self + .apply_url_aliases + .then(|| self.url_alias.as_ref()) + .flatten() + .or(self.url.as_ref()), + remote::Direction::Push => self + .apply_url_aliases + .then(|| self.push_url_alias.as_ref()) + .flatten() + .or(self.push_url.as_ref()) + .or_else(|| self.url(remote::Direction::Fetch)), } } } diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index b3c2e0bee7e..c8ac4c15fca 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -1,5 +1,5 @@ +use crate::bstr::{BStr, BString, ByteVec}; use crate::remote::find; -use crate::remote::find::Error; use crate::Remote; impl crate::Repository { @@ -29,7 +29,11 @@ impl crate::Repository { .resolved .string_filter("remote", name.into(), field, &mut filter) .map(|url| { - git_url::parse::parse(url.as_ref()).map_err(|err| find::Error::UrlInvalid { kind, source: err }) + git_url::parse::parse(url.as_ref()).map_err(|err| find::Error::UrlInvalid { + kind, + url: url.into_owned(), + source: err, + }) }) }; let url = config_url("url", "fetch"); @@ -49,7 +53,7 @@ impl crate::Repository { .map(|spec| { git_refspec::parse(spec.as_ref(), op) .map(|spec| spec.to_owned()) - .map_err(|err| Error::RefSpec { + .map_err(|err| find::Error::RefSpec { spec: spec.into_owned(), kind, source: err, @@ -90,12 +94,79 @@ impl crate::Repository { Some(Err(err)) => return Some(Err(err)), None => Vec::new(), }; + + let mut url_alias = None; + let mut push_url_alias = None; + if let Some(sections) = self.config.resolved.sections_by_name_and_filter("url", &mut filter) { + let mut rewrite_url = None::<(usize, &BStr)>; + let mut rewrite_push_url = None::<(usize, &BStr)>; + let url = url.as_ref().map(|url| url.to_bstring().expect("still valid")); + let push_url = push_url.as_ref().map(|url| url.to_bstring().expect("still valid")); + for section in sections { + let rewrite_with = match section.header().subsection_name() { + Some(base) => base, + None => continue, + }; + if let Some(url) = url.as_deref() { + for instead_of in section.values("insteadOf") { + if url.starts_with(instead_of.as_ref()) { + let (bytes_matched, prev_rewrite_with) = + rewrite_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); + if *bytes_matched < instead_of.len() { + *bytes_matched = instead_of.len(); + *prev_rewrite_with = rewrite_with; + } + } + } + } + if let Some(url) = push_url.as_deref() { + for instead_of in section.values("pushInsteadOf") { + if url.starts_with(instead_of.as_ref()) { + let (bytes_matched, prev_rewrite_with) = + rewrite_push_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); + if *bytes_matched < instead_of.len() { + *bytes_matched = instead_of.len(); + *prev_rewrite_with = rewrite_with; + } + } + } + } + } + + fn replace_url( + url: Option, + rewrite: Option<(usize, &BStr)>, + kind: &'static str, + ) -> Option> { + url.zip(rewrite).map(|(mut url, (bytes_at_start, replace_with))| { + url.replace_range(..bytes_at_start, replace_with); + git_url::parse(&url).map_err(|err| find::Error::RewrittenUrlInvalid { + kind, + source: err, + rewritten_url: url, + }) + }) + } + url_alias = match replace_url(url, rewrite_url, "fetch") { + Some(Ok(url)) => Some(url), + Some(Err(err)) => return Some(Err(err)), + None => None, + }; + push_url_alias = match replace_url(push_url, rewrite_push_url, "push") { + Some(Ok(url)) => Some(url), + Some(Err(err)) => return Some(Err(err)), + None => None, + }; + } Some(Ok(Remote { name: name.to_owned().into(), url, + url_alias, push_url, + push_url_alias, fetch_specs, push_specs, + apply_url_aliases: true, repo: self, })) } diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index 250ccede45d..4c07d8abead 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -172,12 +172,18 @@ pub struct Remote<'repo> { /// The url of the host to talk to, after application of replacements. If it is unset, the `push_url` must be set. /// and fetches aren't possible. pub(crate) url: Option, + /// The rewritten `url`, if it was rewritten. + pub(crate) url_alias: Option, /// The url to use for pushing specifically. pub(crate) push_url: Option, + /// The rewritten `push_url`, if it was rewritten. + pub(crate) push_url_alias: Option, /// Refspecs for use when fetching. pub(crate) fetch_specs: Vec, /// Refspecs for use when pushing. pub(crate) push_specs: Vec, + /// If false, default true, we will apply url rewrites. + pub(crate) apply_url_aliases: bool, // /// Delete local tracking branches that don't exist on the remote anymore. // pub(crate) prune: bool, // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 82285c21fea..07e97c29b9d 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -65,7 +65,6 @@ mod find_remote { } #[test] - #[ignore] fn instead_of_url_rewriting() -> crate::Result { let repo = remote::repo("url-rewriting"); @@ -76,12 +75,29 @@ mod find_remote { let remote = repo.find_remote("origin")?; assert_eq!( - remote.url(Direction::Fetch).expect("present").to_string(), + remote.url(Direction::Fetch).expect("present").to_bstring()?, expected_fetch_url, ); + { + let actual_push_url = remote.url(Direction::Push).expect("present").to_bstring()?; + assert_ne!( + actual_push_url, expected_push_url, + "here we actually resolve something that git doesn't for unknown reason" + ); + assert_eq!( + actual_push_url, "ssh://dev/null", + "file:// gets replaced actually and it's a valid url" + ); + } + + let remote = remote.apply_url_aliases(false); + assert_eq!( + remote.url(Direction::Fetch).expect("present").to_bstring()?, + "https://github.com/foobar/gitoxide" + ); assert_eq!( - remote.url(Direction::Push).expect("present").to_string(), - expected_push_url, + remote.url(Direction::Push).expect("present").to_bstring()?, + "file://dev/null" ); Ok(()) } From 6c15bf450066525df439df1f719a0bd4720730cc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 21:18:13 +0800 Subject: [PATCH 028/125] refactor (#450) --- git-repository/src/repository/remote.rs | 132 +++++++++++++----------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index c8ac4c15fca..99cf2638e18 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -95,69 +95,11 @@ impl crate::Repository { None => Vec::new(), }; - let mut url_alias = None; - let mut push_url_alias = None; - if let Some(sections) = self.config.resolved.sections_by_name_and_filter("url", &mut filter) { - let mut rewrite_url = None::<(usize, &BStr)>; - let mut rewrite_push_url = None::<(usize, &BStr)>; - let url = url.as_ref().map(|url| url.to_bstring().expect("still valid")); - let push_url = push_url.as_ref().map(|url| url.to_bstring().expect("still valid")); - for section in sections { - let rewrite_with = match section.header().subsection_name() { - Some(base) => base, - None => continue, - }; - if let Some(url) = url.as_deref() { - for instead_of in section.values("insteadOf") { - if url.starts_with(instead_of.as_ref()) { - let (bytes_matched, prev_rewrite_with) = - rewrite_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); - if *bytes_matched < instead_of.len() { - *bytes_matched = instead_of.len(); - *prev_rewrite_with = rewrite_with; - } - } - } - } - if let Some(url) = push_url.as_deref() { - for instead_of in section.values("pushInsteadOf") { - if url.starts_with(instead_of.as_ref()) { - let (bytes_matched, prev_rewrite_with) = - rewrite_push_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); - if *bytes_matched < instead_of.len() { - *bytes_matched = instead_of.len(); - *prev_rewrite_with = rewrite_with; - } - } - } - } - } - - fn replace_url( - url: Option, - rewrite: Option<(usize, &BStr)>, - kind: &'static str, - ) -> Option> { - url.zip(rewrite).map(|(mut url, (bytes_at_start, replace_with))| { - url.replace_range(..bytes_at_start, replace_with); - git_url::parse(&url).map_err(|err| find::Error::RewrittenUrlInvalid { - kind, - source: err, - rewritten_url: url, - }) - }) - } - url_alias = match replace_url(url, rewrite_url, "fetch") { - Some(Ok(url)) => Some(url), - Some(Err(err)) => return Some(Err(err)), - None => None, - }; - push_url_alias = match replace_url(push_url, rewrite_push_url, "push") { - Some(Ok(url)) => Some(url), - Some(Err(err)) => return Some(Err(err)), - None => None, + let (url_alias, push_url_alias) = + match rewrite_urls(&self.config.resolved, url.as_ref(), push_url.as_ref(), filter) { + Ok(t) => t, + Err(err) => return Some(Err(err)), }; - } Some(Ok(Remote { name: name.to_owned().into(), url, @@ -173,3 +115,69 @@ impl crate::Repository { } } } + +fn rewrite_urls( + config: &git_config::File<'static>, + url: Option<&git_url::Url>, + push_url: Option<&git_url::Url>, + mut filter: fn(&git_config::file::Metadata) -> bool, +) -> Result<(Option, Option), find::Error> { + let mut url_alias = None; + let mut push_url_alias = None; + if let Some(sections) = config.sections_by_name_and_filter("url", &mut filter) { + let mut rewrite_url = None::<(usize, &BStr)>; + let mut rewrite_push_url = None::<(usize, &BStr)>; + let url = url.as_ref().map(|url| url.to_bstring().expect("still valid")); + let push_url = push_url.as_ref().map(|url| url.to_bstring().expect("still valid")); + for section in sections { + let rewrite_with = match section.header().subsection_name() { + Some(base) => base, + None => continue, + }; + if let Some(url) = url.as_deref() { + for instead_of in section.values("insteadOf") { + if url.starts_with(instead_of.as_ref()) { + let (bytes_matched, prev_rewrite_with) = + rewrite_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); + if *bytes_matched < instead_of.len() { + *bytes_matched = instead_of.len(); + *prev_rewrite_with = rewrite_with; + } + } + } + } + if let Some(url) = push_url.as_deref() { + for instead_of in section.values("pushInsteadOf") { + if url.starts_with(instead_of.as_ref()) { + let (bytes_matched, prev_rewrite_with) = + rewrite_push_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); + if *bytes_matched < instead_of.len() { + *bytes_matched = instead_of.len(); + *prev_rewrite_with = rewrite_with; + } + } + } + } + } + + fn replace_url( + url: Option, + rewrite: Option<(usize, &BStr)>, + kind: &'static str, + ) -> Result, find::Error> { + url.zip(rewrite) + .map(|(mut url, (bytes_at_start, replace_with))| { + url.replace_range(..bytes_at_start, replace_with); + git_url::parse(&url).map_err(|err| find::Error::RewrittenUrlInvalid { + kind, + source: err, + rewritten_url: url, + }) + }) + .transpose() + } + url_alias = replace_url(url, rewrite_url, "fetch")?; + push_url_alias = replace_url(push_url, rewrite_push_url, "push")?; + } + Ok((url_alias, push_url_alias)) +} From 12589cc6f08e4d7aabae30bcdadaa0c2b4850229 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 21:56:44 +0800 Subject: [PATCH 029/125] change!: adapt to changes in `git-url` and use `BString` to represent URLs. (#450) They can contain paths, which is why `String` can't repsent a URL losslessly. For HTTP urls these are ultimately UTF-8 strings though. --- git-credentials/src/helper.rs | 35 +++++++++++-------- git-protocol/src/fetch/tests/arguments.rs | 6 ++-- git-protocol/src/fetch_fn.rs | 2 +- .../src/client/blocking_io/connect.rs | 11 +++--- git-transport/src/client/blocking_io/file.rs | 4 +-- .../src/client/blocking_io/http/curl/mod.rs | 7 ++-- .../client/blocking_io/http/curl/remote.rs | 5 +-- .../src/client/blocking_io/http/mod.rs | 34 ++++++++++-------- .../src/client/blocking_io/http/traits.rs | 5 +-- git-transport/src/client/git/async_io.rs | 5 +-- git-transport/src/client/git/blocking_io.rs | 5 +-- git-transport/src/client/git/mod.rs | 4 +-- git-transport/src/client/traits.rs | 7 ++-- .../tests/client/blocking_io/http/mock.rs | 2 +- gitoxide-core/src/organize.rs | 13 +++---- 15 files changed, 82 insertions(+), 63 deletions(-) diff --git a/git-credentials/src/helper.rs b/git-credentials/src/helper.rs index 2240a82dba0..752d3281605 100644 --- a/git-credentials/src/helper.rs +++ b/git-credentials/src/helper.rs @@ -1,3 +1,4 @@ +use bstr::{BStr, BString}; use std::{ io::{self, Write}, process::{Command, Stdio}, @@ -31,11 +32,11 @@ quick_error! { #[derive(Clone, Debug)] pub enum Action<'a> { /// Provide credentials using the given repository URL (as &str) as context. - Fill(&'a str), - /// Approve the credentials as identified by the previous input as `Vec`. - Approve(Vec), - /// Reject the credentials as identified by the previous input as `Vec`. - Reject(Vec), + Fill(&'a BStr), + /// Approve the credentials as identified by the previous input provided as `BString`. + Approve(BString), + /// Reject the credentials as identified by the previous input provided as `BString`. + Reject(BString), } impl<'a> Action<'a> { @@ -54,7 +55,7 @@ impl<'a> Action<'a> { /// A handle to [approve][NextAction::approve()] or [reject][NextAction::reject()] the outcome of the initial action. #[derive(Clone, Debug)] pub struct NextAction { - previous_output: Vec, + previous_output: BString, } impl NextAction { @@ -134,20 +135,20 @@ pub fn action(action: Action<'_>) -> Result { password: find("password")?, }, next: NextAction { - previous_output: stdout, + previous_output: stdout.into(), }, })) } } /// Encode `url` to `out` for consumption by a `git credentials` helper program. -pub fn encode_message(url: &str, mut out: impl io::Write) -> io::Result<()> { +pub fn encode_message(url: &BStr, mut out: impl io::Write) -> io::Result<()> { validate(url)?; writeln!(out, "url={}\n", url) } -fn validate(url: &str) -> io::Result<()> { - if url.contains('\u{0}') || url.contains('\n') { +fn validate(url: &BStr) -> io::Result<()> { + if url.contains(&0) || url.contains(&b'\n') { return Err(io::Error::new( io::ErrorKind::Other, "token to encode must not contain newlines or null bytes", @@ -165,7 +166,9 @@ pub fn decode_message(mut input: impl io::Read) -> io::Result { - url: String, + url: BString, user_agent_header: &'static str, desired_version: crate::Protocol, supported_versions: [crate::Protocol; 1], @@ -37,7 +38,7 @@ pub struct Transport { impl Transport { /// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol. - pub fn new(url: &str, desired_version: crate::Protocol) -> Self { + pub fn new(url: &BStr, desired_version: crate::Protocol) -> Self { Transport { url: url.to_owned(), user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")), @@ -87,12 +88,13 @@ impl Transport { } } -fn append_url(base: &str, suffix: &str) -> String { - if base.ends_with('/') { - format!("{}{}", base, suffix) - } else { - format!("{}/{}", base, suffix) +fn append_url(base: &BStr, suffix: &str) -> BString { + let mut buf = base.to_owned(); + if base.last() != Some(&b'/') { + buf.push(b'/'); } + buf.extend_from_slice(suffix.as_bytes()); + buf } impl client::TransportWithoutIO for Transport { @@ -107,7 +109,7 @@ impl client::TransportWithoutIO for Transport { on_into_read: client::MessageKind, ) -> Result, client::Error> { let service = self.service.expect("handshake() must have been called first"); - let url = append_url(&self.url, service.as_str()); + let url = append_url(self.url.as_ref(), service.as_str()); let static_headers = &[ Cow::Borrowed(self.user_agent_header), Cow::Owned(format!("Content-Type: application/x-{}-request", service.as_str())), @@ -127,7 +129,9 @@ impl client::TransportWithoutIO for Transport { headers, body, post_body, - } = self.http.post(&url, static_headers.iter().chain(&dynamic_headers))?; + } = self + .http + .post(url.as_ref(), static_headers.iter().chain(&dynamic_headers))?; let line_provider = self .line_provider .as_mut() @@ -145,8 +149,8 @@ impl client::TransportWithoutIO for Transport { )) } - fn to_url(&self) -> String { - self.url.to_owned() + fn to_url(&self) -> BString { + self.url.clone() } fn supported_protocol_versions(&self) -> &[Protocol] { @@ -164,7 +168,7 @@ impl client::Transport for Transport { service: Service, extra_parameters: &'a [(&'a str, Option<&'a str>)], ) -> Result, client::Error> { - let url = append_url(&self.url, &format!("info/refs?service={}", service.as_str())); + let url = append_url(self.url.as_ref(), &format!("info/refs?service={}", service.as_str())); let static_headers = [Cow::Borrowed(self.user_agent_header)]; let mut dynamic_headers = Vec::>::new(); if self.desired_version != Protocol::V1 || !extra_parameters.is_empty() { @@ -190,7 +194,9 @@ impl client::Transport for Transport { dynamic_headers.push(format!("Git-Protocol: {}", parameters).into()); } self.add_basic_auth_if_present(&mut dynamic_headers)?; - let GetResponse { headers, body } = self.http.get(&url, static_headers.iter().chain(&dynamic_headers))?; + let GetResponse { headers, body } = self + .http + .get(url.as_ref(), static_headers.iter().chain(&dynamic_headers))?; >::check_content_type(service, "advertisement", headers)?; let line_reader = self @@ -279,6 +285,6 @@ impl ExtendedBufRead for HeadersThenBody Result, Infallible> { +pub fn connect(url: &BStr, desired_version: crate::Protocol) -> Result, Infallible> { Ok(Transport::new(url, desired_version)) } diff --git a/git-transport/src/client/blocking_io/http/traits.rs b/git-transport/src/client/blocking_io/http/traits.rs index 194389ca6f2..c2e707496f4 100644 --- a/git-transport/src/client/blocking_io/http/traits.rs +++ b/git-transport/src/client/blocking_io/http/traits.rs @@ -1,3 +1,4 @@ +use bstr::BStr; use std::io; use quick_error::quick_error; @@ -62,7 +63,7 @@ pub trait Http { /// The `headers` are provided verbatim and include both the key as well as the value. fn get( &mut self, - url: &str, + url: &BStr, headers: impl IntoIterator>, ) -> Result, Error>; @@ -74,7 +75,7 @@ pub trait Http { /// to prevent deadlocks. fn post( &mut self, - url: &str, + url: &BStr, headers: impl IntoIterator>, ) -> Result, Error>; } diff --git a/git-transport/src/client/git/async_io.rs b/git-transport/src/client/git/async_io.rs index 0771b02c01f..5c6fff937b0 100644 --- a/git-transport/src/client/git/async_io.rs +++ b/git-transport/src/client/git/async_io.rs @@ -26,7 +26,7 @@ where on_into_read, )) } - fn to_url(&self) -> String { + fn to_url(&self) -> BString { self.custom_url.as_ref().map_or_else( || { git_url::Url { @@ -36,7 +36,8 @@ where port: None, path: self.path.clone(), } - .to_string() + .to_bstring() + .expect("valid and not mutated") }, |url| url.clone(), ) diff --git a/git-transport/src/client/git/blocking_io.rs b/git-transport/src/client/git/blocking_io.rs index 544a2e618b9..f65f9e12c8d 100644 --- a/git-transport/src/client/git/blocking_io.rs +++ b/git-transport/src/client/git/blocking_io.rs @@ -26,7 +26,7 @@ where )) } - fn to_url(&self) -> String { + fn to_url(&self) -> BString { self.custom_url.as_ref().map_or_else( || { git_url::Url { @@ -36,7 +36,8 @@ where port: None, path: self.path.clone(), } - .to_string() + .to_bstring() + .expect("valid URL which isn't mutated") }, |url| url.clone(), ) diff --git a/git-transport/src/client/git/mod.rs b/git-transport/src/client/git/mod.rs index 2aaa6f42210..3f549e34b3d 100644 --- a/git-transport/src/client/git/mod.rs +++ b/git-transport/src/client/git/mod.rs @@ -22,7 +22,7 @@ pub struct Connection { pub(in crate::client) virtual_host: Option<(String, Option)>, pub(in crate::client) desired_version: Protocol, supported_versions: [Protocol; 1], - custom_url: Option, + custom_url: Option, pub(in crate::client) mode: ConnectMode, } @@ -37,7 +37,7 @@ impl Connection { /// The URL is required as parameter for authentication helpers which are called in transports /// that support authentication. Even though plain git transports don't support that, this /// may well be the case in custom transports. - pub fn custom_url(mut self, url: Option) -> Self { + pub fn custom_url(mut self, url: Option) -> Self { self.custom_url = url; self } diff --git a/git-transport/src/client/traits.rs b/git-transport/src/client/traits.rs index f57b2fe2d09..bd3fa79f08e 100644 --- a/git-transport/src/client/traits.rs +++ b/git-transport/src/client/traits.rs @@ -1,3 +1,4 @@ +use bstr::BString; use std::ops::{Deref, DerefMut}; #[cfg(any(feature = "blocking-client", feature = "async-client"))] @@ -26,7 +27,7 @@ pub trait TransportWithoutIO { /// Returns the canonical URL pointing to the destination of this transport. /// Please note that local paths may not be represented correctly, as they will go through a potentially lossy /// unicode conversion. - fn to_url(&self) -> String; + fn to_url(&self) -> BString; /// If the actually advertised server version is contained in the returned slice or empty, continue as normal, /// assume the server's protocol version is desired or acceptable. @@ -59,7 +60,7 @@ impl TransportWithoutIO for Box { self.deref_mut().request(write_mode, on_into_read) } - fn to_url(&self) -> String { + fn to_url(&self) -> BString { self.deref().to_url() } @@ -82,7 +83,7 @@ impl TransportWithoutIO for &mut T { self.deref_mut().request(write_mode, on_into_read) } - fn to_url(&self) -> String { + fn to_url(&self) -> BString { self.deref().to_url() } diff --git a/git-transport/tests/client/blocking_io/http/mock.rs b/git-transport/tests/client/blocking_io/http/mock.rs index 49c74ee5e1a..a4fb784c7fa 100644 --- a/git-transport/tests/client/blocking_io/http/mock.rs +++ b/git-transport/tests/client/blocking_io/http/mock.rs @@ -104,7 +104,7 @@ pub fn serve_and_connect( &server.addr.port(), path ); - let client = git_transport::client::http::connect(&url, version)?; + let client = git_transport::client::http::connect(url.as_str().into(), version)?; assert_eq!(url, client.to_url()); Ok((server, client)) } diff --git a/gitoxide-core/src/organize.rs b/gitoxide-core/src/organize.rs index 330c69880bc..0dde4071b03 100644 --- a/gitoxide-core/src/organize.rs +++ b/gitoxide-core/src/organize.rs @@ -168,17 +168,18 @@ fn handle( progress.info(format!( "Skipping repository at {:?} whose remote does not have a path: {:?}", git_workdir.display(), - url.to_string() + url.to_bstring()? )); return Ok(()); } let destination = canonicalized_destination - .join( - url.host - .as_ref() - .ok_or_else(|| anyhow::Error::msg(format!("Remote URLs must have host names: {}", url)))?, - ) + .join(url.host.as_ref().ok_or_else(|| { + anyhow::Error::msg(format!( + "Remote URLs must have host names: {}", + url.to_bstring().expect("valid URL") + )) + })?) .join(to_relative({ let mut path = git_url::expand_path(None, url.path.as_bstr())?; match kind { From f84360ca56be1ec9d95ad03566932622d9b0d2a6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Aug 2022 21:58:59 +0800 Subject: [PATCH 030/125] thanks clippy --- git-repository/src/repository/remote.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 99cf2638e18..e3c3dd769c6 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -138,7 +138,7 @@ fn rewrite_urls( for instead_of in section.values("insteadOf") { if url.starts_with(instead_of.as_ref()) { let (bytes_matched, prev_rewrite_with) = - rewrite_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); + rewrite_url.get_or_insert((instead_of.len(), rewrite_with)); if *bytes_matched < instead_of.len() { *bytes_matched = instead_of.len(); *prev_rewrite_with = rewrite_with; @@ -150,7 +150,7 @@ fn rewrite_urls( for instead_of in section.values("pushInsteadOf") { if url.starts_with(instead_of.as_ref()) { let (bytes_matched, prev_rewrite_with) = - rewrite_push_url.get_or_insert_with(|| (instead_of.len(), rewrite_with)); + rewrite_push_url.get_or_insert((instead_of.len(), rewrite_with)); if *bytes_matched < instead_of.len() { *bytes_matched = instead_of.len(); *prev_rewrite_with = rewrite_with; From 2905e1b0c5d75214fc8dc279f149e1b3bc8caaf3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 10 Aug 2022 18:46:46 +0800 Subject: [PATCH 031/125] refactor (#450) --- git-repository/src/reference/mod.rs | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/git-repository/src/reference/mod.rs b/git-repository/src/reference/mod.rs index 358681c48f3..64cd20987f9 100644 --- a/git-repository/src/reference/mod.rs +++ b/git-repository/src/reference/mod.rs @@ -18,6 +18,20 @@ pub use git_ref::{Category, Kind}; /// Access impl<'repo> Reference<'repo> { + /// Returns the attached id we point to, or `None` if this is a symbolic ref. + pub fn try_id(&self) -> Option> { + match self.inner.target { + git_ref::Target::Symbolic(_) => None, + git_ref::Target::Peeled(oid) => oid.to_owned().attach(self.repo).into(), + } + } + + /// Returns the attached id we point to, or panic if this is a symbolic ref. + pub fn id(&self) -> Id<'repo> { + self.try_id() + .expect("BUG: tries to obtain object id from symbolic target") + } + /// Return the target to which this reference points to. pub fn target(&self) -> git_ref::TargetRef<'_> { self.inner.target.to_ref() @@ -44,21 +58,9 @@ impl<'repo> Reference<'repo> { pub(crate) fn from_ref(reference: git_ref::Reference, repo: &'repo crate::Repository) -> Self { Reference { inner: reference, repo } } +} - /// Returns the attached id we point to, or `None` if this is a symbolic ref. - pub fn try_id(&self) -> Option> { - match self.inner.target { - git_ref::Target::Symbolic(_) => None, - git_ref::Target::Peeled(oid) => oid.to_owned().attach(self.repo).into(), - } - } - - /// Returns the attached id we point to, or panic if this is a symbolic ref. - pub fn id(&self) -> crate::Id<'repo> { - self.try_id() - .expect("BUG: tries to obtain object id from symbolic target") - } - +impl<'repo> Reference<'repo> { /// Follow all symbolic targets this reference might point to and peel the underlying object /// to the end of the chain, and return it. /// From f41e588595ff179abc39817dd1fa9f39fb14e6c0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 10 Aug 2022 20:06:17 +0800 Subject: [PATCH 032/125] refactor (#450) - re-use url rewrite mappings and lazily load the into cache - move rewrite utilities into `remote` module to allow reusing it for anonymous remotes --- git-repository/src/config/cache.rs | 17 ++++- git-repository/src/config/mod.rs | 8 +- git-repository/src/open.rs | 15 ++-- git-repository/src/remote.rs | 99 +++++++++++++++++++++++++ git-repository/src/repository/remote.rs | 88 ++++++---------------- git-repository/tests/repository/mod.rs | 9 ++- 6 files changed, 156 insertions(+), 80 deletions(-) diff --git a/git-repository/src/config/cache.rs b/git-repository/src/config/cache.rs index 53a69ae7ee1..2a765fa4a5a 100644 --- a/git-repository/src/config/cache.rs +++ b/git-repository/src/config/cache.rs @@ -3,7 +3,7 @@ use std::{convert::TryFrom, path::PathBuf}; use git_config::{Boolean, Integer}; use super::{Cache, Error}; -use crate::{bstr::ByteSlice, repository, repository::identity, revision::spec::parse::ObjectKindHint}; +use crate::{bstr::ByteSlice, remote, repository, repository::identity, revision::spec::parse::ObjectKindHint}; /// 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`. @@ -232,10 +232,12 @@ impl Cache { is_bare, ignore_case, hex_len, + filter_config_section, excludes_file, xdg_config_home_env, home_env, personas: Default::default(), + url_rewrite: Default::default(), git_prefix, }) } @@ -266,12 +268,23 @@ impl Cache { } } +impl std::fmt::Debug for Cache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cache").finish_non_exhaustive() + } +} + /// Access impl Cache { - pub fn personas(&self) -> &identity::Personas { + pub(crate) fn personas(&self) -> &identity::Personas { self.personas .get_or_init(|| identity::Personas::from_config_and_env(&self.resolved, &self.git_prefix)) } + + pub(crate) fn url_rewrite(&self) -> &remote::url::Rewrite { + self.url_rewrite + .get_or_init(|| remote::url::Rewrite::from_config(&self.resolved, self.filter_config_section)) + } } pub(crate) fn interpolate_context<'a>( diff --git a/git-repository/src/config/mod.rs b/git-repository/src/config/mod.rs index 2dea9c5adda..7b537b05a66 100644 --- a/git-repository/src/config/mod.rs +++ b/git-repository/src/config/mod.rs @@ -1,7 +1,7 @@ pub use git_config::*; use git_features::threading::OnceCell; -use crate::{bstr::BString, permission, repository::identity, revision::spec, Repository}; +use crate::{bstr::BString, permission, remote, repository::identity, revision::spec, Repository}; pub(crate) mod cache; mod snapshot; @@ -44,7 +44,7 @@ pub enum Error { } /// Utility type to keep pre-obtained configuration values. -#[derive(Debug, Clone)] +#[derive(Clone)] pub(crate) struct Cache { pub resolved: crate::Config, /// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count. @@ -59,6 +59,10 @@ pub(crate) struct Cache { pub reflog: Option, /// identities for later use, lazy initialization. pub personas: OnceCell, + /// A lazily loaded rewrite list for remote urls + pub url_rewrite: OnceCell, + /// The config section filter from the options used to initialize this instance. Keep these in sync! + filter_config_section: fn(&git_config::file::Metadata) -> bool, /// The object kind to pick if a prefix is ambiguous. pub object_kind_hint: Option, /// If true, we are on a case-insensitive file system. diff --git a/git-repository/src/open.rs b/git-repository/src/open.rs index f78681911bf..a8531e05548 100644 --- a/git-repository/src/open.rs +++ b/git-repository/src/open.rs @@ -66,6 +66,7 @@ pub struct Options { pub(crate) replacement_objects: ReplacementObjects, pub(crate) permissions: Permissions, pub(crate) git_dir_trust: Option, + /// Warning: this one is copied to to config::Cache - don't change it after repo open or keep in sync. pub(crate) filter_config_section: Option bool>, pub(crate) lossy_config: Option, pub(crate) bail_if_untrusted: bool, @@ -342,11 +343,13 @@ impl ThreadSafeRepository { let home = std::env::var_os("HOME") .map(PathBuf::from) .and_then(|home| env.home.check(home).ok().flatten()); + + let mut filter_config_section = filter_config_section.unwrap_or(config::section::is_trusted); let config = config::Cache::from_stage_one( repo_config, common_dir_ref, head.as_ref().and_then(|head| head.target.try_name()), - filter_config_section.unwrap_or(config::section::is_trusted), + filter_config_section, git_install_dir.as_deref(), home.as_deref(), env.clone(), @@ -359,12 +362,10 @@ impl ThreadSafeRepository { // core.worktree might be used to overwrite the worktree directory if !config.is_bare { - if let Some(wt) = config.resolved.path_filter( - "core", - None, - "worktree", - &mut filter_config_section.unwrap_or(config::section::is_trusted), - ) { + if let Some(wt) = config + .resolved + .path_filter("core", None, "worktree", &mut filter_config_section) + { let wt_path = wt .interpolate(interpolate_context(git_install_dir.as_deref(), home.as_deref())) .map_err(config::Error::PathInterpolation)?; diff --git a/git-repository/src/remote.rs b/git-repository/src/remote.rs index 6a9d5cfaa2a..c0ad6503925 100644 --- a/git-repository/src/remote.rs +++ b/git-repository/src/remote.rs @@ -95,6 +95,105 @@ mod access { } } +pub(crate) mod url { + use crate::bstr::{BStr, BString, ByteVec}; + use crate::remote::Direction; + use git_features::threading::OwnShared; + + #[derive(Debug, Clone)] + pub(crate) struct Replace { + find: BString, + with: OwnShared, + } + + #[derive(Default, Debug, Clone)] + pub(crate) struct Rewrite { + url_rewrite: Vec, + push_url_rewrite: Vec, + } + + /// Init + impl Rewrite { + pub fn from_config( + config: &git_config::File<'static>, + mut filter: fn(&git_config::file::Metadata) -> bool, + ) -> Rewrite { + config + .sections_by_name_and_filter("url", &mut filter) + .map(|sections| { + let mut url_rewrite = Vec::new(); + let mut push_url_rewrite = Vec::new(); + for section in sections { + let replace = match section.header().subsection_name() { + Some(base) => OwnShared::new(base.to_owned()), + None => continue, + }; + + for instead_of in section.values("insteadOf") { + url_rewrite.push(Replace { + with: OwnShared::clone(&replace), + find: instead_of.into_owned(), + }); + } + for instead_of in section.values("pushInsteadOf") { + push_url_rewrite.push(Replace { + with: OwnShared::clone(&replace), + find: instead_of.into_owned(), + }); + } + } + Rewrite { + url_rewrite, + push_url_rewrite, + } + }) + .unwrap_or_default() + } + } + + /// Access + impl Rewrite { + fn replacements_for(&self, direction: Direction) -> &[Replace] { + match direction { + Direction::Fetch => &self.url_rewrite, + Direction::Push => &self.push_url_rewrite, + } + } + + pub fn rewrite_url(&self, url: &git_url::Url, direction: Direction) -> Option { + if self.replacements_for(direction).is_empty() { + None + } else { + let mut url = url.to_bstring().ok()?; + self.rewrite_url_in_place(&mut url, direction).then(|| url) + } + } + + /// Rewrite the given `url` of `direction` and return `true` if a replacement happened. + /// + /// Note that the result must still be checked for validity, it might not be a valid URL as we do a syntax-unaware replacement. + pub fn rewrite_url_in_place(&self, url: &mut BString, direction: Direction) -> bool { + self.replacements_for(direction) + .iter() + .fold(None::<(usize, &BStr)>, |mut acc, replace| { + if url.starts_with(replace.find.as_ref()) { + let (bytes_matched, prev_rewrite_with) = + acc.get_or_insert((replace.find.len(), replace.with.as_slice().into())); + if *bytes_matched < replace.find.len() { + *bytes_matched = replace.find.len(); + *prev_rewrite_with = replace.with.as_slice().into(); + } + }; + acc + }) + .map(|(bytes_matched, replace_with)| { + url.replace_range(..bytes_matched, replace_with); + }) + .is_some() + } + } +} + /// The direction of an operation carried out (or to be carried out) through a remote. #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub enum Direction { diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index e3c3dd769c6..c8e95e7f2ec 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -1,6 +1,5 @@ -use crate::bstr::{BStr, BString, ByteVec}; use crate::remote::find; -use crate::Remote; +use crate::{config, remote, Remote}; impl crate::Repository { /// Find the remote with the given `name` or report an error, similar to [`try_find_remote(…)`][Self::try_find_remote()]. @@ -95,11 +94,10 @@ impl crate::Repository { None => Vec::new(), }; - let (url_alias, push_url_alias) = - match rewrite_urls(&self.config.resolved, url.as_ref(), push_url.as_ref(), filter) { - Ok(t) => t, - Err(err) => return Some(Err(err)), - }; + let (url_alias, push_url_alias) = match rewrite_urls(&self.config, url.as_ref(), push_url.as_ref()) { + Ok(t) => t, + Err(err) => return Some(Err(err)), + }; Some(Ok(Remote { name: name.to_owned().into(), url, @@ -117,67 +115,27 @@ impl crate::Repository { } fn rewrite_urls( - config: &git_config::File<'static>, + config: &config::Cache, url: Option<&git_url::Url>, push_url: Option<&git_url::Url>, - mut filter: fn(&git_config::file::Metadata) -> bool, ) -> Result<(Option, Option), find::Error> { - let mut url_alias = None; - let mut push_url_alias = None; - if let Some(sections) = config.sections_by_name_and_filter("url", &mut filter) { - let mut rewrite_url = None::<(usize, &BStr)>; - let mut rewrite_push_url = None::<(usize, &BStr)>; - let url = url.as_ref().map(|url| url.to_bstring().expect("still valid")); - let push_url = push_url.as_ref().map(|url| url.to_bstring().expect("still valid")); - for section in sections { - let rewrite_with = match section.header().subsection_name() { - Some(base) => base, - None => continue, - }; - if let Some(url) = url.as_deref() { - for instead_of in section.values("insteadOf") { - if url.starts_with(instead_of.as_ref()) { - let (bytes_matched, prev_rewrite_with) = - rewrite_url.get_or_insert((instead_of.len(), rewrite_with)); - if *bytes_matched < instead_of.len() { - *bytes_matched = instead_of.len(); - *prev_rewrite_with = rewrite_with; - } - } - } - } - if let Some(url) = push_url.as_deref() { - for instead_of in section.values("pushInsteadOf") { - if url.starts_with(instead_of.as_ref()) { - let (bytes_matched, prev_rewrite_with) = - rewrite_push_url.get_or_insert((instead_of.len(), rewrite_with)); - if *bytes_matched < instead_of.len() { - *bytes_matched = instead_of.len(); - *prev_rewrite_with = rewrite_with; - } - } - } - } - } - - fn replace_url( - url: Option, - rewrite: Option<(usize, &BStr)>, - kind: &'static str, - ) -> Result, find::Error> { - url.zip(rewrite) - .map(|(mut url, (bytes_at_start, replace_with))| { - url.replace_range(..bytes_at_start, replace_with); - git_url::parse(&url).map_err(|err| find::Error::RewrittenUrlInvalid { - kind, - source: err, - rewritten_url: url, - }) + let rewrite = |url: Option<&git_url::Url>, direction: remote::Direction| { + url.and_then(|url| config.url_rewrite().rewrite_url(url, direction)) + .map(|url| { + git_url::parse(&url).map_err(|err| find::Error::RewrittenUrlInvalid { + kind: match direction { + remote::Direction::Fetch => "fetch", + remote::Direction::Push => "push", + }, + source: err, + rewritten_url: url, }) - .transpose() - } - url_alias = replace_url(url, rewrite_url, "fetch")?; - push_url_alias = replace_url(push_url, rewrite_push_url, "push")?; - } + }) + .transpose() + }; + + let url_alias = rewrite(url, remote::Direction::Fetch)?; + let push_url_alias = rewrite(push_url, remote::Direction::Push)?; + Ok((url_alias, push_url_alias)) } diff --git a/git-repository/tests/repository/mod.rs b/git-repository/tests/repository/mod.rs index 492df5a622b..2668000dba7 100644 --- a/git-repository/tests/repository/mod.rs +++ b/git-repository/tests/repository/mod.rs @@ -9,10 +9,11 @@ mod worktree; #[test] fn size_in_memory() { - let expected = [688, 696]; + let expected = [744, 760]; + let actual_size = std::mem::size_of::(); assert!( - expected.contains(&std::mem::size_of::()), - "size of Repository shouldn't change without us noticing, it's meant to be cloned: should have been within {:?}", - expected + expected.contains(&actual_size), + "size of Repository shouldn't change without us noticing, it's meant to be cloned: should have been within {:?}, was {}", + expected, actual_size ); } From 92c0aa343e5cba86dc4b2d4006927542610bc802 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 10 Aug 2022 21:32:35 +0800 Subject: [PATCH 033/125] remote-name by reference, which can be useful to find remotes with multiple fallbacks (#450) --- crate-status.md | 3 + etc/check-package-size.sh | 2 +- git-repository/src/reference/mod.rs | 39 ++++++++++++- .../tests/fixtures/make_remote_repos.sh | 1 + git-repository/tests/reference/mod.rs | 57 +++++++++++++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) diff --git a/crate-status.md b/crate-status.md index be16abf595b..7e5fb15b135 100644 --- a/crate-status.md +++ b/crate-status.md @@ -461,6 +461,9 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. * **references** * [x] peel to end * [x] ref-log access + * [x] remote name + * [x] find remote itself + - [ ] respect `branch..merge` in the returned remote. * **remotes** * [ ] clone * [ ] shallow diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh index aff6a2fd162..d3d7cf893cc 100755 --- a/etc/check-package-size.sh +++ b/etc/check-package-size.sh @@ -53,6 +53,6 @@ echo "in root: gitoxide CLI" (enter git-odb && indent cargo diet -n --package-size-limit 120KB) (enter git-protocol && indent cargo diet -n --package-size-limit 50KB) (enter git-packetline && indent cargo diet -n --package-size-limit 35KB) -(enter git-repository && indent cargo diet -n --package-size-limit 130KB) +(enter git-repository && indent cargo diet -n --package-size-limit 140KB) (enter git-transport && indent cargo diet -n --package-size-limit 50KB) (enter gitoxide-core && indent cargo diet -n --package-size-limit 80KB) diff --git a/git-repository/src/reference/mod.rs b/git-repository/src/reference/mod.rs index 64cd20987f9..164808dabcd 100644 --- a/git-repository/src/reference/mod.rs +++ b/git-repository/src/reference/mod.rs @@ -2,8 +2,9 @@ use git_odb::pack::Find; use git_ref::file::ReferenceExt; +use std::borrow::Cow; -use crate::{Id, Reference}; +use crate::{bstr, remote, Id, Reference}; pub mod iter; @@ -46,6 +47,42 @@ impl<'repo> Reference<'repo> { pub fn detach(self) -> git_ref::Reference { self.inner } + + /// Find the name of our remote for `direction` as configured in `branch..remote|pushRemote` respectively. + /// If `Some()` it can be used in [`Repository::find_remote(…)`][crate::Repository::find_remote()], or if `None` then + /// [Repository::remote_default_name()][crate::Repository::remote_default_name()] could be used in its place. + /// + /// Return `None` if no remote is configured. + /// + /// # Note + /// + /// - it's recommended to use the [`remote(…)`][Self::remote()] method as it will configure the remote with additional + /// information. + /// - `branch..pushRemote` falls back to `branch..remote`. + pub fn remote_name(&self, direction: remote::Direction) -> Option> { + use bstr::{ByteSlice, ByteVec}; + let name = self.name().shorten().to_str().ok()?; + (direction == remote::Direction::Push) + .then(|| self.repo.config.resolved.string("branch", Some(name), "pushRemote")) + .flatten() + .or_else(|| self.repo.config.resolved.string("branch", Some(name), "remote")) + .and_then(|name| match name { + Cow::Borrowed(n) => n.to_str().ok().map(Cow::Borrowed), + Cow::Owned(n) => Vec::from(n).into_string().ok().map(Cow::Owned), + }) + } + + /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like + /// + /// - `branch..merge` to know which branch on the remote side corresponds to this one for merging when pulling. + pub fn remote( + &self, + direction: remote::Direction, + ) -> Option, remote::find::existing::Error>> { + let name = self.remote_name(direction)?; + // TODO: use `branch..merge` + self.repo.find_remote(name.as_ref()).into() + } } impl<'repo> std::fmt::Debug for Reference<'repo> { diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index c0bbffd70de..b7ae5ae003c 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -105,6 +105,7 @@ git clone --shared base push-default git remote add myself . git remote rename origin new-origin git config remote.pushDefault myself + git config branch.main.pushRemote myself ) git clone --shared base push-url diff --git a/git-repository/tests/reference/mod.rs b/git-repository/tests/reference/mod.rs index d5c69bd9a23..d070124a7cd 100644 --- a/git-repository/tests/reference/mod.rs +++ b/git-repository/tests/reference/mod.rs @@ -63,3 +63,60 @@ mod find { Ok(()) } } + +mod remote { + use crate::remote; + use git_repository as git; + + #[test] + fn push_defaults_to_fetch() -> crate::Result { + let repo = remote::repo("many-fetchspecs"); + let branch = repo.head()?.try_into_referent().expect("history"); + assert_eq!( + branch + .remote_name(git::remote::Direction::Push) + .expect("fallback to fetch"), + branch.remote_name(git::remote::Direction::Fetch).expect("configured"), + "push falls back to fetch" + ); + assert_eq!( + branch + .remote(git::remote::Direction::Push) + .expect("configured")? + .name() + .expect("set"), + "origin" + ); + Ok(()) + } + + #[test] + fn separate_push_and_fetch() -> crate::Result { + let repo = remote::repo("push-default"); + let branch = repo.head()?.try_into_referent().expect("history"); + + assert_eq!(branch.remote_name(git::remote::Direction::Push).expect("set"), "myself"); + assert_eq!( + branch.remote_name(git::remote::Direction::Fetch).expect("set"), + "new-origin" + ); + + assert_ne!( + branch.remote(git::remote::Direction::Push).transpose()?, + branch.remote(git::remote::Direction::Fetch).transpose()? + ); + Ok(()) + } + + #[test] + fn not_configured() -> crate::Result { + let repo = remote::repo("base"); + let branch = repo.head()?.try_into_referent().expect("history"); + + assert_eq!(branch.remote_name(git::remote::Direction::Push), None); + assert_eq!(branch.remote_name(git::remote::Direction::Fetch), None); + assert_eq!(branch.remote(git::remote::Direction::Fetch).transpose()?, None); + + Ok(()) + } +} From ba1c1622d848769784f5f2eaf7945f29cc8a864e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 10 Aug 2022 21:48:03 +0800 Subject: [PATCH 034/125] refactor (#450) --- git-repository/src/reference/mod.rs | 40 ++----------------------- git-repository/src/reference/remote.rs | 41 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 git-repository/src/reference/remote.rs diff --git a/git-repository/src/reference/mod.rs b/git-repository/src/reference/mod.rs index 164808dabcd..66fdf6f287d 100644 --- a/git-repository/src/reference/mod.rs +++ b/git-repository/src/reference/mod.rs @@ -2,11 +2,11 @@ use git_odb::pack::Find; use git_ref::file::ReferenceExt; -use std::borrow::Cow; -use crate::{bstr, remote, Id, Reference}; +use crate::{Id, Reference}; pub mod iter; +mod remote; mod errors; pub use errors::{edit, find, head_commit, head_id, peel}; @@ -47,42 +47,6 @@ impl<'repo> Reference<'repo> { pub fn detach(self) -> git_ref::Reference { self.inner } - - /// Find the name of our remote for `direction` as configured in `branch..remote|pushRemote` respectively. - /// If `Some()` it can be used in [`Repository::find_remote(…)`][crate::Repository::find_remote()], or if `None` then - /// [Repository::remote_default_name()][crate::Repository::remote_default_name()] could be used in its place. - /// - /// Return `None` if no remote is configured. - /// - /// # Note - /// - /// - it's recommended to use the [`remote(…)`][Self::remote()] method as it will configure the remote with additional - /// information. - /// - `branch..pushRemote` falls back to `branch..remote`. - pub fn remote_name(&self, direction: remote::Direction) -> Option> { - use bstr::{ByteSlice, ByteVec}; - let name = self.name().shorten().to_str().ok()?; - (direction == remote::Direction::Push) - .then(|| self.repo.config.resolved.string("branch", Some(name), "pushRemote")) - .flatten() - .or_else(|| self.repo.config.resolved.string("branch", Some(name), "remote")) - .and_then(|name| match name { - Cow::Borrowed(n) => n.to_str().ok().map(Cow::Borrowed), - Cow::Owned(n) => Vec::from(n).into_string().ok().map(Cow::Owned), - }) - } - - /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like - /// - /// - `branch..merge` to know which branch on the remote side corresponds to this one for merging when pulling. - pub fn remote( - &self, - direction: remote::Direction, - ) -> Option, remote::find::existing::Error>> { - let name = self.remote_name(direction)?; - // TODO: use `branch..merge` - self.repo.find_remote(name.as_ref()).into() - } } impl<'repo> std::fmt::Debug for Reference<'repo> { diff --git a/git-repository/src/reference/remote.rs b/git-repository/src/reference/remote.rs new file mode 100644 index 00000000000..f6946b4f44d --- /dev/null +++ b/git-repository/src/reference/remote.rs @@ -0,0 +1,41 @@ +use crate::bstr::{ByteSlice, ByteVec}; +use crate::{remote, Reference}; +use std::borrow::Cow; + +/// Remotes +impl<'repo> Reference<'repo> { + /// Find the name of our remote for `direction` as configured in `branch..remote|pushRemote` respectively. + /// If `Some()` it can be used in [`Repository::find_remote(…)`][crate::Repository::find_remote()], or if `None` then + /// [Repository::remote_default_name()][crate::Repository::remote_default_name()] could be used in its place. + /// + /// Return `None` if no remote is configured. + /// + /// # Note + /// + /// - it's recommended to use the [`remote(…)`][Self::remote()] method as it will configure the remote with additional + /// information. + /// - `branch..pushRemote` falls back to `branch..remote`. + pub fn remote_name(&self, direction: remote::Direction) -> Option> { + let name = self.name().shorten().to_str().ok()?; + (direction == remote::Direction::Push) + .then(|| self.repo.config.resolved.string("branch", Some(name), "pushRemote")) + .flatten() + .or_else(|| self.repo.config.resolved.string("branch", Some(name), "remote")) + .and_then(|name| match name { + Cow::Borrowed(n) => n.to_str().ok().map(Cow::Borrowed), + Cow::Owned(n) => Vec::from(n).into_string().ok().map(Cow::Owned), + }) + } + + /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like + /// + /// - `branch..merge` to know which branch on the remote side corresponds to this one for merging when pulling. + pub fn remote( + &self, + direction: remote::Direction, + ) -> Option, remote::find::existing::Error>> { + let name = self.remote_name(direction)?; + // TODO: use `branch..merge` + self.repo.find_remote(name.as_ref()).into() + } +} From 7a512ecdf236afc0b3d562d60fa81ab62c00cd9d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 10 Aug 2022 22:20:33 +0800 Subject: [PATCH 035/125] feat: `Head::into_remote()` to try really hard to find the correct remote (#450) --- git-repository/src/head.rs | 240 ------------------ git-repository/src/head/log.rs | 35 +++ git-repository/src/head/mod.rs | 111 ++++++++ git-repository/src/head/peel.rs | 119 +++++++++ git-repository/src/reference/remote.rs | 9 +- git-repository/src/repository/config.rs | 4 + .../tests/fixtures/make_remote_repos.sh | 10 +- git-repository/tests/git-with-regex.rs | 1 + git-repository/tests/git.rs | 2 + git-repository/tests/head/mod.rs | 14 + git-repository/tests/reference/mod.rs | 47 ++-- 11 files changed, 334 insertions(+), 258 deletions(-) delete mode 100644 git-repository/src/head.rs create mode 100644 git-repository/src/head/log.rs create mode 100644 git-repository/src/head/mod.rs create mode 100644 git-repository/src/head/peel.rs create mode 100644 git-repository/tests/head/mod.rs diff --git a/git-repository/src/head.rs b/git-repository/src/head.rs deleted file mode 100644 index 5b0205befc3..00000000000 --- a/git-repository/src/head.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! -use std::convert::TryInto; - -use git_hash::ObjectId; -use git_ref::FullNameRef; - -use crate::{ - ext::{ObjectIdExt, ReferenceExt}, - Head, -}; - -/// Represents the kind of `HEAD` reference. -#[derive(Clone)] -pub enum Kind { - /// The existing reference the symbolic HEAD points to. - /// - /// This is the common case. - Symbolic(git_ref::Reference), - /// The yet-to-be-created reference the symbolic HEAD refers to. - /// - /// This is the case in a newly initialized repository. - Unborn(git_ref::FullName), - /// The head points to an object directly, not to a symbolic reference. - /// - /// This state is less common and can occur when checking out commits directly. - Detached { - /// The object to which the head points to - target: ObjectId, - /// Possibly the final destination of `target` after following the object chain from tag objects to commits. - peeled: Option, - }, -} - -impl Kind { - /// Attach this instance to a `repo` to produce a [`Head`]. - pub fn attach(self, repo: &crate::Repository) -> Head<'_> { - Head { kind: self, repo } - } -} - -impl<'repo> Head<'repo> { - /// Returns the name of this references, always `HEAD`. - pub fn name(&self) -> &'static FullNameRef { - // TODO: use a statically checked version of this when available. - "HEAD".try_into().expect("HEAD is valid") - } - - /// Returns the full reference name of this head if it is not detached, or `None` otherwise. - pub fn referent_name(&self) -> Option<&FullNameRef> { - Some(match &self.kind { - Kind::Symbolic(r) => r.name.as_ref(), - Kind::Unborn(name) => name.as_ref(), - Kind::Detached { .. } => return None, - }) - } - /// Returns true if this instance is detached, and points to an object directly. - pub fn is_detached(&self) -> bool { - matches!(self.kind, Kind::Detached { .. }) - } - - // TODO: tests - /// Returns the id the head points to, which isn't possible on unborn heads. - pub fn id(&self) -> Option> { - match &self.kind { - Kind::Symbolic(r) => r.target.try_id().map(|oid| oid.to_owned().attach(self.repo)), - Kind::Detached { peeled, target } => { - (*peeled).unwrap_or_else(|| target.to_owned()).attach(self.repo).into() - } - Kind::Unborn(_) => None, - } - } - - /// Try to transform this instance into the symbolic reference that it points to, or return `None` if head is detached or unborn. - pub fn try_into_referent(self) -> Option> { - match self.kind { - Kind::Symbolic(r) => r.attach(self.repo).into(), - _ => None, - } - } -} -/// -pub mod log { - use std::convert::TryInto; - - use git_hash::ObjectId; - - use crate::{ - bstr::{BString, ByteSlice}, - Head, - }; - - impl<'repo> Head<'repo> { - /// Return a platform for obtaining iterators on the reference log associated with the `HEAD` reference. - pub fn log_iter(&self) -> git_ref::file::log::iter::Platform<'static, 'repo> { - git_ref::file::log::iter::Platform { - store: &self.repo.refs, - name: "HEAD".try_into().expect("HEAD is always valid"), - buf: Vec::new(), - } - } - - /// Return a list of all branch names that were previously checked out with the first-ever checked out branch - /// being the first entry of the list, and the most recent is the last, along with the commit they were pointing to - /// at the time. - pub fn prior_checked_out_branches(&self) -> std::io::Result>> { - Ok(self.log_iter().all()?.map(|log| { - log.filter_map(Result::ok) - .filter_map(|line| { - line.message - .strip_prefix(b"checkout: moving from ") - .and_then(|from_to| from_to.find(" to ").map(|pos| &from_to[..pos])) - .map(|from_branch| (from_branch.as_bstr().to_owned(), line.previous_oid())) - }) - .collect() - })) - } - } -} - -/// -pub mod peel { - use crate::{ - ext::{ObjectIdExt, ReferenceExt}, - Head, - }; - - mod error { - use crate::{object, reference}; - - /// The error returned by [Head::peel_to_id_in_place()][super::Head::peel_to_id_in_place()] and [Head::into_fully_peeled_id()][super::Head::into_fully_peeled_id()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - FindExistingObject(#[from] object::find::existing::OdbError), - #[error(transparent)] - PeelReference(#[from] reference::peel::Error), - } - } - pub use error::Error; - - use crate::head::Kind; - - /// - pub mod to_commit { - use crate::object; - - /// The error returned by [Head::peel_to_commit_in_place()][super::Head::peel_to_commit_in_place()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Peel(#[from] super::Error), - #[error("Branch '{name}' does not have any commits")] - Unborn { name: git_ref::FullName }, - #[error(transparent)] - ObjectKind(#[from] object::try_into::Error), - } - } - - impl<'repo> Head<'repo> { - // TODO: tests - /// Peel this instance to make obtaining its final target id possible, while returning an error on unborn heads. - pub fn peeled(mut self) -> Result { - self.peel_to_id_in_place().transpose()?; - Ok(self) - } - - // TODO: tests - /// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no - /// more object to follow, and return that object id. - /// - /// Returns `None` if the head is unborn. - pub fn peel_to_id_in_place(&mut self) -> Option, Error>> { - Some(match &mut self.kind { - Kind::Unborn(_name) => return None, - Kind::Detached { - peeled: Some(peeled), .. - } => Ok((*peeled).attach(self.repo)), - Kind::Detached { peeled: None, target } => { - match target - .attach(self.repo) - .object() - .map_err(Into::into) - .and_then(|obj| obj.peel_tags_to_end().map_err(Into::into)) - .map(|peeled| peeled.id) - { - Ok(peeled) => { - self.kind = Kind::Detached { - peeled: Some(peeled), - target: *target, - }; - Ok(peeled.attach(self.repo)) - } - Err(err) => Err(err), - } - } - Kind::Symbolic(r) => { - let mut nr = r.clone().attach(self.repo); - let peeled = nr.peel_to_id_in_place().map_err(Into::into); - *r = nr.detach(); - peeled - } - }) - } - - // TODO: tests - // TODO: something similar in `crate::Reference` - /// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no - /// more object to follow, transform the id into a commit if possible and return that. - /// - /// Returns an error if the head is unborn or if it doesn't point to a commit. - pub fn peel_to_commit_in_place(&mut self) -> Result, to_commit::Error> { - let id = self.peel_to_id_in_place().ok_or_else(|| to_commit::Error::Unborn { - name: self.referent_name().expect("unborn").to_owned(), - })??; - id.object() - .map_err(|err| to_commit::Error::Peel(Error::FindExistingObject(err))) - .and_then(|object| object.try_into_commit().map_err(Into::into)) - } - - /// Consume this instance and transform it into the final object that it points to, or `None` if the `HEAD` - /// reference is yet to be born. - pub fn into_fully_peeled_id(self) -> Option, Error>> { - Some(match self.kind { - Kind::Unborn(_name) => return None, - Kind::Detached { - peeled: Some(peeled), .. - } => Ok(peeled.attach(self.repo)), - Kind::Detached { peeled: None, target } => target - .attach(self.repo) - .object() - .map_err(Into::into) - .and_then(|obj| obj.peel_tags_to_end().map_err(Into::into)) - .map(|obj| obj.id.attach(self.repo)), - Kind::Symbolic(r) => r.attach(self.repo).peel_to_id_in_place().map_err(Into::into), - }) - } - } -} diff --git a/git-repository/src/head/log.rs b/git-repository/src/head/log.rs new file mode 100644 index 00000000000..b4a00319e9c --- /dev/null +++ b/git-repository/src/head/log.rs @@ -0,0 +1,35 @@ +use std::convert::TryInto; + +use git_hash::ObjectId; + +use crate::{ + bstr::{BString, ByteSlice}, + Head, +}; + +impl<'repo> Head<'repo> { + /// Return a platform for obtaining iterators on the reference log associated with the `HEAD` reference. + pub fn log_iter(&self) -> git_ref::file::log::iter::Platform<'static, 'repo> { + git_ref::file::log::iter::Platform { + store: &self.repo.refs, + name: "HEAD".try_into().expect("HEAD is always valid"), + buf: Vec::new(), + } + } + + /// Return a list of all branch names that were previously checked out with the first-ever checked out branch + /// being the first entry of the list, and the most recent is the last, along with the commit they were pointing to + /// at the time. + pub fn prior_checked_out_branches(&self) -> std::io::Result>> { + Ok(self.log_iter().all()?.map(|log| { + log.filter_map(Result::ok) + .filter_map(|line| { + line.message + .strip_prefix(b"checkout: moving from ") + .and_then(|from_to| from_to.find(" to ").map(|pos| &from_to[..pos])) + .map(|from_branch| (from_branch.as_bstr().to_owned(), line.previous_oid())) + }) + .collect() + })) + } +} diff --git a/git-repository/src/head/mod.rs b/git-repository/src/head/mod.rs new file mode 100644 index 00000000000..b5559eb1c48 --- /dev/null +++ b/git-repository/src/head/mod.rs @@ -0,0 +1,111 @@ +//! +use std::convert::TryInto; + +use git_hash::ObjectId; +use git_ref::FullNameRef; + +use crate::{ + ext::{ObjectIdExt, ReferenceExt}, + Head, +}; + +/// Represents the kind of `HEAD` reference. +#[derive(Clone)] +pub enum Kind { + /// The existing reference the symbolic HEAD points to. + /// + /// This is the common case. + Symbolic(git_ref::Reference), + /// The yet-to-be-created reference the symbolic HEAD refers to. + /// + /// This is the case in a newly initialized repository. + Unborn(git_ref::FullName), + /// The head points to an object directly, not to a symbolic reference. + /// + /// This state is less common and can occur when checking out commits directly. + Detached { + /// The object to which the head points to + target: ObjectId, + /// Possibly the final destination of `target` after following the object chain from tag objects to commits. + peeled: Option, + }, +} + +impl Kind { + /// Attach this instance to a `repo` to produce a [`Head`]. + pub fn attach(self, repo: &crate::Repository) -> Head<'_> { + Head { kind: self, repo } + } +} + +/// Access +impl<'repo> Head<'repo> { + /// Returns the name of this references, always `HEAD`. + pub fn name(&self) -> &'static FullNameRef { + // TODO: use a statically checked version of this when available. + "HEAD".try_into().expect("HEAD is valid") + } + + /// Returns the full reference name of this head if it is not detached, or `None` otherwise. + pub fn referent_name(&self) -> Option<&FullNameRef> { + Some(match &self.kind { + Kind::Symbolic(r) => r.name.as_ref(), + Kind::Unborn(name) => name.as_ref(), + Kind::Detached { .. } => return None, + }) + } + /// Returns true if this instance is detached, and points to an object directly. + pub fn is_detached(&self) -> bool { + matches!(self.kind, Kind::Detached { .. }) + } + + // TODO: tests + /// Returns the id the head points to, which isn't possible on unborn heads. + pub fn id(&self) -> Option> { + match &self.kind { + Kind::Symbolic(r) => r.target.try_id().map(|oid| oid.to_owned().attach(self.repo)), + Kind::Detached { peeled, target } => { + (*peeled).unwrap_or_else(|| target.to_owned()).attach(self.repo).into() + } + Kind::Unborn(_) => None, + } + } + + /// Try to transform this instance into the symbolic reference that it points to, or return `None` if head is detached or unborn. + pub fn try_into_referent(self) -> Option> { + match self.kind { + Kind::Symbolic(r) => r.attach(self.repo).into(), + _ => None, + } + } +} + +mod remote { + use super::Head; + use crate::{remote, Remote}; + + /// Remote + impl<'repo> Head<'repo> { + /// Return the remote with which the currently checked our reference can be handled as configured by `branch..remote|pushRemote` + /// or fall back to the non-branch specific remote configuration. + /// + /// This is equivalent to calling [`Reference::remote(…)`][crate::Reference::remote()] and + /// [`Repository::remote_default_name()`][crate::Repository::remote_default_name()] in order. + pub fn into_remote( + self, + direction: remote::Direction, + ) -> Option, remote::find::existing::Error>> { + let repo = self.repo; + self.try_into_referent()?.remote(direction).or_else(|| { + repo.remote_default_name(direction) + .map(|name| repo.find_remote(name.as_ref())) + }) + } + } +} + +/// +pub mod log; + +/// +pub mod peel; diff --git a/git-repository/src/head/peel.rs b/git-repository/src/head/peel.rs new file mode 100644 index 00000000000..7f13180e0fb --- /dev/null +++ b/git-repository/src/head/peel.rs @@ -0,0 +1,119 @@ +use crate::{ + ext::{ObjectIdExt, ReferenceExt}, + Head, +}; + +mod error { + use crate::{object, reference}; + + /// The error returned by [Head::peel_to_id_in_place()][super::Head::peel_to_id_in_place()] and [Head::into_fully_peeled_id()][super::Head::into_fully_peeled_id()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + FindExistingObject(#[from] object::find::existing::OdbError), + #[error(transparent)] + PeelReference(#[from] reference::peel::Error), + } +} + +pub use error::Error; + +use crate::head::Kind; + +/// +pub mod to_commit { + use crate::object; + + /// The error returned by [Head::peel_to_commit_in_place()][super::Head::peel_to_commit_in_place()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Peel(#[from] super::Error), + #[error("Branch '{name}' does not have any commits")] + Unborn { name: git_ref::FullName }, + #[error(transparent)] + ObjectKind(#[from] object::try_into::Error), + } +} + +impl<'repo> Head<'repo> { + // TODO: tests + /// Peel this instance to make obtaining its final target id possible, while returning an error on unborn heads. + pub fn peeled(mut self) -> Result { + self.peel_to_id_in_place().transpose()?; + Ok(self) + } + + // TODO: tests + /// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no + /// more object to follow, and return that object id. + /// + /// Returns `None` if the head is unborn. + pub fn peel_to_id_in_place(&mut self) -> Option, Error>> { + Some(match &mut self.kind { + Kind::Unborn(_name) => return None, + Kind::Detached { + peeled: Some(peeled), .. + } => Ok((*peeled).attach(self.repo)), + Kind::Detached { peeled: None, target } => { + match target + .attach(self.repo) + .object() + .map_err(Into::into) + .and_then(|obj| obj.peel_tags_to_end().map_err(Into::into)) + .map(|peeled| peeled.id) + { + Ok(peeled) => { + self.kind = Kind::Detached { + peeled: Some(peeled), + target: *target, + }; + Ok(peeled.attach(self.repo)) + } + Err(err) => Err(err), + } + } + Kind::Symbolic(r) => { + let mut nr = r.clone().attach(self.repo); + let peeled = nr.peel_to_id_in_place().map_err(Into::into); + *r = nr.detach(); + peeled + } + }) + } + + // TODO: tests + // TODO: something similar in `crate::Reference` + /// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no + /// more object to follow, transform the id into a commit if possible and return that. + /// + /// Returns an error if the head is unborn or if it doesn't point to a commit. + pub fn peel_to_commit_in_place(&mut self) -> Result, to_commit::Error> { + let id = self.peel_to_id_in_place().ok_or_else(|| to_commit::Error::Unborn { + name: self.referent_name().expect("unborn").to_owned(), + })??; + id.object() + .map_err(|err| to_commit::Error::Peel(Error::FindExistingObject(err))) + .and_then(|object| object.try_into_commit().map_err(Into::into)) + } + + /// Consume this instance and transform it into the final object that it points to, or `None` if the `HEAD` + /// reference is yet to be born. + pub fn into_fully_peeled_id(self) -> Option, Error>> { + Some(match self.kind { + Kind::Unborn(_name) => return None, + Kind::Detached { + peeled: Some(peeled), .. + } => Ok(peeled.attach(self.repo)), + Kind::Detached { peeled: None, target } => target + .attach(self.repo) + .object() + .map_err(Into::into) + .and_then(|obj| obj.peel_tags_to_end().map_err(Into::into)) + .map(|obj| obj.id.attach(self.repo)), + Kind::Symbolic(r) => r.attach(self.repo).peel_to_id_in_place().map_err(Into::into), + }) + } +} diff --git a/git-repository/src/reference/remote.rs b/git-repository/src/reference/remote.rs index f6946b4f44d..643a9a00a6b 100644 --- a/git-repository/src/reference/remote.rs +++ b/git-repository/src/reference/remote.rs @@ -17,10 +17,15 @@ impl<'repo> Reference<'repo> { /// - `branch..pushRemote` falls back to `branch..remote`. pub fn remote_name(&self, direction: remote::Direction) -> Option> { let name = self.name().shorten().to_str().ok()?; + let config = &self.repo.config.resolved; (direction == remote::Direction::Push) - .then(|| self.repo.config.resolved.string("branch", Some(name), "pushRemote")) + .then(|| { + config + .string("branch", Some(name), "pushRemote") + .or_else(|| config.string("remote", None, "pushDefault")) + }) .flatten() - .or_else(|| self.repo.config.resolved.string("branch", Some(name), "remote")) + .or_else(|| config.string("branch", Some(name), "remote")) .and_then(|name| match name { Cow::Borrowed(n) => n.to_str().ok().map(Cow::Borrowed), Cow::Owned(n) => Vec::from(n).into_string().ok().map(Cow::Owned), diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index af360655402..956ac5de4f8 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -38,6 +38,10 @@ mod remote { /// /// 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. pub fn remote_default_name(&self, direction: remote::Direction) -> Option> { let name = (direction == remote::Direction::Push) .then(|| { diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index b7ae5ae003c..e95f315c726 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -105,7 +105,6 @@ git clone --shared base push-default git remote add myself . git remote rename origin new-origin git config remote.pushDefault myself - git config branch.main.pushRemote myself ) git clone --shared base push-url @@ -123,6 +122,15 @@ git clone --shared base many-fetchspecs git config --add remote.origin.fetch HEAD ) +git clone --shared base branch-push-remote +( + cd branch-push-remote + + git remote rename origin new-origin + git remote add myself . + git config branch.main.pushRemote myself +) + git init --bare url-rewriting ( cd url-rewriting diff --git a/git-repository/tests/git-with-regex.rs b/git-repository/tests/git-with-regex.rs index 98857c6649c..0be6ff1efe4 100644 --- a/git-repository/tests/git-with-regex.rs +++ b/git-repository/tests/git-with-regex.rs @@ -2,6 +2,7 @@ mod util; use util::*; mod commit; +mod head; mod id; mod init; mod object; diff --git a/git-repository/tests/git.rs b/git-repository/tests/git.rs index 434ea4f0c56..305de9db9dd 100644 --- a/git-repository/tests/git.rs +++ b/git-repository/tests/git.rs @@ -6,6 +6,8 @@ use util::*; #[cfg(not(feature = "regex"))] mod commit; #[cfg(not(feature = "regex"))] +mod head; +#[cfg(not(feature = "regex"))] mod id; #[cfg(not(feature = "regex"))] mod init; diff --git a/git-repository/tests/head/mod.rs b/git-repository/tests/head/mod.rs new file mode 100644 index 00000000000..8bd1534d86d --- /dev/null +++ b/git-repository/tests/head/mod.rs @@ -0,0 +1,14 @@ +mod remote { + use crate::remote; + use git_repository as git; + + #[test] + fn unborn_is_none() -> crate::Result { + let repo = remote::repo("url-rewriting"); + assert_eq!( + repo.head()?.into_remote(git::remote::Direction::Fetch).transpose()?, + None + ); + Ok(()) + } +} diff --git a/git-repository/tests/reference/mod.rs b/git-repository/tests/reference/mod.rs index d070124a7cd..7339fc66165 100644 --- a/git-repository/tests/reference/mod.rs +++ b/git-repository/tests/reference/mod.rs @@ -71,7 +71,8 @@ mod remote { #[test] fn push_defaults_to_fetch() -> crate::Result { let repo = remote::repo("many-fetchspecs"); - let branch = repo.head()?.try_into_referent().expect("history"); + let head = repo.head()?; + let branch = head.clone().try_into_referent().expect("history"); assert_eq!( branch .remote_name(git::remote::Direction::Push) @@ -87,35 +88,51 @@ mod remote { .expect("set"), "origin" ); + assert_eq!( + head.into_remote(git::remote::Direction::Push) + .expect("same with branch")? + .name() + .expect("set"), + "origin" + ); Ok(()) } #[test] fn separate_push_and_fetch() -> crate::Result { - let repo = remote::repo("push-default"); - let branch = repo.head()?.try_into_referent().expect("history"); - - assert_eq!(branch.remote_name(git::remote::Direction::Push).expect("set"), "myself"); - assert_eq!( - branch.remote_name(git::remote::Direction::Fetch).expect("set"), - "new-origin" - ); - - assert_ne!( - branch.remote(git::remote::Direction::Push).transpose()?, - branch.remote(git::remote::Direction::Fetch).transpose()? - ); + for name in ["push-default", "branch-push-remote"] { + let repo = remote::repo(name); + let head = repo.head()?; + let branch = head.clone().try_into_referent().expect("history"); + + assert_eq!(branch.remote_name(git::remote::Direction::Push).expect("set"), "myself"); + assert_eq!( + branch.remote_name(git::remote::Direction::Fetch).expect("set"), + "new-origin" + ); + + assert_ne!( + branch.remote(git::remote::Direction::Push).transpose()?, + branch.remote(git::remote::Direction::Fetch).transpose()? + ); + assert_ne!( + head.clone().into_remote(git::remote::Direction::Push).transpose()?, + head.into_remote(git::remote::Direction::Fetch).transpose()? + ); + } Ok(()) } #[test] fn not_configured() -> crate::Result { let repo = remote::repo("base"); - let branch = repo.head()?.try_into_referent().expect("history"); + let head = repo.head()?; + let branch = head.clone().try_into_referent().expect("history"); assert_eq!(branch.remote_name(git::remote::Direction::Push), None); assert_eq!(branch.remote_name(git::remote::Direction::Fetch), None); assert_eq!(branch.remote(git::remote::Direction::Fetch).transpose()?, None); + assert_eq!(head.into_remote(git::remote::Direction::Fetch).transpose()?, None); Ok(()) } From a67fc26b80e5d1183ddc5c6598396214f3e19945 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 10:36:57 +0800 Subject: [PATCH 036/125] feat: more conversions for `TryFrom`: `String` and `&str` (#450) --- git-repository/src/remote/access.rs | 0 git-repository/src/remote/errors.rs | 0 git-repository/src/{remote.rs => remote/mod.rs} | 0 git-repository/src/remote/url.rs | 0 git-url/src/lib.rs | 16 ++++++++++++++++ 5 files changed, 16 insertions(+) create mode 100644 git-repository/src/remote/access.rs create mode 100644 git-repository/src/remote/errors.rs rename git-repository/src/{remote.rs => remote/mod.rs} (100%) create mode 100644 git-repository/src/remote/url.rs diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/git-repository/src/remote/errors.rs b/git-repository/src/remote/errors.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/git-repository/src/remote.rs b/git-repository/src/remote/mod.rs similarity index 100% rename from git-repository/src/remote.rs rename to git-repository/src/remote/mod.rs diff --git a/git-repository/src/remote/url.rs b/git-repository/src/remote/url.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index a6e451ad3a7..0d03578f8dc 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -137,6 +137,22 @@ impl Url { } } +impl TryFrom<&str> for Url { + type Error = parse::Error; + + fn try_from(value: &str) -> Result { + Self::from_bytes(value.as_bytes()) + } +} + +impl TryFrom for Url { + type Error = parse::Error; + + fn try_from(value: String) -> Result { + Self::from_bytes(value.as_bytes()) + } +} + impl TryFrom<&BStr> for Url { type Error = parse::Error; From 9b07b91ad065836e7473df6635025659af2865ee Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 10:40:37 +0800 Subject: [PATCH 037/125] =?UTF-8?q?feat:=20`Repository::remote=5Fat(?= =?UTF-8?q?=E2=80=A6)`=20to=20create=20an=20unnamed=20remote=20(#450)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- git-repository/src/remote/access.rs | 49 ++++ git-repository/src/remote/errors.rs | 59 +++++ git-repository/src/remote/mod.rs | 271 +++++++--------------- git-repository/src/remote/url.rs | 96 ++++++++ git-repository/src/repository/remote.rs | 58 ++--- git-repository/tests/repository/remote.rs | 55 ++++- 6 files changed, 344 insertions(+), 244 deletions(-) diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index e69de29bb2d..e013d6c615d 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -0,0 +1,49 @@ +use crate::{remote, Remote}; +use git_refspec::RefSpec; + +/// Builder methods +impl Remote<'_> { + /// By default, `url..insteadOf|pushInsteadOf` configuration variables will be applied to rewrite fetch and push + /// urls unless `toggle` is `false`. + pub fn apply_url_aliases(mut self, toggle: bool) -> Self { + self.apply_url_aliases = toggle; + self + } +} + +impl Remote<'_> { + /// Return the name of this remote or `None` if it wasn't persisted to disk yet. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Return the set of ref-specs used for `direction`, which may be empty, in order of occurrence in the configuration. + pub fn refspecs(&self, direction: remote::Direction) -> &[RefSpec] { + match direction { + remote::Direction::Fetch => &self.fetch_specs, + remote::Direction::Push => &self.push_specs, + } + } + + /// Return the url used for the given `direction` with rewrites from `url..insteadOf|pushInsteadOf` applied unless + /// [`apply_url_aliases(false)`][Self::apply_url_aliases()] was called before. + /// For pushing, this is the `remote..pushUrl` or the `remote..url` used for fetching, and for fetching it's + /// the `remote..url`. + /// Note that it's possible to only have the push url set, in which case there will be no way to fetch from the remote as + /// the push-url isn't used for that. + pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { + match direction { + remote::Direction::Fetch => self + .apply_url_aliases + .then(|| self.url_alias.as_ref()) + .flatten() + .or(self.url.as_ref()), + remote::Direction::Push => self + .apply_url_aliases + .then(|| self.push_url_alias.as_ref()) + .flatten() + .or(self.push_url.as_ref()) + .or_else(|| self.url(remote::Direction::Fetch)), + } + } +} diff --git a/git-repository/src/remote/errors.rs b/git-repository/src/remote/errors.rs index e69de29bb2d..3bb78f5b22e 100644 --- a/git-repository/src/remote/errors.rs +++ b/git-repository/src/remote/errors.rs @@ -0,0 +1,59 @@ +/// +pub mod init { + use crate::bstr::BString; + + /// The error returned by [`Repository::remote_at(…)`][crate::Repository::remote_at()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Url(#[from] git_url::parse::Error), + #[error("The rewritten {kind} url {rewritten_url:?} failed to parse")] + RewrittenUrlInvalid { + kind: &'static str, + rewritten_url: BString, + source: git_url::parse::Error, + }, + } +} + +/// +pub mod find { + use crate::bstr::BString; + use crate::remote; + + /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("{spec:?} {kind} ref-spec failed to parse")] + RefSpec { + spec: BString, + kind: &'static str, + source: git_refspec::parse::Error, + }, + #[error("Neither 'url` nor 'pushUrl' fields were set in the remote's configuration.")] + UrlMissing, + #[error("The {kind} url couldn't be parsed")] + Url { + kind: &'static str, + url: BString, + source: git_url::parse::Error, + }, + #[error(transparent)] + Init(#[from] remote::init::Error), + } + + /// + pub mod existing { + /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Find(#[from] super::Error), + #[error("The remote named {name:?} did not exist")] + NotFound { name: String }, + } + } +} diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index c0ad6503925..b4c9585f45c 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -1,204 +1,95 @@ -mod errors { - /// - pub mod find { - use crate::bstr::BString; - - /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error("{spec:?} {kind} ref-spec failed to parse")] - RefSpec { - spec: BString, - kind: &'static str, - source: git_refspec::parse::Error, - }, - #[error("Neither 'url` nor 'pushUrl' fields were set in the remote's configuration.")] - UrlMissing, - #[error("The {kind} url couldn't be parsed")] - UrlInvalid { - kind: &'static str, - url: BString, - source: git_url::parse::Error, - }, - #[error("The rewritten {kind} url {rewritten_url:?} failed to parse")] - RewrittenUrlInvalid { - kind: &'static str, - rewritten_url: BString, - source: git_url::parse::Error, - }, - } - - /// - pub mod existing { - /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(#[from] super::Error), - #[error("The remote named {name:?} did not exist")] - NotFound { name: String }, - } - } - } +/// The direction of an operation carried out (or to be carried out) through a remote. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub enum Direction { + /// Push local changes to the remote. + Push, + /// Fetch changes from the remote to the local repository. + Fetch, } -pub use errors::find; -mod access { - use crate::{remote, Remote}; +mod create { + use crate::{config, remote, Remote, Repository}; use git_refspec::RefSpec; - - /// Builder methods - impl Remote<'_> { - /// By default, `url..insteadOf|pushInsteadOf` configuration variables will be applied to rewrite fetch and push - /// urls unless `toggle` is `false`. - pub fn apply_url_aliases(mut self, toggle: bool) -> Self { - self.apply_url_aliases = toggle; - self - } - } - - impl Remote<'_> { - /// Return the name of this remote or `None` if it wasn't persisted to disk yet. - pub fn name(&self) -> Option<&str> { - self.name.as_deref() + use std::convert::TryInto; + + /// Initialization + impl<'repo> Remote<'repo> { + pub(crate) fn from_preparsed_config( + name: Option, + url: Option, + push_url: Option, + fetch_specs: Vec, + push_specs: Vec, + repo: &'repo Repository, + ) -> Result { + debug_assert!( + url.is_some() || push_url.is_some(), + "BUG: fetch or push url must be set at least" + ); + let (url_alias, push_url_alias) = rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())?; + Ok(Remote { + name: name.to_owned().into(), + url, + url_alias, + push_url, + push_url_alias, + fetch_specs, + push_specs, + apply_url_aliases: true, + repo, + }) } - /// Return the set of ref-specs used for `direction`, which may be empty, in order of occurrence in the configuration. - pub fn refspecs(&self, direction: remote::Direction) -> &[RefSpec] { - match direction { - remote::Direction::Fetch => &self.fetch_specs, - remote::Direction::Push => &self.push_specs, - } + pub(crate) fn from_fetch_url(url: Url, repo: &'repo Repository) -> Result + where + Url: TryInto, + remote::init::Error: From, + { + let url = url.try_into()?; + let (url_alias, _) = rewrite_urls(&repo.config, Some(&url), None)?; + Ok(Remote { + name: None, + url: Some(url), + url_alias, + push_url: None, + push_url_alias: None, + fetch_specs: Vec::new(), + push_specs: Vec::new(), + apply_url_aliases: true, + repo, + }) } - - /// Return the url used for the given `direction` with rewrites from `url..insteadOf|pushInsteadOf` applied unless - /// [`apply_url_aliases(false)`][Self::apply_url_aliases()] was called before. - /// For pushing, this is the `remote..pushUrl` or the `remote..url` used for fetching, and for fetching it's - /// the `remote..url`. - pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { - match direction { - remote::Direction::Fetch => self - .apply_url_aliases - .then(|| self.url_alias.as_ref()) - .flatten() - .or(self.url.as_ref()), - remote::Direction::Push => self - .apply_url_aliases - .then(|| self.push_url_alias.as_ref()) - .flatten() - .or(self.push_url.as_ref()) - .or_else(|| self.url(remote::Direction::Fetch)), - } - } - } -} - -pub(crate) mod url { - use crate::bstr::{BStr, BString, ByteVec}; - use crate::remote::Direction; - use git_features::threading::OwnShared; - - #[derive(Debug, Clone)] - pub(crate) struct Replace { - find: BString, - with: OwnShared, - } - - #[derive(Default, Debug, Clone)] - pub(crate) struct Rewrite { - url_rewrite: Vec, - push_url_rewrite: Vec, } - /// Init - impl Rewrite { - pub fn from_config( - config: &git_config::File<'static>, - mut filter: fn(&git_config::file::Metadata) -> bool, - ) -> Rewrite { - config - .sections_by_name_and_filter("url", &mut filter) - .map(|sections| { - let mut url_rewrite = Vec::new(); - let mut push_url_rewrite = Vec::new(); - for section in sections { - let replace = match section.header().subsection_name() { - Some(base) => OwnShared::new(base.to_owned()), - None => continue, - }; - - for instead_of in section.values("insteadOf") { - url_rewrite.push(Replace { - with: OwnShared::clone(&replace), - find: instead_of.into_owned(), - }); - } - for instead_of in section.values("pushInsteadOf") { - push_url_rewrite.push(Replace { - with: OwnShared::clone(&replace), - find: instead_of.into_owned(), - }); - } - } - Rewrite { - url_rewrite, - push_url_rewrite, - } + fn rewrite_urls( + config: &config::Cache, + url: Option<&git_url::Url>, + push_url: Option<&git_url::Url>, + ) -> Result<(Option, Option), remote::init::Error> { + let rewrite = |url: Option<&git_url::Url>, direction: remote::Direction| { + url.and_then(|url| config.url_rewrite().rewrite_url(url, direction)) + .map(|url| { + git_url::parse(&url).map_err(|err| remote::init::Error::RewrittenUrlInvalid { + kind: match direction { + remote::Direction::Fetch => "fetch", + remote::Direction::Push => "push", + }, + source: err, + rewritten_url: url, + }) }) - .unwrap_or_default() - } - } + .transpose() + }; - /// Access - impl Rewrite { - fn replacements_for(&self, direction: Direction) -> &[Replace] { - match direction { - Direction::Fetch => &self.url_rewrite, - Direction::Push => &self.push_url_rewrite, - } - } - - pub fn rewrite_url(&self, url: &git_url::Url, direction: Direction) -> Option { - if self.replacements_for(direction).is_empty() { - None - } else { - let mut url = url.to_bstring().ok()?; - self.rewrite_url_in_place(&mut url, direction).then(|| url) - } - } + let url_alias = rewrite(url, remote::Direction::Fetch)?; + let push_url_alias = rewrite(push_url, remote::Direction::Push)?; - /// Rewrite the given `url` of `direction` and return `true` if a replacement happened. - /// - /// Note that the result must still be checked for validity, it might not be a valid URL as we do a syntax-unaware replacement. - pub fn rewrite_url_in_place(&self, url: &mut BString, direction: Direction) -> bool { - self.replacements_for(direction) - .iter() - .fold(None::<(usize, &BStr)>, |mut acc, replace| { - if url.starts_with(replace.find.as_ref()) { - let (bytes_matched, prev_rewrite_with) = - acc.get_or_insert((replace.find.len(), replace.with.as_slice().into())); - if *bytes_matched < replace.find.len() { - *bytes_matched = replace.find.len(); - *prev_rewrite_with = replace.with.as_slice().into(); - } - }; - acc - }) - .map(|(bytes_matched, replace_with)| { - url.replace_range(..bytes_matched, replace_with); - }) - .is_some() - } + Ok((url_alias, push_url_alias)) } } -/// The direction of an operation carried out (or to be carried out) through a remote. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] -pub enum Direction { - /// Push local changes to the remote. - Push, - /// Fetch changes from the remote to the local repository. - Fetch, -} +mod errors; +pub use errors::{find, init}; + +mod access; +pub(crate) mod url; diff --git a/git-repository/src/remote/url.rs b/git-repository/src/remote/url.rs index e69de29bb2d..cb34f447a3c 100644 --- a/git-repository/src/remote/url.rs +++ b/git-repository/src/remote/url.rs @@ -0,0 +1,96 @@ +use crate::bstr::{BStr, BString, ByteVec}; +use crate::remote::Direction; +use git_features::threading::OwnShared; + +#[derive(Debug, Clone)] +pub(crate) struct Replace { + find: BString, + with: OwnShared, +} + +#[derive(Default, Debug, Clone)] +pub(crate) struct Rewrite { + url_rewrite: Vec, + push_url_rewrite: Vec, +} + +/// Init +impl Rewrite { + pub fn from_config( + config: &git_config::File<'static>, + mut filter: fn(&git_config::file::Metadata) -> bool, + ) -> Rewrite { + config + .sections_by_name_and_filter("url", &mut filter) + .map(|sections| { + let mut url_rewrite = Vec::new(); + let mut push_url_rewrite = Vec::new(); + for section in sections { + let replace = match section.header().subsection_name() { + Some(base) => OwnShared::new(base.to_owned()), + None => continue, + }; + + for instead_of in section.values("insteadOf") { + url_rewrite.push(Replace { + with: OwnShared::clone(&replace), + find: instead_of.into_owned(), + }); + } + for instead_of in section.values("pushInsteadOf") { + push_url_rewrite.push(Replace { + with: OwnShared::clone(&replace), + find: instead_of.into_owned(), + }); + } + } + Rewrite { + url_rewrite, + push_url_rewrite, + } + }) + .unwrap_or_default() + } +} + +/// Access +impl Rewrite { + fn replacements_for(&self, direction: Direction) -> &[Replace] { + match direction { + Direction::Fetch => &self.url_rewrite, + Direction::Push => &self.push_url_rewrite, + } + } + + pub fn rewrite_url(&self, url: &git_url::Url, direction: Direction) -> Option { + if self.replacements_for(direction).is_empty() { + None + } else { + let mut url = url.to_bstring().ok()?; + self.rewrite_url_in_place(&mut url, direction).then(|| url) + } + } + + /// Rewrite the given `url` of `direction` and return `true` if a replacement happened. + /// + /// Note that the result must still be checked for validity, it might not be a valid URL as we do a syntax-unaware replacement. + pub fn rewrite_url_in_place(&self, url: &mut BString, direction: Direction) -> bool { + self.replacements_for(direction) + .iter() + .fold(None::<(usize, &BStr)>, |mut acc, replace| { + if url.starts_with(replace.find.as_ref()) { + let (bytes_matched, prev_rewrite_with) = + acc.get_or_insert((replace.find.len(), replace.with.as_slice().into())); + if *bytes_matched < replace.find.len() { + *bytes_matched = replace.find.len(); + *prev_rewrite_with = replace.with.as_slice().into(); + } + }; + acc + }) + .map(|(bytes_matched, replace_with)| { + url.replace_range(..bytes_matched, replace_with); + }) + .is_some() + } +} diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index c8e95e7f2ec..9e32629a454 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -1,7 +1,16 @@ use crate::remote::find; -use crate::{config, remote, Remote}; +use crate::{remote, Remote}; +use std::convert::TryInto; impl crate::Repository { + /// Create a new remote available at the given `url`. + pub fn remote_at(&self, url: Url) -> Result, remote::init::Error> + where + Url: TryInto, + remote::init::Error: From, + { + Remote::from_fetch_url(url, self) + } /// Find the remote with the given `name` or report an error, similar to [`try_find_remote(…)`][Self::try_find_remote()]. /// /// Note that we will include remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()]. @@ -28,7 +37,7 @@ impl crate::Repository { .resolved .string_filter("remote", name.into(), field, &mut filter) .map(|url| { - git_url::parse::parse(url.as_ref()).map_err(|err| find::Error::UrlInvalid { + git_url::parse::parse(url.as_ref()).map_err(|err| find::Error::Url { kind, url: url.into_owned(), source: err, @@ -94,48 +103,11 @@ impl crate::Repository { None => Vec::new(), }; - let (url_alias, push_url_alias) = match rewrite_urls(&self.config, url.as_ref(), push_url.as_ref()) { - Ok(t) => t, - Err(err) => return Some(Err(err)), - }; - Some(Ok(Remote { - name: name.to_owned().into(), - url, - url_alias, - push_url, - push_url_alias, - fetch_specs, - push_specs, - apply_url_aliases: true, - repo: self, - })) + Some( + Remote::from_preparsed_config(name.to_owned().into(), url, push_url, fetch_specs, push_specs, self) + .map_err(Into::into), + ) } } } } - -fn rewrite_urls( - config: &config::Cache, - url: Option<&git_url::Url>, - push_url: Option<&git_url::Url>, -) -> Result<(Option, Option), find::Error> { - let rewrite = |url: Option<&git_url::Url>, direction: remote::Direction| { - url.and_then(|url| config.url_rewrite().rewrite_url(url, direction)) - .map(|url| { - git_url::parse(&url).map_err(|err| find::Error::RewrittenUrlInvalid { - kind: match direction { - remote::Direction::Fetch => "fetch", - remote::Direction::Push => "push", - }, - source: err, - rewritten_url: url, - }) - }) - .transpose() - }; - - let url_alias = rewrite(url, remote::Direction::Fetch)?; - let push_url_alias = rewrite(push_url, remote::Direction::Push)?; - - Ok((url_alias, push_url_alias)) -} diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 07e97c29b9d..64df770eb33 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -1,3 +1,42 @@ +mod remote_at { + use crate::remote; + use git_repository::remote::Direction; + + #[test] + fn url_rewrites_are_respected() -> crate::Result { + let repo = remote::repo("url-rewriting"); + let remote = repo.remote_at("https://github.com/foobar/gitoxide")?; + + assert_eq!(remote.name(), None, "anonymous remotes are unnamed"); + let rewritten_fetch_url = "https://github.com/byron/gitoxide"; + assert_eq!( + remote.url(Direction::Fetch).unwrap().to_bstring()?, + rewritten_fetch_url, + "fetch was rewritten" + ); + assert_eq!( + remote.url(Direction::Push).unwrap().to_bstring()?, + rewritten_fetch_url, + "push is the same as fetch was rewritten" + ); + + // TODO: push-url addition, should be same as this one + let expected_url = "file://dev/null"; + let remote = repo.remote_at(expected_url.to_owned())?; + assert_eq!( + remote.url(Direction::Fetch).unwrap().to_bstring()?, + expected_url, + "there is no rule to rewrite this url" + ); + assert_eq!( + remote.url(Direction::Push).unwrap().to_bstring()?, + expected_url, + "there is no rule to rewrite this url" + ); + Ok(()) + } +} + mod find_remote { use crate::remote; use git_object::bstr::BString; @@ -21,7 +60,7 @@ mod find_remote { assert_eq!(remote.name(), Some(name)); let url = git::url::parse(url.as_bytes()).expect("valid"); - assert_eq!(remote.url(Direction::Fetch), Some(&url)); + assert_eq!(remote.url(Direction::Fetch).unwrap(), &url); assert_eq!( remote.refspecs(Direction::Fetch), @@ -74,12 +113,9 @@ mod find_remote { let expected_push_url: BString = baseline.next().expect("push").into(); let remote = repo.find_remote("origin")?; - assert_eq!( - remote.url(Direction::Fetch).expect("present").to_bstring()?, - expected_fetch_url, - ); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, expected_fetch_url,); { - let actual_push_url = remote.url(Direction::Push).expect("present").to_bstring()?; + let actual_push_url = remote.url(Direction::Push).unwrap().to_bstring()?; assert_ne!( actual_push_url, expected_push_url, "here we actually resolve something that git doesn't for unknown reason" @@ -92,13 +128,10 @@ mod find_remote { let remote = remote.apply_url_aliases(false); assert_eq!( - remote.url(Direction::Fetch).expect("present").to_bstring()?, + remote.url(Direction::Fetch).unwrap().to_bstring()?, "https://github.com/foobar/gitoxide" ); - assert_eq!( - remote.url(Direction::Push).expect("present").to_bstring()?, - "file://dev/null" - ); + assert_eq!(remote.url(Direction::Push).unwrap().to_bstring()?, "file://dev/null"); Ok(()) } From d51ba42c643d8ee03a3c6b648f8524ff04827170 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 11:06:11 +0800 Subject: [PATCH 038/125] feat: `Remote::push_url()` to set it after the fact (#450) --- git-repository/src/remote/access.rs | 18 +++++ git-repository/src/remote/create.rs | 78 +++++++++++++++++++++ git-repository/src/remote/mod.rs | 82 +---------------------- git-repository/src/repository/remote.rs | 2 +- git-repository/tests/repository/remote.rs | 36 +++++++--- 5 files changed, 124 insertions(+), 92 deletions(-) create mode 100644 git-repository/src/remote/create.rs diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index e013d6c615d..cf29d393b54 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -1,5 +1,6 @@ use crate::{remote, Remote}; use git_refspec::RefSpec; +use std::convert::TryInto; /// Builder methods impl Remote<'_> { @@ -9,6 +10,23 @@ impl Remote<'_> { self.apply_url_aliases = toggle; self } + + /// Set the `push_url` to be used when pushing data to a remote. + pub fn push_url(mut self, push_url: Url) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + let push_url = push_url + .try_into() + .map_err(|err| remote::init::Error::Url(err.into()))?; + self.push_url = push_url.into(); + + let (_, push_url_alias) = remote::create::rewrite_urls(&self.repo.config, None, self.push_url.as_ref())?; + self.push_url_alias = push_url_alias; + + Ok(self) + } } impl Remote<'_> { diff --git a/git-repository/src/remote/create.rs b/git-repository/src/remote/create.rs new file mode 100644 index 00000000000..c5751a84b94 --- /dev/null +++ b/git-repository/src/remote/create.rs @@ -0,0 +1,78 @@ +use crate::{config, remote, Remote, Repository}; +use git_refspec::RefSpec; +use std::convert::TryInto; + +/// Initialization +impl<'repo> Remote<'repo> { + pub(crate) fn from_preparsed_config( + name: Option, + url: Option, + push_url: Option, + fetch_specs: Vec, + push_specs: Vec, + repo: &'repo Repository, + ) -> Result { + debug_assert!( + url.is_some() || push_url.is_some(), + "BUG: fetch or push url must be set at least" + ); + let (url_alias, push_url_alias) = rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())?; + Ok(Remote { + name: name.to_owned().into(), + url, + url_alias, + push_url, + push_url_alias, + fetch_specs, + push_specs, + apply_url_aliases: true, + repo, + }) + } + + pub(crate) fn from_fetch_url(url: Url, repo: &'repo Repository) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + let url = url.try_into().map_err(|err| remote::init::Error::Url(err.into()))?; + let (url_alias, _) = rewrite_urls(&repo.config, Some(&url), None)?; + Ok(Remote { + name: None, + url: Some(url), + url_alias, + push_url: None, + push_url_alias: None, + fetch_specs: Vec::new(), + push_specs: Vec::new(), + apply_url_aliases: true, + repo, + }) + } +} + +pub(crate) fn rewrite_urls( + config: &config::Cache, + url: Option<&git_url::Url>, + push_url: Option<&git_url::Url>, +) -> Result<(Option, Option), remote::init::Error> { + let rewrite = |url: Option<&git_url::Url>, direction: remote::Direction| { + url.and_then(|url| config.url_rewrite().rewrite_url(url, direction)) + .map(|url| { + git_url::parse(&url).map_err(|err| remote::init::Error::RewrittenUrlInvalid { + kind: match direction { + remote::Direction::Fetch => "fetch", + remote::Direction::Push => "push", + }, + source: err, + rewritten_url: url, + }) + }) + .transpose() + }; + + let url_alias = rewrite(url, remote::Direction::Fetch)?; + let push_url_alias = rewrite(push_url, remote::Direction::Push)?; + + Ok((url_alias, push_url_alias)) +} diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index b4c9585f45c..3036b096229 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -7,87 +7,7 @@ pub enum Direction { Fetch, } -mod create { - use crate::{config, remote, Remote, Repository}; - use git_refspec::RefSpec; - use std::convert::TryInto; - - /// Initialization - impl<'repo> Remote<'repo> { - pub(crate) fn from_preparsed_config( - name: Option, - url: Option, - push_url: Option, - fetch_specs: Vec, - push_specs: Vec, - repo: &'repo Repository, - ) -> Result { - debug_assert!( - url.is_some() || push_url.is_some(), - "BUG: fetch or push url must be set at least" - ); - let (url_alias, push_url_alias) = rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())?; - Ok(Remote { - name: name.to_owned().into(), - url, - url_alias, - push_url, - push_url_alias, - fetch_specs, - push_specs, - apply_url_aliases: true, - repo, - }) - } - - pub(crate) fn from_fetch_url(url: Url, repo: &'repo Repository) -> Result - where - Url: TryInto, - remote::init::Error: From, - { - let url = url.try_into()?; - let (url_alias, _) = rewrite_urls(&repo.config, Some(&url), None)?; - Ok(Remote { - name: None, - url: Some(url), - url_alias, - push_url: None, - push_url_alias: None, - fetch_specs: Vec::new(), - push_specs: Vec::new(), - apply_url_aliases: true, - repo, - }) - } - } - - fn rewrite_urls( - config: &config::Cache, - url: Option<&git_url::Url>, - push_url: Option<&git_url::Url>, - ) -> Result<(Option, Option), remote::init::Error> { - let rewrite = |url: Option<&git_url::Url>, direction: remote::Direction| { - url.and_then(|url| config.url_rewrite().rewrite_url(url, direction)) - .map(|url| { - git_url::parse(&url).map_err(|err| remote::init::Error::RewrittenUrlInvalid { - kind: match direction { - remote::Direction::Fetch => "fetch", - remote::Direction::Push => "push", - }, - source: err, - rewritten_url: url, - }) - }) - .transpose() - }; - - let url_alias = rewrite(url, remote::Direction::Fetch)?; - let push_url_alias = rewrite(push_url, remote::Direction::Push)?; - - Ok((url_alias, push_url_alias)) - } -} - +mod create; mod errors; pub use errors::{find, init}; diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 9e32629a454..9297575ef8d 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -7,7 +7,7 @@ impl crate::Repository { pub fn remote_at(&self, url: Url) -> Result, remote::init::Error> where Url: TryInto, - remote::init::Error: From, + git_url::parse::Error: From, { Remote::from_fetch_url(url, self) } diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 64df770eb33..a6eb61bb5d4 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -2,6 +2,26 @@ mod remote_at { use crate::remote; use git_repository::remote::Direction; + #[test] + fn url_and_push_url() -> crate::Result { + let repo = remote::repo("base"); + let fetch_url = "https://github.com/byron/gitoxide"; + let remote = repo.remote_at(fetch_url)?; + + assert_eq!(remote.name(), None); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, fetch_url); + assert_eq!(remote.url(Direction::Push).unwrap().to_bstring()?, fetch_url); + + let remote = remote.push_url("user@host.xz:./relative")?; + assert_eq!( + remote.url(Direction::Push).unwrap().to_bstring()?, + "ssh://user@host.xz/relative" + ); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, fetch_url); + + Ok(()) + } + #[test] fn url_rewrites_are_respected() -> crate::Result { let repo = remote::repo("url-rewriting"); @@ -20,18 +40,14 @@ mod remote_at { "push is the same as fetch was rewritten" ); - // TODO: push-url addition, should be same as this one - let expected_url = "file://dev/null"; - let remote = repo.remote_at(expected_url.to_owned())?; - assert_eq!( - remote.url(Direction::Fetch).unwrap().to_bstring()?, - expected_url, - "there is no rule to rewrite this url" - ); + let remote = repo + .remote_at("https://github.com/foobar/gitoxide".to_owned())? + .push_url("file://dev/null".to_owned())?; + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, rewritten_fetch_url); assert_eq!( remote.url(Direction::Push).unwrap().to_bstring()?, - expected_url, - "there is no rule to rewrite this url" + "ssh://dev/null", + "push-url rewrite rules are applied" ); Ok(()) } From 80e4ab782ebb23bb553b6e47209753a2bd8d05a1 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 11:06:58 +0800 Subject: [PATCH 039/125] thanks clippy --- git-repository/src/remote/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-repository/src/remote/create.rs b/git-repository/src/remote/create.rs index c5751a84b94..80bdd863063 100644 --- a/git-repository/src/remote/create.rs +++ b/git-repository/src/remote/create.rs @@ -18,7 +18,7 @@ impl<'repo> Remote<'repo> { ); let (url_alias, push_url_alias) = rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())?; Ok(Remote { - name: name.to_owned().into(), + name, url, url_alias, push_url, From 897c8c19ca8566834fcb9c9bf5c372451c473760 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 15:02:07 +0800 Subject: [PATCH 040/125] Add escape-hatch to eliminate rewrite rule failures on instantiation (#450) Git is able to just ignore errors, so we should do so to, but make it harder to do so, as in the non-standard case. --- git-repository/src/remote/access.rs | 55 ++++++++++++++++------- git-repository/src/remote/create.rs | 17 ++++--- git-repository/src/repository/remote.rs | 37 +++++++++++++-- git-repository/src/types.rs | 2 - git-repository/tests/repository/remote.rs | 33 +++++++++++++- 5 files changed, 117 insertions(+), 27 deletions(-) diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index cf29d393b54..ebdb5a89048 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -4,15 +4,26 @@ use std::convert::TryInto; /// Builder methods impl Remote<'_> { - /// By default, `url..insteadOf|pushInsteadOf` configuration variables will be applied to rewrite fetch and push - /// urls unless `toggle` is `false`. - pub fn apply_url_aliases(mut self, toggle: bool) -> Self { - self.apply_url_aliases = toggle; - self + /// Set the `url` to be used when pushing data to a remote. + pub fn push_url(self, url: Url) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + self.push_url_inner(url, true) } - /// Set the `push_url` to be used when pushing data to a remote. - pub fn push_url(mut self, push_url: Url) -> Result + /// Set the `url` to be used when pushing data to a remote, without applying rewrite rules in case these could be faulty, + /// eliminating one failure mode. + pub fn push_url_without_url_rewrite(self, url: Url) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + self.push_url_inner(url, false) + } + + fn push_url_inner(mut self, push_url: Url, should_rewrite_urls: bool) -> Result where Url: TryInto, git_url::parse::Error: From, @@ -22,13 +33,30 @@ impl Remote<'_> { .map_err(|err| remote::init::Error::Url(err.into()))?; self.push_url = push_url.into(); - let (_, push_url_alias) = remote::create::rewrite_urls(&self.repo.config, None, self.push_url.as_ref())?; + let (_, push_url_alias) = should_rewrite_urls + .then(|| remote::create::rewrite_urls(&self.repo.config, None, self.push_url.as_ref())) + .unwrap_or(Ok((None, None)))?; self.push_url_alias = push_url_alias; Ok(self) } } +/// Modification +impl Remote<'_> { + /// Read `url..insteadOf|pushInsteadOf` configuration variables and apply them to our urls, changing them in place. + /// + /// This happens only once, and none of them is changed even if only one of them has an error. + pub fn apply_rewrite_rules(&mut self) -> Result<&mut Self, remote::init::Error> { + let (url, push_url) = + remote::create::rewrite_urls(&self.repo.config, self.url.as_ref(), self.push_url.as_ref())?; + self.url_alias = url; + self.push_url_alias = push_url; + Ok(self) + } +} + +/// Accesss impl Remote<'_> { /// Return the name of this remote or `None` if it wasn't persisted to disk yet. pub fn name(&self) -> Option<&str> { @@ -51,15 +79,10 @@ impl Remote<'_> { /// the push-url isn't used for that. pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { match direction { - remote::Direction::Fetch => self - .apply_url_aliases - .then(|| self.url_alias.as_ref()) - .flatten() - .or(self.url.as_ref()), + remote::Direction::Fetch => self.url_alias.as_ref().or(self.url.as_ref()), remote::Direction::Push => self - .apply_url_aliases - .then(|| self.push_url_alias.as_ref()) - .flatten() + .push_url_alias + .as_ref() .or(self.push_url.as_ref()) .or_else(|| self.url(remote::Direction::Fetch)), } diff --git a/git-repository/src/remote/create.rs b/git-repository/src/remote/create.rs index 80bdd863063..f11f5433613 100644 --- a/git-repository/src/remote/create.rs +++ b/git-repository/src/remote/create.rs @@ -10,13 +10,16 @@ impl<'repo> Remote<'repo> { push_url: Option, fetch_specs: Vec, push_specs: Vec, + should_rewrite_urls: bool, repo: &'repo Repository, ) -> Result { debug_assert!( url.is_some() || push_url.is_some(), "BUG: fetch or push url must be set at least" ); - let (url_alias, push_url_alias) = rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())?; + let (url_alias, push_url_alias) = should_rewrite_urls + .then(|| rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())) + .unwrap_or(Ok((None, None)))?; Ok(Remote { name, url, @@ -25,18 +28,23 @@ impl<'repo> Remote<'repo> { push_url_alias, fetch_specs, push_specs, - apply_url_aliases: true, repo, }) } - pub(crate) fn from_fetch_url(url: Url, repo: &'repo Repository) -> Result + pub(crate) fn from_fetch_url( + url: Url, + should_rewrite_urls: bool, + repo: &'repo Repository, + ) -> Result where Url: TryInto, git_url::parse::Error: From, { let url = url.try_into().map_err(|err| remote::init::Error::Url(err.into()))?; - let (url_alias, _) = rewrite_urls(&repo.config, Some(&url), None)?; + let (url_alias, _) = should_rewrite_urls + .then(|| rewrite_urls(&repo.config, Some(&url), None)) + .unwrap_or(Ok((None, None)))?; Ok(Remote { name: None, url: Some(url), @@ -45,7 +53,6 @@ impl<'repo> Remote<'repo> { push_url_alias: None, fetch_specs: Vec::new(), push_specs: Vec::new(), - apply_url_aliases: true, repo, }) } diff --git a/git-repository/src/repository/remote.rs b/git-repository/src/repository/remote.rs index 9297575ef8d..ef6af86f9b2 100644 --- a/git-repository/src/repository/remote.rs +++ b/git-repository/src/repository/remote.rs @@ -9,8 +9,20 @@ impl crate::Repository { Url: TryInto, git_url::parse::Error: From, { - Remote::from_fetch_url(url, self) + Remote::from_fetch_url(url, true, self) } + + /// Create a new remote available at the given `url`, 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, + git_url::parse::Error: From, + { + Remote::from_fetch_url(url, false, self) + } + /// Find the remote with the given `name` or report an error, similar to [`try_find_remote(…)`][Self::try_find_remote()]. /// /// Note that we will include remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()]. @@ -31,6 +43,17 @@ impl crate::Repository { /// /// We will only include information if we deem it [trustworthy][crate::open::Options::filter_config_section()]. pub fn try_find_remote(&self, name: &str) -> Option, find::Error>> { + self.try_find_remote_inner(name, 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(&self, name: &str) -> Option, find::Error>> { + self.try_find_remote_inner(name, false) + } + + fn try_find_remote_inner(&self, name: &str, rewrite_urls: bool) -> Option, find::Error>> { let mut filter = self.filter_config_section(); let mut config_url = |field: &str, kind: &'static str| { self.config @@ -104,8 +127,16 @@ impl crate::Repository { }; Some( - Remote::from_preparsed_config(name.to_owned().into(), url, push_url, fetch_specs, push_specs, self) - .map_err(Into::into), + Remote::from_preparsed_config( + name.to_owned().into(), + url, + push_url, + fetch_specs, + push_specs, + rewrite_urls, + self, + ) + .map_err(Into::into), ) } } diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index 7362b0c8dc3..e70abb0f6e2 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -184,8 +184,6 @@ pub struct Remote<'repo> { pub(crate) fetch_specs: Vec, /// Refspecs for use when pushing. pub(crate) push_specs: Vec, - /// If false, default true, we will apply url rewrites. - pub(crate) apply_url_aliases: bool, // /// Delete local tracking branches that don't exist on the remote anymore. // pub(crate) prune: bool, // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index a6eb61bb5d4..5b15e7102bc 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -51,6 +51,36 @@ mod remote_at { ); Ok(()) } + + #[test] + fn url_rewrites_can_be_skipped() -> crate::Result { + let repo = remote::repo("url-rewriting"); + let remote = repo.remote_at_without_url_rewrite("https://github.com/foobar/gitoxide")?; + + assert_eq!(remote.name(), None, "anonymous remotes are unnamed"); + let fetch_url = "https://github.com/foobar/gitoxide"; + assert_eq!( + remote.url(Direction::Fetch).unwrap().to_bstring()?, + fetch_url, + "fetch was rewritten" + ); + assert_eq!( + remote.url(Direction::Push).unwrap().to_bstring()?, + fetch_url, + "push is the same as fetch was rewritten" + ); + + let remote = repo + .remote_at_without_url_rewrite("https://github.com/foobar/gitoxide".to_owned())? + .push_url_without_url_rewrite("file://dev/null".to_owned())?; + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, fetch_url); + assert_eq!( + remote.url(Direction::Push).unwrap().to_bstring()?, + "file://dev/null", + "push-url rewrite rules are not applied" + ); + Ok(()) + } } mod find_remote { @@ -142,12 +172,13 @@ mod find_remote { ); } - let remote = remote.apply_url_aliases(false); + let remote = repo.try_find_remote_without_url_rewrite("origin").expect("exists")?; assert_eq!( remote.url(Direction::Fetch).unwrap().to_bstring()?, "https://github.com/foobar/gitoxide" ); assert_eq!(remote.url(Direction::Push).unwrap().to_bstring()?, "file://dev/null"); + // TODO: apply after the fact. Ok(()) } From 6d8d9b87db3b41a45343c14ad1b50f742d084f11 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 15:26:38 +0800 Subject: [PATCH 041/125] prepare for better error handling around ssh urls (#450) --- git-url/tests/parse/invalid.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/git-url/tests/parse/invalid.rs b/git-url/tests/parse/invalid.rs index 62bcbb4c3a3..62acf2ac8c1 100644 --- a/git-url/tests/parse/invalid.rs +++ b/git-url/tests/parse/invalid.rs @@ -14,3 +14,11 @@ fn missing_path() { fn missing_port_despite_indication() { assert_failure("ssh://host.xz:", "Paths cannot be empty") } + +#[test] +#[ignore] +fn missing_host_in_ssh_url() { + assert_failure("git@hello/world", "foo"); + assert_failure("ssh://git@path/to/dir", "foo"); + assert_failure("ssh://path/to/dir", "foo"); +} From cfd7c0a29f10010841b310e0eb8b000083381a58 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 15:34:48 +0800 Subject: [PATCH 042/125] switch to `thiserror` (#450) --- Cargo.lock | 10 +++---- git-url/Cargo.toml | 2 +- git-url/src/expand_path.rs | 25 ++++++++--------- git-url/src/parse.rs | 50 +++++++++++++--------------------- git-url/tests/parse/invalid.rs | 2 +- 5 files changed, 37 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8249750f08e..9325b26b0a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1675,8 +1675,8 @@ dependencies = [ "git-features", "git-path", "home", - "quick-error", "serde", + "thiserror", "url", ] @@ -2898,18 +2898,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" dependencies = [ "proc-macro2", "quote", diff --git a/git-url/Cargo.toml b/git-url/Cargo.toml index fc5cd573c68..e0de30dde8f 100644 --- a/git-url/Cargo.toml +++ b/git-url/Cargo.toml @@ -21,7 +21,7 @@ serde1 = ["serde", "bstr/serde1"] serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} git-features = { version = "^0.22.0", path = "../git-features" } git-path = { version = "^0.4.0", path = "../git-path" } -quick-error = "2.0.0" +thiserror = "1.0.32" url = "2.1.1" bstr = { version = "0.2.13", default-features = false, features = ["std"] } home = "0.5.3" diff --git a/git-url/src/expand_path.rs b/git-url/src/expand_path.rs index b654c3333dd..b2e2f952946 100644 --- a/git-url/src/expand_path.rs +++ b/git-url/src/expand_path.rs @@ -2,7 +2,6 @@ use std::path::{Path, PathBuf}; use bstr::{BStr, BString, ByteSlice}; -use quick_error::quick_error; /// Whether a repository is resolving for the current user, or the given one. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] @@ -23,18 +22,14 @@ impl From for Option { } } -quick_error! { - /// The error used by [`parse()`], [`with()`] and [`expand_path()`]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - IllformedUtf8{path: BString} { - display("UTF8 conversion on non-unix system failed for path: {}", path) - } - MissingHome(user: Option) { - display("Home directory could not be obtained for {}", match user {Some(user) => format!("user '{}'", user), None => "current user".into()}) - } - } +/// The error used by [`parse()`], [`with()`] and [`expand_path()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("UTF8 conversion on non-unix system failed for path: {path:?}")] + IllformedUtf8 { path: BString }, + #[error("Home directory could not be obtained for {}", match user {Some(user) => format!("user '{}'", user), None => "current user".into()})] + MissingHome { user: Option }, } fn path_segments(path: &BStr) -> Option> { @@ -110,7 +105,9 @@ pub fn with( let path = git_path::try_from_byte_slice(path).map_err(|_| Error::IllformedUtf8 { path: path.to_owned() })?; Ok(match user { Some(user) => home_for_user(user) - .ok_or_else(|| Error::MissingHome(user.to_owned().into()))? + .ok_or_else(|| Error::MissingHome { + user: user.to_owned().into(), + })? .join(make_relative(path)), None => path.into(), }) diff --git a/git-url/src/parse.rs b/git-url/src/parse.rs index d357f07e254..8e023f9f241 100644 --- a/git-url/src/parse.rs +++ b/git-url/src/parse.rs @@ -1,33 +1,23 @@ use std::borrow::Cow; use bstr::ByteSlice; -use quick_error::quick_error; use crate::Scheme; -quick_error! { - /// The Error returned by [`parse()`] - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Utf8(err: std::str::Utf8Error) { - display("Could not decode URL as UTF8") - from() - source(err) - } - Url(err: String) { - display("the URL could not be parsed: {}", err) - } - UnsupportedProtocol(protocol: String) { - display("Protocol '{}' is not supported", protocol) - } - EmptyPath { - display("Paths cannot be empty") - } - RelativeUrl(url: String) { - display("Relative URLs are not permitted: '{}'", url) - } - } +/// The Error returned by [`parse()`] +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Could not decode URL as UTF8")] + Utf8(#[from] std::str::Utf8Error), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error("Protocol {protocol:?} is not supported")] + UnsupportedProtocol { protocol: String }, + #[error("Paths cannot be empty")] + EmptyPath, + #[error("Relative URLs are not permitted: {url:?}")] + RelativeUrl { url: String }, } fn str_to_protocol(s: &str) -> Result { @@ -38,7 +28,7 @@ fn str_to_protocol(s: &str) -> Result { "http" => Scheme::Http, "https" => Scheme::Https, "rad" => Scheme::Radicle, - _ => return Err(Error::UnsupportedProtocol(s.into())), + _ => return Err(Error::UnsupportedProtocol { protocol: s.into() }), }) } @@ -114,22 +104,20 @@ pub fn parse(bytes: &[u8]) -> Result { "{}://{}", guessed_protocol, sanitize_for_protocol(guessed_protocol, url_str) - )) - .map_err(|err| Error::Url(err.to_string()))? + ))? } - Err(err) => return Err(Error::Url(err.to_string())), + Err(err) => return Err(err.into()), }; // SCP like URLs without user parse as 'something' with the scheme being the 'host'. Hosts always have dots. if url.scheme().find('.').is_some() { // try again with prefixed protocol - url = url::Url::parse(&format!("ssh://{}", sanitize_for_protocol("ssh", url_str))) - .map_err(|err| Error::Url(err.to_string()))?; + url = url::Url::parse(&format!("ssh://{}", sanitize_for_protocol("ssh", url_str)))?; } if url.scheme() != "rad" && url.path().is_empty() { return Err(Error::EmptyPath); } if url.cannot_be_a_base() { - return Err(Error::RelativeUrl(url.into())); + return Err(Error::RelativeUrl { url: url.into() }); } to_owned_url(url) diff --git a/git-url/tests/parse/invalid.rs b/git-url/tests/parse/invalid.rs index 62acf2ac8c1..47c894a6a73 100644 --- a/git-url/tests/parse/invalid.rs +++ b/git-url/tests/parse/invalid.rs @@ -2,7 +2,7 @@ use crate::parse::assert_failure; #[test] fn unknown_protocol() { - assert_failure("foo://host.xz/path/to/repo.git/", "Protocol 'foo' is not supported") + assert_failure("foo://host.xz/path/to/repo.git/", "Protocol \"foo\" is not supported") } #[test] From 224c605d11a823bdaad6eb2bae1149bc671fb92d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 15:38:29 +0800 Subject: [PATCH 043/125] remove invalid test as it looks like it parses hosts from paths and that is fine (#450) Git thinks a bit differently, but it's hard to fix without parsing ourselves. --- git-url/tests/parse/invalid.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/git-url/tests/parse/invalid.rs b/git-url/tests/parse/invalid.rs index 47c894a6a73..ef9fb4b375a 100644 --- a/git-url/tests/parse/invalid.rs +++ b/git-url/tests/parse/invalid.rs @@ -14,11 +14,3 @@ fn missing_path() { fn missing_port_despite_indication() { assert_failure("ssh://host.xz:", "Paths cannot be empty") } - -#[test] -#[ignore] -fn missing_host_in_ssh_url() { - assert_failure("git@hello/world", "foo"); - assert_failure("ssh://git@path/to/dir", "foo"); - assert_failure("ssh://path/to/dir", "foo"); -} From 2bcfdee6a3af758a0b70e2af9c4b6f8cc09d8da0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:00:05 +0800 Subject: [PATCH 044/125] fix!: Prohibit invalid state by making parts the url's data private (#450) This fix is meant to improve serialization support which can now happen `to_bstring()` without possibility for error. Empty paths can still be set which won't be valid for all URLs. --- git-url/src/lib.rs | 60 ++++++++++++++++++++++++++++++++----- git-url/tests/parse/file.rs | 16 +++++----- git-url/tests/parse/mod.rs | 17 ++++++----- git-url/tests/parse/ssh.rs | 8 ++--- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index 0d03578f8dc..f9862235bc6 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -12,7 +12,7 @@ use std::{ fmt::{self}, }; -use bstr::BStr; +use bstr::{BStr, BString}; /// pub mod parse; @@ -69,9 +69,9 @@ pub struct Url { /// The URL scheme. pub scheme: Scheme, /// The user to impersonate on the remote. - pub user: Option, + user: Option, /// The host to which to connect. Localhost is implied if `None`. - pub host: Option, + host: Option, /// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used. pub port: Option, /// The path portion of the URL, usually the location of the git repository. @@ -90,6 +90,52 @@ impl Default for Url { } } +/// Instantiation +impl Url { + /// Create a new instance from the given parts, which will be validated by parsing them back. + pub fn from_parts( + scheme: Scheme, + user: Option, + host: Option, + port: Option, + path: BString, + ) -> Result { + parse( + Url { + scheme, + user, + host, + port, + path, + } + .to_bstring() + .as_ref(), + ) + } +} + +/// Modification +impl Url { + /// Set the given `user`, with `None` unsetting it. Returns the previous value. + pub fn set_user(&mut self, user: Option) -> Option { + let prev = self.user.take(); + self.user = user; + prev + } +} + +/// Access +impl Url { + /// Returns the user mentioned in the url, if present. + pub fn user(&self) -> Option<&str> { + self.user.as_deref() + } + /// Returns the host mentioned in the url, if present. + pub fn host(&self) -> Option<&str> { + self.host.as_deref() + } +} + /// Serialization impl Url { /// Write this URL losslessly to `out`, ready to be parsed again. @@ -106,7 +152,7 @@ impl Url { out.write_all(host.as_bytes())?; } (None, None) => {} - _ => return Err(std::io::Error::new(std::io::ErrorKind::Other, "Malformed URL")), + (Some(_user), None) => unreachable!("BUG: should not be possible to have a user but no host"), }; if let Some(port) = &self.port { write!(&mut out, ":{}", port)?; @@ -116,7 +162,7 @@ impl Url { } /// Transform ourselves into a binary string, losslessly, or fail if the URL is malformed due to host or user parts being incorrect. - pub fn to_bstring(&self) -> std::io::Result { + pub fn to_bstring(&self) -> bstr::BString { let mut buf = Vec::with_capacity( (5 + 3) + self.user.as_ref().map(|n| n.len()).unwrap_or_default() @@ -125,8 +171,8 @@ impl Url { + self.port.map(|_| 5).unwrap_or_default() + self.path.len(), ); - self.write_to(&mut buf)?; - Ok(buf.into()) + self.write_to(&mut buf).expect("io cannot fail in memory"); + buf.into() } } diff --git a/git-url/tests/parse/file.rs b/git-url/tests/parse/file.rs index bce754fe3fd..cc65495f076 100644 --- a/git-url/tests/parse/file.rs +++ b/git-url/tests/parse/file.rs @@ -12,14 +12,14 @@ fn file_path_with_protocol() -> crate::Result { #[test] fn file_path_without_protocol() -> crate::Result { - let url = assert_url_and("/path/to/git", url(Scheme::File, None, None, None, b"/path/to/git"))?.to_bstring()?; + let url = assert_url_and("/path/to/git", url(Scheme::File, None, None, None, b"/path/to/git"))?.to_bstring(); assert_eq!(url, "file:///path/to/git"); Ok(()) } #[test] fn no_username_expansion_for_file_paths_without_protocol() -> crate::Result { - let url = assert_url_and("~/path/to/git", url(Scheme::File, None, None, None, b"~/path/to/git"))?.to_bstring()?; + let url = assert_url_and("~/path/to/git", url(Scheme::File, None, None, None, b"~/path/to/git"))?.to_bstring(); assert_eq!(url, "file://~/path/to/git"); Ok(()) } @@ -35,7 +35,7 @@ fn no_username_expansion_for_file_paths_with_protocol() -> crate::Result { fn non_utf8_file_path_without_protocol() -> crate::Result { let parsed = git_url::parse(b"/path/to\xff/git")?; assert_eq!(parsed, url(Scheme::File, None, None, None, b"/path/to\xff/git",)); - let url_lossless = parsed.to_bstring()?; + let url_lossless = parsed.to_bstring(); assert_eq!( url_lossless.to_string(), "file:///path/to�/git", @@ -51,9 +51,9 @@ fn relative_file_path_without_protocol() -> crate::Result { "../../path/to/git", url(Scheme::File, None, None, None, b"../../path/to/git"), )? - .to_bstring()?; + .to_bstring(); assert_eq!(parsed, "file://../../path/to/git"); - let url = assert_url_and("path/to/git", url(Scheme::File, None, None, None, b"path/to/git"))?.to_bstring()?; + let url = assert_url_and("path/to/git", url(Scheme::File, None, None, None, b"path/to/git"))?.to_bstring(); assert_eq!(url, "file://path/to/git"); Ok(()) } @@ -64,7 +64,7 @@ fn interior_relative_file_path_without_protocol() -> crate::Result { "/abs/path/../../path/to/git", url(Scheme::File, None, None, None, b"/abs/path/../../path/to/git"), )? - .to_bstring()?; + .to_bstring(); assert_eq!(url, "file:///abs/path/../../path/to/git"); Ok(()) } @@ -77,7 +77,7 @@ mod windows { #[test] fn file_path_without_protocol() -> crate::Result { let url = - assert_url_and("x:/path/to/git", url(Scheme::File, None, None, None, b"x:/path/to/git"))?.to_bstring()?; + assert_url_and("x:/path/to/git", url(Scheme::File, None, None, None, b"x:/path/to/git"))?.to_bstring(); assert_eq!(url, "file://x:/path/to/git"); Ok(()) } @@ -88,7 +88,7 @@ mod windows { "x:\\path\\to\\git", url(Scheme::File, None, None, None, b"x:\\path\\to\\git"), )? - .to_bstring()?; + .to_bstring(); assert_eq!(url, "file://x:\\path\\to\\git"); Ok(()) } diff --git a/git-url/tests/parse/mod.rs b/git-url/tests/parse/mod.rs index b27daa33473..0077a44ad07 100644 --- a/git-url/tests/parse/mod.rs +++ b/git-url/tests/parse/mod.rs @@ -6,7 +6,7 @@ fn assert_url_and(url: &str, expected: git_url::Url) -> Result crate::Result { - assert_eq!(assert_url_and(url, expected)?.to_bstring().expect("valid"), url); + assert_eq!(assert_url_and(url, expected)?.to_bstring(), url); Ok(()) } @@ -21,13 +21,14 @@ fn url( port: impl Into>, path: &'static [u8], ) -> git_url::Url { - git_url::Url { - scheme: protocol, - user: user.into().map(Into::into), - host: host.into().map(Into::into), - port: port.into(), - path: path.into(), - } + git_url::Url::from_parts( + protocol, + user.into().map(Into::into), + host.into().map(Into::into), + port.into(), + path.into(), + ) + .expect("valid") } mod file; diff --git a/git-url/tests/parse/ssh.rs b/git-url/tests/parse/ssh.rs index 21ad4f2fa87..349069f398a 100644 --- a/git-url/tests/parse/ssh.rs +++ b/git-url/tests/parse/ssh.rs @@ -53,7 +53,7 @@ fn scp_like_without_user() -> crate::Result { "host.xz:path/to/git", url(Scheme::Ssh, None, "host.xz", None, b"/path/to/git"), )? - .to_bstring()?; + .to_bstring(); assert_eq!(url, "ssh://host.xz/path/to/git"); Ok(()) } @@ -64,7 +64,7 @@ fn scp_like_without_user_and_username_expansion_without_username() -> crate::Res "host.xz:~/to/git", url(Scheme::Ssh, None, "host.xz", None, b"/~/to/git"), )? - .to_bstring()?; + .to_bstring(); assert_eq!(url, "ssh://host.xz/~/to/git"); Ok(()) } @@ -75,7 +75,7 @@ fn scp_like_without_user_and_username_expansion_with_username() -> crate::Result "host.xz:~byron/to/git", url(Scheme::Ssh, None, "host.xz", None, b"/~byron/to/git"), )? - .to_bstring()?; + .to_bstring(); assert_eq!(url, "ssh://host.xz/~byron/to/git"); Ok(()) } @@ -86,7 +86,7 @@ fn scp_like_with_user_and_relative_path_turns_into_absolute_path() -> crate::Res "user@host.xz:./relative", url(Scheme::Ssh, "user", "host.xz", None, b"/relative"), )? - .to_bstring()?; + .to_bstring(); assert_eq!(url, "ssh://user@host.xz/relative"); Ok(()) } From f0f5ee602fb46741114affed076716ac12b95138 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:02:18 +0800 Subject: [PATCH 045/125] adapt to changes in `git-url` (#450) --- git-repository/src/remote/url.rs | 2 +- git-repository/tests/repository/remote.rs | 32 +++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/git-repository/src/remote/url.rs b/git-repository/src/remote/url.rs index cb34f447a3c..e5d60199f9e 100644 --- a/git-repository/src/remote/url.rs +++ b/git-repository/src/remote/url.rs @@ -66,7 +66,7 @@ impl Rewrite { if self.replacements_for(direction).is_empty() { None } else { - let mut url = url.to_bstring().ok()?; + let mut url = url.to_bstring(); self.rewrite_url_in_place(&mut url, direction).then(|| url) } } diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 5b15e7102bc..70fc212513e 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -9,15 +9,15 @@ mod remote_at { let remote = repo.remote_at(fetch_url)?; assert_eq!(remote.name(), None); - assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, fetch_url); - assert_eq!(remote.url(Direction::Push).unwrap().to_bstring()?, fetch_url); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring(), fetch_url); + assert_eq!(remote.url(Direction::Push).unwrap().to_bstring(), fetch_url); let remote = remote.push_url("user@host.xz:./relative")?; assert_eq!( - remote.url(Direction::Push).unwrap().to_bstring()?, + remote.url(Direction::Push).unwrap().to_bstring(), "ssh://user@host.xz/relative" ); - assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, fetch_url); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring(), fetch_url); Ok(()) } @@ -30,12 +30,12 @@ mod remote_at { assert_eq!(remote.name(), None, "anonymous remotes are unnamed"); let rewritten_fetch_url = "https://github.com/byron/gitoxide"; assert_eq!( - remote.url(Direction::Fetch).unwrap().to_bstring()?, + remote.url(Direction::Fetch).unwrap().to_bstring(), rewritten_fetch_url, "fetch was rewritten" ); assert_eq!( - remote.url(Direction::Push).unwrap().to_bstring()?, + remote.url(Direction::Push).unwrap().to_bstring(), rewritten_fetch_url, "push is the same as fetch was rewritten" ); @@ -43,9 +43,9 @@ mod remote_at { let remote = repo .remote_at("https://github.com/foobar/gitoxide".to_owned())? .push_url("file://dev/null".to_owned())?; - assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, rewritten_fetch_url); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring(), rewritten_fetch_url); assert_eq!( - remote.url(Direction::Push).unwrap().to_bstring()?, + remote.url(Direction::Push).unwrap().to_bstring(), "ssh://dev/null", "push-url rewrite rules are applied" ); @@ -60,12 +60,12 @@ mod remote_at { assert_eq!(remote.name(), None, "anonymous remotes are unnamed"); let fetch_url = "https://github.com/foobar/gitoxide"; assert_eq!( - remote.url(Direction::Fetch).unwrap().to_bstring()?, + remote.url(Direction::Fetch).unwrap().to_bstring(), fetch_url, "fetch was rewritten" ); assert_eq!( - remote.url(Direction::Push).unwrap().to_bstring()?, + remote.url(Direction::Push).unwrap().to_bstring(), fetch_url, "push is the same as fetch was rewritten" ); @@ -73,9 +73,9 @@ mod remote_at { let remote = repo .remote_at_without_url_rewrite("https://github.com/foobar/gitoxide".to_owned())? .push_url_without_url_rewrite("file://dev/null".to_owned())?; - assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, fetch_url); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring(), fetch_url); assert_eq!( - remote.url(Direction::Push).unwrap().to_bstring()?, + remote.url(Direction::Push).unwrap().to_bstring(), "file://dev/null", "push-url rewrite rules are not applied" ); @@ -159,9 +159,9 @@ mod find_remote { let expected_push_url: BString = baseline.next().expect("push").into(); let remote = repo.find_remote("origin")?; - assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring()?, expected_fetch_url,); + assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring(), expected_fetch_url,); { - let actual_push_url = remote.url(Direction::Push).unwrap().to_bstring()?; + let actual_push_url = remote.url(Direction::Push).unwrap().to_bstring(); assert_ne!( actual_push_url, expected_push_url, "here we actually resolve something that git doesn't for unknown reason" @@ -174,10 +174,10 @@ mod find_remote { let remote = repo.try_find_remote_without_url_rewrite("origin").expect("exists")?; assert_eq!( - remote.url(Direction::Fetch).unwrap().to_bstring()?, + remote.url(Direction::Fetch).unwrap().to_bstring(), "https://github.com/foobar/gitoxide" ); - assert_eq!(remote.url(Direction::Push).unwrap().to_bstring()?, "file://dev/null"); + assert_eq!(remote.url(Direction::Push).unwrap().to_bstring(), "file://dev/null"); // TODO: apply after the fact. Ok(()) } From 9456146531226e5efc6e1e4e2e89b03683f8a422 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:09:11 +0800 Subject: [PATCH 046/125] adapt to changes in `git-url` (#450) --- .../src/client/blocking_io/connect.rs | 30 ++++++++++--------- git-transport/src/client/blocking_io/file.rs | 10 ++----- git-transport/src/client/blocking_io/ssh.rs | 13 ++++---- git-transport/src/client/git/async_io.rs | 14 +++------ git-transport/src/client/git/blocking_io.rs | 13 +++----- 5 files changed, 33 insertions(+), 47 deletions(-) diff --git a/git-transport/src/client/blocking_io/connect.rs b/git-transport/src/client/blocking_io/connect.rs index 1b8cadea91b..3f60b44e9e0 100644 --- a/git-transport/src/client/blocking_io/connect.rs +++ b/git-transport/src/client/blocking_io/connect.rs @@ -12,11 +12,11 @@ use crate::client::Transport; /// Use `desired_version` to set the desired protocol version to use when connecting, but not that the server may downgrade it. pub fn connect(url: &[u8], desired_version: crate::Protocol) -> Result, Error> { let urlb = url; - let url = git_url::parse(urlb)?; + let mut url = git_url::parse(urlb)?; Ok(match url.scheme { git_url::Scheme::Radicle => return Err(Error::UnsupportedScheme(url.scheme)), git_url::Scheme::File => { - if url.user.is_some() || url.host.is_some() || url.port.is_some() { + if url.user().is_some() || url.host().is_some() || url.port.is_some() { return Err(Error::UnsupportedUrlTokens(urlb.into(), url.scheme)); } Box::new( @@ -24,29 +24,31 @@ pub fn connect(url: &[u8], desired_version: crate::Protocol) -> Result)?, ) } - git_url::Scheme::Ssh => Box::new( + git_url::Scheme::Ssh => Box::new({ + let path = std::mem::take(&mut url.path); crate::client::blocking_io::ssh::connect( - url.host.as_ref().expect("host is present in url"), - url.path, + url.host().expect("host is present in url"), + path, desired_version, - url.user.as_deref(), + url.user(), url.port, ) - .map_err(|e| Box::new(e) as Box)?, - ), + .map_err(|e| Box::new(e) as Box)? + }), git_url::Scheme::Git => { - if url.user.is_some() { + if url.user().is_some() { return Err(Error::UnsupportedUrlTokens(urlb.into(), url.scheme)); } - Box::new( + Box::new({ + let path = std::mem::take(&mut url.path); crate::client::git::connect( - url.host.as_ref().expect("host is present in url"), - url.path, + url.host().expect("host is present in url"), + path, desired_version, url.port, ) - .map_err(|e| Box::new(e) as Box)?, - ) + .map_err(|e| Box::new(e) as Box)? + }) } #[cfg(not(feature = "http-client-curl"))] git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), diff --git a/git-transport/src/client/blocking_io/file.rs b/git-transport/src/client/blocking_io/file.rs index a5f5e579c51..4e2f0062439 100644 --- a/git-transport/src/client/blocking_io/file.rs +++ b/git-transport/src/client/blocking_io/file.rs @@ -70,13 +70,7 @@ impl SpawnProcessOnDemand { } fn new_local(path: BString, version: Protocol) -> SpawnProcessOnDemand { SpawnProcessOnDemand { - url: git_url::Url { - scheme: git_url::Scheme::File, - user: None, - host: None, - port: None, - path: path.clone(), - }, + url: git_url::Url::from_parts(git_url::Scheme::File, None, None, None, path.clone()).expect("valid url"), path, ssh_program: None, ssh_args: Vec::new(), @@ -101,7 +95,7 @@ impl client::TransportWithoutIO for SpawnProcessOnDemand { } fn to_url(&self) -> BString { - self.url.to_bstring().expect("valid as it cannot be altered") + self.url.to_bstring() } fn connection_persists_across_multiple_requests(&self) -> bool { diff --git a/git-transport/src/client/blocking_io/ssh.rs b/git-transport/src/client/blocking_io/ssh.rs index cfb414bf08d..3e3ed4e6e67 100644 --- a/git-transport/src/client/blocking_io/ssh.rs +++ b/git-transport/src/client/blocking_io/ssh.rs @@ -64,13 +64,14 @@ pub fn connect( }; let path = git_url::expand_path::for_shell(path); - let url = git_url::Url { - scheme: git_url::Scheme::Ssh, - user: user.map(Into::into), - host: Some(host.clone()), + let url = git_url::Url::from_parts( + git_url::Scheme::Ssh, + user.map(Into::into), + Some(host.clone()), port, - path: path.clone(), - }; + path.clone(), + ) + .expect("valid url"); Ok(match args_and_env { Some((args, envs)) => blocking_io::file::SpawnProcessOnDemand::new_ssh( url, diff --git a/git-transport/src/client/git/async_io.rs b/git-transport/src/client/git/async_io.rs index 5c6fff937b0..ce05158bad3 100644 --- a/git-transport/src/client/git/async_io.rs +++ b/git-transport/src/client/git/async_io.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use bstr::BString; +use bstr::{BString, ByteVec}; use futures_io::{AsyncRead, AsyncWrite}; use futures_lite::AsyncWriteExt; use git_packetline::PacketLineRef; @@ -29,15 +29,9 @@ where fn to_url(&self) -> BString { self.custom_url.as_ref().map_or_else( || { - git_url::Url { - scheme: git_url::Scheme::File, - user: None, - host: None, - port: None, - path: self.path.clone(), - } - .to_bstring() - .expect("valid and not mutated") + let mut buf: BString = "file://".into(); + buf.push_str(&self.path); + buf }, |url| url.clone(), ) diff --git a/git-transport/src/client/git/blocking_io.rs b/git-transport/src/client/git/blocking_io.rs index f65f9e12c8d..5d09c4c8996 100644 --- a/git-transport/src/client/git/blocking_io.rs +++ b/git-transport/src/client/git/blocking_io.rs @@ -29,15 +29,10 @@ where fn to_url(&self) -> BString { self.custom_url.as_ref().map_or_else( || { - git_url::Url { - scheme: git_url::Scheme::File, - user: None, - host: None, - port: None, - path: self.path.clone(), - } - .to_bstring() - .expect("valid URL which isn't mutated") + use bstr::ByteVec; + let mut buf: BString = "file://".into(); + buf.push_str(&self.path); + buf }, |url| url.clone(), ) From 60bfd6d457d75fb4b342e08f329dadc8373de266 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:10:01 +0800 Subject: [PATCH 047/125] adapt to changes in `git-url` (#450) --- cargo-smart-release/src/changelog/write.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cargo-smart-release/src/changelog/write.rs b/cargo-smart-release/src/changelog/write.rs index 331b10d759b..be3a014c045 100644 --- a/cargo-smart-release/src/changelog/write.rs +++ b/cargo-smart-release/src/changelog/write.rs @@ -50,7 +50,7 @@ impl From for RepositoryUrl { impl RepositoryUrl { pub fn is_github(&self) -> bool { - self.inner.host.as_ref().map(|h| h == "github.com").unwrap_or(false) + self.inner.host().map(|h| h == "github.com").unwrap_or(false) } fn cleaned_path(&self) -> String { @@ -59,15 +59,14 @@ impl RepositoryUrl { } pub fn github_https(&self) -> Option { - match &self.inner.host { - Some(host) if host == "github.com" => match self.inner.scheme { + match &self.inner.host() { + Some(host) if *host == "github.com" => match self.inner.scheme { Scheme::Http | Scheme::Https | Scheme::Git => { format!("https://github.com{}", self.cleaned_path()).into() } Scheme::Ssh => self .inner - .user - .as_ref() + .user() .map(|user| format!("https://github.com{}/{}", user, self.cleaned_path())), Scheme::Radicle | Scheme::File => None, }, From 624b1807600344f6969402f5bd193443ec1f1bd6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:10:53 +0800 Subject: [PATCH 048/125] adapt to changes in `git-url` (#450) --- gitoxide-core/src/net.rs | 9 +++++---- gitoxide-core/src/organize.rs | 12 +++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/gitoxide-core/src/net.rs b/gitoxide-core/src/net.rs index 96ed0137396..832fd69ee40 100644 --- a/gitoxide-core/src/net.rs +++ b/gitoxide-core/src/net.rs @@ -87,15 +87,16 @@ mod async_io { desired_version: transport::Protocol, ) -> Result { let urlb = url; - let url = git_repository::url::parse(urlb)?; + let mut url = git_repository::url::parse(urlb)?; Ok(match url.scheme { git_repository::url::Scheme::Git => { - if url.user.is_some() { + if url.user().is_some() { return Err(Error::UnsupportedUrlTokens(urlb.into(), url.scheme)); } + let path = std::mem::take(&mut url.path); git_connect( - url.host.as_ref().expect("host is present in url"), - url.path, + url.host().expect("host is present in url"), + path, desired_version, url.port, ) diff --git a/gitoxide-core/src/organize.rs b/gitoxide-core/src/organize.rs index 0dde4071b03..acd63be0899 100644 --- a/gitoxide-core/src/organize.rs +++ b/gitoxide-core/src/organize.rs @@ -168,18 +168,16 @@ fn handle( progress.info(format!( "Skipping repository at {:?} whose remote does not have a path: {:?}", git_workdir.display(), - url.to_bstring()? + url.to_bstring() )); return Ok(()); } let destination = canonicalized_destination - .join(url.host.as_ref().ok_or_else(|| { - anyhow::Error::msg(format!( - "Remote URLs must have host names: {}", - url.to_bstring().expect("valid URL") - )) - })?) + .join( + url.host() + .ok_or_else(|| anyhow::Error::msg(format!("Remote URLs must have host names: {}", url.to_bstring())))?, + ) .join(to_relative({ let mut path = git_url::expand_path(None, url.path.as_bstr())?; match kind { From dbc6f5da51417842a722b8b3576b6ea21f4dd885 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:15:29 +0800 Subject: [PATCH 049/125] fix docs (#450) --- git-repository/src/remote/access.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index ebdb5a89048..28dddf47ec9 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -47,7 +47,7 @@ impl Remote<'_> { /// Read `url..insteadOf|pushInsteadOf` configuration variables and apply them to our urls, changing them in place. /// /// This happens only once, and none of them is changed even if only one of them has an error. - pub fn apply_rewrite_rules(&mut self) -> Result<&mut Self, remote::init::Error> { + pub fn rewrite_urls(&mut self) -> Result<&mut Self, remote::init::Error> { let (url, push_url) = remote::create::rewrite_urls(&self.repo.config, self.url.as_ref(), self.push_url.as_ref())?; self.url_alias = url; @@ -71,8 +71,8 @@ impl Remote<'_> { } } - /// Return the url used for the given `direction` with rewrites from `url..insteadOf|pushInsteadOf` applied unless - /// [`apply_url_aliases(false)`][Self::apply_url_aliases()] was called before. + /// Return the url used for the given `direction` with rewrites from `url..insteadOf|pushInsteadOf`, unless the instance + /// was created with one of the `_without_url_rewrite()` methods. /// For pushing, this is the `remote..pushUrl` or the `remote..url` used for fetching, and for fetching it's /// the `remote..url`. /// Note that it's possible to only have the push url set, in which case there will be no way to fetch from the remote as From c2afd874aa64e56223af0671964acf995706484d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:33:50 +0800 Subject: [PATCH 050/125] a test for handling bad rewrite urls and its implications (#450) --- .../tests/fixtures/make_remote_repos.sh | 24 +++++++++ git-repository/tests/repository/remote.rs | 53 ++++++++++++++++--- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index e95f315c726..fe8d6cbffa1 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -134,6 +134,7 @@ git clone --shared base branch-push-remote git init --bare url-rewriting ( cd url-rewriting + git remote add origin https://github.com/foobar/gitoxide cat <> config @@ -154,3 +155,26 @@ EOF git remote get-url origin --push } > baseline.git ) + +git init --bare bad-url-rewriting +( + cd bad-url-rewriting + + git remote add origin https://github.com/foobar/gitoxide + cat <> config + +[remote "origin"] + pushUrl = "file://dev/null" + +[url "foo://"] + pushInsteadOf = "file://" + +[url "https://github.com/byron/"] + insteadOf = https://github.com/foobar/ +EOF + + { + git remote get-url origin + git remote get-url origin --push + } > baseline.git +) diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 70fc212513e..f181ab94165 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -164,21 +164,60 @@ mod find_remote { let actual_push_url = remote.url(Direction::Push).unwrap().to_bstring(); assert_ne!( actual_push_url, expected_push_url, - "here we actually resolve something that git doesn't for unknown reason" - ); - assert_eq!( - actual_push_url, "ssh://dev/null", - "file:// gets replaced actually and it's a valid url" + "here we actually resolve something that git doesn't probably because it's missing the host. Our parser is OK with it for some reason." ); + assert_eq!(actual_push_url, "ssh://dev/null", "file:// gets replaced actually"); } - let remote = repo.try_find_remote_without_url_rewrite("origin").expect("exists")?; + let mut remote = repo.try_find_remote_without_url_rewrite("origin").expect("exists")?; assert_eq!( remote.url(Direction::Fetch).unwrap().to_bstring(), "https://github.com/foobar/gitoxide" ); assert_eq!(remote.url(Direction::Push).unwrap().to_bstring(), "file://dev/null"); - // TODO: apply after the fact. + remote.rewrite_urls()?; + assert_eq!(remote.url(Direction::Push).unwrap().to_bstring(), "ssh://dev/null"); + Ok(()) + } + + #[test] + fn bad_url_rewriting_can_be_handled_much_like_git() -> crate::Result { + let repo = remote::repo("bad-url-rewriting"); + + let baseline = std::fs::read(repo.git_dir().join("baseline.git"))?; + let mut baseline = baseline.lines().filter_map(Result::ok); + let expected_fetch_url: BString = baseline.next().expect("fetch").into(); + let expected_push_url: BString = baseline.next().expect("push").into(); + assert_eq!( + expected_push_url, "file://dev/null", + "git leaves the failed one as is without any indication…" + ); + assert_eq!( + expected_fetch_url, "https://github.com/byron/gitoxide", + "…but is able to replace the fetch url successfully" + ); + + let expected_err_msg = "The rewritten push url \"foo://dev/null\" failed to parse"; + assert_eq!( + repo.find_remote("origin").unwrap_err().to_string(), + expected_err_msg, + "this fails by default as rewrites fail" + ); + + let mut remote = repo.try_find_remote_without_url_rewrite("origin").expect("exists")?; + for _round in 0..2 { + assert_eq!( + remote.url(Direction::Fetch).unwrap().to_bstring(), + "https://github.com/foobar/gitoxide", + "no rewrite happened" + ); + assert_eq!(remote.url(Direction::Push).unwrap().to_bstring(), "file://dev/null",); + assert_eq!( + remote.rewrite_urls().unwrap_err().to_string(), + expected_err_msg, + "rewriting fails, leaves both urls as they were, which diverges from git" + ); + } Ok(()) } From e7b451d15751923c002c0e67ed9b8defd27127e0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 16:50:02 +0800 Subject: [PATCH 051/125] Make explicit url rewrites more forgiving similar to how git does it (#450) --- git-repository/src/remote/access.rs | 24 +++++++++++--- git-repository/src/remote/create.rs | 38 +++++++++++++---------- git-repository/src/remote/url.rs | 2 +- git-repository/tests/repository/remote.rs | 22 ++++++++----- 4 files changed, 56 insertions(+), 30 deletions(-) diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index 28dddf47ec9..80bfe7a6356 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -46,12 +46,26 @@ impl Remote<'_> { impl Remote<'_> { /// Read `url..insteadOf|pushInsteadOf` configuration variables and apply them to our urls, changing them in place. /// - /// This happens only once, and none of them is changed even if only one of them has an error. + /// This happens only once, and one if them may be changed even when reporting an error. + /// If both urls fail, only the first error (for fetch urls) is reported. pub fn rewrite_urls(&mut self) -> Result<&mut Self, remote::init::Error> { - let (url, push_url) = - remote::create::rewrite_urls(&self.repo.config, self.url.as_ref(), self.push_url.as_ref())?; - self.url_alias = url; - self.push_url_alias = push_url; + let url_err = match remote::create::rewrite_url(&self.repo.config, self.url.as_ref(), remote::Direction::Fetch) + { + Ok(url) => { + self.url_alias = url; + None + } + Err(err) => err.into(), + }; + let push_url_err = + match remote::create::rewrite_url(&self.repo.config, self.push_url.as_ref(), remote::Direction::Push) { + Ok(url) => { + self.push_url_alias = url; + None + } + Err(err) => err.into(), + }; + url_err.or(push_url_err).map(Err::<&mut Self, _>).transpose()?; Ok(self) } } diff --git a/git-repository/src/remote/create.rs b/git-repository/src/remote/create.rs index f11f5433613..a09073928d1 100644 --- a/git-repository/src/remote/create.rs +++ b/git-repository/src/remote/create.rs @@ -58,28 +58,32 @@ impl<'repo> Remote<'repo> { } } +pub(crate) fn rewrite_url( + config: &config::Cache, + url: Option<&git_url::Url>, + direction: remote::Direction, +) -> Result, remote::init::Error> { + url.and_then(|url| config.url_rewrite().longest(url, direction)) + .map(|url| { + git_url::parse(&url).map_err(|err| remote::init::Error::RewrittenUrlInvalid { + kind: match direction { + remote::Direction::Fetch => "fetch", + remote::Direction::Push => "push", + }, + source: err, + rewritten_url: url, + }) + }) + .transpose() +} + pub(crate) fn rewrite_urls( config: &config::Cache, url: Option<&git_url::Url>, push_url: Option<&git_url::Url>, ) -> Result<(Option, Option), remote::init::Error> { - let rewrite = |url: Option<&git_url::Url>, direction: remote::Direction| { - url.and_then(|url| config.url_rewrite().rewrite_url(url, direction)) - .map(|url| { - git_url::parse(&url).map_err(|err| remote::init::Error::RewrittenUrlInvalid { - kind: match direction { - remote::Direction::Fetch => "fetch", - remote::Direction::Push => "push", - }, - source: err, - rewritten_url: url, - }) - }) - .transpose() - }; - - let url_alias = rewrite(url, remote::Direction::Fetch)?; - let push_url_alias = rewrite(push_url, remote::Direction::Push)?; + let url_alias = rewrite_url(config, url, remote::Direction::Fetch)?; + let push_url_alias = rewrite_url(config, push_url, remote::Direction::Push)?; Ok((url_alias, push_url_alias)) } diff --git a/git-repository/src/remote/url.rs b/git-repository/src/remote/url.rs index e5d60199f9e..f39ceda803d 100644 --- a/git-repository/src/remote/url.rs +++ b/git-repository/src/remote/url.rs @@ -62,7 +62,7 @@ impl Rewrite { } } - pub fn rewrite_url(&self, url: &git_url::Url, direction: Direction) -> Option { + pub fn longest(&self, url: &git_url::Url, direction: Direction) -> Option { if self.replacements_for(direction).is_empty() { None } else { diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index f181ab94165..ea0731c9d64 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -205,17 +205,25 @@ mod find_remote { ); let mut remote = repo.try_find_remote_without_url_rewrite("origin").expect("exists")?; - for _round in 0..2 { - assert_eq!( - remote.url(Direction::Fetch).unwrap().to_bstring(), - "https://github.com/foobar/gitoxide", - "no rewrite happened" - ); + for round in 1..=2 { + if round == 1 { + assert_eq!( + remote.url(Direction::Fetch).unwrap().to_bstring(), + "https://github.com/foobar/gitoxide", + "no rewrite happened" + ); + } else { + assert_eq!( + remote.url(Direction::Fetch).unwrap().to_bstring(), + "https://github.com/byron/gitoxide", + "it can rewrite a single url like git can" + ); + } assert_eq!(remote.url(Direction::Push).unwrap().to_bstring(), "file://dev/null",); assert_eq!( remote.rewrite_urls().unwrap_err().to_string(), expected_err_msg, - "rewriting fails, leaves both urls as they were, which diverges from git" + "rewriting fails, but it will rewrite what it can while reporting a single error." ); } Ok(()) From 75811778d067ec68442bc0700514935977ac4447 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 11 Aug 2022 19:41:43 +0800 Subject: [PATCH 052/125] refactor (#450) --- .../tests/fixtures/make_remote_repos.sh | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/git-repository/tests/fixtures/make_remote_repos.sh b/git-repository/tests/fixtures/make_remote_repos.sh index fe8d6cbffa1..4716a0a4325 100644 --- a/git-repository/tests/fixtures/make_remote_repos.sh +++ b/git-repository/tests/fixtures/make_remote_repos.sh @@ -93,14 +93,12 @@ git init base ) git clone --shared base clone -( - cd clone +(cd clone git remote add myself . ) git clone --shared base push-default -( - cd push-default +(cd push-default git remote add myself . git remote rename origin new-origin @@ -108,23 +106,20 @@ git clone --shared base push-default ) git clone --shared base push-url -( - cd push-url +(cd push-url git config remote.origin.pushUrl . git config remote.origin.push refs/tags/*:refs/tags/* ) git clone --shared base many-fetchspecs -( - cd many-fetchspecs +(cd many-fetchspecs git config --add remote.origin.fetch @ git config --add remote.origin.fetch refs/tags/*:refs/tags/* git config --add remote.origin.fetch HEAD ) git clone --shared base branch-push-remote -( - cd branch-push-remote +(cd branch-push-remote git remote rename origin new-origin git remote add myself . @@ -132,8 +127,7 @@ git clone --shared base branch-push-remote ) git init --bare url-rewriting -( - cd url-rewriting +(cd url-rewriting git remote add origin https://github.com/foobar/gitoxide cat <> config @@ -157,8 +151,7 @@ EOF ) git init --bare bad-url-rewriting -( - cd bad-url-rewriting +(cd bad-url-rewriting git remote add origin https://github.com/foobar/gitoxide cat <> config From bf47405234ba9915d77b64d4a5c1a372be102001 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 09:43:06 +0800 Subject: [PATCH 053/125] refactor (#450) --- git-repository/src/remote/access.rs | 91 ++++++++--------------------- git-repository/src/remote/build.rs | 42 +++++++++++++ git-repository/src/remote/mod.rs | 1 + 3 files changed, 68 insertions(+), 66 deletions(-) create mode 100644 git-repository/src/remote/build.rs diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index 80bfe7a6356..9dfa3579bd2 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -1,44 +1,36 @@ use crate::{remote, Remote}; use git_refspec::RefSpec; -use std::convert::TryInto; -/// Builder methods +/// Access impl Remote<'_> { - /// Set the `url` to be used when pushing data to a remote. - pub fn push_url(self, url: Url) -> Result - where - Url: TryInto, - git_url::parse::Error: From, - { - self.push_url_inner(url, true) + /// Return the name of this remote or `None` if it wasn't persisted to disk yet. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() } - /// Set the `url` to be used when pushing data to a remote, without applying rewrite rules in case these could be faulty, - /// eliminating one failure mode. - pub fn push_url_without_url_rewrite(self, url: Url) -> Result - where - Url: TryInto, - git_url::parse::Error: From, - { - self.push_url_inner(url, false) + /// Return the set of ref-specs used for `direction`, which may be empty, in order of occurrence in the configuration. + pub fn refspecs(&self, direction: remote::Direction) -> &[RefSpec] { + match direction { + remote::Direction::Fetch => &self.fetch_specs, + remote::Direction::Push => &self.push_specs, + } } - fn push_url_inner(mut self, push_url: Url, should_rewrite_urls: bool) -> Result - where - Url: TryInto, - git_url::parse::Error: From, - { - let push_url = push_url - .try_into() - .map_err(|err| remote::init::Error::Url(err.into()))?; - self.push_url = push_url.into(); - - let (_, push_url_alias) = should_rewrite_urls - .then(|| remote::create::rewrite_urls(&self.repo.config, None, self.push_url.as_ref())) - .unwrap_or(Ok((None, None)))?; - self.push_url_alias = push_url_alias; - - Ok(self) + /// Return the url used for the given `direction` with rewrites from `url..insteadOf|pushInsteadOf`, unless the instance + /// was created with one of the `_without_url_rewrite()` methods. + /// For pushing, this is the `remote..pushUrl` or the `remote..url` used for fetching, and for fetching it's + /// the `remote..url`. + /// Note that it's possible to only have the push url set, in which case there will be no way to fetch from the remote as + /// the push-url isn't used for that. + pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { + match direction { + remote::Direction::Fetch => self.url_alias.as_ref().or(self.url.as_ref()), + remote::Direction::Push => self + .push_url_alias + .as_ref() + .or(self.push_url.as_ref()) + .or_else(|| self.url(remote::Direction::Fetch)), + } } } @@ -69,36 +61,3 @@ impl Remote<'_> { Ok(self) } } - -/// Accesss -impl Remote<'_> { - /// Return the name of this remote or `None` if it wasn't persisted to disk yet. - pub fn name(&self) -> Option<&str> { - self.name.as_deref() - } - - /// Return the set of ref-specs used for `direction`, which may be empty, in order of occurrence in the configuration. - pub fn refspecs(&self, direction: remote::Direction) -> &[RefSpec] { - match direction { - remote::Direction::Fetch => &self.fetch_specs, - remote::Direction::Push => &self.push_specs, - } - } - - /// Return the url used for the given `direction` with rewrites from `url..insteadOf|pushInsteadOf`, unless the instance - /// was created with one of the `_without_url_rewrite()` methods. - /// For pushing, this is the `remote..pushUrl` or the `remote..url` used for fetching, and for fetching it's - /// the `remote..url`. - /// Note that it's possible to only have the push url set, in which case there will be no way to fetch from the remote as - /// the push-url isn't used for that. - pub fn url(&self, direction: remote::Direction) -> Option<&git_url::Url> { - match direction { - remote::Direction::Fetch => self.url_alias.as_ref().or(self.url.as_ref()), - remote::Direction::Push => self - .push_url_alias - .as_ref() - .or(self.push_url.as_ref()) - .or_else(|| self.url(remote::Direction::Fetch)), - } - } -} diff --git a/git-repository/src/remote/build.rs b/git-repository/src/remote/build.rs new file mode 100644 index 00000000000..0853680dc6d --- /dev/null +++ b/git-repository/src/remote/build.rs @@ -0,0 +1,42 @@ +use crate::{remote, Remote}; +use std::convert::TryInto; + +/// Builder methods +impl Remote<'_> { + /// Set the `url` to be used when pushing data to a remote. + pub fn push_url(self, url: Url) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + self.push_url_inner(url, true) + } + + /// Set the `url` to be used when pushing data to a remote, without applying rewrite rules in case these could be faulty, + /// eliminating one failure mode. + pub fn push_url_without_url_rewrite(self, url: Url) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + self.push_url_inner(url, false) + } + + fn push_url_inner(mut self, push_url: Url, should_rewrite_urls: bool) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + let push_url = push_url + .try_into() + .map_err(|err| remote::init::Error::Url(err.into()))?; + self.push_url = push_url.into(); + + let (_, push_url_alias) = should_rewrite_urls + .then(|| remote::create::rewrite_urls(&self.repo.config, None, self.push_url.as_ref())) + .unwrap_or(Ok((None, None)))?; + self.push_url_alias = push_url_alias; + + Ok(self) + } +} diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 3036b096229..4ec52f13760 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -7,6 +7,7 @@ pub enum Direction { Fetch, } +mod build; mod create; mod errors; pub use errors::{find, init}; From 30b7b67740f80b9954c7fe77d7007722cb95d673 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 09:51:25 +0800 Subject: [PATCH 054/125] better docs for `git-transport` (#450) --- git-transport/src/client/non_io_types.rs | 2 +- git-transport/src/lib.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/git-transport/src/client/non_io_types.rs b/git-transport/src/client/non_io_types.rs index 6ffabe43500..edcb52e409e 100644 --- a/git-transport/src/client/non_io_types.rs +++ b/git-transport/src/client/non_io_types.rs @@ -32,7 +32,7 @@ pub enum MessageKind { pub(crate) mod connect { use quick_error::quick_error; quick_error! { - /// The error used in [`connect()`]. + /// The error used in [`connect()`][crate::connect()]. #[derive(Debug)] #[allow(missing_docs)] pub enum Error { diff --git a/git-transport/src/lib.rs b/git-transport/src/lib.rs index acd9fb476ae..931c918c7bf 100644 --- a/git-transport/src/lib.rs +++ b/git-transport/src/lib.rs @@ -10,6 +10,7 @@ #![forbid(unsafe_code)] #![deny(rust_2018_idioms, missing_docs)] +pub use bstr; pub use git_packetline as packetline; /// The version of the way client and server communicate. @@ -17,10 +18,19 @@ pub use git_packetline as packetline; #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] pub enum Protocol { + /// Version 1 was the first one conceived, is stateful, and our implementation was seen to cause deadlocks. Prefer V2 V1 = 1, + /// A command-based and stateless protocol with clear semantics, and the one to use assuming the server isn't very old. + /// This is the default. V2 = 2, } +impl Default for Protocol { + fn default() -> Self { + Protocol::V2 + } +} + /// The kind of service to invoke on the client or the server side. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] From 9d9fe6c75de3a8cacdb47e42a1829ec8c732f94f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 10:25:31 +0800 Subject: [PATCH 055/125] refactor (#450) --- gitoxide-core/src/remote.rs | 229 ------------------- gitoxide-core/src/remote/mod.rs | 1 + gitoxide-core/src/remote/refs/async_io.rs | 64 ++++++ gitoxide-core/src/remote/refs/blocking_io.rs | 51 +++++ gitoxide-core/src/remote/refs/mod.rs | 110 +++++++++ 5 files changed, 226 insertions(+), 229 deletions(-) delete mode 100644 gitoxide-core/src/remote.rs create mode 100644 gitoxide-core/src/remote/mod.rs create mode 100644 gitoxide-core/src/remote/refs/async_io.rs create mode 100644 gitoxide-core/src/remote/refs/blocking_io.rs create mode 100644 gitoxide-core/src/remote/refs/mod.rs diff --git a/gitoxide-core/src/remote.rs b/gitoxide-core/src/remote.rs deleted file mode 100644 index 499c973e864..00000000000 --- a/gitoxide-core/src/remote.rs +++ /dev/null @@ -1,229 +0,0 @@ -pub mod refs { - use git_repository::{ - protocol, - protocol::{ - fetch::{Action, Arguments, Ref, Response}, - transport, - }, - }; - - use crate::OutputFormat; - - pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=2; - - use std::io; - - #[derive(Default)] - struct LsRemotes { - refs: Vec, - } - - impl protocol::fetch::DelegateBlocking for LsRemotes { - fn prepare_fetch( - &mut self, - _version: transport::Protocol, - _server: &transport::client::Capabilities, - _features: &mut Vec<(&str, Option<&str>)>, - refs: &[Ref], - ) -> io::Result { - self.refs = refs.into(); - Ok(Action::Cancel) - } - - fn negotiate( - &mut self, - _refs: &[Ref], - _arguments: &mut Arguments, - _previous_response: Option<&Response>, - ) -> io::Result { - unreachable!("not to be called due to Action::Close in `prepare_fetch`") - } - } - - #[cfg(feature = "async-client")] - mod async_io { - use std::io; - - use async_trait::async_trait; - use futures_io::AsyncBufRead; - use git_repository::{ - protocol, - protocol::fetch::{Ref, Response}, - Progress, - }; - - use super::{Context, LsRemotes}; - use crate::{net, remote::refs::print, OutputFormat}; - - #[async_trait(?Send)] - impl protocol::fetch::Delegate for LsRemotes { - async fn receive_pack( - &mut self, - input: impl AsyncBufRead + Unpin + 'async_trait, - progress: impl Progress, - refs: &[Ref], - previous_response: &Response, - ) -> io::Result<()> { - unreachable!("not called for ls-refs") - } - } - - pub async fn list( - protocol: Option, - url: &str, - progress: impl Progress, - ctx: Context, - ) -> anyhow::Result<()> { - let url = url.to_owned(); - let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into()).await?; - blocking::unblock( - // `blocking` really needs a way to unblock futures, which is what it does internally anyway. - // Both fetch() needs unblocking as it executes blocking code within the future, and the other - // block does blocking IO because it's primarily a blocking codebase. - move || { - futures_lite::future::block_on(async move { - let mut delegate = LsRemotes::default(); - protocol::fetch( - transport, - &mut delegate, - protocol::credentials::helper, - progress, - protocol::FetchConnection::TerminateOnSuccessfulCompletion, - ) - .await?; - - match ctx.format { - OutputFormat::Human => drop(print(ctx.out, &delegate.refs)), - #[cfg(feature = "serde1")] - OutputFormat::Json => serde_json::to_writer_pretty( - ctx.out, - &delegate.refs.into_iter().map(super::JsonRef::from).collect::>(), - )?, - } - Ok(()) - }) - }, - ) - .await - } - } - #[cfg(feature = "async-client")] - pub use self::async_io::list; - - #[cfg(feature = "blocking-client")] - mod blocking_io { - use std::io; - - use git_repository::{ - protocol, - protocol::fetch::{Ref, Response}, - Progress, - }; - - #[cfg(feature = "serde1")] - use super::JsonRef; - use super::{print, Context, LsRemotes}; - use crate::{net, OutputFormat}; - - impl protocol::fetch::Delegate for LsRemotes { - fn receive_pack( - &mut self, - _input: impl io::BufRead, - _progress: impl Progress, - _refs: &[Ref], - _previous_response: &Response, - ) -> io::Result<()> { - unreachable!("not called for ls-refs") - } - } - - pub fn list( - protocol: Option, - url: &str, - progress: impl Progress, - ctx: Context, - ) -> anyhow::Result<()> { - let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into())?; - let mut delegate = LsRemotes::default(); - protocol::fetch( - transport, - &mut delegate, - protocol::credentials::helper, - progress, - protocol::FetchConnection::TerminateOnSuccessfulCompletion, - )?; - - match ctx.format { - OutputFormat::Human => drop(print(ctx.out, &delegate.refs)), - #[cfg(feature = "serde1")] - OutputFormat::Json => serde_json::to_writer_pretty( - ctx.out, - &delegate.refs.into_iter().map(JsonRef::from).collect::>(), - )?, - }; - Ok(()) - } - } - #[cfg(feature = "blocking-client")] - pub use blocking_io::list; - - pub struct Context { - pub thread_limit: Option, - pub format: OutputFormat, - pub out: W, - } - - #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] - pub enum JsonRef { - Peeled { - path: String, - tag: String, - object: String, - }, - Direct { - path: String, - object: String, - }, - Symbolic { - path: String, - target: String, - object: String, - }, - } - - impl From for JsonRef { - fn from(value: Ref) -> Self { - match value { - Ref::Direct { path, object } => JsonRef::Direct { - path: path.to_string(), - object: object.to_string(), - }, - Ref::Symbolic { path, target, object } => JsonRef::Symbolic { - path: path.to_string(), - target: target.to_string(), - object: object.to_string(), - }, - Ref::Peeled { path, tag, object } => JsonRef::Peeled { - path: path.to_string(), - tag: tag.to_string(), - object: object.to_string(), - }, - } - } - } - - pub(crate) fn print(mut out: impl io::Write, refs: &[Ref]) -> io::Result<()> { - for r in refs { - match r { - Ref::Direct { path, object } => writeln!(&mut out, "{} {}", object.to_hex(), path), - Ref::Peeled { path, object, tag } => { - writeln!(&mut out, "{} {} tag:{}", object.to_hex(), path, tag) - } - Ref::Symbolic { path, target, object } => { - writeln!(&mut out, "{} {} symref-target:{}", object.to_hex(), path, target) - } - }?; - } - Ok(()) - } -} diff --git a/gitoxide-core/src/remote/mod.rs b/gitoxide-core/src/remote/mod.rs new file mode 100644 index 00000000000..f3ce0777cae --- /dev/null +++ b/gitoxide-core/src/remote/mod.rs @@ -0,0 +1 @@ +pub mod refs; diff --git a/gitoxide-core/src/remote/refs/async_io.rs b/gitoxide-core/src/remote/refs/async_io.rs new file mode 100644 index 00000000000..45b3ce67e61 --- /dev/null +++ b/gitoxide-core/src/remote/refs/async_io.rs @@ -0,0 +1,64 @@ +use std::io; + +use async_trait::async_trait; +use futures_io::AsyncBufRead; +use git_repository::{ + protocol, + protocol::fetch::{Ref, Response}, + Progress, +}; + +use super::{Context, LsRemotes}; +use crate::{net, remote::refs::print, OutputFormat}; + +#[async_trait(?Send)] +impl protocol::fetch::Delegate for LsRemotes { + async fn receive_pack( + &mut self, + input: impl AsyncBufRead + Unpin + 'async_trait, + progress: impl Progress, + refs: &[Ref], + previous_response: &Response, + ) -> io::Result<()> { + unreachable!("not called for ls-refs") + } +} + +pub async fn list( + protocol: Option, + url: &str, + progress: impl Progress, + ctx: Context, +) -> anyhow::Result<()> { + let url = url.to_owned(); + let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into()).await?; + blocking::unblock( + // `blocking` really needs a way to unblock futures, which is what it does internally anyway. + // Both fetch() needs unblocking as it executes blocking code within the future, and the other + // block does blocking IO because it's primarily a blocking codebase. + move || { + futures_lite::future::block_on(async move { + let mut delegate = LsRemotes::default(); + protocol::fetch( + transport, + &mut delegate, + protocol::credentials::helper, + progress, + protocol::FetchConnection::TerminateOnSuccessfulCompletion, + ) + .await?; + + match ctx.format { + OutputFormat::Human => drop(print(ctx.out, &delegate.refs)), + #[cfg(feature = "serde1")] + OutputFormat::Json => serde_json::to_writer_pretty( + ctx.out, + &delegate.refs.into_iter().map(super::JsonRef::from).collect::>(), + )?, + } + Ok(()) + }) + }, + ) + .await +} diff --git a/gitoxide-core/src/remote/refs/blocking_io.rs b/gitoxide-core/src/remote/refs/blocking_io.rs new file mode 100644 index 00000000000..a7e24c0393a --- /dev/null +++ b/gitoxide-core/src/remote/refs/blocking_io.rs @@ -0,0 +1,51 @@ +use std::io; + +use git_repository::{ + protocol, + protocol::fetch::{Ref, Response}, + Progress, +}; + +#[cfg(feature = "serde1")] +use super::JsonRef; +use super::{print, Context, LsRemotes}; +use crate::{net, OutputFormat}; + +impl protocol::fetch::Delegate for LsRemotes { + fn receive_pack( + &mut self, + _input: impl io::BufRead, + _progress: impl Progress, + _refs: &[Ref], + _previous_response: &Response, + ) -> io::Result<()> { + unreachable!("not called for ls-refs") + } +} + +pub fn list( + protocol: Option, + url: &str, + progress: impl Progress, + ctx: Context, +) -> anyhow::Result<()> { + let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into())?; + let mut delegate = LsRemotes::default(); + protocol::fetch( + transport, + &mut delegate, + protocol::credentials::helper, + progress, + protocol::FetchConnection::TerminateOnSuccessfulCompletion, + )?; + + match ctx.format { + OutputFormat::Human => drop(print(ctx.out, &delegate.refs)), + #[cfg(feature = "serde1")] + OutputFormat::Json => serde_json::to_writer_pretty( + ctx.out, + &delegate.refs.into_iter().map(JsonRef::from).collect::>(), + )?, + }; + Ok(()) +} diff --git a/gitoxide-core/src/remote/refs/mod.rs b/gitoxide-core/src/remote/refs/mod.rs new file mode 100644 index 00000000000..cc4dfa0a265 --- /dev/null +++ b/gitoxide-core/src/remote/refs/mod.rs @@ -0,0 +1,110 @@ +use git_repository::{ + protocol, + protocol::{ + fetch::{Action, Arguments, Ref, Response}, + transport, + }, +}; + +use crate::OutputFormat; + +pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=2; + +use std::io; + +#[derive(Default)] +struct LsRemotes { + refs: Vec, +} + +impl protocol::fetch::DelegateBlocking for LsRemotes { + fn prepare_fetch( + &mut self, + _version: transport::Protocol, + _server: &transport::client::Capabilities, + _features: &mut Vec<(&str, Option<&str>)>, + refs: &[Ref], + ) -> io::Result { + self.refs = refs.into(); + Ok(Action::Cancel) + } + + fn negotiate( + &mut self, + _refs: &[Ref], + _arguments: &mut Arguments, + _previous_response: Option<&Response>, + ) -> io::Result { + unreachable!("not to be called due to Action::Close in `prepare_fetch`") + } +} + +#[cfg(feature = "async-client")] +mod async_io; +#[cfg(feature = "async-client")] +pub use self::async_io::list; + +#[cfg(feature = "blocking-client")] +mod blocking_io; +#[cfg(feature = "blocking-client")] +pub use blocking_io::list; + +pub struct Context { + pub thread_limit: Option, + pub format: OutputFormat, + pub out: W, +} + +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub enum JsonRef { + Peeled { + path: String, + tag: String, + object: String, + }, + Direct { + path: String, + object: String, + }, + Symbolic { + path: String, + target: String, + object: String, + }, +} + +impl From for JsonRef { + fn from(value: Ref) -> Self { + match value { + Ref::Direct { path, object } => JsonRef::Direct { + path: path.to_string(), + object: object.to_string(), + }, + Ref::Symbolic { path, target, object } => JsonRef::Symbolic { + path: path.to_string(), + target: target.to_string(), + object: object.to_string(), + }, + Ref::Peeled { path, tag, object } => JsonRef::Peeled { + path: path.to_string(), + tag: tag.to_string(), + object: object.to_string(), + }, + } + } +} + +pub(crate) fn print(mut out: impl io::Write, refs: &[Ref]) -> io::Result<()> { + for r in refs { + match r { + Ref::Direct { path, object } => writeln!(&mut out, "{} {}", object.to_hex(), path), + Ref::Peeled { path, object, tag } => { + writeln!(&mut out, "{} {} tag:{}", object.to_hex(), path, tag) + } + Ref::Symbolic { path, target, object } => { + writeln!(&mut out, "{} {} symref-target:{}", object.to_hex(), path, target) + } + }?; + } + Ok(()) +} From 71a43d0bc12661efcb9c94697c704f700a3be488 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 10:25:42 +0800 Subject: [PATCH 056/125] change!: use `thiserror` instead of `quickerror` (#450) --- Cargo.lock | 1 - git-transport/Cargo.toml | 1 - .../src/client/blocking_io/connect.rs | 110 ++++++++++-------- .../client/blocking_io/http/curl/remote.rs | 4 +- .../src/client/blocking_io/http/mod.rs | 52 +++++---- .../src/client/blocking_io/http/traits.rs | 31 ++--- git-transport/src/client/blocking_io/ssh.rs | 16 +-- git-transport/src/client/capabilities.rs | 52 ++++----- git-transport/src/client/git/blocking_io.rs | 44 +++---- git-transport/src/client/mod.rs | 2 +- git-transport/src/client/non_io_types.rs | 53 ++++----- 11 files changed, 168 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10f125335a0..22d022542df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1646,7 +1646,6 @@ dependencies = [ "git-url", "maybe-async", "pin-project-lite", - "quick-error", "serde", "thiserror", ] diff --git a/git-transport/Cargo.toml b/git-transport/Cargo.toml index 8c380ebc31b..9a945ef1f38 100644 --- a/git-transport/Cargo.toml +++ b/git-transport/Cargo.toml @@ -56,7 +56,6 @@ git-sec = { version = "^0.3.0", path = "../git-sec" } git-packetline = { version = "^0.12.5", path = "../git-packetline" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} -quick-error = "2.0.0" bstr = { version = "0.2.13", default-features = false, features = ["std"] } # for async-client diff --git a/git-transport/src/client/blocking_io/connect.rs b/git-transport/src/client/blocking_io/connect.rs index 3f60b44e9e0..48b0b3b7562 100644 --- a/git-transport/src/client/blocking_io/connect.rs +++ b/git-transport/src/client/blocking_io/connect.rs @@ -1,61 +1,71 @@ pub use crate::client::non_io_types::connect::Error; -use crate::client::Transport; -/// A general purpose connector connecting to a repository identified by the given `url`. -/// -/// This includes connections to -/// [local repositories][crate::client::file::connect()], -/// [repositories over ssh][crate::client::ssh::connect()], -/// [git daemons][crate::client::git::connect()], -/// and if compiled in connections to [git repositories over https][crate::client::http::connect()]. -/// -/// Use `desired_version` to set the desired protocol version to use when connecting, but not that the server may downgrade it. -pub fn connect(url: &[u8], desired_version: crate::Protocol) -> Result, Error> { - let urlb = url; - let mut url = git_url::parse(urlb)?; - Ok(match url.scheme { - git_url::Scheme::Radicle => return Err(Error::UnsupportedScheme(url.scheme)), - git_url::Scheme::File => { - if url.user().is_some() || url.host().is_some() || url.port.is_some() { - return Err(Error::UnsupportedUrlTokens(urlb.into(), url.scheme)); - } - Box::new( - crate::client::blocking_io::file::connect(url.path, desired_version) - .map_err(|e| Box::new(e) as Box)?, - ) - } - git_url::Scheme::Ssh => Box::new({ - let path = std::mem::take(&mut url.path); - crate::client::blocking_io::ssh::connect( - url.host().expect("host is present in url"), - path, - desired_version, - url.user(), - url.port, - ) - .map_err(|e| Box::new(e) as Box)? - }), - git_url::Scheme::Git => { - if url.user().is_some() { - return Err(Error::UnsupportedUrlTokens(urlb.into(), url.scheme)); +pub(crate) mod function { + use crate::client::non_io_types::connect::Error; + use crate::client::Transport; + + /// A general purpose connector connecting to a repository identified by the given `url`. + /// + /// This includes connections to + /// [local repositories][crate::client::file::connect()], + /// [repositories over ssh][crate::client::ssh::connect()], + /// [git daemons][crate::client::git::connect()], + /// and if compiled in connections to [git repositories over https][crate::client::http::connect()]. + /// + /// Use `desired_version` to set the desired protocol version to use when connecting, but not that the server may downgrade it. + pub fn connect(url: &[u8], desired_version: crate::Protocol) -> Result, Error> { + let urlb = url; + let mut url = git_url::parse(urlb)?; + Ok(match url.scheme { + git_url::Scheme::Radicle => return Err(Error::UnsupportedScheme(url.scheme)), + git_url::Scheme::File => { + if url.user().is_some() || url.host().is_some() || url.port.is_some() { + return Err(Error::UnsupportedUrlTokens { + url: urlb.into(), + scheme: url.scheme, + }); + } + Box::new( + crate::client::blocking_io::file::connect(url.path, desired_version) + .map_err(|e| Box::new(e) as Box)?, + ) } - Box::new({ + git_url::Scheme::Ssh => Box::new({ let path = std::mem::take(&mut url.path); - crate::client::git::connect( + crate::client::blocking_io::ssh::connect( url.host().expect("host is present in url"), path, desired_version, + url.user(), url.port, ) .map_err(|e| Box::new(e) as Box)? - }) - } - #[cfg(not(feature = "http-client-curl"))] - git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), - #[cfg(feature = "http-client-curl")] - git_url::Scheme::Https | git_url::Scheme::Http => Box::new( - crate::client::http::connect(urlb.into(), desired_version) - .map_err(|e| Box::new(e) as Box)?, - ), - }) + }), + git_url::Scheme::Git => { + if url.user().is_some() { + return Err(Error::UnsupportedUrlTokens { + url: urlb.into(), + scheme: url.scheme, + }); + } + Box::new({ + let path = std::mem::take(&mut url.path); + crate::client::git::connect( + url.host().expect("host is present in url"), + path, + desired_version, + url.port, + ) + .map_err(|e| Box::new(e) as Box)? + }) + } + #[cfg(not(feature = "http-client-curl"))] + git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), + #[cfg(feature = "http-client-curl")] + git_url::Scheme::Https | git_url::Scheme::Http => Box::new( + crate::client::http::connect(urlb.into(), desired_version) + .map_err(|e| Box::new(e) as Box)?, + ), + }) + } } diff --git a/git-transport/src/client/blocking_io/http/curl/remote.rs b/git-transport/src/client/blocking_io/http/curl/remote.rs index a972723ac3b..3d08357e259 100644 --- a/git-transport/src/client/blocking_io/http/curl/remote.rs +++ b/git-transport/src/client/blocking_io/http/curl/remote.rs @@ -182,6 +182,8 @@ pub fn new() -> ( impl From for http::Error { fn from(err: curl::Error) -> Self { - http::Error::Detail(err.to_string()) + http::Error::Detail { + description: err.to_string(), + } } } diff --git a/git-transport/src/client/blocking_io/http/mod.rs b/git-transport/src/client/blocking_io/http/mod.rs index 5d7d1c6f562..c9833f99064 100644 --- a/git-transport/src/client/blocking_io/http/mod.rs +++ b/git-transport/src/client/blocking_io/http/mod.rs @@ -2,7 +2,7 @@ use bstr::{BStr, BString}; use std::{ borrow::Cow, convert::Infallible, - io::{self, BufRead, Read}, + io::{BufRead, Read}, }; use git_packetline::PacketLineRef; @@ -27,9 +27,9 @@ pub type Impl = curl::Curl; pub struct Transport { url: BString, user_agent_header: &'static str, - desired_version: crate::Protocol, - supported_versions: [crate::Protocol; 1], - actual_version: crate::Protocol, + desired_version: Protocol, + supported_versions: [Protocol; 1], + actual_version: Protocol, http: H, service: Option, line_provider: Option>, @@ -38,7 +38,7 @@ pub struct Transport { impl Transport { /// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol. - pub fn new(url: &BStr, desired_version: crate::Protocol) -> Self { + pub fn new(url: &BStr, desired_version: Protocol) -> Self { Transport { url: url.to_owned(), user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")), @@ -62,10 +62,12 @@ impl Transport { .iter() .any(|l| l == &wanted_content_type) { - return Err(client::Error::Http(Error::Detail(format!( - "Didn't find '{}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported.", - wanted_content_type - )))); + return Err(client::Error::Http(Error::Detail { + description: format!( + "Didn't find '{}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported.", + wanted_content_type + ), + })); } Ok(()) } @@ -106,8 +108,8 @@ impl client::TransportWithoutIO for Transport { fn request( &mut self, write_mode: client::WriteMode, - on_into_read: client::MessageKind, - ) -> Result, client::Error> { + on_into_read: MessageKind, + ) -> Result, client::Error> { let service = self.service.expect("handshake() must have been called first"); let url = append_url(self.url.as_ref(), service.as_str()); let static_headers = &[ @@ -207,11 +209,13 @@ impl client::Transport for Transport { line_reader.as_read().read_to_string(&mut announced_service)?; let expected_service_announcement = format!("# service={}", service.as_str()); if announced_service.trim() != expected_service_announcement { - return Err(client::Error::Http(Error::Detail(format!( - "Expected to see {:?}, but got {:?}", - expected_service_announcement, - announced_service.trim() - )))); + return Err(client::Error::Http(Error::Detail { + description: format!( + "Expected to see {:?}, but got {:?}", + expected_service_announcement, + announced_service.trim() + ), + })); } let capabilities::recv::Outcome { @@ -236,24 +240,24 @@ struct HeadersThenBody { } impl HeadersThenBody { - fn handle_headers(&mut self) -> io::Result<()> { + fn handle_headers(&mut self) -> std::io::Result<()> { if let Some(headers) = self.headers.take() { >::check_content_type(self.service, "result", headers) - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))? + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))? } Ok(()) } } -impl io::Read for HeadersThenBody { - fn read(&mut self, buf: &mut [u8]) -> io::Result { +impl Read for HeadersThenBody { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.handle_headers()?; self.body.read(buf) } } -impl io::BufRead for HeadersThenBody { - fn fill_buf(&mut self) -> io::Result<&[u8]> { +impl BufRead for HeadersThenBody { + fn fill_buf(&mut self) -> std::io::Result<&[u8]> { self.handle_headers()?; self.body.fill_buf() } @@ -268,7 +272,7 @@ impl ExtendedBufRead for HeadersThenBody Option>> { + fn peek_data_line(&mut self) -> Option>> { if let Err(err) = self.handle_headers() { return Some(Err(err)); } @@ -285,6 +289,6 @@ impl ExtendedBufRead for HeadersThenBody Result, Infallible> { +pub fn connect(url: &BStr, desired_version: Protocol) -> Result, Infallible> { Ok(Transport::new(url, desired_version)) } diff --git a/git-transport/src/client/blocking_io/http/traits.rs b/git-transport/src/client/blocking_io/http/traits.rs index c2e707496f4..06f48db7bda 100644 --- a/git-transport/src/client/blocking_io/http/traits.rs +++ b/git-transport/src/client/blocking_io/http/traits.rs @@ -1,22 +1,13 @@ use bstr::BStr; -use std::io; -use quick_error::quick_error; - -quick_error! { - /// The error used by the [Http] trait. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Detail(description: String) { - display("{}", description) - } - PostBody(err: io::Error) { - display("An IO error occurred while uploading the body of a POST request") - from() - source(err) - } - } +/// The error used by the [Http] trait. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("{description}")] + Detail { description: String }, + #[error("An IO error occurred while uploading the body of a POST request")] + PostBody(#[from] std::io::Error), } /// The return value of [Http::get()]. @@ -52,11 +43,11 @@ impl From> for GetResponse { #[allow(clippy::type_complexity)] pub trait Http { /// A type providing headers line by line. - type Headers: io::BufRead + Unpin; + type Headers: std::io::BufRead + Unpin; /// A type providing the response. - type ResponseBody: io::BufRead; + type ResponseBody: std::io::BufRead; /// A type allowing to write the content to post. - type PostBody: io::Write; + type PostBody: std::io::Write; /// Initiate a `GET` request to `url` provided the given `headers`. /// diff --git a/git-transport/src/client/blocking_io/ssh.rs b/git-transport/src/client/blocking_io/ssh.rs index 3e3ed4e6e67..ea2a0477957 100644 --- a/git-transport/src/client/blocking_io/ssh.rs +++ b/git-transport/src/client/blocking_io/ssh.rs @@ -1,19 +1,15 @@ use std::borrow::Cow; use bstr::BString; -use quick_error::quick_error; use crate::{client::blocking_io, Protocol}; -quick_error! { - /// The error used in [`connect()`]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - UnsupportedSshCommand(command: String) { - display("The ssh command '{}' is not currently supported", command) - } - } +/// The error used in [`connect()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("The ssh command {0:?} is not currently supported")] + UnsupportedSshCommand(String), } /// Connect to `host` using the ssh program to obtain data from the repository at `path` on the remote. diff --git a/git-transport/src/client/capabilities.rs b/git-transport/src/client/capabilities.rs index dc50a3f09c4..3bda61ade18 100644 --- a/git-transport/src/client/capabilities.rs +++ b/git-transport/src/client/capabilities.rs @@ -1,38 +1,25 @@ -use std::io; - use bstr::{BStr, BString, ByteSlice}; -use quick_error::quick_error; #[cfg(any(feature = "blocking-client", feature = "async-client"))] use crate::client; use crate::Protocol; -quick_error! { - /// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - MissingDelimitingNullByte { - display("Capabilities were missing entirely as there was no 0 byte") - } - NoCapabilities { - display("there was not a single capability behind the delimiter") - } - MissingVersionLine { - display("a version line was expected, but none was retrieved") - } - MalformattedVersionLine(actual: String) { - display("expected 'version X', got '{}'", actual) - } - UnsupportedVersion(wanted: Protocol, got: String) { - display("Got unsupported version '{}', expected '{}'", got, *wanted as usize) - } - Io(err: io::Error) { - display("An IO error occurred while reading V2 lines") - from() - source(err) - } - } +/// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Capabilities were missing entirely as there was no 0 byte")] + MissingDelimitingNullByte, + #[error("there was not a single capability behind the delimiter")] + NoCapabilities, + #[error("a version line was expected, but none was retrieved")] + MissingVersionLine, + #[error("expected 'version X', got {0:?}")] + MalformattedVersionLine(String), + #[error("Got unsupported version '{}', expected {actual:?}", *desired as u8)] + UnsupportedVersion { desired: Protocol, actual: String }, + #[error("An IO error occurred while reading V2 lines")] + Io(#[from] std::io::Error), } /// A structure to represent multiple [capabilities][Capability] or features supported by the server. @@ -113,7 +100,10 @@ impl Capabilities { return Err(Error::MalformattedVersionLine(version_line)); } if value != " 2" { - return Err(Error::UnsupportedVersion(Protocol::V2, value.to_owned())); + return Err(Error::UnsupportedVersion { + desired: Protocol::V2, + actual: value.to_owned(), + }); } Ok(Capabilities { value_sep: b'\n', @@ -181,7 +171,7 @@ pub mod recv { /// /// This is `Some` only when protocol v1 is used. The [`io::BufRead`] must be exhausted by /// the caller. - pub refs: Option>, + pub refs: Option>, /// The [`Protocol`] the remote advertised. pub protocol: Protocol, } diff --git a/git-transport/src/client/git/blocking_io.rs b/git-transport/src/client/git/blocking_io.rs index 5d09c4c8996..bc1dddff9f3 100644 --- a/git-transport/src/client/git/blocking_io.rs +++ b/git-transport/src/client/git/blocking_io.rs @@ -1,4 +1,4 @@ -use std::{io, io::Write}; +use std::io::Write; use bstr::BString; use git_packetline::PacketLineRef; @@ -10,8 +10,8 @@ use crate::{ impl client::TransportWithoutIO for git::Connection where - R: io::Read, - W: io::Write, + R: std::io::Read, + W: std::io::Write, { fn request( &mut self, @@ -57,8 +57,8 @@ where impl client::Transport for git::Connection where - R: io::Read, - W: io::Write, + R: std::io::Read, + W: std::io::Write, { fn handshake<'a>( &mut self, @@ -92,8 +92,8 @@ where impl git::Connection where - R: io::Read, - W: io::Write, + R: std::io::Read, + W: std::io::Write, { /// Create a connection from the given `read` and `write`, asking for `desired_version` as preferred protocol /// and the transfer of the repository at `repository_path`. @@ -137,29 +137,19 @@ where /// pub mod connect { - use std::{ - io, - net::{TcpStream, ToSocketAddrs}, - }; + use std::net::{TcpStream, ToSocketAddrs}; use bstr::BString; - use quick_error::quick_error; use crate::client::git; - quick_error! { - /// The error used in [`connect()`]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Io(err: io::Error){ - display("An IO error occurred when connecting to the server") - from() - source(err) - } - VirtualHostInvalid(host: String) { - display("Could not parse '{}' as virtual host with format [:port]", host) - } - } + /// The error used in [`connect()`]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("An IO error occurred when connecting to the server")] + Io(#[from] std::io::Error), + #[error("Could not parse {host:?} as virtual host with format [:port]")] + VirtualHostInvalid { host: String }, } fn parse_host(input: String) -> Result<(String, Option), Error> { @@ -168,7 +158,7 @@ pub mod connect { (Some(host), None) => (host.to_owned(), None), (Some(host), Some(port)) => ( host.to_owned(), - Some(port.parse().map_err(|_| Error::VirtualHostInvalid(input))?), + Some(port.parse().map_err(|_| Error::VirtualHostInvalid { host: input })?), ), _ => unreachable!("we expect at least one token, the original string"), }) diff --git a/git-transport/src/client/mod.rs b/git-transport/src/client/mod.rs index bb50ca4e185..996c5ed0eb4 100644 --- a/git-transport/src/client/mod.rs +++ b/git-transport/src/client/mod.rs @@ -18,7 +18,7 @@ pub use blocking_io::{ }; #[cfg(feature = "blocking-client")] #[doc(inline)] -pub use connect::connect; +pub use connect::function::connect; /// pub mod capabilities; diff --git a/git-transport/src/client/non_io_types.rs b/git-transport/src/client/non_io_types.rs index edcb52e409e..8d8400f4788 100644 --- a/git-transport/src/client/non_io_types.rs +++ b/git-transport/src/client/non_io_types.rs @@ -29,39 +29,28 @@ pub enum MessageKind { Text(&'static [u8]), } +#[cfg(any(feature = "blocking-client", feature = "async-client"))] pub(crate) mod connect { - use quick_error::quick_error; - quick_error! { - /// The error used in [`connect()`][crate::connect()]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Url(err: git_url::parse::Error) { - display("The URL could not be parsed") - from() - source(err) - } - PathConversion(err: bstr::Utf8Error) { - display("The git repository paths could not be converted to UTF8") - from() - source(err) - } - Connection(err: Box) { - display("connection failed") - from() - source(&**err) - } - UnsupportedUrlTokens(url: bstr::BString, scheme: git_url::Scheme) { - display("The url '{}' contains information that would not be used by the '{}' protocol", url, scheme) - } - UnsupportedScheme(scheme: git_url::Scheme) { - display("The '{}' protocol is currently unsupported", scheme) - } - #[cfg(not(feature = "http-client-curl"))] - CompiledWithoutHttp(scheme: git_url::Scheme) { - display("'{}' is not compiled in. Compile with the 'http-client-curl' cargo feature", scheme) - } - } + /// The error used in [`connect()`][crate::connect()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Url(#[from] git_url::parse::Error), + #[error("The git repository path could not be converted to UTF8")] + PathConversion(#[from] bstr::Utf8Error), + #[error("connection failed")] + Connection(#[from] Box), + #[error("The url {url:?} contains information that would not be used by the {scheme} protocol")] + UnsupportedUrlTokens { + url: bstr::BString, + scheme: git_url::Scheme, + }, + #[error("The '{0}' protocol is currently unsupported")] + UnsupportedScheme(git_url::Scheme), + #[cfg(not(feature = "http-client-curl"))] + #[error("'{0}' is not compiled in. Compile with the 'http-client-curl' cargo feature")] + CompiledWithoutHttp(git_url::Scheme), } } From 3a024ed2bbab19c613349e88abc41502c5cfa8e2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 10:32:30 +0800 Subject: [PATCH 057/125] adjust to changes in `git-transport` (#450) --- gitoxide-core/src/net.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gitoxide-core/src/net.rs b/gitoxide-core/src/net.rs index 45e9d55c9ec..25e208234a2 100644 --- a/gitoxide-core/src/net.rs +++ b/gitoxide-core/src/net.rs @@ -91,7 +91,10 @@ mod async_io { Ok(match url.scheme { git_repository::url::Scheme::Git => { if url.user().is_some() { - return Err(Error::UnsupportedUrlTokens(urlb.into(), url.scheme)); + return Err(Error::UnsupportedUrlTokens { + url: urlb.into(), + scheme: url.scheme, + }); } let path = std::mem::take(&mut url.path); git_connect( From ef187f0180d89544d9015cbc2bc03d8cb51f4615 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 13:02:32 +0800 Subject: [PATCH 058/125] feat: `Remote::with_refspec()` to add new unique refspecs (#450) --- git-repository/src/remote/build.rs | 26 +++++++++++++++++++++++ git-repository/tests/repository/remote.rs | 18 +++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/git-repository/src/remote/build.rs b/git-repository/src/remote/build.rs index 0853680dc6d..3d506d23092 100644 --- a/git-repository/src/remote/build.rs +++ b/git-repository/src/remote/build.rs @@ -1,3 +1,4 @@ +use crate::bstr::BStr; use crate::{remote, Remote}; use std::convert::TryInto; @@ -39,4 +40,29 @@ impl Remote<'_> { Ok(self) } + + /// Add `spec` as refspec for `direction` to our list if it's unique. + pub fn with_refspec<'a>( + mut self, + spec: impl Into<&'a BStr>, + direction: remote::Direction, + ) -> Result { + use remote::Direction::*; + let spec = git_refspec::parse( + spec.into(), + match direction { + Push => git_refspec::parse::Operation::Push, + Fetch => git_refspec::parse::Operation::Fetch, + }, + )? + .to_owned(); + let specs = match direction { + Push => &mut self.push_specs, + Fetch => &mut self.fetch_specs, + }; + if !specs.contains(&spec) { + specs.push(spec); + } + Ok(self) + } } diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index ea0731c9d64..463c248a842 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -12,13 +12,29 @@ mod remote_at { assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring(), fetch_url); assert_eq!(remote.url(Direction::Push).unwrap().to_bstring(), fetch_url); - let remote = remote.push_url("user@host.xz:./relative")?; + let mut remote = remote.push_url("user@host.xz:./relative")?; assert_eq!( remote.url(Direction::Push).unwrap().to_bstring(), "ssh://user@host.xz/relative" ); assert_eq!(remote.url(Direction::Fetch).unwrap().to_bstring(), fetch_url); + for (spec, direction) in [ + ("refs/heads/push", Direction::Push), + ("refs/heads/fetch", Direction::Fetch), + ] { + assert_eq!( + remote.refspecs(direction), + &[], + "no specs are preset for newly created remotes" + ); + remote = remote.with_refspec(spec, direction)?; + assert_eq!(remote.refspecs(direction).len(), 1, "the new refspec was added"); + + remote = remote.with_refspec(spec, direction)?; + assert_eq!(remote.refspecs(direction).len(), 1, "duplicates are disallowed"); + } + Ok(()) } From f933ae3dea69bd7d432aaf47de62f2ecbb31605c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 13:53:28 +0800 Subject: [PATCH 059/125] a sketch for the remote connection API, for async and blocking (#450) --- Makefile | 2 + git-repository/src/remote/mod.rs | 98 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/Makefile b/Makefile index 44e7634c3b9..b4ee8075338 100644 --- a/Makefile +++ b/Makefile @@ -150,6 +150,8 @@ unit-tests: ## run all unit tests && cargo test --features async-client \ && cargo test cd git-repository && cargo test \ + && cargo test --features async-network-client \ + && cargo test --features blocking-network-client \ && cargo test --features regex cd gitoxide-core && cargo test --lib diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 4ec52f13760..859fa8a58d4 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -7,9 +7,107 @@ pub enum Direction { Fetch, } +impl Direction { + /// Return ourselves as string suitable for use as verb in an english sentence. + pub fn as_str(&self) -> &'static str { + match self { + Direction::Push => "push", + Direction::Fetch => "fetch", + } + } +} + mod build; mod create; mod errors; +#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] +pub mod connect { + #![allow(missing_docs)] + use crate::remote::Connection; + use crate::{remote, Remote}; + use git_protocol::transport::client::Transport; + + mod error { + use crate::bstr::BString; + use crate::remote; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Connect(#[from] git_protocol::transport::client::connect::Error), + #[error("The {} url was missing - don't know where to establish a connection to", direction.as_str())] + MissingUrl { direction: remote::Direction }, + #[error("Protocol named {given:?} is not a valid protocol. Choose between 1 and 2")] + UnknownProtocol { given: BString }, + } + } + pub use error::Error; + + impl<'repo> Remote<'repo> { + /// Create a new connection into `direction` using `transport` to communicate. + /// + /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()]. + /// It's meant to be used when async operation is needed with runtimes of the user's choice. + pub fn into_connection(self, transport: T, direction: remote::Direction) -> Connection<'repo, T> + where + T: Transport, + { + Connection { + remote: self, + direction, + transport, + } + } + + /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. + #[cfg(feature = "blocking-network-client")] + pub fn connect( + &self, + direction: remote::Direction, + ) -> Result>, Error> { + use git_protocol::transport::Protocol; + let _protocol = self + .repo + .config + .resolved + .integer("protocol", None, "version") + .unwrap_or(Ok(2)) + .map_err(|err| Error::UnknownProtocol { given: err.input }) + .and_then(|num| { + Ok(match num { + 1 => Protocol::V1, + 2 => Protocol::V2, + num => { + return Err(Error::UnknownProtocol { + given: num.to_string().into(), + }) + } + }) + })?; + let _url = self.url(direction).ok_or(Error::MissingUrl { direction })?; + todo!() + // transport::connect( + // url , + // protocol, + // ) + } + } +} +#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] +mod connection { + #![allow(missing_docs, dead_code)] + use crate::remote; + use crate::Remote; + + pub struct Connection<'repo, T> { + pub(crate) remote: Remote<'repo>, + pub(crate) direction: remote::Direction, + pub(crate) transport: T, + } +} +#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] +pub use connection::Connection; + pub use errors::{find, init}; mod access; From f6a6a499f20e12e2bcca734bdf3c8599d37f6a6f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 20:16:46 +0800 Subject: [PATCH 060/125] feat: `connect()` method is available in when `async-std` feature is set along with `async-client` (#450) This makes some async support available even trough the base crate, which otherwise would require establishing a connection (with a runtime of choice) by hand. --- Makefile | 1 + git-transport/Cargo.toml | 4 ++ git-transport/src/client/async_io/connect.rs | 41 +++++++++++++++++++ git-transport/src/client/async_io/mod.rs | 6 +-- .../src/client/blocking_io/connect.rs | 2 +- git-transport/src/client/git/async_io.rs | 34 +++++++++++++++ git-transport/src/lib.rs | 5 ++- 7 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 git-transport/src/client/async_io/connect.rs diff --git a/Makefile b/Makefile index b4ee8075338..25b47859e65 100644 --- a/Makefile +++ b/Makefile @@ -109,6 +109,7 @@ check: ## Build all code in suitable configurations cd git-transport && cargo check \ && cargo check --features blocking-client \ && cargo check --features async-client \ + && cargo check --features async-client,async-std \ && cargo check --features http-client-curl cd git-transport && if cargo check --all-features 2>/dev/null; then false; else true; fi cd git-protocol && cargo check \ diff --git a/git-transport/Cargo.toml b/git-transport/Cargo.toml index 9a945ef1f38..e387cf4c777 100644 --- a/git-transport/Cargo.toml +++ b/git-transport/Cargo.toml @@ -70,6 +70,10 @@ curl = { version = "0.4", optional = true, features = ["static-curl", "static-ss thiserror = "1.0.26" base64 = { version = "0.13.0", optional = true } +## If used in conjunction with `async-client`, the `connect()` method will be come available along with supporting the git protocol over TCP, +## where the TCP stream is created using this crate. +async-std = { version = "1.12.0", optional = true } + document-features = { version = "0.2.0", optional = true } [dev-dependencies] diff --git a/git-transport/src/client/async_io/connect.rs b/git-transport/src/client/async_io/connect.rs new file mode 100644 index 00000000000..8104b85773e --- /dev/null +++ b/git-transport/src/client/async_io/connect.rs @@ -0,0 +1,41 @@ +pub use crate::client::non_io_types::connect::Error; + +#[cfg(any(feature = "async-std"))] +pub(crate) mod function { + use crate::client::git; + use crate::client::non_io_types::connect::Error; + + /// A general purpose connector connecting to a repository identified by the given `url`. + /// + /// This includes connections to + /// [git daemons][crate::client::git::connect()] only at the moment. + /// + /// Use `desired_version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. + pub async fn connect( + url: &[u8], + desired_version: crate::Protocol, + ) -> Result { + let urlb = url; + let mut url = git_url::parse(urlb)?; + Ok(match url.scheme { + git_url::Scheme::Git => { + if url.user().is_some() { + return Err(Error::UnsupportedUrlTokens { + url: urlb.into(), + scheme: url.scheme, + }); + } + let path = std::mem::take(&mut url.path); + git::Connection::new_tcp( + url.host().expect("host is present in url"), + url.port, + path, + desired_version, + ) + .await + .map_err(|e| Box::new(e) as Box)? + } + scheme => return Err(Error::UnsupportedScheme(scheme)), + }) + } +} diff --git a/git-transport/src/client/async_io/mod.rs b/git-transport/src/client/async_io/mod.rs index f6cf6165a07..2b12f2da802 100644 --- a/git-transport/src/client/async_io/mod.rs +++ b/git-transport/src/client/async_io/mod.rs @@ -8,6 +8,6 @@ mod traits; pub use traits::{SetServiceResponse, Transport, TransportV2Ext}; /// -pub mod connect { - pub use crate::client::non_io_types::connect::Error; -} +pub mod connect; +#[cfg(any(feature = "async-std"))] +pub use connect::function::connect; diff --git a/git-transport/src/client/blocking_io/connect.rs b/git-transport/src/client/blocking_io/connect.rs index 48b0b3b7562..2620c6ba89e 100644 --- a/git-transport/src/client/blocking_io/connect.rs +++ b/git-transport/src/client/blocking_io/connect.rs @@ -12,7 +12,7 @@ pub(crate) mod function { /// [git daemons][crate::client::git::connect()], /// and if compiled in connections to [git repositories over https][crate::client::http::connect()]. /// - /// Use `desired_version` to set the desired protocol version to use when connecting, but not that the server may downgrade it. + /// Use `desired_version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. pub fn connect(url: &[u8], desired_version: crate::Protocol) -> Result, Error> { let urlb = url; let mut url = git_url::parse(urlb)?; diff --git a/git-transport/src/client/git/async_io.rs b/git-transport/src/client/git/async_io.rs index ce05158bad3..cad747e33a5 100644 --- a/git-transport/src/client/git/async_io.rs +++ b/git-transport/src/client/git/async_io.rs @@ -119,3 +119,37 @@ where } } } + +#[cfg(feature = "async-std")] +mod async_net { + use crate::client::git; + use crate::client::Error; + use async_std::net::TcpStream; + use std::time::Duration; + + impl git::Connection { + /// Create a new TCP connection using the `git` protocol of `desired_version`, and make a connection to `host` + /// at `port` for accessing the repository at `path` on the server side. + pub async fn new_tcp( + host: &str, + port: Option, + path: bstr::BString, + desired_version: crate::Protocol, + ) -> Result, Error> { + let read = async_std::io::timeout( + Duration::from_secs(5), + TcpStream::connect(&(host, port.unwrap_or(9418))), + ) + .await?; + let write = read.clone(); + Ok(git::Connection::new( + read, + write, + desired_version, + path, + None::<(String, _)>, + git::ConnectMode::Daemon, + )) + } + } +} diff --git a/git-transport/src/lib.rs b/git-transport/src/lib.rs index 931c918c7bf..1abf70cf6d9 100644 --- a/git-transport/src/lib.rs +++ b/git-transport/src/lib.rs @@ -55,7 +55,10 @@ impl Service { pub mod client; #[doc(inline)] -#[cfg(feature = "blocking-client")] +#[cfg(any( + feature = "blocking-client", + all(feature = "async-client", any(feature = "async-std")) +))] pub use client::connect; #[cfg(all(feature = "async-client", feature = "blocking-client"))] From bb9074140f6b79097001692c06a64ad5c76fddef Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 15 Aug 2022 20:31:23 +0800 Subject: [PATCH 061/125] use `git-transport` async-std support and connect (#450) --- Cargo.lock | 1 + gitoxide-core/Cargo.toml | 3 +- gitoxide-core/src/net.rs | 72 +--------------------------------------- 3 files changed, 4 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22d022542df..ef41f50be8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1764,6 +1764,7 @@ dependencies = [ "git-features", "git-pack", "git-repository", + "git-transport", "git-url", "itertools", "jwalk", diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index 388c5096708..9d8ce19eb5d 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -27,7 +27,7 @@ estimate-hours = ["itertools", "rayon", "fs-err"] blocking-client = ["git-repository/blocking-network-client"] ## The client to connect to git servers will be async, while supporting only the 'git' transport itself. ## It's the most limited and can be seen as example on how to use custom transports for custom servers. -async-client = ["git-repository/async-network-client", "async-trait", "futures-io", "async-net", "async-io", "futures-lite", "blocking"] +async-client = ["git-repository/async-network-client", "git-transport-configuration-only/async-std", "async-trait", "futures-io", "async-net", "async-io", "futures-lite", "blocking"] #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. @@ -38,6 +38,7 @@ serde1 = ["git-commitgraph/serde1", "git-repository/serde1", "serde_json", "serd # deselect everything else (like "performance") as this should be controllable by the parent application. git-repository = { version = "^0.20.0", path = "../git-repository", default-features = false, features = ["local", "unstable"]} # TODO: eventually 'unstable' shouldn't be needed anymore git-pack-for-configuration-only = { package = "git-pack", version = "^0.21.0", path = "../git-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static"] } +git-transport-configuration-only = { package = "git-transport", version = "^0.19.0", path = "../git-transport", default-features = false } git-commitgraph = { version = "^0.8.0", path = "../git-commitgraph" } git-config = { version = "^0.6.0", path = "../git-config" } git-features = { version = "^0.22.0", path = "../git-features" } diff --git a/gitoxide-core/src/net.rs b/gitoxide-core/src/net.rs index 25e208234a2..8177060fed4 100644 --- a/gitoxide-core/src/net.rs +++ b/gitoxide-core/src/net.rs @@ -41,76 +41,6 @@ impl Default for Protocol { Protocol::V2 } } -#[cfg(feature = "async-client")] -mod async_io { - use std::{io, time::Duration}; - use async_net::TcpStream; - use futures_lite::FutureExt; - use git_repository::{ - objs::bstr::BString, - protocol::{ - transport, - transport::{ - client, - client::{connect::Error, git}, - }, - }, - }; - - async fn git_connect( - host: &str, - path: BString, - desired_version: transport::Protocol, - port: Option, - ) -> Result, Error> { - let read = TcpStream::connect(&(host, port.unwrap_or(9418))) - .or(async { - async_io::Timer::after(Duration::from_secs(5)).await; - Err(io::ErrorKind::TimedOut.into()) - }) - .await - .map_err(|e| Box::new(e) as Box)?; - let write = read.clone(); - Ok(git::Connection::new( - read, - write, - desired_version, - path, - None::<(String, _)>, - git::ConnectMode::Daemon, - )) - } - - pub async fn connect( - url: &[u8], - desired_version: transport::Protocol, - ) -> Result { - let urlb = url; - let mut url = git_repository::url::parse(urlb)?; - Ok(match url.scheme { - git_repository::url::Scheme::Git => { - if url.user().is_some() { - return Err(Error::UnsupportedUrlTokens { - url: urlb.into(), - scheme: url.scheme, - }); - } - let path = std::mem::take(&mut url.path); - git_connect( - url.host().expect("host is present in url"), - path, - desired_version, - url.port, - ) - .await? - } - scheme => return Err(Error::UnsupportedScheme(scheme)), - }) - } -} -#[cfg(feature = "blocking-client")] +#[cfg(any(feature = "async-client", feature = "blocking-client"))] pub use git_repository::protocol::transport::connect; - -#[cfg(feature = "async-client")] -pub use self::async_io::connect; From b3362ae76b1c0ba0291412c2f96941a522860cf2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 10:16:58 +0800 Subject: [PATCH 062/125] refactor (#450) --- git-repository/src/remote/access.rs | 5 ++- git-repository/src/remote/build.rs | 2 +- git-repository/src/remote/errors.rs | 19 ------------ .../src/remote/{create.rs => init.rs} | 31 +++++++++++++++---- git-repository/src/remote/mod.rs | 6 ++-- 5 files changed, 32 insertions(+), 31 deletions(-) rename git-repository/src/remote/{create.rs => init.rs} (73%) diff --git a/git-repository/src/remote/access.rs b/git-repository/src/remote/access.rs index 9dfa3579bd2..277f9c5f6b0 100644 --- a/git-repository/src/remote/access.rs +++ b/git-repository/src/remote/access.rs @@ -41,8 +41,7 @@ impl Remote<'_> { /// This happens only once, and one if them may be changed even when reporting an error. /// If both urls fail, only the first error (for fetch urls) is reported. pub fn rewrite_urls(&mut self) -> Result<&mut Self, remote::init::Error> { - let url_err = match remote::create::rewrite_url(&self.repo.config, self.url.as_ref(), remote::Direction::Fetch) - { + let url_err = match remote::init::rewrite_url(&self.repo.config, self.url.as_ref(), remote::Direction::Fetch) { Ok(url) => { self.url_alias = url; None @@ -50,7 +49,7 @@ impl Remote<'_> { Err(err) => err.into(), }; let push_url_err = - match remote::create::rewrite_url(&self.repo.config, self.push_url.as_ref(), remote::Direction::Push) { + match remote::init::rewrite_url(&self.repo.config, self.push_url.as_ref(), remote::Direction::Push) { Ok(url) => { self.push_url_alias = url; None diff --git a/git-repository/src/remote/build.rs b/git-repository/src/remote/build.rs index 3d506d23092..08404cfaa52 100644 --- a/git-repository/src/remote/build.rs +++ b/git-repository/src/remote/build.rs @@ -34,7 +34,7 @@ impl Remote<'_> { self.push_url = push_url.into(); let (_, push_url_alias) = should_rewrite_urls - .then(|| remote::create::rewrite_urls(&self.repo.config, None, self.push_url.as_ref())) + .then(|| remote::init::rewrite_urls(&self.repo.config, None, self.push_url.as_ref())) .unwrap_or(Ok((None, None)))?; self.push_url_alias = push_url_alias; diff --git a/git-repository/src/remote/errors.rs b/git-repository/src/remote/errors.rs index 3bb78f5b22e..806a4758161 100644 --- a/git-repository/src/remote/errors.rs +++ b/git-repository/src/remote/errors.rs @@ -1,22 +1,3 @@ -/// -pub mod init { - use crate::bstr::BString; - - /// The error returned by [`Repository::remote_at(…)`][crate::Repository::remote_at()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Url(#[from] git_url::parse::Error), - #[error("The rewritten {kind} url {rewritten_url:?} failed to parse")] - RewrittenUrlInvalid { - kind: &'static str, - rewritten_url: BString, - source: git_url::parse::Error, - }, - } -} - /// pub mod find { use crate::bstr::BString; diff --git a/git-repository/src/remote/create.rs b/git-repository/src/remote/init.rs similarity index 73% rename from git-repository/src/remote/create.rs rename to git-repository/src/remote/init.rs index a09073928d1..30b114ea74c 100644 --- a/git-repository/src/remote/create.rs +++ b/git-repository/src/remote/init.rs @@ -2,6 +2,25 @@ use crate::{config, remote, Remote, Repository}; use git_refspec::RefSpec; use std::convert::TryInto; +mod error { + use crate::bstr::BString; + + /// The error returned by [`Repository::remote_at(…)`][crate::Repository::remote_at()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Url(#[from] git_url::parse::Error), + #[error("The rewritten {kind} url {rewritten_url:?} failed to parse")] + RewrittenUrlInvalid { + kind: &'static str, + rewritten_url: BString, + source: git_url::parse::Error, + }, + } +} +pub use error::Error; + /// Initialization impl<'repo> Remote<'repo> { pub(crate) fn from_preparsed_config( @@ -12,7 +31,7 @@ impl<'repo> Remote<'repo> { push_specs: Vec, should_rewrite_urls: bool, repo: &'repo Repository, - ) -> Result { + ) -> Result { debug_assert!( url.is_some() || push_url.is_some(), "BUG: fetch or push url must be set at least" @@ -36,12 +55,12 @@ impl<'repo> Remote<'repo> { url: Url, should_rewrite_urls: bool, repo: &'repo Repository, - ) -> Result + ) -> Result where Url: TryInto, git_url::parse::Error: From, { - let url = url.try_into().map_err(|err| remote::init::Error::Url(err.into()))?; + let url = url.try_into().map_err(|err| Error::Url(err.into()))?; let (url_alias, _) = should_rewrite_urls .then(|| rewrite_urls(&repo.config, Some(&url), None)) .unwrap_or(Ok((None, None)))?; @@ -62,10 +81,10 @@ pub(crate) fn rewrite_url( config: &config::Cache, url: Option<&git_url::Url>, direction: remote::Direction, -) -> Result, remote::init::Error> { +) -> Result, Error> { url.and_then(|url| config.url_rewrite().longest(url, direction)) .map(|url| { - git_url::parse(&url).map_err(|err| remote::init::Error::RewrittenUrlInvalid { + git_url::parse(&url).map_err(|err| Error::RewrittenUrlInvalid { kind: match direction { remote::Direction::Fetch => "fetch", remote::Direction::Push => "push", @@ -81,7 +100,7 @@ pub(crate) fn rewrite_urls( config: &config::Cache, url: Option<&git_url::Url>, push_url: Option<&git_url::Url>, -) -> Result<(Option, Option), remote::init::Error> { +) -> Result<(Option, Option), Error> { let url_alias = rewrite_url(config, url, remote::Direction::Fetch)?; let push_url_alias = rewrite_url(config, push_url, remote::Direction::Push)?; diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 859fa8a58d4..7efd262a24b 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -18,8 +18,10 @@ impl Direction { } mod build; -mod create; mod errors; +/// +pub mod init; + #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] pub mod connect { #![allow(missing_docs)] @@ -108,7 +110,7 @@ mod connection { #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] pub use connection::Connection; -pub use errors::{find, init}; +pub use errors::find; mod access; pub(crate) mod url; From f6506e0c463bdccbcfd9324bc312da9cc957d8e6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 10:30:33 +0800 Subject: [PATCH 063/125] change!: Use `&BStr` as input instead of `[u8]` (#450) This signals that it's indeed intended to be human readable while allowing it to be a path as well without loss, at least theoretically. After all we currently don't have a way to parse invalid UTF-8. --- git-url/src/lib.rs | 6 +++--- git-url/src/parse.rs | 13 +++++++------ git-url/tests/parse/file.rs | 3 ++- git-url/tests/parse/mod.rs | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index f9862235bc6..a5e31d5ecbe 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -178,7 +178,7 @@ impl Url { impl Url { /// Parse a URL from `bytes` - pub fn from_bytes(bytes: &[u8]) -> Result { + pub fn from_bytes(bytes: &BStr) -> Result { parse(bytes) } } @@ -187,7 +187,7 @@ impl TryFrom<&str> for Url { type Error = parse::Error; fn try_from(value: &str) -> Result { - Self::from_bytes(value.as_bytes()) + Self::from_bytes(value.into()) } } @@ -195,7 +195,7 @@ impl TryFrom for Url { type Error = parse::Error; fn try_from(value: String) -> Result { - Self::from_bytes(value.as_bytes()) + Self::from_bytes(value.as_str().into()) } } diff --git a/git-url/src/parse.rs b/git-url/src/parse.rs index 8e023f9f241..c05c5129639 100644 --- a/git-url/src/parse.rs +++ b/git-url/src/parse.rs @@ -1,8 +1,9 @@ use std::borrow::Cow; -use bstr::ByteSlice; +use bstr::{BStr, ByteSlice}; use crate::Scheme; +pub use bstr; /// The Error returned by [`parse()`] #[derive(Debug, thiserror::Error)] @@ -84,17 +85,17 @@ fn to_owned_url(url: url::Url) -> Result { /// /// We cannot and should never have to deal with UTF-16 encoded windows strings, so bytes input is acceptable. /// For file-paths, we don't expect UTF8 encoding either. -pub fn parse(bytes: &[u8]) -> Result { - let guessed_protocol = guess_protocol(bytes); - if possibly_strip_file_protocol(bytes) != bytes || (has_no_explicit_protocol(bytes) && guessed_protocol == "file") { +pub fn parse(input: &BStr) -> Result { + let guessed_protocol = guess_protocol(input); + if possibly_strip_file_protocol(input) != input || (has_no_explicit_protocol(input) && guessed_protocol == "file") { return Ok(crate::Url { scheme: Scheme::File, - path: possibly_strip_file_protocol(bytes).into(), + path: possibly_strip_file_protocol(input).into(), ..Default::default() }); } - let url_str = std::str::from_utf8(bytes)?; + let url_str = std::str::from_utf8(input)?; let mut url = match url::Url::parse(url_str) { Ok(url) => url, Err(::url::ParseError::RelativeUrlWithoutBase) => { diff --git a/git-url/tests/parse/file.rs b/git-url/tests/parse/file.rs index cc65495f076..f697e4a0712 100644 --- a/git-url/tests/parse/file.rs +++ b/git-url/tests/parse/file.rs @@ -1,3 +1,4 @@ +use bstr::ByteSlice; use git_url::Scheme; use crate::parse::{assert_url_and, assert_url_roundtrip, url}; @@ -33,7 +34,7 @@ fn no_username_expansion_for_file_paths_with_protocol() -> crate::Result { #[test] fn non_utf8_file_path_without_protocol() -> crate::Result { - let parsed = git_url::parse(b"/path/to\xff/git")?; + let parsed = git_url::parse(b"/path/to\xff/git".as_bstr())?; assert_eq!(parsed, url(Scheme::File, None, None, None, b"/path/to\xff/git",)); let url_lossless = parsed.to_bstring(); assert_eq!( diff --git a/git-url/tests/parse/mod.rs b/git-url/tests/parse/mod.rs index 0077a44ad07..47e01de55c0 100644 --- a/git-url/tests/parse/mod.rs +++ b/git-url/tests/parse/mod.rs @@ -1,7 +1,7 @@ use git_url::Scheme; fn assert_url_and(url: &str, expected: git_url::Url) -> Result { - assert_eq!(git_url::parse(url.as_bytes())?, expected); + assert_eq!(git_url::parse(url.into())?, expected); Ok(expected) } @@ -11,7 +11,7 @@ fn assert_url_roundtrip(url: &str, expected: git_url::Url) -> crate::Result { } fn assert_failure(url: &str, expected_err: &str) { - assert_eq!(git_url::parse(url.as_bytes()).unwrap_err().to_string(), expected_err); + assert_eq!(git_url::parse(url.into()).unwrap_err().to_string(), expected_err); } fn url( From 52e8c149ff17ce894cc30d03ead1988f52f0663e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 10:31:53 +0800 Subject: [PATCH 064/125] change!: `client::connect()` now takes a `&BStr` as URL (#450) --- git-transport/src/client/async_io/connect.rs | 3 ++- git-transport/src/client/blocking_io/connect.rs | 5 +++-- git-transport/src/client/blocking_io/ssh.rs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/git-transport/src/client/async_io/connect.rs b/git-transport/src/client/async_io/connect.rs index 8104b85773e..f6d12ad00b4 100644 --- a/git-transport/src/client/async_io/connect.rs +++ b/git-transport/src/client/async_io/connect.rs @@ -4,6 +4,7 @@ pub use crate::client::non_io_types::connect::Error; pub(crate) mod function { use crate::client::git; use crate::client::non_io_types::connect::Error; + use bstr::BStr; /// A general purpose connector connecting to a repository identified by the given `url`. /// @@ -12,7 +13,7 @@ pub(crate) mod function { /// /// Use `desired_version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. pub async fn connect( - url: &[u8], + url: &BStr, desired_version: crate::Protocol, ) -> Result { let urlb = url; diff --git a/git-transport/src/client/blocking_io/connect.rs b/git-transport/src/client/blocking_io/connect.rs index 2620c6ba89e..53d616c82a5 100644 --- a/git-transport/src/client/blocking_io/connect.rs +++ b/git-transport/src/client/blocking_io/connect.rs @@ -3,6 +3,7 @@ pub use crate::client::non_io_types::connect::Error; pub(crate) mod function { use crate::client::non_io_types::connect::Error; use crate::client::Transport; + use bstr::BStr; /// A general purpose connector connecting to a repository identified by the given `url`. /// @@ -13,7 +14,7 @@ pub(crate) mod function { /// and if compiled in connections to [git repositories over https][crate::client::http::connect()]. /// /// Use `desired_version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. - pub fn connect(url: &[u8], desired_version: crate::Protocol) -> Result, Error> { + pub fn connect(url: &BStr, desired_version: crate::Protocol) -> Result, Error> { let urlb = url; let mut url = git_url::parse(urlb)?; Ok(match url.scheme { @@ -63,7 +64,7 @@ pub(crate) mod function { git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), #[cfg(feature = "http-client-curl")] git_url::Scheme::Https | git_url::Scheme::Http => Box::new( - crate::client::http::connect(urlb.into(), desired_version) + crate::client::http::connect(urlb, desired_version) .map_err(|e| Box::new(e) as Box)?, ), }) diff --git a/git-transport/src/client/blocking_io/ssh.rs b/git-transport/src/client/blocking_io/ssh.rs index ea2a0477957..739e0e9e092 100644 --- a/git-transport/src/client/blocking_io/ssh.rs +++ b/git-transport/src/client/blocking_io/ssh.rs @@ -100,7 +100,7 @@ mod tests { ("ssh://host.xy/~/repo", "~/repo"), ("ssh://host.xy/~username/repo", "~username/repo"), ] { - let url = git_url::parse(url.as_bytes()).expect("valid url"); + let url = git_url::parse((*url).into()).expect("valid url"); let cmd = connect("host", url.path, Protocol::V1, None, None).expect("parse success"); assert_eq!( cmd.path, From 4ae2390578e086705c640fa74d273e1f82c9ab62 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 10:32:15 +0800 Subject: [PATCH 065/125] adapt to changes in `git-transport` and `git-url(#450) --- git-repository/src/remote/init.rs | 2 +- git-repository/src/remote/mod.rs | 2 ++ git-repository/tests/remote/mod.rs | 16 ++++++++++++++++ git-repository/tests/repository/remote.rs | 2 +- gitoxide-core/src/pack/receive.rs | 4 ++-- gitoxide-core/src/remote/refs/async_io.rs | 2 +- gitoxide-core/src/remote/refs/blocking_io.rs | 2 +- 7 files changed, 24 insertions(+), 6 deletions(-) diff --git a/git-repository/src/remote/init.rs b/git-repository/src/remote/init.rs index 30b114ea74c..6ad08423382 100644 --- a/git-repository/src/remote/init.rs +++ b/git-repository/src/remote/init.rs @@ -84,7 +84,7 @@ pub(crate) fn rewrite_url( ) -> Result, Error> { url.and_then(|url| config.url_rewrite().longest(url, direction)) .map(|url| { - git_url::parse(&url).map_err(|err| Error::RewrittenUrlInvalid { + git_url::parse(url.as_ref()).map_err(|err| Error::RewrittenUrlInvalid { kind: match direction { remote::Direction::Fetch => "fetch", remote::Direction::Push => "push", diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 7efd262a24b..5ab2c2754b0 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -27,6 +27,7 @@ pub mod connect { #![allow(missing_docs)] use crate::remote::Connection; use crate::{remote, Remote}; + // use git_protocol::transport; use git_protocol::transport::client::Transport; mod error { @@ -45,6 +46,7 @@ pub mod connect { } pub use error::Error; + /// Establishing connections to remote hosts impl<'repo> Remote<'repo> { /// Create a new connection into `direction` using `transport` to communicate. /// diff --git a/git-repository/tests/remote/mod.rs b/git-repository/tests/remote/mod.rs index 032c3fd683d..2dd89393fdc 100644 --- a/git-repository/tests/remote/mod.rs +++ b/git-repository/tests/remote/mod.rs @@ -10,3 +10,19 @@ pub(crate) fn repo(name: &str) -> git::Repository { pub(crate) fn cow_str(s: &str) -> Cow { Cow::Borrowed(s) } + +mod connect { + #[cfg(feature = "blocking-network-client")] + mod blocking { + use crate::remote; + use git_repository::remote::Direction::Fetch; + + #[test] + #[ignore] + fn ls_refs() { + let repo = remote::repo("clone"); + let remote = repo.find_remote("origin").unwrap(); + let _connection = remote.connect(Fetch).unwrap(); + } + } +} diff --git a/git-repository/tests/repository/remote.rs b/git-repository/tests/repository/remote.rs index 463c248a842..a0c8a8bb8c0 100644 --- a/git-repository/tests/repository/remote.rs +++ b/git-repository/tests/repository/remote.rs @@ -121,7 +121,7 @@ mod find_remote { let remote = repo.find_remote(name).expect("no error"); assert_eq!(remote.name(), Some(name)); - let url = git::url::parse(url.as_bytes()).expect("valid"); + let url = git::url::parse(url.into()).expect("valid"); assert_eq!(remote.url(Direction::Fetch).unwrap(), &url); assert_eq!( diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 88cf827d1bc..99423e00b6b 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -151,7 +151,7 @@ mod blocking_io { progress: P, ctx: Context, ) -> anyhow::Result<()> { - let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into())?; + let transport = net::connect(url.into(), protocol.unwrap_or_default().into())?; let delegate = CloneDelegate { ctx, directory, @@ -219,7 +219,7 @@ mod async_io { progress: P, ctx: Context, ) -> anyhow::Result<()> { - let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into()).await?; + let transport = net::connect(url.into(), protocol.unwrap_or_default().into()).await?; let mut delegate = CloneDelegate { ctx, directory, diff --git a/gitoxide-core/src/remote/refs/async_io.rs b/gitoxide-core/src/remote/refs/async_io.rs index 45b3ce67e61..b5bdd13e173 100644 --- a/gitoxide-core/src/remote/refs/async_io.rs +++ b/gitoxide-core/src/remote/refs/async_io.rs @@ -31,7 +31,7 @@ pub async fn list( ctx: Context, ) -> anyhow::Result<()> { let url = url.to_owned(); - let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into()).await?; + let transport = net::connect(url.as_str().into(), protocol.unwrap_or_default().into()).await?; blocking::unblock( // `blocking` really needs a way to unblock futures, which is what it does internally anyway. // Both fetch() needs unblocking as it executes blocking code within the future, and the other diff --git a/gitoxide-core/src/remote/refs/blocking_io.rs b/gitoxide-core/src/remote/refs/blocking_io.rs index a7e24c0393a..1e6e3578cf2 100644 --- a/gitoxide-core/src/remote/refs/blocking_io.rs +++ b/gitoxide-core/src/remote/refs/blocking_io.rs @@ -29,7 +29,7 @@ pub fn list( progress: impl Progress, ctx: Context, ) -> anyhow::Result<()> { - let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into())?; + let transport = net::connect(url.into(), protocol.unwrap_or_default().into())?; let mut delegate = LsRemotes::default(); protocol::fetch( transport, From 9509ce4faeca8b4e1527bac625370403495bb03c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 10:59:42 +0800 Subject: [PATCH 066/125] change!: `client::connect()` supports anything that parses into a `git_url::Url`; turn http url back to &str (#450) The http url is always valid UTF-8 and doesn't contain invalid paths, thus we should have the type system reflect that. --- git-transport/src/client/async_io/connect.rs | 12 ++++++---- .../src/client/blocking_io/connect.rs | 17 ++++++++------ git-transport/src/client/blocking_io/file.rs | 4 ++-- .../src/client/blocking_io/http/curl/mod.rs | 7 +++--- .../client/blocking_io/http/curl/remote.rs | 5 ++-- .../src/client/blocking_io/http/mod.rs | 23 ++++++++----------- .../src/client/blocking_io/http/traits.rs | 6 ++--- git-transport/src/client/git/async_io.rs | 10 ++++---- git-transport/src/client/git/blocking_io.rs | 9 ++++---- git-transport/src/client/git/mod.rs | 4 ++-- git-transport/src/client/traits.rs | 7 +++--- 11 files changed, 51 insertions(+), 53 deletions(-) diff --git a/git-transport/src/client/async_io/connect.rs b/git-transport/src/client/async_io/connect.rs index f6d12ad00b4..ba863646ee9 100644 --- a/git-transport/src/client/async_io/connect.rs +++ b/git-transport/src/client/async_io/connect.rs @@ -5,6 +5,7 @@ pub(crate) mod function { use crate::client::git; use crate::client::non_io_types::connect::Error; use bstr::BStr; + use std::convert::TryInto; /// A general purpose connector connecting to a repository identified by the given `url`. /// @@ -12,12 +13,15 @@ pub(crate) mod function { /// [git daemons][crate::client::git::connect()] only at the moment. /// /// Use `desired_version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. - pub async fn connect( + pub async fn connect( url: &BStr, desired_version: crate::Protocol, - ) -> Result { - let urlb = url; - let mut url = git_url::parse(urlb)?; + ) -> Result + where + Url: TryInto, + git_url::parse::Error: From, + { + let mut url = url.try_into().map_err(git_url::parse::Error::from)?; Ok(match url.scheme { git_url::Scheme::Git => { if url.user().is_some() { diff --git a/git-transport/src/client/blocking_io/connect.rs b/git-transport/src/client/blocking_io/connect.rs index 53d616c82a5..13522d77476 100644 --- a/git-transport/src/client/blocking_io/connect.rs +++ b/git-transport/src/client/blocking_io/connect.rs @@ -3,7 +3,7 @@ pub use crate::client::non_io_types::connect::Error; pub(crate) mod function { use crate::client::non_io_types::connect::Error; use crate::client::Transport; - use bstr::BStr; + use std::convert::TryInto; /// A general purpose connector connecting to a repository identified by the given `url`. /// @@ -14,15 +14,18 @@ pub(crate) mod function { /// and if compiled in connections to [git repositories over https][crate::client::http::connect()]. /// /// Use `desired_version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. - pub fn connect(url: &BStr, desired_version: crate::Protocol) -> Result, Error> { - let urlb = url; - let mut url = git_url::parse(urlb)?; + pub fn connect(url: Url, desired_version: crate::Protocol) -> Result, Error> + where + Url: TryInto, + git_url::parse::Error: From, + { + let mut url = url.try_into().map_err(git_url::parse::Error::from)?; Ok(match url.scheme { git_url::Scheme::Radicle => return Err(Error::UnsupportedScheme(url.scheme)), git_url::Scheme::File => { if url.user().is_some() || url.host().is_some() || url.port.is_some() { return Err(Error::UnsupportedUrlTokens { - url: urlb.into(), + url: url.to_bstring(), scheme: url.scheme, }); } @@ -45,7 +48,7 @@ pub(crate) mod function { git_url::Scheme::Git => { if url.user().is_some() { return Err(Error::UnsupportedUrlTokens { - url: urlb.into(), + url: url.to_bstring(), scheme: url.scheme, }); } @@ -64,7 +67,7 @@ pub(crate) mod function { git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), #[cfg(feature = "http-client-curl")] git_url::Scheme::Https | git_url::Scheme::Http => Box::new( - crate::client::http::connect(urlb, desired_version) + crate::client::http::connect(&url.to_bstring().to_string(), desired_version) .map_err(|e| Box::new(e) as Box)?, ), }) diff --git a/git-transport/src/client/blocking_io/file.rs b/git-transport/src/client/blocking_io/file.rs index 4e2f0062439..0e23075acd2 100644 --- a/git-transport/src/client/blocking_io/file.rs +++ b/git-transport/src/client/blocking_io/file.rs @@ -94,8 +94,8 @@ impl client::TransportWithoutIO for SpawnProcessOnDemand { .request(write_mode, on_into_read) } - fn to_url(&self) -> BString { - self.url.to_bstring() + fn to_url(&self) -> String { + self.url.to_bstring().to_string() } fn connection_persists_across_multiple_requests(&self) -> bool { diff --git a/git-transport/src/client/blocking_io/http/curl/mod.rs b/git-transport/src/client/blocking_io/http/curl/mod.rs index 3fba53582da..06085bf5d68 100644 --- a/git-transport/src/client/blocking_io/http/curl/mod.rs +++ b/git-transport/src/client/blocking_io/http/curl/mod.rs @@ -1,4 +1,3 @@ -use bstr::BStr; use std::{ sync::mpsc::{Receiver, SyncSender}, thread, @@ -34,7 +33,7 @@ impl Curl { fn make_request( &mut self, - url: &BStr, + url: &str, headers: impl IntoIterator>, upload: bool, ) -> Result, http::Error> { @@ -88,7 +87,7 @@ impl crate::client::http::Http for Curl { fn get( &mut self, - url: &BStr, + url: &str, headers: impl IntoIterator>, ) -> Result, http::Error> { self.make_request(url, headers, false).map(Into::into) @@ -96,7 +95,7 @@ impl crate::client::http::Http for Curl { fn post( &mut self, - url: &BStr, + url: &str, headers: impl IntoIterator>, ) -> Result, http::Error> { self.make_request(url, headers, true) diff --git a/git-transport/src/client/blocking_io/http/curl/remote.rs b/git-transport/src/client/blocking_io/http/curl/remote.rs index 3d08357e259..eac4d8414b4 100644 --- a/git-transport/src/client/blocking_io/http/curl/remote.rs +++ b/git-transport/src/client/blocking_io/http/curl/remote.rs @@ -1,4 +1,3 @@ -use bstr::{BString, ByteSlice}; use std::{ io, io::{Read, Write}, @@ -90,7 +89,7 @@ impl curl::easy::Handler for Handler { } pub struct Request { - pub url: BString, + pub url: String, pub headers: curl::easy::List, pub upload: bool, } @@ -112,7 +111,7 @@ pub fn new() -> ( let mut handle = Easy2::new(Handler::default()); for Request { url, headers, upload } in req_recv { - handle.url(&url.to_str_lossy())?; + handle.url(&url)?; // GitHub sends 'chunked' to avoid unknown clients to choke on the data, I suppose handle.post(upload)?; diff --git a/git-transport/src/client/blocking_io/http/mod.rs b/git-transport/src/client/blocking_io/http/mod.rs index c9833f99064..8c6b5269fdf 100644 --- a/git-transport/src/client/blocking_io/http/mod.rs +++ b/git-transport/src/client/blocking_io/http/mod.rs @@ -1,4 +1,3 @@ -use bstr::{BStr, BString}; use std::{ borrow::Cow, convert::Infallible, @@ -25,7 +24,7 @@ pub type Impl = curl::Curl; /// A transport for supporting arbitrary http clients by abstracting interactions with them into the [Http] trait. pub struct Transport { - url: BString, + url: String, user_agent_header: &'static str, desired_version: Protocol, supported_versions: [Protocol; 1], @@ -38,7 +37,7 @@ pub struct Transport { impl Transport { /// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol. - pub fn new(url: &BStr, desired_version: Protocol) -> Self { + pub fn new(url: &str, desired_version: Protocol) -> Self { Transport { url: url.to_owned(), user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")), @@ -90,12 +89,12 @@ impl Transport { } } -fn append_url(base: &BStr, suffix: &str) -> BString { +fn append_url(base: &str, suffix: &str) -> String { let mut buf = base.to_owned(); - if base.last() != Some(&b'/') { - buf.push(b'/'); + if base.as_bytes().last() != Some(&b'/') { + buf.push('/'); } - buf.extend_from_slice(suffix.as_bytes()); + buf.push_str(suffix); buf } @@ -111,7 +110,7 @@ impl client::TransportWithoutIO for Transport { on_into_read: MessageKind, ) -> Result, client::Error> { let service = self.service.expect("handshake() must have been called first"); - let url = append_url(self.url.as_ref(), service.as_str()); + let url = append_url(&self.url, service.as_str()); let static_headers = &[ Cow::Borrowed(self.user_agent_header), Cow::Owned(format!("Content-Type: application/x-{}-request", service.as_str())), @@ -131,9 +130,7 @@ impl client::TransportWithoutIO for Transport { headers, body, post_body, - } = self - .http - .post(url.as_ref(), static_headers.iter().chain(&dynamic_headers))?; + } = self.http.post(&url, static_headers.iter().chain(&dynamic_headers))?; let line_provider = self .line_provider .as_mut() @@ -151,7 +148,7 @@ impl client::TransportWithoutIO for Transport { )) } - fn to_url(&self) -> BString { + fn to_url(&self) -> String { self.url.clone() } @@ -289,6 +286,6 @@ impl ExtendedBufRead for HeadersThenBody Result, Infallible> { +pub fn connect(url: &str, desired_version: Protocol) -> Result, Infallible> { Ok(Transport::new(url, desired_version)) } diff --git a/git-transport/src/client/blocking_io/http/traits.rs b/git-transport/src/client/blocking_io/http/traits.rs index 06f48db7bda..be39502dab3 100644 --- a/git-transport/src/client/blocking_io/http/traits.rs +++ b/git-transport/src/client/blocking_io/http/traits.rs @@ -1,5 +1,3 @@ -use bstr::BStr; - /// The error used by the [Http] trait. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] @@ -54,7 +52,7 @@ pub trait Http { /// The `headers` are provided verbatim and include both the key as well as the value. fn get( &mut self, - url: &BStr, + url: &str, headers: impl IntoIterator>, ) -> Result, Error>; @@ -66,7 +64,7 @@ pub trait Http { /// to prevent deadlocks. fn post( &mut self, - url: &BStr, + url: &str, headers: impl IntoIterator>, ) -> Result, Error>; } diff --git a/git-transport/src/client/git/async_io.rs b/git-transport/src/client/git/async_io.rs index cad747e33a5..717ec39aefc 100644 --- a/git-transport/src/client/git/async_io.rs +++ b/git-transport/src/client/git/async_io.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use bstr::{BString, ByteVec}; +use bstr::BString; use futures_io::{AsyncRead, AsyncWrite}; use futures_lite::AsyncWriteExt; use git_packetline::PacketLineRef; @@ -26,12 +26,12 @@ where on_into_read, )) } - fn to_url(&self) -> BString { + fn to_url(&self) -> String { self.custom_url.as_ref().map_or_else( || { - let mut buf: BString = "file://".into(); - buf.push_str(&self.path); - buf + let mut possibly_lossy_url = self.path.to_string(); + possibly_lossy_url.insert_str(0, "file://"); + possibly_lossy_url }, |url| url.clone(), ) diff --git a/git-transport/src/client/git/blocking_io.rs b/git-transport/src/client/git/blocking_io.rs index bc1dddff9f3..240f00c9903 100644 --- a/git-transport/src/client/git/blocking_io.rs +++ b/git-transport/src/client/git/blocking_io.rs @@ -26,13 +26,12 @@ where )) } - fn to_url(&self) -> BString { + fn to_url(&self) -> String { self.custom_url.as_ref().map_or_else( || { - use bstr::ByteVec; - let mut buf: BString = "file://".into(); - buf.push_str(&self.path); - buf + let mut possibly_lossy_url = self.path.to_string(); + possibly_lossy_url.insert_str(0, "file://"); + possibly_lossy_url }, |url| url.clone(), ) diff --git a/git-transport/src/client/git/mod.rs b/git-transport/src/client/git/mod.rs index 3f549e34b3d..2aaa6f42210 100644 --- a/git-transport/src/client/git/mod.rs +++ b/git-transport/src/client/git/mod.rs @@ -22,7 +22,7 @@ pub struct Connection { pub(in crate::client) virtual_host: Option<(String, Option)>, pub(in crate::client) desired_version: Protocol, supported_versions: [Protocol; 1], - custom_url: Option, + custom_url: Option, pub(in crate::client) mode: ConnectMode, } @@ -37,7 +37,7 @@ impl Connection { /// The URL is required as parameter for authentication helpers which are called in transports /// that support authentication. Even though plain git transports don't support that, this /// may well be the case in custom transports. - pub fn custom_url(mut self, url: Option) -> Self { + pub fn custom_url(mut self, url: Option) -> Self { self.custom_url = url; self } diff --git a/git-transport/src/client/traits.rs b/git-transport/src/client/traits.rs index bd3fa79f08e..f57b2fe2d09 100644 --- a/git-transport/src/client/traits.rs +++ b/git-transport/src/client/traits.rs @@ -1,4 +1,3 @@ -use bstr::BString; use std::ops::{Deref, DerefMut}; #[cfg(any(feature = "blocking-client", feature = "async-client"))] @@ -27,7 +26,7 @@ pub trait TransportWithoutIO { /// Returns the canonical URL pointing to the destination of this transport. /// Please note that local paths may not be represented correctly, as they will go through a potentially lossy /// unicode conversion. - fn to_url(&self) -> BString; + fn to_url(&self) -> String; /// If the actually advertised server version is contained in the returned slice or empty, continue as normal, /// assume the server's protocol version is desired or acceptable. @@ -60,7 +59,7 @@ impl TransportWithoutIO for Box { self.deref_mut().request(write_mode, on_into_read) } - fn to_url(&self) -> BString { + fn to_url(&self) -> String { self.deref().to_url() } @@ -83,7 +82,7 @@ impl TransportWithoutIO for &mut T { self.deref_mut().request(write_mode, on_into_read) } - fn to_url(&self) -> BString { + fn to_url(&self) -> String { self.deref().to_url() } From 2141585b1752a15a933a42e3f977142b4dea80fd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 11:00:57 +0800 Subject: [PATCH 067/125] adjust to changes in `git-transport` (#450) --- git-protocol/src/fetch/tests/arguments.rs | 6 ++---- git-protocol/src/fetch_fn.rs | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/git-protocol/src/fetch/tests/arguments.rs b/git-protocol/src/fetch/tests/arguments.rs index 6807d6d1f34..76c8bd0acaf 100644 --- a/git-protocol/src/fetch/tests/arguments.rs +++ b/git-protocol/src/fetch/tests/arguments.rs @@ -18,7 +18,6 @@ struct Transport { #[cfg(feature = "blocking-client")] mod impls { - use bstr::BString; use git_transport::{ client, client::{Error, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, @@ -36,7 +35,7 @@ mod impls { self.inner.request(write_mode, on_into_read) } - fn to_url(&self) -> BString { + fn to_url(&self) -> String { self.inner.to_url() } @@ -63,7 +62,6 @@ mod impls { #[cfg(feature = "async-client")] mod impls { use async_trait::async_trait; - use bstr::BString; use git_transport::{ client, client::{Error, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, @@ -80,7 +78,7 @@ mod impls { self.inner.request(write_mode, on_into_read) } - fn to_url(&self) -> BString { + fn to_url(&self) -> String { self.inner.to_url() } diff --git a/git-protocol/src/fetch_fn.rs b/git-protocol/src/fetch_fn.rs index cf128b9854e..eea1d0ea6fe 100644 --- a/git-protocol/src/fetch_fn.rs +++ b/git-protocol/src/fetch_fn.rs @@ -86,7 +86,8 @@ where let url = transport.to_url(); progress.set_name("authentication"); let credentials::helper::Outcome { identity, next } = - authenticate(credentials::helper::Action::Fill(url.as_ref()))?.expect("FILL provides an identity"); + authenticate(credentials::helper::Action::Fill(url.as_str().into()))? + .expect("FILL provides an identity"); transport.set_identity(identity)?; progress.step(); progress.set_name("handshake (authenticated)"); From eb15a7971f864bfe02073a21d545ce9ac3dc58e5 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 11:02:59 +0800 Subject: [PATCH 068/125] fix async `connect()` to match new signature (#450) --- git-transport/src/client/async_io/connect.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/git-transport/src/client/async_io/connect.rs b/git-transport/src/client/async_io/connect.rs index ba863646ee9..0bf3b196b33 100644 --- a/git-transport/src/client/async_io/connect.rs +++ b/git-transport/src/client/async_io/connect.rs @@ -4,7 +4,6 @@ pub use crate::client::non_io_types::connect::Error; pub(crate) mod function { use crate::client::git; use crate::client::non_io_types::connect::Error; - use bstr::BStr; use std::convert::TryInto; /// A general purpose connector connecting to a repository identified by the given `url`. @@ -14,7 +13,7 @@ pub(crate) mod function { /// /// Use `desired_version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. pub async fn connect( - url: &BStr, + url: Url, desired_version: crate::Protocol, ) -> Result where @@ -26,7 +25,7 @@ pub(crate) mod function { git_url::Scheme::Git => { if url.user().is_some() { return Err(Error::UnsupportedUrlTokens { - url: urlb.into(), + url: url.to_bstring(), scheme: url.scheme, }); } From 94a623b57e8fa2876731f74224b406ac56838edc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 11:03:47 +0800 Subject: [PATCH 069/125] adjust to changes in `git-transport` (#450) --- git-transport/tests/client/blocking_io/http/mock.rs | 2 +- gitoxide-core/src/pack/receive.rs | 4 ++-- gitoxide-core/src/remote/refs/async_io.rs | 2 +- gitoxide-core/src/remote/refs/blocking_io.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/git-transport/tests/client/blocking_io/http/mock.rs b/git-transport/tests/client/blocking_io/http/mock.rs index a4fb784c7fa..49c74ee5e1a 100644 --- a/git-transport/tests/client/blocking_io/http/mock.rs +++ b/git-transport/tests/client/blocking_io/http/mock.rs @@ -104,7 +104,7 @@ pub fn serve_and_connect( &server.addr.port(), path ); - let client = git_transport::client::http::connect(url.as_str().into(), version)?; + let client = git_transport::client::http::connect(&url, version)?; assert_eq!(url, client.to_url()); Ok((server, client)) } diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 99423e00b6b..f194eccdd08 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -151,7 +151,7 @@ mod blocking_io { progress: P, ctx: Context, ) -> anyhow::Result<()> { - let transport = net::connect(url.into(), protocol.unwrap_or_default().into())?; + let transport = net::connect(url, protocol.unwrap_or_default().into())?; let delegate = CloneDelegate { ctx, directory, @@ -219,7 +219,7 @@ mod async_io { progress: P, ctx: Context, ) -> anyhow::Result<()> { - let transport = net::connect(url.into(), protocol.unwrap_or_default().into()).await?; + let transport = net::connect(url.to_string(), protocol.unwrap_or_default().into()).await?; let mut delegate = CloneDelegate { ctx, directory, diff --git a/gitoxide-core/src/remote/refs/async_io.rs b/gitoxide-core/src/remote/refs/async_io.rs index b5bdd13e173..18241c21d0e 100644 --- a/gitoxide-core/src/remote/refs/async_io.rs +++ b/gitoxide-core/src/remote/refs/async_io.rs @@ -31,7 +31,7 @@ pub async fn list( ctx: Context, ) -> anyhow::Result<()> { let url = url.to_owned(); - let transport = net::connect(url.as_str().into(), protocol.unwrap_or_default().into()).await?; + let transport = net::connect(url, protocol.unwrap_or_default().into()).await?; blocking::unblock( // `blocking` really needs a way to unblock futures, which is what it does internally anyway. // Both fetch() needs unblocking as it executes blocking code within the future, and the other diff --git a/gitoxide-core/src/remote/refs/blocking_io.rs b/gitoxide-core/src/remote/refs/blocking_io.rs index 1e6e3578cf2..a2e6d82469c 100644 --- a/gitoxide-core/src/remote/refs/blocking_io.rs +++ b/gitoxide-core/src/remote/refs/blocking_io.rs @@ -29,7 +29,7 @@ pub fn list( progress: impl Progress, ctx: Context, ) -> anyhow::Result<()> { - let transport = net::connect(url.into(), protocol.unwrap_or_default().into())?; + let transport = net::connect(url, protocol.unwrap_or_default().into())?; let mut delegate = LsRemotes::default(); protocol::fetch( transport, From ad101ef973afe559e71de78152a6a25b310d28dd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 16:02:34 +0800 Subject: [PATCH 070/125] refactor (#450) --- git-repository/src/remote/connect.rs | 71 ++++++++++++++++++++++++++ git-repository/src/remote/mod.rs | 74 +--------------------------- 2 files changed, 72 insertions(+), 73 deletions(-) create mode 100644 git-repository/src/remote/connect.rs diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs new file mode 100644 index 00000000000..e05efc3d668 --- /dev/null +++ b/git-repository/src/remote/connect.rs @@ -0,0 +1,71 @@ +#![allow(missing_docs)] + +use crate::remote::Connection; +use crate::{remote, Remote}; +// use git_protocol::transport; +use git_protocol::transport::client::Transport; + +mod error { + use crate::bstr::BString; + use crate::remote; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Connect(#[from] git_protocol::transport::client::connect::Error), + #[error("The {} url was missing - don't know where to establish a connection to", direction.as_str())] + MissingUrl { direction: remote::Direction }, + #[error("Protocol named {given:?} is not a valid protocol. Choose between 1 and 2")] + UnknownProtocol { given: BString }, + } +} + +pub use error::Error; + +/// Establishing connections to remote hosts +impl<'repo> Remote<'repo> { + /// Create a new connection into `direction` using `transport` to communicate. + /// + /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()]. + /// It's meant to be used when async operation is needed with runtimes of the user's choice. + pub fn into_connection(self, transport: T, direction: remote::Direction) -> Connection<'repo, T> + where + T: Transport, + { + Connection { + remote: self, + direction, + transport, + } + } + + /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. + #[cfg(feature = "blocking-network-client")] + pub fn connect(&self, direction: remote::Direction) -> Result>, Error> { + use git_protocol::transport::Protocol; + let _protocol = self + .repo + .config + .resolved + .integer("protocol", None, "version") + .unwrap_or(Ok(2)) + .map_err(|err| Error::UnknownProtocol { given: err.input }) + .and_then(|num| { + Ok(match num { + 1 => Protocol::V1, + 2 => Protocol::V2, + num => { + return Err(Error::UnknownProtocol { + given: num.to_string().into(), + }) + } + }) + })?; + let _url = self.url(direction).ok_or(Error::MissingUrl { direction })?; + todo!() + // transport::connect( + // url , + // protocol, + // ) + } +} diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 5ab2c2754b0..2abc27bdc2d 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -23,80 +23,8 @@ mod errors; pub mod init; #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] -pub mod connect { - #![allow(missing_docs)] - use crate::remote::Connection; - use crate::{remote, Remote}; - // use git_protocol::transport; - use git_protocol::transport::client::Transport; +pub mod connect; - mod error { - use crate::bstr::BString; - use crate::remote; - - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - Connect(#[from] git_protocol::transport::client::connect::Error), - #[error("The {} url was missing - don't know where to establish a connection to", direction.as_str())] - MissingUrl { direction: remote::Direction }, - #[error("Protocol named {given:?} is not a valid protocol. Choose between 1 and 2")] - UnknownProtocol { given: BString }, - } - } - pub use error::Error; - - /// Establishing connections to remote hosts - impl<'repo> Remote<'repo> { - /// Create a new connection into `direction` using `transport` to communicate. - /// - /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()]. - /// It's meant to be used when async operation is needed with runtimes of the user's choice. - pub fn into_connection(self, transport: T, direction: remote::Direction) -> Connection<'repo, T> - where - T: Transport, - { - Connection { - remote: self, - direction, - transport, - } - } - - /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. - #[cfg(feature = "blocking-network-client")] - pub fn connect( - &self, - direction: remote::Direction, - ) -> Result>, Error> { - use git_protocol::transport::Protocol; - let _protocol = self - .repo - .config - .resolved - .integer("protocol", None, "version") - .unwrap_or(Ok(2)) - .map_err(|err| Error::UnknownProtocol { given: err.input }) - .and_then(|num| { - Ok(match num { - 1 => Protocol::V1, - 2 => Protocol::V2, - num => { - return Err(Error::UnknownProtocol { - given: num.to_string().into(), - }) - } - }) - })?; - let _url = self.url(direction).ok_or(Error::MissingUrl { direction })?; - todo!() - // transport::connect( - // url , - // protocol, - // ) - } - } -} #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] mod connection { #![allow(missing_docs, dead_code)] From e55b43ef72bb3f23655c7e0884b8efcf2496f944 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 16:23:35 +0800 Subject: [PATCH 071/125] A first sketch on how connections could be working (#450) I see that `into_connection()` is just the underlying layer, and that convenience methods will be added on `Remote` at some point to do fetches, pushes and `ls-refs` possibly with less boilerplate (but also with less control). --- git-protocol/src/fetch_fn.rs | 4 ++-- git-repository/src/remote/connect.rs | 21 +++++++++++---------- git-repository/tests/remote/connect.rs | 12 ++++++++++++ git-repository/tests/remote/mod.rs | 16 +--------------- git-url/src/parse.rs | 7 +++++++ 5 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 git-repository/tests/remote/connect.rs diff --git a/git-protocol/src/fetch_fn.rs b/git-protocol/src/fetch_fn.rs index eea1d0ea6fe..1dd49a10cc9 100644 --- a/git-protocol/src/fetch_fn.rs +++ b/git-protocol/src/fetch_fn.rs @@ -21,10 +21,10 @@ pub enum FetchConnection { /// When indicating the end-of-fetch, this flag is only relevant in protocol V2. /// Generally it only applies when using persistent transports. /// - /// In most explicit client side failures modes the end-of-operation' notification will be sent to the server automatically. + /// In most explicit client side failure modes the end-of-operation' notification will be sent to the server automatically. TerminateOnSuccessfulCompletion, - /// Indicate that persistent transport connections can be reused by not sending an 'end-of-operation' notification to the server. + /// Indicate that persistent transport connections can be reused by _not_ sending an 'end-of-operation' notification to the server. /// This is useful if multiple `fetch(…)` calls are used in succession. /// /// Note that this has no effect in case of non-persistent connections, like the ones over HTTP. diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index e05efc3d668..0703a0190f8 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -2,7 +2,7 @@ use crate::remote::Connection; use crate::{remote, Remote}; -// use git_protocol::transport; +use git_protocol::transport; use git_protocol::transport::client::Transport; mod error { @@ -28,7 +28,7 @@ impl<'repo> Remote<'repo> { /// /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()]. /// It's meant to be used when async operation is needed with runtimes of the user's choice. - pub fn into_connection(self, transport: T, direction: remote::Direction) -> Connection<'repo, T> + pub fn into_connection_with_transport(self, transport: T, direction: remote::Direction) -> Connection<'repo, T> where T: Transport, { @@ -41,9 +41,12 @@ impl<'repo> Remote<'repo> { /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. #[cfg(feature = "blocking-network-client")] - pub fn connect(&self, direction: remote::Direction) -> Result>, Error> { + pub fn into_connection( + self, + direction: remote::Direction, + ) -> Result>, Error> { use git_protocol::transport::Protocol; - let _protocol = self + let protocol = self .repo .config .resolved @@ -61,11 +64,9 @@ impl<'repo> Remote<'repo> { } }) })?; - let _url = self.url(direction).ok_or(Error::MissingUrl { direction })?; - todo!() - // transport::connect( - // url , - // protocol, - // ) + + let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); + let transport = transport::connect(url, protocol)?; + Ok(self.into_connection_with_transport(transport, direction)) } } diff --git a/git-repository/tests/remote/connect.rs b/git-repository/tests/remote/connect.rs new file mode 100644 index 00000000000..9c483cd41f4 --- /dev/null +++ b/git-repository/tests/remote/connect.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "blocking-network-client")] +mod blocking { + use crate::remote; + use git_repository::remote::Direction::Fetch; + + #[test] + fn ls_refs() { + let repo = remote::repo("clone"); + let remote = repo.find_remote("origin").unwrap(); + let _connection = remote.into_connection(Fetch).unwrap(); + } +} diff --git a/git-repository/tests/remote/mod.rs b/git-repository/tests/remote/mod.rs index 2dd89393fdc..a7ad9428031 100644 --- a/git-repository/tests/remote/mod.rs +++ b/git-repository/tests/remote/mod.rs @@ -11,18 +11,4 @@ pub(crate) fn cow_str(s: &str) -> Cow { Cow::Borrowed(s) } -mod connect { - #[cfg(feature = "blocking-network-client")] - mod blocking { - use crate::remote; - use git_repository::remote::Direction::Fetch; - - #[test] - #[ignore] - fn ls_refs() { - let repo = remote::repo("clone"); - let remote = repo.find_remote("origin").unwrap(); - let _connection = remote.connect(Fetch).unwrap(); - } - } -} +mod connect; diff --git a/git-url/src/parse.rs b/git-url/src/parse.rs index c05c5129639..5f0454adf23 100644 --- a/git-url/src/parse.rs +++ b/git-url/src/parse.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::convert::Infallible; use bstr::{BStr, ByteSlice}; @@ -21,6 +22,12 @@ pub enum Error { RelativeUrl { url: String }, } +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!("Cannot actually happen, but it seems there can't be a blanket impl for this") + } +} + fn str_to_protocol(s: &str) -> Result { Ok(match s { "ssh" => Scheme::Ssh, From 73cb41cf0cc0785c87319b25c72b8b5552f81666 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 16:42:07 +0800 Subject: [PATCH 072/125] Rough sketch of the `Connection` API to list references (#450) --- git-repository/src/remote/connect.rs | 7 ++-- git-repository/src/remote/connection.rs | 47 +++++++++++++++++++++++++ git-repository/src/remote/mod.rs | 17 +++------ 3 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 git-repository/src/remote/connection.rs diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 0703a0190f8..bbd1bb9b029 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -24,17 +24,16 @@ pub use error::Error; /// Establishing connections to remote hosts impl<'repo> Remote<'repo> { - /// Create a new connection into `direction` using `transport` to communicate. + /// Create a new connection using `transport` to communicate. /// /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()]. /// It's meant to be used when async operation is needed with runtimes of the user's choice. - pub fn into_connection_with_transport(self, transport: T, direction: remote::Direction) -> Connection<'repo, T> + pub fn into_connection_with_transport(self, transport: T) -> Connection<'repo, T> where T: Transport, { Connection { remote: self, - direction, transport, } } @@ -67,6 +66,6 @@ impl<'repo> Remote<'repo> { let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); let transport = transport::connect(url, protocol)?; - Ok(self.into_connection_with_transport(transport, direction)) + Ok(self.into_connection_with_transport(transport)) } } diff --git a/git-repository/src/remote/connection.rs b/git-repository/src/remote/connection.rs new file mode 100644 index 00000000000..edec7940138 --- /dev/null +++ b/git-repository/src/remote/connection.rs @@ -0,0 +1,47 @@ +#![allow(missing_docs, dead_code)] + +use crate::Remote; + +pub struct Connection<'repo, T> { + pub(crate) remote: Remote<'repo>, + pub(crate) transport: T, +} + +mod access { + use crate::remote::Connection; + use crate::Remote; + + /// Conversion + impl<'repo, T> Connection<'repo, T> { + /// Dissolve this instance into its parts, `(Remote, Transport)`, the inverse of + /// [`into_connection_with_transport()`][Remote::into_connection_with_transport()]. + pub fn into_parts(self) -> (Remote<'repo>, T) { + (self.remote, self.transport) + } + + /// Drop the transport and additional state to regain the original remote. + pub fn into_remote(self) -> Remote<'repo> { + self.remote + } + } +} + +mod refs { + use crate::remote::Connection; + use git_protocol::transport::client::Transport; + + impl<'repo, T> Connection<'repo, T> + where + T: Transport, + { + /// List all references on the remote that have been filtered through our remote's [`fetch_specs`]. + /// + /// 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 refs(&mut self) -> ! { + todo!() + } + } +} diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 2abc27bdc2d..76670cca9dc 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -18,7 +18,10 @@ impl Direction { } mod build; + mod errors; +pub use errors::find; + /// pub mod init; @@ -26,21 +29,9 @@ pub mod init; pub mod connect; #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] -mod connection { - #![allow(missing_docs, dead_code)] - use crate::remote; - use crate::Remote; - - pub struct Connection<'repo, T> { - pub(crate) remote: Remote<'repo>, - pub(crate) direction: remote::Direction, - pub(crate) transport: T, - } -} +mod connection; #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] pub use connection::Connection; -pub use errors::find; - mod access; pub(crate) mod url; From 8f730ae47b0d9765b51b8b04500ca9e70a1ca743 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 16:44:19 +0800 Subject: [PATCH 073/125] thanks clippy --- git-repository/src/remote/connect.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index bbd1bb9b029..5c722d11fba 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -1,8 +1,7 @@ #![allow(missing_docs)] use crate::remote::Connection; -use crate::{remote, Remote}; -use git_protocol::transport; +use crate::Remote; use git_protocol::transport::client::Transport; mod error { @@ -42,7 +41,7 @@ impl<'repo> Remote<'repo> { #[cfg(feature = "blocking-network-client")] pub fn into_connection( self, - direction: remote::Direction, + direction: crate::remote::Direction, ) -> Result>, Error> { use git_protocol::transport::Protocol; let protocol = self @@ -65,7 +64,7 @@ impl<'repo> Remote<'repo> { })?; let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); - let transport = transport::connect(url, protocol)?; + let transport = git_protocol::transport::connect(url, protocol)?; Ok(self.into_connection_with_transport(transport)) } } From c2dfe338a91a1899ecf4e1eecbab708a2f6bac38 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 16:46:18 +0800 Subject: [PATCH 074/125] fix docs (#450) --- git-repository/src/remote/connection.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git-repository/src/remote/connection.rs b/git-repository/src/remote/connection.rs index edec7940138..c5d158f0566 100644 --- a/git-repository/src/remote/connection.rs +++ b/git-repository/src/remote/connection.rs @@ -34,7 +34,8 @@ mod refs { where T: Transport, { - /// List all references on the remote that have been filtered through our remote's [`fetch_specs`]. + /// 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 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). From 1c5f5617940efe818a5e2aca5afe2cbd7f4ad940 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 18:45:18 +0800 Subject: [PATCH 075/125] sketch of simple delegate to collect listed refs (#450) It will still have to use extra parameters for the handshake to use ls-refs filters to limit the amount of returned references. --- git-protocol/src/lib.rs | 5 ++ git-repository/src/remote/connection.rs | 48 ----------- git-repository/src/remote/connection/mod.rs | 29 +++++++ git-repository/src/remote/connection/refs.rs | 84 ++++++++++++++++++++ git-transport/src/lib.rs | 5 ++ 5 files changed, 123 insertions(+), 48 deletions(-) delete mode 100644 git-repository/src/remote/connection.rs create mode 100644 git-repository/src/remote/connection/mod.rs create mode 100644 git-repository/src/remote/connection/refs.rs diff --git a/git-protocol/src/lib.rs b/git-protocol/src/lib.rs index f346013ea92..0ee520cb583 100644 --- a/git-protocol/src/lib.rs +++ b/git-protocol/src/lib.rs @@ -10,6 +10,11 @@ #![deny(unsafe_code)] #![deny(rust_2018_idioms, missing_docs)] +#[cfg(feature = "async-trait")] +pub use async_trait; +#[cfg(feature = "futures-io")] +pub use futures_io; + pub use git_credentials as credentials; /// A convenience export allowing users of git-protocol to use the transport layer without their own cargo dependency. pub use git_transport as transport; diff --git a/git-repository/src/remote/connection.rs b/git-repository/src/remote/connection.rs deleted file mode 100644 index c5d158f0566..00000000000 --- a/git-repository/src/remote/connection.rs +++ /dev/null @@ -1,48 +0,0 @@ -#![allow(missing_docs, dead_code)] - -use crate::Remote; - -pub struct Connection<'repo, T> { - pub(crate) remote: Remote<'repo>, - pub(crate) transport: T, -} - -mod access { - use crate::remote::Connection; - use crate::Remote; - - /// Conversion - impl<'repo, T> Connection<'repo, T> { - /// Dissolve this instance into its parts, `(Remote, Transport)`, the inverse of - /// [`into_connection_with_transport()`][Remote::into_connection_with_transport()]. - pub fn into_parts(self) -> (Remote<'repo>, T) { - (self.remote, self.transport) - } - - /// Drop the transport and additional state to regain the original remote. - pub fn into_remote(self) -> Remote<'repo> { - self.remote - } - } -} - -mod refs { - use crate::remote::Connection; - use git_protocol::transport::client::Transport; - - impl<'repo, T> Connection<'repo, T> - where - T: Transport, - { - /// 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 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 refs(&mut self) -> ! { - todo!() - } - } -} diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs new file mode 100644 index 00000000000..32b09c4fb6a --- /dev/null +++ b/git-repository/src/remote/connection/mod.rs @@ -0,0 +1,29 @@ +#![allow(missing_docs, dead_code)] + +use crate::Remote; + +pub struct Connection<'repo, T> { + pub(crate) remote: Remote<'repo>, + pub(crate) transport: T, +} + +mod access { + use crate::remote::Connection; + use crate::Remote; + + /// Conversion + impl<'repo, T> Connection<'repo, T> { + /// Dissolve this instance into its parts, `(Remote, Transport)`, the inverse of + /// [`into_connection_with_transport()`][Remote::into_connection_with_transport()]. + pub fn into_parts(self) -> (Remote<'repo>, T) { + (self.remote, self.transport) + } + + /// Drop the transport and additional state to regain the original remote. + pub fn into_remote(self) -> Remote<'repo> { + self.remote + } + } +} + +mod refs; diff --git a/git-repository/src/remote/connection/refs.rs b/git-repository/src/remote/connection/refs.rs new file mode 100644 index 00000000000..01efcbdde1a --- /dev/null +++ b/git-repository/src/remote/connection/refs.rs @@ -0,0 +1,84 @@ +use crate::remote::Connection; +use git_protocol::transport::client::Transport; + +struct Delegate { + refs: Vec, +} + +mod delegate { + use super::Delegate; + use git_protocol::fetch::Action; + use git_protocol::transport; + + impl git_protocol::fetch::DelegateBlocking for Delegate { + fn prepare_fetch( + &mut self, + _version: transport::Protocol, + _server: &transport::client::Capabilities, + _features: &mut Vec<(&str, Option<&str>)>, + refs: &[git_protocol::fetch::Ref], + ) -> std::io::Result { + self.refs = refs.into(); + Ok(Action::Cancel) + } + + fn negotiate( + &mut self, + _refs: &[git_protocol::fetch::Ref], + _arguments: &mut git_protocol::fetch::Arguments, + _previous_response: Option<&git_protocol::fetch::Response>, + ) -> std::io::Result { + unreachable!("not to be called due to Action::Close in `prepare_fetch`") + } + } + + #[cfg(feature = "blocking-network-client")] + mod blocking_io { + impl git_protocol::fetch::Delegate for super::Delegate { + fn receive_pack( + &mut self, + _input: impl std::io::BufRead, + _progress: impl git_features::progress::Progress, + _refs: &[git_protocol::fetch::Ref], + _previous_response: &git_protocol::fetch::Response, + ) -> std::io::Result<()> { + unreachable!("not called for ls-refs") + } + } + } + + #[cfg(feature = "async-network-client")] + mod async_io { + use git_protocol::async_trait::async_trait; + use git_protocol::futures_io::AsyncBufRead; + + #[async_trait(? Send)] + impl git_protocol::fetch::Delegate for super::Delegate { + async fn receive_pack( + &mut self, + _input: impl AsyncBufRead + Unpin + 'async_trait, + _progress: impl git_features::progress::Progress, + _refs: &[git_protocol::fetch::Ref], + _previous_response: &git_protocol::fetch::Response, + ) -> std::io::Result<()> { + unreachable!("not called for ls-refs") + } + } + } +} + +impl<'repo, T> Connection<'repo, T> +where + T: Transport, +{ + /// 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 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 refs(&mut self) -> ! { + todo!() + } +} diff --git a/git-transport/src/lib.rs b/git-transport/src/lib.rs index 1abf70cf6d9..46f3cfe0b02 100644 --- a/git-transport/src/lib.rs +++ b/git-transport/src/lib.rs @@ -10,6 +10,11 @@ #![forbid(unsafe_code)] #![deny(rust_2018_idioms, missing_docs)] +#[cfg(feature = "async-trait")] +pub use async_trait; +#[cfg(feature = "futures-io")] +pub use futures_io; + pub use bstr; pub use git_packetline as packetline; From 35d17b4ea80be611607faa774d1ce0ee9c5000f2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 19:42:42 +0800 Subject: [PATCH 076/125] feat: factor fetch handshake into `fetch::handshake()` function (#450) --- Cargo.lock | 1 + git-protocol/Cargo.toml | 1 + git-protocol/src/fetch/handshake.rs | 139 ++++++++++++++++++++++++++++ git-protocol/src/fetch/mod.rs | 5 + 4 files changed, 146 insertions(+) create mode 100644 git-protocol/src/fetch/handshake.rs diff --git a/Cargo.lock b/Cargo.lock index ef41f50be8c..bf821656ffd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1447,6 +1447,7 @@ dependencies = [ "nom", "quick-error", "serde", + "thiserror", ] [[package]] diff --git a/git-protocol/Cargo.toml b/git-protocol/Cargo.toml index 5d8874e3616..797800c4b83 100644 --- a/git-protocol/Cargo.toml +++ b/git-protocol/Cargo.toml @@ -45,6 +45,7 @@ git-hash = { version = "^0.9.6", path = "../git-hash" } git-credentials = { version = "^0.3.0", path = "../git-credentials" } quick-error = "2.0.0" +thiserror = "1.0.32" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} bstr = { version = "0.2.13", default-features = false, features = ["std"] } nom = { version = "7", default-features = false, features = ["std"]} diff --git a/git-protocol/src/fetch/handshake.rs b/git-protocol/src/fetch/handshake.rs new file mode 100644 index 00000000000..63ed72f4ed2 --- /dev/null +++ b/git-protocol/src/fetch/handshake.rs @@ -0,0 +1,139 @@ +use crate::fetch::Ref; +use git_transport::client::Capabilities; + +/// The result of the [`handshake()`][super::handshake()] function. +pub struct Outcome { + /// The protocol version the server responded with. It might have downgraded the desired version. + pub server_protocol_version: git_transport::Protocol, + /// The references reported as part of the Protocol::V1 handshake, or `None` otherwise as V2 requires a separate request. + pub refs: Option>, + /// The server capabilities. + pub capabilities: Capabilities, +} + +mod error { + use crate::credentials; + use crate::fetch::refs; + use git_transport::client; + + /// The error returned by [`handshake()`][super::handshake()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Credentials(#[from] credentials::helper::Error), + #[error(transparent)] + Transport(#[from] client::Error), + #[error("The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side")] + TransportProtocolPolicyViolation { actual_version: git_transport::Protocol }, + #[error(transparent)] + ParseRefs(#[from] refs::Error), + } +} +pub use error::Error; + +pub(crate) mod function { + use super::{Error, Outcome}; + use crate::credentials; + use crate::fetch::refs; + use git_features::progress; + use git_features::progress::Progress; + use git_transport::client::SetServiceResponse; + use git_transport::{client, Service}; + use maybe_async::maybe_async; + + /// Perform a handshake with the server on the other side of `transport`, with `authenticate` being used if authentication + /// turns out to be required. `extra_parameters` are the parameters `(name, optional value)` to add to the handshake, + /// each time it is performed in case authentication is required. + /// `progress` is used to inform about what's currently happening. + #[maybe_async] + pub async fn handshake( + mut transport: T, + mut authenticate: AuthFn, + extra_parameters: Vec<(String, Option)>, + mut progress: impl Progress, + ) -> Result + where + AuthFn: FnMut(credentials::helper::Action<'_>) -> credentials::helper::Result, + T: client::Transport, + { + let (protocol_version, refs, capabilities) = { + progress.init(None, progress::steps()); + progress.set_name("handshake"); + progress.step(); + + let extra_parameters: Vec<_> = extra_parameters + .iter() + .map(|(k, v)| (k.as_str(), v.as_ref().map(|s| s.as_str()))) + .collect(); + let supported_versions: Vec<_> = transport.supported_protocol_versions().into(); + + let result = transport.handshake(Service::UploadPack, &extra_parameters).await; + let SetServiceResponse { + actual_protocol, + capabilities, + refs, + } = match result { + Ok(v) => Ok(v), + Err(client::Error::Io { ref err }) if err.kind() == std::io::ErrorKind::PermissionDenied => { + drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149 + let url = transport.to_url(); + progress.set_name("authentication"); + let credentials::helper::Outcome { identity, next } = + authenticate(credentials::helper::Action::Fill(url.as_str().into()))? + .expect("FILL provides an identity"); + transport.set_identity(identity)?; + progress.step(); + progress.set_name("handshake (authenticated)"); + match transport.handshake(Service::UploadPack, &extra_parameters).await { + Ok(v) => { + authenticate(next.approve())?; + Ok(v) + } + // Still no permission? Reject the credentials. + Err(client::Error::Io { err }) if err.kind() == std::io::ErrorKind::PermissionDenied => { + authenticate(next.reject())?; + Err(client::Error::Io { err }) + } + // Otherwise, do nothing, as we don't know if it actually got to try the credentials. + // If they were previously stored, they remain. In the worst case, the user has to enter them again + // next time they try. + Err(err) => Err(err), + } + } + Err(err) => Err(err), + }?; + + if !supported_versions.is_empty() && !supported_versions.contains(&actual_protocol) { + return Err(Error::TransportProtocolPolicyViolation { + actual_version: actual_protocol, + }); + } + + let parsed_refs = match refs { + Some(mut refs) => { + assert_eq!( + actual_protocol, + git_transport::Protocol::V1, + "Only V1 auto-responds with refs" + ); + Some( + refs::from_v1_refs_received_as_part_of_handshake_and_capabilities( + &mut refs, + capabilities.iter(), + ) + .await?, + ) + } + None => None, + }; + (actual_protocol, parsed_refs, capabilities) + }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149 + + Ok(Outcome { + server_protocol_version: protocol_version, + refs, + capabilities, + }) + } +} diff --git a/git-protocol/src/fetch/mod.rs b/git-protocol/src/fetch/mod.rs index a65e497bd52..f9cb4a4e049 100644 --- a/git-protocol/src/fetch/mod.rs +++ b/git-protocol/src/fetch/mod.rs @@ -25,5 +25,10 @@ pub use refs::Ref; pub mod response; pub use response::Response; +/// +pub mod handshake; + +pub use handshake::function::handshake; + #[cfg(test)] mod tests; From e91c301342d44cff35ebe12fba4ec10afb4a1922 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 20:02:14 +0800 Subject: [PATCH 077/125] change!: replace `quick-error` with `this-error` (#450) --- Cargo.lock | 1 - git-protocol/Cargo.toml | 1 - git-protocol/src/fetch/error.rs | 56 ++++-------- git-protocol/src/fetch/refs.rs | 80 ++++++++--------- git-protocol/src/fetch/response/async_io.rs | 2 +- .../src/fetch/response/blocking_io.rs | 2 +- git-protocol/src/fetch/response/mod.rs | 87 ++++++++----------- 7 files changed, 93 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf821656ffd..a4885cfb290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1445,7 +1445,6 @@ dependencies = [ "git-transport", "maybe-async", "nom", - "quick-error", "serde", "thiserror", ] diff --git a/git-protocol/Cargo.toml b/git-protocol/Cargo.toml index 797800c4b83..e0a4ed0c5fc 100644 --- a/git-protocol/Cargo.toml +++ b/git-protocol/Cargo.toml @@ -44,7 +44,6 @@ git-transport = { version = "^0.19.0", path = "../git-transport" } git-hash = { version = "^0.9.6", path = "../git-hash" } git-credentials = { version = "^0.3.0", path = "../git-credentials" } -quick-error = "2.0.0" thiserror = "1.0.32" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} bstr = { version = "0.2.13", default-features = false, features = ["std"] } diff --git a/git-protocol/src/fetch/error.rs b/git-protocol/src/fetch/error.rs index 5459ffc9f35..2b3e82201fa 100644 --- a/git-protocol/src/fetch/error.rs +++ b/git-protocol/src/fetch/error.rs @@ -1,48 +1,28 @@ use std::io; use git_transport::client; -use quick_error::quick_error; use crate::{ credentials, fetch::{refs, response}, }; -quick_error! { - /// The error used in [`fetch()`][super::fetch]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Io(err: io::Error) { - display("Could not access repository or failed to read streaming pack file") - from() - source(err) - } - Credentials(err: credentials::helper::Error) { - display("Failed to obtain, approve or reject credentials") - from() - source(err) - } - Transport(err: client::Error) { - display("An error occurred on the transport layer while fetching data") - from() - source(err) - } - SymrefWithoutValue { - display("A symref 'capability' is expected to have a value") - } - TransportProtocolPolicyViolation{actual_version: git_transport::Protocol} { - display("The transport didn't accept the advertised server version {:?} and closed the connection client side", actual_version) - } - Ref(err: refs::Error) { - display("A reference could not be parsed or invariants were not met") - from() - source(err) - } - Response(err: response::Error) { - display("The server response could not be parsed") - from() - source(err) - } - } +/// The error used in [`fetch()`][super::fetch]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Could not access repository or failed to read streaming pack file")] + Io(#[from] io::Error), + #[error(transparent)] + Credentials(#[from] credentials::helper::Error), + #[error(transparent)] + Transport(#[from] client::Error), + #[error("A symref 'capability' is expected to have a value")] + SymrefWithoutValue, + #[error("The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side")] + TransportProtocolPolicyViolation { actual_version: git_transport::Protocol }, + #[error(transparent)] + Ref(#[from] refs::Error), + #[error(transparent)] + Response(#[from] response::Error), } diff --git a/git-protocol/src/fetch/refs.rs b/git-protocol/src/fetch/refs.rs index 989759d43c1..9ba4cd8a0ed 100644 --- a/git-protocol/src/fetch/refs.rs +++ b/git-protocol/src/fetch/refs.rs @@ -1,39 +1,25 @@ use std::io; use bstr::BString; -use quick_error::quick_error; -quick_error! { - /// The error returned when parsing References/refs from the server response. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Io(err: io::Error) { - display("An IO error occurred while reading refs from the server") - from() - source(err) - } - Id(err: git_hash::decode::Error) { - display("Failed to hex-decode object hash") - from() - source(err) - } - MalformedSymref(symref: BString) { - display("'{}' could not be parsed. A symref is expected to look like :.", symref) - } - MalformedV1RefLine(line: String) { - display("'{}' could not be parsed. A V1 ref line should be ' '.", line) - } - MalformedV2RefLine(line: String) { - display("'{}' could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'.", line) - } - UnkownAttribute(attribute: String, line: String) { - display("The ref attribute '{}' is unknown. Found in line '{}'", attribute, line) - } - InvariantViolation(message: &'static str) { - display("{}", message) - } - } +/// The error returned when parsing References/refs from the server response. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + Id(#[from] git_hash::decode::Error), + #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] + MalformedSymref { symref: BString }, + #[error("{0:?} could not be parsed. A V1 ref line should be ' '.")] + MalformedV1RefLine(String), + #[error("{0:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'.")] + MalformedV2RefLine(String), + #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")] + UnkownAttribute { attribute: String, line: String }, + #[error("{message}")] + InvariantViolation { message: &'static str }, } /// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack. @@ -158,13 +144,14 @@ pub(crate) mod shared { } }); for symref in symref_values { - let (left, right) = symref.split_at( - symref - .find_byte(b':') - .ok_or_else(|| refs::Error::MalformedSymref(symref.to_owned()))?, - ); + let (left, right) = + symref.split_at(symref.find_byte(b':').ok_or_else(|| refs::Error::MalformedSymref { + symref: symref.to_owned(), + })?); if left.is_empty() || right.is_empty() { - return Err(refs::Error::MalformedSymref(symref.to_owned())); + return Err(refs::Error::MalformedSymref { + symref: symref.to_owned(), + }); } out_refs.push(InternalRef::SymbolicForLookup { path: left.into(), @@ -198,13 +185,13 @@ pub(crate) mod shared { out_refs .pop() .and_then(InternalRef::unpack_direct) - .ok_or(refs::Error::InvariantViolation( - "Expecting peeled refs to be preceded by direct refs", - ))?; + .ok_or(refs::Error::InvariantViolation { + message: "Expecting peeled refs to be preceded by direct refs", + })?; if previous_path != stripped { - return Err(refs::Error::InvariantViolation( - "Expecting peeled refs to have the same base path as the previous, unpeeled one", - )); + return Err(refs::Error::InvariantViolation { + message: "Expecting peeled refs to have the same base path as the previous, unpeeled one", + }); } out_refs.push(InternalRef::Peeled { path: previous_path, @@ -271,7 +258,10 @@ pub(crate) mod shared { }, }, _ => { - return Err(refs::Error::UnkownAttribute(attribute.to_owned(), trimmed.to_owned())) + return Err(refs::Error::UnkownAttribute { + attribute: attribute.to_owned(), + line: trimmed.to_owned(), + }) } } } diff --git a/git-protocol/src/fetch/response/async_io.rs b/git-protocol/src/fetch/response/async_io.rs index 6472b5b67a0..4758ec17bd4 100644 --- a/git-protocol/src/fetch/response/async_io.rs +++ b/git-protocol/src/fetch/response/async_io.rs @@ -121,7 +121,7 @@ impl Response { // what follows is the packfile itself, which can be read with a sideband enabled reader break 'section true; } - _ => return Err(response::Error::UnknownSectionHeader(line)), + _ => return Err(response::Error::UnknownSectionHeader { header: line }), } }; Ok(Response { diff --git a/git-protocol/src/fetch/response/blocking_io.rs b/git-protocol/src/fetch/response/blocking_io.rs index f63143b3794..9dd5c8b09de 100644 --- a/git-protocol/src/fetch/response/blocking_io.rs +++ b/git-protocol/src/fetch/response/blocking_io.rs @@ -120,7 +120,7 @@ impl Response { // what follows is the packfile itself, which can be read with a sideband enabled reader break 'section true; } - _ => return Err(response::Error::UnknownSectionHeader(line)), + _ => return Err(response::Error::UnknownSectionHeader { header: line }), } }; Ok(Response { diff --git a/git-protocol/src/fetch/response/mod.rs b/git-protocol/src/fetch/response/mod.rs index 80c0a69066a..37e0943ecb8 100644 --- a/git-protocol/src/fetch/response/mod.rs +++ b/git-protocol/src/fetch/response/mod.rs @@ -1,50 +1,35 @@ -use std::io; - use bstr::BString; use git_transport::{client, Protocol}; -use quick_error::quick_error; use crate::fetch::command::Feature; -quick_error! { - /// The error used in the [response module][crate::fetch::response]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Io(err: io::Error) { - display("Failed to read from line reader") - source(err) - } - UploadPack(err: git_transport::packetline::read::Error) { - display("Upload pack reported an error") - source(err) - } - Transport(err: client::Error) { - display("An error occurred when decoding a line") - from() - source(err) - } - MissingServerCapability(feature: &'static str) { - display("Currently we require feature '{}', which is not supported by the server", feature) - } - UnknownLineType(line: String) { - display("Encountered an unknown line prefix in '{}'", line) - } - UnknownSectionHeader(header: String) { - display("Unknown or unsupported header: '{}'", header) - } - } +/// The error returned in the [response module][crate::fetch::response]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Failed to read from line reader")] + Io(std::io::Error), + #[error(transparent)] + UploadPack(#[from] git_transport::packetline::read::Error), + #[error(transparent)] + Transport(#[from] client::Error), + #[error("Currently we require feature {feature:?}, which is not supported by the server")] + MissingServerCapability { feature: &'static str }, + #[error("Encountered an unknown line prefix in {line:?}")] + UnknownLineType { line: String }, + #[error("Unknown or unsupported header: {header:?}")] + UnknownSectionHeader { header: String }, } impl From for Error { - fn from(err: io::Error) -> Self { - if err.kind() == io::ErrorKind::Other { + fn from(err: std::io::Error) -> Self { + if err.kind() == std::io::ErrorKind::Other { match err.into_inner() { Some(err) => match err.downcast::() { Ok(err) => Error::UploadPack(*err), - Err(err) => Error::Io(io::Error::new(io::ErrorKind::Other, err)), + Err(err) => Error::Io(std::io::Error::new(std::io::ErrorKind::Other, err)), }, - None => Error::Io(io::ErrorKind::Other.into()), + None => Error::Io(std::io::ErrorKind::Other.into()), } } else { Error::Io(err) @@ -89,15 +74,15 @@ impl ShallowUpdate { pub fn from_line(line: &str) -> Result { match line.trim_end().split_once(' ') { Some((prefix, id)) => { - let id = - git_hash::ObjectId::from_hex(id.as_bytes()).map_err(|_| Error::UnknownLineType(line.to_owned()))?; + let id = git_hash::ObjectId::from_hex(id.as_bytes()) + .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?; Ok(match prefix { "shallow" => ShallowUpdate::Shallow(id), "unshallow" => ShallowUpdate::Unshallow(id), - _ => return Err(Error::UnknownLineType(line.to_owned())), + _ => return Err(Error::UnknownLineType { line: line.to_owned() }), }) } - None => Err(Error::UnknownLineType(line.to_owned())), + None => Err(Error::UnknownLineType { line: line.to_owned() }), } } } @@ -113,21 +98,21 @@ impl Acknowledgement { "ACK" => { let id = match id { Some(id) => git_hash::ObjectId::from_hex(id.as_bytes()) - .map_err(|_| Error::UnknownLineType(line.to_owned()))?, - None => return Err(Error::UnknownLineType(line.to_owned())), + .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?, + None => return Err(Error::UnknownLineType { line: line.to_owned() }), }; if let Some(description) = description { match description { "common" => {} "ready" => return Ok(Acknowledgement::Ready), - _ => return Err(Error::UnknownLineType(line.to_owned())), + _ => return Err(Error::UnknownLineType { line: line.to_owned() }), } } Acknowledgement::Common(id) } - _ => return Err(Error::UnknownLineType(line.to_owned())), + _ => return Err(Error::UnknownLineType { line: line.to_owned() }), }), - (None, _, _) => Err(Error::UnknownLineType(line.to_owned())), + (None, _, _) => Err(Error::UnknownLineType { line: line.to_owned() }), } } /// Returns the hash of the acknowledged object if this instance acknowledges a common one. @@ -144,11 +129,11 @@ impl WantedRef { pub fn from_line(line: &str) -> Result { match line.trim_end().split_once(' ') { Some((id, path)) => { - let id = - git_hash::ObjectId::from_hex(id.as_bytes()).map_err(|_| Error::UnknownLineType(line.to_owned()))?; + let id = git_hash::ObjectId::from_hex(id.as_bytes()) + .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?; Ok(WantedRef { id, path: path.into() }) } - None => Err(Error::UnknownLineType(line.to_owned())), + None => Err(Error::UnknownLineType { line: line.to_owned() }), } } } @@ -177,14 +162,18 @@ impl Response { let has = |name: &str| features.iter().any(|f| f.0 == name); // Let's focus on V2 standards, and simply not support old servers to keep our code simpler if !has("multi_ack_detailed") { - return Err(Error::MissingServerCapability("multi_ack_detailed")); + return Err(Error::MissingServerCapability { + feature: "multi_ack_detailed", + }); } // It's easy to NOT do sideband for us, but then again, everyone supports it. // CORRECTION: If side-band is off, it would send the packfile without packet line encoding, // which is nothing we ever want to deal with (despite it being more efficient). In V2, this // is not even an option anymore, sidebands are always present. if !has("side-band") && !has("side-band-64k") { - return Err(Error::MissingServerCapability("side-band OR side-band-64k")); + return Err(Error::MissingServerCapability { + feature: "side-band OR side-band-64k", + }); } } Protocol::V2 => {} From aaa1680f9e19b64a0d380ced9559c7325b79dd04 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 20:09:11 +0800 Subject: [PATCH 078/125] Use handshake() function in `fetch()` method (#450) --- git-protocol/src/fetch/error.rs | 12 ++-- git-protocol/src/fetch/handshake.rs | 6 +- git-protocol/src/fetch_fn.rs | 104 ++++++---------------------- 3 files changed, 27 insertions(+), 95 deletions(-) diff --git a/git-protocol/src/fetch/error.rs b/git-protocol/src/fetch/error.rs index 2b3e82201fa..f02b4bf7a9c 100644 --- a/git-protocol/src/fetch/error.rs +++ b/git-protocol/src/fetch/error.rs @@ -2,25 +2,21 @@ use std::io; use git_transport::client; -use crate::{ - credentials, - fetch::{refs, response}, -}; +use crate::fetch::handshake; +use crate::fetch::{refs, response}; /// The error used in [`fetch()`][super::fetch]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { + #[error(transparent)] + Handshake(#[from] handshake::Error), #[error("Could not access repository or failed to read streaming pack file")] Io(#[from] io::Error), #[error(transparent)] - Credentials(#[from] credentials::helper::Error), - #[error(transparent)] Transport(#[from] client::Error), #[error("A symref 'capability' is expected to have a value")] SymrefWithoutValue, - #[error("The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side")] - TransportProtocolPolicyViolation { actual_version: git_transport::Protocol }, #[error(transparent)] Ref(#[from] refs::Error), #[error(transparent)] diff --git a/git-protocol/src/fetch/handshake.rs b/git-protocol/src/fetch/handshake.rs index 63ed72f4ed2..41891cded02 100644 --- a/git-protocol/src/fetch/handshake.rs +++ b/git-protocol/src/fetch/handshake.rs @@ -51,13 +51,13 @@ pub(crate) mod function { mut transport: T, mut authenticate: AuthFn, extra_parameters: Vec<(String, Option)>, - mut progress: impl Progress, + progress: &mut impl Progress, ) -> Result where AuthFn: FnMut(credentials::helper::Action<'_>) -> credentials::helper::Result, T: client::Transport, { - let (protocol_version, refs, capabilities) = { + let (server_protocol_version, refs, capabilities) = { progress.init(None, progress::steps()); progress.set_name("handshake"); progress.step(); @@ -131,7 +131,7 @@ pub(crate) mod function { }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149 Ok(Outcome { - server_protocol_version: protocol_version, + server_protocol_version, refs, capabilities, }) diff --git a/git-protocol/src/fetch_fn.rs b/git-protocol/src/fetch_fn.rs index 1dd49a10cc9..97590bc0894 100644 --- a/git-protocol/src/fetch_fn.rs +++ b/git-protocol/src/fetch_fn.rs @@ -1,13 +1,8 @@ -use std::io; - -use git_features::{progress, progress::Progress}; -use git_transport::{ - client, - client::{SetServiceResponse, TransportV2Ext}, - Service, -}; +use git_features::progress::Progress; +use git_transport::{client, client::TransportV2Ext}; use maybe_async::maybe_async; +use crate::fetch::handshake; use crate::{ credentials, fetch::{refs, Action, Arguments, Command, Delegate, Error, LsRefsAction, Response}, @@ -53,7 +48,7 @@ impl Default for FetchConnection { pub async fn fetch( mut transport: T, mut delegate: D, - mut authenticate: F, + authenticate: F, mut progress: impl Progress, fetch_mode: FetchConnection, ) -> Result<(), Error> @@ -62,78 +57,19 @@ where D: Delegate, T: client::Transport, { - let (protocol_version, parsed_refs, capabilities) = { - progress.init(None, progress::steps()); - progress.set_name("handshake"); - progress.step(); - - let extra_parameters = delegate.handshake_extra_parameters(); - let extra_parameters: Vec<_> = extra_parameters - .iter() - .map(|(k, v)| (k.as_str(), v.as_ref().map(|s| s.as_str()))) - .collect(); - let supported_versions: Vec<_> = transport.supported_protocol_versions().into(); - - let result = transport.handshake(Service::UploadPack, &extra_parameters).await; - let SetServiceResponse { - actual_protocol, - capabilities, - refs, - } = match result { - Ok(v) => Ok(v), - Err(client::Error::Io { ref err }) if err.kind() == io::ErrorKind::PermissionDenied => { - drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149 - let url = transport.to_url(); - progress.set_name("authentication"); - let credentials::helper::Outcome { identity, next } = - authenticate(credentials::helper::Action::Fill(url.as_str().into()))? - .expect("FILL provides an identity"); - transport.set_identity(identity)?; - progress.step(); - progress.set_name("handshake (authenticated)"); - match transport.handshake(Service::UploadPack, &extra_parameters).await { - Ok(v) => { - authenticate(next.approve())?; - Ok(v) - } - // Still no permission? Reject the credentials. - Err(client::Error::Io { err }) if err.kind() == io::ErrorKind::PermissionDenied => { - authenticate(next.reject())?; - Err(client::Error::Io { err }) - } - // Otherwise, do nothing, as we don't know if it actually got to try the credentials. - // If they were previously stored, they remain. In the worst case, the user has to enter them again - // next time they try. - Err(err) => Err(err), - } - } - Err(err) => Err(err), - }?; - - if !supported_versions.is_empty() && !supported_versions.contains(&actual_protocol) { - return Err(Error::TransportProtocolPolicyViolation { - actual_version: actual_protocol, - }); - } - - let parsed_refs = match refs { - Some(mut refs) => { - assert_eq!( - actual_protocol, - git_transport::Protocol::V1, - "Only V1 auto-responds with refs" - ); - Some( - refs::from_v1_refs_received_as_part_of_handshake_and_capabilities(&mut refs, capabilities.iter()) - .await?, - ) - } - None => None, - }; - (actual_protocol, parsed_refs, capabilities) - }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149 - - let parsed_refs = match parsed_refs { + let handshake::Outcome { + server_protocol_version: protocol_version, + refs, + capabilities, + } = crate::fetch::handshake( + &mut transport, + authenticate, + delegate.handshake_extra_parameters(), + &mut progress, + ) + .await?; + + let refs = match refs { Some(refs) => refs, None => { assert_eq!( @@ -180,7 +116,7 @@ where let fetch = Command::Fetch; let mut fetch_features = fetch.default_features(protocol_version, &capabilities); - match delegate.prepare_fetch(protocol_version, &capabilities, &mut fetch_features, &parsed_refs) { + match delegate.prepare_fetch(protocol_version, &capabilities, &mut fetch_features, &refs) { Ok(Action::Cancel) => { return if matches!(protocol_version, git_transport::Protocol::V1) || matches!(fetch_mode, FetchConnection::TerminateOnSuccessfulCompletion) @@ -208,7 +144,7 @@ where progress.step(); progress.set_name(format!("negotiate (round {})", round)); round += 1; - let action = delegate.negotiate(&parsed_refs, &mut arguments, previous_response.as_ref())?; + let action = delegate.negotiate(&refs, &mut arguments, previous_response.as_ref())?; let mut reader = arguments.send(&mut transport, action == Action::Cancel).await?; if sideband_all { setup_remote_progress(&mut progress, &mut reader); @@ -220,7 +156,7 @@ where if !sideband_all { setup_remote_progress(&mut progress, &mut reader); } - delegate.receive_pack(reader, progress, &parsed_refs, &response).await?; + delegate.receive_pack(reader, progress, &refs, &response).await?; break 'negotiation; } else { match action { From 41a7391d86efff39a2fce126041c50429bda224c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 20:13:52 +0800 Subject: [PATCH 079/125] refactor (#450) --- git-protocol/src/fetch/mod.rs | 1 - git-protocol/src/fetch/refs.rs | 381 --------------------- git-protocol/src/fetch/refs/async_io.rs | 45 +++ git-protocol/src/fetch/refs/blocking_io.rs | 44 +++ git-protocol/src/fetch/refs/mod.rs | 81 +++++ git-protocol/src/fetch/refs/shared.rs | 207 +++++++++++ 6 files changed, 377 insertions(+), 382 deletions(-) delete mode 100644 git-protocol/src/fetch/refs.rs create mode 100644 git-protocol/src/fetch/refs/async_io.rs create mode 100644 git-protocol/src/fetch/refs/blocking_io.rs create mode 100644 git-protocol/src/fetch/refs/mod.rs create mode 100644 git-protocol/src/fetch/refs/shared.rs diff --git a/git-protocol/src/fetch/mod.rs b/git-protocol/src/fetch/mod.rs index f9cb4a4e049..38cda8e1354 100644 --- a/git-protocol/src/fetch/mod.rs +++ b/git-protocol/src/fetch/mod.rs @@ -27,7 +27,6 @@ pub use response::Response; /// pub mod handshake; - pub use handshake::function::handshake; #[cfg(test)] diff --git a/git-protocol/src/fetch/refs.rs b/git-protocol/src/fetch/refs.rs deleted file mode 100644 index 9ba4cd8a0ed..00000000000 --- a/git-protocol/src/fetch/refs.rs +++ /dev/null @@ -1,381 +0,0 @@ -use std::io; - -use bstr::BString; - -/// The error returned when parsing References/refs from the server response. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Id(#[from] git_hash::decode::Error), - #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] - MalformedSymref { symref: BString }, - #[error("{0:?} could not be parsed. A V1 ref line should be ' '.")] - MalformedV1RefLine(String), - #[error("{0:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'.")] - MalformedV2RefLine(String), - #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")] - UnkownAttribute { attribute: String, line: String }, - #[error("{message}")] - InvariantViolation { message: &'static str }, -} - -/// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] -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 hash of the tag the ref points to. - tag: git_hash::ObjectId, - /// The hash of the object the `tag` points to. - object: git_hash::ObjectId, - }, - /// A ref pointing to a commit object - Direct { - /// The path at which the ref is located, like `/refs/heads/main`. - path: 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 path of the ref the symbolic ref points to, see issue [#205] for details - /// - /// [#205]: https://github.com/Byron/gitoxide/issues/205 - target: BString, - /// The hash of the object the `target` ref points to. - object: git_hash::ObjectId, - }, -} - -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) { - match self { - Ref::Direct { path, object, .. } - | Ref::Peeled { path, tag: object, .. } // the tag acts as reference - | Ref::Symbolic { path, object, .. } => (path, object), - } - } -} - -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -pub(crate) mod shared { - use bstr::{BString, ByteSlice}; - - use crate::fetch::{refs, Ref}; - - impl From for Ref { - fn from(v: InternalRef) -> Self { - match v { - InternalRef::Symbolic { - path, - target: Some(target), - object, - } => Ref::Symbolic { 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 }, - InternalRef::SymbolicForLookup { .. } => { - unreachable!("this case should have been removed during processing") - } - } - } - } - - #[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))] - pub(crate) enum InternalRef { - /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit - Peeled { - path: BString, - tag: git_hash::ObjectId, - object: git_hash::ObjectId, - }, - /// A ref pointing to a commit object - Direct { path: BString, object: git_hash::ObjectId }, - /// A symbolic ref pointing to `target` ref, which in turn points to an `object` - Symbolic { - path: BString, - /// It is `None` if the target is unreachable as it points to another namespace than the one is currently set - /// on the server (i.e. based on the repository at hand or the user performing the operation). - /// - /// The latter is more of an edge case, please [this issue][#205] for details. - target: Option, - object: git_hash::ObjectId, - }, - /// extracted from V1 capabilities, which contain some important symbolic refs along with their targets - /// These don't contain the Id - SymbolicForLookup { path: BString, target: Option }, - } - - impl InternalRef { - fn unpack_direct(self) -> Option<(BString, git_hash::ObjectId)> { - match self { - InternalRef::Direct { path, object } => Some((path, object)), - _ => None, - } - } - fn lookup_symbol_has_path(&self, predicate_path: &str) -> bool { - matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path) - } - } - - pub(crate) fn from_capabilities<'a>( - capabilities: impl Iterator>, - ) -> Result, refs::Error> { - let mut out_refs = Vec::new(); - let symref_values = capabilities.filter_map(|c| { - if c.name() == b"symref".as_bstr() { - c.value().map(ToOwned::to_owned) - } else { - None - } - }); - for symref in symref_values { - let (left, right) = - symref.split_at(symref.find_byte(b':').ok_or_else(|| refs::Error::MalformedSymref { - symref: symref.to_owned(), - })?); - if left.is_empty() || right.is_empty() { - return Err(refs::Error::MalformedSymref { - symref: symref.to_owned(), - }); - } - out_refs.push(InternalRef::SymbolicForLookup { - path: left.into(), - target: match &right[1..] { - b"(null)" => None, - name => Some(name.into()), - }, - }) - } - Ok(out_refs) - } - - pub(in crate::fetch::refs) fn parse_v1( - num_initial_out_refs: usize, - out_refs: &mut Vec, - line: &str, - ) -> Result<(), refs::Error> { - let trimmed = line.trim_end(); - let (hex_hash, path) = trimmed.split_at( - trimmed - .find(' ') - .ok_or_else(|| refs::Error::MalformedV1RefLine(trimmed.to_owned()))?, - ); - let path = &path[1..]; - if path.is_empty() { - return Err(refs::Error::MalformedV1RefLine(trimmed.to_owned())); - } - match path.strip_suffix("^{}") { - Some(stripped) => { - let (previous_path, tag) = - out_refs - .pop() - .and_then(InternalRef::unpack_direct) - .ok_or(refs::Error::InvariantViolation { - message: "Expecting peeled refs to be preceded by direct refs", - })?; - if previous_path != stripped { - return Err(refs::Error::InvariantViolation { - message: "Expecting peeled refs to have the same base path as the previous, unpeeled one", - }); - } - out_refs.push(InternalRef::Peeled { - path: previous_path, - tag, - object: git_hash::ObjectId::from_hex(hex_hash.as_bytes())?, - }); - } - None => { - let object = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?; - match out_refs - .iter() - .take(num_initial_out_refs) - .position(|r| r.lookup_symbol_has_path(path)) - { - Some(position) => match out_refs.swap_remove(position) { - InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic { - path: path.into(), - object, - target, - }), - _ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"), - }, - None => out_refs.push(InternalRef::Direct { - object, - path: path.into(), - }), - }; - } - } - Ok(()) - } - - pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { - let trimmed = line.trim_end(); - let mut tokens = trimmed.splitn(3, ' '); - match (tokens.next(), tokens.next()) { - (Some(hex_hash), Some(path)) => { - let id = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?; - if path.is_empty() { - return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); - } - Ok(if let Some(attribute) = tokens.next() { - let mut tokens = attribute.splitn(2, ':'); - match (tokens.next(), tokens.next()) { - (Some(attribute), Some(value)) => { - if value.is_empty() { - return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); - } - match attribute { - "peeled" => Ref::Peeled { - path: path.into(), - object: git_hash::ObjectId::from_hex(value.as_bytes())?, - tag: id, - }, - "symref-target" => match value { - "(null)" => Ref::Direct { - path: path.into(), - object: id, - }, - name => Ref::Symbolic { - path: path.into(), - object: id, - target: name.into(), - }, - }, - _ => { - return Err(refs::Error::UnkownAttribute { - attribute: attribute.to_owned(), - line: trimmed.to_owned(), - }) - } - } - } - _ => return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), - } - } else { - Ref::Direct { - object: id, - path: path.into(), - } - }) - } - _ => Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), - } - } -} - -#[cfg(feature = "async-client")] -mod async_io { - use futures_io::AsyncBufRead; - use futures_lite::AsyncBufReadExt; - - use crate::fetch::{refs, Ref}; - - /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. - pub async fn from_v2_refs(in_refs: &mut (dyn AsyncBufRead + Unpin)) -> Result, refs::Error> { - let mut out_refs = Vec::new(); - let mut line = String::new(); - loop { - line.clear(); - let bytes_read = in_refs.read_line(&mut line).await?; - if bytes_read == 0 { - break; - } - out_refs.push(refs::shared::parse_v2(&line)?); - } - Ok(out_refs) - } - - /// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the - /// handshake. - /// Together they form a complete set of refs. - /// - /// # Note - /// - /// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as - /// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. - pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( - in_refs: &mut (dyn AsyncBufRead + Unpin), - capabilities: impl Iterator>, - ) -> Result, refs::Error> { - let mut out_refs = refs::shared::from_capabilities(capabilities)?; - let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); - let mut line = String::new(); - loop { - line.clear(); - let bytes_read = in_refs.read_line(&mut line).await?; - if bytes_read == 0 { - break; - } - refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, &line)?; - } - Ok(out_refs.into_iter().map(Into::into).collect()) - } -} -#[cfg(feature = "async-client")] -pub use async_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs}; - -#[cfg(feature = "blocking-client")] -mod blocking_io { - use std::io; - - use crate::fetch::{refs, Ref}; - - /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. - pub fn from_v2_refs(in_refs: &mut dyn io::BufRead) -> Result, refs::Error> { - let mut out_refs = Vec::new(); - let mut line = String::new(); - loop { - line.clear(); - let bytes_read = in_refs.read_line(&mut line)?; - if bytes_read == 0 { - break; - } - out_refs.push(refs::shared::parse_v2(&line)?); - } - Ok(out_refs) - } - - /// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the - /// handshake. - /// Together they form a complete set of refs. - /// - /// # Note - /// - /// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as - /// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. - pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( - in_refs: &mut dyn io::BufRead, - capabilities: impl Iterator>, - ) -> Result, refs::Error> { - let mut out_refs = refs::shared::from_capabilities(capabilities)?; - let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); - let mut line = String::new(); - loop { - line.clear(); - let bytes_read = in_refs.read_line(&mut line)?; - if bytes_read == 0 { - break; - } - refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, &line)?; - } - Ok(out_refs.into_iter().map(Into::into).collect()) - } -} -#[cfg(feature = "blocking-client")] -pub use blocking_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs}; diff --git a/git-protocol/src/fetch/refs/async_io.rs b/git-protocol/src/fetch/refs/async_io.rs new file mode 100644 index 00000000000..dcaff04fe0f --- /dev/null +++ b/git-protocol/src/fetch/refs/async_io.rs @@ -0,0 +1,45 @@ +use futures_io::AsyncBufRead; +use futures_lite::AsyncBufReadExt; + +use crate::fetch::{refs, Ref}; + +/// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. +pub async fn from_v2_refs(in_refs: &mut (dyn AsyncBufRead + Unpin)) -> Result, refs::Error> { + let mut out_refs = Vec::new(); + let mut line = String::new(); + loop { + line.clear(); + let bytes_read = in_refs.read_line(&mut line).await?; + if bytes_read == 0 { + break; + } + out_refs.push(refs::shared::parse_v2(&line)?); + } + Ok(out_refs) +} + +/// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the +/// handshake. +/// Together they form a complete set of refs. +/// +/// # Note +/// +/// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as +/// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. +pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( + in_refs: &mut (dyn AsyncBufRead + Unpin), + capabilities: impl Iterator>, +) -> Result, refs::Error> { + let mut out_refs = refs::shared::from_capabilities(capabilities)?; + let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); + let mut line = String::new(); + loop { + line.clear(); + let bytes_read = in_refs.read_line(&mut line).await?; + if bytes_read == 0 { + break; + } + refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, &line)?; + } + Ok(out_refs.into_iter().map(Into::into).collect()) +} diff --git a/git-protocol/src/fetch/refs/blocking_io.rs b/git-protocol/src/fetch/refs/blocking_io.rs new file mode 100644 index 00000000000..10d592f3e87 --- /dev/null +++ b/git-protocol/src/fetch/refs/blocking_io.rs @@ -0,0 +1,44 @@ +use std::io; + +use crate::fetch::{refs, Ref}; + +/// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. +pub fn from_v2_refs(in_refs: &mut dyn io::BufRead) -> Result, refs::Error> { + let mut out_refs = Vec::new(); + let mut line = String::new(); + loop { + line.clear(); + let bytes_read = in_refs.read_line(&mut line)?; + if bytes_read == 0 { + break; + } + out_refs.push(refs::shared::parse_v2(&line)?); + } + Ok(out_refs) +} + +/// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the +/// handshake. +/// Together they form a complete set of refs. +/// +/// # Note +/// +/// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as +/// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. +pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( + in_refs: &mut dyn io::BufRead, + capabilities: impl Iterator>, +) -> Result, refs::Error> { + let mut out_refs = refs::shared::from_capabilities(capabilities)?; + let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); + let mut line = String::new(); + loop { + line.clear(); + let bytes_read = in_refs.read_line(&mut line)?; + if bytes_read == 0 { + break; + } + refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, &line)?; + } + Ok(out_refs.into_iter().map(Into::into).collect()) +} diff --git a/git-protocol/src/fetch/refs/mod.rs b/git-protocol/src/fetch/refs/mod.rs new file mode 100644 index 00000000000..9ebf02154fa --- /dev/null +++ b/git-protocol/src/fetch/refs/mod.rs @@ -0,0 +1,81 @@ +use std::io; + +use bstr::BString; + +/// The error returned when parsing References/refs from the server response. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + Id(#[from] git_hash::decode::Error), + #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] + MalformedSymref { symref: BString }, + #[error("{0:?} could not be parsed. A V1 ref line should be ' '.")] + MalformedV1RefLine(String), + #[error("{0:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'.")] + MalformedV2RefLine(String), + #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")] + UnkownAttribute { attribute: String, line: String }, + #[error("{message}")] + InvariantViolation { message: &'static str }, +} + +/// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +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 hash of the tag the ref points to. + tag: git_hash::ObjectId, + /// The hash of the object the `tag` points to. + object: git_hash::ObjectId, + }, + /// A ref pointing to a commit object + Direct { + /// The path at which the ref is located, like `/refs/heads/main`. + path: 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 path of the ref the symbolic ref points to, see issue [#205] for details + /// + /// [#205]: https://github.com/Byron/gitoxide/issues/205 + target: BString, + /// The hash of the object the `target` ref points to. + object: git_hash::ObjectId, + }, +} + +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) { + match self { + Ref::Direct { path, object, .. } + | Ref::Peeled { path, tag: object, .. } // the tag acts as reference + | Ref::Symbolic { path, object, .. } => (path, object), + } + } +} + +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +pub(crate) mod shared; + +#[cfg(feature = "async-client")] +mod async_io; +#[cfg(feature = "async-client")] +pub use async_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs}; + +#[cfg(feature = "blocking-client")] +mod blocking_io; +#[cfg(feature = "blocking-client")] +pub use blocking_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs}; diff --git a/git-protocol/src/fetch/refs/shared.rs b/git-protocol/src/fetch/refs/shared.rs new file mode 100644 index 00000000000..0b8ba15e9d6 --- /dev/null +++ b/git-protocol/src/fetch/refs/shared.rs @@ -0,0 +1,207 @@ +use bstr::{BString, ByteSlice}; + +use crate::fetch::{refs, Ref}; + +impl From for Ref { + fn from(v: InternalRef) -> Self { + match v { + InternalRef::Symbolic { + path, + target: Some(target), + object, + } => Ref::Symbolic { 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 }, + InternalRef::SymbolicForLookup { .. } => { + unreachable!("this case should have been removed during processing") + } + } + } +} + +#[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))] +pub(crate) enum InternalRef { + /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit + Peeled { + path: BString, + tag: git_hash::ObjectId, + object: git_hash::ObjectId, + }, + /// A ref pointing to a commit object + Direct { path: BString, object: git_hash::ObjectId }, + /// A symbolic ref pointing to `target` ref, which in turn points to an `object` + Symbolic { + path: BString, + /// It is `None` if the target is unreachable as it points to another namespace than the one is currently set + /// on the server (i.e. based on the repository at hand or the user performing the operation). + /// + /// The latter is more of an edge case, please [this issue][#205] for details. + target: Option, + object: git_hash::ObjectId, + }, + /// extracted from V1 capabilities, which contain some important symbolic refs along with their targets + /// These don't contain the Id + SymbolicForLookup { path: BString, target: Option }, +} + +impl InternalRef { + fn unpack_direct(self) -> Option<(BString, git_hash::ObjectId)> { + match self { + InternalRef::Direct { path, object } => Some((path, object)), + _ => None, + } + } + fn lookup_symbol_has_path(&self, predicate_path: &str) -> bool { + matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path) + } +} + +pub(crate) fn from_capabilities<'a>( + capabilities: impl Iterator>, +) -> Result, refs::Error> { + let mut out_refs = Vec::new(); + let symref_values = capabilities.filter_map(|c| { + if c.name() == b"symref".as_bstr() { + c.value().map(ToOwned::to_owned) + } else { + None + } + }); + for symref in symref_values { + let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| refs::Error::MalformedSymref { + symref: symref.to_owned(), + })?); + if left.is_empty() || right.is_empty() { + return Err(refs::Error::MalformedSymref { + symref: symref.to_owned(), + }); + } + out_refs.push(InternalRef::SymbolicForLookup { + path: left.into(), + target: match &right[1..] { + b"(null)" => None, + name => Some(name.into()), + }, + }) + } + Ok(out_refs) +} + +pub(in crate::fetch::refs) fn parse_v1( + num_initial_out_refs: usize, + out_refs: &mut Vec, + line: &str, +) -> Result<(), refs::Error> { + let trimmed = line.trim_end(); + let (hex_hash, path) = trimmed.split_at( + trimmed + .find(' ') + .ok_or_else(|| refs::Error::MalformedV1RefLine(trimmed.to_owned()))?, + ); + let path = &path[1..]; + if path.is_empty() { + return Err(refs::Error::MalformedV1RefLine(trimmed.to_owned())); + } + match path.strip_suffix("^{}") { + Some(stripped) => { + let (previous_path, tag) = + out_refs + .pop() + .and_then(InternalRef::unpack_direct) + .ok_or(refs::Error::InvariantViolation { + message: "Expecting peeled refs to be preceded by direct refs", + })?; + if previous_path != stripped { + return Err(refs::Error::InvariantViolation { + message: "Expecting peeled refs to have the same base path as the previous, unpeeled one", + }); + } + out_refs.push(InternalRef::Peeled { + path: previous_path, + tag, + object: git_hash::ObjectId::from_hex(hex_hash.as_bytes())?, + }); + } + None => { + let object = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?; + match out_refs + .iter() + .take(num_initial_out_refs) + .position(|r| r.lookup_symbol_has_path(path)) + { + Some(position) => match out_refs.swap_remove(position) { + InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic { + path: path.into(), + object, + target, + }), + _ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"), + }, + None => out_refs.push(InternalRef::Direct { + object, + path: path.into(), + }), + }; + } + } + Ok(()) +} + +pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { + let trimmed = line.trim_end(); + let mut tokens = trimmed.splitn(3, ' '); + match (tokens.next(), tokens.next()) { + (Some(hex_hash), Some(path)) => { + let id = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?; + if path.is_empty() { + return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); + } + Ok(if let Some(attribute) = tokens.next() { + let mut tokens = attribute.splitn(2, ':'); + match (tokens.next(), tokens.next()) { + (Some(attribute), Some(value)) => { + if value.is_empty() { + return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); + } + match attribute { + "peeled" => Ref::Peeled { + path: path.into(), + object: git_hash::ObjectId::from_hex(value.as_bytes())?, + tag: id, + }, + "symref-target" => match value { + "(null)" => Ref::Direct { + path: path.into(), + object: id, + }, + name => Ref::Symbolic { + path: path.into(), + object: id, + target: name.into(), + }, + }, + _ => { + return Err(refs::Error::UnkownAttribute { + attribute: attribute.to_owned(), + line: trimmed.to_owned(), + }) + } + } + } + _ => return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), + } + } else { + Ref::Direct { + object: id, + path: path.into(), + } + }) + } + _ => Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), + } +} From d2de51d65f9b4ab895e19cc1b307e42a9bb4bbd8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 20:40:44 +0800 Subject: [PATCH 080/125] feat: add `fetch::refs()` method to invoke `ls-refs` for V2 and parse the result (#450) --- git-protocol/src/fetch/error.rs | 2 +- git-protocol/src/fetch/handshake.rs | 2 +- git-protocol/src/fetch/refs/async_io.rs | 6 +- git-protocol/src/fetch/refs/blocking_io.rs | 5 +- git-protocol/src/fetch/refs/function.rs | 60 ++++++++++++++++++++ git-protocol/src/fetch/refs/mod.rs | 65 +++++++++++++++------- git-protocol/src/fetch/refs/shared.rs | 30 +++++----- git-protocol/src/fetch_fn.rs | 6 +- 8 files changed, 132 insertions(+), 44 deletions(-) create mode 100644 git-protocol/src/fetch/refs/function.rs diff --git a/git-protocol/src/fetch/error.rs b/git-protocol/src/fetch/error.rs index f02b4bf7a9c..5ab02ff5966 100644 --- a/git-protocol/src/fetch/error.rs +++ b/git-protocol/src/fetch/error.rs @@ -18,7 +18,7 @@ pub enum Error { #[error("A symref 'capability' is expected to have a value")] SymrefWithoutValue, #[error(transparent)] - Ref(#[from] refs::Error), + Ref(#[from] refs::parse::Error), #[error(transparent)] Response(#[from] response::Error), } diff --git a/git-protocol/src/fetch/handshake.rs b/git-protocol/src/fetch/handshake.rs index 41891cded02..c1cf5212455 100644 --- a/git-protocol/src/fetch/handshake.rs +++ b/git-protocol/src/fetch/handshake.rs @@ -27,7 +27,7 @@ mod error { #[error("The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side")] TransportProtocolPolicyViolation { actual_version: git_transport::Protocol }, #[error(transparent)] - ParseRefs(#[from] refs::Error), + ParseRefs(#[from] refs::parse::Error), } } pub use error::Error; diff --git a/git-protocol/src/fetch/refs/async_io.rs b/git-protocol/src/fetch/refs/async_io.rs index dcaff04fe0f..3fa1a99ce1b 100644 --- a/git-protocol/src/fetch/refs/async_io.rs +++ b/git-protocol/src/fetch/refs/async_io.rs @@ -1,10 +1,10 @@ use futures_io::AsyncBufRead; use futures_lite::AsyncBufReadExt; -use crate::fetch::{refs, Ref}; +use crate::fetch::{refs, refs::parse::Error, Ref}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. -pub async fn from_v2_refs(in_refs: &mut (dyn AsyncBufRead + Unpin)) -> Result, refs::Error> { +pub async fn from_v2_refs(in_refs: &mut (dyn AsyncBufRead + Unpin)) -> Result, Error> { let mut out_refs = Vec::new(); let mut line = String::new(); loop { @@ -29,7 +29,7 @@ pub async fn from_v2_refs(in_refs: &mut (dyn AsyncBufRead + Unpin)) -> Result( in_refs: &mut (dyn AsyncBufRead + Unpin), capabilities: impl Iterator>, -) -> Result, refs::Error> { +) -> Result, refs::parse::Error> { let mut out_refs = refs::shared::from_capabilities(capabilities)?; let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); let mut line = String::new(); diff --git a/git-protocol/src/fetch/refs/blocking_io.rs b/git-protocol/src/fetch/refs/blocking_io.rs index 10d592f3e87..af2130bdc0c 100644 --- a/git-protocol/src/fetch/refs/blocking_io.rs +++ b/git-protocol/src/fetch/refs/blocking_io.rs @@ -1,9 +1,10 @@ use std::io; +use crate::fetch::refs::parse::Error; use crate::fetch::{refs, Ref}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. -pub fn from_v2_refs(in_refs: &mut dyn io::BufRead) -> Result, refs::Error> { +pub fn from_v2_refs(in_refs: &mut dyn io::BufRead) -> Result, Error> { let mut out_refs = Vec::new(); let mut line = String::new(); loop { @@ -28,7 +29,7 @@ pub fn from_v2_refs(in_refs: &mut dyn io::BufRead) -> Result, refs::Err pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( in_refs: &mut dyn io::BufRead, capabilities: impl Iterator>, -) -> Result, refs::Error> { +) -> Result, Error> { let mut out_refs = refs::shared::from_capabilities(capabilities)?; let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); let mut line = String::new(); diff --git a/git-protocol/src/fetch/refs/function.rs b/git-protocol/src/fetch/refs/function.rs new file mode 100644 index 00000000000..37b23ff4c59 --- /dev/null +++ b/git-protocol/src/fetch/refs/function.rs @@ -0,0 +1,60 @@ +use super::Error; +use crate::fetch::refs::from_v2_refs; +use crate::fetch::{Command, LsRefsAction, Ref}; +use crate::fetch_fn::indicate_end_of_interaction; +use bstr::BString; +use git_features::progress::Progress; +use git_transport::client::{Capabilities, Transport, TransportV2Ext}; +use git_transport::Protocol; +use maybe_async::maybe_async; + +/// Invoke an ls-refs command on `transport` (assuming `protocol_version` 2 or panic), which requires a prior handshake that yielded +/// server `capabilities`. `prepare_ls_refs(arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback. +#[maybe_async] +pub async fn refs( + mut transport: impl Transport, + protocol_version: Protocol, + capabilities: &Capabilities, + mut prepare_ls_refs: impl FnMut( + &Capabilities, + &mut Vec, + &mut Vec<(&str, Option<&str>)>, + ) -> std::io::Result, + progress: &mut impl Progress, +) -> Result, Error> { + assert_eq!( + protocol_version, + Protocol::V2, + "Only V2 needs a separate request to get specific refs" + ); + + let ls_refs = Command::LsRefs; + let mut ls_features = ls_refs.default_features(protocol_version, &capabilities); + let mut ls_args = ls_refs.initial_arguments(&ls_features); + let refs = match prepare_ls_refs(&capabilities, &mut ls_args, &mut ls_features) { + Ok(LsRefsAction::Skip) => Vec::new(), + Ok(LsRefsAction::Continue) => { + ls_refs.validate_argument_prefixes_or_panic(protocol_version, &capabilities, &ls_args, &ls_features); + + progress.step(); + progress.set_name("list refs"); + let mut remote_refs = transport + .invoke( + ls_refs.as_str(), + ls_features.into_iter(), + if ls_args.is_empty() { + None + } else { + Some(ls_args.into_iter()) + }, + ) + .await?; + from_v2_refs(&mut remote_refs).await? + } + Err(err) => { + indicate_end_of_interaction(transport).await?; + return Err(err.into()); + } + }; + Ok(refs) +} diff --git a/git-protocol/src/fetch/refs/mod.rs b/git-protocol/src/fetch/refs/mod.rs index 9ebf02154fa..4a0c24702a2 100644 --- a/git-protocol/src/fetch/refs/mod.rs +++ b/git-protocol/src/fetch/refs/mod.rs @@ -1,25 +1,47 @@ -use std::io; - use bstr::BString; -/// The error returned when parsing References/refs from the server response. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Id(#[from] git_hash::decode::Error), - #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] - MalformedSymref { symref: BString }, - #[error("{0:?} could not be parsed. A V1 ref line should be ' '.")] - MalformedV1RefLine(String), - #[error("{0:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'.")] - MalformedV2RefLine(String), - #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")] - UnkownAttribute { attribute: String, line: String }, - #[error("{message}")] - InvariantViolation { message: &'static str }, +mod error { + use crate::fetch::refs::parse; + + /// The error returned by [refs()][super::refs()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Transport(#[from] git_transport::client::Error), + #[error(transparent)] + Parse(#[from] parse::Error), + } +} +pub use error::Error; + +/// +pub mod parse { + use bstr::BString; + + /// The error returned when parsing References/refs from the server response. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Id(#[from] git_hash::decode::Error), + #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] + MalformedSymref { symref: BString }, + #[error("{0:?} could not be parsed. A V1 ref line should be ' '.")] + MalformedV1RefLine(String), + #[error( + "{0:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + )] + MalformedV2RefLine(String), + #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")] + UnkownAttribute { attribute: String, line: String }, + #[error("{message}")] + InvariantViolation { message: &'static str }, + } } /// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack. @@ -67,6 +89,9 @@ impl Ref { } } +pub(crate) mod function; +pub use function::refs; + #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub(crate) mod shared; diff --git a/git-protocol/src/fetch/refs/shared.rs b/git-protocol/src/fetch/refs/shared.rs index 0b8ba15e9d6..46a8ad53620 100644 --- a/git-protocol/src/fetch/refs/shared.rs +++ b/git-protocol/src/fetch/refs/shared.rs @@ -1,6 +1,6 @@ use bstr::{BString, ByteSlice}; -use crate::fetch::{refs, Ref}; +use crate::fetch::{refs::parse::Error, Ref}; impl From for Ref { fn from(v: InternalRef) -> Self { @@ -63,7 +63,7 @@ impl InternalRef { pub(crate) fn from_capabilities<'a>( capabilities: impl Iterator>, -) -> Result, refs::Error> { +) -> Result, Error> { let mut out_refs = Vec::new(); let symref_values = capabilities.filter_map(|c| { if c.name() == b"symref".as_bstr() { @@ -73,11 +73,11 @@ pub(crate) fn from_capabilities<'a>( } }); for symref in symref_values { - let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| refs::Error::MalformedSymref { + let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| Error::MalformedSymref { symref: symref.to_owned(), })?); if left.is_empty() || right.is_empty() { - return Err(refs::Error::MalformedSymref { + return Err(Error::MalformedSymref { symref: symref.to_owned(), }); } @@ -96,16 +96,16 @@ pub(in crate::fetch::refs) fn parse_v1( num_initial_out_refs: usize, out_refs: &mut Vec, line: &str, -) -> Result<(), refs::Error> { +) -> Result<(), Error> { let trimmed = line.trim_end(); let (hex_hash, path) = trimmed.split_at( trimmed .find(' ') - .ok_or_else(|| refs::Error::MalformedV1RefLine(trimmed.to_owned()))?, + .ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned()))?, ); let path = &path[1..]; if path.is_empty() { - return Err(refs::Error::MalformedV1RefLine(trimmed.to_owned())); + return Err(Error::MalformedV1RefLine(trimmed.to_owned())); } match path.strip_suffix("^{}") { Some(stripped) => { @@ -113,11 +113,11 @@ pub(in crate::fetch::refs) fn parse_v1( out_refs .pop() .and_then(InternalRef::unpack_direct) - .ok_or(refs::Error::InvariantViolation { + .ok_or(Error::InvariantViolation { message: "Expecting peeled refs to be preceded by direct refs", })?; if previous_path != stripped { - return Err(refs::Error::InvariantViolation { + return Err(Error::InvariantViolation { message: "Expecting peeled refs to have the same base path as the previous, unpeeled one", }); } @@ -152,21 +152,21 @@ pub(in crate::fetch::refs) fn parse_v1( Ok(()) } -pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { +pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { let trimmed = line.trim_end(); let mut tokens = trimmed.splitn(3, ' '); match (tokens.next(), tokens.next()) { (Some(hex_hash), Some(path)) => { let id = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?; if path.is_empty() { - return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); + return Err(Error::MalformedV2RefLine(trimmed.to_owned())); } Ok(if let Some(attribute) = tokens.next() { let mut tokens = attribute.splitn(2, ':'); match (tokens.next(), tokens.next()) { (Some(attribute), Some(value)) => { if value.is_empty() { - return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); + return Err(Error::MalformedV2RefLine(trimmed.to_owned())); } match attribute { "peeled" => Ref::Peeled { @@ -186,14 +186,14 @@ pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { }, }, _ => { - return Err(refs::Error::UnkownAttribute { + return Err(Error::UnkownAttribute { attribute: attribute.to_owned(), line: trimmed.to_owned(), }) } } } - _ => return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), + _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned())), } } else { Ref::Direct { @@ -202,6 +202,6 @@ pub(in crate::fetch::refs) fn parse_v2(line: &str) -> Result { } }) } - _ => Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), + _ => Err(Error::MalformedV2RefLine(trimmed.to_owned())), } } diff --git a/git-protocol/src/fetch_fn.rs b/git-protocol/src/fetch_fn.rs index 97590bc0894..030c6d55fe7 100644 --- a/git-protocol/src/fetch_fn.rs +++ b/git-protocol/src/fetch_fn.rs @@ -121,7 +121,7 @@ where return if matches!(protocol_version, git_transport::Protocol::V1) || matches!(fetch_mode, FetchConnection::TerminateOnSuccessfulCompletion) { - indicate_end_of_interaction(transport).await + indicate_end_of_interaction(transport).await.map_err(Into::into) } else { Ok(()) }; @@ -174,7 +174,9 @@ where } #[maybe_async] -async fn indicate_end_of_interaction(mut transport: impl client::Transport) -> Result<(), Error> { +pub(crate) async fn indicate_end_of_interaction( + mut transport: impl client::Transport, +) -> Result<(), git_transport::client::Error> { // An empty request marks the (early) end of the interaction. Only relevant in stateful transports though. if transport.connection_persists_across_multiple_requests() { transport From 37504e64076e6e32730d21bae6ee2044a5dc155f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 20:49:25 +0800 Subject: [PATCH 081/125] Use `fetch::refs()` function in `fetch()` (#450) --- git-protocol/src/fetch/error.rs | 2 +- git-protocol/src/fetch/mod.rs | 1 + git-protocol/src/fetch/refs/mod.rs | 1 - git-protocol/src/fetch_fn.rs | 49 ++++++------------------------ git-protocol/tests/fetch/v2.rs | 4 +-- 5 files changed, 13 insertions(+), 44 deletions(-) diff --git a/git-protocol/src/fetch/error.rs b/git-protocol/src/fetch/error.rs index 5ab02ff5966..ceee02d874b 100644 --- a/git-protocol/src/fetch/error.rs +++ b/git-protocol/src/fetch/error.rs @@ -18,7 +18,7 @@ pub enum Error { #[error("A symref 'capability' is expected to have a value")] SymrefWithoutValue, #[error(transparent)] - Ref(#[from] refs::parse::Error), + Refs(#[from] refs::Error), #[error(transparent)] Response(#[from] response::Error), } diff --git a/git-protocol/src/fetch/mod.rs b/git-protocol/src/fetch/mod.rs index 38cda8e1354..962ae31c7ff 100644 --- a/git-protocol/src/fetch/mod.rs +++ b/git-protocol/src/fetch/mod.rs @@ -20,6 +20,7 @@ mod error; pub use error::Error; /// pub mod refs; +pub use refs::function::refs; pub use refs::Ref; /// pub mod response; diff --git a/git-protocol/src/fetch/refs/mod.rs b/git-protocol/src/fetch/refs/mod.rs index 4a0c24702a2..42f2d7073af 100644 --- a/git-protocol/src/fetch/refs/mod.rs +++ b/git-protocol/src/fetch/refs/mod.rs @@ -90,7 +90,6 @@ impl Ref { } pub(crate) mod function; -pub use function::refs; #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub(crate) mod shared; diff --git a/git-protocol/src/fetch_fn.rs b/git-protocol/src/fetch_fn.rs index 030c6d55fe7..6d11a29d8da 100644 --- a/git-protocol/src/fetch_fn.rs +++ b/git-protocol/src/fetch_fn.rs @@ -1,11 +1,11 @@ use git_features::progress::Progress; -use git_transport::{client, client::TransportV2Ext}; +use git_transport::client; use maybe_async::maybe_async; use crate::fetch::handshake; use crate::{ credentials, - fetch::{refs, Action, Arguments, Command, Delegate, Error, LsRefsAction, Response}, + fetch::{Action, Arguments, Command, Delegate, Error, Response}, }; /// A way to indicate how to treat the connection underlying the transport, potentially allowing to reuse it. @@ -72,45 +72,14 @@ where let refs = match refs { Some(refs) => refs, None => { - assert_eq!( + crate::fetch::refs( + &mut transport, protocol_version, - git_transport::Protocol::V2, - "Only V2 needs a separate request to get specific refs" - ); - - let ls_refs = Command::LsRefs; - let mut ls_features = ls_refs.default_features(protocol_version, &capabilities); - let mut ls_args = ls_refs.initial_arguments(&ls_features); - match delegate.prepare_ls_refs(&capabilities, &mut ls_args, &mut ls_features) { - Ok(LsRefsAction::Skip) => Vec::new(), - Ok(LsRefsAction::Continue) => { - ls_refs.validate_argument_prefixes_or_panic( - protocol_version, - &capabilities, - &ls_args, - &ls_features, - ); - - progress.step(); - progress.set_name("list refs"); - let mut remote_refs = transport - .invoke( - ls_refs.as_str(), - ls_features.into_iter(), - if ls_args.is_empty() { - None - } else { - Some(ls_args.into_iter()) - }, - ) - .await?; - refs::from_v2_refs(&mut remote_refs).await? - } - Err(err) => { - indicate_end_of_interaction(transport).await?; - return Err(err.into()); - } - } + &capabilities, + |a, b, c| delegate.prepare_ls_refs(a, b, c), + &mut progress, + ) + .await? } }; diff --git a/git-protocol/tests/fetch/v2.rs b/git-protocol/tests/fetch/v2.rs index e4dd73e50e8..3f6d9c6eb50 100644 --- a/git-protocol/tests/fetch/v2.rs +++ b/git-protocol/tests/fetch/v2.rs @@ -131,11 +131,11 @@ async fn ls_remote_abort_in_prep_ls_refs() -> crate::Result { b"0044git-upload-pack does/not/matter\x00\x00version=2\x00value-only\x00key=value\x000000".as_bstr() ); match err { - fetch::Error::Io(err) => { + fetch::Error::Refs(fetch::refs::Error::Io(err)) => { assert_eq!(err.kind(), std::io::ErrorKind::Other); assert_eq!(err.get_ref().expect("other error").to_string(), "hello world"); } - _ => panic!("should not have another error here"), + err => panic!("should not have another error here, got: {}", err), } Ok(()) } From f48ac49e167c93b4d7937b4c538b0d9389e7bbe8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 20:50:09 +0800 Subject: [PATCH 082/125] thanks clippy --- git-protocol/src/fetch/refs/function.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git-protocol/src/fetch/refs/function.rs b/git-protocol/src/fetch/refs/function.rs index 37b23ff4c59..6f367d3af6a 100644 --- a/git-protocol/src/fetch/refs/function.rs +++ b/git-protocol/src/fetch/refs/function.rs @@ -29,12 +29,12 @@ pub async fn refs( ); let ls_refs = Command::LsRefs; - let mut ls_features = ls_refs.default_features(protocol_version, &capabilities); + let mut ls_features = ls_refs.default_features(protocol_version, capabilities); let mut ls_args = ls_refs.initial_arguments(&ls_features); - let refs = match prepare_ls_refs(&capabilities, &mut ls_args, &mut ls_features) { + let refs = match prepare_ls_refs(capabilities, &mut ls_args, &mut ls_features) { Ok(LsRefsAction::Skip) => Vec::new(), Ok(LsRefsAction::Continue) => { - ls_refs.validate_argument_prefixes_or_panic(protocol_version, &capabilities, &ls_args, &ls_features); + ls_refs.validate_argument_prefixes_or_panic(protocol_version, capabilities, &ls_args, &ls_features); progress.step(); progress.set_name("list refs"); From a6105cb30dd5f1ff3126320984d3b05b4b939587 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 20:58:29 +0800 Subject: [PATCH 083/125] fix docs (#450) --- git-protocol/src/fetch/error.rs | 4 +--- git-protocol/src/fetch/handshake.rs | 2 +- git-protocol/src/fetch/refs/mod.rs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/git-protocol/src/fetch/error.rs b/git-protocol/src/fetch/error.rs index ceee02d874b..bb7bce749af 100644 --- a/git-protocol/src/fetch/error.rs +++ b/git-protocol/src/fetch/error.rs @@ -5,7 +5,7 @@ use git_transport::client; use crate::fetch::handshake; use crate::fetch::{refs, response}; -/// The error used in [`fetch()`][super::fetch]. +/// The error used in [`fetch()`][crate::fetch()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -15,8 +15,6 @@ pub enum Error { Io(#[from] io::Error), #[error(transparent)] Transport(#[from] client::Error), - #[error("A symref 'capability' is expected to have a value")] - SymrefWithoutValue, #[error(transparent)] Refs(#[from] refs::Error), #[error(transparent)] diff --git a/git-protocol/src/fetch/handshake.rs b/git-protocol/src/fetch/handshake.rs index c1cf5212455..c105f812126 100644 --- a/git-protocol/src/fetch/handshake.rs +++ b/git-protocol/src/fetch/handshake.rs @@ -16,7 +16,7 @@ mod error { use crate::fetch::refs; use git_transport::client; - /// The error returned by [`handshake()`][super::handshake()]. + /// The error returned by [`handshake()`][crate::fetch::handshake()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { diff --git a/git-protocol/src/fetch/refs/mod.rs b/git-protocol/src/fetch/refs/mod.rs index 42f2d7073af..a847d6107a4 100644 --- a/git-protocol/src/fetch/refs/mod.rs +++ b/git-protocol/src/fetch/refs/mod.rs @@ -3,7 +3,7 @@ use bstr::BString; mod error { use crate::fetch::refs::parse; - /// The error returned by [refs()][super::refs()]. + /// The error returned by [refs()][crate::fetch::refs()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { From d476432395bba27cf0a860055395f26dc53b8f8a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 16 Aug 2022 21:31:44 +0800 Subject: [PATCH 084/125] fix journey test expectations (#450) The error is definitely less confusing now. --- .../receive/file-v-any-no-output-non-existing-single-ref | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-non-existing-single-ref b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-non-existing-single-ref index 6bed155babf..62d4a6f5c56 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-non-existing-single-ref +++ b/tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-non-existing-single-ref @@ -1,5 +1 @@ -Error: The server response could not be parsed - -Caused by: - 0: Upload pack reported an error - 1: unknown ref refs/heads/does-not-exist \ No newline at end of file +Error: unknown ref refs/heads/does-not-exist \ No newline at end of file From 2a881ca1357897c049592d94c58ee1f005b47787 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 18 Aug 2022 16:57:21 +0800 Subject: [PATCH 085/125] prepare for refs implementation, it won't need a delegate anymore. (#450) --- git-repository/src/remote/connect.rs | 10 +-- git-repository/src/remote/connection/mod.rs | 17 +++-- git-repository/src/remote/connection/refs.rs | 68 +------------------- git-repository/tests/remote/connect.rs | 2 +- 4 files changed, 15 insertions(+), 82 deletions(-) diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 5c722d11fba..5dde3817e3c 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -27,7 +27,7 @@ impl<'repo> Remote<'repo> { /// /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()]. /// It's meant to be used when async operation is needed with runtimes of the user's choice. - pub fn into_connection_with_transport(self, transport: T) -> Connection<'repo, T> + pub fn to_connection_with_transport(&self, transport: T) -> Connection<'_, 'repo, T> where T: Transport, { @@ -39,10 +39,10 @@ impl<'repo> Remote<'repo> { /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. #[cfg(feature = "blocking-network-client")] - pub fn into_connection( - self, + pub fn connect( + &self, direction: crate::remote::Direction, - ) -> Result>, Error> { + ) -> Result>, Error> { use git_protocol::transport::Protocol; let protocol = self .repo @@ -65,6 +65,6 @@ impl<'repo> Remote<'repo> { let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); let transport = git_protocol::transport::connect(url, protocol)?; - Ok(self.into_connection_with_transport(transport)) + Ok(self.to_connection_with_transport(transport)) } } diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index 32b09c4fb6a..44c84262a3f 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -2,8 +2,8 @@ use crate::Remote; -pub struct Connection<'repo, T> { - pub(crate) remote: Remote<'repo>, +pub struct Connection<'a, 'repo, T> { + pub(crate) remote: &'a Remote<'repo>, pub(crate) transport: T, } @@ -11,16 +11,15 @@ mod access { use crate::remote::Connection; use crate::Remote; - /// Conversion - impl<'repo, T> Connection<'repo, T> { - /// Dissolve this instance into its parts, `(Remote, Transport)`, the inverse of - /// [`into_connection_with_transport()`][Remote::into_connection_with_transport()]. - pub fn into_parts(self) -> (Remote<'repo>, T) { - (self.remote, self.transport) + /// Access and conversion + impl<'a, 'repo, T> Connection<'a, 'repo, T> { + /// Obtain the transport from this instance. + pub fn into_transport(self) -> T { + self.transport } /// Drop the transport and additional state to regain the original remote. - pub fn into_remote(self) -> Remote<'repo> { + pub fn remote(&self) -> &Remote<'repo> { self.remote } } diff --git a/git-repository/src/remote/connection/refs.rs b/git-repository/src/remote/connection/refs.rs index 01efcbdde1a..22870815921 100644 --- a/git-repository/src/remote/connection/refs.rs +++ b/git-repository/src/remote/connection/refs.rs @@ -1,73 +1,7 @@ use crate::remote::Connection; use git_protocol::transport::client::Transport; -struct Delegate { - refs: Vec, -} - -mod delegate { - use super::Delegate; - use git_protocol::fetch::Action; - use git_protocol::transport; - - impl git_protocol::fetch::DelegateBlocking for Delegate { - fn prepare_fetch( - &mut self, - _version: transport::Protocol, - _server: &transport::client::Capabilities, - _features: &mut Vec<(&str, Option<&str>)>, - refs: &[git_protocol::fetch::Ref], - ) -> std::io::Result { - self.refs = refs.into(); - Ok(Action::Cancel) - } - - fn negotiate( - &mut self, - _refs: &[git_protocol::fetch::Ref], - _arguments: &mut git_protocol::fetch::Arguments, - _previous_response: Option<&git_protocol::fetch::Response>, - ) -> std::io::Result { - unreachable!("not to be called due to Action::Close in `prepare_fetch`") - } - } - - #[cfg(feature = "blocking-network-client")] - mod blocking_io { - impl git_protocol::fetch::Delegate for super::Delegate { - fn receive_pack( - &mut self, - _input: impl std::io::BufRead, - _progress: impl git_features::progress::Progress, - _refs: &[git_protocol::fetch::Ref], - _previous_response: &git_protocol::fetch::Response, - ) -> std::io::Result<()> { - unreachable!("not called for ls-refs") - } - } - } - - #[cfg(feature = "async-network-client")] - mod async_io { - use git_protocol::async_trait::async_trait; - use git_protocol::futures_io::AsyncBufRead; - - #[async_trait(? Send)] - impl git_protocol::fetch::Delegate for super::Delegate { - async fn receive_pack( - &mut self, - _input: impl AsyncBufRead + Unpin + 'async_trait, - _progress: impl git_features::progress::Progress, - _refs: &[git_protocol::fetch::Ref], - _previous_response: &git_protocol::fetch::Response, - ) -> std::io::Result<()> { - unreachable!("not called for ls-refs") - } - } - } -} - -impl<'repo, T> Connection<'repo, T> +impl<'a, 'repo, T> Connection<'a, 'repo, T> where T: Transport, { diff --git a/git-repository/tests/remote/connect.rs b/git-repository/tests/remote/connect.rs index 9c483cd41f4..fc39e123dd2 100644 --- a/git-repository/tests/remote/connect.rs +++ b/git-repository/tests/remote/connect.rs @@ -7,6 +7,6 @@ mod blocking { fn ls_refs() { let repo = remote::repo("clone"); let remote = repo.find_remote("origin").unwrap(); - let _connection = remote.into_connection(Fetch).unwrap(); + let _connection = remote.connect(Fetch).unwrap(); } } From d6b4878d9832a0279e0dd19c1ca520a282289e69 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 19 Aug 2022 15:06:54 +0800 Subject: [PATCH 086/125] feat: re-export maybe_async (#450) That way, API users can also provide dynamically sync or async APIs. --- git-protocol/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/git-protocol/src/lib.rs b/git-protocol/src/lib.rs index 0ee520cb583..dc2ea0ff5ac 100644 --- a/git-protocol/src/lib.rs +++ b/git-protocol/src/lib.rs @@ -14,6 +14,7 @@ pub use async_trait; #[cfg(feature = "futures-io")] pub use futures_io; +pub use maybe_async; pub use git_credentials as credentials; /// A convenience export allowing users of git-protocol to use the transport layer without their own cargo dependency. From 5220f9a59fb699e111342b076145a6899d36d433 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 19 Aug 2022 15:39:32 +0800 Subject: [PATCH 087/125] fix: Make async `conenct()` signature compatible with the blocking implementation. (#450) --- git-transport/src/client/async_io/connect.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/git-transport/src/client/async_io/connect.rs b/git-transport/src/client/async_io/connect.rs index 0bf3b196b33..2ebf347f6f5 100644 --- a/git-transport/src/client/async_io/connect.rs +++ b/git-transport/src/client/async_io/connect.rs @@ -15,7 +15,7 @@ pub(crate) mod function { pub async fn connect( url: Url, desired_version: crate::Protocol, - ) -> Result + ) -> Result, Error> where Url: TryInto, git_url::parse::Error: From, @@ -30,14 +30,16 @@ pub(crate) mod function { }); } let path = std::mem::take(&mut url.path); - git::Connection::new_tcp( - url.host().expect("host is present in url"), - url.port, - path, - desired_version, + Box::new( + git::Connection::new_tcp( + url.host().expect("host is present in url"), + url.port, + path, + desired_version, + ) + .await + .map_err(|e| Box::new(e) as Box)?, ) - .await - .map_err(|e| Box::new(e) as Box)? } scheme => return Err(Error::UnsupportedScheme(scheme)), }) From f30db4c683fbd0250dce8073b3b2f3bd13e67d83 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 19 Aug 2022 15:39:57 +0800 Subject: [PATCH 088/125] Make `Remote::connect()` both sync and async. (#450) --- Cargo.lock | 2 ++ Makefile | 1 + git-repository/Cargo.toml | 3 +++ git-repository/src/remote/connect.rs | 7 ++++--- git-repository/tests/remote/connect.rs | 4 ++-- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9df11830929..4b10c27c2da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1503,6 +1503,7 @@ name = "git-repository" version = "0.21.0" dependencies = [ "anyhow", + "async-std", "byte-unit", "clru", "document-features", @@ -1537,6 +1538,7 @@ dependencies = [ "git-worktree", "is_ci", "log", + "maybe-async", "regex", "serde", "serial_test 0.8.0", diff --git a/Makefile b/Makefile index 5e7036c634f..dd8ff939adf 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,7 @@ check: ## Build all code in suitable configurations cd git-protocol && if cargo check --all-features 2>/dev/null; then false; else true; fi cd git-repository && cargo check --no-default-features --features local \ && cargo check --no-default-features --features async-network-client \ + && cargo check --no-default-features --features async-network-client-async-std \ && cargo check --no-default-features --features blocking-network-client \ && cargo check --no-default-features --features blocking-network-client,blocking-http-transport \ && cargo check --no-default-features --features one-stop-shop \ diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index 149f6a68bb7..295f587697b 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -32,6 +32,8 @@ default = ["max-performance", "one-stop-shop"] ## Make `git-protocol` available along with an async client. async-network-client = ["git-protocol/async-client"] +## Use this if your crate uses `async-std` as runtime, and enable basic runtime integration when connecting to remote servers. +async-network-client-async-std = ["async-std", "async-network-client", "git-transport/async-std"] ## Make `git-protocol` available along with a blocking client. blocking-network-client = ["git-protocol/blocking-client"] ## Stacks with `blocking-network-client` to provide support for HTTP/S, and implies blocking networking as a whole. @@ -116,6 +118,7 @@ byte-unit = "4.0" log = "0.4.14" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} smallvec = "1.9.0" +async-std = { version = "1.12.0", optional = true } ## For use in rev-parse, which provides searching commits by running a regex on their message. ## diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 5dde3817e3c..b6740160944 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -38,8 +38,9 @@ impl<'repo> Remote<'repo> { } /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. - #[cfg(feature = "blocking-network-client")] - pub fn connect( + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] + #[git_protocol::maybe_async::maybe_async] + pub async fn connect( &self, direction: crate::remote::Direction, ) -> Result>, Error> { @@ -64,7 +65,7 @@ impl<'repo> Remote<'repo> { })?; let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); - let transport = git_protocol::transport::connect(url, protocol)?; + let transport = git_protocol::transport::connect(url, protocol).await?; Ok(self.to_connection_with_transport(transport)) } } diff --git a/git-repository/tests/remote/connect.rs b/git-repository/tests/remote/connect.rs index fc39e123dd2..fd9a2b4ff69 100644 --- a/git-repository/tests/remote/connect.rs +++ b/git-repository/tests/remote/connect.rs @@ -1,10 +1,10 @@ #[cfg(feature = "blocking-network-client")] -mod blocking { +mod blocking_io { use crate::remote; use git_repository::remote::Direction::Fetch; #[test] - fn ls_refs() { + fn refs() { let repo = remote::repo("clone"); let remote = repo.find_remote("origin").unwrap(); let _connection = remote.connect(Fetch).unwrap(); From 563e56f8f970f0bb0cf8a6404479422a398e712e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 19 Aug 2022 16:57:03 +0800 Subject: [PATCH 089/125] first vague implementation of ls-refs (#450) It fails, however, as the URL seems to not work with git-upload pack just yet. --- Cargo.lock | 1 - git-repository/src/remote/connect.rs | 21 ++++-- .../src/remote/connection/list_refs.rs | 67 +++++++++++++++++++ git-repository/src/remote/connection/mod.rs | 16 ++++- git-repository/src/remote/connection/refs.rs | 18 ----- .../tests/remote/{connect.rs => list_refs.rs} | 7 +- git-repository/tests/remote/mod.rs | 2 +- 7 files changed, 100 insertions(+), 32 deletions(-) create mode 100644 git-repository/src/remote/connection/list_refs.rs delete mode 100644 git-repository/src/remote/connection/refs.rs rename git-repository/tests/remote/{connect.rs => list_refs.rs} (56%) diff --git a/Cargo.lock b/Cargo.lock index 4b10c27c2da..602e87a2b13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1538,7 +1538,6 @@ dependencies = [ "git-worktree", "is_ci", "log", - "maybe-async", "regex", "serde", "serial_test 0.8.0", diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index b6740160944..100357bf4fc 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -1,7 +1,7 @@ #![allow(missing_docs)] -use crate::remote::Connection; -use crate::Remote; +use crate::remote::{connection, Connection}; +use crate::{Progress, Remote}; use git_protocol::transport::client::Transport; mod error { @@ -23,27 +23,34 @@ pub use error::Error; /// Establishing connections to remote hosts impl<'repo> Remote<'repo> { - /// Create a new connection using `transport` to communicate. + /// Create a new connection using `transport` to communicate, with `progress` to indicate changes. /// /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()]. /// It's meant to be used when async operation is needed with runtimes of the user's choice. - pub fn to_connection_with_transport(&self, transport: T) -> Connection<'_, 'repo, T> + pub fn to_connection_with_transport(&self, transport: T, progress: P) -> Connection<'_, 'repo, T, P> where T: Transport, + P: Progress, { Connection { remote: self, transport, + progress, + state: connection::State::Connected, } } /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] #[git_protocol::maybe_async::maybe_async] - pub async fn connect( + pub async fn connect

( &self, direction: crate::remote::Direction, - ) -> Result>, Error> { + progress: P, + ) -> Result, P>, Error> + where + P: Progress, + { use git_protocol::transport::Protocol; let protocol = self .repo @@ -66,6 +73,6 @@ impl<'repo> Remote<'repo> { let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); let transport = git_protocol::transport::connect(url, protocol).await?; - Ok(self.to_connection_with_transport(transport)) + Ok(self.to_connection_with_transport(transport, progress)) } } diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs new file mode 100644 index 00000000000..b9714ae62e1 --- /dev/null +++ b/git-repository/src/remote/connection/list_refs.rs @@ -0,0 +1,67 @@ +use crate::remote::{Connection, Direction}; +use git_protocol::transport::client::Transport; + +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), + } +} +use crate::remote::connection::State; +pub use error::Error; +use git_features::progress::Progress; + +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. + /// + /// This method is idempotent and only runs once. + #[git_protocol::maybe_async::maybe_async] + pub async fn list_refs(&mut self) -> Result<&[git_protocol::fetch::Ref], Error> { + match self.state { + State::Connected => { + let mut outcome = git_protocol::fetch::handshake( + &mut self.transport, + git_protocol::credentials::helper, + Vec::new(), + &mut self.progress, + )?; + 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? + } + }; + self.state = State::HandshakeWithRefs { outcome, refs }; + self.list_refs() + } + State::HandshakeWithRefs { ref refs, .. } => Ok(&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 44c84262a3f..6c806579801 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -2,9 +2,19 @@ use crate::Remote; -pub struct Connection<'a, 'repo, T> { +pub(crate) enum State { + Connected, + HandshakeWithRefs { + outcome: git_protocol::fetch::handshake::Outcome, + refs: Vec, + }, +} + +pub struct Connection<'a, 'repo, T, P> { pub(crate) remote: &'a Remote<'repo>, pub(crate) transport: T, + pub(crate) progress: P, + pub(crate) state: State, } mod access { @@ -12,7 +22,7 @@ mod access { use crate::Remote; /// Access and conversion - impl<'a, 'repo, T> Connection<'a, 'repo, T> { + impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { /// Obtain the transport from this instance. pub fn into_transport(self) -> T { self.transport @@ -25,4 +35,4 @@ mod access { } } -mod refs; +mod list_refs; diff --git a/git-repository/src/remote/connection/refs.rs b/git-repository/src/remote/connection/refs.rs deleted file mode 100644 index 22870815921..00000000000 --- a/git-repository/src/remote/connection/refs.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::remote::Connection; -use git_protocol::transport::client::Transport; - -impl<'a, 'repo, T> Connection<'a, 'repo, T> -where - T: Transport, -{ - /// 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 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 refs(&mut self) -> ! { - todo!() - } -} diff --git a/git-repository/tests/remote/connect.rs b/git-repository/tests/remote/list_refs.rs similarity index 56% rename from git-repository/tests/remote/connect.rs rename to git-repository/tests/remote/list_refs.rs index fd9a2b4ff69..4834855430c 100644 --- a/git-repository/tests/remote/connect.rs +++ b/git-repository/tests/remote/list_refs.rs @@ -1,12 +1,15 @@ #[cfg(feature = "blocking-network-client")] mod blocking_io { use crate::remote; + use git_features::progress; use git_repository::remote::Direction::Fetch; #[test] - fn refs() { + #[ignore] + fn all() { let repo = remote::repo("clone"); let remote = repo.find_remote("origin").unwrap(); - let _connection = remote.connect(Fetch).unwrap(); + let mut connection = remote.connect(Fetch, progress::Discard).unwrap(); + let _refs = connection.list_refs().unwrap(); } } diff --git a/git-repository/tests/remote/mod.rs b/git-repository/tests/remote/mod.rs index a7ad9428031..64aee6a0262 100644 --- a/git-repository/tests/remote/mod.rs +++ b/git-repository/tests/remote/mod.rs @@ -11,4 +11,4 @@ pub(crate) fn cow_str(s: &str) -> Cow { Cow::Borrowed(s) } -mod connect; +mod list_refs; From 581f8ae2313fa886d788feed74c10b4624e8de63 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 19 Aug 2022 19:30:55 +0800 Subject: [PATCH 090/125] thanks clippy --- git-repository/Cargo.toml | 4 ++-- git-repository/src/remote/connect.rs | 1 - git-repository/src/remote/connection/list_refs.rs | 10 +++++++--- git-repository/src/remote/mod.rs | 1 + 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index 2230d5bb01f..7e5e42c59f8 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -31,11 +31,11 @@ default = ["max-performance", "one-stop-shop"] #! Either `async-*` or `blocking-*` versions of these toggles may be enabled at a time. ## Make `git-protocol` available along with an async client. -async-network-client = ["git-protocol/async-client"] +async-network-client = ["git-protocol/async-client", "unstable"] ## Use this if your crate uses `async-std` as runtime, and enable basic runtime integration when connecting to remote servers. async-network-client-async-std = ["async-std", "async-network-client", "git-transport/async-std"] ## Make `git-protocol` available along with a blocking client. -blocking-network-client = ["git-protocol/blocking-client"] +blocking-network-client = ["git-protocol/blocking-client", "unstable"] ## Stacks with `blocking-network-client` to provide support for HTTP/S, and implies blocking networking as a whole. blocking-http-transport = ["git-transport/http-client-curl"] diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 100357bf4fc..93464d19d32 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -18,7 +18,6 @@ mod error { UnknownProtocol { given: BString }, } } - pub use error::Error; /// Establishing connections to remote hosts diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs index b9714ae62e1..afa45c1c20c 100644 --- a/git-repository/src/remote/connection/list_refs.rs +++ b/git-repository/src/remote/connection/list_refs.rs @@ -33,7 +33,8 @@ where git_protocol::credentials::helper, Vec::new(), &mut self.progress, - )?; + ) + .await?; let refs = match outcome.refs.take() { Some(refs) => refs, None => { @@ -48,9 +49,12 @@ where } }; self.state = State::HandshakeWithRefs { outcome, refs }; - self.list_refs() + match &self.state { + State::HandshakeWithRefs { refs, .. } => Ok(refs), + _ => unreachable!(), + } } - State::HandshakeWithRefs { ref refs, .. } => Ok(&refs), + State::HandshakeWithRefs { ref refs, .. } => Ok(refs), } } diff --git a/git-repository/src/remote/mod.rs b/git-repository/src/remote/mod.rs index 76670cca9dc..9fc9666c66a 100644 --- a/git-repository/src/remote/mod.rs +++ b/git-repository/src/remote/mod.rs @@ -25,6 +25,7 @@ pub use errors::find; /// pub mod init; +/// #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] pub mod connect; From 5ba9e1d20a8d523571a153f9f4e3e1a5285335b5 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 20 Aug 2022 16:45:17 +0800 Subject: [PATCH 091/125] feat: provide a function to gracefully shut donw a fetch transport (#450) --- git-protocol/src/fetch/mod.rs | 18 ++++++++++++++++++ git-protocol/src/fetch/refs/function.rs | 3 +-- git-protocol/src/fetch_fn.rs | 16 +--------------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/git-protocol/src/fetch/mod.rs b/git-protocol/src/fetch/mod.rs index 962ae31c7ff..effa154c6c1 100644 --- a/git-protocol/src/fetch/mod.rs +++ b/git-protocol/src/fetch/mod.rs @@ -30,5 +30,23 @@ pub use response::Response; pub mod handshake; pub use handshake::function::handshake; +/// Send a message to indicate the remote side that there is nothing more to expect from us, indicating a graceful shutdown. +#[maybe_async::maybe_async] +pub async fn indicate_end_of_interaction( + mut transport: impl git_transport::client::Transport, +) -> Result<(), git_transport::client::Error> { + // An empty request marks the (early) end of the interaction. Only relevant in stateful transports though. + if transport.connection_persists_across_multiple_requests() { + transport + .request( + git_transport::client::WriteMode::Binary, + git_transport::client::MessageKind::Flush, + )? + .into_read() + .await?; + } + Ok(()) +} + #[cfg(test)] mod tests; diff --git a/git-protocol/src/fetch/refs/function.rs b/git-protocol/src/fetch/refs/function.rs index 6f367d3af6a..5e2d979f187 100644 --- a/git-protocol/src/fetch/refs/function.rs +++ b/git-protocol/src/fetch/refs/function.rs @@ -1,7 +1,6 @@ use super::Error; use crate::fetch::refs::from_v2_refs; -use crate::fetch::{Command, LsRefsAction, Ref}; -use crate::fetch_fn::indicate_end_of_interaction; +use crate::fetch::{indicate_end_of_interaction, Command, LsRefsAction, Ref}; use bstr::BString; use git_features::progress::Progress; use git_transport::client::{Capabilities, Transport, TransportV2Ext}; diff --git a/git-protocol/src/fetch_fn.rs b/git-protocol/src/fetch_fn.rs index 6d11a29d8da..3558fad142f 100644 --- a/git-protocol/src/fetch_fn.rs +++ b/git-protocol/src/fetch_fn.rs @@ -2,7 +2,7 @@ use git_features::progress::Progress; use git_transport::client; use maybe_async::maybe_async; -use crate::fetch::handshake; +use crate::fetch::{handshake, indicate_end_of_interaction}; use crate::{ credentials, fetch::{Action, Arguments, Command, Delegate, Error, Response}, @@ -142,20 +142,6 @@ where Ok(()) } -#[maybe_async] -pub(crate) async fn indicate_end_of_interaction( - mut transport: impl client::Transport, -) -> Result<(), git_transport::client::Error> { - // An empty request marks the (early) end of the interaction. Only relevant in stateful transports though. - if transport.connection_persists_across_multiple_requests() { - transport - .request(client::WriteMode::Binary, client::MessageKind::Flush)? - .into_read() - .await?; - } - Ok(()) -} - fn setup_remote_progress( progress: &mut impl Progress, reader: &mut Box, From 86c80e6db53fdc548221ab2dab2f84d66fef691f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 20 Aug 2022 16:46:54 +0800 Subject: [PATCH 092/125] A first working test to show all refs of a remote. (#450) --- git-repository/src/remote/connect.rs | 14 ++++++++- .../src/remote/connection/list_refs.rs | 4 +-- git-repository/src/remote/connection/mod.rs | 29 ++++++++++++++----- git-repository/tests/remote/list_refs.rs | 4 +-- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 93464d19d32..6be824234b2 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -16,6 +16,8 @@ mod error { MissingUrl { direction: remote::Direction }, #[error("Protocol named {given:?} is not a valid protocol. Choose between 1 and 2")] UnknownProtocol { given: BString }, + #[error("Could not verify that file:// url is a valid git directory before attempting to use it")] + FileUrl(#[from] git_discover::is_git::Error), } } pub use error::Error; @@ -70,7 +72,17 @@ impl<'repo> Remote<'repo> { }) })?; - let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); + let mut url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); + if url.scheme == git_url::Scheme::File { + let mut dir = git_path::from_bstr(url.path.as_ref()); + let kind = git_discover::is_git(dir.as_ref()).or_else(|_| { + dir.to_mut().push(git_discover::DOT_GIT_DIR); + git_discover::is_git(dir.as_ref()) + })?; + let (git_dir, _work_dir) = git_discover::repository::Path::from_dot_git_dir(dir.into_owned(), kind) + .into_repository_and_work_tree_directories(); + url.path = git_path::into_bstr(git_dir).into_owned(); + } let transport = git_protocol::transport::connect(url, protocol).await?; Ok(self.to_connection_with_transport(transport, progress)) } diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs index afa45c1c20c..5892b403340 100644 --- a/git-repository/src/remote/connection/list_refs.rs +++ b/git-repository/src/remote/connection/list_refs.rs @@ -1,4 +1,6 @@ +use crate::remote::connection::State; use crate::remote::{Connection, Direction}; +use git_features::progress::Progress; use git_protocol::transport::client::Transport; mod error { @@ -10,9 +12,7 @@ mod error { ListRefs(#[from] git_protocol::fetch::refs::Error), } } -use crate::remote::connection::State; pub use error::Error; -use git_features::progress::Progress; impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> where diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index 6c806579801..51f58901a65 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -10,24 +10,39 @@ pub(crate) enum State { }, } -pub struct Connection<'a, 'repo, T, P> { +pub struct Connection<'a, 'repo, T, P> +where + T: git_protocol::transport::client::Transport, +{ pub(crate) remote: &'a Remote<'repo>, pub(crate) transport: T, pub(crate) progress: P, pub(crate) state: State, } +mod impls { + use crate::remote::Connection; + use git_protocol::transport::client::Transport; + + impl<'a, 'repo, T, P> Drop for Connection<'a, 'repo, T, P> + where + T: Transport, + { + fn drop(&mut self) { + git_protocol::fetch::indicate_end_of_interaction(&mut self.transport).ok(); + } + } +} + mod access { use crate::remote::Connection; use crate::Remote; /// Access and conversion - impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { - /// Obtain the transport from this instance. - pub fn into_transport(self) -> T { - self.transport - } - + impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> + where + T: git_protocol::transport::client::Transport, + { /// Drop the transport and additional state to regain the original remote. pub fn remote(&self) -> &Remote<'repo> { self.remote diff --git a/git-repository/tests/remote/list_refs.rs b/git-repository/tests/remote/list_refs.rs index 4834855430c..6c7e6da5798 100644 --- a/git-repository/tests/remote/list_refs.rs +++ b/git-repository/tests/remote/list_refs.rs @@ -5,11 +5,11 @@ mod blocking_io { use git_repository::remote::Direction::Fetch; #[test] - #[ignore] fn all() { let repo = remote::repo("clone"); let remote = repo.find_remote("origin").unwrap(); let mut connection = remote.connect(Fetch, progress::Discard).unwrap(); - let _refs = connection.list_refs().unwrap(); + let refs = connection.list_refs().unwrap(); + assert_eq!(refs.len(), 14, "it gets all remote refs, independently of the refspec."); } } From eba5b13aa08229ff97f0a2be66ec80aadd4b9d1f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 20 Aug 2022 16:52:04 +0800 Subject: [PATCH 093/125] refactor (#450) --- git-repository/src/remote/connect.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 6be824234b2..c70b102a752 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -1,7 +1,7 @@ #![allow(missing_docs)] use crate::remote::{connection, Connection}; -use crate::{Progress, Remote}; +use crate::{remote, Progress, Remote}; use git_protocol::transport::client::Transport; mod error { @@ -46,7 +46,7 @@ impl<'repo> Remote<'repo> { #[git_protocol::maybe_async::maybe_async] pub async fn connect

( &self, - direction: crate::remote::Direction, + direction: remote::Direction, progress: P, ) -> Result, P>, Error> where @@ -72,6 +72,12 @@ impl<'repo> Remote<'repo> { }) })?; + let url = self.processed_url(direction)?; + let transport = git_protocol::transport::connect(url, protocol).await?; + Ok(self.to_connection_with_transport(transport, progress)) + } + + fn processed_url(&self, direction: remote::Direction) -> Result { let mut url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); if url.scheme == git_url::Scheme::File { let mut dir = git_path::from_bstr(url.path.as_ref()); @@ -83,7 +89,6 @@ impl<'repo> Remote<'repo> { .into_repository_and_work_tree_directories(); url.path = git_path::into_bstr(git_dir).into_owned(); } - let transport = git_protocol::transport::connect(url, protocol).await?; - Ok(self.to_connection_with_transport(transport, progress)) + Ok(url) } } From 332a9784e61c102b46faa710ad9f6e5a208caa04 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 20 Aug 2022 16:55:01 +0800 Subject: [PATCH 094/125] add docs (#450) --- git-repository/src/remote/connect.rs | 4 ++-- git-repository/src/remote/connection/mod.rs | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index c70b102a752..c564f1dac91 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use crate::remote::{connection, Connection}; use crate::{remote, Progress, Remote}; use git_protocol::transport::client::Transport; @@ -8,7 +6,9 @@ mod error { use crate::bstr::BString; use crate::remote; + /// The error returned by [connect()][crate::Remote::connect()]. #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] pub enum Error { #[error(transparent)] Connect(#[from] git_protocol::transport::client::connect::Error), diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index 51f58901a65..f051b648567 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -1,15 +1,18 @@ -#![allow(missing_docs, dead_code)] - use crate::Remote; pub(crate) enum State { Connected, HandshakeWithRefs { + #[allow(dead_code)] outcome: git_protocol::fetch::handshake::Outcome, refs: Vec, }, } +/// A type to represent an ongoing connection to a remote host, typically with the connection already established. +/// +/// It can be used to perform a variety of operations with the remote without worrying about protocol details, +/// much like a remote procedure call. pub struct Connection<'a, 'repo, T, P> where T: git_protocol::transport::client::Transport, From 129176f013052b6ef6eb37b4274fa68c1e0b11a3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 20 Aug 2022 19:14:56 +0800 Subject: [PATCH 095/125] change connection API to be consuming, otherwise async mode doesn't work due to lack of async drop (#450) --- git-repository/src/remote/connect.rs | 40 ++++++------- .../src/remote/connection/list_refs.rs | 58 +++++++++---------- git-repository/src/remote/connection/mod.rs | 36 ++---------- git-repository/tests/remote/list_refs.rs | 2 +- 4 files changed, 54 insertions(+), 82 deletions(-) diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index c564f1dac91..0d74334558e 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -1,5 +1,5 @@ -use crate::remote::{connection, Connection}; -use crate::{remote, Progress, Remote}; +use crate::remote::Connection; +use crate::{Progress, Remote}; use git_protocol::transport::client::Transport; mod error { @@ -37,7 +37,6 @@ impl<'repo> Remote<'repo> { remote: self, transport, progress, - state: connection::State::Connected, } } @@ -46,13 +45,27 @@ impl<'repo> Remote<'repo> { #[git_protocol::maybe_async::maybe_async] pub async fn connect

( &self, - direction: remote::Direction, + direction: crate::remote::Direction, progress: P, ) -> Result, P>, Error> where P: Progress, { use git_protocol::transport::Protocol; + fn sanitize(mut url: git_url::Url) -> Result { + if url.scheme == git_url::Scheme::File { + let mut dir = git_path::from_bstr(url.path.as_ref()); + let kind = git_discover::is_git(dir.as_ref()).or_else(|_| { + dir.to_mut().push(git_discover::DOT_GIT_DIR); + git_discover::is_git(dir.as_ref()) + })?; + let (git_dir, _work_dir) = git_discover::repository::Path::from_dot_git_dir(dir.into_owned(), kind) + .into_repository_and_work_tree_directories(); + url.path = git_path::into_bstr(git_dir).into_owned(); + } + Ok(url) + } + let protocol = self .repo .config @@ -72,23 +85,8 @@ impl<'repo> Remote<'repo> { }) })?; - let url = self.processed_url(direction)?; - let transport = git_protocol::transport::connect(url, protocol).await?; + let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); + let transport = git_protocol::transport::connect(sanitize(url)?, protocol).await?; Ok(self.to_connection_with_transport(transport, progress)) } - - fn processed_url(&self, direction: remote::Direction) -> Result { - let mut url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); - if url.scheme == git_url::Scheme::File { - let mut dir = git_path::from_bstr(url.path.as_ref()); - let kind = git_discover::is_git(dir.as_ref()).or_else(|_| { - dir.to_mut().push(git_discover::DOT_GIT_DIR); - git_discover::is_git(dir.as_ref()) - })?; - let (git_dir, _work_dir) = git_discover::repository::Path::from_dot_git_dir(dir.into_owned(), kind) - .into_repository_and_work_tree_directories(); - url.path = git_path::into_bstr(git_dir).into_owned(); - } - Ok(url) - } } diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs index 5892b403340..1d2945d64e3 100644 --- a/git-repository/src/remote/connection/list_refs.rs +++ b/git-repository/src/remote/connection/list_refs.rs @@ -1,4 +1,4 @@ -use crate::remote::connection::State; +use crate::remote::connection::HandshakeWithRefs; use crate::remote::{Connection, Direction}; use git_features::progress::Progress; use git_protocol::transport::client::Transport; @@ -10,6 +10,8 @@ mod error { 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), } } pub use error::Error; @@ -22,40 +24,36 @@ where /// 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. - /// - /// This method is idempotent and only runs once. #[git_protocol::maybe_async::maybe_async] - pub async fn list_refs(&mut self) -> Result<&[git_protocol::fetch::Ref], Error> { - match self.state { - State::Connected => { - let mut outcome = git_protocol::fetch::handshake( + 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 outcome = git_protocol::fetch::handshake( + &mut self.transport, + git_protocol::credentials::helper, + Vec::new(), + &mut self.progress, + ) + .await?; + let refs = match outcome.refs.take() { + Some(refs) => refs, + None => { + git_protocol::fetch::refs( &mut self.transport, - git_protocol::credentials::helper, - Vec::new(), + outcome.server_protocol_version, + &outcome.capabilities, + |_a, _b, _c| Ok(git_protocol::fetch::delegate::LsRefsAction::Continue), &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? - } - }; - self.state = State::HandshakeWithRefs { outcome, refs }; - match &self.state { - State::HandshakeWithRefs { refs, .. } => Ok(refs), - _ => unreachable!(), - } + .await? } - State::HandshakeWithRefs { ref refs, .. } => Ok(refs), - } + }; + Ok(HandshakeWithRefs { outcome, refs }) } /// List all references on the remote that have been filtered through our remote's [`refspecs`][crate::Remote::refspecs()] diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index f051b648567..07e4c5011fa 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -1,40 +1,19 @@ use crate::Remote; -pub(crate) enum State { - Connected, - HandshakeWithRefs { - #[allow(dead_code)] - outcome: git_protocol::fetch::handshake::Outcome, - refs: Vec, - }, +pub(crate) struct HandshakeWithRefs { + #[allow(dead_code)] + outcome: git_protocol::fetch::handshake::Outcome, + refs: Vec, } /// A type to represent an ongoing connection to a remote host, typically with the connection already established. /// /// It can be used to perform a variety of operations with the remote without worrying about protocol details, /// much like a remote procedure call. -pub struct Connection<'a, 'repo, T, P> -where - T: git_protocol::transport::client::Transport, -{ +pub struct Connection<'a, 'repo, T, P> { pub(crate) remote: &'a Remote<'repo>, pub(crate) transport: T, pub(crate) progress: P, - pub(crate) state: State, -} - -mod impls { - use crate::remote::Connection; - use git_protocol::transport::client::Transport; - - impl<'a, 'repo, T, P> Drop for Connection<'a, 'repo, T, P> - where - T: Transport, - { - fn drop(&mut self) { - git_protocol::fetch::indicate_end_of_interaction(&mut self.transport).ok(); - } - } } mod access { @@ -42,10 +21,7 @@ mod access { use crate::Remote; /// Access and conversion - impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> - where - T: git_protocol::transport::client::Transport, - { + impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { /// Drop the transport and additional state to regain the original remote. pub fn remote(&self) -> &Remote<'repo> { self.remote diff --git a/git-repository/tests/remote/list_refs.rs b/git-repository/tests/remote/list_refs.rs index 6c7e6da5798..f668d6a4ff0 100644 --- a/git-repository/tests/remote/list_refs.rs +++ b/git-repository/tests/remote/list_refs.rs @@ -8,7 +8,7 @@ mod blocking_io { fn all() { let repo = remote::repo("clone"); let remote = repo.find_remote("origin").unwrap(); - let mut connection = remote.connect(Fetch, progress::Discard).unwrap(); + let connection = remote.connect(Fetch, progress::Discard).unwrap(); let refs = connection.list_refs().unwrap(); assert_eq!(refs.len(), 14, "it gets all remote refs, independently of the refspec."); } From f8f124943f73bacf816c6d0055f0b66659fd3906 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 20 Aug 2022 19:42:35 +0800 Subject: [PATCH 096/125] basic parsing of `gix remote refs` without any implementation. (#450) Doing so requires some fiddling with launching async functions correctly withing `gix`. In theory, that's already possible thanks to `pack receive`, but let's see how well this really works. --- gitoxide-core/src/repository/mod.rs | 18 ++++++------------ gitoxide-core/src/repository/remote.rs | 1 + src/plumbing/main.rs | 15 +++++++++++++++ src/plumbing/options.rs | 22 ++++++++++++++++++++++ 4 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 gitoxide-core/src/repository/remote.rs diff --git a/gitoxide-core/src/repository/mod.rs b/gitoxide-core/src/repository/mod.rs index 71ce148d319..eda9d2f564c 100644 --- a/gitoxide-core/src/repository/mod.rs +++ b/gitoxide-core/src/repository/mod.rs @@ -14,18 +14,12 @@ pub fn init(directory: Option) -> Result Result<()> { })?; match cmd { + Subcommands::Remote(remote::Platform { name: _, cmd }) => match cmd { + remote::Subcommands::Refs => prepare_and_run( + "config-list", + verbose, + progress, + progress_keep_open, + None, + move |_progress, _out, _err| { + Ok(()) + // core::repository::remote::refs(repository(Mode::Lenient)?, name, format, out) + }, + ), + } + .map(|_| ()), Subcommands::Config(config::Platform { filter }) => prepare_and_run( "config-list", verbose, diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index c6e8d006cd4..348a7c4cfe2 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -72,6 +72,8 @@ pub enum Subcommands { /// Interact with the mailmap. #[clap(subcommand)] Mailmap(mailmap::Subcommands), + /// Interact with the remote hosts. + Remote(remote::Platform), /// Interact with the exclude files like .gitignore. #[clap(subcommand)] Exclude(exclude::Subcommands), @@ -94,6 +96,26 @@ pub mod config { } } +pub mod remote { + #[derive(Debug, clap::Parser)] + pub struct Platform { + /// The name of the remote to connect to. + #[clap(long, short = 'n', default_value = "origin")] + pub name: String, + + /// Subcommands + #[clap(subcommand)] + pub cmd: Subcommands, + } + + #[derive(Debug, clap::Subcommand)] + #[clap(visible_alias = "remotes")] + pub enum Subcommands { + /// Print all references available on the remote + Refs, + } +} + pub mod mailmap { #[derive(Debug, clap::Subcommand)] pub enum Subcommands { From db4df250d7e58518015bed0b9a1e3391b209cb29 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 21 Aug 2022 16:57:36 +0800 Subject: [PATCH 097/125] Try to use maybe async for the simplest of possibly blocking remote interactions (#450) It's probably OK to just do that for the sake of simplicity instead of going all in the way it's done in `pack-receive`. --- gitoxide-core/src/repository/remote.rs | 17 ++++++++ src/plumbing/main.rs | 59 +++++++++++++++++--------- src/plumbing/options.rs | 1 + 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs index 8b137891791..7d22044dd91 100644 --- a/gitoxide-core/src/repository/remote.rs +++ b/gitoxide-core/src/repository/remote.rs @@ -1 +1,18 @@ +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +mod net { + use crate::OutputFormat; + use git_repository as git; + #[git::protocol::maybe_async::maybe_async] + pub async fn refs( + _repo: git::Repository, + _name: &str, + _format: OutputFormat, + _progress: impl git::Progress, + _out: impl std::io::Write, + ) -> anyhow::Result<()> { + todo!() + } +} +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +pub use net::refs; diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index abd8394713b..69034e209bf 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -30,7 +30,10 @@ pub mod async_util { verbose: bool, name: &str, range: impl Into>, - ) -> (Option, Option) { + ) -> ( + Option, + git_features::progress::DoOrDiscard, + ) { use crate::shared::{self, STANDARD_RANGE}; shared::init_env_logger(); @@ -38,9 +41,9 @@ pub mod async_util { let progress = shared::progress_tree(); let sub_progress = progress.add_child(name); let ui_handle = shared::setup_line_renderer_range(&progress, range.into().unwrap_or(STANDARD_RANGE)); - (Some(ui_handle), Some(sub_progress)) + (Some(ui_handle), Some(sub_progress).into()) } else { - (None, None) + (None, None.into()) } } } @@ -88,18 +91,35 @@ pub fn main() -> Result<()> { })?; match cmd { - Subcommands::Remote(remote::Platform { name: _, cmd }) => match cmd { - remote::Subcommands::Refs => prepare_and_run( - "config-list", - verbose, - progress, - progress_keep_open, - None, - move |_progress, _out, _err| { - Ok(()) - // core::repository::remote::refs(repository(Mode::Lenient)?, name, format, out) - }, - ), + Subcommands::Remote(remote::Platform { name, cmd }) => match cmd { + #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] + remote::Subcommands::Refs => { + #[cfg(feature = "gitoxide-core-blocking-client")] + { + prepare_and_run( + "config-list", + verbose, + progress, + progress_keep_open, + None, + move |progress, out, _err| { + core::repository::remote::refs(repository(Mode::Lenient)?, &name, format, progress, out) + }, + ) + } + #[cfg(feature = "gitoxide-core-async-client")] + { + let (_handle, progress) = + async_util::prepare(verbose, "remote-ref-list", Some(core::remote::refs::PROGRESS_RANGE)); + futures_lite::future::block_on(core::repository::remote::refs( + repository(Mode::Lenient)?, + &name, + format, + progress, + std::io::stdout(), + )) + } + } } .map(|_| ()), Subcommands::Config(config::Platform { filter }) => prepare_and_run( @@ -118,17 +138,16 @@ pub fn main() -> Result<()> { free::remote::Subcommands::RefList { protocol, url } => { let (_handle, progress) = async_util::prepare(verbose, "remote-ref-list", Some(core::remote::refs::PROGRESS_RANGE)); - let fut = core::remote::refs::list( + futures_lite::future::block_on(core::remote::refs::list( protocol, &url, - git_features::progress::DoOrDiscard::from(progress), + progress, core::remote::refs::Context { thread_limit, format, out: std::io::stdout(), }, - ); - return futures_lite::future::block_on(fut); + )) } #[cfg(feature = "gitoxide-core-blocking-client")] free::remote::Subcommands::RefList { protocol, url } => prepare_and_run( @@ -313,7 +332,7 @@ pub fn main() -> Result<()> { directory, refs_directory, refs.into_iter().map(|s| s.into()).collect(), - git_features::progress::DoOrDiscard::from(progress), + progress, core::pack::receive::Context { thread_limit, format, diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index 348a7c4cfe2..b8ec93a2910 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -112,6 +112,7 @@ pub mod remote { #[clap(visible_alias = "remotes")] pub enum Subcommands { /// Print all references available on the remote + #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] Refs, } } From bb6813abf365728d9851ee205b2c25b925a0f06a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 21 Aug 2022 17:04:18 +0800 Subject: [PATCH 098/125] thanks clippy --- src/plumbing/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 69034e209bf..3b53a17da57 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -91,6 +91,7 @@ pub fn main() -> Result<()> { })?; match cmd { + #[cfg_attr(feature = "small", allow(unused_variables))] Subcommands::Remote(remote::Platform { name, cmd }) => match cmd { #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] remote::Subcommands::Refs => { @@ -120,8 +121,7 @@ pub fn main() -> Result<()> { )) } } - } - .map(|_| ()), + }, Subcommands::Config(config::Platform { filter }) => prepare_and_run( "config-list", verbose, From 5d6d5ca305615568dfedbcc10ea86294c0a0472d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 10:16:37 +0800 Subject: [PATCH 099/125] feat: `gix remote refs` to list all remote references of a suitable remote. (#450) This takes into account either a named remote, or the remote associated with the current branch, or the default remote it could deduce or obtain from the configuration. --- gitoxide-core/Cargo.toml | 2 +- gitoxide-core/src/repository/remote.rs | 95 ++++++++++++++++++++++++-- src/plumbing/main.rs | 14 ++-- src/plumbing/options.rs | 6 +- 4 files changed, 104 insertions(+), 13 deletions(-) diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index 0fdbf12b314..7d755b9a4e2 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -27,7 +27,7 @@ estimate-hours = ["itertools", "rayon", "fs-err"] blocking-client = ["git-repository/blocking-network-client"] ## The client to connect to git servers will be async, while supporting only the 'git' transport itself. ## It's the most limited and can be seen as example on how to use custom transports for custom servers. -async-client = ["git-repository/async-network-client", "git-transport-configuration-only/async-std", "async-trait", "futures-io", "async-net", "async-io", "futures-lite", "blocking"] +async-client = ["git-repository/async-network-client-async-std", "git-transport-configuration-only/async-std", "async-trait", "futures-io", "async-net", "async-io", "futures-lite", "blocking"] #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs index 7d22044dd91..6eaf5ef397c 100644 --- a/gitoxide-core/src/repository/remote.rs +++ b/gitoxide-core/src/repository/remote.rs @@ -1,17 +1,100 @@ #[cfg(any(feature = "blocking-client", feature = "async-client"))] mod net { use crate::OutputFormat; + use anyhow::Context; use git_repository as git; + use git_repository::protocol::fetch; #[git::protocol::maybe_async::maybe_async] pub async fn refs( - _repo: git::Repository, - _name: &str, - _format: OutputFormat, - _progress: impl git::Progress, - _out: impl std::io::Write, + repo: git::Repository, + name: Option<&str>, + format: OutputFormat, + mut progress: impl git::Progress, + out: impl std::io::Write, ) -> anyhow::Result<()> { - todo!() + let remote = match name { + Some(name) => repo.find_remote(name)?, + None => repo + .head()? + .into_remote(git::remote::Direction::Fetch) + .context("Cannot find a remote for unborn branch")??, + }; + progress.info(format!( + "Connecting to {:?}", + remote + .url(git::remote::Direction::Fetch) + .context("Remote didn't have a URL to connect to")? + .to_bstring() + )); + let refs = remote + .connect(git::remote::Direction::Fetch, progress) + .await? + .list_refs() + .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::>())? + } + }; + Ok(()) + } + + #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] + pub enum JsonRef { + Peeled { + path: String, + tag: String, + object: String, + }, + Direct { + path: String, + object: String, + }, + Symbolic { + path: String, + target: String, + object: String, + }, + } + + impl From for JsonRef { + fn from(value: fetch::Ref) -> Self { + match value { + fetch::Ref::Direct { path, object } => JsonRef::Direct { + path: path.to_string(), + object: object.to_string(), + }, + fetch::Ref::Symbolic { 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 { + path: path.to_string(), + tag: tag.to_string(), + object: object.to_string(), + }, + } + } + } + + 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) + } + }?; + } + Ok(()) } } #[cfg(any(feature = "blocking-client", feature = "async-client"))] diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 3b53a17da57..023287119b3 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -98,23 +98,29 @@ pub fn main() -> Result<()> { #[cfg(feature = "gitoxide-core-blocking-client")] { prepare_and_run( - "config-list", + "remote-refs", verbose, progress, progress_keep_open, None, move |progress, out, _err| { - core::repository::remote::refs(repository(Mode::Lenient)?, &name, format, progress, out) + core::repository::remote::refs( + repository(Mode::Lenient)?, + name.as_deref(), + format, + progress, + out, + ) }, ) } #[cfg(feature = "gitoxide-core-async-client")] { let (_handle, progress) = - async_util::prepare(verbose, "remote-ref-list", Some(core::remote::refs::PROGRESS_RANGE)); + async_util::prepare(verbose, "remote-refs", Some(core::remote::refs::PROGRESS_RANGE)); futures_lite::future::block_on(core::repository::remote::refs( repository(Mode::Lenient)?, - &name, + name.as_deref(), format, progress, std::io::stdout(), diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index b8ec93a2910..a7a579d459b 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -100,8 +100,10 @@ pub mod remote { #[derive(Debug, clap::Parser)] pub struct Platform { /// The name of the remote to connect to. - #[clap(long, short = 'n', default_value = "origin")] - pub name: String, + /// + /// If unset, the current branch will determine the remote. + #[clap(long, short = 'n')] + pub name: Option, /// Subcommands #[clap(subcommand)] From b7a5f7a3b5cf058f503cc18d18fc75356ab98955 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 10:27:58 +0800 Subject: [PATCH 100/125] feat: `TryFrom` which is useful if urls are obtained from the command-line. (#450) --- git-url/Cargo.toml | 3 ++- git-url/src/lib.rs | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/git-url/Cargo.toml b/git-url/Cargo.toml index 3bffc71194d..5a140932681 100644 --- a/git-url/Cargo.toml +++ b/git-url/Cargo.toml @@ -16,9 +16,10 @@ doctest = false serde1 = ["serde", "bstr/serde1"] [dependencies] -serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} git-features = { version = "^0.22.1", path = "../git-features" } git-path = { version = "^0.4.0", path = "../git-path" } + +serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} thiserror = "1.0.32" url = "2.1.1" bstr = { version = "0.2.13", default-features = false, features = ["std"] } diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index 1b205272261..8e4419e4517 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -8,6 +8,7 @@ #![deny(rust_2018_idioms, missing_docs)] #![forbid(unsafe_code)] +use std::path::PathBuf; use std::{ convert::TryFrom, fmt::{self}, @@ -200,6 +201,15 @@ impl TryFrom for Url { } } +impl TryFrom for Url { + type Error = parse::Error; + + fn try_from(value: PathBuf) -> Result { + use std::convert::TryInto; + git_path::into_bstr(value).try_into() + } +} + impl TryFrom<&BStr> for Url { type Error = parse::Error; From 7a1769009d68d14a134f368f93245abab0fb41dd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 10:37:20 +0800 Subject: [PATCH 101/125] feat: `TryFrom<&OsStr>` to allow direct usage of `Url` in `clap`. (#450) In `clap_derive`, this needs `parse(try_from_os_str = std::convert::TryFrom::try_from)`. --- git-url/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/git-url/src/lib.rs b/git-url/src/lib.rs index 8e4419e4517..56a7cf0d2d8 100644 --- a/git-url/src/lib.rs +++ b/git-url/src/lib.rs @@ -210,6 +210,17 @@ impl TryFrom for Url { } } +impl TryFrom<&std::ffi::OsStr> for Url { + type Error = parse::Error; + + fn try_from(value: &std::ffi::OsStr) -> Result { + use std::convert::TryInto; + git_path::os_str_into_bstr(value) + .expect("no illformed UTF-8 on Windows") + .try_into() + } +} + impl TryFrom<&BStr> for Url { type Error = parse::Error; From df3cf18a6ac1e4f35f6d11d62184a43722397bbe Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 10:38:42 +0800 Subject: [PATCH 102/125] Add support for passing urls directly to bypass all remote repository logic. (#450) This, however, still means a repository is required even though it won't be used. --- gitoxide-core/src/repository/remote.rs | 11 +++++++---- src/plumbing/main.rs | 4 +++- src/plumbing/options.rs | 6 ++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs index 6eaf5ef397c..74b15c5faf5 100644 --- a/gitoxide-core/src/repository/remote.rs +++ b/gitoxide-core/src/repository/remote.rs @@ -1,7 +1,7 @@ #[cfg(any(feature = "blocking-client", feature = "async-client"))] mod net { use crate::OutputFormat; - use anyhow::Context; + use anyhow::{bail, Context}; use git_repository as git; use git_repository::protocol::fetch; @@ -9,16 +9,19 @@ mod net { pub async fn refs( repo: git::Repository, name: Option<&str>, + url: Option, format: OutputFormat, mut progress: impl git::Progress, out: impl std::io::Write, ) -> anyhow::Result<()> { - let remote = match name { - Some(name) => repo.find_remote(name)?, - None => repo + let remote = match (name, url) { + (Some(name), None) => repo.find_remote(name)?, + (None, None) => repo .head()? .into_remote(git::remote::Direction::Fetch) .context("Cannot find a remote for unborn branch")??, + (None, Some(url)) => repo.remote_at(url)?, + (Some(_), Some(_)) => bail!("Must not set both the remote name and the url - they are mutually exclusive"), }; progress.info(format!( "Connecting to {:?}", diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 023287119b3..d71a797b647 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -92,7 +92,7 @@ pub fn main() -> Result<()> { match cmd { #[cfg_attr(feature = "small", allow(unused_variables))] - Subcommands::Remote(remote::Platform { name, cmd }) => match cmd { + Subcommands::Remote(remote::Platform { name, url, cmd }) => match cmd { #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] remote::Subcommands::Refs => { #[cfg(feature = "gitoxide-core-blocking-client")] @@ -107,6 +107,7 @@ pub fn main() -> Result<()> { core::repository::remote::refs( repository(Mode::Lenient)?, name.as_deref(), + url, format, progress, out, @@ -121,6 +122,7 @@ pub fn main() -> Result<()> { futures_lite::future::block_on(core::repository::remote::refs( repository(Mode::Lenient)?, name.as_deref(), + url, format, progress, std::io::stdout(), diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index a7a579d459b..d5627f3acf3 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -97,6 +97,8 @@ pub mod config { } pub mod remote { + use git_repository as git; + #[derive(Debug, clap::Parser)] pub struct Platform { /// The name of the remote to connect to. @@ -105,6 +107,10 @@ pub mod remote { #[clap(long, short = 'n')] pub name: Option, + /// Connect directly to the given URL, forgoing any configuration from the repository. + #[clap(long, short = 'u', conflicts_with("name"), parse(try_from_os_str = std::convert::TryFrom::try_from))] + pub url: Option, + /// Subcommands #[clap(subcommand)] pub cmd: Subcommands, From e0be6e9558add3255de63f3785306daace2707a6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 10:46:52 +0800 Subject: [PATCH 103/125] refactor (#450) Prepare for `protocol` configuration. --- gitoxide-core/src/repository/remote.rs | 23 ++++++++++++++++------- src/plumbing/main.rs | 8 ++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs index 74b15c5faf5..345a12677e9 100644 --- a/gitoxide-core/src/repository/remote.rs +++ b/gitoxide-core/src/repository/remote.rs @@ -1,21 +1,30 @@ #[cfg(any(feature = "blocking-client", feature = "async-client"))] mod net { use crate::OutputFormat; - use anyhow::{bail, Context}; + use anyhow::bail; use git_repository as git; use git_repository::protocol::fetch; + pub mod refs { + use crate::OutputFormat; + + pub struct Context { + pub format: OutputFormat, + pub name: Option, + pub url: Option, + } + } + #[git::protocol::maybe_async::maybe_async] - pub async fn refs( + pub async fn refs_fn( repo: git::Repository, - name: Option<&str>, - url: Option, - format: OutputFormat, mut progress: impl git::Progress, out: impl std::io::Write, + refs::Context { format, name, url }: refs::Context, ) -> anyhow::Result<()> { + use anyhow::Context; let remote = match (name, url) { - (Some(name), None) => repo.find_remote(name)?, + (Some(name), None) => repo.find_remote(&name)?, (None, None) => repo .head()? .into_remote(git::remote::Direction::Fetch) @@ -101,4 +110,4 @@ mod net { } } #[cfg(any(feature = "blocking-client", feature = "async-client"))] -pub use net::refs; +pub use net::{refs, refs_fn as refs}; diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index d71a797b647..11df5cf6664 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -106,11 +106,9 @@ pub fn main() -> Result<()> { move |progress, out, _err| { core::repository::remote::refs( repository(Mode::Lenient)?, - name.as_deref(), - url, - format, progress, out, + core::repository::remote::refs::Context { name, url, format }, ) }, ) @@ -121,11 +119,9 @@ pub fn main() -> Result<()> { async_util::prepare(verbose, "remote-refs", Some(core::remote::refs::PROGRESS_RANGE)); futures_lite::future::block_on(core::repository::remote::refs( repository(Mode::Lenient)?, - name.as_deref(), - url, - format, progress, std::io::stdout(), + core::repository::remote::refs::Context { name, url, format }, )) } } From e5c53a8d44914fd3e57b3d2cc2755210ea18e28b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 10:59:46 +0800 Subject: [PATCH 104/125] Support for overriding the protocol version to use when connecting. (#450) --- git-repository/src/remote/build.rs | 5 +++ git-repository/src/remote/connect.rs | 38 ++++++++++++--------- git-repository/src/remote/connection/mod.rs | 2 +- git-repository/src/remote/init.rs | 2 ++ git-repository/src/types.rs | 2 ++ git-repository/tests/remote/list_refs.rs | 17 ++++++--- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/git-repository/src/remote/build.rs b/git-repository/src/remote/build.rs index 08404cfaa52..c37ca1a6e42 100644 --- a/git-repository/src/remote/build.rs +++ b/git-repository/src/remote/build.rs @@ -4,6 +4,11 @@ use std::convert::TryInto; /// Builder methods impl Remote<'_> { + /// Force the transport protocol version to the given version, instead of loading it from the configuration. + pub fn force_transport_protocol(mut self, version: impl Into>) -> Self { + self.force_protocol = version.into(); + self + } /// Set the `url` to be used when pushing data to a remote. pub fn push_url(self, url: Url) -> Result where diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 0d74334558e..c41e94b0c81 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -66,24 +66,28 @@ impl<'repo> Remote<'repo> { Ok(url) } - let protocol = self - .repo - .config - .resolved - .integer("protocol", None, "version") - .unwrap_or(Ok(2)) - .map_err(|err| Error::UnknownProtocol { given: err.input }) - .and_then(|num| { - Ok(match num { - 1 => Protocol::V1, - 2 => Protocol::V2, - num => { - return Err(Error::UnknownProtocol { - given: num.to_string().into(), + let protocol = self.force_protocol.map_or_else( + || { + self.repo + .config + .resolved + .integer("protocol", None, "version") + .unwrap_or(Ok(2)) + .map_err(|err| Error::UnknownProtocol { given: err.input }) + .and_then(|num| { + Ok(match num { + 1 => Protocol::V1, + 2 => Protocol::V2, + num => { + return Err(Error::UnknownProtocol { + given: num.to_string().into(), + }) + } }) - } - }) - })?; + }) + }, + |version| Ok(version), + )?; let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); let transport = git_protocol::transport::connect(sanitize(url)?, protocol).await?; diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index 07e4c5011fa..7705e75e410 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -20,7 +20,7 @@ mod access { use crate::remote::Connection; use crate::Remote; - /// Access and conversion + /// Access impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> { /// Drop the transport and additional state to regain the original remote. pub fn remote(&self) -> &Remote<'repo> { diff --git a/git-repository/src/remote/init.rs b/git-repository/src/remote/init.rs index 6ad08423382..567ac88ecfc 100644 --- a/git-repository/src/remote/init.rs +++ b/git-repository/src/remote/init.rs @@ -40,6 +40,7 @@ impl<'repo> Remote<'repo> { .then(|| rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())) .unwrap_or(Ok((None, None)))?; Ok(Remote { + force_protocol: None, name, url, url_alias, @@ -65,6 +66,7 @@ impl<'repo> Remote<'repo> { .then(|| rewrite_urls(&repo.config, Some(&url), None)) .unwrap_or(Ok((None, None)))?; Ok(Remote { + force_protocol: None, name: None, url: Some(url), url_alias, diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index 6c2a401f3aa..497b7af641f 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -198,6 +198,8 @@ pub struct Remote<'repo> { pub(crate) fetch_specs: Vec, /// Refspecs for use when pushing. pub(crate) push_specs: Vec, + /// The protocol to use when connecting, if explicitly overridden. + pub(crate) force_protocol: Option, // /// Delete local tracking branches that don't exist on the remote anymore. // pub(crate) prune: bool, // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. diff --git a/git-repository/tests/remote/list_refs.rs b/git-repository/tests/remote/list_refs.rs index f668d6a4ff0..6412592b0a2 100644 --- a/git-repository/tests/remote/list_refs.rs +++ b/git-repository/tests/remote/list_refs.rs @@ -2,14 +2,21 @@ mod blocking_io { use crate::remote; use git_features::progress; + use git_repository as git; use git_repository::remote::Direction::Fetch; #[test] fn all() { - let repo = remote::repo("clone"); - let remote = repo.find_remote("origin").unwrap(); - let connection = remote.connect(Fetch, progress::Discard).unwrap(); - let refs = connection.list_refs().unwrap(); - assert_eq!(refs.len(), 14, "it gets all remote refs, independently of the refspec."); + for version in [ + None, + Some(git::protocol::transport::Protocol::V2), + Some(git::protocol::transport::Protocol::V1), + ] { + let repo = remote::repo("clone"); + let remote = repo.find_remote("origin").unwrap().force_transport_protocol(version); + let connection = remote.connect(Fetch, progress::Discard).unwrap(); + let refs = connection.list_refs().unwrap(); + assert_eq!(refs.len(), 14, "it gets all remote refs, independently of the refspec."); + } } } From 856f8031c607c120d34a08c51b2750e3f6d4d127 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 11:03:04 +0800 Subject: [PATCH 105/125] thanks clippy --- git-repository/src/remote/connect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index c41e94b0c81..52a6be33e90 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -86,7 +86,7 @@ impl<'repo> Remote<'repo> { }) }) }, - |version| Ok(version), + Ok, )?; let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); From 5e924cb5d8e2a11cb4b44ec451c840136314da54 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 11:04:17 +0800 Subject: [PATCH 106/125] fix build (#450) --- git-repository/src/remote/build.rs | 1 + git-repository/src/remote/init.rs | 2 ++ git-repository/src/types.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/git-repository/src/remote/build.rs b/git-repository/src/remote/build.rs index c37ca1a6e42..6c06b99113d 100644 --- a/git-repository/src/remote/build.rs +++ b/git-repository/src/remote/build.rs @@ -5,6 +5,7 @@ use std::convert::TryInto; /// Builder methods impl Remote<'_> { /// Force the transport protocol version to the given version, instead of loading it from the configuration. + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] pub fn force_transport_protocol(mut self, version: impl Into>) -> Self { self.force_protocol = version.into(); self diff --git a/git-repository/src/remote/init.rs b/git-repository/src/remote/init.rs index 567ac88ecfc..8d08e7f8c5f 100644 --- a/git-repository/src/remote/init.rs +++ b/git-repository/src/remote/init.rs @@ -40,6 +40,7 @@ impl<'repo> Remote<'repo> { .then(|| rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())) .unwrap_or(Ok((None, None)))?; Ok(Remote { + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] force_protocol: None, name, url, @@ -66,6 +67,7 @@ impl<'repo> Remote<'repo> { .then(|| rewrite_urls(&repo.config, Some(&url), None)) .unwrap_or(Ok((None, None)))?; Ok(Remote { + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] force_protocol: None, name: None, url: Some(url), diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index 497b7af641f..1f69cfda56a 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -199,6 +199,7 @@ pub struct Remote<'repo> { /// Refspecs for use when pushing. pub(crate) push_specs: Vec, /// The protocol to use when connecting, if explicitly overridden. + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] pub(crate) force_protocol: Option, // /// Delete local tracking branches that don't exist on the remote anymore. // pub(crate) prune: bool, From 69ec5940d3f37eb4dace8f1ed7616b5988984d15 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 13:14:17 +0800 Subject: [PATCH 107/125] rename!: `File::set_raw_[multi_]value()` to `::set_existing_raw_[multi_]value`. (#450) This makes clear that the method will fail if the value doesn't yet exist. --- git-config/src/file/access/raw.rs | 14 ++++++++------ git-config/tests/file/access/raw/set_raw_value.rs | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/git-config/src/file/access/raw.rs b/git-config/src/file/access/raw.rs index 6a6d90a3838..9fea5a22170 100644 --- a/git-config/src/file/access/raw.rs +++ b/git-config/src/file/access/raw.rs @@ -330,6 +330,8 @@ impl<'event> File<'event> { } /// Sets a value in a given section, optional subsection, and key value. + /// Note sections named `section_name` and `subsection_name` (if not `None`) + /// must exist for this method to work. /// /// # Examples /// @@ -351,7 +353,7 @@ impl<'event> File<'event> { /// # use bstr::BStr; /// # use std::convert::TryFrom; /// # let mut git_config = git_config::File::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); - /// git_config.set_raw_value("core", None, "a", "e".into())?; + /// git_config.set_existing_raw_value("core", None, "a", "e".into())?; /// assert_eq!(git_config.raw_value("core", None, "a")?, Cow::::Borrowed("e".into())); /// assert_eq!( /// git_config.raw_values("core", None, "a")?, @@ -363,7 +365,7 @@ impl<'event> File<'event> { /// ); /// # Ok::<(), Box>(()) /// ``` - pub fn set_raw_value( + pub fn set_existing_raw_value( &mut self, section_name: impl AsRef, subsection_name: Option<&str>, @@ -413,7 +415,7 @@ impl<'event> File<'event> { /// "y", /// "z", /// ]; - /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?; + /// git_config.set_existing_raw_multi_value("core", None, "a", new_values.into_iter())?; /// let fetched_config = git_config.raw_values("core", None, "a")?; /// assert!(fetched_config.contains(&Cow::::Borrowed("x".into()))); /// assert!(fetched_config.contains(&Cow::::Borrowed("y".into()))); @@ -433,7 +435,7 @@ impl<'event> File<'event> { /// "x", /// "y", /// ]; - /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?; + /// git_config.set_existing_raw_multi_value("core", None, "a", new_values.into_iter())?; /// let fetched_config = git_config.raw_values("core", None, "a")?; /// assert!(fetched_config.contains(&Cow::::Borrowed("x".into()))); /// assert!(fetched_config.contains(&Cow::::Borrowed("y".into()))); @@ -454,11 +456,11 @@ impl<'event> File<'event> { /// "z", /// "discarded", /// ]; - /// git_config.set_raw_multi_value("core", None, "a", new_values)?; + /// git_config.set_existing_raw_multi_value("core", None, "a", new_values)?; /// assert!(!git_config.raw_values("core", None, "a")?.contains(&Cow::::Borrowed("discarded".into()))); /// # Ok::<(), git_config::lookup::existing::Error>(()) /// ``` - pub fn set_raw_multi_value<'a, Iter, Item>( + pub fn set_existing_raw_multi_value<'a, Iter, Item>( &mut self, section_name: impl AsRef, subsection_name: Option<&str>, diff --git a/git-config/tests/file/access/raw/set_raw_value.rs b/git-config/tests/file/access/raw/set_raw_value.rs index 89cf772c5c1..f4ab382825b 100644 --- a/git-config/tests/file/access/raw/set_raw_value.rs +++ b/git-config/tests/file/access/raw/set_raw_value.rs @@ -4,7 +4,7 @@ fn file(input: &str) -> git_config::File<'static> { fn assert_set_value(value: &str) { let mut file = file("[a]k=b\n[a]\nk=c\nk=d"); - file.set_raw_value("a", None, "k", value.into()).unwrap(); + file.set_existing_raw_value("a", None, "k", value.into()).unwrap(); assert_eq!(file.raw_value("a", None, "k").unwrap().as_ref(), value); let file: git_config::File = file.to_string().parse().unwrap(); From 5902f54b93101a6290fcf89f9f13fdbea3678e00 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 13:51:19 +0800 Subject: [PATCH 108/125] =?UTF-8?q?feat:=20`File::section=5Fmut=5For=5Fcre?= =?UTF-8?q?ate=5Fnew(=E2=80=A6)`=20to=20obtain=20an=20existing=20or=20new?= =?UTF-8?q?=20section=20for=20mutation.=20(#450)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- git-config/src/file/access/mutate.rs | 27 ++++++++++++++++++++++-- git-config/tests/file/mutable/section.rs | 16 ++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/git-config/src/file/access/mutate.rs b/git-config/src/file/access/mutate.rs index 7810f7c32a3..1b7880fe15a 100644 --- a/git-config/src/file/access/mutate.rs +++ b/git-config/src/file/access/mutate.rs @@ -11,7 +11,7 @@ use crate::{ /// Mutating low-level access methods. impl<'event> File<'event> { - /// Returns an mutable section with a given `name` and optional `subsection_name`. + /// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_. pub fn section_mut<'a>( &'a mut self, name: impl AsRef, @@ -30,7 +30,30 @@ impl<'event> File<'event> { .to_mut(nl)) } - /// Returns the last found mutable section with a given `name` and optional `subsection_name`, that matches `filter`. + /// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_. + pub fn section_mut_or_create_new<'a>( + &'a mut self, + name: impl AsRef, + subsection_name: Option<&str>, + ) -> Result, section::header::Error> { + let name = name.as_ref(); + match self + .section_ids_by_name_and_subname(name.as_ref(), subsection_name) + .map(|it| it.rev().next().expect("BUG: Section lookup vec was empty")) + { + Ok(id) => { + let nl = self.detect_newline_style_smallvec(); + Ok(self + .sections + .get_mut(&id) + .expect("BUG: Section did not have id from lookup") + .to_mut(nl)) + } + Err(_) => self.new_section(name.to_owned(), subsection_name.map(|n| Cow::Owned(n.to_owned()))), + } + } + + /// Returns the last found mutable section with a given `name` and optional `subsection_name`, that matches `filter`, _if it exists_. /// /// If there are sections matching `section_name` and `subsection_name` but the `filter` rejects all of them, `Ok(None)` /// is returned. diff --git a/git-config/tests/file/mutable/section.rs b/git-config/tests/file/mutable/section.rs index 2add8166b84..ce963c1fe65 100644 --- a/git-config/tests/file/mutable/section.rs +++ b/git-config/tests/file/mutable/section.rs @@ -1,3 +1,19 @@ +#[test] +fn section_mut_must_exist_as_section_is_not_created_automatically() { + let mut config = multi_value_section(); + assert!(config.section_mut("foo", None).is_err()); +} + +#[test] +fn section_mut_or_create_new_is_infallible() -> crate::Result { + let mut config = multi_value_section(); + let section = config.section_mut_or_create_new("name", Some("subsection"))?; + assert_eq!(section.header().name(), "name"); + assert_eq!(section.header().subsection_name().expect("set"), "subsection"); + assert_eq!(section.to_bstring(), "[name \"subsection\"]\n"); + Ok(()) +} + mod remove { use super::multi_value_section; From 2b2357e9cc54539e0dbe7c0e22802f2b884160d8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 14:30:05 +0800 Subject: [PATCH 109/125] change!: Add `File::set_raw_value()` to unconditionally set single values, and make the value itself easier to provide. (#450) --- git-config/src/file/access/raw.rs | 51 ++++++++++++++-- git-config/src/file/mod.rs | 13 ++++ git-config/tests/file/access/raw/mod.rs | 1 + .../file/access/raw/set_existing_raw_value.rs | 60 +++++++++++++++++++ .../tests/file/access/raw/set_raw_value.rs | 18 +++++- 5 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 git-config/tests/file/access/raw/set_existing_raw_value.rs diff --git a/git-config/src/file/access/raw.rs b/git-config/src/file/access/raw.rs index 9fea5a22170..ac4e7df15ca 100644 --- a/git-config/src/file/access/raw.rs +++ b/git-config/src/file/access/raw.rs @@ -1,3 +1,4 @@ +use std::convert::TryInto; use std::{borrow::Cow, collections::HashMap}; use bstr::BStr; @@ -329,7 +330,7 @@ impl<'event> File<'event> { } } - /// Sets a value in a given section, optional subsection, and key value. + /// Sets a value in a given `section_name`, optional `subsection_name`, and `key`. /// Note sections named `section_name` and `subsection_name` (if not `None`) /// must exist for this method to work. /// @@ -353,7 +354,7 @@ impl<'event> File<'event> { /// # use bstr::BStr; /// # use std::convert::TryFrom; /// # let mut git_config = git_config::File::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); - /// git_config.set_existing_raw_value("core", None, "a", "e".into())?; + /// git_config.set_existing_raw_value("core", None, "a", "e")?; /// assert_eq!(git_config.raw_value("core", None, "a")?, Cow::::Borrowed("e".into())); /// assert_eq!( /// git_config.raw_values("core", None, "a")?, @@ -365,17 +366,59 @@ impl<'event> File<'event> { /// ); /// # Ok::<(), Box>(()) /// ``` - pub fn set_existing_raw_value( + pub fn set_existing_raw_value<'b>( &mut self, section_name: impl AsRef, subsection_name: Option<&str>, key: impl AsRef, - new_value: &BStr, + new_value: impl Into<&'b BStr>, ) -> Result<(), lookup::existing::Error> { self.raw_value_mut(section_name, subsection_name, key.as_ref()) .map(|mut entry| entry.set(new_value)) } + /// Sets a value in a given `section_name`, optional `subsection_name`, and `key`. + /// Creates the section if necessary and the key as well, or overwrites the last existing value otherwise. + /// + /// # Examples + /// + /// Given the config, + /// + /// ```text + /// [core] + /// a = b + /// ``` + /// + /// Setting a new value to the key `core.a` will yield the following: + /// + /// ``` + /// # use git_config::File; + /// # use std::borrow::Cow; + /// # use bstr::BStr; + /// # use std::convert::TryFrom; + /// # let mut git_config = git_config::File::try_from("[core]a=b").unwrap(); + /// let prev = git_config.set_raw_value("core", None, "a", "e")?; + /// git_config.set_raw_value("core", None, "b", "f")?; + /// assert_eq!(prev.expect("present").as_ref(), "b"); + /// assert_eq!(git_config.raw_value("core", None, "a")?, Cow::::Borrowed("e".into())); + /// assert_eq!(git_config.raw_value("core", None, "b")?, Cow::::Borrowed("f".into())); + /// # Ok::<(), Box>(()) + /// ``` + pub fn set_raw_value<'b, Key, E>( + &mut self, + section_name: impl AsRef, + subsection_name: Option<&str>, + key: Key, + new_value: impl Into<&'b BStr>, + ) -> Result>, crate::file::set_raw_value::Error> + where + Key: TryInto, Error = E>, + section::key::Error: From, + { + let mut section = self.section_mut_or_create_new(section_name, subsection_name)?; + Ok(section.set(key.try_into().map_err(section::key::Error::from)?, new_value)) + } + /// Sets a multivar in a given section, optional subsection, and key value. /// /// This internally zips together the new values and the existing values. diff --git a/git-config/src/file/mod.rs b/git-config/src/file/mod.rs index aae24b6f3c3..e799b79b494 100644 --- a/git-config/src/file/mod.rs +++ b/git-config/src/file/mod.rs @@ -38,6 +38,19 @@ pub mod rename_section { } } +/// +pub mod set_raw_value { + /// The error returned by [`File::set_raw_value(…)`][crate::File::set_raw_value()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Header(#[from] crate::parse::section::header::Error), + #[error(transparent)] + Key(#[from] crate::parse::section::key::Error), + } +} + /// Additional information about a section. #[derive(Clone, Debug, PartialOrd, PartialEq, Ord, Eq, Hash)] pub struct Metadata { diff --git a/git-config/tests/file/access/raw/mod.rs b/git-config/tests/file/access/raw/mod.rs index 8198d29b1c9..0afd4700382 100644 --- a/git-config/tests/file/access/raw/mod.rs +++ b/git-config/tests/file/access/raw/mod.rs @@ -1,3 +1,4 @@ mod raw_multi_value; mod raw_value; +mod set_existing_raw_value; mod set_raw_value; diff --git a/git-config/tests/file/access/raw/set_existing_raw_value.rs b/git-config/tests/file/access/raw/set_existing_raw_value.rs new file mode 100644 index 00000000000..7741fd3be96 --- /dev/null +++ b/git-config/tests/file/access/raw/set_existing_raw_value.rs @@ -0,0 +1,60 @@ +fn file(input: &str) -> git_config::File<'static> { + input.parse().unwrap() +} + +fn assert_set_value(value: &str) { + let mut file = file("[a]k=b\n[a]\nk=c\nk=d"); + file.set_existing_raw_value("a", None, "k", value).unwrap(); + assert_eq!(file.raw_value("a", None, "k").unwrap().as_ref(), value); + + let file: git_config::File = file.to_string().parse().unwrap(); + assert_eq!( + file.raw_value("a", None, "k").unwrap().as_ref(), + value, + "{:?} didn't have expected value {:?}", + file.to_string(), + value + ); +} + +#[test] +fn single_line() { + assert_set_value("hello world"); +} + +#[test] +fn starts_with_whitespace() { + assert_set_value("\ta"); + assert_set_value(" a"); +} + +#[test] +fn ends_with_whitespace() { + assert_set_value("a\t"); + assert_set_value("a "); +} + +#[test] +fn quotes_and_backslashes() { + assert_set_value(r#""hello"\"there"\\\b\x"#); +} + +#[test] +fn multi_line() { + assert_set_value("a\nb \n\t c"); +} + +#[test] +fn comment_included() { + assert_set_value(";hello "); + assert_set_value(" # hello"); +} + +#[test] +fn non_existing_values_cannot_be_set() { + let mut file = git_config::File::default(); + assert!( + file.set_existing_raw_value("new", None, "key", "value").is_err(), + "new values are not ever created" + ); +} diff --git a/git-config/tests/file/access/raw/set_raw_value.rs b/git-config/tests/file/access/raw/set_raw_value.rs index f4ab382825b..6691a230f95 100644 --- a/git-config/tests/file/access/raw/set_raw_value.rs +++ b/git-config/tests/file/access/raw/set_raw_value.rs @@ -3,8 +3,8 @@ fn file(input: &str) -> git_config::File<'static> { } fn assert_set_value(value: &str) { - let mut file = file("[a]k=b\n[a]\nk=c\nk=d"); - file.set_existing_raw_value("a", None, "k", value.into()).unwrap(); + let mut file = file("[a]\nk=c\nk=d"); + file.set_raw_value("a", None, "k", value).unwrap(); assert_eq!(file.raw_value("a", None, "k").unwrap().as_ref(), value); let file: git_config::File = file.to_string().parse().unwrap(); @@ -49,3 +49,17 @@ fn comment_included() { assert_set_value(";hello "); assert_set_value(" # hello"); } + +#[test] +fn non_existing_values_cannot_be_set() -> crate::Result { + let mut file = git_config::File::default(); + file.set_raw_value("new", None, "key", "value")?; + file.set_raw_value("new", "subsection".into(), "key", "subsection-value")?; + + assert_eq!(file.string("new", None, "key").expect("present").as_ref(), "value"); + assert_eq!( + file.string("new", Some("subsection"), "key").expect("present").as_ref(), + "subsection-value" + ); + Ok(()) +} From 2a839f3209f3bd35e0c0f7edff664cc953059f65 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 14:32:49 +0800 Subject: [PATCH 110/125] feat: `Repository::config_snapshot_mut()` to mutate configuration values in memory. (#450) It's a first step towards writing changes back to disk, which can work already, but probably wouldn't as we currently don't localize changes to only one section type, i.e. Api, but instead may change values from other sections. --- git-repository/src/config/mod.rs | 10 ++++++ git-repository/src/config/snapshot.rs | 43 +++++++++++++++++++---- git-repository/src/repository/config.rs | 7 +++- git-repository/tests/repository/config.rs | 33 +++++++++++++++++ 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/git-repository/src/config/mod.rs b/git-repository/src/config/mod.rs index 7b537b05a66..138ee7b4554 100644 --- a/git-repository/src/config/mod.rs +++ b/git-repository/src/config/mod.rs @@ -13,6 +13,16 @@ pub struct Snapshot<'repo> { pub(crate) repo: &'repo Repository, } +/// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped. +/// Note that the values will only affect this instance of the parent repository, and not other clones that may exist. +/// +/// Note that these values won't update even if the underlying file(s) change. +// TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk. +pub struct SnapshotMut<'repo> { + pub(crate) repo: &'repo mut Repository, + pub(crate) config: git_config::File<'static>, +} + pub(crate) mod section { pub fn is_trusted(meta: &git_config::file::Metadata) -> bool { meta.trust == git_sec::Trust::Full || meta.source.kind() != git_config::source::Kind::Repository diff --git a/git-repository/src/config/snapshot.rs b/git-repository/src/config/snapshot.rs index 61caba594fa..11f6d1df447 100644 --- a/git-repository/src/config/snapshot.rs +++ b/git-repository/src/config/snapshot.rs @@ -1,7 +1,4 @@ -use std::{ - borrow::Cow, - fmt::{Debug, Formatter}, -}; +use std::borrow::Cow; use crate::{ bstr::BStr, @@ -94,8 +91,40 @@ impl<'repo> Snapshot<'repo> { } } -impl Debug for Snapshot<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.repo.config.resolved.to_string()) +mod _impls { + use crate::config::{Snapshot, SnapshotMut}; + use std::fmt::{Debug, Formatter}; + use std::ops::{Deref, DerefMut}; + + impl Debug for Snapshot<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.repo.config.resolved.to_string()) + } + } + + impl Debug for SnapshotMut<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.config.to_string()) + } + } + + impl Drop for SnapshotMut<'_> { + fn drop(&mut self) { + self.repo.config.resolved = std::mem::take(&mut self.config).into(); + } + } + + impl Deref for SnapshotMut<'_> { + type Target = git_config::File<'static>; + + fn deref(&self) -> &Self::Target { + &self.config + } + } + + impl DerefMut for SnapshotMut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.config + } } } diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs index 956ac5de4f8..120fc16fdbf 100644 --- a/git-repository/src/repository/config.rs +++ b/git-repository/src/repository/config.rs @@ -4,12 +4,17 @@ use std::collections::BTreeSet; /// General Configuration impl crate::Repository { - /// Return /// 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. + pub fn config_snapshot_mut(&mut self) -> config::SnapshotMut<'_> { + let config = self.config.resolved.as_ref().clone(); + config::SnapshotMut { repo: self, config } + } + /// The options used to open the repository. pub fn open_options(&self) -> &crate::open::Options { &self.options diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index a3cf6003d55..69630bb97fe 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -200,3 +200,36 @@ fn access_values_and_identity() { } } } + +#[test] +fn config_snapshot_mut() { + let mut repo = named_repo("make_config_repo.sh").unwrap(); + let repo_clone = repo.clone(); + let key = "hallo.welt"; + let key_subsection = "hallo.unter.welt"; + assert_eq!(repo.config_snapshot().boolean(key), None, "no value there just yet"); + assert_eq!(repo.config_snapshot().string(key_subsection), None); + + { + let mut config = repo.config_snapshot_mut(); + config.set_raw_value("hallo", None, "welt", "true").unwrap(); + config.set_raw_value("hallo", Some("unter"), "welt", "value").unwrap(); + } + + assert_eq!( + repo.config_snapshot().boolean(key), + Some(true), + "value was set and applied" + ); + assert_eq!( + repo.config_snapshot().string(key_subsection).as_deref(), + Some("value".into()) + ); + + assert_eq!( + repo_clone.config_snapshot().boolean(key), + None, + "values are not written back automatically nor are they shared between clones" + ); + assert_eq!(repo_clone.config_snapshot().string(key_subsection), None); +} From 17455c9d93ad38bfee2560f5a4e60324dee3b4e5 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 14:47:21 +0800 Subject: [PATCH 111/125] feat: `File::section_mut_or_create_new_filter()` to allow chosing which sections to add values to. (#450) --- git-config/src/file/access/mutate.rs | 27 ++++++++++++++++++------ git-config/tests/file/mutable/section.rs | 16 +++++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/git-config/src/file/access/mutate.rs b/git-config/src/file/access/mutate.rs index 1b7880fe15a..6f5772f5a72 100644 --- a/git-config/src/file/access/mutate.rs +++ b/git-config/src/file/access/mutate.rs @@ -29,19 +29,34 @@ impl<'event> File<'event> { .expect("BUG: Section did not have id from lookup") .to_mut(nl)) } - - /// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_. + /// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_, or create a new section. pub fn section_mut_or_create_new<'a>( &'a mut self, name: impl AsRef, subsection_name: Option<&str>, + ) -> Result, section::header::Error> { + self.section_mut_or_create_new_filter(name, subsection_name, &mut |_| true) + } + + /// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_ **and** passes `filter`, or create + /// a new section. + pub fn section_mut_or_create_new_filter<'a>( + &'a mut self, + name: impl AsRef, + subsection_name: Option<&str>, + filter: &mut MetadataFilter, ) -> Result, section::header::Error> { let name = name.as_ref(); match self .section_ids_by_name_and_subname(name.as_ref(), subsection_name) - .map(|it| it.rev().next().expect("BUG: Section lookup vec was empty")) - { - Ok(id) => { + .ok() + .and_then(|it| { + it.rev().find(|id| { + let s = &self.sections[id]; + filter(s.meta()) + }) + }) { + Some(id) => { let nl = self.detect_newline_style_smallvec(); Ok(self .sections @@ -49,7 +64,7 @@ impl<'event> File<'event> { .expect("BUG: Section did not have id from lookup") .to_mut(nl)) } - Err(_) => self.new_section(name.to_owned(), subsection_name.map(|n| Cow::Owned(n.to_owned()))), + None => self.new_section(name.to_owned(), subsection_name.map(|n| Cow::Owned(n.to_owned()))), } } diff --git a/git-config/tests/file/mutable/section.rs b/git-config/tests/file/mutable/section.rs index ce963c1fe65..e6fe1c61d5a 100644 --- a/git-config/tests/file/mutable/section.rs +++ b/git-config/tests/file/mutable/section.rs @@ -10,7 +10,21 @@ fn section_mut_or_create_new_is_infallible() -> crate::Result { let section = config.section_mut_or_create_new("name", Some("subsection"))?; assert_eq!(section.header().name(), "name"); assert_eq!(section.header().subsection_name().expect("set"), "subsection"); - assert_eq!(section.to_bstring(), "[name \"subsection\"]\n"); + Ok(()) +} + +#[test] +fn section_mut_or_create_new_filter_may_reject_existing_sections() -> crate::Result { + let mut config = multi_value_section(); + let section = config.section_mut_or_create_new_filter("a", None, &mut |_| false)?; + assert_eq!(section.header().name(), "a"); + assert_eq!(section.header().subsection_name(), None); + assert_eq!(section.to_bstring(), "[a]\n"); + assert_eq!( + section.meta(), + &git_config::file::Metadata::api(), + "new sections are of source 'API'" + ); Ok(()) } From 9937d0e00df3a523484c7ae2850be2712a1a4c9a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 14:57:52 +0800 Subject: [PATCH 112/125] feat: `File::set_raw_value_filter()` to set values only in sections passing a filter. (#450) --- git-config/src/file/access/raw.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/git-config/src/file/access/raw.rs b/git-config/src/file/access/raw.rs index ac4e7df15ca..e2b84da6a38 100644 --- a/git-config/src/file/access/raw.rs +++ b/git-config/src/file/access/raw.rs @@ -415,7 +415,24 @@ impl<'event> File<'event> { Key: TryInto, Error = E>, section::key::Error: From, { - let mut section = self.section_mut_or_create_new(section_name, subsection_name)?; + self.set_raw_value_filter(section_name, subsection_name, key, new_value, &mut |_| true) + } + + /// Similar to [`set_raw_value()`][Self::set_raw_value()], but only sets existing values in sections matching + /// `filter`, creating a new section otherwise. + pub fn set_raw_value_filter<'b, Key, E>( + &mut self, + section_name: impl AsRef, + subsection_name: Option<&str>, + key: Key, + new_value: impl Into<&'b BStr>, + filter: &mut MetadataFilter, + ) -> Result>, crate::file::set_raw_value::Error> + where + Key: TryInto, Error = E>, + section::key::Error: From, + { + let mut section = self.section_mut_or_create_new_filter(section_name, subsection_name, filter)?; Ok(section.set(key.try_into().map_err(section::key::Error::from)?, new_value)) } From 1a74da5bb6969306f77663dfb8d63b04428d031f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 15:06:37 +0800 Subject: [PATCH 113/125] set remote protocol version using configuration instead of using a special mechanism. (#450) --- git-repository/src/remote/build.rs | 6 ---- git-repository/src/remote/connect.rs | 41 ++++++++++++------------ git-repository/src/remote/init.rs | 4 --- git-repository/src/types.rs | 3 -- git-repository/tests/remote/list_refs.rs | 9 ++++-- 5 files changed, 27 insertions(+), 36 deletions(-) diff --git a/git-repository/src/remote/build.rs b/git-repository/src/remote/build.rs index 6c06b99113d..08404cfaa52 100644 --- a/git-repository/src/remote/build.rs +++ b/git-repository/src/remote/build.rs @@ -4,12 +4,6 @@ use std::convert::TryInto; /// Builder methods impl Remote<'_> { - /// Force the transport protocol version to the given version, instead of loading it from the configuration. - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] - pub fn force_transport_protocol(mut self, version: impl Into>) -> Self { - self.force_protocol = version.into(); - self - } /// Set the `url` to be used when pushing data to a remote. pub fn push_url(self, url: Url) -> Result where diff --git a/git-repository/src/remote/connect.rs b/git-repository/src/remote/connect.rs index 52a6be33e90..a79fef1bb8f 100644 --- a/git-repository/src/remote/connect.rs +++ b/git-repository/src/remote/connect.rs @@ -41,6 +41,9 @@ impl<'repo> Remote<'repo> { } /// Connect to the url suitable for `direction` and return a handle through which operations can be performed. + /// + /// Note that the `protocol.version` configuration key affects the transport protocol used to connect, + /// with `2` being the default. #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] #[git_protocol::maybe_async::maybe_async] pub async fn connect

( @@ -66,28 +69,24 @@ impl<'repo> Remote<'repo> { Ok(url) } - let protocol = self.force_protocol.map_or_else( - || { - self.repo - .config - .resolved - .integer("protocol", None, "version") - .unwrap_or(Ok(2)) - .map_err(|err| Error::UnknownProtocol { given: err.input }) - .and_then(|num| { - Ok(match num { - 1 => Protocol::V1, - 2 => Protocol::V2, - num => { - return Err(Error::UnknownProtocol { - given: num.to_string().into(), - }) - } + let protocol = self + .repo + .config + .resolved + .integer("protocol", None, "version") + .unwrap_or(Ok(2)) + .map_err(|err| Error::UnknownProtocol { given: err.input }) + .and_then(|num| { + Ok(match num { + 1 => Protocol::V1, + 2 => Protocol::V2, + num => { + return Err(Error::UnknownProtocol { + given: num.to_string().into(), }) - }) - }, - Ok, - )?; + } + }) + })?; let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); let transport = git_protocol::transport::connect(sanitize(url)?, protocol).await?; diff --git a/git-repository/src/remote/init.rs b/git-repository/src/remote/init.rs index 8d08e7f8c5f..6ad08423382 100644 --- a/git-repository/src/remote/init.rs +++ b/git-repository/src/remote/init.rs @@ -40,8 +40,6 @@ impl<'repo> Remote<'repo> { .then(|| rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref())) .unwrap_or(Ok((None, None)))?; Ok(Remote { - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] - force_protocol: None, name, url, url_alias, @@ -67,8 +65,6 @@ impl<'repo> Remote<'repo> { .then(|| rewrite_urls(&repo.config, Some(&url), None)) .unwrap_or(Ok((None, None)))?; Ok(Remote { - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] - force_protocol: None, name: None, url: Some(url), url_alias, diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index 1f69cfda56a..6c2a401f3aa 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -198,9 +198,6 @@ pub struct Remote<'repo> { pub(crate) fetch_specs: Vec, /// Refspecs for use when pushing. pub(crate) push_specs: Vec, - /// The protocol to use when connecting, if explicitly overridden. - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] - pub(crate) force_protocol: Option, // /// Delete local tracking branches that don't exist on the remote anymore. // pub(crate) prune: bool, // /// Delete tags that don't exist on the remote anymore, equivalent to pruning the refspec `refs/tags/*:refs/tags/*`. diff --git a/git-repository/tests/remote/list_refs.rs b/git-repository/tests/remote/list_refs.rs index 6412592b0a2..ccdeb0baa24 100644 --- a/git-repository/tests/remote/list_refs.rs +++ b/git-repository/tests/remote/list_refs.rs @@ -12,8 +12,13 @@ mod blocking_io { Some(git::protocol::transport::Protocol::V2), Some(git::protocol::transport::Protocol::V1), ] { - let repo = remote::repo("clone"); - let remote = repo.find_remote("origin").unwrap().force_transport_protocol(version); + let mut repo = remote::repo("clone"); + if let Some(version) = version { + repo.config_snapshot_mut() + .set_raw_value("protocol", None, "version", (version as u8).to_string().as_str()) + .unwrap(); + } + let remote = repo.find_remote("origin").unwrap(); let connection = remote.connect(Fetch, progress::Discard).unwrap(); let refs = connection.list_refs().unwrap(); assert_eq!(refs.len(), 14, "it gets all remote refs, independently of the refspec."); From 541544953c52ff3df8c8e21f6aca366840faca3e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 15:20:19 +0800 Subject: [PATCH 114/125] refactor (#450) --- git-config/src/file/init/from_env.rs | 20 +++++++------------- git-config/tests/file/init/from_env.rs | 12 +++++++----- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/git-config/src/file/init/from_env.rs b/git-config/src/file/init/from_env.rs index b721758b55a..a75d9b1dd65 100644 --- a/git-config/src/file/init/from_env.rs +++ b/git-config/src/file/init/from_env.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, convert::TryFrom}; +use std::convert::TryFrom; use crate::{file, file::init, parse, parse::section, path::interpolate, File}; @@ -58,18 +58,12 @@ impl File<'static> { key_val: key.to_string(), })?; - let mut section = match config.section_mut(key.section_name, key.subsection_name) { - Ok(section) => section, - Err(_) => config.new_section( - key.section_name.to_owned(), - key.subsection_name.map(|subsection| Cow::Owned(subsection.to_owned())), - )?, - }; - - section.push( - section::Key::try_from(key.value_name.to_owned())?, - git_path::os_str_into_bstr(&value).expect("no illformed UTF-8").as_ref(), - ); + config + .section_mut_or_create_new(key.section_name, key.subsection_name)? + .push( + section::Key::try_from(key.value_name.to_owned())?, + git_path::os_str_into_bstr(&value).expect("no illformed UTF-8").as_ref(), + ); } let mut buf = Vec::new(); diff --git a/git-config/tests/file/init/from_env.rs b/git-config/tests/file/init/from_env.rs index 0cb9109b596..5ede003d3f2 100644 --- a/git-config/tests/file/init/from_env.rs +++ b/git-config/tests/file/init/from_env.rs @@ -35,19 +35,21 @@ fn parse_error_with_invalid_count() { #[test] #[serial] -fn single_key_value_pair() { +fn single_key_value_pair() -> crate::Result { let _env = Env::new() .set("GIT_CONFIG_COUNT", "1") .set("GIT_CONFIG_KEY_0", "core.key") .set("GIT_CONFIG_VALUE_0", "value"); - let config = File::from_env(Default::default()).unwrap().unwrap(); + let config = File::from_env(Default::default())?.unwrap(); + assert_eq!(config.raw_value("core", None, "key")?, Cow::<[u8]>::Borrowed(b"value")); assert_eq!( - config.raw_value("core", None, "key").unwrap(), - Cow::<[u8]>::Borrowed(b"value") + config.section("core", None)?.meta(), + &git_config::file::Metadata::from(git_config::Source::Env), + "source if configured correctly" ); - assert_eq!(config.num_values(), 1); + Ok(()) } #[test] From b6cd6ace412b0c0df4bacbe7ed7ef6608f27909c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 15:32:28 +0800 Subject: [PATCH 115/125] feat!: `file::SectionMut::push()` now supports values without key-value separator. (#450) These make a difference as those without `=` are considered boolean true. Currently pushing onto a section is the only way to write them. --- git-config/src/file/access/mutate.rs | 2 +- git-config/src/file/init/from_env.rs | 7 ++++++- git-config/src/file/mutable/section.rs | 13 ++++++++----- git-config/tests/file/mutable/section.rs | 15 ++++++++++++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/git-config/src/file/access/mutate.rs b/git-config/src/file/access/mutate.rs index 6f5772f5a72..b70930eca1d 100644 --- a/git-config/src/file/access/mutate.rs +++ b/git-config/src/file/access/mutate.rs @@ -116,7 +116,7 @@ impl<'event> File<'event> { /// # use git_config::parse::section; /// let mut git_config = git_config::File::default(); /// let mut section = git_config.new_section("hello", Some("world".into()))?; - /// section.push(section::Key::try_from("a")?, "b"); + /// section.push(section::Key::try_from("a")?, Some("b".into())); /// let nl = section.newline().to_owned(); /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}")); /// let _section = git_config.new_section("core", None); diff --git a/git-config/src/file/init/from_env.rs b/git-config/src/file/init/from_env.rs index a75d9b1dd65..aee3c47787a 100644 --- a/git-config/src/file/init/from_env.rs +++ b/git-config/src/file/init/from_env.rs @@ -62,7 +62,12 @@ impl File<'static> { .section_mut_or_create_new(key.section_name, key.subsection_name)? .push( section::Key::try_from(key.value_name.to_owned())?, - git_path::os_str_into_bstr(&value).expect("no illformed UTF-8").as_ref(), + Some( + git_path::os_str_into_bstr(&value) + .expect("no illformed UTF-8") + .as_ref() + .into(), + ), ); } diff --git a/git-config/src/file/mutable/section.rs b/git-config/src/file/mutable/section.rs index fac6f04febd..2adbcc13ba3 100644 --- a/git-config/src/file/mutable/section.rs +++ b/git-config/src/file/mutable/section.rs @@ -28,16 +28,19 @@ pub struct SectionMut<'a, 'event> { /// Mutating methods. impl<'a, 'event> SectionMut<'a, 'event> { - /// Adds an entry to the end of this section name `key` and `value`. - pub fn push<'b>(&mut self, key: Key<'event>, value: impl Into<&'b BStr>) { + /// Adds an entry to the end of this section name `key` and `value`. If `value` is None`, no equal sign will be written leaving + /// just the key. This is useful for boolean values which are true if merely the key exists. + pub fn push<'b>(&mut self, key: Key<'event>, value: Option<&'b BStr>) { let body = &mut self.section.body.0; if let Some(ws) = &self.whitespace.pre_key { body.push(Event::Whitespace(ws.clone())); } body.push(Event::SectionKey(key)); - body.extend(self.whitespace.key_value_separators()); - body.push(Event::Value(escape_value(value.into()).into())); + if let Some(value) = value { + body.extend(self.whitespace.key_value_separators()); + body.push(Event::Value(escape_value(value.into()).into())); + } if self.implicit_newline { body.push(Event::Newline(BString::from(self.newline.to_vec()).into())); } @@ -86,7 +89,7 @@ impl<'a, 'event> SectionMut<'a, 'event> { pub fn set<'b>(&mut self, key: Key<'event>, value: impl Into<&'b BStr>) -> Option> { match self.key_and_value_range_by(&key) { None => { - self.push(key, value); + self.push(key, Some(value.into())); None } Some((_, value_range)) => { diff --git a/git-config/tests/file/mutable/section.rs b/git-config/tests/file/mutable/section.rs index e6fe1c61d5a..aae8ac1252b 100644 --- a/git-config/tests/file/mutable/section.rs +++ b/git-config/tests/file/mutable/section.rs @@ -115,10 +115,19 @@ mod set { } mod push { - use std::convert::TryFrom; + use std::convert::{TryFrom, TryInto}; use git_config::parse::section::Key; + #[test] + fn none_as_value_omits_the_key_value_separator() -> crate::Result { + let mut file = git_config::File::default(); + let mut section = file.section_mut_or_create_new("a", Some("sub"))?; + section.push("key".try_into()?, None); + assert_eq!(file.to_bstring(), "[a \"sub\"]\n\tkey\n"); + Ok(()) + } + #[test] fn whitespace_is_derived_from_whitespace_before_first_value() -> crate::Result { for (input, expected_pre_key, expected_sep) in [ @@ -169,7 +178,7 @@ mod push { let mut config = git_config::File::default(); let mut section = config.new_section("a", None).unwrap(); section.set_implicit_newline(false); - section.push(Key::try_from("k").unwrap(), value); + section.push(Key::try_from("k").unwrap(), Some(value.into())); let expected = expected .replace("$head", &format!("[a]{nl}", nl = section.newline())) .replace("$nl", §ion.newline().to_string()); @@ -193,7 +202,7 @@ mod set_leading_whitespace { let nl = section.newline().to_owned(); section.set_leading_whitespace(Some(Cow::Owned(BString::from(format!("{nl}\t"))))); - section.push(Key::try_from("a")?, "v"); + section.push(Key::try_from("a")?, Some("v".into())); assert_eq!(config.to_string(), format!("[core]{nl}{nl}\ta = v{nl}")); Ok(()) From 8f4ad3cbd4c9254b6b234d9944d6298b5ca2bb60 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 15:34:07 +0800 Subject: [PATCH 116/125] Adjust to changes in `git-config` (#450) --- git-repository/src/create.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/git-repository/src/create.rs b/git-repository/src/create.rs index 8fb1c2ed5f0..14050eb79e8 100644 --- a/git-repository/src/create.rs +++ b/git-repository/src/create.rs @@ -183,13 +183,13 @@ pub fn into( let caps = fs_capabilities.unwrap_or_else(|| git_worktree::fs::Capabilities::probe(&dot_git)); let mut core = config.new_section("core", None).expect("valid section name"); - core.push(key("repositoryformatversion"), "0"); - core.push(key("filemode"), bool(caps.executable_bit)); - core.push(key("bare"), bool(bare)); - core.push(key("logallrefupdates"), bool(!bare)); - core.push(key("symlinks"), bool(caps.symlink)); - core.push(key("ignorecase"), bool(caps.ignore_case)); - core.push(key("precomposeunicode"), bool(caps.precompose_unicode)); + core.push(key("repositoryformatversion"), Some("0".into())); + core.push(key("filemode"), Some(bool(caps.executable_bit).into())); + core.push(key("bare"), Some(bool(bare).into())); + core.push(key("logallrefupdates"), Some(bool(!bare).into())); + core.push(key("symlinks"), Some(bool(caps.symlink).into())); + core.push(key("ignorecase"), Some(bool(caps.ignore_case).into())); + core.push(key("precomposeunicode"), Some(bool(caps.precompose_unicode).into())); } let mut cursor = PathCursor(&mut dot_git); let config_path = cursor.at("config"); From 7c585162454c476fe93f032c8a2329cffd7c054f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 17:45:54 +0800 Subject: [PATCH 117/125] fix: Keep track of a severe limitation and prepare tests for fixing it. (#450) This also changes behaviour, but merely removes a hack in `Boolean` which considered empty strings true, even though they are supposed to be false. --- git-config/src/file/access/comfort.rs | 20 +++++++++--- git-config/src/file/access/read_only.rs | 4 +-- git-config/src/file/section/body.rs | 4 ++- git-config/src/lib.rs | 6 ++++ git-config/src/parse/nom/mod.rs | 1 + git-config/src/values/boolean.rs | 10 +++--- git-config/tests/file/access/read_only.rs | 38 +++++++++++++++-------- git-config/tests/values/boolean.rs | 2 +- 8 files changed, 59 insertions(+), 26 deletions(-) diff --git a/git-config/src/file/access/comfort.rs b/git-config/src/file/access/comfort.rs index 0f15288a724..b50a0f8f07c 100644 --- a/git-config/src/file/access/comfort.rs +++ b/git-config/src/file/access/comfort.rs @@ -2,7 +2,8 @@ use std::{borrow::Cow, convert::TryFrom}; use bstr::BStr; -use crate::{file::MetadataFilter, value, File}; +use crate::parse::section; +use crate::{file::MetadataFilter, lookup, value, File}; /// Comfortable API for accessing values impl<'event> File<'event> { @@ -80,9 +81,20 @@ impl<'event> File<'event> { key: impl AsRef, filter: &mut MetadataFilter, ) -> Option> { - self.raw_value_filter(section_name, subsection_name, key, filter) - .ok() - .map(|v| crate::Boolean::try_from(v).map(|b| b.into())) + let section_name = section_name.as_ref(); + let key = key.as_ref(); + match self.raw_value_filter(section_name, subsection_name, key, filter) { + Ok(v) => Some(crate::Boolean::try_from(v).map(|b| b.into())), + Err(lookup::existing::Error::KeyMissing) => { + let section = self + .section_filter(section_name, subsection_name, filter) + .ok() + .flatten()?; + let key = section::Key::try_from(key).ok()?; + section.key_and_value_range_by(&key).map(|_| Ok(true)) + } + Err(_err) => None, + } } /// Like [`value()`][File::value()], but returning an `Option` if the integer wasn't found. diff --git a/git-config/src/file/access/read_only.rs b/git-config/src/file/access/read_only.rs index 4915cb3f8f1..3c4c0551a40 100644 --- a/git-config/src/file/access/read_only.rs +++ b/git-config/src/file/access/read_only.rs @@ -103,13 +103,13 @@ impl<'event> File<'event> { /// a_value, /// vec![ /// Boolean(true), - /// Boolean(true), + /// Boolean(false), /// Boolean(false), /// ] /// ); /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Vec = git_config.values("core", None, "c").unwrap(); - /// assert_eq!(c_value, vec![Boolean(true)]); + /// assert_eq!(c_value, vec![Boolean(false)]); /// # Ok::<(), Box>(()) /// ``` /// diff --git a/git-config/src/file/section/body.rs b/git-config/src/file/section/body.rs index 875f1c45ed5..7a390bba772 100644 --- a/git-config/src/file/section/body.rs +++ b/git-config/src/file/section/body.rs @@ -14,10 +14,12 @@ pub struct Body<'event>(pub(crate) crate::parse::section::Events<'event>); /// Access impl<'event> Body<'event> { /// Retrieves the last matching value in a section with the given key, if present. + /// + /// Note that we consider values without key separator `=` non-existing. #[must_use] pub fn value(&self, key: impl AsRef) -> Option> { let key = Key::from_str_unchecked(key.as_ref()); - let (_, range) = self.key_and_value_range_by(&key)?; + let (_key_range, range) = self.key_and_value_range_by(&key)?; let mut concatenated = BString::default(); for event in &self.0[range] { diff --git a/git-config/src/lib.rs b/git-config/src/lib.rs index 2f9e2f90c9b..dc801ce4700 100644 --- a/git-config/src/lib.rs +++ b/git-config/src/lib.rs @@ -22,6 +22,12 @@ //! - Legacy headers like `[section.subsection]` are supposed to be turned into to lower case and compared //! case-sensitively. We keep its case and compare case-insensitively. //! +//! # Limitations +//! +//! - Keys like `a.b` are interpreted as true but `a.b = ` are interpreted as false in `git`. +//! This library, however, considers both to be true due to its inability to differentiate them at present. +//! Even to though the information is available, the API doesn't expose it. +//! //! [^1]: When read values do not need normalization and it wasn't parsed in 'owned' mode. //! //! [`git-config` files]: https://git-scm.com/docs/git-config#_configuration_file diff --git a/git-config/src/parse/nom/mod.rs b/git-config/src/parse/nom/mod.rs index c54a963a34b..a0d105f110b 100644 --- a/git-config/src/parse/nom/mod.rs +++ b/git-config/src/parse/nom/mod.rs @@ -288,6 +288,7 @@ fn config_value<'a>(i: &'a [u8], dispatch: &mut impl FnMut(Event<'a>)) -> IResul let (i, newlines) = value_impl(i, dispatch)?; Ok((i, newlines)) } else { + // TODO: remove this and fix everything, or else we can't fix the boolean limitation dispatch(Event::Value(Cow::Borrowed("".into()))); Ok((i, 0)) } diff --git a/git-config/src/values/boolean.rs b/git-config/src/values/boolean.rs index 365e00f3366..ed10fb2210c 100644 --- a/git-config/src/values/boolean.rs +++ b/git-config/src/values/boolean.rs @@ -69,12 +69,12 @@ impl serde::Serialize for Boolean { } fn parse_true(value: &BStr) -> bool { - value.eq_ignore_ascii_case(b"yes") - || value.eq_ignore_ascii_case(b"on") - || value.eq_ignore_ascii_case(b"true") - || value.is_empty() + value.eq_ignore_ascii_case(b"yes") || value.eq_ignore_ascii_case(b"on") || value.eq_ignore_ascii_case(b"true") } fn parse_false(value: &BStr) -> bool { - value.eq_ignore_ascii_case(b"no") || value.eq_ignore_ascii_case(b"off") || value.eq_ignore_ascii_case(b"false") + value.eq_ignore_ascii_case(b"no") + || value.eq_ignore_ascii_case(b"off") + || value.eq_ignore_ascii_case(b"false") + || value.is_empty() } diff --git a/git-config/tests/file/access/read_only.rs b/git-config/tests/file/access/read_only.rs index acc4e728256..a7994997f17 100644 --- a/git-config/tests/file/access/read_only.rs +++ b/git-config/tests/file/access/read_only.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, convert::TryFrom, error::Error}; +use std::{borrow::Cow, convert::TryFrom}; use bstr::BStr; use git_config::{ @@ -40,15 +40,15 @@ fn get_value_for_all_provided_values() -> crate::Result { assert!(!config.value::("core", None, "bool-explicit")?.0); assert!(!config.boolean("core", None, "bool-explicit").expect("exists")?); - assert!(config.value::("core", None, "bool-implicit")?.0); assert!( - config - .try_value::("core", None, "bool-implicit") - .expect("exists")? - .0 + !config.value::("core", None, "bool-implicit")?.0, + "this cannot work like in git as the value isn't there for us" + ); + assert!( + !config.boolean("core", None, "bool-implicit").expect("present")?, + "this should work, but doesn't yet" ); - assert!(config.boolean("core", None, "bool-implicit").expect("present")?); assert_eq!(config.string("doesnt", None, "exist"), None); assert_eq!( @@ -161,7 +161,14 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { let file = File::try_from(config)?; // Checks that we check the last entry first still - assert!(file.value::("core", None, "bool-implicit")?.0); + assert!( + !file.value::("core", None, "bool-implicit")?.0, + "this one can't do it, needs special handling" + ); + assert!( + !file.boolean("core", None, "bool-implicit").expect("present")?, + "this should work, but doesn't yet" + ); assert!(!file.value::("core", None, "bool-explicit")?.0); @@ -196,15 +203,20 @@ fn value_names_are_case_insensitive() -> crate::Result { } #[test] -fn single_section() -> Result<(), Box> { +fn single_section() { let config = File::try_from("[core]\na=b\nc").unwrap(); let first_value = config.string("core", None, "a").unwrap(); - let second_value: Boolean = config.value("core", None, "c")?; - assert_eq!(first_value, cow_str("b")); - assert!(second_value.0); - Ok(()) + assert!( + config.raw_value("core", None, "c").is_ok(), + "value is considered false as it is without '=', so it's like not present, BUT this parses strangely which needs fixing (see TODO nom parse)" + ); + + assert!( + !config.boolean("core", None, "c").expect("present").unwrap(), + "asking for a boolean is true true, as per git rules, but doesn't work yet" + ); } #[test] diff --git a/git-config/tests/values/boolean.rs b/git-config/tests/values/boolean.rs index 61488748b1d..8a3bb80f140 100644 --- a/git-config/tests/values/boolean.rs +++ b/git-config/tests/values/boolean.rs @@ -10,6 +10,7 @@ fn from_str_false() -> crate::Result { assert!(!Boolean::try_from(b("off"))?.0); assert!(!Boolean::try_from(b("false"))?.0); assert!(!Boolean::try_from(b("0"))?.0); + assert!(!Boolean::try_from(b(""))?.0); Ok(()) } @@ -18,7 +19,6 @@ fn from_str_true() -> crate::Result { assert_eq!(Boolean::try_from(b("yes")).map(Into::into), Ok(true)); assert_eq!(Boolean::try_from(b("on")), Ok(Boolean(true))); assert_eq!(Boolean::try_from(b("true")), Ok(Boolean(true))); - assert_eq!(Boolean::try_from(b("")).map(|b| b.is_true()), Ok(true)); assert!(Boolean::try_from(b("1"))?.0); assert!(Boolean::try_from(b("+10"))?.0); assert!(Boolean::try_from(b("-1"))?.0); From d35cd2a12c6db3d655ce10cad5c027bde99e19b4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 17:50:57 +0800 Subject: [PATCH 118/125] feat: `SnapshotMut::apply_cli_overrides()` to make it easy to support things like `-c` (#450) --- git-repository/src/config/mod.rs | 1 + git-repository/src/config/snapshot.rs | 53 ++++++++++++ git-repository/src/lib.rs | 10 ++- git-repository/tests/repository/config.rs | 98 ++++++++++++++++------- 4 files changed, 133 insertions(+), 29 deletions(-) diff --git a/git-repository/src/config/mod.rs b/git-repository/src/config/mod.rs index 138ee7b4554..c880b2019ba 100644 --- a/git-repository/src/config/mod.rs +++ b/git-repository/src/config/mod.rs @@ -5,6 +5,7 @@ use crate::{bstr::BString, permission, remote, repository::identity, revision::s pub(crate) mod cache; mod snapshot; +pub use snapshot::apply_cli_overrides; /// A platform to access configuration values as read from disk. /// diff --git a/git-repository/src/config/snapshot.rs b/git-repository/src/config/snapshot.rs index 11f6d1df447..cfa50ceac39 100644 --- a/git-repository/src/config/snapshot.rs +++ b/git-repository/src/config/snapshot.rs @@ -81,6 +81,59 @@ impl<'repo> Snapshot<'repo> { } } +/// +pub mod apply_cli_overrides { + use crate::bstr::{BString, ByteSlice}; + use crate::config::SnapshotMut; + use std::convert::TryFrom; + + /// The error returned by [SnapshotMut::apply_cli_overrides()][super::SnapshotMut::apply_cli_overrides]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("{input:?} is not a valid configuration key. Examples are 'core.abbrev' or 'remote.origin.url'")] + InvalidKey { input: BString }, + #[error("Key {key:?} could not be parsed")] + SectionKey { + key: BString, + source: git_config::parse::section::key::Error, + }, + #[error(transparent)] + SectionHeader(#[from] git_config::parse::section::header::Error), + } + + impl SnapshotMut<'_> { + /// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true` + /// to the repository configuration, marked with [source CLI][git_config::Source::Cli]. + pub fn apply_cli_overrides( + &mut self, + values: impl IntoIterator>, + ) -> Result<(), Error> { + let mut file = git_config::File::new(git_config::file::Metadata::from(git_config::Source::Cli)); + for key_value in values { + let key_value = key_value.into(); + let mut tokens = key_value.splitn(2, |b| *b == b'=').map(|v| v.trim()); + let key = tokens.next().expect("always one value").as_bstr(); + let value = tokens.next(); + let key = git_config::parse::key(key.to_str().map_err(|_| Error::InvalidKey { input: key.into() })?) + .ok_or_else(|| Error::InvalidKey { input: key.into() })?; + let mut section = file.section_mut_or_create_new(key.section_name, key.subsection_name)?; + section.push( + git_config::parse::section::Key::try_from(key.value_name.to_owned()).map_err(|err| { + Error::SectionKey { + source: err, + key: key.value_name.into(), + } + })?, + value.map(|v| v.as_bstr()), + ); + } + self.config.append(file); + Ok(()) + } + } +} + /// Utilities and additional access impl<'repo> Snapshot<'repo> { /// Returns the underlying configuration implementation for a complete API, despite being a little less convenient. diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index bfd23ba3949..e7986c145f5 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -345,7 +345,8 @@ pub mod discover; /// pub mod env { - use std::ffi::OsString; + use crate::bstr::{BString, ByteVec}; + use std::ffi::{OsStr, OsString}; /// Equivalent to `std::env::args_os()`, but with precomposed unicode on MacOS and other apple platforms. #[cfg(not(target_vendor = "apple"))] @@ -364,6 +365,13 @@ pub mod env { None => arg, }) } + + /// Convert the given `input` into a `BString`, useful as `parse(try_from_os_str = )` function. + pub fn os_str_to_bstring(input: &OsStr) -> Result { + Vec::from_os_string(input.into()) + .map(Into::into) + .map_err(|_| input.to_string_lossy().into_owned()) + } } mod kind; diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index 69630bb97fe..ca13ed34ad5 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -201,35 +201,77 @@ fn access_values_and_identity() { } } -#[test] -fn config_snapshot_mut() { - let mut repo = named_repo("make_config_repo.sh").unwrap(); - let repo_clone = repo.clone(); - let key = "hallo.welt"; - let key_subsection = "hallo.unter.welt"; - assert_eq!(repo.config_snapshot().boolean(key), None, "no value there just yet"); - assert_eq!(repo.config_snapshot().string(key_subsection), None); - - { - let mut config = repo.config_snapshot_mut(); - config.set_raw_value("hallo", None, "welt", "true").unwrap(); - config.set_raw_value("hallo", Some("unter"), "welt", "value").unwrap(); +mod config_section_mut { + use crate::named_repo; + + #[test] + fn values_are_set_in_memory_only() { + let mut repo = named_repo("make_config_repo.sh").unwrap(); + let repo_clone = repo.clone(); + let key = "hallo.welt"; + let key_subsection = "hallo.unter.welt"; + assert_eq!(repo.config_snapshot().boolean(key), None, "no value there just yet"); + assert_eq!(repo.config_snapshot().string(key_subsection), None); + + { + let mut config = repo.config_snapshot_mut(); + config.set_raw_value("hallo", None, "welt", "true").unwrap(); + config.set_raw_value("hallo", Some("unter"), "welt", "value").unwrap(); + } + + assert_eq!( + repo.config_snapshot().boolean(key), + Some(true), + "value was set and applied" + ); + assert_eq!( + repo.config_snapshot().string(key_subsection).as_deref(), + Some("value".into()) + ); + + assert_eq!( + repo_clone.config_snapshot().boolean(key), + None, + "values are not written back automatically nor are they shared between clones" + ); + assert_eq!(repo_clone.config_snapshot().string(key_subsection), None); } - assert_eq!( - repo.config_snapshot().boolean(key), - Some(true), - "value was set and applied" - ); - assert_eq!( - repo.config_snapshot().string(key_subsection).as_deref(), - Some("value".into()) - ); + #[test] + fn apply_cli_overrides() -> crate::Result { + let mut repo = named_repo("make_config_repo.sh").unwrap(); + repo.config_snapshot_mut().apply_cli_overrides([ + "a.b=c", + "remote.origin.url = url", + "implicit.bool-false = ", + "implicit.bool-true", + ])?; + // TODO: fix printing - reversing order of bool-true/false prints wrongly, making round-tripping impossible. - assert_eq!( - repo_clone.config_snapshot().boolean(key), - None, - "values are not written back automatically nor are they shared between clones" - ); - assert_eq!(repo_clone.config_snapshot().string(key_subsection), None); + let config = repo.config_snapshot(); + assert_eq!(config.string("a.b").expect("present").as_ref(), "c"); + assert_eq!(config.string("remote.origin.url").expect("present").as_ref(), "url"); + assert_eq!( + config.string("implicit.bool-true"), + None, + "no keysep is interpreted as 'not present' as we don't make up values" + ); + assert_eq!( + config.string("implicit.bool-false").expect("present").as_ref(), + "", + "empty values are fine" + ); + assert_eq!( + config.boolean("implicit.bool-false"), + Some(false), + "empty values are boolean true" + ); + assert_eq!( + config.boolean("implicit.bool-true"), + Some(true), + "values without key-sep are true" + ); + + Ok(()) + } } From 45a30f0f31a99cda5cf105408e9c3905f43071f2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 17:51:32 +0800 Subject: [PATCH 119/125] feat: Support for `-c/--config` in `gix` (#450) --- src/plumbing/main.rs | 15 +++++++++++---- src/plumbing/options.rs | 9 +++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 11df5cf6664..78508eeb758 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -7,7 +7,7 @@ use std::{ }, }; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Parser; use git_repository::bstr::io::BufReadExt; use gitoxide_core as core; @@ -55,20 +55,27 @@ pub fn main() -> Result<()> { let format = args.format; let cmd = args.cmd; let object_hash = args.object_hash; + let config = args.config; use git_repository as git; let repository = args.repository; enum Mode { Strict, Lenient, } - let repository = move |mode: Mode| { + let repository = move |mode: Mode| -> Result { let mut mapping: git::sec::trust::Mapping = Default::default(); let toggle = matches!(mode, Mode::Strict); mapping.full = mapping.full.strict_config(toggle); mapping.reduced = mapping.reduced.strict_config(toggle); - git::ThreadSafeRepository::discover_opts(repository, Default::default(), mapping) + let mut repo = git::ThreadSafeRepository::discover_opts(repository, Default::default(), mapping) .map(git::Repository::from) - .map(|r| r.apply_environment()) + .map(|r| r.apply_environment())?; + if !config.is_empty() { + repo.config_snapshot_mut() + .apply_cli_overrides(config) + .context("Unable to parse command-line configuration")?; + } + Ok(repo) }; let progress; diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index d5627f3acf3..1c493207553 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -1,3 +1,5 @@ +use git_repository as git; +use git_repository::bstr::BString; use std::path::PathBuf; use gitoxide_core as core; @@ -11,6 +13,13 @@ pub struct Args { #[clap(short = 'r', long, default_value = ".")] pub repository: PathBuf, + /// Add these values to the configuration in the form of `key=value` or `key`. + /// + /// For example, if `key` is `core.abbrev`, set configuration like `[core] abbrev = key`, + /// or `remote.origin.url = foo` to set `[remote "origin"] url = foo`. + #[clap(long, short = 'c', parse(try_from_os_str = git::env::os_str_to_bstring))] + pub config: Vec, + #[clap(long, short = 't')] /// The amount of threads to use for some operations. /// From 2770431f8742d6249574f53f06ec0026f9d241d6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 17:52:33 +0800 Subject: [PATCH 120/125] thanks clippy --- git-config/src/file/mutable/section.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-config/src/file/mutable/section.rs b/git-config/src/file/mutable/section.rs index 2adbcc13ba3..8a6c131a4c6 100644 --- a/git-config/src/file/mutable/section.rs +++ b/git-config/src/file/mutable/section.rs @@ -39,7 +39,7 @@ impl<'a, 'event> SectionMut<'a, 'event> { body.push(Event::SectionKey(key)); if let Some(value) = value { body.extend(self.whitespace.key_value_separators()); - body.push(Event::Value(escape_value(value.into()).into())); + body.push(Event::Value(escape_value(value).into())); } if self.implicit_newline { body.push(Event::Newline(BString::from(self.newline.to_vec()).into())); From 7d30eb36e6aa7f679c97c5056cd5453f8e89ab10 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 17:54:10 +0800 Subject: [PATCH 121/125] fix docs (#450) --- git-repository/src/config/snapshot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-repository/src/config/snapshot.rs b/git-repository/src/config/snapshot.rs index cfa50ceac39..0dbbc83d409 100644 --- a/git-repository/src/config/snapshot.rs +++ b/git-repository/src/config/snapshot.rs @@ -87,7 +87,7 @@ pub mod apply_cli_overrides { use crate::config::SnapshotMut; use std::convert::TryFrom; - /// The error returned by [SnapshotMut::apply_cli_overrides()][super::SnapshotMut::apply_cli_overrides]. + /// The error returned by [SnapshotMut::apply_cli_overrides()][crate::config::SnapshotMut::apply_cli_overrides()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { From dda995790c260131048484a11e66185b9c841311 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 18:46:34 +0800 Subject: [PATCH 122/125] change!: remove `gix free remote ref-list` in favor of `gix remote refs` (#450) The functinality is the same, but the latter is built on top of a repository which is slightly less flexible, but preferable over maintaining a non-repo version. --- gitoxide-core/src/lib.rs | 2 - gitoxide-core/src/pack/receive.rs | 6 +- gitoxide-core/src/remote/mod.rs | 1 - gitoxide-core/src/remote/refs/async_io.rs | 64 ------- gitoxide-core/src/remote/refs/blocking_io.rs | 51 ------ gitoxide-core/src/remote/refs/mod.rs | 110 ------------ gitoxide-core/src/repository/remote.rs | 6 +- src/plumbing/main.rs | 47 +---- src/plumbing/options.rs | 28 --- tests/journey/gix.sh | 168 +++++++++--------- ...te ref-list-no-networking-in-small-failure | 6 - .../remote/refs}/file-v-any | 0 .../remote/refs}/file-v-any-json | 0 13 files changed, 97 insertions(+), 392 deletions(-) delete mode 100644 gitoxide-core/src/remote/mod.rs delete mode 100644 gitoxide-core/src/remote/refs/async_io.rs delete mode 100644 gitoxide-core/src/remote/refs/blocking_io.rs delete mode 100644 gitoxide-core/src/remote/refs/mod.rs delete mode 100644 tests/snapshots/plumbing/no-repo/remote/ref-list/remote ref-list-no-networking-in-small-failure rename tests/snapshots/plumbing/{no-repo/remote/ref-list => repository/remote/refs}/file-v-any (100%) rename tests/snapshots/plumbing/{no-repo/remote/ref-list => repository/remote/refs}/file-v-any-json (100%) diff --git a/gitoxide-core/src/lib.rs b/gitoxide-core/src/lib.rs index 158e19e5287..c146f582dad 100644 --- a/gitoxide-core/src/lib.rs +++ b/gitoxide-core/src/lib.rs @@ -51,8 +51,6 @@ pub mod mailmap; #[cfg(feature = "organize")] pub mod organize; pub mod pack; -#[cfg(any(feature = "async-client", feature = "blocking-client"))] -pub mod remote; pub mod repository; #[cfg(all(feature = "async-client", feature = "blocking-client"))] diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index f194eccdd08..30455eef927 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -18,7 +18,7 @@ use git_repository::{ Progress, }; -use crate::{remote::refs::JsonRef, OutputFormat}; +use crate::OutputFormat; pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=3; @@ -272,7 +272,7 @@ pub struct JsonOutcome { pub index_path: Option, pub data_path: Option, - pub refs: Vec, + pub refs: Vec, } impl JsonOutcome { @@ -298,7 +298,7 @@ fn print(out: &mut impl io::Write, res: pack::bundle::write::Outcome, refs: &[Re print_hash_and_path(out, "index", res.index.index_hash, res.index_path)?; print_hash_and_path(out, "pack", res.index.data_hash, res.data_path)?; writeln!(out)?; - crate::remote::refs::print(out, refs)?; + crate::repository::remote::refs::print(out, refs)?; Ok(()) } diff --git a/gitoxide-core/src/remote/mod.rs b/gitoxide-core/src/remote/mod.rs deleted file mode 100644 index f3ce0777cae..00000000000 --- a/gitoxide-core/src/remote/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod refs; diff --git a/gitoxide-core/src/remote/refs/async_io.rs b/gitoxide-core/src/remote/refs/async_io.rs deleted file mode 100644 index 18241c21d0e..00000000000 --- a/gitoxide-core/src/remote/refs/async_io.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::io; - -use async_trait::async_trait; -use futures_io::AsyncBufRead; -use git_repository::{ - protocol, - protocol::fetch::{Ref, Response}, - Progress, -}; - -use super::{Context, LsRemotes}; -use crate::{net, remote::refs::print, OutputFormat}; - -#[async_trait(?Send)] -impl protocol::fetch::Delegate for LsRemotes { - async fn receive_pack( - &mut self, - input: impl AsyncBufRead + Unpin + 'async_trait, - progress: impl Progress, - refs: &[Ref], - previous_response: &Response, - ) -> io::Result<()> { - unreachable!("not called for ls-refs") - } -} - -pub async fn list( - protocol: Option, - url: &str, - progress: impl Progress, - ctx: Context, -) -> anyhow::Result<()> { - let url = url.to_owned(); - let transport = net::connect(url, protocol.unwrap_or_default().into()).await?; - blocking::unblock( - // `blocking` really needs a way to unblock futures, which is what it does internally anyway. - // Both fetch() needs unblocking as it executes blocking code within the future, and the other - // block does blocking IO because it's primarily a blocking codebase. - move || { - futures_lite::future::block_on(async move { - let mut delegate = LsRemotes::default(); - protocol::fetch( - transport, - &mut delegate, - protocol::credentials::helper, - progress, - protocol::FetchConnection::TerminateOnSuccessfulCompletion, - ) - .await?; - - match ctx.format { - OutputFormat::Human => drop(print(ctx.out, &delegate.refs)), - #[cfg(feature = "serde1")] - OutputFormat::Json => serde_json::to_writer_pretty( - ctx.out, - &delegate.refs.into_iter().map(super::JsonRef::from).collect::>(), - )?, - } - Ok(()) - }) - }, - ) - .await -} diff --git a/gitoxide-core/src/remote/refs/blocking_io.rs b/gitoxide-core/src/remote/refs/blocking_io.rs deleted file mode 100644 index a2e6d82469c..00000000000 --- a/gitoxide-core/src/remote/refs/blocking_io.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::io; - -use git_repository::{ - protocol, - protocol::fetch::{Ref, Response}, - Progress, -}; - -#[cfg(feature = "serde1")] -use super::JsonRef; -use super::{print, Context, LsRemotes}; -use crate::{net, OutputFormat}; - -impl protocol::fetch::Delegate for LsRemotes { - fn receive_pack( - &mut self, - _input: impl io::BufRead, - _progress: impl Progress, - _refs: &[Ref], - _previous_response: &Response, - ) -> io::Result<()> { - unreachable!("not called for ls-refs") - } -} - -pub fn list( - protocol: Option, - url: &str, - progress: impl Progress, - ctx: Context, -) -> anyhow::Result<()> { - let transport = net::connect(url, protocol.unwrap_or_default().into())?; - let mut delegate = LsRemotes::default(); - protocol::fetch( - transport, - &mut delegate, - protocol::credentials::helper, - progress, - protocol::FetchConnection::TerminateOnSuccessfulCompletion, - )?; - - match ctx.format { - OutputFormat::Human => drop(print(ctx.out, &delegate.refs)), - #[cfg(feature = "serde1")] - OutputFormat::Json => serde_json::to_writer_pretty( - ctx.out, - &delegate.refs.into_iter().map(JsonRef::from).collect::>(), - )?, - }; - Ok(()) -} diff --git a/gitoxide-core/src/remote/refs/mod.rs b/gitoxide-core/src/remote/refs/mod.rs deleted file mode 100644 index cc4dfa0a265..00000000000 --- a/gitoxide-core/src/remote/refs/mod.rs +++ /dev/null @@ -1,110 +0,0 @@ -use git_repository::{ - protocol, - protocol::{ - fetch::{Action, Arguments, Ref, Response}, - transport, - }, -}; - -use crate::OutputFormat; - -pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=2; - -use std::io; - -#[derive(Default)] -struct LsRemotes { - refs: Vec, -} - -impl protocol::fetch::DelegateBlocking for LsRemotes { - fn prepare_fetch( - &mut self, - _version: transport::Protocol, - _server: &transport::client::Capabilities, - _features: &mut Vec<(&str, Option<&str>)>, - refs: &[Ref], - ) -> io::Result { - self.refs = refs.into(); - Ok(Action::Cancel) - } - - fn negotiate( - &mut self, - _refs: &[Ref], - _arguments: &mut Arguments, - _previous_response: Option<&Response>, - ) -> io::Result { - unreachable!("not to be called due to Action::Close in `prepare_fetch`") - } -} - -#[cfg(feature = "async-client")] -mod async_io; -#[cfg(feature = "async-client")] -pub use self::async_io::list; - -#[cfg(feature = "blocking-client")] -mod blocking_io; -#[cfg(feature = "blocking-client")] -pub use blocking_io::list; - -pub struct Context { - pub thread_limit: Option, - pub format: OutputFormat, - pub out: W, -} - -#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] -pub enum JsonRef { - Peeled { - path: String, - tag: String, - object: String, - }, - Direct { - path: String, - object: String, - }, - Symbolic { - path: String, - target: String, - object: String, - }, -} - -impl From for JsonRef { - fn from(value: Ref) -> Self { - match value { - Ref::Direct { path, object } => JsonRef::Direct { - path: path.to_string(), - object: object.to_string(), - }, - Ref::Symbolic { path, target, object } => JsonRef::Symbolic { - path: path.to_string(), - target: target.to_string(), - object: object.to_string(), - }, - Ref::Peeled { path, tag, object } => JsonRef::Peeled { - path: path.to_string(), - tag: tag.to_string(), - object: object.to_string(), - }, - } - } -} - -pub(crate) fn print(mut out: impl io::Write, refs: &[Ref]) -> io::Result<()> { - for r in refs { - match r { - Ref::Direct { path, object } => writeln!(&mut out, "{} {}", object.to_hex(), path), - Ref::Peeled { path, object, tag } => { - writeln!(&mut out, "{} {} tag:{}", object.to_hex(), path, tag) - } - Ref::Symbolic { path, target, object } => { - writeln!(&mut out, "{} {} symref-target:{}", object.to_hex(), path, target) - } - }?; - } - Ok(()) -} diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs index 345a12677e9..0270ca624df 100644 --- a/gitoxide-core/src/repository/remote.rs +++ b/gitoxide-core/src/repository/remote.rs @@ -8,11 +8,15 @@ mod net { pub mod refs { use crate::OutputFormat; + pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=2; + pub struct Context { pub format: OutputFormat, pub name: Option, pub url: Option, } + + pub(crate) use super::print; } #[git::protocol::maybe_async::maybe_async] @@ -110,4 +114,4 @@ mod net { } } #[cfg(any(feature = "blocking-client", feature = "async-client"))] -pub use net::{refs, refs_fn as refs}; +pub use net::{refs, refs_fn as refs, JsonRef}; diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 78508eeb758..b3d51c10f4c 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -109,7 +109,7 @@ pub fn main() -> Result<()> { verbose, progress, progress_keep_open, - None, + core::repository::remote::refs::PROGRESS_RANGE, move |progress, out, _err| { core::repository::remote::refs( repository(Mode::Lenient)?, @@ -122,8 +122,11 @@ pub fn main() -> Result<()> { } #[cfg(feature = "gitoxide-core-async-client")] { - let (_handle, progress) = - async_util::prepare(verbose, "remote-refs", Some(core::remote::refs::PROGRESS_RANGE)); + let (_handle, progress) = async_util::prepare( + verbose, + "remote-refs", + Some(core::repository::remote::refs::PROGRESS_RANGE), + ); futures_lite::future::block_on(core::repository::remote::refs( repository(Mode::Lenient)?, progress, @@ -143,44 +146,6 @@ pub fn main() -> Result<()> { ) .map(|_| ()), Subcommands::Free(subcommands) => match subcommands { - #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] - free::Subcommands::Remote(subcommands) => match subcommands { - #[cfg(feature = "gitoxide-core-async-client")] - free::remote::Subcommands::RefList { protocol, url } => { - let (_handle, progress) = - async_util::prepare(verbose, "remote-ref-list", Some(core::remote::refs::PROGRESS_RANGE)); - futures_lite::future::block_on(core::remote::refs::list( - protocol, - &url, - progress, - core::remote::refs::Context { - thread_limit, - format, - out: std::io::stdout(), - }, - )) - } - #[cfg(feature = "gitoxide-core-blocking-client")] - free::remote::Subcommands::RefList { protocol, url } => prepare_and_run( - "remote-ref-list", - verbose, - progress, - progress_keep_open, - core::remote::refs::PROGRESS_RANGE, - move |progress, out, _err| { - core::remote::refs::list( - protocol, - &url, - progress, - core::remote::refs::Context { - thread_limit, - format, - out, - }, - ) - }, - ), - }, free::Subcommands::CommitGraph(subcommands) => match subcommands { free::commitgraph::Subcommands::Verify { path, statistics } => prepare_and_run( "commitgraph-verify", diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index 1c493207553..921e175a4b4 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -251,10 +251,6 @@ pub mod free { #[derive(Debug, clap::Subcommand)] #[clap(visible_alias = "no-repo")] pub enum Subcommands { - /// Subcommands for interacting with git remote server. - #[clap(subcommand)] - #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] - Remote(remote::Subcommands), /// Subcommands for interacting with commit-graphs #[clap(subcommand)] CommitGraph(commitgraph::Subcommands), @@ -270,30 +266,6 @@ pub mod free { Index(index::Platform), } - /// - #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] - pub mod remote { - use gitoxide_core as core; - - #[derive(Debug, clap::Subcommand)] - pub enum Subcommands { - /// List remote references from a remote identified by a url. - /// - /// This is the plumbing equivalent of `git ls-remote`. - /// Supported URLs are documented here: - RefList { - /// The protocol version to use. Valid values are 1 and 2 - #[clap(long, short = 'p')] - protocol: Option, - - /// the URLs or path from which to receive references - /// - /// See here for a list of supported URLs: - url: String, - }, - } - } - /// pub mod commitgraph { use std::path::PathBuf; diff --git a/tests/journey/gix.sh b/tests/journey/gix.sh index 4b1b902e149..cded2d91421 100644 --- a/tests/journey/gix.sh +++ b/tests/journey/gix.sh @@ -68,6 +68,89 @@ title "gix (with repository)" fi ) ) + + title "gix remote" + (when "running 'remote'" + snapshot="$snapshot/remote" + title "gix remote refs" + (with "the 'refs' subcommand" + snapshot="$snapshot/refs" + (small-repo-in-sandbox + if [[ "$kind" != "small" ]]; then + + if [[ "$kind" != "async" ]]; then + (with "file:// protocol" + (with "version 1" + it "generates the correct output" && { + WITH_SNAPSHOT="$snapshot/file-v-any" \ + expect_run $SUCCESSFULLY "$exe_plumbing" -c protocol.version=1 remote -u .git refs + } + ) + (with "version 2" + it "generates the correct output" && { + WITH_SNAPSHOT="$snapshot/file-v-any" \ + expect_run $SUCCESSFULLY "$exe_plumbing" -c protocol.version=2 remote -u "$PWD" refs + } + ) + if test "$kind" = "max"; then + (with "--format json" + it "generates the correct output in JSON format" && { + WITH_SNAPSHOT="$snapshot/file-v-any-json" \ + expect_run $SUCCESSFULLY "$exe_plumbing" --format json remote -u . refs + } + ) + fi + ) + fi + + (with "git:// protocol" + launch-git-daemon + (with "version 1" + it "generates the correct output" && { + WITH_SNAPSHOT="$snapshot/file-v-any" \ + expect_run $SUCCESSFULLY "$exe_plumbing" --config protocol.version=1 remote --url git://localhost/ refs + } + ) + (with "version 2" + it "generates the correct output" && { + WITH_SNAPSHOT="$snapshot/file-v-any" \ + expect_run $SUCCESSFULLY "$exe_plumbing" -c protocol.version=2 remote -u git://localhost/ refs + } + ) + ) + if [[ "$kind" == "small" ]]; then + (with "https:// protocol (in small builds)" + it "fails as http is not compiled in" && { + WITH_SNAPSHOT="$snapshot/fail-http-in-small" \ + expect_run $WITH_FAILURE "$exe_plumbing" -c protocol.version=1 remote -u https://github.com/byron/gitoxide refs + } + ) + fi + (on_ci + if [[ "$kind" = "max" ]]; then + (with "https:// protocol" + (with "version 1" + it "generates the correct output" && { + expect_run $SUCCESSFULLY "$exe_plumbing" -c protocol.version=1 remote -u https://github.com/byron/gitoxide refs + } + ) + (with "version 2" + it "generates the correct output" && { + expect_run $SUCCESSFULLY "$exe_plumbing" -c protocol.version=2 remote -u https://github.com/byron/gitoxide refs + } + ) + ) + fi + ) + else + it "fails as the CLI doesn't include networking in 'small' mode" && { + WITH_SNAPSHOT="$snapshot/remote ref-list-no-networking-in-small-failure" \ + expect_run 2 "$exe_plumbing" -c protocol.version=1 remote -u .git refs + } + fi + ) + ) + ) ) (with "gix free" @@ -527,91 +610,6 @@ title "gix (with repository)" ) ) - title "gix free remote" - (when "running 'remote'" - snapshot="$snapshot/remote" - title "gix remote ref-list" - (with "the 'ref-list' subcommand" - snapshot="$snapshot/ref-list" - (small-repo-in-sandbox - if [[ "$kind" != "small" ]]; then - - if [[ "$kind" != "async" ]]; then - (with "file:// protocol" - (with "version 1" - it "generates the correct output" && { - WITH_SNAPSHOT="$snapshot/file-v-any" \ - expect_run $SUCCESSFULLY "$exe_plumbing" free remote ref-list -p 1 .git - } - ) - (with "version 2" - it "generates the correct output" && { - WITH_SNAPSHOT="$snapshot/file-v-any" \ - expect_run $SUCCESSFULLY "$exe_plumbing" free remote ref-list --protocol 2 "$PWD/.git" - } - ) - if test "$kind" = "max"; then - (with "--format json" - it "generates the correct output in JSON format" && { - WITH_SNAPSHOT="$snapshot/file-v-any-json" \ - expect_run $SUCCESSFULLY "$exe_plumbing" --format json free remote ref-list .git - } - ) - fi - ) - fi - - (with "git:// protocol" - launch-git-daemon - (with "version 1" - it "generates the correct output" && { - WITH_SNAPSHOT="$snapshot/file-v-any" \ - expect_run $SUCCESSFULLY "$exe_plumbing" free remote ref-list -p 1 git://localhost/ - } - ) - (with "version 2" - it "generates the correct output" && { - WITH_SNAPSHOT="$snapshot/file-v-any" \ - expect_run $SUCCESSFULLY "$exe_plumbing" free remote ref-list -p 2 git://localhost/ - } - ) - ) - if [[ "$kind" == "small" ]]; then - (with "https:// protocol (in small builds)" - it "fails as http is not compiled in" && { - WITH_SNAPSHOT="$snapshot/fail-http-in-small" \ - expect_run $WITH_FAILURE "$exe_plumbing" free remote ref-list -p 1 https://github.com/byron/gitoxide - } - ) - fi - (on_ci - if [[ "$kind" = "max" ]]; then - (with "https:// protocol" - (with "version 1" - it "generates the correct output" && { - expect_run $SUCCESSFULLY "$exe_plumbing" free remote ref-list -p 1 https://github.com/byron/gitoxide - } - ) - (with "version 2" - it "generates the correct output" && { - expect_run $SUCCESSFULLY "$exe_plumbing" free remote ref-list -p 2 https://github.com/byron/gitoxide - } - ) - ) - fi - ) - else - it "fails as the CLI doesn't include networking in 'small' mode" && { - WITH_SNAPSHOT="$snapshot/remote ref-list-no-networking-in-small-failure" \ - expect_run 2 "$exe_plumbing" free remote ref-list -p 1 .git - } - fi - ) - ) - ) - - - title "gix free commit-graph" (when "running 'commit-graph'" snapshot="$snapshot/commit-graph" diff --git a/tests/snapshots/plumbing/no-repo/remote/ref-list/remote ref-list-no-networking-in-small-failure b/tests/snapshots/plumbing/no-repo/remote/ref-list/remote ref-list-no-networking-in-small-failure deleted file mode 100644 index 885fe4f7406..00000000000 --- a/tests/snapshots/plumbing/no-repo/remote/ref-list/remote ref-list-no-networking-in-small-failure +++ /dev/null @@ -1,6 +0,0 @@ -error: Found argument 'remote' which wasn't expected, or isn't valid in this context - -USAGE: - gix free - -For more information try --help \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/remote/ref-list/file-v-any b/tests/snapshots/plumbing/repository/remote/refs/file-v-any similarity index 100% rename from tests/snapshots/plumbing/no-repo/remote/ref-list/file-v-any rename to tests/snapshots/plumbing/repository/remote/refs/file-v-any diff --git a/tests/snapshots/plumbing/no-repo/remote/ref-list/file-v-any-json b/tests/snapshots/plumbing/repository/remote/refs/file-v-any-json similarity index 100% rename from tests/snapshots/plumbing/no-repo/remote/ref-list/file-v-any-json rename to tests/snapshots/plumbing/repository/remote/refs/file-v-any-json From 7a871c2a5691ae973804ff66af9608070733b366 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 18:59:42 +0800 Subject: [PATCH 123/125] fix config tests on windows (#450) --- git-config/tests/file/mutable/section.rs | 3 ++- .../refs/remote ref-list-no-networking-in-small-failure | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure diff --git a/git-config/tests/file/mutable/section.rs b/git-config/tests/file/mutable/section.rs index aae8ac1252b..0f878bb8fde 100644 --- a/git-config/tests/file/mutable/section.rs +++ b/git-config/tests/file/mutable/section.rs @@ -124,7 +124,8 @@ mod push { let mut file = git_config::File::default(); let mut section = file.section_mut_or_create_new("a", Some("sub"))?; section.push("key".try_into()?, None); - assert_eq!(file.to_bstring(), "[a \"sub\"]\n\tkey\n"); + let expected = format!("[a \"sub\"]{nl}\tkey{nl}", nl = section.newline()); + assert_eq!(file.to_bstring(), expected); Ok(()) } diff --git a/tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure b/tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure new file mode 100644 index 00000000000..7279eeb4f14 --- /dev/null +++ b/tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure @@ -0,0 +1,6 @@ +error: Found argument 'refs' which wasn't expected, or isn't valid in this context + +USAGE: + gix remote [OPTIONS] + +For more information try --help \ No newline at end of file From 08c50a47fa901457194539c7db74ad47ab2f8b60 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 19:21:32 +0800 Subject: [PATCH 124/125] fix: Properly handle boolean values such that `a` is true but `a=` is false. (#450) This is even consistent when no booleans are used, such that `a` has no value as if it is not present, it's only available for booleans which must be specified. --- git-config/src/file/access/read_only.rs | 2 +- git-config/src/file/mutable/section.rs | 3 ++- git-config/src/file/section/body.rs | 11 +++++++---- git-config/src/lib.rs | 6 ------ git-config/src/parse/nom/mod.rs | 4 +++- git-config/tests/file/access/read_only.rs | 20 ++++++++++---------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/git-config/src/file/access/read_only.rs b/git-config/src/file/access/read_only.rs index 3c4c0551a40..56e274d7b05 100644 --- a/git-config/src/file/access/read_only.rs +++ b/git-config/src/file/access/read_only.rs @@ -40,7 +40,7 @@ impl<'event> File<'event> { /// let config = r#" /// [core] /// a = 10k - /// c + /// c = false /// "#; /// let git_config = git_config::File::try_from(config)?; /// // You can either use the turbofish to determine the type... diff --git a/git-config/src/file/mutable/section.rs b/git-config/src/file/mutable/section.rs index 8a6c131a4c6..38fd3ec6f1c 100644 --- a/git-config/src/file/mutable/section.rs +++ b/git-config/src/file/mutable/section.rs @@ -92,7 +92,8 @@ impl<'a, 'event> SectionMut<'a, 'event> { self.push(key, Some(value.into())); None } - Some((_, value_range)) => { + Some((key_range, value_range)) => { + let value_range = value_range.unwrap_or(key_range.end - 1..key_range.end); let range_start = value_range.start; let ret = self.remove_internal(value_range); self.section diff --git a/git-config/src/file/section/body.rs b/git-config/src/file/section/body.rs index 7a390bba772..9f904fa64ea 100644 --- a/git-config/src/file/section/body.rs +++ b/git-config/src/file/section/body.rs @@ -20,6 +20,7 @@ impl<'event> Body<'event> { pub fn value(&self, key: impl AsRef) -> Option> { let key = Key::from_str_unchecked(key.as_ref()); let (_key_range, range) = self.key_and_value_range_by(&key)?; + let range = range?; let mut concatenated = BString::default(); for event in &self.0[range] { @@ -109,9 +110,10 @@ impl<'event> Body<'event> { &self.0 } - /// Returns the the range containing the value events for the `key`. - /// If the value is not found, then this returns an empty range. - pub(crate) fn key_and_value_range_by(&self, key: &Key<'_>) -> Option<(Range, Range)> { + /// Returns the the range containing the value events for the `key`, with value range being `None` if there is no key-value separator + /// and only a 'fake' Value event with an empty string in side. + /// If the value is not found, `None` is returned. + pub(crate) fn key_and_value_range_by(&self, key: &Key<'_>) -> Option<(Range, Option>)> { let mut value_range = Range::default(); let mut key_start = None; for (i, e) in self.0.iter().enumerate().rev() { @@ -140,7 +142,8 @@ impl<'event> Body<'event> { // value end needs to be offset by one so that the last value's index // is included in the range let value_range = value_range.start..value_range.end + 1; - (key_start..value_range.end, value_range) + let key_range = key_start..value_range.end; + (key_range, (value_range.start != key_start + 1).then(|| value_range)) }) } } diff --git a/git-config/src/lib.rs b/git-config/src/lib.rs index dc801ce4700..2f9e2f90c9b 100644 --- a/git-config/src/lib.rs +++ b/git-config/src/lib.rs @@ -22,12 +22,6 @@ //! - Legacy headers like `[section.subsection]` are supposed to be turned into to lower case and compared //! case-sensitively. We keep its case and compare case-insensitively. //! -//! # Limitations -//! -//! - Keys like `a.b` are interpreted as true but `a.b = ` are interpreted as false in `git`. -//! This library, however, considers both to be true due to its inability to differentiate them at present. -//! Even to though the information is available, the API doesn't expose it. -//! //! [^1]: When read values do not need normalization and it wasn't parsed in 'owned' mode. //! //! [`git-config` files]: https://git-scm.com/docs/git-config#_configuration_file diff --git a/git-config/src/parse/nom/mod.rs b/git-config/src/parse/nom/mod.rs index a0d105f110b..1a2da7b8516 100644 --- a/git-config/src/parse/nom/mod.rs +++ b/git-config/src/parse/nom/mod.rs @@ -288,7 +288,9 @@ fn config_value<'a>(i: &'a [u8], dispatch: &mut impl FnMut(Event<'a>)) -> IResul let (i, newlines) = value_impl(i, dispatch)?; Ok((i, newlines)) } else { - // TODO: remove this and fix everything, or else we can't fix the boolean limitation + // This is a special way of denoting 'empty' values which a lot of code depends on. + // Hence, rather to fix this everywhere else, leave it here and fix it where it matters, namely + // when it's about differentiating between a missing key-vaue separator, and one followed by emptiness. dispatch(Event::Value(Cow::Borrowed("".into()))); Ok((i, 0)) } diff --git a/git-config/tests/file/access/read_only.rs b/git-config/tests/file/access/read_only.rs index a7994997f17..50941ac77ed 100644 --- a/git-config/tests/file/access/read_only.rs +++ b/git-config/tests/file/access/read_only.rs @@ -41,12 +41,12 @@ fn get_value_for_all_provided_values() -> crate::Result { assert!(!config.boolean("core", None, "bool-explicit").expect("exists")?); assert!( - !config.value::("core", None, "bool-implicit")?.0, + config.value::("core", None, "bool-implicit").is_err(), "this cannot work like in git as the value isn't there for us" ); assert!( - !config.boolean("core", None, "bool-implicit").expect("present")?, - "this should work, but doesn't yet" + config.boolean("core", None, "bool-implicit").expect("present")?, + "this should work" ); assert_eq!(config.string("doesnt", None, "exist"), None); @@ -177,11 +177,11 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { #[test] fn section_names_are_case_insensitive() -> crate::Result { - let config = "[core] bool-implicit"; + let config = "[core] a=true"; let file = File::try_from(config)?; assert_eq!( - file.value::("core", None, "bool-implicit").unwrap(), - file.value::("CORE", None, "bool-implicit").unwrap() + file.value::("core", None, "a").unwrap(), + file.value::("CORE", None, "a").unwrap() ); Ok(()) @@ -209,13 +209,13 @@ fn single_section() { assert_eq!(first_value, cow_str("b")); assert!( - config.raw_value("core", None, "c").is_ok(), - "value is considered false as it is without '=', so it's like not present, BUT this parses strangely which needs fixing (see TODO nom parse)" + config.raw_value("core", None, "c").is_err(), + "value is considered false as it is without '=', so it's like not present" ); assert!( - !config.boolean("core", None, "c").expect("present").unwrap(), - "asking for a boolean is true true, as per git rules, but doesn't work yet" + config.boolean("core", None, "c").expect("present").unwrap(), + "asking for a boolean is true true, as per git rules" ); } From dcd66197315a9826102b7439b1ab33e72593fccf Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 22 Aug 2022 19:25:09 +0800 Subject: [PATCH 125/125] remove TODO, doesn't apply anymore (#450) --- git-repository/tests/repository/config.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs index ca13ed34ad5..2149a089649 100644 --- a/git-repository/tests/repository/config.rs +++ b/git-repository/tests/repository/config.rs @@ -243,10 +243,9 @@ mod config_section_mut { repo.config_snapshot_mut().apply_cli_overrides([ "a.b=c", "remote.origin.url = url", - "implicit.bool-false = ", "implicit.bool-true", + "implicit.bool-false = ", ])?; - // TODO: fix printing - reversing order of bool-true/false prints wrongly, making round-tripping impossible. let config = repo.config_snapshot(); assert_eq!(config.string("a.b").expect("present").as_ref(), "c");