1use git2::Repository;
2use std::collections::HashMap;
3use std::path::Path;
4use std::sync::OnceLock;
5use tera::{Function, Value};
6
7struct GitInfo {
8 hash: Option<String>,
9 short_hash: Option<String>,
10 branch: Option<String>,
11 commit_date: Option<i64>,
12 commit_message: Option<String>,
13 is_dirty: bool,
14}
15
16static GIT_INFO: OnceLock<GitInfo> = OnceLock::new();
17
18fn get_git_info() -> &'static GitInfo {
19 GIT_INFO.get_or_init(|| {
20 let repo = match Repository::discover(".") {
21 Ok(r) => r,
22 Err(_) => {
23 return GitInfo {
24 hash: None,
25 short_hash: None,
26 branch: None,
27 commit_date: None,
28 commit_message: None,
29 is_dirty: false,
30 };
31 }
32 };
33
34 let head = repo.head().ok();
35 let oid = head.as_ref().and_then(|h| h.target());
36 let commit = oid.and_then(|o| repo.find_commit(o).ok());
37
38 let hash = oid.map(|o| o.to_string());
39 let short_hash = hash.as_ref().map(|h| h[..7].to_string());
40
41 let branch = head.as_ref().and_then(|h| {
42 if h.is_branch() {
43 h.shorthand().map(|s| s.to_string())
44 } else {
45 None
46 }
47 });
48
49 let commit_date = commit.as_ref().map(|c| c.time().seconds());
50 let commit_message = commit
51 .as_ref()
52 .and_then(|c| c.message().map(|m| m.trim().to_string()));
53
54 let is_dirty = repo.statuses(None).map(|s| !s.is_empty()).unwrap_or(false);
55
56 GitInfo {
57 hash,
58 short_hash,
59 branch,
60 commit_date,
61 commit_message,
62 is_dirty,
63 }
64 })
65}
66
67pub fn make_git_hash() -> impl Function {
68 |_: &HashMap<String, Value>| -> tera::Result<Value> {
69 Ok(get_git_info()
70 .hash
71 .clone()
72 .map(Value::String)
73 .unwrap_or(Value::Null))
74 }
75}
76
77pub fn make_git_short_hash() -> impl Function {
78 |_: &HashMap<String, Value>| -> tera::Result<Value> {
79 Ok(get_git_info()
80 .short_hash
81 .clone()
82 .map(Value::String)
83 .unwrap_or(Value::Null))
84 }
85}
86
87pub fn make_git_branch() -> impl Function {
88 |_: &HashMap<String, Value>| -> tera::Result<Value> {
89 Ok(get_git_info()
90 .branch
91 .clone()
92 .map(Value::String)
93 .unwrap_or(Value::Null))
94 }
95}
96
97pub fn make_git_commit_date() -> impl Function {
98 |_: &HashMap<String, Value>| -> tera::Result<Value> {
99 Ok(get_git_info()
100 .commit_date
101 .map(|ts| Value::Number(ts.into()))
102 .unwrap_or(Value::Null))
103 }
104}
105
106pub fn make_git_commit_message() -> impl Function {
107 |_: &HashMap<String, Value>| -> tera::Result<Value> {
108 Ok(get_git_info()
109 .commit_message
110 .clone()
111 .map(Value::String)
112 .unwrap_or(Value::Null))
113 }
114}
115
116pub fn make_git_is_dirty() -> impl Function {
117 |_: &HashMap<String, Value>| -> tera::Result<Value> { Ok(Value::Bool(get_git_info().is_dirty)) }
118}
119
120pub fn register_git_functions(tera: &mut tera::Tera) {
121 tera.register_function("git_hash", make_git_hash());
122 tera.register_function("git_short_hash", make_git_short_hash());
123 tera.register_function("git_branch", make_git_branch());
124 tera.register_function("git_commit_date", make_git_commit_date());
125 tera.register_function("git_commit_message", make_git_commit_message());
126 tera.register_function("git_is_dirty", make_git_is_dirty());
127}
128
129#[derive(Debug, Clone, Default)]
131pub struct FileGitInfo {
132 pub hash: Option<String>,
133 pub short_hash: Option<String>,
134 pub commit_date: Option<i64>,
135 pub author: Option<String>,
136 pub is_dirty: bool,
137}
138
139pub fn get_file_git_info(path: &Path) -> FileGitInfo {
141 let repo = match Repository::discover(".") {
142 Ok(r) => r,
143 Err(_) => return FileGitInfo::default(),
144 };
145
146 let workdir = match repo.workdir() {
148 Some(w) => w,
149 None => return FileGitInfo::default(),
150 };
151
152 let relative_path = match path.strip_prefix(workdir) {
153 Ok(p) => p,
154 Err(_) => path,
155 };
156
157 let is_dirty = repo
159 .status_file(relative_path)
160 .map(|s| !s.is_empty())
161 .unwrap_or(false);
162
163 let mut revwalk = match repo.revwalk() {
165 Ok(r) => r,
166 Err(_) => return FileGitInfo::default(),
167 };
168
169 if revwalk.push_head().is_err() {
170 return FileGitInfo::default();
171 }
172
173 for oid in revwalk.flatten() {
174 let commit = match repo.find_commit(oid) {
175 Ok(c) => c,
176 Err(_) => continue,
177 };
178
179 let tree = match commit.tree() {
181 Ok(t) => t,
182 Err(_) => continue,
183 };
184
185 let parent_tree = commit.parent(0).ok().and_then(|p| p.tree().ok());
187
188 let diff = match repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&tree), None) {
189 Ok(d) => d,
190 Err(_) => continue,
191 };
192
193 let file_changed = diff.deltas().any(|delta| {
194 delta
195 .new_file()
196 .path()
197 .map(|p| p == relative_path)
198 .unwrap_or(false)
199 || delta
200 .old_file()
201 .path()
202 .map(|p| p == relative_path)
203 .unwrap_or(false)
204 });
205
206 if file_changed {
207 let hash = oid.to_string();
208 let short_hash = hash[..7].to_string();
209 let commit_date = commit.time().seconds();
210 let author = commit.author().name().map(|s| s.to_string());
211
212 return FileGitInfo {
213 hash: Some(hash),
214 short_hash: Some(short_hash),
215 commit_date: Some(commit_date),
216 author,
217 is_dirty,
218 };
219 }
220 }
221
222 FileGitInfo {
223 is_dirty,
224 ..Default::default()
225 }
226}