| 1 |
|
| 2 |
|
| 3 |
use super::format::{short_id, signature_line, when}; |
| 4 |
use super::open; |
| 5 |
use super::ui; |
| 6 |
use anyhow::Result; |
| 7 |
use oxy_core::hash::ChangeHash; |
| 8 |
use oxy_core::identity::Identity; |
| 9 |
use oxy_core::repo::{self, IdentityScope}; |
| 10 |
use std::path::{Path, PathBuf}; |
| 11 |
|
| 12 |
pub fn create(name: Option<String>, email: Option<String>, local: bool) -> Result<i32> { |
| 13 |
let mut repo = open()?; |
| 14 |
let name = name.unwrap_or_else(|| repo.config.author_name.clone()); |
| 15 |
let email = email.unwrap_or_else(|| repo.config.author_email.clone()); |
| 16 |
let scope = if local { IdentityScope::Repo } else { IdentityScope::User }; |
| 17 |
let (key, _) = repo.create_identity(&name, &email, scope)?; |
| 18 |
println!("{} identity {} <{email}>", ui::done("Created"), ui::author(&name)); |
| 19 |
println!("{}", ui::kv("key", key)); |
| 20 |
println!("{}", ui::kv("secret", ui::path(repo::default_keys_dir().display()))); |
| 21 |
match scope { |
| 22 |
IdentityScope::User => println!("{}", ui::kv("scope", format!("default author in {}", ui::path(repo.user_config_path().display())))), |
| 23 |
IdentityScope::Repo => println!("{}", ui::kv("scope", "this repository's .oxy/config only")), |
| 24 |
} |
| 25 |
if !repo.config.sign_changes { |
| 26 |
println!(" {} {}", ui::note(), ui::dim("changes will carry this author; run oxy config --user core.sign change to sign each commit")); |
| 27 |
} |
| 28 |
Ok(0) |
| 29 |
} |
| 30 |
|
| 31 |
pub fn show() -> Result<i32> { |
| 32 |
let repo = open()?; |
| 33 |
let Some(key) = repo.config.author_key else { |
| 34 |
println!("No identity configured; run `oxy identity create`."); |
| 35 |
return Ok(0); |
| 36 |
}; |
| 37 |
let id = repo.identity(&key)?; |
| 38 |
println!("{} {} <{}>", ui::section("identity"), ui::author(&id.name), id.email); |
| 39 |
println!("{}", ui::kv("key", key)); |
| 40 |
println!("{}", ui::kv("created", format!("{} {}", when(id.created), ui::dim(format!("({})", ui::ago(id.created)))))); |
| 41 |
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"))) })); |
| 42 |
println!("{}", ui::kv("signing", if repo.config.sign_changes { "every change (core.sign = change)" } else { "at push via seals (core.sign = seal)" })); |
| 43 |
Ok(0) |
| 44 |
} |
| 45 |
|
| 46 |
pub fn verify(change: Option<&str>) -> Result<i32> { |
| 47 |
let repo = open()?; |
| 48 |
let hashes: Vec<ChangeHash> = match change { |
| 49 |
Some(c) => vec![repo.resolve_change(c)?], |
| 50 |
None => repo.log()?.into_iter().filter(|e| !e.is_working_copy).map(|e| e.hash).collect(), |
| 51 |
}; |
| 52 |
let mut bad = 0; |
| 53 |
for h in hashes { |
| 54 |
let Ok(c) = repo.store().get_change(&h) else { |
| 55 |
println!("{} {}", ui::hash(format!("#{}", h.short(12))), ui::dim("not present (shallow)")); |
| 56 |
continue; |
| 57 |
}; |
| 58 |
let line = signature_line(&repo, &h)?; |
| 59 |
let styled = if line.starts_with("INVALID") { |
| 60 |
bad += 1; |
| 61 |
ui::err(format!("{} {line}", ui::glyph("✖", "x"))) |
| 62 |
} else if line == "unsigned" { |
| 63 |
ui::dim(format!("{} {line}", ui::glyph("·", "-"))) |
| 64 |
} else { |
| 65 |
ui::ok(format!("{} {line}", ui::glyph("✔", "v"))) |
| 66 |
}; |
| 67 |
println!("{} {} {styled}", ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12)))); |
| 68 |
} |
| 69 |
Ok(if bad > 0 { 1 } else { 0 }) |
| 70 |
} |
| 71 |
|
| 72 |
pub fn config(key: &str, value: &str, user: bool) -> Result<i32> { |
| 73 |
if user { |
| 74 |
let path = repo::default_user_config(); |
| 75 |
repo::Config::set_user(&path, key, value)?; |
| 76 |
println!("{} {key} = {value} {}", ui::done("Set"), ui::dim(format!("(in {})", path.display()))); |
| 77 |
} else { |
| 78 |
let mut repo = open()?; |
| 79 |
repo.config.set(key, value)?; |
| 80 |
repo.save_config()?; |
| 81 |
println!("{} {key} = {value} {}", ui::done("Set"), ui::dim("(in this repository)")); |
| 82 |
} |
| 83 |
Ok(0) |
| 84 |
} |
| 85 |
|
| 86 |
pub fn export(path: Option<PathBuf>) -> Result<i32> { |
| 87 |
let repo = open()?; |
| 88 |
let Some(key) = repo.config.author_key else { |
| 89 |
anyhow::bail!("no identity configured; run `oxy identity create`"); |
| 90 |
}; |
| 91 |
let id = repo.identity(&key)?; |
| 92 |
let path = path.unwrap_or_else(|| PathBuf::from(format!("{}.identity", key.short(16)))); |
| 93 |
std::fs::write(&path, id.encode())?; |
| 94 |
println!("{} identity {} <{}> ({}) to {}", ui::done("Wrote"), ui::author(&id.name), id.email, key.short(12), ui::path(path.display())); |
| 95 |
Ok(0) |
| 96 |
} |
| 97 |
|
| 98 |
pub fn policy_init() -> Result<i32> { |
| 99 |
let repo = open()?; |
| 100 |
let sp = repo.policy_init()?; |
| 101 |
println!("{} policy serial {} with root key {} {}", ui::done("Created"), sp.policy.serial, sp.policy.roots[0].short(12), ui::dim("(push to publish it)")); |
| 102 |
Ok(0) |
| 103 |
} |
| 104 |
|
| 105 |
pub fn policy_show() -> Result<i32> { |
| 106 |
let repo = open()?; |
| 107 |
let Some((r, sp)) = repo.current_policy()? else { |
| 108 |
println!("No policy: this repository is unverified. Run `oxy policy init` to create one."); |
| 109 |
return Ok(0); |
| 110 |
}; |
| 111 |
let p = &sp.policy; |
| 112 |
let name = |k: &oxy_core::hash::KeyId| p.identity(k).map(|i| format!("{} <{}>", i.name, i.email)).unwrap_or_else(|| "(unknown identity)".into()); |
| 113 |
let expired = p.is_expired(oxy_core::objects::Hlc::now().wall_ms); |
| 114 |
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() }); |
| 115 |
println!("{}", ui::section(format!("roots {}", ui::dim(format!("(threshold {})", p.root_threshold))))); |
| 116 |
for k in &p.roots { |
| 117 |
println!(" {} {} {}", ui::ok(ui::glyph("🔑", "*")), ui::hash(k.short(12)), ui::author(name(k))); |
| 118 |
} |
| 119 |
println!("{}", ui::section("rules")); |
| 120 |
for rule in &p.rules { |
| 121 |
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" }))); |
| 122 |
for k in &rule.keys { |
| 123 |
println!(" {} {}", ui::hash(k.short(12)), ui::author(name(k))); |
| 124 |
} |
| 125 |
} |
| 126 |
if !p.revoked.is_empty() { |
| 127 |
println!("{}", ui::section("revoked")); |
| 128 |
for k in &p.revoked { |
| 129 |
println!(" {} {} {}", ui::err(ui::glyph("✖", "x")), ui::hash(k.short(12)), ui::author(name(k))); |
| 130 |
} |
| 131 |
} |
| 132 |
Ok(0) |
| 133 |
} |
| 134 |
|
| 135 |
pub fn policy_grant(identity_file: &Path, pattern: Option<&str>) -> Result<i32> { |
| 136 |
let repo = open()?; |
| 137 |
let id = Identity::decode(&std::fs::read(identity_file)?)?; |
| 138 |
let key = id.hash(); |
| 139 |
let sp = repo.policy_grant(id.clone(), pattern)?; |
| 140 |
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))); |
| 141 |
Ok(0) |
| 142 |
} |
| 143 |
|
| 144 |
pub fn policy_revoke(key: &str) -> Result<i32> { |
| 145 |
let repo = open()?; |
| 146 |
let Some((_, sp)) = repo.current_policy()? else { |
| 147 |
anyhow::bail!("this repository has no policy"); |
| 148 |
}; |
| 149 |
let matches: Vec<_> = sp.policy.identities.iter().map(|i| i.hash()).filter(|k| k.starts_with_hex(key)).collect(); |
| 150 |
let full = match matches.len() { |
| 151 |
1 => matches[0], |
| 152 |
0 => anyhow::bail!("no key in the policy starts with {key:?}"), |
| 153 |
_ => anyhow::bail!("{key:?} is ambiguous"), |
| 154 |
}; |
| 155 |
let sp = repo.policy_revoke(&full)?; |
| 156 |
println!("{} {} {}", ui::done("Revoked"), ui::hash(full.short(12)), ui::dim(format!("(policy serial {}; push to publish)", sp.policy.serial))); |
| 157 |
Ok(0) |
| 158 |
} |