543 lines
15 KiB
Go
543 lines
15 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 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
|
|
}
|