crates/cli/git-ents/src/commands/review.rs
review.rshistorycomment on this file
| 1 | //! `git ents review`: a thin wrapper around `ents_forge::review`'s |
| 2 | //! business logic — this module only resolves the signer/actor identity |
| 3 | //! against [`LocalRoot`] (plus, for [`new`], the reviewer's own member id — |
| 4 | //! the composite key's `<member>` segment, `meta-ref.identity-binding`) and |
| 5 | //! translates a reached `Outcome` into a CLI-facing [`Result`] |
| 6 | //! (`crate::mutate::outcome_to_result`), exactly as every other mutation |
| 7 | //! command does. Every operation is the library call itself |
| 8 | //! (`lens.parity`); nothing here re-implements one. |
| 9 | |
| 10 | use ents_forge::comment::Comment; |
| 11 | use ents_forge::review; |
| 12 | use ents_forge::review::{NewReview, Review}; |
| 13 | use ents_model::MemberId; |
| 14 | use ents_receive::Identity; |
| 15 | |
| 16 | use super::{actor, signer}; |
| 17 | use crate::error::Result; |
| 18 | use crate::mutate::outcome_to_result; |
| 19 | use crate::root::LocalRoot; |
| 20 | |
| 21 | /// `git ents review new`: review a commit as the signer's own member, |
| 22 | /// writing both its entity ref and its retention pin — or, when this |
| 23 | /// member already has a review of an ancestor of the target, advancing |
| 24 | /// that same review fast-forward (`model.review-pin`). |
| 25 | /// |
| 26 | /// # Errors |
| 27 | /// |
| 28 | /// [`crate::error::Error::Forge`] if `new.target` does not resolve to a |
| 29 | /// commit, or serialization or `receive` itself fails for either ref; see |
| 30 | /// [`crate::mutate::outcome_to_result`] for how a reached refusal renders. |
| 31 | pub fn new(root: &LocalRoot, new: NewReview, key: Option<std::path::PathBuf>) -> Result<String> { |
| 32 | let signer = signer(root, key)?; |
| 33 | let member = reviewer_member_id(root, &signer)?; |
| 34 | let identity = Identity { |
| 35 | actor: actor(&signer), |
| 36 | author: None, |
| 37 | sign: &|payload| signer.sign(payload), |
| 38 | }; |
| 39 | let (target, outcome) = review::new( |
| 40 | &root.refs, |
| 41 | &root.objects, |
| 42 | &root.events, |
| 43 | &root.path, |
| 44 | new, |
| 45 | &member, |
| 46 | &identity, |
| 47 | root.mode(), |
| 48 | )?; |
| 49 | outcome_to_result(outcome, None)?; |
| 50 | Ok(target) |
| 51 | } |
| 52 | |
| 53 | /// `git ents review withdraw`: retract the signer's own review of `target`, |
| 54 | /// resolving the reviewer's member id exactly as [`new`] does — the |
| 55 | /// withdrawing member is always the signing identity's own resolved |
| 56 | /// member, never one named on the command line, so this can never be |
| 57 | /// pointed at someone else's review (`gate.owner-mutation` refuses it even |
| 58 | /// if it were). |
| 59 | /// |
| 60 | /// # Errors |
| 61 | /// |
| 62 | /// [`crate::error::Error::Forge`] (wrapping [`ents_forge::Error::NotFound`]) |
| 63 | /// if this member has no existing review reaching `target`; otherwise as |
| 64 | /// [`new`]. |
| 65 | pub fn withdraw(root: &LocalRoot, target: String, key: Option<std::path::PathBuf>) -> Result<String> { |
| 66 | let signer = signer(root, key)?; |
| 67 | let member = reviewer_member_id(root, &signer)?; |
| 68 | let identity = Identity { |
| 69 | actor: actor(&signer), |
| 70 | author: None, |
| 71 | sign: &|payload| signer.sign(payload), |
| 72 | }; |
| 73 | let (target, outcome) = review::withdraw( |
| 74 | &root.refs, |
| 75 | &root.objects, |
| 76 | &root.events, |
| 77 | &root.path, |
| 78 | &target, |
| 79 | &member, |
| 80 | &identity, |
| 81 | root.mode(), |
| 82 | )?; |
| 83 | outcome_to_result(outcome, None)?; |
| 84 | Ok(target) |
| 85 | } |
| 86 | |
| 87 | /// The member id owning the signer's key — the composite review key's |
| 88 | /// `<member>` segment — via the same key-to-member scan |
| 89 | /// [`super::members::find_by_key`] already performs for `git ents members |
| 90 | /// check`. When the signing key enrolls no member (`roots.local`'s |
| 91 | /// advisory gate never requires enrollment before a local mutation lands), |
| 92 | /// falls back to the same fingerprint-derived placeholder [`super::actor`] |
| 93 | /// already uses for its own commit signature: a composite review key still |
| 94 | /// needs *some* member segment, and `gate.owner-mutation` — not this |
| 95 | /// fallback — is what actually keys ownership once a real deployment's |
| 96 | /// mandatory gate is in force. |
| 97 | /// |
| 98 | /// # Errors |
| 99 | /// |
| 100 | /// Propagates a ref-store or object read failure. |
| 101 | fn reviewer_member_id(root: &LocalRoot, signer: &crate::sign::Signer) -> Result<MemberId> { |
| 102 | let pubkey = signer.public_openssh(); |
| 103 | if let Some((username, _state)) = |
| 104 | super::members::find_by_key(&root.refs, &root.objects, &pubkey)? |
| 105 | { |
| 106 | return Ok(MemberId::new(username)); |
| 107 | } |
| 108 | Ok(MemberId::new(super::short_fingerprint(signer))) |
| 109 | } |
| 110 | |
| 111 | /// `git ents review list [--target rev]`: every review recorded in this |
| 112 | /// repository, keyed by its composite `(target, member)` segments, |
| 113 | /// optionally filtered to those reviewing `target`. |
| 114 | /// |
| 115 | /// # Errors |
| 116 | /// |
| 117 | /// Propagates a ref-store, object read, or revision-resolution failure. |
| 118 | pub fn list(root: &LocalRoot, target: Option<String>) -> Result<Vec<((String, MemberId), Review)>> { |
| 119 | Ok(review::list( |
| 120 | &root.refs, |
| 121 | &root.objects, |
| 122 | &root.path, |
| 123 | target.as_deref(), |
| 124 | )?) |
| 125 | } |
| 126 | |
| 127 | /// `git ents review show`: `target`/`member`'s review, plus its discussion |
| 128 | /// thread. |
| 129 | /// |
| 130 | /// # Errors |
| 131 | /// |
| 132 | /// [`crate::error::Error::Forge`] (wrapping [`ents_forge::Error::NotFound`]) |
| 133 | /// if `target`/`member` has no review ref. |
| 134 | pub fn show( |
| 135 | root: &LocalRoot, |
| 136 | target: &str, |
| 137 | member: &str, |
| 138 | ) -> Result<(Review, Vec<(String, Comment)>)> { |
| 139 | Ok(review::show( |
| 140 | &root.refs, |
| 141 | &root.objects, |
| 142 | target, |
| 143 | &MemberId::new(member), |
| 144 | )?) |
| 145 | } |