OXYGENHUB
# 978470f76e66 oxygen / src / cli / identity.rs
6.0 KiB · Rust · last changed Pack merging above 16 packs; modern CLI presentation (colour, spinners, --plain, NO_COLOR); oxy prompt with manifest-based dirty check and docs/prompt.md 1 month ago source blame history raw
1
//! Identity, verification, and configuration commands.
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!("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 {
54
        let c = repo.store().get_change(&h)?;
55
        let line = signature_line(&repo, &h)?;
56
        let styled = if line.starts_with("INVALID") {
57
            bad += 1;
58
            ui::err(&line)
59
        } else if line == "unsigned" {
60
            ui::dim(&line)
61
        } else {
62
            ui::ok(&line)
63
        };
64
        println!("{} {}  {styled}", ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12))));
65
    }
66
    Ok(if bad > 0 { 1 } else { 0 })
67
}
68
69
pub fn config(key: &str, value: &str, user: bool) -> Result<i32> {
70
    if user {
71
        let path = repo::default_user_config();
72
        repo::Config::set_user(&path, key, value)?;
73
        println!("{key} = {value} (in {})", path.display());
74
    } else {
75
        let mut repo = open()?;
76
        repo.config.set(key, value)?;
77
        repo.save_config()?;
78
        println!("{key} = {value} (in this repository)");
79
    }
80
    Ok(0)
81
}
82
83
pub fn export(path: Option<PathBuf>) -> Result<i32> {
84
    let repo = open()?;
85
    let Some(key) = repo.config.author_key else {
86
        anyhow::bail!("no identity configured; run `oxy identity create`");
87
    };
88
    let id = repo.identity(&key)?;
89
    let path = path.unwrap_or_else(|| PathBuf::from(format!("{}.identity", key.short(16))));
90
    std::fs::write(&path, id.encode())?;
91
    println!("Wrote identity {} <{}> ({}) to {}", id.name, id.email, key.short(12), path.display());
92
    Ok(0)
93
}
94
95
pub fn policy_init() -> Result<i32> {
96
    let repo = open()?;
97
    let sp = repo.policy_init()?;
98
    println!("Created policy serial {} with root key {}; push to publish it.", sp.policy.serial, sp.policy.roots[0].short(12));
99
    Ok(0)
100
}
101
102
pub fn policy_show() -> Result<i32> {
103
    let repo = open()?;
104
    let Some((r, sp)) = repo.current_policy()? else {
105
        println!("No policy: this repository is unverified. Run `oxy policy init` to create one.");
106
        return Ok(0);
107
    };
108
    let p = &sp.policy;
109
    let name = |k: &oxy_core::hash::KeyId| p.identity(k).map(|i| format!("{} <{}>", i.name, i.email)).unwrap_or_else(|| "(unknown identity)".into());
110
    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 { "" });
111
    println!("roots (threshold {}):", p.root_threshold);
112
    for k in &p.roots {
113
        println!("  {}  {}", k.short(12), name(k));
114
    }
115
    println!("rules:");
116
    for rule in &p.rules {
117
        println!("  {:12} threshold {} require {} conflicts {}", rule.pattern, rule.threshold, rule.require.name(), if rule.allow_conflicts { "allowed" } else { "refused" });
118
        for k in &rule.keys {
119
            println!("      {}  {}", k.short(12), name(k));
120
        }
121
    }
122
    if !p.revoked.is_empty() {
123
        println!("revoked:");
124
        for k in &p.revoked {
125
            println!("  {}  {}", k.short(12), name(k));
126
        }
127
    }
128
    Ok(0)
129
}
130
131
pub fn policy_grant(identity_file: &Path, pattern: Option<&str>) -> Result<i32> {
132
    let repo = open()?;
133
    let id = Identity::decode(&std::fs::read(identity_file)?)?;
134
    let key = id.hash();
135
    let sp = repo.policy_grant(id.clone(), pattern)?;
136
    println!("Granted {} <{}> ({}) on {}; policy serial {}. Push to publish.", id.name, id.email, key.short(12), pattern.unwrap_or("*"), sp.policy.serial);
137
    Ok(0)
138
}
139
140
pub fn policy_revoke(key: &str) -> Result<i32> {
141
    let repo = open()?;
142
    let Some((_, sp)) = repo.current_policy()? else {
143
        anyhow::bail!("this repository has no policy");
144
    };
145
    let matches: Vec<_> = sp.policy.identities.iter().map(|i| i.hash()).filter(|k| k.starts_with_hex(key)).collect();
146
    let full = match matches.len() {
147
        1 => matches[0],
148
        0 => anyhow::bail!("no key in the policy starts with {key:?}"),
149
        _ => anyhow::bail!("{key:?} is ambiguous"),
150
    };
151
    let sp = repo.policy_revoke(&full)?;
152
    println!("Revoked {}; policy serial {}. Push to publish.", full.short(12), sp.policy.serial);
153
    Ok(0)
154
}