SHA256
1
0
forked from adamm/autogits

68 Commits

Author SHA256 Message Date
e8b6066bae wip 2025-07-27 23:11:42 +02:00
42e2713cd8 Merge remote-tracking branch 'gitea/main' 2025-07-26 13:56:16 +02:00
3d24dce5c0 common: rabbit refactor
Generalize interface to allow processing of any events, not just
Gitea events.
2025-07-26 13:54:51 +02:00
0cefb45d8a allow imports of + in reponames 2025-07-25 14:57:13 +02:00
ddbb824006 group-review: fix group review approval and disapproval
and add unit tests for this
2025-07-25 13:54:30 +02:00
69dac4ec31 common: fix manifest path when pkg is path 2025-07-18 23:02:03 +02:00
b7e03ab465 common: use standard function for Basenamej 2025-07-18 20:41:57 +02:00
76aec3aabb PR: PrjGit description generation in one place
This allows for easier editing and for guidance of what will
happen next
2025-07-18 16:26:35 +02:00
19fb7e277b importer: case when OBS package name != repo name
also handle just one package import, for testing
2025-07-18 16:21:52 +02:00
51261f1bc1 PR: Add manifest to subdir mapping functions 2025-07-18 15:54:53 +02:00
949810709d import: handle previusly clones scmsync repos 2025-07-17 16:43:53 +02:00
c012570e89 importer: map packages to Gitea names 2025-07-16 21:28:26 +02:00
44a3b15a7d Merge pull request 'gitea_status_proxy' (#56) from ldragon/autogits:proxy into main
Reviewed-on: adamm/autogits#56
Reviewed-by: Adam Majer <adamm@noreply.src.opensuse.org>
2025-07-16 12:44:48 +02:00
c5db1c83a7 PR: detect and rebase project git commits
When project is advanced, and we have other package changes
to same project, the project git changes need to be rebased. The
simplest way of doing this is to skip all the submodule conflicts
and re-create them. This allows the submodules changes to be
mergeable again.
2025-07-15 19:08:05 +02:00
9f0909621b PR: fix timeline fetches
only fetch latest reviews from a user, not all
2025-07-15 11:06:17 +02:00
b3914b04bd Fix logic in crash protection
We must not access review.User object if it is nil
2025-07-11 12:02:37 +02:00
b43a19189e Enable code stream publishing 2025-07-11 12:02:08 +02:00
01b665230e message typo 2025-07-11 12:01:58 +02:00
1a07d4c541 Create Pull Requests to specified branches
instead of always using DefaultBranch. This means that target needs
always gets specified now.
2025-07-11 12:01:43 +02:00
22e44dff47 Don't fail on project git pull request creation. 2025-07-11 12:01:24 +02:00
f9021d08b9 PR: fix case where submodule cannot be initialized
Sometimes the commit is already cleaned up and Project Git cannot
be initialized. This should not be an error. Only fatal error
is if we can't update the PR to current state.
2025-07-10 18:28:09 +02:00
7a0394e51b PR: use "open" not "opened" as state 2025-07-10 16:54:28 +02:00
518bc15696 PR: close empty prjgit PRs 2025-07-09 20:39:38 +02:00
51873eb048 PR: log prjgit PrjGit creator 2025-07-09 20:06:13 +02:00
4f33ce979c PR: use MergeBase as ref. branch for prjgit
The target branch can be moving target, so not appropriate
2025-07-09 19:42:26 +02:00
7cc4db2283 common: prune removed remote branches
During a repository update, we need to remove branches that
no longer exist on remote from local cache.
2025-07-09 18:28:37 +02:00
4d9e2f8cab PR: update PrjGit PR when package PRs are removed or added 2025-07-09 18:05:05 +02:00
ed4f27a19e PR: refactor 2025-07-09 17:33:44 +02:00
e438b5b064 common: fix parsing of submodule commit id from tree object 2025-06-26 14:25:20 +02:00
885bb7e537 forward: fix logic
* fix approval/request changes string
* use common.DevelProject fetcher code
* fix parsing of Requests meta
2025-06-26 14:24:21 +02:00
977d75f6e9 reviews: only react to comment
also, reviews are reverse sorted.
fixed some bugs
2025-06-25 16:13:08 +02:00
42a9ee48e0 import: update config files 2025-06-24 16:03:33 +02:00
9333e5c3da PR: fix README quoting 2025-06-24 14:33:06 +02:00
5e29c88dc8 PR: fix README quoting 2025-06-24 14:32:23 +02:00
4f0f101620 importer: handle case of devel project in git 2025-06-23 18:48:09 +02:00
253f009da3 common: Add devel project query 2025-06-23 18:47:12 +02:00
5e66a14fa9 forward-bot: finish initial braindump 2025-06-17 23:39:47 +02:00
e79122e494 forward-bot: additional first code 2025-06-17 19:27:00 +02:00
0b4b1a4e21 common: Add basic OBS request APIs 2025-06-17 19:24:13 +02:00
0019546e30 forward-bot: initial skeleton 2025-06-17 00:46:26 +02:00
6438a8625a Replace PrjGit creation logic 2025-06-16 14:22:21 +02:00
3928fa6429 PR: use config project git branch, not default 2025-06-13 00:06:02 +02:00
e92ac4a592 PR: refactor 2025-06-12 23:51:04 +02:00
a1520ebfb0 PR: PRSet consistency check 2025-06-12 18:44:16 +02:00
c8d65a3ae5 PR: refactor
Move AssociatedPR fetching
2025-06-11 16:28:02 +02:00
b849a72f31 PR: request optional reviews
Ignore these reviews in approval, for otherwise they can be used
to fetch optional review information
2025-06-10 18:48:42 +02:00
568a2f3df8 PR: Add ability to parse optional reviewers
Document reviewer syntax in the Readme.md
2025-06-10 17:20:33 +02:00
30c8b2fe57 PR: require PRs to be in opened state
PR's that are not opened (eg. closed, or merged) cannot be part of
a consistent PRset. Either everything is merged, or everything should
be opened.
2025-06-10 16:31:44 +02:00
69b0f9a5ed PR: fix error logging 2025-06-10 15:59:07 +02:00
a283d4f26f PR: no submitter reviews needed 2025-06-07 21:52:47 +02:00
af898a6b8d pr: manual project only merge ok is manual merge ok 2025-06-07 21:42:11 +02:00
b89cdb7664 PR: fix parsing comments from timeline 2025-06-05 19:15:53 +02:00
d37bfaa9d3 common: workaround case when user do not have gitea accounts and cannot get reviews assigned 2025-06-04 14:59:46 +02:00
90cca05b31 common: fix maintainership parsing when no maintienrs explicitly set 2025-06-04 13:56:04 +02:00
7c229500c1 common: debug logging 2025-06-03 23:46:53 +02:00
290424c4a7 common: sort timeline in desc order 2025-06-03 23:42:02 +02:00
703fa101a4 group-review: fix crash in notification handling when no config 2025-06-03 17:48:09 +02:00
66e4982e2d group-review: fix build 2025-06-03 16:59:46 +02:00
09b1c415dd PR: fix deadlock in verification routines via git/org locking 2025-06-03 16:18:00 +02:00
629b941558 PR: use correct path for local repo cache 2025-06-03 14:13:58 +02:00
aa50481c00 PR: add test for unauthorized merge reviews 2025-06-03 10:48:17 +02:00
bc714ee22d PR: fix build 2025-06-03 10:40:49 +02:00
b8cc0357a7 PR: limit manual merge to Projects
Add "ManualMergeProject" to require "merge ok" sign-offs on
project level only
2025-06-03 00:07:34 +02:00
aed0ac3ee9 PR: allow maintainers to approve merges by default 2025-06-02 23:54:05 +02:00
cca3575596 PR: add "merge ok" manual merge option 2025-06-02 16:22:50 +02:00
69dcebcf74 common: use Timeline for reviews
Gitea doesn't keep track of Stale reviews well. We should parse
Timeline of a PR *always* and apply our own logic to this instead
2025-05-30 16:51:30 +02:00
7da9daddd5 direct: fix error formatting element 2025-05-27 12:33:51 +02:00
cd0c3bc759 common: fix tests 2025-05-27 12:11:21 +02:00
45 changed files with 2984 additions and 1110 deletions

View File

@@ -59,6 +59,10 @@ type AutogitConfig struct {
Reviewers []string // only used by `pr` workflow
ReviewGroups []ReviewGroup
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
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
}
type AutogitConfigs []*AutogitConfig

View File

@@ -10,6 +10,62 @@ import (
mock_common "src.opensuse.org/autogits/common/mock"
)
func TestProjectConfigMatcher(t *testing.T) {
configs := common.AutogitConfigs{
{
Organization: "test",
GitProjectName: "test/prjgit#main",
},
{
Organization: "test",
Branch: "main",
GitProjectName: "test/prjgit#main",
},
}
tests := []struct {
name string
org string
repo string
branch string
config int
}{
{
name: "invalid match",
org: "foo",
repo: "bar",
config: -1,
},
{
name: "default branch",
org: "test",
repo: "foo",
branch: "",
config: 0,
},
{
name: "main branch",
org: "test",
repo: "foo",
branch: "main",
config: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := configs.GetPrjGitConfig(test.org, test.repo, test.branch)
if test.config < 0 {
if c != nil {
t.Fatal("Expected nil. Got:", *c)
}
} else if config := configs[test.config]; c != config {
t.Fatal("Expected", *config, "got", *c)
}
})
}
}
func TestConfigWorkflowParser(t *testing.T) {
tests := []struct {
name string
@@ -41,10 +97,14 @@ func TestConfigWorkflowParser(t *testing.T) {
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.config_json), "abc", nil)
gitea.EXPECT().GetRepository("foo", "bar").Return(&test.repo, nil)
_, err := common.ReadWorkflowConfig(gitea, "foo/bar")
config, err := common.ReadWorkflowConfig(gitea, "foo/bar")
if err != nil {
t.Fatal(err)
}
if config.ManualMergeOnly != false {
t.Fatal("This should be false")
}
})
}
}
@@ -82,7 +142,7 @@ func TestProjectGitParser(t *testing.T) {
res: [3]string{"oorg", "foo.bar", "point"},
},
{
name: "whitespace shouldn't matter",
name: "whitespace shouldn't matter",
prjgit: " oorg / \nfoo.bar\t # point ",
res: [3]string{"oorg", "foo.bar", "point"},
},

View File

@@ -44,6 +44,10 @@ type GitStatusLister interface {
GitStatus(cwd string) ([]GitStatusData, error)
}
type GitDiffLister interface {
GitDiff(cwd, base, head string) (string, error)
}
type Git interface {
// error if git, but wrong remote
GitClone(repo, branch, remoteUrl string) (string, error) // clone, or check if path is already checked out remote and force pulls, error otherwise. Return remotename, errror
@@ -63,6 +67,8 @@ type Git interface {
GitExecOrPanic(cwd string, params ...string)
GitExec(cwd string, params ...string) error
GitExecWithOutput(cwd string, params ...string) (string, error)
GitDiffLister
}
type GitHandlerImpl struct {
@@ -133,6 +139,7 @@ func (s *gitHandlerGeneratorImpl) CreateGitHandler(org string) (Git, error) {
}
func (s *gitHandlerGeneratorImpl) ReadExistingPath(org string) (Git, error) {
LogDebug("Locking git org:", org)
s.lock_lock.Lock()
defer s.lock_lock.Unlock()
@@ -154,6 +161,7 @@ func (s *gitHandlerGeneratorImpl) ReadExistingPath(org string) (Git, error) {
func (s *gitHandlerGeneratorImpl) ReleaseLock(org string) {
m, ok := s.lock[org]
if ok {
LogDebug("Unlocking git org:", org)
m.Unlock()
}
}
@@ -235,7 +243,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
e.GitExecOrPanic(repo, "submodule", "deinit", "--all", "--force")
}
e.GitExecOrPanic(repo, "fetch", remoteName, remoteBranch)
e.GitExecOrPanic(repo, "fetch", "--prune", remoteName, remoteBranch)
}
refsBytes, err := os.ReadFile(path.Join(e.GitPath, repo, ".git/refs/remotes", remoteName, "HEAD"))
@@ -257,7 +265,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
LogDebug("branch", branch)
}
args := []string{"fetch", remoteName, branch}
args := []string{"fetch", "--prune", remoteName, branch}
if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
args = slices.Insert(args, 1, "--unshallow")
}
@@ -284,6 +292,7 @@ func (e *GitHandlerImpl) GitRemoteHead(gitDir, remote, branchName string) (strin
}
func (e *GitHandlerImpl) Close() error {
LogDebug("Unlocking git lock")
e.lock.Unlock()
return nil
}
@@ -760,6 +769,8 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
done.Lock()
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
LogDebug("Getting submodules for:", commitId)
go func() {
defer done.Unlock()
defer close(data_out.ch)
@@ -837,7 +848,7 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
go func() {
defer func() {
if recover() != nil {
subCommitId = "wrong"
subCommitId = ""
commitId = "ok"
valid = false
}
@@ -892,7 +903,7 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
}
wg.Wait()
return subCommitId, len(subCommitId) == len(commitId)
return subCommitId, len(subCommitId) > 0
}
const (
@@ -907,6 +918,16 @@ type GitStatusData struct {
Path string
Status int
States [3]string
/*
<sub> A 4 character field describing the submodule state.
"N..." when the entry is not a submodule.
"S<c><m><u>" when the entry is a submodule.
<c> is "C" if the commit changed; otherwise ".".
<m> is "M" if it has tracked changes; otherwise ".".
<u> is "U" if there are untracked changes; otherwise ".".
*/
SubmoduleChanges string
}
func parseGitStatusHexString(data io.ByteReader) (string, error) {
@@ -929,6 +950,20 @@ func parseGitStatusHexString(data io.ByteReader) (string, error) {
}
}
func parseGitStatusString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 100)
for {
c, err := data.ReadByte()
if err != nil {
return "", errors.New("Unexpected EOF. Expected NUL string term")
}
if c == 0 || c == ' ' {
return string(str), nil
}
str = append(str, c)
}
}
func parseGitStatusStringWithSpace(data io.ByteReader) (string, error) {
str := make([]byte, 0, 100)
for {
c, err := data.ReadByte()
@@ -969,7 +1004,7 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err
}
ret.Status = GitStatus_Modified
ret.Path, err = parseGitStatusString(data)
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
@@ -979,11 +1014,11 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err
}
ret.Status = GitStatus_Renamed
ret.Path, err = parseGitStatusString(data)
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
ret.States[0], err = parseGitStatusString(data)
ret.States[0], err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
@@ -993,7 +1028,7 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err
}
ret.Status = GitStatus_Untracked
ret.Path, err = parseGitStatusString(data)
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
@@ -1003,15 +1038,22 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err
}
ret.Status = GitStatus_Ignored
ret.Path, err = parseGitStatusString(data)
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
case 'u':
var err error
if err = skipGitStatusEntry(data, 7); err != nil {
if err = skipGitStatusEntry(data, 2); err != nil {
return nil, err
}
if ret.SubmoduleChanges, err = parseGitStatusString(data); err != nil {
return nil, err
}
if err = skipGitStatusEntry(data, 4); err != nil {
return nil, err
}
if ret.States[0], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
@@ -1022,7 +1064,7 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err
}
ret.Status = GitStatus_Unmerged
ret.Path, err = parseGitStatusString(data)
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
@@ -1069,3 +1111,26 @@ func (e *GitHandlerImpl) GitStatus(cwd string) (ret []GitStatusData, err error)
return parseGitStatusData(bufio.NewReader(bytes.NewReader(out)))
}
func (e *GitHandlerImpl) GitDiff(cwd, base, head string) (string, error) {
LogDebug("getting diff from", base, "..", head)
cmd := exec.Command("/usr/bin/git", "diff", base+".."+head)
cmd.Env = []string{
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
"GIT_LFS_SKIP_SMUDGE=1",
"GIT_CONFIG_GLOBAL=/dev/null",
}
cmd.Dir = filepath.Join(e.GitPath, cwd)
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
LogError(string(data))
return len(data), nil
})
LogDebug("command run:", cmd.Args)
out, err := cmd.Output()
if err != nil {
LogError("Error running command", cmd.Args, err)
}
return string(out), nil
}

View File

@@ -555,6 +555,8 @@ func TestGitStatusParse(t *testing.T) {
Path: ".gitmodules",
Status: GitStatus_Unmerged,
States: [3]string{"587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76", "d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c", "087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33"},
SubmoduleChanges: "N...",
},
},
},

View File

@@ -29,7 +29,6 @@ import (
"path"
"path/filepath"
"slices"
"strings"
"time"
transport "github.com/go-openapi/runtime/client"
@@ -86,7 +85,15 @@ type GiteaMaintainershipReader interface {
type GiteaPRFetcher interface {
GetPullRequest(org, project string, num int64) (*models.PullRequest, error)
GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, refOrg, refRepo string, Index int64) (*models.PullRequest, error)
}
type GiteaPRUpdater interface {
UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error)
}
type GiteaPRTimelineFetcher interface {
GiteaPRFetcher
GiteaTimelineFetcher
}
type GiteaCommitFetcher interface {
@@ -101,14 +108,19 @@ type GiteaCommentFetcher interface {
GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error)
}
type GiteaPRChecker interface {
type GiteaReviewTimelineFetcher interface {
GiteaReviewFetcher
GiteaTimelineFetcher
}
type GiteaPRChecker interface {
GiteaReviewTimelineFetcher
GiteaCommentFetcher
GiteaMaintainershipReader
}
type GiteaReviewFetcherAndRequester interface {
GiteaReviewFetcher
GiteaReviewTimelineFetcher
GiteaCommentFetcher
GiteaReviewRequester
}
@@ -155,6 +167,7 @@ type Gitea interface {
GiteaReviewUnrequester
GiteaReviewer
GiteaPRFetcher
GiteaPRUpdater
GiteaCommitFetcher
GiteaReviewFetcher
GiteaCommentFetcher
@@ -164,18 +177,19 @@ type Gitea interface {
GiteaCommitStatusGetter
GiteaCommitStatusSetter
GiteaSetRepoOptions
GiteaTimelineFetcher
GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error)
GetDonePullNotifications(page int64) ([]*models.NotificationThread, error)
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
SetNotificationRead(notificationId int64) error
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)
GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, refOrg, refRepo string, Index int64) (*models.PullRequest, error)
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)
GetPullRequests(org, project string) ([]*models.PullRequest, error)
GetCurrentUser() (*models.User, error)
}
@@ -222,6 +236,52 @@ func (gitea *GiteaTransport) GetPullRequest(org, project string, num int64) (*mo
return pr.Payload, err
}
func (gitea *GiteaTransport) UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error) {
pr, err := gitea.client.Repository.RepoEditPullRequest(
repository.NewRepoEditPullRequestParams().
WithOwner(org).
WithRepo(repo).
WithIndex(num).
WithBody(options),
gitea.transport.DefaultAuthentication,
)
return pr.Payload, err
}
func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRequest, error) {
var page, limit int64
prs := make([]*models.PullRequest, 0)
limit = 20
state := "open"
for {
page++
req, err := gitea.client.Repository.RepoListPullRequests(
repository.
NewRepoListPullRequestsParams().
WithDefaults().
WithOwner(org).
WithRepo(repo).
WithState(&state).
WithPage(&page).
WithLimit(&limit),
gitea.transport.DefaultAuthentication)
if err != nil {
return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", org, repo, err)
}
prs = slices.Concat(prs, req.Payload)
if len(req.Payload) < int(limit) {
break
}
}
return prs, nil
}
func (gitea *GiteaTransport) GetCommitStatus(org, repo, hash string) ([]*models.CommitStatus, error) {
page := int64(1)
limit := int64(10)
@@ -367,29 +427,42 @@ func (gitea *GiteaTransport) SetRepoOptions(owner, repo string, manual_merge boo
return ok.Payload, err
}
func (gitea *GiteaTransport) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) {
bigLimit := int64(100000)
const (
GiteaNotificationType_Pull = "Pull"
)
params := notification.NewNotifyGetListParams().
WithDefaults().
WithSubjectType([]string{"Pull"}).
WithStatusTypes([]string{"unread"}).
WithLimit(&bigLimit)
func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) {
bigLimit := int64(20)
ret := make([]*models.NotificationThread, 0, 100)
if since != nil {
s := strfmt.DateTime(*since)
params.SetSince(&s)
for page := int64(1); ; page++ {
params := notification.NewNotifyGetListParams().
WithDefaults().
WithSubjectType([]string{Type}).
WithStatusTypes([]string{"unread"}).
WithLimit(&bigLimit).
WithPage(&page)
if since != nil {
s := strfmt.DateTime(*since)
params.SetSince(&s)
}
list, err := gitea.client.Notification.NotifyGetList(params, gitea.transport.DefaultAuthentication)
if err != nil {
return nil, err
}
ret = slices.Concat(ret, list.Payload)
if len(list.Payload) < int(bigLimit) {
break
}
}
list, err := gitea.client.Notification.NotifyGetList(params, gitea.transport.DefaultAuthentication)
if err != nil {
return nil, err
}
return list.Payload, nil
return ret, nil
}
func (gitea *GiteaTransport) GetDonePullNotifications(page int64) ([]*models.NotificationThread, error) {
func (gitea *GiteaTransport) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error) {
limit := int64(20)
t := true
@@ -399,7 +472,7 @@ func (gitea *GiteaTransport) GetDonePullNotifications(page int64) ([]*models.Not
list, err := gitea.client.Notification.NotifyGetList(
notification.NewNotifyGetListParams().
WithAll(&t).
WithSubjectType([]string{"Pull"}).
WithSubjectType([]string{Type}).
WithStatusTypes([]string{"read"}).
WithLimit(&limit).
WithPage(&page),
@@ -530,14 +603,14 @@ func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git Git, org, repoName s
func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
prOptions := models.CreatePullRequestOption{
Base: repo.DefaultBranch,
Base: targetId,
Head: srcId,
Title: title,
Body: body,
}
if pr, err := gitea.client.Repository.RepoGetPullRequestByBaseHead(
repository.NewRepoGetPullRequestByBaseHeadParams().WithOwner(repo.Owner.UserName).WithRepo(repo.Name).WithBase(repo.DefaultBranch).WithHead(srcId),
repository.NewRepoGetPullRequestByBaseHeadParams().WithOwner(repo.Owner.UserName).WithRepo(repo.Name).WithBase(targetId).WithHead(srcId),
gitea.transport.DefaultAuthentication,
); err == nil {
return pr.Payload, nil
@@ -560,48 +633,6 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
return pr.GetPayload(), nil
}
func (gitea *GiteaTransport) GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, refOrg, refRepo string, Index int64) (*models.PullRequest, error) {
var page int64
state := "open"
prLine := fmt.Sprintf(PrPattern, refOrg, refRepo, Index)
LogDebug("Finding PrjGitPR for", prLine, " Looking in", prjGitOrg, "/", prjGitRepo)
for {
page++
prs, err := gitea.client.Repository.RepoListPullRequests(
repository.
NewRepoListPullRequestsParams().
WithDefaults().
WithOwner(prjGitOrg).
WithRepo(prjGitRepo).
WithState(&state).
WithPage(&page),
gitea.transport.DefaultAuthentication)
if err != nil {
return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", prjGitOrg, prjGitRepo, err)
}
// payload_processing:
for _, pr := range prs.Payload {
lines := strings.Split(pr.Body, "\n")
for _, line := range lines {
if strings.TrimSpace(line) == prLine {
LogDebug("Found PR:", pr.Index)
return pr, nil
}
}
}
if len(prs.Payload) < 10 {
break
}
}
return nil, nil
}
func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewers ...string) ([]*models.PullReview, error) {
reviewOptions := models.PullReviewRequestOptions{
Reviewers: reviewers,
@@ -687,20 +718,18 @@ func (gitea *GiteaTransport) AddComment(pr *models.PullRequest, comment string)
}
func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
limit := int64(20)
page := int64(1)
resCount := limit
resCount := 1
retData := []*models.TimelineComment{}
for resCount == limit {
for resCount > 0 {
res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(
issue.NewIssueGetCommentsAndTimelineParams().
WithOwner(org).
WithRepo(repo).
WithIndex(idx).
WithPage(&page).
WithLimit(&limit),
WithPage(&page),
gitea.transport.DefaultAuthentication,
)
@@ -708,14 +737,16 @@ func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models
return nil, err
}
resCount = int64(len(res.Payload))
resCount = len(res.Payload)
LogDebug("page:", page, "len:", resCount)
page++
retData = append(retData, res.Payload...)
}
LogDebug("total results:", len(retData))
slices.SortFunc(retData, func(a, b *models.TimelineComment) int {
return time.Time(a.Created).Compare(time.Time(b.Created))
return time.Time(b.Created).Compare(time.Time(a.Created))
})
return retData, nil

View File

@@ -63,7 +63,7 @@ func FetchProjectMaintainershipData(gitea GiteaMaintainershipReader, org, prjGit
if m != nil {
m.IsDir = dir
m.FetchPackage = func(pkg string) ([]byte, error) {
data , _, err := gitea.FetchMaintainershipDirFile(org, prjGit, branch, pkg)
data, _, err := gitea.FetchMaintainershipDirFile(org, prjGit, branch, pkg)
return data, err
}
}
@@ -127,18 +127,11 @@ prjMaintainer:
}
func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullReview, submitter string) bool {
reviewers, found := data.Data[pkg]
if !found {
if pkg != ProjectKey && data.IsDir {
r, err := data.FetchPackage(pkg)
if err != nil {
return false
}
reviewers = parsePkgDirData(pkg, r)
data.Data[pkg] = reviewers
} else {
return true
}
var reviewers []string
if pkg != ProjectKey {
reviewers = data.ListPackageMaintainers(pkg)
} else {
reviewers = data.ListProjectMaintainers()
}
if len(reviewers) == 0 {
@@ -146,11 +139,12 @@ func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullRevi
}
LogDebug("Looking for review by:", reviewers)
if slices.Contains(reviewers, submitter) {
LogDebug("Submitter is maintainer. Approving.")
return true
}
for _, review := range reviews {
if slices.Contains(reviewers, submitter) {
LogDebug("Submitter is maintainer. Approving.")
return true
}
if !review.Stale && review.State == ReviewStateApproved && slices.Contains(reviewers, review.User.UserName) {
LogDebug("Reviewed by", review.User.UserName)
return true
@@ -190,7 +184,7 @@ func (data *MaintainershipMap) WriteMaintainershipFile(writer io.StringWriter) e
keys = slices.Delete(keys, i, len(keys))
}
slices.Sort(keys)
for i, pkg := range(keys) {
for i, pkg := range keys {
eol := ","
if i == len(keys)-1 {
eol = ""

56
common/manifest.go Normal file
View File

@@ -0,0 +1,56 @@
package common
import (
"os"
"path"
"strings"
"gopkg.in/yaml.v3"
)
type Manifest struct {
Subdirectories []string
}
func (m *Manifest) SubdirForPackage(pkg string) string {
if m == nil {
return pkg
}
idx := -1
matchLen := 0
basePkg := path.Base(pkg)
lowercasePkg := strings.ToLower(basePkg)
for i, sub := range m.Subdirectories {
basename := strings.ToLower(path.Base(sub))
if strings.HasPrefix(lowercasePkg, basename) && matchLen < len(basename) {
idx = i
matchLen = len(basename)
}
}
if idx > -1 {
return path.Join(m.Subdirectories[idx], basePkg)
}
return pkg
}
func ReadManifestFile(filename string) (*Manifest, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return ParseManifestFile(data)
}
func ParseManifestFile(data []byte) (*Manifest, error) {
ret := &Manifest{}
err := yaml.Unmarshal(data, ret)
if err != nil {
return nil, err
}
return ret, nil
}

56
common/manifest_test.go Normal file
View File

@@ -0,0 +1,56 @@
package common_test
import (
"testing"
"src.opensuse.org/autogits/common"
)
func TestManifestSubdirAssignments(t *testing.T) {
tests := []struct {
Name string
ManifestContent string
Packages []string
ManifestLocations []string
}{
{
Name: "empty manifest",
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "boost", "NodeJS"},
ManifestLocations: []string{"atom", "blarg", "Foobar", "X-Ray", "boost", "NodeJS"},
},
{
Name: "only few subdirs manifest",
ManifestContent: "subdirectories:\n - a\n - b",
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "Boost", "NodeJS"},
ManifestLocations: []string{"a/atom", "b/blarg", "Foobar", "X-Ray", "b/Boost", "NodeJS"},
},
{
Name: "multilayer subdirs manifest",
ManifestContent: "subdirectories:\n - a\n - b\n - libs/boo",
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "Boost", "NodeJS"},
ManifestLocations: []string{"a/atom", "b/blarg", "Foobar", "X-Ray", "libs/boo/Boost", "NodeJS"},
},
{
Name: "multilayer subdirs manifest with trailing /",
ManifestContent: "subdirectories:\n - a\n - b\n - libs/boo/\n - somedir/Node/",
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "Boost", "NodeJS", "foobar/node2"},
ManifestLocations: []string{"a/atom", "b/blarg", "Foobar", "X-Ray", "libs/boo/Boost", "somedir/Node/NodeJS", "somedir/Node/node2"},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
m, err := common.ParseManifestFile([]byte(test.ManifestContent))
if err != nil {
t.Fatal(err)
}
for i, pkg := range test.Packages {
expected := test.ManifestLocations[i]
if l := m.SubdirForPackage(pkg); l != expected {
t.Error("Expected:", expected, "but got:", l)
}
}
})
}
}

View File

@@ -156,6 +156,34 @@ type GroupMeta struct {
Persons PersonGroup `xml:"person"`
}
type RequestStateMeta struct {
XMLName xml.Name `xml:"state"`
State string `xml:"name,attr"`
}
type RequestActionTarget struct {
XMLName xml.Name
Project string `xml:"project,attr"`
Package string `xml:"package,attr"`
Revision *string `xml:"rev,attr,optional"`
}
type RequestActionMeta struct {
XMLName xml.Name `xml:"action"`
Type string `xml:"type,attr"`
Source *RequestActionTarget `xml:"source,optional"`
Target *RequestActionTarget `xml:"target,optional"`
}
type RequestMeta struct {
XMLName xml.Name `xml:"request"`
Id int `xml:"id,attr"`
Creator string `xml:"creator,attr"`
Action *RequestActionMeta `xml:"action"`
State RequestStateMeta `xml:"state"`
}
func parseProjectMeta(data []byte) (*ProjectMeta, error) {
var meta ProjectMeta
err := xml.Unmarshal(data, &meta)
@@ -166,8 +194,83 @@ func parseProjectMeta(data []byte) (*ProjectMeta, error) {
return &meta, nil
}
const (
RequestStatus_Unknown = "unknown"
RequestStatus_Accepted = "accepted"
RequestStatus_Superseded = "superseded"
RequestStatus_Declined = "declined"
RequestStatus_Revoked = "revoked"
RequestStatus_New = "new"
RequestStatus_Review = "review"
)
func (status *RequestStateMeta) IsFinal() bool {
switch status.State {
case RequestStatus_Declined, RequestStatus_Revoked, RequestStatus_Accepted, RequestStatus_Superseded:
return true
}
return false
}
func parseRequestXml(data []byte) (*RequestMeta, error) {
ret := RequestMeta{}
LogDebug("parsing: ", string(data))
if err := xml.Unmarshal(data, &ret); err != nil {
return nil, err
}
return &ret, nil
}
func (c *ObsClient) CreateSubmitRequest(sourcePrj, sourcePkg, targetPrj string) (*RequestMeta, error) {
url := c.baseUrl.JoinPath("request")
query := url.Query()
query.Add("cmd", "create")
url.RawQuery = query.Encode()
request := `<request>
<action type="submit">
<source project="` + sourcePrj + `" package="` + sourcePkg + `">
</source>
<target project="` + targetPrj + `" package="` + sourcePkg + `">
</target>
</action>
</request>`
res, err := c.ObsRequestRaw("POST", url.String(), strings.NewReader(request))
if err != nil {
return nil, err
} else if res.StatusCode != 200 {
return nil, fmt.Errorf("Unexpected return code: %d", res.StatusCode)
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return parseRequestXml(data)
}
func (c *ObsClient) RequestStatus(requestID int) (*RequestMeta, error) {
res, err := c.ObsRequest("GET", []string{"request", fmt.Sprint(requestID)}, nil)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("Unexpected return code: %d", res.StatusCode)
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return parseRequestXml(data)
}
func (c *ObsClient) GetGroupMeta(gid string) (*GroupMeta, error) {
res, err := c.ObsRequest("GET", c.baseUrl.JoinPath("group", gid).String(), nil)
res, err := c.ObsRequest("GET", []string{"group", gid}, nil)
if err != nil {
return nil, err
}
@@ -197,7 +300,7 @@ func (c *ObsClient) GetGroupMeta(gid string) (*GroupMeta, error) {
}
func (c *ObsClient) GetUserMeta(uid string) (*UserMeta, error) {
res, err := c.ObsRequest("GET", c.baseUrl.JoinPath("person", uid).String(), nil)
res, err := c.ObsRequest("GET", []string{"person", uid}, nil)
if err != nil {
return nil, err
}
@@ -226,7 +329,11 @@ func (c *ObsClient) GetUserMeta(uid string) (*UserMeta, error) {
return &meta, nil
}
func (c *ObsClient) ObsRequest(method string, url string, body io.Reader) (*http.Response, error) {
func (c *ObsClient) ObsRequest(method string, url_path []string, body io.Reader) (*http.Response, error) {
return c.ObsRequestRaw(method, c.baseUrl.JoinPath(url_path...).String(), body)
}
func (c *ObsClient) ObsRequestRaw(method string, url string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
@@ -322,7 +429,7 @@ func (c *ObsClient) ObsRequest(method string, url string, body io.Reader) (*http
}
func (c *ObsClient) GetProjectMeta(project string) (*ProjectMeta, error) {
req := c.baseUrl.JoinPath("source", project, "_meta").String()
req := []string{"source", project, "_meta"}
res, err := c.ObsRequest("GET", req, nil)
if err != nil {
@@ -348,7 +455,7 @@ func (c *ObsClient) GetProjectMeta(project string) (*ProjectMeta, error) {
}
func (c *ObsClient) GetPackageMeta(project, pkg string) (*PackageMeta, error) {
res, err := c.ObsRequest("GET", c.baseUrl.JoinPath("source", project, pkg, "_meta").String(), nil)
res, err := c.ObsRequest("GET", []string{"source", project, pkg, "_meta"}, nil)
if err != nil {
return nil, err
@@ -422,7 +529,7 @@ func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error {
return err
}
res, err := c.ObsRequest("PUT", c.baseUrl.JoinPath("source", meta.Name, "_meta").String(), io.NopCloser(bytes.NewReader(xml)))
res, err := c.ObsRequest("PUT", []string{"source", meta.Name, "_meta"}, io.NopCloser(bytes.NewReader(xml)))
if err != nil {
return err
}
@@ -443,7 +550,7 @@ func (c *ObsClient) DeleteProject(project string) error {
query := url.Query()
query.Add("force", "1")
url.RawQuery = query.Encode()
res, err := c.ObsRequest("DELETE", url.String(), nil)
res, err := c.ObsRequestRaw("DELETE", url.String(), nil)
if err != nil {
return err
@@ -455,25 +562,44 @@ func (c *ObsClient) DeleteProject(project string) error {
}
return nil
}
func (c *ObsClient) BuildLog(prj, pkg, repo, arch string) (io.ReadCloser, error) {
url := c.baseUrl.JoinPath("build", prj, repo, arch, pkg, "_log")
query := url.Query()
query.Add("nostream", "1")
query.Add("start", "0")
url.RawQuery = query.Encode()
res, err := c.ObsRequestRaw("GET", url.String(), nil)
if err != nil {
return nil, err
}
return res.Body, nil
}
type PackageBuildStatus struct {
Package string `xml:"package,attr"`
Code string `xml:"code,attr"`
Details string `xml:"details"`
LastUpdate int64
}
type BuildResult struct {
Project string `xml:"project,attr"`
Repository string `xml:"repository,attr"`
Arch string `xml:"arch,attr"`
Code string `xml:"code,attr"`
Dirty bool `xml:"dirty,attr"`
ScmSync string `xml:"scmsync"`
ScmInfo string `xml:"scminfo"`
Status []PackageBuildStatus `xml:"status"`
Binaries []BinaryList `xml:"binarylist"`
Project string `xml:"project,attr"`
Repository string `xml:"repository,attr"`
Arch string `xml:"arch,attr"`
Code string `xml:"code,attr"`
Dirty bool `xml:"dirty,attr"`
ScmSync string `xml:"scmsync"`
ScmInfo string `xml:"scminfo"`
Status []PackageBuildStatus `xml:"status"`
Binaries []BinaryList `xml:"binarylist"`
LastUpdate int64
}
type Binary struct {
@@ -493,6 +619,7 @@ type BuildResultList struct {
Result []BuildResult `xml:"result"`
isLastBuild bool
LastUpdate int64
}
func (r *BuildResultList) GetPackageList() []string {
@@ -515,6 +642,48 @@ func (r *BuildResultList) GetPackageList() []string {
return pkgList
}
func packageSort(A, B PackageBuildStatus) int {
return strings.Compare(A.Package, B.Package)
}
func repoSort(A, B BuildResult) int {
eq := strings.Compare(A.Project, B.Project)
if eq == 0 {
eq = strings.Compare(A.Repository, B.Repository)
if eq == 0 {
eq = strings.Compare(A.Arch, B.Arch)
}
}
return eq
}
func (r *BuildResultList) MergePackageState(now int64, pkgState *BuildResultList) {
for _, nr := range pkgState.Result {
idx, found := slices.BinarySearchFunc(r.Result, nr, repoSort)
// not found, new repo?
if !found {
nr.LastUpdate = now
r.Result = slices.Insert(r.Result, idx, nr)
continue
}
// update current repo
repo := &r.Result[idx]
// update all the packages in the repo
for _, p := range nr.Status {
p.LastUpdate = now
idx, found := slices.BinarySearchFunc(repo.Status, p, packageSort)
if !found {
repo.Status = slices.Insert(repo.Status, idx, p)
continue
}
repo.Status[idx] = p
}
}
}
func (r *BuildResultList) BuildResultSummary() (success, finished bool) {
if r == nil {
return false, false
@@ -718,9 +887,7 @@ func (obs ObsProjectNotFound) Error() string {
}
func (c *ObsClient) ProjectConfig(project string) (string, error) {
u := c.baseUrl.JoinPath("source", project, "_config")
res, err := c.ObsRequest("GET", u.String(), nil)
res, err := c.ObsRequest("GET", []string{"source", project, "_config"}, nil)
if err != nil {
return "", err
}
@@ -761,7 +928,7 @@ func (c *ObsClient) BuildStatusWithState(project string, opts *BuildResultOption
}
}
u.RawQuery = query.Encode()
res, err := c.ObsRequest("GET", u.String(), nil)
res, err := c.ObsRequestRaw("GET", u.String(), nil)
if err != nil {
return nil, err
}
@@ -784,5 +951,11 @@ func (c *ObsClient) BuildStatusWithState(project string, opts *BuildResultOption
if ret != nil {
ret.isLastBuild = opts.LastBuild
}
slices.SortFunc(ret.Result, repoSort)
for _, r := range ret.Result {
slices.SortFunc(r.Status, packageSort)
}
return ret, err
}

View File

@@ -55,6 +55,52 @@ func TestParsingOfBuildResults(t *testing.T) {
}
}
func TestParsingRequestResults(t *testing.T) {
res, err := parseRequestXml([]byte(metaRequestData))
if err != nil {
t.Fatal(err)
}
if res.Id != 42 ||
res.Action.Source.Project != "home:foo-user" ||
res.Action.Source.Package != "obs-server" ||
*res.Action.Source.Revision != "521e" ||
res.Action.Target.Project != "OBS:Unstable" ||
res.Action.Target.Revision != nil {
t.Fatal(res)
}
}
const metaRequestData = `<?xml version="1.0" encoding="UTF-8"?>
<request id="42" creator="foo-user">
<action type="submit">
<source project="home:foo-user" package="obs-server" rev="521e">
</source>
<target project="OBS:Unstable" package="obs-server">
</target>
<options>
<sourceupdate>cleanup</sourceupdate>
</options>
</action>
<state name="accepted" who="bar-user" when="2021-01-15T13:39:43">
<comment>allright</comment>
</state>
<review state="accepted" when="2021-01-15T15:49:32" who="obs-maintainer" by_user="obs-maintainer">
</review>
<review state="accepted" when="2021-01-15T15:49:32" who="obs-maintainer" by_group="obs-group">
</review>
<review state="accepted" when="2021-01-15T15:49:32" who="obs-maintainer" by_project="OBS:Unstable">
</review>
<review state="accepted" when="2021-01-15T15:49:32" who="obs-maintainer" by_package="obs-server">
</review>
<history who="foo" when="2021-01-15T13:39:43">
<description>Request created</description>
<comment>Please review sources</comment>
</history>
<description>A little version update</description>
</request>`
const metaPrjData = `
<project name="home:adamm">
<title>Adam's Home Projects</title>

View File

@@ -25,6 +25,13 @@ type PRSet struct {
BotUser string
}
func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
org = prinfo.PR.Base.Repo.Owner.UserName
repo = prinfo.PR.Base.Repo.Name
idx = prinfo.PR.Index
return
}
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 {
@@ -53,7 +60,42 @@ func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRIn
return retSet, nil
}
func FetchPRSet(user string, gitea GiteaPRFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
func LastPrjGitRefOnTimeline(gitea GiteaPRTimelineFetcher, org, repo string, num int64, prjGitOrg, prjGitRepo string) (*models.PullRequest, error) {
prRefLine := fmt.Sprintf(PrPattern, org, repo, num)
timeline, err := gitea.GetTimeline(org, repo, num)
if err != nil {
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
return nil, err
}
for idx := len(timeline) - 1; idx >= 0; idx-- {
item := timeline[idx]
issue := item.RefIssue
if item.Type == TimelineCommentType_PullRequestRef &&
issue != nil &&
issue.Repository != nil &&
issue.Repository.Owner == prjGitOrg &&
issue.Repository.Name == prjGitRepo {
lines := SplitLines(item.RefIssue.Body)
for _, line := range lines {
if strings.TrimSpace(line) == prRefLine {
LogDebug("Found PrjGit PR in Timeline:", issue.Index)
// found prjgit PR in timeline. Return it
return gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index)
}
}
}
}
LogDebug("PrjGit RefIssue not found on timeline in", org, repo, num)
return nil, Timeline_RefIssueNotFound
}
func FetchPRSet(user string, gitea GiteaPRTimelineFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
var pr *models.PullRequest
var err error
@@ -63,7 +105,7 @@ func FetchPRSet(user string, gitea GiteaPRFetcher, org, repo string, num int64,
return nil, err
}
} else {
if pr, err = gitea.GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, org, repo, num); err != nil {
if pr, err = LastPrjGitRefOnTimeline(gitea, org, repo, num, prjGitOrg, prjGitRepo); err != nil && err != Timeline_RefIssueNotFound {
return nil, err
}
@@ -80,26 +122,53 @@ func FetchPRSet(user string, gitea GiteaPRFetcher, org, repo string, num int64,
}
return &PRSet{
PRs: prs,
Config: config,
PRs: prs,
Config: config,
BotUser: user,
}, nil
}
func (rs *PRSet) IsPrjGitPR(pr *models.PullRequest) bool {
org, repo, _ := rs.Config.GetPrjGit()
return pr.Base.Repo.Name == repo && pr.Base.Repo.Owner.UserName == org
func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) {
for _, p := range rs.PRs {
if p.PR.Base.RepoID == pr.Base.RepoID &&
p.PR.Head.Sha == pr.Head.Sha &&
p.PR.Base.Name == pr.Base.Name {
return p, true
}
}
return nil, false
}
func (rs *PRSet) GetPrjGitPR() (*models.PullRequest, error) {
var ret *models.PullRequest
func (rs *PRSet) AddPR(pr *models.PullRequest) *PRInfo {
if pr, found := rs.Find(pr); found {
return pr
}
prinfo := &PRInfo{
PR: pr,
}
rs.PRs = append(rs.PRs, prinfo)
return prinfo
}
func (rs *PRSet) IsPrjGitPR(pr *models.PullRequest) bool {
org, repo, branch := rs.Config.GetPrjGit()
return pr.Base.Name == branch && pr.Base.Repo.Name == repo && pr.Base.Repo.Owner.UserName == org
}
var PRSet_PrjGitMissing error = errors.New("No PrjGit PR found")
var PRSet_MultiplePrjGit error = errors.New("Multiple PrjGit PRs in one review set")
func (rs *PRSet) GetPrjGitPR() (*PRInfo, error) {
var ret *PRInfo
for _, prinfo := range rs.PRs {
if rs.IsPrjGitPR(prinfo.PR) {
if ret == nil {
ret = prinfo.PR
ret = prinfo
} else {
return nil, errors.New("Multiple PrjGit PRs in one review set")
return nil, PRSet_MultiplePrjGit
}
}
}
@@ -108,21 +177,37 @@ func (rs *PRSet) GetPrjGitPR() (*models.PullRequest, error) {
return ret, nil
}
return nil, errors.New("No PrjGit PR found")
return nil, PRSet_PrjGitMissing
}
func (rs *PRSet) NeedRecreatingPrjGit(currentBranchHash string) bool {
pr, err := rs.GetPrjGitPR()
if err != nil {
return true
}
return pr.PR.Base.Sha == currentBranchHash
}
func (rs *PRSet) IsConsistent() bool {
prjpr, err := rs.GetPrjGitPR()
prjpr_info, err := rs.GetPrjGitPR()
if err != nil {
return false
}
prjpr := prjpr_info.PR
_, prjpr_set := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(prjpr.Body)))
if len(prjpr_set) != len(rs.PRs)-1 { // 1 to many mapping
LogDebug("Number of PR from links:", len(prjpr_set), "is not what's expected", len(rs.PRs)-1)
return false
}
next_rs:
for _, prinfo := range rs.PRs {
if prinfo.PR.State != "open" {
return false
}
if prjpr == prinfo.PR {
continue
}
@@ -142,14 +227,16 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
for _, pr := range rs.PRs {
reviewers := []string{}
if rs.IsPrjGitPR(pr.PR) {
reviewers = configReviewers.Prj
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())
}
} else {
pkg := pr.PR.Base.Repo.Name
reviewers = slices.Concat(configReviewers.Pkg, maintainers.ListProjectMaintainers(), maintainers.ListPackageMaintainers(pkg))
reviewers = slices.Concat(configReviewers.Pkg, maintainers.ListProjectMaintainers(), maintainers.ListPackageMaintainers(pkg), configReviewers.PkgOptional)
}
slices.Sort(reviewers)
@@ -160,9 +247,13 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
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
}
@@ -170,6 +261,7 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
user := reviewers[idx]
if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) {
reviewers = slices.Delete(reviewers, idx, idx+1)
LogDebug("removing reviewer:", user)
} else {
idx++
}
@@ -177,8 +269,13 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
// get maintainers associated with the PR too
if len(reviewers) > 0 {
if _, err := gitea.RequestReviews(pr.PR, reviewers...); err != nil {
return fmt.Errorf("Cannot create reviews on %s/%s#%d for [%s]: %w", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index, strings.Join(reviewers, ", "), err)
LogDebug("Requesting reviews from:", reviewers)
if !IsDryRun {
for _, r := range reviewers {
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)
}
}
}
}
}
@@ -188,7 +285,54 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData) bool {
configReviewers := ParseReviewers(rs.Config.Reviewers)
is_reviewed := false
is_manually_reviewed_ok := false
if need_manual_review := rs.Config.ManualMergeOnly || rs.Config.ManualMergeProject; need_manual_review {
prjgit, err := rs.GetPrjGitPR()
if err == nil && prjgit != nil {
reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers())
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)
if err != nil {
LogError("Cannot fetch gita reaviews for PR:", err)
return false
}
prjgit.Reviews = r
if prjgit.Reviews.IsManualMergeOK() {
is_manually_reviewed_ok = true
}
}
if !is_manually_reviewed_ok && !rs.Config.ManualMergeProject {
for _, pr := range rs.PRs {
if rs.IsPrjGitPR(pr.PR) {
continue
}
pkg := pr.PR.Base.Repo.Name
reviewers := slices.Concat(configReviewers.Pkg, maintainers.ListPackageMaintainers(pkg))
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)
if err != nil {
LogError("Cannot fetch gita reaviews for PR:", err)
return false
}
pr.Reviews = r
if !pr.Reviews.IsManualMergeOK() {
LogInfo("Not approved manual merge. PR:", pr.PR.URL)
return false
}
}
is_manually_reviewed_ok = true
}
if !is_manually_reviewed_ok {
LogInfo("manual merge not ok")
return false
}
}
for _, pr := range rs.PRs {
var reviewers []string
var pkg string
@@ -210,14 +354,15 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
LogError("Cannot fetch gita reaviews for PR:", err)
return false
}
is_reviewed = r.IsApproved()
LogDebug(pr.PR.Base.Repo.Name, is_reviewed)
if !is_reviewed {
is_manually_reviewed_ok = r.IsApproved()
LogDebug(pr.PR.Base.Repo.Name, is_manually_reviewed_ok)
if !is_manually_reviewed_ok {
return false
}
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review {
if is_reviewed = maintainers.IsApproved(pkg, r.reviews, pr.PR.User.UserName); !is_reviewed {
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.reviews, pr.PR.User.UserName); !is_manually_reviewed_ok {
LogDebug(" not approved?", pkg)
return false
}
@@ -225,14 +370,15 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
LogDebug("PrjGit PR -- bot created, no need for review")
}
}
return is_reviewed
return is_manually_reviewed_ok
}
func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
prjgit, err := rs.GetPrjGitPR()
prjgit_info, err := rs.GetPrjGitPR()
if err != nil {
return err
}
prjgit := prjgit_info.PR
remote, err := git.GitClone(DefaultGitPrj, rs.Config.Branch, prjgit.Base.Repo.SSHURL)
PanicOnError(err)

View File

@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path"
"slices"
"strings"
"testing"
@@ -14,6 +15,37 @@ 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 reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
timeline := make([]*models.TimelineComment, len(reviews))
for idx, review := range reviews {
if review.ID == 0 {
review.ID = int64(idx) + 100
}
timeline[idx] = &models.TimelineComment{
Type: common.TimelineCommentType_Review,
ReviewID: review.ID,
}
}
return timeline
}
func TestPR(t *testing.T) {
baseConfig := common.AutogitConfig{
@@ -27,6 +59,7 @@ func TestPR(t *testing.T) {
pr *models.PullRequest
pr_err error
reviews []*models.PullReview
timeline []*models.TimelineComment
review_error error
}
@@ -40,26 +73,26 @@ func TestPR(t *testing.T) {
consistentSet bool
prjGitPRIndex int
reviewSetFetcher func(*mock_common.MockGiteaPRFetcher) (*common.PRSet, error)
reviewSetFetcher func(*mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error)
}{
{
name: "Error fetching PullRequest",
data: []prdata{
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}, pr_err: errors.New("Missing PR")},
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}, pr_err: errors.New("Missing PR")},
},
prjGitPRIndex: -1,
},
{
name: "Error fetching PullRequest in PrjGit",
data: []prdata{
{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}, pr_err: errors.New("missing PR")},
{pr: &models.PullRequest{Body: "", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}, pr_err: errors.New("missing PR")},
{pr: &models.PullRequest{Body: "", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
},
},
{
name: "Error fetching prjgit",
data: []prdata{
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
},
resLen: 1,
prjGitPRIndex: -1,
@@ -67,8 +100,8 @@ func TestPR(t *testing.T) {
{
name: "Review set is consistent",
data: []prdata{
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
},
resLen: 2,
prjGitPRIndex: 1,
@@ -78,8 +111,8 @@ func TestPR(t *testing.T) {
{
name: "Review set is consistent: 1pkg",
data: []prdata{
{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
},
resLen: 2,
prjGitPRIndex: 1,
@@ -88,9 +121,9 @@ func TestPR(t *testing.T) {
{
name: "Review set is consistent: 2pkg",
data: []prdata{
{pr: &models.PullRequest{Body: "some desc", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
{pr: &models.PullRequest{Body: "some other desc\nPR: foo/fer#33", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}}},
{pr: &models.PullRequest{Body: "some desc", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "some other desc\nPR: foo/fer#33", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
},
resLen: 3,
prjGitPRIndex: 1,
@@ -100,7 +133,7 @@ func TestPR(t *testing.T) {
name: "Review set of prjgit PR is consistent",
data: []prdata{
{
pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}},
pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -112,38 +145,385 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0,
consistentSet: true,
reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRFetcher) (*common.PRSet, error) {
return common.FetchPRSet(mock, "foo", "barPrj", 42, &baseConfig)
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
},
},
{
name: "Review set is consistent: 2pkg",
data: []prdata{
{pr: &models.PullRequest{Body: "PR: foo/barPrj#222", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
{pr: &models.PullRequest{Body: "PR: test/repo2#41", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#20", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#222", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo2#41", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#20", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
},
resLen: 3,
prjGitPRIndex: 2,
consistentSet: true,
},
{
name: "WIP PR is not approved",
data: []prdata{
{
pr: &models.PullRequest{Body: "", Title: "WIP: some title", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
},
resLen: 1,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
},
},
{
name: "Manual review is missing",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 2,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
})
},
},
{
name: "Manual review is done, via PrjGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "merge ok", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 2,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
})
},
},
{
name: "Manual review is done, via PrjGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "merge ok", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 2,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
ManualMergeProject: true,
})
},
},
{
name: "Manual review is not done, via PrjGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "merge ok", User: &models.User{UserName: "notm2"}, State: common.ReviewStateApproved},
{Body: "merge not ok", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 2,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
ManualMergeProject: true,
})
},
},
{
name: "Manual review is done via PackageGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "Merge ok", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 2,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
})
},
},
{
name: "Manual review done via PkgGits",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "Merge OK!", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "merge ok", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 3,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
})
},
},
{
name: "Manual review done via PkgGits not allowed",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "Merge OK!", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "merge ok", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 3,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
ManualMergeProject: true,
})
},
},
{
name: "Manual review is is missing on one PR",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
},
},
},
resLen: 3,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
ManualMergeOnly: true,
})
},
},
{
name: "PR is approved with negative optional review",
data: []prdata{
{
pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "bot"}, State: common.ReviewStateRequestChanges},
},
},
},
resLen: 1,
prjGitPRIndex: 0,
consistentSet: true,
reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
config := common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2", "~*bot"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
}
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &config)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
pr_mock := mock_common.NewMockGiteaPRFetcher(ctl)
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
review_mock := mock_common.NewMockGiteaPRChecker(ctl)
// reviewer_mock := mock_common.NewMockGiteaReviewRequester(ctl)
if test.reviewSetFetcher == nil { // if we are fetching the prjgit directly, the these mocks are not called
if test.prjGitPRIndex >= 0 {
pr_mock.EXPECT().GetAssociatedPrjGitPR(baseConfig.Organization, baseConfig.GitProjectName, test.data[0].pr.Base.Repo.Owner.UserName, test.data[0].pr.Base.Repo.Name, test.data[0].pr.Index).
pr_mock.EXPECT().GetPullRequest(baseConfig.Organization, baseConfig.GitProjectName, test.prjGitPRIndex).
Return(test.data[test.prjGitPRIndex].pr, test.data[test.prjGitPRIndex].pr_err)
} else if test.prjGitPRIndex < 0 {
// no prjgit PR
pr_mock.EXPECT().GetAssociatedPrjGitPR(baseConfig.Organization, baseConfig.GitProjectName, test.data[0].pr.Base.Repo.Owner.UserName, test.data[0].pr.Base.Repo.Name, test.data[0].pr.Index).
pr_mock.EXPECT().GetPullRequest(baseConfig.Organization, baseConfig.GitProjectName, gomock.Any()).
Return(nil, nil)
}
}
@@ -155,6 +535,10 @@ func TestPR(t *testing.T) {
test_err = data.pr_err
}
review_mock.EXPECT().GetPullRequestReviews(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.reviews, data.review_error).AnyTimes()
if data.timeline == nil {
data.timeline = reviewsToTimeline(data.reviews)
}
review_mock.EXPECT().GetTimeline(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.timeline, nil).AnyTimes()
}
var res *common.PRSet
@@ -163,7 +547,7 @@ func TestPR(t *testing.T) {
if test.reviewSetFetcher != nil {
res, err = test.reviewSetFetcher(pr_mock)
} else {
res, err = common.FetchPRSet(pr_mock, "test", "repo", 42, &baseConfig)
res, err = common.FetchPRSet("test", pr_mock, "test", "repo", 42, &baseConfig)
}
if err == nil {
@@ -198,7 +582,7 @@ func TestPR(t *testing.T) {
pr_found := false
if test.prjGitPRIndex >= 0 {
for i := range test.data {
if PrjGitPR == test.data[i].pr && i == test.prjGitPRIndex {
if PrjGitPR.PR == test.data[i].pr && i == test.prjGitPRIndex {
t.Log("found at index", i)
pr_found = true
}
@@ -222,6 +606,8 @@ func TestPR(t *testing.T) {
*/
maintainers := mock_common.NewMockMaintainershipData(ctl)
maintainers.EXPECT().ListPackageMaintainers(gomock.Any()).Return([]string{}).AnyTimes()
maintainers.EXPECT().ListProjectMaintainers().Return([]string{}).AnyTimes()
maintainers.EXPECT().IsApproved(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes()
if isApproved := res.IsApproved(review_mock, maintainers); isApproved != test.reviewed {
@@ -242,8 +628,10 @@ func TestPRAssignReviewers(t *testing.T) {
reviewer string
}
pkgReviews []*models.PullReview
prjReviews []*models.PullReview
pkgReviews []*models.PullReview
pkgTimeline []*models.TimelineComment
prjReviews []*models.PullReview
prjTimeline []*models.TimelineComment
expectedReviewerCall [2][]string
}{
@@ -331,8 +719,8 @@ func TestPRAssignReviewers(t *testing.T) {
},
pkgReviews: []*models.PullReview{
{
State: common.ReviewStateApproved,
User: &models.User{UserName: "user2"},
State: common.ReviewStateApproved,
User: &models.User{UserName: "user2"},
},
{
State: common.ReviewStatePending,
@@ -353,15 +741,59 @@ func TestPRAssignReviewers(t *testing.T) {
},
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer"}},
},
{
name: "Stale optional review is not done, re-request it",
config: common.AutogitConfig{
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2", "~bot"},
},
pkgReviews: []*models.PullReview{
{
State: common.ReviewStateApproved,
User: &models.User{UserName: "bot"},
Stale: true,
},
{
State: common.ReviewStateApproved,
User: &models.User{UserName: "user2"},
},
{
State: common.ReviewStatePending,
User: &models.User{UserName: "prjmaintainer"},
},
},
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"},
},
},
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer", "bot"}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
pr_mock := mock_common.NewMockGiteaPRFetcher(ctl)
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
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"},
@@ -371,7 +803,8 @@ func TestPRAssignReviewers(t *testing.T) {
Index: 1,
}, nil)
review_mock.EXPECT().GetPullRequestReviews("other", "pkgrepo", int64(1)).Return(test.pkgReviews, nil)
pr_mock.EXPECT().GetAssociatedPrjGitPR("org", "repo", "other", "pkgrepo", int64(1)).Return(&models.PullRequest{
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"}},
@@ -380,11 +813,12 @@ func TestPRAssignReviewers(t *testing.T) {
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().Return([]string{"prjmaintainer"}).AnyTimes()
maintainership_mock.EXPECT().ListPackageMaintainers("pkgrepo").Return([]string{"pkgmaintainer"}).AnyTimes()
prs, _ := common.FetchPRSet(pr_mock, "other", "pkgrepo", int64(1), &test.config)
prs, _ := common.FetchPRSet("test", pr_mock, "other", "pkgrepo", int64(1), &test.config)
if len(prs.PRs) != 2 {
t.Fatal("PRs not fetched")
}
@@ -393,8 +827,9 @@ func TestPRAssignReviewers(t *testing.T) {
if !prs.IsPrjGitPR(pr.PR) {
r = test.expectedReviewerCall[1]
}
if len(r) > 0 {
review_mock.EXPECT().RequestReviews(pr.PR, r).Return(nil, nil)
slices.Sort(r)
for _, reviewer := range r {
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
}
}
prs.AssignReviewers(review_mock, maintainership_mock)
@@ -428,7 +863,7 @@ func TestPRAssignReviewers(t *testing.T) {
for _, test := range prjgit_tests {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
pr_mock := mock_common.NewMockGiteaPRFetcher(ctl)
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
@@ -441,10 +876,11 @@ func TestPRAssignReviewers(t *testing.T) {
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().Return([]string{"prjmaintainer"}).AnyTimes()
prs, _ := common.FetchPRSet(pr_mock, "org", "repo", int64(1), &test.config)
prs, _ := common.FetchPRSet("test", pr_mock, "org", "repo", int64(1), &test.config)
if len(prs.PRs) != 1 {
t.Fatal("PRs not fetched")
}
@@ -453,8 +889,8 @@ func TestPRAssignReviewers(t *testing.T) {
if !prs.IsPrjGitPR(pr.PR) {
t.Fatal("only prjgit pr here")
}
if len(r) > 0 {
review_mock.EXPECT().RequestReviews(pr.PR, r).Return(nil, nil)
for _, reviewer := range r {
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
}
}
prs.AssignReviewers(review_mock, maintainership_mock)
@@ -516,7 +952,7 @@ func TestPRMerge(t *testing.T) {
mergeError: "Aborting merge",
},
{
name: "Merge conflict in modules",
name: "Merge conflict in modules, auto-resolved",
pr: &models.PullRequest{
Base: &models.PRBranchInfo{
@@ -539,19 +975,23 @@ func TestPRMerge(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
mock := mock_common.NewMockGiteaPRFetcher(ctl)
mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
testDir := t.TempDir()
t.Log("dir:", testDir)
mock.EXPECT().GetPullRequest("org", "prj", int64(1)).Return(test.pr, nil)
set, err := common.FetchPRSet(mock, "org", "prj", 1, config)
set, err := common.FetchPRSet("test", mock, "org", "prj", 1, config)
if err != nil {
t.Fatal(err)
}
gh, _ := common.AllocateGitWorkTree(testDir, "", "")
err = set.Merge(gh)
git, err := gh.CreateGitHandler("org")
err = set.Merge(reviewUnrequestMock, git)
if err != nil && (test.mergeError == "" || (len(test.mergeError) > 0 && !strings.Contains(err.Error(), test.mergeError))) {
os.CopyFS("/tmp/upstream", os.DirFS(repoDir))
@@ -561,3 +1001,53 @@ func TestPRMerge(t *testing.T) {
})
}
}
func TestPRChanges(t *testing.T) {
tests := []struct {
name string
PRs []*models.PullRequest
PrjPRs *models.PullRequest
}{
{
name: "Pkg PR is closed",
PRs: []*models.PullRequest{
{
Base: &models.PRBranchInfo{Repo: &models.Repository{Owner: &models.User{UserName: "org"}, Name: "repo"}},
Index: 42,
State: "merged",
},
},
PrjPRs: &models.PullRequest{
Title: "some PR",
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "prjgit", Owner: &models.User{UserName: "org"}}},
Body: "PR: org/repo#42",
State: "opened",
},
},
}
config := common.AutogitConfig{
Branch: "main",
GitProjectName: "org/prjgit#branch",
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
mock_fetcher := mock_common.NewMockGiteaPRTimelineFetcher(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)
}
PRs, err := common.FetchPRSet("user", mock_fetcher, "org", "repo", 42, &config)
if err != nil {
t.Fatal(err)
}
if PRs.IsConsistent() {
t.Fatal("Inconsistent set!")
}
})
}
}

View File

@@ -22,55 +22,32 @@ import (
"crypto/tls"
"fmt"
"net/url"
"runtime/debug"
"slices"
"strings"
"time"
rabbitmq "github.com/rabbitmq/amqp091-go"
)
const RequestType_CreateBrachTag = "create"
const RequestType_DeleteBranchTag = "delete"
const RequestType_Fork = "fork"
const RequestType_Issue = "issues"
const RequestType_IssueAssign = "issue_assign"
const RequestType_IssueComment = "issue_comment"
const RequestType_IssueLabel = "issue_label"
const RequestType_IssueMilestone = "issue_milestone"
const RequestType_Push = "push"
const RequestType_Repository = "repository"
const RequestType_Release = "release"
const RequestType_PR = "pull_request"
const RequestType_PRAssign = "pull_request_assign"
const RequestType_PRLabel = "pull_request_label"
const RequestType_PRComment = "pull_request_comment"
const RequestType_PRMilestone = "pull_request_milestone"
const RequestType_PRSync = "pull_request_sync"
const RequestType_PRReviewAccepted = "pull_request_review_approved"
const RequestType_PRReviewRejected = "pull_request_review_rejected"
const RequestType_PRReviewRequest = "pull_request_review_request"
const RequestType_PRReviewComment = "pull_request_review_comment"
const RequestType_Wiki = "wiki"
type RequestProcessor interface {
ProcessFunc(*Request) error
}
type ListenDefinitions struct {
type RabbitConnection struct {
RabbitURL *url.URL // amqps://user:password@host/queue
GitAuthor string
Handlers map[string]RequestProcessor
Orgs []string
queueName string
ch *rabbitmq.Channel
topics []string
topicSubChanges chan string // +topic = subscribe, -topic = unsubscribe
}
type RabbitProcessor interface {
GenerateTopics() []string
Connection() *RabbitConnection
ProcessRabbitMessage(msg RabbitMessage) error
}
type RabbitMessage rabbitmq.Delivery
func (l *ListenDefinitions) processTopicChanges(ch *rabbitmq.Channel, queueName string) {
func (l *RabbitConnection) ProcessTopicChanges() {
for {
topic, ok := <-l.topicSubChanges
if !ok {
@@ -80,11 +57,11 @@ func (l *ListenDefinitions) processTopicChanges(ch *rabbitmq.Channel, queueName
LogDebug(" topic change:", topic)
switch topic[0] {
case '+':
if err := ch.QueueBind(queueName, topic[1:], "pubsub", false, nil); err != nil {
if err := l.ch.QueueBind(l.queueName, topic[1:], "pubsub", false, nil); err != nil {
LogError(err)
}
case '-':
if err := ch.QueueUnbind(queueName, topic[1:], "pubsub", nil); err != nil {
if err := l.ch.QueueUnbind(l.queueName, topic[1:], "pubsub", nil); err != nil {
LogError(err)
}
default:
@@ -93,7 +70,7 @@ func (l *ListenDefinitions) processTopicChanges(ch *rabbitmq.Channel, queueName
}
}
func (l *ListenDefinitions) processRabbitMQ(msgCh chan<- RabbitMessage) error {
func (l *RabbitConnection) ProcessRabbitMQ(msgCh chan<- RabbitMessage) error {
queueName := l.RabbitURL.Path
l.RabbitURL.Path = ""
@@ -152,7 +129,7 @@ func (l *ListenDefinitions) processRabbitMQ(msgCh chan<- RabbitMessage) error {
LogDebug(" -- listening to topics:")
l.topicSubChanges = make(chan string)
defer close(l.topicSubChanges)
go l.processTopicChanges(ch, q.Name)
go l.ProcessTopicChanges()
for _, topic := range l.topics {
l.topicSubChanges <- "+" + topic
@@ -174,18 +151,18 @@ func (l *ListenDefinitions) processRabbitMQ(msgCh chan<- RabbitMessage) error {
}
}
func (l *ListenDefinitions) connectAndProcessRabbitMQ(ch chan<- RabbitMessage) {
func (l *RabbitConnection) ConnectAndProcessRabbitMQ(ch chan<- RabbitMessage) {
defer func() {
if r := recover(); r != nil {
LogError(r)
LogError("'crash' RabbitMQ worker. Recovering... reconnecting...")
time.Sleep(5 * time.Second)
go l.connectAndProcessRabbitMQ(ch)
go l.ConnectAndProcessRabbitMQ(ch)
}
}()
for {
err := l.processRabbitMQ(ch)
err := l.ProcessRabbitMQ(ch)
if err != nil {
LogError("Error in RabbitMQ connection. %#v", err)
LogInfo("Reconnecting in 2 seconds...")
@@ -194,49 +171,20 @@ func (l *ListenDefinitions) connectAndProcessRabbitMQ(ch chan<- RabbitMessage) {
}
}
func (l *ListenDefinitions) connectToRabbitMQ() chan RabbitMessage {
func (l *RabbitConnection) ConnectToRabbitMQ(processor RabbitProcessor) <-chan RabbitMessage {
LogInfo("RabbitMQ connection:", l.RabbitURL.String())
l.RabbitURL.User = url.UserPassword(rabbitUser, rabbitPassword)
l.topics = processor.GenerateTopics()
ch := make(chan RabbitMessage, 100)
go l.connectAndProcessRabbitMQ(ch)
go l.ConnectAndProcessRabbitMQ(ch)
return ch
}
func ProcessEvent(f RequestProcessor, request *Request) {
defer func() {
if r := recover(); r != nil {
LogError("panic caught")
if err, ok := r.(error); !ok {
LogError(err)
}
LogError(string(debug.Stack()))
}
}()
if err := f.ProcessFunc(request); err != nil {
LogError(err)
}
}
func (l *ListenDefinitions) generateTopics() []string {
topics := make([]string, 0, len(l.Handlers)*len(l.Orgs))
scope := "suse"
if l.RabbitURL.Hostname() == "rabbit.opensuse.org" {
scope = "opensuse"
}
for _, org := range l.Orgs {
for requestType, _ := range l.Handlers {
topics = append(topics, fmt.Sprintf("%s.src.%s.%s.#", scope, org, requestType))
}
}
slices.Sort(topics)
return slices.Compact(topics)
}
func (l *ListenDefinitions) UpdateTopics() {
newTopics := l.generateTopics()
func (l *RabbitConnection) UpdateTopics(processor RabbitProcessor) {
newTopics := processor.GenerateTopics()
j := 0
next_new_topic:
@@ -273,14 +221,8 @@ next_new_topic:
l.topics = newTopics
}
func (l *ListenDefinitions) ProcessRabbitMQEvents() error {
LogInfo("RabbitMQ connection:", l.RabbitURL.String())
LogDebug("# Handlers:", len(l.Handlers))
LogDebug("# Orgs:", len(l.Orgs))
l.RabbitURL.User = url.UserPassword(rabbitUser, rabbitPassword)
l.topics = l.generateTopics()
ch := l.connectToRabbitMQ()
func ProcessRabbitMQEvents(processor RabbitProcessor) error {
ch := processor.Connection().ConnectToRabbitMQ(processor)
for {
msg, ok := <-ch
@@ -289,36 +231,8 @@ func (l *ListenDefinitions) ProcessRabbitMQEvents() error {
}
LogDebug("event:", msg.RoutingKey)
route := strings.Split(msg.RoutingKey, ".")
if len(route) > 3 {
reqType := route[3]
org := route[2]
if !slices.Contains(l.Orgs, org) {
LogInfo("Got event for unhandeled org:", org)
continue
}
LogDebug("org:", org, "type:", reqType)
if handler, found := l.Handlers[reqType]; found {
/* h, err := CreateRequestHandler()
if err != nil {
log.Println("Cannot create request handler", err)
continue
}
*/
req, err := ParseRequestJSON(reqType, msg.Body)
if err != nil {
LogError("Error parsing request JSON:", err)
continue
} else {
LogDebug("processing req", req.Type)
// h.Request = req
ProcessEvent(handler, req)
}
}
if err := processor.ProcessRabbitMessage(msg); err != nil {
LogError("Error processing", msg.RoutingKey, err)
}
}
}

130
common/rabbitmq_gitea.go Normal file
View File

@@ -0,0 +1,130 @@
package common
/*
* This file is part of Autogits.
*
* Copyright © 2024 SUSE LLC
*
* Autogits is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 2 of the License, or (at your option) any later
* version.
*
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Foobar. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"fmt"
"runtime/debug"
"slices"
"strings"
)
const (
RequestType_CreateBrachTag = "create"
RequestType_DeleteBranchTag = "delete"
RequestType_Fork = "fork"
RequestType_Issue = "issues"
RequestType_IssueAssign = "issue_assign"
RequestType_IssueComment = "issue_comment"
RequestType_IssueLabel = "issue_label"
RequestType_IssueMilestone = "issue_milestone"
RequestType_Push = "push"
RequestType_Repository = "repository"
RequestType_Release = "release"
RequestType_PR = "pull_request"
RequestType_PRAssign = "pull_request_assign"
RequestType_PRLabel = "pull_request_label"
RequestType_PRComment = "pull_request_comment"
RequestType_PRMilestone = "pull_request_milestone"
RequestType_PRSync = "pull_request_sync"
RequestType_PRReviewAccepted = "pull_request_review_approved"
RequestType_PRReviewRejected = "pull_request_review_rejected"
RequestType_PRReviewRequest = "pull_request_review_request"
RequestType_PRReviewComment = "pull_request_review_comment"
RequestType_Wiki = "wiki"
)
type RequestProcessor interface {
ProcessFunc(*Request) error
}
type RabbitMQGiteaEventsProcessor struct {
Handlers map[string]RequestProcessor
Orgs []string
c *RabbitConnection
}
func (gitea *RabbitMQGiteaEventsProcessor) Connection() *RabbitConnection {
if gitea.c == nil {
gitea.c = &RabbitConnection{}
}
return gitea.c
}
func (gitea *RabbitMQGiteaEventsProcessor) GenerateTopics() []string {
topics := make([]string, 0, len(gitea.Handlers)*len(gitea.Orgs))
scope := "suse"
if gitea.c.RabbitURL.Hostname() == "rabbit.opensuse.org" {
scope = "opensuse"
}
for _, org := range gitea.Orgs {
for requestType, _ := range gitea.Handlers {
topics = append(topics, fmt.Sprintf("%s.src.%s.%s.#", scope, org, requestType))
}
}
slices.Sort(topics)
return slices.Compact(topics)
}
func (gitea *RabbitMQGiteaEventsProcessor) ProcessRabbitMessage(msg RabbitMessage) error {
route := strings.Split(msg.RoutingKey, ".")
if len(route) > 3 {
reqType := route[3]
org := route[2]
if !slices.Contains(gitea.Orgs, org) {
LogInfo("Got event for unhandeled org:", org)
return nil
}
LogDebug("org:", org, "type:", reqType)
if handler, found := gitea.Handlers[reqType]; found {
req, err := ParseRequestJSON(reqType, msg.Body)
if err != nil {
LogError("Error parsing request JSON:", err)
return nil
} else {
LogDebug("processing req", req.Type)
// h.Request = req
ProcessEvent(handler, req)
}
}
}
return fmt.Errorf("Invalid routing key: %s", route)
}
func ProcessEvent(f RequestProcessor, request *Request) {
defer func() {
if r := recover(); r != nil {
LogError("panic caught")
if err, ok := r.(error); !ok {
LogError(err)
}
LogError(string(debug.Stack()))
}
}()
if err := f.ProcessFunc(request); err != nil {
LogError(err)
}
}

115
common/rabbitmq_obs.go Normal file
View File

@@ -0,0 +1,115 @@
package common
import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
)
const (
ObsMessageType_PackageBuildFail = "package.build_fail"
ObsMessageType_PackageBuildSuccess = "package.build_success"
ObsMessageType_PackageBuildUnchanged = "package.build_unchanged"
ObsMessageType_RepoBuildFinished = "repo.build_finished"
ObsMessageType_RepoBuildStarted = "repo.build_started"
)
type BuildResultMsg struct {
Status string
Project string `json:"project"`
Package string `json:"package"`
Repo string `json:"repository"`
Arch string `json:"arch"`
StartTime int32 `json:"starttime"`
EndTime int32 `json:"endtime"`
WorkerID string `json:"workerid"`
Version string `json:"versrel"`
Build string `json:"buildtype"`
}
type RepoBuildMsg struct {
Status string
Project string `json:"project"`
Repo string `json:"repo"`
Arch string `json:"arch"`
BuildId string `json:"buildid"`
}
var ObsRabbitMessageError_UnknownMessageType error = errors.New("Unknown message type")
var ObsRabbitMessageError_ParseError error = errors.New("JSON parsing error")
func ParseObsRabbitMessaege(ObsMessageType string, data []byte) (interface{}, error) {
unmarshall := func(data []byte, v any) (interface{}, error) {
if err := json.Unmarshal(data, v); err != nil {
return nil, fmt.Errorf("%w: %s", ObsRabbitMessageError_ParseError, err)
}
return v, nil
}
switch ObsMessageType {
case ObsMessageType_PackageBuildSuccess, ObsMessageType_PackageBuildUnchanged:
ret := &BuildResultMsg{Status: "succeeded"}
return unmarshall(data, ret)
case ObsMessageType_PackageBuildFail:
ret := &BuildResultMsg{Status: "failed"}
return unmarshall(data, ret)
case ObsMessageType_RepoBuildFinished:
ret := &RepoBuildMsg{Status: "finished"}
return unmarshall(data, ret)
case ObsMessageType_RepoBuildStarted:
ret := &RepoBuildMsg{Status: "building"}
return unmarshall(data, ret)
}
return nil, fmt.Errorf("%w: %s", ObsRabbitMessageError_UnknownMessageType, ObsMessageType)
}
type ObsMessageProcessor func(topic string, data []byte) error
type RabbitMQObsBuildStatusProcessor struct {
Handlers map[string]ObsMessageProcessor
c *RabbitConnection
}
func (o *RabbitMQObsBuildStatusProcessor) routingKeyPrefix() string {
if strings.HasSuffix(o.c.RabbitURL.Hostname(), "opensuse.org") {
return "opensuse"
}
return "suse"
}
func (o *RabbitMQObsBuildStatusProcessor) GenerateTopics() []string {
prefix := o.routingKeyPrefix()
msgs := make([]string, len(o.Handlers))
idx := 0
for k, _ := range o.Handlers {
msgs[idx] = prefix + ".obs." + k
idx++
}
slices.Sort(msgs)
return msgs
}
func (o *RabbitMQObsBuildStatusProcessor) Connection() *RabbitConnection {
if o.c == nil {
o.c = &RabbitConnection{}
}
return o.c
}
func (o *RabbitMQObsBuildStatusProcessor) ProcessRabbitMessage(msg RabbitMessage) error {
prefix := o.routingKeyPrefix() + ".obs."
topic := strings.TrimPrefix(msg.RoutingKey, prefix)
if h, ok := o.Handlers[topic]; ok {
return h(topic, msg.Body)
}
return fmt.Errorf("Unhandled message received: %s", msg.RoutingKey)
}

View File

@@ -50,11 +50,13 @@ func TestListenDefinitionsTopicUpdate(t *testing.T) {
u, _ := url.Parse("amqps://rabbit.example.com")
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
l := ListenDefinitions{
Orgs: test.orgs1,
Handlers: make(map[string]RequestProcessor),
topicSubChanges: make(chan string, len(test.topicDelta)*10),
RabbitURL: u,
l := &RabbitMQGiteaEventsProcessor{
Orgs: test.orgs1,
Handlers: make(map[string]RequestProcessor),
c: &RabbitConnection{
RabbitURL: u,
topicSubChanges: make(chan string, len(test.topicDelta)*10),
},
}
slices.Sort(test.topicDelta)
@@ -64,11 +66,11 @@ func TestListenDefinitionsTopicUpdate(t *testing.T) {
}
changes := []string{}
l.UpdateTopics()
l.c.UpdateTopics(l)
a:
for {
select {
case c := <-l.topicSubChanges:
case c := <-l.c.topicSubChanges:
changes = append(changes, c)
default:
changes = []string{}
@@ -78,13 +80,13 @@ func TestListenDefinitionsTopicUpdate(t *testing.T) {
l.Orgs = test.orgs2
l.UpdateTopics()
l.c.UpdateTopics(l)
changes = []string{}
b:
for {
select {
case c := <-l.topicSubChanges:
case c := <-l.c.topicSubChanges:
changes = append(changes, c)
default:
slices.Sort(changes)

View File

@@ -7,21 +7,33 @@ import (
type Reviewers struct {
Prj []string
Pkg []string
PrjOptional []string
PkgOptional []string
}
func ParseReviewers(input []string) *Reviewers {
r := &Reviewers{}
for _, reviewer := range input {
pkg := &r.Pkg
prj := &r.Prj
if reviewer[0] == '~' {
pkg = &r.PkgOptional
prj = &r.PrjOptional
reviewer = reviewer[1:]
}
switch reviewer[0] {
case '*':
r.Prj = append(r.Prj, reviewer[1:])
r.Pkg = append(r.Pkg, reviewer[1:])
*prj = append(*prj, reviewer[1:])
*pkg = append(*pkg, reviewer[1:])
case '-':
r.Prj = append(r.Prj, reviewer[1:])
*prj = append(*prj, reviewer[1:])
case '+':
r.Pkg = append(r.Pkg, reviewer[1:])
*pkg = append(*pkg, reviewer[1:])
default:
r.Pkg = append(r.Pkg, reviewer)
*pkg = append(*pkg, reviewer)
}
}

View File

@@ -12,8 +12,10 @@ func TestReviewers(t *testing.T) {
name string
input []string
prj []string
pkg []string
prj []string
pkg []string
pkg_optional []string
prj_optional []string
}{
{
name: "project and package reviewers",
@@ -22,6 +24,15 @@ func TestReviewers(t *testing.T) {
prj: []string{"5", "7", common.Bot_BuildReview},
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},
pkg: []string{"2", "3", "6"},
prj_optional: []string{"5"},
pkg_optional: []string{"1", "5"},
},
}
for _, test := range tests {
@@ -31,7 +42,13 @@ func TestReviewers(t *testing.T) {
t.Error("unexpected return of ForProject():", reviewers.Prj)
}
if !slices.Equal(reviewers.Pkg, test.pkg) {
t.Error("unexpected return of ForProject():", reviewers.Pkg)
t.Error("unexpected return of ForPackage():", reviewers.Pkg)
}
if !slices.Equal(reviewers.PrjOptional, test.prj_optional) {
t.Error("unexpected return of ForProjectOptional():", reviewers.Prj)
}
if !slices.Equal(reviewers.PkgOptional, test.pkg_optional) {
t.Error("unexpected return of ForPackageOptional():", reviewers.Pkg)
}
})
}

View File

@@ -1,7 +1,9 @@
package common
import (
"regexp"
"slices"
"strings"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
@@ -9,28 +11,110 @@ import (
type PRReviews struct {
reviews []*models.PullReview
reviewers []string
comments []*models.TimelineComment
}
func FetchGiteaReviews(rf GiteaReviewFetcher, reviewers []string, org, repo string, no int64) (*PRReviews, error) {
reviews, err := rf.GetPullRequestReviews(org, repo, no)
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, reviewers []string, org, repo string, no int64) (*PRReviews, error) {
timeline, err := rf.GetTimeline(org, repo, no)
if err != nil {
return nil, err
}
rawReviews, err := rf.GetPullRequestReviews(org, repo, no)
if err != nil {
return nil, err
}
reviews := make([]*models.PullReview, 0, len(reviewers))
var comments []*models.TimelineComment
alreadyHaveUserReview := func(user string) bool {
for _, r := range reviews {
if r.User != nil && r.User.UserName == user {
return true
}
}
return false
}
for idx, item := range timeline {
if item.Type == TimelineCommentType_Review {
for _, r := range rawReviews {
if r.ID == item.ReviewID {
if !alreadyHaveUserReview(r.User.UserName) {
reviews = append(reviews, r)
}
break
}
}
} else if item.Type == TimelineCommentType_Comment {
comments = append(comments, item)
} else if item.Type == TimelineCommentType_PushPull {
LogDebug("cut-off", item.Created)
timeline = timeline[0:idx]
break
} else {
LogDebug("Unhandled timeline type:", item.Type)
}
}
LogDebug("num comments:", len(comments), "reviews:", len(reviews), len(timeline))
return &PRReviews{
reviews: reviews,
reviewers: reviewers,
comments: comments,
}, nil
}
const ManualMergeOK = "^merge\\s+ok(\\W|$)"
var merge_ok_regex *regexp.Regexp = regexp.MustCompile(ManualMergeOK)
func bodyCommandManualMergeOK(body string) bool {
lines := SplitLines(body)
for _, line := range lines {
if merge_ok_regex.MatchString(strings.ToLower(line)) {
return true
}
}
return false
}
func (r *PRReviews) IsManualMergeOK() bool {
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 bodyCommandManualMergeOK(c.Body) {
return true
}
}
}
for _, c := range r.reviews {
if c.Updated != c.Submitted {
continue
}
if slices.Contains(r.reviewers, c.User.UserName) {
if bodyCommandManualMergeOK(c.Body) {
return true
}
}
}
return false
}
func (r *PRReviews) IsApproved() bool {
goodReview := true
LogDebug("reviewers:", r.reviewers)
for _, reviewer := range r.reviewers {
goodReview = false
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
break
}

View File

@@ -14,6 +14,7 @@ func TestReviews(t *testing.T) {
tests := []struct {
name string
reviews []*models.PullReview
timeline []*models.TimelineComment
reviewers []string
fetchErr error
isApproved bool
@@ -25,17 +26,17 @@ func TestReviews(t *testing.T) {
isApproved: true,
},
{
name: "Single reviewer done",
reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}},
reviewers: []string{"user1"},
isApproved: true,
name: "Single reviewer done",
reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}},
reviewers: []string{"user1"},
isApproved: true,
isReviewedByTest1: true,
},
{
name: "Two reviewer, one not approved",
reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}},
reviewers: []string{"user1", "user2"},
isApproved: false,
name: "Two reviewer, one not approved",
reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}},
reviewers: []string{"user1", "user2"},
isApproved: false,
isReviewedByTest1: true,
},
{
@@ -44,8 +45,8 @@ func TestReviews(t *testing.T) {
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, Stale: true},
},
reviewers: []string{"user1", "user2"},
isApproved: false,
reviewers: []string{"user1", "user2"},
isApproved: false,
isReviewedByTest1: true,
},
{
@@ -54,8 +55,8 @@ func TestReviews(t *testing.T) {
&models.PullReview{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
},
reviewers: []string{"user1", "user2"},
isApproved: false,
reviewers: []string{"user1", "user2"},
isApproved: false,
isPendingByTest1: true,
},
{
@@ -63,9 +64,9 @@ func TestReviews(t *testing.T) {
reviews: []*models.PullReview{
&models.PullReview{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true},
},
reviewers: []string{"user1", "user2"},
isApproved: false,
isPendingByTest1: false,
reviewers: []string{"user1", "user2"},
isApproved: false,
isPendingByTest1: false,
isReviewedByTest1: false,
},
{
@@ -74,8 +75,8 @@ func TestReviews(t *testing.T) {
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
},
reviewers: []string{"user1", "user2"},
isApproved: true,
reviewers: []string{"user1", "user2"},
isApproved: true,
isReviewedByTest1: true,
},
{
@@ -84,8 +85,8 @@ func TestReviews(t *testing.T) {
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, Dismissed: true},
},
reviewers: []string{"user1", "user2"},
isApproved: false,
reviewers: []string{"user1", "user2"},
isApproved: false,
isReviewedByTest1: true,
},
{
@@ -94,9 +95,9 @@ func TestReviews(t *testing.T) {
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
},
reviewers: []string{"user1", "user2"},
fetchErr: errors.New("System error fetching reviews."),
isApproved: true,
reviewers: []string{"user1", "user2"},
fetchErr: errors.New("System error fetching reviews."),
isApproved: true,
isReviewedByTest1: true,
},
{
@@ -106,8 +107,23 @@ func TestReviews(t *testing.T) {
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user4"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
},
reviewers: []string{"user1", "user2"},
isApproved: true,
reviewers: []string{"user1", "user2"},
isApproved: true,
isReviewedByTest1: true,
},
{
name: "Review ignored before push",
reviews: []*models.PullReview{
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}, ID: 1001},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, ID: 1000},
},
timeline: []*models.TimelineComment{
&models.TimelineComment{Type: common.TimelineCommentType_Review, ReviewID: 1001},
&models.TimelineComment{Type: common.TimelineCommentType_PushPull},
&models.TimelineComment{Type: common.TimelineCommentType_Review, ReviewID: 1000},
},
reviewers: []string{"user1", "user2"},
isApproved: false,
isReviewedByTest1: true,
},
}
@@ -115,8 +131,12 @@ func TestReviews(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
rf := mock_common.NewMockGiteaReviewFetcher(ctl)
rf := mock_common.NewMockGiteaReviewTimelineFetcher(ctl)
if test.timeline == nil {
test.timeline = reviewsToTimeline(test.reviews)
}
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)

View File

@@ -113,6 +113,10 @@ func (s *Submodule) parseKeyValue(line string) error {
return nil
}
func (s *Submodule) ManifestSubmodulePath(manifest *Manifest) string {
return manifest.SubdirForPackage(s.Path)
}
func ParseSubmodulesFile(reader io.Reader) ([]Submodule, error) {
data, err := io.ReadAll(reader)
if err != nil {

View File

@@ -8,20 +8,20 @@ import (
)
const (
CommentType_ReviewRequested = "review_request"
CommentType_Review = "review"
CommentType_PushPull = "pull_push"
CommentType_DismissReview = "dismiss_review"
TimelineCommentType_ReviewRequested = "review_request"
TimelineCommentType_Review = "review"
TimelineCommentType_PushPull = "pull_push"
TimelineCommentType_PullRequestRef = "pull_ref"
TimelineCommentType_DismissReview = "dismiss_review"
TimelineCommentType_Comment = "comment"
)
func FetchTimelineSinceReviewRequestOrPush(gitea GiteaTimelineFetcher, groupName, headSha, org, repo string, id int64) ([]*models.TimelineComment, error) {
func FetchTimelineSinceLastPush(gitea GiteaTimelineFetcher, headSha, org, repo string, id int64) ([]*models.TimelineComment, error) {
timeline, err := gitea.GetTimeline(org, repo, id)
if err != nil {
return nil, err
}
idx := len(timeline) - 1
//{"is_force_push":true,"commit_ids":["36e43509be1b13a1a8fc63a4361405de04cc621ab16935f88968c46193221bb6","732246a48fbc6bac9df16c0b0ca23ce0f6fbabd9990795863b6d1f0ef3f242c8"]}
type PullPushData struct {
IsForcePush bool `json:"is_force_push"`
@@ -29,24 +29,35 @@ func FetchTimelineSinceReviewRequestOrPush(gitea GiteaTimelineFetcher, groupName
}
// trim timeline to last push update or last time review request was requested
for ; idx > 0; idx-- {
e := timeline[idx]
if e.Type == CommentType_PushPull {
for i, e := range timeline {
if e.Type == TimelineCommentType_PushPull {
var push PullPushData
if err := json.Unmarshal([]byte(e.Body), &push); err != nil {
LogError(err)
}
if slices.Contains(push.CommitIds, headSha) {
break
return timeline[0:i], nil
}
} else if e.Type == CommentType_ReviewRequested && e.Assignee != nil && e.Assignee.UserName == groupName {
// review request is cut-off for reviews too
break
}
}
timeline = timeline[idx:]
return timeline, nil
}
func FetchTimelineSinceReviewRequestOrPush(gitea GiteaTimelineFetcher, groupName, headSha, org, repo string, id int64) ([]*models.TimelineComment, error) {
timeline, err := FetchTimelineSinceLastPush(gitea, headSha, org, repo, id)
if err != nil {
return nil, err
}
// trim timeline to last push update or last time review request was requested
for i, e := range timeline {
if e.Type == TimelineCommentType_ReviewRequested && e.Assignee != nil && e.Assignee.UserName == groupName {
// review request is cut-off for reviews too
return timeline[0:i], nil
}
}
return timeline, nil
}

View File

@@ -19,11 +19,16 @@ package common
*/
import (
"bufio"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
func SplitLines(str string) []string {
@@ -121,3 +126,50 @@ func (giturl *GitUrl) RemoteName() string {
return strings.ToLower(giturl.Org) + "_" + strings.ToLower(giturl.Repo)
}
func PRtoString(pr *models.PullRequest) string {
if pr == nil {
return "(null)"
}
return fmt.Sprintf("%s/%s#%d", pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
}
type DevelProject struct {
Project, Package string
}
type DevelProjects []*DevelProject
func FetchDevelProjects() (DevelProjects, error) {
res, err := http.Get("https://src.opensuse.org/openSUSE/Factory/raw/branch/main/pkgs/_meta/devel_packages")
if err != nil {
return nil, err
}
defer res.Body.Close()
scanner := bufio.NewScanner(res.Body)
ret := []*DevelProject{}
for scanner.Scan() {
d := SplitStringNoEmpty(scanner.Text(), " ")
if len(d) == 2 {
ret = append(ret, &DevelProject{
Project: d[1],
Package: d[0],
})
}
}
return ret, nil
}
var DevelProjectNotFound = errors.New("Devel project not found")
func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
for _, item := range d {
if item.Package == pkg {
return item.Project, nil
}
}
return "", DevelProjectNotFound
}

View File

@@ -73,6 +73,10 @@ func runObsCommand(args ...string) ([]string, error) {
var DebugMode bool
func giteaPackage(pkg string) string {
return strings.ReplaceAll(pkg, "+", "_")
}
func projectMaintainer(obs *common.ObsClient, prj string) ([]string, []string) { // users, groups
meta, err := obs.GetProjectMeta(prj)
if err != nil {
@@ -168,13 +172,14 @@ func gitImporter(prj, pkg string) error {
func cloneDevel(git common.Git, gitDir, outName, urlString string) error {
url, err := url.Parse(urlString)
branch := url.Fragment
// branch := url.Fragment
url.Fragment = ""
params := []string{"clone"}
if len(branch) > 0 {
params = append(params, "-b", branch)
}
/* if len(branch) > 0 {
params = append(params, "-b", branch)
}
*/
params = append(params, url.String(), outName)
if err != nil {
@@ -185,13 +190,16 @@ func cloneDevel(git common.Git, gitDir, outName, urlString string) error {
}
func importRepos(packages []string) {
RepoToObsName := make(map[string]string)
factoryRepos := make([]*models.Repository, 0, len(packages)*2)
develProjectPackages := make([]string, 0, len(packages))
for _, pkg := range packages {
src_pkg_name := strings.Split(pkg, ":")
RepoToObsName[giteaPackage(src_pkg_name[0])] = src_pkg_name[0]
repo, err := client.Repository.RepoGet(
repository.NewRepoGetParams().
WithDefaults().WithOwner("pool").WithRepo(src_pkg_name[0]),
WithDefaults().WithOwner("pool").WithRepo(giteaPackage(src_pkg_name[0])),
r.DefaultAuthentication)
if err != nil {
@@ -206,10 +214,19 @@ func importRepos(packages []string) {
}
}
log.Println("Num repos found:", len(factoryRepos))
if len(develProjectPackages) > 0 {
log.Println("Num of repos that need to create:", len(develProjectPackages))
log.Println("Create the following packages in pool to continue:", strings.Join(develProjectPackages, " "))
if forceNonPoolPackages {
log.Println(" IGNORING and will create these as non-pool packages!")
} else {
os.Exit(1)
}
}
oldPackageNames := make([]string, 0, len(factoryRepos))
for _, repo := range factoryRepos {
oldPackageNames = append(oldPackageNames, repo.Name)
oldPackageNames = append(oldPackageNames, RepoToObsName[repo.Name])
}
// fork packags from pool
@@ -231,48 +248,60 @@ func importRepos(packages []string) {
log.Println("adding remotes...")
for i := 0; i < len(factoryRepos); i++ {
pkg := factoryRepos[i]
pkgName := RepoToObsName[pkg.Name]
gitName := pkg.Name
// verify that package was created by `git-importer`, or it's scmsync package and clone it
fi, err := os.Stat(filepath.Join(git.GetPath(), pkg.Name))
fi, err := os.Stat(filepath.Join(git.GetPath(), gitName))
if os.IsNotExist(err) {
if slices.Contains(develProjectPackages, pkg.Name) {
if slices.Contains(develProjectPackages, pkgName) {
// failed import of former factory package
log.Println("Failed to import former factory pkg:", pkgName)
continue
}
// scmsync?
devel_project, err := runObsCommand("develproject", "openSUSE:Factory", pkg.Name)
if err != nil || len(devel_project) != 1 {
log.Panicln("devel project len:", len(devel_project), "for", pkg.Name, "err:", err)
devel_project, err := devel_projects.GetDevelProject(pkgName)
if err != nil {
log.Panicln("devel project not found for", RepoToObsName[pkg.Name], "err:", err)
}
d := strings.Split(devel_project[0], "/")
if len(d) != 2 {
log.Panicln("expected devel project/package. got:", d)
}
meta, _ := obs.GetPackageMeta(d[0], d[1])
meta, _ := obs.GetPackageMeta(devel_project, pkgName)
if len(meta.ScmSync) > 0 {
if err2 := cloneDevel(git, "", pkg.Name, meta.ScmSync); err != nil {
if err2 := cloneDevel(git, "", gitName, meta.ScmSync); err != nil {
log.Panicln(err2)
}
git.GitExecOrPanic(pkg.Name, "checkout", "-B", "main")
if err2 := git.GitExec(gitName, "checkout", "-B", "main"); err2 != nil {
git.GitExecOrPanic(gitName, "checkout", "-B", "master")
}
continue
}
// try again, should now exist
if fi, err = os.Stat(filepath.Join(git.GetPath(), pkg.Name)); err != nil {
if fi, err = os.Stat(filepath.Join(git.GetPath(), gitName)); err != nil {
log.Panicln(err)
}
} else if err != nil {
log.Panicln(err)
} else {
// verify that we do not have scmsync for imported packages
meta, err := obs.GetPackageMeta(prj, pkg.Name)
meta, err := obs.GetPackageMeta(prj, pkgName)
if err != nil {
log.Panicln(err)
}
if len(meta.ScmSync) > 0 {
log.Panicln("importing an scmsync package??:", prj, pkg.Name)
u, err := url.Parse(meta.ScmSync)
if err != nil {
log.Println("Invlid scmsync in", pkg, meta.ScmSync, err)
}
o, err := url.Parse(strings.TrimSpace(git.GitExecWithOutputOrPanic(gitName, "remote", "get-url", "origin")))
log.Println("Invlid scmsync in git repo", pkg, meta.ScmSync, err)
if u.Host != o.Host || u.Path != u.Path {
log.Panicln("importing an scmsync package??:", prj, gitName)
} else {
log.Println("previous SCMSYNC package. Pull.")
git.GitExecOrPanic(gitName, "pull", "origin", "HEAD:main")
}
}
}
@@ -281,11 +310,11 @@ func importRepos(packages []string) {
}
// add remote repos
out := git.GitExecWithOutputOrPanic(pkg.Name, "remote", "show", "-n")
out := git.GitExecWithOutputOrPanic(gitName, "remote", "show", "-n")
switch pkg.Owner.UserName {
case "pool":
if !slices.Contains(strings.Split(out, "\n"), "pool") {
out := git.GitExecWithOutputOrPanic(pkg.Name, "remote", "add", "pool", pkg.CloneURL)
out := git.GitExecWithOutputOrPanic(gitName, "remote", "add", "pool", pkg.CloneURL)
if len(strings.TrimSpace(out)) > 1 {
log.Println(out)
}
@@ -329,7 +358,7 @@ func importRepos(packages []string) {
break
} else {
log.Panicln(" *** factory has no branches", branches)
}
}
}
pool_branch := "factory"
@@ -392,12 +421,22 @@ func importRepos(packages []string) {
for i := 0; i < len(develProjectPackages); i++ {
pkg := develProjectPackages[i]
meta, _ := obs.GetPackageMeta(prj, pkg)
if len(meta.ScmSync) > 0 {
if err2 := cloneDevel(git, "", pkg, meta.ScmSync); err2 != nil {
log.Panicln(err2)
meta, err := obs.GetPackageMeta(prj, pkg)
if err != nil {
meta, err = obs.GetPackageMeta(prj, pkg)
if err != nil {
log.Println("Error fetching pkg meta for:", prj, pkg, err)
}
}
if meta == nil {
log.Println(" **** pkg meta is nil? ****")
} else if len(meta.ScmSync) > 0 {
if _, err := os.Stat(path.Join(git.GetPath(), pkg)); os.IsNotExist(err) {
if err2 := cloneDevel(git, "", pkg, meta.ScmSync); err2 != nil {
log.Panicln(err2)
}
git.GitExecOrPanic(pkg, "checkout", "-B", "main")
}
git.GitExecOrPanic(pkg, "checkout", "-B", "main")
continue
} else {
common.PanicOnError(gitImporter(prj, pkg))
@@ -459,7 +498,7 @@ func importRepos(packages []string) {
remotes := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkg.Name, "remote", "show"), "\n")
if !slices.Contains(remotes, "develorigin") {
git.GitExecOrPanic(pkg.Name, "remote", "add", "develorigin", repo.SSHURL)
// git.GitExecOrPanic(pkg.Name, "fetch", "devel")
// git.GitExecOrPanic(pkgName, "fetch", "devel")
}
if slices.Contains(remotes, "origin") {
git.GitExecOrPanic(pkg.Name, "lfs", "fetch", "--all")
@@ -467,8 +506,8 @@ func importRepos(packages []string) {
}
git.GitExecOrPanic(pkg.Name, "push", "develorigin", "main", "-f")
git.GitExec(pkg.Name, "push", "develorigin", "--delete", "factory", "devel")
// git.GitExecOrPanic(pkg.Name, "checkout", "-B", "main", "devel/main")
_, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(repo.Name).WithBody(&models.EditRepoOption{
// git.GitExecOrPanic(pkg.ame, "checkout", "-B", "main", "devel/main")
_, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(giteaPackage(repo.Name)).WithBody(&models.EditRepoOption{
DefaultBranch: "main",
DefaultMergeStyle: "fast-forward-only",
HasPullRequests: true,
@@ -493,12 +532,13 @@ func importRepos(packages []string) {
for _, pkg := range develProjectPackages {
var repo *models.Repository
if repoData, err := client.Repository.RepoGet(repository.NewRepoGetParams().WithOwner(org).WithRepo(pkg), r.DefaultAuthentication); err != nil {
if repoData, err := client.Repository.RepoGet(repository.NewRepoGetParams().WithOwner(org).WithRepo(giteaPackage(pkg)), r.DefaultAuthentication); err != nil {
giteaPkg := giteaPackage(pkg)
_, err := client.Organization.CreateOrgRepo(organization.NewCreateOrgRepoParams().WithOrg(org).WithBody(
&models.CreateRepoOption{
ObjectFormatName: "sha256",
AutoInit: false,
Name: &pkg,
Name: &giteaPkg,
DefaultBranch: "main",
}),
r.DefaultAuthentication,
@@ -508,7 +548,7 @@ func importRepos(packages []string) {
log.Panicln("Error creating new package repository:", pkg, err)
}
ret, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(pkg).WithBody(
ret, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(giteaPkg).WithBody(
&models.EditRepoOption{
HasPullRequests: true,
HasPackages: false,
@@ -548,7 +588,7 @@ func importRepos(packages []string) {
git.GitExecOrPanic(pkg, "push", "develorigin", "main", "-f")
git.GitExec(pkg, "push", "develorigin", "--delete", "factory", "devel")
_, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(pkg).WithBody(&models.EditRepoOption{
_, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(giteaPackage(pkg)).WithBody(&models.EditRepoOption{
DefaultBranch: "main",
DefaultMergeStyle: "fast-forward-only",
}), r.DefaultAuthentication)
@@ -647,7 +687,7 @@ func syncPackageCollaborators(pkg string, orig_uids []common.PersonRepoMeta) []s
missing := []string{}
uids := make([]common.PersonRepoMeta, len(orig_uids))
copy(uids, orig_uids)
collab, err := client.Repository.RepoListCollaborators(repository.NewRepoListCollaboratorsParams().WithOwner(org).WithRepo(pkg), r.DefaultAuthentication)
collab, err := client.Repository.RepoListCollaborators(repository.NewRepoListCollaboratorsParams().WithOwner(org).WithRepo(giteaPackage(pkg)), r.DefaultAuthentication)
if err != nil {
if errors.Is(err, &repository.RepoListCollaboratorsNotFound{}) {
return missing
@@ -668,7 +708,7 @@ func syncPackageCollaborators(pkg string, orig_uids []common.PersonRepoMeta) []s
log.Println("missing collabs for", pkg, ":", uids)
}
for _, u := range uids {
_, err := client.Repository.RepoAddCollaborator(repository.NewRepoAddCollaboratorParams().WithOwner(org).WithRepo(pkg).WithBody(&models.AddCollaboratorOption{
_, err := client.Repository.RepoAddCollaborator(repository.NewRepoAddCollaboratorParams().WithOwner(org).WithRepo(giteaPackage(pkg)).WithBody(&models.AddCollaboratorOption{
Permission: "write",
}).WithCollaborator(u.UserID), r.DefaultAuthentication)
@@ -700,6 +740,10 @@ func syncMaintainersToGitea(pkgs []string) {
devs := []string{}
for _, group := range prjMeta.Groups {
if group.GroupID == "factory-maintainers" {
log.Println("Ignoring factory-maintainers")
continue
}
teamMembers, err := obs.GetGroupMeta(group.GroupID)
if err != nil {
log.Panicln("failed to get group", err)
@@ -795,13 +839,20 @@ func createPrjGit() {
git.GitExecOrPanic(common.DefaultGitPrj, "add", "_config")
}
file, err = os.Create(path.Join(git.GetPath(), common.DefaultGitPrj, "project.build"))
file, err = os.Create(path.Join(git.GetPath(), common.DefaultGitPrj, "staging.config"))
if err != nil {
log.Panicln(err)
}
file.Write([]byte(prj))
file.WriteString("{\n // Reference build project\n \"ObsProject\": \"" + prj + "\",\n}\n")
file.Close()
git.GitExecOrPanic(common.DefaultGitPrj, "add", "project.build")
git.GitExecOrPanic(common.DefaultGitPrj, "add", "staging.config")
if file, err = os.Create(path.Join(git.GetPath(), common.DefaultGitPrj, "workflow.config")); err != nil {
log.Panicln(err)
}
file.WriteString("{\n \"Workflows\": [\"direct\", \"pr\"],\n \"Organization\": \"" + org + "\",\n}\n")
file.Close()
git.GitExecOrPanic(common.DefaultGitPrj, "add", "workflow.config")
}
}
}
@@ -812,6 +863,8 @@ var git common.Git
var obs *common.ObsClient
var prj, org string
var forceBadPool bool
var forceNonPoolPackages bool
var devel_projects common.DevelProjects
func main() {
if err := common.RequireGiteaSecretToken(); err != nil {
@@ -828,6 +881,7 @@ func main() {
flags.SetOutput(helpString)
//workflowConfig := flag.String("config", "", "Repository and workflow definition file")
giteaHost := flags.String("gitea", "src.opensuse.org", "Gitea instance")
obsUrl := flags.String("obs-url", "https://api.opensuse.org", "OBS API Url")
//rabbitUrl := flag.String("url", "amqps://rabbit.opensuse.org", "URL for RabbitMQ instance")
flags.BoolVar(&DebugMode, "debug", false, "Extra debugging information")
// revNew := flag.Int("nrevs", 20, "Number of new revisions in factory branch. Indicator of broken history import")
@@ -836,6 +890,8 @@ func main() {
getMaintainers := flags.Bool("maintainers-only", false, "Get maintainers only and exit")
syncMaintainers := flags.Bool("sync-maintainers-only", false, "Sync maintainers to Gitea and exit")
flags.BoolVar(&forceBadPool, "bad-pool", false, "Force packages if pool has no branches due to bad import")
flags.BoolVar(&forceNonPoolPackages, "non-pool", false, "Allow packages that are not in pool to be created. WARNING: Can't add to factory later!")
specificPackage := flags.String("package", "", "Process specific package only, ignoring the others")
if help := flags.Parse(os.Args[1:]); help == flag.ErrHelp || flags.NArg() != 2 {
printHelp(helpString.String())
@@ -847,25 +903,30 @@ func main() {
// r.SetDebug(true)
client = apiclient.New(r, nil)
obs, _ = common.NewObsClient("api.opensuse.org")
obs, _ = common.NewObsClient(*obsUrl)
gh := common.GitHandlerGeneratorImpl{}
var gh common.GitHandlerGenerator
var err error
git, err = gh.CreateGitHandler("Autogits - Devel Importer", "not.exist", "devel-importer")
devel_projects, err = common.FetchDevelProjects()
if err != nil {
log.Panicln("Failed to allocate git handler. Err:", err)
log.Panic("Cannot load devel projects:", err)
}
log.Println("# devel projects loaded:", len(devel_projects))
if DebugMode {
if len(*debugGitPath) > 0 {
git.Close()
git, err = gh.ReadExistingPath("Autogits - Devel Importer", "not.exist", *debugGitPath)
gh, err = common.AllocateGitWorkTree(*debugGitPath, "Autogits - Devel Importer", "not.exist")
if err != nil {
log.Panicln(err)
}
}
log.Println(" - working directory:" + git.GetPath())
} else {
defer git.Close()
dir, _ := os.MkdirTemp(os.TempDir(), "devel-importer")
gh, err = common.AllocateGitWorkTree(dir, "Autogits - Devel Importer", "not.exist")
if err != nil {
log.Panicln("Failed to allocate git handler", err)
}
}
prj = flags.Arg(0)
@@ -879,6 +940,13 @@ func main() {
}
}
git, err = gh.CreateGitHandler(org)
if err != nil {
log.Panicln("Cannot create git", err)
}
defer git.Close()
log.Println(" - working directory:" + git.GetPath())
/*
for _, pkg := range packages {
if _, err := client.Organization.CreateOrgRepo(organization.NewCreateOrgRepoParams().WithOrg(org).WithBody(
@@ -920,11 +988,15 @@ func main() {
if *purgeOnly {
log.Println("Purging repositories...")
for _, pkg := range packages {
client.Repository.RepoDelete(repository.NewRepoDeleteParams().WithOwner(org).WithRepo(pkg), r.DefaultAuthentication)
client.Repository.RepoDelete(repository.NewRepoDeleteParams().WithOwner(org).WithRepo(giteaPackage(pkg)), r.DefaultAuthentication)
}
os.Exit(10)
}
if len(*specificPackage) != 0 {
importRepos([]string{*specificPackage})
return
}
importRepos(packages)
syncMaintainersToGitea(packages)
}

View File

@@ -11,6 +11,7 @@ import (
"strconv"
"strings"
"time"
"unicode"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
@@ -21,19 +22,32 @@ var acceptRx *regexp.Regexp
var rejectRx *regexp.Regexp
var groupName string
func InitRegex(groupName string) {
acceptRx = regexp.MustCompile("\\s*:\\s*LGTM")
rejectRx = regexp.MustCompile("\\s*:\\s*")
func InitRegex(newGroupName string) {
groupName = newGroupName
acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
rejectRx = regexp.MustCompile("^:\\s*")
}
func ParseReviewLine(reviewText string) (bool, string) {
line := strings.TrimSpace(reviewText)
glen := len(groupName)
if len(line) < glen || line[0:glen] != groupName {
groupTextName := "@" + groupName
glen := len(groupTextName)
if len(line) < glen || line[0:glen] != groupTextName {
return false, line
}
return true, line[glen:]
l := line[glen:]
for idx, r := range l {
if unicode.IsSpace(r) {
continue
} else if r == ':' {
return true, l[idx:]
} else {
return false, line
}
}
return false, line
}
func ReviewAccepted(reviewText string) bool {
@@ -99,23 +113,22 @@ var commentStrings = []string{
"change_time_estimate",
}*/
func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.PullReview {
var good_review *models.PullReview
func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
for _, t := range timeline {
if t.Type == common.CommentType_Review && t.User != nil && t.User.UserName == user && t.Created == t.Updated {
for _, r := range reviews {
if r.ID == t.ReviewID && (ReviewAccepted(r.Body) || ReviewRejected(r.Body)) {
good_review = r
break
}
if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated {
if ReviewAccepted(t.Body) || ReviewRejected(t.Body) {
return t
}
} else if (t.Type == common.CommentType_DismissReview || t.Type == common.CommentType_ReviewRequested) && t.Assignee != nil && t.Assignee.UserName == user {
good_review = nil
}
}
return good_review
return nil
}
func UnrequestReviews(gitea common.Gitea, org, repo string, id int64, users []string) {
if err := 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) {
@@ -167,6 +180,10 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
if config == nil {
common.LogError("Cannot find config for:", fmt.Sprintf("%s/%s#%s", org, repo, pr.Base.Name))
return
}
if pr.State == "closed" {
// dismiss the review
common.LogInfo(" -- closed request, so nothing to review")
@@ -200,9 +217,10 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
for _, reviewer := range requestReviewers {
if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
if review.State == common.ReviewStateApproved && ReviewAccepted(review.Body) {
if ReviewAccepted(review.Body) {
if !common.IsDryRun {
gitea.AddReviewComment(pr, common.ReviewStateApproved, "Signed off by: "+reviewer)
UnrequestReviews(gitea, org, repo, id, requestReviewers)
if !common.IsDryRun {
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
@@ -210,11 +228,12 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
}
common.LogInfo(" -> approved by", reviewer)
common.LogInfo(" review at", review.Submitted)
common.LogInfo(" review at", review.Created)
return
} else if review.State == common.ReviewStateRequestChanges && ReviewRejected(review.Body) {
} else if ReviewRejected(review.Body) {
if !common.IsDryRun {
gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Request changes. See review by: "+reviewer)
gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
UnrequestReviews(gitea, org, repo, id, requestReviewers)
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
@@ -239,10 +258,24 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
} else {
common.LogDebug(" Not requesting additional reviewers")
}
// 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 {
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, ", "), ". To review as part of this group, create a comment with contents @"+groupName+": LGTM on a separate line to accept a review. To request changes, write @"+groupName+": followed by reason for rejection. Do not use reviews to review as a group. Editing a comment invalidates that comment.")
gitea.AddComment(pr, helpComment)
}
}
func PeriodReviewCheck(gitea common.Gitea) {
notifications, err := gitea.GetPullNotifications(nil)
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
common.LogError(" Error fetching unread notifications: %w", err)
return

View File

@@ -2,6 +2,76 @@ package main
import "testing"
func TestReviews(t *testing.T) {
func TestReviewApprovalCheck(t *testing.T) {
tests := []struct {
Name string
GroupName string
InString string
Approved bool
Rejected bool
}{
{
Name: "Empty String",
GroupName: "group",
InString: "",
},
{
Name: "Random Text",
GroupName: "group",
InString: "some things LGTM",
},
{
Name: "Group name with Random Text means disapproval",
GroupName: "group",
InString: "@group: some things LGTM",
Rejected: true,
},
{
Name: "Bad name with Approval",
GroupName: "group2",
InString: "@group: LGTM",
},
{
Name: "Bad name with Approval",
GroupName: "group2",
InString: "@group: LGTM",
},
{
Name: "LGTM approval",
GroupName: "group2",
InString: "@group2: LGTM",
Approved: true,
},
{
Name: "approval",
GroupName: "group2",
InString: "@group2: approved",
Approved: true,
},
{
Name: "approval",
GroupName: "group2",
InString: "@group2: approve",
Approved: true,
},
{
Name: "disapproval",
GroupName: "group2",
InString: "@group2: disapprove",
Rejected: true,
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
InitRegex(test.GroupName)
if r := ReviewAccepted(test.InString); r != test.Approved {
t.Error("ReviewAccepted() returned", r, "expecting", test.Approved)
}
if r := ReviewRejected(test.InString); r != test.Rejected {
t.Error("ReviewRejected() returned", r, "expecting", test.Rejected)
}
})
}
}

1
obs-forward-bot/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
forward-bot

293
obs-forward-bot/main.go Normal file
View File

@@ -0,0 +1,293 @@
package main
import (
"flag"
"fmt"
"log"
"net/url"
"os"
"regexp"
"runtime/debug"
"strconv"
"strings"
"time"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
var LastDevelProjectUpdate *time.Time
var Git common.GitHandlerGenerator
func DevelProjectForPR(pr *models.PullRequest) (*common.DevelProject, error) {
devels, err := common.FetchDevelProjects()
if err != nil {
common.LogError("Failed to fetch devel projects:", err)
return nil, err
}
org := pr.Head.Repo.Owner.UserName
pkg := pr.Head.Repo.Name
common.LogDebug("Looking for devel package", org, pkg)
for _, devel_project := range devels {
if devel_project.Package == pkg {
common.LogDebug("Fetching prject meta for", devel_project.Project)
meta, err := Obs.GetProjectMeta(devel_project.Project)
if err != nil {
common.LogError("Failed to fetch devel project OBS meta", err)
return nil, err
}
u, err := url.Parse(meta.ScmSync)
if err != nil {
common.LogError("Failed to parse project scm", err)
return nil, err
}
if u.Hostname() != "src.opensuse.org" || strings.TrimSuffix(u.Path[1:], ".git") != org+"/_ObsPrj" {
common.LogError("Invalid ScmSync format for devel project", meta.ScmSync, "Expected:", u.Path, "!=", org+"/_ObsPrj")
return nil, fmt.Errorf("Invalid ScmSync format for devel project %s", meta.ScmSync)
}
g, err := Git.CreateGitHandler(org)
if err != nil {
common.LogError("Failed to alloate git:", err)
return nil, err
}
defer g.Close()
branch := u.Fragment
u.Fragment = ""
_, err = g.GitClone(common.DefaultGitPrj, branch, u.String())
common.PanicOnError(err)
expectedSha, ok := g.GitSubmoduleCommitId(common.DefaultGitPrj, pkg, branch)
if !ok {
common.LogError("Failed to find", pkg, "in projectgit")
return nil, fmt.Errorf("failed to find %s in projectgit", pkg)
}
if expectedSha == pr.Head.Sha {
// found a match back to the devel project
return devel_project, nil
}
return nil, fmt.Errorf("Failed to match submission to devel project")
}
}
return nil, fmt.Errorf("Failed to find PR in a devel project. Ignoring")
}
func ProcessNotification(notification *models.NotificationThread) {
defer func() {
if r := recover(); r != nil {
common.LogInfo("panic cought --- recovered")
common.LogError(string(debug.Stack()))
}
}()
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 {
common.LogError("Invalid format of notification:", subject.URL, err)
return
}
match := rx.FindStringSubmatch(u.Path)
if match == nil {
common.LogError("** Unexpected format of notification:", subject.URL)
return
}
org := match[1]
repo := match[2]
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)
if err != nil {
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
return
}
repository := notification.Repository
repoorg := repository.Owner.UserName
reponame := repository.Name
if repoorg != org || reponame != repo {
common.LogError(" *** failed to parse org notification. Expected", repoorg, reponame)
return
}
headSha := pr.Head.Sha
timeline, err := common.FetchTimelineSinceLastPush(Gitea, headSha, org, repo, id)
if err != nil {
common.LogError("Failed to fetch comments:", err)
return
}
ObsSrFormat := "OBS SR#%d\n"
ExtractSR := func(body string) int {
rx := regexp.MustCompile("^OBS SR#(\\d+)$")
for _, line := range common.SplitLines(body) {
if m := rx.FindStringSubmatch(line); m != nil && len(m) == 2 {
id, _ := strconv.ParseInt(m[1], 10, 32)
return int(id)
}
}
return 0
}
common.LogDebug("notification", org, repo, id)
superseed := false
for _, timeline := range timeline {
if timeline.Type == common.TimelineCommentType_Comment && timeline.User.UserName == GiteaUser {
// check if SR comment referenced here
if sr := ExtractSR(timeline.Body); sr > 0 {
status, err := Obs.RequestStatus(sr)
if err != nil {
common.LogError("Failed to request OBS request status", err)
return
}
if superseed {
break
}
common.LogInfo("Found status:", status.State.State)
if !common.IsDryRun {
if status.State.State == common.RequestStatus_Accepted {
if _, err := Gitea.AddReviewComment(pr, common.ReviewStateApproved, "SR was accepted in OBS. Approving."); err != nil {
common.LogError("Failed to add review comment to PR:", err)
return
}
} else if status.State.State == common.RequestStatus_Declined || status.State.State == common.RequestStatus_Revoked {
if _, err := Gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "SR was rejected in OBS. Rejecting."); err != nil {
common.LogError("Failed to add review comment to PR:", err)
return
}
} else {
common.LogDebug("Request is in state:", status.State.State, "Waiting.")
return
}
Gitea.SetNotificationRead(notification.ID)
} else {
}
return
}
} else if timeline.Type == common.TimelineCommentType_PushPull {
superseed = true
}
}
// no current SR running, create one
dp, err := DevelProjectForPR(pr)
if err != nil {
common.LogDebug("Failed to process PR:", err)
return
}
if !common.IsDryRun {
meta, err := Obs.CreateSubmitRequest(dp.Project, dp.Package, ObsTarget)
if err != nil {
common.LogError("Failed to create OBS SR: ", dp.Project, dp.Package, "=>", ObsTarget, err)
return
}
for {
// make sure we leave comment here
err = Gitea.AddComment(pr, "Created OBS submit request to "+ObsTarget+"\n\n"+fmt.Sprintf(ObsSrFormat, meta.Id))
if err == nil {
break
}
common.LogError("Failed to create Gitea comment:", err)
common.LogInfo("Waiting 1 minute and retrying to leave comment...")
time.Sleep(time.Minute)
}
} else {
common.LogInfo("Would create a SR from", dp.Project, "/", dp.Package, "=>", ObsTarget)
}
}
func ProcessNotifications() {
// process PRs and issues
notifications, err := Gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
common.LogError("Failed to get notifications.", err)
return
}
for _, notification := range notifications {
ProcessNotification(notification)
}
}
var GiteaUser string
var Gitea common.Gitea
var Obs *common.ObsClient
var ObsTarget, GiteaTargetBranch, GiteaOrg string
func main() {
GiteaHost := flag.String("gitea-host", "https://src.opensuse.org", "Gitea host")
ObsHost := flag.String("obs-host", "https://api.opensuse.org", "OBS instance")
flag.StringVar(&ObsTarget, "obs-target", "openSUSE:Factory", "")
flag.StringVar(&GiteaTargetBranch, "gitea-target", "factory", "")
flag.StringVar(&GiteaOrg, "gitea-org", "pool", "")
debug := flag.Bool("debug", false, "Debug logging")
GitRepoPath := flag.String("git-path", "", "Git repo path")
flag.BoolVar(&common.IsDryRun, "dry", false, "no-op operation")
flag.Parse()
if *debug {
common.SetLoggingLevel(common.LogLevelDebug)
}
var err error
if err = common.RequireGiteaSecretToken(); err != nil {
log.Panic(err)
}
if err = common.RequireObsSecretToken(); err != nil {
log.Panic(err)
}
if Obs, err = common.NewObsClient(*ObsHost); err != nil {
log.Panic(err)
}
Gitea = common.AllocateGiteaTransport(*GiteaHost)
if user, err := Gitea.GetCurrentUser(); err != nil {
log.Panic(err)
} else {
GiteaUser = user.UserName
}
common.LogInfo("Current user:", GiteaUser)
if len(*GitRepoPath) == 0 {
*GitRepoPath, err = os.MkdirTemp(os.TempDir(), "forward-bot")
if err != nil {
common.LogError("Failed to create tempdir:", err)
return
}
}
Git, err = common.AllocateGitWorkTree(*GitRepoPath, "bot", "nothing")
if err != nil {
common.LogError("Failed to allocate git tree", err)
return
}
for {
common.LogDebug("--- Starting processing notifications ---")
ProcessNotifications()
common.LogDebug("--- End processing notifications ---")
time.Sleep(time.Minute * 5)
}
}

View File

@@ -315,7 +315,9 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
}
meta.ScmSync = pr.Head.Repo.CloneURL + "?" + strings.Join(urlPkg, "&") + "#" + pr.Head.Sha
meta.Title = fmt.Sprintf("PR#%d to %s", pr.Index, pr.Base.Name)
meta.PublicFlags = common.Flags{Contents: "<disable/>"}
// QE wants it published ... also we should not hardcode it here, since
// it is configurable via the :PullRequest project
// meta.PublicFlags = common.Flags{Contents: "<disable/>"}
meta.Groups = nil
meta.Persons = nil
@@ -459,7 +461,7 @@ func FetchOurLatestActionableReview(gitea common.Gitea, org, repo string, id int
for idx := len(reviews) - 1; idx >= 0; idx-- {
review := reviews[idx]
if review.User != nil || review.User.UserName == Username {
if review.User == nil || review.User.UserName == Username {
if IsDryRun {
// for purposes of moving forward a no-op check
return review, nil
@@ -547,7 +549,7 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
}
if pr.State != "closed" {
common.LogInfo(" ignoring peding PR", thread.Subject.HTMLURL, " state:", pr.State)
common.LogInfo(" ignoring pending PR", thread.Subject.HTMLURL, " state:", pr.State)
return false
}
@@ -888,7 +890,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
func PollWorkNotifications(giteaUrl string) {
gitea := common.AllocateGiteaTransport(giteaUrl)
data, err := gitea.GetPullNotifications(nil)
data, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
common.LogError(err)
@@ -918,7 +920,7 @@ func PollWorkNotifications(giteaUrl string) {
cleanupFinished := false
for page := int64(1); !cleanupFinished; page++ {
cleanupFinished = true
if data, err := gitea.GetDonePullNotifications(page); data != nil {
if data, err := gitea.GetDoneNotifications(common.GiteaNotificationType_Pull, page); data != nil {
for _, n := range data {
if n.Unread {
common.LogError("Done notification is unread or pinned?", *n.Subject)

View File

@@ -22,6 +22,7 @@ import (
"bytes"
"flag"
"fmt"
"io"
"log"
"net/http"
@@ -78,7 +79,7 @@ func ProjectStatusSummarySvg(project string) []byte {
return ret.Bytes()
}
func PackageStatusSummarySvg(status common.PackageBuildStatus) []byte {
func PackageStatusSummarySvg(status *common.PackageBuildStatus) []byte {
buildStatus, ok := common.ObsBuildStatusDetails[status.Code]
if !ok {
buildStatus = common.ObsBuildStatusDetails["error"]
@@ -108,8 +109,10 @@ func main() {
key := flag.String("key-file", "", "Private key for the TLS certificate")
listen := flag.String("listen", "[::1]:8080", "Listening string")
disableTls := flag.Bool("no-tls", false, "Disable TLS")
obsHost := flag.String("obs-host", "api.opensuse.org", "OBS API endpoint for package status information")
obsHost := flag.String("obs-host", "https://api.opensuse.org", "OBS API endpoint for package status information")
flag.BoolVar(&debug, "debug", false, "Enable debug logging")
RabbitMQHost := flag.String("rabbit-mq", "amqps://rabbit.opensuse.org", "RabbitMQ message bus server")
Topic := flag.String("topic", "opensuse.obs", "RabbitMQ topic prefix")
flag.Parse()
common.PanicOnError(common.RequireObsSecretToken())
@@ -143,21 +146,25 @@ func main() {
res.Header().Add("content-type", "image/svg+xml")
prjStatus := GetCurrentStatus(prj)
if prjStatus == nil {
status := GetDetailedBuildStatus(prj, pkg, repo, arch)
res.Write(PackageStatusSummarySvg(status))
})
http.HandleFunc("GET /{Project}/{Package}/{Repository}/{Arch}/buildlog", func(res http.ResponseWriter, req *http.Request) {
prj := req.PathValue("Project")
pkg := req.PathValue("Package")
repo := req.PathValue("Repository")
arch := req.PathValue("Arch")
// status := GetDetailedBuildStatus(prj, pkg, repo, arch)
data, err := obs.BuildLog(prj, pkg, repo, arch)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
common.LogError("Failed to fetch build log for:", prj, pkg, repo, arch, err)
return
}
defer data.Close()
for _, r := range prjStatus.Result {
if r.Arch == arch && r.Repository == repo {
for _, status := range r.Status {
if status.Package == pkg {
res.Write(PackageStatusSummarySvg(status))
return
}
}
}
}
io.Copy(res, data)
})
go ProcessUpdates()

View File

@@ -0,0 +1,3 @@
package main

View File

@@ -1,8 +1,8 @@
package main
import (
"log"
"slices"
"strings"
"sync"
"time"
@@ -24,14 +24,86 @@ type StatusUpdateMsg struct {
func GetCurrentStatus(project string) *common.BuildResultList {
statusMutex.RLock()
defer statusMutex.RUnlock()
if ret, found := CurrentStatus[project]; found {
statusMutex.RUnlock()
return ret
} else {
go WatchObsProject(obs, project)
}
res, err := obs.BuildStatus(project)
statusMutex.RUnlock()
statusMutex.Lock()
defer statusMutex.Unlock()
if err != nil {
return res
}
CurrentStatus[project] = res
now := time.Now().Unix()
CurrentStatus[project].LastUpdate = now
for _, r := range res.Result {
r.LastUpdate = now
for _, p := range r.Status {
p.LastUpdate = now
}
slices.SortFunc(r.Status, packageSort)
}
slices.SortFunc(res.Result, repoSort)
return res
}
func updatePrjPackage(prjState *common.BuildResultList, pkg string, now int64, pkgState *common.BuildResultList) {
for prjState.
Result[0].Status[0].Package
}
func extractPackageBuildStatus(prjState *common.BuildResultList, pkg string) []*common.PackageBuildStatus {
}
func GetDetailedPackageBuildStatus(prj, pkg string) []*common.PackageBuildStatus {
statusMutex.RLock()
now := time.Now().Unix()
cachedPrj, found := CurrentStatus[prj]
if found {
statusMutex.Unlock()
if now-cachedPrj.LastUpdate < 60 {
return extractPackageBuildStatus(cachedPrj, pkg)
}
}
ret, err := obs.BuildStatus(prj, pkg)
if err != nil {
return nil
}
statusMutex.Lock()
defer statusMutex.Unlock()
updatePrjPackage(cachedPrj, pkg, now, ret)
return extractPackageBuildStatus(cachedPrj, pkg)
}
func GetDetailedBuildStatus(prj, pkg, repo, arch string) *common.PackageBuildStatus {
prjStatus := GetCurrentStatus(prj)
if prjStatus == nil {
return nil
}
for _, r := range prjStatus.Result {
if r.Arch == arch && r.Repository == repo {
for _, status := range r.Status {
if status.Package == pkg {
return &status
}
}
}
}
return nil
}
func ProcessUpdates() {
@@ -53,30 +125,3 @@ func ProcessUpdates() {
}
}
}
func WatchObsProject(obs common.ObsStatusFetcherWithState, ObsProject string) {
old_state := ""
mutex.Lock()
if pos, found := slices.BinarySearch(WatchedRepos, ObsProject); found {
mutex.Unlock()
return
} else {
WatchedRepos = slices.Insert(WatchedRepos, pos, ObsProject)
mutex.Unlock()
}
LogDebug("+ watching", ObsProject)
opts := common.BuildResultOptions{}
for {
state, err := obs.BuildStatusWithState(ObsProject, &opts)
if err != nil {
log.Println(" *** Error fetching build for", ObsProject, err)
time.Sleep(time.Minute)
} else {
opts.OldState = state.State
LogDebug(" --> update", ObsProject, " => ", old_state)
StatusUpdateCh <- StatusUpdateMsg{ObsProject: ObsProject, Result: state}
}
}
}

View File

@@ -103,7 +103,7 @@ func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, co
prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj)
if err != nil {
return fmt.Errorf("Error accessing/creating prjgit: %s/%s#%d err: %w", gitOrg, gitPrj, gitBranch, err)
return fmt.Errorf("Error accessing/creating prjgit: %s/%s#%s err: %w", gitOrg, gitPrj, gitBranch, err)
}
remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL)
@@ -526,7 +526,7 @@ func main() {
log.Fatal(err)
}
var defs common.ListenDefinitions
defs := &common.RabbitMQGiteaEventsProcessor{}
var err error
if len(*basePath) == 0 {
@@ -557,7 +557,7 @@ func main() {
}
log.Println("*** Reconfiguring ***")
updateConfiguration(*configFilename, &defs.Orgs)
defs.UpdateTopics()
defs.Connection().UpdateTopics(defs)
}
}()
signal.Notify(signalChannel, syscall.SIGHUP)
@@ -573,18 +573,17 @@ func main() {
updateConfiguration(*configFilename, &defs.Orgs)
defs.GitAuthor = GitAuthor
defs.RabbitURL, err = url.Parse(*rabbitUrl)
defs.Connection().RabbitURL, err = url.Parse(*rabbitUrl)
if err != nil {
log.Panicf("cannot parse server URL. Err: %#v\n", err)
}
go consistencyCheckProcess()
log.Println("defs:", defs)
log.Println("defs:", *defs)
defs.Handlers = make(map[string]common.RequestProcessor)
defs.Handlers[common.RequestType_Push] = &PushActionProcessor{}
defs.Handlers[common.RequestType_Repository] = &RepositoryActionProcessor{}
log.Fatal(defs.ProcessRabbitMQEvents())
log.Fatal(common.ProcessRabbitMQEvents(defs))
}

View File

@@ -29,7 +29,9 @@ JSON
* _GitProjectName_: package in above org, or `org/package` for PrjGit
* _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.
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
example:
@@ -44,6 +46,29 @@ example:
...
]
Reviewers
---------
Reviews is a list of accounts that need to review package and/or project. They have specific syntax
[~][*|-|+]username
General prefix of ~ indicates advisory reviewer. They will be requested, but ignored otherwise.
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
`[foo, -bar, ~*moo]`
results in
* foo -> package reviews
* bar -> project reviews
* moo -> package and project reviews, but ignored
Maintainership
--------------

View File

@@ -0,0 +1,18 @@
package interfaces
import "src.opensuse.org/autogits/common"
//go:generate mockgen -source=state_checker.go -destination=../mock/state_checker.go -typed -package mock_main
type StateChecker interface {
VerifyProjectState(configs *common.AutogitConfig) ([]*PRToProcess, error)
CheckRepos() error
ConsistencyCheckProcess() error
}
type PRToProcess struct {
Org, Repo, Branch string
}

View File

@@ -162,9 +162,9 @@ func main() {
checker := CreateDefaultStateChecker(*checkOnStart, req, Gitea, time.Duration(*checkIntervalHours)*time.Hour)
go checker.ConsistencyCheckProcess()
listenDefs := common.ListenDefinitions{
listenDefs := &common.RabbitMQGiteaEventsProcessor{
Orgs: orgs,
GitAuthor: GitAuthor,
// GitAuthor: GitAuthor,
Handlers: map[string]common.RequestProcessor{
common.RequestType_PR: req,
common.RequestType_PRSync: req,
@@ -172,7 +172,7 @@ func main() {
common.RequestType_PRReviewRejected: req,
},
}
listenDefs.RabbitURL, _ = url.Parse(*rabbitUrl)
listenDefs.Connection().RabbitURL, _ = url.Parse(*rabbitUrl)
common.PanicOnError(listenDefs.ProcessRabbitMQEvents())
common.PanicOnError(common.ProcessRabbitMQEvents(listenDefs))
}

View File

@@ -16,32 +16,12 @@ import (
)
func TestProjectBranchName(t *testing.T) {
req := common.PullRequestWebhookEvent{
Repository: &common.Repository{
Name: "testingRepo",
},
Pull_Request: &common.PullRequest{
Number: 10,
},
}
branchName := prGitBranchNameForPR(&req)
branchName := prGitBranchNameForPR("testingRepo", 10)
if branchName != "PR_testingRepo#10" {
t.Error("Unexpected branch name:", branchName)
}
}
func TestProjctGitSync(t *testing.T) {
req := common.PullRequestWebhookEvent{
Action: "pull",
Number: 0,
}
if err := processPrjGitPullRequestSync(&req); err != nil {
t.Error(err)
}
}
const LocalCMD = "---"
func gitExecs(t *testing.T, git *common.GitHandlerImpl, cmds [][]string) {
@@ -133,7 +113,6 @@ func TestUpdatePrBranch(t *testing.T) {
}
git := &common.GitHandlerImpl{
DebugLogger: true,
GitCommiter: "TestCommiter",
GitEmail: "test@testing",
GitPath: t.TempDir(),
@@ -146,7 +125,7 @@ func TestUpdatePrBranch(t *testing.T) {
req.Pull_Request.Base.Sha = strings.TrimSpace(revs[1])
req.Pull_Request.Head.Sha = strings.TrimSpace(revs[0])
updateSubmoduleInPR(req, git)
updateSubmoduleInPR("mainRepo", revs[0], git)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", "created commit"))
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", "origin", "+HEAD:+testing"))
git.GitExecOrPanic("prj", "reset", "--hard", "testing")
@@ -171,7 +150,6 @@ func TestCreatePrBranch(t *testing.T) {
}
git := &common.GitHandlerImpl{
DebugLogger: true,
GitCommiter: "TestCommiter",
GitEmail: "test@testing",
GitPath: t.TempDir(),
@@ -184,7 +162,7 @@ func TestCreatePrBranch(t *testing.T) {
req.Pull_Request.Base.Sha = strings.TrimSpace(revs[1])
req.Pull_Request.Head.Sha = strings.TrimSpace(revs[0])
updateSubmoduleInPR(req, git)
updateSubmoduleInPR("testRepo", revs[0], git)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", "created commit"))
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", "origin", "+HEAD:testingCreated"))

View File

@@ -5,14 +5,44 @@ package main
import (
"fmt"
"path"
"runtime/debug"
"slices"
"strings"
"github.com/opentracing/opentracing-go/log"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
func prGitBranchNameForPR(req *common.PullRequestWebhookEvent) string {
return fmt.Sprintf("PR_%s#%d", req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number)
func prGitBranchNameForPR(repo string, prNo int) string {
return fmt.Sprintf("PR_%s#%d", repo, prNo)
}
func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
title_refs := make([]string, 0, len(prset.PRs)-1)
refs := make([]string, 0, len(prset.PRs)-1)
for _, pr := range prset.PRs {
org, repo, idx := pr.PRComponents()
title_refs = append(title_refs, repo)
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
refs = append(refs, ref)
}
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")
if prset.Config.ManualMergeOnly {
desc = desc + "\n\nManualMergeOnly enabled. To merge, 'merge ok' is required in either the project PR or every package PR."
}
if prset.Config.ManualMergeProject {
desc = desc + "\nManualMergeProject enabled. To merge, 'merge ok' is required by project maintainer in the project PR."
}
if !prset.Config.ManualMergeOnly && !prset.Config.ManualMergeProject {
desc = desc + "\nAutomatic merge enabled. This will merge when all review requirements are satisfied."
}
return
}
func verifyRepositoryConfiguration(repo *models.Repository) error {
@@ -29,13 +59,13 @@ func verifyRepositoryConfiguration(repo *models.Repository) error {
return err
}
func updateSubmoduleInPR(req *common.PullRequestWebhookEvent, git common.Git) {
common.LogDebug(req, git)
submoduleName := req.Pull_Request.Base.Repo.Name
commitID := req.Pull_Request.Head.Sha
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submoduleName))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submoduleName), "fetch", "--depth", "1", "origin", commitID))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submoduleName), "checkout", commitID))
func updateSubmoduleInPR(submodule, headSha string, git common.Git) {
common.LogDebug("updating submodule", submodule, "to HEAD", headSha)
// NOTE: this can fail if current PrjGit is pointing to outdated, GC'ed commit
// as long as we can update to newer one later, we are still OK
git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submodule)
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "fetch", "--depth", "1", "origin", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", headSha))
}
type PRProcessor struct {
@@ -49,121 +79,229 @@ func AllocatePRProcessor(req *common.PullRequestWebhookEvent, configs common.Aut
id := req.Pull_Request.Number
branch := req.Pull_Request.Base.Ref
assumed_git_project_name := org + "/" + repo + "#" + branch
PRstr := fmt.Sprintf("%s/%s#%d", org, repo, id)
common.LogInfo("*** Starting processing PR:", PRstr)
common.LogInfo("*** Starting processing PR:", PRstr, "branch:", branch)
c := configs.GetPrjGitConfig(org, repo, branch)
if c == nil {
config := configs.GetPrjGitConfig(org, repo, branch)
if config == nil {
if req.Pull_Request.Base.Repo.Default_Branch == branch {
c = configs.GetPrjGitConfig(org, repo, "")
common.LogDebug("Default branch submission...", org, repo)
config = configs.GetPrjGitConfig(org, repo, "")
}
}
if c == nil {
if config == nil {
common.LogError("Cannot find config for PR.")
return nil, fmt.Errorf("Cannot find config for PR")
}
var config *common.AutogitConfig
for _, c := range configs {
if c.GitProjectName == assumed_git_project_name {
config = c
break
}
if c.Organization == org {
// default branch *or* match branch
if (c.Branch == "" && branch == req.Pull_Request.Base.Repo.Default_Branch) ||
(c.Branch != "" && c.Branch == branch) {
config = c
break
}
}
}
common.LogDebug("found config", config)
if config == nil {
common.LogError("Cannot find config for branch '%s'", req.Pull_Request.Base.Ref)
return nil, fmt.Errorf("Cannot find config for branch '%s'", req.Pull_Request.Base.Ref)
}
git, err := GitHandler.CreateGitHandler(branch)
git, err := GitHandler.CreateGitHandler(config.Organization)
if err != nil {
common.LogError("Cannot allocate GitHandler:", err)
return nil, fmt.Errorf("Error allocating GitHandler. Err: %w", err)
}
common.LogDebug("git path:", git.GetPath())
// git.GitExecOrPanic("", "config", "set", "--global", "advice.submoduleMergeConflict", "false")
// git.GitExecOrPanic("", "config", "set", "--global", "advice.mergeConflict", "false")
return &PRProcessor{
config: config,
git: git,
}, nil
}
func (pr *PRProcessor) CreateOrUpdatePrjGitPR(req *common.PullRequestWebhookEvent) error {
config := pr.config
func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
git := pr.git
branchName := prGitBranchNameForPR(req)
org, prj, _ := config.GetPrjGit()
prOrg := req.Pull_Request.Base.Repo.Owner.Username
prRepo := req.Pull_Request.Base.Repo.Name
if org == prOrg && prj == prRepo {
common.LogDebug("PrjGit PR. No need to update it...")
return nil
}
prjGit, err := Gitea.CreateRepositoryIfNotExist(git, org, prj)
common.PanicOnErrorWithMsg(err, "Error creating a prjgitrepo:", err)
common.PanicOnError(verifyRepositoryConfiguration(prjGit))
remoteName, err := git.GitClone(common.DefaultGitPrj, config.Branch, prjGit.SSHURL)
common.PanicOnError(err)
// check if branch already there, and check that out if available
if err := git.GitExec(common.DefaultGitPrj, "fetch", remoteName, branchName); err == nil {
git.GitExecOrPanic(common.DefaultGitPrj, "checkout", "-B", branchName, remoteName+"/"+branchName)
}
commitMsg := fmt.Sprintf(`auto-created for %s
This commit was autocreated by %s
referencing
`+common.PrPattern,
prRepo,
GitAuthor,
prOrg,
prRepo,
req.Pull_Request.Number,
)
subList, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
common.PanicOnError(err)
if id := subList[prRepo]; id != req.Pull_Request.Head.Sha {
updateSubmoduleInPR(req, git)
status, err := git.GitStatus(common.DefaultGitPrj)
common.LogDebug("status:", status)
common.LogDebug("submodule", prRepo, " hash:", id, " -> ", req.Pull_Request.Head.Sha)
common.PanicOnError(err)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", remoteName, "+HEAD:"+branchName))
if err != nil {
common.LogError("Error fetching submodule list for PrjGit", err)
return err
}
_, err = Gitea.CreatePullRequestIfNotExist(prjGit, branchName, prjGit.DefaultBranch,
fmt.Sprintf("Forwarded PR: %s", prRepo),
fmt.Sprintf(`This is a forwarded pull request by %s
referencing the following pull request:
for _, pr := range prset.PRs {
if prset.IsPrjGitPR(pr.PR) {
continue
}
`+common.PrPattern,
GitAuthor, prOrg, prRepo, req.Pull_Request.Number),
)
org, repo, idx := pr.PRComponents()
prHead := pr.PR.Head.Sha
revert := false
return err
if pr.PR.State != "open" {
prjGitPR, err := prset.GetPrjGitPR()
if prjGitPR != nil {
// remove PR from PrjGit
var valid bool
if prHead, valid = git.GitSubmoduleCommitId(common.DefaultGitPrj, repo, prjGitPR.PR.MergeBase); !valid {
common.LogError("Failed fetching original submodule commit id for repo")
return err
}
}
revert = true
}
// find 'repo' in the submodule list
submodule_found := false
for submodulePath, id := range subList {
if path.Base(submodulePath) == repo {
submodule_found = true
if id != prHead {
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
commitMsg := fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "referencing\n", ref)
if revert {
commitMsg = fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "removing\n", ref)
}
updateSubmoduleInPR(submodulePath, prHead, git)
status, err := git.GitStatus(common.DefaultGitPrj)
common.LogDebug("status:", status)
common.LogDebug("submodule", repo, " hash:", id, " -> ", prHead)
common.PanicOnError(err)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
}
submodule_found = true
break
}
}
if !submodule_found {
common.LogError("Failed to find expected repo:", repo)
}
}
return nil
}
func (pr *PRProcessor) CreatePRjGitPR(prjGitPRbranch string, prset *common.PRSet) error {
git := pr.git
PrjGitOrg, PrjGitRepo, PrjGitBranch := prset.Config.GetPrjGit()
PrjGit, err := Gitea.GetRepository(PrjGitOrg, PrjGitRepo)
if err != nil {
common.LogError("Failed to fetch PrjGit repository data.", PrjGitOrg, PrjGitRepo, err)
return err
}
RemoteName, err := git.GitClone(common.DefaultGitPrj, PrjGitBranch, PrjGit.SSHURL)
common.PanicOnError(err)
git.GitExecOrPanic(common.DefaultGitPrj, "checkout", "-B", prjGitPRbranch, RemoteName+"/"+PrjGitBranch)
headCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
if err != nil {
common.LogError("Failed to fetch PrjGit branch", prjGitPRbranch, err)
return err
}
if err := pr.SetSubmodulesToMatchPRSet(prset); err != nil {
return err
}
newHeadCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
if err != nil {
common.LogError("Failed to fetch updated PrjGit branch", prjGitPRbranch, err)
return err
}
if !common.IsDryRun && headCommit != newHeadCommit {
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", RemoteName, "+HEAD:"+prjGitPRbranch))
title, desc := PrjGitDescription(prset)
pr, err := 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{
RemoveDeadline: true,
})
prinfo := prset.AddPR(pr)
prinfo.RemoteName = RemoteName
}
return nil
}
func (pr *PRProcessor) RebaseAndSkipSubmoduleCommits(prset *common.PRSet, branch string) error {
git := pr.git
PrjGitPR, err := prset.GetPrjGitPR()
common.PanicOnError(err)
remoteBranch := PrjGitPR.RemoteName + "/" + branch
common.LogDebug("Rebasing on top of", remoteBranch)
for conflict := git.GitExec(common.DefaultGitPrj, "rebase", remoteBranch); conflict != nil; {
statuses, err := git.GitStatus(common.DefaultGitPrj)
if err != nil {
git.GitExecOrPanic(common.DefaultGitPrj, "rebase", "--abort")
common.PanicOnError(err)
}
for _, s := range statuses {
if s.SubmoduleChanges != "S..." {
git.GitExecOrPanic(common.DefaultGitPrj, "rebase", "--abort")
return fmt.Errorf("Unexpected conflict in rebase. %s", s)
}
}
conflict = git.GitExec(common.DefaultGitPrj, "rebase", "--skip")
}
return nil
}
func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
_, _, PrjGitBranch := prset.Config.GetPrjGit()
PrjGitPR, err := prset.GetPrjGitPR()
if err != nil {
common.LogError("Updating PrjGitPR but not found?", err)
return err
}
git := pr.git
PrjGit := PrjGitPR.PR.Base.Repo
prjGitPRbranch := PrjGitPR.PR.Head.Name
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
common.PanicOnError(err)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitBranch)
forcePush := false
// trust Gitea here on mergeability
if !PrjGitPR.PR.Mergeable {
common.PanicOnError(pr.RebaseAndSkipSubmoduleCommits(prset, PrjGitBranch))
forcePush = true
}
headCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
if err != nil {
common.LogError("Failed to fetch PrjGit branch", prjGitPRbranch, err)
return err
}
if err := pr.SetSubmodulesToMatchPRSet(prset); err != nil {
return err
}
newHeadCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
if err != nil {
common.LogError("Failed to fetch updated PrjGit branch", prjGitPRbranch, err)
return err
}
if !common.IsDryRun && headCommit != newHeadCommit {
params := []string{"push", PrjGitPR.RemoteName, "+HEAD:" + prjGitPRbranch}
if forcePush {
params = slices.Insert(params, 1, "-f")
}
common.PanicOnError(git.GitExec(common.DefaultGitPrj, params...))
// update PR
PrjGitTitle, PrjGitBody := PrjGitDescription(prset)
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
RemoveDeadline: true,
Title: PrjGitTitle,
Body: PrjGitBody,
})
}
return nil
}
func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
@@ -174,16 +312,10 @@ func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
common.LogInfo("processing opened PR:", req.Pull_Request.Url)
prOrg := req.Pull_Request.Base.Repo.Owner.Username
prRepo := req.Pull_Request.Base.Repo.Name
prNo := int(req.Pull_Request.Number)
common.LogError(req)
prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit()
if prOrg != prjGitOrg || prRepo != prjGitRepo {
if err := pr.CreateOrUpdatePrjGitPR(req); err != nil {
return err
}
}
prset, err := common.FetchPRSet(CurrentUser.UserName, Gitea, prOrg, prRepo, req.Number, config)
if err != nil {
common.LogError("Cannot fetch PRSet:", err)
@@ -191,26 +323,69 @@ func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
}
common.LogInfo("fetched PRSet of size:", len(prset.PRs))
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
prjGitPR, err := prset.GetPrjGitPR()
if err == common.PRSet_PrjGitMissing {
common.LogDebug("Missing PrjGit. Need to create one...")
if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil {
return err
}
} else if err == nil {
common.LogDebug("Found PrjGit PR:", common.PRtoString(prjGitPR.PR))
prjGitPRbranch = prjGitPR.PR.Head.Name
if prjGitPR.PR.State != "open" {
// close entire prset
common.LogInfo("PR State is closed:", prjGitPR.PR.State)
for _, pr := range prset.PRs {
if pr.PR.State == "open" {
org, repo, idx := pr.PRComponents()
Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
State: "closed",
})
}
}
return nil
}
if len(prset.PRs) > 1 {
for _, pr := range prset.PRs {
if prset.IsPrjGitPR(pr.PR) {
continue
}
}
}
if err = pr.UpdatePrjGitPR(prset); err != nil {
return err
}
}
if prjGitPR == nil {
prjGitPR, err = prset.GetPrjGitPR()
if err != nil {
common.LogError("Error fetching PrjGitPR:", err)
return nil
}
}
common.LogDebug("Updated PR")
// 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()
if pr, err := prset.GetPrjGitPR(); err == nil {
remote, err := git.GitClone(common.DefaultGitPrj, prjGitBranch, pr.Base.Repo.CloneURL)
common.LogDebug("Submodule parse begin")
orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.RemoteName+"/"+branch) // merge base must remote branch, checked in prjgit udate
common.PanicOnError(err)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", remote, pr.Base.Ref, pr.Head.Ref)
common.LogDebug("Fetch done")
orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.Base.Sha)
common.PanicOnError(err)
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.Head.Sha)
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.Head.Sha)
common.PanicOnError(err)
common.LogDebug("Submodule parse done")
reset_submodule := func(submodule, sha string) {
spath := path.Join(common.DefaultGitPrj, submodule)
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "update", "--init", "--depth", "1", submodule)
git.GitExecOrPanic(spath, "fetch", "origin", sha)
git.GitExecOrPanic(spath, "checkout", sha)
updateSubmoduleInPR(submodule, sha, git)
}
for path, commit := range new_subs {
@@ -233,25 +408,42 @@ func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
if len(stats) > 0 {
git.GitExecOrPanic(common.DefaultGitPrj, "commit", "-a", "-m", "Sync submodule updates with PR-set")
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "deinit", "--all", "--force")
git.GitExecOrPanic(common.DefaultGitPrj, "push")
if !common.IsDryRun {
git.GitExecOrPanic(common.DefaultGitPrj, "push")
}
}
}
// request build review
PR, err := prset.GetPrjGitPR()
if err != nil {
common.LogError("Error fetching PrjGitPR:", err)
return nil
}
common.LogDebug(" num of reviewers:", len(PR.RequestedReviewers))
org, repo, branch := config.GetPrjGit()
common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch)
if err != nil {
return err
}
err = prset.AssignReviewers(Gitea, maintainers)
// 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 && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
common.LogDebug(" --> checking if superflous PR")
diff, err := git.GitDiff(common.DefaultGitPrj, prjGitPR.PR.MergeBase, prjGitPR.PR.Head.Sha)
if err != nil {
return err
}
if len(diff) == 0 {
common.LogInfo("PR is no-op and can be closed. Closing.")
if !common.IsDryRun {
Gitea.AddComment(prjGitPR.PR, "Pull request no longer contains any changes. Closing.")
_, err = Gitea.UpdatePullRequest(prjGitPR.PR.Base.Repo.Owner.UserName, prjGitPR.PR.Base.Repo.Name, prjGitPR.PR.Index, &models.EditPullRequestOption{
State: "closed",
})
if err != nil {
return err
}
}
return nil
}
common.LogDebug(" --> NOT superflous PR")
}
prset.AssignReviewers(Gitea, maintainers)
for _, pr := range prset.PRs {
if err := verifyRepositoryConfiguration(pr.PR.Base.Repo); err != nil {
common.LogError("Cannot set manual merge... aborting processing")
@@ -271,25 +463,23 @@ func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
return err
}
type PullRequestProcessor interface {
Process(req *common.PullRequestWebhookEvent, git common.Git, config *common.AutogitConfig) error
}
type RequestProcessor struct {
configuredRepos map[string][]*common.AutogitConfig
}
func ProcesPullRequest(req *common.PullRequestWebhookEvent, configs []*common.AutogitConfig) error {
defer func() {
if r := recover(); r != nil {
common.LogInfo("panic cought --- recovered")
common.LogError(string(debug.Stack()))
}
}()
if len(configs) < 1 {
// ignoring pull request against unconfigured project (could be just regular sources?)
return nil
}
if req.Pull_Request.State != "open" {
common.LogError("Can only deal with open PRs. This one is", req.Pull_Request.State)
return nil
}
pr, err := AllocatePRProcessor(req, configs)
if err != nil {
log.Error(err)

View File

@@ -1,84 +0,0 @@
package main
import (
"testing"
"go.uber.org/mock/gomock"
"src.opensuse.org/autogits/common"
mock_common "src.opensuse.org/autogits/common/mock"
)
func TestClosePR(t *testing.T) {
pr := PullRequestClosed{}
config := &common.AutogitConfig{
Reviewers: []string{"reviewer1", "reviewer2"},
Branch: "branch",
Organization: "test",
GitProjectName: "prj",
}
event := &common.PullRequestWebhookEvent{
Action: "closed",
Number: 1,
Pull_Request: &common.PullRequest{
Id: 1,
Base: common.Head{
Ref: "branch",
Sha: "testing",
Repo: &common.Repository{
Name: "testRepo",
Default_Branch: "main1",
},
},
Head: common.Head{
Ref: "branch",
Sha: "testing",
Repo: &common.Repository{
Name: "testRepo",
Default_Branch: "main1",
},
},
},
Repository: &common.Repository{
Owner: &common.Organization{
Username: "test",
},
},
}
git := &common.GitHandlerImpl{
GitCommiter: "tester",
GitEmail: "test@suse.com",
}
t.Run("PR git closed request against PrjGit == no action", func(t *testing.T) {
ctl := gomock.NewController(t)
pr.gitea = mock_common.NewMockGitea(ctl)
git.GitPath = t.TempDir()
config.GitProjectName = "testRepo"
event.Repository.Name = "testRepo"
if err := pr.Process(event, git, config); err != nil {
t.Error("Error PrjGit closed request. Should be no error.", err)
}
})
t.Run("PR git closed", func(t *testing.T) {
ctl := gomock.NewController(t)
pr.gitea = mock_common.NewMockGitea(ctl)
git.GitPath = t.TempDir()
config.GitProjectName = "prjGit"
event.Repository.Name = "tester"
if err := pr.Process(event, git, config); err != nil {
t.Error("Error PrjGit closed request. Should be no error.", err)
}
})
}

View File

@@ -12,13 +12,13 @@ import (
)
func TestOpenPR(t *testing.T) {
pr := PullRequestOpened{}
config := &common.AutogitConfig{
Reviewers: []string{"reviewer1", "reviewer2"},
Branch: "branch",
Organization: "test",
GitProjectName: "prj",
pr := PRProcessor{
config: &common.AutogitConfig{
Reviewers: []string{"reviewer1", "reviewer2"},
Branch: "branch",
Organization: "test",
GitProjectName: "prj",
},
}
event := &common.PullRequestWebhookEvent{
@@ -60,14 +60,14 @@ func TestOpenPR(t *testing.T) {
t.Run("PR git opened request against PrjGit == no action", func(t *testing.T) {
ctl := gomock.NewController(t)
pr.gitea = mock_common.NewMockGitea(ctl)
Gitea = mock_common.NewMockGitea(ctl)
git.GitPath = t.TempDir()
config.GitProjectName = "testRepo"
pr.config.GitProjectName = "testRepo"
event.Repository.Name = "testRepo"
if err := pr.Process(event, git, config); err != nil {
if err := pr.Process(event); err != nil {
t.Error("Error PrjGit opened request. Should be no error.", err)
}
})
@@ -76,10 +76,10 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea
Gitea = gitea
event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy"
pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir()
setupGitForTests(t, git)
@@ -101,7 +101,7 @@ func TestOpenPR(t *testing.T) {
UserName: "test",
},
}
gitea.EXPECT().GetAssociatedPrjGitPR("test", "prjcopy", "test", "testRepo", int64(1)).Return(nil, nil)
// 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().GetPullRequest("test", "testRepo", int64(1)).Return(giteaPR, nil)
@@ -111,7 +111,7 @@ func TestOpenPR(t *testing.T) {
gitea.EXPECT().FetchMaintainershipDirFile("test", "prjcopy", "branch", "_project").Return(nil, "", repository.NewRepoGetRawFileNotFound())
gitea.EXPECT().FetchMaintainershipFile("test", "prjcopy", "branch").Return(nil, "", repository.NewRepoGetRawFileNotFound())
err := pr.Process(event, git, config)
err := pr.Process(event)
if err != nil {
t.Error("error:", err)
}
@@ -121,17 +121,17 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea
Gitea = gitea
event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy"
pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir()
setupGitForTests(t, git)
failedErr := errors.New("Returned error here")
gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(nil, failedErr)
err := pr.Process(event, git, config)
err := pr.Process(event)
if err != failedErr {
t.Error("error:", err)
}
@@ -140,10 +140,10 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea
Gitea = gitea
event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy"
pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir()
setupGitForTests(t, git)
@@ -155,7 +155,7 @@ func TestOpenPR(t *testing.T) {
gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil)
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, failedErr)
err := pr.Process(event, git, config)
err := pr.Process(event)
if err != failedErr {
t.Error("error:", err)
}
@@ -164,10 +164,10 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea
Gitea = gitea
event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy"
pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir()
setupGitForTests(t, git)
@@ -189,7 +189,7 @@ func TestOpenPR(t *testing.T) {
},
}
failedErr := errors.New("Returned error here")
gitea.EXPECT().GetAssociatedPrjGitPR("test", "prjcopy", "test", "testRepo", int64(1)).Return(nil, nil)
// gitea.EXPECT().GetAssociatedPrjGitPR("test", "prjcopy", "test", "testRepo", int64(1)).Return(nil, nil)
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)
@@ -199,7 +199,7 @@ func TestOpenPR(t *testing.T) {
gitea.EXPECT().FetchMaintainershipDirFile("test", "prjcopy", "branch", "_project").Return(nil, "", repository.NewRepoGetRawFileNotFound())
gitea.EXPECT().FetchMaintainershipFile("test", "prjcopy", "branch").Return(nil, "", repository.NewRepoGetRawFileNotFound())
err := pr.Process(event, git, config)
err := pr.Process(event)
if errors.Unwrap(err) != failedErr {
t.Error("error:", err)
}

View File

@@ -1,63 +0,0 @@
package main
import (
)
/*
func TestPRReviewed(t *testing.T) {
testData := []struct {
title string
error error
}{
{
title: "forward project review",
},
}
event := &common.PullRequestWebhookEvent{
Action: "reviewed",
Number: 1,
Pull_Request: &common.PullRequest{
Id: 1,
Base: common.Head{
Ref: "branch",
Sha: "testing",
Repo: &common.Repository{
Name: "testRepo",
Default_Branch: "main1",
},
},
Head: common.Head{
Ref: "branch",
Sha: "testing",
Repo: &common.Repository{
Name: "testRepo",
Default_Branch: "main1",
},
},
},
Repository: &common.Repository{
Name: "testRepo",
Owner: &common.Organization{
Username: "test",
},
},
}
for _, test := range testData {
t.Run(test.title, func(t *testing.T) {
ctl := gomock.NewController(t)
mock := mock_common.NewMockGitea(ctl)
s := PullRequestReviewed{
gitea: mock,
}
mock.EXPECT().GetPullRequest("test", "testRepo", int64(1)).Return(nil, nil)
if err := s.Process(event, nil, nil); err != test.error {
t.Error("unexected error:", err, "Expected:", test.error)
}
})
}
}
*/

View File

@@ -1,5 +1,5 @@
package main
/*
import (
"bytes"
"errors"
@@ -16,13 +16,13 @@ import (
)
func TestSyncPR(t *testing.T) {
pr := PullRequestSynced{}
config := &common.AutogitConfig{
Reviewers: []string{"reviewer1", "reviewer2"},
Branch: "testing",
Organization: "test",
GitProjectName: "prj",
pr := PRProcessor{
config: &common.AutogitConfig{
Reviewers: []string{"reviewer1", "reviewer2"},
Branch: "testing",
Organization: "test",
GitProjectName: "prj",
},
}
event := &common.PullRequestWebhookEvent{
@@ -36,7 +36,7 @@ func TestSyncPR(t *testing.T) {
Repo: &common.Repository{
Name: "testRepo",
Owner: &common.Organization{
Username: config.Organization,
Username: pr.config.Organization,
},
Default_Branch: "main1",
},
@@ -52,7 +52,7 @@ func TestSyncPR(t *testing.T) {
},
Repository: &common.Repository{
Owner: &common.Organization{
Username: config.Organization,
Username: pr.config.Organization,
},
},
}
@@ -104,14 +104,14 @@ func TestSyncPR(t *testing.T) {
t.Run("PR sync request against PrjGit == no action", func(t *testing.T) {
ctl := gomock.NewController(t)
pr.gitea = mock_common.NewMockGitea(ctl)
Gitea = mock_common.NewMockGitea(ctl)
git.GitPath = t.TempDir()
config.GitProjectName = "testRepo"
pr.config.GitProjectName = "testRepo"
event.Repository.Name = "testRepo"
if err := pr.Process(event, git, config); err != nil {
if err := pr.Process(event); err != nil {
t.Error("Error PrjGit sync request. Should be no error.", err)
}
})
@@ -123,7 +123,7 @@ func TestSyncPR(t *testing.T) {
pr.gitea = mock
git.GitPath = t.TempDir()
config.GitProjectName = "prjGit"
pr.config.GitProjectName = "prjGit"
event.Repository.Name = "testRepo"
setupGitForTests(t, git)
@@ -132,10 +132,10 @@ func TestSyncPR(t *testing.T) {
defer func() { PrjGitPR.Head.Sha = oldSha }()
PrjGitPR.Head.Sha = "ab8adab91edb476b9762097d10c6379aa71efd6b60933a1c0e355ddacf419a95"
mock.EXPECT().GetPullRequest(config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil)
mock.EXPECT().GetPullRequest(config.Organization, "prj", int64(24)).Return(PrjGitPR, nil)
mock.EXPECT().GetPullRequest(pr.config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil)
mock.EXPECT().GetPullRequest(pr.config.Organization, "prj", int64(24)).Return(PrjGitPR, nil)
err := pr.Process(event, git, config)
err := pr.Process(event)
if err == nil || err.Error() != "Cannot fetch submodule commit id in prjgit for 'testRepo'" {
t.Error("Invalid error received.", err)
@@ -149,7 +149,7 @@ func TestSyncPR(t *testing.T) {
pr.gitea = mock
git.GitPath = t.TempDir()
config.GitProjectName = "prjGit"
pr.config.GitProjectName = "prjGit"
event.Repository.Name = "tester"
setupGitForTests(t, git)
@@ -173,19 +173,19 @@ func TestSyncPR(t *testing.T) {
ctl := gomock.NewController(t)
mock := mock_common.NewMockGitea(ctl)
pr.gitea = mock
Gitea = mock
git.GitPath = t.TempDir()
config.GitProjectName = "prjGit"
pr.config.GitProjectName = "prjGit"
event.Repository.Name = "testRepo"
setupGitForTests(t, git)
// mock.EXPECT().GetAssociatedPrjGitPR(event).Return(PrjGitPR, nil)
mock.EXPECT().GetPullRequest(config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil)
mock.EXPECT().GetPullRequest(config.Organization, "prj", int64(24)).Return(PrjGitPR, nil)
mock.EXPECT().GetPullRequest(pr.config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil)
mock.EXPECT().GetPullRequest(pr.config.Organization, "prj", int64(24)).Return(PrjGitPR, nil)
err := pr.Process(event, git, config)
err := pr.Process(event)
if err != nil {
t.Error("Invalid error received.", err)
@@ -199,14 +199,12 @@ func TestSyncPR(t *testing.T) {
t.Error(b.String())
}
/*
* does nothing on next sync of already synced data -- PR is updated
*/
// does nothing on next sync of already synced data -- PR is updated
os.RemoveAll(path.Join(git.GitPath, common.DefaultGitPrj))
mock.EXPECT().GetPullRequest(config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil)
mock.EXPECT().GetPullRequest(config.Organization, "prj", int64(24)).Return(PrjGitPR, nil)
err = pr.Process(event, git, config)
mock.EXPECT().GetPullRequest(pr.config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil)
mock.EXPECT().GetPullRequest(pr.config.Organization, "prj", int64(24)).Return(PrjGitPR, nil)
err = pr.Process(event)
if err != nil {
t.Error("Invalid error received.", err)
@@ -231,3 +229,4 @@ func TestSyncPR(t *testing.T) {
}
})
}
*/

View File

@@ -1,208 +0,0 @@
package main
import (
"bytes"
"fmt"
"log"
"testing"
"go.uber.org/mock/gomock"
"src.opensuse.org/autogits/common"
mock_common "src.opensuse.org/autogits/common/mock"
mock_main "src.opensuse.org/autogits/workflow-pr/mock"
)
func TestPRProcessor(t *testing.T) {
tests := []struct {
title string
action string
req func(req *RequestProcessor, mock PullRequestProcessor)
}{
{
title: "Open routine called for PR opening",
action: "opened",
req: func(req *RequestProcessor, mock PullRequestProcessor) {
req.Opened = mock
},
},
{
title: "Re-Open routine called for PR reopening",
action: "reopened",
req: func(req *RequestProcessor, mock PullRequestProcessor) {
req.Opened = mock
},
},
{
title: "Sync routine called for PR sync requests",
action: "synchronized",
req: func(req *RequestProcessor, mock PullRequestProcessor) {
req.Synced = mock
},
},
{
title: "Close routine called for PR closing",
action: "closed",
req: func(req *RequestProcessor, mock PullRequestProcessor) {
req.Closed = mock
},
},
{
title: "Close routine called for PR closing",
action: "reviewed",
req: func(req *RequestProcessor, mock PullRequestProcessor) {
req.Review = mock
},
},
}
var logBuf bytes.Buffer
oldOut := log.Writer()
log.SetOutput(&logBuf)
defer log.SetOutput(oldOut)
testConfiguration := make(map[string][]*common.AutogitConfig)
testConfiguration["test"] = make([]*common.AutogitConfig, 1, 1)
testConfiguration["test"][0] = &common.AutogitConfig{
Branch: "branch",
}
event := &common.PullRequestWebhookEvent{
// Action: "opened",
Number: 1,
Pull_Request: &common.PullRequest{
Id: 1,
Base: common.Head{
Ref: "branch",
Repo: &common.Repository{
Name: "testRepo",
},
},
Head: common.Head{
Ref: "branch",
Repo: &common.Repository{
Name: "testRepo",
},
},
},
Repository: &common.Repository{
Owner: &common.Organization{
Username: "test",
},
},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
ctl := gomock.NewController(t)
mock := mock_main.NewMockPullRequestProcessor(ctl)
mock.EXPECT().Process(event, gomock.Any(), testConfiguration["test"][0]).Return(nil)
req := &RequestProcessor{
configuredRepos: testConfiguration,
}
test.req(req, mock)
event.Action = test.action
err := req.ProcessFunc(&common.Request{
Data: event,
})
if err != nil {
t.Error("Error processing open PR:", err)
t.Error(logBuf.String())
}
})
}
req := &RequestProcessor{
configuredRepos: testConfiguration,
}
t.Run("Edit PR handling", func(t *testing.T) {
/* ctl := gomock.NewController(t)
closedMock := mock_main.NewMockPullRequestProcessor(ctl)
closedMock.EXPECT().Process(event, gomock.Any(), testConfiguration["test"][0]).Return(nil)
*/
// req.Closed = closedMock
event.Action = "edited"
err := req.ProcessFunc(&common.Request{
Data: event,
})
if err != nil {
t.Error("Error processing edit PR:", err)
t.Error(logBuf.String())
}
})
t.Run("Unknown PR-type handling", func(t *testing.T) {
event.Action = "not existing action"
err := req.ProcessFunc(&common.Request{
Data: event,
})
if err == nil {
t.Error(logBuf.String())
}
})
t.Run("Missing branch in config present in PR", func(t *testing.T) {
baseRef := event.Pull_Request.Base.Ref
event.Pull_Request.Base.Ref = "not present"
err := req.ProcessFunc(&common.Request{
Data: event,
})
event.Pull_Request.Base.Ref = baseRef
if err == nil {
t.Error(logBuf.String())
}
})
t.Run("Invalid data present in PR", func(t *testing.T) {
baseConfig := req.configuredRepos
req.configuredRepos = make(map[string][]*common.AutogitConfig)
err := req.ProcessFunc(&common.Request{
Data: nil,
})
req.configuredRepos = baseConfig
if err == nil {
t.Error(logBuf.String())
}
})
t.Run("Ignoring requests against unconfigured repos", func(t *testing.T) {
baseConfig := req.configuredRepos
req.configuredRepos = make(map[string][]*common.AutogitConfig)
err := req.ProcessFunc(&common.Request{
Data: event,
})
req.configuredRepos = baseConfig
if err != nil {
t.Error(logBuf.String())
}
})
t.Run("Failures of git handler creation", func(t *testing.T) {
ctl := gomock.NewController(t)
gitHandler := mock_common.NewMockGitHandlerGenerator(ctl)
gitHandler.EXPECT().CreateGitHandler(gomock.Any()).Return(nil, fmt.Errorf("some error"))
err := req.ProcessFunc(&common.Request{
Data: event,
})
if err == nil {
t.Error(logBuf.String())
}
})
}

View File

@@ -11,23 +11,16 @@ import (
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
"src.opensuse.org/autogits/workflow-pr/interfaces"
)
//go:generate mockgen -source=repo_check.go -destination=mock/repo_check.go -typed
type StateChecker interface {
VerifyProjectState(configs *common.AutogitConfig) error
CheckRepos() error
ConsistencyCheckProcess() error
}
type DefaultStateChecker struct {
exitCheckLoop bool
checkOnStart bool
checkInterval time.Duration
processor *RequestProcessor
i StateChecker
i interfaces.StateChecker
}
func CreateDefaultStateChecker(checkOnStart bool, processor *RequestProcessor, gitea common.Gitea, interval time.Duration) *DefaultStateChecker {
@@ -40,11 +33,20 @@ func CreateDefaultStateChecker(checkOnStart bool, processor *RequestProcessor, g
return s
}
func (s *DefaultStateChecker) ProcessPR(git common.Git, pr *models.PullRequest, config *common.AutogitConfig) error {
func pullRequestToEventState(state models.StateType) string {
switch state {
case "open":
return "opened"
}
return string(state)
}
func (s *DefaultStateChecker) ProcessPR(pr *models.PullRequest, config *common.AutogitConfig) error {
var event common.PullRequestWebhookEvent
event.Pull_Request = common.PullRequestFromModel(pr)
event.Action = string(pr.State) + "ed"
event.Action = pullRequestToEventState(pr.State)
event.Number = pr.Index
event.Repository = common.RepositoryFromModel(pr.Base.Repo)
event.Sender = *common.UserFromModel(pr.User)
@@ -53,7 +55,7 @@ func (s *DefaultStateChecker) ProcessPR(git common.Git, pr *models.PullRequest,
return ProcesPullRequest(&event, common.AutogitConfigs{config})
}
func (s *DefaultStateChecker) VerifyProjectState(config *common.AutogitConfig) error {
func (s *DefaultStateChecker) VerifyProjectState(config *common.AutogitConfig) ([]*interfaces.PRToProcess, error) {
defer func() {
if r := recover(); r != nil {
common.LogError("panic caught")
@@ -64,33 +66,31 @@ func (s *DefaultStateChecker) VerifyProjectState(config *common.AutogitConfig) e
}
}()
prsToProcess := []*interfaces.PRToProcess{}
prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit()
common.LogInfo(" checking", prjGitOrg+"/"+prjGitRepo+"#"+prjGitBranch)
git, err := GitHandler.CreateGitHandler(config.Organization)
common.LogDebug("Git Path:", git.GetPath())
if err != nil {
return fmt.Errorf("Cannot create git handler: %w", err)
return nil, fmt.Errorf("Cannot create git handler: %w", err)
}
defer git.Close()
repo, err := Gitea.CreateRepositoryIfNotExist(git, prjGitOrg, prjGitRepo)
if err != nil {
return fmt.Errorf("Error fetching or creating '%s/%s#%s' -- aborting verifyProjectState(). Err: %w", prjGitBranch, prjGitRepo, prjGitBranch, err)
return nil, fmt.Errorf("Error fetching or creating '%s/%s#%s' -- aborting verifyProjectState(). Err: %w", prjGitBranch, prjGitRepo, prjGitBranch, err)
}
_, err = git.GitClone(prjGitRepo, prjGitBranch, repo.SSHURL)
common.PanicOnError(err)
prs, err := Gitea.GetRecentPullRequests(prjGitOrg, prjGitRepo, prjGitBranch)
if err != nil {
return fmt.Errorf("Error fetching PrjGit Prs for %s/%s#%s: %w", prjGitOrg, prjGitRepo, prjGitBranch, err)
}
for _, pr := range prs {
s.ProcessPR(git, pr, config)
}
common.LogDebug(" - # of PRs to check in PrjGit:", len(prs))
prsToProcess = append(prsToProcess, &interfaces.PRToProcess{
Org: prjGitOrg,
Repo: prjGitRepo,
Branch: prjGitBranch,
})
submodules, err := git.GitSubmoduleList(prjGitRepo, "HEAD")
nextSubmodule:
@@ -112,20 +112,16 @@ nextSubmodule:
branch = repo.DefaultBranch
}
prs, err := Gitea.GetRecentPullRequests(config.Organization, submoduleName, branch)
if err != nil {
return fmt.Errorf("Error fetching pull requests for %s/%s#%s. Err: %w", config.Organization, submoduleName, branch, err)
}
common.LogDebug(" - # of PRs to check:", len(prs))
for _, pr := range prs {
s.ProcessPR(git, pr, config)
}
prsToProcess = append(prsToProcess, &interfaces.PRToProcess{
Org: config.Organization,
Repo: submoduleName,
Branch: branch,
})
// check if the commited changes are syned with branches
commits, err := Gitea.GetRecentCommits(config.Organization, submoduleName, branch, 10)
if err != nil {
return fmt.Errorf("Error fetching recent commits for %s/%s. Err: %w", config.Organization, submoduleName, err)
return nil, fmt.Errorf("Error fetching recent commits for %s/%s. Err: %w", config.Organization, submoduleName, err)
}
for idx, commit := range commits {
@@ -149,7 +145,7 @@ nextSubmodule:
url := git.GitExecWithOutputOrPanic(subDir, "remote", "get-url", "origin", "--push")
sshUrl, err := common.TranslateHttpsToSshUrl(strings.TrimSpace(url))
if err != nil {
return fmt.Errorf("Cannot traslate HTTPS git URL to SSH_URL. %w", err)
return prsToProcess, fmt.Errorf("Cannot traslate HTTPS git URL to SSH_URL. %w", err)
}
git.GitExecOrPanic(subDir, "remote", "set-url", "origin", "--push", sshUrl)
git.GitExecOrPanic(subDir, "push", "origin", branch)
@@ -157,7 +153,7 @@ nextSubmodule:
}
// forward any package-gits referred by the project git, but don't go back
return nil
return prsToProcess, nil
}
func (s *DefaultStateChecker) CheckRepos() error {
@@ -172,10 +168,25 @@ func (s *DefaultStateChecker) CheckRepos() error {
}
common.LogInfo(" ++ starting verification, org:", org, "config:", config.GitProjectName)
if err := s.i.VerifyProjectState(config); err != nil {
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)
}
if len(prs) > 0 {
common.LogDebug(fmt.Sprintf("%s/%s#%s", pr.Org, pr.Repo, pr.Branch), " - # of PRs to check:", len(prs))
}
for _, pr := range prs {
s.ProcessPR(pr, config)
}
}
common.LogInfo(" ++ verification complete, org:", org, "config:", config.GitProjectName)
}
}

View File

@@ -106,7 +106,6 @@ func TestRepoCheck(t *testing.T) {
ctl := gomock.NewController(t)
state := mock_main.NewMockStateChecker(ctl)
gitea := mock_common.NewMockGitea(ctl)
git := mock_common.NewMockGitHandlerGenerator(ctl)
config1 := &common.AutogitConfig{
GitProjectName: "git_repo1",
@@ -124,7 +123,7 @@ func TestRepoCheck(t *testing.T) {
c.i = state
err := errors.New("test error")
state.EXPECT().VerifyProjectState(configs.configuredRepos["repo1_org"][0]).Return(err)
state.EXPECT().VerifyProjectState(configs.configuredRepos["repo1_org"][0]).Return(nil, err)
r := c.CheckRepos()
@@ -157,7 +156,6 @@ func TestVerifyProjectState(t *testing.T) {
gitea := mock_common.NewMockGitea(ctl)
git := &common.GitHandlerImpl{
DebugLogger: true,
GitCommiter: "TestCommiter",
GitEmail: "test@testing",
GitPath: t.TempDir(),
@@ -186,11 +184,12 @@ func TestVerifyProjectState(t *testing.T) {
gitea.EXPECT().GetRecentCommits(org, "testRepo", "testing", gomock.Any())
c := CreateDefaultStateChecker(false, configs, gitea, 0)
/*
c.git = &testGit{
git: git,
}
}*/
err := c.VerifyProjectState(configs.configuredRepos[org][0])
_, err := c.VerifyProjectState(configs.configuredRepos[org][0])
if err != nil {
t.Error(err)
@@ -203,7 +202,6 @@ func TestVerifyProjectState(t *testing.T) {
process := mock_main.NewMockPullRequestProcessor(ctl)
git := &common.GitHandlerImpl{
DebugLogger: true,
GitCommiter: "TestCommiter",
GitEmail: "test@testing",
GitPath: t.TempDir(),
@@ -266,13 +264,14 @@ func TestVerifyProjectState(t *testing.T) {
gitea.EXPECT().GetRecentCommits(org, "testRepo", "testing", gomock.Any())
c := CreateDefaultStateChecker(false, configs, gitea, 0)
/*
c.git = &testGit{
git: git,
}
}*/
process.EXPECT().Process(gomock.Any(), gomock.Any(), gomock.Any())
c.processor.Opened = process
// c.processor.Opened = process
err := c.VerifyProjectState(configs.configuredRepos[org][0])
_, err := c.VerifyProjectState(configs.configuredRepos[org][0])
if err != nil {
t.Error(err)