#![allow(unknown_lints)]
#![allow(bare_trait_objects)]
extern crate clap;
extern crate git2;
extern crate libc;
extern crate pcre2;
extern crate scutiger_core;
#[cfg(test)]
extern crate tempfile;
mod libgit2;
mod pager;
#[cfg(test)]
pub mod fixtures;
use clap::{App, AppSettings, Arg, ArgMatches};
use git2::{Object, Reference, Repository, Time};
use scutiger_core::errors::{Error, ExitStatus};
use std::io;
use std::process;
enum SortKind {
Visited,
Committed,
Authored,
}
struct Program<'p> {
repo: &'p Repository,
kind: SortKind,
}
impl<'p> Program<'p> {
fn new(repo: &'p Repository, kind: SortKind) -> Self {
Program { repo, kind }
}
fn stringify<'b>(obj: Option<(Object<'b>, Option<Reference<'b>>)>) -> Option<String> {
match obj {
Some((_, Some(r2))) => Some(r2.name()?.into()),
Some((r1, None)) => Some(format!("{}", r1.id())),
None => None,
}
}
fn commit_date_for(r: &Reference) -> Option<Time> {
let commit = r.peel_to_commit().ok()?;
let time = commit.committer().when();
Some(time)
}
fn author_date_for(r: &Reference) -> Option<Time> {
let commit = r.peel_to_commit().ok()?;
let time = commit.author().when();
Some(time)
}
fn run<'a>(&self, repo: &'a Repository) -> Result<Box<Iterator<Item = String> + 'a>, Error> {
match self.kind {
SortKind::Visited => {
let chain: Vec<String> = vec!["HEAD".to_string()];
let range = 1..;
let iter = chain
.into_iter()
.chain(range.map(|i| format!("@{{-{}}}", i)));
Ok(Box::new(
iter.map(move |rev| Self::stringify(repo.revparse_ext(&rev).ok()))
.take_while(Option::is_some)
.map(Option::unwrap),
))
}
SortKind::Committed => {
let mut refs: Vec<_> = repo
.references()?
.filter_map(Result::ok)
.map(|r| (r.target(), Self::commit_date_for(&r), r))
.filter(|&(oid, time, _)| oid.is_some() && time.is_some())
.collect();
refs.sort_by(|a, b| b.1.cmp(&a.1));
Ok(Box::new(
refs.into_iter()
.filter_map(|(_, _, r)| Some(r.name()?.to_string())),
))
}
SortKind::Authored => {
let mut refs: Vec<_> = repo
.references()?
.filter_map(Result::ok)
.map(|r| (r.target(), Self::author_date_for(&r), r))
.filter(|&(oid, time, _)| oid.is_some() && time.is_some())
.collect();
refs.sort_by(|a, b| b.1.cmp(&a.1));
Ok(Box::new(
refs.into_iter()
.filter_map(|(_, _, r)| Some(r.name()?.to_string())),
))
}
}
}
fn main<O: io::Write, E: io::Write>(&self, output: &mut O, error: &mut E) -> i32 {
match self.run(self.repo) {
Ok(iter) => {
for s in iter {
writeln!(output, "{}", s).unwrap();
}
ExitStatus::Success as i32
}
Err(e) => {
writeln!(error, "{}", e).unwrap();
e.exit_status() as i32
}
}
}
}
fn parse_options<'a>() -> App<'a, 'a> {
App::new("git-recent-refs")
.setting(AppSettings::AllowMissingPositional)
.about("Find a commit based on commit message")
.arg(
Arg::with_name("sort")
.long("sort")
.takes_value(true)
.possible_values(&["visitdate", "committerdate", "authordate"])
.help("Sort refs by the given type"),
)
}
fn sort_type(kind: &str) -> Result<SortKind, Error> {
match kind {
"visitdate" => Ok(SortKind::Visited),
"authordate" => Ok(SortKind::Authored),
"committerdate" => Ok(SortKind::Committed),
_ => unimplemented!(),
}
}
fn program<'a>(repo: &'a Repository, matches: &'a ArgMatches) -> Program<'a> {
Program::new(
repo,
sort_type(matches.value_of("sort").unwrap_or("visitdate")).unwrap(),
)
}
fn repo() -> Repository {
let repo = git2::Repository::discover(".");
match repo {
Ok(r) => r,
Err(e) => {
eprintln!("{}", e);
process::exit(4);
}
}
}
fn setup() {
libgit2::init();
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
fn main() {
setup();
let app = parse_options();
let matches = app.get_matches();
let repo = repo();
let prog = program(&repo, &matches);
let mut pager = pager::Pager::new(&repo).expect("unable to spawn pager");
process::exit(prog.main(&mut pager.stdout(), &mut io::stderr()));
}
#[cfg(test)]
mod tests {
use super::fixtures::TestRepository;
use super::{Error, Program, SortKind};
use git2::build::CheckoutBuilder;
use git2::Oid;
fn run<'a>(
fixtures: &'a TestRepository,
sorttype: SortKind,
) -> Result<Box<Iterator<Item = String> + 'a>, Error> {
Program::new(&fixtures.repo, sorttype).run(&fixtures.repo)
}
fn oid(hex: &str) -> Oid {
Oid::from_str(hex).unwrap()
}
fn checkout_branch(fixtures: &TestRepository, branch: &str) {
if branch.len() == 40 {
fixtures.repo.set_head_detached(oid(branch)).unwrap();
} else {
let full_branch = format!("refs/heads/{}", branch);
fixtures.repo.set_head(&full_branch).unwrap();
};
let mut cb = CheckoutBuilder::new();
fixtures.repo.checkout_head(Some(cb.force())).unwrap();
}
#[test]
fn visited_results() {
let fixtures = TestRepository::new();
let rev = "4cf979cf194179a3b9dc1d65cc4dc29cfed32614";
checkout_branch(&fixtures, "fixup");
checkout_branch(&fixtures, rev);
checkout_branch(&fixtures, "branch");
checkout_branch(&fixtures, "dev");
assert_eq!(
run(&fixtures, SortKind::Visited)
.unwrap()
.collect::<Vec<_>>(),
vec![
"refs/heads/dev",
"refs/heads/branch",
rev,
"refs/heads/fixup",
"refs/heads/dev"
],
);
}
#[test]
fn committer_results() {
let fixtures = TestRepository::new();
assert_eq!(
run(&fixtures, SortKind::Committed)
.unwrap()
.collect::<Vec<_>>(),
vec![
"refs/heads/merge3",
"refs/heads/merge2",
"refs/heads/merge1",
"refs/heads/dev",
"refs/heads/fixup",
"refs/heads/branch"
],
);
}
}