use crate::error::BumpError;
use gix::Repository;
use gix::bstr::ByteSlice;
use gix::objs::Tree;
use gix::objs::tree::EntryKind;
use gix::refs::transaction::PreviousValue;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn get_git_author(repo: &Repository) -> Result<(String, String), BumpError> {
let config = repo.config_snapshot();
let name = config
.string("user.name")
.map(|s| s.to_string())
.or_else(|| {
std::env::var("GIT_AUTHOR_NAME")
.or_else(|_| std::env::var("GIT_COMMITTER_NAME"))
.ok()
})
.ok_or_else(|| {
BumpError::GitError(
"read author".into(),
"user.name is not set in git config".into(),
)
})?;
let email = config
.string("user.email")
.map(|s| s.to_string())
.or_else(|| {
std::env::var("GIT_AUTHOR_EMAIL")
.or_else(|_| std::env::var("GIT_COMMITTER_EMAIL"))
.ok()
})
.ok_or_else(|| {
BumpError::GitError(
"read author".into(),
"user.email is not set in git config".into(),
)
})?;
Ok((name, email))
}
pub fn assert_clean_working_tree(repo: &Repository) -> Result<(), BumpError> {
let index = repo
.open_index()
.map_err(|e| BumpError::GitError("open_index".into(), e.to_string()))?;
let workdir = repo
.workdir()
.ok_or_else(|| BumpError::GitError("workdir".into(), "Bare repository".into()))?;
let mut has_changes = false;
for entry in index.entries() {
let rel_path = entry.path(&index);
let abs_path = workdir.join(rel_path.to_str_lossy().as_ref());
if let Ok(on_disk) = std::fs::read(&abs_path) {
let blob_id = repo
.write_blob(&on_disk)
.map_err(|e| BumpError::GitError("write_blob(check)".into(), e.to_string()))?;
if blob_id.detach() != entry.id {
has_changes = true;
break;
}
}
}
if has_changes {
return Err(BumpError::GitError(
"dirty check".into(),
"Git working directory is not clean. Commit or stash your changes first.".into(),
));
}
Ok(())
}
fn now_utc_time_str() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!("{} +0000", secs)
}
pub fn commit_files(
repo: &Repository,
files: &[String],
message: &str,
author_name: &str,
author_email: &str,
) -> Result<gix::ObjectId, BumpError> {
let head_commit = repo
.head_commit()
.map_err(|e| BumpError::GitError("head_commit".into(), e.to_string()))?;
let parent_id = head_commit.id;
let current_tree = head_commit
.tree()
.map_err(|e| BumpError::GitError("head_tree".into(), e.to_string()))?;
let mut tree_entries: Vec<gix::objs::tree::Entry> = current_tree
.iter()
.collect::<Result<Vec<_>, _>>()
.map_err(|e| BumpError::GitError("tree_iter".into(), e.to_string()))?
.into_iter()
.map(|e| gix::objs::tree::Entry {
mode: e.mode(),
filename: e.filename().to_owned(),
oid: e.oid().into(),
})
.collect();
let mut file_blobs: Vec<(gix::bstr::BString, gix::ObjectId)> = Vec::new();
for path_str in files {
let content = std::fs::read(path_str)?;
let blob_id = repo
.write_blob(&content)
.map_err(|e| BumpError::GitError("write_blob".into(), e.to_string()))?;
let file_name = std::path::Path::new(path_str)
.file_name()
.ok_or_else(|| {
BumpError::GitError("filename".into(), format!("Invalid path: {path_str}"))
})?
.as_encoded_bytes()
.as_bstr()
.to_owned();
if let Some(existing) = tree_entries.iter_mut().find(|e| e.filename == file_name) {
existing.oid = blob_id.detach();
} else {
tree_entries.push(gix::objs::tree::Entry {
mode: EntryKind::Blob.into(),
filename: file_name.clone(),
oid: blob_id.detach(),
});
}
file_blobs.push((file_name, blob_id.detach()));
}
tree_entries.sort_by(|a, b| a.filename.cmp(&b.filename));
let new_tree_id = repo
.write_object(&Tree {
entries: tree_entries,
})
.map_err(|e| BumpError::GitError("write_tree".into(), e.to_string()))?;
let time_str = now_utc_time_str();
let sig = gix::actor::SignatureRef {
name: author_name.as_bytes().as_bstr(),
email: author_email.as_bytes().as_bstr(),
time: &time_str,
};
let commit_id = repo
.commit_as(sig, sig, "HEAD", message, new_tree_id, [parent_id])
.map_err(|e| BumpError::GitError("commit_as".into(), e.to_string()))?;
let mut index = repo
.open_index()
.map_err(|e| BumpError::GitError("open_index".into(), e.to_string()))?;
for (file_name, blob_id) in &file_blobs {
for (entry, path) in index.entries_mut_with_paths() {
if path == file_name.as_bstr() {
entry.id = *blob_id;
break;
}
}
}
index
.write(gix::index::write::Options::default())
.map_err(|e| BumpError::GitError("write_index".into(), e.to_string()))?;
Ok(commit_id.detach())
}
pub fn create_tag(
repo: &Repository,
tag_name: &str,
commit_id: gix::ObjectId,
) -> Result<(), BumpError> {
repo.tag_reference(tag_name, commit_id, PreviousValue::Any)
.map_err(|e| BumpError::GitError("tag_reference".into(), e.to_string()))?;
Ok(())
}