git-ents.gitmain
⌘K
foforge
comment.rs248 lines · 7.3 KB · rusthistorycomment on this file
1//! `git ents comment`: a thin wrapper around `ents_forge::comment`'s
2//! business logic — this module only resolves the signer/actor identity
3//! against [`LocalRoot`], translates a reached `Outcome` into a CLI-facing
4//! [`Result`] (`crate::mutate::outcome_to_result`), and renders the
5//! machine-readable listing, exactly as every other mutation command does.
6//! Every operation is the library call itself (`lens.parity`); nothing
7//! here re-implements one.
8
9use ents_forge::comment;
10use ents_forge::comment::{Comment, ListFilter, Listed, NewComment};
11use ents_receive::Identity;
12
13use super::{actor, signer};
14use crate::error::Result;
15use crate::mutate::outcome_to_result;
16use crate::root::LocalRoot;
17
18/// `git ents comment list`: every comment recorded in this repository.
19///
20/// # Errors
21///
22/// Propagates a ref-store or object read failure.
23pub fn list(root: &LocalRoot) -> Result<Vec<(String, Comment)>> {
24 Ok(comment::list(&root.refs, &root.objects)?)
25}
26
27/// `git ents comment list [--worktree] [--state ...] [--context ...]`:
28/// matching comments with each anchor projected onto the working tree
29/// (with `worktree`) or `HEAD`, plus the refs whose stored tree this
30/// build could not read back (reported after the listing, never
31/// silently dropped).
32///
33/// # Errors
34///
35/// Propagates a ref-store, object read, or projection failure.
36pub fn list_projected(
37 root: &LocalRoot,
38 worktree: bool,
39 filter: &ListFilter,
40) -> Result<(Vec<Listed>, Vec<ents_forge::Unreadable>)> {
41 Ok(comment::list_projected(
42 &root.refs,
43 &root.objects,
44 &root.path,
45 worktree,
46 filter,
47 )?)
48}
49
50/// `git ents comment add`: create a comment about something.
51///
52/// # Errors
53///
54/// [`crate::error::Error::Forge`] if the comment is about nothing, its
55/// arguments do not parse, or anchoring, serialization, or `receive`
56/// itself fails; see [`crate::mutate::outcome_to_result`] for how a
57/// reached refusal renders.
58pub fn add(root: &LocalRoot, new: NewComment, key: Option<std::path::PathBuf>) -> Result<String> {
59 let signer = signer(root, key)?;
60 let identity = Identity {
61 actor: actor(&signer),
62 author: None,
63 sign: &|payload| signer.sign(payload),
64 };
65 let (id, outcome) = comment::add(
66 &root.refs,
67 &root.objects,
68 &root.events,
69 &root.path,
70 new,
71 &identity,
72 root.mode(),
73 )?;
74 outcome_to_result(outcome, None)?;
75 Ok(id)
76}
77
78/// `git ents comment reply`: a comment whose parent is `parent_id`.
79///
80/// # Errors
81///
82/// See [`add`]; additionally [`ents_forge::Error::NotFound`] (wrapped)
83/// when `parent_id` names no comment.
84pub fn reply(
85 root: &LocalRoot,
86 parent_id: &str,
87 body: String,
88 key: Option<std::path::PathBuf>,
89) -> Result<String> {
90 let signer = signer(root, key)?;
91 let identity = Identity {
92 actor: actor(&signer),
93 author: None,
94 sign: &|payload| signer.sign(payload),
95 };
96 let (id, outcome) = comment::reply(
97 &root.refs,
98 &root.objects,
99 &root.events,
100 parent_id,
101 body,
102 &identity,
103 root.mode(),
104 )?;
105 outcome_to_result(outcome, None)?;
106 Ok(id)
107}
108
109/// `git ents comment resolve` / `reopen`: record the state mutation on the
110/// comment's own ref.
111///
112/// # Errors
113///
114/// See [`add`].
115pub fn set_state(
116 root: &LocalRoot,
117 id: &str,
118 resolve: bool,
119 key: Option<std::path::PathBuf>,
120) -> Result<()> {
121 let signer = signer(root, key)?;
122 let identity = Identity {
123 actor: actor(&signer),
124 author: None,
125 sign: &|payload| signer.sign(payload),
126 };
127 let outcome = if resolve {
128 comment::resolve(
129 &root.refs,
130 &root.objects,
131 &root.events,
132 id,
133 &identity,
134 root.mode(),
135 Some(&signer.public_openssh()),
136 )?
137 } else {
138 comment::reopen(
139 &root.refs,
140 &root.objects,
141 &root.events,
142 id,
143 &identity,
144 root.mode(),
145 Some(&signer.public_openssh()),
146 )?
147 };
148 outcome_to_result(outcome, None)?;
149 Ok(())
150}
151
152/// `git ents comment show`: `id`'s comment and, when anchored, its anchor
153/// projected onto `rev` or the working tree.
154///
155/// # Errors
156///
157/// [`crate::error::Error::Forge`] (wrapping [`ents_forge::Error::NotFound`])
158/// if `id` has no comment ref.
159pub fn show(
160 root: &LocalRoot,
161 id: &str,
162 rev: &str,
163 worktree: bool,
164) -> Result<(
165 Comment,
166 Option<(ents_anchor::Anchor, ents_anchor::Projection)>,
167)> {
168 Ok(comment::show(
169 &root.refs,
170 &root.objects,
171 &root.path,
172 id,
173 rev,
174 worktree,
175 )?)
176}
177
178/// One record of `git ents comment list --porcelain`'s stable
179/// machine-readable form (`lens.parity`: id, state, projected location,
180/// and body, sufficient for an agent to enumerate and resolve every open
181/// comment with no editor attached):
182///
183/// ```text
184/// <id> <state> <projection> <location>
185/// context <c> (only when the comment names one)
186/// parent <id> (only when the comment is a reply)
187/// \t<body line> (every body line, tab-prefixed)
188/// ```
189///
190/// `projection` is `current`, `relocated`, `outdated`, or `deleted`, and
191/// `-` for a comment with no anchor; `location` is `path:start-end`
192/// (`path` alone for a whole-file anchor) and `-` when there is no anchor
193/// or the file is gone. Records are separated by one blank line — a blank
194/// body line renders as a lone tab, so it can never terminate a record.
195/// This is the record grammar every other family's porcelain shares
196/// (`lens.porcelain`, via [`ents_forge::present::record`]).
197// @relation(lens.porcelain, scope=function)
198#[must_use]
199pub fn porcelain(rows: &[Listed]) -> String {
200 let mut out = String::new();
201 for (index, row) in rows.iter().enumerate() {
202 if index > 0 {
203 out.push('\n');
204 }
205 let (projection, location) = match (&row.projection, &row.anchor) {
206 (Some(projection), Some(anchor)) => porcelain_projection(projection, anchor),
207 _ => ("-".to_owned(), "-".to_owned()),
208 };
209 out.push_str(&format!(
210 "{} {} {} {}\n",
211 row.id, row.comment.state, projection, location
212 ));
213 if let Some(context) = &row.comment.context {
214 out.push_str(&format!("context {context}\n"));
215 }
216 if let Some(parent) = &row.comment.parent {
217 out.push_str(&format!("parent {parent}\n"));
218 }
219 for line in row.comment.body.lines() {
220 out.push('\t');
221 out.push_str(line);
222 out.push('\n');
223 }
224 }
225 out
226}
227
228/// The `(projection, location)` columns of one porcelain record.
229fn porcelain_projection(
230 projection: &ents_anchor::Projection,
231 anchor: &ents_anchor::Anchor,
232) -> (String, String) {
233 use ents_anchor::Projection;
234 let location = match projection {
235 Projection::Current => location(&anchor.path, anchor.lines),
236 Projection::Relocated { path, lines } => location(path, *lines),
237 Projection::Outdated { path } => location(path, None),
238 Projection::Deleted => "-".to_owned(),
239 };
240 (projection.label().to_owned(), location)
241}
242
243fn location(path: &str, lines: Option<ents_anchor::LineRange>) -> String {
244 match lines {
245 Some(range) => format!("{path}:{}-{}", range.start, range.end),
246 None => path.to_owned(),
247 }
248}