package gitea import ( "bytes" "embed" "strings" "text/template" "code.gitea.io/sdk/gitea" "github.com/golangci/golangci-lint/pkg/report" "github.com/golangci/golangci-lint/pkg/result" "golang.org/x/exp/slog" "code.icb4dc0.de/prskr/nitter/nitters" ) const summaryFooter = "\n\n(Created by nitter)" var ( _ nitters.Nitter = (*prNitter)(nil) //go:embed templates/* templatesFS embed.FS templates *template.Template ) func init() { if tmpl, err := template.New("issue_templates").ParseFS(templatesFS, "templates/*.tmpl.md"); err != nil { panic(err) } else { templates = tmpl } } func NewPRNitter(logger *slog.Logger, cli Client, cfg *GiteaPRConfig) *prNitter { if logger == nil { logger = slog.Default() } return &prNitter{ Client: cli, logger: logger, cfg: cfg, } } type prNitter struct { Client logger *slog.Logger cfg *GiteaPRConfig } func (p prNitter) Report(report *report.Data, issues []result.Issue) error { me, _, err := p.GetMyUserInfo() if err != nil { return err } slog.Debug("Running as", slog.String("name", me.UserName)) pr, _, err := p.GetPullRequest(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex) if err != nil { slog.Error("Failed to get PR details", err, slog.Int64("index", p.cfg.PRIndex)) return err } review, err := p.preparePullReview(report, issues, me, pr) if err != nil { return err } if err := p.cleanOutdatedReviews(me); err != nil { return err } if _, _, err := p.CreatePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, *review); err != nil { slog.Error("Failed to submit new PR review", err) return err } return nil } func (p prNitter) preparePullReview( report *report.Data, issues []result.Issue, me *gitea.User, pr *gitea.PullRequest, ) (*gitea.CreatePullReviewOptions, error) { templateBuf := bytes.Buffer{} summaryData := map[string]any{ "Report": report, "Issues": issues, } if err := templates.ExecuteTemplate(&templateBuf, "issue_summary.tmpl.md", summaryData); err != nil { return nil, err } _, _ = templateBuf.WriteString(summaryFooter) pullReviewOptions := &gitea.CreatePullReviewOptions{ State: p.cfg.ReviewState, Body: templateBuf.String(), Comments: make([]gitea.CreatePullReviewComment, 0, len(issues)), } if len(issues) == 0 { if me.ID != pr.Poster.ID { pullReviewOptions.State = gitea.ReviewStateApproved } else { slog.Warn( "Cannot approve PR if token is issued by the same user - fallback to comment state", slog.String("nitter_user", me.UserName), slog.String("pr_poster", pr.Poster.UserName), ) pullReviewOptions.State = gitea.ReviewStateComment } } 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 nil, err } pullReviewOptions.Comments = append(pullReviewOptions.Comments, gitea.CreatePullReviewComment{ Path: issues[i].Pos.Filename, Body: templateBuf.String(), NewLineNum: int64(issues[i].Pos.Line), }) templateBuf.Reset() } } return pullReviewOptions, nil } func (p prNitter) cleanOutdatedReviews(me *gitea.User) error { reviews, _, err := p.ListPullReviews(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, gitea.ListPullReviewsOptions{}) if err != nil { return err } for i := range reviews { review := reviews[i] if review.Reviewer.ID == me.ID && strings.HasSuffix(review.Body, summaryFooter) { if _, err = p.DeletePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, review.ID); err != nil { slog.Error("Failed to delete existing review", err, slog.Int64("reviewId", review.ID)) return err } } } return nil }