git_prole/git/
branch.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use std::fmt::Debug;

use camino::Utf8Path;
use command_error::CommandExt;
use command_error::OutputContext;
use rustc_hash::FxHashSet;
use tracing::instrument;
use utf8_command::Utf8Output;

use crate::AppGit;

use super::BranchRef;
use super::GitLike;
use super::LocalBranchRef;

/// Git methods for dealing with worktrees.
#[repr(transparent)]
pub struct GitBranch<'a, G>(&'a G);

impl<G> Debug for GitBranch<'_, G>
where
    G: GitLike,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("GitBranch")
            .field(&self.0.get_current_dir().as_ref())
            .finish()
    }
}

impl<'a, G> GitBranch<'a, G>
where
    G: GitLike,
{
    pub fn new(git: &'a G) -> Self {
        Self(git)
    }

    /// Lists local branches.
    #[instrument(level = "trace")]
    pub fn list_local(&self) -> miette::Result<FxHashSet<LocalBranchRef>> {
        self.0
            .refs()
            .for_each_ref(Some(&["refs/heads/**"]))?
            .into_iter()
            .map(LocalBranchRef::try_from)
            .collect::<Result<FxHashSet<_>, _>>()
    }

    /// Lists local and remote branches.
    #[instrument(level = "trace")]
    pub fn list(&self) -> miette::Result<FxHashSet<BranchRef>> {
        self.0
            .refs()
            .for_each_ref(Some(&["refs/heads/**", "refs/remotes/**"]))?
            .into_iter()
            .map(BranchRef::try_from)
            .collect::<Result<FxHashSet<_>, _>>()
    }

    /// Does a local branch exist?
    #[instrument(level = "trace")]
    pub fn exists_local(&self, branch: &str) -> miette::Result<bool> {
        Ok(self
            .0
            .command()
            .args(["show-ref", "--quiet", "--branches", branch])
            .output_checked_as(|context: OutputContext<Utf8Output>| {
                Ok::<_, command_error::Error>(context.status().success())
            })?)
    }

    /// Does the given branch name exist as a local branch, a unique remote branch, or neither?
    pub fn local_or_remote(&self, branch: &str) -> miette::Result<Option<BranchRef>> {
        if self.exists_local(branch)? {
            Ok(Some(LocalBranchRef::new(branch.to_owned()).into()))
        } else if let Some(remote) = self.0.remote().for_branch(branch)? {
            // This is the implicit behavior documented in `git-worktree(1)`.
            Ok(Some(remote.into()))
        } else {
            Ok(None)
        }
    }

    pub fn current(&self) -> miette::Result<Option<LocalBranchRef>> {
        match self.0.refs().rev_parse_symbolic_full_name("HEAD")? {
            Some(ref_name) => Ok(Some(LocalBranchRef::try_from(ref_name)?)),
            None => Ok(None),
        }
    }

    /// Get the branch that a given branch is tracking.
    pub fn upstream(&self, branch: &str) -> miette::Result<Option<BranchRef>> {
        match self
            .0
            .refs()
            .rev_parse_symbolic_full_name(&format!("{branch}@{{upstream}}"))?
        {
            Some(ref_name) => Ok(Some(BranchRef::try_from(ref_name)?)),
            // NOTE: `branch` may not exist at all!
            None => Ok(None),
        }
    }
}

impl<'a, C> GitBranch<'a, AppGit<'a, C>>
where
    C: AsRef<Utf8Path>,
{
    /// Get the user's preferred default branch.
    #[instrument(level = "trace")]
    pub fn preferred(&self) -> miette::Result<Option<BranchRef>> {
        if let Some(default_remote) = self.0.remote().preferred()? {
            return self
                .0
                .remote()
                .default_branch(&default_remote)
                .map(BranchRef::from)
                .map(Some);
        }

        let preferred_branches = self.0.config.file.branch_names();
        let all_branches = self.0.branch().list_local()?;
        for preferred_branch in preferred_branches {
            let preferred_branch = LocalBranchRef::new(preferred_branch);
            if all_branches.contains(&preferred_branch) {
                return Ok(Some(preferred_branch.into()));
            } else if let Some(remote_branch) =
                self.0.remote().for_branch(preferred_branch.branch_name())?
            {
                return Ok(Some(remote_branch.into()));
            }
        }

        Ok(None)
    }
}