Merge pull request #2417 from vitorpamplona/claude/review-nip34-compliance-WGPXe

Add NIP-34 Git pull requests, status events, and repository state
This commit is contained in:
Vitor Pamplona
2026-04-15 20:41:37 -04:00
committed by GitHub
35 changed files with 2170 additions and 39 deletions
@@ -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/<pr-event-id>`.
*/
@Immutable
class UserGraspListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
/** Grasp server URLs in preference order (first = most preferred). */
fun grasps(): List<String> = tags.mapNotNull(GraspTag::parse)
fun graspsNorm(): List<NormalizedRelayUrl> = 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<String>,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<UserGraspListEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(ALT)
grasps.forEach { add(GraspTag.assemble(it)) }
initializer()
}
fun buildNorm(
grasps: List<NormalizedRelayUrl>,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<UserGraspListEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(ALT)
grasps.forEach { add(GraspTag.assemble(it)) }
initializer()
}
}
}
@@ -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>): 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<String>): 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)
}
}
@@ -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", <commit>]`. */
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<GitRepositoryEvent>,
earliestUniqueCommit: String,
commit: String? = null,
parentCommit: String? = null,
commitPgpSig: String? = null,
committer: Committer? = null,
notify: List<PTag> = emptyList(),
root: Boolean = false,
rootRevision: Boolean = false,
createdAt: Long = TimeUtils.now(),
signer: NostrSigner,
): GitPatchEvent {
val content = patch
val tags =
mutableListOf(
arrayOf<String>(),
)
initializer: TagArrayBuilder<GitPatchEvent>.() -> 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<GitRepositoryEvent>,
earliestUniqueCommit: String,
replyingTo: EventHintBundle<GitPatchEvent>,
commit: String? = null,
parentCommit: String? = null,
commitPgpSig: String? = null,
committer: Committer? = null,
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitPatchEvent>.() -> 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()
}
}
}
@@ -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<GitPatchEvent>.repository(rep: ATag) = addUnique(rep.toATagArray())
fun TagArrayBuilder<GitPatchEvent>.repository(rep: EventHintBundle<GitRepositoryEvent>) = 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", <commit>]` 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<GitPatchEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
fun TagArrayBuilder<GitPatchEvent>.commit(commit: String) = addUnique(CommitTag.assemble(commit))
fun TagArrayBuilder<GitPatchEvent>.parentCommit(commit: String) = addUnique(ParentCommitTag.assemble(commit))
fun TagArrayBuilder<GitPatchEvent>.commitPgpSig(sig: String) = addUnique(CommitPgpSigTag.assemble(sig))
fun TagArrayBuilder<GitPatchEvent>.committer(
name: String?,
email: String?,
timestamp: String?,
timezoneInMinutes: String?,
) = addUnique(CommitterTag.assemble(name, email, timestamp, timezoneInMinutes))
fun TagArrayBuilder<GitPatchEvent>.committer(committer: Committer) = committer(committer.name, committer.email, committer.timestamp, committer.timezoneInMinutes)
@@ -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>): 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)
}
}
@@ -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>): 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)
}
}
@@ -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<String>): 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 ?: "",
)
}
}
@@ -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>): 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)
}
}
@@ -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<Array<String>>,
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<String> = tags.mapNotNull(CloneTag::parse)
fun subject(): String? = tags.firstNotNullOfOrNull(SubjectTag::parse)
fun labels(): List<String> = 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<GitRepositoryEvent>,
earliestUniqueCommit: String,
currentCommit: String,
cloneUrls: List<String>,
subject: String? = null,
labels: List<String> = emptyList(),
branchName: String? = null,
mergeBase: String? = null,
rootPatch: EventHintBundle<com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent>? = null,
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitPullRequestEvent>.() -> Unit = {},
) = eventTemplate<GitPullRequestEvent>(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()
}
}
}
@@ -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<Array<String>>,
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<String> = 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<GitPullRequestEvent>,
repository: EventHintBundle<GitRepositoryEvent>,
earliestUniqueCommit: String,
currentCommit: String,
cloneUrls: List<String>,
mergeBase: String? = null,
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitPullRequestUpdateEvent>.() -> Unit = {},
) = eventTemplate<GitPullRequestUpdateEvent>(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()
}
}
}
@@ -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<GitPullRequestEvent>.repository(rep: ATag) = addUnique(rep.toATagArray())
fun TagArrayBuilder<GitPullRequestEvent>.repository(rep: EventHintBundle<GitRepositoryEvent>) = addUnique(rep.toATag().toATagArray())
fun TagArrayBuilder<GitPullRequestEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
fun TagArrayBuilder<GitPullRequestEvent>.currentCommit(commit: String) = addUnique(CurrentCommitTag.assemble(commit))
fun TagArrayBuilder<GitPullRequestEvent>.cloneUrl(url: String) = add(CloneTag.assemble(url))
fun TagArrayBuilder<GitPullRequestEvent>.subject(subject: String) = addUnique(SubjectTag.assemble(subject))
fun TagArrayBuilder<GitPullRequestEvent>.branchName(name: String) = addUnique(BranchNameTag.assemble(name))
fun TagArrayBuilder<GitPullRequestEvent>.mergeBase(commit: String) = addUnique(MergeBaseTag.assemble(commit))
fun TagArrayBuilder<GitPullRequestEvent>.rootPatch(patch: EventHintBundle<GitPatchEvent>) = add(patch.toETagArray())
@@ -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<GitPullRequestUpdateEvent>.repository(rep: ATag) = addUnique(rep.toATagArray())
fun TagArrayBuilder<GitPullRequestUpdateEvent>.repository(rep: EventHintBundle<GitRepositoryEvent>) = addUnique(rep.toATag().toATagArray())
fun TagArrayBuilder<GitPullRequestUpdateEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
fun TagArrayBuilder<GitPullRequestUpdateEvent>.currentCommit(commit: String) = addUnique(CurrentCommitTag.assemble(commit))
fun TagArrayBuilder<GitPullRequestUpdateEvent>.cloneUrl(url: String) = add(CloneTag.assemble(url))
fun TagArrayBuilder<GitPullRequestUpdateEvent>.mergeBase(commit: String) = addUnique(MergeBaseTag.assemble(commit))
/** Adds the NIP-22 `E` tag pointing at the parent Pull Request. */
fun TagArrayBuilder<GitPullRequestUpdateEvent>.parentPullRequest(pr: EventHintBundle<GitPullRequestEvent>) =
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<GitPullRequestUpdateEvent>.parentPullRequestAuthor(pr: EventHintBundle<GitPullRequestEvent>) =
addUnique(
RootAuthorTag.assemble(
pr.event.pubKey,
pr.authorHomeRelay,
),
)
@@ -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>): 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)
}
}
@@ -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>): 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)
}
}
@@ -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>): 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)
}
}
@@ -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<String> = tags.mapNotNull(WebTag::parse)
/** First clone URL, for backwards compatibility. Prefer [clones]. */
fun clone() = tags.firstNotNullOfOrNull(CloneTag::parse)
fun clones(): List<String> = 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<String> = 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<HexKey> = tags.firstNotNullOfOrNull(MaintainersTag::parse) ?: emptyList()
fun hashtags(): List<String> = tags.hashtags()
/** Earliest-unique-commit ID (NIP-34 `["r", <commit>, "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<String>,
cloneUrls: List<String>,
relays: List<String>,
maintainers: List<HexKey>,
hashtags: List<String>,
earliestUniqueCommit: String?,
personalFork: Boolean = false,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitRepositoryEvent>.() -> 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()
}
}
}
@@ -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<GitRepositoryEvent>.name(name: String) = addUnique(NameTag.assemble(name))
fun TagArrayBuilder<GitRepositoryEvent>.description(description: String) = addUnique(DescriptionTag.assemble(description))
fun TagArrayBuilder<GitRepositoryEvent>.webUrl(webUrl: String) = addUnique(WebTag.assemble(webUrl))
fun TagArrayBuilder<GitRepositoryEvent>.webUrl(webUrl: String) = add(WebTag.assemble(webUrl))
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrl(cloneUrl: String) = addUnique(CloneTag.assemble(cloneUrl))
fun TagArrayBuilder<GitRepositoryEvent>.webUrls(webUrls: List<String>) = addAll(webUrls.map(WebTag::assemble))
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrl(cloneUrl: String) = add(CloneTag.assemble(cloneUrl))
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrls(cloneUrls: List<String>) = addAll(cloneUrls.map(CloneTag::assemble))
fun TagArrayBuilder<GitRepositoryEvent>.relays(relays: List<String>) = addUnique(RelaysTag.assemble(relays))
fun TagArrayBuilder<GitRepositoryEvent>.maintainers(maintainers: List<HexKey>) = addUnique(MaintainersTag.assemble(maintainers))
fun TagArrayBuilder<GitRepositoryEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
@@ -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", "<commit-id>", "euc"]
*/
class EucTag {
companion object {
const val TAG_NAME = "r"
const val MARKER = "euc"
fun parse(tag: Array<String>): 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)
}
}
@@ -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", "<pubkey1>", "<pubkey2>", ...]
*/
class MaintainersTag {
companion object {
const val TAG_NAME = "maintainers"
fun parse(tag: Array<String>): List<HexKey>? {
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<HexKey>): Array<String> = (listOf(TAG_NAME) + maintainers.filter { it.isNotEmpty() }).toTypedArray()
}
}
@@ -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<String>): List<String>? {
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<String>): List<NormalizedRelayUrl>? {
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<String>): Array<String> = (listOf(TAG_NAME) + relays.filter { it.isNotEmpty() }).toTypedArray()
fun assembleNorm(relays: List<NormalizedRelayUrl>): Array<String> = (listOf(TAG_NAME) + relays.map { it.url }).toTypedArray()
}
}
@@ -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<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun refs(): List<RefTag> = tags.mapNotNull(RefTag::parse)
fun branches(): List<RefTag> = refs().filter { it.kind == RefTag.Kind.BRANCH }
fun tagRefs(): List<RefTag> = 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<RefTag>,
head: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitRepositoryStateEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTag)
refs.forEach { ref(it) }
head?.let { head(it) }
initializer()
}
}
}
@@ -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<GitRepositoryStateEvent>.ref(ref: RefTag) = addUnique(ref.toTagArray())
fun TagArrayBuilder<GitRepositoryStateEvent>.branch(
name: String,
commit: String,
parentLineage: List<String> = emptyList(),
) = ref(RefTag.branch(name, commit, parentLineage))
fun TagArrayBuilder<GitRepositoryStateEvent>.tag(
name: String,
commit: String,
parentLineage: List<String> = emptyList(),
) = ref(RefTag.tag(name, commit, parentLineage))
fun TagArrayBuilder<GitRepositoryStateEvent>.head(branchName: String) = addUnique(HeadTag.assemble(branchName))
@@ -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/<branch>"]`.
*/
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>): 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/<branch>` payload. */
fun parseRaw(tag: Array<String>): 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)
}
}
@@ -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/<branch>", "<commit-id>", "<shorthand-parent>", ...]
*
* and a tag ref as:
*
* ["refs/tags/<tag>", "<commit-id>", "<shorthand-parent>", ...]
*
* 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<String>,
) {
fun toTagArray(): Array<String> = (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<String>): Boolean {
if (tag.isEmpty()) return false
return tag[0].startsWith(PREFIX_HEADS) || tag[0].startsWith(PREFIX_TAGS)
}
fun parse(tag: Array<String>): 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<String> = emptyList(),
) = RefTag(PREFIX_HEADS + name, commit, parentLineage)
fun tag(
name: String,
commit: String,
parentLineage: List<String> = emptyList(),
) = RefTag(PREFIX_TAGS + name, commit, parentLineage)
}
}
@@ -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<Array<String>>,
content: String,
sig: HexKey,
) : GitStatusEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun mergeCommit(): String? = tags.firstNotNullOfOrNull(MergeCommitTag::parse)
fun appliedAsCommits(): List<String> = tags.mapNotNull(AppliedAsCommitsTag::parse).flatten()
fun appliedPatchIds(): List<HexKey> = tags.mapNotNull(QTag::parseEventId)
companion object {
const val KIND = KIND_APPLIED
const val ALT = "A Git Applied Status"
fun <T : Event> build(
content: String,
target: EventHintBundle<T>,
appliedPatches: List<EventHintBundle<com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent>> = emptyList(),
mergeCommit: String? = null,
appliedAsCommits: List<String> = emptyList(),
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitStatusAppliedEvent>.() -> Unit = {},
) = GitStatusBuilders.buildStatus<GitStatusAppliedEvent, T>(
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()
}
}
}
@@ -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 <E : GitStatusEvent, T : Event> buildStatus(
kind: Int,
altDescriptor: String,
content: String,
target: EventHintBundle<T>,
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<E>.() -> Unit = {},
) = eventTemplate<E>(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()
}
}
@@ -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<Array<String>>,
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 <T : com.vitorpamplona.quartz.nip01Core.core.Event> build(
content: String,
target: EventHintBundle<T>,
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitStatusClosedEvent>.() -> Unit = {},
) = GitStatusBuilders.buildStatus<GitStatusClosedEvent, T>(
kind = KIND,
altDescriptor = ALT,
content = content,
target = target,
notify = notify,
createdAt = createdAt,
initializer = initializer,
)
}
}
@@ -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<Array<String>>,
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 <T : com.vitorpamplona.quartz.nip01Core.core.Event> build(
content: String,
target: EventHintBundle<T>,
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitStatusDraftEvent>.() -> Unit = {},
) = GitStatusBuilders.buildStatus<GitStatusDraftEvent, T>(
kind = KIND,
altDescriptor = ALT,
content = content,
target = target,
notify = notify,
createdAt = createdAt,
initializer = initializer,
)
}
}
@@ -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<Array<String>>,
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<String> =
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
}
}
@@ -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<Array<String>>,
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 <T : com.vitorpamplona.quartz.nip01Core.core.Event> build(
content: String,
target: EventHintBundle<T>,
notify: List<PTag> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitStatusOpenEvent>.() -> Unit = {},
) = GitStatusBuilders.buildStatus<GitStatusOpenEvent, T>(
kind = KIND,
altDescriptor = ALT,
content = content,
target = target,
notify = notify,
createdAt = createdAt,
initializer = initializer,
)
}
}
@@ -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", "<commit1>", "<commit2>", ...]
*/
class AppliedAsCommitsTag {
companion object {
const val TAG_NAME = "applied-as-commits"
fun parse(tag: Array<String>): List<String>? {
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<String>): Array<String> = (listOf(TAG_NAME) + commits.filter { it.isNotEmpty() }).toTypedArray()
}
}
@@ -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>): 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)
}
}
@@ -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<UserTag> = emptyList(),
privateAuthors: List<UserTag> = emptyList(),
publicAuthors: List<GitAuthorTag> = emptyList(),
privateAuthors: List<GitAuthorTag> = 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 {
@@ -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", "<pubkey-hex>", "<optional-relay-url>", "<optional-petname>"]
*
* 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<String>): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
fun isTagged(
tag: Array<String>,
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<String>): 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<String>): 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() })
}
}
@@ -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)