Compare commits
14 Commits
gitea-api-
...
reparent
| Author | SHA256 | Date | |
|---|---|---|---|
| e78db48ba2 | |||
| 8678167498 | |||
| 842284505b | |||
| 2d52659223 | |||
| e1af02efd9 | |||
| cc1f178872 | |||
| 40f3cfe238 | |||
| 6e9f1c8073 | |||
| 443b7eca24 | |||
| c355624194 | |||
| 2bd6321fb6 | |||
| 67d57c4eae | |||
| 5abb1773db | |||
| 2fb18c4641 |
@@ -11,6 +11,7 @@ The bots that drive Git Workflow for package management
|
||||
* obs-forward-bot -- forwards PR as OBS sr (TODO)
|
||||
* obs-staging-bot -- build bot for a PR
|
||||
* obs-status-service -- report build status of an OBS project as an SVG
|
||||
* reparent-bot -- creates new repositories as reverse-forks of source repositories. Used to add new packages to projects.
|
||||
* workflow-pr -- keeps PR to _ObsPrj consistent with a PR to a package update
|
||||
* workflow-direct -- update _ObsPrj based on direct pushes and repo creations/removals from organization
|
||||
* staging-utils -- review tooling for PR (TODO)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
func TestLabelKey(t *testing.T) {
|
||||
@@ -53,7 +54,7 @@ func TestConfigLabelParser(t *testing.T) {
|
||||
DefaultBranch: "master",
|
||||
}
|
||||
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.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)
|
||||
@@ -175,7 +176,7 @@ func TestConfigWorkflowParser(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
gitea := mock_common.NewMockGiteaFileContentAndRepoFetcher(ctl)
|
||||
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.config_json), "abc", nil)
|
||||
gitea.EXPECT().GetRepository("foo", "bar").Return(&test.repo, nil)
|
||||
|
||||
@@ -79,6 +79,16 @@ type GiteaIssueFetcher interface {
|
||||
GetIssue(org, repo string, idx int64) (*models.Issue, error)
|
||||
}
|
||||
|
||||
const (
|
||||
IssueType_All = 0
|
||||
IssueType_Issue = 1
|
||||
IssueType_PR = 2
|
||||
)
|
||||
|
||||
type GiteaIssueLister interface {
|
||||
GetOpenIssues(org, repo, labels string, issue_type int, q string) ([]*models.Issue, error)
|
||||
}
|
||||
|
||||
type GiteaTimelineFetcher interface {
|
||||
ResetTimelineCache(org, repo string, idx int64)
|
||||
GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error)
|
||||
@@ -179,6 +189,10 @@ type GiteaMerger interface {
|
||||
ManualMergePR(org, repo string, id int64, commitid string, delBranch bool) error
|
||||
}
|
||||
|
||||
type GiteaReparenter interface {
|
||||
ReparentRepository(owner, repo, org string) (*models.Repository, error)
|
||||
}
|
||||
|
||||
type Gitea interface {
|
||||
GiteaComment
|
||||
GiteaRepoFetcher
|
||||
@@ -188,6 +202,7 @@ type Gitea interface {
|
||||
GiteaPRFetcher
|
||||
GiteaPRUpdater
|
||||
GiteaMerger
|
||||
GiteaReparenter
|
||||
GiteaCommitFetcher
|
||||
GiteaReviewFetcher
|
||||
GiteaCommentFetcher
|
||||
@@ -200,6 +215,7 @@ type Gitea interface {
|
||||
GiteaLabelGetter
|
||||
GiteaLabelSettter
|
||||
GiteaIssueFetcher
|
||||
GiteaIssueLister
|
||||
|
||||
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
|
||||
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
|
||||
@@ -281,6 +297,23 @@ func (gitea *GiteaTransport) UpdatePullRequest(org, repo string, num int64, opti
|
||||
return pr.Payload, err
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) ReparentRepository(owner, repo, org string) (*models.Repository, error) {
|
||||
params := repository.NewCreateForkParams().
|
||||
WithOwner(owner).
|
||||
WithRepo(repo).
|
||||
WithBody(&models.CreateForkOption{
|
||||
Organization: org,
|
||||
Reparent: true,
|
||||
})
|
||||
|
||||
res, err := gitea.client.Repository.CreateFork(params, gitea.transport.DefaultAuthentication)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) ManualMergePR(org, repo string, num int64, commitid string, delBranch bool) error {
|
||||
manual_merge := "manually-merged"
|
||||
_, err := gitea.client.Repository.RepoMergePullRequest(
|
||||
@@ -528,6 +561,32 @@ func (gitea *GiteaTransport) UpdateIssue(owner, repo string, idx int64, options
|
||||
return ret.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetOpenIssues(org, repo, labels string, filter_type int, q string) ([]*models.Issue, error) {
|
||||
open := "open"
|
||||
issue_type := "issue"
|
||||
pr_type := "pull_request"
|
||||
params := issue.NewIssueListIssuesParams().WithOwner(org).WithRepo(repo).WithState(&open)
|
||||
if len(labels) > 0 {
|
||||
params = params.WithLabels(&labels)
|
||||
}
|
||||
switch filter_type {
|
||||
case IssueType_Issue:
|
||||
params = params.WithType(&issue_type)
|
||||
case IssueType_PR:
|
||||
params = params.WithType(&pr_type)
|
||||
}
|
||||
if len(q) != 0 {
|
||||
params = params.WithQ(&q)
|
||||
}
|
||||
|
||||
ret, err := gitea.client.Issue.IssueListIssues(params, gitea.transport.DefaultAuthentication)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret.Payload, nil
|
||||
}
|
||||
|
||||
const (
|
||||
GiteaNotificationType_Pull = "Pull"
|
||||
)
|
||||
@@ -842,6 +901,7 @@ func (gitea *GiteaTransport) ResetTimelineCache(org, repo string, idx int64) {
|
||||
Cache, IsCached := giteaTimelineCache[prID]
|
||||
if IsCached {
|
||||
Cache.lastCheck = Cache.lastCheck.Add(-time.Hour)
|
||||
giteaTimelineCache[prID] = Cache
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/client/repository"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
func TestMaintainership(t *testing.T) {
|
||||
@@ -172,7 +173,7 @@ func TestMaintainership(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run(test.name+"_File", func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
mi := mock_common.NewMockGiteaMaintainershipReader(ctl)
|
||||
|
||||
// tests with maintainership file
|
||||
@@ -185,7 +186,7 @@ func TestMaintainership(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run(test.name+"_Dir", func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
mi := mock_common.NewMockGiteaMaintainershipReader(ctl)
|
||||
|
||||
// run same tests with directory maintainership data
|
||||
|
||||
@@ -207,6 +207,69 @@ func (c *MockGiteaIssueFetcherGetIssueCall) DoAndReturn(f func(string, string, i
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaIssueLister is a mock of GiteaIssueLister interface.
|
||||
type MockGiteaIssueLister struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGiteaIssueListerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockGiteaIssueListerMockRecorder is the mock recorder for MockGiteaIssueLister.
|
||||
type MockGiteaIssueListerMockRecorder struct {
|
||||
mock *MockGiteaIssueLister
|
||||
}
|
||||
|
||||
// NewMockGiteaIssueLister creates a new mock instance.
|
||||
func NewMockGiteaIssueLister(ctrl *gomock.Controller) *MockGiteaIssueLister {
|
||||
mock := &MockGiteaIssueLister{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaIssueListerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGiteaIssueLister) EXPECT() *MockGiteaIssueListerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetOpenIssues mocks base method.
|
||||
func (m *MockGiteaIssueLister) GetOpenIssues(org, repo, labels string, issue_type int, q string) ([]*models.Issue, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOpenIssues", org, repo, labels, issue_type, q)
|
||||
ret0, _ := ret[0].([]*models.Issue)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOpenIssues indicates an expected call of GetOpenIssues.
|
||||
func (mr *MockGiteaIssueListerMockRecorder) GetOpenIssues(org, repo, labels, issue_type, q any) *MockGiteaIssueListerGetOpenIssuesCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOpenIssues", reflect.TypeOf((*MockGiteaIssueLister)(nil).GetOpenIssues), org, repo, labels, issue_type, q)
|
||||
return &MockGiteaIssueListerGetOpenIssuesCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaIssueListerGetOpenIssuesCall wrap *gomock.Call
|
||||
type MockGiteaIssueListerGetOpenIssuesCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaIssueListerGetOpenIssuesCall) Return(arg0 []*models.Issue, arg1 error) *MockGiteaIssueListerGetOpenIssuesCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaIssueListerGetOpenIssuesCall) Do(f func(string, string, string, int, string) ([]*models.Issue, error)) *MockGiteaIssueListerGetOpenIssuesCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaIssueListerGetOpenIssuesCall) DoAndReturn(f func(string, string, string, int, string) ([]*models.Issue, error)) *MockGiteaIssueListerGetOpenIssuesCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaTimelineFetcher is a mock of GiteaTimelineFetcher interface.
|
||||
type MockGiteaTimelineFetcher struct {
|
||||
ctrl *gomock.Controller
|
||||
@@ -2458,6 +2521,69 @@ func (c *MockGiteaMergerManualMergePRCall) DoAndReturn(f func(string, string, in
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGiteaReparenter is a mock of GiteaReparenter interface.
|
||||
type MockGiteaReparenter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGiteaReparenterMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockGiteaReparenterMockRecorder is the mock recorder for MockGiteaReparenter.
|
||||
type MockGiteaReparenterMockRecorder struct {
|
||||
mock *MockGiteaReparenter
|
||||
}
|
||||
|
||||
// NewMockGiteaReparenter creates a new mock instance.
|
||||
func NewMockGiteaReparenter(ctrl *gomock.Controller) *MockGiteaReparenter {
|
||||
mock := &MockGiteaReparenter{ctrl: ctrl}
|
||||
mock.recorder = &MockGiteaReparenterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGiteaReparenter) EXPECT() *MockGiteaReparenterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ReparentRepository mocks base method.
|
||||
func (m *MockGiteaReparenter) ReparentRepository(owner, repo, org string) (*models.Repository, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReparentRepository", owner, repo, org)
|
||||
ret0, _ := ret[0].(*models.Repository)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReparentRepository indicates an expected call of ReparentRepository.
|
||||
func (mr *MockGiteaReparenterMockRecorder) ReparentRepository(owner, repo, org any) *MockGiteaReparenterReparentRepositoryCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReparentRepository", reflect.TypeOf((*MockGiteaReparenter)(nil).ReparentRepository), owner, repo, org)
|
||||
return &MockGiteaReparenterReparentRepositoryCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaReparenterReparentRepositoryCall wrap *gomock.Call
|
||||
type MockGiteaReparenterReparentRepositoryCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaReparenterReparentRepositoryCall) Return(arg0 *models.Repository, arg1 error) *MockGiteaReparenterReparentRepositoryCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaReparenterReparentRepositoryCall) Do(f func(string, string, string) (*models.Repository, error)) *MockGiteaReparenterReparentRepositoryCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaReparenterReparentRepositoryCall) DoAndReturn(f func(string, string, string) (*models.Repository, error)) *MockGiteaReparenterReparentRepositoryCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockGitea is a mock of Gitea interface.
|
||||
type MockGitea struct {
|
||||
ctrl *gomock.Controller
|
||||
@@ -3030,6 +3156,45 @@ func (c *MockGiteaGetNotificationsCall) DoAndReturn(f func(string, *time.Time) (
|
||||
return c
|
||||
}
|
||||
|
||||
// GetOpenIssues mocks base method.
|
||||
func (m *MockGitea) GetOpenIssues(org, repo, labels string, issue_type int, q string) ([]*models.Issue, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOpenIssues", org, repo, labels, issue_type, q)
|
||||
ret0, _ := ret[0].([]*models.Issue)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOpenIssues indicates an expected call of GetOpenIssues.
|
||||
func (mr *MockGiteaMockRecorder) GetOpenIssues(org, repo, labels, issue_type, q any) *MockGiteaGetOpenIssuesCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOpenIssues", reflect.TypeOf((*MockGitea)(nil).GetOpenIssues), org, repo, labels, issue_type, q)
|
||||
return &MockGiteaGetOpenIssuesCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaGetOpenIssuesCall wrap *gomock.Call
|
||||
type MockGiteaGetOpenIssuesCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaGetOpenIssuesCall) Return(arg0 []*models.Issue, arg1 error) *MockGiteaGetOpenIssuesCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaGetOpenIssuesCall) Do(f func(string, string, string, int, string) ([]*models.Issue, error)) *MockGiteaGetOpenIssuesCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaGetOpenIssuesCall) DoAndReturn(f func(string, string, string, int, string) ([]*models.Issue, error)) *MockGiteaGetOpenIssuesCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetOrganization mocks base method.
|
||||
func (m *MockGitea) GetOrganization(orgName string) (*models.Organization, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -3499,6 +3664,45 @@ func (c *MockGiteaManualMergePRCall) DoAndReturn(f func(string, string, int64, s
|
||||
return c
|
||||
}
|
||||
|
||||
// ReparentRepository mocks base method.
|
||||
func (m *MockGitea) ReparentRepository(owner, repo, org string) (*models.Repository, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReparentRepository", owner, repo, org)
|
||||
ret0, _ := ret[0].(*models.Repository)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReparentRepository indicates an expected call of ReparentRepository.
|
||||
func (mr *MockGiteaMockRecorder) ReparentRepository(owner, repo, org any) *MockGiteaReparentRepositoryCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReparentRepository", reflect.TypeOf((*MockGitea)(nil).ReparentRepository), owner, repo, org)
|
||||
return &MockGiteaReparentRepositoryCall{Call: call}
|
||||
}
|
||||
|
||||
// MockGiteaReparentRepositoryCall wrap *gomock.Call
|
||||
type MockGiteaReparentRepositoryCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockGiteaReparentRepositoryCall) Return(arg0 *models.Repository, arg1 error) *MockGiteaReparentRepositoryCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockGiteaReparentRepositoryCall) Do(f func(string, string, string) (*models.Repository, error)) *MockGiteaReparentRepositoryCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockGiteaReparentRepositoryCall) DoAndReturn(f func(string, string, string) (*models.Repository, error)) *MockGiteaReparentRepositoryCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// RequestReviews mocks base method.
|
||||
func (m *MockGitea) RequestReviews(pr *models.PullRequest, reviewer ...string) ([]*models.PullReview, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
func TestFetchPRSet_Linkage(t *testing.T) {
|
||||
@@ -45,7 +46,7 @@ func TestFetchPRSet_Linkage(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("Fetch from ProjectGit PR", func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
defer ctl.Finish()
|
||||
mockGitea := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
mockGitea.EXPECT().ResetTimelineCache(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
@@ -74,7 +75,7 @@ func TestFetchPRSet_Linkage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Fetch from Package PR via Timeline", func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
defer ctl.Finish()
|
||||
mockGitea := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
mockGitea.EXPECT().ResetTimelineCache(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
func TestPRSet_Merge_Special(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
defer ctl.Finish()
|
||||
|
||||
mockGitea := mock_common.NewMockGiteaReviewUnrequester(ctl)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -561,7 +562,7 @@ func TestPR(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
pr_mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
pr_mock.EXPECT().ResetTimelineCache(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
review_mock := mock_common.NewMockGiteaPRChecker(ctl)
|
||||
@@ -1307,7 +1308,7 @@ func TestPRMerge(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
mock.EXPECT().ResetTimelineCache(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
|
||||
@@ -1381,7 +1382,7 @@ func TestPRChanges(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
mock_fetcher := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
||||
mock_fetcher.EXPECT().ResetTimelineCache(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
mock_fetcher.EXPECT().GetPullRequest("org", "prjgit", int64(42)).Return(test.PrjPRs, nil).AnyTimes()
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
func TestReviews(t *testing.T) {
|
||||
@@ -141,7 +142,7 @@ func TestReviews(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := NewController(t)
|
||||
ctl := test_utils.NewController(t)
|
||||
rf := mock_common.NewMockGiteaReviewTimelineFetcher(ctl)
|
||||
|
||||
if test.timeline == nil {
|
||||
|
||||
14
common/test_utils/common.go
Normal file
14
common/test_utils/common.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package test_utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
func NewController(t *testing.T) *gomock.Controller {
|
||||
common.SetTestLogger(t)
|
||||
return gomock.NewController(t)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package common_test
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
)
|
||||
|
||||
func TestGitUrlParse(t *testing.T) {
|
||||
@@ -307,7 +307,27 @@ func TestNewPackageIssueParsing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func NewController(t *testing.T) *gomock.Controller {
|
||||
common.SetTestLogger(t)
|
||||
return gomock.NewController(t)
|
||||
func TestIssueToString(t *testing.T) {
|
||||
t.Run("nil issue", func(t *testing.T) {
|
||||
if got := common.IssueToString(nil); got != "(nil)" {
|
||||
t.Errorf("expected (nil), got %s", got)
|
||||
}
|
||||
})
|
||||
t.Run("nil repository", func(t *testing.T) {
|
||||
// This test currently panics, which is a bug.
|
||||
common.IssueToString(&models.Issue{Index: 1})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPRtoString(t *testing.T) {
|
||||
t.Run("nil pr", func(t *testing.T) {
|
||||
if got := common.PRtoString(nil); got != "(null)" {
|
||||
t.Errorf("expected (null), got %s", got)
|
||||
}
|
||||
})
|
||||
t.Run("nil Base", func(t *testing.T) {
|
||||
// This test currently panics, which is a bug.
|
||||
common.PRtoString(&models.PullRequest{Index: 1})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
263
reparent-bot/bot.go
Normal file
263
reparent-bot/bot.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
)
|
||||
|
||||
type RepoInfo struct {
|
||||
Owner string
|
||||
Name string
|
||||
Branch string
|
||||
}
|
||||
|
||||
type ReparentBot struct {
|
||||
configs common.AutogitConfigs
|
||||
gitea common.Gitea
|
||||
giteaUrl string
|
||||
|
||||
botUser string
|
||||
|
||||
maintainershipFetcher MaintainershipFetcher
|
||||
}
|
||||
|
||||
func (bot *ReparentBot) ParseSourceReposFromIssue(issue *models.Issue) []RepoInfo {
|
||||
var sourceRepos []RepoInfo
|
||||
rx := regexp.MustCompile(`([_a-zA-Z0-9\.-]+)/([_a-zA-Z0-9\.-]+)(?:#([_a-zA-Z0-9\.\-/]+))?$`)
|
||||
|
||||
for _, line := range strings.Split(issue.Body, "\n") {
|
||||
matches := rx.FindStringSubmatch(strings.TrimSpace(line))
|
||||
if len(matches) == 4 {
|
||||
sourceRepos = append(sourceRepos, RepoInfo{Owner: matches[1], Name: matches[2], Branch: matches[3]})
|
||||
}
|
||||
}
|
||||
return sourceRepos
|
||||
}
|
||||
|
||||
type MaintainershipFetcher interface {
|
||||
FetchProjectMaintainershipData(gitea common.GiteaMaintainershipReader, config *common.AutogitConfig) (common.MaintainershipData, error)
|
||||
}
|
||||
|
||||
type RealMaintainershipFetcher struct{}
|
||||
|
||||
func (f *RealMaintainershipFetcher) FetchProjectMaintainershipData(gitea common.GiteaMaintainershipReader, config *common.AutogitConfig) (common.MaintainershipData, error) {
|
||||
return common.FetchProjectMaintainershipData(gitea, config)
|
||||
}
|
||||
|
||||
func (bot *ReparentBot) GetMaintainers(config *common.AutogitConfig) ([]string, error) {
|
||||
m, err := bot.maintainershipFetcher.FetchProjectMaintainershipData(bot.gitea, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.ListProjectMaintainers(config.ReviewGroups), nil
|
||||
}
|
||||
|
||||
var serializeMutex sync.Mutex
|
||||
|
||||
func (bot *ReparentBot) ProcessIssue(org, repo string, issue *models.Issue) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.LogInfo("panic caught --- recovered")
|
||||
common.LogError(string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
serializeMutex.Lock()
|
||||
defer serializeMutex.Unlock()
|
||||
|
||||
common.LogInfo(">>> Starting processing issue:", common.IssueToString(issue))
|
||||
defer common.LogInfo("<<< End processing issue:", common.IssueToString(issue))
|
||||
|
||||
if issue.State == "closed" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(strings.ToUpper(issue.Title), "[ADD]") {
|
||||
return nil
|
||||
}
|
||||
|
||||
newRepoLabel := false
|
||||
for _, l := range issue.Labels {
|
||||
if l.Name == common.Label_NewRepository {
|
||||
newRepoLabel = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !newRepoLabel {
|
||||
common.LogDebug("Not a new repository. Nothing to do here.")
|
||||
return nil
|
||||
}
|
||||
|
||||
bot.gitea.ResetTimelineCache(org, repo, issue.Index)
|
||||
timeline, err := bot.gitea.GetTimeline(org, repo, issue.Index)
|
||||
if err != nil {
|
||||
common.LogError("Failed to fetch issue timeline:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
sourceRepos := bot.ParseSourceReposFromIssue(issue)
|
||||
if len(sourceRepos) == 0 {
|
||||
common.LogDebug("Could not parse any source repos from issue body")
|
||||
return nil
|
||||
}
|
||||
|
||||
targetBranch := strings.TrimPrefix(issue.Ref, "refs/heads/")
|
||||
|
||||
config := bot.configs.GetPrjGitConfig(org, repo, targetBranch)
|
||||
if config == nil {
|
||||
for _, c := range bot.configs {
|
||||
if c.Organization == org && c.Branch == targetBranch {
|
||||
config = c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if config == nil {
|
||||
return fmt.Errorf("no config found for %s/%s#%s", org, org, targetBranch)
|
||||
}
|
||||
|
||||
maintainers, err := bot.GetMaintainers(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(maintainers) == 0 {
|
||||
return fmt.Errorf("no maintainers found for %s/%s#%s", org, repo, targetBranch)
|
||||
}
|
||||
|
||||
repos := make([]struct {
|
||||
repo *models.Repository
|
||||
name string
|
||||
}, len(sourceRepos))
|
||||
for idx, sourceInfo := range sourceRepos {
|
||||
source, err := bot.gitea.GetRepository(sourceInfo.Owner, sourceInfo.Name)
|
||||
branch := ""
|
||||
if sourceInfo.Branch != "" {
|
||||
branch = sourceInfo.Branch
|
||||
} else if source != nil {
|
||||
branch = source.DefaultBranch
|
||||
}
|
||||
repos[idx].name = sourceInfo.Owner + "/" + sourceInfo.Name + "#" + branch
|
||||
|
||||
if err != nil {
|
||||
common.LogError("failed to fetch source repo", repos[idx].name, ":", err)
|
||||
return nil
|
||||
}
|
||||
if source == nil {
|
||||
msg := fmt.Sprintf("Source repository not found: %s", repos[idx].name)
|
||||
bot.AddCommentOnce(org, repo, issue.Index, timeline, msg)
|
||||
common.LogError(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
if source.Parent != nil && source.Parent.Owner.UserName == config.Organization {
|
||||
common.LogDebug("Already reparented repo. Nothing to do here.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// README: issue creator *must be* owner of the repo, OR repository must not be a fork
|
||||
if issue.User.UserName != sourceInfo.Owner && source.Fork {
|
||||
msg := fmt.Sprintf("@%s: You are not the owner of %s and it is already a fork. Skipping.", issue.User.UserName, repos[idx].name)
|
||||
bot.AddCommentOnce(org, repo, issue.Index, timeline, msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if already exists in target org
|
||||
existing, err := bot.gitea.GetRepository(org, sourceInfo.Name)
|
||||
if err == nil && existing != nil {
|
||||
bot.AddCommentOnce(org, repo, issue.Index, timeline, fmt.Sprintf("Repository %s already exists in organization.", sourceInfo.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
repos[idx].repo = source
|
||||
}
|
||||
// Check for approval in comments
|
||||
approved := false
|
||||
for _, e := range timeline {
|
||||
if e.Type == common.TimelineCommentType_Comment &&
|
||||
e.User != nil &&
|
||||
bot.IsMaintainer(e.User.UserName, maintainers) &&
|
||||
bot.IsApproval(e.Body) &&
|
||||
e.Updated == e.Created {
|
||||
|
||||
approved = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if approved {
|
||||
common.LogInfo("Issue approved, processing source repos...")
|
||||
if !common.IsDryRun {
|
||||
for _, source := range repos {
|
||||
r := source.repo
|
||||
_, err := bot.gitea.ReparentRepository(r.Owner.UserName, r.Name, org)
|
||||
if err != nil {
|
||||
common.LogError("Reparent failed for", source.name, ":", err)
|
||||
continue
|
||||
}
|
||||
bot.AddCommentOnce(org, repo, issue.Index, timeline, fmt.Sprintf("Repository %s forked successfully.", source.name))
|
||||
}
|
||||
// bot.gitea.UpdateIssue(org, repo, issue.Index, &models.EditIssueOption{State: "closed"})
|
||||
} else {
|
||||
common.LogInfo("Dry run: would process %d source repos for issue %d", len(sourceRepos), issue.Index)
|
||||
}
|
||||
} else {
|
||||
// Request review/assignment if not already done
|
||||
found := false
|
||||
for _, a := range issue.Assignees {
|
||||
if bot.IsMaintainer(a.UserName, maintainers) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
common.LogInfo("Requesting review from maintainers:", maintainers)
|
||||
if !common.IsDryRun {
|
||||
_, err := bot.gitea.UpdateIssue(org, repo, issue.Index, &models.EditIssueOption{
|
||||
Assignees: maintainers,
|
||||
})
|
||||
if err != nil {
|
||||
common.LogError("Failed to assign maintainers:", err)
|
||||
}
|
||||
bot.AddCommentOnce(org, repo, issue.Index, timeline,
|
||||
"Review requested from maintainers: "+strings.Join(maintainers, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bot *ReparentBot) IsMaintainer(user string, maintainers []string) bool {
|
||||
return slices.Contains(maintainers, user)
|
||||
}
|
||||
|
||||
func (bot *ReparentBot) IsApproval(body string) bool {
|
||||
body = strings.ToLower(strings.TrimSpace(body))
|
||||
return strings.Contains(body, "approved") || strings.Contains(body, "lgtm")
|
||||
}
|
||||
|
||||
func (bot *ReparentBot) HasComment(timeline []*models.TimelineComment, message string) bool {
|
||||
for _, e := range timeline {
|
||||
if e.Type == common.TimelineCommentType_Comment && e.User != nil && e.User.UserName == bot.botUser && strings.TrimSpace(e.Body) == strings.TrimSpace(message) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (bot *ReparentBot) AddCommentOnce(org, repo string, index int64, timeline []*models.TimelineComment, msg string) {
|
||||
if bot.HasComment(timeline, msg) {
|
||||
return
|
||||
}
|
||||
bot.gitea.AddComment(&models.PullRequest{Base: &models.PRBranchInfo{Repo: &models.Repository{Owner: &models.User{UserName: org}, Name: repo}}, Index: index}, msg)
|
||||
}
|
||||
561
reparent-bot/bot_test.go
Normal file
561
reparent-bot/bot_test.go
Normal file
@@ -0,0 +1,561 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
type MockMaintainershipFetcher struct {
|
||||
data common.MaintainershipData
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *MockMaintainershipFetcher) FetchProjectMaintainershipData(gitea common.GiteaMaintainershipReader, config *common.AutogitConfig) (common.MaintainershipData, error) {
|
||||
return m.data, m.err
|
||||
}
|
||||
|
||||
type MockMaintainershipData struct {
|
||||
maintainers []string
|
||||
}
|
||||
|
||||
func (m *MockMaintainershipData) ListProjectMaintainers(groups []*common.ReviewGroup) []string {
|
||||
return m.maintainers
|
||||
}
|
||||
func (m *MockMaintainershipData) ListPackageMaintainers(pkg string, groups []*common.ReviewGroup) []string {
|
||||
return m.maintainers
|
||||
}
|
||||
func (m *MockMaintainershipData) IsApproved(pkg string, reviews []*models.PullReview, submitter string, groups []*common.ReviewGroup) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestParseSourceReposFromIssue(t *testing.T) {
|
||||
bot := &ReparentBot{}
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected []RepoInfo
|
||||
}{
|
||||
{
|
||||
name: "single repo",
|
||||
body: "https://src.opensuse.org/owner/repo",
|
||||
expected: []RepoInfo{
|
||||
{Owner: "owner", Name: "repo", Branch: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "repo with branch",
|
||||
body: "https://src.opensuse.org/owner/repo#branch",
|
||||
expected: []RepoInfo{
|
||||
{Owner: "owner", Name: "repo", Branch: "branch"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple repos",
|
||||
body: "Check these out:\nhttps://src.opensuse.org/o1/r1\nhttps://src.opensuse.org/o2/r2#b2",
|
||||
expected: []RepoInfo{
|
||||
{Owner: "o1", Name: "r1", Branch: ""},
|
||||
{Owner: "o2", Name: "r2", Branch: "b2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no repos",
|
||||
body: "Nothing here",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "not matching url",
|
||||
body: "invalid link",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := bot.ParseSourceReposFromIssue(&models.Issue{Body: tc.body})
|
||||
if len(got) != len(tc.expected) {
|
||||
t.Fatalf("expected %d repos, got %d", len(tc.expected), len(got))
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tc.expected[i] {
|
||||
t.Errorf("at index %d: expected %+v, got %+v", i, tc.expected[i], got[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMaintainer(t *testing.T) {
|
||||
bot := &ReparentBot{}
|
||||
maintainers := []string{"alice", "bob"}
|
||||
tests := []struct {
|
||||
user string
|
||||
expected bool
|
||||
}{
|
||||
{"alice", true},
|
||||
{"bob", true},
|
||||
{"charlie", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.user, func(t *testing.T) {
|
||||
if got := bot.IsMaintainer(tc.user, maintainers); got != tc.expected {
|
||||
t.Errorf("expected %v for %s, got %v", tc.expected, tc.user, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsApproval(t *testing.T) {
|
||||
bot := &ReparentBot{}
|
||||
tests := []struct {
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"approved", true},
|
||||
{"LGTM", true},
|
||||
{"Looks good to me, approved!", true},
|
||||
{"not yet", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.body, func(t *testing.T) {
|
||||
if got := bot.IsApproval(tc.body); got != tc.expected {
|
||||
t.Errorf("expected %v for %s, got %v", tc.expected, tc.body, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasComment(t *testing.T) {
|
||||
bot := &ReparentBot{botUser: "bot"}
|
||||
timeline := []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_Comment, User: &models.User{UserName: "user"}, Body: "hello"},
|
||||
{Type: common.TimelineCommentType_Comment, User: &models.User{UserName: "bot"}, Body: "ping"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
msg string
|
||||
expected bool
|
||||
}{
|
||||
{"exists", "ping", true},
|
||||
{"exists with spaces", " ping ", true},
|
||||
{"not exists", "pong", false},
|
||||
{"other user", "hello", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := bot.HasComment(timeline, tc.msg); got != tc.expected {
|
||||
t.Errorf("expected %v for %s, got %v", tc.expected, tc.msg, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessIssue(t *testing.T) {
|
||||
var mockGitea *mock.MockGitea
|
||||
mockFetcher := &MockMaintainershipFetcher{}
|
||||
bot := &ReparentBot{
|
||||
botUser: "bot",
|
||||
maintainershipFetcher: mockFetcher,
|
||||
configs: common.AutogitConfigs{
|
||||
&common.AutogitConfig{Organization: "org", Branch: "master"},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issue *models.Issue
|
||||
dryRun bool
|
||||
setupMock func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "closed issue",
|
||||
issue: &models.Issue{
|
||||
State: "closed",
|
||||
},
|
||||
setupMock: func() {},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no [ADD] prefix",
|
||||
issue: &models.Issue{
|
||||
State: "open",
|
||||
Title: "Just a comment",
|
||||
},
|
||||
setupMock: func() {},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no NewRepository label",
|
||||
issue: &models.Issue{
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
},
|
||||
setupMock: func() {},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "timeline fetch error",
|
||||
issue: &models.Issue{
|
||||
Index: 10,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(10)).Return(nil, errors.New("error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no source repos parsed",
|
||||
issue: &models.Issue{
|
||||
Index: 11,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Body: "No links here",
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(11)).Return(nil, nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing config",
|
||||
issue: &models.Issue{
|
||||
Index: 1,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/wrong-branch",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(1)).Return(nil, nil)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "config found in loop",
|
||||
issue: &models.Issue{
|
||||
Index: 6,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(6)).Return(nil, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(&models.Repository{DefaultBranch: "master"}, nil)
|
||||
mockGitea.EXPECT().GetRepository("org", "repo").Return(nil, nil)
|
||||
mockGitea.EXPECT().UpdateIssue("org", "repo", int64(6), gomock.Any()).Return(nil, nil)
|
||||
mockGitea.EXPECT().AddComment(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "maintainer fetch error",
|
||||
issue: &models.Issue{
|
||||
Index: 12,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(12)).Return(nil, nil)
|
||||
mockFetcher.err = errors.New("error")
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no maintainers found",
|
||||
issue: &models.Issue{
|
||||
Index: 13,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(13)).Return(nil, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: nil}
|
||||
mockFetcher.err = nil
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not approved - requests review",
|
||||
issue: &models.Issue{
|
||||
Index: 2,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(2)).Return(nil, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(&models.Repository{DefaultBranch: "master"}, nil)
|
||||
mockGitea.EXPECT().GetRepository("org", "repo").Return(nil, nil)
|
||||
mockGitea.EXPECT().UpdateIssue("org", "repo", int64(2), gomock.Any()).Return(nil, nil)
|
||||
mockGitea.EXPECT().AddComment(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "approved - processes repos",
|
||||
issue: &models.Issue{
|
||||
Index: 3,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
User: &models.User{UserName: "owner"},
|
||||
},
|
||||
setupMock: func() {
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "m1"},
|
||||
Body: "approved",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(3)).Return(timeline, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
|
||||
repo := &models.Repository{Name: "repo", Owner: &models.User{UserName: "owner"}, DefaultBranch: "master"}
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(repo, nil)
|
||||
mockGitea.EXPECT().GetRepository("org", "repo").Return(nil, nil)
|
||||
mockGitea.EXPECT().ReparentRepository("owner", "repo", "org").Return(nil, nil)
|
||||
mockGitea.EXPECT().AddComment(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "approved - dry run",
|
||||
dryRun: true,
|
||||
issue: &models.Issue{
|
||||
Index: 3,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
User: &models.User{UserName: "owner"},
|
||||
},
|
||||
setupMock: func() {
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "m1"},
|
||||
Body: "approved",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(3)).Return(timeline, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(&models.Repository{DefaultBranch: "master"}, nil)
|
||||
mockGitea.EXPECT().GetRepository("org", "repo").Return(nil, nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "approved - source repo fetch error",
|
||||
issue: &models.Issue{
|
||||
Index: 3,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
User: &models.User{UserName: "owner"},
|
||||
},
|
||||
setupMock: func() {
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "m1"},
|
||||
Body: "approved",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(3)).Return(timeline, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(nil, errors.New("error"))
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "approved - source repo not found",
|
||||
issue: &models.Issue{
|
||||
Index: 3,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
User: &models.User{UserName: "owner"},
|
||||
},
|
||||
setupMock: func() {
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "m1"},
|
||||
Body: "approved",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(3)).Return(timeline, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(nil, nil)
|
||||
mockGitea.EXPECT().AddComment(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "approved - reparent error",
|
||||
issue: &models.Issue{
|
||||
Index: 3,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
User: &models.User{UserName: "owner"},
|
||||
},
|
||||
setupMock: func() {
|
||||
timeline := []*models.TimelineComment{
|
||||
{
|
||||
Type: common.TimelineCommentType_Comment,
|
||||
User: &models.User{UserName: "m1"},
|
||||
Body: "approved",
|
||||
},
|
||||
}
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(3)).Return(timeline, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
|
||||
repo := &models.Repository{Name: "repo", Owner: &models.User{UserName: "owner"}, DefaultBranch: "master"}
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(repo, nil)
|
||||
mockGitea.EXPECT().GetRepository("org", "repo").Return(nil, nil)
|
||||
mockGitea.EXPECT().ReparentRepository("owner", "repo", "org").Return(nil, errors.New("error"))
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "user nil panic",
|
||||
issue: &models.Issue{
|
||||
Index: 99,
|
||||
State: "open",
|
||||
Title: "[ADD] My Repo",
|
||||
Labels: []*models.Label{{Name: common.Label_NewRepository}},
|
||||
Ref: "refs/heads/master",
|
||||
Body: "https://src.opensuse.org/owner/repo",
|
||||
User: nil,
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetTimeline("org", "repo", int64(99)).Return(nil, nil)
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
mockGitea.EXPECT().GetRepository("owner", "repo").Return(&models.Repository{DefaultBranch: "master", Fork: true}, nil)
|
||||
mockGitea.EXPECT().GetRepository("org", "repo").Return(nil, nil)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctrl := test_utils.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockGitea = mock.NewMockGitea(ctrl)
|
||||
mockGitea.EXPECT().ResetTimelineCache(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
bot.gitea = mockGitea
|
||||
|
||||
common.IsDryRun = tc.dryRun
|
||||
if tc.issue != nil {
|
||||
if tc.issue.Repository == nil {
|
||||
tc.issue.Repository = &models.RepositoryMeta{Owner: "org", Name: "repo"}
|
||||
}
|
||||
if tc.issue.User == nil && tc.name != "user nil panic" {
|
||||
tc.issue.User = &models.User{UserName: "owner"}
|
||||
}
|
||||
}
|
||||
tc.setupMock()
|
||||
err := bot.ProcessIssue("org", "repo", tc.issue)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("ProcessIssue() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
common.IsDryRun = false
|
||||
}
|
||||
|
||||
func TestGetMaintainers(t *testing.T) {
|
||||
mockFetcher := &MockMaintainershipFetcher{}
|
||||
bot := &ReparentBot{maintainershipFetcher: mockFetcher}
|
||||
config := &common.AutogitConfig{}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
mockFetcher.data = &MockMaintainershipData{maintainers: []string{"m1"}}
|
||||
mockFetcher.err = nil
|
||||
got, err := bot.GetMaintainers(config)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0] != "m1" {
|
||||
t.Errorf("expected [m1], got %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
mockFetcher.err = errors.New("error")
|
||||
_, err := bot.GetMaintainers(config)
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddCommentOnce(t *testing.T) {
|
||||
ctrl := test_utils.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockGitea := mock.NewMockGitea(ctrl)
|
||||
bot := &ReparentBot{gitea: mockGitea, botUser: "bot"}
|
||||
|
||||
timeline := []*models.TimelineComment{
|
||||
{Type: common.TimelineCommentType_Comment, User: &models.User{UserName: "bot"}, Body: "already there"},
|
||||
}
|
||||
|
||||
t.Run("already exists", func(t *testing.T) {
|
||||
bot.AddCommentOnce("org", "repo", 1, timeline, "already there")
|
||||
// No expectation means it should NOT call AddComment
|
||||
})
|
||||
|
||||
t.Run("new comment", func(t *testing.T) {
|
||||
mockGitea.EXPECT().AddComment(gomock.Any(), "new").Return(nil)
|
||||
bot.AddCommentOnce("org", "repo", 1, timeline, "new")
|
||||
})
|
||||
}
|
||||
|
||||
142
reparent-bot/main.go
Normal file
142
reparent-bot/main.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
func (bot *ReparentBot) PeriodCheck() {
|
||||
common.LogDebug("--- starting periodic check ---")
|
||||
for _, c := range bot.configs {
|
||||
org, repo, _ := c.GetPrjGit()
|
||||
|
||||
issues, err := bot.gitea.GetOpenIssues(org, repo, common.Label_NewRepository, common.IssueType_Issue, "[ADD]")
|
||||
if err != nil {
|
||||
common.LogError("Error fetching issues for processing:", err)
|
||||
return
|
||||
}
|
||||
common.LogDebug("Processing potential new issues for", org+"/"+repo, len(issues))
|
||||
for _, issue := range issues {
|
||||
err := bot.ProcessIssue(org, repo, issue)
|
||||
if err != nil {
|
||||
common.LogError("issue processing error:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
common.LogDebug("--- ending periodic check ---")
|
||||
}
|
||||
|
||||
func main() {
|
||||
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used")
|
||||
rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent")
|
||||
interval := flag.Int64("interval", 10, "Notification polling interval in minutes (min 1 min)")
|
||||
configFile := flag.String("config", "", "PrjGit listing config file")
|
||||
logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]")
|
||||
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
|
||||
testMain := flag.Bool("test.main", false, "Internal use for testing main")
|
||||
flag.Parse()
|
||||
|
||||
if *testMain {
|
||||
return
|
||||
}
|
||||
|
||||
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 *configFile == "" {
|
||||
common.LogError("Missing config file")
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := common.ReadConfigFile(*configFile)
|
||||
if err != nil {
|
||||
common.LogError("Failed to read config file", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.RequireGiteaSecretToken(); err != nil {
|
||||
common.LogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.RequireRabbitSecrets(); err != nil {
|
||||
common.LogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
giteaTransport := common.AllocateGiteaTransport(*giteaUrl)
|
||||
configs, err := common.ResolveWorkflowConfigs(giteaTransport, configData)
|
||||
if err != nil {
|
||||
common.LogError("Cannot parse workflow configs:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if *interval < 1 {
|
||||
*interval = 1
|
||||
}
|
||||
|
||||
bot := &ReparentBot{
|
||||
gitea: giteaTransport,
|
||||
configs: configs,
|
||||
giteaUrl: *giteaUrl,
|
||||
maintainershipFetcher: &RealMaintainershipFetcher{},
|
||||
}
|
||||
|
||||
common.LogInfo(" ** reparent-bot starting")
|
||||
common.LogInfo(" ** polling interval:", *interval, "min")
|
||||
common.LogInfo(" ** connecting to RabbitMQ:", *rabbitMqHost)
|
||||
|
||||
u, err := url.Parse(*rabbitMqHost)
|
||||
if err != nil {
|
||||
common.LogError("Cannot parse RabbitMQ host:", err)
|
||||
return
|
||||
}
|
||||
|
||||
botUser, err := bot.gitea.GetCurrentUser()
|
||||
if err != nil {
|
||||
common.LogError("Failed to fetch current user:", err)
|
||||
return
|
||||
}
|
||||
bot.botUser = botUser.UserName
|
||||
|
||||
process_issue := IssueProcessor{
|
||||
bot: bot,
|
||||
}
|
||||
|
||||
eventsProcessor := &common.RabbitMQGiteaEventsProcessor{
|
||||
Orgs: []string{},
|
||||
Handlers: map[string]common.RequestProcessor{
|
||||
common.RequestType_Issue: &process_issue,
|
||||
common.RequestType_IssueComment: &process_issue,
|
||||
},
|
||||
}
|
||||
eventsProcessor.Connection().RabbitURL = u
|
||||
for _, c := range bot.configs {
|
||||
if org, _, _ := c.GetPrjGit(); !slices.Contains(eventsProcessor.Orgs, org) {
|
||||
eventsProcessor.Orgs = append(eventsProcessor.Orgs, org)
|
||||
}
|
||||
}
|
||||
go common.ProcessRabbitMQEvents(eventsProcessor)
|
||||
|
||||
for {
|
||||
bot.PeriodCheck()
|
||||
time.Sleep(time.Duration(*interval * int64(time.Minute)))
|
||||
}
|
||||
}
|
||||
32
reparent-bot/main_test.go
Normal file
32
reparent-bot/main_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMainFunction(t *testing.T) {
|
||||
os.Setenv("AUTOGITS_GITEA_TOKEN", "dummy")
|
||||
os.Setenv("AUTOGITS_RABBIT_USER", "dummy")
|
||||
os.Setenv("AUTOGITS_RABBIT_PASSWORD", "dummy")
|
||||
os.Setenv("AUTOGITS_CONFIG", "test_config.json")
|
||||
|
||||
// Backup and restore os.Args and flag.CommandLine
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
os.Args = []string{"cmd", "-test.main"}
|
||||
|
||||
// Reset flags
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Log("Recovered from main panic:", r)
|
||||
}
|
||||
}()
|
||||
|
||||
main()
|
||||
}
|
||||
|
||||
37
reparent-bot/rabbit.go
Normal file
37
reparent-bot/rabbit.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
type IssueProcessor struct {
|
||||
bot *ReparentBot
|
||||
}
|
||||
|
||||
func (s *IssueProcessor) ProcessFunc(req *common.Request) error {
|
||||
var org, repo string
|
||||
var index int64
|
||||
|
||||
switch data := req.Data.(type) {
|
||||
case *common.IssueWebhookEvent:
|
||||
org = data.Repository.Owner.Username
|
||||
repo = data.Repository.Name
|
||||
index = int64(data.Issue.Number)
|
||||
case *common.IssueCommentWebhookEvent:
|
||||
org = data.Repository.Owner.Username
|
||||
repo = data.Repository.Name
|
||||
index = int64(data.Issue.Number)
|
||||
default:
|
||||
return fmt.Errorf("Unhandled request type: %s", req.Type)
|
||||
}
|
||||
|
||||
issue, err := s.bot.gitea.GetIssue(org, repo, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.bot.ProcessIssue(org, repo, issue)
|
||||
}
|
||||
|
||||
|
||||
110
reparent-bot/rabbit_test.go
Normal file
110
reparent-bot/rabbit_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
mock "src.opensuse.org/autogits/common/mock"
|
||||
"src.opensuse.org/autogits/common/test_utils"
|
||||
)
|
||||
|
||||
func TestIssueProcessor_ProcessFunc(t *testing.T) {
|
||||
ctrl := test_utils.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockGitea := mock.NewMockGitea(ctrl)
|
||||
bot := &ReparentBot{gitea: mockGitea}
|
||||
processor := &IssueProcessor{bot: bot}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *common.Request
|
||||
setupMock func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "issue event",
|
||||
req: &common.Request{
|
||||
Type: common.RequestType_Issue,
|
||||
Data: &common.IssueWebhookEvent{
|
||||
Repository: &common.Repository{
|
||||
Name: "repo",
|
||||
Owner: &common.Organization{Username: "org"},
|
||||
},
|
||||
Issue: &common.IssueDetail{
|
||||
Number: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetIssue("org", "repo", int64(1)).Return(&models.Issue{
|
||||
State: "closed",
|
||||
Repository: &models.RepositoryMeta{Owner: "org", Name: "repo"},
|
||||
}, nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "issue comment event",
|
||||
req: &common.Request{
|
||||
Type: common.RequestType_IssueComment,
|
||||
Data: &common.IssueCommentWebhookEvent{
|
||||
Repository: &common.Repository{
|
||||
Name: "repo",
|
||||
Owner: &common.Organization{Username: "org"},
|
||||
},
|
||||
Issue: &common.IssueDetail{
|
||||
Number: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetIssue("org", "repo", int64(2)).Return(&models.Issue{
|
||||
State: "closed",
|
||||
Repository: &models.RepositoryMeta{Owner: "org", Name: "repo"},
|
||||
}, nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unhandled type",
|
||||
req: &common.Request{
|
||||
Type: "unhandled",
|
||||
Data: nil,
|
||||
},
|
||||
setupMock: func() {},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "get issue error",
|
||||
req: &common.Request{
|
||||
Type: common.RequestType_Issue,
|
||||
Data: &common.IssueWebhookEvent{
|
||||
Repository: &common.Repository{
|
||||
Name: "repo",
|
||||
Owner: &common.Organization{Username: "org"},
|
||||
},
|
||||
Issue: &common.IssueDetail{
|
||||
Number: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMock: func() {
|
||||
mockGitea.EXPECT().GetIssue("org", "repo", int64(3)).Return(nil, errors.New("error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.setupMock()
|
||||
err := processor.ProcessFunc(tc.req)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("ProcessFunc() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user