1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
use gix_error::{ResultExt, bail};
use gix_ref::{FullName, FullNameRef};
use gix_refspec::{MatchGroup, RefSpec, match_group};
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
use crate::types::RemoteDetached;
use crate::{
Remote, Result,
bstr::{BStr, BString, ByteVec},
remote,
};
/// Access
impl<'repo> Remote<'repo> {
/// Return the name of this remote or `None` if it wasn't persisted to disk yet.
pub fn name(&self) -> Option<&remote::Name<'static>> {
self.name.as_ref()
}
/// Return our repository reference.
pub fn repo(&self) -> &'repo crate::Repository {
self.repo
}
/// Return the set of ref-specs used for `direction`, which may be empty, in order of occurrence in the configuration.
pub fn refspecs(&self, direction: remote::Direction) -> &[RefSpec] {
match direction {
remote::Direction::Fetch => &self.fetch_specs,
remote::Direction::Push => &self.push_specs,
}
}
/// Return how we handle tags when fetching the remote.
pub fn fetch_tags(&self) -> remote::fetch::Tags {
self.fetch_tags
}
/// Return the name of the default branch on the remote, like `refs/heads/main`, as recorded locally,
/// or `None` if it isn't known.
///
/// The record is the symbolic reference `refs/remotes/<name>/HEAD`. Cloning with `git` or `gix`, and `git remote set-head`,
/// point it to the remote-tracking branch of the default branch, like `refs/remotes/origin/main`,
/// and since Git 2.48, `git fetch` creates it if it's missing. The fetch refspecs of this remote then
/// map this remote-tracking branch back to the branch on the remote, and it's an error if they map
/// more than one remote reference to it.
///
/// `None` is returned if this remote has no name, if `refs/remotes/<name>/HEAD` doesn't exist or isn't symbolic,
/// or if no fetch refspec maps a remote reference to its target. The source must identify a branch in `refs/heads/`
/// (also accepting exact names like `heads/main`); sources like `HEAD`, object IDs, tags, or unqualified names like
/// `main` return `None` as their branch cannot be determined offline.
///
/// As the remote isn't contacted, it may have changed its default branch since, and the returned branch
/// may not exist anymore. `git2::Remote::default_branch()`, on the other hand, asks the connected remote
/// for its current `HEAD`.
#[doc(alias = "git2")]
pub fn default_branch(&self) -> Result<Option<FullName>> {
let Some(name) = self.name() else {
return Ok(None);
};
let mut head_name = BString::from("refs/remotes/");
head_name.push_str(name.as_bstr());
head_name.push_str("/HEAD");
// Names that can't be part of a reference name can't have remote-tracking branches either.
let Ok(head_name) = FullName::try_from(head_name) else {
return Ok(None);
};
let Some(head) = self.repo.try_find_reference(head_name.as_bstr())? else {
return Ok(None);
};
let target = head.target();
let Some(tracking_branch) = target.try_name() else {
return Ok(None);
};
let outcome = self.reverse_map_tracking_ref(tracking_branch);
let num_mappings = outcome.mappings.len();
let mut mappings = outcome.mappings.into_iter();
let Some(mapping) = mappings.next() else {
return Ok(None);
};
if let Some(other_mapping) = mappings.next() {
bail!(gix_error::validation(format!(
"Found {num_mappings} mappings to '{}', including '{}' and '{}', so the default branch is ambiguous",
tracking_branch.as_bstr(),
mapping.lhs,
other_mapping.lhs
)));
}
let match_group::SourceRef::FullName(remote_branch) = mapping.lhs else {
return Ok(None);
};
let remote_branch = if remote_branch.starts_with(b"refs/heads/") {
remote_branch.into_owned()
} else if remote_branch.starts_with(b"heads/")
&& outcome.group.specs.iter().any(|spec| {
spec.source() == Some(remote_branch.as_ref()) && spec.destination() == Some(tracking_branch.as_bstr())
})
{
// Git expands exact sources, not globs. Check all specs as identical mappings are deduplicated.
let mut full_name = BString::from("refs/");
full_name.push_str(remote_branch.as_ref());
full_name
} else {
return Ok(None);
};
FullName::try_from(remote_branch)
.map(Some)
.or_raise(|| gix_error::validation("The remote reference that the remote HEAD maps to has an invalid name"))
}
/// Reverse-map a local tracking reference through this remote's fetch refspecs by name only.
pub(crate) fn reverse_map_tracking_ref<'spec, 'name>(
&'spec self,
tracking_ref: &'name FullNameRef,
) -> match_group::match_rhs::Outcome<'spec, 'name> {
MatchGroup::from_fetch_specs(self.fetch_specs.iter().map(RefSpec::to_ref)).match_rhs(std::iter::once(
match_group::Item {
full_ref_name: tracking_ref.as_bstr(),
target: self.repo.object_hash().null_ref(),
object: None,
},
))
}
/// Return the first url used for the given `direction` with rewrites from `url.<base>.insteadOf|pushInsteadOf`, unless the instance
/// was created with one of the `_without_url_rewrite()` methods.
/// See [`urls()`](Self::urls()) for how rewrite rules differ between fetch URLs, explicit push URLs, and push fallbacks.
/// For pushing, this is the first `remote.<name>.pushUrl` or the first `remote.<name>.url` used for fetching, and for
/// fetching it's the first `remote.<name>.url`. Unlike `git remote get-url`, a missing fetch URL doesn't fall back to a
/// symbolic remote name, but a URL-shaped remote name may itself be used as the fallback URL.
/// Note that it's possible to only have the push url set, in which case there will be no way to fetch from the remote as
/// the push-url isn't used for that.
pub fn url(&self, direction: remote::Direction) -> Option<&gix_url::Url> {
self.urls(direction).next()
}
/// Return all urls used for the given `direction` with rewrites from `url.<base>.insteadOf|pushInsteadOf`, unless the
/// instance was created with one of the `_without_url_rewrite()` methods.
///
/// Fetch URLs are rewritten with `url.<base>.insteadOf`. Explicit `remote.<name>.pushUrl` values are also rewritten with
/// `insteadOf`, and `pushInsteadOf` is ignored for them. If no explicit push URL is configured, the fetch URLs are used
/// as push fallbacks: matching `pushInsteadOf` rules take precedence, with `insteadOf` used when none match.
///
/// Values are returned in configuration order.
pub fn urls(&self, direction: remote::Direction) -> impl Iterator<Item = &gix_url::Url> + '_ {
let (urls, aliases) = self.urls_and_aliases(direction);
debug_assert_eq!(
urls.len(),
aliases.len(),
"each URL should have a corresponding rewrite slot"
);
urls.iter()
.zip(aliases)
.map(|(url, alias)| alias.as_ref().unwrap_or(url))
}
fn urls_and_aliases(&self, direction: remote::Direction) -> (&[gix_url::Url], &[Option<gix_url::Url>]) {
match direction {
remote::Direction::Fetch => (&self.urls, &self.url_aliases),
remote::Direction::Push if self.push_urls.is_empty() => (&self.urls, &self.url_push_aliases),
remote::Direction::Push => (&self.push_urls, &self.push_url_aliases),
}
}
/// Return a clone of this remote without its repository reference.
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
pub(crate) fn detached(&self) -> RemoteDetached {
self.clone().into()
}
}
/// Access
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
impl RemoteDetached {
/// Return the name of this remote or `None` if it wasn't persisted to disk yet.
pub(crate) fn name(&self) -> Option<&remote::Name<'static>> {
self.name.as_ref()
}
/// Return the set of ref-specs used for fetching, which may be empty, in order of occurrence in the configuration.
pub(crate) fn fetch_refspecs(&self) -> &[RefSpec] {
&self.fetch_specs
}
}
/// Modification
impl Remote<'_> {
/// Re-read `url.<base>.insteadOf|pushInsteadOf` and recompute the effective URLs returned by [`url()`](Self::url()) and
/// [`urls()`](Self::urls()). This may be called repeatedly to refresh rewrite rules after configuration changes.
///
/// Every URL is attempted non-destructively: successful rewrites remain effective if another rewritten URL is malformed,
/// while a failed entry keeps using its original URL. The first error is returned in fetch, push-fallback, explicit-push
/// order. See [`urls()`](Self::urls()) for which rules apply to each category.
pub fn rewrite_urls(&mut self) -> Result<&mut Self> {
let (url_aliases, url_err) =
remote::init::rewrite_url_aliases_non_destructive(&self.repo.config, &self.urls, remote::Direction::Fetch);
self.url_aliases = url_aliases;
let url_push_err = if self.push_urls.is_empty() {
let (url_push_aliases, err) = remote::init::rewrite_url_aliases_with_fallback_non_destructive(
&self.repo.config,
&self.urls,
remote::Direction::Push,
remote::Direction::Fetch,
);
self.url_push_aliases = url_push_aliases;
err
} else {
self.url_push_aliases = vec![None; self.urls.len()];
None
};
let (push_url_aliases, push_url_err) = remote::init::rewrite_url_aliases_non_destructive_with_error_kind(
&self.repo.config,
&self.push_urls,
remote::Direction::Fetch,
remote::Direction::Push,
);
self.push_url_aliases = push_url_aliases;
url_err
.or(url_push_err)
.or(push_url_err)
.map(Err::<&mut Self, _>)
.transpose()?;
Ok(self)
}
/// Replace all currently set refspecs, typically from configuration, with the given `specs` for `direction`,
/// or `None` if one of the input specs could not be parsed.
pub fn replace_refspecs<Spec>(
&mut self,
specs: impl IntoIterator<Item = Spec>,
direction: remote::Direction,
) -> Result
where
Spec: AsRef<BStr>,
{
use remote::Direction::*;
let specs: Vec<_> = specs
.into_iter()
.map(|spec| {
gix_refspec::parse(
spec.as_ref(),
match direction {
Push => gix_refspec::parse::Operation::Push,
Fetch => gix_refspec::parse::Operation::Fetch,
},
)
.map(|url| url.to_owned())
})
.collect::<std::result::Result<_, _>>()?;
let dst = match direction {
Push => &mut self.push_specs,
Fetch => &mut self.fetch_specs,
};
*dst = specs;
Ok(())
}
}
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
impl From<Remote<'_>> for RemoteDetached {
fn from(
Remote {
name,
urls,
url_aliases,
url_push_aliases: _,
fetch_specs,
fetch_tags,
push_urls: _,
push_url_aliases: _,
push_specs: _,
repo: _,
}: Remote<'_>,
) -> Self {
RemoteDetached {
name,
urls,
url_aliases,
fetch_specs,
fetch_tags,
}
}
}