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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
#![allow(clippy::result_large_err)]
use crate::{Remote, bstr::BStr, config, remote, remote::find};
impl crate::Repository {
/// Create a new remote available at the given `url`.
///
/// It's configured to fetch included tags by default, similar to git.
/// See [`with_fetch_tags(…)`][Remote::with_fetch_tags()] for a way to change it.
///
/// URL rewrite rules are applied immediately. A malformed `pushInsteadOf` result is ignored when this fetch URL is merely
/// used as the push fallback, keeping the original URL usable; malformed `insteadOf` results are reported as errors.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// # mod doctest { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/doctest.rs")); }
/// # let repo = doctest::open_repo(doctest::basic_repo_dir()?)?;
/// let remote = repo.remote_at("https://github.com/byron/gitoxide")?;
///
/// assert_eq!(remote.name(), None);
/// assert_eq!(
/// remote
/// .url(gix::remote::Direction::Fetch)
/// .expect("fetch URL")
/// .to_bstring(),
/// "https://github.com/byron/gitoxide"
/// );
/// # Ok(()) }
/// ```
pub fn remote_at<Url, E>(&self, url: Url) -> Result<Remote<'_>, remote::init::Error>
where
Url: TryInto<gix_url::Url, Error = E>,
gix_url::parse::Error: From<E>,
{
Remote::from_fetch_url(url, true, self)
}
/// Create a new remote available at the given `url` similarly to [`remote_at()`][crate::Repository::remote_at()],
/// but don't rewrite the url according to rewrite rules.
/// This eliminates a failure mode in case the rewritten URL is faulty, allowing to selectively [apply rewrite
/// rules][Remote::rewrite_urls()] later and do so non-destructively.
pub fn remote_at_without_url_rewrite<Url, E>(&self, url: Url) -> Result<Remote<'_>, remote::init::Error>
where
Url: TryInto<gix_url::Url, Error = E>,
gix_url::parse::Error: From<E>,
{
Remote::from_fetch_url(url, false, self)
}
/// Find the configured remote with the given `name_or_url` or report an error,
/// similar to [`try_find_remote(…)`][Self::try_find_remote()].
///
/// Note that we will obtain remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()].
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// # mod doctest { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/doctest.rs")); }
/// # let repo = doctest::open_repo(doctest::remote_repo_dir("clone")?)?;
/// let remote = repo.find_remote("origin")?;
///
/// assert_eq!(remote.name().expect("configured").as_bstr(), "origin");
/// assert_eq!(remote.refspecs(gix::remote::Direction::Fetch).len(), 1);
/// # Ok(()) }
/// ```
pub fn find_remote(&self, name_or_url: impl gix_utils::AsBStr) -> Result<Remote<'_>, find::existing::Error> {
let name_or_url = name_or_url.as_bstr();
Ok(self
.try_find_remote(name_or_url)
.ok_or_else(|| find::existing::Error::NotFound {
name: name_or_url.into(),
})??)
}
/// Find the default remote as configured, or `None` if no such configuration could be found.
///
/// See [`remote_default_name()`](Self::remote_default_name()) for more information on the `direction` parameter.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// # mod doctest { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/doctest.rs")); }
/// # let repo = doctest::open_repo(doctest::remote_repo_dir("clone")?)?;
/// let remote = repo
/// .find_default_remote(gix::remote::Direction::Fetch)
/// .expect("configured")?;
///
/// assert_eq!(remote.name().expect("named").as_bstr(), "origin");
/// # Ok(()) }
/// ```
pub fn find_default_remote(
&self,
direction: remote::Direction,
) -> Option<Result<Remote<'_>, find::existing::Error>> {
self.remote_default_name(direction).map(|name| self.find_remote(name))
}
/// Find the configured remote with the given `name_or_url` or return `None` if it doesn't exist,
/// for the purpose of fetching or pushing data.
///
/// There are various error kinds related to partial information or incorrectly formatted URLs or ref-specs.
/// Also note that the created `Remote` may have neither fetch nor push ref-specs set at all.
/// Unlike Git, a configured remote with a symbolic name and no effective fetch URL remains without a fetch URL. If the
/// requested remote name is itself a URL, it is used as the fetch URL instead. This applies equally to remotes whose URL
/// list was cleared by an empty value and remotes configured only with a push URL.
///
/// URL rewrite rules are applied while constructing the remote. A malformed `pushInsteadOf` result is ignored when a fetch
/// URL is used as the push fallback, keeping the original URL usable. Malformed `insteadOf` results for fetch URLs or explicit
/// push URLs are reported as errors. Use [`try_find_remote_without_url_rewrite()`](Self::try_find_remote_without_url_rewrite)
/// with [`Remote::rewrite_urls()`] to defer rewriting and handle such errors after constructing the remote.
///
/// Note that ref-specs are de-duplicated right away which may change their order. This doesn't affect matching in any way
/// as negations/excludes are applied after includes.
///
/// We will only include information if we deem it [trustworthy][crate::open::Options::filter_config_section()].
pub fn try_find_remote<'a>(&self, name_or_url: impl Into<&'a BStr>) -> Option<Result<Remote<'_>, find::Error>> {
self.try_find_remote_inner(name_or_url.into(), true)
}
/// This method emulate what `git fetch <remote>` does in order to obtain a remote to fetch from.
///
/// As such, with `name_or_url` being `Some`, it will:
///
/// * use `name_or_url` verbatim if it is a URL, creating a new remote in memory as needed.
/// * find the named remote if `name_or_url` is a remote name
///
/// If `name_or_url` is `None`:
///
/// * use the current `HEAD` branch to find a configured remote
/// * fall back to either a generally configured remote or the only configured remote.
///
/// Fail if no remote could be found despite all of the above.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// # mod doctest { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/doctest.rs")); }
/// # let repo = doctest::open_repo(doctest::remote_repo_dir("clone")?)?;
/// let remote_names: Vec<_> = repo.remote_names().into_iter().map(|name| name.to_string()).collect();
/// assert_eq!(remote_names, vec!["myself".to_owned(), "origin".to_owned()]);
///
/// let remote = repo.find_fetch_remote(None)?;
/// assert_eq!(remote.name().expect("configured").as_bstr(), "origin");
/// # Ok(()) }
/// ```
pub fn find_fetch_remote(&self, name_or_url: Option<&BStr>) -> Result<Remote<'_>, find::for_fetch::Error> {
Ok(match name_or_url {
Some(name) => match self.try_find_remote(name).and_then(Result::ok) {
Some(remote) => remote,
None => self.remote_at(gix_url::parse(name)?)?,
},
None => self
.head()?
.into_remote(remote::Direction::Fetch)
.transpose()?
.map(Ok)
.or_else(|| self.find_default_remote(remote::Direction::Fetch))
.ok_or(find::for_fetch::Error::ExactlyOneRemoteNotAvailable)??,
})
}
/// Similar to [`try_find_remote()`][Self::try_find_remote()], but removes a failure mode if rewritten URLs turn out to be invalid
/// as it skips rewriting them.
/// Use this in conjunction with [`Remote::rewrite_urls()`] to non-destructively apply the rules and keep the failed urls unchanged.
pub fn try_find_remote_without_url_rewrite<'a>(
&self,
name_or_url: impl Into<&'a BStr>,
) -> Option<Result<Remote<'_>, find::Error>> {
self.try_find_remote_inner(name_or_url.into(), false)
}
fn try_find_remote_inner<'a>(
&self,
name_or_url: impl Into<&'a BStr>,
rewrite_urls: bool,
) -> Option<Result<Remote<'_>, find::Error>> {
fn config_spec<T: config::tree::keys::Validate>(
specs: Vec<crate::bstr::BString>,
name_or_url: &BStr,
key: &'static config::tree::keys::Any<T>,
op: gix_refspec::parse::Operation,
) -> Result<Vec<gix_refspec::RefSpec>, find::Error> {
let kind = key.name;
specs
.into_iter()
.map(|spec| {
key.try_into_refspec(spec, op).map_err(|err| find::Error::RefSpec {
remote_name: name_or_url.into(),
kind,
source: err,
})
})
.collect::<Result<Vec<_>, _>>()
.map(|mut specs| {
specs.sort();
specs.dedup();
specs
})
}
let mut filter = self.filter_config_section();
let name_or_url = name_or_url.into();
// Git considers any matching remote section configured, even if it only contains unrelated keys like `prune`.
let remote_is_configured = self
.config
.resolved
.sections_by_name("remote")
.is_some_and(|mut sections| {
sections
.any(|section| section.header().subsection_name() == Some(name_or_url) && filter(section.meta()))
});
let mut config_urls = |key: &'static config::tree::keys::Url, kind: &'static str| {
self.config
.resolved
.strings_filter(&format!("remote.{}.{}", name_or_url, key.name), &mut filter)
.map(|urls| {
let mut effective_urls = Vec::new();
for url in urls {
if url.is_empty() {
// empty urls are a sentinel, indicating all prior urls should be cleared.
// This makes overriding global remote configuration possible.
effective_urls.clear();
} else {
effective_urls.push(url);
}
}
effective_urls
.into_iter()
.map(|url| {
key.try_into_url(url).map_err(|err| find::Error::Url {
kind,
remote_name: name_or_url.into(),
source: err,
})
})
.collect()
})
};
let urls = config_urls(&config::tree::Remote::URL, "fetch");
let push_urls = config_urls(&config::tree::Remote::PUSH_URL, "push");
let config = &self.config.resolved;
let fetch_specs = config
.strings_filter(&format!("remote.{}.{}", name_or_url, "fetch"), &mut filter)
.map(|specs| {
config_spec(
specs,
name_or_url,
&config::tree::Remote::FETCH,
gix_refspec::parse::Operation::Fetch,
)
});
let push_specs = config
.strings_filter(&format!("remote.{}.{}", name_or_url, "push"), &mut filter)
.map(|specs| {
config_spec(
specs,
name_or_url,
&config::tree::Remote::PUSH,
gix_refspec::parse::Operation::Push,
)
});
let fetch_tags = config
.string_filter(&format!("remote.{}.{}", name_or_url, "tagOpt"), &mut filter)
.map(|value| {
config::tree::Remote::TAG_OPT
.try_into_tag_opt(value)
.map_err(Into::into)
});
let fetch_tags = match fetch_tags {
Some(Ok(v)) => v,
Some(Err(err)) => return Some(Err(err)),
None => Default::default(),
};
match (urls, fetch_specs, push_urls, push_specs) {
(None, None, None, None) if !remote_is_configured => None,
(urls, fetch_specs, push_urls, push_specs) => {
let mut urls = match urls {
Some(Ok(v)) => v,
Some(Err(err)) => return Some(Err(err)),
None => Vec::new(),
};
let push_urls = match push_urls {
Some(Ok(v)) => v,
Some(Err(err)) => return Some(Err(err)),
None => Vec::new(),
};
let fetch_specs = match fetch_specs {
Some(Ok(v)) => v,
Some(Err(err)) => return Some(Err(err)),
None => Vec::new(),
};
let push_specs = match push_specs {
Some(Ok(v)) => v,
Some(Err(err)) => return Some(Err(err)),
None => Vec::new(),
};
if urls.is_empty() {
let name_is_url = matches!(
remote::Name::try_from(std::borrow::Cow::Borrowed(name_or_url)),
Ok(remote::Name::Url(_))
) || gix_path::is_absolute(gix_path::from_bstr(name_or_url));
match config::tree::Remote::URL.try_into_url(std::borrow::Cow::Borrowed(name_or_url)) {
Ok(url) if name_is_url || url.scheme != gix_url::Scheme::File => urls.push(url),
Ok(_) => {}
Err(source) if name_is_url => {
return Some(Err(find::Error::Url {
kind: "fetch",
remote_name: name_or_url.into(),
source,
}));
}
Err(_) => {}
}
}
Some(
Remote::from_preparsed_config(
Some(name_or_url.to_owned()),
urls,
push_urls,
fetch_specs,
push_specs,
rewrite_urls,
fetch_tags,
self,
)
.map_err(Into::into),
)
}
}
}
}