gnostr_asyncgit/sync/branch/
mod.rs

1//! branch functions
2
3pub mod merge_commit;
4pub mod merge_ff;
5pub mod merge_rebase;
6pub mod rename;
7
8use std::collections::HashSet;
9
10use git2::{Branch, BranchType, Repository};
11use scopetime::scope_time;
12
13use super::{RepoPath, utils::bytes2string};
14use crate::{
15	error::{Error, Result},
16	sync::{
17		CommitId, remotes::get_default_remote_for_push_in_repo,
18		repository::repo, utils::get_head_repo,
19	},
20};
21
22/// returns the branch-name head is currently pointing to
23/// this might be expensive, see `cached::BranchName`
24pub(crate) fn get_branch_name(
25	repo_path: &RepoPath,
26) -> Result<String> {
27	let repo = repo(repo_path)?;
28
29	get_branch_name_repo(&repo)
30}
31
32/// ditto
33pub(crate) fn get_branch_name_repo(
34	repo: &Repository,
35) -> Result<String> {
36	scope_time!("get_branch_name_repo");
37
38	let head_ref = repo.head().map_err(|e| {
39		if e.code() == git2::ErrorCode::UnbornBranch {
40			Error::NoHead
41		} else {
42			e.into()
43		}
44	})?;
45
46	bytes2string(head_ref.shorthand_bytes())
47}
48
49///
50#[derive(Clone, Debug)]
51pub struct LocalBranch {
52	///
53	pub is_head: bool,
54	///
55	pub has_upstream: bool,
56	///
57	pub upstream: Option<UpstreamBranch>,
58	///
59	pub remote: Option<String>,
60}
61
62///
63#[derive(Clone, Debug)]
64pub struct UpstreamBranch {
65	///
66	pub reference: String,
67}
68
69///
70#[derive(Clone, Debug)]
71pub struct RemoteBranch {
72	///
73	pub has_tracking: bool,
74}
75
76///
77#[derive(Clone, Debug)]
78pub enum BranchDetails {
79	///
80	Local(LocalBranch),
81	///
82	Remote(RemoteBranch),
83}
84
85///
86#[derive(Clone, Debug)]
87pub struct BranchInfo {
88	///
89	pub name: String,
90	///
91	pub reference: String,
92	///
93	pub top_commit_message: String,
94	///
95	pub top_commit: CommitId,
96	///
97	pub details: BranchDetails,
98}
99
100impl BranchInfo {
101	/// returns details about local branch or None
102	pub const fn local_details(&self) -> Option<&LocalBranch> {
103		if let BranchDetails::Local(details) = &self.details {
104			return Some(details);
105		}
106
107		None
108	}
109}
110
111///
112pub fn validate_branch_name(name: &str) -> Result<bool> {
113	scope_time!("validate_branch_name");
114
115	let valid = Branch::name_is_valid(name)?;
116
117	Ok(valid)
118}
119
120/// returns a list of `BranchInfo` with a simple summary on each
121/// branch `local` filters for local branches otherwise remote
122/// branches will be returned
123pub fn get_branches_info(
124	repo_path: &RepoPath,
125	local: bool,
126) -> Result<Vec<BranchInfo>> {
127	scope_time!("get_branches_info");
128
129	let repo = repo(repo_path)?;
130
131	let (filter, remotes_with_tracking) = if local {
132		(BranchType::Local, HashSet::default())
133	} else {
134		let remotes: HashSet<_> = repo
135			.branches(Some(BranchType::Local))?
136			.filter_map(|b| {
137				let branch = b.ok()?.0;
138				let upstream = branch.upstream();
139				upstream
140					.ok()?
141					.name_bytes()
142					.ok()
143					.map(ToOwned::to_owned)
144			})
145			.collect();
146		(BranchType::Remote, remotes)
147	};
148
149	let mut branches_for_display: Vec<BranchInfo> = repo
150		.branches(Some(filter))?
151		.map(|b| {
152			let branch = b?.0;
153			let top_commit = branch.get().peel_to_commit()?;
154			let reference = bytes2string(branch.get().name_bytes())?;
155			let upstream = branch.upstream();
156
157			let remote = repo
158				.branch_upstream_remote(&reference)
159				.ok()
160				.as_ref()
161				.and_then(git2::Buf::as_str)
162				.map(String::from);
163
164			let name_bytes = branch.name_bytes()?;
165
166			let upstream_branch =
167				upstream.ok().and_then(|upstream| {
168					bytes2string(upstream.get().name_bytes())
169						.ok()
170						.map(|reference| UpstreamBranch { reference })
171				});
172
173			let details = if local {
174				BranchDetails::Local(LocalBranch {
175					is_head: branch.is_head(),
176					has_upstream: upstream_branch.is_some(),
177					upstream: upstream_branch,
178					remote,
179				})
180			} else {
181				BranchDetails::Remote(RemoteBranch {
182					has_tracking: remotes_with_tracking
183						.contains(name_bytes),
184				})
185			};
186
187			Ok(BranchInfo {
188				name: bytes2string(name_bytes)?,
189				reference,
190				top_commit_message: bytes2string(
191					top_commit.summary_bytes().unwrap_or_default(),
192				)?,
193				top_commit: top_commit.id().into(),
194				details,
195			})
196		})
197		.filter_map(Result::ok)
198		.collect();
199
200	branches_for_display.sort_by(|a, b| a.name.cmp(&b.name));
201
202	Ok(branches_for_display)
203}
204
205///
206#[derive(Debug, Default)]
207pub struct BranchCompare {
208	///
209	pub ahead: usize,
210	///
211	pub behind: usize,
212}
213
214///
215pub(crate) fn branch_set_upstream_after_push(
216	repo: &Repository,
217	branch_name: &str,
218) -> Result<()> {
219	scope_time!("branch_set_upstream");
220
221	let mut branch =
222		repo.find_branch(branch_name, BranchType::Local)?;
223
224	if branch.upstream().is_err() {
225		let remote = get_default_remote_for_push_in_repo(repo)?;
226		let upstream_name = format!("{remote}/{branch_name}");
227		branch.set_upstream(Some(upstream_name.as_str()))?;
228	}
229
230	Ok(())
231}
232
233/// returns remote of the upstream tracking branch for `branch`
234pub fn get_branch_remote(
235	repo_path: &RepoPath,
236	branch: &str,
237) -> Result<Option<String>> {
238	let repo = repo(repo_path)?;
239	let branch = repo.find_branch(branch, BranchType::Local)?;
240	let reference = bytes2string(branch.get().name_bytes())?;
241	let remote_name = repo.branch_upstream_remote(&reference).ok();
242	if let Some(remote_name) = remote_name {
243		Ok(Some(bytes2string(remote_name.as_ref())?))
244	} else {
245		Ok(None)
246	}
247}
248
249/// returns whether the pull merge strategy is set to rebase
250pub fn config_is_pull_rebase(repo_path: &RepoPath) -> Result<bool> {
251	let repo = repo(repo_path)?;
252	let config = repo.config()?;
253
254	if let Ok(rebase) = config.get_entry("pull.rebase") {
255		let value =
256			rebase.value().map(String::from).unwrap_or_default();
257		return Ok(value == "true");
258	};
259
260	Ok(false)
261}
262
263///
264pub fn branch_compare_upstream(
265	repo_path: &RepoPath,
266	branch: &str,
267) -> Result<BranchCompare> {
268	scope_time!("branch_compare_upstream");
269
270	let repo = repo(repo_path)?;
271
272	let branch = repo.find_branch(branch, BranchType::Local)?;
273
274	let upstream = branch.upstream()?;
275
276	let branch_commit =
277		branch.into_reference().peel_to_commit()?.id();
278
279	let upstream_commit =
280		upstream.into_reference().peel_to_commit()?.id();
281
282	let (ahead, behind) =
283		repo.graph_ahead_behind(branch_commit, upstream_commit)?;
284
285	Ok(BranchCompare { ahead, behind })
286}
287
288/// Switch branch to given `branch_name`.
289///
290/// Method will fail if there are conflicting changes between current
291/// and target branch. However, if files are not conflicting, they
292/// will remain in tree (e.g. tracked new file is not conflicting and
293/// therefore is kept in tree even after checkout).
294pub fn checkout_branch(
295	repo_path: &RepoPath,
296	branch_name: &str,
297) -> Result<()> {
298	scope_time!("checkout_branch");
299
300	let repo = repo(repo_path)?;
301
302	let branch = repo.find_branch(branch_name, BranchType::Local)?;
303
304	let branch_ref = branch.into_reference();
305
306	let target_treeish = branch_ref.peel_to_tree()?;
307	let target_treeish_object = target_treeish.as_object();
308
309	// modify state to match branch's state
310	repo.checkout_tree(
311		target_treeish_object,
312		Some(&mut git2::build::CheckoutBuilder::new()),
313	)?;
314
315	let branch_ref = branch_ref.name().ok_or_else(|| {
316		Error::Generic(String::from("branch ref not found"))
317	});
318
319	// modify HEAD to point to given branch
320	repo.set_head(branch_ref?)?;
321
322	Ok(())
323}
324
325/// Detach HEAD to point to a commit then checkout HEAD, does not work
326/// if there are uncommitted changes
327pub fn checkout_commit(
328	repo_path: &RepoPath,
329	commit_hash: CommitId,
330) -> Result<()> {
331	scope_time!("checkout_commit");
332
333	let repo = repo(repo_path)?;
334	let cur_ref = repo.head()?;
335	let statuses = repo.statuses(Some(
336		git2::StatusOptions::new().include_ignored(false),
337	))?;
338
339	if statuses.is_empty() {
340		repo.set_head_detached(commit_hash.into())?;
341
342		if let Err(e) = repo.checkout_head(Some(
343			git2::build::CheckoutBuilder::new().force(),
344		)) {
345			repo.set_head(
346				bytes2string(cur_ref.name_bytes())?.as_str(),
347			)?;
348			return Err(Error::Git(e));
349		}
350		Ok(())
351	} else {
352		Err(Error::UncommittedChanges)
353	}
354}
355
356///
357pub fn checkout_remote_branch(
358	repo_path: &RepoPath,
359	branch: &BranchInfo,
360) -> Result<()> {
361	scope_time!("checkout_remote_branch");
362
363	let repo = repo(repo_path)?;
364	let cur_ref = repo.head()?;
365
366	if !repo
367		.statuses(Some(
368			git2::StatusOptions::new().include_ignored(false),
369		))?
370		.is_empty()
371	{
372		return Err(Error::UncommittedChanges);
373	}
374
375	let name = branch.name.find('/').map_or_else(
376		|| branch.name.clone(),
377		|pos| branch.name[pos..].to_string(),
378	);
379
380	let commit = repo.find_commit(branch.top_commit.into())?;
381	let mut new_branch = repo.branch(&name, &commit, false)?;
382	new_branch.set_upstream(Some(&branch.name))?;
383
384	repo.set_head(
385		bytes2string(new_branch.into_reference().name_bytes())?
386			.as_str(),
387	)?;
388
389	if let Err(e) = repo.checkout_head(Some(
390		git2::build::CheckoutBuilder::new().force(),
391	)) {
392		// This is safe because cur_ref was just found
393		repo.set_head(bytes2string(cur_ref.name_bytes())?.as_str())?;
394		return Err(Error::Git(e));
395	}
396	Ok(())
397}
398
399/// The user must not be on the branch for the branch to be deleted
400pub fn delete_branch(
401	repo_path: &RepoPath,
402	branch_ref: &str,
403) -> Result<()> {
404	scope_time!("delete_branch");
405
406	let repo = repo(repo_path)?;
407	let branch_as_ref = repo.find_reference(branch_ref)?;
408	let mut branch = git2::Branch::wrap(branch_as_ref);
409	if branch.is_head() {
410		return Err(Error::Generic("You cannot be on the branch you want to delete, switch branch, then delete this branch".to_string()));
411	}
412	branch.delete()?;
413	Ok(())
414}
415
416/// creates a new branch pointing to current HEAD commit and updating
417/// HEAD to new branch
418pub fn create_branch(
419	repo_path: &RepoPath,
420	name: &str,
421) -> Result<String> {
422	scope_time!("create_branch");
423
424	let repo = repo(repo_path)?;
425
426	let head_id = get_head_repo(&repo)?;
427	let head_commit = repo.find_commit(head_id.into())?;
428
429	let branch = repo.branch(name, &head_commit, false)?;
430	let branch_ref = branch.into_reference();
431	let branch_ref_name = bytes2string(branch_ref.name_bytes())?;
432	repo.set_head(branch_ref_name.as_str())?;
433
434	Ok(branch_ref_name)
435}
436
437#[cfg(test)]
438mod tests_branch_name {
439	use super::*;
440	use crate::sync::tests::{repo_init, repo_init_empty};
441
442	#[test]
443	fn test_smoke() {
444		let (_td, repo) = repo_init().unwrap();
445		let root = repo.path().parent().unwrap();
446		let repo_path: &RepoPath =
447			&root.as_os_str().to_str().unwrap().into();
448
449		assert_eq!(
450			get_branch_name(repo_path).unwrap().as_str(),
451			"master"
452		);
453	}
454
455	#[test]
456	fn test_empty_repo() {
457		let (_td, repo) = repo_init_empty().unwrap();
458		let root = repo.path().parent().unwrap();
459		let repo_path: &RepoPath =
460			&root.as_os_str().to_str().unwrap().into();
461
462		assert!(matches!(
463			get_branch_name(repo_path),
464			Err(Error::NoHead)
465		));
466	}
467}
468
469#[cfg(test)]
470mod tests_create_branch {
471	use super::*;
472	use crate::sync::tests::repo_init;
473
474	#[test]
475	fn test_smoke() {
476		let (_td, repo) = repo_init().unwrap();
477		let root = repo.path().parent().unwrap();
478		let repo_path: &RepoPath =
479			&root.as_os_str().to_str().unwrap().into();
480
481		create_branch(repo_path, "branch1").unwrap();
482
483		assert_eq!(
484			get_branch_name(repo_path).unwrap().as_str(),
485			"branch1"
486		);
487	}
488}
489
490#[cfg(test)]
491mod tests_branch_compare {
492	use super::*;
493	use crate::sync::tests::repo_init;
494
495	#[test]
496	fn test_smoke() {
497		let (_td, repo) = repo_init().unwrap();
498		let root = repo.path().parent().unwrap();
499		let repo_path: &RepoPath =
500			&root.as_os_str().to_str().unwrap().into();
501
502		create_branch(repo_path, "test").unwrap();
503
504		let res = branch_compare_upstream(repo_path, "test");
505
506		assert_eq!(res.is_err(), true);
507	}
508}
509
510#[cfg(test)]
511mod tests_branches {
512	use super::*;
513	use crate::sync::{
514		remotes::{get_remotes, push::push_branch},
515		rename_branch,
516		tests::{
517			debug_cmd_print, repo_clone, repo_init, repo_init_bare,
518			write_commit_file,
519		},
520	};
521
522	#[test]
523	fn test_smoke() {
524		let (_td, repo) = repo_init().unwrap();
525		let root = repo.path().parent().unwrap();
526		let repo_path: &RepoPath =
527			&root.as_os_str().to_str().unwrap().into();
528
529		assert_eq!(
530			get_branches_info(repo_path, true)
531				.unwrap()
532				.iter()
533				.map(|b| b.name.clone())
534				.collect::<Vec<_>>(),
535			vec!["master"]
536		);
537	}
538
539	#[test]
540	fn test_multiple() {
541		let (_td, repo) = repo_init().unwrap();
542		let root = repo.path().parent().unwrap();
543		let repo_path: &RepoPath =
544			&root.as_os_str().to_str().unwrap().into();
545
546		create_branch(repo_path, "test").unwrap();
547
548		assert_eq!(
549			get_branches_info(repo_path, true)
550				.unwrap()
551				.iter()
552				.map(|b| b.name.clone())
553				.collect::<Vec<_>>(),
554			vec!["master", "test"]
555		);
556	}
557
558	fn clone_branch_commit_push(target: &str, branch_name: &str) {
559		let (dir, repo) = repo_clone(target).unwrap();
560		let dir = dir.path().to_str().unwrap();
561
562		write_commit_file(&repo, "f1.txt", "foo", "c1");
563		rename_branch(&dir.into(), "refs/heads/master", branch_name)
564			.unwrap();
565		push_branch(
566			&dir.into(),
567			"origin",
568			branch_name,
569			false,
570			false,
571			None,
572			None,
573		)
574		.unwrap();
575	}
576
577	#[test]
578	fn test_remotes_of_branches() {
579		let (r1_path, _remote1) = repo_init_bare().unwrap();
580		let (r2_path, _remote2) = repo_init_bare().unwrap();
581		let (_r, repo) = repo_init().unwrap();
582
583		let r1_path = r1_path.path().to_str().unwrap();
584		let r2_path = r2_path.path().to_str().unwrap();
585
586		//Note: create those test branches in our remotes
587		clone_branch_commit_push(r1_path, "r1branch");
588		clone_branch_commit_push(r2_path, "r2branch");
589
590		let root = repo.path().parent().unwrap();
591		let repo_path: &RepoPath =
592			&root.as_os_str().to_str().unwrap().into();
593
594		//add the remotes
595		repo.remote("r1", r1_path).unwrap();
596		repo.remote("r2", r2_path).unwrap();
597
598		//verify we got the remotes
599		let remotes = get_remotes(repo_path).unwrap();
600		assert_eq!(remotes, vec![
601			String::from("r1"),
602			String::from("r2")
603		]);
604
605		//verify we got only master right now
606		let branches = get_branches_info(repo_path, true).unwrap();
607		assert_eq!(branches.len(), 1);
608		assert_eq!(branches[0].name, String::from("master"));
609
610		//pull stuff from the two remotes
611		debug_cmd_print(repo_path, "git pull r1");
612		debug_cmd_print(repo_path, "git pull r2");
613
614		//create local tracking branches
615		debug_cmd_print(
616			repo_path,
617			"git checkout --track r1/r1branch",
618		);
619		debug_cmd_print(
620			repo_path,
621			"git checkout --track r2/r2branch",
622		);
623
624		let branches = get_branches_info(repo_path, true).unwrap();
625		assert_eq!(branches.len(), 3);
626		assert_eq!(
627			branches[1]
628				.local_details()
629				.unwrap()
630				.remote
631				.as_ref()
632				.unwrap(),
633			"r1"
634		);
635		assert_eq!(
636			branches[2]
637				.local_details()
638				.unwrap()
639				.remote
640				.as_ref()
641				.unwrap(),
642			"r2"
643		);
644
645		assert_eq!(
646			get_branch_remote(repo_path, "r1branch")
647				.unwrap()
648				.unwrap(),
649			String::from("r1")
650		);
651
652		assert_eq!(
653			get_branch_remote(repo_path, "r2branch")
654				.unwrap()
655				.unwrap(),
656			String::from("r2")
657		);
658	}
659
660	#[test]
661	fn test_branch_remote_no_upstream() {
662		let (_r, repo) = repo_init().unwrap();
663		let root = repo.path().parent().unwrap();
664		let repo_path: &RepoPath =
665			&root.as_os_str().to_str().unwrap().into();
666
667		assert_eq!(
668			get_branch_remote(repo_path, "master").unwrap(),
669			None
670		);
671	}
672
673	#[test]
674	fn test_branch_remote_no_branch() {
675		let (_r, repo) = repo_init().unwrap();
676		let root = repo.path().parent().unwrap();
677		let repo_path: &RepoPath =
678			&root.as_os_str().to_str().unwrap().into();
679
680		assert!(get_branch_remote(repo_path, "foo").is_err());
681	}
682}
683
684#[cfg(test)]
685mod tests_checkout {
686	use std::{fs::File, path::Path};
687
688	use super::*;
689	use crate::sync::{stage_add_file, tests::repo_init};
690
691	#[test]
692	fn test_smoke() {
693		let (_td, repo) = repo_init().unwrap();
694		let root = repo.path().parent().unwrap();
695		let repo_path: &RepoPath =
696			&root.as_os_str().to_str().unwrap().into();
697
698		assert!(checkout_branch(repo_path, "master").is_ok());
699		assert!(checkout_branch(repo_path, "foobar").is_err());
700	}
701
702	#[test]
703	fn test_multiple() {
704		let (_td, repo) = repo_init().unwrap();
705		let root = repo.path().parent().unwrap();
706		let repo_path: &RepoPath =
707			&root.as_os_str().to_str().unwrap().into();
708
709		create_branch(repo_path, "test").unwrap();
710
711		assert!(checkout_branch(repo_path, "test").is_ok());
712		assert!(checkout_branch(repo_path, "master").is_ok());
713		assert!(checkout_branch(repo_path, "test").is_ok());
714	}
715
716	#[test]
717	fn test_branch_with_slash_in_name() {
718		let (_td, repo) = repo_init().unwrap();
719		let root = repo.path().parent().unwrap();
720		let repo_path: &RepoPath =
721			&root.as_os_str().to_str().unwrap().into();
722
723		create_branch(repo_path, "foo/bar").unwrap();
724		checkout_branch(repo_path, "foo/bar").unwrap();
725	}
726
727	#[test]
728	fn test_staged_new_file() {
729		let (_td, repo) = repo_init().unwrap();
730		let root = repo.path().parent().unwrap();
731		let repo_path: &RepoPath =
732			&root.as_os_str().to_str().unwrap().into();
733
734		create_branch(repo_path, "test").unwrap();
735
736		let filename = "file.txt";
737		let file = root.join(filename);
738		File::create(&file).unwrap();
739
740		stage_add_file(&repo_path, &Path::new(filename)).unwrap();
741
742		assert!(checkout_branch(repo_path, "test").is_ok());
743	}
744}
745
746#[cfg(test)]
747mod tests_checkout_commit {
748	use super::*;
749	use crate::sync::{
750		RepoPath,
751		tests::{repo_init, write_commit_file},
752	};
753
754	#[test]
755	fn test_smoke() {
756		let (_td, repo) = repo_init().unwrap();
757		let root = repo.path().parent().unwrap();
758		let repo_path: &RepoPath =
759			&root.as_os_str().to_str().unwrap().into();
760
761		let commit =
762			write_commit_file(&repo, "test_1.txt", "test", "commit1");
763		write_commit_file(&repo, "test_2.txt", "test", "commit2");
764
765		checkout_commit(repo_path, commit).unwrap();
766
767		assert!(repo.head_detached().unwrap());
768		assert_eq!(
769			repo.head().unwrap().target().unwrap(),
770			commit.get_oid()
771		);
772	}
773}
774
775#[cfg(test)]
776mod test_delete_branch {
777	use super::*;
778	use crate::sync::tests::repo_init;
779
780	#[test]
781	fn test_delete_branch() {
782		let (_td, repo) = repo_init().unwrap();
783		let root = repo.path().parent().unwrap();
784		let repo_path: &RepoPath =
785			&root.as_os_str().to_str().unwrap().into();
786
787		create_branch(repo_path, "branch1").unwrap();
788		create_branch(repo_path, "branch2").unwrap();
789
790		checkout_branch(repo_path, "branch1").unwrap();
791
792		assert_eq!(
793			repo.branches(None)
794				.unwrap()
795				.nth(1)
796				.unwrap()
797				.unwrap()
798				.0
799				.name()
800				.unwrap()
801				.unwrap(),
802			"branch2"
803		);
804
805		delete_branch(repo_path, "refs/heads/branch2").unwrap();
806
807		assert_eq!(
808			repo.branches(None)
809				.unwrap()
810				.nth(1)
811				.unwrap()
812				.unwrap()
813				.0
814				.name()
815				.unwrap()
816				.unwrap(),
817			"master"
818		);
819	}
820}
821
822#[cfg(test)]
823mod test_remote_branches {
824	use super::*;
825	use crate::sync::{
826		remotes::push::push_branch,
827		tests::{repo_clone, repo_init_bare, write_commit_file},
828	};
829
830	impl BranchInfo {
831		/// returns details about remote branch or None
832		const fn remote_details(&self) -> Option<&RemoteBranch> {
833			if let BranchDetails::Remote(details) = &self.details {
834				Some(details)
835			} else {
836				None
837			}
838		}
839	}
840
841	#[test]
842	fn test_remote_branches() {
843		let (r1_dir, _repo) = repo_init_bare().unwrap();
844
845		let (clone1_dir, clone1) =
846			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
847
848		let clone1_dir = clone1_dir.path().to_str().unwrap();
849
850		// clone1
851
852		write_commit_file(&clone1, "test.txt", "test", "commit1");
853
854		push_branch(
855			&clone1_dir.into(),
856			"origin",
857			"master",
858			false,
859			false,
860			None,
861			None,
862		)
863		.unwrap();
864
865		create_branch(&clone1_dir.into(), "foo").unwrap();
866
867		write_commit_file(&clone1, "test.txt", "test2", "commit2");
868
869		push_branch(
870			&clone1_dir.into(),
871			"origin",
872			"foo",
873			false,
874			false,
875			None,
876			None,
877		)
878		.unwrap();
879
880		// clone2
881
882		let (clone2_dir, _clone2) =
883			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
884
885		let clone2_dir = clone2_dir.path().to_str().unwrap();
886
887		let local_branches =
888			get_branches_info(&clone2_dir.into(), true).unwrap();
889
890		assert_eq!(local_branches.len(), 1);
891
892		let branches =
893			get_branches_info(&clone2_dir.into(), false).unwrap();
894		assert_eq!(dbg!(&branches).len(), 3);
895		assert_eq!(&branches[0].name, "origin/HEAD");
896		assert_eq!(&branches[1].name, "origin/foo");
897		assert_eq!(&branches[2].name, "origin/master");
898	}
899
900	#[test]
901	fn test_checkout_remote_branch() {
902		let (r1_dir, _repo) = repo_init_bare().unwrap();
903
904		let (clone1_dir, clone1) =
905			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
906		let clone1_dir = clone1_dir.path().to_str().unwrap();
907
908		// clone1
909
910		write_commit_file(&clone1, "test.txt", "test", "commit1");
911		push_branch(
912			&clone1_dir.into(),
913			"origin",
914			"master",
915			false,
916			false,
917			None,
918			None,
919		)
920		.unwrap();
921		create_branch(&clone1_dir.into(), "foo").unwrap();
922		write_commit_file(&clone1, "test.txt", "test2", "commit2");
923		push_branch(
924			&clone1_dir.into(),
925			"origin",
926			"foo",
927			false,
928			false,
929			None,
930			None,
931		)
932		.unwrap();
933
934		// clone2
935
936		let (clone2_dir, _clone2) =
937			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
938
939		let clone2_dir = clone2_dir.path().to_str().unwrap();
940
941		let local_branches =
942			get_branches_info(&clone2_dir.into(), true).unwrap();
943
944		assert_eq!(local_branches.len(), 1);
945
946		let branches =
947			get_branches_info(&clone2_dir.into(), false).unwrap();
948
949		// checkout origin/foo
950		checkout_remote_branch(&clone2_dir.into(), &branches[1])
951			.unwrap();
952
953		assert_eq!(
954			get_branches_info(&clone2_dir.into(), true)
955				.unwrap()
956				.len(),
957			2
958		);
959
960		assert_eq!(
961			&get_branch_name(&clone2_dir.into()).unwrap(),
962			"foo"
963		);
964	}
965
966	#[test]
967	fn test_checkout_remote_branch_hirachical() {
968		let (r1_dir, _repo) = repo_init_bare().unwrap();
969
970		let (clone1_dir, clone1) =
971			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
972		let clone1_dir = clone1_dir.path().to_str().unwrap();
973
974		// clone1
975
976		let branch_name = "bar/foo";
977
978		write_commit_file(&clone1, "test.txt", "test", "commit1");
979		push_branch(
980			&clone1_dir.into(),
981			"origin",
982			"master",
983			false,
984			false,
985			None,
986			None,
987		)
988		.unwrap();
989		create_branch(&clone1_dir.into(), branch_name).unwrap();
990		write_commit_file(&clone1, "test.txt", "test2", "commit2");
991		push_branch(
992			&clone1_dir.into(),
993			"origin",
994			branch_name,
995			false,
996			false,
997			None,
998			None,
999		)
1000		.unwrap();
1001
1002		// clone2
1003
1004		let (clone2_dir, _clone2) =
1005			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
1006		let clone2_dir = clone2_dir.path().to_str().unwrap();
1007
1008		let branches =
1009			get_branches_info(&clone2_dir.into(), false).unwrap();
1010
1011		checkout_remote_branch(&clone2_dir.into(), &branches[1])
1012			.unwrap();
1013
1014		assert_eq!(
1015			&get_branch_name(&clone2_dir.into()).unwrap(),
1016			branch_name
1017		);
1018	}
1019
1020	#[test]
1021	fn test_has_tracking() {
1022		let (r1_dir, _repo) = repo_init_bare().unwrap();
1023
1024		let (clone1_dir, clone1) =
1025			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
1026		let clone1_dir = clone1_dir.path().to_str().unwrap();
1027
1028		// clone1
1029
1030		write_commit_file(&clone1, "test.txt", "test", "commit1");
1031		push_branch(
1032			&clone1_dir.into(),
1033			"origin",
1034			"master",
1035			false,
1036			false,
1037			None,
1038			None,
1039		)
1040		.unwrap();
1041		create_branch(&clone1_dir.into(), "foo").unwrap();
1042		write_commit_file(&clone1, "test.txt", "test2", "commit2");
1043		push_branch(
1044			&clone1_dir.into(),
1045			"origin",
1046			"foo",
1047			false,
1048			false,
1049			None,
1050			None,
1051		)
1052		.unwrap();
1053
1054		let branches_1 =
1055			get_branches_info(&clone1_dir.into(), false).unwrap();
1056
1057		assert!(branches_1[0].remote_details().unwrap().has_tracking);
1058		assert!(branches_1[1].remote_details().unwrap().has_tracking);
1059
1060		// clone2
1061
1062		let (clone2_dir, _clone2) =
1063			repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
1064
1065		let clone2_dir = clone2_dir.path().to_str().unwrap();
1066
1067		let branches_2 =
1068			get_branches_info(&clone2_dir.into(), false).unwrap();
1069
1070		assert!(
1071			!branches_2[0].remote_details().unwrap().has_tracking
1072		);
1073		assert!(
1074			!branches_2[1].remote_details().unwrap().has_tracking
1075		);
1076		assert!(branches_2[2].remote_details().unwrap().has_tracking);
1077	}
1078}