11 KiB
Plan
Two work streams:
- Fork support (display + navigation UI) — show when a repository is a fork and let the user jump to the upstream repository.
- Pull request improvement — bring PR creation/updating in line with the other NIP-34 clients (nak, ngit).
1. Fork support
Background: what NIP-34 says about forks
NIP-34 has no fork event kind — a fork is an ordinary kind-30617 announcement by another author (or the same author under a different d). Fork-ness is expressed by two tags:
utag on the fork's announcement:["u", "30617:<upstream-pubkey>:<upstream-id>|<git-url>", "<relay-hint>", "<upstream-author-pubkey>"]. Includingumeans the author does not assert maintainership of the primary project (the fork is a subordinate of the upstream).- EUC (
rtag witheucmarker): shared between the fork and its upstream (and other mirrors), so clients can group them. For a permanent fork, the EUC is the first commit after the fork point.
Current state
Announcement::from_eventparses theutag into an opaque string (crates/signed_core/src/model.rs:212; only the first value is kept, manually, because the SDK'sNip34Taghas noUpstreamvariant).effective_maintainersexcludes the fork author (model.rs:250) — already correct per NIP-34.- The About dialog shows the raw upstream string as a plain row (
crates/workspace/src/views/repo_detail/about.rs:68). - Nothing shows fork-ness in the repo list or the repo detail header, and there is no way to navigate to the upstream.
Goal
- Repo list card (
crates/workspace/src/views/repo_list.rs::render_card): show a "Forked from " badge instead of/in addition to the description, with a fork icon. - Repo detail header (
crates/workspace/src/views/repo_detail/mod.rs::render_header): show a "Forked from " text button near the repo name. - Clicking the upstream opens the upstream repository as a center panel (same as clicking any repo card).
Design
1.1 Structured Upstream model (signed_core)
Add a structured type and keep the manual parse:
pub struct Upstream {
/// `30617:<pubkey>:<id>` (navigable) or a git URL (not navigable).
pub target: UpstreamTarget,
pub relay_hint: Option<RelayUrl>,
pub author: Option<PublicKey>,
}
pub enum UpstreamTarget {
/// Parseable via the SDK `Coordinate` (`30617:<pubkey-hex>:<id>`).
Repo(RepoAddr),
/// Git https URL form: no NIP-34 announcement, not navigable.
GitUrl(Url),
}
- Change
Announcement.upstream: Option<String>toOption<Upstream>; parse all threeuvalues (the SDK'sNip34Tag::parseis not usable here — keep the manualtag.kind() == "u"branch and extend it). RepoAddris the SDKCoordinate(crates/signed_core/src/addr.rs), soCoordinate::from_strgives the upstream address directly; validate it is kind30617.- Update
about.rs(rendersupstream),effective_maintainers, and themodel.rstests (parses_upstream_tag,effective_maintainers_exclude_owner_for_subordinate_forks).
1.2 Resolving the upstream announcement
Opening a panel needs an Announcement (RepoDetailView::new), so resolve the upstream announcement before (or while) opening:
- Lookup, no fetch: the global
RepoListStoreholds every announcement in the local database (crates/signed_state/src/repo_list.rs:51). Look up the upstreamRepoAddrthere — covers the common case (upstream already browsed/known) with zero network. - Miss → fetch, then open: add a
Backendmethod (e.g.fetch_announcement(addr) -> Task<Result<Announcement>>) doing a one-shot query withfilters::announcement(addr)(crates/signed_core/src/filters.rs:21), mirroring the bootstrap fetch inRepoStore::subscribe_remote(crates/signed_state/src/repo.rs:196). Show the upstream as a disabled/loading row until it resolves; on failure fall back to showing the raw address. - Git-URL upstreams: not navigable — render as plain text with a copy action (like
copy_rowinsigned_ui), no panel.
1.3 Shared "open repo panel" helper
The open-panel sequence is currently duplicated three times:
crates/workspace/src/views/repo_list.rs:170(RepoListView::open_repo)crates/workspace/src/views/sidebar/mod.rs:155(SidebarPanel::open_repo)crates/workspace/src/views/sidebar/create_repo_dialog.rs:259(open_repo)
Extract one helper (e.g. open_repo_panel(dock_area, announcement, window, cx) in the workspace views layer) and reuse it from all three plus the new fork button, so the fork navigation behaves exactly like clicking a repo card.
1.4 Repo list badge
In render_card, when announcement.upstream is set:
- Resolve the upstream's display name via the
RepoListStorelookup (1.2); fall back to the raw address string. - Render a small "Forked from " line (fork icon +
text_xsmuted), replacing or joining the description line. Add agit-fork.svgasset tocrates/assets/assets/icons/+ aCustomIconName::GitForkvariant (lucide'sgit-fork), or reusegit-branch.svgif an asset addition is undesirable.
1.5 Repo detail header
In render_header (crates/workspace/src/views/repo_detail/mod.rs:1231), next to the repo name:
- "Forked from " as a text button (
gpui_base::Buttonor styleddiv), which calls the shared open-panel helper with the resolved upstream announcement. - Keep the About dialog row in sync: make it the same clickable control (or at least the same resolved display name).
- Handle "upstream not in store yet": spawn the
fetch_announcementtask; button shows a subtle loading state; on success open the panel (needswindow/cx— the task is spawned on the view,apply_announcement-style flow).
1.6 (Stretch) Fork grouping by EUC
RepoListStore already has euc per announcement; add a "N forks" count on the detail header by scanning announcements sharing the same EUC, with a filter or navigation into the explore list. Not required for the first iteration.
Checklist
signed_core:Upstream/UpstreamTargettypes + fullu-tag parse;Announcement.upstreamtype change; tests.Backend::fetch_announcement(addr)one-shot fetch.- Shared
open_repo_panelhelper; switch the three existing call sites. - Repo list card fork badge (+
git-fork.svgasset if used). - Repo detail header "Forked from" button + About row sync.
- Manual test: fork with coordinate upstream (navigates), fork with git-URL upstream (copy only), upstream announcement absent (fetch-then-open).
2. Pull request improvement
Why
Current PR creation (RepoStore::open_pull_request, crates/signed_state/src/repo.rs:626; dialog crates/workspace/src/views/repo_detail/pull_requests.rs:288) requires pasting git format-patch output, publishes no merge-base/branch-name, cannot update an existing PR, and advertises clone URLs the author usually cannot push to. Compared with nak (pr send/pr update/pr merge) and ngit (push-based PRs with merge-base inference), the gaps are:
| Area | Today | Fix (phase) |
|---|---|---|
| Patch generation | manual paste | generate from a local checkout (P2) |
merge-base tag |
always None |
compute vs state HEAD (P1) |
branch-name tag |
never | send local branch name (P1) |
| PR updates (kind 1619) | not producible | author-only update flow (P1) |
| Update reader trusts any author | latest_update has no author filter |
filter by PR author (P1) |
clone URL truthfulness |
repo mirrors (author can't push) | push tip to grasp first (P3) |
| Multi-commit series | one oversized event | NIP-10 chain / size-aware (P3) |
| Pre-publish validation | none | git am --check dry run (P2) |
| Draft on create | no | optional 1633 status (P1) |
| Merge provenance | plain 1631 | merge-commit/applied-as-commits (P4) |
Phase 1 — Correctness & interop (small, surgical)
- Compute and publish
merge-base,branch-name,rEUC inopen_pull_request:- Target tip =
RepoStore.headref from the state announcement (refs/head,crates/signed_state/src/repo.rs:28-30); addsigned_git::merge_base(repo, a, b)(shell out likeapply_patch). - Fill the
GitPullRequestbuilder's existingmerge_base/branch_namefields (currently hardcodedNone,repo.rs:691-702); pass the branch name and tip through from the dialog. - Add the
rEUC tag manually to the PR event (the SDK builder omits it; NIP-34 recommends it for subscription efficiency).
- Target tip =
- Add
RepoStore::update_pull_request(root, new_tip, …)producing a kind-1619 event via the SDKGitPullRequestUpdatebuilder (E/P/KNIP-22 tags) plus a chained root-revision patch (t root-revision,ereply to the original root patch). Author-only, mirroring nak'spr update. Wire a button intopull_request_detail.rs. - Fix
latest_update(crates/workspace/src/views/repo_detail/pull_request_detail.rs:1125): filter by the root PR's author (nak and ngit both restrict tip updates to the PR author). - Draft toggle in the new-PR dialog: publish a 1633 status right after the PR event (reuse
set_status).
Phase 2 — UX: replace the paste
- Local-repo picker replaces the paste textarea (keep it as an advanced fallback): user picks a git checkout (or the app's
GitCachemirror), source branch and target branch. The app then:- resolves the tip (
git rev-parse), - computes
merge-basevs the target tip, - runs
format-patch base..tip --stdoutitself (addsigned_git::format_patch_between, like nak), - dry-runs
git am --3way --checkagainst the cached clone before publishing (signed_git::apply_patchinfra,crates/signed_git/src/lib.rs:176), surfacing "does not apply" before anything hits the relays.
- resolves the tip (
Phase 3 — Truthful clone URLs (interop)
- Push before publishing: add
signed_git::push_commit_ref(path, url, commit, ref)and reuse the grasp-push infrastructure (grasp_base_url,push_to_grasp_servers,crates/signed_state/src/backend.rs:1472,1500) to push the tip torefs/nostr/<event-id>on the announced grasp servers (nak'sgitPushCommitToGraspRefs). On success theclonetag carries the real URL; on failure fall back to the current patch-event model with a warning. - Size-aware publishing: split multi-commit mboxes into a NIP-10-chained 1617 series (each < 60 KB per NIP-34) or go PR-only above that size; adopt ngit's patch→PR upgrade (new PR + close-status for the original patch).
- Optional: GRASP-06
/prs/<npub>/<id>.git+ kind-10317 user grasp-list fallback (ngit's server-selection cascade). Fork support (section 1) makes the fork's own grasp server a natural push target here.
Phase 4 — Merge provenance
- In
merge_pull_request(crates/signed_state/src/repo.rs:817), publish the 1631 status withmerge-commit(orapplied-as-commits) andqtags so nak/ngit/GitWorkshop show merge provenance correctly.
Checklist
- P1: merge-base + branch-name +
rEUC on creation. - P1:
update_pull_request(1619) + UI button; author check. - P1:
latest_updateauthor filter. - P1: draft toggle on create.
- P2: local checkout picker + generated patch + pre-publish apply check.
- P3: push tip to grasp, truthful
clonetags, size-aware series. - P4: merge status tags.