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.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitPgpSigTag
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitTag
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.tags.Committer
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitterTag
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.tags.ParentCommitTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@@ -62,8 +74,6 @@ class GitPatchEvent(
|
||||
tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }
|
||||
?: tags.firstOrNull { it.size > 1 && it[0] == "a" }
|
||||
|
||||
private fun repositoryHex() = innerRepository()?.getOrNull(1)
|
||||
|
||||
fun repositoryAddress() =
|
||||
innerRepository()?.let {
|
||||
if (it.size > 1) {
|
||||
@@ -85,42 +95,109 @@ class GitPatchEvent(
|
||||
}
|
||||
}
|
||||
|
||||
fun commit() = tags.firstOrNull { it.size > 1 && it[0] == "commit" }?.get(1)
|
||||
fun commit() = tags.firstNotNullOfOrNull(CommitTag::parse)
|
||||
|
||||
fun parentCommit() = tags.firstOrNull { it.size > 1 && it[0] == "parent-commit" }?.get(1)
|
||||
fun parentCommit() = tags.firstNotNullOfOrNull(ParentCommitTag::parse)
|
||||
|
||||
fun commitPGPSig() = tags.firstOrNull { it.size > 1 && it[0] == "commit-pgp-sig" }?.get(1)
|
||||
fun commitPGPSig() = tags.firstNotNullOfOrNull(CommitPgpSigTag::parse)
|
||||
|
||||
fun committer() =
|
||||
tags.filter { it.size > 1 && it[0] == "committer" }.mapNotNull {
|
||||
Committer(it.getOrNull(1), it.getOrNull(2), it.getOrNull(3), it.getOrNull(4))
|
||||
}
|
||||
fun committer() = tags.mapNotNull(CommitterTag::parse)
|
||||
|
||||
data class Committer(
|
||||
val name: String?,
|
||||
val email: String?,
|
||||
val timestamp: String?,
|
||||
val timezoneInMinutes: String?,
|
||||
)
|
||||
/** Earliest unique commit of the target repository, encoded as `["r", <commit>]`. */
|
||||
fun earliestUniqueCommit(): String? = tags.firstOrNull { it.size > 1 && it[0] == "r" && it[1].isNotEmpty() }?.get(1)
|
||||
|
||||
/** `true` if this event is tagged `["t", "root"]` (root of a patch series). */
|
||||
fun isRoot(): Boolean = tags.any { HashtagTag.isTagged(it, ROOT) }
|
||||
|
||||
/** `true` if this event is tagged `["t", "root-revision"]` (root of a revision series). */
|
||||
fun isRootRevision(): Boolean = tags.any { HashtagTag.isTagged(it, ROOT_REVISION) }
|
||||
|
||||
companion object {
|
||||
const val KIND = 1617
|
||||
const val ALT = "A Git Patch"
|
||||
const val ROOT = "root"
|
||||
const val ROOT_REVISION = "root-revision"
|
||||
|
||||
suspend fun create(
|
||||
/**
|
||||
* Build a NIP-34 kind-1617 patch event with all required tags.
|
||||
*
|
||||
* @param patch the raw `git format-patch` output for the content field.
|
||||
* @param repository an [EventHintBundle] pointing at the target repository announcement.
|
||||
* @param earliestUniqueCommit the repository's earliest unique commit ID, used as the `r` tag.
|
||||
* @param commit optional current commit hash (`commit` tag).
|
||||
* @param parentCommit optional parent commit hash (`parent-commit` tag).
|
||||
* @param commitPgpSig optional PGP signature or empty string for unsigned (`commit-pgp-sig` tag).
|
||||
* @param committer optional committer metadata (`committer` tag).
|
||||
* @param notify additional recipient pubkeys, beyond the repo owner who is always included.
|
||||
* @param root set to `true` to tag the patch as the root of a new patch series (`["t", "root"]`).
|
||||
* @param rootRevision set to `true` to tag the patch as the root of a revision series (`["t", "root-revision"]`).
|
||||
*/
|
||||
fun build(
|
||||
patch: String,
|
||||
repository: EventHintBundle<GitRepositoryEvent>,
|
||||
earliestUniqueCommit: String,
|
||||
commit: String? = null,
|
||||
parentCommit: String? = null,
|
||||
commitPgpSig: String? = null,
|
||||
committer: Committer? = null,
|
||||
notify: List<PTag> = emptyList(),
|
||||
root: Boolean = false,
|
||||
rootRevision: Boolean = false,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
signer: NostrSigner,
|
||||
): GitPatchEvent {
|
||||
val content = patch
|
||||
val tags =
|
||||
mutableListOf(
|
||||
arrayOf<String>(),
|
||||
)
|
||||
initializer: TagArrayBuilder<GitPatchEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, patch, createdAt) {
|
||||
alt(ALT)
|
||||
repository(repository)
|
||||
euc(earliestUniqueCommit)
|
||||
pTag(repository.event.pubKey, repository.authorHomeRelay)
|
||||
if (notify.isNotEmpty()) pTags(notify)
|
||||
commit?.let { commit(it) }
|
||||
parentCommit?.let { parentCommit(it) }
|
||||
commitPgpSig?.let { commitPgpSig(it) }
|
||||
committer?.let {
|
||||
committer(it.name, it.email, it.timestamp, it.timezoneInMinutes)
|
||||
}
|
||||
if (root) hashtag(ROOT)
|
||||
if (rootRevision) hashtag(ROOT_REVISION)
|
||||
initializer()
|
||||
}
|
||||
|
||||
tags.add(AltTag.assemble(ALT))
|
||||
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
/**
|
||||
* Build a patch that replies to an earlier patch in the same series,
|
||||
* adding NIP-10 `e` reply tags that point at the previous patch.
|
||||
*/
|
||||
fun reply(
|
||||
patch: String,
|
||||
repository: EventHintBundle<GitRepositoryEvent>,
|
||||
earliestUniqueCommit: String,
|
||||
replyingTo: EventHintBundle<GitPatchEvent>,
|
||||
commit: String? = null,
|
||||
parentCommit: String? = null,
|
||||
commitPgpSig: String? = null,
|
||||
committer: Committer? = null,
|
||||
notify: List<PTag> = emptyList(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<GitPatchEvent>.() -> Unit = {},
|
||||
) = build(
|
||||
patch = patch,
|
||||
repository = repository,
|
||||
earliestUniqueCommit = earliestUniqueCommit,
|
||||
commit = commit,
|
||||
parentCommit = parentCommit,
|
||||
commitPgpSig = commitPgpSig,
|
||||
committer = committer,
|
||||
notify = notify,
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
add(
|
||||
MarkedETag.assemble(
|
||||
replyingTo.event.id,
|
||||
replyingTo.relay,
|
||||
MarkedETag.MARKER.REPLY,
|
||||
replyingTo.event.pubKey,
|
||||
),
|
||||
)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.EucTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.MaintainersTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.RelaysTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
@@ -48,13 +54,44 @@ class GitRepositoryEvent(
|
||||
|
||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||
|
||||
/** First web URL, for backwards compatibility. Prefer [webs]. */
|
||||
fun web() = tags.firstNotNullOfOrNull(WebTag::parse)
|
||||
|
||||
fun webs(): List<String> = tags.mapNotNull(WebTag::parse)
|
||||
|
||||
/** First clone URL, for backwards compatibility. Prefer [clones]. */
|
||||
fun clone() = tags.firstNotNullOfOrNull(CloneTag::parse)
|
||||
|
||||
fun clones(): List<String> = tags.mapNotNull(CloneTag::parse)
|
||||
|
||||
/**
|
||||
* Relays the repository author monitors for patches and issues. NIP-34
|
||||
* encodes this as a single multi-value tag, so the first matching
|
||||
* `relays` tag wins.
|
||||
*/
|
||||
fun relays(): List<String> = tags.firstNotNullOfOrNull(RelaysTag::parse) ?: emptyList()
|
||||
|
||||
/**
|
||||
* Accepted maintainer pubkeys. The event author is an implicit maintainer
|
||||
* and is not required to appear in this list.
|
||||
*/
|
||||
fun maintainers(): List<HexKey> = tags.firstNotNullOfOrNull(MaintainersTag::parse) ?: emptyList()
|
||||
|
||||
fun hashtags(): List<String> = tags.hashtags()
|
||||
|
||||
/** Earliest-unique-commit ID (NIP-34 `["r", <commit>, "euc"]`). */
|
||||
fun earliestUniqueCommit(): String? = tags.firstNotNullOfOrNull(EucTag::parse)
|
||||
|
||||
/**
|
||||
* `true` if the event carries `["t", "personal-fork"]`, signaling the
|
||||
* author does not actively seek patches/feedback for this repository.
|
||||
*/
|
||||
fun isPersonalFork(): Boolean = tags.any { HashtagTag.isTagged(it, PERSONAL_FORK) }
|
||||
|
||||
companion object {
|
||||
const val KIND = 30617
|
||||
const val ALT_DESCRIPTION = "Git Repository"
|
||||
const val PERSONAL_FORK = "personal-fork"
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
fun build(
|
||||
@@ -74,5 +111,34 @@ class GitRepositoryEvent(
|
||||
cloneUrl?.let { cloneUrl(it) }
|
||||
initializer()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
fun build(
|
||||
name: String,
|
||||
description: String?,
|
||||
webUrls: List<String>,
|
||||
cloneUrls: List<String>,
|
||||
relays: List<String>,
|
||||
maintainers: List<HexKey>,
|
||||
hashtags: List<String>,
|
||||
earliestUniqueCommit: String?,
|
||||
personalFork: Boolean = false,
|
||||
dTag: String = Uuid.random().toString(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<GitRepositoryEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTag)
|
||||
name(name)
|
||||
description?.let { description(it) }
|
||||
webUrls.forEach { webUrl(it) }
|
||||
cloneUrls.forEach { cloneUrl(it) }
|
||||
if (relays.isNotEmpty()) relays(relays)
|
||||
if (maintainers.isNotEmpty()) maintainers(maintainers)
|
||||
if (hashtags.isNotEmpty()) hashtags(hashtags)
|
||||
earliestUniqueCommit?.let { euc(it) }
|
||||
if (personalFork) hashtag(PERSONAL_FORK)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -20,16 +20,30 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip34Git.repository
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.EucTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.MaintainersTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.RelaysTag
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.description(description: String) = addUnique(DescriptionTag.assemble(description))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.webUrl(webUrl: String) = addUnique(WebTag.assemble(webUrl))
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.webUrl(webUrl: String) = add(WebTag.assemble(webUrl))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrl(cloneUrl: String) = addUnique(CloneTag.assemble(cloneUrl))
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.webUrls(webUrls: List<String>) = addAll(webUrls.map(WebTag::assemble))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrl(cloneUrl: String) = add(CloneTag.assemble(cloneUrl))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrls(cloneUrls: List<String>) = addAll(cloneUrls.map(CloneTag::assemble))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.relays(relays: List<String>) = addUnique(RelaysTag.assemble(relays))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.maintainers(maintainers: List<HexKey>) = addUnique(MaintainersTag.assemble(maintainers))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
|
||||
|
||||
+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.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.tags.GitAuthorTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@@ -45,13 +45,13 @@ class GitAuthorListEvent(
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PubKeyHintProvider {
|
||||
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
|
||||
override fun pubKeyHints() = tags.mapNotNull(GitAuthorTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
|
||||
override fun linkedPubKeys() = tags.mapNotNull(GitAuthorTag::parseKey)
|
||||
|
||||
fun publicAuthors() = tags.mapNotNull(UserTag::parse)
|
||||
fun publicAuthors() = tags.mapNotNull(GitAuthorTag::parse)
|
||||
|
||||
suspend fun privateAuthors(signer: NostrSigner) = privateTags(signer)?.mapNotNull(UserTag::parse)
|
||||
suspend fun privateAuthors(signer: NostrSigner) = privateTags(signer)?.mapNotNull(GitAuthorTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10017
|
||||
@@ -60,8 +60,8 @@ class GitAuthorListEvent(
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
|
||||
|
||||
suspend fun create(
|
||||
publicAuthors: List<UserTag> = emptyList(),
|
||||
privateAuthors: List<UserTag> = emptyList(),
|
||||
publicAuthors: List<GitAuthorTag> = emptyList(),
|
||||
privateAuthors: List<GitAuthorTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GitAuthorListEvent =
|
||||
@@ -74,7 +74,7 @@ class GitAuthorListEvent(
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: GitAuthorListEvent,
|
||||
author: UserTag,
|
||||
author: GitAuthorTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
@@ -98,7 +98,7 @@ class GitAuthorListEvent(
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: GitAuthorListEvent,
|
||||
author: UserTag,
|
||||
author: GitAuthorTag,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GitAuthorListEvent {
|
||||
|
||||
+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.selection.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.nip32Labeling.LabelEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.state.GitRepositoryStateEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
@@ -402,7 +410,15 @@ class EventFactory {
|
||||
GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitPullRequestEvent.KIND -> GitPullRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitPullRequestUpdateEvent.KIND -> GitPullRequestUpdateEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitRepositoryStateEvent.KIND -> GitRepositoryStateEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitStatusOpenEvent.KIND -> GitStatusOpenEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitStatusAppliedEvent.KIND -> GitStatusAppliedEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitStatusClosedEvent.KIND -> GitStatusClosedEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GitStatusDraftEvent.KIND -> GitStatusDraftEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
UserGraspListEvent.KIND -> UserGraspListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GoodWikiAuthorListEvent.KIND -> GoodWikiAuthorListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GoodWikiRelayListEvent.KIND -> GoodWikiRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
Reference in New Issue
Block a user