Skip to content

Commit 60d0ffc

Browse files
julianknutsenclaude
andcommitted
Abstract createGitHubPR behind GitHubPRClient and expand test coverage
Extend GitHubPRClient with 9 git-tree and PR creation methods (GetRef, GetCommitTree, CreateBlob, CreateTree, CreateCommit, CreateRef, UpdateRef, CreatePR, UpdatePR). Refactor createGitHubPR to use the interface, eliminating all raw ghPath/ghAPICall usage. Add 7 TestCreateGitHubPR cases covering new PR, existing PR update, ref fallback, and error paths. Expand errors.txtar with mutually exclusive flag combos and browse/sync arg validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a6862c2 commit 60d0ffc

File tree

5 files changed

+418
-95
lines changed

5 files changed

+418
-95
lines changed

cmd/wl/cmd_review.go

Lines changed: 19 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,8 @@ func runGitHubPR(stdout io.Writer, cfg *federation.Config, doltPath, branch stri
231231
prTitle := fmt.Sprintf("[wl] %s", title)
232232

233233
// Create git-native branch on fork + cross-fork PR to upstream.
234-
prURL, err := createGitHubPR(ghPath, cfg.GitHubRepo, cfg.ForkOrg, cfg.ForkDB, branch, prTitle, mdBuf.String(), stdout)
234+
client := newGHClient(ghPath)
235+
prURL, err := createGitHubPR(client, cfg.GitHubRepo, cfg.ForkOrg, cfg.ForkDB, branch, prTitle, mdBuf.String(), stdout)
235236
if err != nil {
236237
return err
237238
}
@@ -240,113 +241,54 @@ func runGitHubPR(stdout io.Writer, cfg *federation.Config, doltPath, branch stri
240241
return nil
241242
}
242243

243-
func createGitHubPR(ghPath, upstreamRepo, forkOrg, forkDB, wlBranch, title, mdBody string, stdout io.Writer) (string, error) {
244+
func createGitHubPR(client GitHubPRClient, upstreamRepo, forkOrg, forkDB, wlBranch, title, mdBody string, stdout io.Writer) (string, error) {
244245
forkRepo := forkOrg + "/" + forkDB
245246
wantedID := extractWantedID(wlBranch)
246247
markerPath := ".wasteland/" + wantedID + ".md"
247248

248249
// 1. Get fork's default branch SHA.
249250
fmt.Fprintln(stdout, " Getting fork HEAD...")
250-
refData, err := ghAPICall(ghPath, "GET", fmt.Sprintf("repos/%s/git/ref/heads/main", forkRepo), "")
251+
headSHA, err := client.GetRef(forkRepo, "heads/main")
251252
if err != nil {
252253
return "", fmt.Errorf("getting fork HEAD: %w", err)
253254
}
254-
var ref struct {
255-
Object struct {
256-
SHA string `json:"sha"`
257-
} `json:"object"`
258-
}
259-
if err := json.Unmarshal(refData, &ref); err != nil {
260-
return "", fmt.Errorf("parsing fork HEAD: %w", err)
261-
}
262-
headSHA := ref.Object.SHA
263255

264256
// 2. Get base tree SHA from the commit.
265-
commitData, err := ghAPICall(ghPath, "GET", fmt.Sprintf("repos/%s/git/commits/%s", forkRepo, headSHA), "")
257+
baseTreeSHA, err := client.GetCommitTree(forkRepo, headSHA)
266258
if err != nil {
267259
return "", fmt.Errorf("getting base commit: %w", err)
268260
}
269-
var commitObj struct {
270-
Tree struct {
271-
SHA string `json:"sha"`
272-
} `json:"tree"`
273-
}
274-
if err := json.Unmarshal(commitData, &commitObj); err != nil {
275-
return "", fmt.Errorf("parsing base commit: %w", err)
276-
}
277-
baseTreeSHA := commitObj.Tree.SHA
278261

279262
// 3. Create blob with marker file content.
280263
fmt.Fprintln(stdout, " Creating marker file...")
281-
blobBody, _ := json.Marshal(map[string]string{
282-
"content": mdBody,
283-
"encoding": "utf-8",
284-
})
285-
blobData, err := ghAPICall(ghPath, "POST", fmt.Sprintf("repos/%s/git/blobs", forkRepo), string(blobBody))
264+
blobSHA, err := client.CreateBlob(forkRepo, mdBody, "utf-8")
286265
if err != nil {
287266
return "", fmt.Errorf("creating blob: %w", err)
288267
}
289-
var blob struct {
290-
SHA string `json:"sha"`
291-
}
292-
if err := json.Unmarshal(blobData, &blob); err != nil {
293-
return "", fmt.Errorf("parsing blob response: %w", err)
294-
}
295268

296269
// 4. Create tree with marker file.
297-
treeBody, _ := json.Marshal(map[string]interface{}{
298-
"base_tree": baseTreeSHA,
299-
"tree": []map[string]string{{
300-
"path": markerPath,
301-
"mode": "100644",
302-
"type": "blob",
303-
"sha": blob.SHA,
304-
}},
305-
})
306-
treeData, err := ghAPICall(ghPath, "POST", fmt.Sprintf("repos/%s/git/trees", forkRepo), string(treeBody))
270+
treeSHA, err := client.CreateTree(forkRepo, baseTreeSHA, []TreeEntry{{
271+
Path: markerPath,
272+
Mode: "100644",
273+
Type: "blob",
274+
SHA: blobSHA,
275+
}})
307276
if err != nil {
308277
return "", fmt.Errorf("creating tree: %w", err)
309278
}
310-
var tree struct {
311-
SHA string `json:"sha"`
312-
}
313-
if err := json.Unmarshal(treeData, &tree); err != nil {
314-
return "", fmt.Errorf("parsing tree response: %w", err)
315-
}
316279

317280
// 5. Create commit on fork.
318281
fmt.Fprintln(stdout, " Creating commit...")
319-
newCommitBody, _ := json.Marshal(map[string]interface{}{
320-
"message": fmt.Sprintf("wl review: %s", wlBranch),
321-
"tree": tree.SHA,
322-
"parents": []string{headSHA},
323-
})
324-
newCommitData, err := ghAPICall(ghPath, "POST", fmt.Sprintf("repos/%s/git/commits", forkRepo), string(newCommitBody))
282+
commitSHA, err := client.CreateCommit(forkRepo, fmt.Sprintf("wl review: %s", wlBranch), treeSHA, []string{headSHA})
325283
if err != nil {
326284
return "", fmt.Errorf("creating commit: %w", err)
327285
}
328-
var newCommit struct {
329-
SHA string `json:"sha"`
330-
}
331-
if err := json.Unmarshal(newCommitData, &newCommit); err != nil {
332-
return "", fmt.Errorf("parsing commit response: %w", err)
333-
}
334286

335287
// 6. Create or update ref on fork.
336288
fmt.Fprintln(stdout, " Pushing branch to fork...")
337-
refBody, _ := json.Marshal(map[string]string{
338-
"ref": "refs/heads/" + wlBranch,
339-
"sha": newCommit.SHA,
340-
})
341-
_, err = ghAPICall(ghPath, "POST", fmt.Sprintf("repos/%s/git/refs", forkRepo), string(refBody))
342-
if err != nil {
289+
if err := client.CreateRef(forkRepo, "refs/heads/"+wlBranch, commitSHA); err != nil {
343290
// Ref may already exist — force-update it.
344-
updateBody, _ := json.Marshal(map[string]interface{}{
345-
"sha": newCommit.SHA,
346-
"force": true,
347-
})
348-
_, err = ghAPICall(ghPath, "PATCH", fmt.Sprintf("repos/%s/git/refs/heads/%s", forkRepo, wlBranch), string(updateBody))
349-
if err != nil {
291+
if err := client.UpdateRef(forkRepo, "heads/"+wlBranch, commitSHA, true); err != nil {
350292
return "", fmt.Errorf("creating/updating ref: %w", err)
351293
}
352294
}
@@ -355,33 +297,17 @@ func createGitHubPR(ghPath, upstreamRepo, forkOrg, forkDB, wlBranch, title, mdBo
355297
fmt.Fprintln(stdout, " Opening PR...")
356298
head := forkOrg + ":" + wlBranch
357299

358-
existingURL, existingNumber := findExistingPR(ghPath, upstreamRepo, head)
300+
existingURL, existingNumber := client.FindPR(upstreamRepo, head)
359301
if existingNumber != "" {
360-
// Update existing PR body.
361-
updateBody, _ := json.Marshal(map[string]string{
362-
"body": mdBody,
363-
})
364-
_, _ = ghAPICall(ghPath, "PATCH", fmt.Sprintf("repos/%s/pulls/%s", upstreamRepo, existingNumber), string(updateBody))
302+
_ = client.UpdatePR(upstreamRepo, existingNumber, map[string]string{"body": mdBody})
365303
return existingURL, nil
366304
}
367305

368-
prBody, _ := json.Marshal(map[string]string{
369-
"title": title,
370-
"body": mdBody,
371-
"head": head,
372-
"base": "main",
373-
})
374-
prData, err := ghAPICall(ghPath, "POST", fmt.Sprintf("repos/%s/pulls", upstreamRepo), string(prBody))
306+
prURL, err := client.CreatePR(upstreamRepo, title, mdBody, head, "main")
375307
if err != nil {
376308
return "", fmt.Errorf("creating PR: %w", err)
377309
}
378-
var pr struct {
379-
HTMLURL string `json:"html_url"`
380-
}
381-
if err := json.Unmarshal(prData, &pr); err != nil {
382-
return "", fmt.Errorf("parsing PR response: %w", err)
383-
}
384-
return pr.HTMLURL, nil
310+
return prURL, nil
385311
}
386312

387313
// findExistingPR checks for an open PR on upstream with the given head ref.

cmd/wl/cmd_review_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,132 @@ func TestCloseGitHubPR(t *testing.T) {
332332
}
333333
}
334334

335+
func TestCreateGitHubPR(t *testing.T) {
336+
baseFake := func() *fakeGitHubPRClient {
337+
return &fakeGitHubPRClient{
338+
prs: map[string]fakePR{},
339+
GetRefSHA: "abc123",
340+
GetCommitTreeSHA: "tree456",
341+
CreateBlobSHA: "blob789",
342+
CreateTreeSHA: "newtree",
343+
CreateCommitSHA: "newcommit",
344+
CreatePRURL: "https://github.com/upstream/repo/pull/42",
345+
}
346+
}
347+
348+
tests := []struct {
349+
name string
350+
setup func(*fakeGitHubPRClient)
351+
existingPR *fakePR // if set, FindPR returns this
352+
wantURL string
353+
wantErr string
354+
wantCreateRef int
355+
wantUpdateRef int
356+
wantCreatePR int
357+
wantUpdatePR int
358+
}{
359+
{
360+
name: "new PR success",
361+
wantURL: "https://github.com/upstream/repo/pull/42",
362+
wantCreateRef: 1,
363+
wantCreatePR: 1,
364+
},
365+
{
366+
name: "existing PR updates body",
367+
existingPR: &fakePR{URL: "https://github.com/upstream/repo/pull/7", Number: "7"},
368+
wantURL: "https://github.com/upstream/repo/pull/7",
369+
wantCreateRef: 1,
370+
wantUpdatePR: 1,
371+
},
372+
{
373+
name: "ref exists falls back to UpdateRef",
374+
setup: func(f *fakeGitHubPRClient) {
375+
f.CreateRefErr = fmt.Errorf("ref already exists")
376+
},
377+
wantURL: "https://github.com/upstream/repo/pull/42",
378+
wantCreateRef: 1,
379+
wantUpdateRef: 1,
380+
wantCreatePR: 1,
381+
},
382+
{
383+
name: "GetRef fails",
384+
setup: func(f *fakeGitHubPRClient) {
385+
f.GetRefErr = fmt.Errorf("not found")
386+
},
387+
wantErr: "getting fork HEAD",
388+
},
389+
{
390+
name: "CreateBlob fails",
391+
setup: func(f *fakeGitHubPRClient) {
392+
f.CreateBlobErr = fmt.Errorf("quota exceeded")
393+
},
394+
wantErr: "creating blob",
395+
},
396+
{
397+
name: "CreateRef and UpdateRef both fail",
398+
setup: func(f *fakeGitHubPRClient) {
399+
f.CreateRefErr = fmt.Errorf("ref exists")
400+
f.UpdateRefErr = fmt.Errorf("permission denied")
401+
},
402+
wantErr: "creating/updating ref",
403+
wantCreateRef: 1,
404+
wantUpdateRef: 1,
405+
},
406+
{
407+
name: "CreatePR fails",
408+
setup: func(f *fakeGitHubPRClient) {
409+
f.CreatePRErr = fmt.Errorf("validation failed")
410+
},
411+
wantErr: "creating PR",
412+
wantCreateRef: 1,
413+
wantCreatePR: 1,
414+
},
415+
}
416+
417+
for _, tc := range tests {
418+
t.Run(tc.name, func(t *testing.T) {
419+
client := baseFake()
420+
if tc.existingPR != nil {
421+
client.prs["myfork:wl/rig/w-123"] = *tc.existingPR
422+
}
423+
if tc.setup != nil {
424+
tc.setup(client)
425+
}
426+
427+
var buf bytes.Buffer
428+
url, err := createGitHubPR(client, "upstream/repo", "myfork", "forkdb", "wl/rig/w-123", "[wl] Test", "## diff", &buf)
429+
430+
if tc.wantErr != "" {
431+
if err == nil {
432+
t.Fatal("expected error")
433+
}
434+
if !strings.Contains(err.Error(), tc.wantErr) {
435+
t.Errorf("error %q should contain %q", err, tc.wantErr)
436+
}
437+
return
438+
}
439+
if err != nil {
440+
t.Fatalf("unexpected error: %v", err)
441+
}
442+
if url != tc.wantURL {
443+
t.Errorf("got URL %q, want %q", url, tc.wantURL)
444+
}
445+
if len(client.CreateRefCalls) != tc.wantCreateRef {
446+
t.Errorf("CreateRef calls = %d, want %d", len(client.CreateRefCalls), tc.wantCreateRef)
447+
}
448+
if len(client.UpdateRefCalls) != tc.wantUpdateRef {
449+
t.Errorf("UpdateRef calls = %d, want %d", len(client.UpdateRefCalls), tc.wantUpdateRef)
450+
}
451+
if len(client.CreatePRCalls) != tc.wantCreatePR {
452+
t.Errorf("CreatePR calls = %d, want %d", len(client.CreatePRCalls), tc.wantCreatePR)
453+
}
454+
if len(client.UpdatePRCalls) != tc.wantUpdatePR {
455+
t.Errorf("UpdatePR calls = %d, want %d", len(client.UpdatePRCalls), tc.wantUpdatePR)
456+
}
457+
})
458+
}
459+
}
460+
335461
func TestExtractWantedID(t *testing.T) {
336462
tests := []struct {
337463
branch, want string

0 commit comments

Comments
 (0)