135 lines
4.5 KiB
Rust
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(¤t_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)
|
|
}
|
|
}
|