autogits/bots-common/gitea_utils.go
2024-08-23 15:19:37 +02:00

497 lines
12 KiB
Go

package common
import (
"errors"
"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"
func (h *RequestHandler) allocateGiteaTransport() (*transport.Runtime, *apiclient.GiteaAPI) {
r := transport.New("src.opensuse.org", apiclient.DefaultBasePath, [](string){"https"})
r.DefaultAuthentication = transport.BearerToken(giteaToken)
// r.SetDebug(true)
return r, apiclient.New(r, nil)
}
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 = ""
)
func (h *RequestHandler) GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error) {
if h.HasError() {
return nil, nil, h.Error
}
transport, client := h.allocateGiteaTransport()
pr, err := client.Repository.RepoGetPullRequest(
repository.NewRepoGetPullRequestParams().
WithDefaults().
WithOwner(org).
WithRepo(project).
WithIndex(num),
transport.DefaultAuthentication,
)
if err != nil {
h.Error = err
h.LogPlainError(err)
return nil, nil, err
}
limit := int64(1000)
reviews, err := client.Repository.RepoListPullReviews(
repository.NewRepoListPullReviewsParams().
WithDefaults().
WithOwner(org).
WithRepo(project).
WithIndex(num).
WithLimit(&limit),
transport.DefaultAuthentication,
)
if err != nil {
h.Error = err
h.LogPlainError(err)
return nil, nil, err
}
return pr.Payload, reviews.Payload, nil
}
func (h *RequestHandler) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) {
if h.HasError() {
return nil, h.Error
}
bigLimit := int64(100000)
transport, client := h.allocateGiteaTransport()
params := notification.NewNotifyGetListParams().
WithDefaults().
WithSubjectType([]string{"Pull"}).
WithStatusTypes([]string{"unread"}).
WithLimit(&bigLimit)
if since != nil {
s := strfmt.DateTime(*since)
params.SetSince(&s)
}
list, err := client.Notification.NotifyGetList(params, transport.DefaultAuthentication)
if err != nil {
h.Error = err
return nil, err
}
if !list.IsSuccess() {
h.Error = fmt.Errorf("Cannot fetch notifications: %s", list.Error())
return nil, h.Error
}
return list.Payload, nil
}
func (h *RequestHandler) SetNotificationRead(notificationId int64) error {
if h.HasError() {
return h.Error
}
transport, client := h.allocateGiteaTransport()
list, err := client.Notification.NotifyReadThread(
notification.NewNotifyReadThreadParams().
WithDefaults().
WithID(fmt.Sprint(notificationId)),
transport.DefaultAuthentication,
)
if err != nil {
h.LogError("Error setting notification: %d: %v", notificationId, err)
h.Error = err
return err
}
if !list.IsSuccess() {
h.Error = fmt.Errorf("Cannot update notifications: %d", notificationId)
return h.Error
}
return nil
}
func (h *RequestHandler) CreateRepositoryIfNotExist(org Organization, repoName string) *models.Repository {
if h.HasError() {
return nil
}
transport, client := h.allocateGiteaTransport()
repo, err := client.Repository.RepoGet(
repository.NewRepoGetParams().WithDefaults().WithOwner(org.Username).WithRepo(repoName),
transport.DefaultAuthentication)
if err != nil {
switch err.(type) {
case *repository.RepoGetNotFound:
h.Log("repo '%s' does not exist. Trying to create it ....", repoName)
repo, err := 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:
h.Log("repo '%s' created, with notification error?", repoName)
default:
h.Error = err
h.LogError("error creating repo '%s' under '%s': %s", repoName, org.Username, err.Error())
return nil
}
} else {
h.Log("repo '%s' created", repoName)
}
// initialize repository
h.Error = os.Mkdir(filepath.Join(h.GitPath, DefaultGitPrj), 0700)
if h.HasError() {
return nil
}
h.GitExec(DefaultGitPrj, "init", "--object-format="+repo.Payload.ObjectFormatName)
h.GitExec(DefaultGitPrj, "checkout", "-b", repo.Payload.DefaultBranch)
if h.HasError() {
return nil
}
readmeFilename := filepath.Join(h.GitPath, DefaultGitPrj, "README.md")
{
file, _ := os.Create(readmeFilename)
defer file.Close()
io.WriteString(file, ReadmeBoilerplate)
}
h.GitExec(DefaultGitPrj, "add", "README.md")
h.GitExec(DefaultGitPrj, "commit", "-m", "Automatic devel project creation")
h.GitExec(DefaultGitPrj, "remote", "add", "origin", repo.Payload.SSHURL)
return repo.Payload
default:
h.Error = err
h.LogError("cannot fetch repo data for '%s' / '%s' : %w", org.Username, repoName, err)
}
}
return repo.Payload
}
func (h *RequestHandler) CreatePullRequest(repo *models.Repository, srcId, targetId, title, body string) *models.PullRequest {
if h.HasError() {
return nil
}
transport, client := h.allocateGiteaTransport()
prOptions := models.CreatePullRequestOption{
Base: repo.DefaultBranch,
Head: srcId,
Title: title,
Body: body,
}
pr, err := client.Repository.RepoCreatePullRequest(
repository.
NewRepoCreatePullRequestParams().
WithDefaults().
WithOwner(repo.Owner.UserName).
WithRepo(repo.Name).
WithBody(&prOptions),
transport.DefaultAuthentication,
)
if err != nil {
h.LogError("Cannot create pull request: %v", err)
h.Error = err
return nil
}
if !pr.IsSuccess() {
h.LogError("PR creation failed: %s", pr.Error())
h.Error = errors.New(pr.Error())
return nil
}
return pr.GetPayload()
}
func (h *RequestHandler) RequestReviews(pr *models.PullRequest, reviewer string) []*models.PullReview {
if h.HasError() {
return nil
}
transport, client := h.allocateGiteaTransport()
reviewOptions := models.PullReviewRequestOptions{
Reviewers: []string{reviewer},
}
review, err := client.Repository.RepoCreatePullReviewRequests(
repository.
NewRepoCreatePullReviewRequestsParams().
WithOwner(pr.Base.Repo.Owner.UserName).
WithRepo(pr.Base.Repo.Name).
WithIndex(pr.Index).
WithBody(&reviewOptions),
transport.DefaultAuthentication,
)
if err != nil {
h.LogError("Cannot create pull request: %v", err)
h.Error = err
return nil
}
if !review.IsSuccess() {
h.LogError("PR creation failed: %s", review.Error())
h.Error = errors.New(review.Error())
return nil
}
return review.GetPayload()
}
func (h *RequestHandler) IsReviewed(pr *models.PullRequest) (bool, error) {
if h.HasError() {
return false, h.Error
}
transport, client := h.allocateGiteaTransport()
// TODO: get review from project git
reviewers := pr.RequestedReviewers
var page, limit int64
var reviews []*models.PullReview
page = 0
limit = 20
for {
res, err := client.Repository.RepoListPullReviews(
repository.NewRepoListPullReviewsParams().
WithOwner(pr.Base.Repo.Owner.UserName).
WithRepo(pr.Base.Repo.Name).
WithPage(&page).
WithLimit(&limit),
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 (h *RequestHandler) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) {
transport, client := h.allocateGiteaTransport()
h.Log("%#v", *pr)
c, err := 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,
}),
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 (h *RequestHandler) GetAssociatedPrjGitPR(pr *PullRequestAction) *models.PullRequest {
if h.HasError() {
return nil
}
transport, client := h.allocateGiteaTransport()
var page, maxSize int64
page = 1
maxSize = 10000
state := "open"
prs, err := client.Repository.RepoListPullRequests(
repository.
NewRepoListPullRequestsParams().
WithDefaults().
WithOwner(pr.Repository.Owner.Username).
WithRepo(DefaultGitPrj).
WithState(&state).
WithLimit(&maxSize).
WithPage(&page),
transport.DefaultAuthentication)
if err != nil {
h.Error = fmt.Errorf("cannot fetch PR list for %s / %s : %v", pr.Repository.Owner.Username, pr.Repository.Name, err)
return nil
}
if !prs.IsSuccess() {
h.Error = fmt.Errorf("cannot fetch PR list for %s / %s : %s", pr.Repository.Owner.Username, pr.Repository.Name, prs.Error())
}
prLine := fmt.Sprintf(PrPattern, pr.Repository.Owner.Username, pr.Repository.Name, pr.Number)
h.Log("attemping to match line: '%s'", 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
}
}
}
return nil
}
func (h *RequestHandler) GetRepositoryFileContent(repo *models.Repository, hash, path string) ([]byte, error) {
if h.HasError() {
return nil, h.Error
}
transport, client := h.allocateGiteaTransport()
var retData []byte
dataOut := writeFunc(func(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
retData = data
return len(data), nil
})
file, err := client.Repository.RepoGetRawFile(
repository.NewRepoGetRawFileParams().
WithOwner(repo.Owner.UserName).
WithRepo(repo.Name).
WithFilepath(path).
WithRef(&hash),
transport.DefaultAuthentication,
dataOut,
repository.WithContentTypeApplicationOctetStream,
)
if err != nil {
return nil, err
}
if !file.IsSuccess() {
return nil, fmt.Errorf("Invalid response from server (%d): %s", file.Code(), file.Error())
}
return retData, nil
}
func (h *RequestHandler) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) {
return h.GetRepositoryFileContent(pr.Head.Repo, pr.Head.Sha, path)
}