Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ Help:
Use 'deepsource <command> --help/-h' for more information about the command.
```

### Listing Issues by Commit

You can list issues found in the analysis run for a specific commit using the `--commit` flag (resolves [#261](https://github.com/DeepSourceCorp/cli/issues/261)):

```sh
# List issues for a specific commit SHA
deepsource issues list --commit abc123def456

# Combine with other flags
deepsource issues list --commit abc123 --analyzer python --limit 50
deepsource issues list --commit abc123 --json --output-file results.json
```

This is useful for checking issues on PR branches and non-default branches where DeepSource has already run analysis.

## Documentation

For complete documentation, refer to the [CLI Documentation](https://docs.deepsource.com/docs/cli)
Expand Down
48 changes: 39 additions & 9 deletions command/issues/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"os"
"regexp"

"github.com/MakeNowJust/heredoc"
"github.com/deepsourcelabs/cli/config"
Expand All @@ -19,9 +20,12 @@ import (

const MAX_ISSUE_LIMIT = 100

var commitSHAPattern = regexp.MustCompile(`^[0-9a-fA-F]{6,40}$`)

type IssuesListOptions struct {
FileArg []string
RepoArg string
CommitArg string
AnalyzerArg []string
LimitArg int
OutputFilenameArg string
Expand Down Expand Up @@ -66,7 +70,10 @@ func NewCmdIssuesList() *cobra.Command {

To export listed issues to a SARIF file, use the %[14]s flag:
%[15]s
`, utils.Cyan("deepsource issues list"), utils.Yellow("--repo"), utils.Cyan("deepsource issues list --repo repo_name"), utils.Yellow("--analyzer"), utils.Cyan("deepsource issues list --analyzer python"), utils.Yellow("--limit"), utils.Cyan("deepsource issues list --limit 100"), utils.Yellow("--output-file"), utils.Cyan("deepsource issues list --output-file file_name"), utils.Yellow("--json"), utils.Cyan("deepsource issues list --json --output-file example.json"), utils.Yellow("--csv"), utils.Cyan("deepsource issues list --csv --output-file example.csv"), utils.Yellow("--sarif"), utils.Cyan("deepsource issues list --sarif --output-file example.sarif"))

To list issues for a specific commit (e.g. on a PR branch), use the %[16]s flag:
%[17]s
`, utils.Cyan("deepsource issues list"), utils.Yellow("--repo"), utils.Cyan("deepsource issues list --repo repo_name"), utils.Yellow("--analyzer"), utils.Cyan("deepsource issues list --analyzer python"), utils.Yellow("--limit"), utils.Cyan("deepsource issues list --limit 100"), utils.Yellow("--output-file"), utils.Cyan("deepsource issues list --output-file file_name"), utils.Yellow("--json"), utils.Cyan("deepsource issues list --json --output-file example.json"), utils.Yellow("--csv"), utils.Cyan("deepsource issues list --csv --output-file example.csv"), utils.Yellow("--sarif"), utils.Cyan("deepsource issues list --sarif --output-file example.sarif"), utils.Yellow("--commit"), utils.Cyan("deepsource issues list --commit abc123def456"))

cmd := &cobra.Command{
Use: "list",
Expand Down Expand Up @@ -99,6 +106,12 @@ func NewCmdIssuesList() *cobra.Command {
// --sarif flag
cmd.Flags().BoolVar(&opts.SARIFArg, "sarif", false, "Output reported issues in SARIF format")

// --commit flag
cmd.Flags().StringVar(&opts.CommitArg, "commit", "", "List issues from the analysis run for a specific commit SHA")

// --commit and --repo are mutually exclusive
cmd.MarkFlagsMutuallyExclusive("commit", "repo")

return cmd
}

Expand All @@ -121,10 +134,19 @@ func (opts *IssuesListOptions) Run() (err error) {
return fmt.Errorf("The maximum allowed limit to fetch issues is 100. Found %d", opts.LimitArg)
}

// Get the remote repository URL for which issues have to be listed
opts.SelectedRemote, err = utils.ResolveRemote(opts.RepoArg)
if err != nil {
return err
// Validate --commit flag
if opts.CommitArg != "" {
if !commitSHAPattern.MatchString(opts.CommitArg) {
return fmt.Errorf("invalid commit SHA: %q (expected 6-40 hex characters)", opts.CommitArg)
}
}

// Get the remote repository URL (not needed when querying by commit)
if opts.CommitArg == "" {
opts.SelectedRemote, err = utils.ResolveRemote(opts.RepoArg)
if err != nil {
return err
}
}

// Fetch the list of issues using SDK (deepsource package) based on user input
Expand Down Expand Up @@ -159,10 +181,18 @@ func (opts *IssuesListOptions) getIssuesData(ctx context.Context) (err error) {
return err
}

// Fetch list of issues for the whole project
opts.issuesData, err = deepsource.GetIssues(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider, opts.LimitArg)
if err != nil {
return err
// If --commit is specified, fetch issues from the analysis run for that commit
if opts.CommitArg != "" {
opts.issuesData, err = deepsource.GetIssuesByCommit(ctx, opts.CommitArg, opts.LimitArg)
if err != nil {
return err
}
} else {
// Fetch list of issues for the whole project (default branch)
opts.issuesData, err = deepsource.GetIssues(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider, opts.LimitArg)
if err != nil {
return err
}
}

var filteredIssues []issues.Issue
Expand Down
18 changes: 18 additions & 0 deletions deepsource/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,24 @@ func (c Client) GetIssues(ctx context.Context, owner, repoName, provider string,
return res, nil
}

// Returns the list of issues found in an analysis run for a specific commit.
// commitOID : The commit SHA to look up (full or abbreviated, 6-40 hex characters)
// limit : The amount of issues to be listed. The default limit is 30 while the maximum limit is currently 100.
func (c Client) GetIssuesByCommit(ctx context.Context, commitOID string, limit int) ([]issues.Issue, error) {
req := issuesQuery.RunIssuesListRequest{
Params: issuesQuery.RunIssuesListParams{
CommitOID: commitOID,
Limit: limit,
},
}
res, err := req.Do(ctx, c)
if err != nil {
return nil, err
}

return res, nil
}

// Returns the list of issues reported for a certain file in a certain repository whose data is sent as parameters.
// Owner : The username of the owner of the repository
// repoName : The name of the repository whose activation status has to be queried
Expand Down
152 changes: 152 additions & 0 deletions deepsource/issues/queries/list_run_issues.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Lists the issues found in an analysis run for a specific commit
package issues

import (
"context"
"fmt"

"github.com/deepsourcelabs/cli/deepsource/issues"
"github.com/deepsourcelabs/graphql"
)

const fetchRunIssuesQuery = `query GetRunIssues(
$commitOid: String!
$limit: Int!
) {
run(commitOid: $commitOid) {
status
branchName
checks {
edges {
node {
analyzer {
name
shortcode
}
status
occurrences(first: $limit) {
edges {
node {
path
beginLine
endLine
issue {
title
shortcode
category
severity
analyzer {
name
shortcode
}
}
}
}
}
}
}
}
}
}`

type RunIssuesListParams struct {
CommitOID string
Limit int
}

type RunIssuesListRequest struct {
Params RunIssuesListParams
}

type RunIssuesListResponse struct {
Run struct {
Status string `json:"status"`
BranchName string `json:"branchName"`
Checks struct {
Edges []struct {
Node struct {
Analyzer struct {
Name string `json:"name"`
Shortcode string `json:"shortcode"`
} `json:"analyzer"`
Status string `json:"status"`
Occurrences struct {
Edges []struct {
Node struct {
Path string `json:"path"`
BeginLine int `json:"beginLine"`
EndLine int `json:"endLine"`
Issue struct {
Title string `json:"title"`
Shortcode string `json:"shortcode"`
Category string `json:"category"`
Severity string `json:"severity"`
Analyzer struct {
Name string `json:"name"`
Shortcode string `json:"shortcode"`
} `json:"analyzer"`
} `json:"issue"`
} `json:"node"`
} `json:"edges"`
} `json:"occurrences"`
} `json:"node"`
} `json:"edges"`
} `json:"checks"`
} `json:"run"`
}

func (r RunIssuesListRequest) Do(ctx context.Context, client IGQLClient) ([]issues.Issue, error) {
req := graphql.NewRequest(fetchRunIssuesQuery)
req.Var("commitOid", r.Params.CommitOID)
req.Var("limit", r.Params.Limit)

// set header fields
req.Header.Set("Cache-Control", "no-cache")
tokenHeader := fmt.Sprintf("Bearer %s", client.GetToken())
req.Header.Add("Authorization", tokenHeader)

// run it and capture the response
var respData RunIssuesListResponse
if err := client.GQL().Run(ctx, req, &respData); err != nil {
return nil, err
}

if respData.Run.Status == "" {
return nil, fmt.Errorf("no analysis run found for commit %s", r.Params.CommitOID)
}

issuesData := []issues.Issue{}
for _, checkEdge := range respData.Run.Checks.Edges {
if len(checkEdge.Node.Occurrences.Edges) == 0 {
continue
}

for _, occurrenceEdge := range checkEdge.Node.Occurrences.Edges {
issueData := issues.Issue{
IssueText: occurrenceEdge.Node.Issue.Title,
IssueCode: occurrenceEdge.Node.Issue.Shortcode,
IssueCategory: occurrenceEdge.Node.Issue.Category,
IssueSeverity: occurrenceEdge.Node.Issue.Severity,
Location: issues.Location{
Path: occurrenceEdge.Node.Path,
Position: issues.Position{
BeginLine: occurrenceEdge.Node.BeginLine,
EndLine: occurrenceEdge.Node.EndLine,
},
},
Analyzer: issues.AnalyzerMeta{
Shortcode: occurrenceEdge.Node.Issue.Analyzer.Shortcode,
},
}
issuesData = append(issuesData, issueData)
}
}

// The limit is applied per-analyzer check in the GraphQL query,
// so cap the total to the requested limit.
if r.Params.Limit > 0 && len(issuesData) > r.Params.Limit {
issuesData = issuesData[:r.Params.Limit]
}

return issuesData, nil
}