use std::path::PathBuf;
use gix::bstr::{BStr, ByteSlice};
use crate::doc::Doc;
use crate::error::{usage, Result};
use crate::Ctx;
struct Rev {
short: String,
date: String,
author: String,
status: String,
summary: String,
}
pub fn run(ctx: &Ctx, id: &str) -> Result<()> {
let prj = ctx.open()?;
let (docs, _) = ctx.backend.load_docs(&prj);
let doc = prj.find(&docs, id)?;
let basename = doc
.path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| usage("git history: document has no file name"))?
.to_owned();
let repo = gix::discover(&prj.root).map_err(|e| usage(format!("git history: {e}")))?;
let revs = collect(&repo, basename.as_bytes().as_bstr(), id)
.map_err(|e| usage(format!("git history: {e}")))?;
if revs.is_empty() {
println!("{id}: no committed history found");
return Ok(());
}
let author_w = revs.iter().map(|r| r.author.len()).max().unwrap_or(0);
println!(
"History of {id} ({} revision{}, newest first):",
revs.len(),
if revs.len() == 1 { "" } else { "s" }
);
for r in &revs {
println!(
" {} {} {:<aw$} {:<14} {}",
r.short,
r.date,
r.author,
r.status,
r.summary,
aw = author_w
);
}
Ok(())
}
fn collect(repo: &gix::Repository, basename: &BStr, id: &str) -> anyhow::Result<Vec<Rev>> {
let mut revs = Vec::new();
let mut commit = repo.head_commit()?;
let mut last_oid: Option<gix::ObjectId> = None;
loop {
let tree = commit.tree()?;
if let Some(oid) = find_blob(repo, &tree, basename)? {
if last_oid.as_ref() != Some(&oid) {
last_oid = Some(oid);
let blob = repo.find_object(oid)?.into_blob();
let text = String::from_utf8_lossy(&blob.data);
let status = Doc::parse(PathBuf::from(format!("{id}.md")), &text)
.ok()
.and_then(|d| d.status().map(str::to_string))
.unwrap_or_else(|| "-".into());
revs.push(Rev {
short: commit.id().to_hex_with_len(8).to_string(),
date: commit.time()?.format(gix::date::time::format::SHORT)?,
author: commit.author()?.name.to_string(),
status,
summary: commit.message()?.summary().to_string(),
});
}
}
let parents: Vec<_> = commit.parent_ids().collect();
let Some(parent_id) = parents.first().copied() else {
break;
};
commit = repo.find_commit(parent_id)?;
}
Ok(revs)
}
fn find_blob(
repo: &gix::Repository,
tree: &gix::Tree,
basename: &BStr,
) -> anyhow::Result<Option<gix::ObjectId>> {
for entry in tree.iter() {
let entry = entry?;
if entry.mode().is_tree() {
let sub = repo.find_tree(entry.oid().to_owned())?;
if let Some(found) = find_blob(repo, &sub, basename)? {
return Ok(Some(found));
}
} else if entry.filename() == basename {
return Ok(Some(entry.oid().to_owned()));
}
}
Ok(None)
}