workflow-pr: wip

This commit is contained in:
Adam Majer 2024-12-05 18:38:35 +01:00
parent a025328fef
commit 77751ecc46
5 changed files with 312 additions and 23 deletions

View File

@ -0,0 +1,92 @@
package common
import (
"bufio"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
)
const PrPattern = "PR: %s/%s#%d"
type BasicPR struct {
org, repo string
num uint64
}
var validOrgAndRepoRx *regexp.Regexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
func parsePrLine(line string) (BasicPR, error) {
var ret BasicPR
trimmedLine := strings.TrimSpace(line)
// min size > 9 -> must fit all parameters in th PrPattern with at least one item per parameter
if len(trimmedLine) < 9 || trimmedLine[0:4] != "PR: " {
return ret, errors.New("Line too short")
}
trimmedLine = trimmedLine[4:]
org := strings.SplitN(trimmedLine, "/", 2)
ret.org = org[0]
if len(org) != 2 {
return ret, errors.New("missing / separator")
}
repo := strings.SplitN(org[1], "#", 2)
ret.repo = repo[0]
if len(repo) != 2 {
return ret, errors.New("Missing # separator")
}
// Gitea requires that each org and repo be [A-Za-z0-9_-]+
var err error
if ret.num, err = strconv.ParseUint(repo[1], 10, 64); err != nil {
return ret, errors.New("Invalid number")
}
if !validOrgAndRepoRx.MatchString(repo[0]) || !validOrgAndRepoRx.MatchString(org[0]) {
return ret, errors.New("Invalid repo or org character set")
}
return ret, nil
}
func ExtractAssociatedDescriptionAndPRs(data *bufio.Scanner) (string, []BasicPR) {
prs := make([]BasicPR, 0, 1)
var desc strings.Builder
for data.Scan() {
line := data.Text()
pr, err := parsePrLine(line)
if err != nil {
desc.WriteString(line)
desc.WriteByte('\n')
} else {
prs = append(prs, pr)
}
}
return strings.TrimSpace(desc.String()), prs
}
func prToLine(writer io.Writer, pr BasicPR) {
fmt.Fprintf(writer, PrPattern, pr.org, pr.repo, pr.num)
writer.Write([]byte("\n"))
}
func AppendPRsToDescription(desc string, prs []BasicPR) string {
var out strings.Builder
out.WriteString(strings.TrimSpace(desc))
out.WriteString("\n\n")
for _, pr := range prs {
prToLine(&out, pr)
}
return out.String()
}

View File

@ -0,0 +1,192 @@
package common
import (
"bufio"
"slices"
"strings"
"testing"
)
func newStringScanner(s string) *bufio.Scanner {
return bufio.NewScanner(strings.NewReader(s))
}
func TestAssociatedPRScanner(t *testing.T) {
t.Run("No PRs", func(t *testing.T) {
if _, out := ExtractAssociatedDescriptionAndPRs(newStringScanner("")); len(out) != 0 {
t.Error("Unexpected output", out)
}
})
t.Run("Single PR", func(t *testing.T) {
const singlePRText = `Some header of the issue
Followed by some description
PR: test/foo#4
`
_, out := ExtractAssociatedDescriptionAndPRs(newStringScanner(singlePRText))
if len(out) != 1 {
t.Error("Unexpected output", out)
return
}
expected := BasicPR{
org: "test",
repo: "foo",
num: 4,
}
if out[0] != expected {
t.Error("Unexpected", out, "Expected", expected)
}
})
t.Run("Multiple PRs", func(t *testing.T) {
const multiplePRText = `Some header of the issue
Followed by some description
PR: test/foo#4
PR: test/goo#5
`
_, out := ExtractAssociatedDescriptionAndPRs(newStringScanner(multiplePRText))
if len(out) != 2 {
t.Error("Unexpected output", out)
return
}
expected1 := BasicPR{
org: "test",
repo: "foo",
num: 4,
}
expected2 := BasicPR{
org: "test",
repo: "goo",
num: 5,
}
if !slices.Contains(out, expected1) {
t.Error("Unexpected", out, "Expected", expected1)
}
if !slices.Contains(out, expected2) {
t.Error("Unexpected", out, "Expected", expected2)
}
})
t.Run("Multiple PRs with whitespace", func(t *testing.T) {
const whitespacePRText = `Some header of the issue
PR: test/goo#5
Followed by some description
PR: test/foo#4
`
desc, out := ExtractAssociatedDescriptionAndPRs(newStringScanner(whitespacePRText))
if len(out) != 2 {
t.Error("Unexpected output", out)
return
}
const expectedDesc = `Some header of the issue
Followed by some description`
expected1 := BasicPR{
org: "test",
repo: "foo",
num: 4,
}
expected2 := BasicPR{
org: "test",
repo: "goo",
num: 5,
}
if !slices.Contains(out, expected1) {
t.Error("Unexpected", out, "Expected", expected1)
}
if !slices.Contains(out, expected2) {
t.Error("Unexpected", out, "Expected", expected2)
}
if desc != expectedDesc {
t.Error("unexpected desc", desc)
}
})
t.Run("Multiple PRs with missing names and other special cases to ignore", func(t *testing.T) {
const whitespacePRText = `Some header of the issue
PR: foobar#5
PR: rd/goo5
PR: test/#5
PR: /goo#5
PR: test/goo#
PR: test / goo # 10
PR: test/gool# 10
PR: test/goo#5
Followed by some description
PR: test/foo#4
`
desc, out := ExtractAssociatedDescriptionAndPRs(newStringScanner(whitespacePRText))
if len(out) != 2 {
t.Error("Unexpected output", out)
return
}
const expectedDesc = `Some header of the issue
PR: foobar#5
PR: rd/goo5
PR: test/#5
PR: /goo#5
PR: test/goo#
PR: test / goo # 10
PR: test/gool# 10
Followed by some description`
if desc != expectedDesc {
t.Error(len(desc), "vs", len(expectedDesc))
t.Error("description doesn't match expected. ", desc)
}
expected1 := BasicPR{
org: "test",
repo: "foo",
num: 4,
}
expected2 := BasicPR{
org: "test",
repo: "goo",
num: 5,
}
if !slices.Contains(out, expected1) {
t.Error("Unexpected", out, "Expected", expected1)
}
if !slices.Contains(out, expected2) {
t.Error("Unexpected", out, "Expected", expected2)
}
})
t.Run("Append PRs to end of description", func(t *testing.T) {
d := AppendPRsToDescription("something", []BasicPR{
BasicPR{org: "a", repo: "b", num: 100},
})
const expectedDesc = `something
PR: a/b#100
`
if d != expectedDesc {
t.Error(len(d), "vs", len(expectedDesc))
t.Error("unpected output", d)
}
})
}

View File

@ -37,8 +37,6 @@ import (
//go:generate mockgen -source=gitea_utils.go -destination=mock/gitea_utils.go -typed
const PrPattern = "PR: %s/%s#%d"
// maintainer list file in ProjectGit
const MaintainershipFile = "_maitnainership.json"
@ -58,6 +56,10 @@ const (
ReviewStateUnknown models.ReviewStateType = ""
)
type GiteaPRFetcher interface {
GetAssociatedPRs(org, repo string, prNo int64) ([]*models.PullRequest, error)
}
type Gitea interface {
GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error)
GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error)
@ -73,6 +75,8 @@ type Gitea interface {
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)
GiteaPRFetcher
}
type GiteaTransport struct {
@ -407,6 +411,25 @@ func (gitea *GiteaTransport) GetAssociatedPrjGitPR(pr *PullRequestWebhookEvent)
return nil, nil
}
func (gitea *GiteaTransport) GetAssociatedPRs(org, repo string, prNo int64) ([]*models.PullRequest, error) {
prData, err := gitea.client.Repository.RepoGetPullRequest(
repository.NewRepoGetPullRequestParams().
WithOwner(org).
WithRepo(repo).
WithIndex(prNo),
gitea.transport.DefaultAuthentication)
if err != nil {
return nil, err
}
desc := prData.Payload.Body
strings.Split(desc, "\n")
return nil, nil
}
func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, error) {
var retData []byte

View File

@ -15,7 +15,6 @@ type MaintainershipMap map[string][]string
type GiteaMaintainershipInterface interface {
FetchMaintainershipFile(org, prjGit, branch string) ([]byte, error)
GetPullRequestAndReviews(org, pkg string, num int64) (*models.PullRequest, []*models.PullReview, error)
}
func parseMaintainershipData(data []byte) (MaintainershipMap, error) {
@ -62,7 +61,7 @@ prjMaintainer:
return pkgMaintainers
}
func CheckIfMaintainersApproved(gitea GiteaMaintainershipInterface, config common.AutogitConfig, prjGitPRNumber int64) (bool, error) {
func CheckIfMaintainersApproved(gitea GiteaPRInterface, config common.AutogitConfig, prjGitPRNumber int64) (bool, error) {
pr, reviews, _ := gitea.GetPullRequestAndReviews(config.Organization, config.GitProjectName, prjGitPRNumber)
data, _ := gitea.FetchMaintainershipFile(config.Organization, config.GitProjectName, config.Branch)

View File

@ -1,32 +1,15 @@
package main
import (
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
//go:generate mockgen -source=pr.go -destination=mock/pr.go -typed
type GiteaPRInterface interface {
GetPullRequestAndReviews(org, pkg string, num int64) (*models.PullRequest, []*models.PullReview, error)
GetAssociatedPrjGitPR(org, repo string, id int) (*models.PullRequest, error)
GetAssociatedPRs(org, repo string, id int) ([]*models.PullRequest, error)
}
type PRInterface struct {
gitea common.Gitea
}
func AllocatePRInterface(gitea common.Gitea) GiteaPRInterface {
return &PRInterface{
gitea: gitea,
}
}
func (s *PRInterface) GetAssociatedPrjGitPR(org, repo string, id int) (*models.PullRequest, error) {
return nil, nil
}
func (s *PRInterface) GetAssociatedPRs(org, repo string, id int) ([]*models.PullRequest, error) {
return nil, nil
}