use git2::{Commit, Oid, Repository, Sort};
use git_whennes::error::{Error, Result};
use pico_args::Arguments;
const HELP: &str = "
Find the merge commit and pull request a commit came from.
Usage:
git-whennes <commit>
Options:
-h, --help Show this help message.
-b, --branch Branch to find where `commit` has been merged into it.
Default: init.defaultBranch falling back to 'master'";
const FORGE_OPTS: &str = "
-u, --url Print the URL linking to where `commit` was merged into
mainline.
-o, --open Open the URL linking to where `commit` was merged into
mainline.
-r, --remote Name of the remote to get the merge's URL.
Default: origin";
#[derive(Debug)]
struct Args {
sha: String,
main_branch: Option<String>,
#[cfg(feature = "forge")]
show_url: bool,
#[cfg(feature = "forge")]
open_url: bool,
#[cfg(feature = "forge")]
remote: String,
}
fn main() -> Result<()> {
env_logger::init();
let args = parse_args()?;
let repo = Repository::open_from_env()?;
let commit_to_check = get_commit(&repo, &args.sha)?;
if commit_to_check.parent_count() > 1 {
log::warn!("Merge commit");
show_commit(commit_to_check)?;
std::process::exit(1);
}
let main_branch = if let Some(main_branch) = args.main_branch {
main_branch
} else {
let config = repo.config()?;
let entry = config.get_entry("init.defaultbranch")?;
entry.value().unwrap_or("master").into()
};
log::debug!("Using '{}' as the main branch", main_branch);
let merge_commit = find_merge_simple(&repo, &args.sha, &main_branch)?;
if cfg!(feature = "forge") && (args.show_url || args.open_url) {
use git_whennes::forge;
let url = forge::get_url(&repo, &args.remote, merge_commit)?;
if args.show_url {
println!("{}", url);
}
if args.open_url {
let ret = open::that(&url).unwrap();
if ret.success() {
eprintln!("Error opening {}", url);
std::process::exit(ret.code().unwrap_or(0));
}
}
} else {
show_commit(merge_commit)?;
}
Ok(())
}
fn show_commit(commit: Commit) -> Result<()> {
let summary_line: &str = commit
.message()
.map(|msg| msg.lines().nth(0).unwrap_or_default())
.unwrap_or_default();
let obj = commit.as_object();
let short_id = obj.short_id()?;
println!("{} {}", short_id.as_str().unwrap(), summary_line);
Ok(())
}
fn get_commit<'repo>(repo: &'repo Repository, sha: &str) -> Result<Commit<'repo>> {
let obj = repo.revparse_single(sha)?;
let commit = obj.peel_to_commit()?;
Ok(commit)
}
fn find_merge_simple<'repo>(
repo: &'repo Repository,
from: &str,
to: &str,
) -> Result<Commit<'repo>> {
let from_commit = repo.find_commit(Oid::from_str(from)?)?;
let spec = format!("{}..{}", from, to);
let mut revwalk = repo.revwalk()?;
revwalk.simplify_first_parent()?;
revwalk.set_sorting(Sort::REVERSE)?;
revwalk.push_range(&spec)?;
for id in revwalk {
let id = id?;
let commit = repo.find_commit(id)?;
if commit.parent_count() > 1 && repo.graph_descendant_of(id, from_commit.id())? {
return Ok(commit);
}
}
Err(Error::NoMergeCommit { r#for: from.into() })
}
fn parse_args() -> Result<Args> {
let mut args = Arguments::from_env();
if args.contains(["-h", "--help"]) {
let help = if cfg!(feature = "forge") {
HELP.to_owned() + FORGE_OPTS
} else {
HELP.to_owned()
};
println!("{}", help);
std::process::exit(1);
}
Ok(Args {
main_branch: args.opt_value_from_str(["-b", "--branch"])?,
#[cfg(feature = "forge")]
show_url: args.contains(["-u", "--url"]),
#[cfg(feature = "forge")]
open_url: args.contains(["-o", "--open"]),
#[cfg(feature = "forge")]
remote: args
.opt_value_from_str(["-r", "--remote"])?
.unwrap_or("origin".into()),
sha: args.free_from_str()?,
})
}