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 Reviewers []string // only used by `pr` workflow
ReviewGroups []ReviewGroup ReviewGroups []ReviewGroup
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers 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 type AutogitConfigs []*AutogitConfig

View File

@@ -10,6 +10,62 @@ import (
mock_common "src.opensuse.org/autogits/common/mock" 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) { func TestConfigWorkflowParser(t *testing.T) {
tests := []struct { tests := []struct {
name string 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().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.config_json), "abc", nil)
gitea.EXPECT().GetRepository("foo", "bar").Return(&test.repo, 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 { if err != nil {
t.Fatal(err) 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"}, res: [3]string{"oorg", "foo.bar", "point"},
}, },
{ {
name: "whitespace shouldn't matter", name: "whitespace shouldn't matter",
prjgit: " oorg / \nfoo.bar\t # point ", prjgit: " oorg / \nfoo.bar\t # point ",
res: [3]string{"oorg", "foo.bar", "point"}, res: [3]string{"oorg", "foo.bar", "point"},
}, },

View File

@@ -44,6 +44,10 @@ type GitStatusLister interface {
GitStatus(cwd string) ([]GitStatusData, error) GitStatus(cwd string) ([]GitStatusData, error)
} }
type GitDiffLister interface {
GitDiff(cwd, base, head string) (string, error)
}
type Git interface { type Git interface {
// error if git, but wrong remote // 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 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) GitExecOrPanic(cwd string, params ...string)
GitExec(cwd string, params ...string) error GitExec(cwd string, params ...string) error
GitExecWithOutput(cwd string, params ...string) (string, error) GitExecWithOutput(cwd string, params ...string) (string, error)
GitDiffLister
} }
type GitHandlerImpl struct { type GitHandlerImpl struct {
@@ -133,6 +139,7 @@ func (s *gitHandlerGeneratorImpl) CreateGitHandler(org string) (Git, error) {
} }
func (s *gitHandlerGeneratorImpl) ReadExistingPath(org string) (Git, error) { func (s *gitHandlerGeneratorImpl) ReadExistingPath(org string) (Git, error) {
LogDebug("Locking git org:", org)
s.lock_lock.Lock() s.lock_lock.Lock()
defer s.lock_lock.Unlock() defer s.lock_lock.Unlock()
@@ -154,6 +161,7 @@ func (s *gitHandlerGeneratorImpl) ReadExistingPath(org string) (Git, error) {
func (s *gitHandlerGeneratorImpl) ReleaseLock(org string) { func (s *gitHandlerGeneratorImpl) ReleaseLock(org string) {
m, ok := s.lock[org] m, ok := s.lock[org]
if ok { if ok {
LogDebug("Unlocking git org:", org)
m.Unlock() 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, "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")) 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) 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" { if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
args = slices.Insert(args, 1, "--unshallow") args = slices.Insert(args, 1, "--unshallow")
} }
@@ -284,6 +292,7 @@ func (e *GitHandlerImpl) GitRemoteHead(gitDir, remote, branchName string) (strin
} }
func (e *GitHandlerImpl) Close() error { func (e *GitHandlerImpl) Close() error {
LogDebug("Unlocking git lock")
e.lock.Unlock() e.lock.Unlock()
return nil return nil
} }
@@ -760,6 +769,8 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
done.Lock() done.Lock()
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)} data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
LogDebug("Getting submodules for:", commitId)
go func() { go func() {
defer done.Unlock() defer done.Unlock()
defer close(data_out.ch) defer close(data_out.ch)
@@ -837,7 +848,7 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
go func() { go func() {
defer func() { defer func() {
if recover() != nil { if recover() != nil {
subCommitId = "wrong" subCommitId = ""
commitId = "ok" commitId = "ok"
valid = false valid = false
} }
@@ -892,7 +903,7 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
} }
wg.Wait() wg.Wait()
return subCommitId, len(subCommitId) == len(commitId) return subCommitId, len(subCommitId) > 0
} }
const ( const (
@@ -907,6 +918,16 @@ type GitStatusData struct {
Path string Path string
Status int Status int
States [3]string 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) { 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) { 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) str := make([]byte, 0, 100)
for { for {
c, err := data.ReadByte() c, err := data.ReadByte()
@@ -969,7 +1004,7 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err return nil, err
} }
ret.Status = GitStatus_Modified ret.Status = GitStatus_Modified
ret.Path, err = parseGitStatusString(data) ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -979,11 +1014,11 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err return nil, err
} }
ret.Status = GitStatus_Renamed ret.Status = GitStatus_Renamed
ret.Path, err = parseGitStatusString(data) ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.States[0], err = parseGitStatusString(data) ret.States[0], err = parseGitStatusStringWithSpace(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -993,7 +1028,7 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err return nil, err
} }
ret.Status = GitStatus_Untracked ret.Status = GitStatus_Untracked
ret.Path, err = parseGitStatusString(data) ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1003,15 +1038,22 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err return nil, err
} }
ret.Status = GitStatus_Ignored ret.Status = GitStatus_Ignored
ret.Path, err = parseGitStatusString(data) ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
case 'u': case 'u':
var err error var err error
if err = skipGitStatusEntry(data, 7); err != nil { if err = skipGitStatusEntry(data, 2); err != nil {
return nil, err 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 { if ret.States[0], err = parseGitStatusHexString(data); err != nil {
return nil, err return nil, err
} }
@@ -1022,7 +1064,7 @@ func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
return nil, err return nil, err
} }
ret.Status = GitStatus_Unmerged ret.Status = GitStatus_Unmerged
ret.Path, err = parseGitStatusString(data) ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1069,3 +1111,26 @@ func (e *GitHandlerImpl) GitStatus(cwd string) (ret []GitStatusData, err error)
return parseGitStatusData(bufio.NewReader(bytes.NewReader(out))) 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", Path: ".gitmodules",
Status: GitStatus_Unmerged, Status: GitStatus_Unmerged,
States: [3]string{"587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76", "d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c", "087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33"}, States: [3]string{"587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76", "d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c", "087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33"},
SubmoduleChanges: "N...",
}, },
}, },
}, },

View File

@@ -29,7 +29,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"slices" "slices"
"strings"
"time" "time"
transport "github.com/go-openapi/runtime/client" transport "github.com/go-openapi/runtime/client"
@@ -86,7 +85,15 @@ type GiteaMaintainershipReader interface {
type GiteaPRFetcher interface { type GiteaPRFetcher interface {
GetPullRequest(org, project string, num int64) (*models.PullRequest, error) 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 { type GiteaCommitFetcher interface {
@@ -101,14 +108,19 @@ type GiteaCommentFetcher interface {
GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error) GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error)
} }
type GiteaPRChecker interface { type GiteaReviewTimelineFetcher interface {
GiteaReviewFetcher GiteaReviewFetcher
GiteaTimelineFetcher
}
type GiteaPRChecker interface {
GiteaReviewTimelineFetcher
GiteaCommentFetcher GiteaCommentFetcher
GiteaMaintainershipReader GiteaMaintainershipReader
} }
type GiteaReviewFetcherAndRequester interface { type GiteaReviewFetcherAndRequester interface {
GiteaReviewFetcher GiteaReviewTimelineFetcher
GiteaCommentFetcher GiteaCommentFetcher
GiteaReviewRequester GiteaReviewRequester
} }
@@ -155,6 +167,7 @@ type Gitea interface {
GiteaReviewUnrequester GiteaReviewUnrequester
GiteaReviewer GiteaReviewer
GiteaPRFetcher GiteaPRFetcher
GiteaPRUpdater
GiteaCommitFetcher GiteaCommitFetcher
GiteaReviewFetcher GiteaReviewFetcher
GiteaCommentFetcher GiteaCommentFetcher
@@ -164,18 +177,19 @@ type Gitea interface {
GiteaCommitStatusGetter GiteaCommitStatusGetter
GiteaCommitStatusSetter GiteaCommitStatusSetter
GiteaSetRepoOptions GiteaSetRepoOptions
GiteaTimelineFetcher
GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
GetDonePullNotifications(page int64) ([]*models.NotificationThread, error) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
SetNotificationRead(notificationId int64) error SetNotificationRead(notificationId int64) error
GetOrganization(orgName string) (*models.Organization, error) GetOrganization(orgName string) (*models.Organization, error)
GetOrganizationRepositories(orgName string) ([]*models.Repository, error) GetOrganizationRepositories(orgName string) ([]*models.Repository, error)
CreateRepositoryIfNotExist(git Git, org, repoName string) (*models.Repository, error) CreateRepositoryIfNotExist(git Git, org, repoName string) (*models.Repository, error)
CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error)
GetAssociatedPrjGitPR(prjGitOrg, prjGitRepo, refOrg, refRepo string, Index int64) (*models.PullRequest, error)
GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, string, error) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, string, error)
GetRecentPullRequests(org, repo, branch string) ([]*models.PullRequest, error) GetRecentPullRequests(org, repo, branch string) ([]*models.PullRequest, error)
GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error) GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)
GetPullRequests(org, project string) ([]*models.PullRequest, error)
GetCurrentUser() (*models.User, error) GetCurrentUser() (*models.User, error)
} }
@@ -222,6 +236,52 @@ func (gitea *GiteaTransport) GetPullRequest(org, project string, num int64) (*mo
return pr.Payload, err 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) { func (gitea *GiteaTransport) GetCommitStatus(org, repo, hash string) ([]*models.CommitStatus, error) {
page := int64(1) page := int64(1)
limit := int64(10) limit := int64(10)
@@ -367,29 +427,42 @@ func (gitea *GiteaTransport) SetRepoOptions(owner, repo string, manual_merge boo
return ok.Payload, err return ok.Payload, err
} }
func (gitea *GiteaTransport) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) { const (
bigLimit := int64(100000) GiteaNotificationType_Pull = "Pull"
)
params := notification.NewNotifyGetListParams(). func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) {
WithDefaults(). bigLimit := int64(20)
WithSubjectType([]string{"Pull"}). ret := make([]*models.NotificationThread, 0, 100)
WithStatusTypes([]string{"unread"}).
WithLimit(&bigLimit)
if since != nil { for page := int64(1); ; page++ {
s := strfmt.DateTime(*since) params := notification.NewNotifyGetListParams().
params.SetSince(&s) 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) return ret, nil
if err != nil {
return nil, err
}
return list.Payload, nil
} }
func (gitea *GiteaTransport) GetDonePullNotifications(page int64) ([]*models.NotificationThread, error) { func (gitea *GiteaTransport) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error) {
limit := int64(20) limit := int64(20)
t := true t := true
@@ -399,7 +472,7 @@ func (gitea *GiteaTransport) GetDonePullNotifications(page int64) ([]*models.Not
list, err := gitea.client.Notification.NotifyGetList( list, err := gitea.client.Notification.NotifyGetList(
notification.NewNotifyGetListParams(). notification.NewNotifyGetListParams().
WithAll(&t). WithAll(&t).
WithSubjectType([]string{"Pull"}). WithSubjectType([]string{Type}).
WithStatusTypes([]string{"read"}). WithStatusTypes([]string{"read"}).
WithLimit(&limit). WithLimit(&limit).
WithPage(&page), 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) { func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
prOptions := models.CreatePullRequestOption{ prOptions := models.CreatePullRequestOption{
Base: repo.DefaultBranch, Base: targetId,
Head: srcId, Head: srcId,
Title: title, Title: title,
Body: body, Body: body,
} }
if pr, err := gitea.client.Repository.RepoGetPullRequestByBaseHead( 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, gitea.transport.DefaultAuthentication,
); err == nil { ); err == nil {
return pr.Payload, nil return pr.Payload, nil
@@ -560,48 +633,6 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
return pr.GetPayload(), nil 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) { func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewers ...string) ([]*models.PullReview, error) {
reviewOptions := models.PullReviewRequestOptions{ reviewOptions := models.PullReviewRequestOptions{
Reviewers: reviewers, 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) { func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
limit := int64(20)
page := int64(1) page := int64(1)
resCount := limit resCount := 1
retData := []*models.TimelineComment{} retData := []*models.TimelineComment{}
for resCount == limit { for resCount > 0 {
res, err := gitea.client.Issue.IssueGetCommentsAndTimeline( res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(
issue.NewIssueGetCommentsAndTimelineParams(). issue.NewIssueGetCommentsAndTimelineParams().
WithOwner(org). WithOwner(org).
WithRepo(repo). WithRepo(repo).
WithIndex(idx). WithIndex(idx).
WithPage(&page). WithPage(&page),
WithLimit(&limit),
gitea.transport.DefaultAuthentication, gitea.transport.DefaultAuthentication,
) )
@@ -708,14 +737,16 @@ func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models
return nil, err return nil, err
} }
resCount = int64(len(res.Payload)) resCount = len(res.Payload)
LogDebug("page:", page, "len:", resCount)
page++ page++
retData = append(retData, res.Payload...) retData = append(retData, res.Payload...)
} }
LogDebug("total results:", len(retData))
slices.SortFunc(retData, func(a, b *models.TimelineComment) int { 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 return retData, nil

View File

@@ -63,7 +63,7 @@ func FetchProjectMaintainershipData(gitea GiteaMaintainershipReader, org, prjGit
if m != nil { if m != nil {
m.IsDir = dir m.IsDir = dir
m.FetchPackage = func(pkg string) ([]byte, error) { 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 return data, err
} }
} }
@@ -127,18 +127,11 @@ prjMaintainer:
} }
func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullReview, submitter string) bool { func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullReview, submitter string) bool {
reviewers, found := data.Data[pkg] var reviewers []string
if !found { if pkg != ProjectKey {
if pkg != ProjectKey && data.IsDir { reviewers = data.ListPackageMaintainers(pkg)
r, err := data.FetchPackage(pkg) } else {
if err != nil { reviewers = data.ListProjectMaintainers()
return false
}
reviewers = parsePkgDirData(pkg, r)
data.Data[pkg] = reviewers
} else {
return true
}
} }
if len(reviewers) == 0 { if len(reviewers) == 0 {
@@ -146,11 +139,12 @@ func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullRevi
} }
LogDebug("Looking for review by:", reviewers) LogDebug("Looking for review by:", reviewers)
if slices.Contains(reviewers, submitter) {
LogDebug("Submitter is maintainer. Approving.")
return true
}
for _, review := range reviews { 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) { if !review.Stale && review.State == ReviewStateApproved && slices.Contains(reviewers, review.User.UserName) {
LogDebug("Reviewed by", review.User.UserName) LogDebug("Reviewed by", review.User.UserName)
return true return true
@@ -190,7 +184,7 @@ func (data *MaintainershipMap) WriteMaintainershipFile(writer io.StringWriter) e
keys = slices.Delete(keys, i, len(keys)) keys = slices.Delete(keys, i, len(keys))
} }
slices.Sort(keys) slices.Sort(keys)
for i, pkg := range(keys) { for i, pkg := range keys {
eol := "," eol := ","
if i == len(keys)-1 { if i == len(keys)-1 {
eol = "" 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"` 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) { func parseProjectMeta(data []byte) (*ProjectMeta, error) {
var meta ProjectMeta var meta ProjectMeta
err := xml.Unmarshal(data, &meta) err := xml.Unmarshal(data, &meta)
@@ -166,8 +194,83 @@ func parseProjectMeta(data []byte) (*ProjectMeta, error) {
return &meta, nil 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -197,7 +300,7 @@ func (c *ObsClient) GetGroupMeta(gid string) (*GroupMeta, error) {
} }
func (c *ObsClient) GetUserMeta(uid string) (*UserMeta, 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 { if err != nil {
return nil, err return nil, err
} }
@@ -226,7 +329,11 @@ func (c *ObsClient) GetUserMeta(uid string) (*UserMeta, error) {
return &meta, nil 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) req, err := http.NewRequest(method, url, body)
if err != nil { 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) { 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) res, err := c.ObsRequest("GET", req, nil)
if err != 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) { 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 { if err != nil {
return nil, err return nil, err
@@ -422,7 +529,7 @@ func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error {
return err 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 { if err != nil {
return err return err
} }
@@ -443,7 +550,7 @@ func (c *ObsClient) DeleteProject(project string) error {
query := url.Query() query := url.Query()
query.Add("force", "1") query.Add("force", "1")
url.RawQuery = query.Encode() url.RawQuery = query.Encode()
res, err := c.ObsRequest("DELETE", url.String(), nil) res, err := c.ObsRequestRaw("DELETE", url.String(), nil)
if err != nil { if err != nil {
return err return err
@@ -455,25 +562,44 @@ func (c *ObsClient) DeleteProject(project string) error {
} }
return nil 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 { type PackageBuildStatus struct {
Package string `xml:"package,attr"` Package string `xml:"package,attr"`
Code string `xml:"code,attr"` Code string `xml:"code,attr"`
Details string `xml:"details"` Details string `xml:"details"`
LastUpdate int64
} }
type BuildResult struct { type BuildResult struct {
Project string `xml:"project,attr"` Project string `xml:"project,attr"`
Repository string `xml:"repository,attr"` Repository string `xml:"repository,attr"`
Arch string `xml:"arch,attr"` Arch string `xml:"arch,attr"`
Code string `xml:"code,attr"`
Dirty bool `xml:"dirty,attr"` Code string `xml:"code,attr"`
ScmSync string `xml:"scmsync"` Dirty bool `xml:"dirty,attr"`
ScmInfo string `xml:"scminfo"` ScmSync string `xml:"scmsync"`
Status []PackageBuildStatus `xml:"status"` ScmInfo string `xml:"scminfo"`
Binaries []BinaryList `xml:"binarylist"` Status []PackageBuildStatus `xml:"status"`
Binaries []BinaryList `xml:"binarylist"`
LastUpdate int64
} }
type Binary struct { type Binary struct {
@@ -493,6 +619,7 @@ type BuildResultList struct {
Result []BuildResult `xml:"result"` Result []BuildResult `xml:"result"`
isLastBuild bool isLastBuild bool
LastUpdate int64
} }
func (r *BuildResultList) GetPackageList() []string { func (r *BuildResultList) GetPackageList() []string {
@@ -515,6 +642,48 @@ func (r *BuildResultList) GetPackageList() []string {
return pkgList 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) { func (r *BuildResultList) BuildResultSummary() (success, finished bool) {
if r == nil { if r == nil {
return false, false return false, false
@@ -718,9 +887,7 @@ func (obs ObsProjectNotFound) Error() string {
} }
func (c *ObsClient) ProjectConfig(project string) (string, error) { func (c *ObsClient) ProjectConfig(project string) (string, error) {
u := c.baseUrl.JoinPath("source", project, "_config") res, err := c.ObsRequest("GET", []string{"source", project, "_config"}, nil)
res, err := c.ObsRequest("GET", u.String(), nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -761,7 +928,7 @@ func (c *ObsClient) BuildStatusWithState(project string, opts *BuildResultOption
} }
} }
u.RawQuery = query.Encode() u.RawQuery = query.Encode()
res, err := c.ObsRequest("GET", u.String(), nil) res, err := c.ObsRequestRaw("GET", u.String(), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -784,5 +951,11 @@ func (c *ObsClient) BuildStatusWithState(project string, opts *BuildResultOption
if ret != nil { if ret != nil {
ret.isLastBuild = opts.LastBuild ret.isLastBuild = opts.LastBuild
} }
slices.SortFunc(ret.Result, repoSort)
for _, r := range ret.Result {
slices.SortFunc(r.Status, packageSort)
}
return ret, err 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 = ` const metaPrjData = `
<project name="home:adamm"> <project name="home:adamm">
<title>Adam's Home Projects</title> <title>Adam's Home Projects</title>

View File

@@ -25,6 +25,13 @@ type PRSet struct {
BotUser string 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) { func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRInfo, config *AutogitConfig) ([]*PRInfo, error) {
for _, p := range currentSet { 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 { 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 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 pr *models.PullRequest
var err error var err error
@@ -63,7 +105,7 @@ func FetchPRSet(user string, gitea GiteaPRFetcher, org, repo string, num int64,
return nil, err return nil, err
} }
} else { } 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 return nil, err
} }
@@ -80,26 +122,53 @@ func FetchPRSet(user string, gitea GiteaPRFetcher, org, repo string, num int64,
} }
return &PRSet{ return &PRSet{
PRs: prs, PRs: prs,
Config: config, Config: config,
BotUser: user, BotUser: user,
}, nil }, nil
} }
func (rs *PRSet) IsPrjGitPR(pr *models.PullRequest) bool { func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) {
org, repo, _ := rs.Config.GetPrjGit() for _, p := range rs.PRs {
return pr.Base.Repo.Name == repo && pr.Base.Repo.Owner.UserName == org 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) { func (rs *PRSet) AddPR(pr *models.PullRequest) *PRInfo {
var ret *models.PullRequest 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 { for _, prinfo := range rs.PRs {
if rs.IsPrjGitPR(prinfo.PR) { if rs.IsPrjGitPR(prinfo.PR) {
if ret == nil { if ret == nil {
ret = prinfo.PR ret = prinfo
} else { } 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 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 { func (rs *PRSet) IsConsistent() bool {
prjpr, err := rs.GetPrjGitPR() prjpr_info, err := rs.GetPrjGitPR()
if err != nil { if err != nil {
return false return false
} }
prjpr := prjpr_info.PR
_, prjpr_set := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(prjpr.Body))) _, prjpr_set := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(prjpr.Body)))
if len(prjpr_set) != len(rs.PRs)-1 { // 1 to many mapping 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 return false
} }
next_rs: next_rs:
for _, prinfo := range rs.PRs { for _, prinfo := range rs.PRs {
if prinfo.PR.State != "open" {
return false
}
if prjpr == prinfo.PR { if prjpr == prinfo.PR {
continue continue
} }
@@ -142,14 +227,16 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
for _, pr := range rs.PRs { for _, pr := range rs.PRs {
reviewers := []string{} reviewers := []string{}
if rs.IsPrjGitPR(pr.PR) { 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 { if len(rs.PRs) == 1 {
reviewers = slices.Concat(reviewers, maintainers.ListProjectMaintainers()) reviewers = slices.Concat(reviewers, maintainers.ListProjectMaintainers())
} }
} else { } else {
pkg := pr.PR.Base.Repo.Name 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) slices.Sort(reviewers)
@@ -160,9 +247,13 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
reviewers = slices.Delete(reviewers, idx, idx+1) reviewers = slices.Delete(reviewers, idx, idx+1)
} }
LogDebug("PR: ", pr.PR.Base.Repo.Name, pr.PR.Index)
LogDebug("reviewers for PR:", reviewers)
// remove reviewers that were already requested and are not stale // 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) reviews, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
if err != nil { if err != nil {
LogError("Error fetching reviews:", err)
return err return err
} }
@@ -170,6 +261,7 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
user := reviewers[idx] user := reviewers[idx]
if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) { if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) {
reviewers = slices.Delete(reviewers, idx, idx+1) reviewers = slices.Delete(reviewers, idx, idx+1)
LogDebug("removing reviewer:", user)
} else { } else {
idx++ idx++
} }
@@ -177,8 +269,13 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
// get maintainers associated with the PR too // get maintainers associated with the PR too
if len(reviewers) > 0 { if len(reviewers) > 0 {
if _, err := gitea.RequestReviews(pr.PR, reviewers...); err != nil { LogDebug("Requesting reviews from:", reviewers)
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) 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 { func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData) bool {
configReviewers := ParseReviewers(rs.Config.Reviewers) 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 { for _, pr := range rs.PRs {
var reviewers []string var reviewers []string
var pkg string var pkg string
@@ -210,14 +354,15 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
LogError("Cannot fetch gita reaviews for PR:", err) LogError("Cannot fetch gita reaviews for PR:", err)
return false return false
} }
is_reviewed = r.IsApproved()
LogDebug(pr.PR.Base.Repo.Name, is_reviewed) is_manually_reviewed_ok = r.IsApproved()
if !is_reviewed { LogDebug(pr.PR.Base.Repo.Name, is_manually_reviewed_ok)
if !is_manually_reviewed_ok {
return false return false
} }
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review { 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) LogDebug(" not approved?", pkg)
return false return false
} }
@@ -225,14 +370,15 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
LogDebug("PrjGit PR -- bot created, no need for review") 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 { func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
prjgit, err := rs.GetPrjGitPR() prjgit_info, err := rs.GetPrjGitPR()
if err != nil { if err != nil {
return err return err
} }
prjgit := prjgit_info.PR
remote, err := git.GitClone(DefaultGitPrj, rs.Config.Branch, prjgit.Base.Repo.SSHURL) remote, err := git.GitClone(DefaultGitPrj, rs.Config.Branch, prjgit.Base.Repo.SSHURL)
PanicOnError(err) PanicOnError(err)

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"slices"
"strings" "strings"
"testing" "testing"
@@ -14,6 +15,37 @@ import (
"src.opensuse.org/autogits/common/gitea-generated/models" "src.opensuse.org/autogits/common/gitea-generated/models"
mock_common "src.opensuse.org/autogits/common/mock" 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) { func TestPR(t *testing.T) {
baseConfig := common.AutogitConfig{ baseConfig := common.AutogitConfig{
@@ -27,6 +59,7 @@ func TestPR(t *testing.T) {
pr *models.PullRequest pr *models.PullRequest
pr_err error pr_err error
reviews []*models.PullReview reviews []*models.PullReview
timeline []*models.TimelineComment
review_error error review_error error
} }
@@ -40,26 +73,26 @@ func TestPR(t *testing.T) {
consistentSet bool consistentSet bool
prjGitPRIndex int prjGitPRIndex int
reviewSetFetcher func(*mock_common.MockGiteaPRFetcher) (*common.PRSet, error) reviewSetFetcher func(*mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error)
}{ }{
{ {
name: "Error fetching PullRequest", name: "Error fetching PullRequest",
data: []prdata{ 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, prjGitPRIndex: -1,
}, },
{ {
name: "Error fetching PullRequest in PrjGit", name: "Error fetching PullRequest in PrjGit",
data: []prdata{ 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: "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"}}}}}, {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", name: "Error fetching prjgit",
data: []prdata{ 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, resLen: 1,
prjGitPRIndex: -1, prjGitPRIndex: -1,
@@ -67,8 +100,8 @@ func TestPR(t *testing.T) {
{ {
name: "Review set is consistent", name: "Review set is consistent",
data: []prdata{ 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"}},
{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: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
}, },
resLen: 2, resLen: 2,
prjGitPRIndex: 1, prjGitPRIndex: 1,
@@ -78,8 +111,8 @@ func TestPR(t *testing.T) {
{ {
name: "Review set is consistent: 1pkg", name: "Review set is consistent: 1pkg",
data: []prdata{ 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: 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"}}}}}, {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, resLen: 2,
prjGitPRIndex: 1, prjGitPRIndex: 1,
@@ -88,9 +121,9 @@ func TestPR(t *testing.T) {
{ {
name: "Review set is consistent: 2pkg", name: "Review set is consistent: 2pkg",
data: []prdata{ 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: "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"}}}}}, {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"}}}}}, {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, resLen: 3,
prjGitPRIndex: 1, prjGitPRIndex: 1,
@@ -100,7 +133,7 @@ func TestPR(t *testing.T) {
name: "Review set of prjgit PR is consistent", name: "Review set of prjgit PR is consistent",
data: []prdata{ 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{ reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved}, {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: "super2"}, State: common.ReviewStateApproved},
@@ -112,38 +145,385 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: true, reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet(mock, "foo", "barPrj", 42, &baseConfig) return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
}, },
}, },
{ {
name: "Review set is consistent: 2pkg", name: "Review set is consistent: 2pkg",
data: []prdata{ 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: 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"}}}}}, {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"}}}}}, {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"}}}}}, {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, resLen: 3,
prjGitPRIndex: 2, prjGitPRIndex: 2,
consistentSet: true, 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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
pr_mock := mock_common.NewMockGiteaPRFetcher(ctl) pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
review_mock := mock_common.NewMockGiteaPRChecker(ctl) review_mock := mock_common.NewMockGiteaPRChecker(ctl)
// reviewer_mock := mock_common.NewMockGiteaReviewRequester(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.reviewSetFetcher == nil { // if we are fetching the prjgit directly, the these mocks are not called
if test.prjGitPRIndex >= 0 { 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) Return(test.data[test.prjGitPRIndex].pr, test.data[test.prjGitPRIndex].pr_err)
} else if test.prjGitPRIndex < 0 { } else if test.prjGitPRIndex < 0 {
// no prjgit PR // 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) Return(nil, nil)
} }
} }
@@ -155,6 +535,10 @@ func TestPR(t *testing.T) {
test_err = data.pr_err 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() 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 var res *common.PRSet
@@ -163,7 +547,7 @@ func TestPR(t *testing.T) {
if test.reviewSetFetcher != nil { if test.reviewSetFetcher != nil {
res, err = test.reviewSetFetcher(pr_mock) res, err = test.reviewSetFetcher(pr_mock)
} else { } 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 { if err == nil {
@@ -198,7 +582,7 @@ func TestPR(t *testing.T) {
pr_found := false pr_found := false
if test.prjGitPRIndex >= 0 { if test.prjGitPRIndex >= 0 {
for i := range test.data { 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) t.Log("found at index", i)
pr_found = true pr_found = true
} }
@@ -222,6 +606,8 @@ func TestPR(t *testing.T) {
*/ */
maintainers := mock_common.NewMockMaintainershipData(ctl) 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() maintainers.EXPECT().IsApproved(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes()
if isApproved := res.IsApproved(review_mock, maintainers); isApproved != test.reviewed { if isApproved := res.IsApproved(review_mock, maintainers); isApproved != test.reviewed {
@@ -242,8 +628,10 @@ func TestPRAssignReviewers(t *testing.T) {
reviewer string reviewer string
} }
pkgReviews []*models.PullReview pkgReviews []*models.PullReview
prjReviews []*models.PullReview pkgTimeline []*models.TimelineComment
prjReviews []*models.PullReview
prjTimeline []*models.TimelineComment
expectedReviewerCall [2][]string expectedReviewerCall [2][]string
}{ }{
@@ -331,8 +719,8 @@ func TestPRAssignReviewers(t *testing.T) {
}, },
pkgReviews: []*models.PullReview{ pkgReviews: []*models.PullReview{
{ {
State: common.ReviewStateApproved, State: common.ReviewStateApproved,
User: &models.User{UserName: "user2"}, User: &models.User{UserName: "user2"},
}, },
{ {
State: common.ReviewStatePending, State: common.ReviewStatePending,
@@ -353,15 +741,59 @@ func TestPRAssignReviewers(t *testing.T) {
}, },
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer"}}, 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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
pr_mock := mock_common.NewMockGiteaPRFetcher(ctl) pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl) review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
maintainership_mock := mock_common.NewMockMaintainershipData(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{ pr_mock.EXPECT().GetPullRequest("other", "pkgrepo", int64(1)).Return(&models.PullRequest{
Body: "Some description is here", Body: "Some description is here",
User: &models.User{UserName: "submitter"}, User: &models.User{UserName: "submitter"},
@@ -371,7 +803,8 @@ func TestPRAssignReviewers(t *testing.T) {
Index: 1, Index: 1,
}, nil) }, nil)
review_mock.EXPECT().GetPullRequestReviews("other", "pkgrepo", int64(1)).Return(test.pkgReviews, 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), Body: fmt.Sprintf(common.PrPattern, "other", "pkgrepo", 1),
User: &models.User{UserName: "bot1"}, User: &models.User{UserName: "bot1"},
RequestedReviewers: []*models.User{{UserName: "main_reviewer"}}, RequestedReviewers: []*models.User{{UserName: "main_reviewer"}},
@@ -380,11 +813,12 @@ func TestPRAssignReviewers(t *testing.T) {
Index: 42, Index: 42,
}, nil) }, nil)
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(42)).Return(test.prjReviews, 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().ListProjectMaintainers().Return([]string{"prjmaintainer"}).AnyTimes()
maintainership_mock.EXPECT().ListPackageMaintainers("pkgrepo").Return([]string{"pkgmaintainer"}).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 { if len(prs.PRs) != 2 {
t.Fatal("PRs not fetched") t.Fatal("PRs not fetched")
} }
@@ -393,8 +827,9 @@ func TestPRAssignReviewers(t *testing.T) {
if !prs.IsPrjGitPR(pr.PR) { if !prs.IsPrjGitPR(pr.PR) {
r = test.expectedReviewerCall[1] r = test.expectedReviewerCall[1]
} }
if len(r) > 0 { slices.Sort(r)
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) prs.AssignReviewers(review_mock, maintainership_mock)
@@ -428,7 +863,7 @@ func TestPRAssignReviewers(t *testing.T) {
for _, test := range prjgit_tests { for _, test := range prjgit_tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
pr_mock := mock_common.NewMockGiteaPRFetcher(ctl) pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl) review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
maintainership_mock := mock_common.NewMockMaintainershipData(ctl) maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
@@ -441,10 +876,11 @@ func TestPRAssignReviewers(t *testing.T) {
Index: 1, Index: 1,
}, nil) }, nil)
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(1)).Return(test.prjReviews, 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() 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 { if len(prs.PRs) != 1 {
t.Fatal("PRs not fetched") t.Fatal("PRs not fetched")
} }
@@ -453,8 +889,8 @@ func TestPRAssignReviewers(t *testing.T) {
if !prs.IsPrjGitPR(pr.PR) { if !prs.IsPrjGitPR(pr.PR) {
t.Fatal("only prjgit pr here") t.Fatal("only prjgit pr here")
} }
if len(r) > 0 { for _, reviewer := range r {
review_mock.EXPECT().RequestReviews(pr.PR, r).Return(nil, nil) review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
} }
} }
prs.AssignReviewers(review_mock, maintainership_mock) prs.AssignReviewers(review_mock, maintainership_mock)
@@ -516,7 +952,7 @@ func TestPRMerge(t *testing.T) {
mergeError: "Aborting merge", mergeError: "Aborting merge",
}, },
{ {
name: "Merge conflict in modules", name: "Merge conflict in modules, auto-resolved",
pr: &models.PullRequest{ pr: &models.PullRequest{
Base: &models.PRBranchInfo{ Base: &models.PRBranchInfo{
@@ -539,19 +975,23 @@ func TestPRMerge(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(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() testDir := t.TempDir()
t.Log("dir:", testDir) t.Log("dir:", testDir)
mock.EXPECT().GetPullRequest("org", "prj", int64(1)).Return(test.pr, nil) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
gh, _ := common.AllocateGitWorkTree(testDir, "", "") 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))) { if err != nil && (test.mergeError == "" || (len(test.mergeError) > 0 && !strings.Contains(err.Error(), test.mergeError))) {
os.CopyFS("/tmp/upstream", os.DirFS(repoDir)) 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" "crypto/tls"
"fmt" "fmt"
"net/url" "net/url"
"runtime/debug"
"slices"
"strings" "strings"
"time" "time"
rabbitmq "github.com/rabbitmq/amqp091-go" rabbitmq "github.com/rabbitmq/amqp091-go"
) )
const RequestType_CreateBrachTag = "create" type RabbitConnection struct {
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 {
RabbitURL *url.URL // amqps://user:password@host/queue RabbitURL *url.URL // amqps://user:password@host/queue
GitAuthor string queueName string
Handlers map[string]RequestProcessor ch *rabbitmq.Channel
Orgs []string
topics []string topics []string
topicSubChanges chan string // +topic = subscribe, -topic = unsubscribe topicSubChanges chan string // +topic = subscribe, -topic = unsubscribe
} }
type RabbitProcessor interface {
GenerateTopics() []string
Connection() *RabbitConnection
ProcessRabbitMessage(msg RabbitMessage) error
}
type RabbitMessage rabbitmq.Delivery type RabbitMessage rabbitmq.Delivery
func (l *ListenDefinitions) processTopicChanges(ch *rabbitmq.Channel, queueName string) { func (l *RabbitConnection) ProcessTopicChanges() {
for { for {
topic, ok := <-l.topicSubChanges topic, ok := <-l.topicSubChanges
if !ok { if !ok {
@@ -80,11 +57,11 @@ func (l *ListenDefinitions) processTopicChanges(ch *rabbitmq.Channel, queueName
LogDebug(" topic change:", topic) LogDebug(" topic change:", topic)
switch topic[0] { switch topic[0] {
case '+': 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) LogError(err)
} }
case '-': 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) LogError(err)
} }
default: 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 queueName := l.RabbitURL.Path
l.RabbitURL.Path = "" l.RabbitURL.Path = ""
@@ -152,7 +129,7 @@ func (l *ListenDefinitions) processRabbitMQ(msgCh chan<- RabbitMessage) error {
LogDebug(" -- listening to topics:") LogDebug(" -- listening to topics:")
l.topicSubChanges = make(chan string) l.topicSubChanges = make(chan string)
defer close(l.topicSubChanges) defer close(l.topicSubChanges)
go l.processTopicChanges(ch, q.Name) go l.ProcessTopicChanges()
for _, topic := range l.topics { for _, topic := range l.topics {
l.topicSubChanges <- "+" + topic 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() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
LogError(r) LogError(r)
LogError("'crash' RabbitMQ worker. Recovering... reconnecting...") LogError("'crash' RabbitMQ worker. Recovering... reconnecting...")
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
go l.connectAndProcessRabbitMQ(ch) go l.ConnectAndProcessRabbitMQ(ch)
} }
}() }()
for { for {
err := l.processRabbitMQ(ch) err := l.ProcessRabbitMQ(ch)
if err != nil { if err != nil {
LogError("Error in RabbitMQ connection. %#v", err) LogError("Error in RabbitMQ connection. %#v", err)
LogInfo("Reconnecting in 2 seconds...") 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) ch := make(chan RabbitMessage, 100)
go l.connectAndProcessRabbitMQ(ch) go l.ConnectAndProcessRabbitMQ(ch)
return ch return ch
} }
func ProcessEvent(f RequestProcessor, request *Request) { func (l *RabbitConnection) UpdateTopics(processor RabbitProcessor) {
defer func() { newTopics := processor.GenerateTopics()
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()
j := 0 j := 0
next_new_topic: next_new_topic:
@@ -273,14 +221,8 @@ next_new_topic:
l.topics = newTopics l.topics = newTopics
} }
func (l *ListenDefinitions) ProcessRabbitMQEvents() error { func ProcessRabbitMQEvents(processor RabbitProcessor) error {
LogInfo("RabbitMQ connection:", l.RabbitURL.String()) ch := processor.Connection().ConnectToRabbitMQ(processor)
LogDebug("# Handlers:", len(l.Handlers))
LogDebug("# Orgs:", len(l.Orgs))
l.RabbitURL.User = url.UserPassword(rabbitUser, rabbitPassword)
l.topics = l.generateTopics()
ch := l.connectToRabbitMQ()
for { for {
msg, ok := <-ch msg, ok := <-ch
@@ -289,36 +231,8 @@ func (l *ListenDefinitions) ProcessRabbitMQEvents() error {
} }
LogDebug("event:", msg.RoutingKey) LogDebug("event:", msg.RoutingKey)
if err := processor.ProcessRabbitMessage(msg); err != nil {
route := strings.Split(msg.RoutingKey, ".") LogError("Error processing", msg.RoutingKey, err)
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)
}
}
} }
} }
} }

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

View File

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

View File

@@ -12,8 +12,10 @@ func TestReviewers(t *testing.T) {
name string name string
input []string input []string
prj []string prj []string
pkg []string pkg []string
pkg_optional []string
prj_optional []string
}{ }{
{ {
name: "project and package reviewers", name: "project and package reviewers",
@@ -22,6 +24,15 @@ func TestReviewers(t *testing.T) {
prj: []string{"5", "7", common.Bot_BuildReview}, prj: []string{"5", "7", common.Bot_BuildReview},
pkg: []string{"1", "2", "3", "5", "6"}, 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 { for _, test := range tests {
@@ -31,7 +42,13 @@ func TestReviewers(t *testing.T) {
t.Error("unexpected return of ForProject():", reviewers.Prj) t.Error("unexpected return of ForProject():", reviewers.Prj)
} }
if !slices.Equal(reviewers.Pkg, test.pkg) { 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 package common
import ( import (
"regexp"
"slices" "slices"
"strings"
"src.opensuse.org/autogits/common/gitea-generated/models" "src.opensuse.org/autogits/common/gitea-generated/models"
) )
@@ -9,28 +11,110 @@ import (
type PRReviews struct { type PRReviews struct {
reviews []*models.PullReview reviews []*models.PullReview
reviewers []string reviewers []string
comments []*models.TimelineComment
} }
func FetchGiteaReviews(rf GiteaReviewFetcher, reviewers []string, org, repo string, no int64) (*PRReviews, error) { func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, reviewers []string, org, repo string, no int64) (*PRReviews, error) {
reviews, err := rf.GetPullRequestReviews(org, repo, no) timeline, err := rf.GetTimeline(org, repo, no)
if err != nil { if err != nil {
return nil, err 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{ return &PRReviews{
reviews: reviews, reviews: reviews,
reviewers: reviewers, reviewers: reviewers,
comments: comments,
}, nil }, 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 { func (r *PRReviews) IsApproved() bool {
goodReview := true goodReview := true
LogDebug("reviewers:", r.reviewers)
for _, reviewer := range r.reviewers { for _, reviewer := range r.reviewers {
goodReview = false goodReview = false
for _, review := range r.reviews { for _, review := range r.reviews {
if review.User.UserName == reviewer && review.State == ReviewStateApproved && !review.Stale && !review.Dismissed { if review.User.UserName == reviewer && review.State == ReviewStateApproved && !review.Stale && !review.Dismissed {
LogDebug(" -- found review: ", review.User.UserName)
goodReview = true goodReview = true
break break
} }

View File

@@ -14,6 +14,7 @@ func TestReviews(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
reviews []*models.PullReview reviews []*models.PullReview
timeline []*models.TimelineComment
reviewers []string reviewers []string
fetchErr error fetchErr error
isApproved bool isApproved bool
@@ -25,17 +26,17 @@ func TestReviews(t *testing.T) {
isApproved: true, isApproved: true,
}, },
{ {
name: "Single reviewer done", name: "Single reviewer done",
reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}}, reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}},
reviewers: []string{"user1"}, reviewers: []string{"user1"},
isApproved: true, isApproved: true,
isReviewedByTest1: true, isReviewedByTest1: true,
}, },
{ {
name: "Two reviewer, one not approved", name: "Two reviewer, one not approved",
reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}}, reviews: []*models.PullReview{&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user1"}}},
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: false, isApproved: false,
isReviewedByTest1: true, 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: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, Stale: true}, &models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, Stale: true},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: false, isApproved: false,
isReviewedByTest1: true, 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.ReviewStateRequestReview, User: &models.User{UserName: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}}, &models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: false, isApproved: false,
isPendingByTest1: true, isPendingByTest1: true,
}, },
{ {
@@ -63,9 +64,9 @@ func TestReviews(t *testing.T) {
reviews: []*models.PullReview{ reviews: []*models.PullReview{
&models.PullReview{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true}, &models.PullReview{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: false, isApproved: false,
isPendingByTest1: false, isPendingByTest1: false,
isReviewedByTest1: 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: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}}, &models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: true, isApproved: true,
isReviewedByTest1: 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: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, Dismissed: true}, &models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, Dismissed: true},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: false, isApproved: false,
isReviewedByTest1: true, 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: "user1"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}}, &models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
fetchErr: errors.New("System error fetching reviews."), fetchErr: errors.New("System error fetching reviews."),
isApproved: true, isApproved: true,
isReviewedByTest1: 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: "user4"}},
&models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}}, &models.PullReview{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: true, 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, isReviewedByTest1: true,
}, },
} }
@@ -115,8 +131,12 @@ func TestReviews(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(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) rf.EXPECT().GetPullRequestReviews("test", "pr", int64(1)).Return(test.reviews, test.fetchErr)
reviews, err := common.FetchGiteaReviews(rf, test.reviewers, "test", "pr", 1) reviews, err := common.FetchGiteaReviews(rf, test.reviewers, "test", "pr", 1)

View File

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

View File

@@ -8,20 +8,20 @@ import (
) )
const ( const (
CommentType_ReviewRequested = "review_request" TimelineCommentType_ReviewRequested = "review_request"
CommentType_Review = "review" TimelineCommentType_Review = "review"
CommentType_PushPull = "pull_push" TimelineCommentType_PushPull = "pull_push"
CommentType_DismissReview = "dismiss_review" 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) timeline, err := gitea.GetTimeline(org, repo, id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
idx := len(timeline) - 1
//{"is_force_push":true,"commit_ids":["36e43509be1b13a1a8fc63a4361405de04cc621ab16935f88968c46193221bb6","732246a48fbc6bac9df16c0b0ca23ce0f6fbabd9990795863b6d1f0ef3f242c8"]} //{"is_force_push":true,"commit_ids":["36e43509be1b13a1a8fc63a4361405de04cc621ab16935f88968c46193221bb6","732246a48fbc6bac9df16c0b0ca23ce0f6fbabd9990795863b6d1f0ef3f242c8"]}
type PullPushData struct { type PullPushData struct {
IsForcePush bool `json:"is_force_push"` 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 // trim timeline to last push update or last time review request was requested
for ; idx > 0; idx-- { for i, e := range timeline {
e := timeline[idx] if e.Type == TimelineCommentType_PushPull {
if e.Type == CommentType_PushPull {
var push PullPushData var push PullPushData
if err := json.Unmarshal([]byte(e.Body), &push); err != nil { if err := json.Unmarshal([]byte(e.Body), &push); err != nil {
LogError(err) LogError(err)
} }
if slices.Contains(push.CommitIds, headSha) { 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 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 ( import (
"bufio"
"errors"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"regexp" "regexp"
"slices" "slices"
"strings" "strings"
"src.opensuse.org/autogits/common/gitea-generated/models"
) )
func SplitLines(str string) []string { func SplitLines(str string) []string {
@@ -121,3 +126,50 @@ func (giturl *GitUrl) RemoteName() string {
return strings.ToLower(giturl.Org) + "_" + strings.ToLower(giturl.Repo) 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 var DebugMode bool
func giteaPackage(pkg string) string {
return strings.ReplaceAll(pkg, "+", "_")
}
func projectMaintainer(obs *common.ObsClient, prj string) ([]string, []string) { // users, groups func projectMaintainer(obs *common.ObsClient, prj string) ([]string, []string) { // users, groups
meta, err := obs.GetProjectMeta(prj) meta, err := obs.GetProjectMeta(prj)
if err != nil { if err != nil {
@@ -168,13 +172,14 @@ func gitImporter(prj, pkg string) error {
func cloneDevel(git common.Git, gitDir, outName, urlString string) error { func cloneDevel(git common.Git, gitDir, outName, urlString string) error {
url, err := url.Parse(urlString) url, err := url.Parse(urlString)
branch := url.Fragment // branch := url.Fragment
url.Fragment = "" url.Fragment = ""
params := []string{"clone"} params := []string{"clone"}
if len(branch) > 0 { /* if len(branch) > 0 {
params = append(params, "-b", branch) params = append(params, "-b", branch)
} }
*/
params = append(params, url.String(), outName) params = append(params, url.String(), outName)
if err != nil { if err != nil {
@@ -185,13 +190,16 @@ func cloneDevel(git common.Git, gitDir, outName, urlString string) error {
} }
func importRepos(packages []string) { func importRepos(packages []string) {
RepoToObsName := make(map[string]string)
factoryRepos := make([]*models.Repository, 0, len(packages)*2) factoryRepos := make([]*models.Repository, 0, len(packages)*2)
develProjectPackages := make([]string, 0, len(packages)) develProjectPackages := make([]string, 0, len(packages))
for _, pkg := range packages { for _, pkg := range packages {
src_pkg_name := strings.Split(pkg, ":") src_pkg_name := strings.Split(pkg, ":")
RepoToObsName[giteaPackage(src_pkg_name[0])] = src_pkg_name[0]
repo, err := client.Repository.RepoGet( repo, err := client.Repository.RepoGet(
repository.NewRepoGetParams(). repository.NewRepoGetParams().
WithDefaults().WithOwner("pool").WithRepo(src_pkg_name[0]), WithDefaults().WithOwner("pool").WithRepo(giteaPackage(src_pkg_name[0])),
r.DefaultAuthentication) r.DefaultAuthentication)
if err != nil { if err != nil {
@@ -206,10 +214,19 @@ func importRepos(packages []string) {
} }
} }
log.Println("Num repos found:", len(factoryRepos)) 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)) oldPackageNames := make([]string, 0, len(factoryRepos))
for _, repo := range factoryRepos { for _, repo := range factoryRepos {
oldPackageNames = append(oldPackageNames, repo.Name) oldPackageNames = append(oldPackageNames, RepoToObsName[repo.Name])
} }
// fork packags from pool // fork packags from pool
@@ -231,48 +248,60 @@ func importRepos(packages []string) {
log.Println("adding remotes...") log.Println("adding remotes...")
for i := 0; i < len(factoryRepos); i++ { for i := 0; i < len(factoryRepos); i++ {
pkg := 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 // 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 os.IsNotExist(err) {
if slices.Contains(develProjectPackages, pkg.Name) { if slices.Contains(develProjectPackages, pkgName) {
// failed import of former factory package // failed import of former factory package
log.Println("Failed to import former factory pkg:", pkgName)
continue continue
} }
// scmsync? // scmsync?
devel_project, err := runObsCommand("develproject", "openSUSE:Factory", pkg.Name) devel_project, err := devel_projects.GetDevelProject(pkgName)
if err != nil || len(devel_project) != 1 { if err != nil {
log.Panicln("devel project len:", len(devel_project), "for", pkg.Name, "err:", err) log.Panicln("devel project not found for", RepoToObsName[pkg.Name], "err:", err)
} }
d := strings.Split(devel_project[0], "/") meta, _ := obs.GetPackageMeta(devel_project, pkgName)
if len(d) != 2 {
log.Panicln("expected devel project/package. got:", d)
}
meta, _ := obs.GetPackageMeta(d[0], d[1])
if len(meta.ScmSync) > 0 { 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) 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 continue
} }
// try again, should now exist // 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) log.Panicln(err)
} }
} else if err != nil { } else if err != nil {
log.Panicln(err) log.Panicln(err)
} else { } else {
// verify that we do not have scmsync for imported packages // 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 { if err != nil {
log.Panicln(err) log.Panicln(err)
} }
if len(meta.ScmSync) > 0 { 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 // add remote repos
out := git.GitExecWithOutputOrPanic(pkg.Name, "remote", "show", "-n") out := git.GitExecWithOutputOrPanic(gitName, "remote", "show", "-n")
switch pkg.Owner.UserName { switch pkg.Owner.UserName {
case "pool": case "pool":
if !slices.Contains(strings.Split(out, "\n"), "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 { if len(strings.TrimSpace(out)) > 1 {
log.Println(out) log.Println(out)
} }
@@ -329,7 +358,7 @@ func importRepos(packages []string) {
break break
} else { } else {
log.Panicln(" *** factory has no branches", branches) log.Panicln(" *** factory has no branches", branches)
} }
} }
pool_branch := "factory" pool_branch := "factory"
@@ -392,12 +421,22 @@ func importRepos(packages []string) {
for i := 0; i < len(develProjectPackages); i++ { for i := 0; i < len(develProjectPackages); i++ {
pkg := develProjectPackages[i] pkg := develProjectPackages[i]
meta, _ := obs.GetPackageMeta(prj, pkg) meta, err := obs.GetPackageMeta(prj, pkg)
if len(meta.ScmSync) > 0 { if err != nil {
if err2 := cloneDevel(git, "", pkg, meta.ScmSync); err2 != nil { meta, err = obs.GetPackageMeta(prj, pkg)
log.Panicln(err2) 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 continue
} else { } else {
common.PanicOnError(gitImporter(prj, pkg)) common.PanicOnError(gitImporter(prj, pkg))
@@ -459,7 +498,7 @@ func importRepos(packages []string) {
remotes := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkg.Name, "remote", "show"), "\n") remotes := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkg.Name, "remote", "show"), "\n")
if !slices.Contains(remotes, "develorigin") { if !slices.Contains(remotes, "develorigin") {
git.GitExecOrPanic(pkg.Name, "remote", "add", "develorigin", repo.SSHURL) 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") { if slices.Contains(remotes, "origin") {
git.GitExecOrPanic(pkg.Name, "lfs", "fetch", "--all") git.GitExecOrPanic(pkg.Name, "lfs", "fetch", "--all")
@@ -467,8 +506,8 @@ func importRepos(packages []string) {
} }
git.GitExecOrPanic(pkg.Name, "push", "develorigin", "main", "-f") git.GitExecOrPanic(pkg.Name, "push", "develorigin", "main", "-f")
git.GitExec(pkg.Name, "push", "develorigin", "--delete", "factory", "devel") git.GitExec(pkg.Name, "push", "develorigin", "--delete", "factory", "devel")
// git.GitExecOrPanic(pkg.Name, "checkout", "-B", "main", "devel/main") // git.GitExecOrPanic(pkg.ame, "checkout", "-B", "main", "devel/main")
_, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(repo.Name).WithBody(&models.EditRepoOption{ _, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(giteaPackage(repo.Name)).WithBody(&models.EditRepoOption{
DefaultBranch: "main", DefaultBranch: "main",
DefaultMergeStyle: "fast-forward-only", DefaultMergeStyle: "fast-forward-only",
HasPullRequests: true, HasPullRequests: true,
@@ -493,12 +532,13 @@ func importRepos(packages []string) {
for _, pkg := range develProjectPackages { for _, pkg := range develProjectPackages {
var repo *models.Repository 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( _, err := client.Organization.CreateOrgRepo(organization.NewCreateOrgRepoParams().WithOrg(org).WithBody(
&models.CreateRepoOption{ &models.CreateRepoOption{
ObjectFormatName: "sha256", ObjectFormatName: "sha256",
AutoInit: false, AutoInit: false,
Name: &pkg, Name: &giteaPkg,
DefaultBranch: "main", DefaultBranch: "main",
}), }),
r.DefaultAuthentication, r.DefaultAuthentication,
@@ -508,7 +548,7 @@ func importRepos(packages []string) {
log.Panicln("Error creating new package repository:", pkg, err) 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{ &models.EditRepoOption{
HasPullRequests: true, HasPullRequests: true,
HasPackages: false, HasPackages: false,
@@ -548,7 +588,7 @@ func importRepos(packages []string) {
git.GitExecOrPanic(pkg, "push", "develorigin", "main", "-f") git.GitExecOrPanic(pkg, "push", "develorigin", "main", "-f")
git.GitExec(pkg, "push", "develorigin", "--delete", "factory", "devel") 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", DefaultBranch: "main",
DefaultMergeStyle: "fast-forward-only", DefaultMergeStyle: "fast-forward-only",
}), r.DefaultAuthentication) }), r.DefaultAuthentication)
@@ -647,7 +687,7 @@ func syncPackageCollaborators(pkg string, orig_uids []common.PersonRepoMeta) []s
missing := []string{} missing := []string{}
uids := make([]common.PersonRepoMeta, len(orig_uids)) uids := make([]common.PersonRepoMeta, len(orig_uids))
copy(uids, 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 err != nil {
if errors.Is(err, &repository.RepoListCollaboratorsNotFound{}) { if errors.Is(err, &repository.RepoListCollaboratorsNotFound{}) {
return missing return missing
@@ -668,7 +708,7 @@ func syncPackageCollaborators(pkg string, orig_uids []common.PersonRepoMeta) []s
log.Println("missing collabs for", pkg, ":", uids) log.Println("missing collabs for", pkg, ":", uids)
} }
for _, u := range 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", Permission: "write",
}).WithCollaborator(u.UserID), r.DefaultAuthentication) }).WithCollaborator(u.UserID), r.DefaultAuthentication)
@@ -700,6 +740,10 @@ func syncMaintainersToGitea(pkgs []string) {
devs := []string{} devs := []string{}
for _, group := range prjMeta.Groups { for _, group := range prjMeta.Groups {
if group.GroupID == "factory-maintainers" {
log.Println("Ignoring factory-maintainers")
continue
}
teamMembers, err := obs.GetGroupMeta(group.GroupID) teamMembers, err := obs.GetGroupMeta(group.GroupID)
if err != nil { if err != nil {
log.Panicln("failed to get group", err) log.Panicln("failed to get group", err)
@@ -795,13 +839,20 @@ func createPrjGit() {
git.GitExecOrPanic(common.DefaultGitPrj, "add", "_config") 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 { if err != nil {
log.Panicln(err) log.Panicln(err)
} }
file.Write([]byte(prj)) file.WriteString("{\n // Reference build project\n \"ObsProject\": \"" + prj + "\",\n}\n")
file.Close() 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 obs *common.ObsClient
var prj, org string var prj, org string
var forceBadPool bool var forceBadPool bool
var forceNonPoolPackages bool
var devel_projects common.DevelProjects
func main() { func main() {
if err := common.RequireGiteaSecretToken(); err != nil { if err := common.RequireGiteaSecretToken(); err != nil {
@@ -828,6 +881,7 @@ func main() {
flags.SetOutput(helpString) flags.SetOutput(helpString)
//workflowConfig := flag.String("config", "", "Repository and workflow definition file") //workflowConfig := flag.String("config", "", "Repository and workflow definition file")
giteaHost := flags.String("gitea", "src.opensuse.org", "Gitea instance") 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") //rabbitUrl := flag.String("url", "amqps://rabbit.opensuse.org", "URL for RabbitMQ instance")
flags.BoolVar(&DebugMode, "debug", false, "Extra debugging information") 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") // 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") getMaintainers := flags.Bool("maintainers-only", false, "Get maintainers only and exit")
syncMaintainers := flags.Bool("sync-maintainers-only", false, "Sync maintainers to Gitea 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(&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 { if help := flags.Parse(os.Args[1:]); help == flag.ErrHelp || flags.NArg() != 2 {
printHelp(helpString.String()) printHelp(helpString.String())
@@ -847,25 +903,30 @@ func main() {
// r.SetDebug(true) // r.SetDebug(true)
client = apiclient.New(r, nil) 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 var err error
git, err = gh.CreateGitHandler("Autogits - Devel Importer", "not.exist", "devel-importer")
devel_projects, err = common.FetchDevelProjects()
if err != nil { 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 DebugMode {
if len(*debugGitPath) > 0 { if len(*debugGitPath) > 0 {
git.Close() gh, err = common.AllocateGitWorkTree(*debugGitPath, "Autogits - Devel Importer", "not.exist")
git, err = gh.ReadExistingPath("Autogits - Devel Importer", "not.exist", *debugGitPath)
if err != nil { if err != nil {
log.Panicln(err) log.Panicln(err)
} }
} }
log.Println(" - working directory:" + git.GetPath())
} else { } 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) 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 { for _, pkg := range packages {
if _, err := client.Organization.CreateOrgRepo(organization.NewCreateOrgRepoParams().WithOrg(org).WithBody( if _, err := client.Organization.CreateOrgRepo(organization.NewCreateOrgRepoParams().WithOrg(org).WithBody(
@@ -920,11 +988,15 @@ func main() {
if *purgeOnly { if *purgeOnly {
log.Println("Purging repositories...") log.Println("Purging repositories...")
for _, pkg := range packages { 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) os.Exit(10)
} }
if len(*specificPackage) != 0 {
importRepos([]string{*specificPackage})
return
}
importRepos(packages) importRepos(packages)
syncMaintainersToGitea(packages) syncMaintainersToGitea(packages)
} }

View File

@@ -11,6 +11,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode"
"src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models" "src.opensuse.org/autogits/common/gitea-generated/models"
@@ -21,19 +22,32 @@ var acceptRx *regexp.Regexp
var rejectRx *regexp.Regexp var rejectRx *regexp.Regexp
var groupName string var groupName string
func InitRegex(groupName string) { func InitRegex(newGroupName string) {
acceptRx = regexp.MustCompile("\\s*:\\s*LGTM") groupName = newGroupName
rejectRx = regexp.MustCompile("\\s*:\\s*") acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
rejectRx = regexp.MustCompile("^:\\s*")
} }
func ParseReviewLine(reviewText string) (bool, string) { func ParseReviewLine(reviewText string) (bool, string) {
line := strings.TrimSpace(reviewText) line := strings.TrimSpace(reviewText)
glen := len(groupName) groupTextName := "@" + groupName
if len(line) < glen || line[0:glen] != groupName { glen := len(groupTextName)
if len(line) < glen || line[0:glen] != groupTextName {
return false, line 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 { func ReviewAccepted(reviewText string) bool {
@@ -99,23 +113,22 @@ var commentStrings = []string{
"change_time_estimate", "change_time_estimate",
}*/ }*/
func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.PullReview { func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
var good_review *models.PullReview
for _, t := range timeline { for _, t := range timeline {
if t.Type == common.CommentType_Review && t.User != nil && t.User.UserName == user && t.Created == t.Updated { if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated {
for _, r := range reviews { if ReviewAccepted(t.Body) || ReviewRejected(t.Body) {
if r.ID == t.ReviewID && (ReviewAccepted(r.Body) || ReviewRejected(r.Body)) { return t
good_review = r
break
}
} }
} 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) { 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) 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" { if pr.State == "closed" {
// dismiss the review // dismiss the review
common.LogInfo(" -- closed request, so nothing to review") common.LogInfo(" -- closed request, so nothing to review")
@@ -200,9 +217,10 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
for _, reviewer := range requestReviewers { for _, reviewer := range requestReviewers {
if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil { if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
if review.State == common.ReviewStateApproved && ReviewAccepted(review.Body) { if ReviewAccepted(review.Body) {
if !common.IsDryRun { if !common.IsDryRun {
gitea.AddReviewComment(pr, common.ReviewStateApproved, "Signed off by: "+reviewer) gitea.AddReviewComment(pr, common.ReviewStateApproved, "Signed off by: "+reviewer)
UnrequestReviews(gitea, org, repo, id, requestReviewers)
if !common.IsDryRun { if !common.IsDryRun {
if err := gitea.SetNotificationRead(notification.ID); err != nil { if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err) 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(" -> approved by", reviewer)
common.LogInfo(" review at", review.Submitted) common.LogInfo(" review at", review.Created)
return return
} else if review.State == common.ReviewStateRequestChanges && ReviewRejected(review.Body) { } else if ReviewRejected(review.Body) {
if !common.IsDryRun { 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 { if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err) common.LogDebug(" Cannot set notification as read", err)
} }
@@ -239,10 +258,24 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
} else { } else {
common.LogDebug(" Not requesting additional reviewers") 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) { func PeriodReviewCheck(gitea common.Gitea) {
notifications, err := gitea.GetPullNotifications(nil) notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil { if err != nil {
common.LogError(" Error fetching unread notifications: %w", err) common.LogError(" Error fetching unread notifications: %w", err)
return return

View File

@@ -2,6 +2,76 @@ package main
import "testing" 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.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.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.Groups = nil
meta.Persons = 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-- { for idx := len(reviews) - 1; idx >= 0; idx-- {
review := reviews[idx] review := reviews[idx]
if review.User != nil || review.User.UserName == Username { if review.User == nil || review.User.UserName == Username {
if IsDryRun { if IsDryRun {
// for purposes of moving forward a no-op check // for purposes of moving forward a no-op check
return review, nil return review, nil
@@ -547,7 +549,7 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
} }
if pr.State != "closed" { 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 return false
} }
@@ -888,7 +890,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
func PollWorkNotifications(giteaUrl string) { func PollWorkNotifications(giteaUrl string) {
gitea := common.AllocateGiteaTransport(giteaUrl) gitea := common.AllocateGiteaTransport(giteaUrl)
data, err := gitea.GetPullNotifications(nil) data, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil { if err != nil {
common.LogError(err) common.LogError(err)
@@ -918,7 +920,7 @@ func PollWorkNotifications(giteaUrl string) {
cleanupFinished := false cleanupFinished := false
for page := int64(1); !cleanupFinished; page++ { for page := int64(1); !cleanupFinished; page++ {
cleanupFinished = true 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 { for _, n := range data {
if n.Unread { if n.Unread {
common.LogError("Done notification is unread or pinned?", *n.Subject) common.LogError("Done notification is unread or pinned?", *n.Subject)

View File

@@ -22,6 +22,7 @@ import (
"bytes" "bytes"
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"net/http" "net/http"
@@ -78,7 +79,7 @@ func ProjectStatusSummarySvg(project string) []byte {
return ret.Bytes() return ret.Bytes()
} }
func PackageStatusSummarySvg(status common.PackageBuildStatus) []byte { func PackageStatusSummarySvg(status *common.PackageBuildStatus) []byte {
buildStatus, ok := common.ObsBuildStatusDetails[status.Code] buildStatus, ok := common.ObsBuildStatusDetails[status.Code]
if !ok { if !ok {
buildStatus = common.ObsBuildStatusDetails["error"] buildStatus = common.ObsBuildStatusDetails["error"]
@@ -108,8 +109,10 @@ func main() {
key := flag.String("key-file", "", "Private key for the TLS certificate") key := flag.String("key-file", "", "Private key for the TLS certificate")
listen := flag.String("listen", "[::1]:8080", "Listening string") listen := flag.String("listen", "[::1]:8080", "Listening string")
disableTls := flag.Bool("no-tls", false, "Disable TLS") 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") 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() flag.Parse()
common.PanicOnError(common.RequireObsSecretToken()) common.PanicOnError(common.RequireObsSecretToken())
@@ -143,21 +146,25 @@ func main() {
res.Header().Add("content-type", "image/svg+xml") res.Header().Add("content-type", "image/svg+xml")
prjStatus := GetCurrentStatus(prj) status := GetDetailedBuildStatus(prj, pkg, repo, arch)
if prjStatus == nil { 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 return
} }
defer data.Close()
for _, r := range prjStatus.Result { io.Copy(res, data)
if r.Arch == arch && r.Repository == repo {
for _, status := range r.Status {
if status.Package == pkg {
res.Write(PackageStatusSummarySvg(status))
return
}
}
}
}
}) })
go ProcessUpdates() go ProcessUpdates()

View File

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

View File

@@ -1,8 +1,8 @@
package main package main
import ( import (
"log"
"slices" "slices"
"strings"
"sync" "sync"
"time" "time"
@@ -24,14 +24,86 @@ type StatusUpdateMsg struct {
func GetCurrentStatus(project string) *common.BuildResultList { func GetCurrentStatus(project string) *common.BuildResultList {
statusMutex.RLock() statusMutex.RLock()
defer statusMutex.RUnlock()
if ret, found := CurrentStatus[project]; found { if ret, found := CurrentStatus[project]; found {
statusMutex.RUnlock()
return ret 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 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() { 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) prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj)
if err != nil { 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) remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL)
@@ -526,7 +526,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
var defs common.ListenDefinitions defs := &common.RabbitMQGiteaEventsProcessor{}
var err error var err error
if len(*basePath) == 0 { if len(*basePath) == 0 {
@@ -557,7 +557,7 @@ func main() {
} }
log.Println("*** Reconfiguring ***") log.Println("*** Reconfiguring ***")
updateConfiguration(*configFilename, &defs.Orgs) updateConfiguration(*configFilename, &defs.Orgs)
defs.UpdateTopics() defs.Connection().UpdateTopics(defs)
} }
}() }()
signal.Notify(signalChannel, syscall.SIGHUP) signal.Notify(signalChannel, syscall.SIGHUP)
@@ -573,18 +573,17 @@ func main() {
updateConfiguration(*configFilename, &defs.Orgs) updateConfiguration(*configFilename, &defs.Orgs)
defs.GitAuthor = GitAuthor defs.Connection().RabbitURL, err = url.Parse(*rabbitUrl)
defs.RabbitURL, err = url.Parse(*rabbitUrl)
if err != nil { if err != nil {
log.Panicf("cannot parse server URL. Err: %#v\n", err) log.Panicf("cannot parse server URL. Err: %#v\n", err)
} }
go consistencyCheckProcess() go consistencyCheckProcess()
log.Println("defs:", defs) log.Println("defs:", *defs)
defs.Handlers = make(map[string]common.RequestProcessor) defs.Handlers = make(map[string]common.RequestProcessor)
defs.Handlers[common.RequestType_Push] = &PushActionProcessor{} defs.Handlers[common.RequestType_Push] = &PushActionProcessor{}
defs.Handlers[common.RequestType_Repository] = &RepositoryActionProcessor{} 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 * _GitProjectName_: package in above org, or `org/package` for PrjGit
* _Reviewers_: accounts associated with mandatory reviews for PrjGit. Can trigger additional * _Reviewers_: accounts associated with mandatory reviews for PrjGit. Can trigger additional
review requests for PrjGit or associated PkgGit repos. Only when all reviews are 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: 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 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) checker := CreateDefaultStateChecker(*checkOnStart, req, Gitea, time.Duration(*checkIntervalHours)*time.Hour)
go checker.ConsistencyCheckProcess() go checker.ConsistencyCheckProcess()
listenDefs := common.ListenDefinitions{ listenDefs := &common.RabbitMQGiteaEventsProcessor{
Orgs: orgs, Orgs: orgs,
GitAuthor: GitAuthor, // GitAuthor: GitAuthor,
Handlers: map[string]common.RequestProcessor{ Handlers: map[string]common.RequestProcessor{
common.RequestType_PR: req, common.RequestType_PR: req,
common.RequestType_PRSync: req, common.RequestType_PRSync: req,
@@ -172,7 +172,7 @@ func main() {
common.RequestType_PRReviewRejected: req, 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) { func TestProjectBranchName(t *testing.T) {
req := common.PullRequestWebhookEvent{ branchName := prGitBranchNameForPR("testingRepo", 10)
Repository: &common.Repository{
Name: "testingRepo",
},
Pull_Request: &common.PullRequest{
Number: 10,
},
}
branchName := prGitBranchNameForPR(&req)
if branchName != "PR_testingRepo#10" { if branchName != "PR_testingRepo#10" {
t.Error("Unexpected branch name:", branchName) 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 = "---" const LocalCMD = "---"
func gitExecs(t *testing.T, git *common.GitHandlerImpl, cmds [][]string) { func gitExecs(t *testing.T, git *common.GitHandlerImpl, cmds [][]string) {
@@ -133,7 +113,6 @@ func TestUpdatePrBranch(t *testing.T) {
} }
git := &common.GitHandlerImpl{ git := &common.GitHandlerImpl{
DebugLogger: true,
GitCommiter: "TestCommiter", GitCommiter: "TestCommiter",
GitEmail: "test@testing", GitEmail: "test@testing",
GitPath: t.TempDir(), GitPath: t.TempDir(),
@@ -146,7 +125,7 @@ func TestUpdatePrBranch(t *testing.T) {
req.Pull_Request.Base.Sha = strings.TrimSpace(revs[1]) req.Pull_Request.Base.Sha = strings.TrimSpace(revs[1])
req.Pull_Request.Head.Sha = strings.TrimSpace(revs[0]) 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, "commit", "-a", "-m", "created commit"))
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", "origin", "+HEAD:+testing")) common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", "origin", "+HEAD:+testing"))
git.GitExecOrPanic("prj", "reset", "--hard", "testing") git.GitExecOrPanic("prj", "reset", "--hard", "testing")
@@ -171,7 +150,6 @@ func TestCreatePrBranch(t *testing.T) {
} }
git := &common.GitHandlerImpl{ git := &common.GitHandlerImpl{
DebugLogger: true,
GitCommiter: "TestCommiter", GitCommiter: "TestCommiter",
GitEmail: "test@testing", GitEmail: "test@testing",
GitPath: t.TempDir(), GitPath: t.TempDir(),
@@ -184,7 +162,7 @@ func TestCreatePrBranch(t *testing.T) {
req.Pull_Request.Base.Sha = strings.TrimSpace(revs[1]) req.Pull_Request.Base.Sha = strings.TrimSpace(revs[1])
req.Pull_Request.Head.Sha = strings.TrimSpace(revs[0]) 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, "commit", "-a", "-m", "created commit"))
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", "origin", "+HEAD:testingCreated")) common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", "origin", "+HEAD:testingCreated"))

View File

@@ -5,14 +5,44 @@ package main
import ( import (
"fmt" "fmt"
"path" "path"
"runtime/debug"
"slices"
"strings"
"github.com/opentracing/opentracing-go/log" "github.com/opentracing/opentracing-go/log"
"src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models" "src.opensuse.org/autogits/common/gitea-generated/models"
) )
func prGitBranchNameForPR(req *common.PullRequestWebhookEvent) string { func prGitBranchNameForPR(repo string, prNo int) string {
return fmt.Sprintf("PR_%s#%d", req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number) 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 { func verifyRepositoryConfiguration(repo *models.Repository) error {
@@ -29,13 +59,13 @@ func verifyRepositoryConfiguration(repo *models.Repository) error {
return err return err
} }
func updateSubmoduleInPR(req *common.PullRequestWebhookEvent, git common.Git) { func updateSubmoduleInPR(submodule, headSha string, git common.Git) {
common.LogDebug(req, git) common.LogDebug("updating submodule", submodule, "to HEAD", headSha)
submoduleName := req.Pull_Request.Base.Repo.Name // NOTE: this can fail if current PrjGit is pointing to outdated, GC'ed commit
commitID := req.Pull_Request.Head.Sha // as long as we can update to newer one later, we are still OK
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submoduleName)) git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submodule)
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submoduleName), "fetch", "--depth", "1", "origin", commitID)) common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "fetch", "--depth", "1", "origin", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submoduleName), "checkout", commitID)) common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", headSha))
} }
type PRProcessor struct { type PRProcessor struct {
@@ -49,121 +79,229 @@ func AllocatePRProcessor(req *common.PullRequestWebhookEvent, configs common.Aut
id := req.Pull_Request.Number id := req.Pull_Request.Number
branch := req.Pull_Request.Base.Ref branch := req.Pull_Request.Base.Ref
assumed_git_project_name := org + "/" + repo + "#" + branch
PRstr := fmt.Sprintf("%s/%s#%d", org, repo, id) 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) config := configs.GetPrjGitConfig(org, repo, branch)
if c == nil { if config == nil {
if req.Pull_Request.Base.Repo.Default_Branch == branch { 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.") common.LogError("Cannot find config for PR.")
return nil, fmt.Errorf("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) common.LogDebug("found config", config)
if config == nil { if config == nil {
common.LogError("Cannot find config for branch '%s'", req.Pull_Request.Base.Ref) 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) 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 { if err != nil {
common.LogError("Cannot allocate GitHandler:", err) common.LogError("Cannot allocate GitHandler:", err)
return nil, fmt.Errorf("Error allocating GitHandler. Err: %w", err) return nil, fmt.Errorf("Error allocating GitHandler. Err: %w", err)
} }
common.LogDebug("git path:", git.GetPath()) common.LogDebug("git path:", git.GetPath())
// git.GitExecOrPanic("", "config", "set", "--global", "advice.submoduleMergeConflict", "false")
// git.GitExecOrPanic("", "config", "set", "--global", "advice.mergeConflict", "false")
return &PRProcessor{ return &PRProcessor{
config: config, config: config,
git: git, git: git,
}, nil }, nil
} }
func (pr *PRProcessor) CreateOrUpdatePrjGitPR(req *common.PullRequestWebhookEvent) error { func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
config := pr.config
git := pr.git 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") subList, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
common.PanicOnError(err) if err != nil {
common.LogError("Error fetching submodule list for PrjGit", err)
if id := subList[prRepo]; id != req.Pull_Request.Head.Sha { return err
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))
} }
_, err = Gitea.CreatePullRequestIfNotExist(prjGit, branchName, prjGit.DefaultBranch, for _, pr := range prset.PRs {
fmt.Sprintf("Forwarded PR: %s", prRepo), if prset.IsPrjGitPR(pr.PR) {
fmt.Sprintf(`This is a forwarded pull request by %s continue
referencing the following pull request: }
`+common.PrPattern, org, repo, idx := pr.PRComponents()
GitAuthor, prOrg, prRepo, req.Pull_Request.Number), 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 { 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) common.LogInfo("processing opened PR:", req.Pull_Request.Url)
prOrg := req.Pull_Request.Base.Repo.Owner.Username prOrg := req.Pull_Request.Base.Repo.Owner.Username
prRepo := req.Pull_Request.Base.Repo.Name prRepo := req.Pull_Request.Base.Repo.Name
prNo := int(req.Pull_Request.Number)
common.LogError(req) 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) prset, err := common.FetchPRSet(CurrentUser.UserName, Gitea, prOrg, prRepo, req.Number, config)
if err != nil { if err != nil {
common.LogError("Cannot fetch PRSet:", err) 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)) 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* // 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 // reset anything that changed that is not part of the prset
// package removals/additions are *not* counted here // package removals/additions are *not* counted here
org, repo, branch := config.GetPrjGit()
if pr, err := prset.GetPrjGitPR(); err == nil { 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) common.PanicOnError(err)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", remote, pr.Base.Ref, pr.Head.Ref) new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.Head.Sha)
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)
common.PanicOnError(err) common.PanicOnError(err)
common.LogDebug("Submodule parse done") common.LogDebug("Submodule parse done")
reset_submodule := func(submodule, sha string) { reset_submodule := func(submodule, sha string) {
spath := path.Join(common.DefaultGitPrj, submodule) updateSubmoduleInPR(submodule, sha, git)
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "update", "--init", "--depth", "1", submodule)
git.GitExecOrPanic(spath, "fetch", "origin", sha)
git.GitExecOrPanic(spath, "checkout", sha)
} }
for path, commit := range new_subs { for path, commit := range new_subs {
@@ -233,25 +408,42 @@ func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
if len(stats) > 0 { if len(stats) > 0 {
git.GitExecOrPanic(common.DefaultGitPrj, "commit", "-a", "-m", "Sync submodule updates with PR-set") 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, "submodule", "deinit", "--all", "--force")
git.GitExecOrPanic(common.DefaultGitPrj, "push") if !common.IsDryRun {
git.GitExecOrPanic(common.DefaultGitPrj, "push")
}
} }
} }
// request build review common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
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()
maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch) maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch)
if err != nil { if err != nil {
return err 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 { for _, pr := range prset.PRs {
if err := verifyRepositoryConfiguration(pr.PR.Base.Repo); err != nil { if err := verifyRepositoryConfiguration(pr.PR.Base.Repo); err != nil {
common.LogError("Cannot set manual merge... aborting processing") common.LogError("Cannot set manual merge... aborting processing")
@@ -271,25 +463,23 @@ func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
return err return err
} }
type PullRequestProcessor interface {
Process(req *common.PullRequestWebhookEvent, git common.Git, config *common.AutogitConfig) error
}
type RequestProcessor struct { type RequestProcessor struct {
configuredRepos map[string][]*common.AutogitConfig configuredRepos map[string][]*common.AutogitConfig
} }
func ProcesPullRequest(req *common.PullRequestWebhookEvent, configs []*common.AutogitConfig) error { 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 { if len(configs) < 1 {
// ignoring pull request against unconfigured project (could be just regular sources?) // ignoring pull request against unconfigured project (could be just regular sources?)
return nil 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) pr, err := AllocatePRProcessor(req, configs)
if err != nil { if err != nil {
log.Error(err) 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) { func TestOpenPR(t *testing.T) {
pr := PullRequestOpened{} pr := PRProcessor{
config: &common.AutogitConfig{
config := &common.AutogitConfig{ Reviewers: []string{"reviewer1", "reviewer2"},
Reviewers: []string{"reviewer1", "reviewer2"}, Branch: "branch",
Branch: "branch", Organization: "test",
Organization: "test", GitProjectName: "prj",
GitProjectName: "prj", },
} }
event := &common.PullRequestWebhookEvent{ 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) { t.Run("PR git opened request against PrjGit == no action", func(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
pr.gitea = mock_common.NewMockGitea(ctl) Gitea = mock_common.NewMockGitea(ctl)
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
config.GitProjectName = "testRepo" pr.config.GitProjectName = "testRepo"
event.Repository.Name = "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) t.Error("Error PrjGit opened request. Should be no error.", err)
} }
}) })
@@ -76,10 +76,10 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl) gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea Gitea = gitea
event.Repository.Name = "testRepo" event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy" pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
setupGitForTests(t, git) setupGitForTests(t, git)
@@ -101,7 +101,7 @@ func TestOpenPR(t *testing.T) {
UserName: "test", 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().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil)
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil) gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil)
gitea.EXPECT().GetPullRequest("test", "testRepo", int64(1)).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().FetchMaintainershipDirFile("test", "prjcopy", "branch", "_project").Return(nil, "", repository.NewRepoGetRawFileNotFound())
gitea.EXPECT().FetchMaintainershipFile("test", "prjcopy", "branch").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 { if err != nil {
t.Error("error:", err) t.Error("error:", err)
} }
@@ -121,17 +121,17 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl) gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea Gitea = gitea
event.Repository.Name = "testRepo" event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy" pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
setupGitForTests(t, git) setupGitForTests(t, git)
failedErr := errors.New("Returned error here") failedErr := errors.New("Returned error here")
gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(nil, failedErr) gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(nil, failedErr)
err := pr.Process(event, git, config) err := pr.Process(event)
if err != failedErr { if err != failedErr {
t.Error("error:", err) t.Error("error:", err)
} }
@@ -140,10 +140,10 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl) gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea Gitea = gitea
event.Repository.Name = "testRepo" event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy" pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
setupGitForTests(t, git) setupGitForTests(t, git)
@@ -155,7 +155,7 @@ func TestOpenPR(t *testing.T) {
gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil) gitea.EXPECT().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil)
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, failedErr) gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, failedErr)
err := pr.Process(event, git, config) err := pr.Process(event)
if err != failedErr { if err != failedErr {
t.Error("error:", err) t.Error("error:", err)
} }
@@ -164,10 +164,10 @@ func TestOpenPR(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
gitea := mock_common.NewMockGitea(ctl) gitea := mock_common.NewMockGitea(ctl)
pr.gitea = gitea Gitea = gitea
event.Repository.Name = "testRepo" event.Repository.Name = "testRepo"
config.GitProjectName = "prjcopy" pr.config.GitProjectName = "prjcopy"
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
setupGitForTests(t, git) setupGitForTests(t, git)
@@ -189,7 +189,7 @@ func TestOpenPR(t *testing.T) {
}, },
} }
failedErr := errors.New("Returned error here") 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().CreateRepositoryIfNotExist(git, "test", "prjcopy").Return(prjgit, nil)
gitea.EXPECT().GetPullRequest("test", "testRepo", int64(1)).Return(giteaPR, nil) gitea.EXPECT().GetPullRequest("test", "testRepo", int64(1)).Return(giteaPR, nil)
gitea.EXPECT().GetPullRequestReviews("org", "SomeRepo", int64(13)).Return([]*models.PullReview{}, 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().FetchMaintainershipDirFile("test", "prjcopy", "branch", "_project").Return(nil, "", repository.NewRepoGetRawFileNotFound())
gitea.EXPECT().FetchMaintainershipFile("test", "prjcopy", "branch").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 { if errors.Unwrap(err) != failedErr {
t.Error("error:", err) 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 package main
/*
import ( import (
"bytes" "bytes"
"errors" "errors"
@@ -16,13 +16,13 @@ import (
) )
func TestSyncPR(t *testing.T) { func TestSyncPR(t *testing.T) {
pr := PullRequestSynced{} pr := PRProcessor{
config: &common.AutogitConfig{
config := &common.AutogitConfig{ Reviewers: []string{"reviewer1", "reviewer2"},
Reviewers: []string{"reviewer1", "reviewer2"}, Branch: "testing",
Branch: "testing", Organization: "test",
Organization: "test", GitProjectName: "prj",
GitProjectName: "prj", },
} }
event := &common.PullRequestWebhookEvent{ event := &common.PullRequestWebhookEvent{
@@ -36,7 +36,7 @@ func TestSyncPR(t *testing.T) {
Repo: &common.Repository{ Repo: &common.Repository{
Name: "testRepo", Name: "testRepo",
Owner: &common.Organization{ Owner: &common.Organization{
Username: config.Organization, Username: pr.config.Organization,
}, },
Default_Branch: "main1", Default_Branch: "main1",
}, },
@@ -52,7 +52,7 @@ func TestSyncPR(t *testing.T) {
}, },
Repository: &common.Repository{ Repository: &common.Repository{
Owner: &common.Organization{ 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) { t.Run("PR sync request against PrjGit == no action", func(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
pr.gitea = mock_common.NewMockGitea(ctl) Gitea = mock_common.NewMockGitea(ctl)
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
config.GitProjectName = "testRepo" pr.config.GitProjectName = "testRepo"
event.Repository.Name = "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) t.Error("Error PrjGit sync request. Should be no error.", err)
} }
}) })
@@ -123,7 +123,7 @@ func TestSyncPR(t *testing.T) {
pr.gitea = mock pr.gitea = mock
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
config.GitProjectName = "prjGit" pr.config.GitProjectName = "prjGit"
event.Repository.Name = "testRepo" event.Repository.Name = "testRepo"
setupGitForTests(t, git) setupGitForTests(t, git)
@@ -132,10 +132,10 @@ func TestSyncPR(t *testing.T) {
defer func() { PrjGitPR.Head.Sha = oldSha }() defer func() { PrjGitPR.Head.Sha = oldSha }()
PrjGitPR.Head.Sha = "ab8adab91edb476b9762097d10c6379aa71efd6b60933a1c0e355ddacf419a95" PrjGitPR.Head.Sha = "ab8adab91edb476b9762097d10c6379aa71efd6b60933a1c0e355ddacf419a95"
mock.EXPECT().GetPullRequest(config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil) mock.EXPECT().GetPullRequest(pr.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, "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'" { if err == nil || err.Error() != "Cannot fetch submodule commit id in prjgit for 'testRepo'" {
t.Error("Invalid error received.", err) t.Error("Invalid error received.", err)
@@ -149,7 +149,7 @@ func TestSyncPR(t *testing.T) {
pr.gitea = mock pr.gitea = mock
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
config.GitProjectName = "prjGit" pr.config.GitProjectName = "prjGit"
event.Repository.Name = "tester" event.Repository.Name = "tester"
setupGitForTests(t, git) setupGitForTests(t, git)
@@ -173,19 +173,19 @@ func TestSyncPR(t *testing.T) {
ctl := gomock.NewController(t) ctl := gomock.NewController(t)
mock := mock_common.NewMockGitea(ctl) mock := mock_common.NewMockGitea(ctl)
pr.gitea = mock Gitea = mock
git.GitPath = t.TempDir() git.GitPath = t.TempDir()
config.GitProjectName = "prjGit" pr.config.GitProjectName = "prjGit"
event.Repository.Name = "testRepo" event.Repository.Name = "testRepo"
setupGitForTests(t, git) setupGitForTests(t, git)
// mock.EXPECT().GetAssociatedPrjGitPR(event).Return(PrjGitPR, nil) // mock.EXPECT().GetAssociatedPrjGitPR(event).Return(PrjGitPR, nil)
mock.EXPECT().GetPullRequest(config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil) mock.EXPECT().GetPullRequest(pr.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, "prj", int64(24)).Return(PrjGitPR, nil)
err := pr.Process(event, git, config) err := pr.Process(event)
if err != nil { if err != nil {
t.Error("Invalid error received.", err) t.Error("Invalid error received.", err)
@@ -199,14 +199,12 @@ func TestSyncPR(t *testing.T) {
t.Error(b.String()) 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)) os.RemoveAll(path.Join(git.GitPath, common.DefaultGitPrj))
mock.EXPECT().GetPullRequest(config.Organization, "testRepo", event.Pull_Request.Number).Return(modelPR, nil) mock.EXPECT().GetPullRequest(pr.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, "prj", int64(24)).Return(PrjGitPR, nil)
err = pr.Process(event, git, config) err = pr.Process(event)
if err != nil { if err != nil {
t.Error("Invalid error received.", err) 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"
"src.opensuse.org/autogits/common/gitea-generated/models" "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 { type DefaultStateChecker struct {
exitCheckLoop bool exitCheckLoop bool
checkOnStart bool checkOnStart bool
checkInterval time.Duration checkInterval time.Duration
processor *RequestProcessor processor *RequestProcessor
i StateChecker i interfaces.StateChecker
} }
func CreateDefaultStateChecker(checkOnStart bool, processor *RequestProcessor, gitea common.Gitea, interval time.Duration) *DefaultStateChecker { 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 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 var event common.PullRequestWebhookEvent
event.Pull_Request = common.PullRequestFromModel(pr) event.Pull_Request = common.PullRequestFromModel(pr)
event.Action = string(pr.State) + "ed" event.Action = pullRequestToEventState(pr.State)
event.Number = pr.Index event.Number = pr.Index
event.Repository = common.RepositoryFromModel(pr.Base.Repo) event.Repository = common.RepositoryFromModel(pr.Base.Repo)
event.Sender = *common.UserFromModel(pr.User) 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}) 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() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
common.LogError("panic caught") common.LogError("panic caught")
@@ -64,33 +66,31 @@ func (s *DefaultStateChecker) VerifyProjectState(config *common.AutogitConfig) e
} }
}() }()
prsToProcess := []*interfaces.PRToProcess{}
prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit() prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit()
common.LogInfo(" checking", prjGitOrg+"/"+prjGitRepo+"#"+prjGitBranch) common.LogInfo(" checking", prjGitOrg+"/"+prjGitRepo+"#"+prjGitBranch)
git, err := GitHandler.CreateGitHandler(config.Organization) git, err := GitHandler.CreateGitHandler(config.Organization)
common.LogDebug("Git Path:", git.GetPath()) common.LogDebug("Git Path:", git.GetPath())
if err != nil { 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() defer git.Close()
repo, err := Gitea.CreateRepositoryIfNotExist(git, prjGitOrg, prjGitRepo) repo, err := Gitea.CreateRepositoryIfNotExist(git, prjGitOrg, prjGitRepo)
if err != nil { 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) _, err = git.GitClone(prjGitRepo, prjGitBranch, repo.SSHURL)
common.PanicOnError(err) 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 { prsToProcess = append(prsToProcess, &interfaces.PRToProcess{
s.ProcessPR(git, pr, config) Org: prjGitOrg,
} Repo: prjGitRepo,
Branch: prjGitBranch,
common.LogDebug(" - # of PRs to check in PrjGit:", len(prs)) })
submodules, err := git.GitSubmoduleList(prjGitRepo, "HEAD") submodules, err := git.GitSubmoduleList(prjGitRepo, "HEAD")
nextSubmodule: nextSubmodule:
@@ -112,20 +112,16 @@ nextSubmodule:
branch = repo.DefaultBranch branch = repo.DefaultBranch
} }
prs, err := Gitea.GetRecentPullRequests(config.Organization, submoduleName, branch) prsToProcess = append(prsToProcess, &interfaces.PRToProcess{
if err != nil { Org: config.Organization,
return fmt.Errorf("Error fetching pull requests for %s/%s#%s. Err: %w", config.Organization, submoduleName, branch, err) Repo: submoduleName,
} Branch: branch,
common.LogDebug(" - # of PRs to check:", len(prs)) })
for _, pr := range prs {
s.ProcessPR(git, pr, config)
}
// check if the commited changes are syned with branches // check if the commited changes are syned with branches
commits, err := Gitea.GetRecentCommits(config.Organization, submoduleName, branch, 10) commits, err := Gitea.GetRecentCommits(config.Organization, submoduleName, branch, 10)
if err != nil { 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 { for idx, commit := range commits {
@@ -149,7 +145,7 @@ nextSubmodule:
url := git.GitExecWithOutputOrPanic(subDir, "remote", "get-url", "origin", "--push") url := git.GitExecWithOutputOrPanic(subDir, "remote", "get-url", "origin", "--push")
sshUrl, err := common.TranslateHttpsToSshUrl(strings.TrimSpace(url)) sshUrl, err := common.TranslateHttpsToSshUrl(strings.TrimSpace(url))
if err != nil { 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, "remote", "set-url", "origin", "--push", sshUrl)
git.GitExecOrPanic(subDir, "push", "origin", branch) 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 // forward any package-gits referred by the project git, but don't go back
return nil return prsToProcess, nil
} }
func (s *DefaultStateChecker) CheckRepos() error { func (s *DefaultStateChecker) CheckRepos() error {
@@ -172,10 +168,25 @@ func (s *DefaultStateChecker) CheckRepos() error {
} }
common.LogInfo(" ++ starting verification, org:", org, "config:", config.GitProjectName) 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) common.LogError(" *** verification failed, org:", org, err)
errorList = append(errorList, 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) common.LogInfo(" ++ verification complete, org:", org, "config:", config.GitProjectName)
} }
} }

View File

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