use std::{convert::Into, str};
use color_eyre::eyre::{bail, eyre, Result};
use git2::{
build::CheckoutBuilder, BranchType, ErrorCode, FetchOptions, Repository, SubmoduleUpdateOptions,
};
use log::{debug, trace};
use crate::tasks::git::{fetch::remote_callbacks, status::ensure_repo_clean};
pub(super) fn checkout_branch(
repo: &Repository,
branch_name: &str,
short_branch: &str,
upstream_remote_name: &str,
force: bool,
) -> Result<()> {
match repo.find_branch(short_branch, BranchType::Local) {
Ok(_) => (),
Err(e) if e.code() == ErrorCode::NotFound => {
debug!("Branch {short_branch} doesn't exist, creating it...",);
let branch_target = format!("{upstream_remote_name}/{short_branch}");
let branch_commit = repo
.find_branch(&branch_target, BranchType::Remote)?
.get()
.peel_to_commit()?;
let mut branch = repo.branch(short_branch, &branch_commit, false)?;
branch.set_upstream(Some(&branch_target))?;
}
Err(e) => return Err(e.into()),
};
match repo.head() {
Ok(current_head) => {
let current_head = current_head.name();
trace!("Current head is {current_head:?}, branch_name is {branch_name}",);
if !force && !repo.head_detached()? && current_head == Some(branch_name) {
debug!("Repo head is already {branch_name}, skipping branch checkout...",);
return Ok(());
}
}
Err(e) if e.code() == ErrorCode::UnbornBranch => {
trace!("No current head, continuing with branch checkout...");
}
Err(e) => {
bail!(e);
}
}
if !force {
ensure_repo_clean(repo)?;
}
debug!("Setting head to {branch_name}");
set_and_checkout_head(repo, branch_name, force)?;
Ok(())
}
pub(super) fn set_and_checkout_head(
repo: &Repository,
branch_name: &str,
force: bool,
) -> Result<()> {
if force {
debug!("Force checking out {branch_name}");
} else {
ensure_repo_clean(repo)?;
}
repo.set_head(branch_name)?;
force_checkout_head(repo)?;
Ok(())
}
fn force_checkout_head(repo: &Repository) -> Result<()> {
debug!("Force checking out HEAD.");
repo.checkout_head(Some(
CheckoutBuilder::new()
.force()
.allow_conflicts(true)
.recreate_missing(true)
.conflict_style_diff3(true)
.conflict_style_merge(true),
))?;
for mut submodule in repo.submodules()? {
trace!("Updating submodule: {:?}", submodule.name());
let mut checkout_builder = CheckoutBuilder::new();
checkout_builder
.force()
.allow_conflicts(true)
.recreate_missing(true)
.conflict_style_diff3(true)
.conflict_style_merge(true);
let mut count = 0;
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(remote_callbacks(&mut count));
submodule.update(
false,
Some(
SubmoduleUpdateOptions::new()
.fetch(fetch_options)
.checkout(checkout_builder),
),
)?;
let submodule_repo = submodule.open()?;
force_checkout_head(&submodule_repo)?;
}
Ok(())
}
pub(super) fn needs_checkout(repo: &Repository, branch_name: &str) -> bool {
match repo.head().map_err(Into::into).and_then(|h| {
h.shorthand()
.map(ToOwned::to_owned)
.ok_or_else(|| eyre!("Current branch is not valid UTF-8"))
}) {
Ok(current_branch) if current_branch == branch_name => {
debug!("Already on branch: '{branch_name}'");
false
}
Ok(current_branch) => {
debug!("Current branch: {current_branch}");
true
}
Err(e) => {
debug!("Current branch errored: {e}");
true
}
}
}