use std::collections::HashSet;
use anyhow::{Result, bail};
use git2::{BranchType, Repository, StatusOptions};
use crate::core::msg;
use crate::core::repo;
use crate::git;
pub fn run(branch: Option<String>) -> Result<()> {
let repo = repo::open_repo()?;
let workdir = repo::require_workdir(&repo, "switch")?;
check_clean(&repo)?;
let (branch_name, is_remote) = match branch {
Some(arg) => resolve_branch(&repo, &arg)?,
None => pick_branch(&repo)?,
};
if is_remote {
git::branch_switch_detach(workdir, &branch_name)?;
msg::success(&format!("Detached HEAD at `{}`", branch_name));
} else {
git::branch_switch(workdir, &branch_name)?;
msg::success(&format!("Switched to `{}`", branch_name));
}
Ok(())
}
fn check_clean(repo: &Repository) -> Result<()> {
let mut opts = StatusOptions::new();
opts.include_untracked(false);
let statuses = repo.statuses(Some(&mut opts))?;
if !statuses.is_empty() {
bail!(
"Working tree has uncommitted changes.\n\
Stash or commit your changes before switching branches."
);
}
Ok(())
}
fn resolve_branch(repo: &Repository, arg: &str) -> Result<(String, bool)> {
if repo.find_branch(arg, BranchType::Local).is_ok() {
return Ok((arg.to_string(), false));
}
if repo.find_branch(arg, BranchType::Remote).is_ok() {
return Ok((arg.to_string(), true));
}
if let Ok(repo::Target::Branch(name)) =
repo::resolve_arg(repo, arg, &[repo::TargetKind::Branch])
{
return Ok((name, false));
}
bail!("Branch '{}' not found", arg)
}
fn pick_branch(repo: &Repository) -> Result<(String, bool)> {
let current = repo
.head()
.ok()
.and_then(|h| h.shorthand().map(|s| s.to_string()));
let mut items: Vec<(String, bool)> = Vec::new();
let mut local_names: HashSet<String> = HashSet::new();
for branch_result in repo.branches(Some(BranchType::Local))? {
let (branch, _) = branch_result?;
if let Some(name) = branch.name()? {
local_names.insert(name.to_string());
if Some(name) != current.as_deref() {
items.push((name.to_string(), false));
}
}
}
for branch_result in repo.branches(Some(BranchType::Remote))? {
let (branch, _) = branch_result?;
if let Some(name) = branch.name()? {
if name.ends_with("/HEAD") {
continue;
}
let short_name = repo::upstream_local_branch(name);
if !local_names.contains(&short_name) {
items.push((name.to_string(), true));
}
}
}
if items.is_empty() {
bail!("No branches available to switch to");
}
let names: Vec<String> = items.iter().map(|(n, _)| n.clone()).collect();
let selected = msg::select("Select branch to switch to", names)?;
let is_remote = items
.iter()
.find(|(n, _)| n == &selected)
.is_some_and(|(_, r)| *r);
Ok((selected, is_remote))
}