workflow-pr: wip
This commit is contained in:
parent
a025328fef
commit
77751ecc46
92
bots-common/associated_pr_scanner.go
Normal file
92
bots-common/associated_pr_scanner.go
Normal 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()
|
||||
}
|
192
bots-common/associated_pr_scanner_test.go
Normal file
192
bots-common/associated_pr_scanner_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user