package common /* * This file is part of Autogits. * * Copyright © 2024 SUSE LLC * * Autogits is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 2 of the License, or (at your option) any later * version. * * Autogits is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Foobar. If not, see . */ import ( "fmt" "io" "os" "path/filepath" "slices" "strings" "time" transport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" apiclient "src.opensuse.org/autogits/common/gitea-generated/client" "src.opensuse.org/autogits/common/gitea-generated/client/notification" "src.opensuse.org/autogits/common/gitea-generated/client/organization" "src.opensuse.org/autogits/common/gitea-generated/client/repository" "src.opensuse.org/autogits/common/gitea-generated/models" ) //go:generate mockgen -source=gitea_utils.go -destination=mock/gitea_utils.go -typed const PrPattern = "PR: %s/%s#%d" const ( // from Gitea // ReviewStateApproved pr is approved ReviewStateApproved models.ReviewStateType = "APPROVED" // ReviewStatePending pr state is pending ReviewStatePending models.ReviewStateType = "PENDING" // ReviewStateComment is a comment review ReviewStateComment models.ReviewStateType = "COMMENT" // ReviewStateRequestChanges changes for pr are requested ReviewStateRequestChanges models.ReviewStateType = "REQUEST_CHANGES" // ReviewStateRequestReview review is requested from user ReviewStateRequestReview models.ReviewStateType = "REQUEST_REVIEW" // ReviewStateUnknown state of pr is unknown ReviewStateUnknown models.ReviewStateType = "" ) type Gitea interface { GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) SetNotificationRead(notificationId int64) error GetOrganization(orgName string) (*models.Organization, error) GetOrganizationRepositories(orgName string) ([]*models.Repository, error) CreateRepositoryIfNotExist(git *GitHandler, org Organization, repoName string) (*models.Repository, error) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) RequestReviews(pr *models.PullRequest, reviewer string) ([]*models.PullReview, error) IsReviewed(pr *models.PullRequest) (bool, error) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) GetAssociatedPrjGitPR(pr *PullRequestWebhookEvent) (*models.PullRequest, error) GetRepositoryFileContent(repo *models.Repository, hash, path string) ([]byte, error) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) GetRecentPullRequests(org, repo string) ([]*models.PullRequest, error) GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error) } type GiteaTransport struct { transport *transport.Runtime client *apiclient.GiteaAPI } func AllocateGiteaTransport(host string) Gitea { var r GiteaTransport r.transport = transport.New(host, apiclient.DefaultBasePath, [](string){"https"}) r.transport.DefaultAuthentication = transport.BearerToken(giteaToken) r.client = apiclient.New(r.transport, nil) return &r } func (gitea *GiteaTransport) GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error) { pr, err := gitea.client.Repository.RepoGetPullRequest( repository.NewRepoGetPullRequestParams(). WithDefaults(). WithOwner(org). WithRepo(project). WithIndex(num), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, nil, err } limit := int64(1000) reviews, err := gitea.client.Repository.RepoListPullReviews( repository.NewRepoListPullReviewsParams(). WithDefaults(). WithOwner(org). WithRepo(project). WithIndex(num). WithLimit(&limit), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, nil, err } return pr.Payload, reviews.Payload, nil } func (gitea *GiteaTransport) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) { bigLimit := int64(100000) params := notification.NewNotifyGetListParams(). WithDefaults(). WithSubjectType([]string{"Pull"}). WithStatusTypes([]string{"unread"}). WithLimit(&bigLimit) if since != nil { s := strfmt.DateTime(*since) params.SetSince(&s) } list, err := gitea.client.Notification.NotifyGetList(params, gitea.transport.DefaultAuthentication) if err != nil { return nil, err } return list.Payload, nil } func (gitea *GiteaTransport) SetNotificationRead(notificationId int64) error { _, err := gitea.client.Notification.NotifyReadThread( notification.NewNotifyReadThreadParams(). WithDefaults(). WithID(fmt.Sprint(notificationId)), gitea.transport.DefaultAuthentication, ) if err != nil { return fmt.Errorf("Error setting notification: %d. Err: %w", notificationId, err) } return nil } func (gitea *GiteaTransport) GetOrganization(orgName string) (*models.Organization, error) { org, err := gitea.client.Organization.OrgGet( organization.NewOrgGetParams().WithOrg(orgName), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, fmt.Errorf("Error fetching org: '%s' data. Err: %w", orgName, err) } return org.Payload, nil } func (gitea *GiteaTransport) GetOrganizationRepositories(orgName string) ([]*models.Repository, error) { var page int64 repos := make([]*models.Repository, 0, 100) page = 1 for { ret, err := gitea.client.Organization.OrgListRepos( organization.NewOrgListReposParams().WithOrg(orgName).WithPage(&page), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, fmt.Errorf("Error retrieving repository list for org: '%s'. Err: %w", orgName, err) } if len(ret.Payload) == 0 { break } repos = append(repos, ret.Payload...) page++ } return repos, nil } func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git *GitHandler, org Organization, repoName string) (*models.Repository, error) { repo, err := gitea.client.Repository.RepoGet( repository.NewRepoGetParams().WithDefaults().WithOwner(org.Username).WithRepo(repoName), gitea.transport.DefaultAuthentication) if err != nil { switch err.(type) { case *repository.RepoGetNotFound: repo, err := gitea.client.Organization.CreateOrgRepo( organization.NewCreateOrgRepoParams().WithDefaults().WithBody( &models.CreateRepoOption{ AutoInit: false, Name: &repoName, ObjectFormatName: models.CreateRepoOptionObjectFormatNameSha256, }, ).WithOrg(org.Username), nil, ) if err != nil { switch err.(type) { case *organization.CreateOrgRepoCreated: // weird, but ok, repo created default: return nil, fmt.Errorf("error creating repo '%s' under '%s'. Err: %w", repoName, org.Username, err) } } // initialize repository if err = os.Mkdir(filepath.Join(git.GitPath, DefaultGitPrj), 0700); err != nil { return nil, err } if err = git.GitExec(DefaultGitPrj, "init", "--object-format="+repo.Payload.ObjectFormatName); err != nil { return nil, err } if err = git.GitExec(DefaultGitPrj, "checkout", "-b", repo.Payload.DefaultBranch); err != nil { return nil, err } readmeFilename := filepath.Join(git.GitPath, DefaultGitPrj, "README.md") { file, _ := os.Create(readmeFilename) defer file.Close() io.WriteString(file, ReadmeBoilerplate) } if err = git.GitExec(DefaultGitPrj, "add", "README.md"); err != nil { return nil, err } if err = git.GitExec(DefaultGitPrj, "commit", "-m", "Automatic devel project creation"); err != nil { return nil, err } if err = git.GitExec(DefaultGitPrj, "remote", "add", "origin", repo.Payload.SSHURL); err != nil { return nil, err } return repo.Payload, nil default: return nil, fmt.Errorf("cannot fetch repo data for '%s' / '%s' : %w", org.Username, repoName, err) } } return repo.Payload, nil } func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) { prOptions := models.CreatePullRequestOption{ Base: repo.DefaultBranch, Head: srcId, Title: title, Body: body, } if pr, err := gitea.client.Repository.RepoGetPullRequestByBaseHead( repository.NewRepoGetPullRequestByBaseHeadParams().WithOwner(repo.Owner.UserName).WithRepo(repo.Name).WithBase(repo.DefaultBranch).WithHead(srcId), gitea.transport.DefaultAuthentication, ); err == nil { return pr.Payload, nil } pr, err := gitea.client.Repository.RepoCreatePullRequest( repository. NewRepoCreatePullRequestParams(). WithDefaults(). WithOwner(repo.Owner.UserName). WithRepo(repo.Name). WithBody(&prOptions), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, fmt.Errorf("Cannot create pull request. %w", err) } return pr.GetPayload(), nil } func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewer string) ([]*models.PullReview, error) { reviewOptions := models.PullReviewRequestOptions{ Reviewers: []string{reviewer}, } review, err := gitea.client.Repository.RepoCreatePullReviewRequests( repository. NewRepoCreatePullReviewRequestsParams(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithBody(&reviewOptions), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, fmt.Errorf("Cannot create pull request: %w", err) } return review.GetPayload(), nil } func (gitea *GiteaTransport) IsReviewed(pr *models.PullRequest) (bool, error) { // TODO: get review from project git reviewers := pr.RequestedReviewers var page int64 reviews := make([]*models.PullReview, 0, 10) for { page++ res, err := gitea.client.Repository.RepoListPullReviews( repository.NewRepoListPullReviewsParams(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithPage(&page), gitea.transport.DefaultAuthentication) if err != nil { return false, err } if res.IsSuccess() { reviews = append(reviews, res.Payload...) if len(res.Payload) < 10 { break } } } slices.Reverse(reviews) for _, review := range reviews { if review.Stale || review.Dismissed { continue } next_review: for i, reviewer := range reviewers { if review.User.UserName == reviewer.UserName { switch review.State { case ReviewStateApproved: reviewers = slices.Delete(reviewers, i, i) break next_review case ReviewStateRequestChanges: return false, nil } } } } return len(reviewers) == 0, nil } func (gitea *GiteaTransport) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) { c, err := gitea.client.Repository.RepoCreatePullReview( repository.NewRepoCreatePullReviewParams(). WithDefaults(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithBody(&models.CreatePullReviewOptions{ Event: state, Body: comment, }), gitea.transport.DefaultAuthentication, ) /* c, err := client.Repository.RepoSubmitPullReview( repository.NewRepoSubmitPullReviewParams(). WithDefaults(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithID(review.ID). WithBody(&models.SubmitPullReviewOptions{ Event: state, Body: comment, }), transport.DefaultAuthentication, ) */ /* c, err := client.Issue.IssueCreateComment( issue.NewIssueCreateCommentParams(). WithDefaults(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithBody(&models.CreateIssueCommentOption{ Body: &comment, }), transport.DefaultAuthentication) */ if err != nil { return nil, err } return c.Payload, nil } func (gitea *GiteaTransport) GetAssociatedPrjGitPR(pr *PullRequestWebhookEvent) (*models.PullRequest, error) { var page int64 state := "open" for { page++ prs, err := gitea.client.Repository.RepoListPullRequests( repository. NewRepoListPullRequestsParams(). WithDefaults(). WithOwner(pr.Repository.Owner.Username). WithRepo(DefaultGitPrj). WithState(&state). WithPage(&page), gitea.transport.DefaultAuthentication) if err != nil { return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", pr.Repository.Owner.Username, pr.Repository.Name, err) } prLine := fmt.Sprintf(PrPattern, pr.Repository.Owner.Username, pr.Repository.Name, pr.Number) // h.StdLogger.Printf("attemping to match line: '%s'\n", prLine) // payload_processing: for _, pr := range prs.Payload { lines := strings.Split(pr.Body, "\n") for _, line := range lines { if strings.TrimSpace(line) == prLine { return pr, nil } } } if len(prs.Payload) < 10 { break } } return nil, nil } func (gitea *GiteaTransport) GetRepositoryFileContent(repo *models.Repository, hash, path string) ([]byte, error) { var retData []byte dataOut := writeFunc(func(data []byte) (int, error) { if len(data) == 0 { return 0, nil } retData = data return len(data), nil }) _, err := gitea.client.Repository.RepoGetRawFile( repository.NewRepoGetRawFileParams(). WithOwner(repo.Owner.UserName). WithRepo(repo.Name). WithFilepath(path). WithRef(&hash), gitea.transport.DefaultAuthentication, dataOut, repository.WithContentTypeApplicationOctetStream, ) if err != nil { return nil, err } return retData, nil } func (gitea *GiteaTransport) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) { return gitea.GetRepositoryFileContent(pr.Head.Repo, pr.Head.Sha, path) } func (gitea *GiteaTransport) GetRecentPullRequests(org, repo string) ([]*models.PullRequest, error) { prs := make([]*models.PullRequest, 0, 10) var page int64 page = 1 sort := "recentupdate" for { res, err := gitea.client.Repository.RepoListPullRequests( repository.NewRepoListPullRequestsParams(). WithOwner(org). WithRepo(repo). WithPage(&page). WithSort(&sort), gitea.transport.DefaultAuthentication) if err != nil { return nil, err } prs = append(prs, res.Payload...) n := len(res.Payload) if n < 10 { break } // if pr is closed for more than a week, assume that we are done too if time.Since(time.Time(res.Payload[n-1].Updated)) > 7*24*time.Hour { break } page++ } return prs, nil } func (gitea *GiteaTransport) GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error) { not := false var page int64 page = 1 commits, err := gitea.client.Repository.RepoGetAllCommits( repository.NewRepoGetAllCommitsParams(). WithOwner(org). WithRepo(repo). WithSha(&branch). WithPage(&page). WithStat(¬). WithFiles(¬). WithVerification(¬). WithLimit(&commitNo), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, err } return commits.Payload, nil }