| fce44ae9 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
use super::format::{short_id, signature_line, when}; |
| 4 |
use super::open; |
| 135212de Andy McCall ↶ |
5 |
use super::ui; |
| fce44ae9 Andy McCall ↶ |
6 |
use anyhow::Result; |
| 7 |
use oxy_core::hash::ChangeHash; |
| 83e0677b Andy McCall ↶ |
8 |
use oxy_core::identity::Identity; |
| fce44ae9 Andy McCall ↶ |
9 |
use oxy_core::repo::{self, IdentityScope}; |
| 83e0677b Andy McCall ↶ |
10 |
use std::path::{Path, PathBuf}; |
| fce44ae9 Andy McCall ↶ |
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!("Created identity {name} <{email}>"); |
| 19 |
println!("KeyId: {key}"); |
| 20 |
println!("Secret key stored under {}", repo::default_keys_dir().display()); |
| 21 |
match scope { |
| 22 |
IdentityScope::User => println!("Recorded as your default author in {}", repo.user_config_path().display()), |
| 23 |
IdentityScope::Repo => println!("Recorded in this repository's .oxy/config only"), |
| 24 |
} |
| 25 |
if !repo.config.sign_changes { |
| 26 |
println!("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!("{} <{}>", id.name, id.email); |
| 39 |
println!("KeyId: {key}"); |
| 40 |
println!("Created: {}", when(id.created)); |
| 41 |
println!("Secret: {}", if repo.secret_key()?.is_some() { "available" } else { "NOT FOUND in key directory" }); |
| 42 |
println!("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 { |
| 2863456a Andy McCall ↶ |
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 |
}; |
| fce44ae9 Andy McCall ↶ |
58 |
let line = signature_line(&repo, &h)?; |
| 135212de Andy McCall ↶ |
59 |
let styled = if line.starts_with("INVALID") { |
| fce44ae9 Andy McCall ↶ |
60 |
bad += 1; |
| 135212de Andy McCall ↶ |
61 |
ui::err(&line) |
| 62 |
} else if line == "unsigned" { |
| 63 |
ui::dim(&line) |
| 64 |
} else { |
| 65 |
ui::ok(&line) |
| 66 |
}; |
| 67 |
println!("{} {} {styled}", ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12)))); |
| fce44ae9 Andy McCall ↶ |
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} (in {})", path.display()); |
| 77 |
} else { |
| 78 |
let mut repo = open()?; |
| 79 |
repo.config.set(key, value)?; |
| 80 |
repo.save_config()?; |
| 81 |
println!("{key} = {value} (in this repository)"); |
| 82 |
} |
| 83 |
Ok(0) |
| 84 |
} |
| 83e0677b Andy McCall ↶ |
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!("Wrote identity {} <{}> ({}) to {}", id.name, id.email, key.short(12), 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!("Created policy serial {} with root key {}; push to publish it.", sp.policy.serial, sp.policy.roots[0].short(12)); |
| 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 |
println!("policy #{} serial {} expires {}{}", r.short(12), p.serial, when(p.expires), if p.is_expired(oxy_core::objects::Hlc::now().wall_ms) { " EXPIRED" } else { "" }); |
| 114 |
println!("roots (threshold {}):", p.root_threshold); |
| 115 |
for k in &p.roots { |
| 116 |
println!(" {} {}", k.short(12), name(k)); |
| 117 |
} |
| 118 |
println!("rules:"); |
| 119 |
for rule in &p.rules { |
| 120 |
println!(" {:12} threshold {} require {} conflicts {}", rule.pattern, rule.threshold, rule.require.name(), if rule.allow_conflicts { "allowed" } else { "refused" }); |
| 121 |
for k in &rule.keys { |
| 122 |
println!(" {} {}", k.short(12), name(k)); |
| 123 |
} |
| 124 |
} |
| 125 |
if !p.revoked.is_empty() { |
| 126 |
println!("revoked:"); |
| 127 |
for k in &p.revoked { |
| 128 |
println!(" {} {}", k.short(12), name(k)); |
| 129 |
} |
| 130 |
} |
| 131 |
Ok(0) |
| 132 |
} |
| 133 |
|
| 134 |
pub fn policy_grant(identity_file: &Path, pattern: Option<&str>) -> Result<i32> { |
| 135 |
let repo = open()?; |
| 136 |
let id = Identity::decode(&std::fs::read(identity_file)?)?; |
| 137 |
let key = id.hash(); |
| 138 |
let sp = repo.policy_grant(id.clone(), pattern)?; |
| 139 |
println!("Granted {} <{}> ({}) on {}; policy serial {}. Push to publish.", id.name, id.email, key.short(12), pattern.unwrap_or("*"), sp.policy.serial); |
| 140 |
Ok(0) |
| 141 |
} |
| 142 |
|
| 143 |
pub fn policy_revoke(key: &str) -> Result<i32> { |
| 144 |
let repo = open()?; |
| 145 |
let Some((_, sp)) = repo.current_policy()? else { |
| 146 |
anyhow::bail!("this repository has no policy"); |
| 147 |
}; |
| 148 |
let matches: Vec<_> = sp.policy.identities.iter().map(|i| i.hash()).filter(|k| k.starts_with_hex(key)).collect(); |
| 149 |
let full = match matches.len() { |
| 150 |
1 => matches[0], |
| 151 |
0 => anyhow::bail!("no key in the policy starts with {key:?}"), |
| 152 |
_ => anyhow::bail!("{key:?} is ambiguous"), |
| 153 |
}; |
| 154 |
let sp = repo.policy_revoke(&full)?; |
| 155 |
println!("Revoked {}; policy serial {}. Push to publish.", full.short(12), sp.policy.serial); |
| 156 |
Ok(0) |
| 157 |
} |