Skip to content

Commit a5236cf

Browse files
committed
feat: add Repository::upstream_branch_and_remote_name_for_tracking_branch()
It's a way to learn about the Remote and upstream branch which would match the given local tracking branch.
1 parent 66e6834 commit a5236cf

File tree

6 files changed

+152
-11
lines changed

6 files changed

+152
-11
lines changed

gix/src/repository/config/branch.rs

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use gix_ref::{FullName, FullNameRef};
55
use crate::bstr::BStr;
66
use crate::config::cache::util::ApplyLeniencyDefault;
77
use crate::config::tree::{Branch, Push};
8-
use crate::repository::{branch_remote_ref_name, branch_remote_tracking_ref_name};
8+
use crate::repository::{
9+
branch_remote_ref_name, branch_remote_tracking_ref_name, upstream_branch_and_remote_name_for_tracking_branch,
10+
};
911
use crate::{push, remote};
1012

1113
/// Query configuration related to branches.
@@ -20,19 +22,18 @@ impl crate::Repository {
2022
self.subsection_str_names_of("branch")
2123
}
2224

23-
/// Returns the validated reference on the remote associated with the given `name`,
25+
/// Returns the validated reference name of the upstream branch on the remote associated with the given `name`,
2426
/// which will be used when *merging*.
25-
/// The returned value corresponds to the `branch.<short_branch_name>.merge` configuration key.
27+
/// The returned value corresponds to the `branch.<short_branch_name>.merge` configuration key for [`remote::Direction::Fetch`].
28+
/// For the [push direction](`remote::Direction::Push`) the Git configuration is used for a variety of different outcomes,
29+
/// similar to what would happen when running `git push <name>`.
2630
///
27-
/// Returns `None` if there is no value at the given key, or if no remote or remote ref is configured.
28-
/// May return an error if the reference name to be returned is invalid.
31+
/// Returns `None` if there is nothing configured, or if no remote or remote ref is configured.
2932
///
3033
/// ### Note
3134
///
32-
/// This name refers to what Git calls upstream branch (as opposed to upstream *tracking* branch).
35+
/// The returned name refers to what Git calls upstream branch (as opposed to upstream *tracking* branch).
3336
/// The value is also fast to retrieve compared to its tracking branch.
34-
/// Also note that a [remote::Direction] isn't used here as Git only supports (and requires) configuring
35-
/// the remote to fetch from, not the one to push to.
3637
///
3738
/// See also [`Reference::remote_ref_name()`](crate::Reference::remote_ref_name()).
3839
#[doc(alias = "branch_upstream_name", alias = "git2")]
@@ -125,6 +126,73 @@ impl crate::Repository {
125126
.map(|res| res.map_err(Into::into))
126127
}
127128

129+
/// Given a local `tracking_branch` name, find the remote that maps to it along with the name of the branch on
130+
/// the side of the remote, also called upstream branch.
131+
///
132+
/// Return `Ok(None)` if there is no remote with fetch-refspecs that would match `tracking_branch` on the right-hand side,
133+
/// or `Err` if the matches were ambiguous.
134+
///
135+
/// ### Limitations
136+
///
137+
/// A single valid mapping is required as fine-grained matching isn't implemented yet. This means that
138+
pub fn upstream_branch_and_remote_for_tracking_branch(
139+
&self,
140+
tracking_branch: &FullNameRef,
141+
) -> Result<Option<(FullName, crate::Remote<'_>)>, upstream_branch_and_remote_name_for_tracking_branch::Error> {
142+
use upstream_branch_and_remote_name_for_tracking_branch::Error;
143+
if tracking_branch.category() != Some(gix_ref::Category::RemoteBranch) {
144+
return Err(Error::BranchCategory {
145+
full_name: tracking_branch.to_owned(),
146+
});
147+
}
148+
149+
let null = self.object_hash().null();
150+
let item_to_search = gix_refspec::match_group::Item {
151+
full_ref_name: tracking_branch.as_bstr(),
152+
target: &null,
153+
object: None,
154+
};
155+
let mut candidates = Vec::new();
156+
let mut ambiguous_remotes = Vec::new();
157+
for remote_name in self.remote_names() {
158+
let remote = self.find_remote(remote_name.as_ref())?;
159+
let match_group = gix_refspec::MatchGroup::from_fetch_specs(
160+
remote
161+
.refspecs(remote::Direction::Fetch)
162+
.iter()
163+
.map(|spec| spec.to_ref()),
164+
);
165+
let out = match_group.match_rhs(Some(item_to_search).into_iter());
166+
match &out.mappings[..] {
167+
[] => {}
168+
[one] => candidates.push((remote.clone(), one.lhs.clone().into_owned())),
169+
[..] => ambiguous_remotes.push(remote),
170+
}
171+
}
172+
173+
if candidates.len() == 1 {
174+
let (remote, candidate) = candidates.pop().expect("just checked for one entry");
175+
let upstream_branch = match candidate {
176+
gix_refspec::match_group::SourceRef::FullName(name) => gix_ref::FullName::try_from(name.into_owned())?,
177+
gix_refspec::match_group::SourceRef::ObjectId(_) => {
178+
unreachable!("Such a reverse mapping isn't ever produced")
179+
}
180+
};
181+
return Ok(Some((upstream_branch, remote)));
182+
}
183+
if ambiguous_remotes.len() + candidates.len() > 1 {
184+
return Err(Error::AmbiguousRemotes {
185+
remotes: ambiguous_remotes
186+
.into_iter()
187+
.map(|r| r.name)
188+
.chain(candidates.into_iter().map(|(r, _)| r.name))
189+
.flatten()
190+
.collect(),
191+
});
192+
}
193+
Ok(None)
194+
}
195+
128196
/// Returns the unvalidated name of the remote associated with the given `short_branch_name`,
129197
/// typically `main` instead of `refs/heads/main`.
130198
/// In some cases, the returned name will be an URL.

gix/src/repository/mod.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,6 @@ pub mod index_from_tree {
330330

331331
///
332332
pub mod branch_remote_ref_name {
333-
334333
/// The error returned by [Repository::branch_remote_ref_name()](crate::Repository::branch_remote_ref_name()).
335334
#[derive(Debug, thiserror::Error)]
336335
#[allow(missing_docs)]
@@ -346,7 +345,6 @@ pub mod branch_remote_ref_name {
346345

347346
///
348347
pub mod branch_remote_tracking_ref_name {
349-
350348
/// The error returned by [Repository::branch_remote_tracking_ref_name()](crate::Repository::branch_remote_tracking_ref_name()).
351349
#[derive(Debug, thiserror::Error)]
352350
#[allow(missing_docs)]
@@ -360,6 +358,25 @@ pub mod branch_remote_tracking_ref_name {
360358
}
361359
}
362360

361+
///
362+
pub mod upstream_branch_and_remote_name_for_tracking_branch {
363+
/// The error returned by [Repository::upstream_branch_and_remote_name_for_tracking_branch()](crate::Repository::upstream_branch_and_remote_for_tracking_branch()).
364+
#[derive(Debug, thiserror::Error)]
365+
#[allow(missing_docs)]
366+
pub enum Error {
367+
#[error("The input branch '{}' needs to be a remote tracking branch", full_name.as_bstr())]
368+
BranchCategory { full_name: gix_ref::FullName },
369+
#[error(transparent)]
370+
FindRemote(#[from] crate::remote::find::existing::Error),
371+
#[error("Found ambiguous remotes without 1:1 mapping or more than one match: {}", remotes.iter()
372+
.map(|r| r.as_bstr().to_string())
373+
.collect::<Vec<_>>().join(", "))]
374+
AmbiguousRemotes { remotes: Vec<crate::remote::Name<'static>> },
375+
#[error(transparent)]
376+
ValidateUpstreamBranch(#[from] gix_ref::name::Error),
377+
}
378+
}
379+
363380
///
364381
#[cfg(feature = "attributes")]
365382
pub mod pathspec_defaults_ignore_case {

gix/src/repository/worktree.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ impl crate::Repository {
1212
/// Note that these need additional processing to become usable, but provide a first glimpse a typical worktree information.
1313
pub fn worktrees(&self) -> std::io::Result<Vec<worktree::Proxy<'_>>> {
1414
let mut res = Vec::new();
15-
let iter = match std::fs::read_dir(dbg!(self.common_dir()).join("worktrees")) {
15+
let iter = match std::fs::read_dir(self.common_dir().join("worktrees")) {
1616
Ok(iter) => iter,
1717
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(res),
1818
Err(err) => return Err(err),
Binary file not shown.

gix/tests/fixtures/make_remote_config_repos.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ git clone fetch multiple-remotes
138138
git remote add with/two/slashes ../fetch && git fetch with/two/slashes
139139
git remote add with/two ../fetch && git fetch with/two
140140

141+
# add a specialised refspec mapping
142+
git config --add remote.with/two.fetch +refs/heads/special:refs/remotes/with/two/special
143+
# make sure the ref exists
144+
cp .git/refs/remotes/with/two/main .git/refs/remotes/with/two/special
145+
# show Git can checkout such an ambiguous refspec
146+
git checkout -b track-special with/two/special
141147
git checkout -b main --track origin/main
142148
git checkout -b other-main --track other/main
143149
)

gix/tests/gix/repository/config/remote.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ mod branch_remote {
107107
.as_bstr(),
108108
"refs/remotes/remote_repo/main"
109109
);
110+
let (upstream, remote_name) = repo
111+
.upstream_branch_and_remote_for_tracking_branch("refs/remotes/remote_repo/main".try_into()?)?
112+
.expect("mapping exists");
113+
assert_eq!(upstream.as_bstr(), "refs/heads/main");
114+
assert_eq!(
115+
remote_name.name().expect("non-anonymous remote").as_bstr(),
116+
"remote_repo"
117+
);
118+
119+
assert_eq!(
120+
repo.upstream_branch_and_remote_for_tracking_branch("refs/remotes/missing-remote/main".try_into()?)?,
121+
None,
122+
"It's OK to find nothing"
123+
);
124+
110125
for direction in [remote::Direction::Fetch, remote::Direction::Push] {
111126
assert_eq!(
112127
repo.branch_remote_name("main", direction)
@@ -145,6 +160,41 @@ mod branch_remote {
145160
Ok(())
146161
}
147162

163+
#[test]
164+
fn upstream_branch_and_remote_name_for_tracking_branch() -> crate::Result {
165+
let repo = repo("multiple-remotes")?;
166+
for expected_remote_name in ["other", "with/two"] {
167+
let (upstream, remote) = repo
168+
.upstream_branch_and_remote_for_tracking_branch(
169+
format!("refs/remotes/{expected_remote_name}/main")
170+
.as_str()
171+
.try_into()?,
172+
)?
173+
.expect("mapping exists");
174+
assert_eq!(remote.name().expect("named remote").as_bstr(), expected_remote_name);
175+
assert_eq!(upstream.as_bstr(), "refs/heads/main");
176+
}
177+
let err = repo
178+
.upstream_branch_and_remote_for_tracking_branch("refs/remotes/with/two/slashes/main".try_into()?)
179+
.unwrap_err();
180+
assert_eq!(
181+
err.to_string(),
182+
"Found ambiguous remotes without 1:1 mapping or more than one match: with/two, with/two/slashes",
183+
"we aren't very specific report an error just like Git does in case of multi-remote ambiguity"
184+
);
185+
186+
let (upstream, remote) = repo
187+
.upstream_branch_and_remote_for_tracking_branch("refs/remotes/with/two/special".try_into()?)?
188+
.expect("mapping exists");
189+
assert_eq!(remote.name().expect("non-anonymous remote").as_bstr(), "with/two");
190+
assert_eq!(
191+
upstream.as_bstr(),
192+
"refs/heads/special",
193+
"it finds a single mapping even though there are two refspecs"
194+
);
195+
Ok(())
196+
}
197+
148198
#[test]
149199
fn push_default() -> crate::Result {
150200
let repo = repo("fetch")?;

0 commit comments

Comments
 (0)