Skip to content

Commit 841bc68

Browse files
Add auto-pagination, merge analyzers into repo status, add shell completions
- Replace server-side limit params with cursor-based auto-pagination for issues and vulnerabilities queries (new deepsource/pagination package) - --limit flag is now a client-side display cap (0 = fetch all) - Remove repo analyzers subcommand, fold analyzer listing into repo status - Add --install-completions flag to root command (bash, zsh, fish) - Update root help text and add test coverage for pagination
1 parent 6331623 commit 841bc68

41 files changed

Lines changed: 1324 additions & 570 deletions

Some content is hidden

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

command/cmdutil/resolve_run.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,20 @@ func resolveWithPR(
276276
result.Empty = true
277277
return result, nil
278278
}
279+
if finalStatus == "FALLBACK" {
280+
completedRun, fallbackErr := ResolveLatestCompletedRun(ctx, client, branchName, remote)
281+
if fallbackErr != nil {
282+
return nil, fallbackErr
283+
}
284+
if completedRun == nil {
285+
style.Infof(w, "No completed analysis runs found for branch %q.", branchName)
286+
result.Empty = true
287+
return result, nil
288+
}
289+
style.Infof(w, "Analysis is running on commit %s. Showing results from the last analyzed commit (%s).", run.CommitOid[:8], completedRun.CommitOid[:8])
290+
result.CommitOid = completedRun.CommitOid
291+
return result, nil
292+
}
279293
}
280294
if runErr == nil {
281295
result.CommitOid = run.CommitOid

command/cmdutil/resolve_run_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmdutil
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"errors"
78
"path/filepath"
89
"runtime"
@@ -420,6 +421,81 @@ func TestResolveLatestCompletedRun_AllRunning(t *testing.T) {
420421
}
421422
}
422423

424+
// TestResolveWithPR_PendingFallback verifies that resolveWithPR falls back to
425+
// the last completed run when the latest run is PENDING and WaitOrFallback
426+
// returns FALLBACK (non-TTY environment).
427+
func TestResolveWithPR_PendingFallback(t *testing.T) {
428+
pendingData := testutil.LoadGoldenFile(t, testdataPath("get_analysis_runs_pending_response.json"))
429+
mixedData := testutil.LoadGoldenFile(t, testdataPath("get_analysis_runs_mixed_status_response.json"))
430+
431+
callCount := 0
432+
mock := graphqlclient.NewMockClient()
433+
mock.QueryFunc = func(_ context.Context, query string, _ map[string]any, result any) error {
434+
if !strings.Contains(query, "query GetAnalysisRuns(") {
435+
t.Fatalf("unexpected query: %s", query)
436+
}
437+
callCount++
438+
if callCount == 1 {
439+
// First call: ResolveLatestRunForBranch → PENDING run
440+
return json.Unmarshal(pendingData, result)
441+
}
442+
// Second call: ResolveLatestCompletedRun → mixed (RUNNING + SUCCESS)
443+
return json.Unmarshal(mixedData, result)
444+
}
445+
client := deepsource.NewWithGraphQLClient(mock)
446+
447+
var buf bytes.Buffer
448+
result := &AutoBranchResult{BranchName: "feature/new-auth", PRNumber: 42}
449+
450+
got, err := resolveWithPR(context.Background(), &buf, client, "feature/new-auth", testRemote, result)
451+
if err != nil {
452+
t.Fatalf("unexpected error: %v", err)
453+
}
454+
if got.Empty {
455+
t.Fatal("expected non-empty result, got Empty=true")
456+
}
457+
// Should fall back to the completed run's commit OID
458+
const expectedOid = "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1"
459+
if got.CommitOid != expectedOid {
460+
t.Errorf("expected CommitOid=%q (completed run), got %q", expectedOid, got.CommitOid)
461+
}
462+
if !strings.Contains(buf.String(), "Showing results from the last analyzed commit") {
463+
t.Errorf("expected fallback info message, got: %s", buf.String())
464+
}
465+
}
466+
467+
// TestResolveWithPR_PendingNoCompletedRuns verifies that resolveWithPR returns
468+
// Empty=true when the latest run is PENDING and no completed runs exist.
469+
func TestResolveWithPR_PendingNoCompletedRuns(t *testing.T) {
470+
pendingData := testutil.LoadGoldenFile(t, testdataPath("get_analysis_runs_pending_response.json"))
471+
allRunningData := testutil.LoadGoldenFile(t, testdataPath("get_analysis_runs_all_running_response.json"))
472+
473+
callCount := 0
474+
mock := graphqlclient.NewMockClient()
475+
mock.QueryFunc = func(_ context.Context, query string, _ map[string]any, result any) error {
476+
callCount++
477+
if callCount == 1 {
478+
return json.Unmarshal(pendingData, result)
479+
}
480+
return json.Unmarshal(allRunningData, result)
481+
}
482+
client := deepsource.NewWithGraphQLClient(mock)
483+
484+
var buf bytes.Buffer
485+
result := &AutoBranchResult{BranchName: "feature/new-auth", PRNumber: 42}
486+
487+
got, err := resolveWithPR(context.Background(), &buf, client, "feature/new-auth", testRemote, result)
488+
if err != nil {
489+
t.Fatalf("unexpected error: %v", err)
490+
}
491+
if !got.Empty {
492+
t.Fatal("expected Empty=true when no completed runs exist")
493+
}
494+
if !strings.Contains(buf.String(), "No completed analysis runs found") {
495+
t.Errorf("expected 'no completed runs' message, got: %s", buf.String())
496+
}
497+
}
498+
423499
// TestResolveLatestCompletedRun_NoRuns verifies nil is returned when the
424500
// branch has no runs at all.
425501
func TestResolveLatestCompletedRun_NoRuns(t *testing.T) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"repository": {
3+
"analysisRuns": {
4+
"edges": [
5+
{
6+
"node": {
7+
"runUid": "run-uid-pending-1",
8+
"commitOid": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2",
9+
"branchName": "feature/new-auth",
10+
"status": "PENDING",
11+
"createdAt": "2025-03-13T11:00:00Z",
12+
"finishedAt": null,
13+
"updatedAt": "2025-03-13T11:00:00Z",
14+
"summary": {
15+
"occurrencesIntroduced": 0,
16+
"occurrencesResolved": 0,
17+
"occurrencesSuppressed": 0
18+
},
19+
"reportCard": null
20+
}
21+
}
22+
],
23+
"pageInfo": {
24+
"hasNextPage": false,
25+
"endCursor": null
26+
}
27+
}
28+
}
29+
}

command/flags_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestFlagDefaults(t *testing.T) {
2525
name: "issues",
2626
buildCmd: issuesCmd.NewCmdIssues,
2727
expected: []flagExpectation{
28-
{name: "limit", defaultValue: "30"},
28+
{name: "limit", defaultValue: "0"},
2929
{name: "output", defaultValue: "pretty"},
3030
{name: "verbose", defaultValue: "false"},
3131
{name: "pr", defaultValue: "0"},
@@ -43,7 +43,7 @@ func TestFlagDefaults(t *testing.T) {
4343
name: "metrics",
4444
buildCmd: metricsCmd.NewCmdMetrics,
4545
expected: []flagExpectation{
46-
{name: "limit", defaultValue: "30"},
46+
{name: "limit", defaultValue: "0"},
4747
{name: "output", defaultValue: "pretty"},
4848
{name: "verbose", defaultValue: "false"},
4949
{name: "pr", defaultValue: "0"},
@@ -55,7 +55,7 @@ func TestFlagDefaults(t *testing.T) {
5555
name: "vulnerabilities",
5656
buildCmd: vulnsCmd.NewCmdVulnerabilities,
5757
expected: []flagExpectation{
58-
{name: "limit", defaultValue: "100"},
58+
{name: "limit", defaultValue: "0"},
5959
{name: "output", defaultValue: "pretty"},
6060
{name: "verbose", defaultValue: "false"},
6161
{name: "pr", defaultValue: "0"},

command/issues/issues.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ func NewCmdIssues() *cobra.Command {
5959

6060
func NewCmdIssuesWithDeps(deps *cmddeps.Deps) *cobra.Command {
6161
opts := IssuesOptions{
62-
LimitArg: 30,
6362
OutputFormat: "pretty",
6463
deps: deps,
6564
}
@@ -98,7 +97,7 @@ func NewCmdIssuesWithDeps(deps *cmddeps.Deps) *cobra.Command {
9897
cmd.Flags().StringVarP(&opts.RepoArg, "repo", "r", "", "Repository in provider/owner/name format (e.g. gh/owner/name). Supported providers: gh, gl, bb, ads")
9998

10099
// --limit, -l flag
101-
cmd.Flags().IntVarP(&opts.LimitArg, "limit", "l", 30, "Maximum number of issues to fetch")
100+
cmd.Flags().IntVarP(&opts.LimitArg, "limit", "l", 0, "Maximum number of issues to display (0 = all)")
102101

103102
// --output, -o flag
104103
cmd.Flags().StringVarP(&opts.OutputFormat, "output", "o", "pretty", "Output format: pretty, json")
@@ -262,6 +261,12 @@ func (opts *IssuesOptions) Run(ctx context.Context) error {
262261
}
263262

264263
issuesList = opts.filterIssues(issuesList)
264+
265+
// Apply limit cap if set
266+
if opts.LimitArg > 0 && len(issuesList) > opts.LimitArg {
267+
issuesList = issuesList[:opts.LimitArg]
268+
}
269+
265270
opts.issues = issuesList
266271

267272
switch opts.OutputFormat {
@@ -286,11 +291,11 @@ func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource
286291
var err error
287292
switch {
288293
case opts.CommitOid != "":
289-
issuesList, err = client.GetRunIssuesFlat(ctx, opts.CommitOid, opts.LimitArg, serverFilters)
294+
issuesList, err = client.GetRunIssuesFlat(ctx, opts.CommitOid, serverFilters)
290295
case opts.PRNumber > 0:
291-
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, opts.PRNumber, opts.LimitArg)
296+
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, opts.PRNumber)
292297
case opts.DefaultBranch:
293-
issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, opts.LimitArg)
298+
issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider)
294299
default:
295300
var branchNameFunc func() (string, error)
296301
if opts.deps != nil {
@@ -308,12 +313,12 @@ func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource
308313
case ab.PRNumber > 0:
309314
opts.PRNumber = ab.PRNumber
310315
opts.CommitOid = ab.CommitOid
311-
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber, opts.LimitArg)
316+
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber)
312317
case ab.UseRepo:
313-
issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, opts.LimitArg)
318+
issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider)
314319
default:
315320
opts.CommitOid = ab.CommitOid
316-
issuesList, err = client.GetRunIssuesFlat(ctx, ab.CommitOid, opts.LimitArg, serverFilters)
321+
issuesList, err = client.GetRunIssuesFlat(ctx, ab.CommitOid, serverFilters)
317322
}
318323
}
319324
return issuesList, err
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"repository": {
3+
"issues": {
4+
"edges": [
5+
{
6+
"node": {
7+
"occurrences": {
8+
"edges": [
9+
{
10+
"node": {
11+
"path": "cmd/deepsource/main.go",
12+
"beginLine": 42,
13+
"endLine": 42,
14+
"issue": {
15+
"title": "Unchecked error return value of os.ReadFile",
16+
"shortcode": "GO-W1007",
17+
"shortDescription": "Return value of io.ReadAll is not checked for errors",
18+
"category": "BUG_RISK",
19+
"severity": "MAJOR",
20+
"isRecommended": false,
21+
"analyzer": {
22+
"name": "Go",
23+
"shortcode": "go"
24+
}
25+
}
26+
}
27+
}
28+
]
29+
}
30+
}
31+
}
32+
],
33+
"pageInfo": {
34+
"hasNextPage": true,
35+
"endCursor": "cursor-page1"
36+
}
37+
}
38+
}
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"repository": {
3+
"issues": {
4+
"edges": [
5+
{
6+
"node": {
7+
"occurrences": {
8+
"edges": [
9+
{
10+
"node": {
11+
"path": "internal/vcs/remotes.go",
12+
"beginLine": 87,
13+
"endLine": 91,
14+
"issue": {
15+
"title": "HTTP request built with user-controlled URL",
16+
"shortcode": "GO-S1010",
17+
"shortDescription": "Constructing HTTP request with user-controlled URL allows SSRF",
18+
"category": "SECURITY",
19+
"severity": "MAJOR",
20+
"isRecommended": false,
21+
"analyzer": {
22+
"name": "Go",
23+
"shortcode": "go"
24+
}
25+
}
26+
}
27+
}
28+
]
29+
}
30+
}
31+
}
32+
],
33+
"pageInfo": {
34+
"hasNextPage": false,
35+
"endCursor": null
36+
}
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)