git_prole/git/
branch.rs

1use std::fmt::Debug;
2
3use camino::Utf8Path;
4use command_error::CommandExt;
5use command_error::OutputContext;
6use rustc_hash::FxHashSet;
7use tracing::instrument;
8use utf8_command::Utf8Output;
9
10use crate::AppGit;
11
12use super::BranchRef;
13use super::GitLike;
14use super::LocalBranchRef;
15
16/// Git methods for dealing with worktrees.
17#[repr(transparent)]
18pub struct GitBranch<'a, G>(&'a G);
19
20impl<G> Debug for GitBranch<'_, G>
21where
22    G: GitLike,
23{
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_tuple("GitBranch")
26            .field(&self.0.get_current_dir().as_ref())
27            .finish()
28    }
29}
30
31impl<'a, G> GitBranch<'a, G>
32where
33    G: GitLike,
34{
35    pub fn new(git: &'a G) -> Self {
36        Self(git)
37    }
38
39    /// Lists local branches.
40    #[instrument(level = "trace")]
41    pub fn list_local(&self) -> miette::Result<FxHashSet<LocalBranchRef>> {
42        self.0
43            .refs()
44            .for_each_ref(Some(&["refs/heads/**"]))?
45            .into_iter()
46            .map(LocalBranchRef::try_from)
47            .collect::<Result<FxHashSet<_>, _>>()
48    }
49
50    /// Lists local and remote branches.
51    #[instrument(level = "trace")]
52    pub fn list(&self) -> miette::Result<FxHashSet<BranchRef>> {
53        self.0
54            .refs()
55            .for_each_ref(Some(&["refs/heads/**", "refs/remotes/**"]))?
56            .into_iter()
57            .map(BranchRef::try_from)
58            .collect::<Result<FxHashSet<_>, _>>()
59    }
60
61    /// Does a local branch exist?
62    #[instrument(level = "trace")]
63    pub fn exists_local(&self, branch: &str) -> miette::Result<bool> {
64        Ok(self
65            .0
66            .command()
67            .args(["show-ref", "--quiet", "--branches", branch])
68            .output_checked_as(|context: OutputContext<Utf8Output>| {
69                Ok::<_, command_error::Error>(context.status().success())
70            })?)
71    }
72
73    /// Does the given branch name exist as a local branch, a unique remote branch, or neither?
74    pub fn local_or_remote(&self, branch: &str) -> miette::Result<Option<BranchRef>> {
75        if self.exists_local(branch)? {
76            Ok(Some(LocalBranchRef::new(branch.to_owned()).into()))
77        } else if let Some(remote) = self.0.remote().for_branch(branch)? {
78            // This is the implicit behavior documented in `git-worktree(1)`.
79            Ok(Some(remote.into()))
80        } else {
81            Ok(None)
82        }
83    }
84
85    pub fn current(&self) -> miette::Result<Option<LocalBranchRef>> {
86        match self.0.refs().rev_parse_symbolic_full_name("HEAD")? {
87            Some(ref_name) => Ok(Some(LocalBranchRef::try_from(ref_name)?)),
88            None => Ok(None),
89        }
90    }
91
92    /// Get the branch that a given branch is tracking.
93    pub fn upstream(&self, branch: &str) -> miette::Result<Option<BranchRef>> {
94        match self
95            .0
96            .refs()
97            .rev_parse_symbolic_full_name(&format!("{branch}@{{upstream}}"))?
98        {
99            Some(ref_name) => Ok(Some(BranchRef::try_from(ref_name)?)),
100            // NOTE: `branch` may not exist at all!
101            None => Ok(None),
102        }
103    }
104}
105
106impl<'a, C> GitBranch<'a, AppGit<'a, C>>
107where
108    C: AsRef<Utf8Path>,
109{
110    /// Get the user's preferred default branch.
111    #[instrument(level = "trace")]
112    pub fn preferred(&self) -> miette::Result<Option<BranchRef>> {
113        if let Some(default_remote) = self.0.remote().preferred()? {
114            return self
115                .0
116                .remote()
117                .default_branch(&default_remote)
118                .map(BranchRef::from)
119                .map(Some);
120        }
121
122        let preferred_branches = self.0.config.file.branch_names();
123        let all_branches = self.0.branch().list_local()?;
124        for preferred_branch in preferred_branches {
125            let preferred_branch = LocalBranchRef::new(preferred_branch);
126            if all_branches.contains(&preferred_branch) {
127                return Ok(Some(preferred_branch.into()));
128            } else if let Some(remote_branch) =
129                self.0.remote().for_branch(preferred_branch.branch_name())?
130            {
131                return Ok(Some(remote_branch.into()));
132            }
133        }
134
135        Ok(None)
136    }
137}