//! Identity, verification, and configuration commands. use super::format::{short_id, signature_line, when}; use super::open; use super::ui; use anyhow::Result; use oxy_core::hash::ChangeHash; use oxy_core::identity::Identity; use oxy_core::repo::{self, IdentityScope}; use std::path::{Path, PathBuf}; pub fn create(name: Option, email: Option, local: bool) -> Result { let mut repo = open()?; let name = name.unwrap_or_else(|| repo.config.author_name.clone()); let email = email.unwrap_or_else(|| repo.config.author_email.clone()); let scope = if local { IdentityScope::Repo } else { IdentityScope::User }; let (key, _) = repo.create_identity(&name, &email, scope)?; println!("{} identity {} <{email}>", ui::done("Created"), ui::author(&name)); println!("{}", ui::kv("key", key)); println!("{}", ui::kv("secret", ui::path(repo::default_keys_dir().display()))); match scope { IdentityScope::User => println!("{}", ui::kv("scope", format!("default author in {}", ui::path(repo.user_config_path().display())))), IdentityScope::Repo => println!("{}", ui::kv("scope", "this repository's .oxy/config only")), } if !repo.config.sign_changes { println!(" {} {}", ui::note(), ui::dim("changes will carry this author; run oxy config --user core.sign change to sign each commit")); } Ok(0) } pub fn show() -> Result { let repo = open()?; let Some(key) = repo.config.author_key else { println!("No identity configured; run `oxy identity create`."); return Ok(0); }; let id = repo.identity(&key)?; println!("{} {} <{}>", ui::section("identity"), ui::author(&id.name), id.email); println!("{}", ui::kv("key", key)); println!("{}", ui::kv("created", format!("{} {}", when(id.created), ui::dim(format!("({})", ui::ago(id.created)))))); println!("{}", ui::kv("secret", if repo.secret_key()?.is_some() { ui::ok(format!("{} available", ui::glyph("✔", "*"))) } else { ui::err(format!("{} not found in the key directory", ui::glyph("✖", "x"))) })); println!("{}", ui::kv("signing", if repo.config.sign_changes { "every change (core.sign = change)" } else { "at push via seals (core.sign = seal)" })); Ok(0) } pub fn verify(change: Option<&str>) -> Result { let repo = open()?; let hashes: Vec = match change { Some(c) => vec![repo.resolve_change(c)?], None => repo.log()?.into_iter().filter(|e| !e.is_working_copy).map(|e| e.hash).collect(), }; let mut bad = 0; for h in hashes { let Ok(c) = repo.store().get_change(&h) else { println!("{} {}", ui::hash(format!("#{}", h.short(12))), ui::dim("not present (shallow)")); continue; }; let line = signature_line(&repo, &h)?; let styled = if line.starts_with("INVALID") { bad += 1; ui::err(format!("{} {line}", ui::glyph("✖", "x"))) } else if line == "unsigned" { ui::dim(format!("{} {line}", ui::glyph("·", "-"))) } else { ui::ok(format!("{} {line}", ui::glyph("✔", "v"))) }; println!("{} {} {styled}", ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12)))); } Ok(if bad > 0 { 1 } else { 0 }) } pub fn config(key: &str, value: &str, user: bool) -> Result { if user { let path = repo::default_user_config(); repo::Config::set_user(&path, key, value)?; println!("{} {key} = {value} {}", ui::done("Set"), ui::dim(format!("(in {})", path.display()))); } else { let mut repo = open()?; repo.config.set(key, value)?; repo.save_config()?; println!("{} {key} = {value} {}", ui::done("Set"), ui::dim("(in this repository)")); } Ok(0) } pub fn export(path: Option) -> Result { let repo = open()?; let Some(key) = repo.config.author_key else { anyhow::bail!("no identity configured; run `oxy identity create`"); }; let id = repo.identity(&key)?; let path = path.unwrap_or_else(|| PathBuf::from(format!("{}.identity", key.short(16)))); std::fs::write(&path, id.encode())?; println!("{} identity {} <{}> ({}) to {}", ui::done("Wrote"), ui::author(&id.name), id.email, key.short(12), ui::path(path.display())); Ok(0) } pub fn policy_init() -> Result { let repo = open()?; let sp = repo.policy_init()?; println!("{} policy serial {} with root key {} {}", ui::done("Created"), sp.policy.serial, sp.policy.roots[0].short(12), ui::dim("(push to publish it)")); Ok(0) } pub fn policy_show() -> Result { let repo = open()?; let Some((r, sp)) = repo.current_policy()? else { println!("No policy: this repository is unverified. Run `oxy policy init` to create one."); return Ok(0); }; let p = &sp.policy; let name = |k: &oxy_core::hash::KeyId| p.identity(k).map(|i| format!("{} <{}>", i.name, i.email)).unwrap_or_else(|| "(unknown identity)".into()); let expired = p.is_expired(oxy_core::objects::Hlc::now().wall_ms); println!("{} {}{}serial {}{}expires {}{}", ui::section("policy"), ui::hash(format!("#{}", r.short(12))), ui::sep(), p.serial, ui::sep(), when(p.expires), if expired { ui::err(format!(" {} EXPIRED", ui::glyph("✖", "x"))) } else { String::new() }); println!("{}", ui::section(format!("roots {}", ui::dim(format!("(threshold {})", p.root_threshold))))); for k in &p.roots { println!(" {} {} {}", ui::ok(ui::glyph("🔑", "*")), ui::hash(k.short(12)), ui::author(name(k))); } println!("{}", ui::section("rules")); for rule in &p.rules { println!(" {} {:12} {}", ui::ok(ui::glyph("▸", ">")), ui::bookmark(&rule.pattern), ui::dim(format!("threshold {}, require {}, conflicts {}", rule.threshold, rule.require.name(), if rule.allow_conflicts { "allowed" } else { "refused" }))); for k in &rule.keys { println!(" {} {}", ui::hash(k.short(12)), ui::author(name(k))); } } if !p.revoked.is_empty() { println!("{}", ui::section("revoked")); for k in &p.revoked { println!(" {} {} {}", ui::err(ui::glyph("✖", "x")), ui::hash(k.short(12)), ui::author(name(k))); } } Ok(0) } pub fn policy_grant(identity_file: &Path, pattern: Option<&str>) -> Result { let repo = open()?; let id = Identity::decode(&std::fs::read(identity_file)?)?; let key = id.hash(); let sp = repo.policy_grant(id.clone(), pattern)?; println!("{} {} <{}> ({}) on {} {}", ui::done("Granted"), ui::author(&id.name), id.email, key.short(12), ui::bookmark(pattern.unwrap_or("*")), ui::dim(format!("(policy serial {}; push to publish)", sp.policy.serial))); Ok(0) } pub fn policy_revoke(key: &str) -> Result { let repo = open()?; let Some((_, sp)) = repo.current_policy()? else { anyhow::bail!("this repository has no policy"); }; let matches: Vec<_> = sp.policy.identities.iter().map(|i| i.hash()).filter(|k| k.starts_with_hex(key)).collect(); let full = match matches.len() { 1 => matches[0], 0 => anyhow::bail!("no key in the policy starts with {key:?}"), _ => anyhow::bail!("{key:?} is ambiguous"), }; let sp = repo.policy_revoke(&full)?; println!("{} {} {}", ui::done("Revoked"), ui::hash(full.short(12)), ui::dim(format!("(policy serial {}; push to publish)", sp.policy.serial))); Ok(0) }