git_prole/git/refs/
mod.rs1use std::fmt::Debug;
2use std::str::FromStr;
3
4use command_error::CommandExt;
5use command_error::OutputContext;
6use miette::miette;
7use miette::Context;
8use tap::Tap;
9use tracing::instrument;
10use utf8_command::Utf8Output;
11
12use super::commit_hash::CommitHash;
13use super::commitish::ResolvedCommitish;
14use super::head_state::HeadKind;
15use super::GitLike;
16
17mod branch;
18mod local_branch;
19mod name;
20mod remote_branch;
21
22pub use branch::BranchRef;
23pub use local_branch::LocalBranchRef;
24pub use name::Ref;
25pub use remote_branch::RemoteBranchRef;
26
27#[repr(transparent)]
29pub struct GitRefs<'a, G>(&'a G);
30
31impl<G> Debug for GitRefs<'_, G>
32where
33 G: GitLike,
34{
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_tuple("GitRefs")
37 .field(&self.0.get_current_dir().as_ref())
38 .finish()
39 }
40}
41
42impl<'a, G> GitRefs<'a, G>
43where
44 G: GitLike,
45{
46 pub fn new(git: &'a G) -> Self {
47 Self(git)
48 }
49
50 #[expect(dead_code)] pub(crate) fn commit_message(&self, commit: &str) -> miette::Result<String> {
52 Ok(self
53 .0
54 .command()
55 .args(["show", "--no-patch", "--format=%B", commit])
56 .output_checked_utf8()
57 .wrap_err("Failed to get commit message")?
58 .stdout)
59 }
60
61 #[instrument(level = "trace")]
63 pub fn get_head(&self) -> miette::Result<CommitHash> {
64 Ok(self.parse("HEAD")?.expect("HEAD always exists"))
65 }
66
67 #[instrument(level = "trace")]
69 pub fn parse(&self, commitish: &str) -> miette::Result<Option<CommitHash>> {
70 Ok(self
71 .0
72 .as_git()
73 .rev_parse_command()
74 .args(["--verify", "--quiet", "--end-of-options", commitish])
75 .output_checked_as(|context: OutputContext<Utf8Output>| {
76 if context.status().success() {
77 Ok::<_, command_error::Error>(Some(CommitHash::new(
78 context.output().stdout.trim().to_owned(),
79 )))
80 } else {
81 Ok(None)
82 }
83 })?)
84 }
85
86 #[instrument(level = "trace")]
88 pub fn rev_parse_symbolic_full_name(&self, commitish: &str) -> miette::Result<Option<Ref>> {
89 Ok(self
90 .0
91 .as_git()
92 .rev_parse_command()
93 .args([
94 "--symbolic-full-name",
95 "--verify",
96 "--quiet",
97 "--end-of-options",
98 commitish,
99 ])
100 .output_checked_as(|context: OutputContext<Utf8Output>| {
101 if context.status().success() {
102 let trimmed = context.output().stdout.trim();
103 if trimmed.is_empty() {
104 Ok(None)
105 } else {
106 match Ref::from_str(trimmed) {
107 Ok(parsed) => Ok(Some(parsed)),
108 Err(err) => {
109 if commitish.ends_with("HEAD") && trimmed == commitish {
110 tracing::debug!("{commitish} is detached");
111 Ok(None)
112 } else {
113 Err(context.error_msg(err))
114 }
115 }
116 }
117 }
118 } else {
119 Ok(None)
120 }
121 })?)
122 }
123
124 #[instrument(level = "trace")]
126 pub fn resolve_commitish(&self, commitish: &str) -> miette::Result<ResolvedCommitish> {
127 match self.rev_parse_symbolic_full_name(commitish)? {
128 Some(ref_name) => Ok(ResolvedCommitish::Ref(ref_name)),
129 None => Ok(ResolvedCommitish::Commit(
130 self.parse(commitish)?.ok_or_else(|| {
131 miette!("Commitish could not be resolved to a ref or commit hash: {commitish}")
132 })?,
133 )),
134 }
135 }
136
137 #[instrument(level = "trace")]
138 pub fn is_head_detached(&self) -> miette::Result<bool> {
139 let output = self
140 .0
141 .command()
142 .args(["symbolic-ref", "--quiet", "HEAD"])
143 .output_checked_with_utf8::<String>(|_output| Ok(()))?;
144
145 Ok(!output.status.success())
146 }
147
148 #[instrument(level = "trace")]
150 pub fn head_kind(&self) -> miette::Result<HeadKind> {
151 Ok(if self.is_head_detached()? {
152 HeadKind::Detached(self.get_head()?)
153 } else {
154 HeadKind::Branch(
155 LocalBranchRef::try_from(
156 self.rev_parse_symbolic_full_name("HEAD")?
157 .expect("Non-detached HEAD should always be a valid ref"),
158 )
159 .expect("Non-detached HEAD should always be a local branch"),
160 )
161 })
162 }
163
164 #[instrument(level = "trace")]
165 pub fn for_each_ref(&self, globs: Option<&[&str]>) -> miette::Result<Vec<Ref>> {
166 self.0
167 .command()
168 .args(["for-each-ref", "--format=%(refname)"])
169 .tap_mut(|c| {
170 globs.map(|globs| c.args(globs));
171 })
172 .output_checked_utf8()?
173 .stdout
174 .lines()
175 .map(Ref::from_str)
176 .collect()
177 }
178}