mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): derive default clone URL for relay-hosted repos (#2166)
Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1hwqy0rnujtl25dzmlhn8qwux4kr8sjhas3ugltx9j5dm5dwkp2dsqjhytw <bb80478e7c92feaa345bfde6703b86ad86784afd84788facc5951bba35d60a9b@pending-seed.communities.buzz.xyz>
This commit is contained in:
co-authored by
npub1hwqy0rnujtl25dzmlhn8qwux4kr8sjhas3ugltx9j5dm5dwkp2dsqjhytw
parent
8024ce72ef
commit
21dbd32645
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as React from "react";
|
||||
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
import { getCachedRelayOrigin } from "@/shared/lib/mediaUrl";
|
||||
import { signRelayEvent } from "@/shared/api/tauri";
|
||||
import { getIdentity } from "@/shared/api/tauriIdentity";
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ import type {
|
||||
RelayEvent,
|
||||
} from "@/shared/api/types";
|
||||
import { summarizeProjectActivityEvents } from "./projectActivity.mjs";
|
||||
import { effectiveCloneUrls } from "./lib/projectCloneUrl";
|
||||
import type { ProjectIssue } from "./projectIssues.mjs";
|
||||
import { projectIssueEventsToIssues } from "./projectIssues.mjs";
|
||||
import type {
|
||||
@@ -172,11 +174,27 @@ function isDeletedByA(project: Project, deletionEvents: RelayEvent[]): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function eventToProject(event: RelayEvent): Project {
|
||||
/**
|
||||
* Converts a kind:30617 repo announcement into a `Project`.
|
||||
*
|
||||
* `relayOrigin` is the resolved relay HTTP origin (from `getCachedRelayOrigin`)
|
||||
* used to synthesize a canonical clone URL when the announcement omits an
|
||||
* explicit `clone` tag. Callers outside the relay-connected app (e.g. unit
|
||||
* tests) may omit it, in which case no default is derived.
|
||||
*/
|
||||
export function eventToProject(
|
||||
event: RelayEvent,
|
||||
relayOrigin?: string | null,
|
||||
): Project {
|
||||
const d = getTag(event, "d") ?? event.id;
|
||||
const name = getTag(event, "name") || d;
|
||||
const description = getTag(event, "description") || event.content || "";
|
||||
const cloneUrls = getCloneUrls(event);
|
||||
const cloneUrls = effectiveCloneUrls(
|
||||
getCloneUrls(event),
|
||||
relayOrigin,
|
||||
event.pubkey,
|
||||
d,
|
||||
);
|
||||
const webUrl = getTag(event, "web") ?? null;
|
||||
const setupUsers = getAllTags(event, "auth");
|
||||
const contributors = [...new Set([...getAllTags(event, "p"), ...setupUsers])];
|
||||
@@ -235,7 +253,7 @@ export async function fetchProjects(): Promise<Project[]> {
|
||||
]);
|
||||
|
||||
return dedup(events)
|
||||
.map(eventToProject)
|
||||
.map((event) => eventToProject(event, getCachedRelayOrigin()))
|
||||
.filter(
|
||||
(project) =>
|
||||
!isHiddenLocally(project) && !isDeletedByA(project, deletionEvents),
|
||||
@@ -273,7 +291,10 @@ async function fetchProject(projectId: string): Promise<Project | null> {
|
||||
const deduped = dedup(events).filter(
|
||||
(event) => !owner || event.pubkey.toLowerCase() === owner,
|
||||
);
|
||||
const project = deduped.length > 0 ? eventToProject(deduped[0]) : null;
|
||||
const project =
|
||||
deduped.length > 0
|
||||
? eventToProject(deduped[0], getCachedRelayOrigin())
|
||||
: null;
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { deriveRelayCloneUrl, effectiveCloneUrls } from "./projectCloneUrl.ts";
|
||||
|
||||
const OWNER = "a".repeat(64);
|
||||
const ORIGIN = "https://relay.example";
|
||||
|
||||
test("deriveRelayCloneUrl builds the canonical relay-hosted path", () => {
|
||||
assert.equal(
|
||||
deriveRelayCloneUrl(ORIGIN, OWNER, "flappy-bee"),
|
||||
`${ORIGIN}/git/${OWNER}/flappy-bee`,
|
||||
);
|
||||
});
|
||||
|
||||
test("deriveRelayCloneUrl lowercases the owner pubkey", () => {
|
||||
const upper = "A".repeat(64);
|
||||
assert.equal(
|
||||
deriveRelayCloneUrl(ORIGIN, upper, "repo"),
|
||||
`${ORIGIN}/git/${OWNER}/repo`,
|
||||
);
|
||||
});
|
||||
|
||||
test("deriveRelayCloneUrl tolerates a trailing slash on the origin", () => {
|
||||
assert.equal(
|
||||
deriveRelayCloneUrl(`${ORIGIN}/`, OWNER, "repo"),
|
||||
`${ORIGIN}/git/${OWNER}/repo`,
|
||||
);
|
||||
});
|
||||
|
||||
test("deriveRelayCloneUrl fails closed on an unresolved origin", () => {
|
||||
assert.equal(deriveRelayCloneUrl(null, OWNER, "repo"), null);
|
||||
assert.equal(deriveRelayCloneUrl(undefined, OWNER, "repo"), null);
|
||||
assert.equal(deriveRelayCloneUrl("", OWNER, "repo"), null);
|
||||
});
|
||||
|
||||
test("deriveRelayCloneUrl declines a non-hex or wrong-length owner", () => {
|
||||
assert.equal(deriveRelayCloneUrl(ORIGIN, "short", "repo"), null);
|
||||
assert.equal(deriveRelayCloneUrl(ORIGIN, "z".repeat(64), "repo"), null);
|
||||
});
|
||||
|
||||
test("deriveRelayCloneUrl declines a missing repo id", () => {
|
||||
assert.equal(deriveRelayCloneUrl(ORIGIN, OWNER, ""), null);
|
||||
});
|
||||
|
||||
test("effectiveCloneUrls honors explicit clone URLs over the derived default", () => {
|
||||
const explicit = ["https://github.com/octocat/hello"];
|
||||
assert.deepEqual(
|
||||
effectiveCloneUrls(explicit, ORIGIN, OWNER, "repo"),
|
||||
explicit,
|
||||
);
|
||||
});
|
||||
|
||||
test("effectiveCloneUrls derives a default when none is advertised", () => {
|
||||
assert.deepEqual(effectiveCloneUrls([], ORIGIN, OWNER, "flappy-bee"), [
|
||||
`${ORIGIN}/git/${OWNER}/flappy-bee`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("effectiveCloneUrls returns empty when no default can be derived", () => {
|
||||
assert.deepEqual(effectiveCloneUrls([], null, OWNER, "repo"), []);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Deriving a default clone URL for a NIP-34 repo announcement that omits an
|
||||
* explicit `clone` tag.
|
||||
*
|
||||
* Buzz relays serve their own git repositories at a canonical path —
|
||||
* `<relay-origin>/git/<owner-pubkey>/<repo-id>` — which is exactly the shape the
|
||||
* Rust `validate_clone_url` gate enforces. When an announcement carries no
|
||||
* `clone` tag (e.g. it was created via `buzz repos create` without `--clone`),
|
||||
* the desktop would otherwise have no URL to fetch from, so the project detail
|
||||
* view comes up empty. Synthesizing the canonical relay-hosted URL lets those
|
||||
* repositories load while still deferring to any explicit clone URLs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Builds the canonical relay-hosted clone URL for a repository, or `null` when
|
||||
* the inputs cannot produce a valid URL (unresolved relay origin, missing owner
|
||||
* pubkey, or missing repo id). Fails closed rather than emitting a broken URL.
|
||||
*
|
||||
* `relayOrigin` is expected to be a bare origin (scheme + host, e.g.
|
||||
* `https://relay.example`); a trailing slash is tolerated.
|
||||
*/
|
||||
export function deriveRelayCloneUrl(
|
||||
relayOrigin: string | null | undefined,
|
||||
owner: string,
|
||||
dtag: string,
|
||||
): string | null {
|
||||
if (!relayOrigin || !owner || !dtag) return null;
|
||||
// The Rust validator requires a 64-char hex owner pubkey; anything else is
|
||||
// not a relay-hosted repo we can address, so decline rather than guess.
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(owner)) return null;
|
||||
const origin = relayOrigin.replace(/\/+$/, "");
|
||||
return `${origin}/git/${owner.toLowerCase()}/${dtag}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective clone URLs for a project: the explicitly advertised
|
||||
* ones when present, otherwise a single-element list holding the derived
|
||||
* relay-hosted default (or an empty list when no default can be derived).
|
||||
*
|
||||
* Explicit `clone` tags always win — NIP-34 permits pointing `clone` at an
|
||||
* external host (e.g. GitHub), which must not be overridden.
|
||||
*/
|
||||
export function effectiveCloneUrls(
|
||||
cloneUrls: string[],
|
||||
relayOrigin: string | null | undefined,
|
||||
owner: string,
|
||||
dtag: string,
|
||||
): string[] {
|
||||
if (cloneUrls.length > 0) return cloneUrls;
|
||||
const derived = deriveRelayCloneUrl(relayOrigin, owner, dtag);
|
||||
return derived ? [derived] : [];
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
projectsQueryKey,
|
||||
} from "@/features/projects/hooks";
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
import { getCachedRelayOrigin } from "@/shared/lib/mediaUrl";
|
||||
import { signRelayEvent } from "@/shared/api/tauri";
|
||||
import { getIdentity } from "@/shared/api/tauriIdentity";
|
||||
import { KIND_REPO_ANNOUNCEMENT } from "@/shared/constants/kinds";
|
||||
@@ -77,7 +78,7 @@ async function createProject(input: CreateProjectInput): Promise<Project> {
|
||||
"Failed to create project.",
|
||||
);
|
||||
|
||||
return eventToProject(event);
|
||||
return eventToProject(event, getCachedRelayOrigin());
|
||||
}
|
||||
|
||||
/** Mutation that creates a project and inserts it into the projects cache. */
|
||||
|
||||
Reference in New Issue
Block a user