1use anyhow::Result;
2
3use super::git_output;
4use super::semver::{SemVer, parse_semver_tag};
5use super::status::{is_git_dirty, is_git_repo};
6use super::tags::get_first_commit;
7use crate::redact::redact_url_credentials;
8
9#[derive(Debug, Clone)]
10pub struct GitInfo {
11 pub tag: String,
12 pub commit: String,
13 pub short_commit: String,
14 pub branch: String,
15 pub dirty: bool,
16 pub semver: SemVer,
17 pub commit_date: String,
19 pub commit_timestamp: String,
21 pub previous_tag: Option<String>,
24 pub remote_url: String,
26 pub summary: String,
28 pub tag_subject: String,
30 pub tag_contents: String,
32 pub tag_body: String,
34 pub first_commit: Option<String>,
36}
37
38pub fn detect_git_info(tag: &str, skip_validate: bool) -> Result<GitInfo> {
49 if !is_git_repo() {
50 return Ok(GitInfo {
55 tag: tag.to_string(),
56 commit: String::new(),
57 short_commit: String::new(),
58 branch: String::new(),
59 dirty: false,
60 semver: SemVer {
61 major: 0,
62 minor: 0,
63 patch: 0,
64 prerelease: None,
65 build_metadata: None,
66 },
67 commit_date: String::new(),
68 commit_timestamp: String::new(),
69 previous_tag: None,
70 remote_url: String::new(),
71 summary: String::new(),
72 tag_subject: String::new(),
73 tag_contents: String::new(),
74 tag_body: String::new(),
75 first_commit: None,
76 });
77 }
78 let commit = git_output(&["rev-parse", "HEAD"])?;
79 let short_commit = git_output(&["rev-parse", "--short", "HEAD"])?;
80 let branch = git_output(&["rev-parse", "--abbrev-ref", "HEAD"]).unwrap_or_default();
81 let dirty = is_git_dirty();
82 let commit_date = git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%cI"])
83 .unwrap_or_default();
84 let commit_timestamp =
85 git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%at"])
86 .unwrap_or_default();
87 let remote_url_raw = match git_output(&["ls-remote", "--get-url"]) {
98 Ok(url) => url,
99 Err(e) => {
100 tracing::warn!(
101 error = %e,
102 "git ls-remote --get-url failed; remote_url left empty"
103 );
104 String::new()
105 }
106 };
107 let remote_url = redact_url_credentials(&remote_url_raw);
110 let summary = git_output(&[
111 "-c",
112 "log.showSignature=false",
113 "describe",
114 "--tags",
115 "--always",
116 "--dirty",
117 ])
118 .unwrap_or_default();
119
120 let tag_subject = git_output(&["tag", "-l", "--format=%(contents:subject)", tag])
122 .ok()
123 .filter(|s| !s.is_empty())
124 .unwrap_or_else(|| {
125 git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%s"])
126 .unwrap_or_default()
127 });
128 let tag_contents = git_output(&["tag", "-l", "--format=%(contents)", tag])
129 .ok()
130 .filter(|s| !s.is_empty())
131 .unwrap_or_else(|| {
132 git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%B"])
133 .unwrap_or_default()
134 });
135 let tag_body = git_output(&["tag", "-l", "--format=%(contents:body)", tag])
136 .ok()
137 .filter(|s| !s.is_empty())
138 .unwrap_or_else(|| {
139 git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%b"])
140 .unwrap_or_default()
141 });
142
143 let semver = match parse_semver_tag(tag) {
144 Ok(sv) => sv,
145 Err(e) => {
146 if skip_validate {
147 tracing::warn!("current tag is not semver, skipping validation");
148 SemVer {
149 major: 0,
150 minor: 0,
151 patch: 0,
152 prerelease: None,
153 build_metadata: None,
154 }
155 } else {
156 return Err(e);
157 }
158 }
159 };
160 let first_commit = get_first_commit().ok();
161 Ok(GitInfo {
162 tag: tag.to_string(),
163 commit,
164 short_commit,
165 branch,
166 dirty,
167 semver,
168 commit_date,
169 commit_timestamp,
170 previous_tag: None,
171 remote_url,
172 summary,
173 tag_subject,
174 tag_contents,
175 tag_body,
176 first_commit,
177 })
178}