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#[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 #[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 #[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 #[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 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 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 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 None => Ok(None),
102 }
103 }
104}
105
106impl<'a, C> GitBranch<'a, AppGit<'a, C>>
107where
108 C: AsRef<Utf8Path>,
109{
110 #[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}