├── .github
├── CODEOWNERS
└── workflows
│ ├── release.yml
│ └── CI.yml
├── command
├── config
│ ├── generate
│ │ ├── constants.go
│ │ ├── transformers_input.go
│ │ ├── input.go
│ │ ├── types.go
│ │ ├── generic_input.go
│ │ ├── generate.go
│ │ └── analyzers_input.go
│ ├── config.go
│ └── validate
│ │ └── validate.go
├── report
│ ├── tests
│ │ ├── golden_files
│ │ │ ├── report_graphql_error_response_body.json
│ │ │ ├── report_success.txt
│ │ │ ├── report_graphql_success_response_body.json
│ │ │ └── report_grqphql_artifactmetadatainput_response_success.json
│ │ ├── init_test.go
│ │ └── report_workflow_test.go
│ ├── dsn.go
│ ├── types.go
│ ├── query.go
│ ├── dsn_test.go
│ ├── constants.go
│ └── git.go
├── issues
│ ├── list
│ │ ├── testdata
│ │ │ ├── dummy
│ │ │ │ ├── issues_docker.json
│ │ │ │ ├── issues_multiple_analyzers.json
│ │ │ │ ├── issues.json
│ │ │ │ ├── issues_deepsource.json
│ │ │ │ └── issues_data_multi.json
│ │ │ ├── csv
│ │ │ │ └── test.csv
│ │ │ ├── json
│ │ │ │ └── test.json
│ │ │ └── sarif
│ │ │ │ ├── test.sarif
│ │ │ │ └── test_multi.sarif
│ │ ├── types.go
│ │ ├── list_test.go
│ │ └── utils.go
│ └── issues.go
├── repo
│ ├── repo.go
│ ├── status
│ │ └── status.go
│ └── view
│ │ └── view.go
├── auth
│ ├── login
│ │ ├── pat_login_flow.go
│ │ ├── login_flow.go
│ │ └── login.go
│ ├── auth.go
│ ├── logout
│ │ └── logout.go
│ ├── status
│ │ └── status.go
│ └── refresh
│ │ └── refresh.go
├── version
│ ├── command_test.go
│ └── command.go
└── root.go
├── .gitmodules
├── deepsource
├── transformers
│ ├── transformers.go
│ └── queries
│ │ └── get_transformers.go
├── analyzers
│ ├── analyzers.go
│ └── queries
│ │ └── get_analyzers.go
├── tests
│ ├── testdata
│ │ └── analyzer
│ │ │ ├── error_response_body.json
│ │ │ └── request_body.txt
│ ├── init_test.go
│ └── get_analyzers_test.go
├── repository
│ ├── repository.go
│ └── queries
│ │ └── repository_status.go
├── auth
│ ├── token.go
│ ├── device.go
│ └── mutations
│ │ ├── register_device.go
│ │ ├── request_pat.go
│ │ └── refresh_pat.go
├── issues
│ ├── issues_list.go
│ └── queries
│ │ ├── list_issues.go
│ │ └── list_file_issues.go
└── client.go
├── .deepsource.toml
├── .gitignore
├── scripts
└── gen-completions.sh
├── utils
├── colors.go
├── cmd_validator.go
├── remote_resolver.go
├── fetch_remote_test.go
├── remote_resolver_test.go
├── prompt.go
├── fetch_analyzers_transformers.go
├── fetch_remote.go
└── fetch_oidc_token.go
├── cmd
└── deepsource
│ └── main.go
├── version
├── version.go
└── version_test.go
├── LICENSE
├── configvalidator
├── transformer_config_validator.go
├── types.go
├── transformer_config_validator_test.go
├── config_validator.go
├── analyzer_config_validator_test.go
├── generic_config_validator.go
├── analyzer_config_validator.go
├── generic_config_validator_test.go
└── config_validator_test.go
├── config
├── config_test.go
└── config.go
├── README.md
├── Makefile
├── go.mod
└── goreleaser.yaml
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * sourya@deepsource.io
2 |
--------------------------------------------------------------------------------
/command/config/generate/constants.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | const DEEPSOURCE_TOML_VERSION = 1
4 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "sysroot"]
2 | path = sysroot
3 | url = git@github.com:goreleaser/goreleaser-cross-example-sysroot.git
4 |
--------------------------------------------------------------------------------
/deepsource/transformers/transformers.go:
--------------------------------------------------------------------------------
1 | package transformers
2 |
3 | type Transformer struct {
4 | Name string // Name of the Transformer
5 | Shortcode string // Shortcode of the Transformer
6 | }
7 |
--------------------------------------------------------------------------------
/command/report/tests/golden_files/report_graphql_error_response_body.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "createArtifact": {
4 | "ok": false,
5 | "error": "Random error string"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/deepsource/analyzers/analyzers.go:
--------------------------------------------------------------------------------
1 | package analyzers
2 |
3 | type Analyzer struct {
4 | Name string // The name of the analyzer
5 | Shortcode string // The shortcode of analyzer
6 | MetaSchema string // Analyzer meta schema
7 | }
8 |
--------------------------------------------------------------------------------
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | [[analyzers]]
4 | name = "go"
5 |
6 | [analyzers.meta]
7 | import_path = "github.com/deepsourcelabs/cli"
8 |
9 | [[analyzers]]
10 | name = "secrets"
11 |
12 | [[analyzers]]
13 | name = "test-coverage"
--------------------------------------------------------------------------------
/command/issues/list/testdata/dummy/issues_docker.json:
--------------------------------------------------------------------------------
1 | [{"issue_title":"Use arguments JSON notation for CMD and ENTRYPOINT arguments","issue_code":"DOK-DL3025","location":{"path":"Dockerfile","position":{"begin":64,"end":64}},"Analyzer":{"analyzer":"docker"}}]
--------------------------------------------------------------------------------
/deepsource/tests/testdata/analyzer/error_response_body.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Something went wrong. We are investigating this. Kindly contact support@deepsource.io if the problem persists.",
3 | "documentation_url": "https://deepsource.io/docs/"
4 | }
5 |
--------------------------------------------------------------------------------
/deepsource/tests/testdata/analyzer/request_body.txt:
--------------------------------------------------------------------------------
1 | {
2 | analyzers {
3 | edges {
4 | node {
5 | name
6 | shortcode
7 | metaSchema
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/command/report/tests/golden_files/report_success.txt:
--------------------------------------------------------------------------------
1 | DeepSource | Artifact published successfully
2 |
3 | Analyzer test-coverage
4 | Key python
5 | Message Artifact successfully uploaded for repository demo-python on commit d3401b650b2b3f1c2b4008dfd934d00bf3590b78.
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Vendor files
2 | vendor
3 | .release-env
4 |
5 | # Executable
6 | cmd/deepsource/deepsource
7 |
8 | # Goreleaser artifacts
9 | dist
10 |
11 | # Test coverage reports
12 | coverage.out
13 | cover.out
14 |
15 | # Completion files
16 | completions
17 |
18 | .vscode
19 |
--------------------------------------------------------------------------------
/scripts/gen-completions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 | rm -rf completions
5 | mkdir completions
6 |
7 | # Generate completion using the in-built cobra completion command
8 | for shell in bash zsh fish; do
9 | go run cmd/deepsource/main.go completion "$shell" > "completions/deepsource.$shell"
10 | done
11 |
--------------------------------------------------------------------------------
/command/report/tests/golden_files/report_graphql_success_response_body.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "createArtifact": {
4 | "ok": true,
5 | "error": "",
6 | "message": "Artifact successfully uploaded for repository demo-python on commit d3401b650b2b3f1c2b4008dfd934d00bf3590b78."
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/command/report/tests/golden_files/report_grqphql_artifactmetadatainput_response_success.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "__type": {
4 | "inputFields": [
5 | {
6 | "name": "workDir"
7 | },
8 | {
9 | "name": "compressed"
10 | }
11 | ]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/deepsource/repository/repository.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | type Meta struct {
4 | Activated bool // Activation status of the repository. True: Activated. False: Not Activated.
5 | Name string // Name of the repository
6 | Owner string // Owner username of the repository
7 | Provider string // VCS host for the repo. Github/Gitlab/BitBucket supported yet.
8 | }
9 |
--------------------------------------------------------------------------------
/utils/colors.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "github.com/fatih/color"
4 |
5 | func Yellow(format string, a ...interface{}) string {
6 | c := color.New(color.FgYellow, color.Bold)
7 | return c.Sprintf(format, a...)
8 | }
9 |
10 | func Cyan(format string, a ...interface{}) string {
11 | c := color.New(color.FgCyan, color.Bold)
12 | return c.Sprintf(format, a...)
13 | }
14 |
--------------------------------------------------------------------------------
/command/issues/list/testdata/csv/test.csv:
--------------------------------------------------------------------------------
1 | analyzer,issue_code,issue_title,occurence_title,issue_category,path,begin_line,begin_column,end_line,end_column
2 | go,RVV-B0013,Unused method receiver detected,Unused method receiver detected,,deepsource/transformers/queries/get_transformers.go,34,0,34,0
3 | go,RVV-B0013,Unused method receiver detected,Unused method receiver detected,,deepsource/transformers/queries/get_transformers.go,44,0,44,0
4 |
--------------------------------------------------------------------------------
/deepsource/auth/token.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | type PAT struct {
4 | Token string `json:"token"` // PAT received from the server
5 | Expiry string `json:"expiry"` // Token expiry timestamp
6 | User struct {
7 | FirstName string `json:"firstName"` // User's firstname
8 | LastName string `json:"lastName"` // User's lastname
9 | Email string `json:"email"` // User's email address
10 | } `json:"user"`
11 | }
12 |
--------------------------------------------------------------------------------
/command/issues/list/testdata/dummy/issues_multiple_analyzers.json:
--------------------------------------------------------------------------------
1 | [{"issue_title":"Use arguments JSON notation for CMD and ENTRYPOINT arguments","issue_code":"DOK-DL3025","location":{"path":"Dockerfile","position":{"begin":64,"end":64}},"Analyzer":{"analyzer":"docker"}},{"issue_title":"Imported name is not used anywhere in the module","issue_code":"PY-W2000","location":{"path":"python/demo.py","position":{"begin":1,"end":1}},"Analyzer":{"analyzer":"python"}}]
2 |
--------------------------------------------------------------------------------
/command/issues/list/testdata/dummy/issues.json:
--------------------------------------------------------------------------------
1 | [{"issue_title":"Unused method receiver detected","issue_code":"RVV-B0013","location":{"path":"deepsource/transformers/queries/get_transformers.go","position":{"begin":34,"end":34}},"Analyzer":{"analyzer":"go"}},{"issue_title":"Unused method receiver detected","issue_code":"RVV-B0013","location":{"path":"deepsource/transformers/queries/get_transformers.go","position":{"begin":44,"end":44}},"Analyzer":{"analyzer":"go"}}]
--------------------------------------------------------------------------------
/command/issues/list/testdata/dummy/issues_deepsource.json:
--------------------------------------------------------------------------------
1 | [{"issue_title":"Unused method receiver detected","issue_code":"RVV-B0013","location":{"path":"deepsource/transformers/queries/get_transformers.go","position":{"begin":34,"end":34}},"Analyzer":{"analyzer":"go"}},{"issue_title":"Unused method receiver detected","issue_code":"RVV-B0013","location":{"path":"deepsource/transformers/queries/get_transformers.go","position":{"begin":44,"end":44}},"Analyzer":{"analyzer":"go"}}]
--------------------------------------------------------------------------------
/command/issues/issues.go:
--------------------------------------------------------------------------------
1 | package issues
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/deepsourcelabs/cli/command/issues/list"
7 | )
8 |
9 | // Options holds the metadata.
10 | type Options struct{}
11 |
12 | // NewCmdVersion returns the current version of cli being used
13 | func NewCmdIssues() *cobra.Command {
14 | cmd := &cobra.Command{
15 | Use: "issues",
16 | Short: "Show the list of issues in a file in a repository",
17 | }
18 | cmd.AddCommand(list.NewCmdIssuesList())
19 | return cmd
20 | }
21 |
--------------------------------------------------------------------------------
/command/repo/repo.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/deepsourcelabs/cli/command/repo/status"
7 | "github.com/deepsourcelabs/cli/command/repo/view"
8 | )
9 |
10 | // Options holds the metadata.
11 | type Options struct{}
12 |
13 | // NewCmdVersion returns the current version of cli being used
14 | func NewCmdRepo() *cobra.Command {
15 | cmd := &cobra.Command{
16 | Use: "repo",
17 | Short: "Operations related to the project repository",
18 | }
19 | cmd.AddCommand(status.NewCmdRepoStatus())
20 | cmd.AddCommand(view.NewCmdRepoView())
21 | return cmd
22 | }
23 |
--------------------------------------------------------------------------------
/command/auth/login/pat_login_flow.go:
--------------------------------------------------------------------------------
1 | package login
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/deepsourcelabs/cli/config"
7 | )
8 |
9 | // Starts the login flow for the CLI (using PAT)
10 | func (opts *LoginOptions) startPATLoginFlow(cfg *config.CLIConfig, token string) error {
11 | // set personal access token (PAT)
12 | cfg.Token = token
13 |
14 | // Having stored the data in the global Cfg object, write it into the config file present in the local filesystem
15 | err := cfg.WriteFile()
16 | if err != nil {
17 | return fmt.Errorf("Error in writing authentication data to a file. Exiting...")
18 | }
19 | return nil
20 | }
21 |
--------------------------------------------------------------------------------
/command/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/deepsourcelabs/cli/command/config/generate"
5 | "github.com/deepsourcelabs/cli/command/config/validate"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // Options holds the metadata.
10 | type Options struct{}
11 |
12 | // NewCmdVersion returns the current version of cli being used
13 | func NewCmdConfig() *cobra.Command {
14 | cmd := &cobra.Command{
15 | Use: "config ",
16 | Short: "Generate and Validate DeepSource config",
17 | }
18 | cmd.AddCommand(generate.NewCmdConfigGenerate())
19 | cmd.AddCommand(validate.NewCmdValidate())
20 |
21 | return cmd
22 | }
23 |
--------------------------------------------------------------------------------
/command/report/dsn.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | import (
4 | "errors"
5 | "regexp"
6 | )
7 |
8 | var ErrInvalidDSN = errors.New("DeepSource | Error | Invalid DSN. Cross verify DEEPSOURCE_DSN value against the settings page of the repository")
9 |
10 | type DSN struct {
11 | Protocol string
12 | Host string
13 | Token string
14 | }
15 |
16 | func NewDSN(raw string) (*DSN, error) {
17 | dsnPattern := regexp.MustCompile(`^(https?)://([^:@]+)@([^:/]+(?:\:\d+)?)`)
18 | matches := dsnPattern.FindStringSubmatch(raw)
19 | if len(matches) != 4 {
20 | return nil, ErrInvalidDSN
21 | }
22 | return &DSN{
23 | Protocol: matches[1],
24 | Token: matches[2],
25 | Host: matches[3],
26 | }, nil
27 | }
28 |
--------------------------------------------------------------------------------
/command/report/tests/init_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 | "testing"
9 | )
10 |
11 | var srv *http.Server
12 |
13 | func TestMain(m *testing.M) {
14 | log.SetFlags(log.LstdFlags | log.Lshortfile)
15 | srv := graphQLMockAPIServer()
16 | code := m.Run()
17 | srv.Close()
18 | os.Exit(code)
19 | }
20 |
21 | func graphQLMockAPIServer() *http.Server {
22 | srv = &http.Server{Addr: ":8081"}
23 |
24 | http.HandleFunc("/", graphQLAPIMock)
25 | go func() {
26 | err := srv.ListenAndServe()
27 | if err != nil && err != http.ErrServerClosed {
28 | panic(fmt.Sprintf("failed to start HTTP mock server with error=%s", err))
29 | }
30 | }()
31 |
32 | return srv
33 | }
34 |
--------------------------------------------------------------------------------
/deepsource/tests/init_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 | "testing"
9 | )
10 |
11 | var srv *http.Server
12 |
13 | func TestMain(m *testing.M) {
14 | log.SetFlags(log.LstdFlags | log.Lshortfile)
15 | startMockAPIServer()
16 |
17 | code := m.Run()
18 |
19 | srv.Close()
20 | os.Exit(code)
21 | }
22 |
23 | func startMockAPIServer() {
24 | // Start GraphQL server for test
25 | srv = &http.Server{
26 | Addr: ":8081",
27 | }
28 |
29 | http.HandleFunc("/analyzer", mockAnalyzer)
30 |
31 | go func() {
32 | err := srv.ListenAndServe()
33 | if err != nil && err != http.ErrServerClosed {
34 | panic(fmt.Sprintf("failed to start HTTP mock server with error=%s", err))
35 | }
36 | }()
37 | }
38 |
--------------------------------------------------------------------------------
/command/config/generate/transformers_input.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import "github.com/deepsourcelabs/cli/utils"
4 |
5 | // ==========
6 | // Transformers Input Prompt
7 | // ==========
8 | func (o *Options) collectTransformersInput() (err error) {
9 | transformerPromptMsg := "Would you like to activate any Transformers for any languages?"
10 | transformerPromptHelpText := "DeepSource Transformers automatically help to achieve auto-formatting of code. Add a transformer by selecting the code formatting tool of your choice."
11 |
12 | o.ActivatedTransformers, err = utils.SelectFromMultipleOptions(transformerPromptMsg, transformerPromptHelpText, utils.TransformersData.TransformerNames)
13 | if err != nil {
14 | return err
15 | }
16 |
17 | return nil
18 | }
19 |
--------------------------------------------------------------------------------
/deepsource/auth/device.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | type Device struct {
4 | Code string `json:"deviceCode"` // Device Code
5 | UserCode string `json:"userCode"` // 8 figure User code to be used while authentication
6 | VerificationURI string `json:"verificationUri"` // URL to verify user code
7 | VerificationURIComplete string `json:"verificationUriComplete"` // URL to verify user code with the user code being sent as a URL param
8 | ExpiresIn int `json:"expiresIn"` // Time in which the device code expires
9 | Interval int `json:"interval"` // Interval in which the client needs to poll at the endpoint to receive the PAT
10 | }
11 |
--------------------------------------------------------------------------------
/command/auth/auth.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/deepsourcelabs/cli/command/auth/login"
7 | "github.com/deepsourcelabs/cli/command/auth/logout"
8 | "github.com/deepsourcelabs/cli/command/auth/refresh"
9 | "github.com/deepsourcelabs/cli/command/auth/status"
10 | )
11 |
12 | // Options holds the metadata.
13 | type Options struct{}
14 |
15 | // NewCmdAuth handles the auth command which has various sub-commands like `login`, `logout`, `refresh` and `status`
16 | func NewCmdAuth() *cobra.Command {
17 | cmd := &cobra.Command{
18 | Use: "auth",
19 | Short: "Authenticate with DeepSource",
20 | }
21 | cmd.AddCommand(login.NewCmdLogin())
22 | cmd.AddCommand(logout.NewCmdLogout())
23 | cmd.AddCommand(refresh.NewCmdRefresh())
24 | cmd.AddCommand(status.NewCmdStatus())
25 | return cmd
26 | }
27 |
--------------------------------------------------------------------------------
/command/issues/list/testdata/dummy/issues_data_multi.json:
--------------------------------------------------------------------------------
1 | [{"issue_title":"Unused method receiver detected","issue_code":"RVV-B0013","location":{"path":"deepsource/transformers/queries/get_transformers.go","position":{"begin":34,"end":34}},"Analyzer":{"analyzer":"go"}},{"issue_title":"Unused method receiver detected","issue_code":"RVV-B0013","location":{"path":"deepsource/transformers/queries/get_transformers.go","position":{"begin":44,"end":44}},"Analyzer":{"analyzer":"go"}},{"issue_title":"Use arguments JSON notation for CMD and ENTRYPOINT arguments","issue_code":"DOK-DL3025","location":{"path":"Dockerfile","position":{"begin":64,"end":64}},"Analyzer":{"analyzer":"docker"}},{"issue_title":"Imported name is not used anywhere in the module","issue_code":"PY-W2000","location":{"path":"python/demo.py","position":{"begin":1,"end":1}},"Analyzer":{"analyzer":"python"}}]
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | release-cli:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 | submodules: 'true'
20 |
21 | - name: Set up Go
22 | uses: actions/setup-go@v4
23 | with:
24 | go-version: 1.21
25 |
26 | - name: Setup environment variables
27 | run: |-
28 | echo 'GITHUB_TOKEN=${{secrets.GITHUB_TOKEN}}' >> .release-env
29 | echo 'HOMEBREW_TOKEN=${{secrets.DS_BOT_PAT}}' >> .release-env
30 | echo 'DEEPSOURCE_CLI_SENTRY_DSN=${{secrets.SENTRY_DSN}}' >> .release-env
31 |
32 | - name: Publish Release
33 | run: make release
34 |
--------------------------------------------------------------------------------
/command/version/command_test.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/deepsourcelabs/cli/version"
8 | )
9 |
10 | func TestOptions_Run(t *testing.T) {
11 | date, _ := time.Parse("2006-01-02", "2021-01-21")
12 |
13 | getBuildInfo = func() *version.BuildInfo {
14 | return &version.BuildInfo{
15 | Version: "1.5.0",
16 | Date: date,
17 | }
18 | }
19 |
20 | tests := []struct {
21 | name string
22 | o Options
23 | want string
24 | }{
25 | {
26 | name: "must return the string output for command",
27 | o: Options{},
28 | want: "DeepSource CLI version 1.5.0 (2021-01-21)",
29 | },
30 | }
31 | for _, tt := range tests {
32 | t.Run(tt.name, func(t *testing.T) {
33 | o := Options{}
34 | if got := o.Run(); got != tt.want {
35 | t.Errorf("Options.Run() = %v, want %v", got, tt.want)
36 | }
37 | })
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/command/issues/list/types.go:
--------------------------------------------------------------------------------
1 | package list
2 |
3 | // custom types for JSON marshaling
4 |
5 | type IssueJSON struct {
6 | Analyzer string `json:"analyzer"`
7 | IssueCode string `json:"issue_code"`
8 | IssueTitle string `json:"issue_title"`
9 | OccurenceTitle string `json:"occurence_title"`
10 | IssueCategory string `json:"issue_category"`
11 | Location LocationJSON `json:"location"`
12 | }
13 |
14 | type LocationJSON struct {
15 | Path string `json:"path"` // The filepath where the issue is reported
16 | Position PositionJSON `json:"position"` // The position info where the issue is raised
17 | }
18 |
19 | type PositionJSON struct {
20 | Begin LineColumn `json:"begin"` // The line where the code covered under the issue starts
21 | End LineColumn `json:"end"` // The line where the code covered under the issue starts
22 | }
23 |
24 | type LineColumn struct {
25 | Line int `json:"line"`
26 | Column int `json:"column"`
27 | }
28 |
--------------------------------------------------------------------------------
/cmd/deepsource/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "time"
7 |
8 | "github.com/deepsourcelabs/cli/command"
9 | v "github.com/deepsourcelabs/cli/version"
10 | "github.com/getsentry/sentry-go"
11 | "github.com/pterm/pterm"
12 | )
13 |
14 | var (
15 | // Version is the build version. This is set using ldflags -X
16 | version = "development"
17 |
18 | // Date is the build date. This is set using ldflags -X
19 | Date = "YYYY-MM-DD" // YYYY-MM-DD
20 |
21 | // DSN used for sentry
22 | SentryDSN string
23 | )
24 |
25 | func main() {
26 | log.SetFlags(log.LstdFlags | log.Lshortfile)
27 |
28 | // Init sentry
29 | err := sentry.Init(sentry.ClientOptions{
30 | Dsn: SentryDSN,
31 | })
32 | if err != nil {
33 | log.Println("Could not load sentry.")
34 | }
35 | v.SetBuildInfo(version, Date, "", "")
36 |
37 | if err := command.Execute(); err != nil {
38 | // TODO: Handle exit codes here
39 | pterm.Error.Println(err)
40 | sentry.CaptureException(err)
41 | sentry.Flush(2 * time.Second)
42 | os.Exit(1)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/deepsource/auth/mutations/register_device.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/deepsourcelabs/cli/deepsource/auth"
7 | "github.com/deepsourcelabs/graphql"
8 | )
9 |
10 | // GraphQL mutation to register Device get a device code
11 | const registerDeviceMutation = `mutation register {
12 | registerDevice(input:{}) {
13 | deviceCode
14 | userCode
15 | verificationUri
16 | verificationUriComplete
17 | expiresIn
18 | interval
19 | }
20 | }`
21 |
22 | type RegisterDeviceRequest struct{}
23 |
24 | type RegisterDeviceResponse struct {
25 | auth.Device `json:"registerDevice"`
26 | }
27 |
28 | type IGQLClient interface {
29 | GQL() *graphql.Client
30 | }
31 |
32 | func (r RegisterDeviceRequest) Do(ctx context.Context, client IGQLClient) (*auth.Device, error) {
33 | req := graphql.NewRequest(registerDeviceMutation)
34 | req.Header.Set("Cache-Control", "no-cache")
35 |
36 | var res RegisterDeviceResponse
37 | if err := client.GQL().Run(ctx, req, &res); err != nil {
38 | return nil, err
39 | }
40 |
41 | return &res.Device, nil
42 | }
43 |
--------------------------------------------------------------------------------
/command/version/command.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/deepsourcelabs/cli/utils"
7 | "github.com/deepsourcelabs/cli/version"
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | // Options holds the metadata.
12 | type Options struct{}
13 |
14 | // For testing. TODO: cleanup
15 | var getBuildInfo = version.GetBuildInfo
16 |
17 | // NewCmdVersion returns the current version of cli being used
18 | func NewCmdVersion() *cobra.Command {
19 | cmd := &cobra.Command{
20 | Use: "version",
21 | Short: "Get the version of the DeepSource CLI",
22 | Args: utils.NoArgs,
23 | Run: func(cmd *cobra.Command, args []string) {
24 | o := Options{}
25 | fmt.Println(o.Run())
26 | },
27 | SilenceErrors: true,
28 | SilenceUsage: true,
29 | }
30 | return cmd
31 | }
32 |
33 | // Validate impletments the Validate method for the ICommand interface.
34 | func (Options) Validate() error {
35 | return nil
36 | }
37 |
38 | // Run executest the command.
39 | func (Options) Run() string {
40 | buildInfo := getBuildInfo()
41 | if buildInfo == nil {
42 | return ""
43 | }
44 | return getBuildInfo().String()
45 | }
46 |
--------------------------------------------------------------------------------
/deepsource/issues/issues_list.go:
--------------------------------------------------------------------------------
1 | package issues
2 |
3 | type Position struct {
4 | BeginLine int `json:"begin"` // The line where the code covered under the issue starts
5 | EndLine int `json:"end"` // The line where the code covered under the issue starts
6 | }
7 |
8 | type Location struct {
9 | Path string `json:"path"` // The filepath where the issue is reported
10 | Position Position `json:"position"` // The position info where the issue is raised
11 | }
12 |
13 | type AnalyzerMeta struct {
14 | Shortcode string `json:"analyzer"` // Analyzer shortcode
15 | }
16 |
17 | type Issue struct {
18 | IssueText string `json:"issue_title"` // The describing heading of the issue
19 | IssueCode string `json:"issue_code"` // DeepSource code for the issue reported
20 | IssueCategory string `json:"issue_category"` // Category of the issue reported
21 | IssueSeverity string `json:"issue_severity"` // Severity of the issue reported
22 | Location Location `json:"location"` // The location data for the issue reported
23 | Analyzer AnalyzerMeta // The Analyzer which raised the issue
24 | }
25 |
--------------------------------------------------------------------------------
/deepsource/auth/mutations/request_pat.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/deepsourcelabs/cli/deepsource/auth"
7 | "github.com/deepsourcelabs/graphql"
8 | )
9 |
10 | type RequestPATParams struct {
11 | DeviceCode string `json:"deviceCode"`
12 | Description string `json:"description"`
13 | }
14 |
15 | type RequestPATRequest struct {
16 | Params RequestPATParams
17 | }
18 |
19 | // GraphQL mutation to request JWT
20 | const requestPATMutation = `
21 | mutation request($input:RequestPATWithDeviceCodeInput!) {
22 | requestPatWithDeviceCode(input:$input) {
23 | token
24 | expiry
25 | user {
26 | email
27 | }
28 | }
29 | }`
30 |
31 | type RequestPATResponse struct {
32 | auth.PAT `json:"requestPatWithDeviceCode"`
33 | }
34 |
35 | func (r RequestPATRequest) Do(ctx context.Context, client IGQLClient) (*auth.PAT, error) {
36 | req := graphql.NewRequest(requestPATMutation)
37 | req.Header.Set("Cache-Control", "no-cache")
38 | req.Var("input", r.Params)
39 |
40 | var res RequestPATResponse
41 | if err := client.GQL().Run(ctx, req, &res); err != nil {
42 | return nil, err
43 | }
44 |
45 | return &res.PAT, nil
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | run-tests:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | id-token: write # Required to fetch the OIDC token
14 |
15 | steps:
16 | - name: Set up Go 1.x
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: ^1.18
20 |
21 | - name: Check out code into the Go module directory
22 | uses: actions/checkout@v2
23 | with:
24 | fetch-depth: 1
25 | ref: ${{ github.event.pull_request.head.sha }}
26 |
27 | - name: Build the binary
28 | run: make build
29 |
30 | - name: Setup tests
31 | run: make test_setup
32 | env:
33 | CODE_PATH: /home/runner/code
34 |
35 | - name: Run tests
36 | run: make test
37 | env:
38 | CODE_PATH: /home/runner/code
39 |
40 | - name: Report test coverage to DeepSource
41 | run: |
42 | curl https://deepsource.io/cli | sh
43 | ./bin/deepsource report --analyzer test-coverage --key go --value-file ./coverage.out --use-oidc
44 |
--------------------------------------------------------------------------------
/command/issues/list/testdata/json/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "occurences": [
3 | {
4 | "analyzer": "go",
5 | "issue_code": "RVV-B0013",
6 | "issue_title": "Unused method receiver detected",
7 | "occurence_title": "Unused method receiver detected",
8 | "issue_category": "",
9 | "location": {
10 | "path": "deepsource/transformers/queries/get_transformers.go",
11 | "position": {
12 | "begin": {
13 | "line": 34,
14 | "column": 0
15 | },
16 | "end": {
17 | "line": 34,
18 | "column": 0
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "analyzer": "go",
25 | "issue_code": "RVV-B0013",
26 | "issue_title": "Unused method receiver detected",
27 | "occurence_title": "Unused method receiver detected",
28 | "issue_category": "",
29 | "location": {
30 | "path": "deepsource/transformers/queries/get_transformers.go",
31 | "position": {
32 | "begin": {
33 | "line": 44,
34 | "column": 0
35 | },
36 | "end": {
37 | "line": 44,
38 | "column": 0
39 | }
40 | }
41 | }
42 | }
43 | ],
44 | "summary": {
45 | "total_occurences": 2,
46 | "unique_issues": 1
47 | }
48 | }
--------------------------------------------------------------------------------
/command/root.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "github.com/deepsourcelabs/cli/command/auth"
5 | "github.com/deepsourcelabs/cli/command/config"
6 | "github.com/deepsourcelabs/cli/command/issues"
7 | "github.com/deepsourcelabs/cli/command/repo"
8 | "github.com/deepsourcelabs/cli/command/report"
9 | "github.com/deepsourcelabs/cli/command/version"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | func NewCmdRoot() *cobra.Command {
14 | cmd := &cobra.Command{
15 | Use: "deepsource [flags]",
16 | Short: "DeepSource CLI",
17 | Long: `Welcome to DeepSource CLI
18 | Now ship good code directly from the command line.
19 |
20 | Login into DeepSource using the command : deepsource auth login`,
21 | SilenceErrors: true,
22 | SilenceUsage: true,
23 | }
24 |
25 | // Child Commands
26 | cmd.AddCommand(version.NewCmdVersion())
27 | cmd.AddCommand(config.NewCmdConfig())
28 | cmd.AddCommand(auth.NewCmdAuth())
29 | cmd.AddCommand(repo.NewCmdRepo())
30 | cmd.AddCommand(issues.NewCmdIssues())
31 | cmd.AddCommand(report.NewCmdReport())
32 |
33 | return cmd
34 | }
35 |
36 | func Execute() error {
37 | cmd := NewCmdRoot()
38 | return cmd.Execute()
39 | }
40 |
--------------------------------------------------------------------------------
/deepsource/auth/mutations/refresh_pat.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/deepsourcelabs/cli/deepsource/auth"
8 | "github.com/deepsourcelabs/graphql"
9 | )
10 |
11 | // GraphQL query to refresh token
12 | const refreshTokenQuery = `
13 | mutation RefreshPAT {
14 | refreshPat {
15 | token
16 | expiry
17 | user {
18 | email
19 | }
20 | }
21 | }`
22 |
23 | type RefreshTokenParams struct {
24 | Token string `json:"token"`
25 | }
26 |
27 | type RefreshTokenRequest struct {
28 | Params RefreshTokenParams
29 | }
30 |
31 | type RefreshTokenResponse struct {
32 | auth.PAT `json:"refreshPat"`
33 | }
34 |
35 | func (r RefreshTokenRequest) Do(ctx context.Context, client IGQLClient) (*auth.PAT, error) {
36 | req := graphql.NewRequest(refreshTokenQuery)
37 |
38 | // set header fields
39 | req.Header.Set("Cache-Control", "no-cache")
40 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.Params.Token))
41 |
42 | // run it and capture the response
43 | var respData RefreshTokenResponse
44 | if err := client.GQL().Run(ctx, req, &respData); err != nil {
45 | return nil, err
46 | }
47 |
48 | return &respData.PAT, nil
49 | }
50 |
--------------------------------------------------------------------------------
/command/config/generate/input.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/deepsourcelabs/cli/config"
7 | "github.com/deepsourcelabs/cli/deepsource"
8 | "github.com/deepsourcelabs/cli/utils"
9 | )
10 |
11 | // Responsible for collecting user input for generating DeepSource config
12 | func (o *Options) collectUserInput() error {
13 | deepsource, err := deepsource.New(deepsource.ClientOpts{
14 | Token: config.Cfg.Token,
15 | HostName: config.Cfg.Host,
16 | })
17 | if err != nil {
18 | return err
19 | }
20 | ctx := context.Background()
21 |
22 | // Get the list of analyzers and transformers supported by DeepSource
23 | err = utils.GetAnalyzersAndTransformersData(ctx, *deepsource)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | // Get input for analyzers to be activated
29 | err = o.collectAnalyzerInput()
30 | if err != nil {
31 | return err
32 | }
33 |
34 | err = o.collectTransformersInput()
35 | if err != nil {
36 | return err
37 | }
38 |
39 | err = o.collectExcludePatterns()
40 | if err != nil {
41 | return err
42 | }
43 |
44 | err = o.collectTestPatterns()
45 | if err != nil {
46 | return err
47 | }
48 |
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/command/report/types.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | // ReportQueryInput is the schema for variables of artifacts
4 | // report GraphQL query
5 | type ReportQueryInput struct {
6 | AccessToken string `json:"accessToken"`
7 | CommitOID string `json:"commitOid"`
8 | ReporterName string `json:"reporter"`
9 | ReporterVersion string `json:"reporterVersion"`
10 | Key string `json:"key"`
11 | Data string `json:"data"`
12 | AnalyzerShortcode string `json:"analyzer"`
13 | AnalyzerType string `json:"analyzerType,omitempty"`
14 | Metadata interface{} `json:"metadata,omitempty"`
15 | }
16 |
17 | // ReportQueryInput is the structure of artifacts report
18 | // GraphQL query
19 | type ReportQuery struct {
20 | Query string `json:"query"`
21 | Variables struct {
22 | Input ReportQueryInput `json:"input"`
23 | } `json:"variables"`
24 | }
25 |
26 | // QueryResponse is the response returned by artifacts report
27 | // GraphQL query
28 | type QueryResponse struct {
29 | Data struct {
30 | CreateArtifact struct {
31 | Error string `json:"error"`
32 | Message string `json:"message"`
33 | Ok bool `json:"ok"`
34 | } `json:"createArtifact"`
35 | } `json:"data"`
36 | }
37 |
--------------------------------------------------------------------------------
/command/config/generate/types.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | // DSConfig is the struct for .deepsource.toml file
4 | type Analyzer struct {
5 | Name string `toml:"name,omitempty" json:"name,omitempty"`
6 | RuntimeVersion string `toml:"runtime_version,omitempty" json:"runtime_version,omitempty"`
7 | Enabled bool `toml:"enabled" json:"enabled"`
8 | DependencyFilePaths []string `toml:"dependency_file_paths,omitempty" json:"dependency_file_paths,omitempty"`
9 | Meta interface{} `toml:"meta,omitempty" json:"meta,omitempty"`
10 | Thresholds interface{} `toml:"thresholds,omitempty" json:"thresholds,omitempty"`
11 | }
12 |
13 | type Transformer struct {
14 | Name string `toml:"name" json:"name"`
15 | Enabled bool `toml:"enabled" json:"enabled"`
16 | }
17 |
18 | type DSConfig struct {
19 | Version int `toml:"version" json:"version"`
20 | ExcludePatterns []string `toml:"exclude_patterns" json:"exclude_patterns,omitempty"`
21 | TestPatterns []string `toml:"test_patterns" json:"test_patterns,omitempty"`
22 | Analyzers []Analyzer `toml:"analyzers,omitempty" json:"analyzers,omitempty"`
23 | Transformers []Transformer `toml:"transformers,omitempty" json:"transformers,omitempty"`
24 | }
25 |
--------------------------------------------------------------------------------
/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | var buildInfo *BuildInfo
9 |
10 | // BuildInfo describes the compile time information.
11 | type BuildInfo struct {
12 | // Version is the current semver.
13 | Version string `json:"version,omitempty"`
14 | // Date is the build date.
15 | Date time.Time `json:"date,omitempty"`
16 | // gitTreeState is the state of the git tree.
17 | GitTreeState string `json:"git_tree_state,omitempty"`
18 | // GitCommit is the git sha1.
19 | GitCommit string `json:"git_commit,omitempty"`
20 | }
21 |
22 | // Set's the build info as a package global.
23 | func SetBuildInfo(version, dateStr, gitTreeState, gitCommit string) {
24 | date, _ := time.Parse("2006-01-02", dateStr)
25 |
26 | buildInfo = &BuildInfo{
27 | Version: version,
28 | Date: date,
29 | GitTreeState: gitTreeState,
30 | GitCommit: gitCommit,
31 | }
32 | }
33 |
34 | // GetBuildInfo returns the package global `buildInfo`
35 | func GetBuildInfo() *BuildInfo {
36 | return buildInfo
37 | }
38 |
39 | func (bi BuildInfo) String() string {
40 | if bi.Date.IsZero() {
41 | return fmt.Sprintf("DeepSource CLI version %s", bi.Version)
42 | }
43 | dateStr := bi.Date.Format("2006-01-02")
44 | return fmt.Sprintf("DeepSource CLI version %s (%s)", bi.Version, dateStr)
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 DeepSource Corp. (https://deepsource.io) and individual contributors.
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11 |
--------------------------------------------------------------------------------
/configvalidator/transformer_config_validator.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | "github.com/deepsourcelabs/cli/utils"
8 | "github.com/spf13/viper"
9 | )
10 |
11 | // Validates Transformers Config
12 | func (c *ConfigValidator) validateTransformersConfig() {
13 | // If no transformer activated by user, return without any errors
14 | if viper.Get("transformers") == nil {
15 | return
16 | }
17 |
18 | // Transformers should be an array
19 | transformersType := reflect.TypeOf(c.Config.Transformers).Kind().String()
20 | if transformersType != "slice" {
21 | c.pushError(fmt.Sprintf("Value of `transformers` should be an array. Found: %v", transformersType))
22 | }
23 |
24 | // Enabled property validation is handled in the main config validator
25 | // (transformers with invalid enabled types will cause unmarshaling errors)
26 |
27 | // ==== Transformer shortcode validation ====
28 | supported := false
29 | for _, activatedTransformer := range c.Config.Transformers {
30 | for _, supportedTransformer := range utils.TransformersData.TransformerShortcodes {
31 | if activatedTransformer.Name == supportedTransformer {
32 | supported = true
33 | break
34 | }
35 | }
36 | if !supported {
37 | c.pushError(fmt.Sprintf("The Tranformer %s is not supported yet.", activatedTransformer.Name))
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/configvalidator/types.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | // DSConfig is the struct for .deepsource.toml file
4 | type Analyzer struct {
5 | Name string `mapstructure:"name,omitempty" json:"name,omitempty"`
6 | RuntimeVersion string `mapstructure:"runtime_version,omitempty" json:"runtime_version,omitempty"`
7 | Enabled *bool `mapstructure:"enabled,omitempty" json:"enabled,omitempty"`
8 | DependencyFilePaths []string `mapstructure:"dependency_file_paths,omitempty" json:"dependency_file_paths,omitempty"`
9 | Meta interface{} `mapstructure:"meta,omitempty" json:"meta,omitempty"`
10 | Thresholds interface{} `mapstructure:"thresholds,omitempty" json:"thresholds,omitempty"`
11 | }
12 |
13 | type Transformer struct {
14 | Name string `mapstructure:"name,omitempty" json:"name,omitempty"`
15 | Enabled *bool `mapstructure:"enabled,omitempty" json:"enabled,omitempty"`
16 | }
17 |
18 | type DSConfig struct {
19 | Version int `mapstructure:"version,omitempty" json:"version"`
20 | ExcludePatterns []string `mapstructure:"exclude_patterns,omitempty" json:"exclude_patterns,omitempty"`
21 | TestPatterns []string `mapstructure:"test_patterns,omitempty" json:"test_patterns,omitempty"`
22 | Analyzers []Analyzer `mapstructure:"analyzers,omitempty" json:"analyzers,omitempty"`
23 | Transformers []Transformer `mapstructure:"transformers,omitempty" json:"transformers,omitempty"`
24 | }
25 |
--------------------------------------------------------------------------------
/command/report/query.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | // makeQuery makes a HTTP query with a specified body and returns the response
14 | func makeQuery(url string, body []byte, bodyMimeType string, skipCertificateVerification bool) ([]byte, error) {
15 | var resBody []byte
16 | httpClient := &http.Client{
17 | Timeout: time.Second * 60,
18 | }
19 |
20 | if skipCertificateVerification {
21 | // Create a custom HTTP Transport for skipping verification of SSL certificates
22 | // if `--skip-verify` flag is passed.
23 | tr := &http.Transport{
24 | TLSClientConfig: &tls.Config{
25 | InsecureSkipVerify: true,
26 | },
27 | }
28 | httpClient.Transport = tr
29 | }
30 |
31 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
32 | if err != nil {
33 | return nil, err
34 | }
35 | req.Header.Set("Content-Type", bodyMimeType)
36 | res, err := httpClient.Do(req)
37 | if err != nil {
38 | return resBody, err
39 | }
40 | defer res.Body.Close()
41 |
42 | resBody, err = io.ReadAll(res.Body)
43 | if err != nil {
44 | return resBody, err
45 | }
46 |
47 | if res.StatusCode >= http.StatusInternalServerError || res.StatusCode != 200 {
48 | if resBody != nil {
49 | return resBody, fmt.Errorf("Server responded with %s: %s", strconv.Itoa(res.StatusCode), string(resBody))
50 | }
51 | return resBody, fmt.Errorf("Server responded with %s", strconv.Itoa(res.StatusCode))
52 | }
53 |
54 | return resBody, nil
55 | }
56 |
--------------------------------------------------------------------------------
/command/report/dsn_test.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestNewDSN(t *testing.T) {
9 | type args struct {
10 | raw string
11 | }
12 | tests := []struct {
13 | name string
14 | args args
15 | want *DSN
16 | wantErr error
17 | }{
18 | {
19 | name: "valid DSN",
20 | args: args{
21 | raw: "https://e1099ed7240c4045b5ab3fedebc7b5d7@app.deepsource.com",
22 | },
23 | want: &DSN{
24 | Token: "e1099ed7240c4045b5ab3fedebc7b5d7",
25 | Host: "app.deepsource.com",
26 | Protocol: "https",
27 | },
28 | wantErr: nil,
29 | },
30 | {
31 | name: "valid DSN with port",
32 | args: args{
33 | raw: "http://f59a44307@localhost:8081",
34 | },
35 | want: &DSN{
36 | Token: "f59a44307",
37 | Host: "localhost:8081",
38 | Protocol: "http",
39 | },
40 | },
41 | {
42 | name: "invalid DSN no http",
43 | args: args{
44 | raw: "no http",
45 | },
46 | want: nil,
47 | wantErr: ErrInvalidDSN,
48 | },
49 | {
50 | name: "invalid DSN",
51 | args: args{
52 | raw: "https://e1099ed7240c4045b5ab3fedebc7b5d7",
53 | },
54 | want: nil,
55 | wantErr: ErrInvalidDSN,
56 | },
57 | }
58 | for _, tt := range tests {
59 | t.Run(tt.name, func(t *testing.T) {
60 | got, err := NewDSN(tt.args.raw)
61 | if err != tt.wantErr {
62 | t.Errorf("NewDSN() error = %v, wantErr %v", err, tt.wantErr)
63 | return
64 | }
65 | if !reflect.DeepEqual(got, tt.want) {
66 | t.Errorf("NewDSN() = %v, want %v", got, tt.want)
67 | }
68 | })
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/command/auth/logout/logout.go:
--------------------------------------------------------------------------------
1 | package logout
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/deepsourcelabs/cli/config"
8 | "github.com/deepsourcelabs/cli/utils"
9 | "github.com/pterm/pterm"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | type LogoutOptions struct{}
14 |
15 | // NewCmdLogout handles the logout functionality for the CLI
16 | func NewCmdLogout() *cobra.Command {
17 | cmd := &cobra.Command{
18 | Use: "logout",
19 | Short: "Logout of your active DeepSource account",
20 | Args: utils.NoArgs,
21 | RunE: func(cmd *cobra.Command, args []string) error {
22 | opts := LogoutOptions{}
23 | return opts.Run()
24 | },
25 | }
26 | return cmd
27 | }
28 |
29 | func (opts *LogoutOptions) Run() error {
30 | // Fetch config
31 | cfg, err := config.GetConfig()
32 | if err != nil {
33 | return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
34 | }
35 | // Checking if the user has authenticated / logged in or not
36 | if cfg.Token == "" {
37 | return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.")
38 | }
39 |
40 | // Confirm from the user if they want to logout
41 | logoutConfirmationMsg := "Are you sure you want to log out of DeepSource account?"
42 | response, err := utils.ConfirmFromUser(logoutConfirmationMsg, "")
43 | if err != nil {
44 | return err
45 | }
46 |
47 | // If response is true, delete the config file => logged out the user
48 | if response {
49 | err := cfg.Delete()
50 | if err != nil {
51 | return err
52 | }
53 | }
54 | pterm.Info.Println("Logged out from DeepSource (deepsource.io)")
55 | return nil
56 | }
57 |
--------------------------------------------------------------------------------
/command/auth/status/status.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/MakeNowJust/heredoc"
8 | "github.com/deepsourcelabs/cli/config"
9 | "github.com/deepsourcelabs/cli/utils"
10 | "github.com/pterm/pterm"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | type AuthStatusOptions struct{}
15 |
16 | // NewCmdStatus handles the fetching of authentication status of CLI
17 | func NewCmdStatus() *cobra.Command {
18 | doc := heredoc.Docf(`
19 | View the authentication status.
20 |
21 | To check the authentication status, use %[1]s
22 | `, utils.Cyan("deepsource auth status"))
23 |
24 | cmd := &cobra.Command{
25 | Use: "status",
26 | Short: "View the authentication status",
27 | Long: doc,
28 | Args: utils.NoArgs,
29 | RunE: func(cmd *cobra.Command, args []string) error {
30 | opts := AuthStatusOptions{}
31 | return opts.Run()
32 | },
33 | }
34 | return cmd
35 | }
36 |
37 | func (opts *AuthStatusOptions) Run() error {
38 | // Fetch config
39 | cfg, err := config.GetConfig()
40 | if err != nil {
41 | return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
42 | }
43 | // Checking if the user has authenticated / logged in or not
44 | if cfg.Token == "" {
45 | return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.")
46 | }
47 |
48 | // Check if the token has already expired
49 | if !cfg.IsExpired() {
50 | pterm.Info.Printf("Logged in to DeepSource as %s.\n", cfg.User)
51 | } else {
52 | pterm.Info.Println("The authentication has expired. Run \"deepsource auth refresh\" to refresh the credentials.")
53 | }
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/utils/cmd_validator.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | // Validates if the number of args passed to a command is exactly same as that required
11 | func ExactArgs(count int) cobra.PositionalArgs {
12 | return func(cmd *cobra.Command, args []string) error {
13 | arg := "argument"
14 | if count > 1 {
15 | arg = "arguments"
16 | }
17 |
18 | errorMsg := fmt.Sprintf("`%s` requires exactly %d %s. Got %d. Please see `%s --help` for the supported flags and their usage.",
19 | cmd.CommandPath(),
20 | count,
21 | arg,
22 | len(args),
23 | cmd.CommandPath())
24 |
25 | if len(args) != count {
26 | return errors.New(errorMsg)
27 | }
28 | return nil
29 | }
30 | }
31 |
32 | func MaxNArgs(count int) cobra.PositionalArgs {
33 | return func(cmd *cobra.Command, args []string) error {
34 | arg := "argument"
35 | if count > 1 {
36 | arg = "arguments"
37 | }
38 |
39 | errorMsg := fmt.Sprintf("`%s` requires maximum %d %s. Got %d. Please see `%s --help` for the supported flags and their usage.",
40 | cmd.CommandPath(),
41 | count,
42 | arg,
43 | len(args),
44 | cmd.CommandPath())
45 |
46 | if len(args) > count {
47 | return errors.New(errorMsg)
48 | }
49 | return nil
50 | }
51 | }
52 |
53 | // Validates if there is any arg passed to a command which doesn't require any
54 | func NoArgs(cmd *cobra.Command, args []string) error {
55 | errorMsg := fmt.Sprintf("`%s` does not require any argument. Please see `%s --help` for the supported flags and their usage.",
56 | cmd.CommandPath(),
57 | cmd.CommandPath())
58 |
59 | if len(args) > 0 {
60 | return errors.New(errorMsg)
61 | }
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------
/command/report/constants.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | const (
4 | CliVersion = "v0.8.0"
5 | commonUsageMessage = `
6 | Usage:
7 | deepsource []
8 | Available commands are:
9 | report Report an artifact to analyzer
10 | Help:
11 | Use 'deepsource --help' for more information about the command.
12 | Documentation:
13 | https://deepsource.io/docs/cli
14 | `
15 |
16 | reportUsageMessage = `
17 | Usage:
18 | deepsource report []
19 | Available arguments are:
20 | --analyzer Shortcode of the analyzer
21 | --analyzer-type Type of the analyzer (default: "core")
22 | --key Name of the artifact
23 | --value Value of the artifact
24 | --value-file Path to the artifact value file
25 | Examples:
26 | deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml
27 | deepsource report --analyzer git --key lines-changed --value 22
28 | deepsource report --analyzer kube-linter --type community --value-file ./kube-linter.sarif
29 | Notes:
30 | - Pass either '--value' or '--value-file'. If both are passed, contents of '--value' will be considered.
31 | - '--analyzer-type' is optional. If not passed, it will default to 'core'.
32 | Documentation:
33 | https://deepsource.io/docs/cli#report
34 | `
35 | reportGraphqlQuery = "mutation($input: CreateArtifactInput!) {\r\n createArtifact(input: $input) {\r\n ok\r\n message\r\n error\r\n }\r\n}"
36 | reportGraphqlQueryOld = "mutation($input: CreateArtifactInput!) {\r\n createArtifact(input: $input) {\r\n ok\r\n error\r\n }\r\n}"
37 | graphqlCheckCompressed = "query {\r\n __type(name: \"ArtifactMetadataInput\") {\r\n inputFields {\r\n name\r\n }\r\n }\r\n}"
38 | )
39 |
--------------------------------------------------------------------------------
/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | var cfg = CLIConfig{
11 | Host: "deepsource.io",
12 | User: "test",
13 | Token: "test_token",
14 | TokenExpiresIn: time.Time{},
15 | }
16 |
17 | func TestSetTokenExpiry(t *testing.T) {
18 | t.Run("must reset with invalid timestamp", func(t *testing.T) {
19 | str := "invalid"
20 | cfg.SetTokenExpiry(str)
21 | want := "0001-01-01 00:00:00 +0000 UTC"
22 |
23 | assert.Equal(t, cfg.TokenExpiresIn.UTC().String(), want)
24 | })
25 |
26 | t.Run("must work with valid timestamp", func(t *testing.T) {
27 | str := "9999-12-31T23:59:59.999999+00:00"
28 | cfg.SetTokenExpiry(str)
29 | want := "9999-12-31 23:59:59.999999 +0000 UTC"
30 |
31 | assert.Equal(t, cfg.TokenExpiresIn.UTC().String(), want)
32 | })
33 | }
34 |
35 | func TestIsExpired(t *testing.T) {
36 | str := time.Now().UTC().Format("2006-01-02T15:04:05.999999999")
37 | cfg.SetTokenExpiry(str)
38 | result := cfg.IsExpired()
39 | assert.Equal(t, true, result)
40 | }
41 |
42 | func TestConfigDir(t *testing.T) {
43 | _, err := cfg.configDir()
44 | assert.Nil(t, err)
45 | }
46 |
47 | func TestConfigPath(t *testing.T) {
48 | _, err := cfg.configPath()
49 | assert.Nil(t, err)
50 | }
51 |
52 | func TestGetConfig(t *testing.T) {
53 | _, err := GetConfig()
54 | assert.Nil(t, err)
55 | }
56 |
57 | func TestVerifyAuthentication(t *testing.T) {
58 | t.Run("must return nil when token is provided", func(t *testing.T) {
59 | err := cfg.VerifyAuthentication()
60 | assert.Nil(t, err)
61 | })
62 |
63 | t.Run("must not return nil when empty token is provided", func(t *testing.T) {
64 | // set empty token
65 | cfg.Token = ""
66 | err := cfg.VerifyAuthentication()
67 | assert.NotNil(t, err)
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/configvalidator/transformer_config_validator_test.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestValidateTransformersConfig(t *testing.T) {
9 | setDummyAnalyzerTransformerData()
10 | type test struct {
11 | inputConfig string
12 | result bool
13 | }
14 |
15 | tests := map[string]test{
16 | "valid config": {
17 | inputConfig: `
18 | [[transformers]]
19 | name = "black"
20 | enabled = true`,
21 | result: true,
22 | },
23 | "transformers are not mandatory lik}e analyzers": {
24 | inputConfig: `
25 | [[transformers]]
26 | name = "black"
27 | enabled = false`,
28 | result: true,
29 | },
30 | "can't use unsupported analyzer": {
31 | inputConfig: `
32 | [[transformers]]
33 | name = "rick-astley"
34 | enabled = true`,
35 | result: false,
36 | },
37 | "multiple transformers": {
38 | inputConfig: `
39 | [[transformers]]
40 | name = "black"
41 | enabled = true
42 |
43 | [[transformers]]
44 | name = "prettier"
45 | enabled = true`,
46 | result: true,
47 | },
48 | }
49 | for testName, tc := range tests {
50 | t.Run(testName, func(t *testing.T) {
51 | testConfig, err := getConfig([]byte(tc.inputConfig))
52 | if err != nil {
53 | t.Error(err)
54 | }
55 | c := &ConfigValidator{
56 | Config: *testConfig,
57 | Result: Result{
58 | Valid: true,
59 | Errors: []string{},
60 | ConfigReadError: false,
61 | },
62 | }
63 | c.validateTransformersConfig()
64 | if !reflect.DeepEqual(tc.result, c.Result.Valid) {
65 | t.Errorf("expected: %v, got: %v. Error: %v", tc.result, c.Result.Valid, c.Result.Errors)
66 | }
67 | })
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/deepsource/analyzers/queries/get_analyzers.go:
--------------------------------------------------------------------------------
1 | package analyzers
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/deepsourcelabs/cli/deepsource/analyzers"
8 | "github.com/deepsourcelabs/graphql"
9 | )
10 |
11 | // GraphQL query
12 | const listAnalyzersQuery = `
13 | {
14 | analyzers {
15 | edges {
16 | node {
17 | name
18 | shortcode
19 | metaSchema
20 | }
21 | }
22 | }
23 | }`
24 |
25 | type AnalyzersRequest struct{}
26 |
27 | type AnalyzersResponse struct {
28 | Analyzers struct {
29 | Edges []struct {
30 | Node struct {
31 | Name string `json:"name"`
32 | Shortcode string `json:"shortcode"`
33 | MetaSchema string `json:"metaSchema"`
34 | } `json:"node"`
35 | } `json:"edges"`
36 | } `json:"analyzers"`
37 | }
38 |
39 | // GraphQL client interface
40 | type IGQLClient interface {
41 | GQL() *graphql.Client
42 | GetToken() string
43 | }
44 |
45 | func (a AnalyzersRequest) Do(ctx context.Context, client IGQLClient) ([]analyzers.Analyzer, error) {
46 | req := graphql.NewRequest(listAnalyzersQuery)
47 |
48 | // set header fields
49 | req.Header.Set("Cache-Control", "no-cache")
50 | tokenHeader := fmt.Sprintf("Bearer %s", client.GetToken())
51 | req.Header.Add("Authorization", tokenHeader)
52 |
53 | // run it and capture the response
54 | var respData AnalyzersResponse
55 | if err := client.GQL().Run(ctx, req, &respData); err != nil {
56 | return nil, err
57 | }
58 |
59 | // Formatting the query response w.r.t the output format
60 | analyzersData := make([]analyzers.Analyzer, len(respData.Analyzers.Edges))
61 | for index, edge := range respData.Analyzers.Edges {
62 | analyzersData[index].Name = edge.Node.Name
63 | analyzersData[index].Shortcode = edge.Node.Shortcode
64 | analyzersData[index].MetaSchema = edge.Node.MetaSchema
65 | }
66 |
67 | return analyzersData, nil
68 | }
69 |
--------------------------------------------------------------------------------
/deepsource/transformers/queries/get_transformers.go:
--------------------------------------------------------------------------------
1 | package transformers
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/deepsourcelabs/cli/deepsource/transformers"
8 | "github.com/deepsourcelabs/graphql"
9 | )
10 |
11 | // Query to list supported Transformers
12 | const listTransformersQuery = `
13 | {
14 | transformers{
15 | edges{
16 | node{
17 | name
18 | shortcode
19 | }
20 | }
21 | }
22 | }`
23 |
24 | type TransformersRequest struct{}
25 |
26 | type TransformersResponse struct {
27 | Transformers struct {
28 | Edges []struct {
29 | Node struct {
30 | Name string `json:"name"`
31 | Shortcode string `json:"shortcode"`
32 | } `json:"node"`
33 | } `json:"edges"`
34 | } `json:"transformers"`
35 | }
36 |
37 | // GraphQL client interface
38 | type IGQLClient interface {
39 | GQL() *graphql.Client
40 | GetToken() string
41 | }
42 |
43 | func (t TransformersRequest) Do(ctx context.Context, client IGQLClient) ([]transformers.Transformer, error) {
44 | req := graphql.NewRequest(listTransformersQuery)
45 |
46 | // set header fields
47 | req.Header.Set("Cache-Control", "no-cache")
48 | // Adding PAT as header for auth
49 | tokenHeader := fmt.Sprintf("Bearer %s", client.GetToken())
50 | req.Header.Add("Authorization", tokenHeader)
51 |
52 | // run it and capture the response
53 | var respData TransformersResponse
54 | if err := client.GQL().Run(ctx, req, &respData); err != nil {
55 | return nil, err
56 | }
57 |
58 | // Formatting the query response w.r.t the SDK response ([]transformers.Transformer)
59 | transformersData := make([]transformers.Transformer, len(respData.Transformers.Edges))
60 | for index, edge := range respData.Transformers.Edges {
61 | transformersData[index].Name = edge.Node.Name
62 | transformersData[index].Shortcode = edge.Node.Shortcode
63 | }
64 |
65 | return transformersData, nil
66 | }
67 |
--------------------------------------------------------------------------------
/version/version_test.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestBuildInfo_String(t *testing.T) {
10 | date, _ := time.Parse("2006-01-02", "2021-01-21")
11 |
12 | type fields struct {
13 | Version string
14 | Date time.Time
15 | GitTreeState string
16 | GitCommit string
17 | }
18 | tests := []struct {
19 | name string
20 | fields fields
21 | want string
22 | }{
23 | {
24 | "must return the correct version string when date and version is available",
25 | fields{
26 | Version: "1.5.0",
27 | Date: date,
28 | },
29 | "DeepSource CLI version 1.5.0 (2021-01-21)",
30 | },
31 | {
32 | "must return the correct version string when only version is available",
33 | fields{
34 | Version: "1.5.0",
35 | },
36 | "DeepSource CLI version 1.5.0",
37 | },
38 | }
39 | for _, tt := range tests {
40 | t.Run(tt.name, func(t *testing.T) {
41 | bi := BuildInfo{
42 | Version: tt.fields.Version,
43 | Date: tt.fields.Date,
44 | GitTreeState: tt.fields.GitTreeState,
45 | GitCommit: tt.fields.GitCommit,
46 | }
47 | if got := bi.String(); got != tt.want {
48 | t.Errorf("BuildInfo.String() = %v, want %v", got, tt.want)
49 | }
50 | })
51 | }
52 | }
53 |
54 | func TestSetBuildInfo(t *testing.T) {
55 | date, _ := time.Parse("2006-01-02", "2021-01-21")
56 |
57 | want := &BuildInfo{
58 | Version: "1.0.0",
59 | Date: date,
60 | }
61 |
62 | type args struct {
63 | version string
64 | dateStr string
65 | gitTreeState string
66 | gitCommit string
67 | }
68 | tests := []struct {
69 | name string
70 | args args
71 | }{
72 | {
73 | "must set the buildInfo package global",
74 | args{version: "1.0.0", dateStr: "2021-01-21"},
75 | },
76 | }
77 | for _, tt := range tests {
78 | t.Run(tt.name, func(t *testing.T) {
79 | SetBuildInfo(tt.args.version, tt.args.dateStr, tt.args.gitTreeState, tt.args.gitCommit)
80 | })
81 | if !reflect.DeepEqual(buildInfo, want) {
82 | t.Errorf("buildInfo = %v, want %v", buildInfo, want)
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/deepsource/tests/get_analyzers_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "context"
5 | "io/ioutil"
6 | "log"
7 | "net/http"
8 | "reflect"
9 | "testing"
10 |
11 | analyzers "github.com/deepsourcelabs/cli/deepsource/analyzers/queries"
12 | "github.com/deepsourcelabs/graphql"
13 | )
14 |
15 | // mock client
16 | type Client struct {
17 | gql *graphql.Client
18 | token string
19 | }
20 |
21 | // Returns a GraphQL client which can be used to interact with the GQL APIs
22 | func (c Client) GQL() *graphql.Client {
23 | return c.gql
24 | }
25 |
26 | // Returns the token which is required for authentication and thus, interacting with the APIs
27 | func (c Client) GetToken() string {
28 | return c.token
29 | }
30 |
31 | func TestAnalyzers(t *testing.T) {
32 | t.Run("valid GraphQL request", func(t *testing.T) {
33 | // create client
34 | gql := graphql.NewClient("http://localhost:8081/analyzer")
35 | c := Client{gql: gql, token: "secret"}
36 |
37 | // perform request
38 | req := analyzers.AnalyzersRequest{}
39 | ctx := context.Background()
40 | _, err := req.Do(ctx, c)
41 | if err != nil {
42 | t.Error(err.Error())
43 | }
44 | })
45 | }
46 |
47 | // a mock GraphQL handler for testing
48 | func mockAnalyzer(w http.ResponseWriter, r *http.Request) {
49 | req, _ := ioutil.ReadAll(r.Body)
50 |
51 | // Read test graphql request body artifact file
52 | requestBodyData, err := ioutil.ReadFile("./testdata/analyzer/request_body.txt")
53 | if err != nil {
54 | log.Println(err)
55 | return
56 | }
57 |
58 | // Read test graphql success response body artifact file
59 | successResponseBodyData, err := ioutil.ReadFile("./testdata/analyzer/success_response_body.json")
60 | if err != nil {
61 | log.Println(err)
62 | return
63 | }
64 |
65 | // Read test graphql error response body artifact file
66 | errorResponseBodyData, err := ioutil.ReadFile("./testdata/analyzer/error_response_body.json")
67 | if err != nil {
68 | log.Println(err)
69 | return
70 | }
71 |
72 | w.WriteHeader(http.StatusOK)
73 | w.Header().Set("Content-Type", "application/json")
74 |
75 | if reflect.DeepEqual(requestBodyData, req) {
76 | w.Write([]byte(successResponseBodyData))
77 | } else {
78 | w.Write([]byte(errorResponseBodyData))
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/command/issues/list/testdata/sarif/test.sarif:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.1.0",
3 | "$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
4 | "runs": [
5 | {
6 | "tool": {
7 | "driver": {
8 | "informationUri": "https://deepsource.io/directory/analyzers/go",
9 | "name": "DeepSource Go Analyzer",
10 | "rules": [
11 | {
12 | "id": "RVV-B0013",
13 | "name": "Unused method receiver detected",
14 | "shortDescription": null,
15 | "fullDescription": {
16 | "text": ""
17 | },
18 | "helpUri": "https://deepsource.io/directory/analyzers/go/issues/RVV-B0013",
19 | "properties": {
20 | "category": "",
21 | "recommended": ""
22 | }
23 | }
24 | ]
25 | }
26 | },
27 | "results": [
28 | {
29 | "ruleId": "RVV-B0013",
30 | "ruleIndex": 0,
31 | "kind": "fail",
32 | "level": "error",
33 | "message": {
34 | "text": "Unused method receiver detected"
35 | },
36 | "locations": [
37 | {
38 | "physicalLocation": {
39 | "artifactLocation": {
40 | "uri": "deepsource/transformers/queries/get_transformers.go"
41 | },
42 | "region": {
43 | "startLine": 34,
44 | "endLine": 34
45 | }
46 | }
47 | }
48 | ]
49 | },
50 | {
51 | "ruleId": "RVV-B0013",
52 | "ruleIndex": 1,
53 | "kind": "fail",
54 | "level": "error",
55 | "message": {
56 | "text": "Unused method receiver detected"
57 | },
58 | "locations": [
59 | {
60 | "physicalLocation": {
61 | "artifactLocation": {
62 | "uri": "deepsource/transformers/queries/get_transformers.go"
63 | },
64 | "region": {
65 | "startLine": 44,
66 | "endLine": 44
67 | }
68 | }
69 | }
70 | ]
71 | }
72 | ]
73 | }
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/deepsource/repository/queries/repository_status.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/deepsourcelabs/cli/deepsource/repository"
8 | "github.com/deepsourcelabs/graphql"
9 | )
10 |
11 | // Query to fetch the status of the repo data sent as param
12 | const repoStatusQuery = `query RepoStatus($name: String!,$owner: String!, $provider: VCSProvider!){
13 | repository(name:$name, login:$owner, vcsProvider:$provider){
14 | isActivated
15 | }
16 | }`
17 |
18 | type RepoStatusParams struct {
19 | Owner string
20 | RepoName string
21 | Provider string
22 | }
23 |
24 | type RepoStatusRequest struct {
25 | Params RepoStatusParams
26 | }
27 |
28 | type RepoStatusResponse struct {
29 | Repository struct {
30 | Isactivated bool `json:"isActivated"`
31 | } `json:"repository"`
32 | }
33 |
34 | // GraphQL client interface
35 | type IGQLClient interface {
36 | GQL() *graphql.Client
37 | GetToken() string
38 | }
39 |
40 | func (r RepoStatusRequest) Do(ctx context.Context, client IGQLClient) (*repository.Meta, error) {
41 | req := graphql.NewRequest(repoStatusQuery)
42 | req.Var("name", r.Params.RepoName)
43 | req.Var("owner", r.Params.Owner)
44 | req.Var("provider", r.Params.Provider)
45 |
46 | // set header fields
47 | req.Header.Set("Cache-Control", "no-cache")
48 | // Adding PAT as header for auth
49 | tokenHeader := fmt.Sprintf("Bearer %s", client.GetToken())
50 | req.Header.Add("Authorization", tokenHeader)
51 |
52 | // run it and capture the response
53 | var respData RepoStatusResponse
54 | if err := client.GQL().Run(ctx, req, &respData); err != nil {
55 | return nil, err
56 | }
57 |
58 | // Formatting the query response w.r.t the repository.Meta structure
59 | // defined in `repository.go`
60 | repositoryData := repository.Meta{
61 | Activated: false,
62 | Name: r.Params.RepoName,
63 | Owner: r.Params.Owner,
64 | Provider: r.Params.Provider,
65 | }
66 | repositoryData.Name = r.Params.RepoName
67 | repositoryData.Owner = r.Params.Owner
68 | repositoryData.Provider = r.Params.Provider
69 |
70 | // Check and set the activation status
71 | if respData.Repository.Isactivated {
72 | repositoryData.Activated = true
73 | } else {
74 | repositoryData.Activated = false
75 | }
76 |
77 | return &repositoryData, nil
78 | }
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Docs |
7 | Get Started |
8 | Discuss
9 |
10 |
11 |
12 | The Code Health Platform
13 |
14 |
15 |
16 |
17 | ---
18 |
19 | # CLI
20 |
21 | Command line interface to DeepSource
22 |
23 | ## Installation
24 |
25 | ### macOS
26 |
27 | DeepSource CLI is available on macOS via [Homebrew](https://brew.sh/):
28 |
29 | ```sh
30 | brew install deepsourcelabs/cli/deepsource
31 | ```
32 | ### Binary Installation
33 |
34 | ```sh
35 | curl https://deepsource.com/cli | sh
36 | ```
37 | This script will detect the operating system and architecture and puts deepsource binary in `./bin/deepsource`.
38 |
39 | ## Configuring DSN
40 |
41 | In order to report test-coverage to DeepSource using the `report` command, an environment variable named as `DEEPSOURCE_DSN` has to
42 | be set. It's value will be available under 'Settings' tab of the repository page.
43 |
44 | ## Usage
45 |
46 | The CLI provides access to a wide range of commands. Here is a list of the
47 | commands along with their brief description.
48 |
49 | ```
50 | Usage:
51 | deepsource []
52 |
53 | Available commands are:
54 | report Report an artifact to an analyzer
55 | config Generate and Validate DeepSource config
56 | help Help about any command
57 | issues Show the list of issues in a file in a repository
58 | repo Operations related to the project repository
59 | report Report artifacts to DeepSource
60 | version Get the version of the DeepSource CLI
61 |
62 | Help:
63 | Use 'deepsource --help/-h' for more information about the command.
64 | ```
65 |
66 | ## Documentation
67 |
68 | For complete documentation, refer to the [CLI Documentation](https://docs.deepsource.com/docs/cli)
69 |
70 | ## Feedback/Support
71 |
72 | Want to share any feedback or need any help regarding the CLI? Feel free to
73 | open a discussion in the [community forum](https://discuss.deepsource.com)
74 |
75 | ## License
76 |
77 | Licensed under the [BSD 2-Clause "Simplified" License](https://github.com/deepsourcelabs/cli/blob/master/LICENSE).
78 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PACKAGE_NAME := github.com/deepsourcelabs/cli
2 | GOLANG_CROSS_VERSION ?= v1.21.6
3 |
4 | SYSROOT_DIR ?= sysroots
5 | SYSROOT_ARCHIVE ?= sysroots.tar.bz2
6 |
7 | build:
8 | cd cmd/deepsource && GOOS=linux GOARCH=amd64 go build -tags static_all -o /tmp/deepsource .
9 |
10 | build_local:
11 | cd cmd/deepsource && go build -tags static_all -o /tmp/deepsource .
12 |
13 | test:
14 | CGO_ENABLED=1 go test -v ./command/report/tests/... -count=1
15 | echo "\n====TESTING DEEPSOURCE PACKAGE====\n"
16 | CGO_ENABLED=1 go test -v ./deepsource/tests/...
17 | echo "\n====TESTING CONFIG VALIDATOR PACKAGE====\n"
18 | go test -v ./configvalidator/... -count=1
19 | echo "\n====CALCULATING TEST COVERAGE FOR ENTIRE PACKAGE====\n"
20 | go test -v -coverprofile=coverage.out -count=1 ./...
21 |
22 | test_setup:
23 | mkdir -p ${CODE_PATH}
24 | cd ${CODE_PATH} && ls -A1 | xargs rm -rf
25 | git clone https://github.com/DeepSourceCorp/july ${CODE_PATH}
26 | chmod +x /tmp/deepsource
27 | cp ./command/report/tests/golden_files/python_coverage.xml /tmp
28 |
29 | .PHONY: sysroot-pack
30 | sysroot-pack:
31 | @tar cf - $(SYSROOT_DIR) -P | pv -s $[$(du -sk $(SYSROOT_DIR) | awk '{print $1}') * 1024] | pbzip2 > $(SYSROOT_ARCHIVE)
32 |
33 | .PHONY: sysroot-unpack
34 | sysroot-unpack:
35 | @pv $(SYSROOT_ARCHIVE) | pbzip2 -cd | tar -xf -
36 |
37 | .PHONY: release-dry-run
38 | release-dry-run:
39 | @if [ ! -f ".release-env" ]; then \
40 | echo "\033[91m.release-env is required for release\033[0m";\
41 | exit 1;\
42 | fi
43 | @docker run \
44 | --rm \
45 | -e CGO_ENABLED=1 \
46 | --env-file .release-env \
47 | -v /var/run/docker.sock:/var/run/docker.sock \
48 | -v `pwd`:/go/src/$(PACKAGE_NAME) \
49 | -v `pwd`/sysroot:/sysroot \
50 | -w /go/src/$(PACKAGE_NAME) \
51 | ghcr.io/goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \
52 | release --clean --skip-publish --skip-validate
53 |
54 | .PHONY: release
55 | release:
56 | @if [ ! -f ".release-env" ]; then \
57 | echo "\033[91m.release-env is required for release\033[0m";\
58 | exit 1;\
59 | fi
60 | docker run \
61 | --rm \
62 | -e CGO_ENABLED=1 \
63 | --env-file .release-env \
64 | -v /var/run/docker.sock:/var/run/docker.sock \
65 | -v `pwd`:/go/src/$(PACKAGE_NAME) \
66 | -v `pwd`/sysroot:/sysroot \
67 | -w /go/src/$(PACKAGE_NAME) \
68 | ghcr.io/goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \
69 | release --clean
70 |
--------------------------------------------------------------------------------
/command/auth/refresh/refresh.go:
--------------------------------------------------------------------------------
1 | package refresh
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 |
8 | "github.com/MakeNowJust/heredoc"
9 | "github.com/deepsourcelabs/cli/config"
10 | "github.com/deepsourcelabs/cli/deepsource"
11 | "github.com/deepsourcelabs/cli/utils"
12 | "github.com/pterm/pterm"
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | type RefreshOptions struct{}
17 |
18 | // NewCmdRefresh handles the refreshing of authentication credentials
19 | func NewCmdRefresh() *cobra.Command {
20 | doc := heredoc.Docf(`
21 | Refresh stored authentication credentials.
22 |
23 | Authentication credentials expire after a certain amount of time.
24 |
25 | To renew the authentication credentials, use %[1]s
26 | `, utils.Yellow("deepsource auth refresh"))
27 |
28 | opts := RefreshOptions{}
29 |
30 | cmd := &cobra.Command{
31 | Use: "refresh",
32 | Short: "Refresh stored authentication credentials",
33 | Long: doc,
34 | Args: utils.NoArgs,
35 | RunE: func(cmd *cobra.Command, args []string) error {
36 | return opts.Run()
37 | },
38 | }
39 | return cmd
40 | }
41 |
42 | func (opts *RefreshOptions) Run() error {
43 | // Fetch config
44 | cfg, err := config.GetConfig()
45 | if err != nil {
46 | return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
47 | }
48 | // Checking if the user has authenticated / logged in or not
49 | if cfg.Token == "" {
50 | return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.")
51 | }
52 |
53 | // Fetching DS Client
54 | deepsource, err := deepsource.New(deepsource.ClientOpts{
55 | Token: config.Cfg.Token,
56 | HostName: config.Cfg.Host,
57 | })
58 | if err != nil {
59 | return err
60 | }
61 | ctx := context.Background()
62 | // Use the SDK to fetch the new auth data
63 | refreshedConfigData, err := deepsource.RefreshAuthCreds(ctx, cfg.Token)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | // Convert incoming config into the local CLI config format
69 | cfg.User = refreshedConfigData.User.Email
70 | cfg.Token = refreshedConfigData.Token
71 | cfg.SetTokenExpiry(refreshedConfigData.Expiry)
72 |
73 | // Having formatted the data, write it to the config file
74 | err = cfg.WriteFile()
75 | if err != nil {
76 | fmt.Println("Error in writing authentication data to a file. Exiting...")
77 | return err
78 | }
79 | pterm.Info.Println("Authentication successfully refreshed.")
80 | return nil
81 | }
82 |
--------------------------------------------------------------------------------
/configvalidator/config_validator.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/spf13/viper"
9 | )
10 |
11 | const (
12 | MAX_ALLOWED_VERSION = 1
13 | )
14 |
15 | type Result struct {
16 | Valid bool
17 | Errors []string
18 | ConfigReadError bool
19 | }
20 |
21 | // Struct to store the meta (Config) and output (Result) of config validation
22 | type ConfigValidator struct {
23 | Config DSConfig
24 | Result Result
25 | }
26 |
27 | // Entrypoint to the package `configvalidator`
28 | // Accepts DeepSource config as a parameter and validates it
29 | func (c *ConfigValidator) ValidateConfig(inputConfig []byte) Result {
30 | // Base cases
31 | c.Result.Valid = true
32 | c.Result.ConfigReadError = false
33 |
34 | // Making a "config" struct based on DSConfig to store the DeepSource config
35 | config := DSConfig{}
36 | viper.SetConfigType("toml")
37 | err := viper.ReadConfig(bytes.NewBuffer(inputConfig))
38 | if err != nil {
39 | // Error while reading config
40 | c.Result.Valid = false
41 | c.Result.Errors = append(c.Result.Errors, err.Error())
42 | c.Result.ConfigReadError = true
43 | return c.Result
44 | }
45 | // Unmarshaling the configdata into DSConfig struct
46 | err = viper.UnmarshalExact(&config)
47 | if err != nil {
48 | // Check if the error is due to invalid enabled field types
49 | // match `` * cannot parse 'analyzers[0].enabled' as bool: strconv.ParseBool: parsing "falsee": invalid syntax`
50 | if strings.Contains(err.Error(), "strconv.ParseBool") {
51 | c.Result.Valid = false
52 | c.Result.Errors = append(c.Result.Errors, "The `enabled` property should be of boolean type (true/false)")
53 | return c.Result
54 | }
55 | // Other unmarshaling errors
56 | c.Result.Valid = false
57 | c.Result.Errors = append(c.Result.Errors, fmt.Sprintf("Error while parsing config: %v", err))
58 | return c.Result
59 | }
60 | c.Config = config
61 |
62 | // Validate generic config which applies to all analyzers and transformers
63 | // Includes : Version, Exclude Patterns, Test Patterns
64 | c.validateGenericConfig()
65 |
66 | // Validate the Analyzers configuration
67 | c.validateAnalyzersConfig()
68 |
69 | // Validate the Transformers configuration
70 | c.validateTransformersConfig()
71 | return c.Result
72 | }
73 |
74 | // Utility function to push result string into the "ConfigValidator" struct
75 | func (c *ConfigValidator) pushError(errorString string) {
76 | c.Result.Errors = append(c.Result.Errors, errorString)
77 | c.Result.Valid = false
78 | }
79 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/deepsourcelabs/cli
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/AlecAivazis/survey/v2 v2.2.12
7 | github.com/DataDog/zstd v1.5.5
8 | github.com/Jeffail/gabs/v2 v2.6.1
9 | github.com/MakeNowJust/heredoc v1.0.0
10 | github.com/cli/browser v1.1.0
11 | github.com/deepsourcelabs/graphql v0.2.2
12 | github.com/fatih/color v1.12.0
13 | github.com/getsentry/sentry-go v0.6.0
14 | github.com/google/go-cmp v0.5.5
15 | github.com/owenrumney/go-sarif/v2 v2.1.0
16 | github.com/pelletier/go-toml v1.9.2
17 | github.com/pterm/pterm v0.12.23
18 | github.com/spf13/cobra v1.5.0
19 | github.com/spf13/viper v1.7.1
20 | github.com/stretchr/testify v1.7.0
21 | github.com/xeipuuv/gojsonschema v1.2.0
22 | )
23 |
24 | require (
25 | github.com/atomicgo/cursor v0.0.1 // indirect
26 | github.com/davecgh/go-spew v1.1.1 // indirect
27 | github.com/fsnotify/fsnotify v1.4.7 // indirect
28 | github.com/gookit/color v1.4.2 // indirect
29 | github.com/hashicorp/hcl v1.0.0 // indirect
30 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
31 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
32 | github.com/kr/pretty v0.3.0 // indirect
33 | github.com/magiconair/properties v1.8.1 // indirect
34 | github.com/matryer/is v1.4.0 // indirect
35 | github.com/mattn/go-colorable v0.1.8 // indirect
36 | github.com/mattn/go-isatty v0.0.14 // indirect
37 | github.com/mattn/go-runewidth v0.0.13 // indirect
38 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
39 | github.com/mitchellh/mapstructure v1.4.1 // indirect
40 | github.com/pkg/errors v0.9.1 // indirect
41 | github.com/pmezard/go-difflib v1.0.0 // indirect
42 | github.com/rivo/uniseg v0.2.0 // indirect
43 | github.com/rogpeppe/go-internal v1.8.0 // indirect
44 | github.com/spf13/afero v1.1.2 // indirect
45 | github.com/spf13/cast v1.3.0 // indirect
46 | github.com/spf13/jwalterweatherman v1.0.0 // indirect
47 | github.com/spf13/pflag v1.0.5 // indirect
48 | github.com/subosito/gotenv v1.2.0 // indirect
49 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
50 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
51 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
52 | golang.org/x/crypto v0.18.0 // indirect
53 | golang.org/x/sys v0.16.0 // indirect
54 | golang.org/x/term v0.16.0 // indirect
55 | golang.org/x/text v0.14.0 // indirect
56 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
57 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
58 | gopkg.in/ini.v1 v1.51.0 // indirect
59 | gopkg.in/yaml.v2 v2.4.0 // indirect
60 | gopkg.in/yaml.v3 v3.0.1 // indirect
61 | )
62 |
--------------------------------------------------------------------------------
/utils/remote_resolver.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type RemoteData struct {
9 | Owner string
10 | RepoName string
11 | VCSProvider string
12 | }
13 |
14 | func ResolveRemote(repoArg string) (*RemoteData, error) {
15 | var remote RemoteData
16 |
17 | // If the user supplied a --repo flag with the repo URL
18 | if repoArg != "" {
19 | repoData, err := RepoArgumentResolver(repoArg)
20 | if err != nil {
21 | return nil, err
22 | }
23 | remote.VCSProvider = repoData[0]
24 | remote.Owner = repoData[1]
25 | remote.RepoName = repoData[2]
26 | return &remote, nil
27 | }
28 |
29 | // If the user didn't pass --repo flag
30 | // Figure out list of remotes by reading git config
31 | remotesData, err := ListRemotes()
32 | if err != nil {
33 | if strings.Contains(err.Error(), "exit status 128") {
34 | fmt.Println("This repository has not been initialized with git. Please initialize it with git using `git init`")
35 | }
36 | return nil, err
37 | }
38 |
39 | // If there is only one remote, use it
40 | if len(remotesData) == 1 {
41 | for _, value := range remotesData {
42 | remote.Owner = value[0]
43 | remote.RepoName = value[1]
44 | remote.VCSProvider = value[2]
45 | }
46 | return &remote, nil
47 | }
48 |
49 | // If there are more than one remotes, give the option to user
50 | // to select the one which they want
51 | var promptOpts []string
52 | // Preparing the options to show to the user
53 | for _, value := range remotesData {
54 | promptOpts = append(promptOpts, value[3])
55 | }
56 |
57 | selectedRemote, err := SelectFromOptions("Please select the repository:", "", promptOpts)
58 | if err != nil {
59 | return nil, err
60 | }
61 |
62 | // Matching the list of remotes with the one selected by user
63 | for _, value := range remotesData {
64 | if value[3] == selectedRemote {
65 | remote.Owner = value[0]
66 | remote.RepoName = value[1]
67 | remote.VCSProvider = value[2]
68 | }
69 | }
70 | return &remote, nil
71 | }
72 |
73 | // Utility to parse the --repo flag
74 | func RepoArgumentResolver(arg string) ([]string, error) {
75 | // github.com/deepsourcelabs/cli or gh/deepsourcelabs/cli
76 |
77 | argComponents := strings.Split(arg, "/")
78 |
79 | switch argComponents[0] {
80 | case "gh", "github.com":
81 | argComponents[0] = "GITHUB"
82 | case "ghe":
83 | argComponents[0] = "GITHUB_ENTERPRISE"
84 | case "gl", "gitlab.com":
85 | argComponents[0] = "GITLAB"
86 | case "bb", "bitbucket.com":
87 | argComponents[0] = "BITBUCKET"
88 | case "bbdc":
89 | argComponents[0] = "BITBUCKET_DATACENTER"
90 | case "ads":
91 | argComponents[0] = "ADS"
92 | default:
93 | return argComponents, fmt.Errorf("VCSProvider `%s` not supported", argComponents[0])
94 | }
95 |
96 | return argComponents, nil
97 | }
98 |
--------------------------------------------------------------------------------
/command/repo/status/status.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MakeNowJust/heredoc"
8 | "github.com/deepsourcelabs/cli/config"
9 | "github.com/deepsourcelabs/cli/deepsource"
10 | "github.com/deepsourcelabs/cli/utils"
11 | "github.com/pterm/pterm"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | type RepoStatusOptions struct {
16 | RepoArg string
17 | TokenExpired bool
18 | SelectedRemote *utils.RemoteData
19 | }
20 |
21 | // NewCmdRepoStatus handles querying the activation status of the repo supplied as an arg
22 | func NewCmdRepoStatus() *cobra.Command {
23 | opts := RepoStatusOptions{
24 | RepoArg: "",
25 | TokenExpired: config.Cfg.IsExpired(),
26 | }
27 |
28 | doc := heredoc.Docf(`
29 | View the activation status for the repository.
30 |
31 | To check if the current repository is activated on DeepSource, run:
32 | %[1]s
33 |
34 | To check if a specific repository is activated on DeepSource, use the %[2]s flag:
35 | %[3]s
36 | `, utils.Cyan("deepsource repo status"), utils.Yellow("--repo"), utils.Cyan("deepsource repo status --repo repo_name"))
37 |
38 | cmd := &cobra.Command{
39 | Use: "status",
40 | Short: "View the activation status for the repository.",
41 | Long: doc,
42 | Args: utils.NoArgs,
43 | RunE: func(cmd *cobra.Command, args []string) error {
44 | return opts.Run()
45 | },
46 | }
47 |
48 | // --repo, -r flag
49 | cmd.Flags().StringVarP(&opts.RepoArg, "repo", "r", "", "Get the activation status of the specified repository")
50 | return cmd
51 | }
52 |
53 | func (opts *RepoStatusOptions) Run() (err error) {
54 | // Fetch config
55 | cfg, err := config.GetConfig()
56 | if err != nil {
57 | return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
58 | }
59 | err = cfg.VerifyAuthentication()
60 | if err != nil {
61 | return err
62 | }
63 |
64 | // Get the remote repository URL for which issues have to
65 | // be listed
66 | opts.SelectedRemote, err = utils.ResolveRemote(opts.RepoArg)
67 | if err != nil {
68 | return err
69 | }
70 | // Use the SDK to find the activation status
71 | deepsource, err := deepsource.New(deepsource.ClientOpts{
72 | Token: config.Cfg.Token,
73 | HostName: config.Cfg.Host,
74 | })
75 | if err != nil {
76 | return err
77 | }
78 | ctx := context.Background()
79 | statusResponse, err := deepsource.GetRepoStatus(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | // Check response and show corresponding output
85 | if statusResponse.Activated {
86 | pterm.Info.Println("Analysis active on DeepSource (deepsource.io)")
87 | } else {
88 | pterm.Info.Println("DeepSource analysis is currently not activated on this repository.")
89 | }
90 | return nil
91 | }
92 |
--------------------------------------------------------------------------------
/utils/fetch_remote_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | // Test data for PAT-based remote URLs
9 | var remotesPATMap = map[string][]string{
10 | "origin": {"username", "repo", "GITHUB", "username/repo"},
11 | }
12 | var remotesPATList = []string{"origin https://username:ghp_pat@github.com/username/repo (fetch)", "origin https://username:ghp_pat@github.com/username/repo (push)"}
13 |
14 | // Test data for multiple remotes with PATs
15 | var multiRemotePATMap = map[string][]string{
16 | "origin": {"username", "repo", "GITHUB", "username/repo"},
17 | "upstream": {"company", "repo", "GITHUB", "company/repo"},
18 | }
19 | var multiRemotePATList = []string{"origin https://username:ghp_pat@github.com/username/repo (fetch)", "origin https://username:ghp_pat@github.com/username/repo (push)", "upstream https://username:ghp_pat@github.com/company/repo (fetch)", "upstream https://username:ghp_pat@github.com/company/repo (push)"}
20 |
21 | // Test data for multiple remotes
22 | var multiRemoteMap = map[string][]string{
23 | "origin": {"username", "repo", "GITHUB", "username/repo"},
24 | "upstream": {"company", "repo", "GITHUB", "company/repo"},
25 | }
26 | var multiRemoteList = []string{"origin https://github.com/username/repo (fetch)", "origin https://github.com/username/repo (push)", "upstream https://github.com/company/repo (fetch)", "upstream https://github.com/company/repo (push)"}
27 |
28 | // Test data for SSH URLs
29 | var sshRemoteMap = map[string][]string{
30 | "origin": {"username", "repo", "GITHUB", "username/repo"},
31 | "upstream": {"company", "repo", "GITHUB", "company/repo"},
32 | }
33 | var sshRemoteList = []string{"origin git@github.com:username/repo.git (fetch)", "origin git@github.com:username/repo.git (push)", "upstream git@github.com:company/repo.git (fetch)", "upstream git@github.com:company/repo.git (push)"}
34 |
35 | func TestGetRemoteMap(t *testing.T) {
36 | tests := []struct {
37 | name string
38 | remotes []string
39 | want map[string][]string
40 | }{
41 | {
42 | "remote URLs with PATs",
43 | remotesPATList,
44 | remotesPATMap,
45 | },
46 | {
47 | "multiple remote URLs with PATs",
48 | multiRemotePATList,
49 | multiRemotePATMap,
50 | },
51 | {
52 | "multiple remote URLs",
53 | multiRemoteList,
54 | multiRemoteMap,
55 | },
56 | {
57 | "SSH URLs",
58 | sshRemoteList,
59 | sshRemoteMap,
60 | },
61 | }
62 |
63 | for _, tt := range tests {
64 | t.Run(tt.name, func(t *testing.T) {
65 | remoteMap, err := getRemoteMap(tt.remotes)
66 | if err != nil {
67 | t.Error(err)
68 | return
69 | }
70 |
71 | for remote := range remoteMap {
72 | got := remoteMap[remote]
73 | want := tt.want[remote]
74 |
75 | if !reflect.DeepEqual(got, want) {
76 | t.Errorf("got: %s; want: %s\n", got, want)
77 | }
78 | }
79 | })
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/utils/remote_resolver_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestResolveRemote(t *testing.T) {
11 | tests := []struct {
12 | name string
13 | repoArg string
14 | want *RemoteData
15 | wantErr bool
16 | }{
17 | {
18 | name: "valid github remote URL",
19 | repoArg: "github.com/deepsourcelabs/cli",
20 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "GITHUB"},
21 | wantErr: false,
22 | },
23 | {
24 | name: "valid github remote URL (short form)",
25 | repoArg: "gh/deepsourcelabs/cli",
26 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "GITHUB"},
27 | wantErr: false,
28 | },
29 | {
30 | name: "valid github enterprise remote URL (short form)",
31 | repoArg: "ghe/deepsourcelabs/cli",
32 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "GITHUB_ENTERPRISE"},
33 | wantErr: false,
34 | },
35 | {
36 | name: "valid gitlab remote URL",
37 | repoArg: "gitlab.com/deepsourcelabs/cli",
38 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "GITLAB"},
39 | wantErr: false,
40 | },
41 | {
42 | name: "valid gitlab remote URL (short form)",
43 | repoArg: "gl/deepsourcelabs/cli",
44 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "GITLAB"},
45 | wantErr: false,
46 | },
47 | {
48 | name: "valid bitbucket remote URL",
49 | repoArg: "bitbucket.com/deepsourcelabs/cli",
50 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "BITBUCKET"},
51 | wantErr: false,
52 | },
53 | {
54 | name: "valid bitbucket remote URL (short form)",
55 | repoArg: "bb/deepsourcelabs/cli",
56 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "BITBUCKET"},
57 | wantErr: false,
58 | },
59 | {
60 | name: "valid bitbucket datacenter remote URL (short form)",
61 | repoArg: "bbdc/deepsourcelabs/cli",
62 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "BITBUCKET_DATACENTER"},
63 | wantErr: false,
64 | },
65 | {
66 | name: "valid Azure Devops remote URL (short form)",
67 | repoArg: "ads/deepsourcelabs/cli",
68 | want: &RemoteData{Owner: "deepsourcelabs", RepoName: "cli", VCSProvider: "ADS"},
69 | wantErr: false,
70 | },
71 | {
72 | name: "invalid VCS provider",
73 | repoArg: "example.com/deepsourcelabs/cli",
74 | want: nil,
75 | wantErr: true,
76 | },
77 | }
78 |
79 | for _, tt := range tests {
80 | t.Run(tt.name, func(t *testing.T) {
81 | got, err := ResolveRemote(tt.repoArg)
82 |
83 | if tt.wantErr {
84 | assert.NotNil(t, err)
85 | } else {
86 | assert.Nil(t, err)
87 | }
88 |
89 | if !reflect.DeepEqual(got, tt.want) {
90 | t.Errorf("got: %v, want: %v\n", got, tt.want)
91 | }
92 | })
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/utils/prompt.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/AlecAivazis/survey/v2"
7 | "github.com/AlecAivazis/survey/v2/terminal"
8 | )
9 |
10 | // ==========
11 | // Useful APIs of survey library
12 | // ==========
13 |
14 | // Used for (Yes/No) questions
15 | func ConfirmFromUser(msg, helpText string) (bool, error) {
16 | response := false
17 | confirmPrompt := &survey.Confirm{
18 | Renderer: survey.Renderer{},
19 | Message: msg,
20 | Default: true,
21 | Help: helpText,
22 | }
23 |
24 | err := survey.AskOne(confirmPrompt, &response)
25 | if err != nil {
26 | return true, checkInterrupt(err)
27 | }
28 | return response, nil
29 | }
30 |
31 | // Used for Single Option Selection from Multiple Options
32 | // Being used for selecting Java version for configuring meta of Java analyzer
33 | // > * 1
34 | // * 2
35 | // * 3
36 | func SelectFromOptions(msg, helpText string, opts []string) (string, error) {
37 | var result string
38 | prompt := &survey.Select{
39 | Renderer: survey.Renderer{},
40 | Message: msg,
41 | Options: opts,
42 | Default: nil,
43 | Help: helpText,
44 | }
45 | err := survey.AskOne(prompt, &result)
46 | if err != nil {
47 | return "", checkInterrupt(err)
48 | }
49 | return result, nil
50 | }
51 |
52 | // Used for Single Line Text Input
53 | // Being used for getting "Import root" of user for configuring meta of Go analyzer
54 | func GetSingleLineInput(msg, helpText string) (string, error) {
55 | response := ""
56 | prompt := &survey.Input{
57 | Renderer: survey.Renderer{},
58 | Message: msg,
59 | Default: "",
60 | Help: helpText,
61 | }
62 |
63 | err := survey.AskOne(prompt, &response)
64 | if err != nil {
65 | return "", checkInterrupt(err)
66 | }
67 | return response, nil
68 | }
69 |
70 | // Used for multiple inputs from the displayed options
71 | // Example:
72 | // ? Which languages/tools does your project use?
73 | // > [ ] Shell
74 | // [ ] Rust
75 | // [ ] Test Coverage
76 | // [ ] Python
77 | // [ ] Go
78 | func SelectFromMultipleOptions(msg, helpText string, options []string) ([]string, error) {
79 | response := make([]string, 0)
80 | // Extracting languages and tools being used in the project for Analyzers
81 | analyzerPrompt := &survey.MultiSelect{
82 | Renderer: survey.Renderer{},
83 | Message: msg,
84 | Options: options,
85 | Help: helpText,
86 | }
87 | err := survey.AskOne(analyzerPrompt, &response, survey.WithValidator(survey.Required))
88 | if err != nil {
89 | return nil, checkInterrupt(err)
90 | }
91 | return response, nil
92 | }
93 |
94 | // Utility to check for Ctrl+C interrupts
95 | // Survey library doesn't exit on Ctrl+c interrupt. This handler helps in that.
96 | func checkInterrupt(err error) error {
97 | if err == terminal.InterruptErr {
98 | return errors.New("Interrupt received. Exiting...")
99 | }
100 | return err
101 | }
102 |
--------------------------------------------------------------------------------
/configvalidator/analyzer_config_validator_test.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestValidateAnalyzersConfig(t *testing.T) {
9 | setDummyAnalyzerTransformerData()
10 | type test struct {
11 | inputConfig string
12 | result bool
13 | }
14 |
15 | tests := map[string]test{
16 | "valid config": {
17 | inputConfig: `
18 | [[analyzers]]
19 | name = "python"
20 | enabled = true`,
21 | result: true,
22 | },
23 | "name should be a string": {
24 | inputConfig: `
25 | [[analyzers]]
26 | name = 123
27 | enabled = true`,
28 | result: false,
29 | },
30 | "`analyzers` should be an array": {
31 | inputConfig: `
32 | analyzers = "python"
33 | enabled = true`,
34 | result: false,
35 | },
36 | "atleast one analyzer should be enabled": {
37 | inputConfig: `
38 | [[analyzers]]
39 | name = "python"
40 | enabled = false`,
41 | result: false,
42 | },
43 | "name cannot be of an unsupported analyzer": {
44 | inputConfig: `
45 | [[analyzers]]
46 | name = "foobar"
47 | enabled = true`,
48 | result: false,
49 | },
50 | "analyzer with meta config": {
51 | inputConfig: `
52 | [[analyzers]]
53 | name = "python"
54 | enabled = true
55 |
56 | [analyzers.meta]
57 | max_line_length = 100
58 | skip_doc_coverage = ["module", "magic", "class"]`,
59 | result: true,
60 | },
61 | "max_line_length meta property validation": {
62 | inputConfig: `
63 | [[analyzers]]
64 | name = "python"
65 | enabled = true
66 |
67 | [analyzers.meta]
68 | max_line_length = -100`,
69 | result: false,
70 | },
71 | "valid multiple analyzers": {
72 | inputConfig: `
73 | [[analyzers]]
74 | name = "python"
75 | enabled = true
76 |
77 | [analyzers.meta]
78 | max_line_length = 100
79 |
80 | [[analyzers]]
81 | name = "test-coverage"
82 | enabled = true`,
83 | result: true,
84 | },
85 | }
86 | for testName, tc := range tests {
87 | t.Run(testName, func(t *testing.T) {
88 | testConfig, err := getConfig([]byte(tc.inputConfig))
89 | if err != nil {
90 | t.Error(err)
91 | }
92 | c := &ConfigValidator{
93 | Config: *testConfig,
94 | Result: Result{
95 | Valid: true,
96 | Errors: []string{},
97 | ConfigReadError: false,
98 | },
99 | }
100 | c.validateAnalyzersConfig()
101 | if !reflect.DeepEqual(tc.result, c.Result.Valid) {
102 | t.Errorf("expected: %v, got: %v. Error: %v", tc.result, c.Result.Valid, c.Result.Errors)
103 | }
104 | })
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/utils/fetch_analyzers_transformers.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/deepsourcelabs/cli/deepsource"
7 | "github.com/deepsourcelabs/cli/deepsource/analyzers"
8 | "github.com/deepsourcelabs/cli/deepsource/transformers"
9 | )
10 |
11 | type DeepSourceAnalyzersData struct {
12 | AnalyzerNames []string
13 | AnalyzerShortcodes []string
14 | AnalyzersMap map[string]string // Map for {analyzer name : shortcode}
15 | AnalyzersMeta []string
16 | AnalyzersMetaMap map[string]string // Map for {analyzer name: analyzer meta-schema}
17 | }
18 |
19 | type DeepSourceTransformersData struct {
20 | TransformerNames []string
21 | TransformerShortcodes []string
22 | TransformerMap map[string]string // Map for {transformer name:shortcode}
23 | }
24 |
25 | var (
26 | AnalyzersData DeepSourceAnalyzersData
27 | TransformersData DeepSourceTransformersData
28 | )
29 |
30 | var (
31 | analyzersAPIResponse []analyzers.Analyzer
32 | transformersAPIResponse []transformers.Transformer
33 | )
34 |
35 | // Get the list of all the supported analyzers and transformers with
36 | // their corresponding data like shortcode, metaschema etc.
37 | func GetAnalyzersAndTransformersData(ctx context.Context, deepsource deepsource.Client) (err error) {
38 | // Get supported analyzers and transformers data
39 | AnalyzersData.AnalyzersMap = make(map[string]string)
40 | TransformersData.TransformerMap = make(map[string]string)
41 |
42 | analyzersAPIResponse, err = deepsource.GetSupportedAnalyzers(ctx)
43 | if err != nil {
44 | return err
45 | }
46 |
47 | transformersAPIResponse, err = deepsource.GetSupportedTransformers(ctx)
48 | if err != nil {
49 | return err
50 | }
51 | parseSDKResponse()
52 | return nil
53 | }
54 |
55 | // Parses the SDK response of analyzers and transformers data into the format required
56 | // by the validator and generator package
57 | func parseSDKResponse() {
58 | analyzersMap := make(map[string]string)
59 | analyzersMetaMap := make(map[string]string)
60 | transformersMap := make(map[string]string)
61 |
62 | for _, analyzer := range analyzersAPIResponse {
63 | analyzersMap[analyzer.Name] = analyzer.Shortcode
64 | analyzersMetaMap[analyzer.Shortcode] = analyzer.MetaSchema
65 |
66 | AnalyzersData = DeepSourceAnalyzersData{
67 | AnalyzerNames: append(AnalyzersData.AnalyzerNames, analyzer.Name),
68 | AnalyzerShortcodes: append(AnalyzersData.AnalyzerShortcodes, analyzer.Shortcode),
69 | AnalyzersMap: analyzersMap,
70 | AnalyzersMeta: append(AnalyzersData.AnalyzersMeta, analyzer.MetaSchema),
71 | AnalyzersMetaMap: analyzersMetaMap,
72 | }
73 | }
74 |
75 | for _, transformer := range transformersAPIResponse {
76 | transformersMap[transformer.Name] = transformer.Shortcode
77 |
78 | TransformersData = DeepSourceTransformersData{
79 | TransformerNames: append(TransformersData.TransformerNames, transformer.Name),
80 | TransformerShortcodes: append(TransformersData.TransformerShortcodes, transformer.Shortcode),
81 | TransformerMap: transformersMap,
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/configvalidator/generic_config_validator.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "strconv"
7 |
8 | "github.com/spf13/viper"
9 | )
10 |
11 | // Generic Config :
12 | // - Version
13 | // - Exclude_Patterns
14 | // - Test_Patterns
15 |
16 | // Validates version field of the DeepSource config
17 | func (c *ConfigValidator) validateVersion() {
18 | if viper.Get("version") != nil {
19 | // Value of version must be an integer
20 | if reflect.TypeOf(viper.Get("version")).Kind().String() != "int64" {
21 | c.pushError(fmt.Sprintf("Value of `version` must be an integer. Got %s", reflect.TypeOf(viper.Get("version")).Kind().String()))
22 | return
23 | }
24 |
25 | // Should not be zero
26 | versionInt, _ := strconv.Atoi(viper.GetString("version"))
27 | if versionInt < 1 {
28 | c.pushError(fmt.Sprintf("Value for `version` cannot be less than 1. Got %d", versionInt))
29 | }
30 |
31 | // Must be less than MAX_ALLOWED VERSION
32 | if versionInt > MAX_ALLOWED_VERSION {
33 | c.pushError(fmt.Sprintf("Value for `version` cannot be greater than %d. Got %d", MAX_ALLOWED_VERSION, versionInt))
34 | }
35 | return
36 | }
37 | // if version is nil(not present in config)
38 | c.pushError("Property `version` is mandatory.")
39 | }
40 |
41 | // Validates `exclude_patterns` field of the DeepSource config
42 | func (c *ConfigValidator) validateExcludePatterns() {
43 | excludePatterns := viper.Get("exclude_patterns")
44 |
45 | // Sometimes the user doesn't add `exclude_patterns` to the code
46 | // Validate only if excludePatterns present
47 | if excludePatterns != nil {
48 | // Must be a slice of string
49 | exPatternType := reflect.TypeOf(excludePatterns).Kind().String()
50 | if exPatternType != "slice" {
51 | c.pushError(fmt.Sprintf("Value of `exclude_patterns` should be an array of strings. Found: %v", exPatternType))
52 | return
53 | }
54 |
55 | // Value of each exclude pattern can only be a string
56 | for _, ex_pattern := range c.Config.ExcludePatterns {
57 | numValue, err := strconv.Atoi(ex_pattern)
58 | if err == nil {
59 | c.pushError(fmt.Sprintf("Value of `exclude_patterns` paths can only be string. Found: %v", numValue))
60 | }
61 | }
62 | }
63 | }
64 |
65 | // Validates `test_patterns` field of the DeepSource config
66 | func (c *ConfigValidator) validateTestPatterns() {
67 | testPatterns := viper.Get("test_patterns")
68 |
69 | // Sometimes the user doesn't add `test_patterns` to the code
70 | // Validate only if testPatterns present
71 | if testPatterns != nil {
72 | // Must be a slice
73 | testPatternType := reflect.TypeOf(testPatterns).Kind().String()
74 | if testPatternType != "slice" {
75 | c.pushError(fmt.Sprintf("Value of `test_patterns` should be an array of objects. Found: %v", testPatternType))
76 | }
77 |
78 | // Value of each test pattern can only be a string
79 | for _, test_pattern := range c.Config.TestPatterns {
80 | numValue, err := strconv.Atoi(test_pattern)
81 | if err == nil {
82 | c.pushError(fmt.Sprintf("Value of `test_patterns` paths can only be string. Found: %v", numValue))
83 | }
84 | }
85 | }
86 | }
87 |
88 | // Validates generic DeepSource config
89 | func (c *ConfigValidator) validateGenericConfig() {
90 | c.validateVersion()
91 | c.validateExcludePatterns()
92 | c.validateTestPatterns()
93 | }
94 |
--------------------------------------------------------------------------------
/utils/fetch_remote.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "os/exec"
7 | "regexp"
8 | "strings"
9 | )
10 |
11 | func getRemoteMap(remoteList []string) (map[string][]string, error) {
12 | remoteMap := make(map[string][]string)
13 | for _, remoteData := range remoteList {
14 |
15 | var VCSProvider string
16 |
17 | // Split the single remote to fetch the name and URL of the remote
18 | // TLDR; Fetch "origin" and "" from "origin "
19 | remoteParams := strings.Fields(remoteData)
20 | remoteName := remoteParams[0]
21 | remoteURL := remoteParams[1]
22 |
23 | // Parsing out VCS Provider from the remote URL
24 | if strings.Contains(remoteURL, "github") {
25 | VCSProvider = "GITHUB"
26 | } else if strings.Contains(remoteURL, "gitlab") {
27 | VCSProvider = "GITLAB"
28 | } else if strings.Contains(remoteURL, "bitbucket") {
29 | VCSProvider = "BITBUCKET"
30 | } else {
31 | continue
32 | }
33 |
34 | RepoNameRegexp := regexp.MustCompile(`.+/([^/]+)(\.git)?$`)
35 |
36 | // Parsing out repository name from the remote URL using the above regex
37 | matched := RepoNameRegexp.FindStringSubmatch(remoteURL)
38 | repositoryName := strings.TrimSuffix(matched[1], ".git")
39 |
40 | var owner string
41 |
42 | // git@ ssh urls
43 | if strings.HasPrefix(remoteURL, "git@") {
44 | pathURL := strings.Split(remoteURL, ":")
45 | newPathURL := pathURL[1]
46 | u, err := url.Parse(newPathURL)
47 | if err != nil {
48 | continue
49 | }
50 | splitPath := strings.Split(u.Path, "/")
51 | owner = splitPath[0]
52 | } else if strings.HasPrefix(remoteURL, "https://") {
53 | remoteRegexp := regexp.MustCompile(`.+//(.+)/(.+)/(.+)(\.git)?$`)
54 | matched := remoteRegexp.FindStringSubmatch(remoteURL)
55 | owner = matched[2]
56 | }
57 |
58 | completeRepositoryName := fmt.Sprintf("%s/%s", owner, repositoryName)
59 | remoteMap[remoteName] = []string{owner, repositoryName, VCSProvider, completeRepositoryName}
60 | }
61 |
62 | return remoteMap, nil
63 | }
64 |
65 | // 1. Run git remote -v
66 | // 2. Parse the output and get the list of remotes
67 | // 3. Do git config --get --local remote..url
68 | // 4. Parse the urls to filter out reponame,owner,provider
69 | // 5. Send them back
70 |
71 | // Returns a map of remotes to their urls
72 | // { "origin":["reponame","owner","provider"]}
73 | // { "upstream":["reponame","owner","provider"]}
74 | func ListRemotes() (map[string][]string, error) {
75 | remoteMap := make(map[string][]string)
76 |
77 | remotes, err := runCmd("git", []string{"remote", "-v"})
78 | if err != nil {
79 | return remoteMap, err
80 | }
81 |
82 | // Split the remotes into single remote array
83 | remoteList := strings.Split(string(remotes), "\n")
84 |
85 | if len(remoteList) <= 1 {
86 | return remoteMap, fmt.Errorf("no remotes found")
87 | }
88 |
89 | // Removing the last blank element
90 | remoteList = remoteList[:len(remoteList)-1]
91 |
92 | // Get remote map
93 | remoteMap, err = getRemoteMap(remoteList)
94 | if err != nil {
95 | return remoteMap, err
96 | }
97 |
98 | return remoteMap, nil
99 | }
100 |
101 | func runCmd(command string, args []string) (string, error) {
102 | output, err := exec.Command(command, args...).Output()
103 | if err != nil {
104 | return "", err
105 | }
106 |
107 | // Removing trailing null characters
108 | return strings.TrimRight(string(output), "\000"), nil
109 | }
110 |
--------------------------------------------------------------------------------
/command/repo/view/view.go:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "strings"
8 |
9 | "github.com/MakeNowJust/heredoc"
10 | "github.com/cli/browser"
11 | "github.com/deepsourcelabs/cli/config"
12 | "github.com/deepsourcelabs/cli/deepsource"
13 | "github.com/deepsourcelabs/cli/utils"
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | var VCSMap = map[string]string{
18 | "GITHUB": "gh",
19 | "GITHUB_ENTERPRISE": "ghe",
20 | "GITLAB": "gl",
21 | "BITBUCKET": "bb",
22 | "BITBUCKET_DATACENTER": "bbdc",
23 | "ADS": "ads",
24 | }
25 |
26 | type RepoViewOptions struct {
27 | RepoArg string
28 | TokenExpired bool
29 | SelectedRemote *utils.RemoteData
30 | }
31 |
32 | func NewCmdRepoView() *cobra.Command {
33 | opts := RepoViewOptions{
34 | RepoArg: "",
35 | SelectedRemote: &utils.RemoteData{},
36 | }
37 |
38 | doc := heredoc.Docf(`
39 | Open the DeepSource dashboard of a repository.
40 |
41 | Run %[1]s to open the DeepSource dashboard inside the browser.
42 | `, utils.Cyan("deepsource repo view"))
43 |
44 | cmd := &cobra.Command{
45 | Use: "view",
46 | Short: "Open the DeepSource dashboard of a repository",
47 | Long: doc,
48 | Args: utils.NoArgs,
49 | RunE: func(cmd *cobra.Command, args []string) error {
50 | return opts.Run()
51 | },
52 | }
53 |
54 | // --repo, -r flag
55 | cmd.Flags().StringVarP(&opts.RepoArg, "repo", "r", "", "Open the DeepSource dashboard of the specified repository")
56 | return cmd
57 | }
58 |
59 | func (opts *RepoViewOptions) Run() (err error) {
60 | // Fetch config
61 | cfg, err := config.GetConfig()
62 | if err != nil {
63 | return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
64 | }
65 | err = cfg.VerifyAuthentication()
66 | if err != nil {
67 | return err
68 | }
69 |
70 | // Get the remote repository URL for which issues have to
71 | // be listed
72 | opts.SelectedRemote, err = utils.ResolveRemote(opts.RepoArg)
73 | if err != nil {
74 | return err
75 | }
76 |
77 | // Making the "isActivated" (repo status) query again just to confirm if the user has access to that repo
78 | deepsource, err := deepsource.New(deepsource.ClientOpts{
79 | Token: config.Cfg.Token,
80 | HostName: config.Cfg.Host,
81 | })
82 | if err != nil {
83 | return err
84 | }
85 | ctx := context.Background()
86 | _, err = deepsource.GetRepoStatus(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider)
87 | if err != nil {
88 | if strings.Contains(err.Error(), "Signature has expired") {
89 | return errors.New("The token has expired. Please refresh the token using the command `deepsource auth refresh`")
90 | }
91 |
92 | if strings.Contains(err.Error(), "Repository matching query does not exist") {
93 | return errors.New("Unauthorized access. Please login if you haven't using the command `deepsource auth login`")
94 | }
95 | }
96 |
97 | // If the user has access to repo, frame the full URL of the repo and open it on the
98 | // default browser
99 | VCSShortcode := VCSMap[opts.SelectedRemote.VCSProvider]
100 |
101 | // Framing the complete URL
102 | dashboardURL := fmt.Sprintf("https://%s/%s/%s/%s/", config.Cfg.Host, VCSShortcode, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName)
103 | fmt.Printf("Press Enter to open %s in your browser...", dashboardURL)
104 | fmt.Scanln()
105 | return browser.OpenURL(dashboardURL)
106 | }
107 |
--------------------------------------------------------------------------------
/configvalidator/analyzer_config_validator.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "reflect"
7 |
8 | "github.com/deepsourcelabs/cli/utils"
9 | "github.com/xeipuuv/gojsonschema"
10 | )
11 |
12 | // Analyzer Config Validator
13 | func (c *ConfigValidator) validateAnalyzersConfig() {
14 | activatedAnalyzers := make(map[string]interface{})
15 |
16 | // Analyzer array should not be empty
17 | if len(c.Config.Analyzers) == 0 {
18 | c.pushError("There must be atleast one activated `analyzer` in the config. Found: 0")
19 | }
20 |
21 | // Analyzers should be an array
22 | analyzersType := reflect.TypeOf(c.Config.Analyzers).Kind().String()
23 | if analyzersType != "slice" {
24 | c.pushError(fmt.Sprintf("Value of `analyzers` should be an array. Found: %v", analyzersType))
25 | }
26 |
27 | // Count enabled analyzers (missing enabled field defaults to true)
28 | countEnabled := 0
29 | for _, analyzer := range c.Config.Analyzers {
30 | // If enabled is not set (nil), consider it as enabled (true)
31 | // If enabled is set, use its value
32 | isEnabled := analyzer.Enabled == nil || *analyzer.Enabled
33 |
34 | if isEnabled {
35 | countEnabled++
36 | }
37 | }
38 | if countEnabled == 0 && len(c.Config.Analyzers) > 0 {
39 | c.pushError("There must be atleast one enabled `analyzer`. Found: 0")
40 | }
41 |
42 | // ==== Analyzer shortcode validation ====
43 | supported := false
44 | for _, analyzer := range c.Config.Analyzers {
45 | for _, supportedAnalyzer := range utils.AnalyzersData.AnalyzerShortcodes {
46 | if analyzer.Name == supportedAnalyzer {
47 | // Copy the meta of activated analyzer for usage in
48 | // analyzer meta validation
49 | isEnabled := analyzer.Enabled == nil || *analyzer.Enabled
50 | if isEnabled {
51 | activatedAnalyzers[analyzer.Name] = analyzer.Meta
52 | }
53 | supported = true
54 | break
55 | }
56 | }
57 | if !supported {
58 | c.pushError(fmt.Sprintf("Analyzer for \"%s\" is not supported yet.", analyzer.Name))
59 | }
60 |
61 | supported = false
62 | }
63 |
64 | // ==== Meta Schema Validation ====
65 |
66 | // Contains the meta-schema of the particular activated analyzer
67 | var analyzerMetaSchema string
68 | // Contains the user supplied meta
69 | var userActivatedSchema interface{}
70 |
71 | // Iterating over the activated analyzers and
72 | // validating the meta_schema
73 | for analyzer, meta := range activatedAnalyzers {
74 | analyzerMetaSchema = utils.AnalyzersData.AnalyzersMetaMap[analyzer]
75 | userActivatedSchema = meta
76 |
77 | // Loading the Meta Schema obtained from API
78 | schema := gojsonschema.NewStringLoader(analyzerMetaSchema)
79 | // Loading the Meta Schema of the user after converting it
80 | // into a JSON string
81 | jsonUserSchema, _ := json.Marshal(userActivatedSchema)
82 | inputMeta := gojsonschema.NewStringLoader(string(jsonUserSchema))
83 |
84 | // If there is no meta-schema, write empty object in the inputSchema
85 | if string(jsonUserSchema) == "null" {
86 | inputMeta = gojsonschema.NewStringLoader("{}")
87 | }
88 |
89 | // Validate the Meta Schema
90 | result, _ := gojsonschema.Validate(schema, inputMeta)
91 | if result.Valid() {
92 | continue
93 | }
94 | finalErrString := fmt.Sprintf("Errors found while validating meta of %s analyzer: ", analyzer)
95 | for _, err := range result.Errors() {
96 | errString := err.String()
97 | finalErrString = finalErrString + errString
98 | }
99 | c.pushError(finalErrString)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/command/config/generate/generic_input.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/AlecAivazis/survey/v2"
9 | "github.com/deepsourcelabs/cli/utils"
10 | )
11 |
12 | // ==========
13 | // Exclude Patterns Input Prompt
14 | // ==========
15 | func (o *Options) collectExcludePatterns() error {
16 | excludePatternsMsg := "Would you like to add any exclude patterns?"
17 | helpMsg := "Glob patterns of files that should not be analyzed such as auto-generated files, migrations, compatibility files."
18 |
19 | // Confirm from the user if they want to add an exclude pattern
20 | response, err := utils.ConfirmFromUser(excludePatternsMsg, helpMsg)
21 | if err != nil {
22 | return err
23 | }
24 |
25 | // If yes, keep entering patterns until they input n/N
26 | if response {
27 | err := o.inputFilePatterns("exclude", "Select exclude pattern", helpMsg)
28 | if err != nil {
29 | return err
30 | }
31 | }
32 | return nil
33 | }
34 |
35 | // ==========
36 | // Test Patterns Input Prompt
37 | // ==========
38 | func (o *Options) collectTestPatterns() error {
39 | testPatternsMsg := "Would you like to add any test patterns?"
40 | helpMsg := "Glob patterns of the test files. This helps us reduce false positives."
41 |
42 | // Confirm from the user (y/N) if he/she wants to add test patterns
43 | response, err := utils.ConfirmFromUser(testPatternsMsg, helpMsg)
44 | if err != nil {
45 | return err
46 | }
47 |
48 | // If yes, keep entering patterns until they input n/N
49 | if response {
50 | err := o.inputFilePatterns("test", "Select test pattern", helpMsg)
51 | if err != nil {
52 | return err
53 | }
54 | }
55 | return nil
56 | }
57 |
58 | // Single utility function to help in inputting test as well as exclude patterns
59 | // Keeps asking user for pattern and then confirms if they want to add more patterns
60 | // Exits when user enters No (n/N)
61 | func (o *Options) inputFilePatterns(field, msg, helpMsg string) error {
62 | // Infinite loop to keep running until user wants to stop inputting
63 | for {
64 | var filePattern string
65 |
66 | // Input the pattern
67 | filePatternsPrompt := &survey.Input{
68 | Renderer: survey.Renderer{},
69 | Message: msg,
70 | Default: "",
71 | Help: helpMsg,
72 | Suggest: getMatchingFiles,
73 | }
74 | err := survey.AskOne(filePatternsPrompt, &filePattern)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | // Having taken the input of exclude_patterns/test_pattern, append it to the Options struct
80 | if field == "test" {
81 | o.TestPatterns = append(o.TestPatterns, filePattern)
82 | } else {
83 | o.ExcludePatterns = append(o.ExcludePatterns, filePattern)
84 | }
85 |
86 | // Confirm from the user if the user wants to add more patterns
87 | // Iterating this until user says no
88 | // Here field contains : "test"/"exclude" depending upon the invoking
89 | confirmationMsg := fmt.Sprintf("Add more %s patterns?", field)
90 | response, err := utils.ConfirmFromUser(confirmationMsg, "")
91 | if err != nil {
92 | return err
93 | }
94 | if !response {
95 | break
96 | }
97 | }
98 | return nil
99 | }
100 |
101 | // Receives a filepath and returns matching dirs and files
102 | // Used for autocompleting input of "exclude_patterns" and "test_patterns"
103 | func getMatchingFiles(path string) []string {
104 | // Geting matching dirs and files using glob
105 | files, _ := filepath.Glob(path + "*")
106 | cwd, _ := os.Getwd()
107 |
108 | // Iterating over files and appending "/" to directories
109 | for index, file := range files {
110 | fileInfo, _ := os.Stat(filepath.Join(cwd, file))
111 | if fileInfo.IsDir() {
112 | files[index] = files[index] + "/"
113 | }
114 | }
115 | return files
116 | }
117 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/pelletier/go-toml"
10 | )
11 |
12 | var (
13 | configDirFn = os.UserHomeDir
14 | readFileFn = os.ReadFile
15 | )
16 |
17 | const (
18 | ConfigDirName = "/.deepsource/"
19 | ConfigFileName = "/config.toml"
20 | DefaultHostName = "deepsource.io"
21 | )
22 |
23 | type CLIConfig struct {
24 | Host string `toml:"host"`
25 | User string `toml:"user"`
26 | Token string `toml:"token"`
27 | TokenExpiresIn time.Time `toml:"token_expires_in,omitempty"`
28 | }
29 |
30 | var Cfg CLIConfig
31 |
32 | // Sets the token expiry in the desired format
33 | // Sets the token expiry in the desired format
34 | func (cfg *CLIConfig) SetTokenExpiry(str string) {
35 | t, _ := time.Parse(time.RFC3339, str)
36 | cfg.TokenExpiresIn = t.UTC()
37 | }
38 |
39 | // Checks if the token has expired or not
40 | func (cfg CLIConfig) IsExpired() bool {
41 | if cfg.TokenExpiresIn.IsZero() {
42 | return true
43 | }
44 | return time.Now().After(cfg.TokenExpiresIn)
45 | }
46 |
47 | // configDir returns the directory to store the config file.
48 | func (CLIConfig) configDir() (string, error) {
49 | home, err := configDirFn()
50 | if err != nil {
51 | return "", err
52 | }
53 | return filepath.Join(home, ConfigDirName), nil
54 | }
55 |
56 | // configPath returns the file path to the config file.
57 | func (cfg CLIConfig) configPath() (string, error) {
58 | home, err := cfg.configDir()
59 | if err != nil {
60 | return "", err
61 | }
62 | return filepath.Join(home, ConfigFileName), nil
63 | }
64 |
65 | // ReadFile reads the CLI config file.
66 | func (cfg *CLIConfig) ReadConfigFile() error {
67 | path, err := cfg.configPath()
68 | if err != nil {
69 | return err
70 | }
71 |
72 | // check if config exists
73 | _, err = os.Stat(path)
74 | if err != nil {
75 | return nil
76 | }
77 |
78 | data, err := readFileFn(path)
79 | if err != nil {
80 | return err
81 | }
82 | err = toml.Unmarshal(data, cfg)
83 | if err != nil {
84 | return err
85 | }
86 |
87 | return nil
88 | }
89 |
90 | func GetConfig() (*CLIConfig, error) {
91 | if Cfg.Token != "" {
92 | return &Cfg, nil
93 | }
94 |
95 | err := Cfg.ReadConfigFile()
96 | if err != nil {
97 | return &Cfg, err
98 | }
99 | return &Cfg, nil
100 | }
101 |
102 | // WriteFile writes the CLI config to file.
103 | func (cfg *CLIConfig) WriteFile() error {
104 | data, err := toml.Marshal(cfg)
105 | if err != nil {
106 | return err
107 | }
108 |
109 | configDir, err := cfg.configDir()
110 | if err != nil {
111 | return err
112 | }
113 |
114 | if err := os.MkdirAll(configDir, os.ModePerm); err != nil {
115 | return err
116 | }
117 |
118 | path, err := cfg.configPath()
119 | if err != nil {
120 | return err
121 | }
122 |
123 | // Create file
124 | file, err := os.Create(path)
125 | if err != nil {
126 | return err
127 | }
128 | defer file.Close()
129 |
130 | _, err = file.Write(data)
131 |
132 | return err
133 | }
134 |
135 | // Deletes the config during logging out user
136 | func (cfg *CLIConfig) Delete() error {
137 | path, err := cfg.configPath()
138 | if err != nil {
139 | return err
140 | }
141 | return os.Remove(path)
142 | }
143 |
144 | func (cfg *CLIConfig) VerifyAuthentication() error {
145 | // Checking if the user has authenticated / logged in or not
146 | if cfg.Token == "" {
147 | return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.")
148 | }
149 |
150 | // // Check if the token has already expired
151 | // if cfg.IsExpired() {
152 | // return errors.New("The authentication has expired. Run \"deepsource auth refresh\" to refresh the credentials.")
153 | // }
154 |
155 | return nil
156 | }
157 |
--------------------------------------------------------------------------------
/goreleaser.yaml:
--------------------------------------------------------------------------------
1 | project_name: deepsource
2 |
3 | before:
4 | hooks:
5 | - scripts/gen-completions.sh
6 |
7 | builds:
8 | # darwin-amd64
9 | - id: deepsource-darwin-amd64
10 | main: ./cmd/deepsource
11 | env:
12 | - CGO_ENABLED=1
13 | - CC=o64-clang
14 | - CXX=o64-clang++
15 | flags:
16 | - -tags=static_all
17 | goos:
18 | - darwin
19 | goarch:
20 | - amd64
21 | ldflags:
22 | - "-X 'main.version={{ .Version }}' -X 'main.SentryDSN={{ .Env.DEEPSOURCE_CLI_SENTRY_DSN }}'"
23 |
24 | # darwin-arm64
25 | - id: deepsource-darwin-arm64
26 | main: ./cmd/deepsource
27 | env:
28 | - CGO_ENABLED=1
29 | - CC=o64-clang
30 | - CXX=o64-clang++
31 | flags:
32 | - -tags=static_all
33 | goos:
34 | - darwin
35 | goarch:
36 | - arm64
37 | ldflags:
38 | - "-X 'main.version={{ .Version }}' -X 'main.SentryDSN={{ .Env.DEEPSOURCE_CLI_SENTRY_DSN }}'"
39 |
40 | # linux-amd64
41 | - id: deepsource-linux-amd64
42 | main: ./cmd/deepsource
43 | env:
44 | - CGO_ENABLED=1
45 | - CC=x86_64-linux-gnu-gcc
46 | - CXX=x86_64-linux-gnu-g++
47 | flags:
48 | - -tags=static_all
49 | goos:
50 | - linux
51 | goarch:
52 | - amd64
53 | ldflags:
54 | - "-X 'main.version={{ .Version }}' -X 'main.SentryDSN={{ .Env.DEEPSOURCE_CLI_SENTRY_DSN }}'"
55 |
56 | # linux-arm64
57 | - id: deepsource-linux-arm64
58 | main: ./cmd/deepsource
59 | env:
60 | - CGO_ENABLED=1
61 | - CC=aarch64-linux-gnu-gcc
62 | - CXX=aarch64-linux-gnu-g++
63 | flags:
64 | - -tags=static_all
65 | goos:
66 | - linux
67 | goarch:
68 | - arm64
69 | ldflags:
70 | - "-X 'main.version={{ .Version }}' -X 'main.SentryDSN={{ .Env.DEEPSOURCE_CLI_SENTRY_DSN }}'"
71 |
72 | # windows-amd64
73 | - id: "windows-deepsource"
74 | main: ./cmd/deepsource
75 | env:
76 | - CGO_ENABLED=1
77 | - CC=x86_64-w64-mingw32-gcc
78 | - CXX=x86_64-w64-mingw32-g++
79 | flags:
80 | - -tags=static_all
81 | goos:
82 | - windows
83 | goarch:
84 | - amd64
85 | ldflags:
86 | - buildmode=exe
87 | - "-X 'main.version={{ .Version }}' -X 'main.SentryDSN={{ .Env.DEEPSOURCE_CLI_SENTRY_DSN }}'"
88 |
89 | archives:
90 | - id: arch_rename
91 | builds:
92 | - deepsource-darwin-amd64
93 | - deepsource-linux-amd64
94 | - windows-deepsource
95 | name_template: >-
96 | deepsource_{{ .Version }}_{{ .Os }}_
97 | {{- if eq .Arch "amd64" }}x86_64
98 | {{- else if eq .Arch "386" }}i386
99 | {{- else }}{{ .Arch }}{{ end }}
100 | files:
101 | - completions/*
102 | - id: default
103 | name_template: >-
104 | deepsource_{{ .Version }}_{{ .Os }}_{{ .Arch }}
105 | files:
106 | - completions/*
107 | checksum:
108 | name_template: 'checksums.txt'
109 | snapshot:
110 | name_template: "{{ .Tag }}-next"
111 | changelog:
112 | sort: asc
113 | filters:
114 | exclude:
115 | - '^tests:'
116 |
117 | brews:
118 | - tap:
119 | owner: DeepSourceCorp
120 | name: homebrew-cli
121 | branch: cli-release
122 | token: "{{ .Env.HOMEBREW_TOKEN }}"
123 | ids:
124 | - arch_rename
125 | commit_author:
126 | name: deepsourcebot
127 | email: bot@deepsource.io
128 | homepage: "https://github.com/deepsourcelabs/cli"
129 | description: "Command line interface to DeepSource"
130 | license: "BSD 2-Clause Simplified License"
131 | install: |
132 | bin.install "deepsource"
133 | bash_completion.install "completions/deepsource.bash" => "deepsource"
134 | zsh_completion.install "completions/deepsource.zsh" => "_deepsource"
135 | fish_completion.install "completions/deepsource.fish"
136 | skip_upload: auto
137 |
--------------------------------------------------------------------------------
/deepsource/issues/queries/list_issues.go:
--------------------------------------------------------------------------------
1 | // Lists the issues reported in the whole project
2 | package issues
3 |
4 | import (
5 | "context"
6 | "fmt"
7 |
8 | "github.com/deepsourcelabs/cli/deepsource/issues"
9 | "github.com/deepsourcelabs/graphql"
10 | )
11 |
12 | const fetchAllIssuesQuery = `query GetAllIssues(
13 | $name: String!
14 | $owner: String!
15 | $provider: VCSProvider!
16 | $limit: Int!
17 | ) {
18 | repository(name: $name, login: $owner, vcsProvider: $provider, ) {
19 | issues(first: $limit) {
20 | edges {
21 | node {
22 | occurrences {
23 | edges {
24 | node {
25 | path
26 | beginLine
27 | endLine
28 | issue {
29 | title
30 | shortcode
31 | category
32 | severity
33 | isRecommended
34 | analyzer {
35 | name
36 | shortcode
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }`
47 |
48 | type IssuesListParams struct {
49 | Owner string
50 | RepoName string
51 | Provider string
52 | Limit int
53 | }
54 |
55 | type IssuesListRequest struct {
56 | Params IssuesListParams
57 | }
58 |
59 | type IssuesListResponse struct {
60 | Repository struct {
61 | Issues struct {
62 | Edges []struct {
63 | Node struct {
64 | Occurrences struct {
65 | Edges []struct {
66 | Node struct {
67 | Path string `json:"path"`
68 | BeginLine int `json:"beginLine"`
69 | EndLine int `json:"endLine"`
70 | Issue struct {
71 | Title string `json:"title"`
72 | Shortcode string `json:"shortcode"`
73 | Category string `json:"category"`
74 | Severity string `json:"severity"`
75 | IsRecommended bool `json:"isRecommended"`
76 | Analyzer struct {
77 | Name string `json:"name"`
78 | Shortcode string `json:"shortcode"`
79 | } `json:"analyzer"`
80 | } `json:"issue"`
81 | } `json:"node"`
82 | } `json:"edges"`
83 | } `json:"occurrences"`
84 | } `json:"node"`
85 | } `json:"edges"`
86 | } `json:"issues"`
87 | } `json:"repository"`
88 | }
89 |
90 | func (i IssuesListRequest) Do(ctx context.Context, client IGQLClient) ([]issues.Issue, error) {
91 | req := graphql.NewRequest(fetchAllIssuesQuery)
92 | req.Var("name", i.Params.RepoName)
93 | req.Var("owner", i.Params.Owner)
94 | req.Var("provider", i.Params.Provider)
95 | req.Var("limit", i.Params.Limit)
96 |
97 | // set header fields
98 | req.Header.Set("Cache-Control", "no-cache")
99 | // Adding PAT as a header for authentication
100 | tokenHeader := fmt.Sprintf("Bearer %s", client.GetToken())
101 | req.Header.Add("Authorization", tokenHeader)
102 |
103 | // run it and capture the response
104 | var respData IssuesListResponse
105 | if err := client.GQL().Run(ctx, req, &respData); err != nil {
106 | return nil, err
107 | }
108 |
109 | issuesData := []issues.Issue{}
110 | for _, edge := range respData.Repository.Issues.Edges {
111 | if len(edge.Node.Occurrences.Edges) == 0 {
112 | continue
113 | }
114 |
115 | for _, occurenceEdge := range edge.Node.Occurrences.Edges {
116 | issueData := issues.Issue{
117 | IssueText: occurenceEdge.Node.Issue.Title,
118 | IssueCode: occurenceEdge.Node.Issue.Shortcode,
119 | IssueCategory: occurenceEdge.Node.Issue.Category,
120 | IssueSeverity: occurenceEdge.Node.Issue.Severity,
121 | Location: issues.Location{
122 | Path: occurenceEdge.Node.Path,
123 | Position: issues.Position{
124 | BeginLine: occurenceEdge.Node.BeginLine,
125 | EndLine: occurenceEdge.Node.EndLine,
126 | },
127 | },
128 | Analyzer: issues.AnalyzerMeta{
129 | Shortcode: occurenceEdge.Node.Issue.Analyzer.Shortcode,
130 | },
131 | }
132 | issuesData = append(issuesData, issueData)
133 | }
134 | }
135 |
136 | return issuesData, nil
137 | }
138 |
--------------------------------------------------------------------------------
/command/auth/login/login_flow.go:
--------------------------------------------------------------------------------
1 | package login
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "os/user"
8 | "time"
9 |
10 | "github.com/cli/browser"
11 | "github.com/deepsourcelabs/cli/config"
12 | "github.com/deepsourcelabs/cli/deepsource"
13 | "github.com/deepsourcelabs/cli/deepsource/auth"
14 | "github.com/fatih/color"
15 | )
16 |
17 | // Starts the login flow for the CLI
18 | func (opts *LoginOptions) startLoginFlow(cfg *config.CLIConfig) error {
19 | // Register the device and get a device code through the response
20 | ctx := context.Background()
21 | deviceRegistrationResponse, err := registerDevice(ctx)
22 | if err != nil {
23 | return err
24 | }
25 |
26 | // Print the user code and the permission to open browser at verificationURI
27 | c := color.New(color.FgCyan, color.Bold)
28 | c.Printf("Please copy your one-time code: %s\n", deviceRegistrationResponse.UserCode)
29 | c.Printf("Press enter to open deepsource.io in your browser...")
30 | fmt.Scanln()
31 |
32 | // Having received the user code, open the browser at verificationURIComplete
33 | err = browser.OpenURL(deviceRegistrationResponse.VerificationURIComplete)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | // Fetch the PAT using the device registration resonse
39 | var tokenData *auth.PAT
40 | tokenData, opts.AuthTimedOut, err = fetchPAT(ctx, deviceRegistrationResponse)
41 | if err != nil {
42 | return err
43 | }
44 |
45 | // Check if it was a success poll or the Auth timed out
46 | if opts.AuthTimedOut {
47 | return fmt.Errorf("Authentication timed out")
48 | }
49 |
50 | // Storing the useful data for future reference and usage
51 | // in a global config object (Cfg)
52 | cfg.User = tokenData.User.Email
53 | cfg.Token = tokenData.Token
54 | cfg.SetTokenExpiry(tokenData.Expiry)
55 |
56 | // Having stored the data in the global Cfg object, write it into the config file present in the local filesystem
57 | err = cfg.WriteFile()
58 | if err != nil {
59 | return fmt.Errorf("Error in writing authentication data to a file. Exiting...")
60 | }
61 | return nil
62 | }
63 |
64 | func registerDevice(ctx context.Context) (*auth.Device, error) {
65 | // Fetching DeepSource client in order to interact with SDK
66 | deepsource, err := deepsource.New(deepsource.ClientOpts{
67 | Token: config.Cfg.Token,
68 | HostName: config.Cfg.Host,
69 | })
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | // Send a mutation to register device and get the device code
75 | res, err := deepsource.RegisterDevice(ctx)
76 | if err != nil {
77 | return nil, err
78 | }
79 | return res, nil
80 | }
81 |
82 | func fetchPAT(ctx context.Context, deviceRegistrationData *auth.Device) (*auth.PAT, bool, error) {
83 | var tokenData *auth.PAT
84 | var err error
85 | defaultUserName := "user"
86 | defaultHostName := "host"
87 | userName := ""
88 | authTimedOut := true
89 |
90 | /* ======================================================================= */
91 | // The username and hostname to add in the description for the PAT request
92 | /* ======================================================================= */
93 | userData, err := user.Current()
94 | if err != nil {
95 | userName = defaultUserName
96 | } else {
97 | userName = userData.Username
98 | }
99 |
100 | hostName, err := os.Hostname()
101 | if err != nil {
102 | hostName = defaultHostName
103 | }
104 | userDescription := fmt.Sprintf("CLI PAT for %s@%s", userName, hostName)
105 |
106 | // Fetching DeepSource client in order to interact with SDK
107 | deepsource, err := deepsource.New(deepsource.ClientOpts{
108 | Token: config.Cfg.Token,
109 | HostName: config.Cfg.Host,
110 | })
111 | if err != nil {
112 | return nil, authTimedOut, err
113 | }
114 |
115 | // Keep polling the mutation at a certain interval till the expiry timeperiod
116 | ticker := time.NewTicker(time.Duration(deviceRegistrationData.Interval) * time.Second)
117 | pollStartTime := time.Now()
118 |
119 | // Polling for fetching PAT
120 | func() {
121 | for range ticker.C {
122 | tokenData, err = deepsource.Login(ctx, deviceRegistrationData.Code, userDescription)
123 | if err == nil {
124 | authTimedOut = false
125 | return
126 | }
127 | timeElapsed := time.Since(pollStartTime)
128 | if timeElapsed >= time.Duration(deviceRegistrationData.ExpiresIn)*time.Second {
129 | authTimedOut = true
130 | return
131 | }
132 | }
133 | }()
134 |
135 | return tokenData, authTimedOut, nil
136 | }
137 |
--------------------------------------------------------------------------------
/deepsource/issues/queries/list_file_issues.go:
--------------------------------------------------------------------------------
1 | // Lists the issues reported in a single file mentioned by the user
2 | package issues
3 |
4 | import (
5 | "context"
6 | "fmt"
7 |
8 | "github.com/deepsourcelabs/cli/deepsource/issues"
9 | "github.com/deepsourcelabs/graphql"
10 | )
11 |
12 | // Query to fetch issues for a certain file specified by the user
13 | const fetchFileIssuesQuery = `query GetIssuesForPath(
14 | $name: String!
15 | $owner: String!
16 | $provider: VCSProvider!
17 | $limit: Int!
18 | $filepath: String!
19 | ) {
20 | repository(name: $name, login: $owner, vcsProvider: $provider) {
21 | issues(first: $limit, path:$filepath) {
22 | edges {
23 | node {
24 | issue {
25 | shortcode
26 | }
27 | occurrences {
28 | edges {
29 | node {
30 | path
31 | beginLine
32 | endLine
33 | issue {
34 | title
35 | shortcode
36 | category
37 | isRecommended
38 | analyzer {
39 | name
40 | shortcode
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | `
52 |
53 | type FileIssuesListParams struct {
54 | Owner string
55 | RepoName string
56 | Provider string
57 | FilePath string
58 | Limit int
59 | }
60 |
61 | // Request struct
62 | type FileIssuesListRequest struct {
63 | Params FileIssuesListParams
64 | }
65 |
66 | // Response struct
67 | type FileIssuesResponse struct {
68 | Repository struct {
69 | Issues struct {
70 | Edges []struct {
71 | Node struct {
72 | Occurrences struct {
73 | Edges []struct {
74 | Node struct {
75 | Path string `json:"path"`
76 | BeginLine int `json:"beginLine"`
77 | EndLine int `json:"endLine"`
78 | Issue struct {
79 | Title string `json:"title"`
80 | Shortcode string `json:"shortcode"`
81 | Category string `json:"category"`
82 | IsRecommended bool `json:"isRecommended"`
83 | Analyzer struct {
84 | Name string `json:"name"`
85 | Shortcode string `json:"shortcode"`
86 | } `json:"analyzer"`
87 | } `json:"issue"`
88 | } `json:"node"`
89 | } `json:"edges"`
90 | } `json:"occurrences"`
91 | } `json:"node"`
92 | } `json:"edges"`
93 | } `json:"issues"`
94 | } `json:"repository"`
95 | }
96 |
97 | // GraphQL client interface
98 | type IGQLClient interface {
99 | GQL() *graphql.Client
100 | GetToken() string
101 | }
102 |
103 | func (f FileIssuesListRequest) Do(ctx context.Context, client IGQLClient) ([]issues.Issue, error) {
104 | req := graphql.NewRequest(fetchFileIssuesQuery)
105 | req.Header.Set("Cache-Control", "no-cache")
106 |
107 | req.Var("name", f.Params.RepoName)
108 | req.Var("owner", f.Params.Owner)
109 | req.Var("provider", f.Params.Provider)
110 | req.Var("path", f.Params.FilePath)
111 | req.Var("limit", f.Params.Limit)
112 |
113 | // set header fields
114 | req.Header.Set("Cache-Control", "no-cache")
115 |
116 | // Adding token as header for auth
117 | tokenHeader := fmt.Sprintf("Bearer %s", client.GetToken())
118 | req.Header.Add("Authorization", tokenHeader)
119 |
120 | // run it and capture the response
121 | var respData FileIssuesResponse
122 | if err := client.GQL().Run(ctx, req, &respData); err != nil {
123 | return nil, err
124 | }
125 |
126 | // Formatting the query response w.r.t the output format of the SDK as specified in `issues_list.go`
127 | issuesData := []issues.Issue{}
128 | issueData := issues.Issue{}
129 | for _, edge := range respData.Repository.Issues.Edges {
130 | if len(edge.Node.Occurrences.Edges) == 0 {
131 | continue
132 | }
133 |
134 | for _, occurenceEdge := range edge.Node.Occurrences.Edges {
135 | // Check if the path matches the one entered as a flag in the command
136 | if occurenceEdge.Node.Path == f.Params.FilePath {
137 | issueData = issues.Issue{
138 | IssueText: occurenceEdge.Node.Issue.Title,
139 | IssueCode: occurenceEdge.Node.Issue.Shortcode,
140 | Location: issues.Location{
141 | Path: occurenceEdge.Node.Path,
142 | Position: issues.Position{
143 | BeginLine: occurenceEdge.Node.BeginLine,
144 | EndLine: occurenceEdge.Node.EndLine,
145 | },
146 | },
147 | Analyzer: issues.AnalyzerMeta{
148 | Shortcode: occurenceEdge.Node.Issue.Analyzer.Shortcode,
149 | },
150 | }
151 | }
152 | issuesData = append(issuesData, issueData)
153 | }
154 | }
155 |
156 | return issuesData, nil
157 | }
158 |
--------------------------------------------------------------------------------
/command/auth/login/login.go:
--------------------------------------------------------------------------------
1 | package login
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/MakeNowJust/heredoc"
7 | "github.com/deepsourcelabs/cli/config"
8 | "github.com/deepsourcelabs/cli/utils"
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | var accountTypes = []string{"DeepSource (deepsource.io)", "DeepSource Enterprise"}
13 |
14 | // LoginOptions hold the metadata related to login operation
15 | type LoginOptions struct {
16 | AuthTimedOut bool
17 | TokenExpired bool
18 | User string
19 | HostName string
20 | Interactive bool
21 | PAT string
22 | }
23 |
24 | // NewCmdLogin handles the login functionality for the CLI
25 | func NewCmdLogin() *cobra.Command {
26 | doc := heredoc.Docf(`
27 | Log in to DeepSource using the CLI.
28 |
29 | The default authentication mode is a browser-based login flow.
30 | After completion, an authentication token will be stored internally.
31 |
32 | Use %[1]s to pass in a token on standard input, for example:
33 | %[2]s
34 |
35 | Use %[3]s to authenticate with a specific DeepSource instance, for example:
36 | %[4]s
37 | `, utils.Yellow("--with-token"), utils.Cyan("deepsource auth login --with-token dsp_abcd"), utils.Yellow("--hostname"), utils.Cyan("deepsource auth login --hostname my_instance"))
38 |
39 | opts := LoginOptions{
40 | AuthTimedOut: false,
41 | TokenExpired: true,
42 | User: "",
43 | HostName: "",
44 | }
45 |
46 | cmd := &cobra.Command{
47 | Use: "login",
48 | Short: "Log in to DeepSource using Command Line Interface",
49 | Long: doc,
50 | Args: utils.NoArgs,
51 | RunE: func(cmd *cobra.Command, args []string) error {
52 | return opts.Run()
53 | },
54 | }
55 |
56 | // --host, -h flag
57 | cmd.Flags().StringVar(&opts.HostName, "hostname", "", "Authenticate with a specific DeepSource instance")
58 | cmd.Flags().BoolVarP(&opts.Interactive, "interactive", "i", false, "Interactive login prompt for authenticating with DeepSource")
59 | cmd.Flags().StringVar(&opts.PAT, "with-token", "", "Personal Access Token (PAT) for DeepSource")
60 |
61 | return cmd
62 | }
63 |
64 | // Run executes the auth command and starts the login flow if not already authenticated
65 | func (opts *LoginOptions) Run() (err error) {
66 | // Fetch config
67 | cfg, _ := config.GetConfig()
68 | opts.User = cfg.User
69 | opts.TokenExpired = cfg.IsExpired()
70 |
71 | // Login using the interactive mode
72 | if opts.Interactive {
73 | err = opts.handleInteractiveLogin()
74 | if err != nil {
75 | return err
76 | }
77 | }
78 |
79 | // Checking if the user passed a hostname. If yes, storing it in the config
80 | // Else using the default hostname (deepsource.io)
81 | if opts.HostName != "" {
82 | cfg.Host = opts.HostName
83 | } else {
84 | cfg.Host = config.DefaultHostName
85 | }
86 |
87 | // Before starting the login workflow, check here for two conditions:
88 | // Condition 1 : If the token has expired, display a message about it and re-authenticate user
89 | // Condition 2 : If the token has not expired,does the user want to re-authenticate?
90 |
91 | // Checking for condition 1
92 | if !opts.TokenExpired {
93 | // The user is already logged in, confirm re-authentication.
94 | msg := fmt.Sprintf("You're already logged into DeepSource as %s. Do you want to re-authenticate?", opts.User)
95 | response, err := utils.ConfirmFromUser(msg, "")
96 | if err != nil {
97 | return fmt.Errorf("Error in fetching response. Please try again.")
98 | }
99 | // If the response is No, it implies that the user doesn't want to re-authenticate
100 | // In this case, just exit
101 | if !response {
102 | return nil
103 | }
104 | }
105 |
106 | // If PAT is passed, start the login flow through PAT
107 | if opts.PAT != "" {
108 | return opts.startPATLoginFlow(cfg, opts.PAT)
109 | }
110 |
111 | // Condition 2
112 | // `startLoginFlow` implements the authentication flow for the CLI
113 | return opts.startLoginFlow(cfg)
114 | }
115 |
116 | func (opts *LoginOptions) handleInteractiveLogin() error {
117 | // Prompt messages and help texts
118 | loginPromptMessage := "Which account do you want to login into?"
119 | loginPromptHelpText := "Select the type of account you want to authenticate"
120 | hostPromptMessage := "Please enter the hostname:"
121 | hostPromptHelpText := "The hostname of the DeepSource instance to authenticate with"
122 |
123 | // Display prompt to user
124 | loginType, err := utils.SelectFromOptions(loginPromptMessage, loginPromptHelpText, accountTypes)
125 | if err != nil {
126 | return err
127 | }
128 | // Prompt the user for hostname only in the case of on-premise
129 | if loginType == "DeepSource Enterprise" {
130 | opts.HostName, err = utils.GetSingleLineInput(hostPromptMessage, hostPromptHelpText)
131 | if err != nil {
132 | return err
133 | }
134 | }
135 |
136 | return nil
137 | }
138 |
--------------------------------------------------------------------------------
/utils/fetch_oidc_token.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "os"
8 | )
9 |
10 | var (
11 | DEEPSOURCE_AUDIENCE = "DeepSource"
12 | ALLOWED_PROVIDERS = map[string]bool{
13 | "github-actions": true,
14 | }
15 | )
16 |
17 | // FetchOIDCTokenFromProvider fetches the OIDC token from the OIDC token provider.
18 | // It takes the request ID and the request URL as input and returns the OIDC token as a string.
19 | func FetchOIDCTokenFromProvider(requestId, requestUrl string) (string, error) {
20 | // requestid is the bearer token that needs to be sent to the request url
21 | req, err := http.NewRequest("GET", requestUrl, nil)
22 | if err != nil {
23 | return "", err
24 | }
25 | req.Header.Set("Authorization", "Bearer "+requestId)
26 | // set the expected audiences as the audience parameter
27 | q := req.URL.Query()
28 | q.Set("audience", DEEPSOURCE_AUDIENCE)
29 | req.URL.RawQuery = q.Encode()
30 |
31 | // send the request
32 | client := &http.Client{}
33 | resp, err := client.Do(req)
34 | if err != nil {
35 | return "", err
36 | }
37 | defer resp.Body.Close()
38 |
39 | // check if the response is 200
40 | if resp.StatusCode != http.StatusOK {
41 | return "", fmt.Errorf("failed to fetch OIDC token: %s", resp.Status)
42 | }
43 |
44 | // extract the token from the json response. The token is sent under the key `value`
45 | // and the response is a json object
46 | var tokenResponse struct {
47 | Value string `json:"value"`
48 | }
49 | if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
50 | return "", err
51 | }
52 | // check if the token is empty
53 | if tokenResponse.Value == "" {
54 | return "", fmt.Errorf("failed to fetch OIDC token: empty token")
55 | }
56 | // return the token
57 | return tokenResponse.Value, nil
58 | }
59 |
60 | // ExchangeOIDCTokenForTempDSN exchanges the OIDC token for a temporary DSN.
61 | // It sends the OIDC token to the respective DeepSource API endpoint and returns the temp DSN as string.
62 | func ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider string) (string, error) {
63 | apiEndpoint := fmt.Sprintf("%s/services/oidc/%s/", dsEndpoint, provider)
64 | req, err := http.NewRequest("POST", apiEndpoint, nil)
65 | if err != nil {
66 | return "", err
67 | }
68 | req.Header.Set("Authorization", "Bearer "+oidcToken)
69 |
70 | type ExchangeResponse struct {
71 | DSN string `json:"access_token"`
72 | }
73 | resp, err := http.DefaultClient.Do(req)
74 | if err != nil {
75 | return "", err
76 | }
77 | defer resp.Body.Close()
78 | if resp.StatusCode != http.StatusOK {
79 | return "", fmt.Errorf("failed to exchange OIDC token for DSN: %s", resp.Status)
80 | }
81 | var exchangeResponse ExchangeResponse
82 | if err := json.NewDecoder(resp.Body).Decode(&exchangeResponse); err != nil {
83 | return "", err
84 | }
85 | // check if the token is empty
86 | if exchangeResponse.DSN == "" {
87 | return "", fmt.Errorf("failed to exchange OIDC token for DSN: empty token")
88 | }
89 | // return the token
90 | return exchangeResponse.DSN, nil
91 | }
92 |
93 | func GetDSNFromOIDC(requestId, requestUrl, dsEndpoint, provider string) (string, error) {
94 | // infer provider from environment variables.
95 | // Github actions sets the GITHUB_ACTIONS environment variable to true by default.
96 | if os.Getenv("GITHUB_ACTIONS") == "true" {
97 | provider = "github-actions"
98 | }
99 |
100 | if dsEndpoint == "" {
101 | return "", fmt.Errorf("--deepsource-host-endpoint can not be empty")
102 | }
103 |
104 | if provider == "" {
105 | return "", fmt.Errorf("--oidc-provider can not be empty")
106 | }
107 |
108 | isSupported := ALLOWED_PROVIDERS[provider]
109 | if !isSupported {
110 | return "", fmt.Errorf("provider %s is not supported for OIDC Token exchange (Supported Providers: %v)", provider, ALLOWED_PROVIDERS)
111 | }
112 | if requestId == "" || requestUrl == "" {
113 | var foundIDToken, foundRequestURL bool
114 | // try to fetch the token from the environment variables.
115 | // skipcq: CRT-A0014
116 | switch provider {
117 | case "github-actions":
118 | requestId, foundIDToken = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
119 | requestUrl, foundRequestURL = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_URL")
120 | if !(foundIDToken && foundRequestURL) {
121 | errMsg := `failed to fetch "ACTIONS_ID_TOKEN_REQUEST_TOKEN" and "ACTIONS_ID_TOKEN_REQUEST_URL" from environment variables. Please make sure you are running this in a GitHub Actions environment with the required permissions. Or, use '--oidc-request-token' and '--oidc-request-url' flags to pass the token and request URL`
122 | return "", fmt.Errorf("%s", errMsg)
123 | }
124 | }
125 | }
126 | oidcToken, err := FetchOIDCTokenFromProvider(requestId, requestUrl)
127 | if err != nil {
128 | return "", err
129 | }
130 | tempDSN, err := ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider)
131 | if err != nil {
132 | return "", err
133 | }
134 | return tempDSN, nil
135 | }
136 |
--------------------------------------------------------------------------------
/command/config/generate/generate.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/MakeNowJust/heredoc"
10 | "github.com/deepsourcelabs/cli/config"
11 | "github.com/deepsourcelabs/cli/utils"
12 | "github.com/fatih/color"
13 | toml "github.com/pelletier/go-toml"
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | // Options holds the metadata.
18 | type Options struct {
19 | ActivatedAnalyzers []string // Analyzers activated by user
20 | AnalyzerMetaMap map[string][]AnalyzerMetadata
21 | ActivatedTransformers []string // Transformers activated by the user
22 | ExcludePatterns []string
23 | TestPatterns []string
24 | GeneratedConfig string
25 | }
26 |
27 | // NewCmdConfigGenerate handles the generation of DeepSource config based on user inputs
28 | func NewCmdConfigGenerate() *cobra.Command {
29 | o := Options{}
30 |
31 | home, _ := os.UserHomeDir()
32 | doc := heredoc.Docf(`
33 | Generate config for the DeepSource CLI.
34 |
35 | Configs are stored in: %[1]s
36 | `, utils.Cyan(filepath.Join(home, "deepsource", "config.toml")))
37 |
38 | cmd := &cobra.Command{
39 | Use: "generate",
40 | Short: "Generate config for DeepSource",
41 | Long: doc,
42 | Args: utils.NoArgs,
43 | RunE: func(cmd *cobra.Command, args []string) error {
44 | return o.Run()
45 | },
46 | }
47 | return cmd
48 | }
49 |
50 | // Run executes the command.
51 | func (o *Options) Run() error {
52 | // Fetch config
53 | cfg, err := config.GetConfig()
54 | if err != nil {
55 | return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
56 | }
57 | err = cfg.VerifyAuthentication()
58 | if err != nil {
59 | return err
60 | }
61 |
62 | // Step 1: Collect user input
63 | err = o.collectUserInput()
64 | if err != nil {
65 | fmt.Println("\nError occured while collecting input.Exiting...")
66 | return err
67 | }
68 |
69 | // Step 2: Generates config based on user input
70 | err = o.generateDeepSourceConfig()
71 | if err != nil {
72 | fmt.Println("\nError occured while generating config from input.Exiting...")
73 | return err
74 | }
75 |
76 | // Step 3: Write the generated config to a file
77 | err = o.writeConfigToFile()
78 | if err != nil {
79 | fmt.Println("\nError while writing config to project directory. Exiting...")
80 | return err
81 | }
82 |
83 | // Step 4: If everything is successfull, print the success message
84 | cwd, _ := os.Getwd()
85 | c := color.New(color.FgGreen)
86 | successOutput := fmt.Sprintf("\nSuccessfully generated DeepSource config file at %s/.deepsource.toml", cwd)
87 | c.Println(successOutput)
88 |
89 | return nil
90 | }
91 |
92 | // Generates DeepSource config based on the inputs from the user in Options struct
93 | func (o *Options) generateDeepSourceConfig() error {
94 | // Copying version, exclude_patterns and test_patterns into the DSConfig based structure
95 | config := DSConfig{
96 | Version: DEEPSOURCE_TOML_VERSION,
97 | ExcludePatterns: o.ExcludePatterns,
98 | TestPatterns: o.TestPatterns,
99 | }
100 |
101 | // Copying activated analyzers from Options struct to DSConfig based "config" struct
102 | for _, analyzer := range o.ActivatedAnalyzers {
103 | // Configuring the analyzer meta data
104 | metaMap := make(map[string]interface{})
105 | if o.AnalyzerMetaMap[analyzer] != nil {
106 | for _, meta := range o.AnalyzerMetaMap[analyzer] {
107 | metaMap[meta.FieldName] = meta.UserInput
108 | }
109 | }
110 |
111 | activatedAnalyzerData := Analyzer{
112 | Name: utils.AnalyzersData.AnalyzersMap[analyzer],
113 | Enabled: true,
114 | }
115 | if len(metaMap) != 0 {
116 | activatedAnalyzerData.Meta = metaMap
117 | }
118 | config.Analyzers = append(config.Analyzers, activatedAnalyzerData)
119 | }
120 |
121 | // Copying activated transformers from Options struct to DSConfig based "config" struct
122 | for _, transformer := range o.ActivatedTransformers {
123 | config.Transformers = append(config.Transformers, Transformer{
124 | Name: utils.TransformersData.TransformerMap[transformer],
125 | Enabled: true,
126 | })
127 | }
128 |
129 | // Encoding the DSConfig based "config" struct to TOML
130 | // and storing in GeneratedConfig of Options struct
131 | var buf bytes.Buffer
132 | err := toml.NewEncoder(&buf).Order(toml.OrderPreserve).Encode(config)
133 | if err != nil {
134 | return err
135 | }
136 | // Convert the TOML encoded buffer to string
137 | o.GeneratedConfig = buf.String()
138 | return nil
139 | }
140 |
141 | // Writes the generated TOML config into a file
142 | func (o *Options) writeConfigToFile() error {
143 | // Creating file
144 | cwd, _ := os.Getwd()
145 | f, err := os.Create(filepath.Join(cwd, ".deepsource.toml"))
146 | if err != nil {
147 | return err
148 | }
149 | defer f.Close()
150 |
151 | // Writing the string to the file
152 | _, writeError := f.WriteString(o.GeneratedConfig)
153 | if writeError != nil {
154 | return writeError
155 | }
156 | return nil
157 | }
158 |
--------------------------------------------------------------------------------
/configvalidator/generic_config_validator_test.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "bytes"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/spf13/viper"
9 | )
10 |
11 | func TestValidateVersion(t *testing.T) {
12 | type test struct {
13 | inputConfig string
14 | valid bool
15 | }
16 |
17 | tests := map[string]test{
18 | "valid config": {
19 | inputConfig: "version = 1",
20 | valid: true,
21 | },
22 | "wrong version": {
23 | inputConfig: "version = \"foobar\"",
24 | valid: false,
25 | },
26 | "version greater than maximum allowed": {
27 | inputConfig: "version = 352",
28 | valid: false,
29 | },
30 | "version missing": {
31 | inputConfig: "",
32 | valid: false,
33 | },
34 | "version of wrong type": {
35 | inputConfig: "version = \"2\"",
36 | valid: false,
37 | },
38 | }
39 | for testName, tc := range tests {
40 | t.Run(testName, func(t *testing.T) {
41 | testConfig, err := getConfig([]byte(tc.inputConfig))
42 | if err != nil {
43 | t.Error(err)
44 | }
45 | c := &ConfigValidator{
46 | Config: *testConfig,
47 | Result: Result{
48 | Valid: true,
49 | Errors: []string{},
50 | ConfigReadError: false,
51 | },
52 | }
53 | c.validateVersion()
54 | if !reflect.DeepEqual(tc.valid, c.Result.Valid) {
55 | t.Fatalf("%v: expected: %v, got: %v. Error: %v", testName, tc.valid, c.Result.Valid, c.Result.Errors)
56 | }
57 | })
58 | }
59 | }
60 |
61 | func TestValidateExcludePatterns(t *testing.T) {
62 | type test struct {
63 | inputConfig string
64 | valid bool
65 | }
66 |
67 | tests := map[string]test{
68 | "valid exclude_patterns": {
69 | inputConfig: "version= 1\nexclude_patterns = 23",
70 | valid: false,
71 | },
72 | "should be array of string": {
73 | inputConfig: "version= 1\nexclude_patterns = [23,43]",
74 | valid: false,
75 | },
76 | "valid array of string": {
77 | inputConfig: "version = 1\nexclude_patterns = ['hello', 'world']",
78 | valid: true,
79 | },
80 | "strings with double quotes": {
81 | inputConfig: "exclude_patterns = [\"hello\",\"world\"]",
82 | valid: true,
83 | },
84 | "empty exclude_patterns": {
85 | inputConfig: "exclude_patterns = []",
86 | valid: true,
87 | },
88 | "cannot be only string, should be an array": {
89 | inputConfig: "version = 1\nexclude_patterns = 'hello'",
90 | valid: false,
91 | },
92 | }
93 | for testName, tc := range tests {
94 | t.Run(testName, func(t *testing.T) {
95 | testConfig, err := getConfig([]byte(tc.inputConfig))
96 | if err != nil {
97 | t.Error(err)
98 | }
99 | c := &ConfigValidator{
100 | Config: *testConfig,
101 | Result: Result{
102 | Valid: true,
103 | Errors: []string{},
104 | ConfigReadError: false,
105 | },
106 | }
107 | c.validateExcludePatterns()
108 | if !reflect.DeepEqual(tc.valid, c.Result.Valid) {
109 | t.Fatalf("%v: Config : %v, expected: %v, got: %v. Error: %v", testName, tc.inputConfig, tc.valid, c.Result.Valid, c.Result.Errors)
110 | }
111 | })
112 | }
113 | }
114 |
115 | func TestValidateTestPatterns(t *testing.T) {
116 | type test struct {
117 | inputConfig string
118 | valid bool
119 | }
120 |
121 | tests := map[string]test{
122 | "cannot be an integer": {
123 | inputConfig: "test_patterns = 23",
124 | valid: false,
125 | },
126 | "cannot be an array of integers": {
127 | inputConfig: "test_patterns = [23,43]",
128 | valid: false,
129 | },
130 | "should be array of strings": {
131 | inputConfig: "test_patterns = ['hello', 'world']",
132 | valid: true,
133 | },
134 | "strings with double quotes": {
135 | inputConfig: "test_patterns = [\"hello\",\"world\"]",
136 | valid: true,
137 | },
138 | "empty test_patterns": {
139 | inputConfig: "test_patterns = []",
140 | valid: true,
141 | },
142 | "cannot be only string, should be an array of string": {
143 | inputConfig: "test_patterns = 'hello'",
144 | valid: false,
145 | },
146 | }
147 | for testName, tc := range tests {
148 | t.Run(testName, func(t *testing.T) {
149 | testConfig, err := getConfig([]byte(tc.inputConfig))
150 | if err != nil {
151 | t.Error(err)
152 | }
153 | c := &ConfigValidator{
154 | Config: *testConfig,
155 | Result: Result{
156 | Valid: true,
157 | Errors: []string{},
158 | ConfigReadError: false,
159 | },
160 | }
161 | c.validateTestPatterns()
162 | if !reflect.DeepEqual(tc.valid, c.Result.Valid) {
163 | t.Fatalf("%v: Config : %v, expected: %v, got: %v. Error: %v", testName, tc.inputConfig, tc.valid, c.Result.Valid, c.Result.Errors)
164 | }
165 | })
166 | }
167 | }
168 |
169 | // Receives a string of DeepSource config and returns its
170 | // representation in the form of a DSConfig struct
171 | func getConfig(inputConfig []byte) (*DSConfig, error) {
172 | config := DSConfig{}
173 | viper.SetConfigType("toml")
174 | err := viper.ReadConfig(bytes.NewBuffer(inputConfig))
175 | if err != nil {
176 | return nil, err
177 | }
178 | // Unmarshaling the configdata into DSConfig struct
179 | viper.UnmarshalExact(&config)
180 | return &config, nil
181 | }
182 |
--------------------------------------------------------------------------------
/command/issues/list/testdata/sarif/test_multi.sarif:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.1.0",
3 | "$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
4 | "runs": [
5 | {
6 | "tool": {
7 | "driver": {
8 | "informationUri": "https://deepsource.io/directory/analyzers/go",
9 | "name": "DeepSource Go Analyzer",
10 | "rules": [
11 | {
12 | "id": "RVV-B0013",
13 | "name": "Unused method receiver detected",
14 | "shortDescription": null,
15 | "fullDescription": {
16 | "text": ""
17 | },
18 | "helpUri": "https://deepsource.io/directory/analyzers/go/issues/RVV-B0013",
19 | "properties": {
20 | "category": "",
21 | "recommended": ""
22 | }
23 | }
24 | ]
25 | }
26 | },
27 | "results": [
28 | {
29 | "ruleId": "RVV-B0013",
30 | "ruleIndex": 0,
31 | "kind": "fail",
32 | "level": "error",
33 | "message": {
34 | "text": "Unused method receiver detected"
35 | },
36 | "locations": [
37 | {
38 | "physicalLocation": {
39 | "artifactLocation": {
40 | "uri": "deepsource/transformers/queries/get_transformers.go"
41 | },
42 | "region": {
43 | "startLine": 34,
44 | "endLine": 34
45 | }
46 | }
47 | }
48 | ]
49 | },
50 | {
51 | "ruleId": "RVV-B0013",
52 | "ruleIndex": 1,
53 | "kind": "fail",
54 | "level": "error",
55 | "message": {
56 | "text": "Unused method receiver detected"
57 | },
58 | "locations": [
59 | {
60 | "physicalLocation": {
61 | "artifactLocation": {
62 | "uri": "deepsource/transformers/queries/get_transformers.go"
63 | },
64 | "region": {
65 | "startLine": 44,
66 | "endLine": 44
67 | }
68 | }
69 | }
70 | ]
71 | }
72 | ]
73 | },
74 | {
75 | "tool": {
76 | "driver": {
77 | "informationUri": "https://deepsource.io/directory/analyzers/docker",
78 | "name": "DeepSource Docker Analyzer",
79 | "rules": [
80 | {
81 | "id": "DOK-DL3025",
82 | "name": "Use arguments JSON notation for CMD and ENTRYPOINT arguments",
83 | "shortDescription": null,
84 | "fullDescription": {
85 | "text": ""
86 | },
87 | "helpUri": "https://deepsource.io/directory/analyzers/docker/issues/DOK-DL3025",
88 | "properties": {
89 | "category": "",
90 | "recommended": ""
91 | }
92 | }
93 | ]
94 | }
95 | },
96 | "results": [
97 | {
98 | "ruleId": "DOK-DL3025",
99 | "ruleIndex": 0,
100 | "kind": "fail",
101 | "level": "error",
102 | "message": {
103 | "text": "Use arguments JSON notation for CMD and ENTRYPOINT arguments"
104 | },
105 | "locations": [
106 | {
107 | "physicalLocation": {
108 | "artifactLocation": {
109 | "uri": "Dockerfile"
110 | },
111 | "region": {
112 | "startLine": 64,
113 | "endLine": 64
114 | }
115 | }
116 | }
117 | ]
118 | }
119 | ]
120 | },
121 | {
122 | "tool": {
123 | "driver": {
124 | "informationUri": "https://deepsource.io/directory/analyzers/python",
125 | "name": "DeepSource Python Analyzer",
126 | "rules": [
127 | {
128 | "id": "PY-W2000",
129 | "name": "Imported name is not used anywhere in the module",
130 | "shortDescription": null,
131 | "fullDescription": {
132 | "text": ""
133 | },
134 | "helpUri": "https://deepsource.io/directory/analyzers/python/issues/PY-W2000",
135 | "properties": {
136 | "category": "",
137 | "recommended": ""
138 | }
139 | }
140 | ]
141 | }
142 | },
143 | "results": [
144 | {
145 | "ruleId": "PY-W2000",
146 | "ruleIndex": 0,
147 | "kind": "fail",
148 | "level": "error",
149 | "message": {
150 | "text": "Imported name is not used anywhere in the module"
151 | },
152 | "locations": [
153 | {
154 | "physicalLocation": {
155 | "artifactLocation": {
156 | "uri": "python/demo.py"
157 | },
158 | "region": {
159 | "startLine": 1,
160 | "endLine": 1
161 | }
162 | }
163 | }
164 | ]
165 | }
166 | ]
167 | }
168 | ]
169 | }
170 |
--------------------------------------------------------------------------------
/command/issues/list/list_test.go:
--------------------------------------------------------------------------------
1 | package list
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "os"
7 | "reflect"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/deepsourcelabs/cli/deepsource/issues"
12 | )
13 |
14 | // Helper function to read issues from a file.
15 | func ReadIssues(path string) []issues.Issue {
16 | raw, _ := ioutil.ReadFile(path)
17 | var fetchedIssues []issues.Issue
18 | _ = json.Unmarshal(raw, &fetchedIssues)
19 |
20 | return fetchedIssues
21 | }
22 |
23 | func TestListCSV(t *testing.T) {
24 | issues_data := ReadIssues("./testdata/dummy/issues.json")
25 | opts := IssuesListOptions{issuesData: issues_data}
26 | opts.exportCSV("./testdata/exported.csv")
27 |
28 | // read exported and test CSV files
29 | exported, _ := ioutil.ReadFile("./testdata/exported.csv")
30 | test, _ := ioutil.ReadFile("./testdata/csv/test.csv")
31 |
32 | // trim carriage returns
33 | got := strings.TrimSuffix(string(exported), "\n")
34 | want := strings.TrimSuffix(string(test), "\n")
35 |
36 | // cleanup after test
37 | _ = os.Remove("./testdata/exported.csv")
38 |
39 | if !reflect.DeepEqual(got, want) {
40 | t.Errorf("got: %v; want: %v\n", got, want)
41 | }
42 | }
43 |
44 | func TestListJSON(t *testing.T) {
45 | issues_data := ReadIssues("./testdata/dummy/issues.json")
46 | opts := IssuesListOptions{issuesData: issues_data}
47 | opts.exportJSON("./testdata/exported.json")
48 |
49 | // read exported and test JSON files
50 | exported, _ := ioutil.ReadFile("./testdata/exported.json")
51 | test, _ := ioutil.ReadFile("./testdata/json/test.json")
52 |
53 | // trim carriage returns
54 | got := strings.TrimSuffix(string(exported), "\n")
55 | want := strings.TrimSuffix(string(test), "\n")
56 |
57 | // cleanup after test
58 | _ = os.Remove("./testdata/exported.json")
59 |
60 | if !reflect.DeepEqual(got, want) {
61 | t.Errorf("got: %v; want: %v\n", got, want)
62 | }
63 | }
64 |
65 | func TestListSARIF(t *testing.T) {
66 | t.Run("must work with single language repositories", func(t *testing.T) {
67 | // export issues to SARIF
68 | issues_data := ReadIssues("./testdata/dummy/issues.json")
69 |
70 | opts := IssuesListOptions{issuesData: issues_data}
71 | opts.exportSARIF("./testdata/exported.sarif")
72 |
73 | // read exported and test SARIF files
74 | exported, _ := ioutil.ReadFile("./testdata/exported.sarif")
75 | test, _ := ioutil.ReadFile("./testdata/sarif/test.sarif")
76 |
77 | // trim carriage returns
78 | got := strings.TrimSuffix(string(exported), "\n")
79 | want := strings.TrimSuffix(string(test), "\n")
80 |
81 | // cleanup after test
82 | _ = os.Remove("./testdata/exported.sarif")
83 |
84 | if !reflect.DeepEqual(got, want) {
85 | t.Errorf("got: %v; want: %v\n", got, want)
86 | }
87 | })
88 |
89 | t.Run("must work with repositories containing multiple languages", func(t *testing.T) {
90 | // export issues to SARIF
91 | issues_data := ReadIssues("./testdata/dummy/issues_data_multi.json")
92 |
93 | opts := IssuesListOptions{issuesData: issues_data}
94 | opts.exportSARIF("./testdata/exported_multi.sarif")
95 |
96 | // read exported and test SARIF files
97 | exported, _ := ioutil.ReadFile("./testdata/exported_multi.sarif")
98 | test, _ := ioutil.ReadFile("./testdata/sarif/test_multi.sarif")
99 |
100 | // trim carriage returns
101 | got := strings.TrimSuffix(string(exported), "\n")
102 | want := strings.TrimSuffix(string(test), "\n")
103 |
104 | // cleanup after test
105 | _ = os.Remove("./testdata/exported_multi.sarif")
106 |
107 | if !reflect.DeepEqual(got, want) {
108 | t.Errorf("got: %v; want: %v\n", got, want)
109 | }
110 | })
111 | }
112 |
113 | func TestFilterIssuesByPath(t *testing.T) {
114 | t.Run("must work with files", func(t *testing.T) {
115 | issues_data := ReadIssues("./testdata/dummy/issues_data_multi.json")
116 | issues_docker := ReadIssues("./testdata/dummy/issues_docker.json")
117 |
118 | got, _ := filterIssuesByPath("Dockerfile", issues_data)
119 | want := issues_docker
120 | if !reflect.DeepEqual(got, want) {
121 | t.Errorf("got: %v; want: %v\n", got, want)
122 | }
123 | })
124 |
125 | t.Run("must work with directories", func(t *testing.T) {
126 | issues_data := ReadIssues("./testdata/dummy/issues_data_multi.json")
127 | issues_deepsource := ReadIssues("./testdata/dummy/issues_deepsource.json")
128 |
129 | got, _ := filterIssuesByPath("deepsource/", issues_data)
130 | want := issues_deepsource
131 | if !reflect.DeepEqual(got, want) {
132 | t.Errorf("got: %v; want: %v\n", got, want)
133 | }
134 | })
135 | }
136 |
137 | func TestFilterIssuesByAnalyzer(t *testing.T) {
138 | t.Run("must work with a single analyzer", func(t *testing.T) {
139 | issues_data := ReadIssues("./testdata/dummy/issues_data_multi.json")
140 | issues_docker := ReadIssues("./testdata/dummy/issues_docker.json")
141 |
142 | got, _ := filterIssuesByAnalyzer([]string{"docker"}, issues_data)
143 | want := issues_docker
144 | if !reflect.DeepEqual(got, want) {
145 | t.Errorf("got: %v; want: %v\n", got, want)
146 | }
147 | })
148 |
149 | t.Run("must work with multiple analyzers", func(t *testing.T) {
150 | issues_data := ReadIssues("./testdata/dummy/issues_data_multi.json")
151 | issues_multi_analyzers := ReadIssues("./testdata/dummy/issues_multiple_analyzers.json")
152 |
153 | got, _ := filterIssuesByAnalyzer([]string{"docker", "python"}, issues_data)
154 | want := issues_multi_analyzers
155 | if !reflect.DeepEqual(got, want) {
156 | t.Errorf("got: %v; want: %v\n", got, want)
157 | }
158 | })
159 | }
160 |
--------------------------------------------------------------------------------
/command/report/git.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "os/exec"
7 | "strings"
8 | )
9 |
10 | // gitGetHead accepts a git directory and returns head commit OID / error
11 | func gitGetHead(workspaceDir string) (headOID string, warning string, err error) {
12 | // Check if DeepSource's Test coverage action triggered this first before executing any git commands.
13 | headOID, err = getTestCoverageActionCommit()
14 | if headOID != "" {
15 | return
16 | }
17 |
18 | // Check if the `GIT_COMMIT_SHA` environment variable exists. If yes, return this as
19 | // the latest commit sha.
20 | // This is used in cases when the user wants to report tcv from inside a docker container in which they are running tests.
21 | // In this scenario, the container doesn't have data about the latest git commit sha so
22 | // it is injected by the user manually while running the container.
23 | // Example:
24 | // GIT_COMMIT_SHA=$(git --no-pager rev-parse HEAD | tr -d '\n')
25 | // docker run -e DEEPSOURCE_DSN -e GIT_COMMIT_SHA ...
26 | if injectedSHA, isManuallyInjectedSHA := os.LookupEnv("GIT_COMMIT_SHA"); isManuallyInjectedSHA {
27 | return injectedSHA, "", nil
28 | }
29 |
30 | // get the top commit manually, using git command. We will be using this if there's no env variable set for extracting commit.
31 | headOID, err = fetchHeadManually(workspaceDir)
32 | if err != nil {
33 | return
34 | }
35 |
36 | // TRAVIS CI
37 | if envUser := os.Getenv("USER"); envUser == "travis" {
38 | headOID, warning, err = getTravisCommit(headOID)
39 | return
40 | }
41 |
42 | // GITHUB ACTIONS
43 | if _, isGitHubEnv := os.LookupEnv("GITHUB_ACTIONS"); isGitHubEnv {
44 | headOID, warning, err = getGitHubActionsCommit(headOID)
45 | return
46 | }
47 |
48 | // If we are here, it means there weren't any special cases. Return the manually found headOID.
49 | return
50 | }
51 |
52 | // Fetches the latest commit hash using the command `git rev-parse HEAD`
53 | // through git
54 | func fetchHeadManually(directoryPath string) (string, error) {
55 | cmd := exec.Command("git", "--no-pager", "rev-parse", "HEAD")
56 | cmd.Dir = directoryPath
57 |
58 | var stdout, stderr bytes.Buffer
59 | cmd.Stdout = &stdout
60 | cmd.Stderr = &stderr
61 |
62 | err := cmd.Run()
63 | outStr, _ := stdout.String(), stderr.String()
64 | if err != nil {
65 | return "", err
66 | }
67 |
68 | // Trim newline suffix from Commit OID
69 | return strings.TrimSuffix(outStr, "\n"), nil
70 | }
71 |
72 | // Handle special cases for GitHub Actions.
73 | func getGitHubActionsCommit(topCommit string) (headOID string, warning string, err error) {
74 | // When GITHUB_REF is not set, GITHUB_SHA points to original commit.
75 | // When set, it points to the "latest *merge* commit in the branch".
76 | // Early exit when GITHUB_SHA points to the original commit.
77 | // Ref: https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request
78 | if _, isRefPresent := os.LookupEnv("GITHUB_REF"); !isRefPresent {
79 | headOID = os.Getenv("GITHUB_SHA")
80 | return
81 | }
82 |
83 | // Case: Detect Merge commit made by GitHub Actions, which pull_request events are nutorious to make.
84 | // We are anyways going to return `headOID` fetched manually, but want to warn users about the merge commit.
85 |
86 | // When ref is not provided during the checkout step, headOID would be the same as GITHUB_SHA
87 | // This confirms the merge commit.
88 | // event names where GITHUB_SHA would be of a merge commit:
89 | // "pull_request",
90 | // "pull_request_review",
91 | // "pull_request_review",
92 | // "pull_request_review_comment",
93 | eventName := os.Getenv("GITHUB_EVENT_NAME")
94 | eventCommitSha := os.Getenv("GITHUB_SHA")
95 | if strings.HasPrefix(eventName, "pull_request") && topCommit == eventCommitSha {
96 | warning = "Warning: Looks like the checkout step is making a merge commit. " +
97 | "Test coverage Analyzer would not run for the reported artifact because the merge commit doesn't exist upstream.\n" +
98 | "Please refer to the docs for required changes. Ref: https://docs.deepsource.com/docs/analyzers-test-coverage#with-github-actions"
99 | }
100 | headOID = topCommit
101 | return
102 | }
103 |
104 | // Return PR's HEAD ref set as env variable manually by DeepSource's Test coverage action.
105 | func getTestCoverageActionCommit() (headOID string, err error) {
106 | // This is kept separate from `getGitHubActionsCommit` because we don't want to run any git command manually
107 | // before this is checked. Since this is guaranteed to be set if artifact is sent using our GitHub action,
108 | // we can reliably send the commit SHA, and no git commands are executed, making the actions work all the time. \o/
109 |
110 | // We are setting PR's head commit as default using github context as env variable: "GHA_HEAD_COMMIT_SHA"
111 | headOID = os.Getenv("GHA_HEAD_COMMIT_SHA")
112 |
113 | return
114 | }
115 |
116 | // Handle special case for TravisCI
117 | func getTravisCommit(topCommit string) (string, string, error) {
118 | // Travis creates a merge commit for pull requests on forks.
119 | // The head of commit is this merge commit, which does not match the commit of deepsource check.
120 | // Fetch value of pull request SHA. If this is a PR, it will return SHA of HEAD commit of the PR, else "".
121 | // If prSHA is not empty, that means we got an SHA, which is HEAD. Return this.
122 | if prSHA := os.Getenv("TRAVIS_PULL_REQUEST_SHA"); len(prSHA) > 0 {
123 | return prSHA, "", nil
124 | }
125 |
126 | return topCommit, "", nil
127 | }
128 |
--------------------------------------------------------------------------------
/configvalidator/config_validator_test.go:
--------------------------------------------------------------------------------
1 | package configvalidator
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/deepsourcelabs/cli/utils"
7 | )
8 |
9 | func TestValidateConfig(t *testing.T) {
10 | type test struct {
11 | inputConfig string
12 | valid bool
13 | }
14 | setDummyAnalyzerTransformerData()
15 |
16 | tests := map[string]test{
17 | "blank config": {
18 | inputConfig: "",
19 | valid: false,
20 | },
21 | "analyzer should be array": {
22 | inputConfig: `
23 | version = 1
24 | analyzers = "python",
25 | enabled = true`,
26 | valid: false,
27 | },
28 | "zero analyzers": {
29 | inputConfig: `
30 | version = 1
31 |
32 | [[analyzers]]
33 | name = "python"
34 | enabled = false
35 |
36 | [[analyzers]]
37 | name = "javascript"
38 | enabled = true`,
39 | valid: false,
40 | },
41 | "transformer without analyzer": {
42 | inputConfig: `
43 | version = 1
44 |
45 | [[transformers]]
46 | name = "black"
47 | enabled = true`,
48 | valid: false,
49 | },
50 | "no analyzer/transformer activated": {
51 | inputConfig: `
52 | version = 1
53 |
54 | [[analyzers]]
55 | name = "python"
56 | enabled = false
57 |
58 | [[transformers]]
59 | name = "black"
60 | enabled = false
61 |
62 | [[transformers]]
63 | name = "isort"
64 | enabled = false`,
65 | valid: false,
66 | },
67 | "tranformers with analyzer disabled": {
68 | inputConfig: `
69 | version = 1
70 |
71 | [[analyzers]]
72 | name = "python"
73 | enabled = false
74 |
75 | [[transformers]]
76 | name = "black"
77 | enabled = true
78 |
79 | [[transformers]]
80 | name = "isort"
81 | enabled = true`,
82 | valid: false,
83 | },
84 | "non-supported transformer": {
85 | inputConfig: `
86 | version = 1
87 |
88 | [[analyzers]]
89 | name = "python"
90 | enabled = true
91 |
92 | [[transformers]]
93 | name = "egg"
94 | enabled = true`,
95 | valid: false,
96 | },
97 | "transformer must be an array": {
98 | inputConfig: `
99 | version = 1
100 |
101 | [[analyzers]]
102 | name = "python"
103 | enabled = true
104 |
105 | transformers = "egg"
106 | enabled = true`,
107 | valid: false,
108 | },
109 | "valid config with enabled not set (defaults to true)": {
110 | inputConfig: `
111 | version = 1
112 |
113 | [[analyzers]]
114 | name = "python"
115 |
116 | [[transformers]]
117 | name = "black"`,
118 | valid: true,
119 | },
120 |
121 | "invalid config with enabled = \"falsee\" (non-boolean)": {
122 | inputConfig: `
123 | version = 1
124 |
125 | [[analyzers]]
126 | name = "python"
127 | enabled = "falsee"`,
128 | valid: false,
129 | },
130 | "config with syntax error": {
131 | inputConfig: `
132 | version = 1
133 |
134 | [[analyzers]
135 | name = "python"
136 | enabled = false`,
137 | valid: false,
138 | },
139 | }
140 |
141 | for testName, tc := range tests {
142 | t.Run(testName, func(t *testing.T) {
143 | c := &ConfigValidator{}
144 | c.ValidateConfig([]byte(tc.inputConfig))
145 | if tc.valid != c.Result.Valid {
146 | t.Errorf("%s: expected: %v, got: %v. Error: %v", testName, tc.valid, c.Result.Valid, c.Result.Errors)
147 | }
148 | })
149 | }
150 | }
151 |
152 | func setDummyAnalyzerTransformerData() {
153 | analyzersMetaMap := make(map[string]string)
154 | utils.AnalyzersData.AnalyzerShortcodes = []string{"python", "test-coverage"}
155 | utils.AnalyzersData.AnalyzersMeta = []string{`{
156 | "type": "object",
157 | "properties": {
158 | "max_line_length": {
159 | "type": "integer",
160 | "minimum": 79,
161 | "title": "Maximum line length",
162 | "description": "Customize this according to your project's conventions.",
163 | "default": 100
164 | },
165 | "runtime_version": {
166 | "enum": [
167 | "3.x.x",
168 | "2.x.x"
169 | ],
170 | "type": "string",
171 | "title": "Runtime version",
172 | "description": "Set it to the least version of Python that your code runs on.",
173 | "default": "3.x.x"
174 | },
175 | "skip_doc_coverage": {
176 | "type": "array",
177 | "title": "Skip in doc coverage",
178 | "description": "Types of objects that should be skipped while calculating documentation coverage.",
179 | "items": {
180 | "enum": [
181 | "magic",
182 | "init",
183 | "class",
184 | "module",
185 | "nonpublic"
186 | ],
187 | "type": "string"
188 | },
189 | "additionalProperties": false
190 | }
191 | },
192 | "optional_required": [
193 | "runtime_version"
194 | ],
195 | "additionalProperties": false
196 | }`, "{}"}
197 |
198 | analyzersMetaMap["python"] = utils.AnalyzersData.AnalyzersMeta[0]
199 | analyzersMetaMap["test-coverage"] = utils.AnalyzersData.AnalyzersMeta[1]
200 | utils.AnalyzersData.AnalyzersMetaMap = analyzersMetaMap
201 |
202 | utils.TransformersData.TransformerShortcodes = []string{"black", "prettier"}
203 | }
204 |
--------------------------------------------------------------------------------
/command/config/generate/analyzers_input.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/Jeffail/gabs/v2"
7 | "github.com/deepsourcelabs/cli/utils"
8 | )
9 |
10 | // Struct to hold the data regarding the compulsary meta fields as required by analyzers
11 | // Also, the userinput for that field
12 | type AnalyzerMetadata struct {
13 | FieldName string
14 | Type string
15 | Title string
16 | Description string
17 | Options []string
18 | UserInput string
19 | }
20 |
21 | // ==========
22 | // Analyzers Input Prompt
23 | // ==========
24 | func (o *Options) collectAnalyzerInput() (err error) {
25 | // Extracting languages and tools being used in the project for Analyzers
26 | analyzerPromptMsg := "Which languages/tools does your project use?"
27 | analyzerPromptHelpText := "Analyzers will find issues in your code. Add an analyzer by selecting a language you've written your code in."
28 |
29 | o.ActivatedAnalyzers, err = utils.SelectFromMultipleOptions(analyzerPromptMsg, analyzerPromptHelpText, utils.AnalyzersData.AnalyzerNames)
30 | if err != nil {
31 | return err
32 | }
33 |
34 | // Extract the compulsary analyzer meta for analyzers
35 | err = o.extractRequiredAnalyzerMetaFields()
36 | if err != nil {
37 | return err
38 | }
39 |
40 | return nil
41 | }
42 |
43 | // Checks if the field is present in the array containing list of `optional_required`
44 | // analyzer meta fields
45 | func isContains(requiredFieldsList []string, field string) bool {
46 | for _, v := range requiredFieldsList {
47 | if field == v {
48 | return true
49 | }
50 | }
51 | return false
52 | }
53 |
54 | // Uses the `survey` prompt API to gather user input and store in `Options` struct
55 | // `Options` struct is later used for config generation
56 | func (o *Options) inputAnalyzerMeta(requiredFieldsData map[string][]AnalyzerMetadata) (err error) {
57 | // Iterate over the map and fetch the input for the fields from the user
58 | for analyzer, metaFields := range requiredFieldsData {
59 | for i := 0; i < len(metaFields); i++ {
60 | switch metaFields[i].Type {
61 | case "boolean":
62 | metaFields[i].UserInput = "true"
63 | res, err := utils.ConfirmFromUser(metaFields[i].Title, metaFields[i].Description)
64 | if err != nil {
65 | return err
66 | }
67 | if !res {
68 | metaFields[i].UserInput = "false"
69 | }
70 | case "enum":
71 | metaFields[i].UserInput, err = utils.SelectFromOptions(metaFields[i].Title, metaFields[i].Description, metaFields[i].Options)
72 | if err != nil {
73 | return err
74 | }
75 | default:
76 | metaFields[i].UserInput, err = utils.GetSingleLineInput(metaFields[i].Title, metaFields[i].Description)
77 | if err != nil {
78 | return err
79 | }
80 | }
81 | }
82 | requiredFieldsData[analyzer] = metaFields
83 | }
84 | o.AnalyzerMetaMap = requiredFieldsData
85 | return nil
86 | }
87 |
88 | // Extracts the fields that are compulsary according to the meta schema and require input
89 | func populateMetadata(optionalFields []string, jsonParsed *gabs.Container) []AnalyzerMetadata {
90 | requiredFieldsData := make([]AnalyzerMetadata, 0)
91 |
92 | // Iterate through the properties using the parsed json (jsonParsed) and extract the data of the
93 | // required analyzer meta fields
94 | for key, child := range jsonParsed.Search("properties").ChildrenMap() {
95 | if !isContains(optionalFields, key) {
96 | continue
97 | }
98 | propertyJSON, err := gabs.ParseJSON(child.Bytes())
99 | if err != nil {
100 | log.Printf("Error occured while parsing analyzer meta property: %v\n", err)
101 | continue
102 | }
103 |
104 | individualFieldRequiredData := AnalyzerMetadata{
105 | FieldName: key,
106 | Type: propertyJSON.Search("type").Data().(string),
107 | Title: propertyJSON.Search("title").Data().(string),
108 | Description: propertyJSON.Search("description").Data().(string),
109 | }
110 |
111 | // Check for enum property
112 | for _, child := range propertyJSON.Search("enum").Children() {
113 | individualFieldRequiredData.Options = append(individualFieldRequiredData.Options, child.Data().(string))
114 | individualFieldRequiredData.Type = "enum"
115 | }
116 |
117 | // Check for items property
118 | itemsPath := propertyJSON.Path("items")
119 | itemsJSON, _ := gabs.ParseJSON(itemsPath.Bytes())
120 | for _, child := range itemsJSON.Search("enum").Children() {
121 | individualFieldRequiredData.Options = append(individualFieldRequiredData.Options, child.Data().(string))
122 | individualFieldRequiredData.Type = "enum"
123 | }
124 | requiredFieldsData = append(requiredFieldsData, individualFieldRequiredData)
125 | }
126 | return requiredFieldsData
127 | }
128 |
129 | // The primary function to parse the API response of meta schema and filter out the `optional_required` fields
130 | // Calls helper functions (mentioned above) to perform the required meta data extraction
131 | // and handling prompt for inputting these fields
132 | func (o *Options) extractRequiredAnalyzerMetaFields() error {
133 | var optionalFields []string
134 | var requiredMetaData []AnalyzerMetadata
135 | analyzerFieldsData := make(map[string][]AnalyzerMetadata)
136 |
137 | // Extract `optional_required` fields of analyzer meta of selected analyzers
138 | for _, activatedAnalyzer := range o.ActivatedAnalyzers {
139 | analyzerShortcode := utils.AnalyzersData.AnalyzersMap[activatedAnalyzer]
140 | // Assigning optional fields to nil before checking for an analyzer
141 | optionalFields = nil
142 | requiredMetaData = nil
143 |
144 | analyzerMeta := utils.AnalyzersData.AnalyzersMetaMap[analyzerShortcode]
145 | // Parse the analyzer meta of the analyzer using `gabs`
146 | jsonParsed, err := gabs.ParseJSON([]byte(analyzerMeta))
147 | if err != nil {
148 | log.Printf("Error occured while parsing meta for %s analyzer.\n", activatedAnalyzer)
149 | return err
150 | }
151 |
152 | // Search for "optional_required" fields in the meta-schema
153 | for _, child := range jsonParsed.Search("optional_required").Children() {
154 | optionalFields = append(optionalFields, child.Data().(string))
155 | }
156 | // Move on to next analyzer if no "optional_required" fields found
157 | if optionalFields == nil {
158 | continue
159 | }
160 | // Extract the the data to be input for all the required analyzer meta properties
161 | requiredMetaData = populateMetadata(optionalFields, jsonParsed)
162 | analyzerFieldsData[activatedAnalyzer] = requiredMetaData
163 | }
164 | return o.inputAnalyzerMeta(analyzerFieldsData)
165 | }
166 |
--------------------------------------------------------------------------------
/command/config/validate/validate.go:
--------------------------------------------------------------------------------
1 | package validate
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "os"
9 | "os/exec"
10 | "path/filepath"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/deepsourcelabs/cli/config"
15 | "github.com/deepsourcelabs/cli/configvalidator"
16 | "github.com/deepsourcelabs/cli/deepsource"
17 | "github.com/deepsourcelabs/cli/utils"
18 | "github.com/pterm/pterm"
19 | "github.com/spf13/cobra"
20 | )
21 |
22 | // Options holds the metadata.
23 | type Options struct{}
24 |
25 | // NewCmdValidate handles the validation of the DeepSource config (.deepsource.toml)
26 | // Internally it uses the package `configvalidator` to validate the config
27 | func NewCmdValidate() *cobra.Command {
28 | o := Options{}
29 | cmd := &cobra.Command{
30 | Use: "validate",
31 | Short: "Validate DeepSource config",
32 | Args: utils.NoArgs,
33 | RunE: func(cmd *cobra.Command, args []string) error {
34 | return o.Run()
35 | },
36 | }
37 | return cmd
38 | }
39 |
40 | // Run executes the command.
41 | func (o *Options) Run() error {
42 | // Fetch config
43 | cfg, err := config.GetConfig()
44 | if err != nil {
45 | return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
46 | }
47 | err = cfg.VerifyAuthentication()
48 | if err != nil {
49 | return err
50 | }
51 |
52 | // Just an info
53 | pterm.Info.Println("DeepSource config (.deepsource.toml) is mostly present in the root directory of the project.")
54 | fmt.Println()
55 |
56 | // Extract the path of DeepSource config
57 | configPath, err := extractDSConfigPath()
58 | if err != nil {
59 | return err
60 | }
61 |
62 | // Read the config in the form of string and send it
63 | content, err := ioutil.ReadFile(configPath)
64 | if err != nil {
65 | return errors.New("Error occured while reading DeepSource config file. Exiting...")
66 | }
67 |
68 | // Fetch the client
69 | deepsource, err := deepsource.New(deepsource.ClientOpts{
70 | Token: config.Cfg.Token,
71 | HostName: config.Cfg.Host,
72 | })
73 | if err != nil {
74 | return err
75 | }
76 | ctx := context.Background()
77 | // Fetch the list of supported analyzers and transformers' data
78 | // using the SDK
79 | err = utils.GetAnalyzersAndTransformersData(ctx, *deepsource)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | // Create an instance of ConfigValidator struct
85 | var validator configvalidator.ConfigValidator
86 | // Send the config contents to get validated
87 | var result configvalidator.Result = validator.ValidateConfig(content)
88 |
89 | // Checking for all types of errors (due to viper/valid errors/no errors)
90 | // and handling them
91 | if result.ConfigReadError {
92 | // handle printing viper error here
93 | printViperError(content, result.Errors)
94 | } else if !result.Valid {
95 | // handle printing other errors here
96 | printConfigErrors(result.Errors)
97 | } else {
98 | printValidConfig()
99 | }
100 |
101 | return nil
102 | }
103 |
104 | // Extracts the path of DeepSource config (.deepsource.toml) in the user repo
105 | // Checks in the current working directory as well as the root directory
106 | // of the project
107 | func extractDSConfigPath() (string, error) {
108 | var configPath string
109 |
110 | // Get current working directory of user from where this command is run
111 | cwd, err := os.Getwd()
112 | if err != nil {
113 | return "", errors.New("Error occured while fetching current working directory. Exiting...")
114 | }
115 |
116 | // Form the full path of cwd to search for .deepsource.toml
117 | configPath = filepath.Join(cwd, ".deepsource.toml")
118 |
119 | // Check if there is a deepsource.toml file here
120 | if _, err = os.Stat(configPath); err != nil {
121 | // Since, no .deepsource.toml in the cwd,
122 | // fetching the top level directory
123 | output, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
124 | if err != nil {
125 | return "", err
126 | }
127 |
128 | // Removing trailing null characters
129 | path := strings.TrimRight(string(output), "\000\n")
130 |
131 | // Check if the config exists on this path
132 | if _, err = os.Stat(filepath.Join(path, ".deepsource.toml")); err != nil {
133 | return "", errors.New("Error occured while looking for DeepSource config file. Exiting...")
134 | } else {
135 | // If found, use this as configpath
136 | configPath = filepath.Join(path, "/.deepsource.toml")
137 | }
138 | }
139 | return configPath, nil
140 | }
141 |
142 | // Handles printing the output when viper fails to read TOML file due to bad syntax
143 | func printViperError(fileContent []byte, errors []string) {
144 | var errorString string
145 | var errorLine int
146 |
147 | // Parsing viper error output and finding at which line bad syntax is present in
148 | // DeepSource config TOML file
149 | for _, error := range errors {
150 | stripString1 := strings.Split(error, ": ")
151 | errorString = stripString1[2]
152 | errorLine, _ = strconv.Atoi(strings.Trim(strings.Split(stripString1[1], ", ")[0], "("))
153 | }
154 |
155 | // Read .deepsource.toml line by line and store in a var
156 | lineText := strings.Split(string(fileContent), "\n")
157 | fileLength := len(lineText)
158 |
159 | // Print error message
160 | pterm.Error.WithShowLineNumber(false).Printf("Error while reading config : %s\n", errorString)
161 | pterm.Println()
162 |
163 | // Preparing codeframe to show exactly at which line bad syntax is present in TOML file
164 | if errorLine > 2 && errorLine+2 <= fileLength {
165 | for i := errorLine - 2; i <= errorLine+2; i++ {
166 | if i == errorLine {
167 | errorStr := ""
168 | if i >= 10 {
169 | errorStr = fmt.Sprintf("> %d | %s", i, lineText[i-1])
170 | } else {
171 | errorStr = fmt.Sprintf("> %d | %s", i, lineText[i-1])
172 | }
173 | pterm.NewStyle(pterm.FgLightRed).Println(errorStr)
174 | } else {
175 | errorStr := ""
176 | if i >= 10 {
177 | errorStr = fmt.Sprintf(" %d | %s", i, lineText[i-1])
178 | } else {
179 | errorStr = fmt.Sprintf(" %d | %s", i, lineText[i-1])
180 | }
181 | pterm.NewStyle(pterm.FgLightYellow).Println(errorStr)
182 |
183 | }
184 | }
185 | } else {
186 | errorStr := fmt.Sprintf("> %d | %s", errorLine, lineText[errorLine-1])
187 | pterm.NewStyle(pterm.FgLightRed).Println(errorStr)
188 | }
189 | }
190 |
191 | // Handles printing the errors in the DeepSource config (.deepsource.toml)
192 | func printConfigErrors(errors []string) {
193 | for _, error := range errors {
194 | pterm.Error.WithShowLineNumber(false).Println(error)
195 | }
196 | }
197 |
198 | // Handles printing the valid config output
199 | func printValidConfig() {
200 | pterm.Success.Println("Config Valid")
201 | }
202 |
--------------------------------------------------------------------------------
/deepsource/client.go:
--------------------------------------------------------------------------------
1 | // DeepSource SDK
2 | package deepsource
3 |
4 | import (
5 | "context"
6 | "fmt"
7 |
8 | "github.com/deepsourcelabs/cli/deepsource/analyzers"
9 | analyzerQuery "github.com/deepsourcelabs/cli/deepsource/analyzers/queries"
10 | "github.com/deepsourcelabs/cli/deepsource/auth"
11 | authmut "github.com/deepsourcelabs/cli/deepsource/auth/mutations"
12 | "github.com/deepsourcelabs/cli/deepsource/issues"
13 | issuesQuery "github.com/deepsourcelabs/cli/deepsource/issues/queries"
14 | "github.com/deepsourcelabs/cli/deepsource/repository"
15 | repoQuery "github.com/deepsourcelabs/cli/deepsource/repository/queries"
16 | "github.com/deepsourcelabs/cli/deepsource/transformers"
17 | transformerQuery "github.com/deepsourcelabs/cli/deepsource/transformers/queries"
18 | "github.com/deepsourcelabs/graphql"
19 | )
20 |
21 | var defaultHostName = "deepsource.io"
22 |
23 | type ClientOpts struct {
24 | Token string
25 | HostName string
26 | }
27 |
28 | type Client struct {
29 | gql *graphql.Client
30 | token string
31 | }
32 |
33 | // Returns a GraphQL client which can be used to interact with the GQL APIs
34 | func (c Client) GQL() *graphql.Client {
35 | return c.gql
36 | }
37 |
38 | // Returns the PAT which is required for authentication and thus, interacting with the APIs
39 | func (c Client) GetToken() string {
40 | return c.token
41 | }
42 |
43 | // Returns a new GQLClient
44 | func New(cp ClientOpts) (*Client, error) {
45 | apiClientURL := getAPIClientURL(cp.HostName)
46 | gql := graphql.NewClient(apiClientURL)
47 | return &Client{
48 | gql: gql,
49 | token: cp.Token,
50 | }, nil
51 | }
52 |
53 | // // Formats and returns the DeepSource Public API client URL
54 | func getAPIClientURL(hostName string) string {
55 | apiClientURL := fmt.Sprintf("https://api.%s/graphql/", defaultHostName)
56 |
57 | // Check if the domain is different from the default domain (In case of Enterprise users)
58 | if hostName != defaultHostName {
59 | apiClientURL = fmt.Sprintf("https://%s/api/graphql/", hostName)
60 | }
61 | return apiClientURL
62 | }
63 |
64 | // Registers the device and allots it a device code which is further used for fetching
65 | // the PAT and other authentication data
66 | func (c Client) RegisterDevice(ctx context.Context) (*auth.Device, error) {
67 | req := authmut.RegisterDeviceRequest{}
68 | res, err := req.Do(ctx, c)
69 | if err != nil {
70 | return nil, err
71 | }
72 | return res, nil
73 | }
74 |
75 | // Logs in the client using the deviceCode and the user Code and returns the PAT and data which is required for authentication
76 | func (c Client) Login(ctx context.Context, deviceCode, description string) (*auth.PAT, error) {
77 | req := authmut.RequestPATRequest{
78 | Params: authmut.RequestPATParams{
79 | DeviceCode: deviceCode,
80 | Description: description,
81 | },
82 | }
83 |
84 | res, err := req.Do(ctx, c)
85 | if err != nil {
86 | return nil, err
87 | }
88 | return res, nil
89 | }
90 |
91 | // Refreshes the authentication credentials. Takes the refreshToken as a parameter.
92 | func (c Client) RefreshAuthCreds(ctx context.Context, token string) (*auth.PAT, error) {
93 | req := authmut.RefreshTokenRequest{
94 | Params: authmut.RefreshTokenParams{
95 | Token: token,
96 | },
97 | }
98 | res, err := req.Do(ctx, c)
99 | if err != nil {
100 | return nil, err
101 | }
102 | return res, nil
103 | }
104 |
105 | // Returns the list of Analyzers supported by DeepSource along with their meta like shortcode, metaschema.
106 | func (c Client) GetSupportedAnalyzers(ctx context.Context) ([]analyzers.Analyzer, error) {
107 | req := analyzerQuery.AnalyzersRequest{}
108 | res, err := req.Do(ctx, c)
109 | if err != nil {
110 | return nil, err
111 | }
112 | return res, nil
113 | }
114 |
115 | // Returns the list of Transformers supported by DeepSource along with their meta like shortcode.
116 | func (c Client) GetSupportedTransformers(ctx context.Context) ([]transformers.Transformer, error) {
117 | req := transformerQuery.TransformersRequest{}
118 | res, err := req.Do(ctx, c)
119 | if err != nil {
120 | return nil, err
121 | }
122 | return res, nil
123 | }
124 |
125 | // Returns the activation status of the repository whose data is sent as parameters.
126 | // Owner : The username of the owner of the repository
127 | // repoName : The name of the repository whose activation status has to be queried
128 | // provider : The VCS provider which hosts the repo (GITHUB/GITLAB/BITBUCKET)
129 | func (c Client) GetRepoStatus(ctx context.Context, owner, repoName, provider string) (*repository.Meta, error) {
130 | req := repoQuery.RepoStatusRequest{
131 | Params: repoQuery.RepoStatusParams{
132 | Owner: owner,
133 | RepoName: repoName,
134 | Provider: provider,
135 | },
136 | }
137 |
138 | res, err := req.Do(ctx, c)
139 | if err != nil {
140 | return nil, err
141 | }
142 | return res, nil
143 | }
144 |
145 | // Returns the list of issues for a certain repository whose data is sent as parameters.
146 | // Owner : The username of the owner of the repository
147 | // repoName : The name of the repository whose activation status has to be queried
148 | // provider : The VCS provider which hosts the repo (GITHUB/GITLAB/BITBUCKET)
149 | // limit : The amount of issues to be listed. The default limit is 30 while the maximum limit is currently 100.
150 | func (c Client) GetIssues(ctx context.Context, owner, repoName, provider string, limit int) ([]issues.Issue, error) {
151 | req := issuesQuery.IssuesListRequest{
152 | Params: issuesQuery.IssuesListParams{
153 | Owner: owner,
154 | RepoName: repoName,
155 | Provider: provider,
156 | Limit: limit,
157 | },
158 | }
159 | res, err := req.Do(ctx, c)
160 | if err != nil {
161 | return nil, err
162 | }
163 |
164 | return res, nil
165 | }
166 |
167 | // Returns the list of issues reported for a certain file in a certain repository whose data is sent as parameters.
168 | // Owner : The username of the owner of the repository
169 | // repoName : The name of the repository whose activation status has to be queried
170 | // provider : The VCS provider which hosts the repo (GITHUB/GITLAB/BITBUCKET)
171 | // filePath : The relative path of the file. Eg: "tests/mock.py" if a file `mock.py` is present in `tests` directory which in turn is present in the root dir
172 | // limit : The amount of issues to be listed. The default limit is 30 while the maximum limit is currently 100.
173 | func (c Client) GetIssuesForFile(ctx context.Context, owner, repoName, provider, filePath string, limit int) ([]issues.Issue, error) {
174 | req := issuesQuery.FileIssuesListRequest{
175 | Params: issuesQuery.FileIssuesListParams{
176 | Owner: owner,
177 | RepoName: repoName,
178 | Provider: provider,
179 | FilePath: filePath,
180 | Limit: limit,
181 | },
182 | }
183 |
184 | res, err := req.Do(ctx, c)
185 | if err != nil {
186 | return nil, err
187 | }
188 | return res, nil
189 | }
190 |
--------------------------------------------------------------------------------
/command/issues/list/utils.go:
--------------------------------------------------------------------------------
1 | package list
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 |
9 | "github.com/deepsourcelabs/cli/deepsource/issues"
10 | "github.com/owenrumney/go-sarif/v2/sarif"
11 | )
12 |
13 | type ExportData struct {
14 | Occurences []IssueJSON `json:"occurences"`
15 | Summary Summary `json:"summary"`
16 | }
17 |
18 | type Summary struct {
19 | TotalOccurences int `json:"total_occurences"`
20 | UniqueIssues int `json:"unique_issues"`
21 | }
22 |
23 | ///////////////////////
24 | // Filtering utilities
25 | ///////////////////////
26 |
27 | // Filters issues based on a path, works for both directories and files
28 | func filterIssuesByPath(path string, issuesData []issues.Issue) ([]issues.Issue, error) {
29 | var filteredIssues []issues.Issue
30 | for _, issue := range issuesData {
31 | up := ".." + string(os.PathSeparator)
32 |
33 | // get relative path
34 | rel, err := filepath.Rel(path, issue.Location.Path)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | // handle files
40 | if rel == "." {
41 | filteredIssues = append(filteredIssues, issue)
42 | }
43 |
44 | // check if the relative path has a parent directory
45 | if !strings.HasPrefix(rel, up) && rel != ".." {
46 | filteredIssues = append(filteredIssues, issue)
47 | }
48 | }
49 |
50 | return getUniqueIssues(filteredIssues), nil
51 | }
52 |
53 | // Filters issues based on the analyzer shortcode.
54 | func filterIssuesByAnalyzer(analyzer []string, issuesData []issues.Issue) ([]issues.Issue, error) {
55 | var filteredIssues []issues.Issue
56 |
57 | // maintain a map of analyzer shortcodes
58 | analyzerMap := make(map[string]bool)
59 | for _, shortcode := range analyzer {
60 | analyzerMap[shortcode] = true
61 | }
62 |
63 | for _, issue := range issuesData {
64 | if analyzerMap[issue.Analyzer.Shortcode] {
65 | filteredIssues = append(filteredIssues, issue)
66 | }
67 | }
68 |
69 | return getUniqueIssues(filteredIssues), nil
70 | }
71 |
72 | // Returns de-duplicated issues.
73 | func getUniqueIssues(fetchedIssues []issues.Issue) []issues.Issue {
74 | var uniqueIssues []issues.Issue
75 |
76 | // inUnique is a map which is used for checking whether an issue exists already or not
77 | inUnique := make(map[issues.Issue]bool)
78 |
79 | for _, issue := range fetchedIssues {
80 | // if the issue isn't present in inUnique, append the issue to uniqueIssues and update inUnique
81 | if _, ok := inUnique[issue]; !ok {
82 | inUnique[issue] = true
83 | uniqueIssues = append(uniqueIssues, issue)
84 | }
85 | }
86 |
87 | return uniqueIssues
88 | }
89 |
90 | ///////////////////////
91 | // Conversion utilities
92 | ///////////////////////
93 |
94 | // Converts issueData to a JSON-compatible struct
95 | func convertJSON(issueData []issues.Issue) ExportData {
96 | var occurences []IssueJSON
97 | var issueExport ExportData
98 |
99 | set := make(map[string]string)
100 | total_occurences := 0
101 |
102 | for _, issue := range issueData {
103 | issueNew := IssueJSON{
104 | Analyzer: issue.Analyzer.Shortcode,
105 | IssueCode: issue.IssueCode,
106 | IssueTitle: issue.IssueText,
107 | OccurenceTitle: issue.IssueText,
108 | IssueCategory: "",
109 | Location: LocationJSON{
110 | Path: issue.Location.Path,
111 | Position: PositionJSON{
112 | Begin: LineColumn{
113 | Line: issue.Location.Position.BeginLine,
114 | Column: 0,
115 | },
116 | End: LineColumn{
117 | Line: issue.Location.Position.EndLine,
118 | Column: 0,
119 | },
120 | },
121 | },
122 | }
123 |
124 | total_occurences += 1
125 | set[issue.IssueCode] = ""
126 |
127 | occurences = append(occurences, issueNew)
128 | }
129 |
130 | issueExport.Occurences = occurences
131 | issueExport.Summary.TotalOccurences = total_occurences
132 | issueExport.Summary.UniqueIssues = len(set)
133 |
134 | return issueExport
135 | }
136 |
137 | // Converts issueData to a CSV records
138 | func convertCSV(issueData []issues.Issue) [][]string {
139 | records := [][]string{{"analyzer", "issue_code", "issue_title", "occurence_title", "issue_category", "path", "begin_line", "begin_column", "end_line", "end_column"}}
140 |
141 | for _, issue := range issueData {
142 | issueNew := []string{issue.Analyzer.Shortcode, issue.IssueCode, issue.IssueText, issue.IssueText, "", issue.Location.Path, fmt.Sprint(issue.Location.Position.BeginLine), "0", fmt.Sprint(issue.Location.Position.EndLine), "0"}
143 |
144 | records = append(records, issueNew)
145 | }
146 |
147 | return records
148 | }
149 |
150 | // Converts issueData to a SARIF report
151 | func convertSARIF(issueData []issues.Issue) *sarif.Report {
152 | report, err := sarif.New(sarif.Version210)
153 | if err != nil {
154 | return nil
155 | }
156 |
157 | // use a map of shortcodes to append rules and results
158 | type boolIndex struct {
159 | exists bool
160 | index int
161 | }
162 | shortcodes := make(map[string]boolIndex)
163 | var runs []*sarif.Run
164 | count := 0
165 |
166 | // Adding the tools data to the SARIF report corresponding to the number of analyzers activated
167 | for _, issue := range issueData {
168 | if !shortcodes[issue.Analyzer.Shortcode].exists {
169 | driverName := "DeepSource " + strings.Title(issue.Analyzer.Shortcode) + " Analyzer"
170 | informationURI := "https://deepsource.io/directory/analyzers/" + string(issue.Analyzer.Shortcode)
171 |
172 | tool := sarif.Tool{
173 | Driver: &sarif.ToolComponent{
174 | Name: driverName,
175 | InformationURI: &informationURI,
176 | },
177 | }
178 |
179 | run := sarif.NewRun(tool)
180 | runs = append(runs, run)
181 |
182 | // update boolIndex
183 | shortcodes[issue.Analyzer.Shortcode] = boolIndex{exists: true, index: count}
184 | count += 1
185 | }
186 | }
187 |
188 | // use an index map for updating rule index value
189 | idxMap := make(map[int]int)
190 |
191 | // Adding the results data for each analyzer in the report
192 | for _, issue := range issueData {
193 | // TODO: Fetch issue description from the API and populate here
194 | textDescription := ""
195 | fullDescription := sarif.MultiformatMessageString{
196 | Text: &textDescription,
197 | }
198 |
199 | // check if the shortcode exists in the map
200 | if shortcodes[issue.Analyzer.Shortcode].exists {
201 | // fetch shortcode index
202 | idx := shortcodes[issue.Analyzer.Shortcode].index
203 |
204 | // TODO: fetch category and recommended fields
205 | pb := sarif.NewPropertyBag()
206 | pb.Add("category", "")
207 | pb.Add("recommended", "")
208 |
209 | helpURI := "https://deepsource.io/directory/analyzers/" + string(issue.Analyzer.Shortcode) + "/issues/" + string(issue.IssueCode)
210 |
211 | // add rule
212 | runs[idx].AddRule(issue.IssueCode).WithName(issue.IssueText).WithFullDescription(&fullDescription).WithHelpURI(helpURI).WithProperties(pb.Properties)
213 |
214 | // add result
215 | runs[idx].CreateResultForRule(issue.IssueCode).WithLevel("error").WithKind("fail").WithMessage(sarif.NewTextMessage(
216 | issue.IssueText,
217 | )).WithRuleIndex(idxMap[idx]).AddLocation(
218 | sarif.NewLocationWithPhysicalLocation(
219 | sarif.NewPhysicalLocation().WithArtifactLocation(
220 | sarif.NewSimpleArtifactLocation(issue.Location.Path),
221 | ).WithRegion(
222 | sarif.NewSimpleRegion(issue.Location.Position.BeginLine, issue.Location.Position.EndLine),
223 | ),
224 | ),
225 | )
226 |
227 | idxMap[idx] += 1
228 | }
229 | }
230 |
231 | // add all runs to report
232 | for _, run := range runs {
233 | report.AddRun(run)
234 | }
235 |
236 | return report
237 | }
238 |
--------------------------------------------------------------------------------
/command/report/tests/report_workflow_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/json"
7 | "io"
8 | "log"
9 | "net/http"
10 | "os"
11 | "os/exec"
12 | "strings"
13 | "testing"
14 |
15 | "github.com/DataDog/zstd"
16 | "github.com/deepsourcelabs/cli/command/report"
17 | "github.com/google/go-cmp/cmp"
18 | )
19 |
20 | // Workflow tested:
21 | //
22 | // - Run deepsource CLI with report command and value flag
23 | // - Run deepsource CLI with report command and value-file flag
24 |
25 | // Sample values to the run the analyzer on
26 | const (
27 | analyzer = "test-coverage"
28 | commitOid = "b7ff1a5ecb0dce0541b935224f852ee98570bbd4"
29 | dsn = "http://f59ab9314307@localhost:8081"
30 | key = "python"
31 | )
32 |
33 | func graphQLAPIMock(w http.ResponseWriter, r *http.Request) {
34 | // Read request request request body
35 | req, err := io.ReadAll(r.Body)
36 | if err != nil {
37 | log.Println(err)
38 | return
39 | }
40 |
41 | // Check if the request has ArtifactMetadataInput in body
42 | if bytes.Contains(req, []byte("ArtifactMetadataInput")) {
43 | log.Println("ArtifactMetadataInput found in request body")
44 | w.WriteHeader(http.StatusOK)
45 | w.Header().Set("Content-Type", "application/json")
46 |
47 | successResponseBodyData, err := os.ReadFile("./golden_files/report_grqphql_artifactmetadatainput_response_success.json")
48 | if err != nil {
49 | log.Println(err)
50 | return
51 | }
52 | w.Write([]byte(successResponseBodyData))
53 |
54 | } else {
55 |
56 | // Unmarshal request body into ReportQuery
57 | var reportQuery report.ReportQuery
58 | err = json.Unmarshal(req, &reportQuery)
59 | if err != nil {
60 | log.Println(err)
61 | return
62 | }
63 |
64 | requestData := reportQuery.Variables.Input.Data
65 |
66 | // Decode base64 encoded data
67 | decodedData, err := base64.StdEncoding.DecodeString(requestData)
68 | if err != nil {
69 | log.Println(err)
70 | return
71 | }
72 |
73 | // Decompress zstd compressed data
74 | decompressedData, err := zstd.Decompress(nil, decodedData)
75 | if err != nil {
76 | log.Println(err)
77 | return
78 | }
79 |
80 | // Create new ReportQeury object with decompressed data
81 | reportQuery.Variables.Input.Data = string(decompressedData)
82 |
83 | // Read test graphql request body artifact file
84 | requestBodyGoldenFilePath := "./golden_files/report_graphql_request_body.json"
85 |
86 | if reportQuery.Variables.Input.AnalyzerType == "community" {
87 | // There's a separate goldenfile for request made with a type flag passed as community
88 | requestBodyGoldenFilePath = "./golden_files/report_graphql_community_request_body.json"
89 | }
90 |
91 | requestBodyData, err := os.ReadFile(requestBodyGoldenFilePath)
92 | if err != nil {
93 | log.Println(err)
94 | return
95 | }
96 |
97 | // Unmarshal request body into ReportQuery
98 | var requestReportQuery report.ReportQuery
99 | err = json.Unmarshal(requestBodyData, &requestReportQuery)
100 | if err != nil {
101 | log.Println(err)
102 | return
103 | }
104 |
105 | // Make a map for metadata with workdir and compressed.This is to make local tests respect env variables.
106 | metadata := make(map[string]interface{})
107 | metadata["workDir"] = os.Getenv("CODE_PATH")
108 | metadata["compressed"] = "True"
109 | requestReportQuery.Variables.Input.Metadata = metadata
110 |
111 | // Also change the ReporterVersion to the version of the CLI
112 | requestReportQuery.Variables.Input.ReporterVersion = report.CliVersion
113 |
114 | // Read test graphql success response body artifact file
115 | successResponseBodyData, err := os.ReadFile("./golden_files/report_graphql_success_response_body.json")
116 | if err != nil {
117 | log.Println(err)
118 | return
119 | }
120 |
121 | // Read test graphql error response body artifact file
122 | errorResponseBodyData, err := os.ReadFile("./golden_files/report_graphql_error_response_body.json")
123 | if err != nil {
124 | log.Println(err)
125 | return
126 | }
127 |
128 | w.WriteHeader(http.StatusOK)
129 | w.Header().Set("Content-Type", "application/json")
130 |
131 | requestReportQuery.Variables.Input.Data = strings.TrimSuffix(requestReportQuery.Variables.Input.Data, "\n")
132 | reportQuery.Variables.Input.Data = strings.TrimSuffix(reportQuery.Variables.Input.Data, "\n")
133 |
134 | if want, got := requestReportQuery, reportQuery; cmp.Equal(want, got) {
135 | w.Write([]byte(successResponseBodyData))
136 | } else {
137 | if want != got {
138 | log.Printf("Mismatch found:\nDiff: %s\n", cmp.Diff(want, got))
139 | }
140 | w.Write([]byte(errorResponseBodyData))
141 | }
142 | }
143 | }
144 |
145 | func TestReportKeyValueWorkflow(t *testing.T) {
146 | // Read test artifact file
147 | data, err := os.ReadFile("/tmp/python_coverage.xml")
148 | if err != nil {
149 | t.Error(err)
150 | }
151 |
152 | cmd := exec.Command("/tmp/deepsource",
153 | "report",
154 | "--analyzer",
155 | analyzer,
156 | "--key",
157 | key,
158 | "--value",
159 | string(data),
160 | )
161 |
162 | // Set env variables
163 | cmd.Env = os.Environ()
164 | cmd.Env = append(cmd.Env, "DEEPSOURCE_DSN="+dsn)
165 | cmd.Dir = os.Getenv("CODE_PATH")
166 |
167 | var stdout, stderr bytes.Buffer
168 |
169 | cmd.Stdout = &stdout
170 | cmd.Stderr = &stderr
171 |
172 | err = cmd.Run()
173 |
174 | outStr, errStr := stdout.String(), stderr.String()
175 | log.Printf("== Run deepsource CLI command ==\n%s\n%s\n", outStr, errStr)
176 |
177 | if err != nil {
178 | log.Println(outStr)
179 | log.Println(errStr)
180 | t.Errorf("Error executing deepsource CLI command: %v", err)
181 | }
182 |
183 | output, err := os.ReadFile("./golden_files/report_success.txt")
184 | if err != nil {
185 | t.Fatal(err)
186 | }
187 |
188 | if want := string(output); want != outStr {
189 | t.Errorf("Expected: %s, Got: %s", want, outStr)
190 | }
191 | }
192 |
193 | func TestReportKeyValueFileWorkflow(t *testing.T) {
194 | cmd := exec.Command("/tmp/deepsource",
195 | "report",
196 | "--analyzer",
197 | analyzer,
198 | "--key",
199 | key,
200 | "--value-file",
201 | "/tmp/python_coverage.xml",
202 | )
203 |
204 | // Set env variables
205 | cmd.Env = os.Environ()
206 | cmd.Env = append(cmd.Env, "DEEPSOURCE_DSN="+dsn)
207 | cmd.Dir = os.Getenv("CODE_PATH")
208 |
209 | var stdout, stderr bytes.Buffer
210 | cmd.Stdout = &stdout
211 | cmd.Stderr = &stderr
212 |
213 | err := cmd.Run()
214 |
215 | outStr, errStr := stdout.String(), stderr.String()
216 | log.Printf("== Run deepsource CLI command ==\n%s\n%s\n", outStr, errStr)
217 |
218 | if err != nil {
219 | log.Println(outStr)
220 | log.Println(errStr)
221 | t.Errorf("Error executing deepsource CLI command: %v", err)
222 | }
223 |
224 | output, err := os.ReadFile("./golden_files/report_success.txt")
225 | if err != nil {
226 | t.Fatal(err)
227 | }
228 |
229 | if want := string(output); want != outStr {
230 | t.Errorf("Expected: %s, Got: %s", want, outStr)
231 | }
232 | }
233 |
234 | func TestReportAnalyzerTypeWorkflow(t *testing.T) {
235 | cmd := exec.Command("/tmp/deepsource",
236 | "report",
237 | "--analyzer",
238 | analyzer,
239 | "--analyzer-type",
240 | "community",
241 | "--key",
242 | key,
243 | "--value-file",
244 | "/tmp/python_coverage.xml",
245 | )
246 |
247 | // Set env variables
248 | cmd.Env = os.Environ()
249 | cmd.Env = append(cmd.Env, "DEEPSOURCE_DSN="+dsn)
250 | cmd.Dir = os.Getenv("CODE_PATH")
251 |
252 | var stdout, stderr bytes.Buffer
253 | cmd.Stdout = &stdout
254 | cmd.Stderr = &stderr
255 |
256 | err := cmd.Run()
257 |
258 | outStr, errStr := stdout.String(), stderr.String()
259 | log.Printf("== Run deepsource CLI command ==\n%s\n%s\n", outStr, errStr)
260 |
261 | if err != nil {
262 | log.Println(outStr)
263 | log.Println(errStr)
264 | t.Errorf("Error executing deepsource CLI command: %v", err)
265 | }
266 |
267 | output, err := os.ReadFile("./golden_files/report_success.txt")
268 | if err != nil {
269 | t.Fatal(err)
270 | }
271 |
272 | if want := string(output); want != outStr {
273 | t.Errorf("Expected: %s, Got: %s", want, outStr)
274 | }
275 | }
276 |
--------------------------------------------------------------------------------