From d3e208322c7a7d13436638c2dad73fd4b6b106a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 23:44:32 +0000 Subject: [PATCH] =?UTF-8?q?feat(cli):=20add=20`amy=20git=20label`=20(NIP-3?= =?UTF-8?q?2)=20and=20`amy=20git=20apply`=20(patch=20=E2=86=92=20working?= =?UTF-8?q?=20tree)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close two more ngit/nak parity gaps: - `git label TARGET LABEL[,LABEL]` — attach NIP-32 kind:1985 labels to an issue/patch/PR (the `ngit pr label` / `issue label` surface), over quartz's existing `LabelEvent`. Namespace defaults to `ugc`; `--namespace` overrides. - `git apply PATCH_ID` — fetch a kind:1617 patch and apply it to the local working tree via `git am` (the `nak git patch apply` / `ngit pr apply` surface); `--check` dry-runs `git apply --check`, `--print` emits the patch. Shells out to `git` like `git init`, since it operates on the local checkout. Verified end-to-end: a patch published to a relay, fetched, and `git am`'d as a real commit into a scratch repo; labels land as kind 1985. The harness gains 5 assertions (label + a full publish→apply round-trip), now 33 offline. Remaining out-of-scope items are documented: git-packfile push (needs a git write layer quartz lacks) and NIP-34 cover notes (kind 1624, no quartz builder). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr --- cli/README.md | 2 + cli/ROADMAP.md | 2 +- .../com/vitorpamplona/amethyst/cli/Main.kt | 2 + .../amethyst/cli/commands/GitApplyCommand.kt | 114 ++++++++++++++++++ .../amethyst/cli/commands/GitCommands.kt | 9 +- .../amethyst/cli/commands/GitLabelCommand.kt | 88 ++++++++++++++ cli/tests/README.md | 9 +- cli/tests/git/git-nip34-headless.sh | 29 +++++ 8 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitApplyCommand.kt create mode 100644 cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitLabelCommand.kt diff --git a/cli/README.md b/cli/README.md index 90b55a5cef..2ea5a7bd51 100644 --- a/cli/README.md +++ b/cli/README.md @@ -348,6 +348,7 @@ URL) **or** a raw `http(s)` clone URL. This is the git-object read side of |---|---| | `amy git issue REPO --subject S [BODY] [--hashtag T[,T]]` | Publish a kind:1621 issue. BODY from arg or stdin. | | `amy git patch REPO [--file PATH] [--root\|--root-revision] [--commit C] [--parent-commit P] [--in-reply-to ID]` | Publish a kind:1617 patch. Body is `git format-patch` output from `--file` or stdin. | +| `amy git apply PATCH_ID [--check\|--print] [--repo PATH]` | Fetch a kind:1617 patch and apply it to the local working tree (`git am`); `--check` dry-runs, `--print` emits the patch. | | `amy git pr REPO --commit TIP --clone URL[,URL] [--subject S] [--branch-name N] [--merge-base C] [--label L[,L]] [DESC]` | Publish a kind:1618 pull request (references a pushed branch tip by clone URL + commit). | | `amy git pr-update PR --commit TIP --clone URL[,URL] [--merge-base C]` | Publish a kind:1619 update to a pull request's tip. | | `amy git issues\|patches\|prs REPO [--open\|--applied\|--closed\|--draft\|--status a,b] [--limit N]` | List a repo's issues / patches / PRs with their derived status. | @@ -358,6 +359,7 @@ URL) **or** a raw `http(s)` clone URL. This is the git-object read side of | Command | What it does | |---|---| | `amy git comment TARGET [BODY]` | Reply to an issue/patch/PR/repo with a NIP-22 kind:1111 comment. BODY from arg or stdin. | +| `amy git label TARGET LABEL[,LABEL] [--namespace N]` | Attach NIP-32 kind:1985 labels to an issue/patch/PR (namespace defaults to `ugc`). | | `amy git open TARGET [MSG]` | Publish a kind:1630 status (open / reopen / ready-for-review). | | `amy git applied TARGET [MSG] [--merge-commit C] [--commit C[,C]] [--patch ID[,ID]]` | Publish a kind:1631 status (applied / merged / resolved). Aliases: `merged`, `resolved`. | | `amy git close TARGET [MSG]` | Publish a kind:1632 status (closed). | diff --git a/cli/ROADMAP.md b/cli/ROADMAP.md index 7fb1d48690..01b5ed0c75 100644 --- a/cli/ROADMAP.md +++ b/cli/ROADMAP.md @@ -109,7 +109,7 @@ vs streaming `subscribe`). Stateless verbs run with no account or network. | `nip` | `amy nip` | ✅ | repo-first lookup + Nostr fallback (NipText kind:30817, wiki:30818, long-form:30023); `nip list`. | | `kind` | `amy kind` | ✅ | quartz `KindNames` registry (kind → English label + NIP) covering **every** event kind quartz defines (280 entries); number lookup + name search. | | `sync` | `amy sync` | ✅ | NIP-77 Negentropy reconcile with the local store (down/up/both). | -| `git` | `amy git` | ✅ (events + read) | NIP-34: `init` bootstraps a repo from the local `git` checkout (announce + state, like `ngit init`); repo announce (30617) + state (30618), patches (1617), pull requests (1618/1619), issues (1621), NIP-22 comments (1111), status open/applied/closed/draft (1630-1633), GRASP server list (10317); `issues`/`patches`/`prs`/`thread` reads derive status; `browse`/`cat`/`log` read git objects over smart-HTTP v2 (quartz `GitHttpClient`, the same shallow-clone path the Android browser uses). Only git-packfile **push** (writing objects to clone/GRASP servers) is out of scope. | +| `git` | `amy git` | ✅ (events + read) | NIP-34: `init` bootstraps a repo from the local `git` checkout (announce + state, like `ngit init`); repo announce (30617) + state (30618), patches (1617), pull requests (1618/1619), issues (1621), NIP-22 comments (1111), NIP-32 labels (1985), status open/applied/closed/draft (1630-1633), GRASP server list (10317); `issues`/`patches`/`prs`/`thread` reads derive status; `apply` applies a fetched patch to the local tree (`git am`); `browse`/`cat`/`log` read git objects over smart-HTTP v2 (quartz `GitHttpClient`, the same shallow-clone path the Android browser uses). Only git-packfile **push** (writing objects to clone/GRASP servers) and NIP-34 cover notes (1624, no quartz builder yet) are out of scope. | | `podcast` | `amy podcast` | ✅ | NIP-F4 show metadata (10154) + episode publish (54) + list. | | `bunker` | `amy bunker[ connect]` + `amy login bunker://`/`--nostrconnect` | ✅ | NIP-46 remote signer + login, both the `bunker://` and `nostrconnect://` flows, each direction, plus `auth_url` challenge handling (client surfaces the URL + keeps waiting). Interop-verified vs real `nak`. | | `admin` | `amy admin RELAY METHOD` | ✅ | NIP-86 Relay Management over NIP-98 HTTP auth — full method set (ban/allow pubkey + event, kinds, IP block, change name/desc/icon, list-*). Reuses quartz `Nip86Client` + shared `commons` `Nip86Retriever`. Interop-verified against `amy serve`. | diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt index 0a89fd9823..2f0ac667fa 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt @@ -645,6 +645,8 @@ private fun printUsage() { | [--subject S] [--branch-name N] [--merge-base C] [--label L[,L]] | git pr-update PR --commit TIP --clone URL publish a kind:1619 pull-request update | git comment TARGET [BODY] NIP-22 kind:1111 comment on issue/patch/PR/repo + | git label TARGET LABEL[,LABEL] NIP-32 kind:1985 labels on an issue/patch/PR + | git apply PATCH_ID [--check|--print] apply a fetched kind:1617 patch to the local tree | git open|applied|close|draft TARGET [MSG] publish a kind:1630/1631/1632/1633 status | git issues|patches|prs REPO list a repo's issues/patches/PRs + status | [--open|--applied|--closed|--draft] [--limit N] diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitApplyCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitApplyCommand.kt new file mode 100644 index 0000000000..fb33e7db66 --- /dev/null +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitApplyCommand.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.cli.commands + +import com.vitorpamplona.amethyst.cli.Args +import com.vitorpamplona.amethyst.cli.Context +import com.vitorpamplona.amethyst.cli.DataDir +import com.vitorpamplona.amethyst.cli.Output +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import java.io.File + +/** + * `amy git apply PATCH_ID` — fetch a NIP-34 kind:1617 patch and apply it to the + * local git working tree (the `nak git patch apply` / `ngit pr apply` surface). + * The patch content is `git format-patch` output, so by default it is fed to + * `git am` (applied as a commit); `--check` dry-runs `git apply --check` and + * `--print` just emits the patch without touching the tree. + * + * This shells out to `git`, like `git init` — it operates on the local checkout. + */ +object GitApplyCommand { + suspend fun apply( + dataDir: DataDir, + rest: Array, + ): Int { + val args = Args(rest) + val patchRef = args.positional(0, "patch-event-id") + val repoDir = File(args.flag("repo") ?: ".").absoluteFile + val check = args.bool("check") + val print = args.bool("print") + val id = + GitSupport.resolveEventId(patchRef) + ?: return Output.error("bad_args", "expected a note/nevent/64-hex patch id") + args.rejectUnknown("relay") + + Context.openOrAnonymous(dataDir).use { ctx -> + ctx.prepare() + val patch = + GitSupport.fetchEvent(ctx, id, args) as? GitPatchEvent + ?: return Output.error("not_found", "no patch (kind 1617) found for $patchRef") + val content = patch.content + val subject = patch.subject() + + if (print) { + Output.emit(mapOf("patch_id" to patch.id, "subject" to subject, "content" to content)) + return 0 + } + + val (mode, gitArgs) = + if (check) { + "check" to arrayOf("apply", "--check", "-") + } else { + "am" to arrayOf("am", "--") + } + val (code, output) = runGit(repoDir, content, *gitArgs) + if (code != 0) { + // Leave the tree clean on a failed `git am` so a retry isn't blocked. + if (mode == "am") runGit(repoDir, null, "am", "--abort") + return Output.error( + "apply_failed", + "git $mode failed for patch ${patch.id}", + extra = mapOf("patch_id" to patch.id, "mode" to mode, "git_output" to output.trim()), + ) + } + Output.emit( + mapOf( + "patch_id" to patch.id, + "subject" to subject, + "mode" to mode, + "applied" to (mode == "am"), + "git_output" to output.trim(), + ), + ) + return 0 + } + } + + /** Run `git ` in [repoDir], optionally feeding [input] on stdin; returns (exitCode, merged stdout+stderr). */ + private fun runGit( + repoDir: File, + input: String?, + vararg gitArgs: String, + ): Pair = + try { + val proc = + ProcessBuilder(listOf("git", *gitArgs)) + .directory(repoDir) + .redirectErrorStream(true) + .start() + if (input != null) proc.outputStream.use { it.write(input.toByteArray()) } else proc.outputStream.close() + val out = proc.inputStream.readBytes().decodeToString() + proc.waitFor() to out + } catch (e: Exception) { + 1 to (e.message ?: "could not run git (is it installed and is this a git repo?)") + } +} diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitCommands.kt index 9391fd73bc..2ce6b6b2e9 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitCommands.kt @@ -75,6 +75,8 @@ object GitCommands { | git patch REPO [--file PATH] publish a kind:1617 patch (git format-patch | [--root|--root-revision] [--commit C] from --file or stdin) | [--parent-commit P] [--in-reply-to ID] + | git apply PATCH_ID [--check|--print] apply a fetched kind:1617 patch to the local tree + | [--repo PATH] (default: `git am`; --check dry-runs; --print emits it) | git pr REPO --commit TIP --clone URL[,URL] publish a kind:1618 pull request [DESC arg] | [--subject S] [--branch-name N] [--merge-base C] [--label L[,L]] | git pr-update PR --commit TIP --clone URL[,URL] publish a kind:1619 pull-request update @@ -82,8 +84,9 @@ object GitCommands { | [--open|--applied|--closed|--draft|--status a,b] [--limit N] | git thread EVENT_ID print one item + its status timeline + comments | - |Comments & status: + |Comments, labels & status: | git comment TARGET [BODY] NIP-22 kind:1111 comment (BODY arg or stdin) + | git label TARGET LABEL[,LABEL] [--namespace N] NIP-32 kind:1985 labels on an issue/patch/PR | git open|applied|close|draft TARGET [MESSAGE] publish a kind:1630/1631/1632/1633 status | applied: [--merge-commit C] [--commit C[,C]] [--patch ID[,ID]] | @@ -98,7 +101,7 @@ object GitCommands { route( "git", tail, - "git ", + "git ", mapOf( "init" to { rest -> GitInitCommand.init(dataDir, rest) }, "announce" to { rest -> announce(dataDir, rest) }, @@ -118,6 +121,8 @@ object GitCommands { "prs" to { rest -> GitReadCommands.prs(dataDir, rest) }, "thread" to { rest -> GitReadCommands.thread(dataDir, rest) }, "comment" to { rest -> GitCommentCommand.comment(dataDir, rest) }, + "label" to { rest -> GitLabelCommand.label(dataDir, rest) }, + "apply" to { rest -> GitApplyCommand.apply(dataDir, rest) }, "open" to { rest -> GitStatusCommands.open(dataDir, rest) }, "applied" to { rest -> GitStatusCommands.applied(dataDir, rest) }, "merged" to { rest -> GitStatusCommands.applied(dataDir, rest) }, diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitLabelCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitLabelCommand.kt new file mode 100644 index 0000000000..4f39f9376e --- /dev/null +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GitLabelCommand.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.cli.commands + +import com.vitorpamplona.amethyst.cli.Args +import com.vitorpamplona.amethyst.cli.Context +import com.vitorpamplona.amethyst.cli.DataDir +import com.vitorpamplona.amethyst.cli.Output +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip32Labeling.LabelEvent +import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag + +/** + * `amy git label TARGET LABEL[,LABEL]` — attach NIP-32 kind:1985 labels to a + * patch, pull request, or issue (the `ngit pr label` / `issue label` surface). + * Labels default to the `ugc` namespace; override with `--namespace`. + */ +object GitLabelCommand { + suspend fun label( + dataDir: DataDir, + rest: Array, + ): Int { + val args = Args(rest) + val targetRef = args.positional(0, "target-event-id") + val namespace = args.flag("namespace") ?: LabelTag.DEFAULT_NAMESPACE + val labels = + args + .positional(1, "label[,label]") + .split(',') + .map { it.trim() } + .filter { it.isNotEmpty() } + .map { LabelTag(it, namespace) } + if (labels.isEmpty()) return Output.error("bad_args", "git label requires at least one label") + val content = args.flag("content") ?: "" + val id = + GitSupport.resolveEventId(targetRef) + ?: return Output.error("bad_args", "expected a note/nevent/64-hex target id") + args.rejectUnknown("relay") + + Context.open(dataDir).use { ctx -> + ctx.prepare() + val target = + GitSupport.fetchEvent(ctx, id, args) + ?: return Output.error("not_found", "no event found for $targetRef") + val template = + LabelEvent.buildEventLabel( + labeledEventId = target.id, + labeledEventAuthor = target.pubKey, + labels = labels, + content = content, + ) + val signed = ctx.signer.sign(template) + val repoATag = GitSupport.repositoryOf(target) + val repo = repoATag?.let { GitSupport.fetchRepo(ctx, Address(it.kind, it.pubKeyHex, it.dTag), args) } + val targets = GitSupport.deliveryTargets(ctx, repo, args) + val ack = ctx.publish(signed, targets) + RawEventSupport.publishGuard(ack, signed.id)?.let { return it } + Output.emit( + mapOf( + "event_id" to signed.id, + "kind" to signed.kind, + "target" to target.id, + "namespace" to namespace, + "labels" to labels.map { it.label }, + ) + RawEventSupport.ackFields(ack), + ) + return 0 + } + } +} diff --git a/cli/tests/README.md b/cli/tests/README.md index d6e36efc37..19d9098f1f 100644 --- a/cli/tests/README.md +++ b/cli/tests/README.md @@ -72,10 +72,11 @@ Suite notes: git checkout (announce + state derived via `git`), announce (30617) + state (30618) + GRASP list (10317), issue (1621), patch (1617), pull request (1618) + update (1619), NIP-22 - comment (1111), and status events (1630-1633), then asserts the - `issues`/`patches`/`prs`/`thread` reads derive the right status (a closed - issue reads `closed`, an applied PR reads `applied`) and that - `--open`/`--closed` filter correctly. Pass `--live` to additionally exercise + comment (1111), NIP-32 label (1985), and status events (1630-1633). It also + publishes a real `git format-patch` and `git apply`s it back into a scratch + working tree, and asserts the `issues`/`patches`/`prs`/`thread` reads derive + the right status (a closed issue reads `closed`, an applied PR reads + `applied`) and that `--open`/`--closed` filter correctly. Pass `--live` to additionally exercise the git smart-HTTP reads (`git browse`/`cat`/`log`) against a real public repo (`$LIVE_REPO`, default octocat/Hello-World) — skipped by default since it needs a reachable git host. diff --git a/cli/tests/git/git-nip34-headless.sh b/cli/tests/git/git-nip34-headless.sh index 46d3fb284a..bc39042998 100755 --- a/cli/tests/git/git-nip34-headless.sh +++ b/cli/tests/git/git-nip34-headless.sh @@ -182,12 +182,41 @@ banner "comment + status" CMT="$(M git comment "$ISSID" "thanks for reporting" --relay "$RELAY_URL")" assert_eq "$(echo "$CMT" | jq -r '.kind')" "1111" comment.kind "comment is NIP-22 kind 1111" +LABEL="$(M git label "$ISSID" "bug,help-wanted" --relay "$RELAY_URL")" +assert_eq "$(echo "$LABEL" | jq -r '.kind')" "1985" label.kind "label is NIP-32 kind 1985" +assert_eq "$(echo "$LABEL" | jq -r '.labels | length')" "2" label.count "two labels attached" + CLOSE="$(M git close "$ISSID" "wontfix" --relay "$RELAY_URL")" assert_eq "$(echo "$CLOSE" | jq -r '.kind')" "1632" close.kind "close status is kind 1632" APPLIED="$(M git applied "$PRID" "merged it" --merge-commit feed99 --commit feed02 --relay "$RELAY_URL")" assert_eq "$(echo "$APPLIED" | jq -r '.kind')" "1631" applied.kind "applied status is kind 1631" +# ============================================================================= +# git apply — publish a real patch and apply it to a local working tree +# ============================================================================= +banner "git apply (nostr patch → local git am)" +SCRATCH="$(mk_home)/scratch" +git init -q "$SCRATCH" +git -C "$SCRATCH" config user.email a@b.c +git -C "$SCRATCH" config user.name t +echo "line1" >"$SCRATCH/f.txt" +git -C "$SCRATCH" add f.txt +git -C "$SCRATCH" commit -qm "init" +# Make a real commit, capture its format-patch, then roll it back so `apply` can re-add it. +echo "line2" >>"$SCRATCH/f.txt" +git -C "$SCRATCH" commit -qam "add line2" +PATCHTXT="$(git -C "$SCRATCH" format-patch -1 --stdout)" +git -C "$SCRATCH" reset -q --hard HEAD~1 +APATCH="$(printf '%s' "$PATCHTXT" | M git patch "$ADDR" --root --relay "$RELAY_URL")" +APID="$(echo "$APATCH" | jq -r '.event_id')" +CHECK="$(M git apply "$APID" --check --repo "$SCRATCH")" +assert_eq "$(echo "$CHECK" | jq -r '.mode')" "check" apply.check "apply --check dry-runs cleanly" +APPLY="$(M git apply "$APID" --repo "$SCRATCH")" +info "apply: $APPLY" +assert_eq "$(echo "$APPLY" | jq -r '.applied')" "true" apply.applied "patch applied via git am" +assert_eq "$(git -C "$SCRATCH" log --oneline | head -1 | sed 's/^[0-9a-f]* //')" "add line2" apply.commit "commit landed in the working tree" + # ============================================================================= # Status-deriving reads # =============================================================================