Skip to content

Commit 4aa4b05

Browse files
committed
feat: add Repository::branch_remote_tracking_ref_name().
1 parent 404fde5 commit 4aa4b05

File tree

5 files changed

+279
-28
lines changed

5 files changed

+279
-28
lines changed

gix/src/repository/config/mod.rs

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ mod branch {
200200
use crate::bstr::BStr;
201201
use crate::config::cache::util::ApplyLeniencyDefault;
202202
use crate::config::tree::{Branch, Push, Section};
203-
use crate::repository::branch_remote_ref_name;
203+
use crate::repository::{branch_remote_ref_name, branch_remote_tracking_ref_name};
204204
use crate::{push, remote};
205205

206206
/// Query configuration related to branches.
@@ -225,6 +225,9 @@ mod branch {
225225
/// ### Note
226226
///
227227
/// This name refers to what Git calls upstream branch (as opposed to upstream *tracking* branch).
228+
/// The value is also fast to retrieve compared to its tracking branch.
229+
/// Also note that a [remote::Direction] isn't used here as Git only supports (and requires) configuring
230+
/// the remote to fetch from, not the one to push to.
228231
#[doc(alias = "branch_upstream_name", alias = "git2")]
229232
pub fn branch_remote_ref_name(
230233
&self,
@@ -270,34 +273,50 @@ mod branch {
270273
}
271274
}
272275
} else {
273-
let search = gix_refspec::MatchGroup::from_push_specs(
274-
remote
275-
.push_specs
276-
.iter()
277-
.map(gix_refspec::RefSpec::to_ref)
278-
.filter(|spec| spec.destination().is_some()),
279-
);
280-
let null_id = self.object_hash().null();
281-
let out = search.match_remotes(
282-
Some(gix_refspec::match_group::Item {
283-
full_ref_name: name.as_bstr(),
284-
target: &null_id,
285-
object: None,
286-
})
287-
.into_iter(),
288-
);
289-
out.mappings.into_iter().next().and_then(|m| {
290-
m.rhs.map(|name| {
291-
FullName::try_from(name.into_owned())
292-
.map(Cow::Owned)
293-
.map_err(Into::into)
294-
})
295-
})
276+
matching_remote(name, remote.push_specs.iter(), self.object_hash())
277+
.map(|res| res.map_err(Into::into))
296278
}
297279
}
298280
}
299281
}
300282

283+
/// Return the validated name of the reference that tracks the corresponding reference of `name` on the remote for
284+
/// `direction`. Note that a branch with that name might not actually exist.
285+
///
286+
/// * with `remote` being [remote::Direction::Fetch], we return the tracking branch that is on the destination
287+
/// side of a `src:dest` refspec. For instance, with `name` being `main` and the default refspec
288+
/// `refs/heads/*:refs/remotes/origin/*`, `refs/heads/main` would match and produce `refs/remotes/origin/main`.
289+
/// * with `remote` being [remote::Direction::Push], we return the tracking branch that corresponds to the remote
290+
/// branch that we would push to. For instance, with `name` being `main` and no setup at all, we
291+
/// would push to `refs/heads/main` on the remote. And that one would be fetched matching the
292+
/// `refs/heads/*:refs/remotes/origin/*` fetch refspec, hence `refs/remotes/origin/main` is returned.
293+
/// Note that `push` refspecs can be used to map `main` to `other` (using a push refspec `refs/heads/main:refs/heads/other`),
294+
/// which would then lead to `refs/remotes/origin/other` to be returned instead.
295+
///
296+
/// Note that if there is an ambiguity, that is if `name` maps to multiple tracking branches, the first matching mapping
297+
/// is returned, according to the order in which the fetch or push refspecs occour in the configuration file.
298+
#[doc(alias = "branch_upstream_name", alias = "git2")]
299+
pub fn branch_remote_tracking_ref_name(
300+
&self,
301+
name: &FullNameRef,
302+
direction: remote::Direction,
303+
) -> Option<Result<Cow<'_, FullNameRef>, branch_remote_tracking_ref_name::Error>> {
304+
let remote_ref = match self.branch_remote_ref_name(name, direction)? {
305+
Ok(r) => r,
306+
Err(err) => return Some(Err(err.into())),
307+
};
308+
let remote = match self.branch_remote(name.shorten(), direction)? {
309+
Ok(r) => r,
310+
Err(err) => return Some(Err(err.into())),
311+
};
312+
313+
if remote.fetch_specs.is_empty() {
314+
return None;
315+
}
316+
matching_remote(remote_ref.as_ref(), remote.fetch_specs.iter(), self.object_hash())
317+
.map(|res| res.map_err(Into::into))
318+
}
319+
301320
/// Returns the unvalidated name of the remote associated with the given `short_branch_name`,
302321
/// typically `main` instead of `refs/heads/main`.
303322
/// In some cases, the returned name will be an URL.
@@ -353,6 +372,36 @@ mod branch {
353372
})
354373
}
355374
}
375+
376+
fn matching_remote<'a>(
377+
lhs: &FullNameRef,
378+
specs: impl IntoIterator<Item = &'a gix_refspec::RefSpec>,
379+
object_hash: gix_hash::Kind,
380+
) -> Option<Result<Cow<'static, FullNameRef>, gix_validate::reference::name::Error>> {
381+
let search = gix_refspec::MatchGroup {
382+
specs: specs
383+
.into_iter()
384+
.map(gix_refspec::RefSpec::to_ref)
385+
.filter(|spec| spec.source().is_some() && spec.destination().is_some())
386+
.collect(),
387+
};
388+
let null_id = object_hash.null();
389+
let out = search.match_remotes(
390+
Some(gix_refspec::match_group::Item {
391+
full_ref_name: lhs.as_bstr(),
392+
target: &null_id,
393+
object: None,
394+
})
395+
.into_iter(),
396+
);
397+
out.mappings.into_iter().next().and_then(|m| {
398+
m.rhs.map(|name| {
399+
FullName::try_from(name.into_owned())
400+
.map(Cow::Owned)
401+
.map_err(Into::into)
402+
})
403+
})
404+
}
356405
}
357406

358407
impl crate::Repository {

gix/src/repository/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ pub mod branch_remote_ref_name {
8585
}
8686
}
8787

88+
///
89+
pub mod branch_remote_tracking_ref_name {
90+
91+
/// The error returned by [Repository::branch_remote_tracking_ref_name()](crate::Repository::branch_remote_tracking_ref_name()).
92+
#[derive(Debug, thiserror::Error)]
93+
#[allow(missing_docs)]
94+
pub enum Error {
95+
#[error("The name of the tracking reference was invalid")]
96+
ValidateTrackingRef(#[from] gix_validate::reference::name::Error),
97+
#[error("Could not get the remote reference to translate into the local tracking branch")]
98+
RemoteRef(#[from] super::branch_remote_ref_name::Error),
99+
#[error("Couldn't find remote to obtain fetch-specs for mapping to the tracking reference")]
100+
FindRemote(#[from] crate::remote::find::existing::Error),
101+
}
102+
}
103+
88104
/// A type to represent an index which either was loaded from disk as it was persisted there, or created on the fly in memory.
89105
#[cfg(feature = "index")]
90106
pub enum IndexPersistedOrInMemory {
Binary file not shown.

gix/tests/fixtures/make_remote_config_repos.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ set -eu -o pipefail
77
git checkout -b main
88

99
git commit --allow-empty -q -m c1
10+
git branch broken
1011

1112
git remote add --fetch remote_repo .
1213
git branch --set-upstream-to remote_repo/main
1314

15+
git checkout broken
16+
git branch --set-upstream-to remote_repo/broken
17+
1418
git config branch.broken.merge not_a_valid_merge_ref
1519
git config push.default simple
1620
)
@@ -84,3 +88,46 @@ EOF
8488
EOF
8589
)
8690

91+
(mkdir push-remote && cd push-remote
92+
git init -q
93+
94+
git checkout -b main
95+
git commit --allow-empty -q -m c1
96+
97+
cat<<EOF >.git/config
98+
[remote "origin"]
99+
url = .
100+
fetch = +refs/heads/*:refs/remotes/origin/*
101+
102+
[remote "push-origin"]
103+
url = .
104+
fetch = +refs/heads/*:refs/remotes/push-remote/*
105+
106+
[branch "main"]
107+
remote = "origin"
108+
pushRemote = push-origin
109+
merge = refs/heads/other
110+
EOF
111+
)
112+
113+
114+
(mkdir push-remote-default && cd push-remote-default
115+
git init -q
116+
117+
git checkout -b main
118+
git commit --allow-empty -q -m c1
119+
120+
cat<<EOF >.git/config
121+
122+
[remote "push-origin"]
123+
url = .
124+
fetch = +refs/heads/*:refs/remotes/push-remote/*
125+
126+
[branch "main"]
127+
remote = "origin"
128+
merge = refs/heads/other
129+
130+
[remote]
131+
pushDefault = push-origin
132+
EOF
133+
)

0 commit comments

Comments
 (0)