Compare commits
77 Commits
submodulem
...
always-rev
| Author | SHA256 | Date | |
|---|---|---|---|
| 877e93c9bf | |||
| cc69a9348c | |||
| 5b5bb9a5bc | |||
|
|
2f39fc9836 | ||
| 38f4c44fd0 | |||
| 605d3dee06 | |||
| 6f26bcdccc | |||
| fffdf4fad3 | |||
| f6d2239f4d | |||
| 913fb7c046 | |||
| 79318dc169 | |||
| 377ed1c37f | |||
| 51b0487b29 | |||
| 49e32c0ab1 | |||
| 01e4f5f59e | |||
| 19d9fc5f1e | |||
| c4e184140a | |||
| 56c492ccdf | |||
| 3a6009a5a3 | |||
| 2c4d25a5eb | |||
| 052ab37412 | |||
| 925f546272 | |||
| 71fd32a707 | |||
| 581131bdc8 | |||
| 495ed349ea | |||
| 350a255d6e | |||
| e3087e46c2 | |||
| ae6b638df6 | |||
| 2c73cc683a | |||
| 32adfb1111 | |||
| fe8fcbae96 | |||
| 5756f7ceea | |||
| 2be0f808d2 | |||
| 7a0f651eaf | |||
| 2e47104b17 | |||
| 76bfa612c5 | |||
| 71aa0813ad | |||
| cc675c1b24 | |||
| 44e4941120 | |||
| 86acfa6871 | |||
| 7f09b2d2d3 | |||
| f3a37f1158 | |||
| 9d6db86318 | |||
| e11993c81f | |||
| 4bd259a2a0 | |||
| 162ae11cdd | |||
| 8431b47322 | |||
| 3ed5ecc3f0 | |||
| d08ab3efd6 | |||
| a4f6628e52 | |||
| 25073dd619 | |||
| 4293181b4e | |||
| 551a4ef577 | |||
| 6afb18fc58 | |||
| f310220261 | |||
| ef7c0c1cea | |||
| 27230fa03b | |||
| c52d40b760 | |||
| d3ba579a8b | |||
| 9ef8209622 | |||
| ba66dd868e | |||
| 17755fa2b5 | |||
| f94d3a8942 | |||
| 20e1109602 | |||
| c25d3be44e | |||
| 8db558891a | |||
| 0e06ba5993 | |||
| 736769d630 | |||
| 93c970d0dd | |||
| 5544a65947 | |||
| 918723d57b | |||
| a418b48809 | |||
|
55846562c1
|
|||
|
95c7770cad
|
|||
|
1b900e3202
|
|||
|
d083acfd1c
|
|||
|
244160e20e
|
@@ -17,7 +17,7 @@
|
||||
|
||||
|
||||
Name: autogits
|
||||
Version: 0
|
||||
Version: 1
|
||||
Release: 0
|
||||
Summary: GitWorkflow utilities
|
||||
License: GPL-2.0-or-later
|
||||
@@ -41,6 +41,7 @@ Command-line tool to import devel projects from obs to git
|
||||
|
||||
%package doc
|
||||
Summary: Common documentation files
|
||||
BuildArch: noarch
|
||||
|
||||
%description -n autogits-doc
|
||||
Common documentation files
|
||||
@@ -56,10 +57,11 @@ with a topic
|
||||
|
||||
|
||||
%package gitea-status-proxy
|
||||
Summary: gitea-status-proxy
|
||||
Summary: Proxy for setting commit status in Gitea
|
||||
|
||||
%description gitea-status-proxy
|
||||
|
||||
Setting commit status requires code write access token. This proxy
|
||||
is middleware that delegates status setting without access to other APIs
|
||||
|
||||
%package group-review
|
||||
Summary: Reviews of groups defined in ProjectGit
|
||||
@@ -167,12 +169,14 @@ install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publishe
|
||||
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
|
||||
install -D -m0755 gitea_status_proxy/gitea_status_proxy %{buildroot}%{_bindir}/gitea_status_proxy
|
||||
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
|
||||
install -D -m0644 systemd/group-review@.service %{buildroot}%{_unitdir}/group-review@.service
|
||||
install -D -m0755 obs-forward-bot/obs-forward-bot %{buildroot}%{_bindir}/obs-forward-bot
|
||||
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot
|
||||
install -D -m0644 systemd/obs-staging-bot.service %{buildroot}%{_unitdir}/obs-staging-bot.service
|
||||
install -D -m0755 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service
|
||||
install -D -m0644 systemd/obs-status-service.service %{buildroot}%{_unitdir}/obs-status-service.service
|
||||
install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
|
||||
install -D -m0644 systemd/workflow-direct@.service %{buildroot}%{_unitdir}/workflow-direct@.service
|
||||
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
|
||||
install -D -m0755 utils/hujson/hujson %{buildroot}%{_bindir}/hujson
|
||||
|
||||
@@ -188,6 +192,18 @@ install -D -m0755 utils/hujson/hujson
|
||||
%postun gitea-events-rabbitmq-publisher
|
||||
%service_del_postun gitea-events-rabbitmq-publisher.service
|
||||
|
||||
%pre group-review
|
||||
%service_add_pre group-review@.service
|
||||
|
||||
%post group-review
|
||||
%service_add_post group-review@.service
|
||||
|
||||
%preun group-review
|
||||
%service_del_preun group-review@.service
|
||||
|
||||
%postun group-review
|
||||
%service_del_postun group-review@.service
|
||||
|
||||
%pre obs-staging-bot
|
||||
%service_add_pre obs-staging-bot.service
|
||||
|
||||
@@ -212,6 +228,18 @@ install -D -m0755 utils/hujson/hujson
|
||||
%postun obs-status-service
|
||||
%service_del_postun obs-status-service.service
|
||||
|
||||
%pre workflow-pr
|
||||
%service_add_pre workflow-direct@.service
|
||||
|
||||
%post workflow-pr
|
||||
%service_add_post workflow-direct@.service
|
||||
|
||||
%preun workflow-pr
|
||||
%service_del_preun workflow-direct@.service
|
||||
|
||||
%postun workflow-pr
|
||||
%service_del_postun workflow-direct@.service
|
||||
|
||||
%files devel-importer
|
||||
%license COPYING
|
||||
%doc devel-importer/README.md
|
||||
@@ -236,6 +264,7 @@ install -D -m0755 utils/hujson/hujson
|
||||
%license COPYING
|
||||
%doc group-review/README.md
|
||||
%{_bindir}/group-review
|
||||
%{_unitdir}/group-review@.service
|
||||
|
||||
%files obs-forward-bot
|
||||
%license COPYING
|
||||
@@ -261,6 +290,7 @@ install -D -m0755 utils/hujson/hujson
|
||||
%license COPYING
|
||||
%doc workflow-direct/README.md
|
||||
%{_bindir}/workflow-direct
|
||||
%{_unitdir}/workflow-direct@.service
|
||||
|
||||
%files workflow-pr
|
||||
%license COPYING
|
||||
|
||||
@@ -61,6 +61,20 @@ type Permissions struct {
|
||||
Members []string
|
||||
}
|
||||
|
||||
const (
|
||||
Label_StagingAuto = "staging/Auto"
|
||||
Label_ReviewPending = "review/Pending"
|
||||
Label_ReviewDone = "review/Done"
|
||||
)
|
||||
|
||||
func LabelKey(tag_value string) string {
|
||||
// capitalize first letter and remove /
|
||||
if len(tag_value) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.ToUpper(tag_value[0:1]) + strings.ReplaceAll(tag_value[1:], "/", "")
|
||||
}
|
||||
|
||||
type AutogitConfig struct {
|
||||
Workflows []string // [pr, direct, test]
|
||||
Organization string
|
||||
@@ -72,9 +86,12 @@ type AutogitConfig struct {
|
||||
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
|
||||
Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories
|
||||
|
||||
Labels map[string]string // list of tags, if not default, to apply
|
||||
|
||||
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
|
||||
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers
|
||||
ManualMergeProject bool // require merge of ProjectGit PRs with "Merge OK" by ProjectMaintainers and/or reviewers
|
||||
ReviewRequired bool // always require a maintainer review, even if maintainer submits it. Only ignored if no other package or project reviewers
|
||||
}
|
||||
|
||||
type AutogitConfigs []*AutogitConfig
|
||||
@@ -188,6 +205,8 @@ func (configs AutogitConfigs) GetPrjGitConfig(org, repo, branch string) *Autogit
|
||||
if c.GitProjectName == prjgit {
|
||||
return c
|
||||
}
|
||||
}
|
||||
for _, c := range configs {
|
||||
if c.Organization == org && c.Branch == branch {
|
||||
return c
|
||||
}
|
||||
@@ -273,6 +292,14 @@ func (config *AutogitConfig) GetRemoteBranch() string {
|
||||
return "origin_" + config.Branch
|
||||
}
|
||||
|
||||
func (config *AutogitConfig) Label(label string) string {
|
||||
if t, found := config.Labels[LabelKey(label)]; found {
|
||||
return t
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
type StagingConfig struct {
|
||||
ObsProject string
|
||||
RebuildAll bool
|
||||
|
||||
@@ -10,6 +10,67 @@ import (
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
)
|
||||
|
||||
func TestLabelKey(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"": "",
|
||||
"foo": "Foo",
|
||||
"foo/bar": "Foobar",
|
||||
"foo/Bar": "FooBar",
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
if c := common.LabelKey(k); c != v {
|
||||
t.Error("expected", v, "got", c, "input", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLabelParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
label_value string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
json: "{}",
|
||||
label_value: "path/String",
|
||||
},
|
||||
{
|
||||
name: "defined",
|
||||
json: `{"Labels": {"foo": "bar", "PathString": "moo/Label"}}`,
|
||||
label_value: "moo/Label",
|
||||
},
|
||||
{
|
||||
name: "undefined",
|
||||
json: `{"Labels": {"foo": "bar", "NotPathString": "moo/Label"}}`,
|
||||
label_value: "path/String",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
repo := models.Repository{
|
||||
DefaultBranch: "master",
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
gitea := mock_common.NewMockGiteaFileContentAndRepoFetcher(ctl)
|
||||
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.json), "abc", nil)
|
||||
gitea.EXPECT().GetRepository("foo", "bar").Return(&repo, nil)
|
||||
|
||||
config, err := common.ReadWorkflowConfig(gitea, "foo/bar")
|
||||
if err != nil || config == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if l := config.Label("path/String"); l != test.label_value {
|
||||
t.Error("Expecting", test.label_value, "got", l)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectConfigMatcher(t *testing.T) {
|
||||
configs := common.AutogitConfigs{
|
||||
{
|
||||
@@ -21,6 +82,15 @@ func TestProjectConfigMatcher(t *testing.T) {
|
||||
Branch: "main",
|
||||
GitProjectName: "test/prjgit#main",
|
||||
},
|
||||
{
|
||||
Organization: "test",
|
||||
Branch: "main",
|
||||
GitProjectName: "test/bar#never_match",
|
||||
},
|
||||
{
|
||||
Organization: "test",
|
||||
GitProjectName: "test/bar#main",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -50,6 +120,20 @@ func TestProjectConfigMatcher(t *testing.T) {
|
||||
branch: "main",
|
||||
config: 1,
|
||||
},
|
||||
{
|
||||
name: "prjgit only match",
|
||||
org: "test",
|
||||
repo: "bar",
|
||||
branch: "main",
|
||||
config: 3,
|
||||
},
|
||||
{
|
||||
name: "non-default branch match",
|
||||
org: "test",
|
||||
repo: "bar",
|
||||
branch: "something_main",
|
||||
config: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -105,6 +189,10 @@ func TestConfigWorkflowParser(t *testing.T) {
|
||||
if config.ManualMergeOnly != false {
|
||||
t.Fatal("This should be false")
|
||||
}
|
||||
|
||||
if config.Label("foobar") != "foobar" {
|
||||
t.Fatal("undefined label should return default value")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,10 @@ var ExtraGitParams []string
|
||||
|
||||
func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string, error) {
|
||||
cmd := exec.Command("/usr/bin/git", params...)
|
||||
var identityFile string
|
||||
if i := os.Getenv("AUTOGITS_IDENTITY_FILE"); len(i) > 0 {
|
||||
identityFile = " -i " + i
|
||||
}
|
||||
cmd.Env = []string{
|
||||
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||
@@ -358,7 +362,7 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
|
||||
"EMAIL=not@exist@src.opensuse.org",
|
||||
"GIT_LFS_SKIP_SMUDGE=1",
|
||||
"GIT_LFS_SKIP_PUSH=1",
|
||||
"GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes",
|
||||
"GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes" + identityFile,
|
||||
}
|
||||
if len(ExtraGitParams) > 0 {
|
||||
cmd.Env = append(cmd.Env, ExtraGitParams...)
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
transport "github.com/go-openapi/runtime/client"
|
||||
@@ -66,6 +67,14 @@ const (
|
||||
ReviewStateUnknown models.ReviewStateType = ""
|
||||
)
|
||||
|
||||
type GiteaLabelGetter interface {
|
||||
GetLabels(org, repo string, idx int64) ([]*models.Label, error)
|
||||
}
|
||||
|
||||
type GiteaLabelSettter interface {
|
||||
SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error)
|
||||
}
|
||||
|
||||
type GiteaTimelineFetcher interface {
|
||||
GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error)
|
||||
}
|
||||
@@ -91,9 +100,10 @@ type GiteaPRUpdater interface {
|
||||
UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error)
|
||||
}
|
||||
|
||||
type GiteaPRTimelineFetcher interface {
|
||||
type GiteaPRTimelineReviewFetcher interface {
|
||||
GiteaPRFetcher
|
||||
GiteaTimelineFetcher
|
||||
GiteaReviewFetcher
|
||||
}
|
||||
|
||||
type GiteaCommitFetcher interface {
|
||||
@@ -119,10 +129,16 @@ type GiteaPRChecker interface {
|
||||
GiteaMaintainershipReader
|
||||
}
|
||||
|
||||
type GiteaReviewFetcherAndRequester interface {
|
||||
type GiteaReviewFetcherAndRequesterAndUnrequester interface {
|
||||
GiteaReviewTimelineFetcher
|
||||
GiteaCommentFetcher
|
||||
GiteaReviewRequester
|
||||
GiteaReviewUnrequester
|
||||
}
|
||||
|
||||
type GiteaUnreviewTimelineFetcher interface {
|
||||
GiteaTimelineFetcher
|
||||
GiteaReviewUnrequester
|
||||
}
|
||||
|
||||
type GiteaReviewRequester interface {
|
||||
@@ -182,6 +198,8 @@ type Gitea interface {
|
||||
GiteaCommitStatusGetter
|
||||
GiteaCommitStatusSetter
|
||||
GiteaSetRepoOptions
|
||||
GiteaLabelGetter
|
||||
GiteaLabelSettter
|
||||
|
||||
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
|
||||
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
|
||||
@@ -189,7 +207,7 @@ type Gitea interface {
|
||||
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)
|
||||
CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error, bool)
|
||||
GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, string, error)
|
||||
GetRecentPullRequests(org, repo, branch string) ([]*models.PullRequest, error)
|
||||
GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)
|
||||
@@ -466,6 +484,30 @@ func (gitea *GiteaTransport) SetRepoOptions(owner, repo string, manual_merge boo
|
||||
return ok.Payload, err
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetLabels(owner, repo string, idx int64) ([]*models.Label, error) {
|
||||
ret, err := gitea.client.Issue.IssueGetLabels(issue.NewIssueGetLabelsParams().WithOwner(owner).WithRepo(repo).WithIndex(idx), gitea.transport.DefaultAuthentication)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret.Payload, err
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) SetLabels(owner, repo string, idx int64, labels []string) ([]*models.Label, error) {
|
||||
interfaceLabels := make([]interface{}, len(labels))
|
||||
for i, l := range labels {
|
||||
interfaceLabels[i] = l
|
||||
}
|
||||
|
||||
ret, err := gitea.client.Issue.IssueAddLabel(issue.NewIssueAddLabelParams().WithOwner(owner).WithRepo(repo).WithIndex(idx).WithBody(&models.IssueLabelsOption{Labels: interfaceLabels}),
|
||||
gitea.transport.DefaultAuthentication)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret.Payload, nil
|
||||
}
|
||||
|
||||
const (
|
||||
GiteaNotificationType_Pull = "Pull"
|
||||
)
|
||||
@@ -643,7 +685,7 @@ func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git Git, org, repoName s
|
||||
return repo.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
|
||||
func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error, bool) {
|
||||
prOptions := models.CreatePullRequestOption{
|
||||
Base: targetId,
|
||||
Head: srcId,
|
||||
@@ -659,7 +701,7 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
|
||||
WithHead(srcId),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
); err == nil && pr.Payload.State == "open" {
|
||||
return pr.Payload, nil
|
||||
return pr.Payload, nil, false
|
||||
}
|
||||
|
||||
pr, err := gitea.client.Repository.RepoCreatePullRequest(
|
||||
@@ -673,10 +715,10 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create pull request. %w", err)
|
||||
return nil, fmt.Errorf("Cannot create pull request. %w", err), true
|
||||
}
|
||||
|
||||
return pr.GetPayload(), nil
|
||||
return pr.GetPayload(), nil, true
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewers ...string) ([]*models.PullReview, error) {
|
||||
@@ -763,45 +805,79 @@ func (gitea *GiteaTransport) AddComment(pr *models.PullRequest, comment string)
|
||||
return nil
|
||||
}
|
||||
|
||||
type TimelineCacheData struct {
|
||||
data []*models.TimelineComment
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
var giteaTimelineCache map[string]TimelineCacheData = make(map[string]TimelineCacheData)
|
||||
var giteaTimelineCacheMutex sync.RWMutex
|
||||
|
||||
// returns timeline in reverse chronological create order
|
||||
func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
||||
page := int64(1)
|
||||
resCount := 1
|
||||
|
||||
retData := []*models.TimelineComment{}
|
||||
prID := fmt.Sprintf("%s/%s!%d", org, repo, idx)
|
||||
giteaTimelineCacheMutex.RLock()
|
||||
TimelineCache, IsCached := giteaTimelineCache[prID]
|
||||
var LastCachedTime strfmt.DateTime
|
||||
if IsCached {
|
||||
l := len(TimelineCache.data)
|
||||
if l > 0 {
|
||||
LastCachedTime = TimelineCache.data[0].Updated
|
||||
}
|
||||
|
||||
// cache data for 5 seconds
|
||||
if TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
return TimelineCache.data, nil
|
||||
}
|
||||
}
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
|
||||
giteaTimelineCacheMutex.Lock()
|
||||
defer giteaTimelineCacheMutex.Unlock()
|
||||
|
||||
for resCount > 0 {
|
||||
res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(
|
||||
issue.NewIssueGetCommentsAndTimelineParams().
|
||||
WithOwner(org).
|
||||
WithRepo(repo).
|
||||
WithIndex(idx).
|
||||
WithPage(&page),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
opts := issue.NewIssueGetCommentsAndTimelineParams().WithOwner(org).WithRepo(repo).WithIndex(idx).WithPage(&page)
|
||||
if !LastCachedTime.IsZero() {
|
||||
opts = opts.WithSince(&LastCachedTime)
|
||||
}
|
||||
res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(opts, gitea.transport.DefaultAuthentication)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resCount = len(res.Payload)
|
||||
LogDebug("page:", page, "len:", resCount)
|
||||
if resCount == 0 {
|
||||
if resCount = len(res.Payload); resCount == 0 {
|
||||
break
|
||||
}
|
||||
page++
|
||||
|
||||
for _, d := range res.Payload {
|
||||
if d != nil {
|
||||
retData = append(retData, d)
|
||||
if time.Time(d.Created).Compare(time.Time(LastCachedTime)) > 0 {
|
||||
// created after last check, so we append here
|
||||
TimelineCache.data = append(TimelineCache.data, d)
|
||||
} else {
|
||||
// we need something updated in the timeline, maybe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resCount < 10 {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
LogDebug("total results:", len(retData))
|
||||
slices.SortFunc(retData, func(a, b *models.TimelineComment) int {
|
||||
LogDebug("timeline", prID, "# timeline:", len(TimelineCache.data))
|
||||
slices.SortFunc(TimelineCache.data, func(a, b *models.TimelineComment) int {
|
||||
return time.Time(b.Created).Compare(time.Time(a.Created))
|
||||
})
|
||||
|
||||
return retData, nil
|
||||
TimelineCache.lastCheck = time.Now()
|
||||
giteaTimelineCache[prID] = TimelineCache
|
||||
|
||||
return TimelineCache.data, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error) {
|
||||
|
||||
@@ -25,6 +25,7 @@ const ProjectFileKey = "_project"
|
||||
type MaintainershipMap struct {
|
||||
Data map[string][]string
|
||||
IsDir bool
|
||||
Config *AutogitConfig
|
||||
FetchPackage func(string) ([]byte, error)
|
||||
}
|
||||
|
||||
@@ -39,7 +40,9 @@ func parseMaintainershipData(data []byte) (*MaintainershipMap, error) {
|
||||
return maintainers, nil
|
||||
}
|
||||
|
||||
func FetchProjectMaintainershipData(gitea GiteaMaintainershipReader, org, prjGit, branch string) (*MaintainershipMap, error) {
|
||||
func FetchProjectMaintainershipData(gitea GiteaMaintainershipReader, config *AutogitConfig) (*MaintainershipMap, error) {
|
||||
org, prjGit, branch := config.GetPrjGit()
|
||||
|
||||
data, _, err := gitea.FetchMaintainershipDirFile(org, prjGit, branch, ProjectFileKey)
|
||||
dir := true
|
||||
if err != nil || data == nil {
|
||||
@@ -61,6 +64,7 @@ func FetchProjectMaintainershipData(gitea GiteaMaintainershipReader, org, prjGit
|
||||
|
||||
m, err := parseMaintainershipData(data)
|
||||
if m != nil {
|
||||
m.Config = config
|
||||
m.IsDir = dir
|
||||
m.FetchPackage = func(pkg string) ([]byte, error) {
|
||||
data, _, err := gitea.FetchMaintainershipDirFile(org, prjGit, branch, pkg)
|
||||
@@ -149,7 +153,10 @@ func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullRevi
|
||||
}
|
||||
|
||||
LogDebug("Looking for review by:", reviewers)
|
||||
if slices.Contains(reviewers, submitter) {
|
||||
slices.Sort(reviewers)
|
||||
reviewers = slices.Compact(reviewers)
|
||||
SubmitterIdxInReviewers := slices.Index(reviewers, submitter)
|
||||
if SubmitterIdxInReviewers > -1 && (!data.Config.ReviewRequired || len(reviewers) == 1) {
|
||||
LogDebug("Submitter is maintainer. Approving.")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
)
|
||||
|
||||
func TestMaintainership(t *testing.T) {
|
||||
config := common.AutogitConfig{
|
||||
config := &common.AutogitConfig{
|
||||
Branch: "bar",
|
||||
Organization: "foo",
|
||||
GitProjectName: common.DefaultGitPrj,
|
||||
GitProjectName: common.DefaultGitPrj + "#bar",
|
||||
}
|
||||
|
||||
packageTests := []struct {
|
||||
@@ -141,7 +141,7 @@ func TestMaintainership(t *testing.T) {
|
||||
notFoundError := repository.NewRepoGetContentsNotFound()
|
||||
for _, test := range packageTests {
|
||||
runTests := func(t *testing.T, mi common.GiteaMaintainershipReader) {
|
||||
maintainers, err := common.FetchProjectMaintainershipData(mi, config.Organization, config.GitProjectName, config.Branch)
|
||||
maintainers, err := common.FetchProjectMaintainershipData(mi, config)
|
||||
if err != nil && !test.otherError {
|
||||
if test.maintainersFileErr == nil {
|
||||
t.Fatal("Unexpected error recieved", err)
|
||||
@@ -253,3 +253,43 @@ func TestMaintainershipFileWrite(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
maintainers []string
|
||||
config *common.AutogitConfig
|
||||
is_approved bool
|
||||
}{
|
||||
{
|
||||
name: "ReviewRequired=false",
|
||||
maintainers: []string{"maintainer1", "maintainer2"},
|
||||
config: &common.AutogitConfig{ReviewRequired: false},
|
||||
is_approved: true,
|
||||
},
|
||||
{
|
||||
name: "ReviewRequired=true",
|
||||
maintainers: []string{"maintainer1", "maintainer2"},
|
||||
config: &common.AutogitConfig{ReviewRequired: true},
|
||||
is_approved: false,
|
||||
},
|
||||
{
|
||||
name: "ReviewRequired=true",
|
||||
maintainers: []string{"maintainer1"},
|
||||
config: &common.AutogitConfig{ReviewRequired: true},
|
||||
is_approved: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
m := &common.MaintainershipMap{
|
||||
Data: map[string][]string{"": test.maintainers},
|
||||
}
|
||||
m.Config = test.config
|
||||
if approved := m.IsApproved("", nil, "maintainer1", nil); approved != test.is_approved {
|
||||
t.Error("Expected m.IsApproved()->", test.is_approved, "but didn't get it")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,132 @@ import (
|
||||
models "src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
)
|
||||
|
||||
// MockGiteaLabelGetter is a mock of GiteaLabelGetter interface.
|
||||
type MockGiteaLabelGetter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGiteaLabelGetterMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockGiteaLabelGetterMockRecorder is the mock recorder for MockGiteaLabelGetter.
|
||||
type MockGiteaLabelGetterMockRecorder struct {
|
||||
mock *MockGiteaLabelGetter
|
||||
}
|
||||
|
||||
// NewMockGiteaLabelGetter creates a new mock instance.
|
||||
func NewMockGiteaLabelGetter(ctrl *gomock.Controller) *MockGiteaLabelGetter {
|
||||
mock := &MockGiteaLabelGetter{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaLabelGetterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGiteaLabelGetter) EXPECT() *MockGiteaLabelGetterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetLabels mocks base method.
|
||||
func (m *MockGiteaLabelGetter) GetLabels(org, repo string, idx int64) ([]*models.Label, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetLabels", org, repo, idx)
|
||||
ret0, _ := ret[0].([]*models.Label)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetLabels indicates an expected call of GetLabels.
|
||||
func (mr *MockGiteaLabelGetterMockRecorder) GetLabels(org, repo, idx any) *MockGiteaLabelGetterGetLabelsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLabels", reflect.TypeOf((*MockGiteaLabelGetter)(nil).GetLabels), org, repo, idx)
|
||||
return &MockGiteaLabelGetterGetLabelsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaLabelGetterGetLabelsCall wrap *gomock.Call
|
||||
type MockGiteaLabelGetterGetLabelsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaLabelGetterGetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaLabelGetterGetLabelsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaLabelGetterGetLabelsCall) Do(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaLabelGetterGetLabelsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaLabelGetterGetLabelsCall) DoAndReturn(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaLabelGetterGetLabelsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaLabelSettter is a mock of GiteaLabelSettter interface.
|
||||
type MockGiteaLabelSettter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGiteaLabelSettterMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockGiteaLabelSettterMockRecorder is the mock recorder for MockGiteaLabelSettter.
|
||||
type MockGiteaLabelSettterMockRecorder struct {
|
||||
mock *MockGiteaLabelSettter
|
||||
}
|
||||
|
||||
// NewMockGiteaLabelSettter creates a new mock instance.
|
||||
func NewMockGiteaLabelSettter(ctrl *gomock.Controller) *MockGiteaLabelSettter {
|
||||
mock := &MockGiteaLabelSettter{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaLabelSettterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGiteaLabelSettter) EXPECT() *MockGiteaLabelSettterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// SetLabels mocks base method.
|
||||
func (m *MockGiteaLabelSettter) SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetLabels", org, repo, idx, labels)
|
||||
ret0, _ := ret[0].([]*models.Label)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SetLabels indicates an expected call of SetLabels.
|
||||
func (mr *MockGiteaLabelSettterMockRecorder) SetLabels(org, repo, idx, labels any) *MockGiteaLabelSettterSetLabelsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLabels", reflect.TypeOf((*MockGiteaLabelSettter)(nil).SetLabels), org, repo, idx, labels)
|
||||
return &MockGiteaLabelSettterSetLabelsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaLabelSettterSetLabelsCall wrap *gomock.Call
|
||||
type MockGiteaLabelSettterSetLabelsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaLabelSettterSetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaLabelSettterSetLabelsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaLabelSettterSetLabelsCall) Do(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaLabelSettterSetLabelsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaLabelSettterSetLabelsCall) DoAndReturn(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaLabelSettterSetLabelsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaTimelineFetcher is a mock of GiteaTimelineFetcher interface.
|
||||
type MockGiteaTimelineFetcher struct {
|
||||
ctrl *gomock.Controller
|
||||
@@ -436,32 +562,32 @@ func (c *MockGiteaPRUpdaterUpdatePullRequestCall) DoAndReturn(f func(string, str
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaPRTimelineFetcher is a mock of GiteaPRTimelineFetcher interface.
|
||||
type MockGiteaPRTimelineFetcher struct {
|
||||
// MockGiteaPRTimelineReviewFetcher is a mock of GiteaPRTimelineReviewFetcher interface.
|
||||
type MockGiteaPRTimelineReviewFetcher struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGiteaPRTimelineFetcherMockRecorder
|
||||
recorder *MockGiteaPRTimelineReviewFetcherMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockGiteaPRTimelineFetcherMockRecorder is the mock recorder for MockGiteaPRTimelineFetcher.
|
||||
type MockGiteaPRTimelineFetcherMockRecorder struct {
|
||||
mock *MockGiteaPRTimelineFetcher
|
||||
// MockGiteaPRTimelineReviewFetcherMockRecorder is the mock recorder for MockGiteaPRTimelineReviewFetcher.
|
||||
type MockGiteaPRTimelineReviewFetcherMockRecorder struct {
|
||||
mock *MockGiteaPRTimelineReviewFetcher
|
||||
}
|
||||
|
||||
// NewMockGiteaPRTimelineFetcher creates a new mock instance.
|
||||
func NewMockGiteaPRTimelineFetcher(ctrl *gomock.Controller) *MockGiteaPRTimelineFetcher {
|
||||
mock := &MockGiteaPRTimelineFetcher{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaPRTimelineFetcherMockRecorder{mock}
|
||||
// NewMockGiteaPRTimelineReviewFetcher creates a new mock instance.
|
||||
func NewMockGiteaPRTimelineReviewFetcher(ctrl *gomock.Controller) *MockGiteaPRTimelineReviewFetcher {
|
||||
mock := &MockGiteaPRTimelineReviewFetcher{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaPRTimelineReviewFetcherMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGiteaPRTimelineFetcher) EXPECT() *MockGiteaPRTimelineFetcherMockRecorder {
|
||||
func (m *MockGiteaPRTimelineReviewFetcher) EXPECT() *MockGiteaPRTimelineReviewFetcherMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetPullRequest mocks base method.
|
||||
func (m *MockGiteaPRTimelineFetcher) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) {
|
||||
func (m *MockGiteaPRTimelineReviewFetcher) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPullRequest", org, project, num)
|
||||
ret0, _ := ret[0].(*models.PullRequest)
|
||||
@@ -470,37 +596,76 @@ func (m *MockGiteaPRTimelineFetcher) GetPullRequest(org, project string, num int
|
||||
}
|
||||
|
||||
// GetPullRequest indicates an expected call of GetPullRequest.
|
||||
func (mr *MockGiteaPRTimelineFetcherMockRecorder) GetPullRequest(org, project, num any) *MockGiteaPRTimelineFetcherGetPullRequestCall {
|
||||
func (mr *MockGiteaPRTimelineReviewFetcherMockRecorder) GetPullRequest(org, project, num any) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequest", reflect.TypeOf((*MockGiteaPRTimelineFetcher)(nil).GetPullRequest), org, project, num)
|
||||
return &MockGiteaPRTimelineFetcherGetPullRequestCall{Call: call}
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequest", reflect.TypeOf((*MockGiteaPRTimelineReviewFetcher)(nil).GetPullRequest), org, project, num)
|
||||
return &MockGiteaPRTimelineReviewFetcherGetPullRequestCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaPRTimelineFetcherGetPullRequestCall wrap *gomock.Call
|
||||
type MockGiteaPRTimelineFetcherGetPullRequestCall struct {
|
||||
// MockGiteaPRTimelineReviewFetcherGetPullRequestCall wrap *gomock.Call
|
||||
type MockGiteaPRTimelineReviewFetcherGetPullRequestCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaPRTimelineFetcherGetPullRequestCall) Return(arg0 *models.PullRequest, arg1 error) *MockGiteaPRTimelineFetcherGetPullRequestCall {
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestCall) Return(arg0 *models.PullRequest, arg1 error) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaPRTimelineFetcherGetPullRequestCall) Do(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineFetcherGetPullRequestCall {
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestCall) Do(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaPRTimelineFetcherGetPullRequestCall) DoAndReturn(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineFetcherGetPullRequestCall {
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestCall) DoAndReturn(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetPullRequestReviews mocks base method.
|
||||
func (m *MockGiteaPRTimelineReviewFetcher) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPullRequestReviews", org, project, PRnum)
|
||||
ret0, _ := ret[0].([]*models.PullReview)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPullRequestReviews indicates an expected call of GetPullRequestReviews.
|
||||
func (mr *MockGiteaPRTimelineReviewFetcherMockRecorder) GetPullRequestReviews(org, project, PRnum any) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequestReviews", reflect.TypeOf((*MockGiteaPRTimelineReviewFetcher)(nil).GetPullRequestReviews), org, project, PRnum)
|
||||
return &MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall wrap *gomock.Call
|
||||
type MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall) Do(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall) DoAndReturn(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetTimeline mocks base method.
|
||||
func (m *MockGiteaPRTimelineFetcher) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
||||
func (m *MockGiteaPRTimelineReviewFetcher) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx)
|
||||
ret0, _ := ret[0].([]*models.TimelineComment)
|
||||
@@ -509,31 +674,31 @@ func (m *MockGiteaPRTimelineFetcher) GetTimeline(org, repo string, idx int64) ([
|
||||
}
|
||||
|
||||
// GetTimeline indicates an expected call of GetTimeline.
|
||||
func (mr *MockGiteaPRTimelineFetcherMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaPRTimelineFetcherGetTimelineCall {
|
||||
func (mr *MockGiteaPRTimelineReviewFetcherMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaPRTimelineReviewFetcherGetTimelineCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaPRTimelineFetcher)(nil).GetTimeline), org, repo, idx)
|
||||
return &MockGiteaPRTimelineFetcherGetTimelineCall{Call: call}
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaPRTimelineReviewFetcher)(nil).GetTimeline), org, repo, idx)
|
||||
return &MockGiteaPRTimelineReviewFetcherGetTimelineCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaPRTimelineFetcherGetTimelineCall wrap *gomock.Call
|
||||
type MockGiteaPRTimelineFetcherGetTimelineCall struct {
|
||||
// MockGiteaPRTimelineReviewFetcherGetTimelineCall wrap *gomock.Call
|
||||
type MockGiteaPRTimelineReviewFetcherGetTimelineCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaPRTimelineFetcherGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaPRTimelineFetcherGetTimelineCall {
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaPRTimelineReviewFetcherGetTimelineCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaPRTimelineFetcherGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineFetcherGetTimelineCall {
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineReviewFetcherGetTimelineCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaPRTimelineFetcherGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineFetcherGetTimelineCall {
|
||||
func (c *MockGiteaPRTimelineReviewFetcherGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineReviewFetcherGetTimelineCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
@@ -1050,32 +1215,32 @@ func (c *MockGiteaPRCheckerGetTimelineCall) DoAndReturn(f func(string, string, i
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaReviewFetcherAndRequester is a mock of GiteaReviewFetcherAndRequester interface.
|
||||
type MockGiteaReviewFetcherAndRequester struct {
|
||||
// MockGiteaReviewFetcherAndRequesterAndUnrequester is a mock of GiteaReviewFetcherAndRequesterAndUnrequester interface.
|
||||
type MockGiteaReviewFetcherAndRequesterAndUnrequester struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGiteaReviewFetcherAndRequesterMockRecorder
|
||||
recorder *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockGiteaReviewFetcherAndRequesterMockRecorder is the mock recorder for MockGiteaReviewFetcherAndRequester.
|
||||
type MockGiteaReviewFetcherAndRequesterMockRecorder struct {
|
||||
mock *MockGiteaReviewFetcherAndRequester
|
||||
// MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder is the mock recorder for MockGiteaReviewFetcherAndRequesterAndUnrequester.
|
||||
type MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder struct {
|
||||
mock *MockGiteaReviewFetcherAndRequesterAndUnrequester
|
||||
}
|
||||
|
||||
// NewMockGiteaReviewFetcherAndRequester creates a new mock instance.
|
||||
func NewMockGiteaReviewFetcherAndRequester(ctrl *gomock.Controller) *MockGiteaReviewFetcherAndRequester {
|
||||
mock := &MockGiteaReviewFetcherAndRequester{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaReviewFetcherAndRequesterMockRecorder{mock}
|
||||
// NewMockGiteaReviewFetcherAndRequesterAndUnrequester creates a new mock instance.
|
||||
func NewMockGiteaReviewFetcherAndRequesterAndUnrequester(ctrl *gomock.Controller) *MockGiteaReviewFetcherAndRequesterAndUnrequester {
|
||||
mock := &MockGiteaReviewFetcherAndRequesterAndUnrequester{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGiteaReviewFetcherAndRequester) EXPECT() *MockGiteaReviewFetcherAndRequesterMockRecorder {
|
||||
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) EXPECT() *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetIssueComments mocks base method.
|
||||
func (m *MockGiteaReviewFetcherAndRequester) GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error) {
|
||||
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetIssueComments", org, project, issueNo)
|
||||
ret0, _ := ret[0].([]*models.Comment)
|
||||
@@ -1084,37 +1249,37 @@ func (m *MockGiteaReviewFetcherAndRequester) GetIssueComments(org, project strin
|
||||
}
|
||||
|
||||
// GetIssueComments indicates an expected call of GetIssueComments.
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) GetIssueComments(org, project, issueNo any) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) GetIssueComments(org, project, issueNo any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIssueComments", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).GetIssueComments), org, project, issueNo)
|
||||
return &MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall{Call: call}
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIssueComments", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).GetIssueComments), org, project, issueNo)
|
||||
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall struct {
|
||||
// MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall) Return(arg0 []*models.Comment, arg1 error) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall) Return(arg0 []*models.Comment, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall) Do(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall) Do(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall) DoAndReturn(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall) DoAndReturn(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetPullRequestReviews mocks base method.
|
||||
func (m *MockGiteaReviewFetcherAndRequester) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
|
||||
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPullRequestReviews", org, project, PRnum)
|
||||
ret0, _ := ret[0].([]*models.PullReview)
|
||||
@@ -1123,37 +1288,37 @@ func (m *MockGiteaReviewFetcherAndRequester) GetPullRequestReviews(org, project
|
||||
}
|
||||
|
||||
// GetPullRequestReviews indicates an expected call of GetPullRequestReviews.
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) GetPullRequestReviews(org, project, PRnum any) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) GetPullRequestReviews(org, project, PRnum any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).GetPullRequestReviews), org, project, PRnum)
|
||||
return &MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall{Call: call}
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).GetPullRequestReviews), org, project, PRnum)
|
||||
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall struct {
|
||||
// MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall) Do(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall) Do(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall) DoAndReturn(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall) DoAndReturn(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetTimeline mocks base method.
|
||||
func (m *MockGiteaReviewFetcherAndRequester) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
||||
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx)
|
||||
ret0, _ := ret[0].([]*models.TimelineComment)
|
||||
@@ -1162,37 +1327,37 @@ func (m *MockGiteaReviewFetcherAndRequester) GetTimeline(org, repo string, idx i
|
||||
}
|
||||
|
||||
// GetTimeline indicates an expected call of GetTimeline.
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).GetTimeline), org, repo, idx)
|
||||
return &MockGiteaReviewFetcherAndRequesterGetTimelineCall{Call: call}
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).GetTimeline), org, repo, idx)
|
||||
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaReviewFetcherAndRequesterGetTimelineCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterGetTimelineCall struct {
|
||||
// MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaReviewFetcherAndRequesterGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// RequestReviews mocks base method.
|
||||
func (m *MockGiteaReviewFetcherAndRequester) RequestReviews(pr *models.PullRequest, reviewer ...string) ([]*models.PullReview, error) {
|
||||
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) RequestReviews(pr *models.PullRequest, reviewer ...string) ([]*models.PullReview, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{pr}
|
||||
for _, a := range reviewer {
|
||||
@@ -1205,32 +1370,181 @@ func (m *MockGiteaReviewFetcherAndRequester) RequestReviews(pr *models.PullReque
|
||||
}
|
||||
|
||||
// RequestReviews indicates an expected call of RequestReviews.
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) RequestReviews(pr any, reviewer ...any) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) RequestReviews(pr any, reviewer ...any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{pr}, reviewer...)
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).RequestReviews), varargs...)
|
||||
return &MockGiteaReviewFetcherAndRequesterRequestReviewsCall{Call: call}
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).RequestReviews), varargs...)
|
||||
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaReviewFetcherAndRequesterRequestReviewsCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterRequestReviewsCall struct {
|
||||
// MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaReviewFetcherAndRequesterRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaReviewFetcherAndRequesterRequestReviewsCall) Do(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall) Do(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaReviewFetcherAndRequesterRequestReviewsCall) DoAndReturn(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall) DoAndReturn(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// UnrequestReview mocks base method.
|
||||
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) UnrequestReview(org, repo string, id int64, reviwers ...string) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{org, repo, id}
|
||||
for _, a := range reviwers {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "UnrequestReview", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnrequestReview indicates an expected call of UnrequestReview.
|
||||
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) UnrequestReview(org, repo, id any, reviwers ...any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{org, repo, id}, reviwers...)
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnrequestReview", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).UnrequestReview), varargs...)
|
||||
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall wrap *gomock.Call
|
||||
type MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall) Return(arg0 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall) Do(f func(string, string, int64, ...string) error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall) DoAndReturn(f func(string, string, int64, ...string) error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaUnreviewTimelineFetcher is a mock of GiteaUnreviewTimelineFetcher interface.
|
||||
type MockGiteaUnreviewTimelineFetcher struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGiteaUnreviewTimelineFetcherMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockGiteaUnreviewTimelineFetcherMockRecorder is the mock recorder for MockGiteaUnreviewTimelineFetcher.
|
||||
type MockGiteaUnreviewTimelineFetcherMockRecorder struct {
|
||||
mock *MockGiteaUnreviewTimelineFetcher
|
||||
}
|
||||
|
||||
// NewMockGiteaUnreviewTimelineFetcher creates a new mock instance.
|
||||
func NewMockGiteaUnreviewTimelineFetcher(ctrl *gomock.Controller) *MockGiteaUnreviewTimelineFetcher {
|
||||
mock := &MockGiteaUnreviewTimelineFetcher{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaUnreviewTimelineFetcherMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGiteaUnreviewTimelineFetcher) EXPECT() *MockGiteaUnreviewTimelineFetcherMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetTimeline mocks base method.
|
||||
func (m *MockGiteaUnreviewTimelineFetcher) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx)
|
||||
ret0, _ := ret[0].([]*models.TimelineComment)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTimeline indicates an expected call of GetTimeline.
|
||||
func (mr *MockGiteaUnreviewTimelineFetcherMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaUnreviewTimelineFetcher)(nil).GetTimeline), org, repo, idx)
|
||||
return &MockGiteaUnreviewTimelineFetcherGetTimelineCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaUnreviewTimelineFetcherGetTimelineCall wrap *gomock.Call
|
||||
type MockGiteaUnreviewTimelineFetcherGetTimelineCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaUnreviewTimelineFetcherGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaUnreviewTimelineFetcherGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaUnreviewTimelineFetcherGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// UnrequestReview mocks base method.
|
||||
func (m *MockGiteaUnreviewTimelineFetcher) UnrequestReview(org, repo string, id int64, reviwers ...string) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{org, repo, id}
|
||||
for _, a := range reviwers {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "UnrequestReview", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnrequestReview indicates an expected call of UnrequestReview.
|
||||
func (mr *MockGiteaUnreviewTimelineFetcherMockRecorder) UnrequestReview(org, repo, id any, reviwers ...any) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{org, repo, id}, reviwers...)
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnrequestReview", reflect.TypeOf((*MockGiteaUnreviewTimelineFetcher)(nil).UnrequestReview), varargs...)
|
||||
return &MockGiteaUnreviewTimelineFetcherUnrequestReviewCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaUnreviewTimelineFetcherUnrequestReviewCall wrap *gomock.Call
|
||||
type MockGiteaUnreviewTimelineFetcherUnrequestReviewCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall) Return(arg0 error) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall) Do(f func(string, string, int64, ...string) error) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall) DoAndReturn(f func(string, string, int64, ...string) error) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
@@ -1850,12 +2164,13 @@ func (c *MockGiteaAddReviewCommentCall) DoAndReturn(f func(*models.PullRequest,
|
||||
}
|
||||
|
||||
// CreatePullRequestIfNotExist mocks base method.
|
||||
func (m *MockGitea) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
|
||||
func (m *MockGitea) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error, bool) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreatePullRequestIfNotExist", repo, srcId, targetId, title, body)
|
||||
ret0, _ := ret[0].(*models.PullRequest)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
ret2, _ := ret[2].(bool)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// CreatePullRequestIfNotExist indicates an expected call of CreatePullRequestIfNotExist.
|
||||
@@ -1871,19 +2186,19 @@ type MockGiteaCreatePullRequestIfNotExistCall struct {
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaCreatePullRequestIfNotExistCall) Return(arg0 *models.PullRequest, arg1 error) *MockGiteaCreatePullRequestIfNotExistCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
func (c *MockGiteaCreatePullRequestIfNotExistCall) Return(arg0 *models.PullRequest, arg1 error, arg2 bool) *MockGiteaCreatePullRequestIfNotExistCall {
|
||||
c.Call = c.Call.Return(arg0, arg1, arg2)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaCreatePullRequestIfNotExistCall) Do(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error)) *MockGiteaCreatePullRequestIfNotExistCall {
|
||||
func (c *MockGiteaCreatePullRequestIfNotExistCall) Do(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error, bool)) *MockGiteaCreatePullRequestIfNotExistCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaCreatePullRequestIfNotExistCall) DoAndReturn(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error)) *MockGiteaCreatePullRequestIfNotExistCall {
|
||||
func (c *MockGiteaCreatePullRequestIfNotExistCall) DoAndReturn(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error, bool)) *MockGiteaCreatePullRequestIfNotExistCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
@@ -2202,6 +2517,45 @@ func (c *MockGiteaGetIssueCommentsCall) DoAndReturn(f func(string, string, int64
|
||||
return c
|
||||
}
|
||||
|
||||
// GetLabels mocks base method.
|
||||
func (m *MockGitea) GetLabels(org, repo string, idx int64) ([]*models.Label, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetLabels", org, repo, idx)
|
||||
ret0, _ := ret[0].([]*models.Label)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetLabels indicates an expected call of GetLabels.
|
||||
func (mr *MockGiteaMockRecorder) GetLabels(org, repo, idx any) *MockGiteaGetLabelsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLabels", reflect.TypeOf((*MockGitea)(nil).GetLabels), org, repo, idx)
|
||||
return &MockGiteaGetLabelsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaGetLabelsCall wrap *gomock.Call
|
||||
type MockGiteaGetLabelsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaGetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaGetLabelsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaGetLabelsCall) Do(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaGetLabelsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaGetLabelsCall) DoAndReturn(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaGetLabelsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetNotifications mocks base method.
|
||||
func (m *MockGitea) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2793,6 +3147,45 @@ func (c *MockGiteaSetCommitStatusCall) DoAndReturn(f func(string, string, string
|
||||
return c
|
||||
}
|
||||
|
||||
// SetLabels mocks base method.
|
||||
func (m *MockGitea) SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetLabels", org, repo, idx, labels)
|
||||
ret0, _ := ret[0].([]*models.Label)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SetLabels indicates an expected call of SetLabels.
|
||||
func (mr *MockGiteaMockRecorder) SetLabels(org, repo, idx, labels any) *MockGiteaSetLabelsCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLabels", reflect.TypeOf((*MockGitea)(nil).SetLabels), org, repo, idx, labels)
|
||||
return &MockGiteaSetLabelsCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaSetLabelsCall wrap *gomock.Call
|
||||
type MockGiteaSetLabelsCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaSetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaSetLabelsCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaSetLabelsCall) Do(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaSetLabelsCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaSetLabelsCall) DoAndReturn(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaSetLabelsCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// SetNotificationRead mocks base method.
|
||||
func (m *MockGitea) SetNotificationRead(notificationId int64) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
257
common/pr.go
257
common/pr.go
@@ -23,7 +23,8 @@ type PRSet struct {
|
||||
PRs []*PRInfo
|
||||
Config *AutogitConfig
|
||||
|
||||
BotUser string
|
||||
BotUser string
|
||||
HasAutoStaging bool
|
||||
}
|
||||
|
||||
func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
|
||||
@@ -33,6 +34,41 @@ func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
|
||||
return
|
||||
}
|
||||
|
||||
func (prinfo *PRInfo) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, Reviewers []string, BotUser string) {
|
||||
org, repo, idx := prinfo.PRComponents()
|
||||
tl, err := gitea.GetTimeline(org, repo, idx)
|
||||
if err != nil {
|
||||
LogError("Failed to fetch timeline for", PRtoString(prinfo.PR), err)
|
||||
}
|
||||
|
||||
// find review request for each reviewer
|
||||
ReviewersToUnrequest := Reviewers
|
||||
ReviewersAlreadyChecked := []string{}
|
||||
|
||||
for _, tlc := range tl {
|
||||
if tlc.Type == TimelineCommentType_ReviewRequested && tlc.Assignee != nil {
|
||||
user := tlc.Assignee.UserName
|
||||
|
||||
if idx := slices.Index(ReviewersToUnrequest, user); idx >= 0 && !slices.Contains(ReviewersAlreadyChecked, user) {
|
||||
if tlc.User != nil && tlc.User.UserName == BotUser {
|
||||
ReviewersAlreadyChecked = append(ReviewersAlreadyChecked, user)
|
||||
continue
|
||||
}
|
||||
ReviewersToUnrequest = slices.Delete(ReviewersToUnrequest, idx, idx+1)
|
||||
if len(Reviewers) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogDebug("Unrequesting reviewes for", PRtoString(prinfo.PR), ReviewersToUnrequest)
|
||||
err = gitea.UnrequestReview(org, repo, idx, ReviewersToUnrequest...)
|
||||
if err != nil {
|
||||
LogError("Failed to unrequest reviewers for", PRtoString(prinfo.PR), err)
|
||||
}
|
||||
}
|
||||
|
||||
func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRInfo, config *AutogitConfig) ([]*PRInfo, error) {
|
||||
for _, p := range currentSet {
|
||||
if pr.Index == p.PR.Index && pr.Base.Repo.Name == p.PR.Base.Repo.Name && pr.Base.Repo.Owner.UserName == p.PR.Base.Repo.Owner.UserName {
|
||||
@@ -63,7 +99,7 @@ func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRIn
|
||||
|
||||
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
|
||||
|
||||
func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineFetcher, org, repo string, num int64, config *AutogitConfig) (*models.PullRequest, error) {
|
||||
func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*models.PullRequest, error) {
|
||||
timeline, err := gitea.GetTimeline(org, repo, num)
|
||||
if err != nil {
|
||||
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
|
||||
@@ -88,14 +124,19 @@ func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineFetcher, org,
|
||||
}
|
||||
|
||||
pr, err := gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index)
|
||||
switch err.(type) {
|
||||
case *repository.RepoGetPullRequestNotFound: // deleted?
|
||||
continue
|
||||
default:
|
||||
LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *repository.RepoGetPullRequestNotFound: // deleted?
|
||||
continue
|
||||
default:
|
||||
LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if pr.Base.Ref != prjGitBranch {
|
||||
LogDebug("found ref PR on timeline:", PRtoString(pr))
|
||||
if pr.Base.Name != prjGitBranch {
|
||||
LogDebug(" -> not matching:", pr.Base.Name, prjGitBranch)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -115,7 +156,7 @@ func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineFetcher, org,
|
||||
return nil, Timeline_RefIssueNotFound
|
||||
}
|
||||
|
||||
func FetchPRSet(user string, gitea GiteaPRTimelineFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
|
||||
func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
|
||||
var pr *models.PullRequest
|
||||
var err error
|
||||
|
||||
@@ -141,6 +182,15 @@ func FetchPRSet(user string, gitea GiteaPRTimelineFetcher, org, repo string, num
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pr := range prs {
|
||||
org, repo, idx := pr.PRComponents()
|
||||
reviews, err := FetchGiteaReviews(gitea, org, repo, idx)
|
||||
if err != nil {
|
||||
LogError("Error fetching reviews for", PRtoString(pr.PR), ":", err)
|
||||
}
|
||||
pr.Reviews = reviews
|
||||
}
|
||||
|
||||
return &PRSet{
|
||||
PRs: prs,
|
||||
Config: config,
|
||||
@@ -148,6 +198,12 @@ func FetchPRSet(user string, gitea GiteaPRTimelineFetcher, org, repo string, num
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (prset *PRSet) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, reviewers []string) {
|
||||
for _, prinfo := range prset.PRs {
|
||||
prinfo.RemoveReviewers(gitea, reviewers, prset.BotUser)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) {
|
||||
for _, p := range rs.PRs {
|
||||
if p.PR.Base.RepoID == pr.Base.RepoID &&
|
||||
@@ -233,67 +289,150 @@ next_rs:
|
||||
}
|
||||
|
||||
for _, pr := range prjpr_set {
|
||||
if prinfo.PR.Base.Repo.Owner.UserName == pr.Org && prinfo.PR.Base.Repo.Name == pr.Repo && prinfo.PR.Index == pr.Num {
|
||||
if strings.EqualFold(prinfo.PR.Base.Repo.Owner.UserName, pr.Org) && strings.EqualFold(prinfo.PR.Base.Repo.Name, pr.Repo) && prinfo.PR.Index == pr.Num {
|
||||
continue next_rs
|
||||
}
|
||||
}
|
||||
LogDebug(" PR: ", PRtoString(prinfo.PR), "not found in project git PRSet")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintainers MaintainershipData) error {
|
||||
func (rs *PRSet) FindMissingAndExtraReviewers(maintainers MaintainershipData, idx int) (missing, extra []string) {
|
||||
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
||||
|
||||
for _, pr := range rs.PRs {
|
||||
reviewers := []string{}
|
||||
// remove reviewers that were already requested and are not stale
|
||||
prjMaintainers := maintainers.ListProjectMaintainers(nil)
|
||||
LogDebug("project maintainers:", prjMaintainers)
|
||||
|
||||
if rs.IsPrjGitPR(pr.PR) {
|
||||
reviewers = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
|
||||
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
|
||||
if len(rs.PRs) == 1 {
|
||||
reviewers = slices.Concat(reviewers, maintainers.ListProjectMaintainers(nil))
|
||||
}
|
||||
pr := rs.PRs[idx]
|
||||
if rs.IsPrjGitPR(pr.PR) {
|
||||
missing = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
|
||||
if rs.HasAutoStaging {
|
||||
missing = append(missing, Bot_BuildReview)
|
||||
}
|
||||
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
|
||||
// only need project maintainer reviews if:
|
||||
// * not created by a bot and has other PRs, or
|
||||
// * not created by maintainer
|
||||
noReviewPRCreators := []string{}
|
||||
if !rs.Config.ReviewRequired {
|
||||
noReviewPRCreators = prjMaintainers
|
||||
}
|
||||
if len(rs.PRs) > 1 {
|
||||
noReviewPRCreators = append(noReviewPRCreators, rs.BotUser)
|
||||
}
|
||||
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(prjMaintainers...) {
|
||||
LogDebug("Project already reviewed by a project maintainer, remove rest")
|
||||
// do not remove reviewers if they are also maintainers
|
||||
prjMaintainers = slices.DeleteFunc(prjMaintainers, func(m string) bool { return slices.Contains(missing, m) })
|
||||
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
|
||||
} else {
|
||||
pkg := pr.PR.Base.Repo.Name
|
||||
reviewers = slices.Concat(configReviewers.Pkg, maintainers.ListProjectMaintainers(nil), maintainers.ListPackageMaintainers(pkg, nil), configReviewers.PkgOptional)
|
||||
}
|
||||
|
||||
slices.Sort(reviewers)
|
||||
reviewers = slices.Compact(reviewers)
|
||||
|
||||
// submitters do not need to review their own work
|
||||
if idx := slices.Index(reviewers, pr.PR.User.UserName); idx != -1 {
|
||||
reviewers = slices.Delete(reviewers, idx, idx+1)
|
||||
}
|
||||
|
||||
LogDebug("PR: ", pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||
LogDebug("reviewers for PR:", reviewers)
|
||||
|
||||
// remove reviewers that were already requested and are not stale
|
||||
reviews, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||
if err != nil {
|
||||
LogError("Error fetching reviews:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(reviewers); {
|
||||
user := reviewers[idx]
|
||||
if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) {
|
||||
reviewers = slices.Delete(reviewers, idx, idx+1)
|
||||
LogDebug("removing reviewer:", user)
|
||||
// if bot not created PrjGit or prj maintainer, we need to add project reviewers here
|
||||
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) {
|
||||
LogDebug("No need for project maintainers")
|
||||
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
|
||||
} else {
|
||||
idx++
|
||||
LogDebug("Adding prjMaintainers to PrjGit")
|
||||
missing = append(missing, prjMaintainers...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pkg := pr.PR.Base.Repo.Name
|
||||
pkgMaintainers := maintainers.ListPackageMaintainers(pkg, nil)
|
||||
Maintainers := slices.Concat(prjMaintainers, pkgMaintainers)
|
||||
noReviewPkgPRCreators := []string{}
|
||||
if !rs.Config.ReviewRequired {
|
||||
noReviewPkgPRCreators = pkgMaintainers
|
||||
}
|
||||
|
||||
// get maintainers associated with the PR too
|
||||
if len(reviewers) > 0 {
|
||||
LogDebug("Requesting reviews from:", reviewers)
|
||||
LogDebug("packakge maintainers:", Maintainers)
|
||||
|
||||
missing = slices.Concat(configReviewers.Pkg, configReviewers.PkgOptional)
|
||||
if slices.Contains(noReviewPkgPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(Maintainers...) {
|
||||
// submitter is maintainer or already reviewed
|
||||
LogDebug("Package reviewed by maintainer (or subitter is maintainer), remove the rest of them")
|
||||
// do not remove reviewers if they are also maintainers
|
||||
Maintainers = slices.DeleteFunc(Maintainers, func(m string) bool { return slices.Contains(missing, m) })
|
||||
extra = slices.Concat(Maintainers, []string{rs.BotUser})
|
||||
} else {
|
||||
// maintainer review is missing
|
||||
LogDebug("Adding package maintainers to package git")
|
||||
missing = append(missing, pkgMaintainers...)
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(missing)
|
||||
missing = slices.Compact(missing)
|
||||
|
||||
slices.Sort(extra)
|
||||
extra = slices.Compact(extra)
|
||||
|
||||
// submitters cannot review their own work
|
||||
if idx := slices.Index(missing, pr.PR.User.UserName); idx != -1 {
|
||||
missing = slices.Delete(missing, idx, idx+1)
|
||||
}
|
||||
|
||||
LogDebug("PR: ", PRtoString(pr.PR))
|
||||
LogDebug(" preliminary add reviewers for PR:", missing)
|
||||
LogDebug(" preliminary rm reviewers for PR:", extra)
|
||||
|
||||
// remove missing reviewers that are already done or already pending
|
||||
for idx := 0; idx < len(missing); {
|
||||
user := missing[idx]
|
||||
if pr.Reviews.HasPendingReviewBy(user) || pr.Reviews.IsReviewedBy(user) {
|
||||
missing = slices.Delete(missing, idx, idx+1)
|
||||
LogDebug(" removing done/pending reviewer:", user)
|
||||
} else {
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
// remove extra reviews that are actually only pending, and only pending by us
|
||||
for idx := 0; idx < len(extra); {
|
||||
user := extra[idx]
|
||||
rr := pr.Reviews.FindReviewRequester(user)
|
||||
if rr != nil && rr.User.UserName == rs.BotUser && pr.Reviews.HasPendingReviewBy(user) {
|
||||
// good to remove this review
|
||||
idx++
|
||||
} else {
|
||||
// this review should not be considered as extra by us
|
||||
LogDebug(" - cannot find? to remove", user)
|
||||
if rr != nil {
|
||||
LogDebug(" ", rr.User.UserName, "vs.", rs.BotUser, pr.Reviews.HasPendingReviewBy(user))
|
||||
}
|
||||
extra = slices.Delete(extra, idx, idx+1)
|
||||
}
|
||||
}
|
||||
|
||||
LogDebug(" add reviewers for PR:", missing)
|
||||
LogDebug(" rm reviewers for PR:", extra)
|
||||
|
||||
return missing, extra
|
||||
}
|
||||
|
||||
func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequesterAndUnrequester, maintainers MaintainershipData) error {
|
||||
for idx, pr := range rs.PRs {
|
||||
missingReviewers, extraReviewers := rs.FindMissingAndExtraReviewers(maintainers, idx)
|
||||
|
||||
if len(missingReviewers) > 0 {
|
||||
LogDebug(" Requesting reviews from:", missingReviewers)
|
||||
if !IsDryRun {
|
||||
for _, r := range reviewers {
|
||||
for _, r := range missingReviewers {
|
||||
if _, err := gitea.RequestReviews(pr.PR, r); err != nil {
|
||||
LogError("Cannot create reviews on", fmt.Sprintf("%s/%s!%d for [%s]", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index, strings.Join(reviewers, ", ")), err)
|
||||
LogError("Cannot create reviews on", PRtoString(pr.PR), "for user:", r, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(extraReviewers) > 0 {
|
||||
LogDebug(" UnRequesting reviews from:", extraReviewers)
|
||||
if !IsDryRun {
|
||||
for _, r := range extraReviewers {
|
||||
org, repo, idx := pr.PRComponents()
|
||||
if err := gitea.UnrequestReview(org, repo, idx, r); err != nil {
|
||||
LogError("Cannot unrequest reviews on", PRtoString(pr.PR), "for user:", r, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,11 +458,12 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
||||
if err == nil && prjgit != nil {
|
||||
reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers(groups))
|
||||
LogDebug("Fetching reviews for", prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
||||
r, err := FetchGiteaReviews(gitea, reviewers, prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
||||
r, err := FetchGiteaReviews(gitea, prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
||||
if err != nil {
|
||||
LogError("Cannot fetch gita reaviews for PR:", err)
|
||||
return false
|
||||
}
|
||||
r.RequestedReviewers = reviewers
|
||||
prjgit.Reviews = r
|
||||
if prjgit.Reviews.IsManualMergeOK() {
|
||||
is_manually_reviewed_ok = true
|
||||
@@ -339,11 +479,12 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
||||
pkg := pr.PR.Base.Repo.Name
|
||||
reviewers := slices.Concat(configReviewers.Pkg, maintainers.ListPackageMaintainers(pkg, groups))
|
||||
LogDebug("Fetching reviews for", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||
r, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||
r, err := FetchGiteaReviews(gitea, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||
if err != nil {
|
||||
LogError("Cannot fetch gita reaviews for PR:", err)
|
||||
return false
|
||||
}
|
||||
r.RequestedReviewers = reviewers
|
||||
pr.Reviews = r
|
||||
if !pr.Reviews.IsManualMergeOK() {
|
||||
LogInfo("Not approved manual merge. PR:", pr.PR.URL)
|
||||
@@ -365,6 +506,9 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
||||
var pkg string
|
||||
if rs.IsPrjGitPR(pr.PR) {
|
||||
reviewers = configReviewers.Prj
|
||||
if rs.HasAutoStaging {
|
||||
reviewers = append(reviewers, Bot_BuildReview)
|
||||
}
|
||||
pkg = ""
|
||||
} else {
|
||||
reviewers = configReviewers.Pkg
|
||||
@@ -376,11 +520,12 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
||||
return false
|
||||
}
|
||||
|
||||
r, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||
r, err := FetchGiteaReviews(gitea, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||
if err != nil {
|
||||
LogError("Cannot fetch gitea reaviews for PR:", err)
|
||||
return false
|
||||
}
|
||||
r.RequestedReviewers = reviewers
|
||||
|
||||
is_manually_reviewed_ok = r.IsApproved()
|
||||
LogDebug("PR to", pr.PR.Base.Repo.Name, "reviewed?", is_manually_reviewed_ok)
|
||||
@@ -393,7 +538,7 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
||||
|
||||
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review {
|
||||
// Do not expand groups here, as the group-review-bot will ACK if group has reviewed.
|
||||
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.reviews, pr.PR.User.UserName, nil); !is_manually_reviewed_ok {
|
||||
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.Reviews, pr.PR.User.UserName, nil); !is_manually_reviewed_ok {
|
||||
LogDebug(" not approved?", pkg)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package common_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -15,22 +14,23 @@ import (
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
)
|
||||
/*
|
||||
func TestCockpit(t *testing.T) {
|
||||
common.SetLoggingLevel(common.LogLevelDebug)
|
||||
gitea := common.AllocateGiteaTransport("https://src.opensuse.org")
|
||||
tl, err := gitea.GetTimeline("cockpit", "cockpit", 29)
|
||||
if err != nil {
|
||||
t.Fatal("Fail to timeline", err)
|
||||
}
|
||||
t.Log(tl)
|
||||
r, err := common.FetchGiteaReviews(gitea, []string{}, "cockpit", "cockpit", 29)
|
||||
if err != nil {
|
||||
t.Fatal("Error:", err)
|
||||
}
|
||||
|
||||
t.Error(r)
|
||||
}
|
||||
/*
|
||||
func TestCockpit(t *testing.T) {
|
||||
common.SetLoggingLevel(common.LogLevelDebug)
|
||||
gitea := common.AllocateGiteaTransport("https://src.opensuse.org")
|
||||
tl, err := gitea.GetTimeline("cockpit", "cockpit", 29)
|
||||
if err != nil {
|
||||
t.Fatal("Fail to timeline", err)
|
||||
}
|
||||
t.Log(tl)
|
||||
r, err := common.FetchGiteaReviews(gitea, []string{}, "cockpit", "cockpit", 29)
|
||||
if err != nil {
|
||||
t.Fatal("Error:", err)
|
||||
}
|
||||
|
||||
t.Error(r)
|
||||
}
|
||||
*/
|
||||
func reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
|
||||
timeline := make([]*models.TimelineComment, len(reviews))
|
||||
@@ -75,7 +75,7 @@ func TestPR(t *testing.T) {
|
||||
consistentSet bool
|
||||
prjGitPRIndex int
|
||||
|
||||
reviewSetFetcher func(*mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error)
|
||||
reviewSetFetcher func(*mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error)
|
||||
}{
|
||||
{
|
||||
name: "Error fetching PullRequest",
|
||||
@@ -147,7 +147,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: true,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
|
||||
},
|
||||
},
|
||||
@@ -179,7 +179,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: false,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
|
||||
},
|
||||
},
|
||||
@@ -207,7 +207,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: false,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -241,7 +241,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: true,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -275,7 +275,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: true,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -311,7 +311,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: false,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -346,7 +346,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: true,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -388,7 +388,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: true,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -430,7 +430,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: false,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -473,7 +473,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: false,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||
Branch: "branch",
|
||||
@@ -500,7 +500,7 @@ func TestPR(t *testing.T) {
|
||||
prjGitPRIndex: 0,
|
||||
consistentSet: true,
|
||||
reviewed: true,
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
||||
config := common.AutogitConfig{
|
||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2", "~*bot"},
|
||||
Branch: "branch",
|
||||
@@ -515,7 +515,7 @@ func TestPR(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := gomock.NewController(t)
|
||||
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||
pr_mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
review_mock := mock_common.NewMockGiteaPRChecker(ctl)
|
||||
// reviewer_mock := mock_common.NewMockGiteaReviewRequester(ctl)
|
||||
|
||||
@@ -619,283 +619,566 @@ func TestPR(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRAssignReviewers(t *testing.T) {
|
||||
t.Skip("FAIL: unexpected calls, missing calls")
|
||||
func TestFindMissingAndExtraReviewers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.AutogitConfig
|
||||
reviewers []struct {
|
||||
org, repo string
|
||||
num int64
|
||||
reviewer string
|
||||
}
|
||||
|
||||
pkgReviews []*models.PullReview
|
||||
pkgTimeline []*models.TimelineComment
|
||||
prjReviews []*models.PullReview
|
||||
prjTimeline []*models.TimelineComment
|
||||
prset *common.PRSet
|
||||
maintainers common.MaintainershipData
|
||||
|
||||
expectedReviewerCall [2][]string
|
||||
noAutoStaging bool
|
||||
|
||||
expected_missing_reviewers [][]string
|
||||
expected_extra_reviewers [][]string
|
||||
}{
|
||||
{
|
||||
name: "No reviewers",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{},
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{},
|
||||
},
|
||||
},
|
||||
expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
||||
},
|
||||
{
|
||||
name: "One project reviewer only",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1"},
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1"},
|
||||
},
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
||||
|
||||
expected_missing_reviewers: [][]string{
|
||||
[]string{},
|
||||
[]string{"autogits_obs_staging_bot", "user1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "One project reviewer only and no auto staging",
|
||||
noAutoStaging: true,
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1"},
|
||||
},
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
||||
|
||||
expected_missing_reviewers: [][]string{
|
||||
nil,
|
||||
{"user1"},
|
||||
},
|
||||
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
|
||||
},
|
||||
{
|
||||
name: "One project reviewer and one pkg reviewer only",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "user2"},
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "user2"},
|
||||
},
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
||||
|
||||
expected_missing_reviewers: [][]string{
|
||||
[]string{"user2"},
|
||||
[]string{"autogits_obs_staging_bot", "user1"},
|
||||
},
|
||||
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"user2", "prjmaintainer", "pkgmaintainer"}},
|
||||
},
|
||||
{
|
||||
name: "No need to get reviews of submitter",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter"},
|
||||
name: "No need to get reviews of submitter reviewer",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "submitter"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "m1"}}},
|
||||
RequestedReviewers: []string{"m1"},
|
||||
FullTimeline: []*models.TimelineComment{
|
||||
{User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "m1"}, Type: common.TimelineCommentType_ReviewRequested},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter"},
|
||||
},
|
||||
BotUser: "bot",
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"m1", "submitter"}}},
|
||||
|
||||
expected_missing_reviewers: [][]string{
|
||||
nil,
|
||||
{"autogits_obs_staging_bot", "user1"},
|
||||
},
|
||||
expected_extra_reviewers: [][]string{
|
||||
{"m1"},
|
||||
},
|
||||
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
|
||||
},
|
||||
{
|
||||
name: "Reviews are done",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "user2"},
|
||||
},
|
||||
pkgReviews: []*models.PullReview{
|
||||
{
|
||||
State: common.ReviewStateApproved,
|
||||
User: &models.User{UserName: "user2"},
|
||||
name: "No need to get reviews of submitter maintainer",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "submitter"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "foo"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: common.ReviewStateApproved,
|
||||
User: &models.User{UserName: "pkgmaintainer"},
|
||||
},
|
||||
{
|
||||
State: common.ReviewStatePending,
|
||||
User: &models.User{UserName: "prjmaintainer"},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter"},
|
||||
},
|
||||
},
|
||||
prjReviews: []*models.PullReview{
|
||||
{
|
||||
State: common.ReviewStateRequestChanges,
|
||||
User: &models.User{UserName: "user1"},
|
||||
},
|
||||
{
|
||||
State: common.ReviewStateRequestReview,
|
||||
User: &models.User{UserName: "autogits_obs_staging_bot"},
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter"}}},
|
||||
|
||||
expected_missing_reviewers: [][]string{
|
||||
[]string{},
|
||||
[]string{"autogits_obs_staging_bot", "user1"},
|
||||
},
|
||||
expectedReviewerCall: [2][]string{},
|
||||
},
|
||||
{
|
||||
name: "Stale review is not done, re-request it",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "org/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "user2"},
|
||||
name: "Add reviewer if also maintainer where review by maintainer is not needed",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "submitter"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "bot"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter", "*reviewer"},
|
||||
},
|
||||
BotUser: "bot",
|
||||
},
|
||||
pkgReviews: []*models.PullReview{
|
||||
{
|
||||
State: common.ReviewStateApproved,
|
||||
User: &models.User{UserName: "user2"},
|
||||
},
|
||||
{
|
||||
State: common.ReviewStatePending,
|
||||
User: &models.User{UserName: "prjmaintainer"},
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter", "reviewer"}, "": []string{"reviewer"}}},
|
||||
|
||||
expected_missing_reviewers: [][]string{
|
||||
[]string{"reviewer"},
|
||||
[]string{"autogits_obs_staging_bot", "reviewer", "user1"},
|
||||
},
|
||||
prjReviews: []*models.PullReview{
|
||||
{
|
||||
State: common.ReviewStateRequestChanges,
|
||||
User: &models.User{UserName: "user1"},
|
||||
Stale: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Dont remove reviewer if also maintainer",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "submitter"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
|
||||
RequestedReviewers: []string{"reviewer"},
|
||||
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "bot"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
|
||||
RequestedReviewers: []string{"reviewer"},
|
||||
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: common.ReviewStateRequestReview,
|
||||
Stale: true,
|
||||
User: &models.User{UserName: "autogits_obs_staging_bot"},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter", "*reviewer"},
|
||||
},
|
||||
BotUser: "bot",
|
||||
},
|
||||
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer"}},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter", "reviewer"}, "": []string{"reviewer"}}},
|
||||
|
||||
expected_missing_reviewers: [][]string{
|
||||
[]string{},
|
||||
[]string{"autogits_obs_staging_bot", "user1"},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Extra project reviewer on the package",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "submitter"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{
|
||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}},
|
||||
},
|
||||
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer"},
|
||||
FullTimeline: []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user2"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "bot"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{
|
||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
|
||||
},
|
||||
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot"},
|
||||
FullTimeline: []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user1"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter"},
|
||||
},
|
||||
BotUser: "bot",
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer"}, "": {"prjmaintainer"}}},
|
||||
|
||||
expected_missing_reviewers: [][]string{},
|
||||
expected_extra_reviewers: [][]string{{"prjmaintainer"}},
|
||||
},
|
||||
{
|
||||
name: "Stale optional review is not done, re-request it",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "user2", "~bot"},
|
||||
name: "Extra project reviewers on the package and project",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "submitter"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{
|
||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "pkgm1"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "pkgm2"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj1"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "someother"}},
|
||||
},
|
||||
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "pkgm2", "someother", "prj1", "prj2"},
|
||||
FullTimeline: []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj2"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm1"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "bot"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{
|
||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj1"}},
|
||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
|
||||
},
|
||||
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1", "prj2"},
|
||||
FullTimeline: []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter"},
|
||||
},
|
||||
BotUser: "bot",
|
||||
},
|
||||
pkgReviews: []*models.PullReview{
|
||||
{
|
||||
State: common.ReviewStateApproved,
|
||||
User: &models.User{UserName: "bot"},
|
||||
Stale: true,
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer", "pkgm1", "pkgm2"}, "": {"prjmaintainer", "prj1", "prj2"}}},
|
||||
|
||||
expected_missing_reviewers: [][]string{},
|
||||
expected_extra_reviewers: [][]string{{"pkgm1", "pkgm2", "prj1", "prj2", "prjmaintainer"}, {"prj1", "prj2"}},
|
||||
},
|
||||
{
|
||||
name: "No extra project reviewers on the package and project (all pending)",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "submitter"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{
|
||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "pkgmaintainer"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prjmaintainer"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "pkgm1"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "someother"}},
|
||||
},
|
||||
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "someother", "prj1"},
|
||||
FullTimeline: []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm1"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "!bot"}, Assignee: &models.User{UserName: "someother"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "bot"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prj"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{
|
||||
Reviews: []*models.PullReview{
|
||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
|
||||
},
|
||||
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1"},
|
||||
FullTimeline: []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "!bot"}, Assignee: &models.User{UserName: "user1"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: common.ReviewStateApproved,
|
||||
User: &models.User{UserName: "user2"},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prj/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{"-user1", "submitter"},
|
||||
},
|
||||
{
|
||||
State: common.ReviewStatePending,
|
||||
User: &models.User{UserName: "prjmaintainer"},
|
||||
BotUser: "bot",
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer", "pkgm1", "pkgm2"}, "": {"prjmaintainer", "prj1", "prj2"}}},
|
||||
|
||||
expected_missing_reviewers: [][]string{{"pkgm2", "prj2"}},
|
||||
expected_extra_reviewers: [][]string{{}, {"prj1"}},
|
||||
},
|
||||
{
|
||||
name: "Package maintainer submitter, AlwaysRequireReview=false",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "pkgmaintainer"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{},
|
||||
ReviewRequired: false,
|
||||
},
|
||||
},
|
||||
prjReviews: []*models.PullReview{
|
||||
{
|
||||
State: common.ReviewStateRequestChanges,
|
||||
User: &models.User{UserName: "user1"},
|
||||
Stale: true,
|
||||
},
|
||||
{
|
||||
State: common.ReviewStateRequestReview,
|
||||
Stale: true,
|
||||
User: &models.User{UserName: "autogits_obs_staging_bot"},
|
||||
maintainers: &common.MaintainershipMap{
|
||||
Data: map[string][]string{
|
||||
"pkg": {"pkgmaintainer", "pkgm1"},
|
||||
},
|
||||
},
|
||||
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer", "bot"}},
|
||||
noAutoStaging: true,
|
||||
expected_missing_reviewers: [][]string{
|
||||
{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Package maintainer submitter, AlwaysRequireReview=true",
|
||||
prset: &common.PRSet{
|
||||
PRs: []*common.PRInfo{
|
||||
{
|
||||
PR: &models.PullRequest{
|
||||
User: &models.User{UserName: "pkgmaintainer"},
|
||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
||||
},
|
||||
Reviews: &common.PRReviews{},
|
||||
},
|
||||
},
|
||||
Config: &common.AutogitConfig{
|
||||
GitProjectName: "prg/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{},
|
||||
ReviewRequired: true,
|
||||
},
|
||||
},
|
||||
maintainers: &common.MaintainershipMap{
|
||||
Data: map[string][]string{
|
||||
"pkg": {"pkgmaintainer", "pkgm1"},
|
||||
},
|
||||
},
|
||||
noAutoStaging: true,
|
||||
expected_missing_reviewers: [][]string{
|
||||
{"pkgm1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := gomock.NewController(t)
|
||||
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
|
||||
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
|
||||
test.prset.HasAutoStaging = !test.noAutoStaging
|
||||
for idx, pr := range test.prset.PRs {
|
||||
missing, extra := test.prset.FindMissingAndExtraReviewers(test.maintainers, idx)
|
||||
|
||||
if test.pkgTimeline == nil {
|
||||
test.pkgTimeline = reviewsToTimeline(test.pkgReviews)
|
||||
}
|
||||
if test.prjTimeline == nil {
|
||||
test.prjTimeline = reviewsToTimeline(test.prjReviews)
|
||||
}
|
||||
|
||||
pr_mock.EXPECT().GetPullRequest("other", "pkgrepo", int64(1)).Return(&models.PullRequest{
|
||||
Body: "Some description is here",
|
||||
User: &models.User{UserName: "submitter"},
|
||||
RequestedReviewers: []*models.User{},
|
||||
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "pkgrepo", Owner: &models.User{UserName: "other"}}},
|
||||
Head: &models.PRBranchInfo{},
|
||||
Index: 1,
|
||||
}, nil)
|
||||
review_mock.EXPECT().GetPullRequestReviews("other", "pkgrepo", int64(1)).Return(test.pkgReviews, nil)
|
||||
review_mock.EXPECT().GetTimeline("other", "pkgrepo", int64(1)).Return(test.pkgTimeline, nil)
|
||||
pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{
|
||||
Body: fmt.Sprintf(common.PrPattern, "other", "pkgrepo", 1),
|
||||
User: &models.User{UserName: "bot1"},
|
||||
RequestedReviewers: []*models.User{{UserName: "main_reviewer"}},
|
||||
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
|
||||
Head: &models.PRBranchInfo{},
|
||||
Index: 42,
|
||||
}, nil)
|
||||
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(42)).Return(test.prjReviews, nil)
|
||||
review_mock.EXPECT().GetTimeline("org", "repo", int64(42)).Return(test.prjTimeline, nil)
|
||||
|
||||
maintainership_mock.EXPECT().ListProjectMaintainers(gomock.Any()).Return([]string{"prjmaintainer"}).AnyTimes()
|
||||
maintainership_mock.EXPECT().ListPackageMaintainers("pkgrepo", gomock.Any()).Return([]string{"pkgmaintainer"}).AnyTimes()
|
||||
|
||||
prs, _ := common.FetchPRSet("test", pr_mock, "other", "pkgrepo", int64(1), &test.config)
|
||||
if len(prs.PRs) != 2 {
|
||||
t.Fatal("PRs not fetched")
|
||||
}
|
||||
for _, pr := range prs.PRs {
|
||||
r := test.expectedReviewerCall[0]
|
||||
if !prs.IsPrjGitPR(pr.PR) {
|
||||
r = test.expectedReviewerCall[1]
|
||||
// avoid nil dereference below, by adding empty array elements
|
||||
if idx >= len(test.expected_missing_reviewers) {
|
||||
test.expected_missing_reviewers = append(test.expected_missing_reviewers, nil)
|
||||
}
|
||||
slices.Sort(r)
|
||||
for _, reviewer := range r {
|
||||
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
|
||||
if idx >= len(test.expected_extra_reviewers) {
|
||||
test.expected_extra_reviewers = append(test.expected_extra_reviewers, nil)
|
||||
}
|
||||
|
||||
slices.Sort(test.expected_extra_reviewers[idx])
|
||||
slices.Sort(test.expected_missing_reviewers[idx])
|
||||
if slices.Compare(missing, test.expected_missing_reviewers[idx]) != 0 {
|
||||
t.Error("Expected missing reviewers for", common.PRtoString(pr.PR), ":", test.expected_missing_reviewers[idx], "but have:", missing)
|
||||
}
|
||||
|
||||
if slices.Compare(extra, test.expected_extra_reviewers[idx]) != 0 {
|
||||
t.Error("Expected reviewers to remove for", common.PRtoString(pr.PR), ":", test.expected_extra_reviewers[idx], "but have:", extra)
|
||||
}
|
||||
}
|
||||
prs.AssignReviewers(review_mock, maintainership_mock)
|
||||
})
|
||||
}
|
||||
|
||||
prjgit_tests := []struct {
|
||||
name string
|
||||
config common.AutogitConfig
|
||||
reviewers []struct {
|
||||
org, repo string
|
||||
num int64
|
||||
reviewer string
|
||||
}
|
||||
|
||||
prjReviews []*models.PullReview
|
||||
|
||||
expectedReviewerCall [2][]string
|
||||
}{
|
||||
{
|
||||
name: "PrjMaintainers in prjgit review when not part of pkg set",
|
||||
config: common.AutogitConfig{
|
||||
GitProjectName: "org/repo#main",
|
||||
Organization: "org",
|
||||
Branch: "main",
|
||||
Reviewers: []string{},
|
||||
},
|
||||
expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot", "prjmaintainer"}},
|
||||
},
|
||||
}
|
||||
for _, test := range prjgit_tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := gomock.NewController(t)
|
||||
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
|
||||
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
|
||||
|
||||
pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{
|
||||
Body: "Some description is here",
|
||||
User: &models.User{UserName: "submitter"},
|
||||
RequestedReviewers: []*models.User{},
|
||||
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
|
||||
Head: &models.PRBranchInfo{},
|
||||
Index: 1,
|
||||
}, nil)
|
||||
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(1)).Return(test.prjReviews, nil)
|
||||
review_mock.EXPECT().GetTimeline("org", "repo", int64(1)).Return(nil, nil)
|
||||
|
||||
maintainership_mock.EXPECT().ListProjectMaintainers(gomock.Any()).Return([]string{"prjmaintainer"}).AnyTimes()
|
||||
|
||||
prs, _ := common.FetchPRSet("test", pr_mock, "org", "repo", int64(1), &test.config)
|
||||
if len(prs.PRs) != 1 {
|
||||
t.Fatal("PRs not fetched")
|
||||
}
|
||||
for _, pr := range prs.PRs {
|
||||
r := test.expectedReviewerCall[0]
|
||||
if !prs.IsPrjGitPR(pr.PR) {
|
||||
t.Fatal("only prjgit pr here")
|
||||
}
|
||||
for _, reviewer := range r {
|
||||
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
|
||||
}
|
||||
}
|
||||
prs.AssignReviewers(review_mock, maintainership_mock)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -978,7 +1261,7 @@ func TestPRMerge(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := gomock.NewController(t)
|
||||
mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||
mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
|
||||
|
||||
reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
|
||||
@@ -1037,7 +1320,7 @@ func TestPRChanges(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := gomock.NewController(t)
|
||||
mock_fetcher := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||
mock_fetcher := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
mock_fetcher.EXPECT().GetPullRequest("org", "prjgit", int64(42)).Return(test.PrjPRs, nil)
|
||||
for _, pr := range test.PRs {
|
||||
mock_fetcher.EXPECT().GetPullRequest(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index).Return(pr, nil)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
type Reviewers struct {
|
||||
Prj []string
|
||||
Pkg []string
|
||||
@@ -36,10 +32,5 @@ func ParseReviewers(input []string) *Reviewers {
|
||||
*pkg = append(*pkg, reviewer)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(r.Prj, Bot_BuildReview) {
|
||||
r.Prj = append(r.Prj, Bot_BuildReview)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ func TestReviewers(t *testing.T) {
|
||||
name: "project and package reviewers",
|
||||
input: []string{"1", "2", "3", "*5", "+6", "-7"},
|
||||
|
||||
prj: []string{"5", "7", common.Bot_BuildReview},
|
||||
prj: []string{"5", "7"},
|
||||
pkg: []string{"1", "2", "3", "5", "6"},
|
||||
},
|
||||
{
|
||||
name: "optional project and package reviewers",
|
||||
input: []string{"~1", "2", "3", "~*5", "+6", "-7"},
|
||||
|
||||
prj: []string{"7", common.Bot_BuildReview},
|
||||
prj: []string{"7"},
|
||||
pkg: []string{"2", "3", "6"},
|
||||
prj_optional: []string{"5"},
|
||||
pkg_optional: []string{"1", "5"},
|
||||
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
)
|
||||
|
||||
type PRReviews struct {
|
||||
reviews []*models.PullReview
|
||||
reviewers []string
|
||||
comments []*models.TimelineComment
|
||||
Reviews []*models.PullReview
|
||||
RequestedReviewers []string
|
||||
Comments []*models.TimelineComment
|
||||
|
||||
FullTimeline []*models.TimelineComment
|
||||
}
|
||||
|
||||
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, reviewers []string, org, repo string, no int64) (*PRReviews, error) {
|
||||
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64) (*PRReviews, error) {
|
||||
timeline, err := rf.GetTimeline(org, repo, no)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -25,10 +27,14 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, reviewers []string, org, r
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reviews := make([]*models.PullReview, 0, len(reviewers))
|
||||
reviews := make([]*models.PullReview, 0, 10)
|
||||
needNewReviews := []string{}
|
||||
var comments []*models.TimelineComment
|
||||
|
||||
alreadyHaveUserReview := func(user string) bool {
|
||||
if slices.Contains(needNewReviews, user) {
|
||||
return true
|
||||
}
|
||||
for _, r := range reviews {
|
||||
if r.User != nil && r.User.UserName == user {
|
||||
return true
|
||||
@@ -37,32 +43,40 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, reviewers []string, org, r
|
||||
return false
|
||||
}
|
||||
|
||||
LogDebug("FetchingGiteaReviews for", org, repo, no)
|
||||
LogDebug("Number of reviews:", len(rawReviews))
|
||||
LogDebug("Number of items in timeline:", len(timeline))
|
||||
|
||||
cutOffIdx := len(timeline)
|
||||
for idx, item := range timeline {
|
||||
if item.Type == TimelineCommentType_Review {
|
||||
if item.Type == TimelineCommentType_Review || item.Type == TimelineCommentType_ReviewRequested {
|
||||
for _, r := range rawReviews {
|
||||
if r.ID == item.ReviewID {
|
||||
if !alreadyHaveUserReview(r.User.UserName) {
|
||||
reviews = append(reviews, r)
|
||||
if item.Type == TimelineCommentType_Review && idx > cutOffIdx {
|
||||
needNewReviews = append(needNewReviews, r.User.UserName)
|
||||
} else {
|
||||
reviews = append(reviews, r)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if item.Type == TimelineCommentType_Comment {
|
||||
} else if item.Type == TimelineCommentType_Comment && cutOffIdx > idx {
|
||||
comments = append(comments, item)
|
||||
} else if item.Type == TimelineCommentType_PushPull {
|
||||
LogDebug("cut-off", item.Created)
|
||||
timeline = timeline[0:idx]
|
||||
break
|
||||
} else if item.Type == TimelineCommentType_PushPull && cutOffIdx == len(timeline) {
|
||||
LogDebug("cut-off", item.Created, "@", idx)
|
||||
cutOffIdx = idx
|
||||
} else {
|
||||
LogDebug("Unhandled timeline type:", item.Type)
|
||||
}
|
||||
}
|
||||
LogDebug("num comments:", len(comments), "reviews:", len(reviews), len(timeline))
|
||||
LogDebug("num comments:", len(comments), "timeline:", len(reviews))
|
||||
|
||||
return &PRReviews{
|
||||
reviews: reviews,
|
||||
reviewers: reviewers,
|
||||
comments: comments,
|
||||
Reviews: reviews,
|
||||
Comments: comments,
|
||||
FullTimeline: timeline,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -81,23 +95,27 @@ func bodyCommandManualMergeOK(body string) bool {
|
||||
}
|
||||
|
||||
func (r *PRReviews) IsManualMergeOK() bool {
|
||||
for _, c := range r.comments {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range r.Comments {
|
||||
if c.Updated != c.Created {
|
||||
continue
|
||||
}
|
||||
LogDebug("comment:", c.User.UserName, c.Body)
|
||||
if slices.Contains(r.reviewers, c.User.UserName) {
|
||||
if slices.Contains(r.RequestedReviewers, c.User.UserName) {
|
||||
if bodyCommandManualMergeOK(c.Body) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range r.reviews {
|
||||
for _, c := range r.Reviews {
|
||||
if c.Updated != c.Submitted {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(r.reviewers, c.User.UserName) {
|
||||
if slices.Contains(r.RequestedReviewers, c.User.UserName) {
|
||||
if bodyCommandManualMergeOK(c.Body) {
|
||||
return true
|
||||
}
|
||||
@@ -108,11 +126,14 @@ func (r *PRReviews) IsManualMergeOK() bool {
|
||||
}
|
||||
|
||||
func (r *PRReviews) IsApproved() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
goodReview := true
|
||||
|
||||
for _, reviewer := range r.reviewers {
|
||||
for _, reviewer := range r.RequestedReviewers {
|
||||
goodReview = false
|
||||
for _, review := range r.reviews {
|
||||
for _, review := range r.Reviews {
|
||||
if review.User.UserName == reviewer && review.State == ReviewStateApproved && !review.Stale && !review.Dismissed {
|
||||
LogDebug(" -- found review: ", review.User.UserName)
|
||||
goodReview = true
|
||||
@@ -130,7 +151,11 @@ func (r *PRReviews) IsApproved() bool {
|
||||
|
||||
func (r *PRReviews) MissingReviews() []string {
|
||||
missing := []string{}
|
||||
for _, reviewer := range r.reviewers {
|
||||
if r == nil {
|
||||
return missing
|
||||
}
|
||||
|
||||
for _, reviewer := range r.RequestedReviewers {
|
||||
if !r.IsReviewedBy(reviewer) {
|
||||
missing = append(missing, reviewer)
|
||||
}
|
||||
@@ -138,45 +163,64 @@ func (r *PRReviews) MissingReviews() []string {
|
||||
return missing
|
||||
}
|
||||
|
||||
func (r *PRReviews) HasPendingReviewBy(reviewer string) bool {
|
||||
if !slices.Contains(r.reviewers, reviewer) {
|
||||
return false
|
||||
func (r *PRReviews) FindReviewRequester(reviewer string) *models.TimelineComment {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
isPending := false
|
||||
for _, r := range r.reviews {
|
||||
if r.User.UserName == reviewer && !r.Stale {
|
||||
switch r.State {
|
||||
case ReviewStateApproved:
|
||||
fallthrough
|
||||
case ReviewStateRequestChanges:
|
||||
return false
|
||||
case ReviewStateRequestReview:
|
||||
fallthrough
|
||||
case ReviewStatePending:
|
||||
isPending = true
|
||||
}
|
||||
for _, r := range r.FullTimeline {
|
||||
if r.Type == TimelineCommentType_ReviewRequested && r.Assignee.UserName == reviewer {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return isPending
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PRReviews) IsReviewedBy(reviewer string) bool {
|
||||
if !slices.Contains(r.reviewers, reviewer) {
|
||||
func (r *PRReviews) HasPendingReviewBy(reviewer string) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range r.reviews {
|
||||
if r.User.UserName == reviewer && !r.Stale {
|
||||
for _, r := range r.Reviews {
|
||||
if r.User.UserName == reviewer {
|
||||
switch r.State {
|
||||
case ReviewStateApproved:
|
||||
return true
|
||||
case ReviewStateRequestChanges:
|
||||
case ReviewStateRequestReview, ReviewStatePending:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *PRReviews) IsReviewedBy(reviewer string) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range r.Reviews {
|
||||
if r.User.UserName == reviewer && !r.Stale {
|
||||
switch r.State {
|
||||
case ReviewStateApproved, ReviewStateRequestChanges:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *PRReviews) IsReviewedByOneOf(reviewers ...string) bool {
|
||||
for _, reviewer := range reviewers {
|
||||
if r.IsReviewedBy(reviewer) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -62,11 +62,23 @@ func TestReviews(t *testing.T) {
|
||||
{
|
||||
name: "Two reviewer, one stale and pending",
|
||||
reviews: []*models.PullReview{
|
||||
&models.PullReview{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true},
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true},
|
||||
},
|
||||
reviewers: []string{"user1", "user2"},
|
||||
isApproved: false,
|
||||
isPendingByTest1: false,
|
||||
isPendingByTest1: true,
|
||||
isReviewedByTest1: false,
|
||||
},
|
||||
{
|
||||
name: "Two reviewer, one stale and pending, other done",
|
||||
reviews: []*models.PullReview{
|
||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}},
|
||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
||||
},
|
||||
reviewers: []string{"user1", "user2"},
|
||||
isApproved: false,
|
||||
isPendingByTest1: true,
|
||||
isReviewedByTest1: false,
|
||||
},
|
||||
{
|
||||
@@ -139,7 +151,7 @@ func TestReviews(t *testing.T) {
|
||||
rf.EXPECT().GetTimeline("test", "pr", int64(1)).Return(test.timeline, nil)
|
||||
rf.EXPECT().GetPullRequestReviews("test", "pr", int64(1)).Return(test.reviews, test.fetchErr)
|
||||
|
||||
reviews, err := common.FetchGiteaReviews(rf, test.reviewers, "test", "pr", 1)
|
||||
reviews, err := common.FetchGiteaReviews(rf, "test", "pr", 1)
|
||||
|
||||
if test.fetchErr != nil {
|
||||
if err != test.fetchErr {
|
||||
@@ -147,6 +159,7 @@ func TestReviews(t *testing.T) {
|
||||
}
|
||||
return
|
||||
}
|
||||
reviews.RequestedReviewers = test.reviewers
|
||||
|
||||
if r := reviews.IsApproved(); r != test.isApproved {
|
||||
t.Fatal("Unexpected IsReviewed():", r, "vs. expected", test.isApproved)
|
||||
|
||||
110
common/utils.go
110
common/utils.go
@@ -27,10 +27,87 @@ import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
)
|
||||
|
||||
type NewRepos struct {
|
||||
Repos []struct {
|
||||
Organization, Repository, Branch string
|
||||
PackageName string
|
||||
}
|
||||
IsMaintainer bool
|
||||
}
|
||||
|
||||
const maintainership_line = "MAINTAINER"
|
||||
|
||||
var true_lines []string = []string{"1", "TRUE", "YES", "OK", "T"}
|
||||
|
||||
func HasSpace(s string) bool {
|
||||
return strings.IndexFunc(s, unicode.IsSpace) >= 0
|
||||
}
|
||||
|
||||
func FindNewReposInIssueBody(body string) *NewRepos {
|
||||
Issues := &NewRepos{}
|
||||
for _, line := range strings.Split(body, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if ul := strings.ToUpper(line); strings.HasPrefix(ul, "MAINTAINER") {
|
||||
value := ""
|
||||
if idx := strings.IndexRune(ul, ':'); idx > 0 && len(ul) > idx+2 {
|
||||
value = ul[idx+1:]
|
||||
} else if idx := strings.IndexRune(ul, ' '); idx > 0 && len(ul) > idx+2 {
|
||||
value = ul[idx+1:]
|
||||
}
|
||||
|
||||
if slices.Contains(true_lines, strings.TrimSpace(value)) {
|
||||
Issues.IsMaintainer = true
|
||||
}
|
||||
}
|
||||
// line = strings.TrimSpace(line)
|
||||
issue := struct{ Organization, Repository, Branch, PackageName string }{}
|
||||
|
||||
branch := strings.Split(line, "#")
|
||||
repo := strings.Split(branch[0], "/")
|
||||
|
||||
if len(branch) == 2 {
|
||||
issue.Branch = strings.TrimSpace(branch[1])
|
||||
}
|
||||
if len(repo) == 2 {
|
||||
issue.Organization = strings.TrimSpace(repo[0])
|
||||
issue.Repository = strings.TrimSpace(repo[1])
|
||||
issue.PackageName = issue.Repository
|
||||
|
||||
if idx := strings.Index(strings.ToUpper(issue.Branch), " AS "); idx > 0 && len(issue.Branch) > idx+5 {
|
||||
issue.PackageName = strings.TrimSpace(issue.Branch[idx+3:])
|
||||
issue.Branch = strings.TrimSpace(issue.Branch[0:idx])
|
||||
}
|
||||
|
||||
if HasSpace(issue.Organization) || HasSpace(issue.Repository) || HasSpace(issue.PackageName) || HasSpace(issue.Branch) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
Issues.Repos = append(Issues.Repos, issue)
|
||||
//PackageNameIdx := strings.Index(strings.ToUpper(line), " AS ")
|
||||
//words := strings.Split(line)
|
||||
}
|
||||
|
||||
if len(Issues.Repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
return Issues
|
||||
}
|
||||
|
||||
func IssueToString(issue *models.Issue) string {
|
||||
if issue == nil {
|
||||
return "(nil)"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s#%d", issue.Repository.Owner, issue.Repository.Name, issue.Index)
|
||||
}
|
||||
|
||||
func SplitLines(str string) []string {
|
||||
return SplitStringNoEmpty(str, "\n")
|
||||
}
|
||||
@@ -168,9 +245,10 @@ func FetchDevelProjects() (DevelProjects, error) {
|
||||
}
|
||||
|
||||
var DevelProjectNotFound = errors.New("Devel project not found")
|
||||
|
||||
func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
|
||||
for _, item := range d {
|
||||
if item.Package == pkg {
|
||||
if item.Package == pkg {
|
||||
return item.Project, nil
|
||||
}
|
||||
}
|
||||
@@ -178,3 +256,33 @@ func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
|
||||
return "", DevelProjectNotFound
|
||||
}
|
||||
|
||||
var removedBranchNameSuffixes []string = []string{
|
||||
"-rm",
|
||||
"-removed",
|
||||
"-deleted",
|
||||
}
|
||||
|
||||
func findRemovedBranchSuffix(branchName string) string {
|
||||
branchName = strings.ToLower(branchName)
|
||||
|
||||
for _, suffix := range removedBranchNameSuffixes {
|
||||
if len(suffix) < len(branchName) && strings.HasSuffix(branchName, suffix) {
|
||||
return suffix
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func IsRemovedBranch(branchName string) bool {
|
||||
return len(findRemovedBranchSuffix(branchName)) > 0
|
||||
}
|
||||
|
||||
func TrimRemovedBranchSuffix(branchName string) string {
|
||||
suffix := findRemovedBranchSuffix(branchName)
|
||||
if len(suffix) > 0 {
|
||||
return branchName[0 : len(branchName)-len(suffix)]
|
||||
}
|
||||
|
||||
return branchName
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
@@ -165,3 +166,142 @@ func TestRemoteName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemovedBranchName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
branchName string
|
||||
isRemoved bool
|
||||
regularName string
|
||||
}{
|
||||
{
|
||||
name: "Empty branch",
|
||||
},
|
||||
{
|
||||
name: "Removed suffix only",
|
||||
branchName: "-rm",
|
||||
isRemoved: false,
|
||||
regularName: "-rm",
|
||||
},
|
||||
{
|
||||
name: "Capital suffix",
|
||||
branchName: "Foo-Rm",
|
||||
isRemoved: true,
|
||||
regularName: "Foo",
|
||||
},
|
||||
{
|
||||
name: "Other suffixes",
|
||||
isRemoved: true,
|
||||
branchName: "Goo-Rm-DeleteD",
|
||||
regularName: "Goo-Rm",
|
||||
},
|
||||
{
|
||||
name: "Other suffixes",
|
||||
isRemoved: true,
|
||||
branchName: "main-REMOVED",
|
||||
regularName: "main",
|
||||
},
|
||||
{
|
||||
name: "Not removed separator",
|
||||
isRemoved: false,
|
||||
branchName: "main;REMOVED",
|
||||
regularName: "main;REMOVED",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if r := common.IsRemovedBranch(test.branchName); r != test.isRemoved {
|
||||
t.Error("Expecting isRemoved:", test.isRemoved, "but received", r)
|
||||
}
|
||||
|
||||
if tn := common.TrimRemovedBranchSuffix(test.branchName); tn != test.regularName {
|
||||
t.Error("Expected stripped branch name to be:", test.regularName, "but have:", tn)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPackageIssueParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
issues *common.NewRepos
|
||||
}{
|
||||
{
|
||||
name: "Nothing",
|
||||
},
|
||||
{
|
||||
name: "Basic repo",
|
||||
input: "org/repo#branch",
|
||||
issues: &common.NewRepos{
|
||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
||||
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Default branch and junk lines and approval for maintainership",
|
||||
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: yes",
|
||||
issues: &common.NewRepos{
|
||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
||||
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
|
||||
},
|
||||
IsMaintainer: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Default branch and junk lines and no maintainership",
|
||||
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: NEVER",
|
||||
issues: &common.NewRepos{
|
||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
||||
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "3 repos with comments and maintainership",
|
||||
input: "\n\nsome comments for org1/repo2 are here and more\n\norg1/repo2#master\n org2/repo3#master\n some/repo3#m\nMaintainer ok",
|
||||
issues: &common.NewRepos{
|
||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
||||
{Organization: "org1", Repository: "repo2", Branch: "master", PackageName: "repo2"},
|
||||
{Organization: "org2", Repository: "repo3", Branch: "master", PackageName: "repo3"},
|
||||
{Organization: "some", Repository: "repo3", Branch: "m", PackageName: "repo3"},
|
||||
},
|
||||
IsMaintainer: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid repos with spaces",
|
||||
input: "or g/repo#branch\norg/r epo#branch\norg/repo#br anch\norg/repo#branch As foo ++",
|
||||
},
|
||||
{
|
||||
name: "Valid repos with spaces",
|
||||
input: " org / repo # branch",
|
||||
issues: &common.NewRepos{
|
||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
||||
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Package name is not repo name",
|
||||
input: " org / repo # branch as repo++ \nmaintainer true",
|
||||
issues: &common.NewRepos{
|
||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
||||
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo++"},
|
||||
},
|
||||
IsMaintainer: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
issue := common.FindNewReposInIssueBody(test.input)
|
||||
if !reflect.DeepEqual(test.issues, issue) {
|
||||
t.Error("Expected", test.issues, "but have", issue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,30 @@ sub ListPackages {
|
||||
return @packages;
|
||||
}
|
||||
|
||||
sub FactoryMd5 {
|
||||
my ($package) = @_;
|
||||
my $out = "";
|
||||
|
||||
if (system("osc ls openSUSE:Factory $package | grep -q build.specials.obscpio") == 0) {
|
||||
system("mkdir _extract") == 0 || die "_extract exists or can't make it. Aborting.";
|
||||
chdir("_extract") || die;
|
||||
system("osc cat openSUSE:Factory $package build.specials.obscpio | cpio -dium 2> /dev/null") == 0 || die;
|
||||
system("rm .* 2> /dev/null");
|
||||
open( my $fh, "find -type f -exec /usr/bin/basename {} \\; | xargs md5sum | awk '{print \$1 FS \$2}' | grep -v d41d8cd98f00b204e9800998ecf8427e |") or die;
|
||||
while ( my $l = <$fh>) {
|
||||
$out = $out.$l;
|
||||
}
|
||||
close($fh);
|
||||
chdir("..") && system("rm -rf _extract") == 0 || die;
|
||||
}
|
||||
open( my $fh, "osc ls -v openSUSE:Factory $package | awk '{print \$1 FS \$7}' | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' |") or die;
|
||||
while (my $l = <$fh>) {
|
||||
$out = $out.$l;
|
||||
}
|
||||
close($fh);
|
||||
return $out;
|
||||
}
|
||||
|
||||
# Read project from first argument
|
||||
sub Usage {
|
||||
die "Usage: $0 <OBS Project> [org [package]]";
|
||||
@@ -65,6 +89,7 @@ sub Usage {
|
||||
|
||||
my $project = shift or Usage();
|
||||
my $org = shift;
|
||||
|
||||
if (not defined($org)) {
|
||||
$org = `osc meta prj $project | grep scmsync | sed -e 's,^.*src.opensuse.org/\\(.*\\)/_ObsPrj.*,\\1,'`;
|
||||
chomp($org);
|
||||
@@ -139,7 +164,7 @@ if ( scalar @tomove > 0 ) {
|
||||
system("git -C $pkg push origin factory") == 0 and
|
||||
system("git obs $super_user api -X PATCH --data '{\"default_branch\": \"factory\"}' /repos/pool/$pkg") == 0
|
||||
or die "Error in creating a pool repo";
|
||||
system("for i in \$(git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ | grep -v '\\(^HEAD\$\\|^factory\$\)'); do git -C $pkg push origin :\$i; done") == 0 or die "failed to cull branches";
|
||||
system("for i in \$(git -C $pkg for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ | grep -v '\\(^HEAD\$\\|^factory\$\\)'); do git -C $pkg push origin :\$i; done") == 0 or die "failed to cull branches";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +192,7 @@ for my $package ( sort(@packages) ) {
|
||||
or ( push( @tomove, $package ) and die "Can't fetch pool for $package" );
|
||||
|
||||
my @commits = FindFactoryCommit($package);
|
||||
my $Md5Hashes = FactoryMd5($package);
|
||||
my $c;
|
||||
my $match = 0;
|
||||
for my $commit (@commits) {
|
||||
@@ -179,16 +205,27 @@ for my $package ( sort(@packages) ) {
|
||||
system("git -C $package lfs fetch pool $commit") == 0
|
||||
and system("git -C $package checkout -B factory $commit") == 0
|
||||
and system("git -C $package lfs checkout") == 0
|
||||
and system(
|
||||
"cd $package; osc ls -v openSUSE:Factory $package | awk '{print \$1 FS \$7}' | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' | md5sum -c --quiet"
|
||||
) == 0
|
||||
and system("bash -c \"diff <(ls -1 $package | sort) <(osc ls openSUSE:Factory $package | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' | sort)\"") == 0
|
||||
)
|
||||
{
|
||||
and chdir($package)) {
|
||||
|
||||
open(my $fh, "|-", "md5sum -c --quiet") or die $!;
|
||||
print $fh $Md5Hashes;
|
||||
close $fh;
|
||||
if ($? >> 8 != 0) {
|
||||
chdir("..") || die;
|
||||
next;
|
||||
}
|
||||
open($fh, "|-", "awk '{print \$2}' | sort | bash -c \"diff <(ls -1 | sort) -\"") or die $!;
|
||||
print $fh $Md5Hashes;
|
||||
close $fh;
|
||||
my $ec = $? >> 8;
|
||||
chdir("..") || die;
|
||||
|
||||
if ($ec == 0) {
|
||||
$c = $commit;
|
||||
$match = 1;
|
||||
last;
|
||||
}
|
||||
|
||||
$c = $commit;
|
||||
$match = 1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
Java:packages
|
||||
Kernel:firmware
|
||||
Kernel:kdump
|
||||
devel:gcc
|
||||
devel:languages:clojure
|
||||
devel:languages:erlang
|
||||
devel:languages:erlang:Factory
|
||||
devel:languages:hare
|
||||
devel:languages:javascript
|
||||
devel:languages:lua
|
||||
devel:languages:nodejs
|
||||
devel:languages:perl
|
||||
devel:languages:python:Factory
|
||||
devel:languages:python:pytest
|
||||
devel:openSUSE:Factory
|
||||
network:chromium
|
||||
network:dhcp
|
||||
network:im:whatsapp
|
||||
network:messaging:xmpp
|
||||
science:HPC
|
||||
server:dns
|
||||
systemsmanagement:cockpit
|
||||
X11:lxde
|
||||
|
||||
@@ -14,15 +14,11 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Context string `json:"context"`
|
||||
State string `json:"state"`
|
||||
TargetUrl string `json:"target_url"`
|
||||
}
|
||||
|
||||
type StatusInput struct {
|
||||
State string `json:"state"`
|
||||
TargetUrl string `json:"target_url"`
|
||||
Description string `json:"description"`
|
||||
Context string `json:"context"`
|
||||
State string `json:"state"`
|
||||
TargetUrl string `json:"target_url"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -59,23 +55,26 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
|
||||
config, ok := r.Context().Value(configKey).(*Config)
|
||||
|
||||
if !ok {
|
||||
common.LogError("Config missing from context")
|
||||
common.LogDebug("Config missing from context")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
header := r.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
common.LogDebug("Authorization header not found")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
token_arr := strings.Split(header, " ")
|
||||
if len(token_arr) != 2 {
|
||||
common.LogDebug("Authorization header malformed")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.EqualFold(token_arr[0], "Bearer") {
|
||||
if !strings.EqualFold(token_arr[0], "token") {
|
||||
common.LogDebug("Token not found in Authorization header")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -83,6 +82,7 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
|
||||
token := token_arr[1]
|
||||
|
||||
if !slices.Contains(config.Keys, token) {
|
||||
common.LogDebug("Provided token is not known")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -104,13 +104,8 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
status := Status{
|
||||
Context: "Build in obs",
|
||||
State: statusinput.State,
|
||||
TargetUrl: statusinput.TargetUrl,
|
||||
}
|
||||
|
||||
status_payload, err := json.Marshal(status)
|
||||
status_payload, err := json.Marshal(statusinput)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
@@ -131,8 +126,8 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "Content-Type")
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ForgeToken))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Authorization", fmt.Sprintf("token %s", ForgeToken))
|
||||
|
||||
resp, err := client.Do(req)
|
||||
|
||||
|
||||
48
gitea_status_proxy/readme.md
Normal file
48
gitea_status_proxy/readme.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# gitea_status_proxy
|
||||
|
||||
Allows bots without code owner permission to set Gitea's commit status
|
||||
|
||||
## Basic usage
|
||||
|
||||
To beging, you need the json config and a Gitea token with permissions to the repository you want to write to.
|
||||
|
||||
Keys should be randomly generated, i.e by using openssl: `openssl rand -base64 48`
|
||||
|
||||
Generate a json config file, with the key generated from running the command above, save as example.json:
|
||||
|
||||
```
|
||||
{
|
||||
"forge_url": "https://src.opensuse.org/api/v1",
|
||||
"keys": ["$YOUR_TOKEN_GOES_HERE"]
|
||||
}
|
||||
```
|
||||
|
||||
### start the proxy:
|
||||
|
||||
```
|
||||
GITEA_TOKEN=YOURTOKEN ./gitea_status_proxy -config example.json
|
||||
2025/10/30 12:53:18 [I] server up and listening on :3000
|
||||
```
|
||||
|
||||
Now the proxy should be able to accept requests under: `localhost:3000/repos/{owner}/{repo}/statuses/{sha}`, the token to be used when authenticating to the proxy must be in the `keys` list of the configuration json file (example.json above)
|
||||
|
||||
### example:
|
||||
|
||||
On a separate terminal, you can use curl to post a status to the proxy, if the GITEA_TOKEN has permissions on the target
|
||||
repository, it will result in a new status being set for the given commit
|
||||
|
||||
```
|
||||
curl -X 'POST' \
|
||||
'localhost:3000/repos/szarate/test-actions-gitea/statuses/cd5847c92fb65a628bdd6015f96ee7e569e1ad6e4fc487acc149b52e788262f9' \
|
||||
-H 'accept: application/json' \
|
||||
-H 'Authorization: token $YOUR_TOKEN_GOES_HERE' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"context": "Proxy test",
|
||||
"description": "Status posted from the proxy",
|
||||
"state": "success",
|
||||
"target_url": "https://src.opensuse.org"
|
||||
}'
|
||||
```
|
||||
|
||||
After this you should be able to the results in the pull request, e.g from above: https://src.opensuse.org/szarate/test-actions-gitea/pulls/1
|
||||
@@ -1,41 +1,65 @@
|
||||
Group Review Bot
|
||||
================
|
||||
|
||||
Areas of responsibility
|
||||
-----------------------
|
||||
This workaround is mainly needed because Gitea does not track which team member performed a review on behalf of a team.
|
||||
|
||||
1. Is used to handle reviews associated with groups defined in the
|
||||
ProjectGit.
|
||||
Main Tasks
|
||||
----------
|
||||
|
||||
2. Assumes: workflow-pr needs to associate and define the PR set from
|
||||
which the groups.json is read (Base of the PrjGit PR)
|
||||
Awaits a comment in the format “@groupreviewbot-name: approve”, then approves the PR with the comment “<user> approved a review on behalf of <groupreviewbot-name>.”
|
||||
|
||||
Target Usage
|
||||
------------
|
||||
|
||||
Projects where policy reviews are required.
|
||||
|
||||
Configiuration
|
||||
Configuration
|
||||
--------------
|
||||
|
||||
Groups are defined in the workflow.config inside the project git. They take following options,
|
||||
The bot is configured via the `ReviewGroups` field in the `workflow.config` file, located in the ProjectGit repository.
|
||||
|
||||
See `ReviewGroups` in the [workflow-pr configuration](../workflow-pr/README.md#config-file).
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
ReviewGroups: [
|
||||
{
|
||||
"Name": "name of the group user",
|
||||
"Reviewers": ["members", "of", "group"],
|
||||
"Silent": (true, false) -- if true, do not explicitly require review requests of group members
|
||||
},
|
||||
],
|
||||
...
|
||||
...
|
||||
"ReviewGroups": [
|
||||
{
|
||||
"Name": "name of the group user",
|
||||
"Reviewers": ["members", "of", "group"],
|
||||
"Silent": "(true, false) -- if true, do not explicitly require review requests of group members"
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Server configuration
|
||||
--------------------------
|
||||
|
||||
**Configuration file:**
|
||||
|
||||
| Field | Type | Notes |
|
||||
| ----- | ----- | ----- |
|
||||
| root | Array of string | Format **org/repo\#branch** |
|
||||
|
||||
Requirements
|
||||
------------
|
||||
* Gitea token to:
|
||||
+ R/W PullRequest
|
||||
+ R/W Notification
|
||||
+ R User
|
||||
Gitea token with following permissions:
|
||||
- R/W PullRequest
|
||||
- R/W Notification
|
||||
- R User
|
||||
|
||||
Env Variables
|
||||
-------------
|
||||
The following variables can be used (and override) command line parameters.
|
||||
|
||||
* `AUTOGITS_CONFIG` - config file location
|
||||
* `AUTOGITS_URL` - Gitea URL
|
||||
* `AUTOGITS_RABBITURL` - RabbitMQ url
|
||||
* `AUTOGITS_DEBUG` - when set, debug level logging enabled
|
||||
|
||||
Authentication env variables
|
||||
* `GITEA_TOKEN` - Gitea user token
|
||||
* `AMQP_USERNAME`, `AMQP_PASSWORD` - username and password for rabbitmq
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
@@ -17,20 +18,23 @@ import (
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
)
|
||||
|
||||
var configs common.AutogitConfigs
|
||||
var acceptRx *regexp.Regexp
|
||||
var rejectRx *regexp.Regexp
|
||||
var groupName string
|
||||
|
||||
func InitRegex(newGroupName string) {
|
||||
groupName = newGroupName
|
||||
acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
|
||||
rejectRx = regexp.MustCompile("^:\\s*")
|
||||
type ReviewBot struct {
|
||||
configs common.AutogitConfigs
|
||||
acceptRx *regexp.Regexp
|
||||
rejectRx *regexp.Regexp
|
||||
groupName string
|
||||
gitea common.Gitea
|
||||
}
|
||||
|
||||
func ParseReviewLine(reviewText string) (bool, string) {
|
||||
func (bot *ReviewBot) InitRegex(newGroupName string) {
|
||||
bot.groupName = newGroupName
|
||||
bot.acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
|
||||
bot.rejectRx = regexp.MustCompile("^:\\s*")
|
||||
}
|
||||
|
||||
func (bot *ReviewBot) ParseReviewLine(reviewText string) (bool, string) {
|
||||
line := strings.TrimSpace(reviewText)
|
||||
groupTextName := "@" + groupName
|
||||
groupTextName := "@" + bot.groupName
|
||||
glen := len(groupTextName)
|
||||
if len(line) < glen || line[0:glen] != groupTextName {
|
||||
return false, line
|
||||
@@ -50,20 +54,20 @@ func ParseReviewLine(reviewText string) (bool, string) {
|
||||
return false, line
|
||||
}
|
||||
|
||||
func ReviewAccepted(reviewText string) bool {
|
||||
func (bot *ReviewBot) ReviewAccepted(reviewText string) bool {
|
||||
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
|
||||
if matched, reviewLine := ParseReviewLine(line); matched {
|
||||
return acceptRx.MatchString(reviewLine)
|
||||
if matched, reviewLine := bot.ParseReviewLine(line); matched {
|
||||
return bot.acceptRx.MatchString(reviewLine)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ReviewRejected(reviewText string) bool {
|
||||
func (bot *ReviewBot) ReviewRejected(reviewText string) bool {
|
||||
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
|
||||
if matched, reviewLine := ParseReviewLine(line); matched {
|
||||
if rejectRx.MatchString(reviewLine) {
|
||||
return !acceptRx.MatchString(reviewLine)
|
||||
if matched, reviewLine := bot.ParseReviewLine(line); matched {
|
||||
if bot.rejectRx.MatchString(reviewLine) {
|
||||
return !bot.acceptRx.MatchString(reviewLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,10 +117,10 @@ var commentStrings = []string{
|
||||
"change_time_estimate",
|
||||
}*/
|
||||
|
||||
func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
|
||||
func (bot *ReviewBot) FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
|
||||
for _, t := range timeline {
|
||||
if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated {
|
||||
if ReviewAccepted(t.Body) || ReviewRejected(t.Body) {
|
||||
if bot.ReviewAccepted(t.Body) || bot.ReviewRejected(t.Body) {
|
||||
return t
|
||||
}
|
||||
}
|
||||
@@ -125,9 +129,9 @@ func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComm
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
|
||||
func (bot *ReviewBot) FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
|
||||
for _, t := range timeline {
|
||||
if t.Type == common.TimelineCommentType_Review && t.User.UserName == groupName && t.Created == t.Updated {
|
||||
if t.Type == common.TimelineCommentType_Review && t.User.UserName == bot.groupName && t.Created == t.Updated {
|
||||
return t
|
||||
}
|
||||
}
|
||||
@@ -135,13 +139,13 @@ func FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.Tim
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnrequestReviews(gitea common.Gitea, org, repo string, id int64, users []string) {
|
||||
if err := gitea.UnrequestReview(org, repo, id, users...); err != nil {
|
||||
func (bot *ReviewBot) UnrequestReviews(org, repo string, id int64, users []string) {
|
||||
if err := bot.gitea.UnrequestReview(org, repo, id, users...); err != nil {
|
||||
common.LogError("Can't remove reviewrs after a review:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessNotifications(notification *models.NotificationThread, gitea common.Gitea) {
|
||||
func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThread) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.LogInfo("panic cought --- recovered")
|
||||
@@ -149,7 +153,7 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
|
||||
}
|
||||
}()
|
||||
|
||||
rx := regexp.MustCompile(`^/?api/v\d+/repos/(?<org>[_a-zA-Z0-9-]+)/(?<project>[_a-zA-Z0-9-]+)/(?:issues|pulls)/(?<num>[0-9]+)$`)
|
||||
rx := regexp.MustCompile(`^/?api/v\d+/repos/(?<org>[_\.a-zA-Z0-9-]+)/(?<project>[_\.a-zA-Z0-9-]+)/(?:issues|pulls)/(?<num>[0-9]+)$`)
|
||||
subject := notification.Subject
|
||||
u, err := url.Parse(notification.Subject.URL)
|
||||
if err != nil {
|
||||
@@ -168,14 +172,14 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
|
||||
id, _ := strconv.ParseInt(match[3], 10, 64)
|
||||
|
||||
common.LogInfo("processing:", fmt.Sprintf("%s/%s!%d", org, repo, id))
|
||||
pr, err := gitea.GetPullRequest(org, repo, id)
|
||||
pr, err := bot.gitea.GetPullRequest(org, repo, id)
|
||||
if err != nil {
|
||||
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ProcessPR(pr); err == nil && !common.IsDryRun {
|
||||
if err := gitea.SetNotificationRead(notification.ID); err != nil {
|
||||
if err := bot.ProcessPR(pr); err == nil && !common.IsDryRun {
|
||||
if err := bot.gitea.SetNotificationRead(notification.ID); err != nil {
|
||||
common.LogDebug(" Cannot set notification as read", err)
|
||||
}
|
||||
} else if err != nil && err != ReviewNotFinished {
|
||||
@@ -185,24 +189,24 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
|
||||
|
||||
var ReviewNotFinished = fmt.Errorf("Review is not finished")
|
||||
|
||||
func ProcessPR(pr *models.PullRequest) error {
|
||||
func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
|
||||
org := pr.Base.Repo.Owner.UserName
|
||||
repo := pr.Base.Repo.Name
|
||||
id := pr.Index
|
||||
|
||||
found := false
|
||||
for _, reviewer := range pr.RequestedReviewers {
|
||||
if reviewer != nil && reviewer.UserName == groupName {
|
||||
if reviewer != nil && reviewer.UserName == bot.groupName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
common.LogInfo(" review is not requested for", groupName)
|
||||
common.LogInfo(" review is not requested for", bot.groupName)
|
||||
return nil
|
||||
}
|
||||
|
||||
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
|
||||
config := bot.configs.GetPrjGitConfig(org, repo, pr.Base.Name)
|
||||
if config == nil {
|
||||
return fmt.Errorf("Cannot find config for: %s", pr.URL)
|
||||
}
|
||||
@@ -212,17 +216,17 @@ func ProcessPR(pr *models.PullRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
reviews, err := gitea.GetPullRequestReviews(org, repo, id)
|
||||
reviews, err := bot.gitea.GetPullRequestReviews(org, repo, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
|
||||
}
|
||||
|
||||
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
|
||||
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(bot.gitea, bot.groupName, pr.Head.Sha, org, repo, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to fetch timeline to review. %w", err)
|
||||
}
|
||||
|
||||
groupConfig, err := config.GetReviewGroup(groupName)
|
||||
groupConfig, err := config.GetReviewGroup(bot.groupName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to fetch review group. %w", err)
|
||||
}
|
||||
@@ -233,30 +237,30 @@ func ProcessPR(pr *models.PullRequest) error {
|
||||
// pr.Head.Sha
|
||||
|
||||
for _, reviewer := range requestReviewers {
|
||||
if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
|
||||
if ReviewAccepted(review.Body) {
|
||||
if review := bot.FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
|
||||
if bot.ReviewAccepted(review.Body) {
|
||||
if !common.IsDryRun {
|
||||
text := reviewer + " approved a review on behalf of " + groupName
|
||||
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
|
||||
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
|
||||
text := reviewer + " approved a review on behalf of " + bot.groupName
|
||||
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
|
||||
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
|
||||
if err != nil {
|
||||
common.LogError(" -> failed to write approval comment", err)
|
||||
}
|
||||
UnrequestReviews(gitea, org, repo, id, requestReviewers)
|
||||
bot.UnrequestReviews(org, repo, id, requestReviewers)
|
||||
}
|
||||
}
|
||||
common.LogInfo(" -> approved by", reviewer)
|
||||
common.LogInfo(" review at", review.Created)
|
||||
return nil
|
||||
} else if ReviewRejected(review.Body) {
|
||||
} else if bot.ReviewRejected(review.Body) {
|
||||
if !common.IsDryRun {
|
||||
text := reviewer + " requested changes on behalf of " + groupName + ". See " + review.HTMLURL
|
||||
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
|
||||
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
|
||||
text := reviewer + " requested changes on behalf of " + bot.groupName + ". See " + review.HTMLURL
|
||||
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
|
||||
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, text)
|
||||
if err != nil {
|
||||
common.LogError(" -> failed to write rejecting comment", err)
|
||||
}
|
||||
UnrequestReviews(gitea, org, repo, id, requestReviewers)
|
||||
bot.UnrequestReviews(org, repo, id, requestReviewers)
|
||||
}
|
||||
}
|
||||
common.LogInfo(" -> declined by", reviewer)
|
||||
@@ -270,7 +274,7 @@ func ProcessPR(pr *models.PullRequest) error {
|
||||
if !groupConfig.Silent && len(requestReviewers) > 0 {
|
||||
common.LogDebug(" Requesting reviews for:", requestReviewers)
|
||||
if !common.IsDryRun {
|
||||
if _, err := gitea.RequestReviews(pr, requestReviewers...); err != nil {
|
||||
if _, err := bot.gitea.RequestReviews(pr, requestReviewers...); err != nil {
|
||||
common.LogDebug(" -> err:", err)
|
||||
}
|
||||
} else {
|
||||
@@ -283,42 +287,40 @@ func ProcessPR(pr *models.PullRequest) error {
|
||||
// add a helpful comment, if not yet added
|
||||
found_help_comment := false
|
||||
for _, t := range timeline {
|
||||
if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == groupName {
|
||||
if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == bot.groupName {
|
||||
found_help_comment = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found_help_comment && !common.IsDryRun {
|
||||
helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
|
||||
helpComment := fmt.Sprintln("Review by", bot.groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
|
||||
"Do **not** use standard review interface to review on behalf of the group.\n"+
|
||||
"To accept the review on behalf of the group, create the following comment: `@"+groupName+": approve`.\n"+
|
||||
"To request changes on behalf of the group, create the following comment: `@"+groupName+": decline` followed with lines justifying the decision.\n"+
|
||||
"To accept the review on behalf of the group, create the following comment: `@"+bot.groupName+": approve`.\n"+
|
||||
"To request changes on behalf of the group, create the following comment: `@"+bot.groupName+": decline` followed with lines justifying the decision.\n"+
|
||||
"Future edits of the comments are ignored, a new comment is required to change the review state.")
|
||||
if slices.Contains(groupConfig.Reviewers, pr.User.UserName) {
|
||||
helpComment = helpComment + "\n\n" +
|
||||
"Submitter is member of this review group, hence they are excluded from being one of the reviewers here"
|
||||
}
|
||||
gitea.AddComment(pr, helpComment)
|
||||
bot.gitea.AddComment(pr, helpComment)
|
||||
}
|
||||
|
||||
return ReviewNotFinished
|
||||
}
|
||||
|
||||
func PeriodReviewCheck() {
|
||||
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
|
||||
func (bot *ReviewBot) PeriodReviewCheck() {
|
||||
notifications, err := bot.gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
|
||||
if err != nil {
|
||||
common.LogError(" Error fetching unread notifications: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, notification := range notifications {
|
||||
ProcessNotifications(notification, gitea)
|
||||
bot.ProcessNotifications(notification)
|
||||
}
|
||||
}
|
||||
|
||||
var gitea common.Gitea
|
||||
|
||||
func main() {
|
||||
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews")
|
||||
rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent")
|
||||
@@ -328,6 +330,24 @@ func main() {
|
||||
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
|
||||
flag.Parse()
|
||||
|
||||
if err := common.SetLoggingLevelFromString(*logging); err != nil {
|
||||
common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if cf := os.Getenv("AUTOGITS_CONFIG"); len(cf) > 0 {
|
||||
*configFile = cf
|
||||
}
|
||||
if url := os.Getenv("AUTOGITS_URL"); len(url) > 0 {
|
||||
*giteaUrl = url
|
||||
}
|
||||
if url := os.Getenv("AUTOGITS_RABBITURL"); len(url) > 0 {
|
||||
*rabbitMqHost = url
|
||||
}
|
||||
if debug := os.Getenv("AUTOGITS_DEBUG"); len(debug) > 0 {
|
||||
common.SetLoggingLevel(common.LogLevelDebug)
|
||||
}
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) != 1 {
|
||||
log.Println(" syntax:")
|
||||
@@ -336,7 +356,7 @@ func main() {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
groupName = args[0]
|
||||
targetGroupName := args[0]
|
||||
|
||||
if *configFile == "" {
|
||||
common.LogError("Missing config file")
|
||||
@@ -359,36 +379,35 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
gitea = common.AllocateGiteaTransport(*giteaUrl)
|
||||
configs, err = common.ResolveWorkflowConfigs(gitea, configData)
|
||||
giteaTransport := common.AllocateGiteaTransport(*giteaUrl)
|
||||
configs, err := common.ResolveWorkflowConfigs(giteaTransport, configData)
|
||||
if err != nil {
|
||||
common.LogError("Cannot parse workflow configs:", err)
|
||||
return
|
||||
}
|
||||
|
||||
reviewer, err := gitea.GetCurrentUser()
|
||||
reviewer, err := giteaTransport.GetCurrentUser()
|
||||
if err != nil {
|
||||
common.LogError("Cannot fetch review user:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.SetLoggingLevelFromString(*logging); err != nil {
|
||||
common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if *interval < 1 {
|
||||
*interval = 1
|
||||
}
|
||||
|
||||
InitRegex(groupName)
|
||||
bot := &ReviewBot{
|
||||
gitea: giteaTransport,
|
||||
configs: configs,
|
||||
}
|
||||
bot.InitRegex(targetGroupName)
|
||||
|
||||
common.LogInfo(" ** processing group reviews for group:", groupName)
|
||||
common.LogInfo(" ** processing group reviews for group:", bot.groupName)
|
||||
common.LogInfo(" ** username in Gitea:", reviewer.UserName)
|
||||
common.LogInfo(" ** polling interval:", *interval, "min")
|
||||
common.LogInfo(" ** connecting to RabbitMQ:", *rabbitMqHost)
|
||||
|
||||
if groupName != reviewer.UserName {
|
||||
if bot.groupName != reviewer.UserName {
|
||||
common.LogError(" ***** Reviewer does not match group name. Aborting. *****")
|
||||
return
|
||||
}
|
||||
@@ -400,10 +419,13 @@ func main() {
|
||||
}
|
||||
|
||||
config_update := ConfigUpdatePush{
|
||||
bot: bot,
|
||||
config_modified: make(chan *common.AutogitConfig),
|
||||
}
|
||||
|
||||
process_issue_pr := IssueCommentProcessor{}
|
||||
process_issue_pr := IssueCommentProcessor{
|
||||
bot: bot,
|
||||
}
|
||||
|
||||
configUpdates := &common.RabbitMQGiteaEventsProcessor{
|
||||
Orgs: []string{},
|
||||
@@ -413,7 +435,7 @@ func main() {
|
||||
},
|
||||
}
|
||||
configUpdates.Connection().RabbitURL = u
|
||||
for _, c := range configs {
|
||||
for _, c := range bot.configs {
|
||||
if org, _, _ := c.GetPrjGit(); !slices.Contains(configUpdates.Orgs, org) {
|
||||
configUpdates.Orgs = append(configUpdates.Orgs, org)
|
||||
}
|
||||
@@ -426,17 +448,17 @@ func main() {
|
||||
select {
|
||||
case configTouched, ok := <-config_update.config_modified:
|
||||
if ok {
|
||||
for idx, c := range configs {
|
||||
for idx, c := range bot.configs {
|
||||
if c == configTouched {
|
||||
org, repo, branch := c.GetPrjGit()
|
||||
prj := fmt.Sprintf("%s/%s#%s", org, repo, branch)
|
||||
common.LogInfo("Detected config update for", prj)
|
||||
|
||||
new_config, err := common.ReadWorkflowConfig(gitea, prj)
|
||||
new_config, err := common.ReadWorkflowConfig(bot.gitea, prj)
|
||||
if err != nil {
|
||||
common.LogError("Failed parsing Project config for", prj, err)
|
||||
} else {
|
||||
configs[idx] = new_config
|
||||
bot.configs[idx] = new_config
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,7 +468,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
PeriodReviewCheck()
|
||||
bot.PeriodReviewCheck()
|
||||
time.Sleep(time.Duration(*interval * int64(time.Minute)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,359 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
)
|
||||
|
||||
func TestProcessPR(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockGitea := mock_common.NewMockGitea(ctrl)
|
||||
groupName := "testgroup"
|
||||
|
||||
bot := &ReviewBot{
|
||||
gitea: mockGitea,
|
||||
groupName: groupName,
|
||||
}
|
||||
bot.InitRegex(groupName)
|
||||
|
||||
org := "myorg"
|
||||
repo := "myrepo"
|
||||
prIndex := int64(1)
|
||||
headSha := "abcdef123456"
|
||||
|
||||
pr := &models.PullRequest{
|
||||
Index: prIndex,
|
||||
URL: "http://gitea/pr/1",
|
||||
State: "open",
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "main",
|
||||
Repo: &models.Repository{
|
||||
Name: repo,
|
||||
Owner: &models.User{
|
||||
UserName: org,
|
||||
},
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: headSha,
|
||||
},
|
||||
User: &models.User{
|
||||
UserName: "submitter",
|
||||
},
|
||||
RequestedReviewers: []*models.User{
|
||||
{UserName: groupName},
|
||||
},
|
||||
}
|
||||
|
||||
prjConfig := &common.AutogitConfig{
|
||||
GitProjectName: org + "/" + repo + "#main",
|
||||
ReviewGroups: []*common.ReviewGroup{
|
||||
{
|
||||
Name: groupName,
|
||||
Reviewers: []string{"reviewer1", "reviewer2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
bot.configs = common.AutogitConfigs{prjConfig}
|
||||
|
||||
t.Run("Review not requested for group", func(t *testing.T) {
|
||||
prNoRequest := *pr
|
||||
prNoRequest.RequestedReviewers = nil
|
||||
err := bot.ProcessPR(&prNoRequest)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PR is closed", func(t *testing.T) {
|
||||
prClosed := *pr
|
||||
prClosed.State = "closed"
|
||||
err := bot.ProcessPR(&prClosed)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Successful Approval", func(t *testing.T) {
|
||||
common.IsDryRun = false
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
// reviewer1 approved in timeline
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "reviewer1"},
|
||||
Body: "@" + groupName + ": approve",
|
||||
},
|
||||
}
|
||||
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
||||
|
||||
expectedText := "reviewer1 approved a review on behalf of " + groupName
|
||||
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
|
||||
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
|
||||
|
||||
err := bot.ProcessPR(pr)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Dry Run - No actions taken", func(t *testing.T) {
|
||||
common.IsDryRun = true
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "reviewer1"},
|
||||
Body: "@" + groupName + ": approve",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
||||
|
||||
// No AddReviewComment or UnrequestReview should be called
|
||||
err := bot.ProcessPR(pr)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Approval already exists - No new comment", func(t *testing.T) {
|
||||
common.IsDryRun = false
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
|
||||
approvalText := "reviewer1 approved a review on behalf of " + groupName
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Review,
|
||||
User: &models.User{UserName: groupName},
|
||||
Body: approvalText,
|
||||
},
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "reviewer1"},
|
||||
Body: "@" + groupName + ": approve",
|
||||
},
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: groupName},
|
||||
Body: "Help comment",
|
||||
},
|
||||
}
|
||||
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
||||
|
||||
// No AddReviewComment, UnrequestReview, or AddComment should be called
|
||||
err := bot.ProcessPR(pr)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Rejection already exists - No new comment", func(t *testing.T) {
|
||||
common.IsDryRun = false
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
|
||||
rejectionText := "reviewer1 requested changes on behalf of " + groupName + ". See http://gitea/comment/123"
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Review,
|
||||
User: &models.User{UserName: groupName},
|
||||
Body: rejectionText,
|
||||
},
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "reviewer1"},
|
||||
Body: "@" + groupName + ": decline",
|
||||
HTMLURL: "http://gitea/comment/123",
|
||||
},
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: groupName},
|
||||
Body: "Help comment",
|
||||
},
|
||||
}
|
||||
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
||||
|
||||
err := bot.ProcessPR(pr)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Pending review - Help comment already exists", func(t *testing.T) {
|
||||
common.IsDryRun = false
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: groupName},
|
||||
Body: "Some help comment",
|
||||
},
|
||||
}
|
||||
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
||||
|
||||
// It will try to request reviews
|
||||
mockGitea.EXPECT().RequestReviews(pr, "reviewer1", "reviewer2").Return(nil, nil)
|
||||
|
||||
// AddComment should NOT be called because bot already has a comment in timeline
|
||||
err := bot.ProcessPR(pr)
|
||||
if err != ReviewNotFinished {
|
||||
t.Errorf("Expected ReviewNotFinished error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Submitter is group member - Excluded from review request", func(t *testing.T) {
|
||||
common.IsDryRun = false
|
||||
prSubmitterMember := *pr
|
||||
prSubmitterMember.User = &models.User{UserName: "reviewer1"}
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(nil, nil)
|
||||
mockGitea.EXPECT().RequestReviews(&prSubmitterMember, "reviewer2").Return(nil, nil)
|
||||
mockGitea.EXPECT().AddComment(&prSubmitterMember, gomock.Any()).Return(nil)
|
||||
err := bot.ProcessPR(&prSubmitterMember)
|
||||
if err != ReviewNotFinished {
|
||||
t.Errorf("Expected ReviewNotFinished error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Successful Rejection", func(t *testing.T) {
|
||||
common.IsDryRun = false
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "reviewer2"},
|
||||
Body: "@" + groupName + ": decline",
|
||||
HTMLURL: "http://gitea/comment/999",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
||||
expectedText := "reviewer2 requested changes on behalf of " + groupName + ". See http://gitea/comment/999"
|
||||
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateRequestChanges, expectedText).Return(nil, nil)
|
||||
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
|
||||
err := bot.ProcessPR(pr)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Config not found", func(t *testing.T) {
|
||||
bot.configs = common.AutogitConfigs{}
|
||||
err := bot.ProcessPR(pr)
|
||||
if err == nil {
|
||||
t.Error("Expected error when config is missing, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Gitea error in GetPullRequestReviews", func(t *testing.T) {
|
||||
bot.configs = common.AutogitConfigs{prjConfig}
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
|
||||
err := bot.ProcessPR(pr)
|
||||
if err == nil {
|
||||
t.Error("Expected error from gitea, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessNotifications(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockGitea := mock_common.NewMockGitea(ctrl)
|
||||
groupName := "testgroup"
|
||||
|
||||
bot := &ReviewBot{
|
||||
gitea: mockGitea,
|
||||
groupName: groupName,
|
||||
}
|
||||
bot.InitRegex(groupName)
|
||||
|
||||
org := "myorg"
|
||||
repo := "myrepo"
|
||||
prIndex := int64(123)
|
||||
notificationID := int64(456)
|
||||
|
||||
notification := &models.NotificationThread{
|
||||
ID: notificationID,
|
||||
Subject: &models.NotificationSubject{
|
||||
URL: fmt.Sprintf("http://gitea/api/v1/repos/%s/%s/pulls/%d", org, repo, prIndex),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("Notification Success", func(t *testing.T) {
|
||||
common.IsDryRun = false
|
||||
pr := &models.PullRequest{
|
||||
Index: prIndex,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "main",
|
||||
Repo: &models.Repository{
|
||||
Name: repo,
|
||||
Owner: &models.User{UserName: org},
|
||||
},
|
||||
},
|
||||
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "headsha",
|
||||
Repo: &models.Repository{
|
||||
Name: repo,
|
||||
Owner: &models.User{UserName: org},
|
||||
},
|
||||
},
|
||||
|
||||
User: &models.User{UserName: "submitter"},
|
||||
RequestedReviewers: []*models.User{{UserName: groupName}},
|
||||
}
|
||||
|
||||
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(pr, nil)
|
||||
|
||||
prjConfig := &common.AutogitConfig{
|
||||
GitProjectName: org + "/" + repo + "#main",
|
||||
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
|
||||
}
|
||||
bot.configs = common.AutogitConfigs{prjConfig}
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "r1"},
|
||||
Body: "@" + groupName + ": approve",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
||||
expectedText := "r1 approved a review on behalf of " + groupName
|
||||
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
|
||||
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
|
||||
|
||||
mockGitea.EXPECT().SetNotificationRead(notificationID).Return(nil)
|
||||
|
||||
bot.ProcessNotifications(notification)
|
||||
|
||||
})
|
||||
|
||||
t.Run("Invalid Notification URL", func(t *testing.T) {
|
||||
badNotification := &models.NotificationThread{
|
||||
Subject: &models.NotificationSubject{
|
||||
URL: "http://gitea/invalid/url",
|
||||
},
|
||||
}
|
||||
bot.ProcessNotifications(badNotification)
|
||||
})
|
||||
|
||||
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
|
||||
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
|
||||
bot.ProcessNotifications(notification)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReviewApprovalCheck(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -60,16 +413,78 @@ func TestReviewApprovalCheck(t *testing.T) {
|
||||
InString: "@group2: disapprove",
|
||||
Rejected: true,
|
||||
},
|
||||
{
|
||||
Name: "Whitespace before colon",
|
||||
GroupName: "group",
|
||||
InString: "@group : LGTM",
|
||||
Approved: true,
|
||||
},
|
||||
{
|
||||
Name: "No whitespace after colon",
|
||||
GroupName: "group",
|
||||
InString: "@group:LGTM",
|
||||
Approved: true,
|
||||
},
|
||||
{
|
||||
Name: "Leading and trailing whitespace on line",
|
||||
GroupName: "group",
|
||||
InString: " @group: LGTM ",
|
||||
Approved: true,
|
||||
},
|
||||
{
|
||||
Name: "Multiline: Approved on second line",
|
||||
GroupName: "group",
|
||||
InString: "Random noise\n@group: approved",
|
||||
Approved: true,
|
||||
},
|
||||
{
|
||||
Name: "Multiline: Multiple group mentions, first wins",
|
||||
GroupName: "group",
|
||||
InString: "@group: decline\n@group: approve",
|
||||
Rejected: true,
|
||||
},
|
||||
{
|
||||
Name: "Multiline: Approved on second line",
|
||||
GroupName: "group",
|
||||
InString: "noise\n@group: approve\nmore noise",
|
||||
Approved: true,
|
||||
},
|
||||
{
|
||||
Name: "Not at start of line (even with whitespace)",
|
||||
GroupName: "group",
|
||||
InString: "Hello @group: approve",
|
||||
Approved: false,
|
||||
},
|
||||
{
|
||||
Name: "Rejecting with reason",
|
||||
GroupName: "group",
|
||||
InString: "@group: decline because of X, Y and Z",
|
||||
Rejected: true,
|
||||
},
|
||||
{
|
||||
Name: "No colon after group",
|
||||
GroupName: "group",
|
||||
InString: "@group LGTM",
|
||||
Approved: false,
|
||||
Rejected: false,
|
||||
},
|
||||
{
|
||||
Name: "Invalid char after group",
|
||||
GroupName: "group",
|
||||
InString: "@group! LGTM",
|
||||
Approved: false,
|
||||
Rejected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
InitRegex(test.GroupName)
|
||||
bot := &ReviewBot{}
|
||||
bot.InitRegex(test.GroupName)
|
||||
|
||||
if r := ReviewAccepted(test.InString); r != test.Approved {
|
||||
if r := bot.ReviewAccepted(test.InString); r != test.Approved {
|
||||
t.Error("ReviewAccepted() returned", r, "expecting", test.Approved)
|
||||
}
|
||||
if r := ReviewRejected(test.InString); r != test.Rejected {
|
||||
if r := bot.ReviewRejected(test.InString); r != test.Rejected {
|
||||
t.Error("ReviewRejected() returned", r, "expecting", test.Rejected)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
type IssueCommentProcessor struct{}
|
||||
type IssueCommentProcessor struct {
|
||||
bot *ReviewBot
|
||||
}
|
||||
|
||||
func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
|
||||
if req.Type != common.RequestType_IssueComment {
|
||||
@@ -19,14 +21,15 @@ func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
|
||||
repo := data.Repository.Name
|
||||
index := int64(data.Issue.Number)
|
||||
|
||||
pr, err := gitea.GetPullRequest(org, repo, index)
|
||||
pr, err := s.bot.gitea.GetPullRequest(org, repo, index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to fetch PullRequest from event: %s/%s!%d Error: %w", org, repo, index, err)
|
||||
}
|
||||
return ProcessPR(pr)
|
||||
return s.bot.ProcessPR(pr)
|
||||
}
|
||||
|
||||
type ConfigUpdatePush struct {
|
||||
bot *ReviewBot
|
||||
config_modified chan *common.AutogitConfig
|
||||
}
|
||||
|
||||
@@ -46,7 +49,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
|
||||
}
|
||||
branch := data.Ref[len(branch_ref):]
|
||||
|
||||
c := configs.GetPrjGitConfig(org, repo, branch)
|
||||
c := s.bot.configs.GetPrjGitConfig(org, repo, branch)
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -64,7 +67,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
|
||||
}
|
||||
|
||||
if modified_config {
|
||||
for _, config := range configs {
|
||||
for _, config := range s.bot.configs {
|
||||
if o, r, _ := config.GetPrjGit(); o == org && r == repo {
|
||||
s.config_modified <- config
|
||||
}
|
||||
|
||||
203
group-review/rabbit_test.go
Normal file
203
group-review/rabbit_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
)
|
||||
|
||||
func TestIssueCommentProcessor(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockGitea := mock_common.NewMockGitea(ctrl)
|
||||
groupName := "testgroup"
|
||||
bot := &ReviewBot{
|
||||
gitea: mockGitea,
|
||||
groupName: groupName,
|
||||
}
|
||||
bot.InitRegex(groupName)
|
||||
|
||||
processor := &IssueCommentProcessor{bot: bot}
|
||||
|
||||
org := "myorg"
|
||||
repo := "myrepo"
|
||||
index := 123
|
||||
|
||||
event := &common.IssueCommentWebhookEvent{
|
||||
Repository: &common.Repository{
|
||||
Name: repo,
|
||||
Owner: &common.Organization{
|
||||
Username: org,
|
||||
},
|
||||
},
|
||||
Issue: &common.IssueDetail{
|
||||
Number: index,
|
||||
},
|
||||
}
|
||||
|
||||
req := &common.Request{
|
||||
Type: common.RequestType_IssueComment,
|
||||
Data: event,
|
||||
}
|
||||
|
||||
t.Run("Successful Processing", func(t *testing.T) {
|
||||
pr := &models.PullRequest{
|
||||
Index: int64(index),
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "main",
|
||||
Repo: &models.Repository{
|
||||
Name: repo,
|
||||
Owner: &models.User{UserName: org},
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "headsha",
|
||||
Repo: &models.Repository{
|
||||
Name: repo,
|
||||
Owner: &models.User{UserName: org},
|
||||
},
|
||||
},
|
||||
User: &models.User{UserName: "submitter"},
|
||||
RequestedReviewers: []*models.User{{UserName: groupName}},
|
||||
}
|
||||
|
||||
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(pr, nil)
|
||||
|
||||
prjConfig := &common.AutogitConfig{
|
||||
GitProjectName: org + "/" + repo + "#main",
|
||||
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
|
||||
}
|
||||
bot.configs = common.AutogitConfigs{prjConfig}
|
||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, int64(index)).Return(nil, nil)
|
||||
mockGitea.EXPECT().GetTimeline(org, repo, int64(index)).Return(nil, nil)
|
||||
mockGitea.EXPECT().RequestReviews(pr, "r1").Return(nil, nil)
|
||||
mockGitea.EXPECT().AddComment(pr, gomock.Any()).Return(nil)
|
||||
|
||||
err := processor.ProcessFunc(req)
|
||||
if err != ReviewNotFinished {
|
||||
t.Errorf("Expected ReviewNotFinished, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
|
||||
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(nil, fmt.Errorf("gitea error"))
|
||||
err := processor.ProcessFunc(req)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Wrong Request Type", func(t *testing.T) {
|
||||
wrongReq := &common.Request{Type: common.RequestType_Push}
|
||||
err := processor.ProcessFunc(wrongReq)
|
||||
if err == nil {
|
||||
t.Error("Expected error for wrong request type, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigUpdatePush(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
groupName := "testgroup"
|
||||
bot := &ReviewBot{
|
||||
groupName: groupName,
|
||||
}
|
||||
bot.InitRegex(groupName)
|
||||
|
||||
configChan := make(chan *common.AutogitConfig, 1)
|
||||
processor := &ConfigUpdatePush{
|
||||
bot: bot,
|
||||
config_modified: configChan,
|
||||
}
|
||||
|
||||
org := "myorg"
|
||||
repo := "myrepo"
|
||||
branch := "main"
|
||||
|
||||
prjConfig := &common.AutogitConfig{
|
||||
GitProjectName: org + "/" + repo + "#" + branch,
|
||||
Organization: org,
|
||||
Branch: branch,
|
||||
}
|
||||
bot.configs = common.AutogitConfigs{prjConfig}
|
||||
|
||||
event := &common.PushWebhookEvent{
|
||||
Ref: "refs/heads/" + branch,
|
||||
Repository: &common.Repository{
|
||||
Name: repo,
|
||||
Owner: &common.Organization{
|
||||
Username: org,
|
||||
},
|
||||
},
|
||||
Commits: []common.Commit{
|
||||
{
|
||||
Modified: []string{common.ProjectConfigFile},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := &common.Request{
|
||||
Type: common.RequestType_Push,
|
||||
Data: event,
|
||||
}
|
||||
|
||||
t.Run("Config Modified", func(t *testing.T) {
|
||||
err := processor.ProcessFunc(req)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case modified := <-configChan:
|
||||
if modified != prjConfig {
|
||||
t.Errorf("Expected modified config to be %v, got %v", prjConfig, modified)
|
||||
}
|
||||
default:
|
||||
t.Error("Expected config modification signal, but none received")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No Config Modified", func(t *testing.T) {
|
||||
noConfigEvent := *event
|
||||
noConfigEvent.Commits = []common.Commit{{Modified: []string{"README.md"}}}
|
||||
noConfigReq := &common.Request{Type: common.RequestType_Push, Data: &noConfigEvent}
|
||||
|
||||
err := processor.ProcessFunc(noConfigReq)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-configChan:
|
||||
t.Error("Did not expect config modification signal")
|
||||
default:
|
||||
// Success
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Wrong Branch Ref", func(t *testing.T) {
|
||||
wrongBranchEvent := *event
|
||||
wrongBranchEvent.Ref = "refs/tags/v1.0"
|
||||
wrongBranchReq := &common.Request{Type: common.RequestType_Push, Data: &wrongBranchEvent}
|
||||
|
||||
err := processor.ProcessFunc(wrongBranchReq)
|
||||
if err == nil {
|
||||
t.Error("Expected error for wrong branch ref, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Config Not Found", func(t *testing.T) {
|
||||
bot.configs = common.AutogitConfigs{}
|
||||
err := processor.ProcessFunc(req)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error even if config not found, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,11 +4,15 @@ OBS Staging Bot
|
||||
Build a PR against a ProjectGit, if review is requested.
|
||||
|
||||
|
||||
Areas of Responsibility
|
||||
-----------------------
|
||||
Main Tasks
|
||||
----------
|
||||
|
||||
* Monitors Notification API in Gitea for review requests
|
||||
* Reviews Package build results in OBS for all changed packages in ProjectGit PR
|
||||
* A build in OBS is initiated when a review for this bot is requested.
|
||||
* The overall build status is reported:
|
||||
* Build successful
|
||||
* Build failed
|
||||
* It checks the build status only for the involved packages compared to the last state of the project for all architectures and all flavors.
|
||||
* It adds an svg with detailed building status.
|
||||
|
||||
|
||||
Target Usage
|
||||
@@ -21,22 +25,46 @@ Configuration File
|
||||
------------------
|
||||
|
||||
Bot reads `staging.config` from the project git or the PR to the project git.
|
||||
It's a JSON file with following syntax
|
||||
It's a JSON file with following syntax:
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"ObsProject": "home:foo:project",
|
||||
"StagingProject": "home:foo:project:staging",
|
||||
"QA": [
|
||||
{
|
||||
"Name": "ProjectBuild",
|
||||
"Origin": "home:foo:product:images"
|
||||
}
|
||||
]
|
||||
"ObsProject": "SUSE:SLFO:1.2",
|
||||
"StagingProject": "SUSE:SLFO:1.2:PullRequest",
|
||||
"QA": [
|
||||
{
|
||||
"Name": "SLES",
|
||||
"Origin": "SUSE:SLFO:Products:SLES:16.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* ObsProject: (**required**) Project where the base project is built. Builds in this project will be used to compare to builds based on sources from the PR
|
||||
* StagingProject: template project that will be used as template for the staging project. Omitting this will use the ObsProject repositories to create the staging. Staging project will be created under the template, or in the bot's home directory if not specified.
|
||||
* QA: set of projects to build ontop of the binaries built in staging.
|
||||
| Field name | Details | Mandatory | Type | Allowed Values | Default |
|
||||
| ----- | ----- | ----- | ----- | ----- | ----- |
|
||||
| *ObsProject* | Product OBS project. Builds in this project will be used to compare to builds based on sources from the PR. | yes | string | `[a-zA-Z0-9-_:]+` | |
|
||||
| *StagingProject* | Used both as base project and prefix for all OBS staging projects. Upon being added as a reviewer to a PrjGit PR, this bot automatically generates an OBS project named *StagingProject:<PR_Number>*. It must be a sub-project of the *ObsProject*. | yes | string | `[a-zA-Z0-9-_:]+` | |
|
||||
| *QA* | Crucial for generating a product build (such as an ISO or FTP tree) that incorporates the packages. | no | array of objects | | |
|
||||
| *QA > Name* | Suffix for the QA OBS staging project. The project is named *StagingProject:<PR_Number>:Name*. | no | string | | |
|
||||
| *QA > Origin* | OBS reference project | no | string | | |
|
||||
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* **OBS staging projects are deleted** when the relative PrjGit PR is closed or merged.
|
||||
|
||||
* **PrjGit PR - staging project**
|
||||
* The OBS staging project utilizes an **scmsync** tag, configured with the `onlybuild` flag, to exclusively build packages associated with this specific PrjGit PR.
|
||||
* The **build config** is inherited from the PrjGit PR config file (even if unchanged).
|
||||
* The **project meta** creates a standard repository following the StagingProject as a project path.
|
||||
* The base *StagingProject* has the macro **FromScratch:** set in its config, which prevents inheriting the configuration from the included project paths.
|
||||
* The bot copies the project maintainers from *StagingProject* to the specific staging project (*StagingProject:<PR_Number>*).
|
||||
* The bot reports “Build successful” only if the build is successful for all repositories and all architectures.
|
||||
|
||||
* **PrjGit PR - QA staging project**
|
||||
* The QA staging project is meant for building the product; the relative build config is inherited from the `QA > Origin` project.
|
||||
* In this case, the **scmsync** tag is inherited from the `QA > Origin` project.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -386,6 +386,28 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
|
||||
}
|
||||
// patch baseMeta to become the new project
|
||||
templateMeta.Name = stagingProject + ":" + subProjectName
|
||||
// freeze tag for now
|
||||
if len(templateMeta.ScmSync) > 0 {
|
||||
repository, err := url.Parse(templateMeta.ScmSync)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
common.LogDebug("getting data for ", repository.EscapedPath())
|
||||
split := strings.Split(repository.EscapedPath(), "/")
|
||||
org, repo := split[1], split[2]
|
||||
|
||||
common.LogDebug("getting commit for ", org, " repo ", repo, " fragment ", repository.Fragment)
|
||||
branch, err := gitea.GetCommit(org, repo, repository.Fragment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// set expanded commit url
|
||||
repository.Fragment = branch.SHA
|
||||
templateMeta.ScmSync = repository.String()
|
||||
common.LogDebug("Setting scmsync url to ", templateMeta.ScmSync)
|
||||
}
|
||||
// Cleanup ReleaseTarget and modify affected path entries
|
||||
for idx, r := range templateMeta.Repositories {
|
||||
templateMeta.Repositories[idx].ReleaseTargets = nil
|
||||
@@ -1044,6 +1066,7 @@ func main() {
|
||||
ObsWebHost = ObsWebHostFromApiHost(*obsApiHost)
|
||||
}
|
||||
|
||||
common.LogDebug("OBS Gitea Host:", GiteaUrl)
|
||||
common.LogDebug("OBS Web Host:", ObsWebHost)
|
||||
common.LogDebug("OBS API Host:", *obsApiHost)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
OBS Status Service
|
||||
==================
|
||||
|
||||
Reports build status of OBS service as an easily to produce SVG
|
||||
Reports build status of OBS service as an easily to produce SVG. Repository
|
||||
results (build results) are cached for 10 seconds and repository listing
|
||||
for OBS instance are cached for 5 minutes -- new repositories take up to
|
||||
5 minutes to be visible.
|
||||
|
||||
Requests for individual build results:
|
||||
|
||||
@@ -17,19 +20,31 @@ Get requests for / will also return 404 statu normally. If the Backend redis
|
||||
server is not available, it will return 500
|
||||
|
||||
|
||||
By default, SVG output is generated, suitable for inclusion. But JSON and XML
|
||||
output is possible by setting `Accept:` request header
|
||||
|
||||
| Accept Request Header | Output format
|
||||
|------------------------|---------------------
|
||||
| | SVG image
|
||||
| application/json | JSON data
|
||||
| application/obs+xml | XML output
|
||||
|
||||
|
||||
Areas of Responsibility
|
||||
-----------------------
|
||||
|
||||
* Monitors RabbitMQ interface for notification of OBS package and project status
|
||||
* Produces SVG output based on GET request
|
||||
* Cache results (sqlite) and periodically update results from OBS (in case of messages are missing)
|
||||
* Fetch and cache internal data from OBS and present it in usable format:
|
||||
+ Generate SVG output for specific OBS project or package
|
||||
+ Generate JSON/XML output for automated processing
|
||||
* Low-overhead
|
||||
|
||||
|
||||
Target Usage
|
||||
------------
|
||||
|
||||
* README.md of package git or project git
|
||||
* inside README.md of package git or project git
|
||||
* comment section of a Gitea PR
|
||||
* automated build result processing
|
||||
|
||||
Running
|
||||
-------
|
||||
@@ -42,3 +57,4 @@ Default parameters can be changed by env variables
|
||||
| `OBS_STATUS_SERVICE_LISTEN` | [::1]:8080 | Listening address and port
|
||||
| `OBS_STATUS_SERVICE_CERT` | /run/obs-status-service.pem | Location of certificate file for service
|
||||
| `OBS_STATUS_SERVICE_KEY` | /run/obs-status-service.pem | Location of key file for service
|
||||
| `REDIS` | | OBS's Redis instance URL
|
||||
|
||||
BIN
obs-status-service/factory.results.json.bz2
LFS
Normal file
BIN
obs-status-service/factory.results.json.bz2
LFS
Normal file
Binary file not shown.
BIN
obs-status-service/gcc15.results.json.bz2
LFS
Normal file
BIN
obs-status-service/gcc15.results.json.bz2
LFS
Normal file
Binary file not shown.
@@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/bzip2"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -82,3 +85,36 @@ func TestStatusSvg(t *testing.T) {
|
||||
os.WriteFile("testpackage.svg", PackageStatusSummarySvg("pkg2", data), 0o777)
|
||||
os.WriteFile("testproject.svg", ProjectStatusSummarySvg(data), 0o777)
|
||||
}
|
||||
|
||||
func TestFactoryResults(t *testing.T) {
|
||||
data, err := os.Open("factory.results.json.bz2")
|
||||
if err != nil {
|
||||
t.Fatal("Openning factory.results.json.bz2 failed:", err)
|
||||
}
|
||||
UncompressedData, err := io.ReadAll(bzip2.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatal("Reading factory.results.json.bz2 failed:", err)
|
||||
}
|
||||
|
||||
var results []*common.BuildResult
|
||||
if err := json.Unmarshal(UncompressedData, &results); err != nil {
|
||||
t.Fatal("Failed parsing test data", err)
|
||||
}
|
||||
|
||||
// add tests here
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
// add test data here
|
||||
{
|
||||
name: "First test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// and test code here
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"html"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SvgWriter struct {
|
||||
@@ -133,7 +134,7 @@ func (svg *SvgWriter) WritePackageStatus(loglink, arch, status, detail string) {
|
||||
}
|
||||
|
||||
func (svg *SvgWriter) WriteProjectStatus(project, repo, arch, status string, count int) {
|
||||
u, err := url.Parse(*ObsUrl + "/project/monitor/" + url.PathEscape(project) + "?defaults=0&" + url.QueryEscape(status) + "=1&arch_" + url.QueryEscape(arch) + "=1&repo_" + url.QueryEscape(repo) + "=1")
|
||||
u, err := url.Parse(*ObsUrl + "/project/monitor/" + url.PathEscape(project) + "?defaults=0&" + url.QueryEscape(status) + "=1&arch_" + url.QueryEscape(arch) + "=1&repo_" + url.QueryEscape(strings.ReplaceAll(repo, ".", "_")) + "=1")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
15
systemd/group-review@.service
Normal file
15
systemd/group-review@.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Group Review bot for %i
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/bin/group-review %i
|
||||
EnvironmentFile=-/etc/default/group-review/%i.env
|
||||
DynamicUser=yes
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
19
systemd/workflow-direct@.service
Normal file
19
systemd/workflow-direct@.service
Normal file
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=WorkflowDirect git bot for %i
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/bin/workflow-direct
|
||||
EnvironmentFile=-/etc/default/%i/workflow-direct.env
|
||||
DynamicUser=yes
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
RuntimeDirectory=%i
|
||||
# SLES 15 doesn't have HOME set for dynamic users, so we improvise
|
||||
BindReadOnlyPaths=/etc/default/%i/known_hosts:/etc/ssh/ssh_known_hosts /etc/default/%i/config.json:%t/%i/config.json /etc/default/%i/id_ed25519 /etc/default/%i/id_ed25519.pub
|
||||
WorkingDirectory=%t/%i
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -1,33 +1,51 @@
|
||||
Direct Workflow bot
|
||||
===================
|
||||
|
||||
The project submodule is automatically updated by the direct bot whenever a branch is updated in a package repository.
|
||||
This bot can coexist with the Workflow PR bot, which is instead triggered by a new package PR.
|
||||
|
||||
Target Usage
|
||||
------------
|
||||
|
||||
Devel project, where direct pushes to package git are possible.
|
||||
|
||||
Areas of responsibility
|
||||
-----------------------
|
||||
|
||||
1. Keep ProjectGit in sync with packages in the organization
|
||||
* on pushes to package, updates the submodule commit id
|
||||
to the default branch HEAD (as configured in Gitea)
|
||||
* on repository adds, creates a new submodule (if non empty)
|
||||
* on repository removal, removes the submodule
|
||||
* **On pushes to package**: updates the submodule commit ID to the default branch HEAD (as configured in Gitea).
|
||||
* **On repository adds**: creates a new submodule (if non-empty).
|
||||
* **On repository removal**: removes the submodule.
|
||||
|
||||
**Note:** If you want to revert a change in a package, you need to do that manually in the project git.
|
||||
|
||||
NOTE: reverts (push HEAD^) are not supported as they would step-on the
|
||||
work of the workflow-pr bot. Manual update of the project git is
|
||||
required in this case.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Uses `workflow.config` for configuration. Parameters
|
||||
Uses `workflow.config` for configuration.
|
||||
|
||||
* _Workflows_: ["direct"] -- direct entry enables direct workflow. **Mandatory**
|
||||
* _Organization_: organization that holds all the packages. **Mandatory**
|
||||
* _Branch_: branch updated in repo's, or blank for default package branch
|
||||
* _GitProjectName_: package in above org, or `org/package#branch` for PrjGit. By default assumes `_ObsPrj` with default branch and in the `Organization`
|
||||
| Field name | Details | Mandatory | Type | Allowed Values | Default |
|
||||
| ----- | ----- | ----- | ----- | ----- | ----- |
|
||||
| *Workflows* | Type of workflow | yes | string | “direct” | |
|
||||
| *Organization* | The organization that holds all the packages | yes | string | | |
|
||||
| *Branch* | The designated branch for packages | no | string | | blank (default package branch) |
|
||||
| *GitProjectName* | Repository and branch where the ProjectGit lives. | no | string | **Format**: `org/project_repo#branch` | By default assumes `_ObsPrj` with default branch in the *Organization* |
|
||||
|
||||
NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
|
||||
|
||||
Target Usage
|
||||
------------
|
||||
|
||||
Devel project, where direct pushes to package git are possible
|
||||
Environment Variables
|
||||
-------
|
||||
|
||||
* `GITEA_TOKEN` (required)
|
||||
* `AMQP_USERNAME` (required)
|
||||
* `AMQP_PASSWORD` (required)
|
||||
* `AUTOGITS_CONFIG` (required)
|
||||
* `AUTOGITS_URL` - default: https://src.opensuse.org
|
||||
* `AUTOGITS_RABBITURL` - default: amqps://rabbit.opensuse.org
|
||||
* `AUTOGITS_DEBUG` - disabled by default, set to any value to enable
|
||||
* `AUTOGITS_CHECK_ON_START` - disabled by default, set to any value to enable
|
||||
* `AUTOGITS_REPO_PATH` - default is temporary directory
|
||||
* `AUTOGITS_IDENTITY_FILE` - in case where we need explicit identify path for ssh specified
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -40,7 +39,7 @@ import (
|
||||
const (
|
||||
AppName = "direct_workflow"
|
||||
GitAuthor = "AutoGits prjgit-updater"
|
||||
GitEmail = "adam+autogits-direct@zombino.com"
|
||||
GitEmail = "autogits-direct@noreply@src.opensuse.org"
|
||||
)
|
||||
|
||||
var configuredRepos map[string][]*common.AutogitConfig
|
||||
@@ -53,18 +52,6 @@ func isConfiguredOrg(org *common.Organization) bool {
|
||||
return found
|
||||
}
|
||||
|
||||
func concatenateErrors(err1, err2 error) error {
|
||||
if err1 == nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
if err2 == nil {
|
||||
return err1
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w\n%w", err1, err2)
|
||||
}
|
||||
|
||||
type RepositoryActionProcessor struct{}
|
||||
|
||||
func (*RepositoryActionProcessor) ProcessFunc(request *common.Request) error {
|
||||
@@ -72,69 +59,90 @@ func (*RepositoryActionProcessor) ProcessFunc(request *common.Request) error {
|
||||
configs, configFound := configuredRepos[action.Organization.Username]
|
||||
|
||||
if !configFound {
|
||||
log.Printf("Repository event for %s. Not configured. Ignoring.\n", action.Organization.Username)
|
||||
common.LogInfo("Repository event for", action.Organization.Username, ". Not configured. Ignoring.", action.Organization.Username)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
if org, repo, _ := config.GetPrjGit(); org == action.Repository.Owner.Username && repo == action.Repository.Name {
|
||||
log.Println("+ ignoring repo event for PrjGit repository", config.GitProjectName)
|
||||
common.LogError("+ ignoring repo event for PrjGit repository", config.GitProjectName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, config := range configs {
|
||||
err = concatenateErrors(err, processConfiguredRepositoryAction(action, config))
|
||||
processConfiguredRepositoryAction(action, config)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, config *common.AutogitConfig) error {
|
||||
func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, config *common.AutogitConfig) {
|
||||
gitOrg, gitPrj, gitBranch := config.GetPrjGit()
|
||||
git, err := gh.CreateGitHandler(config.Organization)
|
||||
common.PanicOnError(err)
|
||||
defer git.Close()
|
||||
|
||||
if len(config.Branch) == 0 {
|
||||
config.Branch = action.Repository.Default_Branch
|
||||
configBranch := config.Branch
|
||||
if len(configBranch) == 0 {
|
||||
configBranch = action.Repository.Default_Branch
|
||||
if common.IsRemovedBranch(configBranch) {
|
||||
common.LogDebug(" - default branch has deleted suffix. Skipping")
|
||||
return
|
||||
}
|
||||
|
||||
if len(configBranch) == 0 {
|
||||
common.LogDebug("Empty default branch in message. Maybe race-condition?")
|
||||
repo, err := gitea.GetRepository(action.Repository.Owner.Username, action.Repository.Name)
|
||||
if err != nil {
|
||||
common.LogError("Failed to fetch repository we have an event for?", action.Repository.Owner.Username, action.Repository.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if len(repo.DefaultBranch) == 0 {
|
||||
common.LogError("Default branch is somehow empty. We cannot do anything.")
|
||||
return
|
||||
}
|
||||
configBranch = repo.DefaultBranch
|
||||
}
|
||||
}
|
||||
|
||||
prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error accessing/creating prjgit: %s/%s#%s err: %w", gitOrg, gitPrj, gitBranch, err)
|
||||
common.LogError("Error accessing/creating prjgit:", gitOrg, gitPrj, gitBranch, err)
|
||||
return
|
||||
}
|
||||
|
||||
remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL)
|
||||
common.PanicOnError(err)
|
||||
git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
|
||||
|
||||
switch action.Action {
|
||||
case "created":
|
||||
if action.Repository.Object_Format_Name != "sha256" {
|
||||
return fmt.Errorf(" - '%s' repo is not sha256. Ignoring.", action.Repository.Name)
|
||||
common.LogError(" - ", action.Repository.Name, "repo is not sha256. Ignoring.")
|
||||
return
|
||||
}
|
||||
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name))
|
||||
defer git.GitExecOrPanic(gitPrj, "submodule", "deinit", "--all")
|
||||
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name))
|
||||
defer git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
|
||||
|
||||
branch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, action.Repository.Name), "branch", "--show-current"))
|
||||
if branch != config.Branch {
|
||||
if err := git.GitExec(path.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "origin", config.Branch+":"+config.Branch); err != nil {
|
||||
return fmt.Errorf("error fetching branch %s. ignoring as non-existent. err: %w", config.Branch, err) // no branch? so ignore repo here
|
||||
if branch != configBranch {
|
||||
if err := git.GitExec(path.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "origin", configBranch+":"+configBranch); err != nil {
|
||||
common.LogError("error fetching branch", configBranch, ". ignoring as non-existent.", err) // no branch? so ignore repo here
|
||||
return
|
||||
}
|
||||
common.PanicOnError(git.GitExec(path.Join(gitPrj, action.Repository.Name), "checkout", config.Branch))
|
||||
common.PanicOnError(git.GitExec(path.Join(gitPrj, action.Repository.Name), "checkout", configBranch))
|
||||
}
|
||||
common.PanicOnError(git.GitExec(gitPrj, "commit", "-m", "Automatic package inclusion via Direct Workflow"))
|
||||
common.PanicOnError(git.GitExec(gitPrj, "commit", "-m", "Auto-inclusion "+action.Repository.Name))
|
||||
if !noop {
|
||||
common.PanicOnError(git.GitExec(gitPrj, "push"))
|
||||
}
|
||||
|
||||
case "deleted":
|
||||
if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil || !stat.IsDir() {
|
||||
if DebugMode {
|
||||
log.Println("delete event for", action.Repository.Name, "-- not in project. Ignoring")
|
||||
}
|
||||
return nil
|
||||
common.LogDebug("delete event for", action.Repository.Name, "-- not in project. Ignoring")
|
||||
return
|
||||
}
|
||||
common.PanicOnError(git.GitExec(gitPrj, "rm", action.Repository.Name))
|
||||
common.PanicOnError(git.GitExec(gitPrj, "commit", "-m", "Automatic package removal via Direct Workflow"))
|
||||
@@ -143,10 +151,9 @@ func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, co
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("%s: %s", "Unknown action type", action.Action)
|
||||
common.LogError("Unknown action type:", action.Action)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type PushActionProcessor struct{}
|
||||
@@ -156,77 +163,83 @@ func (*PushActionProcessor) ProcessFunc(request *common.Request) error {
|
||||
configs, configFound := configuredRepos[action.Repository.Owner.Username]
|
||||
|
||||
if !configFound {
|
||||
log.Printf("Repository event for %s. Not configured. Ignoring.\n", action.Repository.Owner.Username)
|
||||
common.LogDebug("Repository event for", action.Repository.Owner.Username, ". Not configured. Ignoring.")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
if gitOrg, gitPrj, _ := config.GetPrjGit(); gitOrg == action.Repository.Owner.Username && gitPrj == action.Repository.Name {
|
||||
log.Println("+ ignoring push to PrjGit repository", config.GitProjectName)
|
||||
common.LogInfo("+ ignoring push to PrjGit repository", config.GitProjectName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, config := range configs {
|
||||
err = concatenateErrors(err, processConfiguredPushAction(action, config))
|
||||
processConfiguredPushAction(action, config)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func processConfiguredPushAction(action *common.PushWebhookEvent, config *common.AutogitConfig) error {
|
||||
func processConfiguredPushAction(action *common.PushWebhookEvent, config *common.AutogitConfig) {
|
||||
gitOrg, gitPrj, gitBranch := config.GetPrjGit()
|
||||
git, err := gh.CreateGitHandler(config.Organization)
|
||||
common.PanicOnError(err)
|
||||
defer git.Close()
|
||||
|
||||
log.Printf("push to: %s/%s for %s/%s#%s", action.Repository.Owner.Username, action.Repository.Name, gitOrg, gitPrj, gitBranch)
|
||||
if len(config.Branch) == 0 {
|
||||
config.Branch = action.Repository.Default_Branch
|
||||
log.Println(" + default branch", action.Repository.Default_Branch)
|
||||
common.LogDebug("push to:", action.Repository.Owner.Username, action.Repository.Name, "for:", gitOrg, gitPrj, gitBranch)
|
||||
branch := config.Branch
|
||||
if len(branch) == 0 {
|
||||
if common.IsRemovedBranch(branch) {
|
||||
common.LogDebug(" + default branch has removed suffix:", branch, "Skipping.")
|
||||
return
|
||||
}
|
||||
branch = action.Repository.Default_Branch
|
||||
common.LogDebug(" + using default branch", branch)
|
||||
}
|
||||
|
||||
prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error accessing/creating prjgit: %s/%s err: %w", gitOrg, gitPrj, err)
|
||||
common.LogError("Error accessing/creating prjgit:", gitOrg, gitPrj, err)
|
||||
return
|
||||
}
|
||||
|
||||
remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL)
|
||||
common.PanicOnError(err)
|
||||
git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
|
||||
headCommitId, err := git.GitRemoteHead(gitPrj, remoteName, gitBranch)
|
||||
common.PanicOnError(err)
|
||||
commit, ok := git.GitSubmoduleCommitId(gitPrj, action.Repository.Name, headCommitId)
|
||||
for ok && action.Head_Commit.Id == commit {
|
||||
log.Println(" -- nothing to do, commit already in ProjectGit")
|
||||
return nil
|
||||
common.LogDebug(" -- nothing to do, commit already in ProjectGit")
|
||||
return
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil || !stat.IsDir() {
|
||||
if DebugMode {
|
||||
log.Println("Pushed to package that is not part of the project. Ignoring:", err)
|
||||
}
|
||||
return nil
|
||||
if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil {
|
||||
git.GitExecOrPanic(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name)
|
||||
common.LogDebug("Pushed to package that is not part of the project. Re-adding...", err)
|
||||
} else if !stat.IsDir() {
|
||||
common.LogError("Pushed to a package that is not a submodule but exists in the project. Ignoring.")
|
||||
return
|
||||
}
|
||||
git.GitExecOrPanic(gitPrj, "submodule", "update", "--init", "--depth", "1", "--checkout", action.Repository.Name)
|
||||
defer git.GitExecOrPanic(gitPrj, "submodule", "deinit", "--all")
|
||||
git.GitExecOrPanic(gitPrj, "submodule", "update", "--init", "--force", "--depth", "1", "--checkout", action.Repository.Name)
|
||||
defer git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
|
||||
|
||||
if err := git.GitExec(filepath.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "--force", remoteName, config.Branch+":"+config.Branch); err != nil {
|
||||
return fmt.Errorf("error fetching branch %s. ignoring as non-existent. err: %w", config.Branch, err) // no branch? so ignore repo here
|
||||
if err := git.GitExec(filepath.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "--force", "origin", branch+":"+branch); err != nil {
|
||||
common.LogError("Error fetching branch:", branch, "Ignoring as non-existent.", err)
|
||||
return
|
||||
}
|
||||
id, err := git.GitRemoteHead(filepath.Join(gitPrj, action.Repository.Name), remoteName, config.Branch)
|
||||
id, err := git.GitBranchHead(filepath.Join(gitPrj, action.Repository.Name), branch)
|
||||
common.PanicOnError(err)
|
||||
if action.Head_Commit.Id == id {
|
||||
git.GitExecOrPanic(filepath.Join(gitPrj, action.Repository.Name), "checkout", id)
|
||||
git.GitExecOrPanic(gitPrj, "commit", "-a", "-m", "Automatic update via push via Direct Workflow")
|
||||
git.GitExecOrPanic(gitPrj, "commit", "-a", "-m", fmt.Sprintf("'%s' update via Direct Workflow", action.Repository.Name))
|
||||
if !noop {
|
||||
git.GitExecOrPanic(gitPrj, "push", remoteName)
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("push of refs not on the configured branch", config.Branch, ". ignoring.")
|
||||
return nil
|
||||
common.LogDebug("push of refs not on the configured branch", branch, ". ignoring.")
|
||||
}
|
||||
|
||||
func verifyProjectState(git common.Git, org string, config *common.AutogitConfig, configs []*common.AutogitConfig) (err error) {
|
||||
@@ -248,51 +261,65 @@ func verifyProjectState(git common.Git, org string, config *common.AutogitConfig
|
||||
|
||||
remoteName, err := git.GitClone(gitPrj, gitBranch, repo.SSHURL)
|
||||
common.PanicOnError(err)
|
||||
defer git.GitExecOrPanic(gitPrj, "submodule", "deinit", "--all")
|
||||
git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
|
||||
defer git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
|
||||
|
||||
log.Println(" * Getting submodule list")
|
||||
common.LogDebug(" * Getting submodule list")
|
||||
sub, err := git.GitSubmoduleList(gitPrj, "HEAD")
|
||||
common.PanicOnError(err)
|
||||
|
||||
log.Println(" * Getting package links")
|
||||
common.LogDebug(" * Getting package links")
|
||||
var pkgLinks []*PackageRebaseLink
|
||||
if f, err := fs.Stat(os.DirFS(path.Join(git.GetPath(), gitPrj)), common.PrjLinksFile); err == nil && (f.Mode()&fs.ModeType == 0) && f.Size() < 1000000 {
|
||||
if data, err := os.ReadFile(path.Join(git.GetPath(), gitPrj, common.PrjLinksFile)); err == nil {
|
||||
pkgLinks, err = parseProjectLinks(data)
|
||||
if err != nil {
|
||||
log.Println("Cannot parse project links file:", err.Error())
|
||||
common.LogError("Cannot parse project links file:", err.Error())
|
||||
pkgLinks = nil
|
||||
} else {
|
||||
ResolveLinks(org, pkgLinks, gitea)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Println(" - No package links defined")
|
||||
common.LogInfo(" - No package links defined")
|
||||
}
|
||||
|
||||
/* Check existing submodule that they are updated */
|
||||
|
||||
isGitUpdated := false
|
||||
next_package:
|
||||
for filename, commitId := range sub {
|
||||
// ignore project gits
|
||||
//for _, c := range configs {
|
||||
if gitPrj == filename {
|
||||
log.Println(" prjgit as package? ignoring project git:", filename)
|
||||
common.LogDebug(" prjgit as package? ignoring project git:", filename)
|
||||
continue next_package
|
||||
}
|
||||
//}
|
||||
|
||||
log.Printf(" verifying package: %s -> %s(%s)", commitId, filename, config.Branch)
|
||||
commits, err := gitea.GetRecentCommits(org, filename, config.Branch, 10)
|
||||
if len(commits) == 0 {
|
||||
if repo, err := gitea.GetRepository(org, filename); repo == nil && err == nil {
|
||||
branch := config.Branch
|
||||
common.LogDebug(" verifying package:", commitId, "->", filename, "@", branch)
|
||||
if repo, err := gitea.GetRepository(org, filename); repo == nil && err == nil {
|
||||
common.LogDebug(" repository removed...")
|
||||
git.GitExecOrPanic(gitPrj, "rm", filename)
|
||||
isGitUpdated = true
|
||||
continue
|
||||
} else if err != nil {
|
||||
common.LogError("failed fetching repo data", org, filename, err)
|
||||
continue
|
||||
} else if len(branch) == 0 {
|
||||
branch = repo.DefaultBranch
|
||||
common.LogDebug(" -> using default branch", branch)
|
||||
if common.IsRemovedBranch(branch) {
|
||||
common.LogDebug(" Default branch for", filename, "is excluded")
|
||||
git.GitExecOrPanic(gitPrj, "rm", filename)
|
||||
isGitUpdated = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
commits, err := gitea.GetRecentCommits(org, filename, branch, 10)
|
||||
if err != nil {
|
||||
log.Println(" -> failed to fetch recent commits for package:", filename, " Err:", err)
|
||||
common.LogDebug(" -> failed to fetch recent commits for package:", filename, " Err:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -309,7 +336,7 @@ next_package:
|
||||
if l.Pkg == filename {
|
||||
link = l
|
||||
|
||||
log.Println(" -> linked package")
|
||||
common.LogDebug(" -> linked package")
|
||||
// so, we need to rebase here. Can't really optimize, so clone entire package tree and remote
|
||||
pkgPath := path.Join(gitPrj, filename)
|
||||
git.GitExecOrPanic(gitPrj, "submodule", "update", "--init", "--checkout", filename)
|
||||
@@ -323,7 +350,7 @@ next_package:
|
||||
nCommits := len(common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkgPath, "rev-list", "^NOW", "HEAD"), "\n"))
|
||||
if nCommits > 0 {
|
||||
if !noop {
|
||||
git.GitExecOrPanic(pkgPath, "push", "-f", "origin", "HEAD:"+config.Branch)
|
||||
git.GitExecOrPanic(pkgPath, "push", "-f", "origin", "HEAD:"+branch)
|
||||
}
|
||||
isGitUpdated = true
|
||||
}
|
||||
@@ -340,42 +367,27 @@ next_package:
|
||||
common.PanicOnError(git.GitExec(gitPrj, "submodule", "update", "--init", "--depth", "1", "--checkout", filename))
|
||||
common.PanicOnError(git.GitExec(filepath.Join(gitPrj, filename), "fetch", "--depth", "1", "origin", commits[0].SHA))
|
||||
common.PanicOnError(git.GitExec(filepath.Join(gitPrj, filename), "checkout", commits[0].SHA))
|
||||
log.Println(" -> updated to", commits[0].SHA)
|
||||
common.LogDebug(" -> updated to", commits[0].SHA)
|
||||
isGitUpdated = true
|
||||
} else {
|
||||
// probably need `merge-base` or `rev-list` here instead, or the project updated already
|
||||
log.Println(" *** Cannot find SHA of last matching update for package:", filename, " Ignoring")
|
||||
common.LogInfo(" *** Cannot find SHA of last matching update for package:", filename, " Ignoring")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find all missing repositories, and add them
|
||||
if DebugMode {
|
||||
log.Println("checking for missing repositories...")
|
||||
}
|
||||
common.LogDebug("checking for missing repositories...")
|
||||
repos, err := gitea.GetOrganizationRepositories(org)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if DebugMode {
|
||||
log.Println(" nRepos:", len(repos))
|
||||
}
|
||||
common.LogDebug(" nRepos:", len(repos))
|
||||
|
||||
/* Check repositories in org to make sure they are included in project git */
|
||||
next_repo:
|
||||
for _, r := range repos {
|
||||
if DebugMode {
|
||||
log.Println(" -- checking", r.Name)
|
||||
}
|
||||
|
||||
if r.ObjectFormatName != "sha256" {
|
||||
if DebugMode {
|
||||
log.Println(" + ", r.ObjectFormatName, ". Needs to be sha256. Ignoring")
|
||||
}
|
||||
continue next_repo
|
||||
}
|
||||
|
||||
// for _, c := range configs {
|
||||
if gitPrj == r.Name {
|
||||
// ignore project gits
|
||||
@@ -390,43 +402,45 @@ next_repo:
|
||||
}
|
||||
}
|
||||
|
||||
if DebugMode {
|
||||
log.Println(" -- checking repository:", r.Name)
|
||||
}
|
||||
common.LogDebug(" -- checking repository:", r.Name)
|
||||
|
||||
if _, err := gitea.GetRecentCommits(org, r.Name, config.Branch, 1); err != nil {
|
||||
branch := config.Branch
|
||||
if len(branch) == 0 {
|
||||
branch = r.DefaultBranch
|
||||
if common.IsRemovedBranch(branch) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if commits, err := gitea.GetRecentCommits(org, r.Name, branch, 1); err != nil || len(commits) == 0 {
|
||||
// assumption that package does not exist, so not part of project
|
||||
// https://github.com/go-gitea/gitea/issues/31976
|
||||
|
||||
// or, we do not have commits here
|
||||
continue
|
||||
}
|
||||
|
||||
// add repository to git project
|
||||
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--depth", "1", r.CloneURL, r.Name))
|
||||
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", r.CloneURL, r.Name))
|
||||
|
||||
if len(config.Branch) > 0 {
|
||||
branch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, r.Name), "branch", "--show-current"))
|
||||
if branch != config.Branch {
|
||||
if err := git.GitExec(path.Join(gitPrj, r.Name), "fetch", "--depth", "1", "origin", config.Branch+":"+config.Branch); err != nil {
|
||||
return fmt.Errorf("Fetching branch %s for %s/%s failed. Ignoring.", config.Branch, repo.Owner.UserName, r.Name)
|
||||
}
|
||||
common.PanicOnError(git.GitExec(path.Join(gitPrj, r.Name), "checkout", config.Branch))
|
||||
curBranch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, r.Name), "branch", "--show-current"))
|
||||
if branch != curBranch {
|
||||
if err := git.GitExec(path.Join(gitPrj, r.Name), "fetch", "--depth", "1", "origin", branch+":"+branch); err != nil {
|
||||
return fmt.Errorf("Fetching branch %s for %s/%s failed. Ignoring.", branch, repo.Owner.UserName, r.Name)
|
||||
}
|
||||
common.PanicOnError(git.GitExec(path.Join(gitPrj, r.Name), "checkout", branch))
|
||||
}
|
||||
|
||||
isGitUpdated = true
|
||||
}
|
||||
|
||||
if isGitUpdated {
|
||||
common.PanicOnError(git.GitExec(gitPrj, "commit", "-a", "-m", "Automatic update via push via Direct Workflow -- SYNC"))
|
||||
common.PanicOnError(git.GitExec(gitPrj, "commit", "-a", "-m", "Periodic SYNC in Direct Workflow"))
|
||||
if !noop {
|
||||
git.GitExecOrPanic(gitPrj, "push", remoteName)
|
||||
}
|
||||
}
|
||||
|
||||
if DebugMode {
|
||||
log.Println("Verification finished for ", org, ", prjgit:", config.GitProjectName)
|
||||
}
|
||||
|
||||
common.LogInfo("Verification finished for ", org, ", prjgit:", config.GitProjectName)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -437,17 +451,17 @@ var checkInterval time.Duration
|
||||
func checkOrg(org string, configs []*common.AutogitConfig) {
|
||||
git, err := gh.CreateGitHandler(org)
|
||||
if err != nil {
|
||||
log.Println("Faield to allocate GitHandler:", err)
|
||||
common.LogError("Failed to allocate GitHandler:", err)
|
||||
return
|
||||
}
|
||||
defer git.Close()
|
||||
|
||||
for _, config := range configs {
|
||||
log.Printf(" ++ starting verification, org: `%s` config: `%s`\n", org, config.GitProjectName)
|
||||
common.LogInfo(" ++ starting verification, org:", org, "config:", config.GitProjectName)
|
||||
if err := verifyProjectState(git, org, config, configs); err != nil {
|
||||
log.Printf(" *** verification failed, org: `%s`, err: %#v\n", org, err)
|
||||
common.LogError(" *** verification failed, org:", org, err)
|
||||
} else {
|
||||
log.Printf(" ++ verification complete, org: `%s` config: `%s`\n", org, config.GitProjectName)
|
||||
common.LogError(" ++ verification complete, org:", org, config.GitProjectName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,7 +470,7 @@ func checkRepos() {
|
||||
for org, configs := range configuredRepos {
|
||||
if checkInterval > 0 {
|
||||
sleepInterval := checkInterval - checkInterval/2 + time.Duration(rand.Int63n(int64(checkInterval)))
|
||||
log.Println(" - sleep interval", sleepInterval, "until next check")
|
||||
common.LogInfo(" - sleep interval", sleepInterval, "until next check")
|
||||
time.Sleep(sleepInterval)
|
||||
}
|
||||
|
||||
@@ -468,9 +482,9 @@ func consistencyCheckProcess() {
|
||||
if checkOnStart {
|
||||
savedCheckInterval := checkInterval
|
||||
checkInterval = 0
|
||||
log.Println("== Startup consistency check begin...")
|
||||
common.LogInfo("== Startup consistency check begin...")
|
||||
checkRepos()
|
||||
log.Println("== Startup consistency check done...")
|
||||
common.LogInfo("== Startup consistency check done...")
|
||||
checkInterval = savedCheckInterval
|
||||
}
|
||||
|
||||
@@ -485,7 +499,8 @@ var gh common.GitHandlerGenerator
|
||||
func updateConfiguration(configFilename string, orgs *[]string) {
|
||||
configFile, err := common.ReadConfigFile(configFilename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
common.LogError(err)
|
||||
os.Exit(4)
|
||||
}
|
||||
|
||||
configs, _ := common.ResolveWorkflowConfigs(gitea, configFile)
|
||||
@@ -493,9 +508,7 @@ func updateConfiguration(configFilename string, orgs *[]string) {
|
||||
*orgs = make([]string, 0, 1)
|
||||
for _, c := range configs {
|
||||
if slices.Contains(c.Workflows, "direct") {
|
||||
if DebugMode {
|
||||
log.Printf(" + adding org: '%s', branch: '%s', prjgit: '%s'\n", c.Organization, c.Branch, c.GitProjectName)
|
||||
}
|
||||
common.LogDebug(" + adding org:", c.Organization, ", branch:", c.Branch, ", prjgit:", c.GitProjectName)
|
||||
configs := configuredRepos[c.Organization]
|
||||
if configs == nil {
|
||||
configs = make([]*common.AutogitConfig, 0, 1)
|
||||
@@ -509,7 +522,7 @@ func updateConfiguration(configFilename string, orgs *[]string) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFilename := flag.String("config", "", "List of PrjGit")
|
||||
configFilename := flag.String("config", "config.json", "List of PrjGit")
|
||||
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance")
|
||||
rabbitUrl := flag.String("url", "amqps://rabbit.opensuse.org", "URL for RabbitMQ instance")
|
||||
flag.BoolVar(&DebugMode, "debug", false, "Extra debugging information")
|
||||
@@ -520,10 +533,35 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
if err := common.RequireGiteaSecretToken(); err != nil {
|
||||
log.Fatal(err)
|
||||
common.LogError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := common.RequireRabbitSecrets(); err != nil {
|
||||
log.Fatal(err)
|
||||
common.LogError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if cf := os.Getenv("AUTOGITS_CONFIG"); len(cf) > 0 {
|
||||
*configFilename = cf
|
||||
}
|
||||
if url := os.Getenv("AUTOGITS_URL"); len(url) > 0 {
|
||||
*giteaUrl = url
|
||||
}
|
||||
if url := os.Getenv("AUTOGITS_RABBITURL"); len(url) > 0 {
|
||||
*rabbitUrl = url
|
||||
}
|
||||
if debug := os.Getenv("AUTOGITS_DEBUG"); len(debug) > 0 {
|
||||
DebugMode = true
|
||||
}
|
||||
if check := os.Getenv("AUTOGITS_CHECK_ON_START"); len(check) > 0 {
|
||||
checkOnStart = true
|
||||
}
|
||||
if p := os.Getenv("AUTOGITS_REPO_PATH"); len(p) > 0 {
|
||||
*basePath = p
|
||||
}
|
||||
|
||||
if DebugMode {
|
||||
common.SetLoggingLevel(common.LogLevelDebug)
|
||||
}
|
||||
|
||||
defs := &common.RabbitMQGiteaEventsProcessor{}
|
||||
@@ -532,12 +570,14 @@ func main() {
|
||||
if len(*basePath) == 0 {
|
||||
*basePath, err = os.MkdirTemp(os.TempDir(), AppName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
common.LogError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
gh, err = common.AllocateGitWorkTree(*basePath, GitAuthor, GitEmail)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
common.LogError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// handle reconfiguration
|
||||
@@ -552,10 +592,10 @@ func main() {
|
||||
}
|
||||
|
||||
if sig != syscall.SIGHUP {
|
||||
log.Println("Unexpected signal received:", sig)
|
||||
common.LogError("Unexpected signal received:", sig)
|
||||
continue
|
||||
}
|
||||
log.Println("*** Reconfiguring ***")
|
||||
common.LogError("*** Reconfiguring ***")
|
||||
updateConfiguration(*configFilename, &defs.Orgs)
|
||||
defs.Connection().UpdateTopics(defs)
|
||||
}
|
||||
@@ -567,23 +607,25 @@ func main() {
|
||||
gitea = common.AllocateGiteaTransport(*giteaUrl)
|
||||
CurrentUser, err := gitea.GetCurrentUser()
|
||||
if err != nil {
|
||||
log.Fatalln("Cannot fetch current user:", err)
|
||||
common.LogError("Cannot fetch current user:", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
log.Println("Current User:", CurrentUser.UserName)
|
||||
common.LogInfo("Current User:", CurrentUser.UserName)
|
||||
|
||||
updateConfiguration(*configFilename, &defs.Orgs)
|
||||
|
||||
defs.Connection().RabbitURL, err = url.Parse(*rabbitUrl)
|
||||
if err != nil {
|
||||
log.Panicf("cannot parse server URL. Err: %#v\n", err)
|
||||
common.LogError("cannot parse server URL. Err:", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
go consistencyCheckProcess()
|
||||
log.Println("defs:", *defs)
|
||||
common.LogInfo("defs:", *defs)
|
||||
|
||||
defs.Handlers = make(map[string]common.RequestProcessor)
|
||||
defs.Handlers[common.RequestType_Push] = &PushActionProcessor{}
|
||||
defs.Handlers[common.RequestType_Repository] = &RepositoryActionProcessor{}
|
||||
|
||||
log.Fatal(common.ProcessRabbitMQEvents(defs))
|
||||
common.LogError(common.ProcessRabbitMQEvents(defs))
|
||||
}
|
||||
|
||||
@@ -1,54 +1,65 @@
|
||||
Workflow-PR bot
|
||||
===============
|
||||
|
||||
Keeps ProjectGit PR in-sync with a PackageGit PR
|
||||
|
||||
|
||||
Areas of Responsibility
|
||||
-----------------------
|
||||
|
||||
* Detects a PackageGit PR creation against a package and creates a coresponsing PR against the ProjectGit
|
||||
* When a PackageGit PR is updated, the corresponding PR against the ProjectGit is updated
|
||||
* Stores reference to the PackageGit PR in the headers of the ProjectGit PR comments, for later reference
|
||||
* this allows ProjectGit PR to be merged to seperated later (via another tool, for example)
|
||||
* Initiates all staging workflows via review requests
|
||||
|
||||
Keeps ProjectGit PRs in-sync with the relative PackageGit PRs.
|
||||
|
||||
Target Usage
|
||||
------------
|
||||
|
||||
Any project (devel, etc) that accepts PR
|
||||
Any project (devel, codestream, product, etc.) that accepts PRs.
|
||||
|
||||
|
||||
Main Tasks
|
||||
----------
|
||||
|
||||
* **Synchronization**:
|
||||
* When a **PackageGit PR** is created for a package on a specific project branch, a corresponding PR is automatically generated in **ProjectGit**.
|
||||
* When a PackageGit PR is updated, the corresponding PR against the ProjectGit is updated.
|
||||
* A link to the PackageGit PR is stored in the body of the ProjectGit PR comments in the following format:
|
||||
* `PR: organization/package_name!pull_request_number`
|
||||
* Example: `PR: pool/curl!4`
|
||||
* It closes an empty ProjectGit PR (e.g., if a PR was initially created for a single package but later integrated into a larger ProjectGit PR).
|
||||
* It forwards the Work In Progress (WIP) flag to the ProjectGit PR. If the ProjectGit PR references multiple Package PRs, triggering the WIP flag on the ProjectGit PR side only requires a single WIP package PR.
|
||||
|
||||
* **Reviewer Management**:
|
||||
* It adds required reviewers in the ProjectGit PR.
|
||||
* It adds required reviewers in the PackageGit PR.
|
||||
* If new commits are added to a PackageGit PR, reviewers who have already approved it will be re-added.
|
||||
|
||||
* **Merge Management**:
|
||||
* Manages PR merges based on configuration flags (`ManualMergeOnly`, `ManualMergeProject`).
|
||||
* In general, merge only happens if all mandatory reviews are completed.
|
||||
* **ManualMergeProject** is stricter than **ManualMergeOnly** and has higher priority.
|
||||
|
||||
| Flag | Value | Behavior |
|
||||
| ----- | ----- | ----- |
|
||||
| ManualMergeProject | true | Both ProjectGit and PackageGit PRs are merged upon an allowed project maintainer commenting "merge ok” in the ProjectGit PR. |
|
||||
| ManualMergeOnly | true | Both PackageGit PR and ProjectGit PR are merged upon an allowed package maintainer or project maintainer commenting “merge ok” in the PackageGit PR. |
|
||||
| ManualMergeOnly and ManualMergeProject | false | Both ProjectGit and PackageGit PRs are merged as soon as all reviews are completed in both PrjGit and PkgGit PRs. |
|
||||
|
||||
Config file
|
||||
-----------
|
||||
JSON
|
||||
* _Workflows_: ["pr"] -- pr entry enables pr workflow. **Mandatory**
|
||||
* _Organization_: organization that holds all the packages **Mandatory**
|
||||
* _Branch_: branch updated in repo's **Mandatory**
|
||||
* _GitProjectName_: package in above org, or `org/package#branch` for PrjGit. By default assumes `_ObsPrj` with default branch and in the `Organization`
|
||||
* _Reviewers_: accounts associated with mandatory reviews for PrjGit. Can trigger additional
|
||||
review requests for PrjGit or associated PkgGit repos. Only when all reviews are
|
||||
satisfied, will the PrjGit PR be merged. See Reviewers below.
|
||||
* _ManualMergeOnly_: (true, false) only merge if "merge ok" comment/review by package or project maintainers or reviewers
|
||||
* _ManualMergeProject_: (true, false) only merge if "merge ok" by project maintainers or reviewers
|
||||
* _ReviewRequired_: (true, false) ignores that submitter is a maintainer and require a review from other maintainer IFF available
|
||||
* _NoProjectGitPR_: (true, false) do not create PrjGit PRs, but still process reviews, etc.
|
||||
* _Permissions_: permissions and associated accounts/groups. See below.
|
||||
|
||||
NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
|
||||
example:
|
||||
* Filename: `workflow.config`
|
||||
* Location: ProjectGit
|
||||
* Format: non-standard JSON (comments allowed)
|
||||
|
||||
| Field name | Details | Mandatory | Type | Allowed Values | Default |
|
||||
| ----- | ----- | ----- | ----- | ----- | ----- |
|
||||
| *Workflows* | Type of workflow | yes | string | “pr” | |
|
||||
| *Organization* | The organization where PackageGit PRs are expected to occur | yes | string | | |
|
||||
| *Branch* | The designated branch for PackageGit PRs | yes | string | | |
|
||||
| *GitProjectName* | Repository and branch where the ProjectGit lives. | no | string | **Format**: `org/project_repo#branch` | By default assumes `_ObsPrj` with default branch in the *Organization* |
|
||||
| *ManualMergeOnly* | Merges are permitted only upon receiving a "merge ok" comment from designated maintainers in the PkgGit PR. | no | bool | true, false | false |
|
||||
| *ManualMergeProject* | Merges are permitted only upon receiving a "merge ok" comment in the ProjectGit PR from project maintainers. | no | bool | true, false | false |
|
||||
| *ReviewRequired* | If submitter is a maintainer, require review from another maintainer if available. | no | bool | true, false | false |
|
||||
| *NoProjectGitPR* | Do not create PrjGit PR, but still perform other tasks. | no | bool | true, false | false |
|
||||
| *Reviewers* | PrjGit reviewers. Additional review requests are triggered for associated PkgGit PRs. PrjGit PR is merged only when all reviews are complete. | no | array of strings | | `[]` |
|
||||
| *ReviewGroups* | If a group is specified in Reviewers, its members are listed here. | no | array of objects | | `[]` |
|
||||
| *ReviewGroups > Name* | Name of the group | no | string | | |
|
||||
| *ReviewGroups > Reviewers* | Members of the group | no | array of strings | | |
|
||||
| *ReviewGroups > Silent* | Add members for notifications. If true, members are not explicitly requested to review. If one member approves, others are removed. | no | bool | true, false | false |
|
||||
|
||||
[
|
||||
{
|
||||
"Workflows": ["pr", "direct"],
|
||||
"Organization": "autogits",
|
||||
"GitProjectName": "HiddenPrj",
|
||||
"Branch": "hidden",
|
||||
"Reviewers": []
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
Reviewers
|
||||
---------
|
||||
@@ -57,39 +68,72 @@ Reviews is a list of accounts that need to review package and/or project. They h
|
||||
|
||||
[~][*|-|+]username
|
||||
|
||||
General prefix of ~ indicates advisory reviewer. They will be requested, but ignored otherwise.
|
||||
A tilde (`~`) before a prefix signifies an advisory reviewer. Their input is requested, but their review status will not otherwise affect the process.
|
||||
|
||||
Other prefixes indicate project or package association of the reviewer:
|
||||
|
||||
* `*` indicates project *and* package
|
||||
* `-` indicates project-only reviewer
|
||||
* `+` indicates package-only reviewer
|
||||
|
||||
`+` is implied. For example
|
||||
`+` is implied.
|
||||
|
||||
`[foo, -bar, ~*moo]`
|
||||
For example: `[foo, -bar, ~*moo]` results in:
|
||||
* foo: package reviews
|
||||
* bar: project reviews
|
||||
* moo: package and project reviews, but ignored
|
||||
|
||||
results in
|
||||
* foo -> package reviews
|
||||
* bar -> project reviews
|
||||
* moo -> package and project reviews, but ignored
|
||||
Package Deletion Requests
|
||||
-------------------------
|
||||
(NOT YET IMPLEMENTED)
|
||||
|
||||
* **Removing a Package:**
|
||||
To remove a package from a project, submit a ProjectGit Pull Request (PR) that removes the corresponding submodule. The bot will then rename the project branch in the pool by appending "-removed" to its name.
|
||||
|
||||
* **Adding a Package Again:**
|
||||
If you wish to re-add a package, create a new PrjGit PR which adds again the submodule on the branch that has the "-removed" suffix. The bot will automatically remove this suffix from the project branch in the pool.
|
||||
|
||||
|
||||
Labels
|
||||
------
|
||||
|
||||
The following labels are used, when defined in Repo/Org.
|
||||
|
||||
| Label Config Entry | Default label | Description
|
||||
|--------------------|----------------|----------------------------------------
|
||||
| StagingAuto | staging/Auto | Assigned to Project Git PRs when first staged
|
||||
| ReviewPending | review/Pending | Assigned to Project Git PR when package reviews are still pending
|
||||
| ReviewDone | review/Done | Assigned to Project Git PR when reviews are complete on all package PRs
|
||||
|
||||
|
||||
Maintainership
|
||||
--------------
|
||||
|
||||
Maintainership information is defined per project. For reviews, package maintainers are coalesced
|
||||
with project maintainers. A review by any of the maintainers is acceptable.
|
||||
Filename: \_maintainership.json
|
||||
Location: ProjectGit
|
||||
Format: JSON
|
||||
Fields:
|
||||
|
||||
example:
|
||||
| Key | Value | Notes |
|
||||
| ----- | ----- | ----- |
|
||||
| package name | array of strings representing the package maintainers | List of package maintainers |
|
||||
| “” (empty string) | array of strings representing the project maintainers | List of project maintainers |
|
||||
|
||||
{
|
||||
"package1": [ "reviewer", "reviewer2"],
|
||||
"package2": [],
|
||||
Maintainership information is defined per project. For PackageGit PR reviews, package maintainers are combined with project maintainers. A review by any of these maintainers is acceptable.
|
||||
|
||||
// "project" maintainer
|
||||
"": ["reviewer3", "reviewer4"]
|
||||
}
|
||||
If the submitter is a maintainer it will not get a review requested.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
{
|
||||
"package1": [ "reviewer", "reviewer2"],
|
||||
"package2": [],
|
||||
|
||||
// "project" maintainer
|
||||
"": ["reviewer3", "reviewer4"]
|
||||
}
|
||||
```
|
||||
|
||||
Permissions
|
||||
-----------
|
||||
@@ -109,3 +153,11 @@ the `workflow.config`.
|
||||
|
||||
NOTE: Project Maintainers have these permissions automatically.
|
||||
|
||||
Server configuration
|
||||
--------------------------
|
||||
|
||||
**Configuration file:**
|
||||
|
||||
| Field | Type | Notes |
|
||||
| ----- | ----- | ----- |
|
||||
| root | Array of string | Format **org/repo\#branch** |
|
||||
|
||||
@@ -7,7 +7,7 @@ import "src.opensuse.org/autogits/common"
|
||||
|
||||
type StateChecker interface {
|
||||
VerifyProjectState(configs *common.AutogitConfig) ([]*PRToProcess, error)
|
||||
CheckRepos() error
|
||||
CheckRepos()
|
||||
ConsistencyCheckProcess() error
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ func main() {
|
||||
common.RequestType_PRSync: req,
|
||||
common.RequestType_PRReviewAccepted: req,
|
||||
common.RequestType_PRReviewRejected: req,
|
||||
common.RequestType_IssueComment: req,
|
||||
common.RequestType_PRComment: req,
|
||||
},
|
||||
}
|
||||
listenDefs.Connection().RabbitURL, _ = url.Parse(*rabbitUrl)
|
||||
|
||||
@@ -41,6 +41,9 @@ func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
|
||||
slices.Sort(title_refs)
|
||||
slices.Sort(refs)
|
||||
|
||||
title = "Forwarded PRs: " + strings.Join(title_refs, ", ")
|
||||
desc = fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor) + strings.Join(refs, "\n") + "\n"
|
||||
|
||||
@@ -227,12 +230,18 @@ func (pr *PRProcessor) CreatePRjGitPR(prjGitPRbranch string, prset *common.PRSet
|
||||
}
|
||||
|
||||
title, desc := PrjGitDescription(prset)
|
||||
pr, err := Gitea.CreatePullRequestIfNotExist(PrjGit, prjGitPRbranch, PrjGitBranch, title, desc)
|
||||
pr, err, isNew := Gitea.CreatePullRequestIfNotExist(PrjGit, prjGitPRbranch, PrjGitBranch, title, desc)
|
||||
if err != nil {
|
||||
common.LogError("Error creating PrjGit PR:", err)
|
||||
return err
|
||||
}
|
||||
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, pr.Index, &models.EditPullRequestOption{
|
||||
org := PrjGit.Owner.UserName
|
||||
repo := PrjGit.Name
|
||||
idx := pr.Index
|
||||
if isNew {
|
||||
Gitea.SetLabels(org, repo, idx, []string{prset.Config.Label(common.Label_StagingAuto)})
|
||||
}
|
||||
Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
|
||||
RemoveDeadline: true,
|
||||
})
|
||||
|
||||
@@ -269,6 +278,7 @@ func (pr *PRProcessor) RebaseAndSkipSubmoduleCommits(prset *common.PRSet, branch
|
||||
}
|
||||
|
||||
var updatePrjGitError_requeue error = errors.New("Commits do not match. Requeing after 5 seconds.")
|
||||
|
||||
func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
|
||||
_, _, PrjGitBranch := prset.Config.GetPrjGit()
|
||||
PrjGitPR, err := prset.GetPrjGitPR()
|
||||
@@ -345,7 +355,20 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
|
||||
}
|
||||
|
||||
// update PR
|
||||
if PrjGitPR.PR.Body != PrjGitBody || PrjGitPR.PR.Title != PrjGitTitle {
|
||||
isPrTitleSame := func(CurrentTitle, NewTitle string) bool {
|
||||
ctlen := len(CurrentTitle)
|
||||
for _, suffix := range []string{"...", "…"} {
|
||||
slen := len(suffix)
|
||||
if ctlen > 250 && strings.HasSuffix(CurrentTitle, suffix) && len(NewTitle) > ctlen {
|
||||
NewTitle = NewTitle[0:ctlen-slen] + suffix
|
||||
if CurrentTitle == NewTitle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return CurrentTitle == NewTitle
|
||||
}
|
||||
if PrjGitPR.PR.User.UserName == CurrentUser.UserName && (PrjGitPR.PR.Body != PrjGitBody || !isPrTitleSame(PrjGitPR.PR.Title, PrjGitTitle)) {
|
||||
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
|
||||
RemoveDeadline: true,
|
||||
Title: PrjGitTitle,
|
||||
@@ -381,6 +404,10 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
||||
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
|
||||
prjGitPR, err := prset.GetPrjGitPR()
|
||||
if err == common.PRSet_PrjGitMissing {
|
||||
if req.State != "open" {
|
||||
common.LogDebug("This PR is closed and no ProjectGit PR. Ignoring.")
|
||||
return nil
|
||||
}
|
||||
common.LogDebug("Missing PrjGit. Need to create one under branch", prjGitPRbranch)
|
||||
|
||||
if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil {
|
||||
@@ -473,7 +500,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
||||
// make sure that prjgit is consistent and only submodules that are to be *updated*
|
||||
// reset anything that changed that is not part of the prset
|
||||
// package removals/additions are *not* counted here
|
||||
org, repo, branch := config.GetPrjGit()
|
||||
|
||||
// TODO: this is broken...
|
||||
if pr, err := prset.GetPrjGitPR(); err == nil && false {
|
||||
common.LogDebug("Submodule parse begin")
|
||||
@@ -522,11 +549,19 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
||||
} else {
|
||||
common.LogInfo("* No prjgit")
|
||||
}
|
||||
maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch)
|
||||
maintainers, err := common.FetchProjectMaintainershipData(Gitea, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update prset if we should build it or not
|
||||
if prjGitPR != nil {
|
||||
if file, err := git.GitCatFile(common.DefaultGitPrj, prjGitPR.PR.Head.Sha, "staging.config"); err == nil {
|
||||
prset.HasAutoStaging = (file != nil)
|
||||
common.LogDebug(" -> automatic staging enabled?:", prset.HasAutoStaging)
|
||||
}
|
||||
}
|
||||
|
||||
// handle case where PrjGit PR is only one left and there are no changes, then we can just close the PR
|
||||
if len(prset.PRs) == 1 && prjGitPR != nil && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
|
||||
common.LogDebug(" --> checking if superflous PR")
|
||||
@@ -572,7 +607,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
||||
|
||||
type RequestProcessor struct {
|
||||
configuredRepos map[string][]*common.AutogitConfig
|
||||
recursive int
|
||||
recursive int
|
||||
}
|
||||
|
||||
func ProcesPullRequest(pr *models.PullRequest, configs []*common.AutogitConfig) error {
|
||||
@@ -613,7 +648,7 @@ func (w *RequestProcessor) ProcessFunc(request *common.Request) (err error) {
|
||||
common.LogError("Cannot find PR for issue:", req.Pull_Request.Base.Repo.Owner.Username, req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number)
|
||||
return err
|
||||
}
|
||||
} else if req, ok := request.Data.(*common.IssueWebhookEvent); ok {
|
||||
} else if req, ok := request.Data.(*common.IssueCommentWebhookEvent); ok {
|
||||
pr, err = Gitea.GetPullRequest(req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
|
||||
if err != nil {
|
||||
common.LogError("Cannot find PR for issue:", req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
|
||||
|
||||
@@ -103,7 +103,7 @@ func TestOpenPR(t *testing.T) {
|
||||
}
|
||||
// gitea.EXPECT().GetAssociatedPrjGitPR("test", "prjcopy", "test", "testRepo", int64(1)).Return(nil, nil)
|
||||
gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil)
|
||||
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil)
|
||||
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil, true)
|
||||
gitea.EXPECT().GetPullRequest("test", "testRepo", int64(1)).Return(giteaPR, nil)
|
||||
gitea.EXPECT().RequestReviews(giteaPR, "reviewer1", "reviewer2").Return(nil, nil)
|
||||
gitea.EXPECT().GetPullRequestReviews("test", "testRepo", int64(0)).Return([]*models.PullReview{}, nil)
|
||||
@@ -153,7 +153,7 @@ func TestOpenPR(t *testing.T) {
|
||||
}
|
||||
failedErr := errors.New("Returned error here")
|
||||
gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil)
|
||||
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, failedErr)
|
||||
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, failedErr, false)
|
||||
|
||||
err := pr.Process(event)
|
||||
if err != failedErr {
|
||||
@@ -193,7 +193,7 @@ func TestOpenPR(t *testing.T) {
|
||||
gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil)
|
||||
gitea.EXPECT().GetPullRequest("test", "testRepo", int64(1)).Return(giteaPR, nil)
|
||||
gitea.EXPECT().GetPullRequestReviews("org", "SomeRepo", int64(13)).Return([]*models.PullReview{}, nil)
|
||||
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil)
|
||||
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil, true)
|
||||
gitea.EXPECT().RequestReviews(giteaPR, "reviewer1", "reviewer2").Return(nil, failedErr)
|
||||
|
||||
gitea.EXPECT().FetchMaintainershipDirFile("test", "prjcopy", "branch", "_project").Return(nil, "", repository.NewRepoGetRawFileNotFound())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path"
|
||||
@@ -43,6 +42,15 @@ func pullRequestToEventState(state models.StateType) string {
|
||||
}
|
||||
|
||||
func (s *DefaultStateChecker) ProcessPR(pr *models.PullRequest, config *common.AutogitConfig) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.LogError("panic caught in ProcessPR", common.PRtoString(pr))
|
||||
if err, ok := r.(error); !ok {
|
||||
common.LogError(err)
|
||||
}
|
||||
common.LogError(string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
return ProcesPullRequest(pr, common.AutogitConfigs{config})
|
||||
}
|
||||
|
||||
@@ -151,7 +159,7 @@ func (s *DefaultStateChecker) VerifyProjectState(config *common.AutogitConfig) (
|
||||
return PrjGitSubmoduleCheck(config, git, prjGitRepo, submodules)
|
||||
}
|
||||
|
||||
func (s *DefaultStateChecker) CheckRepos() error {
|
||||
func (s *DefaultStateChecker) CheckRepos() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.LogError("panic caught")
|
||||
@@ -161,7 +169,6 @@ func (s *DefaultStateChecker) CheckRepos() error {
|
||||
common.LogError(string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
errorList := make([]error, 0, 10)
|
||||
|
||||
for org, configs := range s.processor.configuredRepos {
|
||||
for _, config := range configs {
|
||||
@@ -175,12 +182,12 @@ func (s *DefaultStateChecker) CheckRepos() error {
|
||||
prs, err := s.i.VerifyProjectState(config)
|
||||
if err != nil {
|
||||
common.LogError(" *** verification failed, org:", org, err)
|
||||
errorList = append(errorList, err)
|
||||
}
|
||||
for _, pr := range prs {
|
||||
prs, err := Gitea.GetRecentPullRequests(pr.Org, pr.Repo, pr.Branch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error fetching pull requests for %s/%s#%s. Err: %w", pr.Org, pr.Repo, pr.Branch, err)
|
||||
common.LogError("Error fetching pull requests for", fmt.Sprintf("%s/%s#%s", pr.Org, pr.Repo, pr.Branch), err)
|
||||
break
|
||||
}
|
||||
if len(prs) > 0 {
|
||||
common.LogDebug(fmt.Sprintf("%s/%s#%s", pr.Org, pr.Repo, pr.Branch), " - # of PRs to check:", len(prs))
|
||||
@@ -193,9 +200,11 @@ func (s *DefaultStateChecker) CheckRepos() error {
|
||||
|
||||
common.LogInfo(" ++ verification complete, org:", org, "config:", config.GitProjectName)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errorList...)
|
||||
if len(configs) == 0 {
|
||||
common.LogError(" org:", org, "has 0 configs?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DefaultStateChecker) ConsistencyCheckProcess() error {
|
||||
|
||||
Reference in New Issue
Block a user