Skip to content

Commit 181715f

Browse files
committed
feat: checkout branch if we can
1 parent fd28d4f commit 181715f

File tree

6 files changed

+163
-42
lines changed

6 files changed

+163
-42
lines changed

pkg/cmd/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
200200
if cmd.Bool("wait") {
201201
console.Spacer()
202202
model := tea.Model(buildCompletionModel{
203-
Build: cbuild.NewModel(client, ctx, *build, downloadPaths),
203+
Build: cbuild.NewModel(client, ctx, *build, cmd.String("branch"), downloadPaths),
204204
})
205205
model, err = tea.NewProgram(model).Run()
206206
if err != nil {

pkg/cmd/buildtargetoutput.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func handleBuildsTargetOutputsRetrieve(ctx context.Context, cmd *cli.Command) er
9090

9191
group := console.Info("Downloading output")
9292
if cmd.Bool("pull") {
93-
return build.PullOutput(res.Output, res.URL, res.Ref, "", group)
93+
return build.PullOutput(res.Output, res.URL, res.Ref, cmd.String("branch"), "", group)
9494
}
9595

9696
return nil

pkg/cmd/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ func initializeWorkspace(ctx context.Context, cmd *cli.Command, client stainless
415415
}
416416

417417
model := buildCompletionModel{
418-
Build: cbuild.NewModel(client, ctx, *build, downloadPaths),
418+
Build: cbuild.NewModel(client, ctx, *build, "main", downloadPaths),
419419
}
420420

421421
_, err = tea.NewProgram(model).Run()

pkg/components/build/model.go

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
package build
22

33
import (
4-
"bytes"
54
"context"
65
"errors"
76
"fmt"
87
"io"
98
"net/http"
109
"net/url"
1110
"os"
12-
"os/exec"
1311
"path/filepath"
1412
"strings"
1513
"time"
1614

1715
tea "github.com/charmbracelet/bubbletea"
1816
"github.com/stainless-api/stainless-api-cli/pkg/console"
17+
"github.com/stainless-api/stainless-api-cli/pkg/git"
1918
"github.com/stainless-api/stainless-api-cli/pkg/stainlessutils"
2019
"github.com/stainless-api/stainless-api-go"
2120
)
@@ -27,6 +26,7 @@ type Model struct {
2726

2827
Client stainless.Client
2928
Ctx context.Context
29+
Branch string // Optional branch name for git checkout
3030
Downloads map[stainless.Target]DownloadStatus // When a BuildTarget has a commit available, this target will download it, if it has been specified in the initialization.
3131
Err error // This will be populated if the model concludes with an error
3232
}
@@ -43,11 +43,13 @@ type FetchBuildMsg stainless.Build
4343
type ErrorMsg error
4444
type DownloadMsg struct {
4545
Target stainless.Target
46+
// One of "not_started", "in_progress", "completed"
47+
Status string
4648
// One of "success", "failure',
4749
Conclusion string
4850
}
4951

50-
func NewModel(client stainless.Client, ctx context.Context, build stainless.Build, downloadPaths map[stainless.Target]string) Model {
52+
func NewModel(client stainless.Client, ctx context.Context, build stainless.Build, branch string, downloadPaths map[stainless.Target]string) Model {
5153
downloads := map[stainless.Target]DownloadStatus{}
5254
for target, path := range downloadPaths {
5355
downloads[target] = DownloadStatus{
@@ -60,6 +62,7 @@ func NewModel(client stainless.Client, ctx context.Context, build stainless.Buil
6062
Build: build,
6163
Client: client,
6264
Ctx: ctx,
65+
Branch: branch,
6366
Downloads: downloads,
6467
}
6568
}
@@ -108,7 +111,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
108111
status, _, conclusion := buildTarget.StepInfo("commit")
109112
downloadable := status == "completed" && conclusion != "fatal"
110113
if download, ok := m.Downloads[target]; ok && downloadable && download.Status == "not_started" {
111-
download.Status = "started"
114+
download.Status = "in_progress"
112115
cmds = append(cmds, m.downloadTarget(target))
113116
m.Downloads[target] = download
114117
}
@@ -136,11 +139,12 @@ func (m Model) downloadTarget(target stainless.Target) tea.Cmd {
136139
if err != nil {
137140
return ErrorMsg(err)
138141
}
139-
err = PullOutput(outputRes.Output, outputRes.URL, outputRes.Ref, m.Downloads[target].Path, console.NewGroup(true))
142+
err = PullOutput(outputRes.Output, outputRes.URL, outputRes.Ref, m.Branch, m.Downloads[target].Path, console.NewGroup(true))
140143
if err != nil {
141-
return DownloadMsg{target, "failure"}
144+
console.Error(fmt.Sprintf("Failed to download %s: %v", target, err))
145+
return DownloadMsg{target, "completed", "failure"}
142146
}
143-
return DownloadMsg{target, "success"}
147+
return DownloadMsg{target, "completed", "success"}
144148
}
145149
}
146150

@@ -216,8 +220,44 @@ func extractFilename(urlStr string, resp *http.Response) string {
216220
return extractFilenameFromURL(urlStr)
217221
}
218222

223+
// checkoutBranchIfSafe attempts to checkout a branch if it's safe to do so.
224+
// Returns true if the branch was checked out, false if we should checkout the ref instead.
225+
func checkoutBranchIfSafe(targetDir, branch, ref string, targetGroup console.Group) (bool, error) {
226+
remoteBranch := "origin/" + branch
227+
228+
// Check if the remote branch exists and matches the ref
229+
remoteSHA, err := git.RevParse(targetDir, remoteBranch)
230+
if err != nil || remoteSHA != ref {
231+
// Remote branch doesn't exist or doesn't match ref, checkout ref instead
232+
return false, nil
233+
}
234+
235+
// Check if local branch exists
236+
localSHA, err := git.RevParse(targetDir, branch)
237+
if err != nil {
238+
// Local branch doesn't exist, create it tracking the remote
239+
targetGroup.Property("checking out branch", branch)
240+
if err := git.Checkout(targetDir, "-b", branch, remoteBranch); err != nil {
241+
return false, err
242+
}
243+
return true, nil
244+
}
245+
246+
// Local branch exists - only checkout if it points to the same SHA as ref
247+
if localSHA == ref {
248+
targetGroup.Property("checking out branch", branch)
249+
if err := git.Checkout(targetDir, branch); err != nil {
250+
return false, err
251+
}
252+
return true, nil
253+
}
254+
255+
// Local branch exists but points to a different SHA, checkout ref instead to be safe
256+
return false, nil
257+
}
258+
219259
// PullOutput handles downloading or cloning a build target output
220-
func PullOutput(output, url, ref, targetDir string, targetGroup console.Group) error {
260+
func PullOutput(output, url, ref, branch, targetDir string, targetGroup console.Group) error {
221261
switch output {
222262
case "git":
223263
// Extract repository name from git URL for directory name
@@ -251,51 +291,51 @@ func PullOutput(output, url, ref, targetDir string, targetGroup console.Group) e
251291
}
252292

253293
// Initialize git repository
254-
cmd := exec.Command("git", "-C", targetDir, "init")
255-
var stderr bytes.Buffer
256-
cmd.Stdout = nil
257-
cmd.Stderr = &stderr
258-
if err := cmd.Run(); err != nil {
259-
return fmt.Errorf("git init failed: %v\nGit error: %s", err, stderr.String())
294+
if err := git.Init(targetDir); err != nil {
295+
return err
260296
}
261297
}
262298

263299
{
264300
// Check if origin remote exists, add it if not present
265-
cmd := exec.Command("git", "-C", targetDir, "remote", "get-url", "origin")
266-
var stderr bytes.Buffer
267-
cmd.Stdout = nil
268-
cmd.Stderr = &stderr
269-
if err := cmd.Run(); err != nil {
301+
if _, err := git.RemoteGetURL(targetDir, "origin"); err != nil {
270302
// Origin doesn't exist, add it with stripped auth
271303
targetGroup.Property("adding remote origin", stripHTTPAuth(url))
272-
addCmd := exec.Command("git", "-C", targetDir, "remote", "add", "origin", stripHTTPAuth(url))
273-
var addStderr bytes.Buffer
274-
addCmd.Stdout = nil
275-
addCmd.Stderr = &addStderr
276-
if err := addCmd.Run(); err != nil {
277-
return fmt.Errorf("git remote add failed: %v\nGit error: %s", err, addStderr.String())
304+
if err := git.RemoteAdd(targetDir, "origin", stripHTTPAuth(url)); err != nil {
305+
return err
278306
}
279307
}
280308

281309
targetGroup.Property("fetching from", stripHTTPAuth(url))
282-
cmd = exec.Command("git", "-C", targetDir, "fetch", url, ref)
283-
cmd.Stdout = nil
284-
cmd.Stderr = &stderr
285-
if err := cmd.Run(); err != nil {
286-
return fmt.Errorf("git fetch failed: %v\nGit error: %s", err, stderr.String())
310+
// Fetch the specific ref
311+
if err := git.Fetch(targetDir, url, ref); err != nil {
312+
return err
313+
}
314+
315+
// Also fetch the branch if provided, so we can check if it points to the same ref
316+
if branch != "" {
317+
// Branch fetch is best-effort - ignore errors
318+
_ = git.Fetch(targetDir, url, branch+":refs/remotes/origin/"+branch)
287319
}
288320
}
289321

290-
// Checkout the specific ref
322+
// Checkout the specific ref or branch
291323
{
292-
targetGroup.Property("checking out ref", ref)
293-
cmd := exec.Command("git", "-C", targetDir, "checkout", ref)
294-
var stderr bytes.Buffer
295-
cmd.Stdout = nil // Suppress git checkout output
296-
cmd.Stderr = &stderr
297-
if err := cmd.Run(); err != nil {
298-
return fmt.Errorf("git checkout failed: %v\nGit error: %s", err, stderr.String())
324+
checkedOutBranch := false
325+
if branch != "" {
326+
var err error
327+
checkedOutBranch, err = checkoutBranchIfSafe(targetDir, branch, ref, targetGroup)
328+
if err != nil {
329+
return err
330+
}
331+
}
332+
333+
if !checkedOutBranch {
334+
// Checkout the ref directly (detached HEAD)
335+
targetGroup.Property("checking out ref", ref)
336+
if err := git.Checkout(targetDir, ref); err != nil {
337+
return err
338+
}
299339
}
300340
}
301341

pkg/components/dev/model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func NewModel(client stainless.Client, ctx context.Context, branch string, fn fu
4545
Ctx: ctx,
4646
Branch: branch,
4747
Help: help.New(),
48-
Build: build.NewModel(client, ctx, stainless.Build{}, downloadPaths),
48+
Build: build.NewModel(client, ctx, stainless.Build{}, branch, downloadPaths),
4949
Diagnostics: diagnostics.NewModel(client, ctx, nil),
5050
}
5151
}

pkg/git/git.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package git
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os/exec"
7+
"strings"
8+
)
9+
10+
// RevParse runs git rev-parse and returns the SHA, or error if it fails
11+
func RevParse(dir, ref string) (string, error) {
12+
cmd := exec.Command("git", "-C", dir, "rev-parse", ref)
13+
var stdout bytes.Buffer
14+
cmd.Stdout = &stdout
15+
if err := cmd.Run(); err != nil {
16+
return "", err
17+
}
18+
return strings.TrimSpace(stdout.String()), nil
19+
}
20+
21+
// Checkout runs git checkout with the given arguments
22+
func Checkout(dir string, args ...string) error {
23+
fullArgs := append([]string{"-C", dir, "checkout"}, args...)
24+
cmd := exec.Command("git", fullArgs...)
25+
var stderr bytes.Buffer
26+
cmd.Stdout = nil
27+
cmd.Stderr = &stderr
28+
if err := cmd.Run(); err != nil {
29+
return fmt.Errorf("git checkout failed: %v\nGit error: %s", err, stderr.String())
30+
}
31+
return nil
32+
}
33+
34+
// Init initializes a git repository in the given directory
35+
func Init(dir string) error {
36+
cmd := exec.Command("git", "-C", dir, "init")
37+
var stderr bytes.Buffer
38+
cmd.Stdout = nil
39+
cmd.Stderr = &stderr
40+
if err := cmd.Run(); err != nil {
41+
return fmt.Errorf("git init failed: %v\nGit error: %s", err, stderr.String())
42+
}
43+
return nil
44+
}
45+
46+
// RemoteAdd adds a remote to the repository
47+
func RemoteAdd(dir, name, url string) error {
48+
cmd := exec.Command("git", "-C", dir, "remote", "add", name, url)
49+
var stderr bytes.Buffer
50+
cmd.Stdout = nil
51+
cmd.Stderr = &stderr
52+
if err := cmd.Run(); err != nil {
53+
return fmt.Errorf("git remote add failed: %v\nGit error: %s", err, stderr.String())
54+
}
55+
return nil
56+
}
57+
58+
// RemoteGetURL gets the URL of a remote
59+
func RemoteGetURL(dir, name string) (string, error) {
60+
cmd := exec.Command("git", "-C", dir, "remote", "get-url", name)
61+
var stdout, stderr bytes.Buffer
62+
cmd.Stdout = &stdout
63+
cmd.Stderr = &stderr
64+
if err := cmd.Run(); err != nil {
65+
return "", err
66+
}
67+
return strings.TrimSpace(stdout.String()), nil
68+
}
69+
70+
// Fetch fetches from a remote
71+
func Fetch(dir, url string, refspecs ...string) error {
72+
args := append([]string{"-C", dir, "fetch", url}, refspecs...)
73+
cmd := exec.Command("git", args...)
74+
var stderr bytes.Buffer
75+
cmd.Stdout = nil
76+
cmd.Stderr = &stderr
77+
if err := cmd.Run(); err != nil {
78+
return fmt.Errorf("git fetch failed: %v\nGit error: %s", err, stderr.String())
79+
}
80+
return nil
81+
}

0 commit comments

Comments
 (0)