use std::process;
use git2::{self, Repository, ResetType};
use process::Command;
use color_eyre::{eyre::Report, eyre::Result, Section};
use eyre::eyre;
use structopt::StructOpt;
extern crate log;
fn main() -> Result<()> {
color_eyre::install()?;
env_logger::init();
run()?;
Ok(())
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "git-quickfix",
about = "Apply patches directly to the main branch.",
verbatim_doc_comment
)]
struct Opt {
branch: String,
#[structopt(short = "m", long = "message", help = "Commit message")]
message: Option<String>,
#[structopt(
long = "push",
short = "u",
help = "Push the newly crated branch to origin."
)]
push: bool,
#[structopt(
long = "keep",
short = "k",
help = "Keep the new quickfix commit on the current branch."
)]
keep: bool,
#[structopt(
help = "The branch to apply the patch onto. Defaults to the default branch on origin (e.g. origin/main).",
long = "onto",
short = "o"
)]
onto: Option<String>,
}
fn run() -> Result<(), Report> {
let opts = Opt::from_args();
let repo = Repository::open_from_env()?;
let onto_branch = match opts.onto {
Some(b) => b,
None => {
get_default_branch(&repo).suggestion("Manually set the target branch with `--onto`.")?
}
};
let fix_commit = repo.head()?.peel_to_commit()?;
let main_commit = repo
.revparse(&onto_branch)?
.from()
.unwrap()
.peel_to_commit()?;
let mut index = repo.cherrypick_commit(&fix_commit, &main_commit, 0, None)?;
let tree_oid = index.write_tree_to(&repo)?;
let tree = repo.find_tree(tree_oid)?;
let signature = repo.signature()?;
let message = fix_commit
.message_raw()
.ok_or_else(|| eyre!("Could not read the commit message."))
.suggestion("Make sure the commit message contains only UTF-8 characters or try to manually cherry-pick the commit.")?;
let commit_oid = repo
.commit(
Some(&format!("refs/heads/{}", opts.branch)),
&fix_commit.author(),
&signature,
message,
&tree,
&[&main_commit],
)
.suggestion(
"You cannot provide an existing branch name. Choose a new branch name or run with.",
)?; log::debug!(
"Wrote quickfixed changes to new commit {} and new branch {}",
commit_oid,
opts.branch
);
if !opts.keep {
if fix_commit.parent_count() != 1 {
return Err(eyre!("Only works with non-merge commits"))
.suggestion("Quickfixing a merge commit is not supported. If you meant to do this please file a ticket with your usecase.");
};
let head_1 = fix_commit.parent(0)?;
repo.reset(&head_1.as_object(), ResetType::Hard, None)?;
}
if opts.push {
log::info!("Pushing new branch to origin.");
let status = Command::new("git")
.args(&["push", "--set-upstream", "origin", &opts.branch])
.status()?;
if !status.success() {
eyre!("Failed to run git push. {}", status);
} else {
log::info!("Git push succeeded");
}
}
Ok(())
}
fn get_default_branch(repo: &Repository) -> Result<String, Report> {
for name in ["origin/main", "origin/master", "origin/devel"].iter() {
match repo.resolve_reference_from_short_name(name) {
Ok(_) => {
log::debug!("Found {} as the default remote branch. A bit hacky -- wrong results certainly possible.", name);
return Ok(name.to_string());
}
Err(_) => continue,
}
}
Err(eyre!("Could not find remote default branch."))
}