Skip to content

Commit e2f3b24

Browse files
authored
forge: ChangeIsMerged -> ChangesAreMerged (#399)
Replace ChangeIsMerged with a bulk ChangesAreMerged API that takes a list of ChangeIDs and returns a list of booleans in the same order. With this in place, we can change 'repo sync' to make a single request for all submitted branches instead of one request per branch. [skip changelog]: No user facing changes in this branch.
1 parent 83da8d2 commit e2f3b24

File tree

6 files changed

+125
-55
lines changed

6 files changed

+125
-55
lines changed

internal/forge/forge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ type Repository interface {
133133
EditChange(ctx context.Context, id ChangeID, opts EditChangeOptions) error
134134
FindChangesByBranch(ctx context.Context, branch string, opts FindChangesOptions) ([]*FindChangeItem, error)
135135
FindChangeByID(ctx context.Context, id ChangeID) (*FindChangeItem, error)
136-
ChangeIsMerged(ctx context.Context, id ChangeID) (bool, error)
136+
ChangesAreMerged(ctx context.Context, ids []ChangeID) ([]bool, error)
137137

138138
// Post and update comments on changes.
139139
PostChangeComment(context.Context, ChangeID, string) (ChangeCommentID, error)

internal/forge/github/integration_test.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,24 +199,22 @@ func TestIntegration_Repository_FindChangesByBranch(t *testing.T) {
199199
})
200200
}
201201

202-
func TestIntegration_Repository_IsMerged(t *testing.T) {
202+
func TestIntegration_Repository_ChangesAreMerged(t *testing.T) {
203203
ctx := context.Background()
204204
rec := newRecorder(t, t.Name())
205205
ghc := githubv4.NewClient(rec.GetDefaultClient())
206206
repo, err := github.NewRepository(ctx, new(github.Forge), "abhinav", "git-spice", logtest.New(t), ghc, _gitSpiceRepoID)
207207
require.NoError(t, err)
208208

209-
t.Run("false", func(t *testing.T) {
210-
ok, err := repo.ChangeIsMerged(ctx, &github.PR{Number: 144})
211-
require.NoError(t, err)
212-
assert.False(t, ok)
213-
})
214-
215-
t.Run("true", func(t *testing.T) {
216-
ok, err := repo.ChangeIsMerged(ctx, &github.PR{Number: 141})
217-
require.NoError(t, err)
218-
assert.True(t, ok)
209+
merged, err := repo.ChangesAreMerged(ctx, []forge.ChangeID{
210+
&github.PR{Number: 196, GQLID: "PR_kwDOJ2BQKs5ylEYu"}, // merged
211+
&github.PR{Number: 387, GQLID: "PR_kwDOJ2BQKs56wX01"}, // open (not merged)
212+
&github.PR{Number: 144, GQLID: "PR_kwDOJ2BQKs5xNeqO"}, // merged
213+
// Explicit GQL ID means we don't need to be in the same repo.
214+
&github.PR{Number: 4, GQLID: githubv4.ID("PR_kwDOMVd0xs51N_9r")}, // closed (not merged)
219215
})
216+
require.NoError(t, err)
217+
assert.Equal(t, []bool{true, false, true, false}, merged)
220218
}
221219

222220
func TestIntegration_Repository_ListChangeTemplates(t *testing.T) {

internal/forge/github/merged.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,41 @@ import (
88
"go.abhg.dev/gs/internal/forge"
99
)
1010

11-
// ChangeIsMerged reports whether a change has been merged.
12-
func (r *Repository) ChangeIsMerged(ctx context.Context, id forge.ChangeID) (bool, error) {
13-
// TODO: Bulk ChangesAreMerged that takes a list of IDs.
14-
11+
// ChangesAreMerged reports whether the given changes have been merged.
12+
// The returned slice is in the same order as the input slice.
13+
func (r *Repository) ChangesAreMerged(ctx context.Context, ids []forge.ChangeID) ([]bool, error) {
1514
var q struct {
16-
Repository struct {
15+
Nodes []struct {
1716
PullRequest struct {
1817
Merged bool `graphql:"merged"`
19-
} `graphql:"pullRequest(number: $number)"`
20-
} `graphql:"repository(owner: $owner, name: $repo)"`
18+
} `graphql:"... on PullRequest"`
19+
} `graphql:"nodes(ids: $ids)"`
20+
}
21+
22+
gqlIDs := make([]githubv4.ID, len(ids))
23+
for i, id := range ids {
24+
// Since before the first stable v0.1.0,
25+
// the data store has tracked the GraphQL ID of each change,
26+
// so this won't actually make a network request.
27+
//
28+
// However, if a [PR] was constructed in-process
29+
// and not from the data store, we need to resolve it,
30+
// and that will make a network request.
31+
var err error
32+
gqlIDs[i], err = r.graphQLID(ctx, mustPR(id))
33+
if err != nil {
34+
return nil, fmt.Errorf("resolve ID %v: %w", id, err)
35+
}
2136
}
22-
err := r.client.Query(ctx, &q, map[string]any{
23-
"owner": githubv4.String(r.owner),
24-
"repo": githubv4.String(r.repo),
25-
"number": githubv4.Int(mustPR(id).Number),
26-
})
27-
if err != nil {
28-
return false, fmt.Errorf("query failed: %w", err)
37+
38+
if err := r.client.Query(ctx, &q, map[string]any{"ids": gqlIDs}); err != nil {
39+
return nil, fmt.Errorf("query failed: %w", err)
40+
}
41+
42+
merged := make([]bool, len(ids))
43+
for i, pr := range q.Nodes {
44+
merged[i] = pr.PullRequest.Merged
2945
}
3046

31-
return q.Repository.PullRequest.Merged, nil
47+
return merged, nil
3248
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
version: 2
3+
interactions:
4+
- id: 0
5+
request:
6+
proto: HTTP/1.1
7+
proto_major: 1
8+
proto_minor: 1
9+
content_length: 187
10+
transfer_encoding: []
11+
trailer: {}
12+
host: api.github.com
13+
remote_addr: ""
14+
request_uri: ""
15+
body: |
16+
{"query":"query($ids:[ID!]!){nodes(ids: $ids){... on PullRequest{merged}}}","variables":{"ids":["PR_kwDOJ2BQKs5ylEYu","PR_kwDOJ2BQKs56wX01","PR_kwDOJ2BQKs5xNeqO","PR_kwDOMVd0xs51N_9r"]}}
17+
form: {}
18+
headers:
19+
Content-Type:
20+
- application/json
21+
url: https://api.github.com/graphql
22+
method: POST
23+
response:
24+
proto: HTTP/2.0
25+
proto_major: 2
26+
proto_minor: 0
27+
transfer_encoding: []
28+
trailer: {}
29+
content_length: -1
30+
uncompressed: true
31+
body: '{"data":{"nodes":[{"merged":true},{"merged":false},{"merged":true},{"merged":false}]}}'
32+
headers:
33+
Content-Type:
34+
- application/json; charset=utf-8
35+
status: 200 OK
36+
code: 200
37+
duration: 269.167291ms

internal/forge/shamhub/merge.go

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"net/http"
88
"os"
99
"os/exec"
10-
"strconv"
1110
"strings"
1211
"time"
1312

@@ -16,57 +15,77 @@ import (
1615
"go.abhg.dev/gs/internal/ioutil"
1716
)
1817

19-
type isMergedResponse struct {
20-
Merged bool `json:"merged"`
18+
type areMergedRequest struct {
19+
IDs []ChangeID `json:"ids"`
2120
}
2221

23-
var _ = shamhubHandler("GET /{owner}/{repo}/change/{number}/merged", (*ShamHub).handleIsMerged)
22+
type areMergedResponse struct {
23+
Merged []bool `json:"merged"`
24+
}
25+
26+
var _ = shamhubHandler("POST /{owner}/{repo}/change/merged", (*ShamHub).handleAreMerged)
2427

25-
func (sh *ShamHub) handleIsMerged(w http.ResponseWriter, r *http.Request) {
26-
owner, repo, numStr := r.PathValue("owner"), r.PathValue("repo"), r.PathValue("number")
27-
if owner == "" || repo == "" || numStr == "" {
28+
func (sh *ShamHub) handleAreMerged(w http.ResponseWriter, r *http.Request) {
29+
owner, repo := r.PathValue("owner"), r.PathValue("repo")
30+
if owner == "" || repo == "" {
2831
http.Error(w, "owner, repo, and number are required", http.StatusBadRequest)
2932
return
3033
}
3134

32-
num, err := strconv.Atoi(numStr)
33-
if err != nil {
35+
var req areMergedRequest
36+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
3437
http.Error(w, err.Error(), http.StatusBadRequest)
3538
return
3639
}
3740

41+
changeNumToIdx := make(map[int]int, len(req.IDs))
42+
for i, id := range req.IDs {
43+
changeNumToIdx[int(id)] = i
44+
}
45+
3846
sh.mu.RLock()
39-
var (
40-
merged bool
41-
found bool
42-
)
47+
merged := make([]bool, len(sh.changes))
4348
for _, c := range sh.changes {
44-
if c.Owner == owner && c.Repo == repo && c.Number == num {
45-
merged = c.State == shamChangeMerged
46-
found = true
47-
break
49+
if c.Owner == owner && c.Repo == repo {
50+
idx, ok := changeNumToIdx[c.Number]
51+
if !ok {
52+
continue
53+
}
54+
merged[idx] = c.State == shamChangeMerged
55+
delete(changeNumToIdx, c.Number)
56+
57+
if len(changeNumToIdx) == 0 {
58+
break
59+
}
4860
}
4961
}
5062
sh.mu.RUnlock()
5163

52-
if !found {
53-
http.Error(w, "change not found", http.StatusNotFound)
64+
if len(changeNumToIdx) > 0 {
65+
w.WriteHeader(http.StatusNotFound)
66+
fmt.Fprintf(w, "changes not found: %v", changeNumToIdx)
5467
return
5568
}
5669

5770
enc := json.NewEncoder(w)
5871
enc.SetIndent("", " ")
59-
if err := enc.Encode(isMergedResponse{Merged: merged}); err != nil {
72+
if err := enc.Encode(areMergedResponse{Merged: merged}); err != nil {
6073
http.Error(w, err.Error(), http.StatusInternalServerError)
6174
}
6275
}
6376

64-
func (f *forgeRepository) ChangeIsMerged(ctx context.Context, fid forge.ChangeID) (bool, error) {
65-
id := fid.(ChangeID)
66-
u := f.apiURL.JoinPath(f.owner, f.repo, "change", strconv.Itoa(int(id)), "merged")
67-
var res isMergedResponse
68-
if err := f.client.Get(ctx, u.String(), &res); err != nil {
69-
return false, fmt.Errorf("is merged: %w", err)
77+
func (f *forgeRepository) ChangesAreMerged(ctx context.Context, fids []forge.ChangeID) ([]bool, error) {
78+
ids := make([]ChangeID, len(fids))
79+
for i, fid := range fids {
80+
ids[i] = fid.(ChangeID)
81+
}
82+
83+
u := f.apiURL.JoinPath(f.owner, f.repo, "change", "merged")
84+
req := areMergedRequest{IDs: ids}
85+
86+
var res areMergedResponse
87+
if err := f.client.Post(ctx, u.String(), req, &res); err != nil {
88+
return nil, fmt.Errorf("are merged: %w", err)
7089
}
7190
return res.Merged, nil
7291
}

repo_sync.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,13 @@ func (cmd *repoSyncCmd) deleteMergedBranches(
272272

273273
// TODO: Once we're recording GraphQL IDs in the store,
274274
// we can combine all submitted PRs into one query.
275-
merged, err := remoteRepo.ChangeIsMerged(ctx, b.Change)
275+
merged, err := remoteRepo.ChangesAreMerged(ctx, []forge.ChangeID{b.Change})
276276
if err != nil {
277277
log.Error("Failed to query CR status", "change", b.Change, "error", err)
278278
continue
279279
}
280280

281-
b.Merged = merged
281+
b.Merged = merged[0]
282282

283283
case b, ok := <-trachedch:
284284
if !ok {

0 commit comments

Comments
 (0)