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 error::{Error, Result},
17 sync::{
18 cred::BasicAuthCredential, remotes::push::ProgressNotification, repository::repo, utils,
19 },
20 ProgressPercent,
21};
22
23pub const DEFAULT_REMOTE_NAME: &str = "origin";
25
26pub fn proxy_auto<'a>() -> ProxyOptions<'a> {
28 let mut proxy = ProxyOptions::new();
29 proxy.auto();
30 proxy
31}
32
33pub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {
35 scope_time!("get_remotes");
36
37 let repo = repo(repo_path)?;
38 let remotes = repo.remotes()?;
39 let remotes: Vec<String> = remotes.iter().flatten().map(String::from).collect();
40
41 Ok(remotes)
42}
43
44pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
47 let repo = repo(repo_path)?;
48 get_default_remote_in_repo(&repo)
49}
50
51fn get_current_branch(repo: &Repository) -> Result<Option<git2::Branch<'_>>> {
55 for b in repo.branches(None)? {
56 let branch = b?.0;
57 if branch.is_head() {
58 return Ok(Some(branch));
59 }
60 }
61 Ok(None)
62}
63
64pub fn get_default_remote_for_fetch(repo_path: &RepoPath) -> Result<String> {
80 let repo = repo(repo_path)?;
81 get_default_remote_for_fetch_in_repo(&repo)
82}
83
84pub(crate) fn get_default_remote_for_fetch_in_repo(repo: &Repository) -> Result<String> {
87 scope_time!("get_default_remote_for_fetch_in_repo");
88
89 let config = repo.config()?;
90
91 let branch = get_current_branch(repo)?;
92
93 if let Some(branch) = branch {
94 let remote_name = bytes2string(branch.name_bytes()?)?;
95
96 let entry_name = format!("branch.{}.remote", &remote_name);
97
98 if let Ok(entry) = config.get_entry(&entry_name) {
99 return bytes2string(entry.value_bytes());
100 }
101 }
102
103 get_default_remote_in_repo(repo)
104}
105
106pub fn get_default_remote_for_push(repo_path: &RepoPath) -> Result<String> {
133 let repo = repo(repo_path)?;
134 get_default_remote_for_push_in_repo(&repo)
135}
136
137pub(crate) fn get_default_remote_for_push_in_repo(repo: &Repository) -> Result<String> {
140 scope_time!("get_default_remote_for_push_in_repo");
141
142 let config = repo.config()?;
143
144 let branch = get_current_branch(repo)?;
145
146 if let Some(branch) = branch {
147 let remote_name = bytes2string(branch.name_bytes()?)?;
148
149 let entry_name = format!("branch.{}.pushRemote", &remote_name);
150
151 if let Ok(entry) = config.get_entry(&entry_name) {
152 return bytes2string(entry.value_bytes());
153 }
154
155 if let Ok(entry) = config.get_entry("remote.pushDefault") {
156 return bytes2string(entry.value_bytes());
157 }
158
159 let entry_name = format!("branch.{}.remote", &remote_name);
160
161 if let Ok(entry) = config.get_entry(&entry_name) {
162 return bytes2string(entry.value_bytes());
163 }
164 }
165
166 get_default_remote_in_repo(repo)
167}
168
169pub(crate) fn get_default_remote_in_repo(repo: &Repository) -> Result<String> {
171 scope_time!("get_default_remote_in_repo");
172
173 let remotes = repo.remotes()?;
174
175 let found_origin = remotes
177 .iter()
178 .any(|r| r.is_some_and(|r| r == DEFAULT_REMOTE_NAME));
179 if found_origin {
180 return Ok(DEFAULT_REMOTE_NAME.into());
181 }
182
183 if remotes.len() == 1 {
185 let first_remote = remotes
186 .iter()
187 .next()
188 .flatten()
189 .map(String::from)
190 .ok_or_else(|| Error::Generic("no remote found".into()))?;
191
192 return Ok(first_remote);
193 }
194
195 Err(Error::NoDefaultRemoteFound)
197}
198
199fn fetch_from_remote(
201 repo_path: &RepoPath,
202 remote: &str,
203 basic_credential: Option<BasicAuthCredential>,
204 progress_sender: Option<Sender<ProgressNotification>>,
205) -> Result<()> {
206 let repo = repo(repo_path)?;
207
208 let mut remote = repo.find_remote(remote)?;
209
210 let mut options = FetchOptions::new();
211 let callbacks = Callbacks::new(progress_sender, basic_credential);
212 options.prune(git2::FetchPrune::On);
213 options.proxy_options(proxy_auto());
214 options.download_tags(git2::AutotagOption::All);
215 options.remote_callbacks(callbacks.callbacks());
216 remote.fetch(&[] as &[&str], Some(&mut options), None)?;
217 remote.fetch(&["refs/tags/*:refs/tags/*"], Some(&mut options), None)?;
219
220 Ok(())
221}
222
223pub fn fetch_all(
225 repo_path: &RepoPath,
226 basic_credential: &Option<BasicAuthCredential>,
227 progress_sender: &Option<Sender<ProgressPercent>>,
228) -> Result<()> {
229 scope_time!("fetch_all");
230
231 let repo = repo(repo_path)?;
232 let remotes = repo
233 .remotes()?
234 .iter()
235 .flatten()
236 .map(String::from)
237 .collect::<Vec<_>>();
238 let remotes_count = remotes.len();
239
240 for (idx, remote) in remotes.into_iter().enumerate() {
241 fetch_from_remote(repo_path, &remote, basic_credential.clone(), None)?;
242
243 if let Some(sender) = progress_sender {
244 let progress = ProgressPercent::new(idx, remotes_count);
245 sender.send(progress)?;
246 }
247 }
248
249 Ok(())
250}
251
252pub(crate) fn fetch(
254 repo_path: &RepoPath,
255 branch: &str,
256 basic_credential: Option<BasicAuthCredential>,
257 progress_sender: Option<Sender<ProgressNotification>>,
258) -> Result<usize> {
259 scope_time!("fetch");
260
261 let repo = repo(repo_path)?;
262 let branch_ref = repo
263 .find_branch(branch, BranchType::Local)?
264 .into_reference();
265 let branch_ref = bytes2string(branch_ref.name_bytes())?;
266 let remote_name = repo.branch_upstream_remote(&branch_ref)?;
267 let remote_name = bytes2string(&remote_name)?;
268 let mut remote = repo.find_remote(&remote_name)?;
269
270 let mut options = FetchOptions::new();
271 options.download_tags(git2::AutotagOption::All);
272 let callbacks = Callbacks::new(progress_sender, basic_credential);
273 options.remote_callbacks(callbacks.callbacks());
274 options.proxy_options(proxy_auto());
275
276 remote.fetch(&[branch], Some(&mut options), None)?;
277
278 Ok(remote.stats().received_bytes())
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::sync::tests::{debug_cmd_print, repo_clone, repo_init};
285
286 #[test]
287 fn test_smoke() {
288 let (remote_dir, _remote) = repo_init().unwrap();
289 let remote_path = remote_dir.path().to_str().unwrap();
290 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
291 let repo_path: &RepoPath = &repo_dir.into_path().as_os_str().to_str().unwrap().into();
292
293 let remotes = get_remotes(repo_path).unwrap();
294
295 assert_eq!(remotes, vec![String::from("origin")]);
296
297 fetch(repo_path, "master", None, None).unwrap();
298 }
299
300 #[test]
301 fn test_default_remote() {
302 let (remote_dir, _remote) = repo_init().unwrap();
303 let remote_path = remote_dir.path().to_str().unwrap();
304 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
305 let repo_path: &RepoPath = &repo_dir.into_path().as_os_str().to_str().unwrap().into();
306
307 debug_cmd_print(
308 repo_path,
309 &format!("git remote add second {remote_path}")[..],
310 );
311
312 let remotes = get_remotes(repo_path).unwrap();
313
314 assert_eq!(
315 remotes,
316 vec![String::from("origin"), String::from("second")]
317 );
318
319 let first = get_default_remote_in_repo(&repo(repo_path).unwrap()).unwrap();
320 assert_eq!(first, String::from("origin"));
321 }
322
323 #[test]
324 fn test_default_remote_out_of_order() {
325 let (remote_dir, _remote) = repo_init().unwrap();
326 let remote_path = remote_dir.path().to_str().unwrap();
327 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
328 let repo_path: &RepoPath = &repo_dir.into_path().as_os_str().to_str().unwrap().into();
329
330 debug_cmd_print(repo_path, "git remote rename origin alternate");
331
332 debug_cmd_print(
333 repo_path,
334 &format!("git remote add origin {remote_path}")[..],
335 );
336
337 let remotes = get_remotes(repo_path).unwrap();
340
341 assert_eq!(
342 remotes,
343 vec![String::from("alternate"), String::from("origin")]
344 );
345
346 let first = get_default_remote_in_repo(&repo(repo_path).unwrap()).unwrap();
347 assert_eq!(first, String::from("origin"));
348 }
349
350 #[test]
351 fn test_default_remote_inconclusive() {
352 let (remote_dir, _remote) = repo_init().unwrap();
353 let remote_path = remote_dir.path().to_str().unwrap();
354 let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
355 let repo_path: &RepoPath = &repo_dir.into_path().as_os_str().to_str().unwrap().into();
356
357 debug_cmd_print(repo_path, "git remote rename origin alternate");
358
359 debug_cmd_print(
360 repo_path,
361 &format!("git remote add someremote {remote_path}")[..],
362 );
363
364 let remotes = get_remotes(repo_path).unwrap();
365 assert_eq!(
366 remotes,
367 vec![String::from("alternate"), String::from("someremote")]
368 );
369
370 let default_remote = get_default_remote_in_repo(&repo(repo_path).unwrap());
371
372 assert!(matches!(default_remote, Err(Error::NoDefaultRemoteFound)));
373 }
374
375 #[test]
376 fn test_default_remote_for_fetch() {
377 let (remote_dir, _remote) = repo_init().unwrap();
378 let remote_path = remote_dir.path().to_str().unwrap();
379 let (repo_dir, repo) = repo_clone(remote_path).unwrap();
380 let repo_path: &RepoPath = &repo_dir.into_path().as_os_str().to_str().unwrap().into();
381
382 debug_cmd_print(repo_path, "git remote rename origin alternate");
383
384 debug_cmd_print(
385 repo_path,
386 &format!("git remote add someremote {remote_path}")[..],
387 );
388
389 let mut config = repo.config().unwrap();
390
391 config
392 .set_str("branch.master.remote", "branchremote")
393 .unwrap();
394
395 let default_fetch_remote = get_default_remote_for_fetch_in_repo(&repo);
396
397 assert!(matches!(default_fetch_remote, Ok(remote_name) if remote_name == "branchremote"));
398 }
399
400 #[test]
401 fn test_default_remote_for_push() {
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.into_path().as_os_str().to_str().unwrap().into();
406
407 debug_cmd_print(repo_path, "git remote rename origin alternate");
408
409 debug_cmd_print(
410 repo_path,
411 &format!("git remote add someremote {remote_path}")[..],
412 );
413
414 let mut config = repo.config().unwrap();
415
416 config
417 .set_str("branch.master.remote", "branchremote")
418 .unwrap();
419
420 let default_push_remote = get_default_remote_for_push_in_repo(&repo);
421
422 assert!(matches!(default_push_remote, Ok(remote_name) if remote_name == "branchremote"));
423
424 config.set_str("remote.pushDefault", "pushdefault").unwrap();
425
426 let default_push_remote = get_default_remote_for_push_in_repo(&repo);
427
428 assert!(matches!(default_push_remote, Ok(remote_name) if remote_name == "pushdefault"));
429
430 config
431 .set_str("branch.master.pushRemote", "branchpushremote")
432 .unwrap();
433
434 let default_push_remote = get_default_remote_for_push_in_repo(&repo);
435
436 assert!(
437 matches!(default_push_remote, Ok(remote_name) if remote_name == "branchpushremote")
438 );
439 }
440}