OXYGENHUB
# 94084087a876 oxygen / src / cli / identity.rs
7.3 KiB · Rust · last changed CLI: modern output across all commands: rail-and-bullet log with -n/-v, relative times, authors, signature marks; glyphs on UTF-8 terminals with ASCII fallback 1 month ago source blame history raw
fce44ae9 Andy McCall ↶ 1
//! Identity, verification, and configuration commands.
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)?;
94084087 Andy McCall ↶ 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())));
fce44ae9 Andy McCall ↶ 21
    match scope {
94084087 Andy McCall ↶ 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")),
fce44ae9 Andy McCall ↶ 24
    }
25
    if !repo.config.sign_changes {
94084087 Andy McCall ↶ 26
        println!("  {} {}", ui::note(), ui::dim("changes will carry this author; run oxy config --user core.sign change to sign each commit"));
fce44ae9 Andy McCall ↶ 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)?;
94084087 Andy McCall ↶ 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)" }));
fce44ae9 Andy McCall ↶ 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;
94084087 Andy McCall ↶ 61
            ui::err(format!("{} {line}", ui::glyph("✖", "x")))
135212de Andy McCall ↶ 62
        } else if line == "unsigned" {
94084087 Andy McCall ↶ 63
            ui::dim(format!("{} {line}", ui::glyph("·", "-")))
135212de Andy McCall ↶ 64
        } else {
94084087 Andy McCall ↶ 65
            ui::ok(format!("{} {line}", ui::glyph("✔", "v")))
135212de Andy McCall ↶ 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)?;
94084087 Andy McCall ↶ 76
        println!("{} {key} = {value} {}", ui::done("Set"), ui::dim(format!("(in {})", path.display())));
fce44ae9 Andy McCall ↶ 77
    } else {
78
        let mut repo = open()?;
79
        repo.config.set(key, value)?;
80
        repo.save_config()?;
94084087 Andy McCall ↶ 81
        println!("{} {key} = {value} {}", ui::done("Set"), ui::dim("(in this repository)"));
fce44ae9 Andy McCall ↶ 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())?;
94084087 Andy McCall ↶ 94
    println!("{} identity {} <{}> ({}) to {}", ui::done("Wrote"), ui::author(&id.name), id.email, key.short(12), ui::path(path.display()));
83e0677b Andy McCall ↶ 95
    Ok(0)
96
}
97
98
pub fn policy_init() -> Result<i32> {
99
    let repo = open()?;
100
    let sp = repo.policy_init()?;
94084087 Andy McCall ↶ 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)"));
83e0677b Andy McCall ↶ 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());
94084087 Andy McCall ↶ 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)))));
83e0677b Andy McCall ↶ 116
    for k in &p.roots {
94084087 Andy McCall ↶ 117
        println!("  {} {}  {}", ui::ok(ui::glyph("🔑", "*")), ui::hash(k.short(12)), ui::author(name(k)));
83e0677b Andy McCall ↶ 118
    }
94084087 Andy McCall ↶ 119
    println!("{}", ui::section("rules"));
83e0677b Andy McCall ↶ 120
    for rule in &p.rules {
94084087 Andy McCall ↶ 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" })));
83e0677b Andy McCall ↶ 122
        for k in &rule.keys {
94084087 Andy McCall ↶ 123
            println!("      {}  {}", ui::hash(k.short(12)), ui::author(name(k)));
83e0677b Andy McCall ↶ 124
        }
125
    }
126
    if !p.revoked.is_empty() {
94084087 Andy McCall ↶ 127
        println!("{}", ui::section("revoked"));
83e0677b Andy McCall ↶ 128
        for k in &p.revoked {
94084087 Andy McCall ↶ 129
            println!("  {} {}  {}", ui::err(ui::glyph("✖", "x")), ui::hash(k.short(12)), ui::author(name(k)));
83e0677b Andy McCall ↶ 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)?;
94084087 Andy McCall ↶ 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)));
83e0677b Andy McCall ↶ 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)?;
94084087 Andy McCall ↶ 156
    println!("{} {} {}", ui::done("Revoked"), ui::hash(full.short(12)), ui::dim(format!("(policy serial {}; push to publish)", sp.policy.serial)));
83e0677b Andy McCall ↶ 157
    Ok(0)
158
}