mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 07:48:27 +00:00
fix(quartz): NIP-34 wire-format interop with ngit (clone/web, issue p, plain r)
Verified Amethyst's NIP-34 events byte-for-byte against the ngit reference implementation (DanConwayDev/ngit-cli) and the spec, and fixed three real interop divergences in quartz — so ngit/gitworkshop and Amethyst read each other's git repos, issues, patches, and PRs without losing data. - Repository announcement `clone`/`web` were emitted as REPEATED single-value tags (`["clone", a]`, `["clone", b]`). The spec and ngit use ONE multi-value tag (`["clone", a, b]`), and ngit's parser keeps only the LAST of repeated known tags — so multi-URL repos silently lost every URL but one in both directions. Now emitted as a single multi-value tag; `clones()`/`webs()` read BOTH the spec form and the legacy repeated form, so old events still parse. (`relays`/`maintainers` were already correct multi-value tags.) - Issues (kind 1621) were missing the `["p", <repo-owner>]` tag that patches and PRs already include — a maintainer watching `#p` wouldn't see them. The builder now adds it (fixes both the CLI and the Android issue-creation path, which both passed an empty notify list). - Patch / PR / PR-update `r` tags carried the `"euc"` marker (`["r", commit, "euc"]`). Per the spec and ngit that marker belongs only on the kind-30617 announcement; other `r` tags are plain `["r", commit]`. A `#r` filter matches either shape, so this is a spec-compliance/byte-parity fix. `alt` (NIP-31) tags are intentionally still omitted — quartz treats the generic alt client-hint as deprecated, and ngit/gitworkshop parse the structured tags, so it isn't required for interop. Adds `GitNip34InteropTest` (5 cases: multi-value write, tolerant read of both forms, issue p-tag, plain patch r-tag) and 4 wire-format assertions to the CLI git harness (37 offline). No regressions in the nip34 or Search suites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
This commit is contained in:
@@ -109,7 +109,7 @@ vs streaming `subscribe`). Stateless verbs run with no account or network.
|
||||
| `nip` | `amy nip` | ✅ | repo-first lookup + Nostr fallback (NipText kind:30817, wiki:30818, long-form:30023); `nip list`. |
|
||||
| `kind` | `amy kind` | ✅ | quartz `KindNames` registry (kind → English label + NIP) covering **every** event kind quartz defines (280 entries); number lookup + name search. |
|
||||
| `sync` | `amy sync` | ✅ | NIP-77 Negentropy reconcile with the local store (down/up/both). |
|
||||
| `git` | `amy git` | ✅ (events + read) | NIP-34: `init` bootstraps a repo from the local `git` checkout (announce + state, like `ngit init`); repo announce (30617) + state (30618), patches (1617), pull requests (1618/1619), issues (1621), NIP-22 comments (1111), NIP-32 labels (1985), status open/applied/closed/draft (1630-1633), GRASP server list (10317); `issues`/`patches`/`prs`/`thread` reads derive status; `apply` applies a fetched patch to the local tree (`git am`); `browse`/`cat`/`log` read git objects over smart-HTTP v2 (quartz `GitHttpClient`, the same shallow-clone path the Android browser uses). Only git-packfile **push** (writing objects to clone/GRASP servers) and NIP-34 cover notes (1624, no quartz builder yet) are out of scope. |
|
||||
| `git` | `amy git` | ✅ (events + read) | NIP-34: `init` bootstraps a repo from the local `git` checkout (announce + state, like `ngit init`); repo announce (30617) + state (30618), patches (1617), pull requests (1618/1619), issues (1621), NIP-22 comments (1111), NIP-32 labels (1985), status open/applied/closed/draft (1630-1633), GRASP server list (10317); `issues`/`patches`/`prs`/`thread` reads derive status; `apply` applies a fetched patch to the local tree (`git am`); `browse`/`cat`/`log` read git objects over smart-HTTP v2 (quartz `GitHttpClient`, the same shallow-clone path the Android browser uses). Only git-packfile **push** (writing objects to clone/GRASP servers) and NIP-34 cover notes (1624, no quartz builder yet) are out of scope. Event tag shapes were verified byte-for-byte against the ngit reference implementation and the NIP-34 spec (`clone`/`web` as single multi-value tags, issue `p`-tag for maintainer routing, plain patch/PR `r` tags); the quartz readers stay tolerant of the legacy repeated form. See `quartz/…/nip34Git/GitNip34InteropTest`. |
|
||||
| `podcast` | `amy podcast` | ✅ | NIP-F4 show metadata (10154) + episode publish (54) + list. |
|
||||
| `bunker` | `amy bunker[ connect]` + `amy login bunker://`/`--nostrconnect` | ✅ | NIP-46 remote signer + login, both the `bunker://` and `nostrconnect://` flows, each direction, plus `auth_url` challenge handling (client surfaces the URL + keeps waiting). Interop-verified vs real `nak`. |
|
||||
| `admin` | `amy admin RELAY METHOD` | ✅ | NIP-86 Relay Management over NIP-98 HTTP auth — full method set (ban/allow pubkey + event, kinds, IP block, change name/desc/icon, list-*). Reuses quartz `Nip86Client` + shared `commons` `Nip86Retriever`. Interop-verified against `amy serve`. |
|
||||
|
||||
@@ -145,6 +145,21 @@ info "state: $STATE"
|
||||
assert_eq "$(echo "$STATE" | jq -r '.branches')" "2" state.branches "two branch refs"
|
||||
assert_eq "$(echo "$STATE" | jq -r '.head')" "main" state.head "HEAD=main"
|
||||
|
||||
# --- ngit interop: wire-format checks --------------------------------------
|
||||
banner "ngit interop wire format"
|
||||
# Announce a repo with TWO clone URLs; NIP-34/ngit want ONE multi-value tag.
|
||||
IADDR="$(M git announce --name interop --clone https://a.git,https://b.git --web https://a.com,https://b.com --earliest-commit abc123 --relay "$RELAY_URL" | jq -r '.address')"
|
||||
IEID="$(M git show "$IADDR" --relay "$RELAY_URL" | jq -r '.event_id')"
|
||||
ITAGS="$(M fetch --id "$IEID" --relay "$RELAY_URL")"
|
||||
assert_eq "$(echo "$ITAGS" | jq -r '[.events[0].tags[] | select(.[0]=="clone")] | length')" "1" interop.clone_single "clone is one tag (not repeated)"
|
||||
assert_eq "$(echo "$ITAGS" | jq -r '.events[0].tags[] | select(.[0]=="clone") | length')" "3" interop.clone_multivalue "clone tag carries both URLs (name + 2 values)"
|
||||
# The tolerant reader must round-trip both URLs back.
|
||||
assert_eq "$(M git show "$IADDR" --relay "$RELAY_URL" | jq -r '.clone | length')" "2" interop.clone_read "reader returns both clone URLs"
|
||||
# Issue must p-tag the repository owner (maintainer routing).
|
||||
IISS="$(M git issue "$IADDR" --subject "interop" "b" --relay "$RELAY_URL" | jq -r '.event_id')"
|
||||
IOWNER="$(echo "$IADDR" | cut -d: -f2)"
|
||||
assert_eq "$(M fetch --id "$IISS" --relay "$RELAY_URL" | jq -r "[.events[0].tags[] | select(.[0]==\"p\" and .[1]==\"$IOWNER\")] | length")" "1" interop.issue_ptag "issue carries the repo owner p tag"
|
||||
|
||||
# GRASP server list (10317) round-trip.
|
||||
GRASP="$(M git grasp set "wss://grasp.example.com,wss://grasp2.example.com" --relay "$RELAY_URL")"
|
||||
assert_eq "$(echo "$GRASP" | jq -r '.kind')" "10317" grasp.kind "grasp list is kind 10317"
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
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.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.SubjectTag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
||||
@@ -130,6 +131,9 @@ class GitIssueEvent(
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
subject(subject)
|
||||
repository(repository)
|
||||
// NIP-34 issues carry the repository owner as a `p` tag so maintainers
|
||||
// watching `#p` are notified; the same tag patches/PRs already include.
|
||||
pTag(repository.event.pubKey, repository.authorHomeRelay)
|
||||
notify(notify)
|
||||
hashtags(topics)
|
||||
|
||||
|
||||
@@ -38,12 +38,12 @@ fun TagArrayBuilder<GitPatchEvent>.repository(rep: EventHintBundle<GitRepository
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* target repository. NIP-34 uses the plain `["r", <commit>]` shape for patches
|
||||
* — the `"euc"` marker appears only on the kind-30617 repository announcement
|
||||
* (confirmed against the ngit reference implementation). A `#r` filter still
|
||||
* matches either shape (both key off the value at index 1).
|
||||
*/
|
||||
fun TagArrayBuilder<GitPatchEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
|
||||
fun TagArrayBuilder<GitPatchEvent>.euc(commit: String) = addUnique(arrayOf(EucTag.TAG_NAME, commit))
|
||||
|
||||
fun TagArrayBuilder<GitPatchEvent>.commit(commit: String) = addUnique(CommitTag.assemble(commit))
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ fun TagArrayBuilder<GitPullRequestEvent>.repository(rep: ATag) = addUnique(rep.t
|
||||
|
||||
fun TagArrayBuilder<GitPullRequestEvent>.repository(rep: EventHintBundle<GitRepositoryEvent>) = addUnique(rep.toATag().toATagArray())
|
||||
|
||||
fun TagArrayBuilder<GitPullRequestEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
|
||||
// NIP-34 pull requests use the plain `["r", <commit>]` shape; the `"euc"` marker
|
||||
// is only on the kind-30617 announcement (see the patch builder for the rationale).
|
||||
fun TagArrayBuilder<GitPullRequestEvent>.euc(commit: String) = addUnique(arrayOf(EucTag.TAG_NAME, commit))
|
||||
|
||||
fun TagArrayBuilder<GitPullRequestEvent>.currentCommit(commit: String) = addUnique(CurrentCommitTag.assemble(commit))
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ fun TagArrayBuilder<GitPullRequestUpdateEvent>.repository(rep: ATag) = addUnique
|
||||
|
||||
fun TagArrayBuilder<GitPullRequestUpdateEvent>.repository(rep: EventHintBundle<GitRepositoryEvent>) = addUnique(rep.toATag().toATagArray())
|
||||
|
||||
fun TagArrayBuilder<GitPullRequestUpdateEvent>.euc(commit: String) = addUnique(EucTag.assemble(commit))
|
||||
// Plain `["r", <commit>]` — the `"euc"` marker is only on the 30617 announcement.
|
||||
fun TagArrayBuilder<GitPullRequestUpdateEvent>.euc(commit: String) = addUnique(arrayOf(EucTag.TAG_NAME, commit))
|
||||
|
||||
fun TagArrayBuilder<GitPullRequestUpdateEvent>.currentCommit(commit: String) = addUnique(CurrentCommitTag.assemble(commit))
|
||||
|
||||
|
||||
@@ -58,14 +58,21 @@ class GitRepositoryEvent(
|
||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||
|
||||
/** First web URL, for backwards compatibility. Prefer [webs]. */
|
||||
fun web() = tags.firstNotNullOfOrNull(WebTag::parse)
|
||||
fun web() = webs().firstOrNull()
|
||||
|
||||
fun webs(): List<String> = tags.mapNotNull(WebTag::parse)
|
||||
/**
|
||||
* All browse URLs. Tolerant of both the NIP-34 spec form (one multi-value
|
||||
* `["web", url1, url2]` tag, which ngit emits) and the legacy repeated
|
||||
* `["web", url1]` / `["web", url2]` form, so a repo announced by any client
|
||||
* round-trips without dropping URLs.
|
||||
*/
|
||||
fun webs(): List<String> = tags.flatMap(WebTag::parseAll)
|
||||
|
||||
/** First clone URL, for backwards compatibility. Prefer [clones]. */
|
||||
fun clone() = tags.firstNotNullOfOrNull(CloneTag::parse)
|
||||
fun clone() = clones().firstOrNull()
|
||||
|
||||
fun clones(): List<String> = tags.mapNotNull(CloneTag::parse)
|
||||
/** All clone URLs — tolerant of both the multi-value and repeated forms (see [webs]). */
|
||||
fun clones(): List<String> = tags.flatMap(CloneTag::parseAll)
|
||||
|
||||
/**
|
||||
* Relays the repository author monitors for patches and issues. NIP-34
|
||||
@@ -131,8 +138,8 @@ class GitRepositoryEvent(
|
||||
dTag(dTag)
|
||||
name(name)
|
||||
description?.let { description(it) }
|
||||
webUrls.forEach { webUrl(it) }
|
||||
cloneUrls.forEach { cloneUrl(it) }
|
||||
if (webUrls.isNotEmpty()) webUrls(webUrls)
|
||||
if (cloneUrls.isNotEmpty()) cloneUrls(cloneUrls)
|
||||
if (relays.isNotEmpty()) relays(relays)
|
||||
if (maintainers.isNotEmpty()) maintainers(maintainers)
|
||||
if (hashtags.isNotEmpty()) hashtags(hashtags)
|
||||
|
||||
@@ -36,11 +36,13 @@ fun TagArrayBuilder<GitRepositoryEvent>.description(description: String) = addUn
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.webUrl(webUrl: String) = add(WebTag.assemble(webUrl))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.webUrls(webUrls: List<String>) = addAll(webUrls.map(WebTag::assemble))
|
||||
/** Emit all browse URLs as one NIP-34 multi-value `["web", url1, url2, …]` tag (the spec/ngit form). */
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.webUrls(webUrls: List<String>) = addUnique(WebTag.assemble(webUrls))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrl(cloneUrl: String) = add(CloneTag.assemble(cloneUrl))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrls(cloneUrls: List<String>) = addAll(cloneUrls.map(CloneTag::assemble))
|
||||
/** Emit all clone URLs as one NIP-34 multi-value `["clone", url1, url2, …]` tag (the spec/ngit form). */
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.cloneUrls(cloneUrls: List<String>) = addUnique(CloneTag.assemble(cloneUrls))
|
||||
|
||||
fun TagArrayBuilder<GitRepositoryEvent>.relays(relays: List<String>) = addUnique(RelaysTag.assemble(relays))
|
||||
|
||||
|
||||
@@ -23,6 +23,13 @@ package com.vitorpamplona.quartz.nip34Git.repository.tags
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* NIP-34 repository `clone` tag. The spec encodes clone URLs as a single
|
||||
* multi-value tag — `["clone", "<url1>", "<url2>", ...]` — so [assemble] emits
|
||||
* that form and [parseAll] reads every value. [parse] (first value only) and
|
||||
* the legacy repeated `["clone", "<url>"]` form are still read for backward
|
||||
* compatibility (see `GitRepositoryEvent.clones`).
|
||||
*/
|
||||
class CloneTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "clone"
|
||||
@@ -34,6 +41,16 @@ class CloneTag {
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
/** Every non-empty URL carried by a single `clone` tag. */
|
||||
fun parseAll(tag: Array<String>): List<String> {
|
||||
ensure(tag.has(1)) { return emptyList() }
|
||||
ensure(tag[0] == TAG_NAME) { return emptyList() }
|
||||
return tag.drop(1).filter { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun assemble(name: String) = arrayOf(TAG_NAME, name)
|
||||
|
||||
/** The spec form: one tag carrying all clone URLs. */
|
||||
fun assemble(urls: List<String>): Array<String> = (listOf(TAG_NAME) + urls.filter { it.isNotEmpty() }).toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ package com.vitorpamplona.quartz.nip34Git.repository.tags
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* NIP-34 repository `web` tag. Like `clone`, the spec encodes browse URLs as a
|
||||
* single multi-value tag — `["web", "<url1>", "<url2>", ...]` — so [assemble]
|
||||
* emits that form and [parseAll] reads every value. [parse] (first value only)
|
||||
* and the legacy repeated form are still read for backward compatibility.
|
||||
*/
|
||||
class WebTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "web"
|
||||
@@ -34,6 +40,16 @@ class WebTag {
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
/** Every non-empty URL carried by a single `web` tag. */
|
||||
fun parseAll(tag: Array<String>): List<String> {
|
||||
ensure(tag.has(1)) { return emptyList() }
|
||||
ensure(tag[0] == TAG_NAME) { return emptyList() }
|
||||
return tag.drop(1).filter { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun assemble(name: String) = arrayOf(TAG_NAME, name)
|
||||
|
||||
/** The spec form: one tag carrying all browse URLs. */
|
||||
fun assemble(urls: List<String>): Array<String> = (listOf(TAG_NAME) + urls.filter { it.isNotEmpty() }).toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Locks in byte-level interoperability with ngit (github.com/DanConwayDev/ngit-cli)
|
||||
* and the NIP-34 spec for the tag shapes that previously diverged:
|
||||
*
|
||||
* 1. `clone` / `web` are ONE multi-value tag, not repeated single-value tags —
|
||||
* ngit's parser keeps only the last of repeated known tags, so the repeated
|
||||
* form silently dropped URLs across implementations.
|
||||
* 2. Issues carry the repository owner as a `p` tag (maintainer routing).
|
||||
* 3. Patch/PR `r` tags are plain `["r", <commit>]`; the `"euc"` marker lives
|
||||
* only on the kind-30617 announcement.
|
||||
*/
|
||||
class GitNip34InteropTest {
|
||||
private val owner = "aa".repeat(32)
|
||||
|
||||
private fun repo(tags: Array<Array<String>>) = GitRepositoryEvent("00", owner, 0, tags, "", "00")
|
||||
|
||||
@Test
|
||||
fun announcementEmitsSingleMultiValueCloneAndWeb() {
|
||||
val tmpl =
|
||||
GitRepositoryEvent.build(
|
||||
name = "demo",
|
||||
description = null,
|
||||
webUrls = listOf("https://a.com", "https://b.com"),
|
||||
cloneUrls = listOf("https://a.git", "https://b.git"),
|
||||
relays = emptyList(),
|
||||
maintainers = emptyList(),
|
||||
hashtags = emptyList(),
|
||||
earliestUniqueCommit = null,
|
||||
dTag = "demo",
|
||||
)
|
||||
assertEquals(1, tmpl.tags.count { it[0] == "clone" }, "clone must be a single tag")
|
||||
assertEquals(1, tmpl.tags.count { it[0] == "web" }, "web must be a single tag")
|
||||
assertEquals(listOf("clone", "https://a.git", "https://b.git"), tmpl.tags.first { it[0] == "clone" }.toList())
|
||||
assertEquals(listOf("web", "https://a.com", "https://b.com"), tmpl.tags.first { it[0] == "web" }.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readsSpecMultiValueForm() {
|
||||
val r =
|
||||
repo(
|
||||
arrayOf(
|
||||
arrayOf("d", "x"),
|
||||
arrayOf("clone", "https://a.git", "https://b.git"),
|
||||
arrayOf("web", "https://a.com", "https://b.com"),
|
||||
),
|
||||
)
|
||||
assertEquals(listOf("https://a.git", "https://b.git"), r.clones())
|
||||
assertEquals(listOf("https://a.com", "https://b.com"), r.webs())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readsLegacyRepeatedForm() {
|
||||
val r =
|
||||
repo(
|
||||
arrayOf(
|
||||
arrayOf("d", "x"),
|
||||
arrayOf("clone", "https://a.git"),
|
||||
arrayOf("clone", "https://b.git"),
|
||||
arrayOf("web", "https://a.com"),
|
||||
arrayOf("web", "https://b.com"),
|
||||
),
|
||||
)
|
||||
assertEquals(listOf("https://a.git", "https://b.git"), r.clones())
|
||||
assertEquals(listOf("https://a.com", "https://b.com"), r.webs())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueCarriesRepositoryOwnerPTag() {
|
||||
val repoEvent = repo(arrayOf(arrayOf("d", "x"), arrayOf("name", "x")))
|
||||
val tmpl = GitIssueEvent.build("subject", "body", EventHintBundle(repoEvent), emptyList(), emptyList())
|
||||
val pTags = tmpl.tags.filter { it[0] == "p" }.map { it[1] }
|
||||
assertTrue(owner in pTags, "issue must p-tag the repository owner for maintainer routing")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun patchRTagIsPlainWithoutEucMarker() {
|
||||
val repoEvent = repo(arrayOf(arrayOf("d", "x"), arrayOf("r", "rootcommit", "euc")))
|
||||
val tmpl =
|
||||
GitPatchEvent.build(
|
||||
patch = "diff",
|
||||
repository = EventHintBundle(repoEvent),
|
||||
earliestUniqueCommit = "rootcommit",
|
||||
commit = "c1",
|
||||
)
|
||||
assertEquals(listOf("r", "rootcommit"), tmpl.tags.first { it[0] == "r" }.toList(), "patch r tag must be plain (no euc marker)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user