Public: anyone can read this; the crew can change it.

A pull takes a new title or body after it was opened, and an issue with it #912

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

steward/pull-body-edit → main 2 commits

  • Merged. steward/pull-body-edit was merged into main as 68255c6.

Review

Sign in to review this change.
2 files changed +74 −10
src/actions/issues.ts +47 −9
@@ -48,7 +48,11 @@ import { isStructural, pillarsOf } from "../core/pillars.ts";
4848 import { getPull } from "../core/pulls.ts";
4949 import { canRead, canWrite, type Repo } from "../core/repos.ts";
5050 import { getUserById, type User } from "../core/users.ts";
51 import { mayDirectCrew, unadoptedAmong } from "../crew/direct.ts";
51 import {
52 directorsAmong,
53 mayDirectCrew,
54 unadoptedAmong,
55 } from "../crew/direct.ts";
5256 import { laterGate } from "../crew/later.ts";
5357 import { handedOverAt } from "../crew/parks.ts";
5458 import { listReviews } from "../core/reviews.ts";
@@ -124,12 +128,14 @@ export const TAKEN_UP = "a writer has taken this up: comment instead";
124128 * A person who writes the repository may change anyone's. The author may
125129 * change their own — while they are still only theirs. Someone who may
126130 * not direct the crew (a reader on a public repository, an outside
127 * contributor) keeps that right only until a person who writes has acted
128 * on the words: adopted the issue (assigned it, or linked it into the map),
129 * or reviewed the pull. After that the words are what was adopted or
130 * reviewed, and a rewrite would put new work in the crew's hands, or a new
131 * `Closes #N` under an approval, with nobody who directs the crew having
132 * seen it (#619). They comment instead. An agent edits only what it wrote,
131 * contributor) keeps that right only until a person who directs the crew
132 * has acted on the words: adopted the issue (assigned it, or linked it into
133 * the map), or reviewed the pull. After that the words are what was
134 * adopted or reviewed, and a rewrite would put new work in the crew's
135 * hands, or a new `Closes #N` under an approval, with nobody who directs
136 * the crew having seen it (#619). They comment instead. Acted is acted:
137 * an assignment taken back later does not hand the words back, since the
138 * next assignment would hand the crew whatever they say by then. An agent edits only what it wrote,
133139 * and its own pull only until a person approved it: what the merge closes
134140 * is what the approval saw.
135141 *
@@ -164,11 +170,43 @@ export function mayEditIssue(
164170 if (mayDirectCrew(app.db, repo, user)) return ok(true);
165171 const untouched =
166172 now.kind === "pull"
167 ? listReviews(app.db, now.id).length === 0
168 : unadoptedAmong(app.db, repo, [now]).has(now.id);
173 ? !reviewedByDirector(app, repo, now)
174 : !everAdopted(app, repo, now) &&
175 unadoptedAmong(app.db, repo, [now]).has(now.id);
169176 return untouched ? ok(true) : err("taken_up");
170177 }
171178
179 /**
180 * Whether a person who directs the crew has ever assigned this issue or
181 * linked it into the map — read from the outbox, which keeps what the row
182 * forgets when an assignment is taken back. A link the crew's planner made
183 * is not adoption, as `unadoptedAmong` says.
184 */
185 function everAdopted(app: App, repo: Repo, issue: Issue): boolean {
186 const actors = app.db
187 .query<{ actor: string }, [string, number]>(
188 `SELECT DISTINCT actor_id AS actor FROM event
189 WHERE repo_id = ?1 AND actor_id IS NOT NULL
190 AND kind IN ('issue.assigned', 'issue.blocked')
191 AND (json_extract(payload, '$.number') = ?2
192 OR json_extract(payload, '$.blockedBy') = ?2)`,
193 )
194 .all(repo.id, issue.number)
195 .map((r) => r.actor);
196 return directorsAmong(app.db, repo, actors).length > 0;
197 }
198
199 /** Whether a person who directs the crew has reviewed this pull, in any state. */
200 function reviewedByDirector(app: App, repo: Repo, pull: Issue): boolean {
201 return (
202 directorsAmong(
203 app.db,
204 repo,
205 listReviews(app.db, pull.id).map((r) => r.authorId),
206 ).length > 0
207 );
208 }
209
172210 /**
173211 * Change the title or the body of an issue or a pull after it was opened:
174212 * the pull whose body still says "work in progress", the issue whose title
test/edit-words.test.ts +27 −1
@@ -523,6 +523,17 @@ describe("new words for an issue or a pull", () => {
523523 });
524524 expect(words(tulips).body).toBe("By the gate, in rows.");
525525 expect(await page(path, his)).not.toContain('id="edit"');
526 // Taken back is not handed back: the next assignment would hand the
527 // crew whatever he says by then.
528 expect(assignIssue(app, alice, repo, tulips, null).ok).toBe(true);
529 expect(
530 (await patch(tokens["bob"]!, path, { body: "Dig up the roses instead." }))
531 .status,
532 ).toBe(403);
533 expect(editIssue(app, bob, repo, tulips, { body: "x" })).toEqual({
534 ok: false,
535 error: "taken_up",
536 });
526537 // The writer still may.
527538 expect(
528539 (
@@ -605,6 +616,21 @@ describe("new words for an issue or a pull", () => {
605616 .status,
606617 ).toBe(200);
607618
619 // A stranger's comment-review is not a director acting: his words stay his.
620 const carol = await person("carol");
621 expect(
622 submitReview(app.db, {
623 issueId: theirs.id,
624 authorId: carol.id,
625 state: "commented",
626 commitOid: theirs.headOid,
627 }).ok,
628 ).toBe(true);
629 expect(
630 (await patch(tokens["bob"]!, path, { body: "Red ones, in a row." }))
631 .status,
632 ).toBe(200);
633
608634 // Alice approved what she read; a new "Closes #N" under it is not his to add.
609635 const reviewed = submitReview(app.db, {
610636 issueId: theirs.id,
@@ -624,7 +650,7 @@ describe("new words for an issue or a pull", () => {
624650 });
625651 expect(words(theirs)).toEqual({
626652 title: "Tulips",
627 body: "Red ones, by the gate.",
653 body: "Red ones, in a row.",
628654 });
629655 expect(openPullsClosing(app.db, repo.id).get(asked.number)?.id).not.toBe(
630656 theirs.id,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs