Skip to content

Commit f3e4ec6

Browse files
Auto-detect current branch for issues, metrics, and vulns commands
- Default behavior now resolves the current git branch and fetches results from its latest analyzed commit instead of the default branch - Add --default-branch flag to explicitly query the default branch - Rename transformers package to codeformatters - Add aliases, EPSS score, and reference URLs to vulnerability data - Add report card and updatedAt fields to analysis runs - Support server-side filtering (source, category, severity) for run-scoped issues queries - Add cmdutil.ResolveLatestRun helper for branch auto-detection - Make auth login/logout and dashboard commands accept injectable deps - Add tests for all new behavior including branch auto-detection, mutual exclusivity, YAML output, output-file, and empty results
1 parent 013045e commit f3e4ec6

51 files changed

Lines changed: 1945 additions & 155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

command/auth/login/login.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"github.com/MakeNowJust/heredoc"
7+
"github.com/deepsourcelabs/cli/command/cmddeps"
78
"github.com/deepsourcelabs/cli/config"
89
"github.com/deepsourcelabs/cli/internal/cli/args"
910
"github.com/deepsourcelabs/cli/internal/cli/prompt"
@@ -22,10 +23,16 @@ type LoginOptions struct {
2223
HostName string
2324
Interactive bool
2425
PAT string
26+
deps *cmddeps.Deps
2527
}
2628

2729
// NewCmdLogin handles the login functionality for the CLI
2830
func NewCmdLogin() *cobra.Command {
31+
return NewCmdLoginWithDeps(nil)
32+
}
33+
34+
// NewCmdLoginWithDeps creates the login command with injectable dependencies.
35+
func NewCmdLoginWithDeps(deps *cmddeps.Deps) *cobra.Command {
2936
doc := heredoc.Docf(`
3037
Log in to DeepSource using the CLI.
3138
@@ -44,6 +51,7 @@ func NewCmdLogin() *cobra.Command {
4451
TokenExpired: true,
4552
User: "",
4653
HostName: "",
54+
deps: deps,
4755
}
4856

4957
cmd := &cobra.Command{
@@ -66,7 +74,13 @@ func NewCmdLogin() *cobra.Command {
6674

6775
// Run executes the auth command and starts the login flow if not already authenticated
6876
func (opts *LoginOptions) Run() (err error) {
69-
svc := authsvc.NewService(config.DefaultManager())
77+
var cfgMgr *config.Manager
78+
if opts.deps != nil && opts.deps.ConfigMgr != nil {
79+
cfgMgr = opts.deps.ConfigMgr
80+
} else {
81+
cfgMgr = config.DefaultManager()
82+
}
83+
svc := authsvc.NewService(cfgMgr)
7084
// Fetch config (errors are non-fatal: a zero config just means "not logged in")
7185
cfg, err := svc.LoadConfig()
7286
if err != nil {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/deepsourcelabs/cli/command/cmddeps"
7+
loginCmd "github.com/deepsourcelabs/cli/command/auth/login"
8+
"github.com/deepsourcelabs/cli/internal/testutil"
9+
)
10+
11+
func TestLoginPATFlow(t *testing.T) {
12+
// Empty token → IsExpired() returns true → skips re-auth prompt
13+
cfgMgr := testutil.CreateExpiredTestConfigManager(t, "", "deepsource.com", "")
14+
15+
deps := &cmddeps.Deps{
16+
ConfigMgr: cfgMgr,
17+
}
18+
19+
cmd := loginCmd.NewCmdLoginWithDeps(deps)
20+
cmd.SetArgs([]string{"--with-token", "dsp_abc123"})
21+
22+
if err := cmd.Execute(); err != nil {
23+
t.Fatalf("unexpected error: %v", err)
24+
}
25+
26+
// Verify token was saved
27+
cfg, err := cfgMgr.Load()
28+
if err != nil {
29+
t.Fatalf("failed to load config: %v", err)
30+
}
31+
if cfg.Token != "dsp_abc123" {
32+
t.Errorf("expected token %q, got %q", "dsp_abc123", cfg.Token)
33+
}
34+
if cfg.Host != "deepsource.com" {
35+
t.Errorf("expected host %q, got %q", "deepsource.com", cfg.Host)
36+
}
37+
}
38+
39+
func TestLoginPATWithHostname(t *testing.T) {
40+
cfgMgr := testutil.CreateExpiredTestConfigManager(t, "", "deepsource.com", "")
41+
42+
deps := &cmddeps.Deps{
43+
ConfigMgr: cfgMgr,
44+
}
45+
46+
cmd := loginCmd.NewCmdLoginWithDeps(deps)
47+
cmd.SetArgs([]string{"--with-token", "dsp_xyz789", "--hostname", "enterprise.example.com"})
48+
49+
if err := cmd.Execute(); err != nil {
50+
t.Fatalf("unexpected error: %v", err)
51+
}
52+
53+
cfg, err := cfgMgr.Load()
54+
if err != nil {
55+
t.Fatalf("failed to load config: %v", err)
56+
}
57+
if cfg.Token != "dsp_xyz789" {
58+
t.Errorf("expected token %q, got %q", "dsp_xyz789", cfg.Token)
59+
}
60+
if cfg.Host != "enterprise.example.com" {
61+
t.Errorf("expected host %q, got %q", "enterprise.example.com", cfg.Host)
62+
}
63+
}
64+
65+
func TestLoginDefaultHostname(t *testing.T) {
66+
cfgMgr := testutil.CreateExpiredTestConfigManager(t, "", "deepsource.com", "")
67+
68+
deps := &cmddeps.Deps{
69+
ConfigMgr: cfgMgr,
70+
}
71+
72+
cmd := loginCmd.NewCmdLoginWithDeps(deps)
73+
cmd.SetArgs([]string{"--with-token", "dsp_default"})
74+
75+
if err := cmd.Execute(); err != nil {
76+
t.Fatalf("unexpected error: %v", err)
77+
}
78+
79+
cfg, err := cfgMgr.Load()
80+
if err != nil {
81+
t.Fatalf("failed to load config: %v", err)
82+
}
83+
// Without --hostname, host defaults to deepsource.com
84+
if cfg.Host != "deepsource.com" {
85+
t.Errorf("expected host %q, got %q", "deepsource.com", cfg.Host)
86+
}
87+
}

command/auth/logout/logout.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package logout
33
import (
44
"errors"
55

6+
"github.com/deepsourcelabs/cli/command/cmddeps"
67
"github.com/deepsourcelabs/cli/config"
78
"github.com/deepsourcelabs/cli/internal/cli/args"
89
"github.com/deepsourcelabs/cli/internal/cli/prompt"
@@ -12,24 +13,37 @@ import (
1213
"github.com/spf13/cobra"
1314
)
1415

15-
type LogoutOptions struct{}
16+
type LogoutOptions struct {
17+
deps *cmddeps.Deps
18+
}
1619

1720
// NewCmdLogout handles the logout functionality for the CLI
1821
func NewCmdLogout() *cobra.Command {
22+
return NewCmdLogoutWithDeps(nil)
23+
}
24+
25+
// NewCmdLogoutWithDeps creates the logout command with injectable dependencies.
26+
func NewCmdLogoutWithDeps(deps *cmddeps.Deps) *cobra.Command {
1927
cmd := &cobra.Command{
2028
Use: "logout",
2129
Short: "Logout of your active DeepSource account",
2230
Args: args.NoArgs,
2331
RunE: func(cmd *cobra.Command, args []string) error {
24-
opts := LogoutOptions{}
32+
opts := LogoutOptions{deps: deps}
2533
return opts.Run()
2634
},
2735
}
2836
return cmd
2937
}
3038

3139
func (opts *LogoutOptions) Run() error {
32-
svc := authsvc.NewService(config.DefaultManager())
40+
var cfgMgr *config.Manager
41+
if opts.deps != nil && opts.deps.ConfigMgr != nil {
42+
cfgMgr = opts.deps.ConfigMgr
43+
} else {
44+
cfgMgr = config.DefaultManager()
45+
}
46+
svc := authsvc.NewService(cfgMgr)
3347
// Fetch config
3448
cfg, err := svc.LoadConfig()
3549
if err != nil {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package tests
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/deepsourcelabs/cli/command/cmddeps"
8+
logoutCmd "github.com/deepsourcelabs/cli/command/auth/logout"
9+
"github.com/deepsourcelabs/cli/internal/testutil"
10+
)
11+
12+
func TestLogoutNotLoggedIn(t *testing.T) {
13+
// Empty token → user is not logged in
14+
cfgMgr := testutil.CreateExpiredTestConfigManager(t, "", "deepsource.com", "")
15+
16+
deps := &cmddeps.Deps{
17+
ConfigMgr: cfgMgr,
18+
}
19+
20+
cmd := logoutCmd.NewCmdLogoutWithDeps(deps)
21+
cmd.SetArgs([]string{})
22+
23+
err := cmd.Execute()
24+
if err == nil {
25+
t.Fatal("expected error when not logged in, got nil")
26+
}
27+
28+
if !strings.Contains(err.Error(), "not logged into DeepSource") {
29+
t.Errorf("expected error to contain %q, got %q", "not logged into DeepSource", err.Error())
30+
}
31+
}

command/cmddeps/deps.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ type Deps struct {
1616
ConfigMgr *config.Manager
1717
Stdout io.Writer
1818
RepoService *reposvc.Service
19+
BranchNameFunc func() (string, error)
1920
}

command/cmdutil/resolve_run.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cmdutil
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os/exec"
8+
"strings"
9+
10+
"github.com/deepsourcelabs/cli/deepsource"
11+
"github.com/deepsourcelabs/cli/internal/vcs"
12+
)
13+
14+
// ResolveLatestRun finds the latest analysis run for the current git branch.
15+
// Returns the commitOid of the latest run and the branch name.
16+
func ResolveLatestRun(
17+
ctx context.Context,
18+
client *deepsource.Client,
19+
remote *vcs.RemoteData,
20+
branchNameFunc func() (string, error),
21+
) (commitOid string, branchName string, err error) {
22+
if branchNameFunc != nil {
23+
branchName, err = branchNameFunc()
24+
} else {
25+
branchName, err = getCurrentBranch()
26+
}
27+
if err != nil {
28+
return "", "", fmt.Errorf("failed to detect current branch: %w", err)
29+
}
30+
31+
runs, err := client.GetAnalysisRuns(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, 30)
32+
if err != nil {
33+
return "", "", fmt.Errorf("failed to fetch analysis runs: %w", err)
34+
}
35+
36+
for _, run := range runs {
37+
if run.BranchName == branchName && run.Status == "SUCCESS" {
38+
return run.CommitOid, branchName, nil
39+
}
40+
}
41+
42+
return "", branchName, fmt.Errorf(
43+
"no analysis runs found for branch %q.\nTry: --default-branch, --commit <oid>, or push and analyze this branch first",
44+
branchName,
45+
)
46+
}
47+
48+
func getCurrentBranch() (string, error) {
49+
cmd := exec.Command("git", "--no-pager", "rev-parse", "--abbrev-ref", "HEAD")
50+
var stdout, stderr bytes.Buffer
51+
cmd.Stdout = &stdout
52+
cmd.Stderr = &stderr
53+
if err := cmd.Run(); err != nil {
54+
return "", err
55+
}
56+
return strings.TrimSuffix(stdout.String(), "\n"), nil
57+
}

command/flags_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package command
2+
3+
import (
4+
"testing"
5+
6+
analysisCmd "github.com/deepsourcelabs/cli/command/analysis"
7+
issuesCmd "github.com/deepsourcelabs/cli/command/issues"
8+
metricsCmd "github.com/deepsourcelabs/cli/command/metrics"
9+
vulnsCmd "github.com/deepsourcelabs/cli/command/vulnerabilities"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
type flagExpectation struct {
14+
name string
15+
defaultValue string
16+
}
17+
18+
func TestFlagDefaults(t *testing.T) {
19+
tests := []struct {
20+
name string
21+
buildCmd func() *cobra.Command
22+
expected []flagExpectation
23+
}{
24+
{
25+
name: "issues",
26+
buildCmd: issuesCmd.NewCmdIssues,
27+
expected: []flagExpectation{
28+
{name: "limit", defaultValue: "30"},
29+
{name: "output", defaultValue: "pretty"},
30+
{name: "verbose", defaultValue: "false"},
31+
{name: "pr", defaultValue: "0"},
32+
{name: "commit", defaultValue: ""},
33+
{name: "repo", defaultValue: ""},
34+
{name: "output-file", defaultValue: ""},
35+
{name: "severity", defaultValue: "[]"},
36+
{name: "category", defaultValue: "[]"},
37+
{name: "analyzer", defaultValue: "[]"},
38+
{name: "code", defaultValue: "[]"},
39+
{name: "path", defaultValue: "[]"},
40+
{name: "source", defaultValue: "[]"},
41+
{name: "default-branch", defaultValue: "false"},
42+
},
43+
},
44+
{
45+
name: "metrics",
46+
buildCmd: metricsCmd.NewCmdMetrics,
47+
expected: []flagExpectation{
48+
{name: "limit", defaultValue: "30"},
49+
{name: "output", defaultValue: "pretty"},
50+
{name: "verbose", defaultValue: "false"},
51+
{name: "pr", defaultValue: "0"},
52+
{name: "commit", defaultValue: ""},
53+
{name: "repo", defaultValue: ""},
54+
{name: "output-file", defaultValue: ""},
55+
},
56+
},
57+
{
58+
name: "vulnerabilities",
59+
buildCmd: vulnsCmd.NewCmdVulnerabilities,
60+
expected: []flagExpectation{
61+
{name: "limit", defaultValue: "100"},
62+
{name: "output", defaultValue: "pretty"},
63+
{name: "verbose", defaultValue: "false"},
64+
{name: "pr", defaultValue: "0"},
65+
{name: "commit", defaultValue: ""},
66+
{name: "repo", defaultValue: ""},
67+
{name: "output-file", defaultValue: ""},
68+
{name: "severity", defaultValue: "[]"},
69+
},
70+
},
71+
{
72+
name: "analysis",
73+
buildCmd: analysisCmd.NewCmdAnalysis,
74+
expected: []flagExpectation{
75+
{name: "limit", defaultValue: "20"},
76+
{name: "output", defaultValue: "pretty"},
77+
{name: "commit", defaultValue: ""},
78+
{name: "repo", defaultValue: ""},
79+
},
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
cmd := tt.buildCmd()
86+
87+
for _, exp := range tt.expected {
88+
flag := cmd.Flags().Lookup(exp.name)
89+
if flag == nil {
90+
t.Errorf("flag %q not found on command %q", exp.name, tt.name)
91+
continue
92+
}
93+
if flag.DefValue != exp.defaultValue {
94+
t.Errorf("flag %q on command %q: expected default %q, got %q",
95+
exp.name, tt.name, exp.defaultValue, flag.DefValue)
96+
}
97+
}
98+
})
99+
}
100+
}

0 commit comments

Comments
 (0)