Skip to content

Commit

Permalink
feat: add Repository::branch_remote_tracking_ref_name().
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Dec 18, 2023
1 parent 404fde5 commit 4aa4b05
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 28 deletions.
97 changes: 73 additions & 24 deletions gix/src/repository/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ mod branch {
use crate::bstr::BStr;
use crate::config::cache::util::ApplyLeniencyDefault;
use crate::config::tree::{Branch, Push, Section};
use crate::repository::branch_remote_ref_name;
use crate::repository::{branch_remote_ref_name, branch_remote_tracking_ref_name};
use crate::{push, remote};

/// Query configuration related to branches.
Expand All @@ -225,6 +225,9 @@ mod branch {
/// ### Note
///
/// This 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.
#[doc(alias = "branch_upstream_name", alias = "git2")]
pub fn branch_remote_ref_name(
&self,
Expand Down Expand Up @@ -270,34 +273,50 @@ mod branch {
}
}
} else {
let search = gix_refspec::MatchGroup::from_push_specs(
remote
.push_specs
.iter()
.map(gix_refspec::RefSpec::to_ref)
.filter(|spec| spec.destination().is_some()),
);
let null_id = self.object_hash().null();
let out = search.match_remotes(
Some(gix_refspec::match_group::Item {
full_ref_name: name.as_bstr(),
target: &null_id,
object: None,
})
.into_iter(),
);
out.mappings.into_iter().next().and_then(|m| {
m.rhs.map(|name| {
FullName::try_from(name.into_owned())
.map(Cow::Owned)
.map_err(Into::into)
})
})
matching_remote(name, remote.push_specs.iter(), self.object_hash())
.map(|res| res.map_err(Into::into))
}
}
}
}

/// Return the validated name of the reference that tracks the corresponding reference of `name` on the remote for
/// `direction`. Note that a branch with that name might not actually exist.
///
/// * with `remote` being [remote::Direction::Fetch], we return the tracking branch that is on the destination
/// side of a `src:dest` refspec. For instance, with `name` being `main` and the default refspec
/// `refs/heads/*:refs/remotes/origin/*`, `refs/heads/main` would match and produce `refs/remotes/origin/main`.
/// * with `remote` being [remote::Direction::Push], we return the tracking branch that corresponds to the remote
/// branch that we would push to. For instance, with `name` being `main` and no setup at all, we
/// would push to `refs/heads/main` on the remote. And that one would be fetched matching the
/// `refs/heads/*:refs/remotes/origin/*` fetch refspec, hence `refs/remotes/origin/main` is returned.
/// Note that `push` refspecs can be used to map `main` to `other` (using a push refspec `refs/heads/main:refs/heads/other`),
/// which would then lead to `refs/remotes/origin/other` to be returned instead.
///
/// Note that if there is an ambiguity, that is if `name` maps to multiple tracking branches, the first matching mapping
/// is returned, according to the order in which the fetch or push refspecs occour in the configuration file.
#[doc(alias = "branch_upstream_name", alias = "git2")]
pub fn branch_remote_tracking_ref_name(
&self,
name: &FullNameRef,
direction: remote::Direction,
) -> Option<Result<Cow<'_, FullNameRef>, branch_remote_tracking_ref_name::Error>> {
let remote_ref = match self.branch_remote_ref_name(name, direction)? {
Ok(r) => r,
Err(err) => return Some(Err(err.into())),
};
let remote = match self.branch_remote(name.shorten(), direction)? {
Ok(r) => r,
Err(err) => return Some(Err(err.into())),
};

if remote.fetch_specs.is_empty() {
return None;
}
matching_remote(remote_ref.as_ref(), remote.fetch_specs.iter(), self.object_hash())
.map(|res| res.map_err(Into::into))
}

/// 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 Expand Up @@ -353,6 +372,36 @@ mod branch {
})
}
}

fn matching_remote<'a>(
lhs: &FullNameRef,
specs: impl IntoIterator<Item = &'a gix_refspec::RefSpec>,
object_hash: gix_hash::Kind,
) -> Option<Result<Cow<'static, FullNameRef>, gix_validate::reference::name::Error>> {
let search = gix_refspec::MatchGroup {
specs: specs
.into_iter()
.map(gix_refspec::RefSpec::to_ref)
.filter(|spec| spec.source().is_some() && spec.destination().is_some())
.collect(),
};
let null_id = object_hash.null();
let out = search.match_remotes(
Some(gix_refspec::match_group::Item {
full_ref_name: lhs.as_bstr(),
target: &null_id,
object: None,
})
.into_iter(),
);
out.mappings.into_iter().next().and_then(|m| {
m.rhs.map(|name| {
FullName::try_from(name.into_owned())
.map(Cow::Owned)
.map_err(Into::into)
})
})
}
}

impl crate::Repository {
Expand Down
16 changes: 16 additions & 0 deletions gix/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ 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)]
pub enum Error {
#[error("The name of the tracking reference was invalid")]
ValidateTrackingRef(#[from] gix_validate::reference::name::Error),
#[error("Could not get the remote reference to translate into the local tracking branch")]
RemoteRef(#[from] super::branch_remote_ref_name::Error),
#[error("Couldn't find remote to obtain fetch-specs for mapping to the tracking reference")]
FindRemote(#[from] crate::remote::find::existing::Error),
}
}

/// A type to represent an index which either was loaded from disk as it was persisted there, or created on the fly in memory.
#[cfg(feature = "index")]
pub enum IndexPersistedOrInMemory {
Expand Down
Binary file not shown.
47 changes: 47 additions & 0 deletions gix/tests/fixtures/make_remote_config_repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ set -eu -o pipefail
git checkout -b main

git commit --allow-empty -q -m c1
git branch broken

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

git checkout broken
git branch --set-upstream-to remote_repo/broken

git config branch.broken.merge not_a_valid_merge_ref
git config push.default simple
)
Expand Down Expand Up @@ -84,3 +88,46 @@ EOF
EOF
)

(mkdir push-remote && cd push-remote
git init -q

git checkout -b main
git commit --allow-empty -q -m c1

cat<<EOF >.git/config
[remote "origin"]
url = .
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "push-origin"]
url = .
fetch = +refs/heads/*:refs/remotes/push-remote/*
[branch "main"]
remote = "origin"
pushRemote = push-origin
merge = refs/heads/other
EOF
)


(mkdir push-remote-default && cd push-remote-default
git init -q

git checkout -b main
git commit --allow-empty -q -m c1

cat<<EOF >.git/config
[remote "push-origin"]
url = .
fetch = +refs/heads/*:refs/remotes/push-remote/*
[branch "main"]
remote = "origin"
merge = refs/heads/other
[remote]
pushDefault = push-origin
EOF
)

0 comments on commit 4aa4b05

Please sign in to comment.