use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use rho_core::{IdentityBundleManifest, LocalIdentityManifest, from_yaml, normalize_actor_id};
use rho_core::commands;
fn usage() -> ! {
eprintln!(
"usage: rho [--profile <github-handle|rho-id>] [--identity <rho-id>] [--home <path>] <dataset|publish|message|request|approve|result|id|crypto|repo|status|gh|commit|env|install-shell|version> ..."
);
std::process::exit(2);
}
fn gh_usage() -> ! {
eprintln!("usage:\n rho gh switch <login>\n rho gh status\n rho gh whoami");
std::process::exit(2);
}
fn command_text(command: &mut Command, label: &str) -> String {
let output = command.output().unwrap_or_else(|error| {
eprintln!("failed to run {label}: {error}");
std::process::exit(1);
});
if !output.status.success() {
eprintln!("{label} failed");
eprint!("{}", String::from_utf8_lossy(&output.stderr));
std::process::exit(output.status.code().unwrap_or(1));
}
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn run_subcommand(command: fn(&[String]) -> rho_core::RhoResult<()>, args: &[String]) -> ! {
if let Err(error) = command(args) {
eprintln!("{error}");
std::process::exit(1);
}
std::process::exit(0);
}
fn try_gh_login() -> Result<String, String> {
let output = Command::new("gh")
.args(["api", "user", "--jq", ".login"])
.output()
.map_err(|error| format!("failed to run gh api user: {error}"))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
}
let login = String::from_utf8_lossy(&output.stdout).trim().to_string();
if login.is_empty() {
return Err("gh api user returned an empty login".to_string());
}
Ok(login)
}
fn gh_user() -> (String, String) {
let login = command_text(
Command::new("gh").args(["api", "user", "--jq", ".login"]),
"gh api user",
);
let id = command_text(
Command::new("gh").args(["api", "user", "--jq", ".id"]),
"gh api user",
);
(login, id)
}
fn identity_login_mismatch(identity: &str, active_login: &str) -> Result<Option<String>, String> {
let canonical = normalize_actor_id(identity).map_err(|error| error.to_string())?;
let handle = github_handle(&canonical)?;
if handle == active_login {
return Ok(None);
}
Ok(Some(format!(
"rho warning: active gh account is {active_login}, but RHO_IDENTITY is {canonical}"
)))
}
fn remove_bool_flag(args: &mut Vec<String>, flag: &str) -> bool {
let before = args.len();
args.retain(|arg| arg != flag);
args.len() != before
}
fn take_value_flag(args: &mut Vec<String>, flags: &[&str]) -> Result<Option<String>, String> {
let mut found = None;
let mut index = 0;
while index < args.len() {
if flags.iter().any(|flag| args[index] == *flag) {
if found.is_some() {
return Err(format!("{} can only be provided once", flags.join("/")));
}
if index + 1 >= args.len() {
return Err(format!("missing value for {}", args[index]));
}
let value = args.remove(index + 1);
args.remove(index);
found = Some(value);
} else {
index += 1;
}
}
Ok(found)
}
fn active_rho_identity() -> Option<String> {
env::var("RHO_IDENTITY")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.and_then(|value| normalize_actor_id(&value).ok())
}
fn rho_home() -> PathBuf {
env::var("RHO_HOME")
.ok()
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.unwrap_or_else(default_rho_home)
}
fn mapped_commit_identity() -> Option<(String, String, String)> {
let identity = active_rho_identity()?;
let handle = github_handle(&identity).ok()?;
let path = rho_home()
.join("identities")
.join("github")
.join(format!("{handle}.yaml"));
let text = fs::read_to_string(path).ok()?;
let manifest: LocalIdentityManifest = from_yaml(&text).ok()?;
let git = manifest.local_identity.git?;
Some((
git.commit_name,
git.commit_email,
format!("rho identity {identity} / gh {}", git.github_login),
))
}
fn commit_identity() -> (String, String, String) {
if let Some(mapped) = mapped_commit_identity() {
return mapped;
}
let (login, id) = gh_user();
(
login.clone(),
format!("{id}+{login}@users.noreply.github.com"),
format!("active gh account {login}"),
)
}
fn handle_gh(args: &[String]) {
let Some(command) = args.first().map(String::as_str) else {
gh_usage();
};
match command {
"switch" => {
let Some(login) = args.get(1) else {
gh_usage();
};
let status = Command::new("gh")
.args(["auth", "switch", "-u", login])
.status()
.unwrap_or_else(|error| {
eprintln!("failed to run gh auth switch: {error}");
std::process::exit(1);
});
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
let (active_login, id) = gh_user();
println!("github active: {active_login}");
println!("github noreply: {id}+{active_login}@users.noreply.github.com");
}
"status" => {
let status = Command::new("gh")
.args(["auth", "status"])
.status()
.unwrap_or_else(|error| {
eprintln!("failed to run gh auth status: {error}");
std::process::exit(1);
});
std::process::exit(status.code().unwrap_or(1));
}
"whoami" => {
let (login, id) = gh_user();
println!("{login}");
println!("{id}+{login}@users.noreply.github.com");
}
_ => gh_usage(),
}
}
fn handle_commit(args: &[String]) {
let mut git_args = args.to_vec();
let require_identity_match = remove_bool_flag(&mut git_args, "--require-identity-match");
let root = take_value_flag(&mut git_args, &["-C", "--root"]).unwrap_or_else(|error| {
eprintln!("{error}");
std::process::exit(2);
});
match active_rho_identity() {
Some(identity) => match try_gh_login() {
Ok(active_login) => match identity_login_mismatch(&identity, &active_login) {
Ok(Some(warning)) => {
eprintln!("{warning}");
if require_identity_match {
std::process::exit(1);
}
}
Ok(None) => {}
Err(error) => {
if require_identity_match {
eprintln!("{error}");
std::process::exit(1);
}
}
},
Err(error) => {
if require_identity_match {
eprintln!("could not verify active gh account: {error}");
std::process::exit(1);
}
}
},
None => {
if require_identity_match {
eprintln!("--require-identity-match requires RHO_IDENTITY");
std::process::exit(1);
}
}
}
let (name, email, source) = commit_identity();
eprintln!("rho commit author: {name} <{email}> ({source})");
let mut command = Command::new("git");
if let Some(root) = root {
command.arg("-C").arg(root);
}
command
.arg("commit")
.args(&git_args)
.env("GIT_AUTHOR_NAME", &name)
.env("GIT_AUTHOR_EMAIL", &email)
.env("GIT_COMMITTER_NAME", &name)
.env("GIT_COMMITTER_EMAIL", &email)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = command.status().unwrap_or_else(|error| {
eprintln!("failed to run git commit: {error}");
std::process::exit(1);
});
std::process::exit(status.code().unwrap_or(1));
}
fn github_handle(identity: &str) -> Result<String, String> {
let canonical = normalize_actor_id(identity).map_err(|error| error.to_string())?;
let Some(handle) = canonical.strip_prefix("rho://id/github/") else {
return Err(format!("unsupported identity id: {canonical}"));
};
if handle.is_empty()
|| !handle
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-')
{
return Err(format!("invalid github identity handle: {handle}"));
}
Ok(handle.to_string())
}
fn normalize_github_identity(identity: &str) -> Result<(String, String), String> {
let canonical = normalize_actor_id(identity).map_err(|error| error.to_string())?;
let handle = github_handle(&canonical)?;
Ok((canonical, handle))
}
fn resolve_profile_identity(profile: &str) -> Result<String, String> {
if profile.starts_with("rho://id/") || profile.contains('/') {
return normalize_actor_id(profile).map_err(|error| error.to_string());
}
let matches = matching_profile_identities(profile);
match matches.as_slice() {
[] => normalize_actor_id(profile).map_err(|error| error.to_string()),
[identity] => Ok(identity.clone()),
_ => Err(format!(
"ambiguous profile {profile}: {}; use a provider-qualified identity like github/{profile}",
matches.join(", ")
)),
}
}
fn matching_profile_identities(handle: &str) -> Vec<String> {
let mut matches = Vec::new();
collect_profile_identities(&rho_home().join("identities"), handle, true, &mut matches);
collect_profile_identities(&rho_home().join("peers"), handle, false, &mut matches);
matches.sort();
matches.dedup();
matches
}
fn collect_profile_identities(root: &Path, handle: &str, local: bool, matches: &mut Vec<String>) {
let Ok(providers) = fs::read_dir(root) else {
return;
};
for provider in providers.flatten() {
let path = provider.path().join(format!("{handle}.yaml"));
if !path.is_file() {
continue;
}
let Ok(text) = fs::read_to_string(path) else {
continue;
};
let identity = if local {
from_yaml::<LocalIdentityManifest>(&text)
.map(|manifest| manifest.local_identity.identity.id)
} else {
from_yaml::<IdentityBundleManifest>(&text).map(|manifest| manifest.identity.id)
};
if let Ok(identity) = identity {
matches.push(identity);
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
struct GlobalOptions {
profile: Option<String>,
identity: Option<String>,
home: Option<PathBuf>,
}
fn parse_global_options(args: &mut Vec<String>) -> Result<GlobalOptions, String> {
let mut options = GlobalOptions::default();
let mut rest_start = 0usize;
while rest_start < args.len() {
match args[rest_start].as_str() {
"--profile" => {
let Some(value) = args.get(rest_start + 1) else {
return Err("missing value after --profile".to_string());
};
options.profile = Some(value.clone());
rest_start += 2;
}
"--identity" => {
let Some(value) = args.get(rest_start + 1) else {
return Err("missing value after --identity".to_string());
};
options.identity = Some(value.clone());
rest_start += 2;
}
"--home" => {
let Some(value) = args.get(rest_start + 1) else {
return Err("missing value after --home".to_string());
};
options.home = Some(PathBuf::from(value));
rest_start += 2;
}
"--" => {
rest_start += 1;
break;
}
_ => break,
}
}
args.drain(0..rest_start);
Ok(options)
}
fn set_process_env(key: &str, value: impl AsRef<OsStr>) {
unsafe {
env::set_var(key, value);
}
}
fn apply_global_options(options: GlobalOptions) -> Result<(), String> {
if options.profile.is_some() && options.identity.is_some() {
return Err("--profile and --identity cannot be used together".to_string());
}
match (options.profile, options.identity) {
(Some(profile), None) => {
let identity = resolve_profile_identity(&profile)?;
let handle = github_handle(&identity)?;
set_process_env("RHO_IDENTITY", &identity);
set_process_env("RHO_ENV_HANDLE", &handle);
switch_gh_account(&handle);
if let Some(home) = options.home {
set_process_env("RHO_HOME", home);
}
}
(None, Some(identity_arg)) => {
let (identity, handle) = normalize_github_identity(&identity_arg)?;
set_process_env("RHO_IDENTITY", &identity);
set_process_env("RHO_ENV_HANDLE", &handle);
switch_gh_account(&handle);
if let Some(home) = options.home {
set_process_env("RHO_HOME", home);
}
}
(None, None) => {
if let Some(home) = options.home {
set_process_env("RHO_HOME", home);
}
}
(Some(_), Some(_)) => unreachable!(),
}
Ok(())
}
fn switch_gh_account(handle: &str) {
if let Ok(active) = try_gh_login()
&& active == handle
{
return;
}
let output = Command::new("gh")
.args(["auth", "switch", "-u", handle])
.output();
match output {
Ok(output) if output.status.success() => {}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!(
"rho warning: could not switch gh account to {handle}: {}",
stderr.trim()
);
}
Err(error) => {
eprintln!("rho warning: could not run gh auth switch -u {handle}: {error}");
}
}
}
fn shell_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\"'\"'"))
}
fn default_rho_home() -> PathBuf {
env::var("RHO_ENV_HOME_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| {
env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."))
.join(".rho")
})
}
fn env_usage() -> ! {
eprintln!(
"usage:\n rho env switch <rho-id> [--home <path>] [--no-prompt]\n rho env clear [--no-prompt]\n rho env status\n rho env shell <rho-id> [--home <path>]"
);
std::process::exit(2);
}
fn arg_value(args: &[String], flag: &str) -> Option<String> {
args.windows(2)
.find(|window| window[0] == flag)
.map(|window| window[1].clone())
}
fn has_flag(args: &[String], flag: &str) -> bool {
args.iter().any(|arg| arg == flag)
}
fn handle_env(args: &[String]) {
let Some(command) = args.first().map(String::as_str) else {
env_usage();
};
match command {
"switch" => {
let identity = args.get(1).unwrap_or_else(|| env_usage()).clone();
let (identity, handle) = normalize_github_identity(&identity).unwrap_or_else(|error| {
eprintln!("{error}");
std::process::exit(1);
});
let home = arg_value(args, "--home")
.map(PathBuf::from)
.unwrap_or_else(default_rho_home);
if let Ok(active_login) = try_gh_login()
&& let Ok(Some(warning)) = identity_login_mismatch(&identity, &active_login)
{
println!("echo {} >&2", shell_quote(&warning));
}
println!("export RHO_IDENTITY={}", shell_quote(&identity));
println!(
"export RHO_HOME={}",
shell_quote(&home.display().to_string())
);
println!("export RHO_ENV_HANDLE={}", shell_quote(&handle));
if !has_flag(args, "--no-prompt") {
println!(
"if [ -z \"${{RHO_ENV_OLD_PS1+x}}\" ]; then export RHO_ENV_OLD_PS1=\"${{PS1:-}}\"; fi"
);
println!(
"export PS1={}\"${{RHO_ENV_OLD_PS1:-}}\"",
shell_quote(&format!("[rho:{handle}] "))
);
}
println!(
"echo {}",
shell_quote(&format!("rho identity active: {identity}"))
);
println!(
"echo {}",
shell_quote(&format!("rho home: {}", home.display()))
);
}
"clear" => {
println!("unset RHO_IDENTITY");
println!("unset RHO_ENV_HANDLE");
println!("unset RHO_HOME");
if !has_flag(args, "--no-prompt") {
println!(
"if [ -n \"${{RHO_ENV_OLD_PS1+x}}\" ]; then export PS1=\"$RHO_ENV_OLD_PS1\"; unset RHO_ENV_OLD_PS1; fi"
);
}
println!("echo 'rho identity cleared'");
}
"status" => {
println!(
"RHO_IDENTITY={}",
env::var("RHO_IDENTITY").unwrap_or_default()
);
println!("RHO_HOME={}", env::var("RHO_HOME").unwrap_or_default());
println!(
"RHO_ENV_HANDLE={}",
env::var("RHO_ENV_HANDLE").unwrap_or_default()
);
}
"shell" => {
let identity = args.get(1).unwrap_or_else(|| env_usage()).clone();
let (identity, handle) = normalize_github_identity(&identity).unwrap_or_else(|error| {
eprintln!("{error}");
std::process::exit(1);
});
let home = arg_value(args, "--home")
.map(PathBuf::from)
.unwrap_or_else(default_rho_home);
let shell = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
let status = Command::new(shell)
.env("RHO_IDENTITY", identity)
.env("RHO_HOME", home)
.env("RHO_ENV_HANDLE", &handle)
.env(
"PS1",
format!("[rho:{handle}] {}", env::var("PS1").unwrap_or_default()),
)
.status()
.unwrap_or_else(|error| {
eprintln!("failed to start shell: {error}");
std::process::exit(1);
});
std::process::exit(status.code().unwrap_or(1));
}
_ => env_usage(),
}
}
fn shell_profile_path(args: &[String]) -> PathBuf {
if let Some(path) = arg_value(args, "--file") {
return PathBuf::from(path);
}
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let shell = env::var("SHELL").unwrap_or_default();
if shell.contains("zsh") {
PathBuf::from(home).join(".zshrc")
} else {
PathBuf::from(home).join(".bashrc")
}
}
fn install_shell_block(bin_dir: &Path) -> String {
format!(
"# >>> rho shell integration >>>\n\
case \":$PATH:\" in\n\
*:{}:*) ;;\n\
*) export PATH={}:$PATH ;;\n\
esac\n\
rho-use() {{ eval \"$(rho env switch \"$@\")\"; }}\n\
rho-clear() {{ eval \"$(rho env clear)\"; }}\n\
# <<< rho shell integration <<<\n",
bin_dir.display(),
shell_quote(&bin_dir.display().to_string())
)
}
fn replace_marked_block(existing: &str, block: &str) -> String {
let start = "# >>> rho shell integration >>>";
let end = "# <<< rho shell integration <<<";
if let Some(start_index) = existing.find(start)
&& let Some(end_relative) = existing[start_index..].find(end)
{
let end_index = start_index + end_relative + end.len();
let mut updated = String::new();
updated.push_str(existing[..start_index].trim_end());
updated.push_str("\n\n");
updated.push_str(block.trim_end());
updated.push('\n');
updated.push_str(existing[end_index..].trim_start_matches('\n'));
return updated;
}
let mut updated = existing.trim_end().to_string();
if !updated.is_empty() {
updated.push_str("\n\n");
}
updated.push_str(block);
updated
}
fn handle_install_shell(args: &[String]) {
if has_flag(args, "--help") || has_flag(args, "-h") {
eprintln!("usage: rho install-shell [--file <shell-rc>] [--print]");
return;
}
let exe = env::current_exe().unwrap_or_else(|_| PathBuf::from("rho"));
let bin_dir = exe.parent().unwrap_or_else(|| Path::new("."));
let block = install_shell_block(bin_dir);
if has_flag(args, "--print") {
print!("{block}");
return;
}
let profile = shell_profile_path(args);
let existing = fs::read_to_string(&profile).unwrap_or_default();
let updated = replace_marked_block(&existing, &block);
if let Some(parent) = profile.parent()
&& let Err(error) = fs::create_dir_all(parent)
{
eprintln!("failed to create {}: {error}", parent.display());
std::process::exit(1);
}
if let Err(error) = fs::write(&profile, updated) {
eprintln!("failed to update {}: {error}", profile.display());
std::process::exit(1);
}
println!("installed rho shell integration: {}", profile.display());
println!("restart your shell or run: source {}", profile.display());
println!("helpers: rho-use <rho-id>, rho-clear, rho env status");
}
fn main() {
let mut args: Vec<String> = env::args().skip(1).collect();
let global_options = parse_global_options(&mut args).unwrap_or_else(|error| {
eprintln!("{error}");
std::process::exit(2);
});
apply_global_options(global_options).unwrap_or_else(|error| {
eprintln!("{error}");
std::process::exit(2);
});
let Some(command) = args.first().cloned() else {
usage();
};
match command.as_str() {
"--version" | "-V" | "version" => {
println!("rho {}", env!("CARGO_PKG_VERSION"));
}
"--help" | "-h" => usage(),
"env" => {
args.remove(0);
handle_env(&args);
}
"install-shell" => {
args.remove(0);
handle_install_shell(&args);
}
"gh" => {
args.remove(0);
handle_gh(&args);
}
"commit" => {
args.remove(0);
handle_commit(&args);
}
"add" => {
args.remove(0);
match args.first().map(String::as_str) {
Some("user") => {
args[0] = "add-user".to_string();
run_subcommand(commands::repo::run, &args);
}
_ => usage(),
}
}
"dataset" => {
args.remove(0);
run_subcommand(commands::dataset::run, &args);
}
"publish" => {
args.remove(0);
run_subcommand(commands::publish::run, &args);
}
"message" => {
args.remove(0);
run_subcommand(commands::message::run, &args);
}
"request" => {
args.remove(0);
run_subcommand(commands::request::run, &args);
}
"tools" => {
args.remove(0);
run_subcommand(commands::tools::run, &args);
}
"run" => {
args.remove(0);
run_subcommand(commands::run::run, &args);
}
"approve" => {
args[0] = "approve".to_string();
run_subcommand(commands::request::run, &args);
}
"result" => {
args.remove(0);
run_subcommand(commands::result::run, &args);
}
"id" => {
args.remove(0);
run_subcommand(commands::id::run, &args);
}
"crypto" => {
args.remove(0);
run_subcommand(commands::crypto::run, &args);
}
"repo" => {
args.remove(0);
run_subcommand(commands::repo::run, &args);
}
"status" => {
args[0] = "status".to_string();
run_subcommand(commands::repo::run, &args);
}
_ => usage(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_leading_global_profile_flags() {
let mut args = vec![
"--profile".to_string(),
"madhavajay".to_string(),
"--home".to_string(),
"/tmp/rho-home".to_string(),
"repo".to_string(),
"doctor".to_string(),
];
let options = parse_global_options(&mut args).unwrap();
assert_eq!(
options,
GlobalOptions {
profile: Some("madhavajay".to_string()),
identity: None,
home: Some(PathBuf::from("/tmp/rho-home")),
}
);
assert_eq!(args, vec!["repo".to_string(), "doctor".to_string()]);
}
#[test]
fn rejects_profile_and_identity_together() {
let error = apply_global_options(GlobalOptions {
profile: Some("madhavajay".to_string()),
identity: Some("github/madhavajay".to_string()),
home: None,
})
.unwrap_err();
assert_eq!(error, "--profile and --identity cannot be used together");
}
#[test]
fn resolves_explicit_github_profile() {
assert_eq!(
resolve_profile_identity("github/madhavajay").unwrap(),
"rho://id/github/madhavajay"
);
assert_eq!(
resolve_profile_identity("rho://id/github/madhavajay").unwrap(),
"rho://id/github/madhavajay"
);
}
#[test]
fn collects_ambiguous_plain_profile_matches() {
let root = env::temp_dir().join(format!(
"rho-profile-test-{}-{}",
std::process::id(),
rho_core::uuid_like()
));
let github = root.join("github");
let other = root.join("x");
fs::create_dir_all(&github).unwrap();
fs::create_dir_all(&other).unwrap();
fs::write(
github.join("madhavajay.yaml"),
"version: 1\nidentity:\n id: rho://id/github/madhavajay\n kind: github\n handle: madhavajay\n display_name: Madhava\n public_keys: []\n proofs: []\n created_at: \"2026-06-16T00:00:00Z\"\n",
)
.unwrap();
fs::write(
other.join("madhavajay.yaml"),
"version: 1\nidentity:\n id: rho://id/x/madhavajay\n kind: x\n handle: madhavajay\n display_name: Madhava\n public_keys: []\n proofs: []\n created_at: \"2026-06-16T00:00:00Z\"\n",
)
.unwrap();
let mut matches = Vec::new();
collect_profile_identities(&root, "madhavajay", false, &mut matches);
matches.sort();
assert_eq!(
matches,
vec![
"rho://id/github/madhavajay".to_string(),
"rho://id/x/madhavajay".to_string()
]
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn detects_identity_login_mismatch() {
assert!(matches!(
identity_login_mismatch("rho://id/github/madhavajay", "madhavajay"),
Ok(None)
));
let warning = identity_login_mismatch("github/madhavajay", "madhavajay-test")
.unwrap()
.unwrap();
assert!(warning.contains("active gh account is madhavajay-test"));
assert!(warning.contains("RHO_IDENTITY is rho://id/github/madhavajay"));
}
#[test]
fn removes_commit_identity_match_flag_before_forwarding_to_git() {
let mut args = vec![
"--require-identity-match".to_string(),
"-m".to_string(),
"message".to_string(),
];
assert!(remove_bool_flag(&mut args, "--require-identity-match"));
assert_eq!(args, vec!["-m".to_string(), "message".to_string()]);
assert!(!remove_bool_flag(&mut args, "--require-identity-match"));
}
#[test]
fn extracts_commit_root_flags_before_forwarding_to_git() {
let mut args = vec![
"-C".to_string(),
"/tmp/project".to_string(),
"-m".to_string(),
"message".to_string(),
];
assert_eq!(
take_value_flag(&mut args, &["-C", "--root"]).unwrap(),
Some("/tmp/project".to_string())
);
assert_eq!(args, vec!["-m".to_string(), "message".to_string()]);
let mut args = vec![
"--root".to_string(),
"/tmp/project".to_string(),
"--allow-empty".to_string(),
];
assert_eq!(
take_value_flag(&mut args, &["-C", "--root"]).unwrap(),
Some("/tmp/project".to_string())
);
assert_eq!(args, vec!["--allow-empty".to_string()]);
}
#[test]
fn rejects_invalid_commit_root_flags() {
let mut missing = vec!["-C".to_string()];
assert!(take_value_flag(&mut missing, &["-C", "--root"]).is_err());
let mut duplicate = vec![
"-C".to_string(),
"/tmp/one".to_string(),
"--root".to_string(),
"/tmp/two".to_string(),
];
assert!(take_value_flag(&mut duplicate, &["-C", "--root"]).is_err());
}
}