diff --git a/gix/src/repository/config/branch.rs b/gix/src/repository/config/branch.rs index 661dbb264d0..d9d231460ad 100644 --- a/gix/src/repository/config/branch.rs +++ b/gix/src/repository/config/branch.rs @@ -5,7 +5,9 @@ use gix_ref::{FullName, FullNameRef}; use crate::bstr::BStr; use crate::config::cache::util::ApplyLeniencyDefault; use crate::config::tree::{Branch, Push}; -use crate::repository::{branch_remote_ref_name, branch_remote_tracking_ref_name}; +use crate::repository::{ + branch_remote_ref_name, branch_remote_tracking_ref_name, upstream_branch_and_remote_name_for_tracking_branch, +}; use crate::{push, remote}; /// Query configuration related to branches. @@ -20,19 +22,18 @@ impl crate::Repository { self.subsection_str_names_of("branch") } - /// Returns the validated reference on the remote associated with the given `name`, + /// Returns the validated reference name of the upstream branch on the remote associated with the given `name`, /// which will be used when *merging*. - /// The returned value corresponds to the `branch..merge` configuration key. + /// The returned value corresponds to the `branch..merge` configuration key for [`remote::Direction::Fetch`]. + /// For the [push direction](`remote::Direction::Push`) the Git configuration is used for a variety of different outcomes, + /// similar to what would happen when running `git push `. /// - /// Returns `None` if there is no value at the given key, or if no remote or remote ref is configured. - /// May return an error if the reference name to be returned is invalid. + /// Returns `None` if there is nothing configured, or if no remote or remote ref is configured. /// /// ### Note /// - /// This name refers to what Git calls upstream branch (as opposed to upstream *tracking* branch). + /// The returned name refers to what Git calls upstream branch (as opposed to upstream *tracking* branch). /// The value is also fast to retrieve compared to its tracking branch. - /// Also note that a [remote::Direction] isn't used here as Git only supports (and requires) configuring - /// the remote to fetch from, not the one to push to. /// /// See also [`Reference::remote_ref_name()`](crate::Reference::remote_ref_name()). #[doc(alias = "branch_upstream_name", alias = "git2")] @@ -125,6 +126,73 @@ impl crate::Repository { .map(|res| res.map_err(Into::into)) } + /// Given a local `tracking_branch` name, find the remote that maps to it along with the name of the branch on + /// the side of the remote, also called upstream branch. + /// + /// Return `Ok(None)` if there is no remote with fetch-refspecs that would match `tracking_branch` on the right-hand side, + /// or `Err` if the matches were ambiguous. + /// + /// ### Limitations + /// + /// A single valid mapping is required as fine-grained matching isn't implemented yet. This means that + pub fn upstream_branch_and_remote_for_tracking_branch( + &self, + tracking_branch: &FullNameRef, + ) -> Result)>, upstream_branch_and_remote_name_for_tracking_branch::Error> { + use upstream_branch_and_remote_name_for_tracking_branch::Error; + if tracking_branch.category() != Some(gix_ref::Category::RemoteBranch) { + return Err(Error::BranchCategory { + full_name: tracking_branch.to_owned(), + }); + } + + let null = self.object_hash().null(); + let item_to_search = gix_refspec::match_group::Item { + full_ref_name: tracking_branch.as_bstr(), + target: &null, + object: None, + }; + let mut candidates = Vec::new(); + let mut ambiguous_remotes = Vec::new(); + for remote_name in self.remote_names() { + let remote = self.find_remote(remote_name.as_ref())?; + let match_group = gix_refspec::MatchGroup::from_fetch_specs( + remote + .refspecs(remote::Direction::Fetch) + .iter() + .map(|spec| spec.to_ref()), + ); + let out = match_group.match_rhs(Some(item_to_search).into_iter()); + match &out.mappings[..] { + [] => {} + [one] => candidates.push((remote.clone(), one.lhs.clone().into_owned())), + [..] => ambiguous_remotes.push(remote), + } + } + + if candidates.len() == 1 { + let (remote, candidate) = candidates.pop().expect("just checked for one entry"); + let upstream_branch = match candidate { + gix_refspec::match_group::SourceRef::FullName(name) => gix_ref::FullName::try_from(name.into_owned())?, + gix_refspec::match_group::SourceRef::ObjectId(_) => { + unreachable!("Such a reverse mapping isn't ever produced") + } + }; + return Ok(Some((upstream_branch, remote))); + } + if ambiguous_remotes.len() + candidates.len() > 1 { + return Err(Error::AmbiguousRemotes { + remotes: ambiguous_remotes + .into_iter() + .map(|r| r.name) + .chain(candidates.into_iter().map(|(r, _)| r.name)) + .flatten() + .collect(), + }); + } + Ok(None) + } + /// Returns the unvalidated name of the remote associated with the given `short_branch_name`, /// typically `main` instead of `refs/heads/main`. /// In some cases, the returned name will be an URL. diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 4d5ca4093fa..20e2fa4d8c7 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -330,7 +330,6 @@ pub mod index_from_tree { /// pub mod branch_remote_ref_name { - /// The error returned by [Repository::branch_remote_ref_name()](crate::Repository::branch_remote_ref_name()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] @@ -346,7 +345,6 @@ pub mod branch_remote_ref_name { /// pub mod branch_remote_tracking_ref_name { - /// The error returned by [Repository::branch_remote_tracking_ref_name()](crate::Repository::branch_remote_tracking_ref_name()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] @@ -360,6 +358,25 @@ pub mod branch_remote_tracking_ref_name { } } +/// +pub mod upstream_branch_and_remote_name_for_tracking_branch { + /// The error returned by [Repository::upstream_branch_and_remote_name_for_tracking_branch()](crate::Repository::upstream_branch_and_remote_for_tracking_branch()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The input branch '{}' needs to be a remote tracking branch", full_name.as_bstr())] + BranchCategory { full_name: gix_ref::FullName }, + #[error(transparent)] + FindRemote(#[from] crate::remote::find::existing::Error), + #[error("Found ambiguous remotes without 1:1 mapping or more than one match: {}", remotes.iter() + .map(|r| r.as_bstr().to_string()) + .collect::>().join(", "))] + AmbiguousRemotes { remotes: Vec> }, + #[error(transparent)] + ValidateUpstreamBranch(#[from] gix_ref::name::Error), + } +} + /// #[cfg(feature = "attributes")] pub mod pathspec_defaults_ignore_case { diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 09ff3910886..16c6686d317 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -12,7 +12,7 @@ impl crate::Repository { /// Note that these need additional processing to become usable, but provide a first glimpse a typical worktree information. pub fn worktrees(&self) -> std::io::Result>> { let mut res = Vec::new(); - let iter = match std::fs::read_dir(dbg!(self.common_dir()).join("worktrees")) { + let iter = match std::fs::read_dir(self.common_dir().join("worktrees")) { Ok(iter) => iter, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(res), Err(err) => return Err(err), diff --git a/gix/tests/fixtures/generated-archives/make_remote_config_repos.tar b/gix/tests/fixtures/generated-archives/make_remote_config_repos.tar index 2f2838bbf7c..15b3589e6ca 100644 Binary files a/gix/tests/fixtures/generated-archives/make_remote_config_repos.tar and b/gix/tests/fixtures/generated-archives/make_remote_config_repos.tar differ diff --git a/gix/tests/fixtures/make_remote_config_repos.sh b/gix/tests/fixtures/make_remote_config_repos.sh index efd444d1c2d..89040f5f723 100755 --- a/gix/tests/fixtures/make_remote_config_repos.sh +++ b/gix/tests/fixtures/make_remote_config_repos.sh @@ -138,6 +138,12 @@ git clone fetch multiple-remotes git remote add with/two/slashes ../fetch && git fetch with/two/slashes git remote add with/two ../fetch && git fetch with/two + # add a specialised refspec mapping + git config --add remote.with/two.fetch +refs/heads/special:refs/remotes/with/two/special + # make sure the ref exists + cp .git/refs/remotes/with/two/main .git/refs/remotes/with/two/special + # show Git can checkout such an ambiguous refspec + git checkout -b track-special with/two/special git checkout -b main --track origin/main git checkout -b other-main --track other/main ) \ No newline at end of file diff --git a/gix/tests/gix/repository/config/remote.rs b/gix/tests/gix/repository/config/remote.rs index ba1e5bdb373..fe4f3f1a2a6 100644 --- a/gix/tests/gix/repository/config/remote.rs +++ b/gix/tests/gix/repository/config/remote.rs @@ -107,6 +107,21 @@ mod branch_remote { .as_bstr(), "refs/remotes/remote_repo/main" ); + let (upstream, remote_name) = repo + .upstream_branch_and_remote_for_tracking_branch("refs/remotes/remote_repo/main".try_into()?)? + .expect("mapping exists"); + assert_eq!(upstream.as_bstr(), "refs/heads/main"); + assert_eq!( + remote_name.name().expect("non-anonymous remote").as_bstr(), + "remote_repo" + ); + + assert_eq!( + repo.upstream_branch_and_remote_for_tracking_branch("refs/remotes/missing-remote/main".try_into()?)?, + None, + "It's OK to find nothing" + ); + for direction in [remote::Direction::Fetch, remote::Direction::Push] { assert_eq!( repo.branch_remote_name("main", direction) @@ -145,6 +160,41 @@ mod branch_remote { Ok(()) } + #[test] + fn upstream_branch_and_remote_name_for_tracking_branch() -> crate::Result { + let repo = repo("multiple-remotes")?; + for expected_remote_name in ["other", "with/two"] { + let (upstream, remote) = repo + .upstream_branch_and_remote_for_tracking_branch( + format!("refs/remotes/{expected_remote_name}/main") + .as_str() + .try_into()?, + )? + .expect("mapping exists"); + assert_eq!(remote.name().expect("named remote").as_bstr(), expected_remote_name); + assert_eq!(upstream.as_bstr(), "refs/heads/main"); + } + let err = repo + .upstream_branch_and_remote_for_tracking_branch("refs/remotes/with/two/slashes/main".try_into()?) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Found ambiguous remotes without 1:1 mapping or more than one match: with/two, with/two/slashes", + "we aren't very specific report an error just like Git does in case of multi-remote ambiguity" + ); + + let (upstream, remote) = repo + .upstream_branch_and_remote_for_tracking_branch("refs/remotes/with/two/special".try_into()?)? + .expect("mapping exists"); + assert_eq!(remote.name().expect("non-anonymous remote").as_bstr(), "with/two"); + assert_eq!( + upstream.as_bstr(), + "refs/heads/special", + "it finds a single mapping even though there are two refspecs" + ); + Ok(()) + } + #[test] fn push_default() -> crate::Result { let repo = repo("fetch")?;