use chrono::{DateTime, TimeZone, Utc};
use git2::{BranchType, Oid, Repository};
use std::collections::HashMap;
use crate::error::{Result, TuicrError};
#[derive(Debug, Clone)]
pub struct CommitInfo {
pub id: String,
pub short_id: String,
pub branch_name: Option<String>,
pub summary: String,
pub body: Option<String>,
pub author: String,
pub time: DateTime<Utc>,
}
fn parse_commit_message(message: &str) -> (String, Option<String>) {
let mut lines = message.lines();
let summary = lines.next().unwrap_or("(no message)").to_string();
let body_text: String = lines
.skip_while(|l| l.trim().is_empty())
.collect::<Vec<_>>()
.join("\n");
let body = if body_text.trim().is_empty() {
None
} else {
Some(body_text)
};
(summary, body)
}
fn get_branch_tip_names(repo: &Repository) -> HashMap<Oid, Vec<String>> {
let mut names_by_tip: HashMap<Oid, Vec<String>> = HashMap::new();
if let Ok(branches) = repo.branches(Some(BranchType::Local)) {
for (branch, _) in branches.flatten() {
let Some(target) = branch.get().target() else {
continue;
};
let Ok(Some(name)) = branch.name() else {
continue;
};
names_by_tip
.entry(target)
.or_default()
.push(name.to_string());
}
}
for names in names_by_tip.values_mut() {
names.sort_unstable();
}
names_by_tip
}
pub fn get_recent_commits(
repo: &Repository,
offset: usize,
limit: usize,
) -> Result<Vec<CommitInfo>> {
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let branch_tip_names = get_branch_tip_names(repo);
let mut commits = Vec::new();
for oid in revwalk.skip(offset).take(limit) {
let oid = oid?;
let commit = repo.find_commit(oid)?;
let id = oid.to_string();
let short_id = id[..7.min(id.len())].to_string();
let full_message = commit.message().unwrap_or("(no message)");
let (summary, body) = parse_commit_message(full_message);
let author = commit.author().name().unwrap_or("Unknown").to_string();
let branch_name = branch_tip_names
.get(&oid)
.and_then(|names| names.first().cloned());
let time = Utc
.timestamp_opt(commit.time().seconds(), 0)
.single()
.unwrap_or_else(Utc::now);
commits.push(CommitInfo {
id,
short_id,
branch_name,
summary,
body,
author,
time,
});
}
Ok(commits)
}
pub fn get_commits_info(repo: &Repository, ids: &[String]) -> Result<Vec<CommitInfo>> {
let branch_tip_names = get_branch_tip_names(repo);
let mut commits = Vec::new();
for id_str in ids {
let oid = Oid::from_str(id_str)
.map_err(|e| TuicrError::VcsCommand(format!("Invalid commit ID {}: {}", id_str, e)))?;
let commit = repo
.find_commit(oid)
.map_err(|e| TuicrError::VcsCommand(format!("Commit not found {}: {}", id_str, e)))?;
let id = oid.to_string();
let short_id = id[..7.min(id.len())].to_string();
let full_message = commit.message().unwrap_or("(no message)");
let (summary, body) = parse_commit_message(full_message);
let author = commit.author().name().unwrap_or("Unknown").to_string();
let branch_name = branch_tip_names
.get(&oid)
.and_then(|names| names.first().cloned());
let time = Utc
.timestamp_opt(commit.time().seconds(), 0)
.single()
.unwrap_or_else(Utc::now);
commits.push(CommitInfo {
id,
short_id,
branch_name,
summary,
body,
author,
time,
});
}
Ok(commits)
}
pub fn resolve_revisions(repo: &Repository, revisions: &str) -> Result<Vec<String>> {
let revspec = repo.revparse(revisions)?;
let mut commit_ids = if revspec.mode().contains(git2::RevparseMode::RANGE) {
let from = revspec.from().ok_or_else(|| {
TuicrError::VcsCommand("Invalid revision range: missing 'from'".into())
})?;
let to = revspec
.to()
.ok_or_else(|| TuicrError::VcsCommand("Invalid revision range: missing 'to'".into()))?;
let mut revwalk = repo.revwalk()?;
revwalk.push(to.id())?;
revwalk.hide(from.id())?;
revwalk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)?;
let mut ids = Vec::new();
for oid in revwalk {
ids.push(oid?.to_string());
}
ids
} else {
let obj = revspec
.from()
.ok_or_else(|| TuicrError::VcsCommand("Invalid revision expression".into()))?;
let commit = obj
.peel_to_commit()
.map_err(|e| TuicrError::VcsCommand(format!("Not a commit: {}", e)))?;
vec![commit.id().to_string()]
};
if commit_ids.is_empty() {
return Err(TuicrError::NoChanges);
}
commit_ids.reverse();
Ok(commit_ids)
}