rs_web/
git.rs

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/// Git info for a specific file
130#[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
139/// Get git info for a specific file (last commit that modified it)
140pub 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    // Get the relative path from repo root
147    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    // Check if file has uncommitted changes
158    let is_dirty = repo
159        .status_file(relative_path)
160        .map(|s| !s.is_empty())
161        .unwrap_or(false);
162
163    // Use git log to find the last commit that modified this file
164    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        // Check if this commit modified our file
180        let tree = match commit.tree() {
181            Ok(t) => t,
182            Err(_) => continue,
183        };
184
185        // Get parent tree (if exists)
186        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}