gnostr_asyncgit/sync/remotes/
mod.rs

1//!
2
3mod 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
24/// origin
25pub const DEFAULT_REMOTE_NAME: &str = "origin";
26
27///
28pub fn proxy_auto<'a>() -> ProxyOptions<'a> {
29	let mut proxy = ProxyOptions::new();
30	proxy.auto();
31	proxy
32}
33
34///
35pub 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
46/// tries to find origin or the only remote that is defined if any
47/// in case of multiple remotes and none named *origin* we fail
48pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
49	let repo = repo(repo_path)?;
50	get_default_remote_in_repo(&repo)
51}
52
53/// Gets the current branch the user is on.
54/// Returns none if they are not on a branch
55/// and Err if there was a problem finding the branch
56fn 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
68/// Tries to find the default repo to fetch from based on
69/// configuration.
70///
71/// > branch.<name>.remote
72/// >
73/// > When on branch `<name>`, it tells `git fetch` and `git push`
74/// > which remote to fetch from or
75/// > push to. [...] If no remote is configured, or if you are not on
76/// > any branch and there is more
77/// > than one remote defined in the repository, it defaults to
78/// > `origin` for fetching [...].
79///
80/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote
81///
82/// Falls back to `get_default_remote_in_repo`.
83pub 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
90// TODO: Very similar to `get_default_remote_for_push_in_repo`. Can
91// probably be refactored.
92pub(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
114/// Tries to find the default repo to push to based on configuration.
115///
116/// > remote.pushDefault
117/// >
118/// > The remote to push to by default. Overrides
119/// > `branch.<name>.remote` for all branches, and is
120/// > overridden by `branch.<name>.pushRemote` for specific branches.
121///
122/// > branch.<name>.remote
123/// >
124/// > When on branch `<name>`, it tells `git fetch` and `git push`
125/// > which remote to fetch from or
126/// > push to. The remote to push to may be overridden with
127/// > `remote.pushDefault` (for all
128/// > branches). The remote to push to, for the current branch, may be
129/// > further overridden by
130/// > `branch.<name>.pushRemote`. If no remote is configured, or if
131/// > you are not on any branch and
132/// > there is more than one remote defined in the repository, it
133/// > defaults to `origin` for fetching
134/// > and `remote.pushDefault` for pushing.
135///
136/// [git-config-remote-push-default]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-remotepushDefault
137/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote
138///
139/// Falls back to `get_default_remote_in_repo`.
140pub 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
147// TODO: Very similar to `get_default_remote_for_fetch_in_repo`. Can
148// probably be refactored.
149pub(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
182/// see `get_default_remote`
183pub(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	// if `origin` exists return that
191	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 only one remote exists pick that
199	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	//inconclusive
213	Err(Error::NoDefaultRemoteFound)
214}
215
216///
217fn 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	// fetch tags (also removing remotely deleted ones)
235	remote.fetch(
236		&["refs/tags/*:refs/tags/*"],
237		Some(&mut options),
238		None,
239	)?;
240
241	Ok(())
242}
243
244/// updates/prunes all branches from all remotes
245pub 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
278/// fetches from upstream/remote for local `branch`
279pub(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		//NOTE: apparently remotes are not chronolically sorted but
386		// alphabetically
387		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}