use std::collections::{BinaryHeap, HashSet};
use std::io::Write;
use std::process::ExitCode;
use clap_complete::ArgValueCompleter;
use color_eyre::eyre::{Result, WrapErr as _, eyre};
use girt::display::DateFormat;
use girt::revision::Selection;
use girt::{Commit, HistoryLimits, IdentityDate, IdentityRef, ObjectId, Objects, ReadLimits};
use crate::common::{self, display_bytes};
use crate::{Context, complete};
#[derive(Debug, clap::Subcommand)]
pub enum Command {
Count {
#[arg(required = true, add = ArgValueCompleter::new(complete::revisions))]
revs: Vec<String>,
},
MergeBase {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
a: String,
#[arg(add = ArgValueCompleter::new(complete::revisions))]
b: String,
#[arg(long)]
all: bool,
},
Contains {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
ancestor: String,
#[arg(add = ArgValueCompleter::new(complete::revisions))]
descendant: String,
},
}
#[derive(Debug, clap::Args)]
pub struct LogArgs {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
revs: Vec<String>,
#[arg(short = 'n', long, value_name = "N")]
limit: Option<usize>,
#[arg(long)]
oneline: bool,
}
pub fn run(command: Command, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let objects = repository.objects()?;
let commit = |rev: &str| common::resolve_commit(&repository, &objects, rev);
let limits = HistoryLimits::trusted();
let mut out = common::stdout();
let code = match command {
Command::Count { revs } => {
let selection = common::select(&repository, &objects, &revs)?;
let hidden = hidden(&objects, &selection)?;
let ids = objects
.walk_with(&selection.include, limits)
.wrap_err("walking history")?;
let count = ids.iter().filter(|id| !hidden.contains(id)).count();
writeln!(out, "{count}")?;
ExitCode::SUCCESS
}
Command::MergeBase { a, b, all } => {
let bases = objects
.merge_bases_with(commit(&a)?, commit(&b)?, limits)
.wrap_err("finding merge bases")?;
let shown = if all {
&bases[..]
} else {
&bases[..bases.len().min(1)]
};
for base in shown {
writeln!(out, "{base}")?;
}
if bases.is_empty() {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
Command::Contains {
ancestor,
descendant,
} => {
let yes = objects
.is_ancestor_with(commit(&ancestor)?, commit(&descendant)?, limits)
.wrap_err("checking ancestry")?;
if !context.git {
writeln!(out, "{}", if yes { "yes" } else { "no" })?;
}
if yes {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
};
out.flush()?;
Ok(code)
}
pub fn log(args: LogArgs, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let objects = repository.objects()?;
let yellow = crate::color::style(context.color, crate::color::YELLOW);
let reset = crate::color::style(context.color, crate::color::RESET);
let selection = common::select(&repository, &objects, &args.revs)?;
let hidden = hidden(&objects, &selection)?;
let width = common::abbrev_len(&repository, &objects)?;
let short = |id: ObjectId| objects.abbreviate(id, width).map(|p| p.to_string());
let mut out = common::stdout();
let mut walk = DateOrder::new(&objects, &selection.include, hidden)?;
let mut first = true;
while let Some((id, commit)) = walk.next_commit()? {
if args.limit.is_some_and(|limit| walk.emitted > limit) {
break;
}
let message = commit.message();
if args.oneline {
let id = if context.git {
short(id)?
} else {
id.to_string()
};
if context.color {
writeln!(
out,
"{yellow}{id}{reset} {}",
display_bytes(&subject(message))
)?;
} else {
writeln!(out, "{id} {}", display_bytes(&subject(message)))?;
}
continue;
}
if !first {
writeln!(out)?;
}
first = false;
let author = commit
.author()
.wrap_err_with(|| format!("parsing author of {id}"))?
.ok_or_else(|| eyre!("commit {id} has no author"))?;
let date = author.date().unwrap_or(IdentityDate {
seconds: 0,
offset_minutes: 0,
});
if context.git {
writeln!(out, "commit {id}")?;
if commit.parents().len() > 1 {
let parents: Vec<_> = commit
.parents()
.iter()
.map(|p| short(*p))
.collect::<Result<_, _>>()?;
writeln!(out, "Merge: {}", parents.join(" "))?;
}
writeln!(out, "Author: {}", identity(author))?;
writeln!(out, "Date: {}", date.format(DateFormat::Default))?;
} else {
if context.color {
writeln!(out, "commit {yellow}{id}{reset}")?;
} else {
writeln!(out, "commit {id}")?;
}
for parent in commit.parents() {
writeln!(out, "parent {parent}")?;
}
writeln!(out, "author {}", identity(author))?;
writeln!(out, "date {}", date.format(DateFormat::Iso))?;
}
writeln!(out)?;
for line in message.trim_ascii_end().split(|&b| b == b'\n') {
if line.is_empty() && !context.git {
writeln!(out)?;
} else {
writeln!(out, " {}", display_bytes(line))?;
}
}
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}
struct DateOrder<'a> {
objects: &'a Objects,
heap: BinaryHeap<(i64, std::cmp::Reverse<usize>, ObjectId)>,
seen: HashSet<ObjectId>,
pushed: usize,
emitted: usize,
}
impl<'a> DateOrder<'a> {
fn new(objects: &'a Objects, roots: &[ObjectId], hidden: HashSet<ObjectId>) -> Result<Self> {
let mut walk = Self {
objects,
heap: BinaryHeap::new(),
seen: hidden,
pushed: 0,
emitted: 0,
};
for root in roots {
walk.push(*root)?;
}
Ok(walk)
}
fn push(&mut self, id: ObjectId) -> Result<()> {
if !self.seen.insert(id) {
return Ok(());
}
let commit = self
.objects
.read_commit_with(id, ReadLimits::trusted())
.wrap_err_with(|| format!("reading commit {id}"))?;
let date = commit
.committer()
.ok()
.flatten()
.and_then(|committer| committer.date().ok())
.map_or(0, |date| date.seconds);
self.heap.push((date, std::cmp::Reverse(self.pushed), id));
self.pushed += 1;
Ok(())
}
fn next_commit(&mut self) -> Result<Option<(ObjectId, Commit)>> {
let Some((_, _, id)) = self.heap.pop() else {
return Ok(None);
};
let commit = self
.objects
.read_commit_with(id, ReadLimits::trusted())
.wrap_err_with(|| format!("reading commit {id}"))?;
if !self.objects.shallow_roots().contains(id) {
for parent in commit.parents() {
self.push(*parent)?;
}
}
self.emitted += 1;
Ok(Some((id, commit)))
}
}
fn hidden(objects: &Objects, selection: &Selection) -> Result<HashSet<ObjectId>> {
if selection.exclude.is_empty() {
return Ok(HashSet::new());
}
let ids = objects
.walk_with(&selection.exclude, HistoryLimits::trusted())
.wrap_err("walking excluded history")?;
Ok(ids.into_iter().collect())
}
fn subject(message: &[u8]) -> Vec<u8> {
let mut lines = message
.split(|&b| b == b'\n')
.map(<[u8]>::trim_ascii)
.skip_while(|line| line.is_empty())
.take_while(|line| !line.is_empty());
let mut subject = lines.next().unwrap_or_default().to_vec();
for line in lines {
subject.push(b' ');
subject.extend_from_slice(line);
}
subject
}
fn identity(identity: IdentityRef<'_>) -> String {
format!(
"{} <{}>",
display_bytes(identity.name),
display_bytes(identity.email)
)
}