| 1 | |
|
| 2 | |
|
| 3 | | use super::ui; |
| 4 | | use anyhow::Result; |
| 5 | | use oxy_core::hash::{ChangeHash, KeyId}; |
| 6 | | use oxy_core::objects::Change; |
| 7 | | use oxy_core::repo::{Repo, SignatureStatus}; |
| 8 | | use oxy_core::state::ChangeMeta; |
| 9 | |
|
| 10 | 2× | pub fn short_id(c: &Change) -> String { |
| 11 | 2× | c.change_id.short(8) |
| 12 | 2× | } |
| 13 | |
|
| 14 | |
|
| 15 | 97× | pub fn plural(n: usize, word: &str) -> String { |
| 16 | 97× | format!("{n} {word}{}", if n == 1 { "" } else { "s" }) |
| 17 | 97× | } |
| 18 | |
|
| 19 | |
|
| 20 | 8× | pub fn when(ms: u64) -> String { |
| 21 | 8× | let secs = (ms / 1000) as i64; |
| 22 | 8× | let days = secs.div_euclid(86_400); |
| 23 | 8× | let rem = secs.rem_euclid(86_400); |
| 24 | | |
| 25 | 8× | let z = days + 719_468; |
| 26 | 8× | let era = z.div_euclid(146_097); |
| 27 | 8× | let doe = z - era * 146_097; |
| 28 | 8× | let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; |
| 29 | 8× | let y = yoe + era * 400; |
| 30 | 8× | let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); |
| 31 | 8× | let mp = (5 * doy + 2) / 153; |
| 32 | 8× | let d = doy - (153 * mp + 2) / 5 + 1; |
| 33 | 8× | let m = if mp < 10 { mp + 3 } else { mp - 9 }; |
| 34 | 8× | let y = if m <= 2 { y + 1 } else { y }; |
| 35 | 8× | format!( |
| 36 | | "{y:04}-{m:02}-{d:02} {:02}:{:02}:{:02}Z", |
| 37 | 8× | rem / 3600, |
| 38 | 8× | (rem % 3600) / 60, |
| 39 | 8× | rem % 60 |
| 40 | | ) |
| 41 | 8× | } |
| 42 | |
|
| 43 | |
|
| 44 | 2× | pub fn signature_line(repo: &Repo, h: &ChangeHash) -> Result<String> { |
| 45 | 2× | let status = repo.authenticate_change(h)?; |
| 46 | 2× | let awaiting = repo |
| 47 | 2× | .store() |
| 48 | 2× | .get_change(h) |
| 49 | 2× | .is_ok_and(|c| awaits_seal(repo, &status, &c.author)); |
| 50 | 1× | Ok(match status { |
| 51 | 0× | SignatureStatus::Unsigned if awaiting => { |
| 52 | 0× | "awaiting seal (the next push seals it under the agent's grant)".to_string() |
| 53 | | } |
| 54 | 1× | SignatureStatus::Unsigned => "unsigned".to_string(), |
| 55 | | SignatureStatus::Valid { |
| 56 | 1× | key, |
| 57 | 1× | name, |
| 58 | 1× | email, |
| 59 | 1× | sealed, |
| 60 | 1× | revoked, |
| 61 | | } => { |
| 62 | 1× | format!( |
| 63 | | "{} by {name} <{email}> key {}{}", |
| 64 | 1× | if sealed { "sealed" } else { "signed" }, |
| 65 | 1× | key.short(12), |
| 66 | 1× | if revoked { " (KEY REVOKED)" } else { "" } |
| 67 | | ) |
| 68 | | } |
| 69 | 0× | SignatureStatus::Invalid(why) => format!("INVALID signature: {why}"), |
| 70 | | }) |
| 71 | 2× | } |
| 72 | |
|
| 73 | |
|
| 74 | |
|
| 75 | | #[derive(Clone, Debug)] |
| 76 | | pub struct Via { |
| 77 | | pub name: String, |
| 78 | | pub email: String, |
| 79 | | pub key: KeyId, |
| 80 | | |
| 81 | | pub scope: Option<String>, |
| 82 | | pub expires: Option<u64>, |
| 83 | | pub revoked: bool, |
| 84 | | } |
| 85 | |
|
| 86 | | impl Via { |
| 87 | | |
| 88 | 0× | pub fn line(&self) -> String { |
| 89 | 0× | let delegation = match (&self.scope, self.revoked) { |
| 90 | 0× | (_, true) => ui::err("delegation revoked"), |
| 91 | 0× | (Some(scope), false) => ui::dim(format!( |
| 92 | | "delegate for {scope}{}", |
| 93 | 0× | self.expires |
| 94 | 0× | .map(|e| format!(", until {}", when(e))) |
| 95 | 0× | .unwrap_or_default() |
| 96 | | )), |
| 97 | 0× | (None, false) => ui::warn("not a delegate of the author now"), |
| 98 | | }; |
| 99 | 0× | format!( |
| 100 | | "{} {} <{}> · {} · key {}", |
| 101 | 0× | ui::glyph("↳", "->"), |
| 102 | 0× | ui::author(&self.name), |
| 103 | | self.email, |
| 104 | | delegation, |
| 105 | 0× | ui::hash(self.key.short(12)) |
| 106 | | ) |
| 107 | 0× | } |
| 108 | | } |
| 109 | |
|
| 110 | |
|
| 111 | |
|
| 112 | 27× | pub fn via(repo: &Repo, status: &SignatureStatus, author: &KeyId) -> Option<Via> { |
| 113 | 8× | match status { |
| 114 | | SignatureStatus::Valid { |
| 115 | 8× | key, |
| 116 | 8× | name, |
| 117 | 8× | email, |
| 118 | 8× | revoked, |
| 119 | | .. |
| 120 | 8× | } if !author.is_zero() && key != author => { |
| 121 | 0× | let entry = repo |
| 122 | 0× | .identity(author) |
| 123 | 0× | .ok() |
| 124 | 0× | .and_then(|a| a.delegate(key).copied()); |
| 125 | | |
| 126 | | |
| 127 | 0× | let certified = entry.is_none() && repo.certified_subject(key).is_some(); |
| 128 | | Some(Via { |
| 129 | 0× | name: name.clone(), |
| 130 | 0× | email: email.clone(), |
| 131 | 0× | key: *key, |
| 132 | 0× | scope: entry.map(|d| d.scope.to_string()), |
| 133 | 0× | expires: entry.and_then(|d| d.expires), |
| 134 | 0× | revoked: *revoked || (entry.is_none() && !certified), |
| 135 | | }) |
| 136 | | } |
| 137 | 27× | _ => None, |
| 138 | | } |
| 139 | 27× | } |
| 140 | |
|
| 141 | |
|
| 142 | |
|
| 143 | |
|
| 144 | |
|
| 145 | 5× | pub fn awaits_seal(repo: &Repo, status: &SignatureStatus, author: &oxy_core::hash::KeyId) -> bool { |
| 146 | 5× | matches!(status, SignatureStatus::Unsigned) |
| 147 | 4× | && repo.config.signer_keyless |
| 148 | 0× | && repo.config.author_key == Some(*author) |
| 149 | 5× | } |
| 150 | |
|
| 151 | |
|
| 152 | |
|
| 153 | |
|
| 154 | 3× | pub fn signature_mark_of(status: &SignatureStatus, awaiting: bool) -> String { |
| 155 | 3× | match status { |
| 156 | 0× | SignatureStatus::Unsigned if awaiting => { |
| 157 | 0× | ui::dim(format!("{} awaiting seal", ui::glyph("◌", "o"))) |
| 158 | | } |
| 159 | 3× | SignatureStatus::Unsigned => ui::dim(format!("{} unsigned", ui::glyph("·", "-"))), |
| 160 | | SignatureStatus::Valid { |
| 161 | 0× | sealed, |
| 162 | | revoked: true, |
| 163 | | .. |
| 164 | 0× | } => ui::warn(format!( |
| 165 | | "{} {} (key revoked)", |
| 166 | 0× | ui::glyph("⚠", "!"), |
| 167 | 0× | if *sealed { "sealed" } else { "signed" } |
| 168 | | )), |
| 169 | 0× | SignatureStatus::Valid { sealed, .. } => ui::ok(format!( |
| 170 | | "{} {}", |
| 171 | 0× | ui::glyph("✔", "v"), |
| 172 | 0× | if *sealed { "sealed" } else { "signed" } |
| 173 | | )), |
| 174 | 0× | SignatureStatus::Invalid(why) => ui::err(format!("{} INVALID: {why}", ui::glyph("✖", "x"))), |
| 175 | | } |
| 176 | 3× | } |
| 177 | |
|
| 178 | |
|
| 179 | |
|
| 180 | 28× | pub fn author_name(repo: &Repo, c: &ChangeMeta) -> String { |
| 181 | 28× | if !c.author.is_zero() { |
| 182 | 11× | if let Ok(id) = repo.identity(&c.author) { |
| 183 | 11× | return id.name; |
| 184 | 0× | } |
| 185 | 0× | return c.author.short(12); |
| 186 | 17× | } |
| 187 | 17× | c.message |
| 188 | 17× | .lines() |
| 189 | 17× | .rev() |
| 190 | 19× | .find_map(|l| l.strip_prefix("Git-Author: ")) |
| 191 | 17× | .map(|a| a.split('<').next().unwrap_or(a).trim().to_string()) |
| 192 | 17× | .unwrap_or_else(|| "unknown".into()) |
| 193 | 28× | } |
| 194 | |
|
| 195 | |
|
| 196 | 82× | pub fn subject_of(message: &str) -> &str { |
| 197 | 82× | match message.lines().next() { |
| 198 | 82× | Some(l) if !l.trim().is_empty() => l, |
| 199 | 0× | _ => "(no description)", |
| 200 | | } |
| 201 | 82× | } |
| 202 | |
|
| 203 | 2× | pub fn deps_line(c: &Change) -> String { |
| 204 | 2× | deps_of(&c.deps) |
| 205 | 2× | } |
| 206 | |
|
| 207 | 2× | pub fn deps_of(deps: &[oxy_core::hash::ChangeHash]) -> String { |
| 208 | 2× | deps.iter() |
| 209 | 2× | .map(|d| format!("#{}", d.short(12))) |
| 210 | 2× | .collect::<Vec<_>>() |
| 211 | 2× | .join(" ") |
| 212 | 2× | } |
| 213 | |
|
| 214 | | #[cfg(test)] |
| 215 | | mod tests { |
| 216 | | use super::*; |
| 217 | |
|
| 218 | | #[test] |
| 219 | 1× | fn dates() { |
| 220 | 1× | assert_eq!(when(0), "1970-01-01 00:00:00Z"); |
| 221 | 1× | assert_eq!(when(1_700_000_000_000), "2023-11-14 22:13:20Z"); |
| 222 | 1× | assert_eq!(plural(1, "head"), "1 head"); |
| 223 | 1× | assert_eq!(plural(2, "head"), "2 heads"); |
| 224 | 1× | } |
| 225 | | } |