diff --git a/.drone.yml b/.drone.yml index a8199ff..e24c735 100644 --- a/.drone.yml +++ b/.drone.yml @@ -14,38 +14,13 @@ steps: image: docker.io/golang:1.20-bullseye network_mode: host commands: - - apt-get update && apt-get install -y jq - go mod download - - go install "github.com/vektra/mockery/v2@$(curl https://api.github.com/repos/vektra/mockery/releases | jq -r '. | first | .tag_name')" - - go generate ./... + - go run github.com/magefile/mage -d build -w . generate volumes: - name: go-cache path: /go - - name: Lint PR - image: docker.io/golangci/golangci-lint:latest - network_mode: host - environment: - CGO_ENABLED: "0" - GOMEMLIMIT: "1150MiB" - NITTER_BASE_ADDRESS: https://code.icb4dc0.de - NITTER_TOKEN: - from_secret: gitea_token - when: - event: - - pull_request - commands: - - apt-get update && apt-get install -y jq - - mkdir out - - golangci-lint run --out-format json --issues-exit-code 0 --new-from-rev "$(curl https://code.icb4dc0.de/api/v1/repos/prskr/nitter/pulls/1 | jq -r '.base.sha')" > out/results.json - - go run main.go gitea pr --namespace "$${DRONE_REPO_NAMESPACE}" --repo "$${DRONE_REPO_NAME}" --result-file out/results.json --pull-index "$${DRONE_PULL_REQUEST}" - depends_on: - - Setup - volumes: - - name: go-cache - path: /go - - - name: Lint repo + - name: Lint image: docker.io/golangci/golangci-lint:latest environment: CGO_ENABLED: "0" @@ -53,13 +28,8 @@ steps: NITTER_BASE_ADDRESS: https://code.icb4dc0.de NITTER_TOKEN: from_secret: gitea_token - when: - event: - - push - - tag commands: - - mkdir out - - golangci-lint run -v + - go run github.com/magefile/mage -d build -w . lint depends_on: - Setup volumes: diff --git a/.gitignore b/.gitignore index 890f962..f0a9847 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,4 @@ mock_*_test.go # vendor/ out/ -# Go workspace file -go.work - .idea/ \ No newline at end of file diff --git a/build/format.go b/build/format.go new file mode 100644 index 0000000..7994721 --- /dev/null +++ b/build/format.go @@ -0,0 +1,33 @@ +//go:build mage + +package main + +import ( + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +func Format() { + mg.Deps(GoImports) + mg.Deps(GoFumpt) +} + +func GoImports() error { + if err := ensureGoTool("goimports", "golang.org/x/tools/cmd/goimports", "latest"); err != nil { + return err + } + + return sh.RunV( + "goimports", + "-local=inetmock.icb4dc0.de/inetmock", + "-w", + WorkingDir, + ) +} + +func GoFumpt() error { + if err := ensureGoTool("gofumpt", "mvdan.cc/gofumpt", "latest"); err != nil { + return err + } + return sh.RunV("gofumpt", "-l", "-w", WorkingDir) +} diff --git a/build/generate.go b/build/generate.go new file mode 100644 index 0000000..fd13c09 --- /dev/null +++ b/build/generate.go @@ -0,0 +1,47 @@ +//go:build mage + +package main + +import ( + "context" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/magefile/mage/target" + "golang.org/x/exp/slog" +) + +func Generate(ctx context.Context) error { + mockeryVersion, err := getLatestReleaseTag(ctx, "vektra/mockery") + if err != nil { + return err + } + if err := ensureGoTool("mockery", "github.com/vektra/mockery/v2", mockeryVersion); err != nil { + return err + } + + mg.Deps(GenerateGo) + + return nil +} + +func GenerateGo() error { + lastMockGeneration, err := target.NewestModTime(GeneratedMockFiles...) + if err != nil { + return err + } + + lastSourceModification, err := target.NewestModTime(GoSourceFiles...) + if err != nil { + return err + } + + slog.Debug("Determined last time mocks where generated", slog.Time("lastMockGeneration", lastMockGeneration)) + + if lastMockGeneration.After(lastSourceModification) { + slog.Info("Skipping unnecessary 'go generate' invocation") + return nil + } + + return sh.RunV("go", "generate", "-x", "./...") +} diff --git a/build/go.mod b/build/go.mod new file mode 100644 index 0000000..7143f07 --- /dev/null +++ b/build/go.mod @@ -0,0 +1,16 @@ +module build + +go 1.20 + +require ( + github.com/carlmjohnson/requests v0.23.2 + github.com/magefile/mage v1.14.0 + golang.org/x/exp v0.0.0-20230307190834-24139beb5833 + golang.org/x/sync v0.1.0 +) + +require ( + code.gitea.io/sdk/gitea v0.15.1 // indirect + github.com/hashicorp/go-version v1.2.1 // indirect + golang.org/x/net v0.5.0 // indirect +) diff --git a/build/go.sum b/build/go.sum new file mode 100644 index 0000000..6a05b40 --- /dev/null +++ b/build/go.sum @@ -0,0 +1,38 @@ +code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= +code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= +code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= +github.com/carlmjohnson/requests v0.23.2 h1:SzaY+/5v8QOvt++7HTXe1xgmIb3wc/bYf2QJmrO73sM= +github.com/carlmjohnson/requests v0.23.2/go.mod h1:09VwhOaRQYCraJcByjEuvuOGO1jxUjIx6vnAEkt2ges= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s= +golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/build/lint.go b/build/lint.go new file mode 100644 index 0000000..74575b7 --- /dev/null +++ b/build/lint.go @@ -0,0 +1,42 @@ +//go:build mage + +package main + +import ( + "context" + "fmt" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +func Lint(ctx context.Context) { + mg.CtxDeps(ctx, Generate) + mg.CtxDeps(ctx, Format) + mg.Deps(LintGo) +} + +func LintGo() (err error) { + if IsPRBuild { + return lintGoPR() + } + + return sh.RunV( + "golangci-lint", + "run", + "-v", + "--issues-exit-code=1", + ) +} + +func lintGoPR() error { + return GoRun( + "main.go", + "gitea", + "lint-pr", + "--from-pr-base", + fmt.Sprintf("--namespace=%s", repoOwner), + fmt.Sprintf("--repo=%s", repoName), + fmt.Sprintf("--pull-index=%d", prIndex), + ) +} diff --git a/build/setup.go b/build/setup.go new file mode 100644 index 0000000..f39fd85 --- /dev/null +++ b/build/setup.go @@ -0,0 +1,99 @@ +//go:build mage + +package main + +import ( + "io/fs" + "os" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/exp/slices" + "golang.org/x/exp/slog" +) + +var ( + WorkingDir string + GoSourceFiles []string + GeneratedMockFiles []string + IsReleaseBuild bool + IsPRBuild bool + + repoOwner = "prskr" + repoName = "nitter" + prIndex int64 = 1 + + dirsToIgnore = []string{ + ".git", + "build", + ".run", + ".task", + } +) + +func init() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr))) + + IsReleaseBuild = strings.EqualFold(os.Getenv("DRONE_BUILD_EVENT"), "tag") + IsPRBuild = strings.EqualFold(os.Getenv("DRONE_BUILD_EVENT"), "pull_request") + + if wd, err := os.Getwd(); err != nil { + slog.Error("Failed to get working directory", err) + os.Exit(1) + } else { + WorkingDir = wd + } + + if owner := os.Getenv("DRONE_REPO_NAMESPACE"); owner != "" { + repoOwner = owner + } + + if name := os.Getenv("DRONE_REPO_NAME"); name != "" { + repoName = name + } + + if prIdxRaw := os.Getenv("DRONE_PULL_REQUEST"); prIdxRaw != "" { + if parsed, err := strconv.ParseInt(prIdxRaw, 10, 64); err != nil { + slog.Error("Failed to parse PR index", err) + os.Exit(1) + } else { + prIndex = parsed + } + } + if err := initSourceFiles(); err != nil { + slog.Error("Failed to init source files", err) + os.Exit(1) + } + + slog.Info("Completed initialization") +} + +func initSourceFiles() error { + return filepath.WalkDir(WorkingDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + if slices.Contains(dirsToIgnore, filepath.Base(path)) { + return fs.SkipDir + } + return nil + } + + _, ext, found := strings.Cut(filepath.Base(path), ".") + if !found { + return nil + } + + switch ext { + case "mock.go": + GeneratedMockFiles = append(GeneratedMockFiles, path) + case "go": + GoSourceFiles = append(GoSourceFiles, path) + } + + return nil + }) +} diff --git a/build/tools.go b/build/tools.go new file mode 100644 index 0000000..784798b --- /dev/null +++ b/build/tools.go @@ -0,0 +1,71 @@ +//go:build mage + +package main + +import ( + "context" + "fmt" + "os/exec" + "path" + "path/filepath" + + "github.com/carlmjohnson/requests" + "github.com/magefile/mage/sh" + "golang.org/x/exp/slog" +) + +var ( + GoReleaser = sh.RunCmd("goreleaser") + GoInstall = sh.RunCmd("go", "install") + GoBuild = sh.RunCmd("go", "build") + GoRun = sh.RunCmd("go", "run") +) + +func getLatestReleaseTag(ctx context.Context, repo string) (tag string, err error) { + type release struct { + TagName string `json:"tag_name"` + } + + var releases []release + + err = requests. + URL(fmt.Sprintf("https://%s", path.Join("api.github.com/repos", repo, "releases"))). + ToJSON(&releases). + Fetch(ctx) + + if err != nil { + return "", err + } + + if len(releases) < 1 { + return "", fmt.Errorf("no release found for repo %s", repo) + } + + return releases[0].TagName, nil +} + +func ensureURLTool(ctx context.Context, toolName, downloadURL string) error { + return checkForTool(toolName, func() error { + return requests. + URL(downloadURL). + ToFile(filepath.Join("/", "usr", "local", "bin", toolName)). + Fetch(ctx) + }) +} + +func ensureGoTool(toolName, importPath, version string) error { + return checkForTool(toolName, func() error { + toolToInstall := fmt.Sprintf("%s@%s", importPath, version) + slog.Info("Installing Go tool", slog.String("toolToInstall", toolToInstall)) + return GoInstall(toolToInstall) + }) +} + +func checkForTool(toolName string, fallbackAction func() error) error { + if _, err := exec.LookPath(toolName); err != nil { + slog.Warn("tool is missing", slog.String("toolName", toolName)) + return fallbackAction() + } + + return nil +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..689106a --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.20 + +use ( + . + ./build +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..a0e00a6 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,9 @@ +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/commands/gitea.go b/internal/commands/gitea.go index 96a76f3..20e4719 100644 --- a/internal/commands/gitea.go +++ b/internal/commands/gitea.go @@ -1,12 +1,44 @@ package commands import ( - gosdk "code.gitea.io/sdk/gitea" + "fmt" + "strings" + + giteaSdk "code.gitea.io/sdk/gitea" "github.com/spf13/cobra" "code.icb4dc0.de/prskr/nitter/nitters/gitea" ) +type reviewStateTypeValue giteaSdk.ReviewStateType + +func (r *reviewStateTypeValue) String() string { + return string(*r) +} + +func (r *reviewStateTypeValue) Set(s string) error { + switch strings.ToUpper(s) { + case string(giteaSdk.ReviewStateApproved): + *r = reviewStateTypeValue(giteaSdk.ReviewStateApproved) + return nil + case string(giteaSdk.ReviewStateRequestChanges): + *r = reviewStateTypeValue(giteaSdk.ReviewStateRequestChanges) + return nil + case string(giteaSdk.ReviewStateComment): + *r = reviewStateTypeValue(giteaSdk.ReviewStateComment) + return nil + default: + return fmt.Errorf("value %s doesn't match any expected state type", s) + } +} + +func (r *reviewStateTypeValue) Type() string { + return "string" +} + +var errorReviewStateType = reviewStateTypeValue(giteaSdk.ReviewStateComment) + +//nolint:lll //flag setup causes long lines func Gitea() *cobra.Command { giteaCmd := &cobra.Command{ Use: "gitea", @@ -18,31 +50,9 @@ func Gitea() *cobra.Command { pr := &cobra.Command{ Use: "pull-request", - Aliases: []string{"merge-request", "pr", "mr", "pull"}, - RunE: func(cmd *cobra.Command, args []string) error { - cfg, err := LoadConfig[gitea.GiteaPRConfig](cmd.Flags()) - if err != nil { - return err - } - - if err = cfg.Validate(); err != nil { - return err - } - - report, issues, err := ReadResultsFile(cmd.Flag("result-file").Value.String()) - if err != nil { - return err - } - - giteaClient, err := gosdk.NewClient(cfg.BaseAddress, gosdk.SetContext(cmd.Context()), gosdk.SetToken(cfg.Token)) - if err != nil { - return err - } - - nitter := gitea.NewPRNitter(giteaClient, cfg) - - return nitter.Report(report, issues) - }, + Short: "Read golangci-lint result and create a PR review", + Aliases: []string{"merge-request", "pr", "pull"}, + RunE: runGiteaPRNitting, } pr.Flags().Int64P( @@ -52,7 +62,97 @@ func Gitea() *cobra.Command { "PR index to add reviews to - note, this is not the ID of the PR but its number [$NITTER_PULL_INDEX]", ) - giteaCmd.AddCommand(pr) + pr.Flags().Var(&errorReviewStateType, "review-state", "state in which to create the review if there are issues [comment,approved,]") + pr.Flags().StringP("result-file", "f", "", "path to the golangci-lint JSON output [$NITTER_RESULT_FILE]") + + lintPr := &cobra.Command{ + Use: "lint-pull-request", + Short: "Run golangci-lint directly and create a PR review based on the captured results - required golangci-lint being in $PATH", + Aliases: []string{"lint-pr", "lint-pull"}, + RunE: runGiteaPRLintNitting, + } + + lintPr.Flags().Var(&errorReviewStateType, "review-state", "state in which to create the review if there are issues [comment,approved,]") + + lintPr.Flags().Int64P( + "pull-index", + "i", + -1, + "PR index to add reviews to - note, this is not the ID of the PR but its number [$NITTER_PULL_INDEX]", + ) + + lintPr.Flags().Bool( + "from-pr-base", + false, + "if enabled, nitter will get the PRs metadata and start golangci-lint with --new-from-rev=", + ) + + lintPr.Flags().StringSlice( + "golangci-lint-extra-args", + nil, + "Extra args to append to golangci-lint run commands, keep in mind that out-format will be set to JSON and --new-from-rev if you enable --from-pr-base", + ) + + giteaCmd.AddCommand(pr, lintPr) return giteaCmd } + +func runGiteaPRNitting(cmd *cobra.Command, args []string) error { + cfg, err := LoadConfig[gitea.GiteaPRConfig](cmd.Flags()) + if err != nil { + return err + } + + if err = cfg.Validate(); err != nil { + return err + } + + report, issues, err := ReadResultsFile(cmd.Flag("result-file").Value.String()) + if err != nil { + return err + } + + giteaClient, err := giteaSdk.NewClient(cfg.BaseAddress, giteaSdk.SetContext(cmd.Context()), giteaSdk.SetToken(cfg.Token)) + if err != nil { + return err + } + + nitter := gitea.NewPRNitter(giteaClient, cfg) + + return nitter.Report(report, issues) +} + +func runGiteaPRLintNitting(cmd *cobra.Command, args []string) error { + cfg, err := LoadConfig[gitea.GiteaPRLintConfig](cmd.Flags()) + if err != nil { + return err + } + + if err = cfg.Validate(); err != nil { + return err + } + + giteaClient, err := giteaSdk.NewClient(cfg.BaseAddress, giteaSdk.SetContext(cmd.Context()), giteaSdk.SetToken(cfg.Token)) + if err != nil { + return err + } + + if cfg.FromPRBase { + var pr *giteaSdk.PullRequest + if pr, _, err = giteaClient.GetPullRequest(cfg.Namespace, cfg.Repo, cfg.PRIndex); err != nil { + return err + } else { + cfg.ExtraArgs = append(cfg.ExtraArgs, fmt.Sprintf("--new-from-rev=%s", pr.Base.Sha)) + } + } + + report, issues, err := RunGolangCiLint(cmd.Context(), cfg.ExtraArgs) + if err != nil { + return err + } + + nitter := gitea.NewPRNitter(giteaClient, &cfg.GiteaPRConfig) + + return nitter.Report(report, issues) +} diff --git a/internal/commands/result.go b/internal/commands/result.go index ae6c8d5..86f8b4a 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -1,10 +1,14 @@ package commands import ( + "bytes" + "context" "encoding/json" "errors" + "fmt" "io" "os" + "os/exec" "github.com/golangci/golangci-lint/pkg/printers" "github.com/golangci/golangci-lint/pkg/report" @@ -39,3 +43,44 @@ func ReadResults(reader io.Reader) (*report.Data, []result.Issue, error) { return printerResult.Report, printerResult.Issues, nil } + +func RunGolangCiLint(ctx context.Context, extraFlags []string) (*report.Data, []result.Issue, error) { + const defaultArgsLen = 3 + golangCiLintArgs := make([]string, 1, defaultArgsLen+len(extraFlags)) + golangCiLintArgs[0] = "run" + golangCiLintArgs = append(golangCiLintArgs, extraFlags...) + golangCiLintArgs = append(golangCiLintArgs, "--issues-exit-code=0", "--out-format=json") + + golangCiLintArgs = append(golangCiLintArgs, extraFlags...) + + cmd := exec.CommandContext( + ctx, + "golangci-lint", + golangCiLintArgs..., + ) + + stderrBuf := bytes.NewBuffer(nil) + + cmd.Stderr = stderrBuf + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, nil, err + } + + if err := cmd.Start(); err != nil { + return nil, nil, err + } + + var printerResult printers.JSONResult + + if err := json.NewDecoder(stdout).Decode(&printerResult); err != nil { + return nil, nil, err + } + + if err := cmd.Wait(); err != nil { + return nil, nil, fmt.Errorf("stderr: %s - %w", stderrBuf.String(), err) + } + + return printerResult.Report, printerResult.Issues, nil +} diff --git a/main.go b/main.go index 47c9a83..90d802c 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,6 @@ var root = &cobra.Command{ func main() { root.PersistentFlags().StringP("namespace", "n", "", "Namespace a.k.a. organization/owner/group of the repository [$NITTER_NAMESPACE]") root.PersistentFlags().StringP("repo", "r", "", "Repo to interact with [$NITTER_REPO]") - root.PersistentFlags().StringP("result-file", "f", "", "path to the golangci-lint JSON output [$NITTER_RESULT_FILE]") ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) defer cancel() diff --git a/nitters/gitea/config.go b/nitters/gitea/config.go index f74e4d9..daa9e64 100644 --- a/nitters/gitea/config.go +++ b/nitters/gitea/config.go @@ -3,6 +3,8 @@ package gitea import ( "errors" + "code.gitea.io/sdk/gitea" + "code.icb4dc0.de/prskr/nitter/nitters" ) @@ -36,7 +38,8 @@ func (gc GiteaConfig) Validate() error { type GiteaPRConfig struct { GiteaConfig `mapstructure:",squash"` - PRIndex int64 `mapstructure:"pull-index"` + PRIndex int64 `mapstructure:"pull-index"` + ReviewState gitea.ReviewStateType `mapstructure:"review-state"` } func (gpc GiteaPRConfig) Validate() error { @@ -50,3 +53,13 @@ func (gpc GiteaPRConfig) Validate() error { return nil } + +type GiteaPRLintConfig struct { + GiteaPRConfig `mapstructure:",squash"` + FromPRBase bool `mapstructure:"from-pr-base"` + ExtraArgs []string `mapstructure:"golangci-lint-extra-args"` +} + +func (gpc GiteaPRLintConfig) Validate() error { + return gpc.GiteaPRConfig.Validate() +} diff --git a/nitters/gitea/pr_nitter.go b/nitters/gitea/pr_nitter.go index a78bca6..f56a879 100644 --- a/nitters/gitea/pr_nitter.go +++ b/nitters/gitea/pr_nitter.go @@ -47,7 +47,6 @@ type prNitter struct { func (p prNitter) Report(report *report.Data, issues []result.Issue) error { templateBuf := bytes.Buffer{} - summaryData := map[string]any{ "Report": report, "Issues": issues, @@ -58,28 +57,31 @@ func (p prNitter) Report(report *report.Data, issues []result.Issue) error { } pullReviewOptions := gitea.CreatePullReviewOptions{ - State: gitea.ReviewStateComment, + State: p.cfg.ReviewState, Body: templateBuf.String(), Comments: make([]gitea.CreatePullReviewComment, 0, len(issues)), } - templateBuf.Reset() - - for i := range issues { - templateData := map[string]any{ - "Issue": issues[i], - } - if err := templates.ExecuteTemplate(&templateBuf, "issue_comment.tmpl.md", templateData); err != nil { - return err - } - - pullReviewOptions.Comments = append(pullReviewOptions.Comments, gitea.CreatePullReviewComment{ - Path: issues[i].Pos.Filename, - Body: templateBuf.String(), - NewLineNum: int64(issues[i].Pos.Line), - }) - + if len(issues) == 0 { + pullReviewOptions.State = gitea.ReviewStateApproved + } else { templateBuf.Reset() + for i := range issues { + templateData := map[string]any{ + "Issue": issues[i], + } + if err := templates.ExecuteTemplate(&templateBuf, "issue_comment.tmpl.md", templateData); err != nil { + return err + } + + pullReviewOptions.Comments = append(pullReviewOptions.Comments, gitea.CreatePullReviewComment{ + Path: issues[i].Pos.Filename, + Body: templateBuf.String(), + NewLineNum: int64(issues[i].Pos.Line), + }) + + templateBuf.Reset() + } } _, _, err := p.CreatePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, pullReviewOptions)