├── .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 | --------------------------------------------------------------------------------