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
|
//go:generate mockgen -source=gitea_utils.go -destination=mock/gitea_utils.go -typed
|
||||||
|
|
||||||
const PrPattern = "PR: %s/%s#%d"
|
|
||||||
|
|
||||||
// maintainer list file in ProjectGit
|
// maintainer list file in ProjectGit
|
||||||
const MaintainershipFile = "_maitnainership.json"
|
const MaintainershipFile = "_maitnainership.json"
|
||||||
|
|
||||||
@ -58,6 +56,10 @@ const (
|
|||||||
ReviewStateUnknown models.ReviewStateType = ""
|
ReviewStateUnknown models.ReviewStateType = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GiteaPRFetcher interface {
|
||||||
|
GetAssociatedPRs(org, repo string, prNo int64) ([]*models.PullRequest, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Gitea interface {
|
type Gitea interface {
|
||||||
GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error)
|
GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error)
|
||||||
GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error)
|
GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error)
|
||||||
@ -73,6 +75,8 @@ type Gitea interface {
|
|||||||
GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error)
|
GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error)
|
||||||
GetRecentPullRequests(org, repo string) ([]*models.PullRequest, error)
|
GetRecentPullRequests(org, repo string) ([]*models.PullRequest, error)
|
||||||
GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)
|
GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)
|
||||||
|
|
||||||
|
GiteaPRFetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
type GiteaTransport struct {
|
type GiteaTransport struct {
|
||||||
@ -407,6 +411,25 @@ func (gitea *GiteaTransport) GetAssociatedPrjGitPR(pr *PullRequestWebhookEvent)
|
|||||||
return nil, nil
|
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) {
|
func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, error) {
|
||||||
var retData []byte
|
var retData []byte
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ type MaintainershipMap map[string][]string
|
|||||||
|
|
||||||
type GiteaMaintainershipInterface interface {
|
type GiteaMaintainershipInterface interface {
|
||||||
FetchMaintainershipFile(org, prjGit, branch string) ([]byte, error)
|
FetchMaintainershipFile(org, prjGit, branch string) ([]byte, error)
|
||||||
GetPullRequestAndReviews(org, pkg string, num int64) (*models.PullRequest, []*models.PullReview, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMaintainershipData(data []byte) (MaintainershipMap, error) {
|
func parseMaintainershipData(data []byte) (MaintainershipMap, error) {
|
||||||
@ -62,7 +61,7 @@ prjMaintainer:
|
|||||||
return pkgMaintainers
|
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)
|
pr, reviews, _ := gitea.GetPullRequestAndReviews(config.Organization, config.GitProjectName, prjGitPRNumber)
|
||||||
data, _ := gitea.FetchMaintainershipFile(config.Organization, config.GitProjectName, config.Branch)
|
data, _ := gitea.FetchMaintainershipFile(config.Organization, config.GitProjectName, config.Branch)
|
||||||
|
|
||||||
|
@ -1,32 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate mockgen -source=pr.go -destination=mock/pr.go -typed
|
//go:generate mockgen -source=pr.go -destination=mock/pr.go -typed
|
||||||
|
|
||||||
type GiteaPRInterface interface {
|
type GiteaPRInterface interface {
|
||||||
|
GetPullRequestAndReviews(org, pkg string, num int64) (*models.PullRequest, []*models.PullReview, error)
|
||||||
|
|
||||||
GetAssociatedPrjGitPR(org, repo string, id int) (*models.PullRequest, error)
|
GetAssociatedPrjGitPR(org, repo string, id int) (*models.PullRequest, error)
|
||||||
GetAssociatedPRs(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