use git2::Repository;
use std::collections::HashMap;
use std::path::Path;
use std::sync::OnceLock;
use tera::{Function, Value};
pub struct GitInfo {
pub hash: Option<String>,
pub short_hash: Option<String>,
pub branch: Option<String>,
pub commit_timestamp: Option<i64>,
pub commit_message: Option<String>,
pub is_dirty: bool,
}
static GIT_INFO: OnceLock<GitInfo> = OnceLock::new();
pub fn get_git_info() -> &'static GitInfo {
GIT_INFO.get_or_init(|| {
let repo = match Repository::discover(".") {
Ok(r) => r,
Err(_) => {
return GitInfo {
hash: None,
short_hash: None,
branch: None,
commit_timestamp: None,
commit_message: None,
is_dirty: false,
};
}
};
let head = repo.head().ok();
let oid = head.as_ref().and_then(|h| h.target());
let commit = oid.and_then(|o| repo.find_commit(o).ok());
let hash = oid.map(|o| o.to_string());
let short_hash = hash.as_ref().map(|h| h[..7].to_string());
let branch = head.as_ref().and_then(|h| {
if h.is_branch() {
h.shorthand().map(|s| s.to_string())
} else {
None
}
});
let commit_timestamp = commit.as_ref().map(|c| c.time().seconds());
let commit_message = commit
.as_ref()
.and_then(|c| c.message().map(|m| m.trim().to_string()));
let is_dirty = repo.statuses(None).map(|s| !s.is_empty()).unwrap_or(false);
GitInfo {
hash,
short_hash,
branch,
commit_timestamp,
commit_message,
is_dirty,
}
})
}
pub fn make_git_hash() -> impl Function {
|_: &HashMap<String, Value>| -> tera::Result<Value> {
Ok(get_git_info()
.hash
.clone()
.map(Value::String)
.unwrap_or(Value::Null))
}
}
pub fn make_git_short_hash() -> impl Function {
|_: &HashMap<String, Value>| -> tera::Result<Value> {
Ok(get_git_info()
.short_hash
.clone()
.map(Value::String)
.unwrap_or(Value::Null))
}
}
pub fn make_git_branch() -> impl Function {
|_: &HashMap<String, Value>| -> tera::Result<Value> {
Ok(get_git_info()
.branch
.clone()
.map(Value::String)
.unwrap_or(Value::Null))
}
}
pub fn make_git_commit_timestamp() -> impl Function {
|_: &HashMap<String, Value>| -> tera::Result<Value> {
Ok(get_git_info()
.commit_timestamp
.map(|ts| Value::Number(ts.into()))
.unwrap_or(Value::Null))
}
}
pub fn make_git_commit_message() -> impl Function {
|_: &HashMap<String, Value>| -> tera::Result<Value> {
Ok(get_git_info()
.commit_message
.clone()
.map(Value::String)
.unwrap_or(Value::Null))
}
}
pub fn make_git_is_dirty() -> impl Function {
|_: &HashMap<String, Value>| -> tera::Result<Value> { Ok(Value::Bool(get_git_info().is_dirty)) }
}
pub fn register_git_functions(tera: &mut tera::Tera) {
tera.register_function("git_hash", make_git_hash());
tera.register_function("git_short_hash", make_git_short_hash());
tera.register_function("git_branch", make_git_branch());
tera.register_function("git_commit_timestamp", make_git_commit_timestamp());
tera.register_function("git_commit_message", make_git_commit_message());
tera.register_function("git_is_dirty", make_git_is_dirty());
}
#[derive(Debug, Clone, Default)]
pub struct FileGitInfo {
pub hash: Option<String>,
pub short_hash: Option<String>,
pub commit_timestamp: Option<i64>,
pub author: Option<String>,
pub is_dirty: bool,
}
pub fn get_file_git_info(path: &Path) -> FileGitInfo {
let repo = match Repository::discover(".") {
Ok(r) => r,
Err(_) => return FileGitInfo::default(),
};
let workdir = match repo.workdir() {
Some(w) => w,
None => return FileGitInfo::default(),
};
let relative_path = match path.strip_prefix(workdir) {
Ok(p) => p,
Err(_) => path,
};
let is_directory = path.is_dir();
let is_dirty = if is_directory {
repo.statuses(None)
.map(|statuses| {
statuses.iter().any(|s| {
s.path()
.map(|p| Path::new(p).starts_with(relative_path))
.unwrap_or(false)
})
})
.unwrap_or(false)
} else {
repo.status_file(relative_path)
.map(|s| !s.is_empty())
.unwrap_or(false)
};
let mut revwalk = match repo.revwalk() {
Ok(r) => r,
Err(_) => return FileGitInfo::default(),
};
if revwalk.push_head().is_err() {
return FileGitInfo::default();
}
for oid in revwalk.flatten() {
let commit = match repo.find_commit(oid) {
Ok(c) => c,
Err(_) => continue,
};
let tree = match commit.tree() {
Ok(t) => t,
Err(_) => continue,
};
let parent_tree = commit.parent(0).ok().and_then(|p| p.tree().ok());
let diff = match repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&tree), None) {
Ok(d) => d,
Err(_) => continue,
};
let path_changed = if is_directory {
diff.deltas().any(|delta| {
delta
.new_file()
.path()
.map(|p| p.starts_with(relative_path))
.unwrap_or(false)
|| delta
.old_file()
.path()
.map(|p| p.starts_with(relative_path))
.unwrap_or(false)
})
} else {
diff.deltas().any(|delta| {
delta
.new_file()
.path()
.map(|p| p == relative_path)
.unwrap_or(false)
|| delta
.old_file()
.path()
.map(|p| p == relative_path)
.unwrap_or(false)
})
};
if path_changed {
let hash = oid.to_string();
let short_hash = hash[..7].to_string();
let commit_timestamp = commit.time().seconds();
let author = commit.author().name().map(|s| s.to_string());
return FileGitInfo {
hash: Some(hash),
short_hash: Some(short_hash),
commit_timestamp: Some(commit_timestamp),
author,
is_dirty,
};
}
}
FileGitInfo {
is_dirty,
..Default::default()
}
}