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