gnostr_asyncgit/sync/remotes/
mod.rs1mod callbacks;
4pub(crate) mod push;
5pub(crate) mod tags;
6
7pub use callbacks::Callbacks;
8use crossbeam_channel::Sender;
9use git2::{BranchType, FetchOptions, ProxyOptions, Repository};
10use scopetime::scope_time;
11pub use tags::tags_missing_remote;
12use utils::bytes2string;
13
14use super::RepoPath;
15use crate::{
16 ProgressPercent,
17 error::{Error, Result},
18 sync::{
19 cred::BasicAuthCredential,
20 remotes::push::ProgressNotification, repository::repo, utils,
21 },
22};
23
24pub const DEFAULT_REMOTE_NAME: &str = "origin";
26
27pub fn proxy_auto<'a>() -> ProxyOptions<'a> {
29 let mut proxy = ProxyOptions::new();
30 proxy.auto();
31 proxy
32}
33
34pub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {
36 scope_time!("get_remotes");
37
38 let repo = repo(repo_path)?;
39 let remotes = repo.remotes()?;
40 let remotes: Vec<String> =
41 remotes.iter().flatten().map(String::from).collect();
42
43 Ok(remotes)
44}
45
46pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
49 let repo = repo(repo_path)?;
50 get_default_remote_in_repo(&repo)
51}
52
53fn get_current_branch(
57 repo: &Repository,
58) -> Result<Option<git2::Branch>> {
59 for b in repo.branches(None)? {
60 let branch = b?.0;
61 if branch.is_head() {
62 return Ok(Some(branch));
63 }
64 }
65 Ok(None)
66}
67
68pub fn get_default_remote_for_fetch(
84 repo_path: &RepoPath,
85) -> Result<String> {
86 let repo = repo(repo_path)?;
87 get_default_remote_for_fetch_in_repo(&repo)
88}
89
90pub(crate) fn get_default_remote_for_fetch_in_repo(
93 repo: &Repository,
94) -> Result<String> {
95 scope_time!("get_default_remote_for_fetch_in_repo");
96
97 let config = repo.config()?;
98
99 let branch = get_current_branch(repo)?;
100
101 if let Some(branch) = branch {
102 let remote_name = bytes2string(branch.name_bytes()?)?;
103
104 let entry_name = format!("branch.{}.remote", &remote_name);
105
106 if let Ok(entry) = config.get_entry(&entry_name) {
107 return bytes2string(entry.value_bytes());
108 }
109 }
110
111 get_default_remote_in_repo(repo)
112}
113
114pub fn get_default_remote_for_push(
141 repo_path: &RepoPath,
142) -> Result<String> {
143 let repo = repo(repo_path)?;
144 get_default_remote_for_push_in_repo(&repo)
145}
146
147pub(crate) fn get_default_remote_for_push_in_repo(
150 repo: &Repository,
151) -> Result<String> {
152 scope_time!("get_default_remote_for_push_in_repo");
153
154 let config = repo.config()?;
155
156 let branch = get_current_branch(repo)?;
157
158 if let Some(branch) = branch {
159 let remote_name = bytes2string(branch.name_bytes()?)?;
160
161 let entry_name =
162 format!("branch.{}.pushRemote", &remote_name);
163
164 if let Ok(entry) = config.get_entry(&entry_name) {
165 return bytes2string(entry.value_bytes());
166 }
167
168 if let Ok(entry) = config.get_entry("remote.pushDefault") {
169 return bytes2string(entry.value_bytes());
170 }
171
172 let entry_name = format!("branch.{}.remote", &remote_name);
173
174 if let Ok(entry) = config.get_entry(&entry_name) {
175 return bytes2string(entry.value_bytes());
176 }
177 }
178
179 get_default_remote_in_repo(repo)
180}
181
182pub(crate) fn get_default_remote_in_repo(
184 repo: &Repository,
185) -> Result<String> {
186 scope_time!("get_default_remote_in_repo");
187
188 let remotes = repo.remotes()?;
189
190 let found_origin = remotes
192 .iter()
193 .any(|r| r.is_some_and(|r| r == DEFAULT_REMOTE_NAME));
194 if found_origin {
195 return Ok(DEFAULT_REMOTE_NAME.into());
196 }
197
198 if remotes.len() == 1 {
200 let first_remote = remotes
201 .iter()
202 .next()
203 .flatten()
204 .map(String::from)
205 .ok_or_else(|| {
206 Error::Generic("no remote found".into())
207 })?;
208
209 return Ok(first_remote);
210 }
211
212 Err(Error::NoDefaultRemoteFound)
214}
215
216fn fetch_from_remote(
218 repo_path: &RepoPath,
219 remote: &str,
220 basic_credential: Option<BasicAuthCredential>,
221 progress_sender: Option<Sender<ProgressNotification>>,
222) -> Result<()> {
223 let repo = repo(repo_path)?;
224
225 let mut remote = repo.find_remote(remote)?;
226
227 let mut options = FetchOptions::new();
228 let callbacks = Callbacks::new(progress_sender, basic_credential);
229 options.prune(git2::FetchPrune::On);
230 options.proxy_options(proxy_auto());
231 options.download_tags(git2::AutotagOption::All);
232 options.remote_callbacks(callbacks.callbacks());
233 remote.fetch(&[] as &[&str], Some(&mut options), None)?;
234 remote.fetch(
236 &["refs/tags/*:refs/tags/*"],
237 Some(&mut options),
238 None,
239 )?;
240
241 Ok(())
242}
243
244pub fn fetch_all(
246 repo_path: &RepoPath,
247 basic_credential: &Option<BasicAuthCredential>,
248 progress_sender: &Option<Sender<ProgressPercent>>,
249) -> Result<()> {
250 scope_time!("fetch_all");
251
252 let repo = repo(repo_path)?;
253 let remotes = repo
254 .remotes()?
255 .iter()
256 .flatten()
257 .map(String::from)
258 .collect::<Vec<_>>();
259 let remotes_count = remotes.len();
260
261 for (idx, remote) in remotes.into_iter().enumerate() {
262 fetch_from_remote(
263 repo_path,
264 &remote,
265 basic_credential.clone(),
266 None,
267 )?;
268
269 if let Some(sender) = progress_sender {
270 let progress = ProgressPercent::new(idx, remotes_count);
271 sender.send(progress)?;
272 }
273 }
274
275 Ok(())
276}
277
278pub(crate) fn fetch(
280 repo_path: &RepoPath,
281 branch: &str,
282 basic_credential: Option<BasicAuthCredential>,
283 progress_sender: Option<Sender<ProgressNotification>>,
284) -> Result<usize> {
285 scope_time!("fetch");
286
287 let repo = repo(repo_path)?;
288 let branch_ref = repo
289 .find_branch(branch, BranchType::Local)?
290 .into_reference();
291 let branch_ref = bytes2string(branch_ref.name_bytes())?;
292 let remote_name = repo.branch_upstream_remote(&branch_ref)?;
293 let remote_name = bytes2string(&remote_name)?;
294 let mut remote = repo.find_remote(&remote_name)?;
295
296 let mut options = FetchOptions::new();
297 options.download_tags(git2::AutotagOption::All);
298 let callbacks = Callbacks::new(progress_sender, basic_credential);
299 options.remote_callbacks(callbacks.callbacks());
300 options.proxy_options(proxy_auto());
301
302 remote.fetch(&[branch], Some(&mut options), None)?;
303
304 Ok(remote.stats().received_bytes())
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::sync::tests::{
311 debug_cmd_print, repo_clone, repo_init,
312 };
313
314 #[test]
315 fn test_smoke() {
316 let (remote_dir, _remote) = repo_init().unwrap();
317 let remote_path = remote_dir.path().to_str().unwrap();
318 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
319 let repo_path: &RepoPath = &repo_dir
320 .into_path()
321 .as_os_str()
322 .to_str()
323 .unwrap()
324 .into();
325
326 let remotes = get_remotes(repo_path).unwrap();
327
328 assert_eq!(remotes, vec![String::from("origin")]);
329
330 fetch(repo_path, "master", None, None).unwrap();
331 }
332
333 #[test]
334 fn test_default_remote() {
335 let (remote_dir, _remote) = repo_init().unwrap();
336 let remote_path = remote_dir.path().to_str().unwrap();
337 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
338 let repo_path: &RepoPath = &repo_dir
339 .into_path()
340 .as_os_str()
341 .to_str()
342 .unwrap()
343 .into();
344
345 debug_cmd_print(
346 repo_path,
347 &format!("git remote add second {remote_path}")[..],
348 );
349
350 let remotes = get_remotes(repo_path).unwrap();
351
352 assert_eq!(remotes, vec![
353 String::from("origin"),
354 String::from("second")
355 ]);
356
357 let first =
358 get_default_remote_in_repo(&repo(repo_path).unwrap())
359 .unwrap();
360 assert_eq!(first, String::from("origin"));
361 }
362
363 #[test]
364 fn test_default_remote_out_of_order() {
365 let (remote_dir, _remote) = repo_init().unwrap();
366 let remote_path = remote_dir.path().to_str().unwrap();
367 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
368 let repo_path: &RepoPath = &repo_dir
369 .into_path()
370 .as_os_str()
371 .to_str()
372 .unwrap()
373 .into();
374
375 debug_cmd_print(
376 repo_path,
377 "git remote rename origin alternate",
378 );
379
380 debug_cmd_print(
381 repo_path,
382 &format!("git remote add origin {remote_path}")[..],
383 );
384
385 let remotes = get_remotes(repo_path).unwrap();
388
389 assert_eq!(remotes, vec![
390 String::from("alternate"),
391 String::from("origin")
392 ]);
393
394 let first =
395 get_default_remote_in_repo(&repo(repo_path).unwrap())
396 .unwrap();
397 assert_eq!(first, String::from("origin"));
398 }
399
400 #[test]
401 fn test_default_remote_inconclusive() {
402 let (remote_dir, _remote) = repo_init().unwrap();
403 let remote_path = remote_dir.path().to_str().unwrap();
404 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
405 let repo_path: &RepoPath = &repo_dir
406 .into_path()
407 .as_os_str()
408 .to_str()
409 .unwrap()
410 .into();
411
412 debug_cmd_print(
413 repo_path,
414 "git remote rename origin alternate",
415 );
416
417 debug_cmd_print(
418 repo_path,
419 &format!("git remote add someremote {remote_path}")[..],
420 );
421
422 let remotes = get_remotes(repo_path).unwrap();
423 assert_eq!(remotes, vec![
424 String::from("alternate"),
425 String::from("someremote")
426 ]);
427
428 let default_remote =
429 get_default_remote_in_repo(&repo(repo_path).unwrap());
430
431 assert!(matches!(
432 default_remote,
433 Err(Error::NoDefaultRemoteFound)
434 ));
435 }
436
437 #[test]
438 fn test_default_remote_for_fetch() {
439 let (remote_dir, _remote) = repo_init().unwrap();
440 let remote_path = remote_dir.path().to_str().unwrap();
441 let (repo_dir, repo) = repo_clone(remote_path).unwrap();
442 let repo_path: &RepoPath = &repo_dir
443 .into_path()
444 .as_os_str()
445 .to_str()
446 .unwrap()
447 .into();
448
449 debug_cmd_print(
450 repo_path,
451 "git remote rename origin alternate",
452 );
453
454 debug_cmd_print(
455 repo_path,
456 &format!("git remote add someremote {remote_path}")[..],
457 );
458
459 let mut config = repo.config().unwrap();
460
461 config
462 .set_str("branch.master.remote", "branchremote")
463 .unwrap();
464
465 let default_fetch_remote =
466 get_default_remote_for_fetch_in_repo(&repo);
467
468 assert!(
469 matches!(default_fetch_remote, Ok(remote_name) if remote_name == "branchremote")
470 );
471 }
472
473 #[test]
474 fn test_default_remote_for_push() {
475 let (remote_dir, _remote) = repo_init().unwrap();
476 let remote_path = remote_dir.path().to_str().unwrap();
477 let (repo_dir, repo) = repo_clone(remote_path).unwrap();
478 let repo_path: &RepoPath = &repo_dir
479 .into_path()
480 .as_os_str()
481 .to_str()
482 .unwrap()
483 .into();
484
485 debug_cmd_print(
486 repo_path,
487 "git remote rename origin alternate",
488 );
489
490 debug_cmd_print(
491 repo_path,
492 &format!("git remote add someremote {remote_path}")[..],
493 );
494
495 let mut config = repo.config().unwrap();
496
497 config
498 .set_str("branch.master.remote", "branchremote")
499 .unwrap();
500
501 let default_push_remote =
502 get_default_remote_for_push_in_repo(&repo);
503
504 assert!(
505 matches!(default_push_remote, Ok(remote_name) if remote_name == "branchremote")
506 );
507
508 config.set_str("remote.pushDefault", "pushdefault").unwrap();
509
510 let default_push_remote =
511 get_default_remote_for_push_in_repo(&repo);
512
513 assert!(
514 matches!(default_push_remote, Ok(remote_name) if remote_name == "pushdefault")
515 );
516
517 config
518 .set_str("branch.master.pushRemote", "branchpushremote")
519 .unwrap();
520
521 let default_push_remote =
522 get_default_remote_for_push_in_repo(&repo);
523
524 assert!(
525 matches!(default_push_remote, Ok(remote_name) if remote_name == "branchpushremote")
526 );
527 }
528}