use eyre::{eyre, Result};
use std::process;
use git2::{self, Repository, RepositoryState, ResetType};
use process::Command;
use color_eyre::{eyre::Report, Section};
extern crate log;
pub fn wrapper_pick_and_clean(
repo: &Repository,
target_branch: &str,
onto_branch: &str,
force_new_branch: bool,
) -> Result<()> {
assure_workspace_is_clean(repo)
.suggestion("Consider auto-stashing your changes with --autostash.")
.suggestion("Running this again with RUST_LOG=debug provides more details.")?;
cherrypick_commit_onto_new_branch(repo, target_branch, onto_branch, force_new_branch)?;
remove_commit_from_head(repo)?;
Ok(())
}
pub fn cherrypick_commit_onto_new_branch(
repo: &Repository,
target_branch: &str,
onto_branch: &str,
force_new_branch: bool,
) -> Result<(), Report> {
let main_commit = repo
.revparse(onto_branch)?
.from()
.unwrap()
.peel_to_commit()?;
let new_branch = repo
.branch(target_branch, &main_commit, force_new_branch)
.suggestion("Consider using --force to overwrite the existing branch")?;
let fix_commit = repo.head()?.peel_to_commit()?;
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 use case.");
};
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(
new_branch.get().name(),
&fix_commit.author(),
&signature,
message,
&tree,
&[&main_commit],
)
.suggestion(
"You cannot provide an existing branch name. Choose a new branch name or run with '--force'.",
)?; log::debug!(
"Wrote quickfixed changes to new commit {} and new branch {}",
commit_oid,
target_branch
);
Ok(())
}
fn remove_commit_from_head(repo: &Repository) -> Result<(), Report> {
let head_1 = repo.head()?.peel_to_commit()?.parent(0)?;
repo.reset(head_1.as_object(), ResetType::Hard, None)?;
Ok(())
}
pub fn push_new_commit(repo: &Repository, branch: &str) -> Result<(), Report> {
let workdir = repo
.workdir()
.ok_or_else(|| eyre!("Could not get workdir"))?;
log::info!("Pushing new branch to origin.");
let status = Command::new("git")
.args(&["push", "--set-upstream", "origin", branch])
.current_dir(workdir)
.status()?;
if !status.success() {
Err(eyre!("Failed to run git push. {}", status))
} else {
log::info!("Git push succeeded");
Ok(())
}
}
pub fn assure_repo_in_normal_state(repo: &Repository) -> Result<()> {
let state = repo.state();
if state != RepositoryState::Clean {
return Err(eyre!(
"The repository is currently not in a clean state ({:?}).",
state
));
}
Ok(())
}
fn assure_workspace_is_clean(repo: &Repository) -> Result<()> {
let mut options = git2::StatusOptions::new();
options.include_ignored(false);
let statuses = repo.statuses(Some(&mut options))?;
for s in statuses.iter() {
log::warn!("Dirty: {:?} -- {:?}", s.path(), s.status());
}
let is_dirty = !statuses.is_empty();
if is_dirty {
Err(eyre!("The repository is dirty."))
} else {
Ok(())
}
}
fn get_default_branch_from_head(repo: &Repository) -> Result<String> {
let workdir = repo
.workdir()
.ok_or_else(|| eyre!("Could not get workdir"))?;
let output = Command::new("git")
.args(&["branch", "-rl", "*/HEAD"])
.current_dir(workdir)
.output()?;
let status = output.status;
if !status.success() {
return Err(eyre!("Failed to run git branch -rl. {}", status));
}
let output = String::from_utf8_lossy(&output.stdout);
let head_reference = output.split("->").nth(1);
if let Some(head_reference) = head_reference {
let head_reference = head_reference.trim();
Ok(head_reference.to_string())
} else {
Err(eyre!("Could not find a default branch."))
}
}
pub fn get_default_branch(repo: &Repository) -> Result<String, Report> {
match get_default_branch_from_head(repo) {
Ok(branch) => return Ok(branch),
Err(e) => {
log::debug!(
"Failed to get default branch from HEAD: {}. Using hardcoded branches",
e
);
}
}
for name in [
"origin/main",
"origin/master",
"origin/devel",
"origin/develop",
]
.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."))
}
pub fn stash(repo: &mut Repository) -> Result<bool> {
let signature = repo.signature()?;
let stashed = match repo.stash_save(&signature, "quickfix: auto-stash", None) {
Ok(stash) => {
log::debug!("Stashed to object {}", stash);
true
}
Err(e) => {
if e.code() == git2::ErrorCode::NotFound && e.class() == git2::ErrorClass::Stash {
log::debug!("Nothing to stash.");
false
} else {
return Err(eyre!("{}", e.message()));
}
}
};
Ok(stashed)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_default_branch() {
let repo = Repository::open(".").unwrap();
let branch = get_default_branch(&repo).unwrap();
assert_eq!(branch, "origin/main");
}
}