config now only has reference to org or prjgits and the rest is defined in the "workflow.config" in the prjgit itself. This allows the config to be updated in the project.
587 lines
17 KiB
Go
587 lines
17 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 (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"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/client/user"
|
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
|
)
|
|
|
|
//go:generate mockgen -source=gitea_utils.go -destination=mock/gitea_utils.go -typed
|
|
|
|
// maintainer list file in ProjectGit
|
|
const (
|
|
MaintainershipFile = "_maintainership.json"
|
|
MaintainershipDir = "maintainership"
|
|
)
|
|
|
|
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 GiteaMaintainershipReader interface {
|
|
FetchMaintainershipFile(org, prjGit, branch string) ([]byte, string, error)
|
|
FetchMaintainershipDirFile(org, prjGit, branch, pkg string) ([]byte, string, error)
|
|
}
|
|
|
|
type GiteaPRFetcher interface {
|
|
GetPullRequest(org, project string, num int64) (*models.PullRequest, error)
|
|
GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, refOrg, refRepo string, Index int64) (*models.PullRequest, error)
|
|
}
|
|
|
|
type GiteaReviewFetcher interface {
|
|
GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error)
|
|
}
|
|
|
|
type GiteaPRChecker interface {
|
|
GiteaReviewFetcher
|
|
GiteaMaintainershipReader
|
|
}
|
|
|
|
type GiteaReviewFetcherAndRequester interface {
|
|
GiteaReviewFetcher
|
|
GiteaReviewRequester
|
|
}
|
|
|
|
type GiteaReviewRequester interface {
|
|
RequestReviews(pr *models.PullRequest, reviewer ...string) ([]*models.PullReview, error)
|
|
}
|
|
|
|
type GiteaReviewer interface {
|
|
AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error)
|
|
}
|
|
|
|
type GiteaRepoFetcher interface {
|
|
GetRepository(org, repo string) (*models.Repository, error)
|
|
}
|
|
|
|
type Gitea interface {
|
|
GiteaRepoFetcher
|
|
GiteaReviewRequester
|
|
GiteaReviewer
|
|
GiteaPRFetcher
|
|
GiteaReviewFetcher
|
|
GiteaMaintainershipReader
|
|
|
|
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 Git, org, repoName string) (*models.Repository, error)
|
|
CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error)
|
|
GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, refOrg, refRepo string, Index int64) (*models.PullRequest, error)
|
|
GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error)
|
|
GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, string, error)
|
|
GetRecentPullRequests(org, repo string) ([]*models.PullRequest, error)
|
|
GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)
|
|
|
|
GetCurrentUser() (*models.User, 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) FetchMaintainershipFile(org, repo, branch string) ([]byte, string, error) {
|
|
return gitea.GetRepositoryFileContent(org, repo, branch, MaintainershipFile)
|
|
}
|
|
|
|
func (gitea *GiteaTransport) FetchMaintainershipDirFile(org, repo, branch, pkg string) ([]byte, string, error) {
|
|
return gitea.GetRepositoryFileContent(org, repo, branch, path.Join(MaintainershipDir, pkg))
|
|
}
|
|
|
|
func (gitea *GiteaTransport) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) {
|
|
pr, err := gitea.client.Repository.RepoGetPullRequest(
|
|
repository.NewRepoGetPullRequestParams().
|
|
WithDefaults().
|
|
WithOwner(org).
|
|
WithRepo(project).
|
|
WithIndex(num),
|
|
gitea.transport.DefaultAuthentication,
|
|
)
|
|
|
|
return pr.Payload, err
|
|
}
|
|
|
|
func (gitea *GiteaTransport) GetRepository(org, pkg string) (*models.Repository, error) {
|
|
repo, err := gitea.client.Repository.RepoGet(repository.NewRepoGetParams().WithDefaults().WithOwner(org).WithRepo(pkg), gitea.transport.DefaultAuthentication)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return repo.Payload, nil
|
|
}
|
|
|
|
func (gitea *GiteaTransport) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
|
|
limit := int64(20)
|
|
var page int64
|
|
var allReviews []*models.PullReview
|
|
|
|
for {
|
|
reviews, err := gitea.client.Repository.RepoListPullReviews(
|
|
repository.NewRepoListPullReviewsParams().
|
|
WithDefaults().
|
|
WithOwner(org).
|
|
WithRepo(project).
|
|
WithIndex(PRnum).
|
|
WithPage(&page).
|
|
WithLimit(&limit),
|
|
gitea.transport.DefaultAuthentication,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allReviews = slices.Concat(allReviews, reviews.Payload)
|
|
if len(reviews.Payload) < int(limit) {
|
|
break
|
|
}
|
|
page++
|
|
}
|
|
|
|
return allReviews, 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 Git, org, repoName string) (*models.Repository, error) {
|
|
repo, err := gitea.client.Repository.RepoGet(
|
|
repository.NewRepoGetParams().WithDefaults().WithOwner(org).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),
|
|
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, err)
|
|
}
|
|
}
|
|
|
|
// initialize repository
|
|
if err = os.Mkdir(filepath.Join(git.GetPath(), 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.GetPath(), 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, 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) GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, refOrg, refRepo string, Index int64) (*models.PullRequest, error) {
|
|
var page int64
|
|
state := "open"
|
|
for {
|
|
page++
|
|
prs, err := gitea.client.Repository.RepoListPullRequests(
|
|
repository.
|
|
NewRepoListPullRequestsParams().
|
|
WithDefaults().
|
|
WithOwner(prjGitOrg).
|
|
WithRepo(prjGitRepo).
|
|
WithState(&state).
|
|
WithPage(&page),
|
|
gitea.transport.DefaultAuthentication)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", prjGitOrg, prjGitRepo, err)
|
|
}
|
|
|
|
prLine := fmt.Sprintf(PrPattern, refOrg, refRepo, Index)
|
|
|
|
// 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) RequestReviews(pr *models.PullRequest, reviewers ...string) ([]*models.PullReview, error) {
|
|
reviewOptions := models.PullReviewRequestOptions{
|
|
Reviewers: reviewers,
|
|
}
|
|
|
|
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 reviews: %w", err)
|
|
}
|
|
|
|
return review.GetPayload(), 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) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error) {
|
|
params := repository.NewRepoGetContentsParams().WithOwner(org).WithRepo(repo).WithFilepath(path)
|
|
if len(hash) > 0 {
|
|
params = params.WithRef(&hash)
|
|
}
|
|
content, err := gitea.client.Repository.RepoGetContents(params,
|
|
gitea.transport.DefaultAuthentication,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if content.Payload.Encoding != "base64" {
|
|
return nil, "", fmt.Errorf("Unhandled content encoding: %s", content.Payload.Encoding)
|
|
}
|
|
|
|
if content.Payload.Size > 10000000 {
|
|
return nil, "", fmt.Errorf("Content length is too large for %s/%s/%s#%s - %d bytes", org, repo, path, hash, content.Payload.Size)
|
|
}
|
|
|
|
data := make([]byte, content.Payload.Size)
|
|
n, err := base64.RawStdEncoding.Decode(data, []byte(content.Payload.Content))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("Error decoding file %s/%s/%s#%s : %w", org, repo, path, hash, err)
|
|
}
|
|
if n != int(content.Payload.Size) {
|
|
return nil, "", fmt.Errorf("Decoded length doesn't match expected for %s/%s/%s#%s - %d vs %d bytes", org, repo, path, hash, content.Payload.Size, n)
|
|
}
|
|
|
|
return data, content.Payload.SHA, nil
|
|
}
|
|
|
|
func (gitea *GiteaTransport) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, string, error) {
|
|
return gitea.GetRepositoryFileContent(pr.Head.Repo.Owner.UserName, pr.Head.Repo.Name, 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 = 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
|
|
}
|
|
|
|
func (gitea *GiteaTransport) GetCurrentUser() (*models.User, error) {
|
|
user, err := gitea.client.User.UserGetCurrent(
|
|
user.NewUserGetCurrentParams(),
|
|
gitea.transport.DefaultAuthentication,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return user.GetPayload(), nil
|
|
}
|