Skip to content

Commit 5b80f54

Browse files
Improve update flow and monorepo sub-repo resolution
- Make update check non-blocking by running it in a background goroutine and showing the result from the previous check instantly - Break ApplyUpdate into PrepareUpdate/DownloadUpdate/VerifyUpdate/ExtractAndInstall with step-by-step progress output - Extract monorepo retry logic into resolveIssuesWithRetry for clarity - Use os.Getwd and filepath.Rel instead of shelling out to pwd - Fix pointer receiver on CLIConfig.IsExpired and unused receiver in pat_login_flow
1 parent 1d9f60a commit 5b80f54

File tree

7 files changed

+117
-68
lines changed

7 files changed

+117
-68
lines changed

cmd/deepsource/main.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,24 @@ func mainRun() (exitCode int) {
7373
func run() int {
7474
v.SetBuildInfo(version, Date, buildMode)
7575

76-
// Check for available updates and notify (skip when running "update" itself)
76+
// Notify about available updates from a previous check (instant, disk-only read).
77+
// Then kick off a background check for the next invocation.
7778
isUpdateCmd := len(os.Args) >= 2 && os.Args[1] == "update"
7879
if !isUpdateCmd && update.ShouldCheckForUpdate() {
79-
client := &http.Client{Timeout: 3 * time.Second}
80-
if err := update.CheckForUpdate(client); err != nil {
81-
debug.Log("update: %v", err)
82-
}
83-
8480
state, err := update.ReadUpdateState()
8581
if err != nil {
8682
debug.Log("update: %v", err)
8783
}
8884
if state != nil {
89-
fmt.Fprintln(os.Stderr, pterm.Yellow(fmt.Sprintf("Update available: v%s, run '%s update' to install.", state.Version, filepath.Base(os.Args[0]))))
85+
fmt.Fprintln(os.Stderr, pterm.Yellow(fmt.Sprintf("Update available: v%s → v%s, run '%s update' to install.", version, state.Version, filepath.Base(os.Args[0]))))
9086
}
87+
88+
go func() {
89+
client := &http.Client{Timeout: 3 * time.Second}
90+
if err := update.CheckForUpdate(client); err != nil {
91+
debug.Log("update: %v", err)
92+
}
93+
}()
9194
}
9295

9396
exitCode := 0

command/auth/login/pat_login_flow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/fatih/color"
1010
)
1111

12-
func (opts *LoginOptions) startPATLoginFlow(svc *authsvc.Service, cfg *config.CLIConfig, token string) error {
12+
func (_ *LoginOptions) startPATLoginFlow(svc *authsvc.Service, cfg *config.CLIConfig, token string) error {
1313
cfg.Token = token
1414

1515
viewer, err := svc.GetViewer(context.Background(), cfg)

command/issues/issues.go

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -252,36 +252,9 @@ func (opts *IssuesOptions) Run(ctx context.Context, cmd *cobra.Command) error {
252252
opts.client = client
253253
opts.remote = remote
254254

255-
issuesList, err := opts.resolveIssues(ctx, client, remote)
255+
issuesList, err := opts.resolveIssuesWithRetry(ctx, client, remote)
256256
if err != nil {
257-
// If the API says this is a monorepo, show a friendly message.
258-
if strings.Contains(err.Error(), "This repository is a monorepo") {
259-
return fmt.Errorf("This is a monorepo. Use --repo to specify a sub-project.\n\nHint: %s", err.Error())
260-
}
261-
262-
// If we auto-detected a sub-repo path and the exact path wasn't found,
263-
// progressively strip the last segment and retry.
264-
if strings.Contains(err.Error(), "Repository does not exist") && remote.SubRepoSuffix != "" {
265-
parts := strings.Split(remote.SubRepoSuffix, ":")
266-
// Try stripping from the end: e.g. a:b:c → a:b → a
267-
for len(parts) > 1 {
268-
parts = parts[:len(parts)-1]
269-
remote.SubRepoSuffix = strings.Join(parts, ":")
270-
baseName := strings.SplitN(remote.RepoName, ":", 2)[0]
271-
remote.RepoName = baseName + ":" + remote.SubRepoSuffix
272-
273-
issuesList, err = opts.resolveIssues(ctx, client, remote)
274-
if err == nil {
275-
break
276-
}
277-
if !strings.Contains(err.Error(), "Repository does not exist") {
278-
return err
279-
}
280-
}
281-
}
282-
if err != nil {
283-
return err
284-
}
257+
return err
285258
}
286259
if issuesList == nil {
287260
return nil
@@ -310,6 +283,41 @@ func (opts *IssuesOptions) Run(ctx context.Context, cmd *cobra.Command) error {
310283
return nil
311284
}
312285

286+
// resolveIssuesWithRetry calls resolveIssues and, for monorepos with an
287+
// auto-detected sub-repo path, progressively strips path segments on
288+
// "Repository does not exist" errors until a match is found.
289+
func (opts *IssuesOptions) resolveIssuesWithRetry(ctx context.Context, client *deepsource.Client, remote *vcs.RemoteData) ([]issues.Issue, error) {
290+
issuesList, err := opts.resolveIssues(ctx, client, remote)
291+
if err == nil {
292+
return issuesList, nil
293+
}
294+
295+
if strings.Contains(err.Error(), "This repository is a monorepo") {
296+
return nil, fmt.Errorf("This is a monorepo. Use --repo to specify a sub-project.\n\nHint: %s", err.Error())
297+
}
298+
299+
if !strings.Contains(err.Error(), "Repository does not exist") || remote.SubRepoSuffix == "" {
300+
return nil, err
301+
}
302+
303+
parts := strings.Split(remote.SubRepoSuffix, ":")
304+
for len(parts) > 1 {
305+
parts = parts[:len(parts)-1]
306+
remote.SubRepoSuffix = strings.Join(parts, ":")
307+
baseName := strings.SplitN(remote.RepoName, ":", 2)[0]
308+
remote.RepoName = baseName + ":" + remote.SubRepoSuffix
309+
310+
issuesList, err = opts.resolveIssues(ctx, client, remote)
311+
if err == nil {
312+
return issuesList, nil
313+
}
314+
if !strings.Contains(err.Error(), "Repository does not exist") {
315+
return nil, err
316+
}
317+
}
318+
return nil, err
319+
}
320+
313321
func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource.Client, remote *vcs.RemoteData) ([]issues.Issue, error) {
314322
serverFilters := opts.buildServerFilters()
315323
prFilters := opts.buildPRFilters()

command/update/update.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package update
33
import (
44
"fmt"
55
"net/http"
6+
"runtime"
67
"time"
78

89
"github.com/deepsourcelabs/cli/buildinfo"
910
"github.com/deepsourcelabs/cli/internal/update"
11+
"github.com/pterm/pterm"
1012
"github.com/spf13/cobra"
1113
)
1214

@@ -29,7 +31,7 @@ func runUpdate(cmd *cobra.Command) error {
2931
return fmt.Errorf("checking for updates: %w", err)
3032
}
3133

32-
state, err := update.ReadUpdateState()
34+
state, err := update.PrepareUpdate()
3335
if err != nil {
3436
return fmt.Errorf("reading update state: %w", err)
3537
}
@@ -40,17 +42,25 @@ func runUpdate(cmd *cobra.Command) error {
4042
return nil
4143
}
4244

43-
fmt.Fprintf(w, "Updating to v%s...\n", state.Version)
45+
fmt.Fprintln(w, pterm.Green("✓")+" Platform: "+runtime.GOOS+"/"+runtime.GOARCH)
46+
fmt.Fprintln(w, pterm.Green("✓")+" Version: v"+state.Version)
4447

4548
applyClient := &http.Client{Timeout: 30 * time.Second}
46-
newVer, err := update.ApplyUpdate(applyClient)
49+
data, err := update.DownloadUpdate(applyClient, state)
4750
if err != nil {
48-
return fmt.Errorf("applying update: %w", err)
51+
return fmt.Errorf("downloading update: %w", err)
4952
}
53+
fmt.Fprintln(w, pterm.Green("✓")+" Downloaded")
5054

51-
if newVer != "" {
52-
fmt.Fprintf(w, "Updated to v%s\n", newVer)
55+
if err := update.VerifyUpdate(data, state); err != nil {
56+
return fmt.Errorf("verifying update: %w", err)
5357
}
58+
fmt.Fprintln(w, pterm.Green("✓")+" Checksum verified")
59+
60+
if err := update.ExtractAndInstall(data, state.ArchiveURL); err != nil {
61+
return fmt.Errorf("installing update: %w", err)
62+
}
63+
fmt.Fprintln(w, pterm.Green("✓")+" Installed")
5464

5565
return nil
5666
}

config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (cfg *CLIConfig) SetTokenExpiry(str string) {
2525
cfg.TokenExpiresIn = t.UTC()
2626
}
2727

28-
func (cfg CLIConfig) IsExpired() bool {
28+
func (cfg *CLIConfig) IsExpired() bool {
2929
if cfg.TokenExpiresIn.IsZero() {
3030
return false
3131
}

internal/update/updater.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,50 +127,78 @@ func CheckForUpdate(client *http.Client) error {
127127
return writeUpdateState(state)
128128
}
129129

130-
// ApplyUpdate reads the state file, downloads the archive, verifies, extracts,
131-
// and replaces the binary. Returns the new version string on success.
132-
// Clears the state file regardless of outcome so we don't retry broken updates forever.
133-
func ApplyUpdate(client *http.Client) (string, error) {
130+
// PrepareUpdate reads and clears the state file, returning the update state.
131+
// Returns nil if no update is available.
132+
func PrepareUpdate() (*UpdateState, error) {
134133
state, err := ReadUpdateState()
135134
if err != nil {
136135
clearUpdateState()
137-
return "", err
136+
return nil, err
138137
}
139138
if state == nil {
140-
return "", nil
139+
return nil, nil
141140
}
142141

143142
// Clear state file up front so a failed update doesn't retry forever.
144-
// The next run will do a fresh CheckForUpdate instead.
145143
clearUpdateState()
144+
return state, nil
145+
}
146146

147-
debug.Log("update: applying update to v%s", state.Version)
148-
149-
data, err := downloadFile(client, state.ArchiveURL)
150-
if err != nil {
151-
return "", err
152-
}
147+
// DownloadUpdate downloads the archive from the state's URL.
148+
func DownloadUpdate(client *http.Client, state *UpdateState) ([]byte, error) {
149+
return downloadFile(client, state.ArchiveURL)
150+
}
153151

154-
if err := verifyChecksum(data, state.SHA256); err != nil {
155-
return "", err
156-
}
152+
// VerifyUpdate checks the SHA256 checksum of the downloaded data.
153+
func VerifyUpdate(data []byte, state *UpdateState) error {
154+
return verifyChecksum(data, state.SHA256)
155+
}
157156

157+
// ExtractAndInstall extracts the binary from the archive and replaces the current executable.
158+
func ExtractAndInstall(data []byte, archiveURL string) error {
158159
binaryName := buildinfo.AppName
159160
if runtime.GOOS == "windows" {
160161
binaryName += ".exe"
161162
}
162163

163164
var binaryData []byte
164-
if strings.HasSuffix(state.ArchiveURL, ".zip") {
165+
var err error
166+
if strings.HasSuffix(archiveURL, ".zip") {
165167
binaryData, err = extractFromZip(data, binaryName)
166168
} else {
167169
binaryData, err = extractFromTarGz(data, binaryName)
168170
}
169171
if err != nil {
172+
return err
173+
}
174+
175+
return replaceBinary(binaryData)
176+
}
177+
178+
// ApplyUpdate reads the state file, downloads the archive, verifies, extracts,
179+
// and replaces the binary. Returns the new version string on success.
180+
// Clears the state file regardless of outcome so we don't retry broken updates forever.
181+
func ApplyUpdate(client *http.Client) (string, error) {
182+
state, err := PrepareUpdate()
183+
if err != nil {
184+
return "", err
185+
}
186+
if state == nil {
187+
return "", nil
188+
}
189+
190+
debug.Log("update: applying update to v%s", state.Version)
191+
192+
data, err := DownloadUpdate(client, state)
193+
if err != nil {
194+
return "", err
195+
}
196+
197+
if err := VerifyUpdate(data, state); err != nil {
170198
return "", err
171199
}
172200

173-
if err := replaceBinary(binaryData); err != nil {
201+
if err := ExtractAndInstall(data, state.ArchiveURL); err != nil {
174202
return "", err
175203
}
176204

internal/vcs/remotes.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package vcs
33
import (
44
"fmt"
55
"net/url"
6+
"os"
67
"os/exec"
8+
"path/filepath"
79
"regexp"
810
"strings"
911

@@ -140,24 +142,22 @@ func detectSubRepoPath() string {
140142
}
141143
toplevel = strings.TrimSpace(toplevel)
142144

143-
cwd, err := runCmd("pwd", nil)
145+
cwd, err := os.Getwd()
144146
if err != nil {
145147
return ""
146148
}
147-
cwd = strings.TrimSpace(cwd)
148149

149150
if cwd == toplevel {
150151
return ""
151152
}
152153

153-
rel := strings.TrimPrefix(cwd, toplevel+"/")
154-
if rel == cwd {
155-
// cwd is not under toplevel (shouldn't happen)
154+
rel, err := filepath.Rel(toplevel, cwd)
155+
if err != nil {
156156
return ""
157157
}
158158

159159
debug.Log("git: sub-repo relative path %q", rel)
160-
return strings.ReplaceAll(rel, "/", ":")
160+
return strings.ReplaceAll(rel, string(os.PathSeparator), ":")
161161
}
162162

163163
func runCmd(command string, args []string) (string, error) {

0 commit comments

Comments
 (0)