use std::io::{self, Write};
use std::path::Path;
use anyhow::{Context, Result, bail};
use crate::config::{Account, Config, is_reserved};
use crate::{gh, git};
pub fn switch(config_path: &Path, alias: &str) -> Result<()> {
let cfg = Config::load(config_path)?;
if cfg.accounts.is_empty() {
bail!("no accounts configured yet — run `gitswitch add <alias>` first");
}
let account = cfg
.get(alias)
.with_context(|| format!("no account named '{alias}' (see `gitswitch list`)"))?;
git::set_identity(&account.name, &account.email)?;
println!(
"Switched git identity to {} <{}>",
account.name, account.email
);
switch_gh(&account.gh_user)?;
Ok(())
}
fn switch_gh(gh_user: &str) -> Result<()> {
match gh::switch(gh_user)? {
gh::SwitchResult::Ok => {
println!("Switched gh account to '{gh_user}'");
return Ok(());
}
gh::SwitchResult::NotInstalled => {
eprintln!("warning: `gh` is not installed, so the GitHub account was not switched.");
eprintln!(" install it from https://cli.github.com to switch auth automatically.");
return Ok(());
}
gh::SwitchResult::Failed(err) => {
eprintln!("The gh account '{gh_user}' isn't logged in yet.");
if !err.is_empty() {
eprintln!(" ({err})");
}
}
}
if !prompt_yes_no(&format!("Log in to GitHub with gh now as '{gh_user}'?"), true)? {
eprintln!(" skipped — run `gh auth login` yourself, then `gitswitch` again.");
return Ok(());
}
if !gh::login()? {
eprintln!("warning: `gh auth login` did not complete; gh account not switched.");
return Ok(());
}
match gh::switch(gh_user)? {
gh::SwitchResult::Ok => println!("Switched gh account to '{gh_user}'"),
gh::SwitchResult::Failed(err) => {
eprintln!("warning: still could not switch to gh account '{gh_user}'.");
if !err.is_empty() {
eprintln!(" {err}");
}
eprintln!(
" you may have logged in as a different account — run `gh auth status` to check."
);
}
gh::SwitchResult::NotInstalled => unreachable!("gh was invoked moments ago"),
}
Ok(())
}
pub fn add(config_path: &Path, alias: &str) -> Result<()> {
if is_reserved(alias) {
bail!("'{alias}' is a reserved command name and cannot be used as an account alias");
}
let mut cfg = Config::load(config_path)?;
let existing = cfg.get(alias).cloned();
if existing.is_some() {
println!("Account '{alias}' already exists; updating it.");
}
let name = prompt("Full name", existing.as_ref().map(|a| a.name.as_str()))?;
let email = prompt("Email", existing.as_ref().map(|a| a.email.as_str()))?;
let gh_user = prompt(
"GitHub username",
existing.as_ref().map(|a| a.gh_user.as_str()),
)?;
cfg.set_account(alias, Account { name, email, gh_user })?;
cfg.save(config_path)?;
println!("Saved account '{alias}'. Switch to it with: gitswitch {alias}");
Ok(())
}
pub fn list(config_path: &Path) -> Result<()> {
let cfg = Config::load(config_path)?;
if cfg.accounts.is_empty() {
println!("No accounts configured. Add one with: gitswitch add <alias>");
return Ok(());
}
let (_, current_email) = git::get_identity();
for (alias, account) in &cfg.accounts {
let marker = if current_email.as_deref() == Some(account.email.as_str()) {
"* "
} else {
" "
};
println!(
"{marker}{alias} {} <{}> (gh: {})",
account.name, account.email, account.gh_user
);
}
Ok(())
}
pub fn current(config_path: &Path) -> Result<()> {
let cfg = Config::load(config_path)?;
let (name, email) = git::get_identity();
match (name, email.clone()) {
(Some(n), Some(e)) => {
let alias = cfg
.accounts
.iter()
.find(|(_, a)| a.email == e)
.map(|(alias, _)| alias.as_str());
match alias {
Some(a) => println!("git identity: {n} <{e}> (account '{a}')"),
None => println!("git identity: {n} <{e}> (no matching account)"),
}
}
_ => println!("git identity: not set"),
}
match gh::current_user() {
Some(user) => println!("gh account: {user}"),
None => println!("gh account: unknown (gh not installed or not logged in)"),
}
Ok(())
}
pub fn remove(config_path: &Path, alias: &str) -> Result<()> {
let mut cfg = Config::load(config_path)?;
cfg.remove_account(alias)?;
cfg.save(config_path)?;
println!("Removed account '{alias}'.");
Ok(())
}
fn prompt_yes_no(question: &str, default: bool) -> Result<bool> {
let hint = if default { "[Y/n]" } else { "[y/N]" };
print!("{question} {hint}: ");
io::stdout().flush().ok();
let mut line = String::new();
let n = io::stdin().read_line(&mut line)?;
if n == 0 {
return Ok(default); }
match line.trim().to_lowercase().as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
_ => Ok(false),
}
}
fn prompt(label: &str, default: Option<&str>) -> Result<String> {
loop {
match default {
Some(d) => print!("{label} [{d}]: "),
None => print!("{label}: "),
}
io::stdout().flush().ok();
let mut line = String::new();
let n = io::stdin().read_line(&mut line)?;
if n == 0 {
bail!("input closed before all fields were entered");
}
let trimmed = line.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
if let Some(d) = default {
return Ok(d.to_string());
}
eprintln!(" {label} cannot be empty.");
}
}