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:
+82
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
-28
@@ -24,14 +24,26 @@ import androidx.compose.runtime.Immutable
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.AddressHintProvider
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
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.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.pTag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
|
||||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
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
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -62,8 +74,6 @@ class GitPatchEvent(
|
|||||||
tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }
|
tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }
|
||||||
?: tags.firstOrNull { it.size > 1 && it[0] == "a" }
|
?: tags.firstOrNull { it.size > 1 && it[0] == "a" }
|
||||||
|
|
||||||
private fun repositoryHex() = innerRepository()?.getOrNull(1)
|
|
||||||
|
|
||||||
fun repositoryAddress() =
|
fun repositoryAddress() =
|
||||||
innerRepository()?.let {
|
innerRepository()?.let {
|
||||||
if (it.size > 1) {
|
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() =
|
fun committer() = tags.mapNotNull(CommitterTag::parse)
|
||||||
tags.filter { it.size > 1 && it[0] == "committer" }.mapNotNull {
|
|
||||||
Committer(it.getOrNull(1), it.getOrNull(2), it.getOrNull(3), it.getOrNull(4))
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Committer(
|
/** Earliest unique commit of the target repository, encoded as `["r", <commit>]`. */
|
||||||
val name: String?,
|
fun earliestUniqueCommit(): String? = tags.firstOrNull { it.size > 1 && it[0] == "r" && it[1].isNotEmpty() }?.get(1)
|
||||||
val email: String?,
|
|
||||||
val timestamp: String?,
|
/** `true` if this event is tagged `["t", "root"]` (root of a patch series). */
|
||||||
val timezoneInMinutes: String?,
|
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 {
|
companion object {
|
||||||
const val KIND = 1617
|
const val KIND = 1617
|
||||||
const val ALT = "A Git Patch"
|
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,
|
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(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
signer: NostrSigner,
|
initializer: TagArrayBuilder<GitPatchEvent>.() -> Unit = {},
|
||||||
): GitPatchEvent {
|
) = eventTemplate(KIND, patch, createdAt) {
|
||||||
val content = patch
|
alt(ALT)
|
||||||
val tags =
|
repository(repository)
|
||||||
mutableListOf(
|
euc(earliestUniqueCommit)
|
||||||
arrayOf<String>(),
|
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))
|
/**
|
||||||
|
* Build a patch that replies to an earlier patch in the same series,
|
||||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
* 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
@@ -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)
|
||||||
+42
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -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 ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+149
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -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())
|
||||||
+64
@@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
+40
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
@@ -26,10 +26,16 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
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.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag
|
import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag
|
||||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag
|
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.NameTag
|
||||||
|
import com.vitorpamplona.quartz.nip34Git.repository.tags.RelaysTag
|
||||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag
|
import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
@@ -48,13 +54,44 @@ class GitRepositoryEvent(
|
|||||||
|
|
||||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||||
|
|
||||||
|
/** First web URL, for backwards compatibility. Prefer [webs]. */
|
||||||
fun web() = tags.firstNotNullOfOrNull(WebTag::parse)
|
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 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 {
|
companion object {
|
||||||
const val KIND = 30617
|
const val KIND = 30617
|
||||||
const val ALT_DESCRIPTION = "Git Repository"
|
const val ALT_DESCRIPTION = "Git Repository"
|
||||||
|
const val PERSONAL_FORK = "personal-fork"
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
fun build(
|
fun build(
|
||||||
@@ -74,5 +111,34 @@ class GitRepositoryEvent(
|
|||||||
cloneUrl?.let { cloneUrl(it) }
|
cloneUrl?.let { cloneUrl(it) }
|
||||||
initializer()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-2
@@ -20,16 +20,30 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip34Git.repository
|
package com.vitorpamplona.quartz.nip34Git.repository
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag
|
import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag
|
||||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag
|
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.NameTag
|
||||||
|
import com.vitorpamplona.quartz.nip34Git.repository.tags.RelaysTag
|
||||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag
|
import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag
|
||||||
|
|
||||||
fun TagArrayBuilder<GitRepositoryEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
fun TagArrayBuilder<GitRepositoryEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
||||||
|
|
||||||
fun TagArrayBuilder<GitRepositoryEvent>.description(description: String) = addUnique(DescriptionTag.assemble(description))
|
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))
|
||||||
|
|||||||
+48
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-9
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
|||||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
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.nip51Lists.remove
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@@ -45,13 +45,13 @@ class GitAuthorListEvent(
|
|||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||||
PubKeyHintProvider {
|
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 {
|
companion object {
|
||||||
const val KIND = 10017
|
const val KIND = 10017
|
||||||
@@ -60,8 +60,8 @@ class GitAuthorListEvent(
|
|||||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
|
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
|
||||||
|
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
publicAuthors: List<UserTag> = emptyList(),
|
publicAuthors: List<GitAuthorTag> = emptyList(),
|
||||||
privateAuthors: List<UserTag> = emptyList(),
|
privateAuthors: List<GitAuthorTag> = emptyList(),
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
): GitAuthorListEvent =
|
): GitAuthorListEvent =
|
||||||
@@ -74,7 +74,7 @@ class GitAuthorListEvent(
|
|||||||
|
|
||||||
suspend fun add(
|
suspend fun add(
|
||||||
earlierVersion: GitAuthorListEvent,
|
earlierVersion: GitAuthorListEvent,
|
||||||
author: UserTag,
|
author: GitAuthorTag,
|
||||||
isPrivate: Boolean,
|
isPrivate: Boolean,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
@@ -98,7 +98,7 @@ class GitAuthorListEvent(
|
|||||||
|
|
||||||
suspend fun remove(
|
suspend fun remove(
|
||||||
earlierVersion: GitAuthorListEvent,
|
earlierVersion: GitAuthorListEvent,
|
||||||
author: UserTag,
|
author: GitAuthorTag,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
): GitAuthorListEvent {
|
): GitAuthorListEvent {
|
||||||
|
|||||||
+107
@@ -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.pack.EmojiPackEvent
|
||||||
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
||||||
import com.vitorpamplona.quartz.nip32Labeling.LabelEvent
|
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.issue.GitIssueEvent
|
||||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
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.reply.GitReplyEvent
|
||||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
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.TorrentCommentEvent
|
||||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
@@ -402,7 +410,15 @@ class EventFactory {
|
|||||||
GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig)
|
GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig)
|
GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
GitPatchEvent.KIND -> GitPatchEvent(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)
|
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)
|
GoodWikiAuthorListEvent.KIND -> GoodWikiAuthorListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
GoodWikiRelayListEvent.KIND -> GoodWikiRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
GoodWikiRelayListEvent.KIND -> GoodWikiRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
|
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
Reference in New Issue
Block a user