Skip to content

Github PR support#1555

Draft
gilescope wants to merge 24 commits into
git-bug:trunkfrom
gilescope:giles-prs
Draft

Github PR support#1555
gilescope wants to merge 24 commits into
git-bug:trunkfrom
gilescope:giles-prs

Conversation

@gilescope

@gilescope gilescope commented Apr 22, 2026

Copy link
Copy Markdown

Claude did this. Feel free to take it / parts / or pass.

Closes #373 (feat: support for pull requests).

PR support itself doesn't have any philosophical questions, but CI for a PR does. Linux doesn't have blocking CI gates — it has trusted approvers. I don't know what decentralised CI looks like though I suspect proof of reproducibility probably comes into it. Either way this is probably out of the scope of this PR — for now it has some links to centralised CI.

I thought this would be a good checkpoint as am going to pull in the project PR — will branch off here as I do that.

Running the webui across many repos

Two flags on git-bug webui let one process serve a whole directory of checked-out repos, so the UI becomes a single pane of glass across an org:

  • --repo <path> (repeatable) — register an extra repo. Its display name comes from the parent dir plus basename (so /a/b/myorg/myrepo shows up as myorg/myrepo).
  • --root <dir> — scan <root>/<org>/<repo>/.git siblings and register each one found. Only directories with git-bug refs are included; symlinks that escape <root> are rejected.

Multi-repo mode forces --read-only (per-repo identity selection isn't wired up yet).

Example layout I use locally:

~/git/work/
  my-github-org/
    repo1/      <- git-bug synced
    repo2/        <- git-bug synced
    ...
  dir-of-forked-stuff/
    foo/                <- git-bug synced
    bar/                <- git-bug synced

Serve the lot:

git-bug webui --root ~/git/work --read-only

Or register individual repos:

git-bug webui \
  --repo ~/git/work/my-github-org/repo1 \
  --repo ~/git/work/my-github-org/repo2 \
  --read-only

The list view shows every registered repo; a repo-scoped search can be done via the repo: / org: query filters, and the Sync button can either sync every registered repo or just the one you're looking at.

Adds common.Type (Issue | PR) with zero-value IssueType so existing
bugs deserialize without migration. Extends common.Status with
MergedStatus and DraftStatus for upcoming PR ops (merged / ready-to-
review transitions).

Signed-off-by: Giles Cope <gilescope@gmail.com>
Snapshot gains a Kind discriminator (Issue | PR), immutable BaseRef /
HeadRef set on Create, a mutable HeadCommit, a MergeCommit populated by
the merge transition, and a Reviews slice. Kind's zero value is
IssueType, so existing serialised bugs decode as issues without
migration; the wire key is "kind" to avoid colliding with OpBase.Type.

New ops gated on Kind == PRType:
  * UpdateHeadOperation — records that the PR's head branch advanced,
    capturing previous/new commit hashes for the timeline.
  * AddReviewOperation — a PR review verdict (approved /
    changes-requested / commented) against a specific head commit.
  * AddReviewCommentOperation — line-anchored review comment pinned to
    (commit, path, start..end line), with optional ReplyTo for threads.
    Anchors are immutable so comments don't rot when the branch moves.

SetStatusOperation now accepts MergedStatus (requires merge_commit) and
DraftStatus; CreateOperation accepts Draft=true to start a PR in draft.
Merge / MarkReady convenience functions wrap the new transitions.

Signed-off-by: Giles Cope <gilescope@gmail.com>
BugExcerpt carries Kind plus the PR-only fields (BaseRef, HeadRef,
HeadCommit, MergeCommit, LenReviews) so the cache can filter and sort
PRs without loading full entities. Gob-serialized old caches decode
correctly: missing fields become zero values, meaning Kind=IssueType
and empty refs — exactly what a pre-PR bug would look like.

Filter.KindFilter and Matcher.Kind wire the new query.Filters.Kind into
the existing OR-match pipeline. BugCache gains Merge / MarkReady /
MergeRaw, and RepoCacheBug gains NewPR / NewPRRaw so callers can
author pull-requests as first-class entities.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Filters gains a Kind []common.Type. The parser accepts either the
"kind" or "type" qualifier (kind:pr, kind:issue, type:pr), and the
existing status qualifier now recognises "merged" and "draft" via the
4-state common.Status. Parser tests cover both qualifiers plus an
unknown-kind rejection.

Signed-off-by: Giles Cope <gilescope@gmail.com>
gqlgen's autobind greedily mapped GraphQL's built-in __Type
introspection node to common.Type, breaking the generated resolvers.
Renaming the Go type to common.Kind (with IssueKind / PRKind values)
removes the collision. The GraphQL enum stays named BugKind and now
binds cleanly to common.Kind.

Also renames snapshot/excerpt field Type->Kind for consistency. The
wire JSON tag remains "kind", so on-disk data is unchanged.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Schema extensions:
  * Bug.kind discriminator (BugKind enum = ISSUE | PR)
  * Bug.baseRef / headRef / headCommit / mergeCommit (null for issues)
  * Bug.reviews + new Review and ReviewComment types
  * Status enum gains MERGED and DRAFT
  * BugCreateOperation exposes kind + base/head refs + draft
  * BugSetStatusOperation exposes mergeCommit
  * New operation types: BugUpdateHeadOperation, BugAddReviewOperation,
    BugAddReviewCommentOperation
  * Corresponding timeline items

BugWrapper interface grows Kind, BaseRef, HeadRef, HeadCommit,
MergeCommit, Reviews (lazy + loaded implementations). gqlgen.yml
binds BugKind -> common.Kind and ReviewState -> bug.ReviewState.
New resolvers stub Author/CreatedAt for Review + ReviewComment and
Author for each new operation / timeline item type.

Regenerated graph/*.generated.go via go generate ./...

Signed-off-by: Giles Cope <gilescope@gmail.com>
git bug new --pr --base REF --head REF [--head-commit HASH] [--draft]
    -t title -m body
Creates a pull-request. When --head-commit is omitted, git bug resolves
the tip of --head in the working repo.

git bug status merge BUG_ID --commit HASH
Marks a PR as merged and records the merge commit. Rejected for issues.

git bug status ready BUG_ID
Transitions a draft PR to open. Rejected for non-draft / non-PR.

Verified end-to-end: creating a PR, filtering kind:pr vs kind:issue,
merging a PR, and rejecting merge of an issue all behave as expected.

Signed-off-by: Giles Cope <gilescope@gmail.com>
The import now makes a second pass over the repo's pullRequests
connection after exhausting the issues one. Each PR becomes a bug
created via NewPRRaw, carrying baseRef / headRef / headCommit / draft
from the GitHub payload plus the usual origin metadata.

Terminal PR states are materialised at create time: if Merged, a
MergeOperation records the merge commit; if Closed-without-merge, a
SetStatus close op fires. Live timeline items are replayed too:
MergedEvent -> Merge, ReadyForReviewEvent -> Open (draft->open),
ConvertToDraftEvent -> ignored (no SetDraft op yet). Issue-shared
timeline items (IssueComment, LabeledEvent, RenamedTitleEvent, ...)
reuse the existing ensureTimelineItem handler via a shim.

Review threads are deliberately out of scope for v1; the data model
already supports them via AddReviewOperation /
AddReviewCommentOperation, so that's a later commit.

Adds a new mocked fixture TestGithubPRImport covering merged / draft /
closed-without-merge; the existing issue-only integration test gets an
empty-PR-query expectation so the second pass is a no-op there.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Flips the Pull-request row from X to ~ for Core / CLI / WebUI (WebUI
works because PRs ride the existing Bug list/detail; no PR-specific UI
chrome yet). Adds a dedicated "PR support (import only)" table with the
subset of features covered by the v1 GitHub import: create / comments /
merge state / draft state. Reviews and review comments remain X.

Also documents the v1 CLI surface (--pr on new, status merge, status
ready, kind:pr filter) and the GraphQL additions. Fixes pre-existing
heading-level skip (H2 -> H4 -> H3) caught by markdownlint while at it.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Extends the PR import to replay PullRequestReview timeline items as
AddReviewOperation, plus the first N (NumReviewComments=50) inline
PullRequestReviewComments as AddReviewCommentOperation anchored to
(commit hash, path, line).

GitHub review-state mapping:
  APPROVED           -> bug.ReviewApproved
  CHANGES_REQUESTED  -> bug.ReviewChangesRequested
  COMMENTED/PENDING/ -> bug.ReviewCommented
  DISMISSED

Outdated review comments (GitHub reports line=0 when the anchor no
longer exists in the current diff) are skipped silently. Reply-to
parent lookups fall back to a top-level comment when the parent isn't
present in the import.

BugCache gains AddReview / AddReviewRaw / AddReviewRawWithId and
AddReviewComment / AddReviewCommentRaw. core.ImportResult gains
ImportEventReview and ImportEventReviewComment with their constructors.

A new integration test TestGithubPRReviewImport covers the full chain:
approved review with a single-line review comment, both imported as
PR-only operations on a bug with Kind=PR.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Flips reviews to done and review comments to partial (capped at 50
per review in v1).

Signed-off-by: Giles Cope <gilescope@gmail.com>
When a PR review carries more than NumReviewComments inline comments,
the inline batch reports PageInfo.HasNextPage=true. The importer now
follows the cursor via a new prReviewCommentsQuery (node(id:review) on
PullRequestReview, selecting comments(first,after)) and keeps importing
until HasNextPage is false.

New mediator method importMediator.QueryReviewComments(ctx, reviewId,
cursor) returns a (nodes, nextCursor, hasNext) triple; ensureReview
loops until exhausted.

Integration test TestGithubPRReviewCommentsPagination covers the two-
page case: one inline comment (HasNextPage=true) + one paged comment,
both end up on the review with body matching.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Signed-off-by: Giles Cope <gilescope@gmail.com>
BugRow shows kind-aware status icons: issues keep the open/closed
circle pair; PRs get a merge-type glyph for open, a purple call-merge
for merged, and an edit pencil for draft.

Bug detail gains:
  * PrInfo panel (shown only for kind=PR) summarising base <- head
    refs, current head commit, and the merge commit when merged.
  * Reviews section rendering each PR review with state badge
    (APPROVED=green, CHANGES_REQUESTED=red, COMMENTED=grey), commit
    anchor, body, and any line-anchored review comments with file /
    line metadata.

SetStatus timeline item recognises the MERGED and DRAFT verbs so
merge / draft transitions render with a sensible past-tense phrase.

Fragments extended: BugRow adds kind; Bug adds kind + baseRef +
headRef + headCommit + mergeCommit + reviews (with nested comments).
TypeScript typecheck green via npx tsc --noEmit.

Signed-off-by: Giles Cope <gilescope@gmail.com>
v1 PR export is update-only: creating a pull-request from git-bug is
skipped with a NewExportNothing because it requires pushing the head
branch to GitHub first (out of scope for the bridge). An attempt to
export-create a PR surfaces a clear message pointing the user to open
it on GitHub and re-import.

For PRs that are already imported (metadata[github-id] present),
existing update paths now route to PR-specific GraphQL mutations:
  * EditCreateComment (body edit) -> updatePullRequest.body
  * SetTitle                      -> updatePullRequest.title
  * SetStatus OPEN/CLOSED         -> updatePullRequest.state
  * SetStatus MERGED              -> skipped (merging must happen on
                                    GitHub; status then re-imports)
  * SetStatus DRAFT               -> skipped in v1
  * UpdateHead / AddReview /
    AddReviewComment              -> skipped in v1 (no direct mapping
                                    for the head-commit tracking op,
                                    and write paths for review threads
                                    are a later commit)
  * LabelChange / AddComment      -> unchanged; GitHub's labelable and
                                    addComment mutations accept PR
                                    node ids, so the existing issue
                                    path covers them.

Adds updatePullRequestMutation + updateGithubPullRequestStatus / Body /
Title helpers using githubv4.UpdatePullRequestInput.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Removes the v1 "cannot create a new pull-request via export" bail-out.
When exportBug finds a git-bug entity with Kind=PR that has no
github-id metadata yet, it now issues a createPullRequest mutation
with RepositoryID + BaseRefName + HeadRefName + Title + Body + Draft
from the CreateOperation.

GitHub requires the head branch to already exist on its side — git-bug
does not push branches. If the branch is missing on the remote, the
createPullRequest call returns a clear GitHub error that surfaces via
NewExportError; the user pushes the branch and re-runs `git bug push`.
The base branch (usually main/master) is assumed to exist.

Adds createPullRequestMutation and createGithubPullRequest helper.
The helper strips the `refs/heads/` prefix from base/head refs since
GitHub's createPullRequest expects short branch names.

CLI --pr flag help now calls out the branch-must-be-pushed requirement.

Signed-off-by: Giles Cope <gilescope@gmail.com>
CLI additions under `git bug bug review`:
  * review                 - list reviews + anchored comments on a PR
  * review add BUG_ID      - record a verdict (--approve / --request-changes /
                             --comment) with optional body and commit anchor
  * review comment REV_ID  - attach a line-anchored comment to a review, with
                             --path / --line / --end-line / --reply-to;
                             replying to a parent inherits the anchor.

Cache helpers (BugCache.AddReview / AddReviewComment) already existed for
the import path; this commit wires the CLI through them.

Export on the GitHub bridge:
  * AddReviewOperation        -> addPullRequestReview mutation; maps
                                 bug.ReviewState to PullRequestReviewEvent
                                 (APPROVED/CHANGES_REQUESTED/COMMENTED).
  * AddReviewCommentOperation -> for replies (ReplyTo set) only, via
                                 addPullRequestReviewComment(inReplyTo:);
                                 standalone new threads are skipped with a
                                 clear message and should be created as
                                 inline comments on the review itself — a
                                 look-ahead bundle is a follow-up.
  * UpdateHeadOperation       -> not exported (the user's `git push` is the
                                 source of truth for branch tips).

New core ExportEvent types ExportEventReview / ExportEventReviewComment and
NewExportReview / NewExportReviewComment constructors.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Signed-off-by: Giles Cope <gilescope@gmail.com>
… config

man pages are refreshed by go generate; they are not markdown-lint
targets so they commit cleanly. The .markdownlint-cli2.jsonc file is a
local config that exempts doc/md/ from MD010 / MD040 / MD012 for tools
that auto-discover it (the global pre-commit hook uses an explicit
--config path and so does not pick it up).

Signed-off-by: Giles Cope <gilescope@gmail.com>
Bug GraphQL type gains originUrl: String — the external-tracker URL
stored on the CreateOp's metadata by the bridge (github-url / gitlab-url
/ jira-url / launchpad-url). The BugWrapper lazy + loaded implementations
probe those keys in order.

WebUI list: each row now shows the bridge's numeric id (git-bug#1459) parsed
from the URL path, rendered as a link that opens the external tracker
in a new tab, next to the git-bug humanId. Click doesn't navigate to
the local bug page (stopPropagation on the external link).

WebUI detail: the bug title area shows the same #NNN link next to the
title, letting reviewers jump to the upstream page with one click.

Regenerated gqlgen code + webui .generated.tsx for the new field.

Signed-off-by: Giles Cope <gilescope@gmail.com>
…gration

git-bug webui gains --repo PATH (repeatable) and --root DIR (scan for
<org>/<repo> siblings). Each extra repo is registered in the existing
MultiRepoCache with name "<org>/<repo>". The cwd repo still serves as
the default, and is de-duplicated from the scan so lockfile contention
is avoided.

Request routing: a new X-Repo-Name HTTP header is parsed by a mux
middleware into the request context; the Repository resolver falls
back to that when no ref is supplied, letting every existing GraphQL
query (which writes `repository { ... }`) scope automatically. This
avoids touching ~12 .graphql files and their generated siblings.

WebUI:
  * Apollo setContext sets X-Repo-Name from a mutable module-level var
    that RepoContextBinder updates from the URL (/r/:repoName/*).
  * New /r/:repoName/* route mounts the usual list/detail/new pages
    with the header bound. Slashes in repo names are URL-encoded.
  * Single-repo routes stay intact. Root "/" adapts: multi-repo mode
    shows a ReposLanding page listing registered repos with counts;
    single-repo mode shows the bug list as before.
  * Header grows a "Repositories" link and a RepoPicker dropdown
    (hidden in single-repo mode).
  * Multi-repo forces --read-only in v1 — auth middleware uses a
    single identity and authoring across N repos needs per-repo
    identity selection, deferred.

Bleve resilience: openBleveIndex now detects stale-but-incompatible
indexes (bleve.Open fails and index_meta.json is present) and wipes the
path before bleve.New. This covers the migration case where the
existing on-disk store is from an older bleve version (e.g. moltdb or
upsidedown) and the new version treats the "store" path as a directory
only. The rebuild is automatic on first access; git refs (source of
truth) are untouched.

Verified against 149 midnightntwrk+shieldedtech repos: all register
cleanly after the new tolerant open path; X-Repo-Name routing returns
the expected bugs for a probed repo (example-bboard → 417 PRs).

Signed-off-by: Giles Cope <gilescope@gmail.com>
Adds a /search route that aggregates matches across every registered
repository using the existing repositories { nodes { allBugs(query) } }
shape. gqlgen resolves allBugs concurrently per repo (each owns its own
bleve index), so 150 repos return in roughly one search's wall time.

Results are grouped by repo with hit counts, empty repos hidden, and
each row links straight to /r/<repo>/bug/<id>. A "view all" link per
repo opens the full repo list filtered by the same query.

Query DSL gains two repo-level selectors:
  * repo:<org>/<name>  — restrict to that repo
  * org:<org>          — restrict to all repos under that org
These are parsed server-side into query.Filters.Repo / .Org (no-op
inside a single-repo query since the scope is already pinned). The
search page extracts them client-side to filter which repositories
render — matching happens before the fan-out view is built so it also
trims render cost for big result sets.

UI additions:
  * Header GlobalSearch — a TextField in the app bar, always visible.
    Enter navigates to /search?q=<value>. When mounted under /r/:name/
    it pre-fills `repo:<org>/<name> status:open` so users see the
    current scope and can remove it for a broader search.
  * New SearchPage + SearchQuery fragment generating SearchQuery.gen.

Scopes + tokens are merged with the DSL: repo:/org: pull out of the
query before it's passed server-side, so the remaining portion (e.g.
"status:open label:bug authN") runs unchanged as the existing query.

Signed-off-by: Giles Cope <gilescope@gmail.com>
Backend adds a new /sync endpoint wired into the webui router:
  GET  /sync  — current status (running, done/total, current repo,
                cumulative imported bugs + identities, per-repo errors)
  POST /sync  — start a background run; 409 if one is already in flight

The handler walks MultiRepoCache.AllRepos() in sorted order, loads each
repo's DefaultBridge (repos without a configured bridge are skipped
silently, not errored), and drains its ImportAll channel. Bugs and
identities are counted; per-repo errors are collected into a map that
the UI surfaces in the button's tooltip.

UI adds a Sync button in the header that posts to /sync, then polls
GET /sync every 2s while running. The label switches between "Sync"
and "<done>/<total>", the spinner confirms activity, and on completion
the Apollo cache is reset so newly-imported rows show up without a
hard page refresh. Errors surface in the tooltip as "<N> errors".

Single-repo mode works too (MultiRepoCache has one registered repo).

Signed-off-by: Giles Cope <gilescope@gmail.com>
Signed-off-by: Giles Cope <gilescope@gmail.com>
@MichaelMure

Copy link
Copy Markdown
Contributor

While that could be interesting for if/when we implement PR support, imho it really should be a separate entity (that is, not retrofitted into bug). Unfortunately, that invalidates most of your PR.

Before any token are spent on implementation, we really need to come to an agreement on:

  • what the data model would be (the most critical point)
  • to some extent, what the workflow would be
  • how the different UIs would work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: support for pull requests

2 participants