gitdataai/lib/git/bare.rs
2026-05-30 01:38:40 +08:00

135 lines
4.5 KiB
Rust

use std::path::PathBuf;
use crate::errors::GitResult;
#[derive(Clone)]
pub struct GitBare {
pub bare_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub struct LastCommitInfo {
pub message: String,
pub time: String,
pub author_name: String,
pub author_email: String,
}
impl GitBare {
pub fn gix_repo(&self) -> crate::errors::GitResult<gix::Repository> {
gix::open(&self.bare_dir).map_err(|e| {
crate::errors::GitError::Internal(format!(
"failed to open gix repository: {e}"
))
})
}
pub fn set_default_branch(&self, branch_name: &str) -> GitResult<()> {
let output = self.git_command_trusted(vec![
"symbolic-ref".to_string(),
"HEAD".to_string(),
format!("refs/heads/{}", branch_name),
])?;
if !output.success {
return Err(crate::errors::GitError::CommandFailed {
status_code: output.status_code,
stderr: output.stderr_lossy(),
});
}
Ok(())
}
pub fn last_commits_for_paths(&self, paths: &[String]) -> GitResult<Vec<Option<LastCommitInfo>>> {
use gix::traverse::commit::simple::CommitTimeOrder;
if paths.is_empty() {
return Ok(vec![]);
}
let repo = self.gix_repo()?;
let head_id = repo.head_id()
.map_err(|e| crate::errors::GitError::Internal(format!("no HEAD: {e}")))?;
let walk = repo.rev_walk(vec![head_id.detach()])
.sorting(gix::revision::walk::Sorting::ByCommitTime(CommitTimeOrder::NewestFirst))
.first_parent_only()
.all()
.map_err(|e| crate::errors::GitError::Internal(format!("rev_walk: {e}")))?;
let mut result: Vec<Option<LastCommitInfo>> = vec![None; paths.len()];
let mut remaining: std::collections::HashSet<usize> = (0..paths.len()).collect();
for walk_item in walk {
if remaining.is_empty() {
break;
}
let info = match walk_item {
Ok(i) => i,
Err(_) => continue,
};
let oid = info.id().detach();
let commit = match repo.find_commit(oid) {
Ok(c) => c,
Err(_) => continue,
};
let decoded = match commit.decode() {
Ok(d) => d,
Err(_) => continue,
};
let current_tree = match repo.find_tree(decoded.tree()) {
Ok(t) => t,
Err(_) => continue,
};
let parent_tree = decoded.parents().next().and_then(|pid| {
let hex = pid.to_hex().to_string();
let gid = gix::hash::ObjectId::from_hex(hex.as_bytes()).ok()?;
let pc = repo.find_commit(gid).ok()?;
let p_decoded = pc.decode().ok()?;
repo.find_tree(p_decoded.tree()).ok()
});
let mut diff_opts = gix::diff::Options::default();
diff_opts.track_path();
let changes = match repo.diff_tree_to_tree(
parent_tree.as_ref(),
Some(&current_tree),
Some(diff_opts),
) {
Ok(d) => d,
Err(_) => continue,
};
let changed: std::collections::HashSet<String> = changes.iter()
.map(|c| c.location().to_string())
.collect();
let is_root = parent_tree.is_none();
let author_sig = match decoded.author() {
Ok(s) => s,
Err(_) => continue,
};
let time = author_sig.time().unwrap_or(gix::date::Time { seconds: 0, offset: 0 });
let msg = commit.message_raw()
.map(|r| r.to_string().trim_end_matches('\n').to_string())
.unwrap_or_default();
let summary = msg.lines().next().unwrap_or("").to_string();
let matched: Vec<usize> = remaining.iter().copied().filter(|&idx| {
is_root || changed.contains(&paths[idx])
}).collect();
for idx in matched {
result[idx] = Some(LastCommitInfo {
message: summary.clone(),
time: format!("{}", time.seconds),
author_name: author_sig.name.to_string(),
author_email: author_sig.email.to_string(),
});
remaining.remove(&idx);
}
}
Ok(result)
}
}