State is Send + Sync; shared lane layout; log_window 119bbf50cb86 resolution
The closure cache sits behind a mutex so hosts can cache loaded states across threads. oxy_core::lanes::layout is the one graph layout for oxy log and the hub. Repo::log_window paginates a state's log.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-27 18:02 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
119bbf50cb86995038a36a89f4315828bc40be3ecb6e7927b85f50c135048610- Depends on
- 1983686fb7a7 252e8cb23fbc 55e5a16a2b9c 750251b21a96 75167672ebcd 7aa2dc9369e3 83e0677b07fe 96f7610fa666 982864068a59 b0e64e81e4fb c3069537d7c5 e02bf0cd0533 fce44ae9ad29 ff0af1ee299d
- Depended on by
- c2e1d09047db a39754d02c58 aedfe465b611 d45dab0484ea 2bf22305bc48 f6451fbbae3f 01a47cd7f0c8 e8d38e47e9d7 0bdc3f91a39f eae595568832 9d333fc9ca1b
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 31 atoms, 1 tree operation, 10 files
Browse the tree at this change
added crates/oxy-core/src/lanes.rs +114 −0
| 1 | + | //! Lane layout for drawing history as a graph: each change takes a lane, |
|
| 2 | + | //! its first dependency continues that lane, other dependencies take free |
|
| 3 | + | //! lanes, so edges flow from a change down to what it depends on. Shared |
|
| 4 | + | //! by `oxy log` and hosts, so every view draws the same picture. |
|
| 5 | + | ||
| 6 | + | use crate::hash::ChangeHash; |
|
| 7 | + | use std::collections::{HashMap, HashSet}; |
|
| 8 | + | ||
| 9 | + | /// A row of the graph: the change's lane and the edges drawn from it. |
|
| 10 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 11 | + | pub struct Row { |
|
| 12 | + | pub lane: usize, |
|
| 13 | + | /// `(from_lane, to_lane)` for each dependency drawn from this row to |
|
| 14 | + | /// the rows below. |
|
| 15 | + | pub edges: Vec<(usize, usize)>, |
|
| 16 | + | /// Lanes that pass through this row without touching it. |
|
| 17 | + | pub through: Vec<usize>, |
|
| 18 | + | } |
|
| 19 | + | ||
| 20 | + | /// `changes` newest first as `(hash, deps)`. A dependency outside the |
|
| 21 | + | /// list ends its lane at the bottom. |
|
| 22 | + | pub fn layout(changes: &[(ChangeHash, Vec<ChangeHash>)]) -> Vec<Row> { |
|
| 23 | + | let mut active: Vec<Option<ChangeHash>> = Vec::new(); |
|
| 24 | + | let mut rows = Vec::with_capacity(changes.len()); |
|
| 25 | + | let known: HashSet<&ChangeHash> = changes.iter().map(|(h, _)| h).collect(); |
|
| 26 | + | let mut lanes: HashMap<ChangeHash, usize> = HashMap::new(); |
|
| 27 | + | for (hash, deps) in changes { |
|
| 28 | + | let lane = match active.iter().position(|a| a.as_ref() == Some(hash)) { |
|
| 29 | + | Some(l) => l, |
|
| 30 | + | None => match active.iter().position(|a| a.is_none()) { |
|
| 31 | + | Some(l) => { |
|
| 32 | + | active[l] = Some(*hash); |
|
| 33 | + | l |
|
| 34 | + | } |
|
| 35 | + | None => { |
|
| 36 | + | active.push(Some(*hash)); |
|
| 37 | + | active.len() - 1 |
|
| 38 | + | } |
|
| 39 | + | }, |
|
| 40 | + | }; |
|
| 41 | + | lanes.insert(*hash, lane); |
|
| 42 | + | let mut edges = Vec::new(); |
|
| 43 | + | let deps: Vec<&ChangeHash> = deps.iter().filter(|d| known.contains(d)).collect(); |
|
| 44 | + | active[lane] = None; |
|
| 45 | + | for (i, d) in deps.iter().enumerate() { |
|
| 46 | + | let target = match active.iter().position(|a| a.as_ref() == Some(*d)) { |
|
| 47 | + | Some(l) => l, |
|
| 48 | + | None => { |
|
| 49 | + | let l = if i == 0 && active[lane].is_none() { |
|
| 50 | + | lane |
|
| 51 | + | } else { |
|
| 52 | + | match active.iter().position(|a| a.is_none()) { |
|
| 53 | + | Some(l) => l, |
|
| 54 | + | None => { |
|
| 55 | + | active.push(None); |
|
| 56 | + | active.len() - 1 |
|
| 57 | + | } |
|
| 58 | + | } |
|
| 59 | + | }; |
|
| 60 | + | active[l] = Some(**d); |
|
| 61 | + | l |
|
| 62 | + | } |
|
| 63 | + | }; |
|
| 64 | + | edges.push((lane, target)); |
|
| 65 | + | } |
|
| 66 | + | let through: Vec<usize> = active |
|
| 67 | + | .iter() |
|
| 68 | + | .enumerate() |
|
| 69 | + | .filter(|(l, a)| a.is_some() && *l != lane && !edges.iter().any(|e| e.1 == *l)) |
|
| 70 | + | .map(|(l, _)| l) |
|
| 71 | + | .collect(); |
|
| 72 | + | rows.push(Row { lane, edges, through }); |
|
| 73 | + | while active.last().is_some_and(|a| a.is_none()) { |
|
| 74 | + | active.pop(); |
|
| 75 | + | } |
|
| 76 | + | } |
|
| 77 | + | rows |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | /// The number of lanes the rows use. |
|
| 81 | + | pub fn width(rows: &[Row]) -> usize { |
|
| 82 | + | rows.iter() |
|
| 83 | + | .map(|r| r.lane.max(r.edges.iter().map(|e| e.1).max().unwrap_or(0)).max(r.through.iter().copied().max().unwrap_or(0))) |
|
| 84 | + | .max() |
|
| 85 | + | .map(|m| m + 1) |
|
| 86 | + | .unwrap_or(1) |
|
| 87 | + | } |
|
| 88 | + | ||
| 89 | + | #[cfg(test)] |
|
| 90 | + | mod tests { |
|
| 91 | + | use super::*; |
|
| 92 | + | use crate::hash::ObjectRef; |
|
| 93 | + | ||
| 94 | + | fn h(n: u8) -> ChangeHash { |
|
| 95 | + | ObjectRef([n; 32]) |
|
| 96 | + | } |
|
| 97 | + | ||
| 98 | + | #[test] |
|
| 99 | + | fn linear_history_is_one_lane() { |
|
| 100 | + | let rows = layout(&[(h(3), vec![h(2)]), (h(2), vec![h(1)]), (h(1), vec![])]); |
|
| 101 | + | assert!(rows.iter().all(|r| r.lane == 0), "{rows:?}"); |
|
| 102 | + | assert_eq!(rows[0].edges, vec![(0, 0)]); |
|
| 103 | + | assert_eq!(width(&rows), 1); |
|
| 104 | + | } |
|
| 105 | + | ||
| 106 | + | #[test] |
|
| 107 | + | fn a_diamond_takes_two_lanes_and_rejoins() { |
|
| 108 | + | let rows = layout(&[(h(4), vec![h(2), h(3)]), (h(2), vec![h(1)]), (h(3), vec![h(1)]), (h(1), vec![])]); |
|
| 109 | + | assert_eq!(rows[0].edges, vec![(0, 0), (0, 1)]); |
|
| 110 | + | assert_eq!(rows[2].lane, 1); |
|
| 111 | + | assert_eq!(rows[2].edges, vec![(1, 0)], "the second branch joins the first lane"); |
|
| 112 | + | assert_eq!(width(&rows), 2); |
|
| 113 | + | } |
|
| 114 | + | } |
modified crates/oxy-core/src/identity.rs +19 −4
| … | 502 unchanged lines |
||
| 503 | 503 | } |
|
| 504 | 504 | ||
| 505 | 505 | /// Check a challenge signature against the identity that claims it. |
|
| 506 | - | pub fn verify_challenge(identity: &Identity, sig: &Signature, challenge: &[u8]) -> Result<(), VerifyError> { |
|
| 506 | + | pub fn verify_challenge( |
|
| 507 | + | identity: &Identity, |
|
| 508 | + | sig: &Signature, |
|
| 509 | + | challenge: &[u8], |
|
| 510 | + | ) -> Result<(), VerifyError> { |
|
| 507 | 511 | if challenge.len() < 16 { |
|
| 508 | 512 | return Err(VerifyError::ShortChallenge); |
|
| 509 | 513 | } |
|
| 510 | 514 | if sig.key != identity.hash() { |
|
| 511 | 515 | return Err(VerifyError::KeyMismatch); |
|
| 512 | 516 | } |
|
| 513 | - | verify(identity, sig, Domain::Challenge, &challenge_target(challenge)) |
|
| 517 | + | verify( |
|
| 518 | + | identity, |
|
| 519 | + | sig, |
|
| 520 | + | Domain::Challenge, |
|
| 521 | + | &challenge_target(challenge), |
|
| 522 | + | ) |
|
| 514 | 523 | } |
|
| 515 | 524 | ||
| 516 | 525 | #[cfg(test)] |
|
| … | 8 unchanged lines |
||
| 525 | 534 | let challenge = [7u8; 32]; |
|
| 526 | 535 | let sig = sk.sign_challenge(&key, &challenge).unwrap(); |
|
| 527 | 536 | assert!(verify_challenge(&id, &sig, &challenge).is_ok()); |
|
| 528 | - | assert!(verify_challenge(&id, &sig, &[8u8; 32]).is_err(), "other bytes"); |
|
| 537 | + | assert!( |
|
| 538 | + | verify_challenge(&id, &sig, &[8u8; 32]).is_err(), |
|
| 539 | + | "other bytes" |
|
| 540 | + | ); |
|
| 529 | 541 | let other = SecretKey::generate().identity("B", "b@example", 1); |
|
| 530 | - | assert!(verify_challenge(&other, &sig, &challenge).is_err(), "other identity"); |
|
| 542 | + | assert!( |
|
| 543 | + | verify_challenge(&other, &sig, &challenge).is_err(), |
|
| 544 | + | "other identity" |
|
| 545 | + | ); |
|
| 531 | 546 | assert!(sk.sign_challenge(&key, b"short").is_err()); |
|
| 532 | 547 | // A challenge target never equals a change signature's target for the same bytes. |
|
| 533 | 548 | assert_ne!(challenge_target(&challenge), ObjectRef(challenge)); |
|
| … | 2 unchanged lines |
modified crates/oxy-core/src/lib.rs +1 −0
| … | 10 unchanged lines |
||
| 11 | 11 | pub mod hash; |
|
| 12 | 12 | pub mod identity; |
|
| 13 | 13 | pub mod ids; |
|
| 14 | + | pub mod lanes; |
|
| 14 | 15 | pub mod objects; |
|
| 15 | 16 | pub mod oplog; |
|
| 16 | 17 | pub mod policy; |
|
| … | 7 unchanged lines |
modified crates/oxy-core/src/repo/mod.rs +8 −0
| … | 1079 unchanged lines |
||
| 1080 | 1080 | Ok(entries) |
|
| 1081 | 1081 | } |
|
| 1082 | 1082 | ||
| 1083 | + | /// A window of `log_state`: entries `offset..offset+limit` and the total |
|
| 1084 | + | /// count, for paginated views. |
|
| 1085 | + | pub fn log_window(&self, heads: &[ChangeHash], offset: usize, limit: usize) -> Result<(Vec<LogEntry>, usize)> { |
|
| 1086 | + | let all = self.log_state(heads)?; |
|
| 1087 | + | let total = all.len(); |
|
| 1088 | + | Ok((all.into_iter().skip(offset).take(limit).collect(), total)) |
|
| 1089 | + | } |
|
| 1090 | + | ||
| 1083 | 1091 | /// The log of the state with these heads, newest first, with no working |
|
| 1084 | 1092 | /// copy involved (for hosted repositories and any explicit state). |
|
| 1085 | 1093 | pub fn log_state(&self, heads: &[ChangeHash]) -> Result<Vec<LogEntry>> { |
|
| … | 136 unchanged lines |
modified crates/oxy-core/src/repo/sync.rs +14 −3
| … | 465 unchanged lines |
||
| 466 | 466 | policy: Option<SignedPolicy>, |
|
| 467 | 467 | tags: Vec<crate::protocol::TagRef>, |
|
| 468 | 468 | ) -> Result<Vec<RefResult>> { |
|
| 469 | - | Ok(self.receive_push_report(bundle, updates, policy, tags)?.results) |
|
| 469 | + | Ok(self |
|
| 470 | + | .receive_push_report(bundle, updates, policy, tags)? |
|
| 471 | + | .results) |
|
| 470 | 472 | } |
|
| 471 | 473 | ||
| 472 | 474 | /// `receive_push` with what a host wants to log: each accepted |
|
| … | 59 unchanged lines |
||
| 532 | 534 | }; |
|
| 533 | 535 | if ok { |
|
| 534 | 536 | names.push(u.name.clone()); |
|
| 535 | - | let heads = self.store.get_state(&u.new_state).map(|s| s.heads).unwrap_or_default(); |
|
| 536 | - | report.updated.push(RefUpdated { name: u.name.clone(), old_state: u.old_state, new_state: u.new_state, heads }); |
|
| 537 | + | let heads = self |
|
| 538 | + | .store |
|
| 539 | + | .get_state(&u.new_state) |
|
| 540 | + | .map(|s| s.heads) |
|
| 541 | + | .unwrap_or_default(); |
|
| 542 | + | report.updated.push(RefUpdated { |
|
| 543 | + | name: u.name.clone(), |
|
| 544 | + | old_state: u.old_state, |
|
| 545 | + | new_state: u.new_state, |
|
| 546 | + | heads, |
|
| 547 | + | }); |
|
| 537 | 548 | } |
|
| 538 | 549 | results.push(RefResult { |
|
| 539 | 550 | name: u.name, |
|
| … | 877 unchanged lines |
modified crates/oxy-core/src/state.rs +22 −9
| … | 9 unchanged lines |
||
| 10 | 10 | self as o, AtomRef, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, ObjectError, TreeOp, |
|
| 11 | 11 | }; |
|
| 12 | 12 | use crate::tree::{TreeGraph, TreeView}; |
|
| 13 | - | use std::cell::RefCell; |
|
| 13 | + | use std::sync::{Arc, Mutex}; |
|
| 14 | 14 | use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; |
|
| 15 | - | use std::rc::Rc; |
|
| 16 | 15 | ||
| 17 | 16 | #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] |
|
| 18 | 17 | pub enum StateError { |
|
| … | 29 unchanged lines |
||
| 48 | 47 | } |
|
| 49 | 48 | } |
|
| 50 | 49 | ||
| 51 | - | #[derive(Default, Clone)] |
|
| 50 | + | #[derive(Default)] |
|
| 52 | 51 | pub struct State { |
|
| 53 | 52 | changes: HashMap<ChangeHash, ChangeMeta>, |
|
| 54 | 53 | tree: TreeGraph, |
|
| 55 | 54 | files: HashMap<NodeId, FileGraph>, |
|
| 56 | 55 | replaces: HashMap<NodeId, Vec<ReplaceRecord>>, |
|
| 57 | - | closure: RefCell<HashMap<ChangeHash, Rc<HashSet<ChangeHash>>>>, |
|
| 56 | + | /// Memoised transitive dependencies. A mutex, not a `RefCell`, so a |
|
| 57 | + | /// loaded state can be shared across threads by a host that caches it. |
|
| 58 | + | closure: Mutex<HashMap<ChangeHash, Arc<HashSet<ChangeHash>>>>, |
|
| 58 | 59 | } |
|
| 59 | 60 | ||
| 61 | + | impl Clone for State { |
|
| 62 | + | fn clone(&self) -> State { |
|
| 63 | + | State { |
|
| 64 | + | changes: self.changes.clone(), |
|
| 65 | + | tree: self.tree.clone(), |
|
| 66 | + | files: self.files.clone(), |
|
| 67 | + | replaces: self.replaces.clone(), |
|
| 68 | + | closure: Mutex::new(self.closure.lock().unwrap().clone()), |
|
| 69 | + | } |
|
| 70 | + | } |
|
| 71 | + | } |
|
| 72 | + | ||
| 60 | 73 | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 61 | 74 | pub enum FileContent { |
|
| 62 | 75 | Text { |
|
| … | 130 unchanged lines |
||
| 193 | 206 | } |
|
| 194 | 207 | self.apply(h, &change); |
|
| 195 | 208 | self.changes.insert(h, ChangeMeta::of(&change)); |
|
| 196 | - | self.closure.borrow_mut().clear(); |
|
| 209 | + | self.closure.lock().unwrap().clear(); |
|
| 197 | 210 | h |
|
| 198 | 211 | } |
|
| 199 | 212 | ||
| … | 179 unchanged lines |
||
| 379 | 392 | } |
|
| 380 | 393 | ||
| 381 | 394 | /// Transitive dependencies of `h`, excluding `h` itself. |
|
| 382 | - | pub fn closure(&self, h: &ChangeHash) -> Rc<HashSet<ChangeHash>> { |
|
| 383 | - | if let Some(c) = self.closure.borrow().get(h) { |
|
| 395 | + | pub fn closure(&self, h: &ChangeHash) -> Arc<HashSet<ChangeHash>> { |
|
| 396 | + | if let Some(c) = self.closure.lock().unwrap().get(h) { |
|
| 384 | 397 | return c.clone(); |
|
| 385 | 398 | } |
|
| 386 | 399 | let mut out = HashSet::new(); |
|
| … | 9 unchanged lines |
||
| 396 | 409 | } |
|
| 397 | 410 | } |
|
| 398 | 411 | } |
|
| 399 | - | let rc = Rc::new(out); |
|
| 400 | - | self.closure.borrow_mut().insert(*h, rc.clone()); |
|
| 412 | + | let rc = Arc::new(out); |
|
| 413 | + | self.closure.lock().unwrap().insert(*h, rc.clone()); |
|
| 401 | 414 | rc |
|
| 402 | 415 | } |
|
| 403 | 416 | ||
| … | 461 unchanged lines |
modified crates/oxy-server/tests/http.rs +4 −1
| … | 468 unchanged lines |
||
| 469 | 469 | client.config.set("remote.origin.token", "s3cret").unwrap(); |
|
| 470 | 470 | let results = client.push("origin", false).unwrap(); |
|
| 471 | 471 | assert!(results.iter().all(|r| r.ok), "{results:?}"); |
|
| 472 | - | assert_eq!(pushes.lock().unwrap().as_slice(), ["andy/app: 1 ok, 1 updated to 1 head(s), 1 imported"]); |
|
| 472 | + | assert_eq!( |
|
| 473 | + | pushes.lock().unwrap().as_slice(), |
|
| 474 | + | ["andy/app: 1 ok, 1 updated to 1 head(s), 1 imported"] |
|
| 475 | + | ); |
|
| 473 | 476 | ||
| 474 | 477 | let hosted = Repo::open(&root.join("repos/andy/app")).unwrap(); |
|
| 475 | 478 | assert_eq!(hosted.bookmark_heads().unwrap().len(), 1); |
|
| … | 134 unchanged lines |
modified docs/plan.md +5 −5
| … | 88 unchanged lines |
||
| 89 | 89 | - [ ] `oxy seal <hash>..` and `--proposal URL` (hub 0.3). |
|
| 90 | 90 | - [ ] `oxy proposal show|land URL` (hub 0.3). |
|
| 91 | 91 | - [ ] Delegate-signed changes from an edit set without a workspace (hub 0.6). |
|
| 92 | - | - [ ] `Repo::log_window` and a shared `graph::layout` (hub 0.1 uses `log_state` and its own layout until then). |
|
| 92 | + | - [x] `Repo::log_window` and the shared `oxy_core::lanes::layout`; `State` is `Send + Sync` (closure cache behind a mutex) so hosts can cache loaded states. |
|
| 93 | 93 | - [x] `receive_push_report` with `PushReport { results, updated: [RefUpdated { name, old_state, new_state, heads }], imported }`; `Backend::after_push` receives it. |
|
| 94 | - | <<<<<<< oxy conflict 1 of 1, side 1 of 2: deleted by change 75f82481 "Challenge signatures and a push report for hosts" |
|
| 95 | - | ------- |
|
| 96 | - | - [ ] `receive_push` reporting old/new state and new changes per reference (hub 0.2 pushes log). |
|
| 97 | - | ======= oxy conflict 1 of 1, side 2 of 2: change d900550d "Roadmap across oxygen, oxygen-vscode, oxygenhub, oxygenhub-vscode and the runner" |
|
| 94 | + | ||
| 95 | + | ## Roadmap |
|
| 96 | + | ||
| 97 | + | The cross-product order of work is in `docs/roadmap.md`. |
|
| 98 | 98 | ||
| 99 | 99 | ## Roadmap |
|
| 100 | 100 | ||
| … | 2 unchanged lines |
modified src/cli/identity.rs +14 −4
| … | 738 unchanged lines |
||
| 739 | 739 | let Some((key, sk)) = repo.secret_key()? else { |
|
| 740 | 740 | anyhow::bail!("no secret key: run `oxy identity create` first"); |
|
| 741 | 741 | }; |
|
| 742 | - | let challenge = hex_bytes(challenge_hex.trim()).ok_or_else(|| anyhow::anyhow!("the challenge must be hex"))?; |
|
| 743 | - | let sig = sk.sign_challenge(&key, &challenge).map_err(|e| anyhow::anyhow!("{e}"))?; |
|
| 742 | + | let challenge = hex_bytes(challenge_hex.trim()) |
|
| 743 | + | .ok_or_else(|| anyhow::anyhow!("the challenge must be hex"))?; |
|
| 744 | + | let sig = sk |
|
| 745 | + | .sign_challenge(&key, &challenge) |
|
| 746 | + | .map_err(|e| anyhow::anyhow!("{e}"))?; |
|
| 744 | 747 | let encoded = hex_string(&oxy_core::identity::Signature::encode_list(&[sig])); |
|
| 745 | 748 | if as_json { |
|
| 746 | - | println!("{{\"key\":\"{}\",\"signature\":\"{}\"}}", key.to_hex(), encoded); |
|
| 749 | + | println!( |
|
| 750 | + | "{{\"key\":\"{}\",\"signature\":\"{}\"}}", |
|
| 751 | + | key.to_hex(), |
|
| 752 | + | encoded |
|
| 753 | + | ); |
|
| 747 | 754 | } else { |
|
| 748 | 755 | println!("{encoded}"); |
|
| 749 | 756 | } |
|
| … | 4 unchanged lines |
||
| 754 | 761 | if !s.len().is_multiple_of(2) { |
|
| 755 | 762 | return None; |
|
| 756 | 763 | } |
|
| 757 | - | (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect() |
|
| 764 | + | (0..s.len()) |
|
| 765 | + | .step_by(2) |
|
| 766 | + | .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()) |
|
| 767 | + | .collect() |
|
| 758 | 768 | } |
|
| 759 | 769 | ||
| 760 | 770 | fn hex_string(b: &[u8]) -> String { |
|
| … | 2 unchanged lines |
modified src/cli/mod.rs +3 −1
| … | 728 unchanged lines |
||
| 729 | 729 | } => identity::create(name, email, local, expires), |
|
| 730 | 730 | IdentityCmd::Show => identity::show(), |
|
| 731 | 731 | IdentityCmd::Export { path } => identity::export(path), |
|
| 732 | - | IdentityCmd::SignChallenge { challenge, json } => identity::sign_challenge(&challenge, json), |
|
| 732 | + | IdentityCmd::SignChallenge { challenge, json } => { |
|
| 733 | + | identity::sign_challenge(&challenge, json) |
|
| 734 | + | } |
|
| 733 | 735 | IdentityCmd::Rotate => identity::rotate(), |
|
| 734 | 736 | IdentityCmd::Renew { expires } => identity::renew(expires), |
|
| 735 | 737 | IdentityCmd::Revoke { key, reason } => identity::revoke(key.as_deref(), &reason), |
|
| … | 46 unchanged lines |