//! Issues: everything `atgc issue` does. //! //! A directory rather than a file from the start, because the seam that //! eventually split [`crate::cmd::pr`] into halves is already here and is //! already the same seam. It is worth stating rather than inheriting: //! //! - [`mod@read`] answers "what issues are there": `issue list` and //! `issue view`. Every request it makes is public, so it needs no token and //! no session and works for accounts nobody here has ever logged in to. Its //! whole difficulty is that an issue record lives in the PDS of whoever //! *filed* it, so one account's issues are complete and immediate while a //! repo's are scattered across PDSes nothing enumerates. Answering the //! repo question at all therefore means asking an index, which is what //! `--source bobbin` does and why the two halves of a listing carry //! different guarantees. //! - [`mod@write`] changes them: `issue create`, `edit`, `close`, `reopen` //! and `comment`. All of it selects an account, announces which one, //! honours `--dry-run` and lands a record in that account's own PDS. Its //! difficulties are the other kind: which issue the user meant, whether a //! state record this account writes would be honoured by anybody, and not //! clobbering a record something else wrote between the read and the write. //! //! The write half calls into the read half and not the other way round: //! `read::classify_issue_ref`, `read::fetch_issue` and `read::state_of` are //! the three it borrows, which is the same direction `pr` settled on and for //! the same reason: naming a thing and reading its state are questions a //! writer has to answer first, and neither needs a session to answer. //! //! # What is not here //! //! Issue *numbers*, image blobs in bodies, `mentions`/`references`, labels, //! and any listing that spans authors. Each is recorded in TODO.md with the //! reason; the first two matter most, because a number is what the web UI //! shows and a cross-author listing is what a maintainer actually wants. pub(crate) mod read; pub(crate) mod write; #[cfg(test)] mod fixtures; /// The `atgc issue` verbs. #[derive(clap::Subcommand, Debug)] pub(crate) enum Command { /// Open an issue on a repo /// /// The issue is a record in your own PDS naming the repo by its DID, so /// this needs no permission on the repo and no membership of anything: /// exactly as `pr create` does not. The repo is the one this checkout's /// `origin` points at unless `--remote` says otherwise. /// /// A body is required, though the lexicon says otherwise: tangled.org's /// ingester drops an issue whose body is empty, so a title-only record /// would federate and never appear. There is no editor: use /// `--body-file -` and a heredoc for a long one. /// /// A local image path in the body: ![crash](shots/crash.png): is /// uploaded to your PDS and embedded in the issue, exactly as in a pull /// body; https:// URLs and blob+at:// URIs pass through untouched. A /// path that names no file, a non-image, or a file over 1 MB is refused /// before anything is sent. /// /// Examples: /// atgc issue create --title 'pr list hangs' --body 'on a 40k-commit repo' /// atgc issue create --title 'typo in README' --body 'second paragraph' /// atgc issue create --title 'it looks like this' --body '![shot](bug.png)' /// atgc issue create --title 'long one' --body-file - <<'EOF' #[command(verbatim_doc_comment)] Create(write::CreateArgs), /// List issues filed by one account /// /// One account's issues, read straight from that account's PDS: yours by /// default, anybody's with `--author`, and no session needed for either. /// Scoped to this checkout's repo unless `--all`. /// /// Bare, this is not "every issue on this repo". An issue record lives in /// the PDS of whoever filed it and nothing enumerates the people who have /// filed against a repo, so that question needs the appview index: /// `--source bobbin` asks it, and the rows come back with their authors, /// their states and a comment count. The index is alpha and its ingest /// stalls, which is why it is opt-in here exactly as it is for `pr list`. /// Your own issues still come from your own PDS and outrank it. /// /// `--json` prints an array of objects. Still no `number`: that one is /// the appview's own id and is in no record and no XRPC response. /// /// Examples: /// atgc issue list /// atgc issue list --source bobbin --state all /// atgc issue list --state all --json | jq -r '.[] | "\(.rkey) \(.title)"' /// atgc issue list --all --author permadeath.com #[command(verbatim_doc_comment)] List(read::ListArgs), /// View an issue /// /// Named by record key or at:// URI: a bare key is looked up in the /// acting account's PDS, or in `--author`'s. A Tangled issue *number* is /// refused rather than guessed at, and the refusal says why: the number /// is the appview's own id and is in no record. /// /// `--comments` adds the discussion. A comment is a record in its /// commenter's PDS and nothing enumerates who has commented, so the /// thread can only come from the appview index: pass `--source bobbin` /// with it, and read what it shows as "what Bobbin has indexed". /// /// Examples: /// atgc issue view 3msg7w7l6hs2x /// atgc issue view 3msg7w7l6hs2x --comments --source bobbin /// atgc issue view at://did:plc:xyz/sh.tangled.repo.issue/3msg7w7l6hs2x /// atgc issue view 3msg7w7l6hs2x --json | jq .state #[command(verbatim_doc_comment)] View(read::ViewArgs), /// Comment on an issue /// /// The comment goes in your own PDS as a `sh.tangled.feed.comment` /// record, so it needs no access to the repo and no permission from /// anyone: unlike `issue close`, Tangled accepts a comment from any /// account. /// /// There is no editor. Use `--body-file -` and a heredoc for a long one. /// A local image path is uploaded and embedded, as in an issue body. /// /// Examples: /// atgc issue comment 3msg7w7l6hs2x --body 'still happens on 0.15' /// atgc issue comment 3msg7w7l6hs2x --body '![after](fixed.png)' /// atgc issue comment 3msg7w7l6hs2x --body-file notes.md #[command(verbatim_doc_comment)] Comment(write::CommentArgs), /// Close an issue /// /// Works on somebody else's issue when you own the repo it was filed on: /// the record this writes is your own, and Tangled decides whose state /// records to honour. /// /// Examples: /// atgc issue close 3msg7w7l6hs2x --dry-run /// atgc issue close at://did:plc:xyz/sh.tangled.repo.issue/3msg7w7l6hs2x #[command(verbatim_doc_comment)] Close(write::StateArgs), /// Reopen a closed issue /// /// Appends an `open` state record rather than deleting the `closed` one: /// state is a log and the newest record wins. /// /// Examples: /// atgc issue reopen 3msg7w7l6hs2x --dry-run /// atgc issue reopen 3msg7w7l6hs2x #[command(verbatim_doc_comment)] Reopen(write::StateArgs), /// Change the title or body of one of your issues /// /// Author-only, unlike `issue close`: the issue record lives in its /// author's PDS and there is no way to write to somebody else's. Neither /// field can be emptied: tangled.org drops an update whose body is /// empty, so clearing one would leave the appview showing the old text /// with nothing saying why. /// /// A new body naming a local image uploads it, and the images the issue /// already embeds stay retained: the record's blob list is merged rather /// than replaced, since an earlier edit's images are still referenced by /// the parts of the body this one did not touch. A body whose text is /// unchanged is still rewritten when it names a local path, because /// sending it is what uploads the file. Edit(write::EditArgs), } /// Run whichever `issue` verb was parsed. pub(crate) async fn run(command: Command) -> anyhow::Result<()> { match command { Command::Create(args) => write::create(args).await, Command::List(args) => read::list(args).await, Command::View(args) => read::view(args).await, Command::Comment(args) => write::comment(args).await, Command::Close(args) => write::close(args).await, Command::Reopen(args) => write::reopen(args).await, Command::Edit(args) => write::edit(args).await, } }