use crate::cli::UI;
use crate::ops::oplog;
use crate::ops::utils::short_oid;
use anyhow::{bail, Result};
use git2::{build::CheckoutBuilder, Repository};
use std::path::Path;
pub fn execute(path: &Path, target: &str, create_branch: bool, force: bool, ui: &UI) -> Result<()> {
let desc = if create_branch {
format!("checkout -b '{}'", target)
} else {
format!("checkout '{}'", target)
};
oplog::with_oplog(path, "checkout", &desc, || {
execute_inner(path, target, create_branch, force, ui)
})
}
fn checkout_opts(force: bool) -> Option<CheckoutBuilder<'static>> {
if force {
let mut cb = CheckoutBuilder::new();
cb.force();
Some(cb)
} else {
None
}
}
fn execute_inner(
path: &Path,
target: &str,
create_branch: bool,
force: bool,
ui: &UI,
) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
if target == "-" {
return switch_to_previous(&repo, force, ui);
}
if create_branch {
let head_commit = repo.head()?.peel_to_commit()?;
let branch = repo.branch(target, &head_commit, false)?;
let obj = branch.get().peel(git2::ObjectType::Commit)?;
let mut opts = checkout_opts(force);
repo.checkout_tree(&obj, opts.as_mut())?;
repo.set_head(&format!("refs/heads/{}", target))?;
ui.success(format!("Switched to a new branch '{}'", target));
return Ok(());
}
if let Ok(branch) = repo.find_branch(target, git2::BranchType::Local) {
let obj = branch.get().peel(git2::ObjectType::Commit)?;
let mut opts = checkout_opts(force);
repo.checkout_tree(&obj, opts.as_mut())?;
repo.set_head(&format!("refs/heads/{}", target))?;
ui.success(format!("Switched to branch '{}'", target));
return Ok(());
}
for remote_name in repo.remotes()?.iter().flatten() {
let remote_branch = format!("{}/{}", remote_name, target);
if let Ok(branch) = repo.find_branch(&remote_branch, git2::BranchType::Remote) {
let commit = branch.get().peel_to_commit()?;
let local_branch = repo.branch(target, &commit, false)?;
let obj = local_branch.get().peel(git2::ObjectType::Commit)?;
let mut opts = checkout_opts(force);
repo.checkout_tree(&obj, opts.as_mut())?;
repo.set_head(&format!("refs/heads/{}", target))?;
ui.success(format!(
"Switched to a new branch '{}' tracking '{}'",
target, remote_branch
));
return Ok(());
}
}
if let Ok(obj) = repo.revparse_single(target) {
let commit = obj.peel_to_commit()?;
let mut opts = checkout_opts(force);
repo.checkout_tree(commit.as_object(), opts.as_mut())?;
repo.set_head_detached(commit.id())?;
ui.info(format!(
"HEAD is now at {} {}",
short_oid(&commit.id()),
commit.summary().unwrap_or("")
));
return Ok(());
}
bail!("pathspec '{}' did not match any known refs", target);
}
fn switch_to_previous(repo: &Repository, force: bool, ui: &UI) -> Result<()> {
let reflog = repo.reflog("HEAD")?;
for i in 0..reflog.len() {
if let Some(entry) = reflog.get(i) {
let msg = entry.message().unwrap_or("");
if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
if let Some(from_branch) = rest.split(" to ").next() {
if repo
.find_branch(from_branch, git2::BranchType::Local)
.is_ok()
{
let branch = repo.find_branch(from_branch, git2::BranchType::Local)?;
let obj = branch.get().peel(git2::ObjectType::Commit)?;
let mut opts = checkout_opts(force);
repo.checkout_tree(&obj, opts.as_mut())?;
repo.set_head(&format!("refs/heads/{}", from_branch))?;
ui.success(format!("Switched to branch '{}'", from_branch));
return Ok(());
}
}
}
}
}
bail!("No previous branch found in reflog");
}