diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/grasp/UserGraspListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/grasp/UserGraspListEvent.kt new file mode 100644 index 000000000..11ebd05b0 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/grasp/UserGraspListEvent.kt @@ -0,0 +1,82 @@ +/* + * 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.quartz.nip34Git.grasp + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.grasp.tags.GraspTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-34 kind 10317 — User Grasp Server List. + * + * Declares a user's preferred grasp (Git-over-Nostr hosting) servers in + * preference order. Functions analogously to NIP-65's relay list: clients + * use it to discover where to push PR tip branches under + * `refs/nostr/`. + */ +@Immutable +class UserGraspListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + /** Grasp server URLs in preference order (first = most preferred). */ + fun grasps(): List = tags.mapNotNull(GraspTag::parse) + + fun graspsNorm(): List = tags.mapNotNull(GraspTag::parseNorm) + + companion object { + const val KIND = 10317 + const val ALT = "Preferred grasp servers for Git over Nostr" + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + + fun build( + grasps: List, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) + grasps.forEach { add(GraspTag.assemble(it)) } + initializer() + } + + fun buildNorm( + grasps: List, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) + grasps.forEach { add(GraspTag.assemble(it)) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/grasp/tags/GraspTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/grasp/tags/GraspTag.kt new file mode 100644 index 000000000..dbc667191 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/grasp/tags/GraspTag.kt @@ -0,0 +1,56 @@ +/* + * 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.quartz.nip34Git.grasp.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 grasp `g` tag: the websocket URL of a preferred grasp (Git over + * Nostr) hosting service: + * + * ["g", "wss://grasp.example.com"] + */ +class GraspTag { + companion object { + const val TAG_NAME = "g" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun parseNorm(tag: Array): NormalizedRelayUrl? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return RelayUrlNormalizer.normalizeOrNull(tag[1]) + } + + fun assemble(url: String) = arrayOf(TAG_NAME, url) + + fun assemble(url: NormalizedRelayUrl) = arrayOf(TAG_NAME, url.url) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index 9bdb1fc60..46a7f9b23 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -24,14 +24,26 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag -import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitPgpSigTag +import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitTag +import com.vitorpamplona.quartz.nip34Git.patch.tags.Committer +import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitterTag +import com.vitorpamplona.quartz.nip34Git.patch.tags.ParentCommitTag +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -62,8 +74,6 @@ class GitPatchEvent( tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } - private fun repositoryHex() = innerRepository()?.getOrNull(1) - fun repositoryAddress() = innerRepository()?.let { if (it.size > 1) { @@ -85,42 +95,109 @@ class GitPatchEvent( } } - fun commit() = tags.firstOrNull { it.size > 1 && it[0] == "commit" }?.get(1) + fun commit() = tags.firstNotNullOfOrNull(CommitTag::parse) - fun parentCommit() = tags.firstOrNull { it.size > 1 && it[0] == "parent-commit" }?.get(1) + fun parentCommit() = tags.firstNotNullOfOrNull(ParentCommitTag::parse) - fun commitPGPSig() = tags.firstOrNull { it.size > 1 && it[0] == "commit-pgp-sig" }?.get(1) + fun commitPGPSig() = tags.firstNotNullOfOrNull(CommitPgpSigTag::parse) - fun committer() = - tags.filter { it.size > 1 && it[0] == "committer" }.mapNotNull { - Committer(it.getOrNull(1), it.getOrNull(2), it.getOrNull(3), it.getOrNull(4)) - } + fun committer() = tags.mapNotNull(CommitterTag::parse) - data class Committer( - val name: String?, - val email: String?, - val timestamp: String?, - val timezoneInMinutes: String?, - ) + /** Earliest unique commit of the target repository, encoded as `["r", ]`. */ + fun earliestUniqueCommit(): String? = tags.firstOrNull { it.size > 1 && it[0] == "r" && it[1].isNotEmpty() }?.get(1) + + /** `true` if this event is tagged `["t", "root"]` (root of a patch series). */ + fun isRoot(): Boolean = tags.any { HashtagTag.isTagged(it, ROOT) } + + /** `true` if this event is tagged `["t", "root-revision"]` (root of a revision series). */ + fun isRootRevision(): Boolean = tags.any { HashtagTag.isTagged(it, ROOT_REVISION) } companion object { const val KIND = 1617 const val ALT = "A Git Patch" + const val ROOT = "root" + const val ROOT_REVISION = "root-revision" - suspend fun create( + /** + * Build a NIP-34 kind-1617 patch event with all required tags. + * + * @param patch the raw `git format-patch` output for the content field. + * @param repository an [EventHintBundle] pointing at the target repository announcement. + * @param earliestUniqueCommit the repository's earliest unique commit ID, used as the `r` tag. + * @param commit optional current commit hash (`commit` tag). + * @param parentCommit optional parent commit hash (`parent-commit` tag). + * @param commitPgpSig optional PGP signature or empty string for unsigned (`commit-pgp-sig` tag). + * @param committer optional committer metadata (`committer` tag). + * @param notify additional recipient pubkeys, beyond the repo owner who is always included. + * @param root set to `true` to tag the patch as the root of a new patch series (`["t", "root"]`). + * @param rootRevision set to `true` to tag the patch as the root of a revision series (`["t", "root-revision"]`). + */ + fun build( patch: String, + repository: EventHintBundle, + earliestUniqueCommit: String, + commit: String? = null, + parentCommit: String? = null, + commitPgpSig: String? = null, + committer: Committer? = null, + notify: List = emptyList(), + root: Boolean = false, + rootRevision: Boolean = false, createdAt: Long = TimeUtils.now(), - signer: NostrSigner, - ): GitPatchEvent { - val content = patch - val tags = - mutableListOf( - arrayOf(), - ) + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, patch, createdAt) { + alt(ALT) + repository(repository) + euc(earliestUniqueCommit) + pTag(repository.event.pubKey, repository.authorHomeRelay) + if (notify.isNotEmpty()) pTags(notify) + commit?.let { commit(it) } + parentCommit?.let { parentCommit(it) } + commitPgpSig?.let { commitPgpSig(it) } + committer?.let { + committer(it.name, it.email, it.timestamp, it.timezoneInMinutes) + } + if (root) hashtag(ROOT) + if (rootRevision) hashtag(ROOT_REVISION) + initializer() + } - tags.add(AltTag.assemble(ALT)) - - return signer.sign(createdAt, KIND, tags.toTypedArray(), content) + /** + * Build a patch that replies to an earlier patch in the same series, + * adding NIP-10 `e` reply tags that point at the previous patch. + */ + fun reply( + patch: String, + repository: EventHintBundle, + earliestUniqueCommit: String, + replyingTo: EventHintBundle, + commit: String? = null, + parentCommit: String? = null, + commitPgpSig: String? = null, + committer: Committer? = null, + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build( + patch = patch, + repository = repository, + earliestUniqueCommit = earliestUniqueCommit, + commit = commit, + parentCommit = parentCommit, + commitPgpSig = commitPgpSig, + committer = committer, + notify = notify, + createdAt = createdAt, + ) { + add( + MarkedETag.assemble( + replyingTo.event.id, + replyingTo.relay, + MarkedETag.MARKER.REPLY, + replyingTo.event.pubKey, + ), + ) + initializer() } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/TagArrayBuilderExt.kt new file mode 100644 index 000000000..2e99b69ff --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/TagArrayBuilderExt.kt @@ -0,0 +1,61 @@ +/* + * 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.quartz.nip34Git.patch + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.aTag.toATag +import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitPgpSigTag +import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitTag +import com.vitorpamplona.quartz.nip34Git.patch.tags.Committer +import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitterTag +import com.vitorpamplona.quartz.nip34Git.patch.tags.ParentCommitTag +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.repository.tags.EucTag + +fun TagArrayBuilder.repository(rep: ATag) = addUnique(rep.toATagArray()) + +fun TagArrayBuilder.repository(rep: EventHintBundle) = addUnique(rep.toATag().toATagArray()) + +/** + * Adds the earliest-unique-commit `r` tag used by patches to point at the + * target repository. NIP-34 uses the plain `["r", ]` shape for + * patches (unlike the repository announcement which marks the tag with + * `euc`). We also emit the marked form so the same event can be matched by + * implementations that look for the marker. + */ +fun TagArrayBuilder.euc(commit: String) = addUnique(EucTag.assemble(commit)) + +fun TagArrayBuilder.commit(commit: String) = addUnique(CommitTag.assemble(commit)) + +fun TagArrayBuilder.parentCommit(commit: String) = addUnique(ParentCommitTag.assemble(commit)) + +fun TagArrayBuilder.commitPgpSig(sig: String) = addUnique(CommitPgpSigTag.assemble(sig)) + +fun TagArrayBuilder.committer( + name: String?, + email: String?, + timestamp: String?, + timezoneInMinutes: String?, +) = addUnique(CommitterTag.assemble(name, email, timestamp, timezoneInMinutes)) + +fun TagArrayBuilder.committer(committer: Committer) = committer(committer.name, committer.email, committer.timestamp, committer.timezoneInMinutes) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitPgpSigTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitPgpSigTag.kt new file mode 100644 index 000000000..4cb5ce12e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitPgpSigTag.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.nip34Git.patch.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 patch `commit-pgp-sig` tag: PGP signature of the commit, or empty + * string to indicate an unsigned commit. + */ +class CommitPgpSigTag { + companion object { + const val TAG_NAME = "commit-pgp-sig" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return tag[1] + } + + fun assemble(sig: String) = arrayOf(TAG_NAME, sig) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitTag.kt new file mode 100644 index 000000000..b285a120f --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitTag.kt @@ -0,0 +1,40 @@ +/* + * 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.quartz.nip34Git.patch.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-34 patch `commit` tag: the current commit hash of the patch. */ +class CommitTag { + companion object { + const val TAG_NAME = "commit" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(commit: String) = arrayOf(TAG_NAME, commit) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitterTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitterTag.kt new file mode 100644 index 000000000..bd6d5363c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/CommitterTag.kt @@ -0,0 +1,64 @@ +/* + * 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.quartz.nip34Git.patch.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 patch `committer` tag: `["committer", name, email, unix-time, tz-offset-minutes]`. + */ +data class Committer( + val name: String?, + val email: String?, + val timestamp: String?, + val timezoneInMinutes: String?, +) + +class CommitterTag { + companion object { + const val TAG_NAME = "committer" + + fun parse(tag: Array): Committer? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return Committer( + tag.getOrNull(1), + tag.getOrNull(2), + tag.getOrNull(3), + tag.getOrNull(4), + ) + } + + fun assemble( + name: String?, + email: String?, + timestamp: String?, + timezoneInMinutes: String?, + ) = arrayOf( + TAG_NAME, + name ?: "", + email ?: "", + timestamp ?: "", + timezoneInMinutes ?: "", + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/ParentCommitTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/ParentCommitTag.kt new file mode 100644 index 000000000..6927b6345 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/patch/tags/ParentCommitTag.kt @@ -0,0 +1,40 @@ +/* + * 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.quartz.nip34Git.patch.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-34 patch `parent-commit` tag: parent commit hash. */ +class ParentCommitTag { + companion object { + const val TAG_NAME = "parent-commit" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(commit: String) = arrayOf(TAG_NAME, commit) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/GitPullRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/GitPullRequestEvent.kt new file mode 100644 index 000000000..2f843a6af --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/GitPullRequestEvent.kt @@ -0,0 +1,149 @@ +/* + * 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.quartz.nip34Git.pr + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip14Subject.SubjectTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.pr.tags.BranchNameTag +import com.vitorpamplona.quartz.nip34Git.pr.tags.CurrentCommitTag +import com.vitorpamplona.quartz.nip34Git.pr.tags.MergeBaseTag +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-34 kind 1618 — Pull Request. + * + * Proposes merging a branch described by a `clone` URL and a current commit + * tip (`c`) without inlining a patch. The markdown content describes the + * change. If this is a revision of a previous patch, an `e` tag points at + * the root patch. + */ +@Immutable +class GitPullRequestEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider, + EventHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + + fun repository() = tags.firstNotNullOfOrNull(ATag::parse) + + fun repositoryAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) + + fun earliestUniqueCommit(): String? = tags.firstOrNull { it.size > 1 && it[0] == "r" && it[1].isNotEmpty() }?.get(1) + + fun currentCommit(): String? = tags.firstNotNullOfOrNull(CurrentCommitTag::parse) + + fun cloneUrls(): List = tags.mapNotNull(CloneTag::parse) + + fun subject(): String? = tags.firstNotNullOfOrNull(SubjectTag::parse) + + fun labels(): List = tags.hashtags() + + fun branchName(): String? = tags.firstNotNullOfOrNull(BranchNameTag::parse) + + fun mergeBase(): String? = tags.firstNotNullOfOrNull(MergeBaseTag::parse) + + /** Root patch event ID if this PR is a revision of a prior patch. */ + fun rootPatchId(): HexKey? = tags.firstNotNullOfOrNull(ETag::parseId) + + companion object { + const val KIND = 1618 + const val ALT = "A Git Pull Request" + + /** + * Build a NIP-34 kind-1618 pull request event. + * + * @param description markdown description of the pull request. + * @param repository event-hint bundle for the target repository. + * @param earliestUniqueCommit repository's earliest unique commit (`r` tag). + * @param currentCommit tip commit of the PR branch (`c` tag). + * @param cloneUrls at least one clone URL where [currentCommit] can be fetched. + * @param subject optional PR subject. + * @param labels optional PR labels (encoded as `t` tags). + * @param branchName optional recommended local checkout branch name. + * @param mergeBase optional most recent common ancestor with the target branch. + * @param rootPatch optional pointer to a prior patch this PR supersedes. + * @param notify additional recipient pubkeys. + */ + fun build( + description: String, + repository: EventHintBundle, + earliestUniqueCommit: String, + currentCommit: String, + cloneUrls: List, + subject: String? = null, + labels: List = emptyList(), + branchName: String? = null, + mergeBase: String? = null, + rootPatch: EventHintBundle? = null, + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + alt(ALT) + repository(repository) + euc(earliestUniqueCommit) + pTag(repository.event.pubKey, repository.authorHomeRelay) + if (notify.isNotEmpty()) pTags(notify) + currentCommit(currentCommit) + cloneUrls.forEach { cloneUrl(it) } + subject?.let { subject(it) } + if (labels.isNotEmpty()) hashtags(labels) + branchName?.let { branchName(it) } + mergeBase?.let { mergeBase(it) } + rootPatch?.let { rootPatch(it) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/GitPullRequestUpdateEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/GitPullRequestUpdateEvent.kt new file mode 100644 index 000000000..81d2e1927 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/GitPullRequestUpdateEvent.kt @@ -0,0 +1,130 @@ +/* + * 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.quartz.nip34Git.pr + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip22Comments.tags.RootAuthorTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootEventTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.pr.tags.CurrentCommitTag +import com.vitorpamplona.quartz.nip34Git.pr.tags.MergeBaseTag +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-34 kind 1619 — Pull Request Update. + * + * Updates the tip of a pending pull request without creating a new PR + * event. Uses NIP-22 `E`/`P` tags to reference the parent PR. + */ +@Immutable +class GitPullRequestUpdateEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider, + EventHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + tags.mapNotNull(RootAuthorTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + tags.mapNotNull { RootAuthorTag.parseKey(it) } + + override fun eventHints() = tags.mapNotNull(RootEventTag::parseAsHint) + + override fun linkedEventIds() = tags.mapNotNull(RootEventTag::parseKey) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + + fun repository() = tags.firstNotNullOfOrNull(ATag::parse) + + fun repositoryAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) + + fun parentPullRequestId(): HexKey? = tags.firstNotNullOfOrNull(RootEventTag::parseKey) + + fun parentPullRequestAuthor(): HexKey? = tags.firstNotNullOfOrNull { RootAuthorTag.parseKey(it) } + + fun currentCommit(): String? = tags.firstNotNullOfOrNull(CurrentCommitTag::parse) + + fun cloneUrls(): List = tags.mapNotNull(CloneTag::parse) + + fun earliestUniqueCommit(): String? = tags.firstOrNull { it.size > 1 && it[0] == "r" && it[1].isNotEmpty() }?.get(1) + + fun mergeBase(): String? = tags.firstNotNullOfOrNull(MergeBaseTag::parse) + + companion object { + const val KIND = 1619 + const val ALT = "A Git Pull Request Update" + + /** + * Build a NIP-34 kind-1619 pull-request update event. + * + * @param parentPullRequest the pull request being updated. + * @param repository the target repository. + * @param earliestUniqueCommit repository's earliest unique commit (`r` tag). + * @param currentCommit new PR tip commit (`c` tag). + * @param cloneUrls at least one clone URL where [currentCommit] can be fetched. + * @param mergeBase optional updated merge-base. + * @param notify optional additional recipient pubkeys. + */ + fun build( + parentPullRequest: EventHintBundle, + repository: EventHintBundle, + earliestUniqueCommit: String, + currentCommit: String, + cloneUrls: List, + mergeBase: String? = null, + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) + parentPullRequest(parentPullRequest) + parentPullRequestAuthor(parentPullRequest) + repository(repository) + euc(earliestUniqueCommit) + pTag(repository.event.pubKey, repository.authorHomeRelay) + if (notify.isNotEmpty()) pTags(notify) + currentCommit(currentCommit) + cloneUrls.forEach { cloneUrl(it) } + mergeBase?.let { mergeBase(it) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/TagArrayBuilderExt.kt new file mode 100644 index 000000000..551384dd0 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/TagArrayBuilderExt.kt @@ -0,0 +1,53 @@ +/* + * 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.quartz.nip34Git.pr + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.aTag.toATag +import com.vitorpamplona.quartz.nip01Core.tags.events.toETagArray +import com.vitorpamplona.quartz.nip14Subject.SubjectTag +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.pr.tags.BranchNameTag +import com.vitorpamplona.quartz.nip34Git.pr.tags.CurrentCommitTag +import com.vitorpamplona.quartz.nip34Git.pr.tags.MergeBaseTag +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.EucTag + +fun TagArrayBuilder.repository(rep: ATag) = addUnique(rep.toATagArray()) + +fun TagArrayBuilder.repository(rep: EventHintBundle) = addUnique(rep.toATag().toATagArray()) + +fun TagArrayBuilder.euc(commit: String) = addUnique(EucTag.assemble(commit)) + +fun TagArrayBuilder.currentCommit(commit: String) = addUnique(CurrentCommitTag.assemble(commit)) + +fun TagArrayBuilder.cloneUrl(url: String) = add(CloneTag.assemble(url)) + +fun TagArrayBuilder.subject(subject: String) = addUnique(SubjectTag.assemble(subject)) + +fun TagArrayBuilder.branchName(name: String) = addUnique(BranchNameTag.assemble(name)) + +fun TagArrayBuilder.mergeBase(commit: String) = addUnique(MergeBaseTag.assemble(commit)) + +fun TagArrayBuilder.rootPatch(patch: EventHintBundle) = add(patch.toETagArray()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/UpdateTagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/UpdateTagArrayBuilderExt.kt new file mode 100644 index 000000000..98f5e21c8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/UpdateTagArrayBuilderExt.kt @@ -0,0 +1,64 @@ +/* + * 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.quartz.nip34Git.pr + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.aTag.toATag +import com.vitorpamplona.quartz.nip22Comments.tags.RootAuthorTag +import com.vitorpamplona.quartz.nip22Comments.tags.RootEventTag +import com.vitorpamplona.quartz.nip34Git.pr.tags.CurrentCommitTag +import com.vitorpamplona.quartz.nip34Git.pr.tags.MergeBaseTag +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.EucTag + +fun TagArrayBuilder.repository(rep: ATag) = addUnique(rep.toATagArray()) + +fun TagArrayBuilder.repository(rep: EventHintBundle) = addUnique(rep.toATag().toATagArray()) + +fun TagArrayBuilder.euc(commit: String) = addUnique(EucTag.assemble(commit)) + +fun TagArrayBuilder.currentCommit(commit: String) = addUnique(CurrentCommitTag.assemble(commit)) + +fun TagArrayBuilder.cloneUrl(url: String) = add(CloneTag.assemble(url)) + +fun TagArrayBuilder.mergeBase(commit: String) = addUnique(MergeBaseTag.assemble(commit)) + +/** Adds the NIP-22 `E` tag pointing at the parent Pull Request. */ +fun TagArrayBuilder.parentPullRequest(pr: EventHintBundle) = + addUnique( + RootEventTag.assemble( + pr.event.id, + pr.relay, + pr.event.pubKey, + ), + ) + +/** Adds the NIP-22 `P` tag pointing at the parent Pull Request's author. */ +fun TagArrayBuilder.parentPullRequestAuthor(pr: EventHintBundle) = + addUnique( + RootAuthorTag.assemble( + pr.event.pubKey, + pr.authorHomeRelay, + ), + ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/BranchNameTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/BranchNameTag.kt new file mode 100644 index 000000000..a241337a9 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/BranchNameTag.kt @@ -0,0 +1,40 @@ +/* + * 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.quartz.nip34Git.pr.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-34 pull-request `branch-name` tag: recommended local checkout name. */ +class BranchNameTag { + companion object { + const val TAG_NAME = "branch-name" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(branchName: String) = arrayOf(TAG_NAME, branchName) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/CurrentCommitTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/CurrentCommitTag.kt new file mode 100644 index 000000000..7109c1d6c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/CurrentCommitTag.kt @@ -0,0 +1,40 @@ +/* + * 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.quartz.nip34Git.pr.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-34 pull-request `c` tag: the current tip commit of the PR branch. */ +class CurrentCommitTag { + companion object { + const val TAG_NAME = "c" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(commit: String) = arrayOf(TAG_NAME, commit) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/MergeBaseTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/MergeBaseTag.kt new file mode 100644 index 000000000..19232d62a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/pr/tags/MergeBaseTag.kt @@ -0,0 +1,43 @@ +/* + * 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.quartz.nip34Git.pr.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 pull-request `merge-base` tag: most recent common ancestor with the + * target branch of the repository. + */ +class MergeBaseTag { + companion object { + const val TAG_NAME = "merge-base" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(commit: String) = arrayOf(TAG_NAME, commit) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt index cdd73a9e3..78aa84d9b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt @@ -26,10 +26,16 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.EucTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.MaintainersTag import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.RelaysTag import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.uuid.ExperimentalUuidApi @@ -48,13 +54,44 @@ class GitRepositoryEvent( fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) + /** First web URL, for backwards compatibility. Prefer [webs]. */ fun web() = tags.firstNotNullOfOrNull(WebTag::parse) + fun webs(): List = tags.mapNotNull(WebTag::parse) + + /** First clone URL, for backwards compatibility. Prefer [clones]. */ fun clone() = tags.firstNotNullOfOrNull(CloneTag::parse) + fun clones(): List = tags.mapNotNull(CloneTag::parse) + + /** + * Relays the repository author monitors for patches and issues. NIP-34 + * encodes this as a single multi-value tag, so the first matching + * `relays` tag wins. + */ + fun relays(): List = tags.firstNotNullOfOrNull(RelaysTag::parse) ?: emptyList() + + /** + * Accepted maintainer pubkeys. The event author is an implicit maintainer + * and is not required to appear in this list. + */ + fun maintainers(): List = tags.firstNotNullOfOrNull(MaintainersTag::parse) ?: emptyList() + + fun hashtags(): List = tags.hashtags() + + /** Earliest-unique-commit ID (NIP-34 `["r", , "euc"]`). */ + fun earliestUniqueCommit(): String? = tags.firstNotNullOfOrNull(EucTag::parse) + + /** + * `true` if the event carries `["t", "personal-fork"]`, signaling the + * author does not actively seek patches/feedback for this repository. + */ + fun isPersonalFork(): Boolean = tags.any { HashtagTag.isTagged(it, PERSONAL_FORK) } + companion object { const val KIND = 30617 const val ALT_DESCRIPTION = "Git Repository" + const val PERSONAL_FORK = "personal-fork" @OptIn(ExperimentalUuidApi::class) fun build( @@ -74,5 +111,34 @@ class GitRepositoryEvent( cloneUrl?.let { cloneUrl(it) } initializer() } + + @OptIn(ExperimentalUuidApi::class) + fun build( + name: String, + description: String?, + webUrls: List, + cloneUrls: List, + relays: List, + maintainers: List, + hashtags: List, + earliestUniqueCommit: String?, + personalFork: Boolean = false, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + dTag(dTag) + name(name) + description?.let { description(it) } + webUrls.forEach { webUrl(it) } + cloneUrls.forEach { cloneUrl(it) } + if (relays.isNotEmpty()) relays(relays) + if (maintainers.isNotEmpty()) maintainers(maintainers) + if (hashtags.isNotEmpty()) hashtags(hashtags) + earliestUniqueCommit?.let { euc(it) } + if (personalFork) hashtag(PERSONAL_FORK) + initializer() + } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt index 32c94a899..4fb760db5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/TagArrayBuilderExt.kt @@ -20,16 +20,30 @@ */ package com.vitorpamplona.quartz.nip34Git.repository +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.EucTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.MaintainersTag import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag +import com.vitorpamplona.quartz.nip34Git.repository.tags.RelaysTag import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) fun TagArrayBuilder.description(description: String) = addUnique(DescriptionTag.assemble(description)) -fun TagArrayBuilder.webUrl(webUrl: String) = addUnique(WebTag.assemble(webUrl)) +fun TagArrayBuilder.webUrl(webUrl: String) = add(WebTag.assemble(webUrl)) -fun TagArrayBuilder.cloneUrl(cloneUrl: String) = addUnique(CloneTag.assemble(cloneUrl)) +fun TagArrayBuilder.webUrls(webUrls: List) = addAll(webUrls.map(WebTag::assemble)) + +fun TagArrayBuilder.cloneUrl(cloneUrl: String) = add(CloneTag.assemble(cloneUrl)) + +fun TagArrayBuilder.cloneUrls(cloneUrls: List) = addAll(cloneUrls.map(CloneTag::assemble)) + +fun TagArrayBuilder.relays(relays: List) = addUnique(RelaysTag.assemble(relays)) + +fun TagArrayBuilder.maintainers(maintainers: List) = addUnique(MaintainersTag.assemble(maintainers)) + +fun TagArrayBuilder.euc(commit: String) = addUnique(EucTag.assemble(commit)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/EucTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/EucTag.kt new file mode 100644 index 000000000..b9a7166ac --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/EucTag.kt @@ -0,0 +1,48 @@ +/* + * 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.quartz.nip34Git.repository.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 earliest-unique-commit marker: an `r` tag whose third element is the + * literal marker `"euc"`. Used to identify repositories across hosting + * locations / forks: + * + * ["r", "", "euc"] + */ +class EucTag { + companion object { + const val TAG_NAME = "r" + const val MARKER = "euc" + + fun parse(tag: Array): String? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[2] == MARKER) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(commit: String) = arrayOf(TAG_NAME, commit, MARKER) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/MaintainersTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/MaintainersTag.kt new file mode 100644 index 000000000..3a071bdfc --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/MaintainersTag.kt @@ -0,0 +1,50 @@ +/* + * 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.quartz.nip34Git.repository.tags + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 repository `maintainers` tag. + * + * A single multi-value tag listing the pubkeys of accepted maintainers (the + * author of the event is always an implicit maintainer): + * + * ["maintainers", "", "", ...] + */ +class MaintainersTag { + companion object { + const val TAG_NAME = "maintainers" + + fun parse(tag: Array): List? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return tag + .drop(1) + .filter { it.length == 64 } + .ifEmpty { null } + } + + fun assemble(maintainers: List): Array = (listOf(TAG_NAME) + maintainers.filter { it.isNotEmpty() }).toTypedArray() + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/RelaysTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/RelaysTag.kt new file mode 100644 index 000000000..4d6197153 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/repository/tags/RelaysTag.kt @@ -0,0 +1,59 @@ +/* + * 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.quartz.nip34Git.repository.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 repository `relays` tag. + * + * A single multi-value tag listing the relays the repository author monitors + * for patches and issues: + * + * ["relays", "wss://relay1", "wss://relay2", ...] + */ +class RelaysTag { + companion object { + const val TAG_NAME = "relays" + + fun parse(tag: Array): List? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return tag.drop(1).filter { it.isNotEmpty() }.ifEmpty { null } + } + + fun parseNorm(tag: Array): List? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return tag + .drop(1) + .mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) } + .ifEmpty { null } + } + + fun assemble(relays: List): Array = (listOf(TAG_NAME) + relays.filter { it.isNotEmpty() }).toTypedArray() + + fun assembleNorm(relays: List): Array = (listOf(TAG_NAME) + relays.map { it.url }).toTypedArray() + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/GitRepositoryStateEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/GitRepositoryStateEvent.kt new file mode 100644 index 000000000..2c0f66f97 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/GitRepositoryStateEvent.kt @@ -0,0 +1,80 @@ +/* + * 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.quartz.nip34Git.state + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip34Git.state.tags.HeadTag +import com.vitorpamplona.quartz.nip34Git.state.tags.RefTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-34 kind 30618 — Repository State Announcement. + * + * Publishes the current branch tips, tag refs, and HEAD for a repository + * identified by the same `d` tag used in the corresponding kind-30617 + * announcement. Absence of any `refs` tags signals that the author has + * stopped tracking state (distinct from a NIP-09 deletion). + */ +@Immutable +class GitRepositoryStateEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun refs(): List = tags.mapNotNull(RefTag::parse) + + fun branches(): List = refs().filter { it.kind == RefTag.Kind.BRANCH } + + fun tagRefs(): List = refs().filter { it.kind == RefTag.Kind.TAG } + + fun head(): String? = tags.firstNotNullOfOrNull(HeadTag::parse) + + /** `true` if the author is no longer tracking state for this repository. */ + fun isDiscontinued(): Boolean = refs().isEmpty() + + companion object { + const val KIND = 30618 + const val ALT_DESCRIPTION = "Git Repository State" + + fun build( + dTag: String, + refs: List, + head: String? = null, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + dTag(dTag) + refs.forEach { ref(it) } + head?.let { head(it) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/TagArrayBuilderExt.kt new file mode 100644 index 000000000..98a919581 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/TagArrayBuilderExt.kt @@ -0,0 +1,41 @@ +/* + * 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.quartz.nip34Git.state + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip34Git.state.tags.HeadTag +import com.vitorpamplona.quartz.nip34Git.state.tags.RefTag + +fun TagArrayBuilder.ref(ref: RefTag) = addUnique(ref.toTagArray()) + +fun TagArrayBuilder.branch( + name: String, + commit: String, + parentLineage: List = emptyList(), +) = ref(RefTag.branch(name, commit, parentLineage)) + +fun TagArrayBuilder.tag( + name: String, + commit: String, + parentLineage: List = emptyList(), +) = ref(RefTag.tag(name, commit, parentLineage)) + +fun TagArrayBuilder.head(branchName: String) = addUnique(HeadTag.assemble(branchName)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/tags/HeadTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/tags/HeadTag.kt new file mode 100644 index 000000000..d5cbd1c4d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/tags/HeadTag.kt @@ -0,0 +1,57 @@ +/* + * 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.quartz.nip34Git.state.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 repository-state HEAD tag: `["HEAD", "ref: refs/heads/"]`. + */ +class HeadTag { + companion object { + const val TAG_NAME = "HEAD" + const val REF_PREFIX = "ref: " + + /** Returns the branch name pointed to by HEAD, or `null` if the tag is malformed. */ + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + val value = tag[1] + ensure(value.startsWith(REF_PREFIX)) { return null } + val ref = value.removePrefix(REF_PREFIX) + ensure(ref.startsWith(RefTag.PREFIX_HEADS)) { return null } + val branch = ref.removePrefix(RefTag.PREFIX_HEADS) + ensure(branch.isNotEmpty()) { return null } + return branch + } + + /** Returns the raw `ref: refs/heads/` payload. */ + fun parseRaw(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(branchName: String) = arrayOf(TAG_NAME, REF_PREFIX + RefTag.PREFIX_HEADS + branchName) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/tags/RefTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/tags/RefTag.kt new file mode 100644 index 000000000..5a786a8c5 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/state/tags/RefTag.kt @@ -0,0 +1,97 @@ +/* + * 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.quartz.nip34Git.state.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 repository-state ref tag. + * + * A branch ref is encoded as: + * + * ["refs/heads/", "", "", ...] + * + * and a tag ref as: + * + * ["refs/tags/", "", "", ...] + * + * The parent elements are optional and allow the maintainer to publish how + * many commits their branch is ahead of a known ancestor. + */ +class RefTag( + val name: String, + val commit: String, + val parentLineage: List, +) { + fun toTagArray(): Array = (listOf(name, commit) + parentLineage).toTypedArray() + + val kind: Kind = + when { + name.startsWith(PREFIX_HEADS) -> Kind.BRANCH + name.startsWith(PREFIX_TAGS) -> Kind.TAG + else -> Kind.OTHER + } + + val shortName: String = + when (kind) { + Kind.BRANCH -> name.removePrefix(PREFIX_HEADS) + Kind.TAG -> name.removePrefix(PREFIX_TAGS) + Kind.OTHER -> name + } + + enum class Kind { BRANCH, TAG, OTHER } + + companion object { + const val PREFIX_HEADS = "refs/heads/" + const val PREFIX_TAGS = "refs/tags/" + + fun isRefTag(tag: Array): Boolean { + if (tag.isEmpty()) return false + return tag[0].startsWith(PREFIX_HEADS) || tag[0].startsWith(PREFIX_TAGS) + } + + fun parse(tag: Array): RefTag? { + ensure(tag.has(1)) { return null } + ensure(isRefTag(tag)) { return null } + ensure(tag[1].isNotEmpty()) { return null } + val lineage = + if (tag.size > 2) { + tag.drop(2).filter { it.isNotEmpty() } + } else { + emptyList() + } + return RefTag(tag[0], tag[1], lineage) + } + + fun branch( + name: String, + commit: String, + parentLineage: List = emptyList(), + ) = RefTag(PREFIX_HEADS + name, commit, parentLineage) + + fun tag( + name: String, + commit: String, + parentLineage: List = emptyList(), + ) = RefTag(PREFIX_TAGS + name, commit, parentLineage) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusAppliedEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusAppliedEvent.kt new file mode 100644 index 000000000..32501ed20 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusAppliedEvent.kt @@ -0,0 +1,98 @@ +/* + * 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.quartz.nip34Git.status + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip34Git.status.tags.AppliedAsCommitsTag +import com.vitorpamplona.quartz.nip34Git.status.tags.MergeCommitTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-34 kind 1631 — Status: Applied / Merged / Resolved. + * + * Reports that a patch/PR/issue has been applied, merged, or resolved. In + * addition to the common status tags, kind 1631 optionally carries: + * + * - `q` tags pointing at the specific applied patch event(s); + * - `merge-commit` with the merge commit hash; + * - `applied-as-commits` listing the individual commit hashes produced by + * applying the patch series. + */ +@Immutable +class GitStatusAppliedEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GitStatusEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun mergeCommit(): String? = tags.firstNotNullOfOrNull(MergeCommitTag::parse) + + fun appliedAsCommits(): List = tags.mapNotNull(AppliedAsCommitsTag::parse).flatten() + + fun appliedPatchIds(): List = tags.mapNotNull(QTag::parseEventId) + + companion object { + const val KIND = KIND_APPLIED + const val ALT = "A Git Applied Status" + + fun build( + content: String, + target: EventHintBundle, + appliedPatches: List> = emptyList(), + mergeCommit: String? = null, + appliedAsCommits: List = emptyList(), + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = GitStatusBuilders.buildStatus( + kind = KIND, + altDescriptor = ALT, + content = content, + target = target, + notify = notify, + createdAt = createdAt, + ) { + appliedPatches.forEach { patch -> + add( + QEventTag.assemble( + patch.event.id, + patch.relay, + patch.event.pubKey, + ), + ) + } + mergeCommit?.let { add(MergeCommitTag.assemble(it)) } + if (appliedAsCommits.isNotEmpty()) { + add(AppliedAsCommitsTag.assemble(appliedAsCommits)) + } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusBuilders.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusBuilders.kt new file mode 100644 index 000000000..b50a25444 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusBuilders.kt @@ -0,0 +1,63 @@ +/* + * 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.quartz.nip34Git.status + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Shared builder logic for NIP-34 status events. Every status event (kind + * 1630/1631/1632/1633) roots itself in a target patch/PR/issue via an `e` + * tag marked `"root"`, includes the target's author as a `p` tag, and + * optionally CC's additional pubkeys. + */ +object GitStatusBuilders { + fun buildStatus( + kind: Int, + altDescriptor: String, + content: String, + target: EventHintBundle, + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(kind, content, createdAt) { + alt(altDescriptor) + add( + MarkedETag.assemble( + target.event.id, + target.relay, + MarkedETag.MARKER.ROOT, + target.event.pubKey, + ), + ) + pTag(target.event.pubKey, target.authorHomeRelay) + if (notify.isNotEmpty()) pTags(notify) + initializer() + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusClosedEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusClosedEvent.kt new file mode 100644 index 000000000..d2b15c35d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusClosedEvent.kt @@ -0,0 +1,60 @@ +/* + * 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.quartz.nip34Git.status + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** NIP-34 kind 1632 — Status: Closed. */ +@Immutable +class GitStatusClosedEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GitStatusEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = KIND_CLOSED + const val ALT = "A Git Closed Status" + + fun build( + content: String, + target: EventHintBundle, + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = GitStatusBuilders.buildStatus( + kind = KIND, + altDescriptor = ALT, + content = content, + target = target, + notify = notify, + createdAt = createdAt, + initializer = initializer, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusDraftEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusDraftEvent.kt new file mode 100644 index 000000000..627f442f3 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusDraftEvent.kt @@ -0,0 +1,60 @@ +/* + * 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.quartz.nip34Git.status + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** NIP-34 kind 1633 — Status: Draft. */ +@Immutable +class GitStatusDraftEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GitStatusEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = KIND_DRAFT + const val ALT = "A Git Draft Status" + + fun build( + content: String, + target: EventHintBundle, + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = GitStatusBuilders.buildStatus( + kind = KIND, + altDescriptor = ALT, + content = content, + target = target, + notify = notify, + createdAt = createdAt, + initializer = initializer, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusEvent.kt new file mode 100644 index 000000000..c228555f8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusEvent.kt @@ -0,0 +1,84 @@ +/* + * 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.quartz.nip34Git.status + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag + +/** + * Common base for NIP-34 status events (kinds 1630/1631/1632/1633). Each + * event reports the status of a patch, pull request, or issue via an `e` + * tag marked `root` pointing at the target event, optionally followed by an + * `e` tag marked `reply` for revision chains. + */ +abstract class GitStatusEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, kind, tags, content, sig), + PubKeyHintProvider, + EventHintProvider, + AddressHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + + override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + + override fun linkedEventIds() = tags.mapNotNull(MarkedETag::parseId) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + + /** The target event ID (patch / PR / issue) this status refers to. */ + fun rootEventId(): HexKey? = tags.firstNotNullOfOrNull(MarkedETag::parseRootId) + + /** The accepted revision root ID when the status applies to a revision. */ + fun replyEventId(): HexKey? = tags.firstNotNullOfOrNull(MarkedETag::parseReply)?.eventId + + fun repositoryAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) + + fun repository() = tags.firstNotNullOfOrNull(ATag::parse) + + /** Earliest unique commit or merge/applied commit IDs, encoded as plain `r` tags. */ + fun referenceCommits(): List = + tags.mapNotNull { tag -> + if (tag.size > 1 && tag[0] == "r" && tag[1].isNotEmpty()) tag[1] else null + } + + companion object { + const val KIND_OPEN = 1630 + const val KIND_APPLIED = 1631 + const val KIND_CLOSED = 1632 + const val KIND_DRAFT = 1633 + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusOpenEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusOpenEvent.kt new file mode 100644 index 000000000..67b2afe71 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/GitStatusOpenEvent.kt @@ -0,0 +1,60 @@ +/* + * 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.quartz.nip34Git.status + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** NIP-34 kind 1630 — Status: Open. */ +@Immutable +class GitStatusOpenEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GitStatusEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = KIND_OPEN + const val ALT = "A Git Open Status" + + fun build( + content: String, + target: EventHintBundle, + notify: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = GitStatusBuilders.buildStatus( + kind = KIND, + altDescriptor = ALT, + content = content, + target = target, + notify = notify, + createdAt = createdAt, + initializer = initializer, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/tags/AppliedAsCommitsTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/tags/AppliedAsCommitsTag.kt new file mode 100644 index 000000000..938b41d27 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/tags/AppliedAsCommitsTag.kt @@ -0,0 +1,47 @@ +/* + * 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.quartz.nip34Git.status.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 status `applied-as-commits` tag used on kind 1631 to list the + * individual commit IDs that resulted from applying a patch series: + * + * ["applied-as-commits", "", "", ...] + */ +class AppliedAsCommitsTag { + companion object { + const val TAG_NAME = "applied-as-commits" + + fun parse(tag: Array): List? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return tag + .drop(1) + .filter { it.isNotEmpty() } + .ifEmpty { null } + } + + fun assemble(commits: List): Array = (listOf(TAG_NAME) + commits.filter { it.isNotEmpty() }).toTypedArray() + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/tags/MergeCommitTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/tags/MergeCommitTag.kt new file mode 100644 index 000000000..7f4d3600c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip34Git/status/tags/MergeCommitTag.kt @@ -0,0 +1,43 @@ +/* + * 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.quartz.nip34Git.status.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 status `merge-commit` tag used on kind 1631 to record the merge + * commit that integrated a patch series or pull request. + */ +class MergeCommitTag { + companion object { + const val TAG_NAME = "merge-commit" + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(commit: String) = arrayOf(TAG_NAME, commit) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt index 14e117603..64dced668 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent -import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.tags.GitAuthorTag import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.utils.TimeUtils @@ -45,13 +45,13 @@ class GitAuthorListEvent( sig: HexKey, ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), PubKeyHintProvider { - override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint) + override fun pubKeyHints() = tags.mapNotNull(GitAuthorTag::parseAsHint) - override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey) + override fun linkedPubKeys() = tags.mapNotNull(GitAuthorTag::parseKey) - fun publicAuthors() = tags.mapNotNull(UserTag::parse) + fun publicAuthors() = tags.mapNotNull(GitAuthorTag::parse) - suspend fun privateAuthors(signer: NostrSigner) = privateTags(signer)?.mapNotNull(UserTag::parse) + suspend fun privateAuthors(signer: NostrSigner) = privateTags(signer)?.mapNotNull(GitAuthorTag::parse) companion object { const val KIND = 10017 @@ -60,8 +60,8 @@ class GitAuthorListEvent( fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "") suspend fun create( - publicAuthors: List = emptyList(), - privateAuthors: List = emptyList(), + publicAuthors: List = emptyList(), + privateAuthors: List = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), ): GitAuthorListEvent = @@ -74,7 +74,7 @@ class GitAuthorListEvent( suspend fun add( earlierVersion: GitAuthorListEvent, - author: UserTag, + author: GitAuthorTag, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -98,7 +98,7 @@ class GitAuthorListEvent( suspend fun remove( earlierVersion: GitAuthorListEvent, - author: UserTag, + author: GitAuthorTag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), ): GitAuthorListEvent { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/tags/GitAuthorTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/tags/GitAuthorTag.kt new file mode 100644 index 000000000..19387ed98 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/tags/GitAuthorTag.kt @@ -0,0 +1,107 @@ +/* + * 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.quartz.nip51Lists.gitAuthorList.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +/** + * NIP-34 / NIP-51 kind-10017 git authors entry. Mirrors the follow-list `p` + * tag shape from NIP-02: + * + * ["p", "", "", ""] + * + * Unlike the mute-list `UserTag` (which stops at the relay hint) this class + * also carries an optional petname, preserving round-trip fidelity for git + * authors lists built on top of standard follow-list formats. + */ +@Immutable +data class GitAuthorTag( + val pubKey: HexKey, + val relayHint: NormalizedRelayUrl? = null, + val petname: String? = null, +) { + fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) + + fun toNPub(): String = pubKey.hexToByteArray().toNpub() + + fun toTagArray() = assemble(pubKey, relayHint, petname) + + fun toTagIdOnly() = assemble(pubKey, null, null) + + companion object { + const val TAG_NAME = "p" + + fun isTagged(tag: Array): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64 + + fun isTagged( + tag: Array, + key: HexKey, + ): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1] == key + + fun parse(tag: Tag): GitAuthorTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val petname = tag.getOrNull(3)?.takeIf { it.isNotEmpty() } + + return GitAuthorTag(tag[1], hint, petname) + } + + fun parseKey(tag: Array): HexKey? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + + fun parseAsHint(tag: Array): PubKeyHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) + } + + fun assemble( + pubkey: HexKey, + relayHint: NormalizedRelayUrl?, + petname: String?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url, petname?.takeIf { it.isNotEmpty() }) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index b7d3de13d..2ebafdbad 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -98,10 +98,18 @@ import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import com.vitorpamplona.quartz.nip32Labeling.LabelEvent +import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent +import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.state.GitRepositoryStateEvent +import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent +import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent +import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent +import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent @@ -402,7 +410,15 @@ class EventFactory { GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig) GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig) GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig) + GitPullRequestEvent.KIND -> GitPullRequestEvent(id, pubKey, createdAt, tags, content, sig) + GitPullRequestUpdateEvent.KIND -> GitPullRequestUpdateEvent(id, pubKey, createdAt, tags, content, sig) GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig) + GitRepositoryStateEvent.KIND -> GitRepositoryStateEvent(id, pubKey, createdAt, tags, content, sig) + GitStatusOpenEvent.KIND -> GitStatusOpenEvent(id, pubKey, createdAt, tags, content, sig) + GitStatusAppliedEvent.KIND -> GitStatusAppliedEvent(id, pubKey, createdAt, tags, content, sig) + GitStatusClosedEvent.KIND -> GitStatusClosedEvent(id, pubKey, createdAt, tags, content, sig) + GitStatusDraftEvent.KIND -> GitStatusDraftEvent(id, pubKey, createdAt, tags, content, sig) + UserGraspListEvent.KIND -> UserGraspListEvent(id, pubKey, createdAt, tags, content, sig) GoodWikiAuthorListEvent.KIND -> GoodWikiAuthorListEvent(id, pubKey, createdAt, tags, content, sig) GoodWikiRelayListEvent.KIND -> GoodWikiRelayListEvent(id, pubKey, createdAt, tags, content, sig) GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)