use git2::{Commit, ErrorClass, ErrorCode, 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 {
log::debug!(
"Using '{}' as main branch, specified by flag",
main_branch
);
main_branch
} else {
let config = repo.config()?;
let entry = match config.get_entry("init.defaultbranch") {
Ok(entry) => {
entry.value().ok_or_else(|| {
log::warn!("Config value for `init.defaultbranch` is not valid UTF-8");
Error::InvalidUtf8
})?.to_string()
}
Err(e) => {
if let (ErrorClass::Config, ErrorCode::NotFound) =
(e.class(), e.code())
{
log::debug!("init.defaultbranch config entry not found, defaulting to master");
"master".to_string()
} else {
Err(e)?
}
}
};
entry
};
log::debug!(
"Finding merge commit for {} using strategy 'simple'",
args.sha
);
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().next().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)
.map_err(|e| Error::RevParseError {
sha: sha.to_string(),
source: e,
})?;
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_object =
repo.revparse_single(from)
.map_err(|e| Error::RevParseError {
sha: from.to_string(),
source: e,
})?;
let from_commit =
from_object.as_commit().ok_or(Error::ExpectedCommitError {
sha: from.to_string(),
})?;
let to_object =
repo.revparse_single(to).map_err(|e| Error::RevParseError {
sha: to.to_string(),
source: e,
})?;
let to_commit =
to_object.as_commit().ok_or(Error::ExpectedCommitError {
sha: to.to_string(),
})?;
let spec = format!("{}..{}", from_commit.id(), to_commit.id(),);
let mut revwalk = repo.revwalk()?;
revwalk.simplify_first_parent()?;
revwalk.set_sorting(Sort::REVERSE)?;
revwalk.push_range(&spec)?;
log::debug!("Walking revisions for spec {} in reverse order", spec);
for id in revwalk {
let id = id?;
let commit =
repo.find_commit(id).map_err(|e| Error::FindCommitError {
sha: id.to_string(),
source: e,
})?;
if commit.parent_count() > 1
&& repo
.graph_descendant_of(id, from_commit.id())
.map_err(|e| Error::GraphDescendantOfError {
commit: id.to_string(),
ancestory: from_commit.id().to_string(),
source: e,
})?
{
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_else(|| "origin".into()),
sha: args.free_from_str()?,
})
}