use std::path::Path;
use anyhow::{Context, Result};
use git2::{Delta, Repository};
#[derive(Debug, Default, Clone)]
pub struct ChangeSet {
pub added: Vec<String>,
pub modified: Vec<String>,
pub deleted: Vec<String>,
}
impl ChangeSet {
pub fn total(&self) -> usize {
self.added.len() + self.modified.len() + self.deleted.len()
}
pub fn to_parse(&self) -> impl Iterator<Item = &String> {
self.added.iter().chain(self.modified.iter())
}
pub fn to_drop(&self) -> impl Iterator<Item = &String> {
self.modified.iter().chain(self.deleted.iter())
}
}
fn path_string(p: Option<&Path>) -> Option<String> {
p.map(|p| p.to_string_lossy().replace('\\', "/"))
}
pub fn head_commit(repo_path: &Path) -> Result<String> {
let repo = Repository::open(repo_path)
.with_context(|| format!("open git repo at {}", repo_path.display()))?;
let commit = repo.head()?.peel_to_commit()?;
Ok(commit.id().to_string())
}
pub fn changed_files(repo_path: &Path, base_sha: &str) -> Result<ChangeSet> {
let repo = Repository::open(repo_path)
.with_context(|| format!("open git repo at {}", repo_path.display()))?;
let base_tree = repo
.revparse_single(base_sha)
.with_context(|| format!("resolve base commit {base_sha}"))?
.peel_to_commit()?
.tree()?;
let head_tree = repo.head()?.peel_to_commit()?.tree()?;
let mut opts = git2::DiffOptions::new();
let diff = repo.diff_tree_to_tree(Some(&base_tree), Some(&head_tree), Some(&mut opts))?;
let mut cs = ChangeSet::default();
for d in diff.deltas() {
let new_p = path_string(d.new_file().path());
let old_p = path_string(d.old_file().path());
match d.status() {
Delta::Added | Delta::Copied => cs.added.extend(new_p),
Delta::Deleted => cs.deleted.extend(old_p),
Delta::Modified | Delta::Typechange => cs.modified.extend(new_p),
Delta::Renamed => {
cs.deleted.extend(old_p);
cs.added.extend(new_p);
}
_ => {}
}
}
Ok(cs)
}
#[derive(Debug, Clone)]
pub struct CommitFiles {
pub sha: String,
pub subject: String,
pub added: Vec<String>,
pub changed: Vec<String>,
}
pub fn history(repo_path: &Path, limit: usize) -> Result<Vec<CommitFiles>> {
let repo = Repository::open(repo_path)
.with_context(|| format!("open git repo at {}", repo_path.display()))?;
let mut walk = repo.revwalk()?;
walk.push_head()?;
walk.simplify_first_parent()?;
let mut out = Vec::new();
for oid in walk.take(limit) {
let commit = repo.find_commit(oid?)?;
let tree = commit.tree()?;
let parent = commit.parents().next().map(|p| p.tree()).transpose()?;
let diff = repo.diff_tree_to_tree(parent.as_ref(), Some(&tree), None)?;
let mut c = CommitFiles {
sha: commit.id().to_string(),
subject: commit
.summary()
.ok()
.flatten()
.unwrap_or_default()
.to_string(),
added: Vec::new(),
changed: Vec::new(),
};
for d in diff.deltas() {
let Some(p) = path_string(d.new_file().path()) else {
continue;
};
if matches!(d.status(), Delta::Added | Delta::Copied | Delta::Renamed) {
c.added.push(p.clone());
}
if !matches!(d.status(), Delta::Deleted) {
c.changed.push(p);
}
}
out.push(c);
}
Ok(out)
}
pub type FileHunks = (String, Vec<(u32, u32)>);
pub fn working_changes(repo_path: &Path, base: &str) -> Result<Vec<FileHunks>> {
let repo = Repository::open(repo_path)
.with_context(|| format!("open git repo at {}", repo_path.display()))?;
let tree = repo.revparse_single(base)?.peel_to_tree()?;
let mut opts = git2::DiffOptions::new();
opts.include_untracked(true).recurse_untracked_dirs(true);
let diff = repo.diff_tree_to_workdir_with_index(Some(&tree), Some(&mut opts))?;
let out: std::cell::RefCell<Vec<FileHunks>> = Default::default();
diff.foreach(
&mut |d, _| {
if let Some(p) = path_string(d.new_file().path()) {
out.borrow_mut().push((p, Vec::new()));
}
true
},
None,
Some(&mut |d, h| {
let mut v = out.borrow_mut();
if let (Some(p), Some(last)) = (path_string(d.new_file().path()), v.last_mut()) {
if last.0 == p {
let start = h.new_start().max(1);
last.1.push((start, start + h.new_lines().max(1) - 1));
}
}
true
}),
None,
)?;
Ok(out.into_inner())
}