Skip to content

Commit

Permalink
feat: add `Repository::upstream_branch_and_remote_name_for_tracking_b…
Browse files Browse the repository at this point in the history
…ranch()`

It's a way to learn about the Remote and upstream branch which would
match the given local tracking branch.
  • Loading branch information
Byron committed Jan 13, 2025
1 parent 66e6834 commit a5236cf
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 11 deletions.
84 changes: 76 additions & 8 deletions gix/src/repository/config/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.<short_branch_name>.merge` configuration key.
/// The returned value corresponds to the `branch.<short_branch_name>.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 <name>`.
///
/// 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")]
Expand Down Expand Up @@ -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<Option<(FullName, crate::Remote<'_>)>, 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.
Expand Down
21 changes: 19 additions & 2 deletions gix/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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)]
Expand All @@ -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::<Vec<_>>().join(", "))]
AmbiguousRemotes { remotes: Vec<crate::remote::Name<'static>> },
#[error(transparent)]
ValidateUpstreamBranch(#[from] gix_ref::name::Error),
}
}

///
#[cfg(feature = "attributes")]
pub mod pathspec_defaults_ignore_case {
Expand Down
2 changes: 1 addition & 1 deletion gix/src/repository/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<worktree::Proxy<'_>>> {
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),
Expand Down
Binary file not shown.
6 changes: 6 additions & 0 deletions gix/tests/fixtures/make_remote_config_repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
50 changes: 50 additions & 0 deletions gix/tests/gix/repository/config/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")?;
Expand Down

0 comments on commit a5236cf

Please sign in to comment.