Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
80867f6
entities/common: add Type enum; promote Status to 4-state
gilescope Apr 17, 2026
15b063e
entities/bug: extend Bug with PR shape and three PR-only ops
gilescope Apr 17, 2026
c4ac2a6
cache: excerpt and matcher learn about Kind and PR fields
gilescope Apr 17, 2026
ef00943
query: parse kind: / type: filters and the new status values
gilescope Apr 17, 2026
849c150
common,bug,cache,query,api: rename common.Type to common.Kind
gilescope Apr 17, 2026
6c16f88
api/graphql: expose PR fields, ops, reviews and the new status values
gilescope Apr 17, 2026
05b9a90
commands/bug: --pr on new; status merge and status ready subcommands
gilescope Apr 17, 2026
8536551
bridge/github: import pull-requests as Kind=PR bugs
gilescope Apr 17, 2026
8a6129f
doc: mark PR support as partial in the feature matrix
gilescope Apr 17, 2026
973d7c0
bridge/github,cache,core: import PR reviews and review comments
gilescope Apr 17, 2026
692b720
doc: reflect review / review comment import in feature matrix
gilescope Apr 17, 2026
a3f2aad
bridge/github: paginate review comments past the inline batch
gilescope Apr 17, 2026
dfb8f1a
doc: review comments are now fully imported
gilescope Apr 17, 2026
ecfe5a8
webui: PR-specific chrome in bug list and detail pages
gilescope Apr 17, 2026
f79aa89
bridge/github: export updates to already-imported pull-requests
gilescope Apr 17, 2026
b44eaf1
bridge/github: export-create for pull-requests
gilescope Apr 17, 2026
8641b30
commands,cache,bridge: author PR reviews locally and export them
gilescope Apr 17, 2026
0126ede
doc: review authoring via CLI and export now supported
gilescope Apr 17, 2026
5bdbdfa
doc: regenerate man pages for new PR commands; add local markdownlint…
gilescope Apr 17, 2026
3b574e7
api,webui: expose originUrl on Bug; show upstream #NNN linked to GitHub
gilescope Apr 17, 2026
61beed3
commands,api,webui: multi-repo webui via --repo and --root + bleve mi…
gilescope Apr 17, 2026
b135498
webui,query: cross-repo search via fan-out + repo:/org: query filters
gilescope Apr 18, 2026
06d216b
webui,api/http: add Sync button that re-pulls every registered repo
gilescope Apr 18, 2026
c929552
feat: PR support ( + dir of repo's checkouts support)
gilescope Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
bridge/github: export updates to already-imported pull-requests
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>
  • Loading branch information
gilescope committed Apr 17, 2026
commit f79aa89f179b94130ee4b5c26678a34b8e330547
102 changes: 96 additions & 6 deletions bridge/github/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,14 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
bugGithubURL = githubURL

} else {
// For PRs we cannot create a GitHub pull-request from git-bug alone —
// GitHub requires an existing head branch on its side, which means
// pushing the branch is a prerequisite outside the bridge's scope.
if snapshot.Kind == common.PRKind {
out <- core.NewExportNothing(b.Id(), "cannot create a new pull-request via export; push the branch and open the PR on GitHub first, then re-import")
return
}

// check that we have a token for operation author
client, err := ge.getClientForIdentity(author.Id())
if err != nil {
Expand Down Expand Up @@ -327,8 +335,14 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
if op.Target == createOp.Id() {

// case bug creation operation: we need to edit the Github issue
if err := ge.updateGithubIssueBody(ctx, client, bugGithubID, op.Message); err != nil {
err := errors.Wrap(err, "editing issue")
var editErr error
if snapshot.Kind == common.PRKind {
editErr = ge.updateGithubPullRequestBody(ctx, client, bugGithubID, op.Message)
} else {
editErr = ge.updateGithubIssueBody(ctx, client, bugGithubID, op.Message)
}
if editErr != nil {
err := errors.Wrap(editErr, "editing issue")
out <- core.NewExportError(err, b.Id())
return
}
Expand Down Expand Up @@ -361,8 +375,32 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
}

case *bug.SetStatusOperation:
if err := ge.updateGithubIssueStatus(ctx, client, bugGithubID, op.Status); err != nil {
err := errors.Wrap(err, "editing status")
// MergedStatus cannot be materialised via API: marking a PR merged
// on GitHub requires an actual merge of the branch, which is out
// of scope for the bridge. Skip with a warning.
if op.Status == common.MergedStatus {
out <- core.NewExportNothing(b.Id(), "cannot export Merge via API; merge the PR on GitHub directly")
id = bugGithubID
url = bugGithubURL
break
}
// DraftStatus has no direct mapping on a plain issue; for PRs we
// use updatePullRequest with convertPullRequestToDraft via a
// separate mutation. v1: skip with warning.
if op.Status == common.DraftStatus {
out <- core.NewExportNothing(b.Id(), "cannot export DraftStatus transition via API in v1")
id = bugGithubID
url = bugGithubURL
break
}
var statusErr error
if snapshot.Kind == common.PRKind {
statusErr = ge.updateGithubPullRequestStatus(ctx, client, bugGithubID, op.Status)
} else {
statusErr = ge.updateGithubIssueStatus(ctx, client, bugGithubID, op.Status)
}
if statusErr != nil {
err := errors.Wrap(statusErr, "editing status")
out <- core.NewExportError(err, b.Id())
return
}
Expand All @@ -373,8 +411,14 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
url = bugGithubURL

case *bug.SetTitleOperation:
if err := ge.updateGithubIssueTitle(ctx, client, bugGithubID, op.Title); err != nil {
err := errors.Wrap(err, "editing title")
var titleErr error
if snapshot.Kind == common.PRKind {
titleErr = ge.updateGithubPullRequestTitle(ctx, client, bugGithubID, op.Title)
} else {
titleErr = ge.updateGithubIssueTitle(ctx, client, bugGithubID, op.Title)
}
if titleErr != nil {
err := errors.Wrap(titleErr, "editing title")
out <- core.NewExportError(err, b.Id())
return
}
Expand All @@ -384,6 +428,13 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
id = bugGithubID
url = bugGithubURL

case *bug.UpdateHeadOperation, *bug.AddReviewOperation, *bug.AddReviewCommentOperation:
// PR-only ops that don't have a direct REST/GraphQL write path in
// v1. Mark as skipped so we don't block subsequent exports.
out <- core.NewExportNothing(b.Id(), fmt.Sprintf("skipping %T in v1 export", op))
id = bugGithubID
url = bugGithubURL

case *bug.LabelChangeOperation:
if err := ge.updateGithubIssueLabels(ctx, client, bugGithubID, op.Added, op.Removed); err != nil {
err := errors.Wrap(err, "updating labels")
Expand Down Expand Up @@ -743,6 +794,45 @@ func (ge *githubExporter) updateGithubIssueTitle(ctx context.Context, gc *rateLi
return nil
}

// updateGithubPullRequestStatus mirrors updateGithubIssueStatus but targets
// PullRequest. Callers must guard MergedStatus / DraftStatus upstream; only
// OpenStatus / ClosedStatus are supported here.
func (ge *githubExporter) updateGithubPullRequestStatus(ctx context.Context, gc *rateLimitHandlerClient, id string, status common.Status) error {
m := &updatePullRequestMutation{}
var state githubv4.PullRequestUpdateState
switch status {
case common.OpenStatus:
state = githubv4.PullRequestUpdateStateOpen
case common.ClosedStatus:
state = githubv4.PullRequestUpdateStateClosed
default:
return fmt.Errorf("cannot export pull-request status %s via updatePullRequest", status)
}
input := githubv4.UpdatePullRequestInput{
PullRequestID: id,
State: &state,
}
return gc.mutate(ctx, m, input, nil, ge.out)
}

func (ge *githubExporter) updateGithubPullRequestBody(ctx context.Context, gc *rateLimitHandlerClient, id, body string) error {
m := &updatePullRequestMutation{}
input := githubv4.UpdatePullRequestInput{
PullRequestID: id,
Body: (*githubv4.String)(&body),
}
return gc.mutate(ctx, m, input, nil, ge.out)
}

func (ge *githubExporter) updateGithubPullRequestTitle(ctx context.Context, gc *rateLimitHandlerClient, id, title string) error {
m := &updatePullRequestMutation{}
input := githubv4.UpdatePullRequestInput{
PullRequestID: id,
Title: (*githubv4.String)(&title),
}
return gc.mutate(ctx, m, input, nil, ge.out)
}

// update github issue labels
func (ge *githubExporter) updateGithubIssueLabels(ctx context.Context, gc *rateLimitHandlerClient, labelableID string, added, removed []common.Label) error {

Expand Down
9 changes: 9 additions & 0 deletions bridge/github/export_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ type updateIssueMutation struct {
} `graphql:"updateIssue(input:$input)"`
}

type updatePullRequestMutation struct {
UpdatePullRequest struct {
PullRequest struct {
ID string `graphql:"id"`
URL string `graphql:"url"`
}
} `graphql:"updatePullRequest(input:$input)"`
}

type addCommentToIssueMutation struct {
AddComment struct {
CommentEdge struct {
Expand Down