SHA256
1
0
forked from adamm/autogits

56 Commits

Author SHA256 Message Date
3d8671a7fe WIP: conflict resolution 2025-07-15 23:24:50 +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
35 changed files with 2273 additions and 889 deletions

View File

@@ -59,6 +59,9 @@ 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
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")
}
}) })
} }
} }

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,14 +427,21 @@ 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"
)
func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) {
bigLimit := int64(20)
ret := make([]*models.NotificationThread, 0, 100)
for page := int64(1); ; page++ {
params := notification.NewNotifyGetListParams(). params := notification.NewNotifyGetListParams().
WithDefaults(). WithDefaults().
WithSubjectType([]string{"Pull"}). WithSubjectType([]string{Type}).
WithStatusTypes([]string{"unread"}). WithStatusTypes([]string{"unread"}).
WithLimit(&bigLimit) WithLimit(&bigLimit).
WithPage(&page)
if since != nil { if since != nil {
s := strfmt.DateTime(*since) s := strfmt.DateTime(*since)
@@ -386,10 +453,16 @@ func (gitea *GiteaTransport) GetPullNotifications(since *time.Time) ([]*models.N
return nil, err return nil, err
} }
return list.Payload, nil ret = slices.Concat(ret, list.Payload)
if len(list.Payload) < int(bigLimit) {
break
}
} }
func (gitea *GiteaTransport) GetDonePullNotifications(page int64) ([]*models.NotificationThread, error) { return ret, nil
}
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

@@ -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)
if err != nil {
return false
}
reviewers = parsePkgDirData(pkg, r)
data.Data[pkg] = reviewers
} else { } else {
return true reviewers = data.ListProjectMaintainers()
}
} }
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)
for _, review := range reviews {
if slices.Contains(reviewers, submitter) { if slices.Contains(reviewers, submitter) {
LogDebug("Submitter is maintainer. Approving.") LogDebug("Submitter is maintainer. Approving.")
return true return true
} }
for _, review := range reviews {
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 = ""

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
@@ -718,9 +825,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 +866,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
} }

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

@@ -53,7 +53,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 +98,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
} }
@@ -86,20 +121,47 @@ func FetchPRSet(user string, gitea GiteaPRFetcher, org, repo string, num int64,
}, 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
}
} }
func (rs *PRSet) GetPrjGitPR() (*models.PullRequest, error) { return nil, false
var ret *models.PullRequest }
func (rs *PRSet) AddPR(pr *models.PullRequest) *PRInfo {
if pr, found := rs.Find(pr); found {
return pr
}
prinfo := &PRInfo{
PR: pr,
}
rs.PRs = append(rs.PRs, prinfo)
return prinfo
}
func (rs *PRSet) IsPrjGitPR(pr *models.PullRequest) bool {
org, repo, branch := rs.Config.GetPrjGit()
return pr.Base.Name == branch && pr.Base.Repo.Name == repo && pr.Base.Repo.Owner.UserName == org
}
var PRSet_PrjGitMissing error = errors.New("No PrjGit PR found")
var PRSet_MultiplePrjGit error = errors.New("Multiple PrjGit PRs in one review set")
func (rs *PRSet) GetPrjGitPR() (*PRInfo, error) {
var ret *PRInfo
for _, prinfo := range rs.PRs { 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 +170,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 +220,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 +240,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 +254,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 +262,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 +278,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 +347,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 +363,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

@@ -0,0 +1,48 @@
package common
import (
"errors"
"strings"
)
var UnknownParser error = errors.New("Cannot parse path")
type PRConflictResolver interface {
/*
stage_content -> { merge_base (stage1), head (stage2), merge_head (stage3) }
*/
Resolve(path string, stage_contents [3]string) error
}
var resolvers []PRConflictResolver = []PRConflictResolver{
&submodule_conflict_resolver{},
}
func ResolveMergeConflict(path string, file_contents [3]string) error {
for _, r := range resolvers {
if err := r.Resolve(path, file_contents); err != UnknownParser {
return err
}
}
return UnknownParser
}
type submodule_conflict_resolver struct{}
func (*submodule_conflict_resolver) Resolve(path string, stage [3]string) error {
if path != ".gitmodules" {
return UnknownParser
}
return UnknownParser
}
type changes_file_resolver struct{}
func (*changes_file_resolver) Resolve(path string, stage [3]string) error {
if !strings.HasSuffix(path, ".changes") {
return UnknownParser
}
return UnknownParser
}

View File

@@ -0,0 +1,10 @@
package common_test
import "testing"
func ResolveSubmoduleConflicts(t *testing.T) {
}
func ResolveChangesFileConflict(t *testing.T) {
}

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 {
@@ -243,7 +629,9 @@ func TestPRAssignReviewers(t *testing.T) {
} }
pkgReviews []*models.PullReview pkgReviews []*models.PullReview
pkgTimeline []*models.TimelineComment
prjReviews []*models.PullReview prjReviews []*models.PullReview
prjTimeline []*models.TimelineComment
expectedReviewerCall [2][]string expectedReviewerCall [2][]string
}{ }{
@@ -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

@@ -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

@@ -14,6 +14,8 @@ func TestReviewers(t *testing.T) {
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
@@ -110,13 +111,32 @@ func TestReviews(t *testing.T) {
isApproved: true, isApproved: true,
isReviewedByTest1: 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,
},
} }
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

@@ -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,51 @@ 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

@@ -168,13 +168,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 {
@@ -206,6 +207,15 @@ 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 {
@@ -241,15 +251,11 @@ func importRepos(packages []string) {
} }
// scmsync? // scmsync?
devel_project, err := runObsCommand("develproject", "openSUSE:Factory", pkg.Name) devel_project, err := devel_projects.GetDevelProject(pkg.Name)
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", pkg.Name, "err:", err)
} }
d := strings.Split(devel_project[0], "/") meta, _ := obs.GetPackageMeta(devel_project, pkg.Name)
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, "", pkg.Name, meta.ScmSync); err != nil {
log.Panicln(err2) log.Panicln(err2)
@@ -700,6 +706,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 +805,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 +829,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 +847,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 +856,7 @@ 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!")
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 +868,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 +905,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(

View File

@@ -28,8 +28,9 @@ func InitRegex(groupName string) {
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
} }
@@ -99,23 +100,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 +167,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 +204,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 +215,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 +245,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

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

@@ -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)

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

@@ -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,17 @@ 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 verifyRepositoryConfiguration(repo *models.Repository) error { func verifyRepositoryConfiguration(repo *models.Repository) error {
@@ -29,13 +32,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,123 +52,244 @@ 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) ([]string, []string, error) {
config := pr.config
git := pr.git git := pr.git
subList, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
if err != nil {
common.LogError("Error fetching submodule list for PrjGit", err)
return nil, nil, err
}
branchName := prGitBranchNameForPR(req) refs := make([]string, 0, len(prset.PRs))
title_refs := make([]string, 0, len(prset.PRs))
for _, pr := range prset.PRs {
if prset.IsPrjGitPR(pr.PR) {
continue
}
org, prj, _ := config.GetPrjGit() org := pr.PR.Base.Repo.Owner.UserName
prOrg := req.Pull_Request.Base.Repo.Owner.Username repo := pr.PR.Base.Repo.Name
prRepo := req.Pull_Request.Base.Repo.Name idx := pr.PR.Index
prHead := pr.PR.Head.Sha
revert := false
if org == prOrg && prj == prRepo { if pr.PR.State != "open" {
common.LogDebug("PrjGit PR. No need to update it...") 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 nil, nil, 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)
} else {
refs = append(refs, ref)
title_refs = append(title_refs, repo)
}
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 title_refs, refs, 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
}
title_refs, refs, err := pr.SetSubmodulesToMatchPRSet(prset)
if 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))
pr, err := Gitea.CreatePullRequestIfNotExist(PrjGit, prjGitPRbranch, PrjGitBranch,
"Forwarded PRs: "+strings.Join(title_refs, ", "),
fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor)+strings.Join(refs, ", "),
)
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 return nil
} }
prjGit, err := Gitea.CreateRepositoryIfNotExist(git, org, prj) func (pr *PRProcessor) RebaseAndSkipSubmoduleCommits(prset *common.PRSet, branch string) error {
common.PanicOnErrorWithMsg(err, "Error creating a prjgitrepo:", err) git := pr.git
PrjGitPR, err := prset.GetPrjGitPR()
common.PanicOnError(verifyRepositoryConfiguration(prjGit))
remoteName, err := git.GitClone(common.DefaultGitPrj, config.Branch, prjGit.SSHURL)
common.PanicOnError(err) common.PanicOnError(err)
// check if branch already there, and check that out if available remoteBranch := PrjGitPR.RemoteName + "/" + branch
if err := git.GitExec(common.DefaultGitPrj, "fetch", remoteName, branchName); err == nil {
git.GitExecOrPanic(common.DefaultGitPrj, "checkout", "-B", branchName, remoteName+"/"+branchName) 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")
} }
commitMsg := fmt.Sprintf(`auto-created for %s return nil
This commit was autocreated by %s
referencing
`+common.PrPattern,
prRepo,
GitAuthor,
prOrg,
prRepo,
req.Pull_Request.Number,
)
subList, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
common.PanicOnError(err)
if id := subList[prRepo]; id != req.Pull_Request.Head.Sha {
updateSubmoduleInPR(req, git)
status, err := git.GitStatus(common.DefaultGitPrj)
common.LogDebug("status:", status)
common.LogDebug("submodule", prRepo, " hash:", id, " -> ", req.Pull_Request.Head.Sha)
common.PanicOnError(err)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", remoteName, "+HEAD:"+branchName))
} }
_, err = Gitea.CreatePullRequestIfNotExist(prjGit, branchName, prjGit.DefaultBranch, func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
fmt.Sprintf("Forwarded PR: %s", prRepo), _, _, PrjGitBranch := prset.Config.GetPrjGit()
fmt.Sprintf(`This is a forwarded pull request by %s PrjGitPR, err := prset.GetPrjGitPR()
referencing the following pull request: if err != nil {
common.LogError("Updating PrjGitPR but not found?", err)
`+common.PrPattern,
GitAuthor, prOrg, prRepo, req.Pull_Request.Number),
)
return 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)
ExpectedMergeCommit, err := git.GitRemoteHead(common.DefaultGitPrj, PrjGitPR.RemoteName, PrjGitBranch)
forcePush := false
if ExpectedMergeCommit != PrjGitPR.PR.MergeBase {
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
}
title_refs, refs, err := pr.SetSubmodulesToMatchPRSet(prset)
if 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 := "Forwarded PRs: " + strings.Join(title_refs, ", ")
PrjGitBody := fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor) + strings.Join(refs, ", ")
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 {
config := pr.config config := pr.config
git := pr.git git := pr.git
@@ -174,16 +298,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 +309,63 @@ 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 := pr.PR.Base.Repo.Owner.UserName
repo := pr.PR.Base.Repo.Name
idx := pr.PR.Index
Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
State: "closed",
})
}
}
return nil
}
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 +388,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")
if !common.IsDryRun {
git.GitExecOrPanic(common.DefaultGitPrj, "push") git.GitExecOrPanic(common.DefaultGitPrj, "push")
} }
} }
// request build review
PR, err := prset.GetPrjGitPR()
if err != nil {
common.LogError("Error fetching PrjGitPR:", err)
return nil
} }
common.LogDebug(" num of reviewers:", len(PR.RequestedReviewers))
org, repo, branch := config.GetPrjGit() common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch) 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 +443,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)