1use anyhow::Result;
2
3use super::git_output;
4use super::remote::strip_url_credentials;
5use super::semver::{SemVer, parse_semver_tag};
6use super::status::{is_git_dirty, is_git_repo};
7use super::tags::get_first_commit;
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 = strip_url_credentials(&remote_url_raw);
109 let summary = git_output(&[
110 "-c",
111 "log.showSignature=false",
112 "describe",
113 "--tags",
114 "--always",
115 "--dirty",
116 ])
117 .unwrap_or_default();
118
119 let tag_subject = git_output(&["tag", "-l", "--format=%(contents:subject)", tag])
121 .ok()
122 .filter(|s| !s.is_empty())
123 .unwrap_or_else(|| {
124 git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%s"])
125 .unwrap_or_default()
126 });
127 let tag_contents = git_output(&["tag", "-l", "--format=%(contents)", tag])
128 .ok()
129 .filter(|s| !s.is_empty())
130 .unwrap_or_else(|| {
131 git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%B"])
132 .unwrap_or_default()
133 });
134 let tag_body = git_output(&["tag", "-l", "--format=%(contents:body)", tag])
135 .ok()
136 .filter(|s| !s.is_empty())
137 .unwrap_or_else(|| {
138 git_output(&["-c", "log.showSignature=false", "log", "-1", "--format=%b"])
139 .unwrap_or_default()
140 });
141
142 let semver = match parse_semver_tag(tag) {
143 Ok(sv) => sv,
144 Err(e) => {
145 if skip_validate {
146 eprintln!("WARNING: current tag is not semver, skipping validation");
147 SemVer {
148 major: 0,
149 minor: 0,
150 patch: 0,
151 prerelease: None,
152 build_metadata: None,
153 }
154 } else {
155 return Err(e);
156 }
157 }
158 };
159 let first_commit = get_first_commit().ok();
160 Ok(GitInfo {
161 tag: tag.to_string(),
162 commit,
163 short_commit,
164 branch,
165 dirty,
166 semver,
167 commit_date,
168 commit_timestamp,
169 previous_tag: None,
170 remote_url,
171 summary,
172 tag_subject,
173 tag_contents,
174 tag_body,
175 first_commit,
176 })
177}