autogits/bots-common/gitea_utils.go
Adam Majer b7ec9a9ffb Handle case when branch not exist
Handle default branch name in push and branch create handlers
Don't panic in this case in case the project has multiple configs
2024-09-12 16:25:22 +02:00

489 lines
13 KiB
Go

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 <https://www.gnu.org/licenses/>.
*/
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"
)
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 GiteaTransport struct {
transport *transport.Runtime
client *apiclient.GiteaAPI
}
func AllocateGiteaTransport(host string) *GiteaTransport {
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) CreatePullRequest(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
prOptions := models.CreatePullRequestOption{
Base: repo.DefaultBranch,
Head: srcId,
Title: title,
Body: body,
}
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, limit int64
var reviews []*models.PullReview
page = 0
limit = 20
for {
res, err := gitea.client.Repository.RepoListPullReviews(
repository.NewRepoListPullReviewsParams().
WithOwner(pr.Base.Repo.Owner.UserName).
WithRepo(pr.Base.Repo.Name).
WithPage(&page).
WithLimit(&limit),
gitea.transport.DefaultAuthentication)
if err != nil {
return false, err
}
if res.IsSuccess() {
r := res.Payload
if reviews == nil {
reviews = r
} else {
reviews = append(reviews, r...)
}
if len(r) < int(limit) {
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, maxSize int64
page = 1
maxSize = 10000
state := "open"
prs, err := gitea.client.Repository.RepoListPullRequests(
repository.
NewRepoListPullRequestsParams().
WithDefaults().
WithOwner(pr.Repository.Owner.Username).
WithRepo(DefaultGitPrj).
WithState(&state).
WithLimit(&maxSize).
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
}
}
}
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) 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(&not).
WithFiles(&not).
WithVerification(&not).
WithLimit(&commitNo),
gitea.transport.DefaultAuthentication,
)
if err != nil {
return nil, err
}
return commits.Payload, nil
}