3 Commits

Author SHA256 Message Date
66a2c0565e utils: specfile fix 2025-10-29 13:26:16 +01:00
d5d6910906 Add command line utility to automerge conflicts
It assumes CWD is the root of the git project
2025-10-29 13:01:25 +01:00
444959540a submodule conflict resolution
If merging a ProjectGit with submodules,
  * removed submodules are to be removed always
  * modified/added submodules are to be present
  * submodules modified in base project and PR should conflict
2025-10-28 19:43:03 +01:00
46 changed files with 1524 additions and 3484 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
*.osc *.osc
*.conf *.conf
utils/gitmodules-automerge/gitmodules-automerge
utils/hujson/hujson

View File

@@ -17,7 +17,7 @@
Name: autogits Name: autogits
Version: 1 Version: 0
Release: 0 Release: 0
Summary: GitWorkflow utilities Summary: GitWorkflow utilities
License: GPL-2.0-or-later License: GPL-2.0-or-later
@@ -41,7 +41,6 @@ Command-line tool to import devel projects from obs to git
%package doc %package doc
Summary: Common documentation files Summary: Common documentation files
BuildArch: noarch
%description -n autogits-doc %description -n autogits-doc
Common documentation files Common documentation files
@@ -57,11 +56,10 @@ with a topic
%package gitea-status-proxy %package gitea-status-proxy
Summary: Proxy for setting commit status in Gitea Summary: gitea-status-proxy
%description gitea-status-proxy %description gitea-status-proxy
Setting commit status requires code write access token. This proxy
is middleware that delegates status setting without access to other APIs
%package group-review %package group-review
Summary: Reviews of groups defined in ProjectGit Summary: Reviews of groups defined in ProjectGit
@@ -98,6 +96,7 @@ Provides: /usr/bin/hujson
%description utils %description utils
HuJSON to JSON parser, using stdin -> stdout pipe HuJSON to JSON parser, using stdin -> stdout pipe
gitmodules-automerge fixes conflicts in .gitmodules conflicts during merge
%package workflow-direct %package workflow-direct
@@ -129,6 +128,9 @@ go build \
go build \ go build \
-C utils/hujson \ -C utils/hujson \
-buildmode=pie -buildmode=pie
go build \
-C utils/gitmodules-automerge \
-buildmode=pie
go build \ go build \
-C gitea-events-rabbitmq-publisher \ -C gitea-events-rabbitmq-publisher \
-buildmode=pie -buildmode=pie
@@ -169,16 +171,15 @@ install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publishe
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
install -D -m0755 gitea_status_proxy/gitea_status_proxy %{buildroot}%{_bindir}/gitea_status_proxy install -D -m0755 gitea_status_proxy/gitea_status_proxy %{buildroot}%{_bindir}/gitea_status_proxy
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
install -D -m0644 systemd/group-review@.service %{buildroot}%{_unitdir}/group-review@.service
install -D -m0755 obs-forward-bot/obs-forward-bot %{buildroot}%{_bindir}/obs-forward-bot install -D -m0755 obs-forward-bot/obs-forward-bot %{buildroot}%{_bindir}/obs-forward-bot
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot
install -D -m0644 systemd/obs-staging-bot.service %{buildroot}%{_unitdir}/obs-staging-bot.service install -D -m0644 systemd/obs-staging-bot.service %{buildroot}%{_unitdir}/obs-staging-bot.service
install -D -m0755 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service install -D -m0755 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service
install -D -m0644 systemd/obs-status-service.service %{buildroot}%{_unitdir}/obs-status-service.service install -D -m0644 systemd/obs-status-service.service %{buildroot}%{_unitdir}/obs-status-service.service
install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
install -D -m0644 systemd/workflow-direct@.service %{buildroot}%{_unitdir}/workflow-direct@.service
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
install -D -m0755 utils/hujson/hujson %{buildroot}%{_bindir}/hujson install -D -m0755 utils/hujson/hujson %{buildroot}%{_bindir}/hujson
install -D -m0755 utils/gitmodules-automerge/gitmodules-automerge %{buildroot}%{_bindir}/gitmodules-automerge
%pre gitea-events-rabbitmq-publisher %pre gitea-events-rabbitmq-publisher
%service_add_pre gitea-events-rabbitmq-publisher.service %service_add_pre gitea-events-rabbitmq-publisher.service
@@ -192,18 +193,6 @@ install -D -m0755 utils/hujson/hujson
%postun gitea-events-rabbitmq-publisher %postun gitea-events-rabbitmq-publisher
%service_del_postun gitea-events-rabbitmq-publisher.service %service_del_postun gitea-events-rabbitmq-publisher.service
%pre group-review
%service_add_pre group-review@.service
%post group-review
%service_add_post group-review@.service
%preun group-review
%service_del_preun group-review@.service
%postun group-review
%service_del_postun group-review@.service
%pre obs-staging-bot %pre obs-staging-bot
%service_add_pre obs-staging-bot.service %service_add_pre obs-staging-bot.service
@@ -228,18 +217,6 @@ install -D -m0755 utils/hujson/hujson
%postun obs-status-service %postun obs-status-service
%service_del_postun obs-status-service.service %service_del_postun obs-status-service.service
%pre workflow-pr
%service_add_pre workflow-direct@.service
%post workflow-pr
%service_add_post workflow-direct@.service
%preun workflow-pr
%service_del_preun workflow-direct@.service
%postun workflow-pr
%service_del_postun workflow-direct@.service
%files devel-importer %files devel-importer
%license COPYING %license COPYING
%doc devel-importer/README.md %doc devel-importer/README.md
@@ -264,7 +241,6 @@ install -D -m0755 utils/hujson/hujson
%license COPYING %license COPYING
%doc group-review/README.md %doc group-review/README.md
%{_bindir}/group-review %{_bindir}/group-review
%{_unitdir}/group-review@.service
%files obs-forward-bot %files obs-forward-bot
%license COPYING %license COPYING
@@ -285,12 +261,12 @@ install -D -m0755 utils/hujson/hujson
%files utils %files utils
%license COPYING %license COPYING
%{_bindir}/hujson %{_bindir}/hujson
%{_bindir}/gitmodules-automerge
%files workflow-direct %files workflow-direct
%license COPYING %license COPYING
%doc workflow-direct/README.md %doc workflow-direct/README.md
%{_bindir}/workflow-direct %{_bindir}/workflow-direct
%{_unitdir}/workflow-direct@.service
%files workflow-pr %files workflow-pr
%license COPYING %license COPYING

View File

@@ -54,7 +54,6 @@ type ReviewGroup struct {
type QAConfig struct { type QAConfig struct {
Name string Name string
Origin string Origin string
BuildDisableRepos []string // which repos to build disable in the new project
} }
type Permissions struct { type Permissions struct {
@@ -62,20 +61,6 @@ type Permissions struct {
Members []string Members []string
} }
const (
Label_StagingAuto = "staging/Auto"
Label_ReviewPending = "review/Pending"
Label_ReviewDone = "review/Done"
)
func LabelKey(tag_value string) string {
// capitalize first letter and remove /
if len(tag_value) == 0 {
return ""
}
return strings.ToUpper(tag_value[0:1]) + strings.ReplaceAll(tag_value[1:], "/", "")
}
type AutogitConfig struct { type AutogitConfig struct {
Workflows []string // [pr, direct, test] Workflows []string // [pr, direct, test]
Organization string Organization string
@@ -87,8 +72,6 @@ type AutogitConfig struct {
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories
Labels map[string]string // list of tags, if not default, to apply
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers 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 ManualMergeProject bool // require merge of ProjectGit PRs with "Merge OK" by ProjectMaintainers and/or reviewers
@@ -205,8 +188,6 @@ func (configs AutogitConfigs) GetPrjGitConfig(org, repo, branch string) *Autogit
if c.GitProjectName == prjgit { if c.GitProjectName == prjgit {
return c return c
} }
}
for _, c := range configs {
if c.Organization == org && c.Branch == branch { if c.Organization == org && c.Branch == branch {
return c return c
} }
@@ -292,14 +273,6 @@ func (config *AutogitConfig) GetRemoteBranch() string {
return "origin_" + config.Branch return "origin_" + config.Branch
} }
func (config *AutogitConfig) Label(label string) string {
if t, found := config.Labels[LabelKey(label)]; found {
return t
}
return label
}
type StagingConfig struct { type StagingConfig struct {
ObsProject string ObsProject string
RebuildAll bool RebuildAll bool

View File

@@ -10,67 +10,6 @@ import (
mock_common "src.opensuse.org/autogits/common/mock" mock_common "src.opensuse.org/autogits/common/mock"
) )
func TestLabelKey(t *testing.T) {
tests := map[string]string{
"": "",
"foo": "Foo",
"foo/bar": "Foobar",
"foo/Bar": "FooBar",
}
for k, v := range tests {
if c := common.LabelKey(k); c != v {
t.Error("expected", v, "got", c, "input", k)
}
}
}
func TestConfigLabelParser(t *testing.T) {
tests := []struct {
name string
json string
label_value string
}{
{
name: "empty",
json: "{}",
label_value: "path/String",
},
{
name: "defined",
json: `{"Labels": {"foo": "bar", "PathString": "moo/Label"}}`,
label_value: "moo/Label",
},
{
name: "undefined",
json: `{"Labels": {"foo": "bar", "NotPathString": "moo/Label"}}`,
label_value: "path/String",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
repo := models.Repository{
DefaultBranch: "master",
}
ctl := gomock.NewController(t)
gitea := mock_common.NewMockGiteaFileContentAndRepoFetcher(ctl)
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.json), "abc", nil)
gitea.EXPECT().GetRepository("foo", "bar").Return(&repo, nil)
config, err := common.ReadWorkflowConfig(gitea, "foo/bar")
if err != nil || config == nil {
t.Fatal(err)
}
if l := config.Label("path/String"); l != test.label_value {
t.Error("Expecting", test.label_value, "got", l)
}
})
}
}
func TestProjectConfigMatcher(t *testing.T) { func TestProjectConfigMatcher(t *testing.T) {
configs := common.AutogitConfigs{ configs := common.AutogitConfigs{
{ {
@@ -82,15 +21,6 @@ func TestProjectConfigMatcher(t *testing.T) {
Branch: "main", Branch: "main",
GitProjectName: "test/prjgit#main", GitProjectName: "test/prjgit#main",
}, },
{
Organization: "test",
Branch: "main",
GitProjectName: "test/bar#never_match",
},
{
Organization: "test",
GitProjectName: "test/bar#main",
},
} }
tests := []struct { tests := []struct {
@@ -120,20 +50,6 @@ func TestProjectConfigMatcher(t *testing.T) {
branch: "main", branch: "main",
config: 1, config: 1,
}, },
{
name: "prjgit only match",
org: "test",
repo: "bar",
branch: "main",
config: 3,
},
{
name: "non-default branch match",
org: "test",
repo: "bar",
branch: "something_main",
config: -1,
},
} }
for _, test := range tests { for _, test := range tests {
@@ -189,10 +105,6 @@ func TestConfigWorkflowParser(t *testing.T) {
if config.ManualMergeOnly != false { if config.ManualMergeOnly != false {
t.Fatal("This should be false") t.Fatal("This should be false")
} }
if config.Label("foobar") != "foobar" {
t.Fatal("undefined label should return default value")
}
}) })
} }
} }

296
common/git_parser.go Normal file
View File

@@ -0,0 +1,296 @@
package common
import (
"errors"
"fmt"
"io"
)
const (
GitStatus_Untracked = 0
GitStatus_Modified = 1
GitStatus_Ignored = 2
GitStatus_Unmerged = 3 // States[0..3] -- Stage1, Stage2, Stage3 of merge objects
GitStatus_Renamed = 4 // orig name in States[0]
)
type GitStatusData struct {
Path string
Status int
States [3]string
/*
<sub> A 4 character field describing the submodule state.
"N..." when the entry is not a submodule.
"S<c><m><u>" when the entry is a submodule.
<c> is "C" if the commit changed; otherwise ".".
<m> is "M" if it has tracked changes; otherwise ".".
<u> is "U" if there are untracked changes; otherwise ".".
*/
SubmoduleChanges string
}
func parseGit_HexString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 32)
for {
c, err := data.ReadByte()
if err != nil {
return "", err
}
switch {
case c == 0 || c == ' ':
return string(str), nil
case c >= 'a' && c <= 'f':
case c >= 'A' && c <= 'F':
case c >= '0' && c <= '9':
default:
return "", errors.New("Invalid character in hex string:" + string(c))
}
str = append(str, c)
}
}
func parseGit_String(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 parseGit_StringWithSpace(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 {
return string(str), nil
}
str = append(str, c)
}
}
func skipGitStatusEntry(data io.ByteReader, skipSpaceLen int) error {
for skipSpaceLen > 0 {
c, err := data.ReadByte()
if err != nil {
return err
}
if c == ' ' {
skipSpaceLen--
}
}
return nil
}
func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
ret := GitStatusData{}
statusType, err := data.ReadByte()
if err != nil {
return nil, nil
}
switch statusType {
case '1':
var err error
if err = skipGitStatusEntry(data, 8); err != nil {
return nil, err
}
ret.Status = GitStatus_Modified
ret.Path, err = parseGit_StringWithSpace(data)
if err != nil {
return nil, err
}
case '2':
var err error
if err = skipGitStatusEntry(data, 9); err != nil {
return nil, err
}
ret.Status = GitStatus_Renamed
ret.Path, err = parseGit_StringWithSpace(data)
if err != nil {
return nil, err
}
ret.States[0], err = parseGit_StringWithSpace(data)
if err != nil {
return nil, err
}
case '?':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Untracked
ret.Path, err = parseGit_StringWithSpace(data)
if err != nil {
return nil, err
}
case '!':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Ignored
ret.Path, err = parseGit_StringWithSpace(data)
if err != nil {
return nil, err
}
case 'u':
var err error
if err = skipGitStatusEntry(data, 2); err != nil {
return nil, err
}
if ret.SubmoduleChanges, err = parseGit_String(data); err != nil {
return nil, err
}
if err = skipGitStatusEntry(data, 4); err != nil {
return nil, err
}
if ret.States[0], err = parseGit_HexString(data); err != nil {
return nil, err
}
if ret.States[1], err = parseGit_HexString(data); err != nil {
return nil, err
}
if ret.States[2], err = parseGit_HexString(data); err != nil {
return nil, err
}
ret.Status = GitStatus_Unmerged
ret.Path, err = parseGit_StringWithSpace(data)
if err != nil {
return nil, err
}
default:
return nil, errors.New("Invalid status type" + string(statusType))
}
return &ret, nil
}
func parseGitStatusData(data io.ByteReader) (Data, error) {
ret := make([]GitStatusData, 0, 10)
for {
data, err := parseSingleStatusEntry(data)
if err != nil {
return nil, err
} else if data == nil {
break
}
ret = append(ret, *data)
}
return ret, nil
}
type Data interface{}
type CommitStatus int
const (
Add CommitStatus = iota
Rm
Copy
Modify
Rename
TypeChange
Unmerged
Unknown
)
type GitDiffRawData struct {
SrcMode, DstMode string
SrcCommit, DstCommit string
Status CommitStatus
Src, Dst string
}
func parseGit_DiffIndexStatus(data io.ByteReader, d *GitDiffRawData) error {
b, err := data.ReadByte()
if err != nil {
return err
}
switch b {
case 'A':
d.Status = Add
case 'C':
d.Status = Copy
case 'D':
d.Status = Rm
case 'M':
d.Status = Modify
case 'R':
d.Status = Rename
case 'T':
d.Status = TypeChange
case 'U':
d.Status = Unmerged
case 'X':
return fmt.Errorf("Unexpected unknown change type. This is a git bug")
}
_, err = parseGit_StringWithSpace(data)
if err != nil {
return err
}
return nil
}
func parseSingleGitDiffIndexRawData(data io.ByteReader) (*GitDiffRawData, error) {
var ret GitDiffRawData
b, err := data.ReadByte()
if err != nil {
return nil, err
}
if b != ':' {
return nil, fmt.Errorf("Expected ':' but got '%s'", string(b))
}
if ret.SrcMode, err = parseGit_String(data); err != nil {
return nil, err
}
if ret.DstMode, err = parseGit_String(data); err != nil {
return nil, err
}
if ret.Src, err = parseGit_String(data); err != nil {
return nil, err
}
if ret.Dst, err = parseGit_String(data); err != nil {
return nil, err
}
if err = parseGit_DiffIndexStatus(data, &ret); err != nil {
return nil, err
}
ret.Dst = ret.Src
switch ret.Status {
case Copy, Rename:
if ret.Src, err = parseGit_StringWithSpace(data); err != nil {
return nil, err
}
}
return &ret, nil
}
func parseGitDiffIndexRawData(data io.ByteReader) (Data, error) {
ret := make([]GitDiffRawData, 0, 10)
for {
data, err := parseSingleGitDiffIndexRawData(data)
if err != nil {
return nil, err
} else if data == nil {
break
}
ret = append(ret, *data)
}
return ret, nil
}

View File

@@ -19,9 +19,7 @@ package common
*/ */
import ( import (
"bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -44,6 +42,11 @@ type GitDirectoryLister interface {
GitDirectoryList(gitPath, commitId string) (dirlist map[string]string, err error) GitDirectoryList(gitPath, commitId string) (dirlist map[string]string, err error)
} }
type GitSubmoduleFileConflictResolver interface {
GitResolveConflicts(cwd, MergeBase, Head, MergeHead string) error
GitResolveSubmoduleFileConflict(s GitStatusData, cwd, mergeBase, head, mergeHead string) error
}
type GitStatusLister interface { type GitStatusLister interface {
GitStatus(cwd string) ([]GitStatusData, error) GitStatus(cwd string) ([]GitStatusData, error)
} }
@@ -75,6 +78,7 @@ type Git interface {
GitExecQuietOrPanic(cwd string, params ...string) GitExecQuietOrPanic(cwd string, params ...string)
GitDiffLister GitDiffLister
GitSubmoduleFileConflictResolver
} }
type GitHandlerImpl struct { type GitHandlerImpl struct {
@@ -350,20 +354,23 @@ var ExtraGitParams []string
func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string, error) { func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string, error) {
cmd := exec.Command("/usr/bin/git", params...) cmd := exec.Command("/usr/bin/git", params...)
var identityFile string
if i := os.Getenv("AUTOGITS_IDENTITY_FILE"); len(i) > 0 {
identityFile = " -i " + i
}
cmd.Env = []string{ cmd.Env = []string{
"GIT_CEILING_DIRECTORIES=" + e.GitPath, "GIT_CEILING_DIRECTORIES=" + e.GitPath,
"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_GLOBAL=/dev/null",
"GIT_AUTHOR_NAME=" + e.GitCommiter,
"GIT_COMMITTER_NAME=" + e.GitCommiter,
"EMAIL=not@exist@src.opensuse.org",
"GIT_LFS_SKIP_SMUDGE=1", "GIT_LFS_SKIP_SMUDGE=1",
"GIT_LFS_SKIP_PUSH=1", "GIT_LFS_SKIP_PUSH=1",
"GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes" + identityFile, "GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes",
} }
if len(e.GitEmail) > 0 {
cmd.Env = append(cmd.Env, "EMAIL="+e.GitEmail)
}
if len(e.GitCommiter) > 0 {
cmd.Env = append(cmd.Env,
"GIT_AUTHOR_NAME="+e.GitCommiter,
"GIT_COMMITTER_NAME="+e.GitCommiter,
)
}
if len(ExtraGitParams) > 0 { if len(ExtraGitParams) > 0 {
cmd.Env = append(cmd.Env, ExtraGitParams...) cmd.Env = append(cmd.Env, ExtraGitParams...)
} }
@@ -1006,193 +1013,10 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
return subCommitId, len(subCommitId) > 0 return subCommitId, len(subCommitId) > 0
} }
const ( func (e *GitHandlerImpl) GitExecWithDataParse(cwd string, dataprocessor func(io.ByteReader) (Data, error), gitcmd string, args ...string) (Data, error) {
GitStatus_Untracked = 0 LogDebug("getting", gitcmd)
GitStatus_Modified = 1 args = append([]string{gitcmd}, args...)
GitStatus_Ignored = 2 cmd := exec.Command("/usr/bin/git", args...)
GitStatus_Unmerged = 3 // States[0..3] -- Stage1, Stage2, Stage3 of merge objects
GitStatus_Renamed = 4 // orig name in States[0]
)
type GitStatusData struct {
Path string
Status int
States [3]string
/*
<sub> A 4 character field describing the submodule state.
"N..." when the entry is not a submodule.
"S<c><m><u>" when the entry is a submodule.
<c> is "C" if the commit changed; otherwise ".".
<m> is "M" if it has tracked changes; otherwise ".".
<u> is "U" if there are untracked changes; otherwise ".".
*/
SubmoduleChanges string
}
func parseGitStatusHexString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 32)
for {
c, err := data.ReadByte()
if err != nil {
return "", err
}
switch {
case c == 0 || c == ' ':
return string(str), nil
case c >= 'a' && c <= 'f':
case c >= 'A' && c <= 'F':
case c >= '0' && c <= '9':
default:
return "", errors.New("Invalid character in hex string:" + string(c))
}
str = append(str, c)
}
}
func parseGitStatusString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 100)
for {
c, err := data.ReadByte()
if err != nil {
return "", errors.New("Unexpected EOF. Expected NUL string term")
}
if c == 0 || c == ' ' {
return string(str), nil
}
str = append(str, c)
}
}
func parseGitStatusStringWithSpace(data io.ByteReader) (string, error) {
str := make([]byte, 0, 100)
for {
c, err := data.ReadByte()
if err != nil {
return "", errors.New("Unexpected EOF. Expected NUL string term")
}
if c == 0 {
return string(str), nil
}
str = append(str, c)
}
}
func skipGitStatusEntry(data io.ByteReader, skipSpaceLen int) error {
for skipSpaceLen > 0 {
c, err := data.ReadByte()
if err != nil {
return err
}
if c == ' ' {
skipSpaceLen--
}
}
return nil
}
func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
ret := GitStatusData{}
statusType, err := data.ReadByte()
if err != nil {
return nil, nil
}
switch statusType {
case '1':
var err error
if err = skipGitStatusEntry(data, 8); err != nil {
return nil, err
}
ret.Status = GitStatus_Modified
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
case '2':
var err error
if err = skipGitStatusEntry(data, 9); err != nil {
return nil, err
}
ret.Status = GitStatus_Renamed
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
ret.States[0], err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
case '?':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Untracked
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
case '!':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Ignored
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
case 'u':
var err error
if err = skipGitStatusEntry(data, 2); err != nil {
return nil, err
}
if ret.SubmoduleChanges, err = parseGitStatusString(data); err != nil {
return nil, err
}
if err = skipGitStatusEntry(data, 4); err != nil {
return nil, err
}
if ret.States[0], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
if ret.States[1], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
if ret.States[2], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
ret.Status = GitStatus_Unmerged
ret.Path, err = parseGitStatusStringWithSpace(data)
if err != nil {
return nil, err
}
default:
return nil, errors.New("Invalid status type" + string(statusType))
}
return &ret, nil
}
func parseGitStatusData(data io.ByteReader) ([]GitStatusData, error) {
ret := make([]GitStatusData, 0, 10)
for {
data, err := parseSingleStatusEntry(data)
if err != nil {
return nil, err
} else if data == nil {
break
}
ret = append(ret, *data)
}
return ret, nil
}
func (e *GitHandlerImpl) GitStatus(cwd string) (ret []GitStatusData, err error) {
LogDebug("getting git-status()")
cmd := exec.Command("/usr/bin/git", "status", "--porcelain=2", "-z")
cmd.Env = []string{ cmd.Env = []string{
"GIT_CEILING_DIRECTORIES=" + e.GitPath, "GIT_CEILING_DIRECTORIES=" + e.GitPath,
"GIT_LFS_SKIP_SMUDGE=1", "GIT_LFS_SKIP_SMUDGE=1",
@@ -1209,7 +1033,12 @@ func (e *GitHandlerImpl) GitStatus(cwd string) (ret []GitStatusData, err error)
LogError("Error running command", cmd.Args, err) LogError("Error running command", cmd.Args, err)
} }
return parseGitStatusData(bufio.NewReader(bytes.NewReader(out))) return dataprocessor(bytes.NewReader(out))
}
func (e *GitHandlerImpl) GitStatus(cwd string) (ret []GitStatusData, err error) {
data, err := e.GitExecWithDataParse(cwd, parseGitStatusData, "status", "--porcelain=2", "-z")
return data.([]GitStatusData), err
} }
func (e *GitHandlerImpl) GitDiff(cwd, base, head string) (string, error) { func (e *GitHandlerImpl) GitDiff(cwd, base, head string) (string, error) {
@@ -1234,3 +1063,122 @@ func (e *GitHandlerImpl) GitDiff(cwd, base, head string) (string, error) {
return string(out), nil return string(out), nil
} }
func (e *GitHandlerImpl) GitDiffIndex(cwd, commit string) ([]GitDiffRawData, error) {
data, err := e.GitExecWithDataParse("diff-index", parseGitDiffIndexRawData, cwd, "diff-index", "-z", "--raw", "--full-index", "--submodule=short", "HEAD")
return data.([]GitDiffRawData), err
}
func (git *GitHandlerImpl) GitResolveConflicts(cwd, mergeBase, head, mergeHead string) error {
status, err := git.GitStatus(cwd)
if err != nil {
return fmt.Errorf("Status failed: %w", err)
}
// we can only resolve conflicts with .gitmodules
for _, s := range status {
if s.Status == GitStatus_Unmerged && s.Path == ".gitmodules" {
if err := git.GitResolveSubmoduleFileConflict(s, cwd, mergeBase, head, mergeHead); err != nil {
return err
}
} else if s.Status == GitStatus_Unmerged {
return fmt.Errorf("Cannot automatically resolve conflict: %s", s.Path)
}
}
return git.GitExec(cwd, "-c", "core.editor=true", "merge", "--continue")
}
func (git *GitHandlerImpl) GitResolveSubmoduleFileConflict(s GitStatusData, cwd, mergeBase, head, mergeHead string) error {
submodules1, err := git.GitSubmoduleList(cwd, mergeBase)
if err != nil {
return fmt.Errorf("Failed to fetch submodules during merge resolution: %w", err)
}
/*
submodules2, err := git.GitSubmoduleList(cwd, head)
if err != nil {
return fmt.Errorf("Failed to fetch submodules during merge resolution: %w", err)
}
*/
submodules3, err := git.GitSubmoduleList(cwd, mergeHead)
if err != nil {
return fmt.Errorf("Failed to fetch submodules during merge resolution: %w", err)
}
// find modified submodules in the mergeHead
modifiedSubmodules := make([]string, 0, 10)
removedSubmodules := make([]string, 0, 10)
addedSubmodules := make([]string, 0, 10)
for submodulePath, oldHash := range submodules1 {
if newHash, found := submodules3[submodulePath]; found && newHash != oldHash {
modifiedSubmodules = append(modifiedSubmodules, submodulePath)
} else if !found {
removedSubmodules = append(removedSubmodules, submodulePath)
}
}
for submodulePath, _ := range submodules3 {
if _, found := submodules1[submodulePath]; !found {
addedSubmodules = append(addedSubmodules, submodulePath)
}
}
// We need to adjust the `submodules` list by the pending changes to the index
s1, err := git.GitExecWithOutput(cwd, "cat-file", "blob", s.States[0])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
s2, err := git.GitExecWithOutput(cwd, "cat-file", "blob", s.States[1])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
s3, err := git.GitExecWithOutput(cwd, "cat-file", "blob", s.States[2])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
_, err = ParseSubmodulesFile(strings.NewReader(s1))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
subs2, err := ParseSubmodulesFile(strings.NewReader(s2))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
subs3, err := ParseSubmodulesFile(strings.NewReader(s3))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
overrideSet := make([]Submodule, 0, len(addedSubmodules)+len(modifiedSubmodules))
for i := range subs3 {
if slices.Contains(addedSubmodules, subs3[i].Path) || slices.Contains(modifiedSubmodules, subs3[i].Path) {
overrideSet = append(overrideSet, subs3[i])
}
}
// merge from subs1 (merge-base), subs2 (changes in base since merge-base HEAD), subs3 (merge_source MERGE_HEAD)
// this will update submodules
SubmoduleCompare := func(a, b Submodule) int { return strings.Compare(a.Path, b.Path) }
CompactCompare := func(a, b Submodule) bool { return a.Path == b.Path }
// remove submodules that are removed in the PR
subs2 = slices.DeleteFunc(subs2, func(a Submodule) bool { return slices.Contains(removedSubmodules, a.Path) })
mergedSubs := slices.Concat(overrideSet, subs2)
slices.SortStableFunc(mergedSubs, SubmoduleCompare)
filteredSubs := slices.CompactFunc(mergedSubs, CompactCompare)
out, err := os.Create(path.Join(git.GetPath(), cwd, ".gitmodules"))
if err != nil {
return fmt.Errorf("Can't open .gitmodules for writing: %w", err)
}
if err = WriteSubmodules(filteredSubs, out); err != nil {
return fmt.Errorf("Can't write .gitmodules: %w", err)
}
if out.Close(); err != nil {
return fmt.Errorf("Can't close .gitmodules: %w", err)
}
git.GitExecOrPanic(cwd, "add", ".gitmodules")
return nil
}

View File

@@ -24,6 +24,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"runtime/debug"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -93,6 +94,145 @@ func TestGitClone(t *testing.T) {
} }
} }
func TestSubmoduleConflictResolution(t *testing.T) {
tests := []struct {
name string
checkout, merge string
result string
merge_fail bool
}{
{
name: "adding two submodules",
checkout: "base_add_b1",
merge: "base_add_b2",
result: `[submodule "pkgA"]
path = pkgA
url = ../pkgA
[submodule "pkgB"]
path = pkgB
url = ../pkgB
[submodule "pkgB1"]
path = pkgB1
url = ../pkgB1
[submodule "pkgB2"]
path = pkgB2
url = ../pkgB2
[submodule "pkgC"]
path = pkgC
url = ../pkgC
`,
},
{
name: "remove one module and add another",
checkout: "base_rm_c",
merge: "base_add_b2",
result: `[submodule "pkgA"]
path = pkgA
url = ../pkgA
[submodule "pkgB"]
path = pkgB
url = ../pkgB
[submodule "pkgB2"]
path = pkgB2
url = ../pkgB2
`,
},
{
name: "add one and remove another",
checkout: "base_add_b2",
merge: "base_rm_c",
result: `[submodule "pkgA"]
path = pkgA
url = ../pkgA
[submodule "pkgB"]
path = pkgB
url = ../pkgB
[submodule "pkgB2"]
path = pkgB2
url = ../pkgB2
`,
},
{
name: "rm modified submodule",
checkout: "base_modify_c",
merge: "base_rm_c",
merge_fail: true,
},
{
name: "modified removed submodule",
checkout: "base_rm_c",
merge: "base_modify_c",
merge_fail: true,
},
}
d, err := os.MkdirTemp(os.TempDir(), "submoduletests")
if err != nil {
t.Fatal(err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(cwd + "/test_repo_setup.sh")
cmd.Dir = d
_, err = cmd.Output()
if err != nil {
t.Fatal(err)
}
gh, err := AllocateGitWorkTree(d, "test", "foo@example.com")
if err != nil {
t.Fatal(err)
}
success := true
noErrorOrFail := func(t *testing.T, err error) {
if err != nil {
t.Fatal(string(debug.Stack()), err)
}
}
for _, test := range tests {
success = t.Run(test.name, func(t *testing.T) {
git, err := gh.ReadExistingPath("prjgit")
defer git.Close()
if err != nil {
t.Fatal(err)
}
noErrorOrFail(t, git.GitExec("", "reset", "--hard"))
noErrorOrFail(t, git.GitExec("", "checkout", "-B", "test", test.checkout))
// noErrorOrFail(t, git.GitExec("", "merge", test.checkout))
err = git.GitExec("", "merge", test.merge)
if err == nil {
t.Fatal("expected a conflict")
}
err = git.GitResolveConflicts("", "main", test.checkout, test.merge)
if err != nil {
if test.merge_fail {
return // success
}
t.Fatal(err)
}
if test.merge_fail {
t.Fatal("Expected fail but succeeded?")
}
data, err := os.ReadFile(git.GetPath() + "/.gitmodules")
if err != nil {
t.Fatal("Cannot read .gitmodules.", err)
}
if string(data) != test.result {
t.Error("Expected", len(test.result), test.result, "but have", len(data), string(data))
}
}) && success
}
if success {
os.RemoveAll(d)
}
}
func TestGitMsgParsing(t *testing.T) { func TestGitMsgParsing(t *testing.T) {
t.Run("tree message with size 56", func(t *testing.T) { t.Run("tree message with size 56", func(t *testing.T) {
const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00" const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00"
@@ -584,12 +724,15 @@ func TestGitStatusParse(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(r) != len(test.res) {
t.Fatal("len(r):", len(r), "is not expected", len(test.res)) res := r.([]GitStatusData)
if len(res) != len(test.res) {
t.Fatal("len(r):", len(res), "is not expected", len(test.res))
} }
for _, expected := range test.res { for _, expected := range test.res {
if !slices.Contains(r, expected) { if !slices.Contains(res, expected) {
t.Fatal("result", r, "doesn't contains expected", expected) t.Fatal("result", r, "doesn't contains expected", expected)
} }
} }

View File

@@ -29,7 +29,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"slices" "slices"
"sync"
"time" "time"
transport "github.com/go-openapi/runtime/client" transport "github.com/go-openapi/runtime/client"
@@ -67,14 +66,6 @@ const (
ReviewStateUnknown models.ReviewStateType = "" ReviewStateUnknown models.ReviewStateType = ""
) )
type GiteaLabelGetter interface {
GetLabels(org, repo string, idx int64) ([]*models.Label, error)
}
type GiteaLabelSettter interface {
SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error)
}
type GiteaTimelineFetcher interface { type GiteaTimelineFetcher interface {
GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error)
} }
@@ -100,10 +91,9 @@ type GiteaPRUpdater interface {
UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error) UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error)
} }
type GiteaPRTimelineReviewFetcher interface { type GiteaPRTimelineFetcher interface {
GiteaPRFetcher GiteaPRFetcher
GiteaTimelineFetcher GiteaTimelineFetcher
GiteaReviewFetcher
} }
type GiteaCommitFetcher interface { type GiteaCommitFetcher interface {
@@ -129,16 +119,10 @@ type GiteaPRChecker interface {
GiteaMaintainershipReader GiteaMaintainershipReader
} }
type GiteaReviewFetcherAndRequesterAndUnrequester interface { type GiteaReviewFetcherAndRequester interface {
GiteaReviewTimelineFetcher GiteaReviewTimelineFetcher
GiteaCommentFetcher GiteaCommentFetcher
GiteaReviewRequester GiteaReviewRequester
GiteaReviewUnrequester
}
type GiteaUnreviewTimelineFetcher interface {
GiteaTimelineFetcher
GiteaReviewUnrequester
} }
type GiteaReviewRequester interface { type GiteaReviewRequester interface {
@@ -198,8 +182,6 @@ type Gitea interface {
GiteaCommitStatusGetter GiteaCommitStatusGetter
GiteaCommitStatusSetter GiteaCommitStatusSetter
GiteaSetRepoOptions GiteaSetRepoOptions
GiteaLabelGetter
GiteaLabelSettter
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
@@ -207,7 +189,7 @@ type Gitea interface {
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, bool) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*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)
@@ -484,30 +466,6 @@ func (gitea *GiteaTransport) SetRepoOptions(owner, repo string, manual_merge boo
return ok.Payload, err return ok.Payload, err
} }
func (gitea *GiteaTransport) GetLabels(owner, repo string, idx int64) ([]*models.Label, error) {
ret, err := gitea.client.Issue.IssueGetLabels(issue.NewIssueGetLabelsParams().WithOwner(owner).WithRepo(repo).WithIndex(idx), gitea.transport.DefaultAuthentication)
if err != nil {
return nil, err
}
return ret.Payload, err
}
func (gitea *GiteaTransport) SetLabels(owner, repo string, idx int64, labels []string) ([]*models.Label, error) {
interfaceLabels := make([]interface{}, len(labels))
for i, l := range labels {
interfaceLabels[i] = l
}
ret, err := gitea.client.Issue.IssueAddLabel(issue.NewIssueAddLabelParams().WithOwner(owner).WithRepo(repo).WithIndex(idx).WithBody(&models.IssueLabelsOption{Labels: interfaceLabels}),
gitea.transport.DefaultAuthentication)
if err != nil {
return nil, err
}
return ret.Payload, nil
}
const ( const (
GiteaNotificationType_Pull = "Pull" GiteaNotificationType_Pull = "Pull"
) )
@@ -685,7 +643,7 @@ func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git Git, org, repoName s
return repo.Payload, nil return repo.Payload, nil
} }
func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error, bool) { func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
prOptions := models.CreatePullRequestOption{ prOptions := models.CreatePullRequestOption{
Base: targetId, Base: targetId,
Head: srcId, Head: srcId,
@@ -701,7 +659,7 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
WithHead(srcId), WithHead(srcId),
gitea.transport.DefaultAuthentication, gitea.transport.DefaultAuthentication,
); err == nil && pr.Payload.State == "open" { ); err == nil && pr.Payload.State == "open" {
return pr.Payload, nil, false return pr.Payload, nil
} }
pr, err := gitea.client.Repository.RepoCreatePullRequest( pr, err := gitea.client.Repository.RepoCreatePullRequest(
@@ -715,10 +673,10 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("Cannot create pull request. %w", err), true return nil, fmt.Errorf("Cannot create pull request. %w", err)
} }
return pr.GetPayload(), nil, true return pr.GetPayload(), 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) {
@@ -805,79 +763,45 @@ func (gitea *GiteaTransport) AddComment(pr *models.PullRequest, comment string)
return nil return nil
} }
type TimelineCacheData struct {
data []*models.TimelineComment
lastCheck time.Time
}
var giteaTimelineCache map[string]TimelineCacheData = make(map[string]TimelineCacheData)
var giteaTimelineCacheMutex sync.RWMutex
// returns timeline in reverse chronological create order
func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) { func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
page := int64(1) page := int64(1)
resCount := 1 resCount := 1
prID := fmt.Sprintf("%s/%s!%d", org, repo, idx) retData := []*models.TimelineComment{}
giteaTimelineCacheMutex.RLock()
TimelineCache, IsCached := giteaTimelineCache[prID]
var LastCachedTime strfmt.DateTime
if IsCached {
l := len(TimelineCache.data)
if l > 0 {
LastCachedTime = TimelineCache.data[0].Updated
}
// cache data for 5 seconds
if TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
giteaTimelineCacheMutex.RUnlock()
return TimelineCache.data, nil
}
}
giteaTimelineCacheMutex.RUnlock()
giteaTimelineCacheMutex.Lock()
defer giteaTimelineCacheMutex.Unlock()
for resCount > 0 { for resCount > 0 {
opts := issue.NewIssueGetCommentsAndTimelineParams().WithOwner(org).WithRepo(repo).WithIndex(idx).WithPage(&page) res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(
if !LastCachedTime.IsZero() { issue.NewIssueGetCommentsAndTimelineParams().
opts = opts.WithSince(&LastCachedTime) WithOwner(org).
} WithRepo(repo).
res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(opts, gitea.transport.DefaultAuthentication) WithIndex(idx).
WithPage(&page),
gitea.transport.DefaultAuthentication,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resCount = len(res.Payload); resCount == 0 { resCount = len(res.Payload)
break LogDebug("page:", page, "len:", resCount)
} if resCount == 0 {
for _, d := range res.Payload {
if d != nil {
if time.Time(d.Created).Compare(time.Time(LastCachedTime)) > 0 {
// created after last check, so we append here
TimelineCache.data = append(TimelineCache.data, d)
} else {
// we need something updated in the timeline, maybe
}
}
}
if resCount < 10 {
break break
} }
page++ page++
for _, d := range res.Payload {
if d != nil {
retData = append(retData, d)
}
}
} }
LogDebug("timeline", prID, "# timeline:", len(TimelineCache.data)) LogDebug("total results:", len(retData))
slices.SortFunc(TimelineCache.data, func(a, b *models.TimelineComment) int { slices.SortFunc(retData, func(a, b *models.TimelineComment) int {
return time.Time(b.Created).Compare(time.Time(a.Created)) return time.Time(b.Created).Compare(time.Time(a.Created))
}) })
TimelineCache.lastCheck = time.Now() return retData, nil
giteaTimelineCache[prID] = TimelineCache
return TimelineCache.data, nil
} }
func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error) { func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error) {

View File

@@ -18,132 +18,6 @@ import (
models "src.opensuse.org/autogits/common/gitea-generated/models" models "src.opensuse.org/autogits/common/gitea-generated/models"
) )
// MockGiteaLabelGetter is a mock of GiteaLabelGetter interface.
type MockGiteaLabelGetter struct {
ctrl *gomock.Controller
recorder *MockGiteaLabelGetterMockRecorder
isgomock struct{}
}
// MockGiteaLabelGetterMockRecorder is the mock recorder for MockGiteaLabelGetter.
type MockGiteaLabelGetterMockRecorder struct {
mock *MockGiteaLabelGetter
}
// NewMockGiteaLabelGetter creates a new mock instance.
func NewMockGiteaLabelGetter(ctrl *gomock.Controller) *MockGiteaLabelGetter {
mock := &MockGiteaLabelGetter{ctrl: ctrl}
mock.recorder = &MockGiteaLabelGetterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGiteaLabelGetter) EXPECT() *MockGiteaLabelGetterMockRecorder {
return m.recorder
}
// GetLabels mocks base method.
func (m *MockGiteaLabelGetter) GetLabels(org, repo string, idx int64) ([]*models.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLabels", org, repo, idx)
ret0, _ := ret[0].([]*models.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLabels indicates an expected call of GetLabels.
func (mr *MockGiteaLabelGetterMockRecorder) GetLabels(org, repo, idx any) *MockGiteaLabelGetterGetLabelsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLabels", reflect.TypeOf((*MockGiteaLabelGetter)(nil).GetLabels), org, repo, idx)
return &MockGiteaLabelGetterGetLabelsCall{Call: call}
}
// MockGiteaLabelGetterGetLabelsCall wrap *gomock.Call
type MockGiteaLabelGetterGetLabelsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaLabelGetterGetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaLabelGetterGetLabelsCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaLabelGetterGetLabelsCall) Do(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaLabelGetterGetLabelsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaLabelGetterGetLabelsCall) DoAndReturn(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaLabelGetterGetLabelsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockGiteaLabelSettter is a mock of GiteaLabelSettter interface.
type MockGiteaLabelSettter struct {
ctrl *gomock.Controller
recorder *MockGiteaLabelSettterMockRecorder
isgomock struct{}
}
// MockGiteaLabelSettterMockRecorder is the mock recorder for MockGiteaLabelSettter.
type MockGiteaLabelSettterMockRecorder struct {
mock *MockGiteaLabelSettter
}
// NewMockGiteaLabelSettter creates a new mock instance.
func NewMockGiteaLabelSettter(ctrl *gomock.Controller) *MockGiteaLabelSettter {
mock := &MockGiteaLabelSettter{ctrl: ctrl}
mock.recorder = &MockGiteaLabelSettterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGiteaLabelSettter) EXPECT() *MockGiteaLabelSettterMockRecorder {
return m.recorder
}
// SetLabels mocks base method.
func (m *MockGiteaLabelSettter) SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLabels", org, repo, idx, labels)
ret0, _ := ret[0].([]*models.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetLabels indicates an expected call of SetLabels.
func (mr *MockGiteaLabelSettterMockRecorder) SetLabels(org, repo, idx, labels any) *MockGiteaLabelSettterSetLabelsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLabels", reflect.TypeOf((*MockGiteaLabelSettter)(nil).SetLabels), org, repo, idx, labels)
return &MockGiteaLabelSettterSetLabelsCall{Call: call}
}
// MockGiteaLabelSettterSetLabelsCall wrap *gomock.Call
type MockGiteaLabelSettterSetLabelsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaLabelSettterSetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaLabelSettterSetLabelsCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaLabelSettterSetLabelsCall) Do(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaLabelSettterSetLabelsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaLabelSettterSetLabelsCall) DoAndReturn(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaLabelSettterSetLabelsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockGiteaTimelineFetcher is a mock of GiteaTimelineFetcher interface. // MockGiteaTimelineFetcher is a mock of GiteaTimelineFetcher interface.
type MockGiteaTimelineFetcher struct { type MockGiteaTimelineFetcher struct {
ctrl *gomock.Controller ctrl *gomock.Controller
@@ -562,32 +436,32 @@ func (c *MockGiteaPRUpdaterUpdatePullRequestCall) DoAndReturn(f func(string, str
return c return c
} }
// MockGiteaPRTimelineReviewFetcher is a mock of GiteaPRTimelineReviewFetcher interface. // MockGiteaPRTimelineFetcher is a mock of GiteaPRTimelineFetcher interface.
type MockGiteaPRTimelineReviewFetcher struct { type MockGiteaPRTimelineFetcher struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockGiteaPRTimelineReviewFetcherMockRecorder recorder *MockGiteaPRTimelineFetcherMockRecorder
isgomock struct{} isgomock struct{}
} }
// MockGiteaPRTimelineReviewFetcherMockRecorder is the mock recorder for MockGiteaPRTimelineReviewFetcher. // MockGiteaPRTimelineFetcherMockRecorder is the mock recorder for MockGiteaPRTimelineFetcher.
type MockGiteaPRTimelineReviewFetcherMockRecorder struct { type MockGiteaPRTimelineFetcherMockRecorder struct {
mock *MockGiteaPRTimelineReviewFetcher mock *MockGiteaPRTimelineFetcher
} }
// NewMockGiteaPRTimelineReviewFetcher creates a new mock instance. // NewMockGiteaPRTimelineFetcher creates a new mock instance.
func NewMockGiteaPRTimelineReviewFetcher(ctrl *gomock.Controller) *MockGiteaPRTimelineReviewFetcher { func NewMockGiteaPRTimelineFetcher(ctrl *gomock.Controller) *MockGiteaPRTimelineFetcher {
mock := &MockGiteaPRTimelineReviewFetcher{ctrl: ctrl} mock := &MockGiteaPRTimelineFetcher{ctrl: ctrl}
mock.recorder = &MockGiteaPRTimelineReviewFetcherMockRecorder{mock} mock.recorder = &MockGiteaPRTimelineFetcherMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGiteaPRTimelineReviewFetcher) EXPECT() *MockGiteaPRTimelineReviewFetcherMockRecorder { func (m *MockGiteaPRTimelineFetcher) EXPECT() *MockGiteaPRTimelineFetcherMockRecorder {
return m.recorder return m.recorder
} }
// GetPullRequest mocks base method. // GetPullRequest mocks base method.
func (m *MockGiteaPRTimelineReviewFetcher) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) { func (m *MockGiteaPRTimelineFetcher) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPullRequest", org, project, num) ret := m.ctrl.Call(m, "GetPullRequest", org, project, num)
ret0, _ := ret[0].(*models.PullRequest) ret0, _ := ret[0].(*models.PullRequest)
@@ -596,76 +470,37 @@ func (m *MockGiteaPRTimelineReviewFetcher) GetPullRequest(org, project string, n
} }
// GetPullRequest indicates an expected call of GetPullRequest. // GetPullRequest indicates an expected call of GetPullRequest.
func (mr *MockGiteaPRTimelineReviewFetcherMockRecorder) GetPullRequest(org, project, num any) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall { func (mr *MockGiteaPRTimelineFetcherMockRecorder) GetPullRequest(org, project, num any) *MockGiteaPRTimelineFetcherGetPullRequestCall {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequest", reflect.TypeOf((*MockGiteaPRTimelineReviewFetcher)(nil).GetPullRequest), org, project, num) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequest", reflect.TypeOf((*MockGiteaPRTimelineFetcher)(nil).GetPullRequest), org, project, num)
return &MockGiteaPRTimelineReviewFetcherGetPullRequestCall{Call: call} return &MockGiteaPRTimelineFetcherGetPullRequestCall{Call: call}
} }
// MockGiteaPRTimelineReviewFetcherGetPullRequestCall wrap *gomock.Call // MockGiteaPRTimelineFetcherGetPullRequestCall wrap *gomock.Call
type MockGiteaPRTimelineReviewFetcherGetPullRequestCall struct { type MockGiteaPRTimelineFetcherGetPullRequestCall struct {
*gomock.Call *gomock.Call
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestCall) Return(arg0 *models.PullRequest, arg1 error) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall { func (c *MockGiteaPRTimelineFetcherGetPullRequestCall) Return(arg0 *models.PullRequest, arg1 error) *MockGiteaPRTimelineFetcherGetPullRequestCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestCall) Do(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall { func (c *MockGiteaPRTimelineFetcherGetPullRequestCall) Do(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineFetcherGetPullRequestCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestCall) DoAndReturn(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestCall { func (c *MockGiteaPRTimelineFetcherGetPullRequestCall) DoAndReturn(f func(string, string, int64) (*models.PullRequest, error)) *MockGiteaPRTimelineFetcherGetPullRequestCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GetPullRequestReviews mocks base method.
func (m *MockGiteaPRTimelineReviewFetcher) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPullRequestReviews", org, project, PRnum)
ret0, _ := ret[0].([]*models.PullReview)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPullRequestReviews indicates an expected call of GetPullRequestReviews.
func (mr *MockGiteaPRTimelineReviewFetcherMockRecorder) GetPullRequestReviews(org, project, PRnum any) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequestReviews", reflect.TypeOf((*MockGiteaPRTimelineReviewFetcher)(nil).GetPullRequestReviews), org, project, PRnum)
return &MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall{Call: call}
}
// MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall wrap *gomock.Call
type MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall) Do(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall) DoAndReturn(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaPRTimelineReviewFetcherGetPullRequestReviewsCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
// GetTimeline mocks base method. // GetTimeline mocks base method.
func (m *MockGiteaPRTimelineReviewFetcher) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) { func (m *MockGiteaPRTimelineFetcher) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx) ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx)
ret0, _ := ret[0].([]*models.TimelineComment) ret0, _ := ret[0].([]*models.TimelineComment)
@@ -674,31 +509,31 @@ func (m *MockGiteaPRTimelineReviewFetcher) GetTimeline(org, repo string, idx int
} }
// GetTimeline indicates an expected call of GetTimeline. // GetTimeline indicates an expected call of GetTimeline.
func (mr *MockGiteaPRTimelineReviewFetcherMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaPRTimelineReviewFetcherGetTimelineCall { func (mr *MockGiteaPRTimelineFetcherMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaPRTimelineFetcherGetTimelineCall {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaPRTimelineReviewFetcher)(nil).GetTimeline), org, repo, idx) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaPRTimelineFetcher)(nil).GetTimeline), org, repo, idx)
return &MockGiteaPRTimelineReviewFetcherGetTimelineCall{Call: call} return &MockGiteaPRTimelineFetcherGetTimelineCall{Call: call}
} }
// MockGiteaPRTimelineReviewFetcherGetTimelineCall wrap *gomock.Call // MockGiteaPRTimelineFetcherGetTimelineCall wrap *gomock.Call
type MockGiteaPRTimelineReviewFetcherGetTimelineCall struct { type MockGiteaPRTimelineFetcherGetTimelineCall struct {
*gomock.Call *gomock.Call
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockGiteaPRTimelineReviewFetcherGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaPRTimelineReviewFetcherGetTimelineCall { func (c *MockGiteaPRTimelineFetcherGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaPRTimelineFetcherGetTimelineCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockGiteaPRTimelineReviewFetcherGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineReviewFetcherGetTimelineCall { func (c *MockGiteaPRTimelineFetcherGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineFetcherGetTimelineCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaPRTimelineReviewFetcherGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineReviewFetcherGetTimelineCall { func (c *MockGiteaPRTimelineFetcherGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaPRTimelineFetcherGetTimelineCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
@@ -1215,32 +1050,32 @@ func (c *MockGiteaPRCheckerGetTimelineCall) DoAndReturn(f func(string, string, i
return c return c
} }
// MockGiteaReviewFetcherAndRequesterAndUnrequester is a mock of GiteaReviewFetcherAndRequesterAndUnrequester interface. // MockGiteaReviewFetcherAndRequester is a mock of GiteaReviewFetcherAndRequester interface.
type MockGiteaReviewFetcherAndRequesterAndUnrequester struct { type MockGiteaReviewFetcherAndRequester struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder recorder *MockGiteaReviewFetcherAndRequesterMockRecorder
isgomock struct{} isgomock struct{}
} }
// MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder is the mock recorder for MockGiteaReviewFetcherAndRequesterAndUnrequester. // MockGiteaReviewFetcherAndRequesterMockRecorder is the mock recorder for MockGiteaReviewFetcherAndRequester.
type MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder struct { type MockGiteaReviewFetcherAndRequesterMockRecorder struct {
mock *MockGiteaReviewFetcherAndRequesterAndUnrequester mock *MockGiteaReviewFetcherAndRequester
} }
// NewMockGiteaReviewFetcherAndRequesterAndUnrequester creates a new mock instance. // NewMockGiteaReviewFetcherAndRequester creates a new mock instance.
func NewMockGiteaReviewFetcherAndRequesterAndUnrequester(ctrl *gomock.Controller) *MockGiteaReviewFetcherAndRequesterAndUnrequester { func NewMockGiteaReviewFetcherAndRequester(ctrl *gomock.Controller) *MockGiteaReviewFetcherAndRequester {
mock := &MockGiteaReviewFetcherAndRequesterAndUnrequester{ctrl: ctrl} mock := &MockGiteaReviewFetcherAndRequester{ctrl: ctrl}
mock.recorder = &MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder{mock} mock.recorder = &MockGiteaReviewFetcherAndRequesterMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) EXPECT() *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder { func (m *MockGiteaReviewFetcherAndRequester) EXPECT() *MockGiteaReviewFetcherAndRequesterMockRecorder {
return m.recorder return m.recorder
} }
// GetIssueComments mocks base method. // GetIssueComments mocks base method.
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error) { func (m *MockGiteaReviewFetcherAndRequester) GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIssueComments", org, project, issueNo) ret := m.ctrl.Call(m, "GetIssueComments", org, project, issueNo)
ret0, _ := ret[0].([]*models.Comment) ret0, _ := ret[0].([]*models.Comment)
@@ -1249,37 +1084,37 @@ func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetIssueComments(org,
} }
// GetIssueComments indicates an expected call of GetIssueComments. // GetIssueComments indicates an expected call of GetIssueComments.
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) GetIssueComments(org, project, issueNo any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall { func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) GetIssueComments(org, project, issueNo any) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIssueComments", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).GetIssueComments), org, project, issueNo) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIssueComments", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).GetIssueComments), org, project, issueNo)
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall{Call: call} return &MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall{Call: call}
} }
// MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall wrap *gomock.Call // MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall wrap *gomock.Call
type MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall struct { type MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall struct {
*gomock.Call *gomock.Call
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall) Return(arg0 []*models.Comment, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall { func (c *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall) Return(arg0 []*models.Comment, arg1 error) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall) Do(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall { func (c *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall) Do(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall) DoAndReturn(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetIssueCommentsCall { func (c *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall) DoAndReturn(f func(string, string, int64) ([]*models.Comment, error)) *MockGiteaReviewFetcherAndRequesterGetIssueCommentsCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
// GetPullRequestReviews mocks base method. // GetPullRequestReviews mocks base method.
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) { func (m *MockGiteaReviewFetcherAndRequester) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPullRequestReviews", org, project, PRnum) ret := m.ctrl.Call(m, "GetPullRequestReviews", org, project, PRnum)
ret0, _ := ret[0].([]*models.PullReview) ret0, _ := ret[0].([]*models.PullReview)
@@ -1288,37 +1123,37 @@ func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetPullRequestReviews
} }
// GetPullRequestReviews indicates an expected call of GetPullRequestReviews. // GetPullRequestReviews indicates an expected call of GetPullRequestReviews.
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) GetPullRequestReviews(org, project, PRnum any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall { func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) GetPullRequestReviews(org, project, PRnum any) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).GetPullRequestReviews), org, project, PRnum) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).GetPullRequestReviews), org, project, PRnum)
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall{Call: call} return &MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall{Call: call}
} }
// MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall wrap *gomock.Call // MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall wrap *gomock.Call
type MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall struct { type MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall struct {
*gomock.Call *gomock.Call
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall { func (c *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall) Do(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall { func (c *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall) Do(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall) DoAndReturn(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetPullRequestReviewsCall { func (c *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall) DoAndReturn(f func(string, string, int64) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterGetPullRequestReviewsCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
// GetTimeline mocks base method. // GetTimeline mocks base method.
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) { func (m *MockGiteaReviewFetcherAndRequester) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx) ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx)
ret0, _ := ret[0].([]*models.TimelineComment) ret0, _ := ret[0].([]*models.TimelineComment)
@@ -1327,37 +1162,37 @@ func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) GetTimeline(org, repo
} }
// GetTimeline indicates an expected call of GetTimeline. // GetTimeline indicates an expected call of GetTimeline.
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall { func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).GetTimeline), org, repo, idx) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).GetTimeline), org, repo, idx)
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall{Call: call} return &MockGiteaReviewFetcherAndRequesterGetTimelineCall{Call: call}
} }
// MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall wrap *gomock.Call // MockGiteaReviewFetcherAndRequesterGetTimelineCall wrap *gomock.Call
type MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall struct { type MockGiteaReviewFetcherAndRequesterGetTimelineCall struct {
*gomock.Call *gomock.Call
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall { func (c *MockGiteaReviewFetcherAndRequesterGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall { func (c *MockGiteaReviewFetcherAndRequesterGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterGetTimelineCall { func (c *MockGiteaReviewFetcherAndRequesterGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaReviewFetcherAndRequesterGetTimelineCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
// RequestReviews mocks base method. // RequestReviews mocks base method.
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) RequestReviews(pr *models.PullRequest, reviewer ...string) ([]*models.PullReview, error) { func (m *MockGiteaReviewFetcherAndRequester) RequestReviews(pr *models.PullRequest, reviewer ...string) ([]*models.PullReview, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []any{pr} varargs := []any{pr}
for _, a := range reviewer { for _, a := range reviewer {
@@ -1370,181 +1205,32 @@ func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) RequestReviews(pr *mo
} }
// RequestReviews indicates an expected call of RequestReviews. // RequestReviews indicates an expected call of RequestReviews.
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) RequestReviews(pr any, reviewer ...any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall { func (mr *MockGiteaReviewFetcherAndRequesterMockRecorder) RequestReviews(pr any, reviewer ...any) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
varargs := append([]any{pr}, reviewer...) varargs := append([]any{pr}, reviewer...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).RequestReviews), varargs...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestReviews", reflect.TypeOf((*MockGiteaReviewFetcherAndRequester)(nil).RequestReviews), varargs...)
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall{Call: call} return &MockGiteaReviewFetcherAndRequesterRequestReviewsCall{Call: call}
} }
// MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall wrap *gomock.Call // MockGiteaReviewFetcherAndRequesterRequestReviewsCall wrap *gomock.Call
type MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall struct { type MockGiteaReviewFetcherAndRequesterRequestReviewsCall struct {
*gomock.Call *gomock.Call
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall { func (c *MockGiteaReviewFetcherAndRequesterRequestReviewsCall) Return(arg0 []*models.PullReview, arg1 error) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall) Do(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall { func (c *MockGiteaReviewFetcherAndRequesterRequestReviewsCall) Do(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall) DoAndReturn(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterRequestReviewsCall { func (c *MockGiteaReviewFetcherAndRequesterRequestReviewsCall) DoAndReturn(f func(*models.PullRequest, ...string) ([]*models.PullReview, error)) *MockGiteaReviewFetcherAndRequesterRequestReviewsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// UnrequestReview mocks base method.
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) UnrequestReview(org, repo string, id int64, reviwers ...string) error {
m.ctrl.T.Helper()
varargs := []any{org, repo, id}
for _, a := range reviwers {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UnrequestReview", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UnrequestReview indicates an expected call of UnrequestReview.
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) UnrequestReview(org, repo, id any, reviwers ...any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{org, repo, id}, reviwers...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnrequestReview", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).UnrequestReview), varargs...)
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall{Call: call}
}
// MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall wrap *gomock.Call
type MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall) Return(arg0 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall) Do(f func(string, string, int64, ...string) error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall) DoAndReturn(f func(string, string, int64, ...string) error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockGiteaUnreviewTimelineFetcher is a mock of GiteaUnreviewTimelineFetcher interface.
type MockGiteaUnreviewTimelineFetcher struct {
ctrl *gomock.Controller
recorder *MockGiteaUnreviewTimelineFetcherMockRecorder
isgomock struct{}
}
// MockGiteaUnreviewTimelineFetcherMockRecorder is the mock recorder for MockGiteaUnreviewTimelineFetcher.
type MockGiteaUnreviewTimelineFetcherMockRecorder struct {
mock *MockGiteaUnreviewTimelineFetcher
}
// NewMockGiteaUnreviewTimelineFetcher creates a new mock instance.
func NewMockGiteaUnreviewTimelineFetcher(ctrl *gomock.Controller) *MockGiteaUnreviewTimelineFetcher {
mock := &MockGiteaUnreviewTimelineFetcher{ctrl: ctrl}
mock.recorder = &MockGiteaUnreviewTimelineFetcherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGiteaUnreviewTimelineFetcher) EXPECT() *MockGiteaUnreviewTimelineFetcherMockRecorder {
return m.recorder
}
// GetTimeline mocks base method.
func (m *MockGiteaUnreviewTimelineFetcher) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTimeline", org, repo, idx)
ret0, _ := ret[0].([]*models.TimelineComment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTimeline indicates an expected call of GetTimeline.
func (mr *MockGiteaUnreviewTimelineFetcherMockRecorder) GetTimeline(org, repo, idx any) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeline", reflect.TypeOf((*MockGiteaUnreviewTimelineFetcher)(nil).GetTimeline), org, repo, idx)
return &MockGiteaUnreviewTimelineFetcherGetTimelineCall{Call: call}
}
// MockGiteaUnreviewTimelineFetcherGetTimelineCall wrap *gomock.Call
type MockGiteaUnreviewTimelineFetcherGetTimelineCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaUnreviewTimelineFetcherGetTimelineCall) Return(arg0 []*models.TimelineComment, arg1 error) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaUnreviewTimelineFetcherGetTimelineCall) Do(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaUnreviewTimelineFetcherGetTimelineCall) DoAndReturn(f func(string, string, int64) ([]*models.TimelineComment, error)) *MockGiteaUnreviewTimelineFetcherGetTimelineCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// UnrequestReview mocks base method.
func (m *MockGiteaUnreviewTimelineFetcher) UnrequestReview(org, repo string, id int64, reviwers ...string) error {
m.ctrl.T.Helper()
varargs := []any{org, repo, id}
for _, a := range reviwers {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UnrequestReview", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UnrequestReview indicates an expected call of UnrequestReview.
func (mr *MockGiteaUnreviewTimelineFetcherMockRecorder) UnrequestReview(org, repo, id any, reviwers ...any) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{org, repo, id}, reviwers...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnrequestReview", reflect.TypeOf((*MockGiteaUnreviewTimelineFetcher)(nil).UnrequestReview), varargs...)
return &MockGiteaUnreviewTimelineFetcherUnrequestReviewCall{Call: call}
}
// MockGiteaUnreviewTimelineFetcherUnrequestReviewCall wrap *gomock.Call
type MockGiteaUnreviewTimelineFetcherUnrequestReviewCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall) Return(arg0 error) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall) Do(f func(string, string, int64, ...string) error) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall) DoAndReturn(f func(string, string, int64, ...string) error) *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
@@ -2164,13 +1850,12 @@ func (c *MockGiteaAddReviewCommentCall) DoAndReturn(f func(*models.PullRequest,
} }
// CreatePullRequestIfNotExist mocks base method. // CreatePullRequestIfNotExist mocks base method.
func (m *MockGitea) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error, bool) { func (m *MockGitea) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreatePullRequestIfNotExist", repo, srcId, targetId, title, body) ret := m.ctrl.Call(m, "CreatePullRequestIfNotExist", repo, srcId, targetId, title, body)
ret0, _ := ret[0].(*models.PullRequest) ret0, _ := ret[0].(*models.PullRequest)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
ret2, _ := ret[2].(bool) return ret0, ret1
return ret0, ret1, ret2
} }
// CreatePullRequestIfNotExist indicates an expected call of CreatePullRequestIfNotExist. // CreatePullRequestIfNotExist indicates an expected call of CreatePullRequestIfNotExist.
@@ -2186,19 +1871,19 @@ type MockGiteaCreatePullRequestIfNotExistCall struct {
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockGiteaCreatePullRequestIfNotExistCall) Return(arg0 *models.PullRequest, arg1 error, arg2 bool) *MockGiteaCreatePullRequestIfNotExistCall { func (c *MockGiteaCreatePullRequestIfNotExistCall) Return(arg0 *models.PullRequest, arg1 error) *MockGiteaCreatePullRequestIfNotExistCall {
c.Call = c.Call.Return(arg0, arg1, arg2) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockGiteaCreatePullRequestIfNotExistCall) Do(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error, bool)) *MockGiteaCreatePullRequestIfNotExistCall { func (c *MockGiteaCreatePullRequestIfNotExistCall) Do(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error)) *MockGiteaCreatePullRequestIfNotExistCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaCreatePullRequestIfNotExistCall) DoAndReturn(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error, bool)) *MockGiteaCreatePullRequestIfNotExistCall { func (c *MockGiteaCreatePullRequestIfNotExistCall) DoAndReturn(f func(*models.Repository, string, string, string, string) (*models.PullRequest, error)) *MockGiteaCreatePullRequestIfNotExistCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
@@ -2517,45 +2202,6 @@ func (c *MockGiteaGetIssueCommentsCall) DoAndReturn(f func(string, string, int64
return c return c
} }
// GetLabels mocks base method.
func (m *MockGitea) GetLabels(org, repo string, idx int64) ([]*models.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLabels", org, repo, idx)
ret0, _ := ret[0].([]*models.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLabels indicates an expected call of GetLabels.
func (mr *MockGiteaMockRecorder) GetLabels(org, repo, idx any) *MockGiteaGetLabelsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLabels", reflect.TypeOf((*MockGitea)(nil).GetLabels), org, repo, idx)
return &MockGiteaGetLabelsCall{Call: call}
}
// MockGiteaGetLabelsCall wrap *gomock.Call
type MockGiteaGetLabelsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaGetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaGetLabelsCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaGetLabelsCall) Do(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaGetLabelsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaGetLabelsCall) DoAndReturn(f func(string, string, int64) ([]*models.Label, error)) *MockGiteaGetLabelsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GetNotifications mocks base method. // GetNotifications mocks base method.
func (m *MockGitea) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) { func (m *MockGitea) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -3147,45 +2793,6 @@ func (c *MockGiteaSetCommitStatusCall) DoAndReturn(f func(string, string, string
return c return c
} }
// SetLabels mocks base method.
func (m *MockGitea) SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLabels", org, repo, idx, labels)
ret0, _ := ret[0].([]*models.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetLabels indicates an expected call of SetLabels.
func (mr *MockGiteaMockRecorder) SetLabels(org, repo, idx, labels any) *MockGiteaSetLabelsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLabels", reflect.TypeOf((*MockGitea)(nil).SetLabels), org, repo, idx, labels)
return &MockGiteaSetLabelsCall{Call: call}
}
// MockGiteaSetLabelsCall wrap *gomock.Call
type MockGiteaSetLabelsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaSetLabelsCall) Return(arg0 []*models.Label, arg1 error) *MockGiteaSetLabelsCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaSetLabelsCall) Do(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaSetLabelsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaSetLabelsCall) DoAndReturn(f func(string, string, int64, []string) ([]*models.Label, error)) *MockGiteaSetLabelsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// SetNotificationRead mocks base method. // SetNotificationRead mocks base method.
func (m *MockGitea) SetNotificationRead(notificationId int64) error { func (m *MockGitea) SetNotificationRead(notificationId int64) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@@ -4,8 +4,6 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"os"
"path"
"slices" "slices"
"strings" "strings"
@@ -23,8 +21,7 @@ type PRSet struct {
PRs []*PRInfo PRs []*PRInfo
Config *AutogitConfig Config *AutogitConfig
BotUser string BotUser string
HasAutoStaging bool
} }
func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) { func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
@@ -34,41 +31,6 @@ func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
return return
} }
func (prinfo *PRInfo) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, Reviewers []string, BotUser string) {
org, repo, idx := prinfo.PRComponents()
tl, err := gitea.GetTimeline(org, repo, idx)
if err != nil {
LogError("Failed to fetch timeline for", PRtoString(prinfo.PR), err)
}
// find review request for each reviewer
ReviewersToUnrequest := Reviewers
ReviewersAlreadyChecked := []string{}
for _, tlc := range tl {
if tlc.Type == TimelineCommentType_ReviewRequested && tlc.Assignee != nil {
user := tlc.Assignee.UserName
if idx := slices.Index(ReviewersToUnrequest, user); idx >= 0 && !slices.Contains(ReviewersAlreadyChecked, user) {
if tlc.User != nil && tlc.User.UserName == BotUser {
ReviewersAlreadyChecked = append(ReviewersAlreadyChecked, user)
continue
}
ReviewersToUnrequest = slices.Delete(ReviewersToUnrequest, idx, idx+1)
if len(Reviewers) == 0 {
break
}
}
}
}
LogDebug("Unrequesting reviewes for", PRtoString(prinfo.PR), ReviewersToUnrequest)
err = gitea.UnrequestReview(org, repo, idx, ReviewersToUnrequest...)
if err != nil {
LogError("Failed to unrequest reviewers for", PRtoString(prinfo.PR), err)
}
}
func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRInfo, config *AutogitConfig) ([]*PRInfo, error) { func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRInfo, config *AutogitConfig) ([]*PRInfo, error) {
for _, p := range currentSet { for _, p := range currentSet {
if pr.Index == p.PR.Index && pr.Base.Repo.Name == p.PR.Base.Repo.Name && pr.Base.Repo.Owner.UserName == p.PR.Base.Repo.Owner.UserName { if pr.Index == p.PR.Index && pr.Base.Repo.Name == p.PR.Base.Repo.Name && pr.Base.Repo.Owner.UserName == p.PR.Base.Repo.Owner.UserName {
@@ -99,7 +61,7 @@ func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRIn
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline") var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*models.PullRequest, error) { func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineFetcher, org, repo string, num int64, config *AutogitConfig) (*models.PullRequest, error) {
timeline, err := gitea.GetTimeline(org, repo, num) timeline, err := gitea.GetTimeline(org, repo, num)
if err != nil { if err != nil {
LogError("Failed to fetch timeline for", org, repo, "#", num, err) LogError("Failed to fetch timeline for", org, repo, "#", num, err)
@@ -124,19 +86,14 @@ func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher,
} }
pr, err := gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index) pr, err := gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index)
if err != nil { switch err.(type) {
switch err.(type) { case *repository.RepoGetPullRequestNotFound: // deleted?
case *repository.RepoGetPullRequestNotFound: // deleted? continue
continue default:
default: LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
continue
}
} }
LogDebug("found ref PR on timeline:", PRtoString(pr)) if pr.Base.Ref != prjGitBranch {
if pr.Base.Name != prjGitBranch {
LogDebug(" -> not matching:", pr.Base.Name, prjGitBranch)
continue continue
} }
@@ -156,7 +113,7 @@ func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher,
return nil, Timeline_RefIssueNotFound return nil, Timeline_RefIssueNotFound
} }
func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) { 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
@@ -182,15 +139,6 @@ func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo strin
return nil, err return nil, err
} }
for _, pr := range prs {
org, repo, idx := pr.PRComponents()
reviews, err := FetchGiteaReviews(gitea, org, repo, idx)
if err != nil {
LogError("Error fetching reviews for", PRtoString(pr.PR), ":", err)
}
pr.Reviews = reviews
}
return &PRSet{ return &PRSet{
PRs: prs, PRs: prs,
Config: config, Config: config,
@@ -198,12 +146,6 @@ func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo strin
}, nil }, nil
} }
func (prset *PRSet) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, reviewers []string) {
for _, prinfo := range prset.PRs {
prinfo.RemoveReviewers(gitea, reviewers, prset.BotUser)
}
}
func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) { func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) {
for _, p := range rs.PRs { for _, p := range rs.PRs {
if p.PR.Base.RepoID == pr.Base.RepoID && if p.PR.Base.RepoID == pr.Base.RepoID &&
@@ -289,144 +231,67 @@ next_rs:
} }
for _, pr := range prjpr_set { for _, pr := range prjpr_set {
if strings.EqualFold(prinfo.PR.Base.Repo.Owner.UserName, pr.Org) && strings.EqualFold(prinfo.PR.Base.Repo.Name, pr.Repo) && prinfo.PR.Index == pr.Num { if prinfo.PR.Base.Repo.Owner.UserName == pr.Org && prinfo.PR.Base.Repo.Name == pr.Repo && prinfo.PR.Index == pr.Num {
continue next_rs continue next_rs
} }
} }
LogDebug(" PR: ", PRtoString(prinfo.PR), "not found in project git PRSet")
return false return false
} }
return true return true
} }
func (rs *PRSet) FindMissingAndExtraReviewers(maintainers MaintainershipData, idx int) (missing, extra []string) { func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintainers MaintainershipData) error {
configReviewers := ParseReviewers(rs.Config.Reviewers) configReviewers := ParseReviewers(rs.Config.Reviewers)
// remove reviewers that were already requested and are not stale for _, pr := range rs.PRs {
prjMaintainers := maintainers.ListProjectMaintainers(nil) reviewers := []string{}
LogDebug("project maintainers:", prjMaintainers)
pr := rs.PRs[idx] if rs.IsPrjGitPR(pr.PR) {
if rs.IsPrjGitPR(pr.PR) { reviewers = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
missing = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional) LogDebug("PrjGit submitter:", pr.PR.User.UserName)
if rs.HasAutoStaging { if len(rs.PRs) == 1 {
missing = append(missing, Bot_BuildReview) reviewers = slices.Concat(reviewers, maintainers.ListProjectMaintainers(nil))
} }
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
// only need project maintainer reviews if:
// * not created by a bot and has other PRs, or
// * not created by maintainer
noReviewPRCreators := prjMaintainers
if len(rs.PRs) > 1 {
noReviewPRCreators = append(noReviewPRCreators, rs.BotUser)
}
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(prjMaintainers...) {
LogDebug("Project already reviewed by a project maintainer, remove rest")
// do not remove reviewers if they are also maintainers
prjMaintainers = slices.DeleteFunc(prjMaintainers, func(m string) bool { return slices.Contains(missing, m) })
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
} else { } else {
// if bot not created PrjGit or prj maintainer, we need to add project reviewers here pkg := pr.PR.Base.Repo.Name
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) { reviewers = slices.Concat(configReviewers.Pkg, maintainers.ListProjectMaintainers(nil), maintainers.ListPackageMaintainers(pkg, nil), configReviewers.PkgOptional)
LogDebug("No need for project maintainers") }
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
slices.Sort(reviewers)
reviewers = slices.Compact(reviewers)
// submitters do not need to review their own work
if idx := slices.Index(reviewers, pr.PR.User.UserName); idx != -1 {
reviewers = slices.Delete(reviewers, idx, idx+1)
}
LogDebug("PR: ", pr.PR.Base.Repo.Name, pr.PR.Index)
LogDebug("reviewers for PR:", reviewers)
// remove reviewers that were already requested and are not stale
reviews, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
if err != nil {
LogError("Error fetching reviews:", err)
return err
}
for idx := 0; idx < len(reviewers); {
user := reviewers[idx]
if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) {
reviewers = slices.Delete(reviewers, idx, idx+1)
LogDebug("removing reviewer:", user)
} else { } else {
LogDebug("Adding prjMaintainers to PrjGit") idx++
missing = append(missing, prjMaintainers...)
} }
} }
} else {
pkg := pr.PR.Base.Repo.Name
pkgMaintainers := maintainers.ListPackageMaintainers(pkg, nil)
Maintainers := slices.Concat(prjMaintainers, pkgMaintainers)
noReviewPkgPRCreators := pkgMaintainers
LogDebug("packakge maintainers:", Maintainers) // get maintainers associated with the PR too
if len(reviewers) > 0 {
missing = slices.Concat(configReviewers.Pkg, configReviewers.PkgOptional) LogDebug("Requesting reviews from:", reviewers)
if slices.Contains(noReviewPkgPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(Maintainers...) {
// submitter is maintainer or already reviewed
LogDebug("Package reviewed by maintainer (or subitter is maintainer), remove the rest of them")
// do not remove reviewers if they are also maintainers
Maintainers = slices.DeleteFunc(Maintainers, func(m string) bool { return slices.Contains(missing, m) })
extra = slices.Concat(Maintainers, []string{rs.BotUser})
} else {
// maintainer review is missing
LogDebug("Adding package maintainers to package git")
missing = append(missing, pkgMaintainers...)
}
}
slices.Sort(missing)
missing = slices.Compact(missing)
slices.Sort(extra)
extra = slices.Compact(extra)
// submitters cannot review their own work
if idx := slices.Index(missing, pr.PR.User.UserName); idx != -1 {
missing = slices.Delete(missing, idx, idx+1)
}
LogDebug("PR: ", PRtoString(pr.PR))
LogDebug(" preliminary add reviewers for PR:", missing)
LogDebug(" preliminary rm reviewers for PR:", extra)
// remove missing reviewers that are already done or already pending
for idx := 0; idx < len(missing); {
user := missing[idx]
if pr.Reviews.HasPendingReviewBy(user) || pr.Reviews.IsReviewedBy(user) {
missing = slices.Delete(missing, idx, idx+1)
LogDebug(" removing done/pending reviewer:", user)
} else {
idx++
}
}
// remove extra reviews that are actually only pending, and only pending by us
for idx := 0; idx < len(extra); {
user := extra[idx]
rr := pr.Reviews.FindReviewRequester(user)
if rr != nil && rr.User.UserName == rs.BotUser && pr.Reviews.HasPendingReviewBy(user) {
// good to remove this review
idx++
} else {
// this review should not be considered as extra by us
LogDebug(" - cannot find? to remove", user)
if rr != nil {
LogDebug(" ", rr.User.UserName, "vs.", rs.BotUser, pr.Reviews.HasPendingReviewBy(user))
}
extra = slices.Delete(extra, idx, idx+1)
}
}
LogDebug(" add reviewers for PR:", missing)
LogDebug(" rm reviewers for PR:", extra)
return missing, extra
}
func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequesterAndUnrequester, maintainers MaintainershipData) error {
for idx, pr := range rs.PRs {
missingReviewers, extraReviewers := rs.FindMissingAndExtraReviewers(maintainers, idx)
if len(missingReviewers) > 0 {
LogDebug(" Requesting reviews from:", missingReviewers)
if !IsDryRun { if !IsDryRun {
for _, r := range missingReviewers { for _, r := range reviewers {
if _, err := gitea.RequestReviews(pr.PR, r); err != nil { if _, err := gitea.RequestReviews(pr.PR, r); err != nil {
LogError("Cannot create reviews on", PRtoString(pr.PR), "for user:", r, err) 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)
}
}
}
}
if len(extraReviewers) > 0 {
LogDebug(" UnRequesting reviews from:", extraReviewers)
if !IsDryRun {
for _, r := range extraReviewers {
org, repo, idx := pr.PRComponents()
if err := gitea.UnrequestReview(org, repo, idx, r); err != nil {
LogError("Cannot unrequest reviews on", PRtoString(pr.PR), "for user:", r, err)
} }
} }
} }
@@ -452,12 +317,11 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
if err == nil && prjgit != nil { if err == nil && prjgit != nil {
reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers(groups)) reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers(groups))
LogDebug("Fetching reviews for", prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index) LogDebug("Fetching reviews for", prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
r, err := FetchGiteaReviews(gitea, prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index) r, err := FetchGiteaReviews(gitea, reviewers, prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
if err != nil { if err != nil {
LogError("Cannot fetch gita reaviews for PR:", err) LogError("Cannot fetch gita reaviews for PR:", err)
return false return false
} }
r.RequestedReviewers = reviewers
prjgit.Reviews = r prjgit.Reviews = r
if prjgit.Reviews.IsManualMergeOK() { if prjgit.Reviews.IsManualMergeOK() {
is_manually_reviewed_ok = true is_manually_reviewed_ok = true
@@ -473,12 +337,11 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
pkg := pr.PR.Base.Repo.Name pkg := pr.PR.Base.Repo.Name
reviewers := slices.Concat(configReviewers.Pkg, maintainers.ListPackageMaintainers(pkg, groups)) reviewers := slices.Concat(configReviewers.Pkg, maintainers.ListPackageMaintainers(pkg, groups))
LogDebug("Fetching reviews for", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index) LogDebug("Fetching reviews for", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
r, err := FetchGiteaReviews(gitea, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index) r, 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("Cannot fetch gita reaviews for PR:", err) LogError("Cannot fetch gita reaviews for PR:", err)
return false return false
} }
r.RequestedReviewers = reviewers
pr.Reviews = r pr.Reviews = r
if !pr.Reviews.IsManualMergeOK() { if !pr.Reviews.IsManualMergeOK() {
LogInfo("Not approved manual merge. PR:", pr.PR.URL) LogInfo("Not approved manual merge. PR:", pr.PR.URL)
@@ -500,9 +363,6 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
var pkg string var pkg string
if rs.IsPrjGitPR(pr.PR) { if rs.IsPrjGitPR(pr.PR) {
reviewers = configReviewers.Prj reviewers = configReviewers.Prj
if rs.HasAutoStaging {
reviewers = append(reviewers, Bot_BuildReview)
}
pkg = "" pkg = ""
} else { } else {
reviewers = configReviewers.Pkg reviewers = configReviewers.Pkg
@@ -514,12 +374,11 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
return false return false
} }
r, err := FetchGiteaReviews(gitea, 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 { if err != nil {
LogError("Cannot fetch gitea reaviews for PR:", err) LogError("Cannot fetch gitea reaviews for PR:", err)
return false return false
} }
r.RequestedReviewers = reviewers
is_manually_reviewed_ok = r.IsApproved() is_manually_reviewed_ok = r.IsApproved()
LogDebug("PR to", pr.PR.Base.Repo.Name, "reviewed?", is_manually_reviewed_ok) LogDebug("PR to", pr.PR.Base.Repo.Name, "reviewed?", is_manually_reviewed_ok)
@@ -532,7 +391,7 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review { if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review {
// Do not expand groups here, as the group-review-bot will ACK if group has reviewed. // Do not expand groups here, as the group-review-bot will ACK if group has reviewed.
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.Reviews, pr.PR.User.UserName, nil); !is_manually_reviewed_ok { if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.reviews, pr.PR.User.UserName, nil); !is_manually_reviewed_ok {
LogDebug(" not approved?", pkg) LogDebug(" not approved?", pkg)
return false return false
} }
@@ -572,80 +431,8 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
err = git.GitExec(DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha) err = git.GitExec(DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha)
if err != nil { if err != nil {
status, statusErr := git.GitStatus(DefaultGitPrj) if resolveError := git.GitResolveConflicts(DefaultGitPrj, prjgit.MergeBase, prjgit.Base.Sha, prjgit.Head.Sha); resolveError != nil {
if statusErr != nil { return fmt.Errorf("Merge failed. (%w): %w", err, resolveError)
return fmt.Errorf("Failed to merge: %w . Status also failed: %w", err, statusErr)
}
// we can only resolve conflicts with .gitmodules
for _, s := range status {
if s.Status == GitStatus_Unmerged {
panic("Can't handle conflicts yet")
if s.Path != ".gitmodules" {
return err
}
submodules, err := git.GitSubmoduleList(DefaultGitPrj, "MERGE_HEAD")
if err != nil {
return fmt.Errorf("Failed to fetch submodules during merge resolution: %w", err)
}
s1, err := git.GitExecWithOutput(DefaultGitPrj, "cat-file", "blob", s.States[0])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
s2, err := git.GitExecWithOutput(DefaultGitPrj, "cat-file", "blob", s.States[1])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
s3, err := git.GitExecWithOutput(DefaultGitPrj, "cat-file", "blob", s.States[2])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
subs1, err := ParseSubmodulesFile(strings.NewReader(s1))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
subs2, err := ParseSubmodulesFile(strings.NewReader(s2))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
subs3, err := ParseSubmodulesFile(strings.NewReader(s3))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
// merge from subs3 (target), subs1 (orig), subs2 (2-nd base that is missing from target base)
// this will update submodules
mergedSubs := slices.Concat(subs1, subs2, subs3)
var filteredSubs []Submodule = make([]Submodule, 0, max(len(subs1), len(subs2), len(subs3)))
nextSub:
for subName := range submodules {
for i := range mergedSubs {
if path.Base(mergedSubs[i].Path) == subName {
filteredSubs = append(filteredSubs, mergedSubs[i])
continue nextSub
}
}
return fmt.Errorf("Cannot find submodule for path: %s", subName)
}
out, err := os.Create(path.Join(git.GetPath(), DefaultGitPrj, ".gitmodules"))
if err != nil {
return fmt.Errorf("Can't open .gitmodules for writing: %w", err)
}
if err = WriteSubmodules(filteredSubs, out); err != nil {
return fmt.Errorf("Can't write .gitmodules: %w", err)
}
if out.Close(); err != nil {
return fmt.Errorf("Can't close .gitmodules: %w", err)
}
git.GitExecOrPanic(DefaultGitPrj, "add", ".gitmodules")
git.GitExecOrPanic(DefaultGitPrj, "-c", "core.editor=true", "merge", "--continue")
}
} }
} }

View File

@@ -2,6 +2,7 @@ package common_test
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@@ -75,7 +76,7 @@ func TestPR(t *testing.T) {
consistentSet bool consistentSet bool
prjGitPRIndex int prjGitPRIndex int
reviewSetFetcher func(*mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) reviewSetFetcher func(*mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error)
}{ }{
{ {
name: "Error fetching PullRequest", name: "Error fetching PullRequest",
@@ -147,7 +148,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: true, reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig) return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
}, },
}, },
@@ -179,7 +180,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: false, reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig) return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
}, },
}, },
@@ -207,7 +208,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: false, reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -241,7 +242,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: true, reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -275,7 +276,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: true, reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -311,7 +312,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: false, reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -346,7 +347,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: true, reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -388,7 +389,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: true, reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -430,7 +431,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: false, reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -473,7 +474,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: false, reviewed: false,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{ return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch", Branch: "branch",
@@ -500,7 +501,7 @@ func TestPR(t *testing.T) {
prjGitPRIndex: 0, prjGitPRIndex: 0,
consistentSet: true, consistentSet: true,
reviewed: true, reviewed: true,
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) { reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
config := common.AutogitConfig{ config := common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2", "~*bot"}, Reviewers: []string{"+super1", "*super2", "m1", "-m2", "~*bot"},
Branch: "branch", Branch: "branch",
@@ -515,7 +516,7 @@ func TestPR(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)
pr_mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(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)
@@ -619,508 +620,283 @@ func TestPR(t *testing.T) {
} }
} }
func TestFindMissingAndExtraReviewers(t *testing.T) { func TestPRAssignReviewers(t *testing.T) {
t.Skip("FAIL: unexpected calls, missing calls")
tests := []struct { tests := []struct {
name string name string
config common.AutogitConfig
reviewers []struct { reviewers []struct {
org, repo string org, repo string
num int64 num int64
reviewer string reviewer string
} }
prset *common.PRSet pkgReviews []*models.PullReview
maintainers common.MaintainershipData pkgTimeline []*models.TimelineComment
prjReviews []*models.PullReview
prjTimeline []*models.TimelineComment
noAutoStaging bool expectedReviewerCall [2][]string
expected_missing_reviewers [][]string
expected_extra_reviewers [][]string
}{ }{
{ {
name: "No reviewers", name: "No reviewers",
prset: &common.PRSet{ config: common.AutogitConfig{
PRs: []*common.PRInfo{ GitProjectName: "prg/repo#main",
{ Organization: "org",
PR: &models.PullRequest{ Branch: "main",
User: &models.User{UserName: "foo"}, Reviewers: []string{},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{},
},
}, },
maintainers: &common.MaintainershipMap{Data: map[string][]string{}}, expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
}, },
{ {
name: "One project reviewer only", name: "One project reviewer only",
prset: &common.PRSet{ config: common.AutogitConfig{
PRs: []*common.PRInfo{ GitProjectName: "prg/repo#main",
{ Organization: "org",
PR: &models.PullRequest{ Branch: "main",
User: &models.User{UserName: "foo"}, Reviewers: []string{"-user1"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1"},
},
},
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
expected_missing_reviewers: [][]string{
[]string{},
[]string{"autogits_obs_staging_bot", "user1"},
},
},
{
name: "One project reviewer only and no auto staging",
noAutoStaging: true,
prset: &common.PRSet{
PRs: []*common.PRInfo{
{
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1"},
},
},
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
expected_missing_reviewers: [][]string{
nil,
{"user1"},
}, },
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
}, },
{ {
name: "One project reviewer and one pkg reviewer only", name: "One project reviewer and one pkg reviewer only",
prset: &common.PRSet{ config: common.AutogitConfig{
PRs: []*common.PRInfo{ GitProjectName: "prg/repo#main",
{ Organization: "org",
PR: &models.PullRequest{ Branch: "main",
User: &models.User{UserName: "foo"}, Reviewers: []string{"-user1", "user2"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2"},
},
},
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
expected_missing_reviewers: [][]string{
[]string{"user2"},
[]string{"autogits_obs_staging_bot", "user1"},
}, },
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"user2", "prjmaintainer", "pkgmaintainer"}},
}, },
{ {
name: "No need to get reviews of submitter reviewer", name: "No need to get reviews of submitter",
prset: &common.PRSet{ config: common.AutogitConfig{
PRs: []*common.PRInfo{ GitProjectName: "prg/repo#main",
{ Organization: "org",
PR: &models.PullRequest{ Branch: "main",
User: &models.User{UserName: "submitter"}, Reviewers: []string{"-user1", "submitter"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "m1"}}},
RequestedReviewers: []string{"m1"},
FullTimeline: []*models.TimelineComment{
{User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "m1"}, Type: common.TimelineCommentType_ReviewRequested},
},
},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "submitter"},
},
BotUser: "bot",
},
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"m1", "submitter"}}},
expected_missing_reviewers: [][]string{
nil,
{"autogits_obs_staging_bot", "user1"},
},
expected_extra_reviewers: [][]string{
{"m1"},
}, },
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
}, },
{ {
name: "No need to get reviews of submitter maintainer", name: "Reviews are done",
prset: &common.PRSet{ config: common.AutogitConfig{
PRs: []*common.PRInfo{ GitProjectName: "prg/repo#main",
{ Organization: "org",
PR: &models.PullRequest{ Branch: "main",
User: &models.User{UserName: "submitter"}, Reviewers: []string{"-user1", "user2"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}}, },
}, pkgReviews: []*models.PullReview{
Reviews: &common.PRReviews{}, {
}, State: common.ReviewStateApproved,
{ User: &models.User{UserName: "user2"},
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{},
},
}, },
Config: &common.AutogitConfig{ {
GitProjectName: "prg/repo#main", State: common.ReviewStateApproved,
Organization: "org", User: &models.User{UserName: "pkgmaintainer"},
Branch: "main", },
Reviewers: []string{"-user1", "submitter"}, {
State: common.ReviewStatePending,
User: &models.User{UserName: "prjmaintainer"},
}, },
}, },
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter"}}}, prjReviews: []*models.PullReview{
{
expected_missing_reviewers: [][]string{ State: common.ReviewStateRequestChanges,
[]string{}, User: &models.User{UserName: "user1"},
[]string{"autogits_obs_staging_bot", "user1"},
},
},
{
name: "Add reviewer if also maintainer where review by maintainer is not needed",
prset: &common.PRSet{
PRs: []*common.PRInfo{
{
PR: &models.PullRequest{
User: &models.User{UserName: "submitter"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "bot"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{},
},
}, },
Config: &common.AutogitConfig{ {
GitProjectName: "prg/repo#main", State: common.ReviewStateRequestReview,
Organization: "org", User: &models.User{UserName: "autogits_obs_staging_bot"},
Branch: "main",
Reviewers: []string{"-user1", "submitter", "*reviewer"},
}, },
BotUser: "bot",
}, },
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter", "reviewer"}, "": []string{"reviewer"}}}, expectedReviewerCall: [2][]string{},
expected_missing_reviewers: [][]string{
[]string{"reviewer"},
[]string{"autogits_obs_staging_bot", "reviewer", "user1"},
},
},
{
name: "Dont remove reviewer if also maintainer",
prset: &common.PRSet{
PRs: []*common.PRInfo{
{
PR: &models.PullRequest{
User: &models.User{UserName: "submitter"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
RequestedReviewers: []string{"reviewer"},
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "bot"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
RequestedReviewers: []string{"reviewer"},
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "submitter", "*reviewer"},
},
BotUser: "bot",
},
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter", "reviewer"}, "": []string{"reviewer"}}},
expected_missing_reviewers: [][]string{
[]string{},
[]string{"autogits_obs_staging_bot", "user1"},
},
},
{
name: "Extra project reviewer on the package",
prset: &common.PRSet{
PRs: []*common.PRInfo{
{
PR: &models.PullRequest{
User: &models.User{UserName: "submitter"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
{State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}},
},
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user2"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
},
},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "bot"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
},
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
},
},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "submitter"},
},
BotUser: "bot",
},
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer"}, "": {"prjmaintainer"}}},
expected_missing_reviewers: [][]string{},
expected_extra_reviewers: [][]string{{"prjmaintainer"}},
}, },
{ {
name: "Extra project reviewers on the package and project", name: "Stale review is not done, re-request it",
prset: &common.PRSet{ config: common.AutogitConfig{
PRs: []*common.PRInfo{ GitProjectName: "org/repo#main",
{ Organization: "org",
PR: &models.PullRequest{ Branch: "main",
User: &models.User{UserName: "submitter"}, Reviewers: []string{"-user1", "user2"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
{State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "pkgm1"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "pkgm2"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prj1"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "someother"}},
},
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "pkgm2", "someother", "prj1", "prj2"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj2"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm2"}},
},
},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "bot"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prj1"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
},
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1", "prj2"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj2"}},
},
},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "submitter"},
},
BotUser: "bot",
}, },
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer", "pkgm1", "pkgm2"}, "": {"prjmaintainer", "prj1", "prj2"}}}, pkgReviews: []*models.PullReview{
{
expected_missing_reviewers: [][]string{}, State: common.ReviewStateApproved,
expected_extra_reviewers: [][]string{{"pkgm1", "pkgm2", "prj1", "prj2", "prjmaintainer"}, {"prj1", "prj2"}}, 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"}},
}, },
{ {
name: "No extra project reviewers on the package and project (all pending)", name: "Stale optional review is not done, re-request it",
prset: &common.PRSet{ config: common.AutogitConfig{
PRs: []*common.PRInfo{ GitProjectName: "prg/repo#main",
{ Organization: "org",
PR: &models.PullRequest{ Branch: "main",
User: &models.User{UserName: "submitter"}, Reviewers: []string{"-user1", "user2", "~bot"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "pkgmaintainer"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prjmaintainer"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "pkgm1"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "someother"}},
},
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "someother", "prj1"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "!bot"}, Assignee: &models.User{UserName: "someother"}},
},
},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "bot"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prj"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
},
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "!bot"}, Assignee: &models.User{UserName: "user1"}},
},
},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prj/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "submitter"},
},
BotUser: "bot",
}, },
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer", "pkgm1", "pkgm2"}, "": {"prjmaintainer", "prj1", "prj2"}}}, pkgReviews: []*models.PullReview{
{
expected_missing_reviewers: [][]string{{"pkgm2", "prj2"}}, State: common.ReviewStateApproved,
expected_extra_reviewers: [][]string{{}, {"prj1"}}, 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) {
test.prset.HasAutoStaging = !test.noAutoStaging ctl := gomock.NewController(t)
for idx, pr := range test.prset.PRs { pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
missing, extra := test.prset.FindMissingAndExtraReviewers(test.maintainers, idx) review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
// avoid nil dereference below, by adding empty array elements if test.pkgTimeline == nil {
if idx >= len(test.expected_missing_reviewers) { test.pkgTimeline = reviewsToTimeline(test.pkgReviews)
test.expected_missing_reviewers = append(test.expected_missing_reviewers, nil) }
} if test.prjTimeline == nil {
if idx >= len(test.expected_extra_reviewers) { test.prjTimeline = reviewsToTimeline(test.prjReviews)
test.expected_extra_reviewers = append(test.expected_extra_reviewers, nil) }
}
slices.Sort(test.expected_extra_reviewers[idx]) pr_mock.EXPECT().GetPullRequest("other", "pkgrepo", int64(1)).Return(&models.PullRequest{
slices.Sort(test.expected_missing_reviewers[idx]) Body: "Some description is here",
if slices.Compare(missing, test.expected_missing_reviewers[idx]) != 0 { User: &models.User{UserName: "submitter"},
t.Error("Expected missing reviewers for", common.PRtoString(pr.PR), ":", test.expected_missing_reviewers[idx], "but have:", missing) RequestedReviewers: []*models.User{},
} Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "pkgrepo", Owner: &models.User{UserName: "other"}}},
Head: &models.PRBranchInfo{},
Index: 1,
}, nil)
review_mock.EXPECT().GetPullRequestReviews("other", "pkgrepo", int64(1)).Return(test.pkgReviews, nil)
review_mock.EXPECT().GetTimeline("other", "pkgrepo", int64(1)).Return(test.pkgTimeline, nil)
pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{
Body: fmt.Sprintf(common.PrPattern, "other", "pkgrepo", 1),
User: &models.User{UserName: "bot1"},
RequestedReviewers: []*models.User{{UserName: "main_reviewer"}},
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
Head: &models.PRBranchInfo{},
Index: 42,
}, nil)
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(42)).Return(test.prjReviews, nil)
review_mock.EXPECT().GetTimeline("org", "repo", int64(42)).Return(test.prjTimeline, nil)
if slices.Compare(extra, test.expected_extra_reviewers[idx]) != 0 { maintainership_mock.EXPECT().ListProjectMaintainers(gomock.Any()).Return([]string{"prjmaintainer"}).AnyTimes()
t.Error("Expected reviewers to remove for", common.PRtoString(pr.PR), ":", test.expected_extra_reviewers[idx], "but have:", extra) maintainership_mock.EXPECT().ListPackageMaintainers("pkgrepo", gomock.Any()).Return([]string{"pkgmaintainer"}).AnyTimes()
prs, _ := common.FetchPRSet("test", pr_mock, "other", "pkgrepo", int64(1), &test.config)
if len(prs.PRs) != 2 {
t.Fatal("PRs not fetched")
}
for _, pr := range prs.PRs {
r := test.expectedReviewerCall[0]
if !prs.IsPrjGitPR(pr.PR) {
r = test.expectedReviewerCall[1]
}
slices.Sort(r)
for _, reviewer := range r {
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
} }
} }
prs.AssignReviewers(review_mock, maintainership_mock)
})
}
prjgit_tests := []struct {
name string
config common.AutogitConfig
reviewers []struct {
org, repo string
num int64
reviewer string
}
prjReviews []*models.PullReview
expectedReviewerCall [2][]string
}{
{
name: "PrjMaintainers in prjgit review when not part of pkg set",
config: common.AutogitConfig{
GitProjectName: "org/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{},
},
expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot", "prjmaintainer"}},
},
}
for _, test := range prjgit_tests {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{
Body: "Some description is here",
User: &models.User{UserName: "submitter"},
RequestedReviewers: []*models.User{},
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
Head: &models.PRBranchInfo{},
Index: 1,
}, nil)
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(1)).Return(test.prjReviews, nil)
review_mock.EXPECT().GetTimeline("org", "repo", int64(1)).Return(nil, nil)
maintainership_mock.EXPECT().ListProjectMaintainers(gomock.Any()).Return([]string{"prjmaintainer"}).AnyTimes()
prs, _ := common.FetchPRSet("test", pr_mock, "org", "repo", int64(1), &test.config)
if len(prs.PRs) != 1 {
t.Fatal("PRs not fetched")
}
for _, pr := range prs.PRs {
r := test.expectedReviewerCall[0]
if !prs.IsPrjGitPR(pr.PR) {
t.Fatal("only prjgit pr here")
}
for _, reviewer := range r {
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
}
}
prs.AssignReviewers(review_mock, maintainership_mock)
}) })
} }
} }
@@ -1165,6 +941,7 @@ func TestPRMerge(t *testing.T) {
pr: &models.PullRequest{ pr: &models.PullRequest{
Base: &models.PRBranchInfo{ Base: &models.PRBranchInfo{
Sha: "e8b0de43d757c96a9d2c7101f4bff404e322f53a1fa4041fb85d646110c38ad4", // "base_add_b1" Sha: "e8b0de43d757c96a9d2c7101f4bff404e322f53a1fa4041fb85d646110c38ad4", // "base_add_b1"
Name: "master",
Repo: &models.Repository{ Repo: &models.Repository{
Name: "prj", Name: "prj",
Owner: &models.User{ Owner: &models.User{
@@ -1185,6 +962,7 @@ func TestPRMerge(t *testing.T) {
pr: &models.PullRequest{ pr: &models.PullRequest{
Base: &models.PRBranchInfo{ Base: &models.PRBranchInfo{
Sha: "4fbd1026b2d7462ebe9229a49100c11f1ad6555520a21ba515122d8bc41328a8", Sha: "4fbd1026b2d7462ebe9229a49100c11f1ad6555520a21ba515122d8bc41328a8",
Name: "master",
Repo: &models.Repository{ Repo: &models.Repository{
Name: "prj", Name: "prj",
Owner: &models.User{ Owner: &models.User{
@@ -1203,7 +981,8 @@ 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.NewMockGiteaPRTimelineReviewFetcher(ctl) mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl) reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
@@ -1262,7 +1041,7 @@ func TestPRChanges(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_fetcher := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl) mock_fetcher := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
mock_fetcher.EXPECT().GetPullRequest("org", "prjgit", int64(42)).Return(test.PrjPRs, nil) mock_fetcher.EXPECT().GetPullRequest("org", "prjgit", int64(42)).Return(test.PrjPRs, nil)
for _, pr := range test.PRs { for _, pr := range test.PRs {
mock_fetcher.EXPECT().GetPullRequest(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index).Return(pr, nil) mock_fetcher.EXPECT().GetPullRequest(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index).Return(pr, nil)

View File

@@ -1,5 +1,9 @@
package common package common
import (
"slices"
)
type Reviewers struct { type Reviewers struct {
Prj []string Prj []string
Pkg []string Pkg []string
@@ -32,5 +36,10 @@ func ParseReviewers(input []string) *Reviewers {
*pkg = append(*pkg, reviewer) *pkg = append(*pkg, reviewer)
} }
} }
if !slices.Contains(r.Prj, Bot_BuildReview) {
r.Prj = append(r.Prj, Bot_BuildReview)
}
return r return r
} }

View File

@@ -21,14 +21,14 @@ func TestReviewers(t *testing.T) {
name: "project and package reviewers", name: "project and package reviewers",
input: []string{"1", "2", "3", "*5", "+6", "-7"}, input: []string{"1", "2", "3", "*5", "+6", "-7"},
prj: []string{"5", "7"}, 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", name: "optional project and package reviewers",
input: []string{"~1", "2", "3", "~*5", "+6", "-7"}, input: []string{"~1", "2", "3", "~*5", "+6", "-7"},
prj: []string{"7"}, prj: []string{"7", common.Bot_BuildReview},
pkg: []string{"2", "3", "6"}, pkg: []string{"2", "3", "6"},
prj_optional: []string{"5"}, prj_optional: []string{"5"},
pkg_optional: []string{"1", "5"}, pkg_optional: []string{"1", "5"},

View File

@@ -9,14 +9,12 @@ import (
) )
type PRReviews struct { type PRReviews struct {
Reviews []*models.PullReview reviews []*models.PullReview
RequestedReviewers []string reviewers []string
Comments []*models.TimelineComment comments []*models.TimelineComment
FullTimeline []*models.TimelineComment
} }
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64) (*PRReviews, error) { func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, reviewers []string, org, repo string, no int64) (*PRReviews, error) {
timeline, err := rf.GetTimeline(org, repo, no) timeline, err := rf.GetTimeline(org, repo, no)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -27,14 +25,10 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64
return nil, err return nil, err
} }
reviews := make([]*models.PullReview, 0, 10) reviews := make([]*models.PullReview, 0, len(reviewers))
needNewReviews := []string{}
var comments []*models.TimelineComment var comments []*models.TimelineComment
alreadyHaveUserReview := func(user string) bool { alreadyHaveUserReview := func(user string) bool {
if slices.Contains(needNewReviews, user) {
return true
}
for _, r := range reviews { for _, r := range reviews {
if r.User != nil && r.User.UserName == user { if r.User != nil && r.User.UserName == user {
return true return true
@@ -43,40 +37,32 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64
return false return false
} }
LogDebug("FetchingGiteaReviews for", org, repo, no)
LogDebug("Number of reviews:", len(rawReviews))
LogDebug("Number of items in timeline:", len(timeline))
cutOffIdx := len(timeline)
for idx, item := range timeline { for idx, item := range timeline {
if item.Type == TimelineCommentType_Review || item.Type == TimelineCommentType_ReviewRequested { if item.Type == TimelineCommentType_Review {
for _, r := range rawReviews { for _, r := range rawReviews {
if r.ID == item.ReviewID { if r.ID == item.ReviewID {
if !alreadyHaveUserReview(r.User.UserName) { if !alreadyHaveUserReview(r.User.UserName) {
if item.Type == TimelineCommentType_Review && idx > cutOffIdx { reviews = append(reviews, r)
needNewReviews = append(needNewReviews, r.User.UserName)
} else {
reviews = append(reviews, r)
}
} }
break break
} }
} }
} else if item.Type == TimelineCommentType_Comment && cutOffIdx > idx { } else if item.Type == TimelineCommentType_Comment {
comments = append(comments, item) comments = append(comments, item)
} else if item.Type == TimelineCommentType_PushPull && cutOffIdx == len(timeline) { } else if item.Type == TimelineCommentType_PushPull {
LogDebug("cut-off", item.Created, "@", idx) LogDebug("cut-off", item.Created)
cutOffIdx = idx timeline = timeline[0:idx]
break
} else { } else {
LogDebug("Unhandled timeline type:", item.Type) LogDebug("Unhandled timeline type:", item.Type)
} }
} }
LogDebug("num comments:", len(comments), "timeline:", len(reviews)) LogDebug("num comments:", len(comments), "reviews:", len(reviews), len(timeline))
return &PRReviews{ return &PRReviews{
Reviews: reviews, reviews: reviews,
Comments: comments, reviewers: reviewers,
FullTimeline: timeline, comments: comments,
}, nil }, nil
} }
@@ -95,27 +81,23 @@ func bodyCommandManualMergeOK(body string) bool {
} }
func (r *PRReviews) IsManualMergeOK() bool { func (r *PRReviews) IsManualMergeOK() bool {
if r == nil { for _, c := range r.comments {
return false
}
for _, c := range r.Comments {
if c.Updated != c.Created { if c.Updated != c.Created {
continue continue
} }
LogDebug("comment:", c.User.UserName, c.Body) LogDebug("comment:", c.User.UserName, c.Body)
if slices.Contains(r.RequestedReviewers, c.User.UserName) { if slices.Contains(r.reviewers, c.User.UserName) {
if bodyCommandManualMergeOK(c.Body) { if bodyCommandManualMergeOK(c.Body) {
return true return true
} }
} }
} }
for _, c := range r.Reviews { for _, c := range r.reviews {
if c.Updated != c.Submitted { if c.Updated != c.Submitted {
continue continue
} }
if slices.Contains(r.RequestedReviewers, c.User.UserName) { if slices.Contains(r.reviewers, c.User.UserName) {
if bodyCommandManualMergeOK(c.Body) { if bodyCommandManualMergeOK(c.Body) {
return true return true
} }
@@ -126,14 +108,11 @@ func (r *PRReviews) IsManualMergeOK() bool {
} }
func (r *PRReviews) IsApproved() bool { func (r *PRReviews) IsApproved() bool {
if r == nil {
return false
}
goodReview := true goodReview := true
for _, reviewer := range r.RequestedReviewers { 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) LogDebug(" -- found review: ", review.User.UserName)
goodReview = true goodReview = true
@@ -151,11 +130,7 @@ func (r *PRReviews) IsApproved() bool {
func (r *PRReviews) MissingReviews() []string { func (r *PRReviews) MissingReviews() []string {
missing := []string{} missing := []string{}
if r == nil { for _, reviewer := range r.reviewers {
return missing
}
for _, reviewer := range r.RequestedReviewers {
if !r.IsReviewedBy(reviewer) { if !r.IsReviewedBy(reviewer) {
missing = append(missing, reviewer) missing = append(missing, reviewer)
} }
@@ -163,64 +138,45 @@ func (r *PRReviews) MissingReviews() []string {
return missing return missing
} }
func (r *PRReviews) FindReviewRequester(reviewer string) *models.TimelineComment {
if r == nil {
return nil
}
for _, r := range r.FullTimeline {
if r.Type == TimelineCommentType_ReviewRequested && r.Assignee.UserName == reviewer {
return r
}
}
return nil
}
func (r *PRReviews) HasPendingReviewBy(reviewer string) bool { func (r *PRReviews) HasPendingReviewBy(reviewer string) bool {
if r == nil { if !slices.Contains(r.reviewers, reviewer) {
return false return false
} }
for _, r := range r.Reviews { isPending := false
if r.User.UserName == reviewer { for _, r := range r.reviews {
if r.User.UserName == reviewer && !r.Stale {
switch r.State { switch r.State {
case ReviewStateRequestReview, ReviewStatePending: case ReviewStateApproved:
return true fallthrough
default: case ReviewStateRequestChanges:
return false return false
case ReviewStateRequestReview:
fallthrough
case ReviewStatePending:
isPending = true
} }
} }
} }
return false return isPending
} }
func (r *PRReviews) IsReviewedBy(reviewer string) bool { func (r *PRReviews) IsReviewedBy(reviewer string) bool {
if r == nil { if !slices.Contains(r.reviewers, reviewer) {
return false return false
} }
for _, r := range r.Reviews { for _, r := range r.reviews {
if r.User.UserName == reviewer && !r.Stale { if r.User.UserName == reviewer && !r.Stale {
switch r.State { switch r.State {
case ReviewStateApproved, ReviewStateRequestChanges: case ReviewStateApproved:
return true
case ReviewStateRequestChanges:
return true return true
default:
return false
} }
} }
} }
return false return false
} }
func (r *PRReviews) IsReviewedByOneOf(reviewers ...string) bool {
for _, reviewer := range reviewers {
if r.IsReviewedBy(reviewer) {
return true
}
}
return false
}

View File

@@ -62,23 +62,11 @@ func TestReviews(t *testing.T) {
{ {
name: "Two reviewer, one stale and pending", name: "Two reviewer, one stale and pending",
reviews: []*models.PullReview{ reviews: []*models.PullReview{
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true}, &models.PullReview{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true},
}, },
reviewers: []string{"user1", "user2"}, reviewers: []string{"user1", "user2"},
isApproved: false, isApproved: false,
isPendingByTest1: true, isPendingByTest1: false,
isReviewedByTest1: false,
},
{
name: "Two reviewer, one stale and pending, other done",
reviews: []*models.PullReview{
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}},
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
},
reviewers: []string{"user1", "user2"},
isApproved: false,
isPendingByTest1: true,
isReviewedByTest1: false, isReviewedByTest1: false,
}, },
{ {
@@ -151,7 +139,7 @@ func TestReviews(t *testing.T) {
rf.EXPECT().GetTimeline("test", "pr", int64(1)).Return(test.timeline, nil) 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", "pr", 1) reviews, err := common.FetchGiteaReviews(rf, test.reviewers, "test", "pr", 1)
if test.fetchErr != nil { if test.fetchErr != nil {
if err != test.fetchErr { if err != test.fetchErr {
@@ -159,7 +147,6 @@ func TestReviews(t *testing.T) {
} }
return return
} }
reviews.RequestedReviewers = test.reviewers
if r := reviews.IsApproved(); r != test.isApproved { if r := reviews.IsApproved(); r != test.isApproved {
t.Fatal("Unexpected IsReviewed():", r, "vs. expected", test.isApproved) t.Fatal("Unexpected IsReviewed():", r, "vs. expected", test.isApproved)

View File

@@ -40,10 +40,24 @@ create_prjgit_sample() {
git submodule -q add ../pkgB2 pkgB2 git submodule -q add ../pkgB2 pkgB2
git commit -q -m "pkgB2 added" git commit -q -m "pkgB2 added"
git checkout main git checkout -b base_rm_c main
git clean -ffxd git clean -ffxd
git submodule -q add -f ../pkgB1 pkgB1 git rm pkgC
git commit -q -m "main adding pkgB1" git commit -q -m 'pkgC removed'
git checkout -b base_modify_c main
git submodule update --init pkgC
pushd pkgC
echo "mofieid" >> README.md
git commit -q -m "modified" README.md
popd
git commit pkgC -m "modifiedC"
git submodule deinit -f pkgC
# git checkout main
# git clean -ffxd
# git submodule -q add -f ../pkgB1 pkgB1
# git commit -q -m "main adding pkgB1"
popd popd
} }

View File

@@ -27,87 +27,10 @@ import (
"regexp" "regexp"
"slices" "slices"
"strings" "strings"
"unicode"
"src.opensuse.org/autogits/common/gitea-generated/models" "src.opensuse.org/autogits/common/gitea-generated/models"
) )
type NewRepos struct {
Repos []struct {
Organization, Repository, Branch string
PackageName string
}
IsMaintainer bool
}
const maintainership_line = "MAINTAINER"
var true_lines []string = []string{"1", "TRUE", "YES", "OK", "T"}
func HasSpace(s string) bool {
return strings.IndexFunc(s, unicode.IsSpace) >= 0
}
func FindNewReposInIssueBody(body string) *NewRepos {
Issues := &NewRepos{}
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
if ul := strings.ToUpper(line); strings.HasPrefix(ul, "MAINTAINER") {
value := ""
if idx := strings.IndexRune(ul, ':'); idx > 0 && len(ul) > idx+2 {
value = ul[idx+1:]
} else if idx := strings.IndexRune(ul, ' '); idx > 0 && len(ul) > idx+2 {
value = ul[idx+1:]
}
if slices.Contains(true_lines, strings.TrimSpace(value)) {
Issues.IsMaintainer = true
}
}
// line = strings.TrimSpace(line)
issue := struct{ Organization, Repository, Branch, PackageName string }{}
branch := strings.Split(line, "#")
repo := strings.Split(branch[0], "/")
if len(branch) == 2 {
issue.Branch = strings.TrimSpace(branch[1])
}
if len(repo) == 2 {
issue.Organization = strings.TrimSpace(repo[0])
issue.Repository = strings.TrimSpace(repo[1])
issue.PackageName = issue.Repository
if idx := strings.Index(strings.ToUpper(issue.Branch), " AS "); idx > 0 && len(issue.Branch) > idx+5 {
issue.PackageName = strings.TrimSpace(issue.Branch[idx+3:])
issue.Branch = strings.TrimSpace(issue.Branch[0:idx])
}
if HasSpace(issue.Organization) || HasSpace(issue.Repository) || HasSpace(issue.PackageName) || HasSpace(issue.Branch) {
continue
}
} else {
continue
}
Issues.Repos = append(Issues.Repos, issue)
//PackageNameIdx := strings.Index(strings.ToUpper(line), " AS ")
//words := strings.Split(line)
}
if len(Issues.Repos) == 0 {
return nil
}
return Issues
}
func IssueToString(issue *models.Issue) string {
if issue == nil {
return "(nil)"
}
return fmt.Sprintf("%s/%s#%d", issue.Repository.Owner, issue.Repository.Name, issue.Index)
}
func SplitLines(str string) []string { func SplitLines(str string) []string {
return SplitStringNoEmpty(str, "\n") return SplitStringNoEmpty(str, "\n")
} }
@@ -245,10 +168,9 @@ func FetchDevelProjects() (DevelProjects, error) {
} }
var DevelProjectNotFound = errors.New("Devel project not found") var DevelProjectNotFound = errors.New("Devel project not found")
func (d DevelProjects) GetDevelProject(pkg string) (string, error) { func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
for _, item := range d { for _, item := range d {
if item.Package == pkg { if item.Package == pkg {
return item.Project, nil return item.Project, nil
} }
} }
@@ -256,33 +178,3 @@ func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
return "", DevelProjectNotFound return "", DevelProjectNotFound
} }
var removedBranchNameSuffixes []string = []string{
"-rm",
"-removed",
"-deleted",
}
func findRemovedBranchSuffix(branchName string) string {
branchName = strings.ToLower(branchName)
for _, suffix := range removedBranchNameSuffixes {
if len(suffix) < len(branchName) && strings.HasSuffix(branchName, suffix) {
return suffix
}
}
return ""
}
func IsRemovedBranch(branchName string) bool {
return len(findRemovedBranchSuffix(branchName)) > 0
}
func TrimRemovedBranchSuffix(branchName string) string {
suffix := findRemovedBranchSuffix(branchName)
if len(suffix) > 0 {
return branchName[0 : len(branchName)-len(suffix)]
}
return branchName
}

View File

@@ -1,7 +1,6 @@
package common_test package common_test
import ( import (
"reflect"
"testing" "testing"
"src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common"
@@ -166,142 +165,3 @@ func TestRemoteName(t *testing.T) {
}) })
} }
} }
func TestRemovedBranchName(t *testing.T) {
tests := []struct {
name string
branchName string
isRemoved bool
regularName string
}{
{
name: "Empty branch",
},
{
name: "Removed suffix only",
branchName: "-rm",
isRemoved: false,
regularName: "-rm",
},
{
name: "Capital suffix",
branchName: "Foo-Rm",
isRemoved: true,
regularName: "Foo",
},
{
name: "Other suffixes",
isRemoved: true,
branchName: "Goo-Rm-DeleteD",
regularName: "Goo-Rm",
},
{
name: "Other suffixes",
isRemoved: true,
branchName: "main-REMOVED",
regularName: "main",
},
{
name: "Not removed separator",
isRemoved: false,
branchName: "main;REMOVED",
regularName: "main;REMOVED",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if r := common.IsRemovedBranch(test.branchName); r != test.isRemoved {
t.Error("Expecting isRemoved:", test.isRemoved, "but received", r)
}
if tn := common.TrimRemovedBranchSuffix(test.branchName); tn != test.regularName {
t.Error("Expected stripped branch name to be:", test.regularName, "but have:", tn)
}
})
}
}
func TestNewPackageIssueParsing(t *testing.T) {
tests := []struct {
name string
input string
issues *common.NewRepos
}{
{
name: "Nothing",
},
{
name: "Basic repo",
input: "org/repo#branch",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
},
},
},
{
name: "Default branch and junk lines and approval for maintainership",
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: yes",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
},
IsMaintainer: true,
},
},
{
name: "Default branch and junk lines and no maintainership",
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: NEVER",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
},
},
},
{
name: "3 repos with comments and maintainership",
input: "\n\nsome comments for org1/repo2 are here and more\n\norg1/repo2#master\n org2/repo3#master\n some/repo3#m\nMaintainer ok",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org1", Repository: "repo2", Branch: "master", PackageName: "repo2"},
{Organization: "org2", Repository: "repo3", Branch: "master", PackageName: "repo3"},
{Organization: "some", Repository: "repo3", Branch: "m", PackageName: "repo3"},
},
IsMaintainer: true,
},
},
{
name: "Invalid repos with spaces",
input: "or g/repo#branch\norg/r epo#branch\norg/repo#br anch\norg/repo#branch As foo ++",
},
{
name: "Valid repos with spaces",
input: " org / repo # branch",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
},
},
},
{
name: "Package name is not repo name",
input: " org / repo # branch as repo++ \nmaintainer true",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo++"},
},
IsMaintainer: true,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
issue := common.FindNewReposInIssueBody(test.input)
if !reflect.DeepEqual(test.issues, issue) {
t.Error("Expected", test.issues, "but have", issue)
}
})
}
}

View File

@@ -58,30 +58,6 @@ sub ListPackages {
return @packages; return @packages;
} }
sub FactoryMd5 {
my ($package) = @_;
my $out = "";
if (system("osc ls openSUSE:Factory $package | grep -q build.specials.obscpio") == 0) {
system("mkdir _extract") == 0 || die "_extract exists or can't make it. Aborting.";
chdir("_extract") || die;
system("osc cat openSUSE:Factory $package build.specials.obscpio | cpio -dium 2> /dev/null") == 0 || die;
system("rm .* 2> /dev/null");
open( my $fh, "find -type f -exec /usr/bin/basename {} \\; | xargs md5sum | awk '{print \$1 FS \$2}' | grep -v d41d8cd98f00b204e9800998ecf8427e |") or die;
while ( my $l = <$fh>) {
$out = $out.$l;
}
close($fh);
chdir("..") && system("rm -rf _extract") == 0 || die;
}
open( my $fh, "osc ls -v openSUSE:Factory $package | awk '{print \$1 FS \$7}' | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' |") or die;
while (my $l = <$fh>) {
$out = $out.$l;
}
close($fh);
return $out;
}
# Read project from first argument # Read project from first argument
sub Usage { sub Usage {
die "Usage: $0 <OBS Project> [org [package]]"; die "Usage: $0 <OBS Project> [org [package]]";
@@ -89,7 +65,6 @@ sub Usage {
my $project = shift or Usage(); my $project = shift or Usage();
my $org = shift; my $org = shift;
if (not defined($org)) { if (not defined($org)) {
$org = `osc meta prj $project | grep scmsync | sed -e 's,^.*src.opensuse.org/\\(.*\\)/_ObsPrj.*,\\1,'`; $org = `osc meta prj $project | grep scmsync | sed -e 's,^.*src.opensuse.org/\\(.*\\)/_ObsPrj.*,\\1,'`;
chomp($org); chomp($org);
@@ -164,7 +139,7 @@ if ( scalar @tomove > 0 ) {
system("git -C $pkg push origin factory") == 0 and system("git -C $pkg push origin factory") == 0 and
system("git obs $super_user api -X PATCH --data '{\"default_branch\": \"factory\"}' /repos/pool/$pkg") == 0 system("git obs $super_user api -X PATCH --data '{\"default_branch\": \"factory\"}' /repos/pool/$pkg") == 0
or die "Error in creating a pool repo"; or die "Error in creating a pool repo";
system("for i in \$(git -C $pkg for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ | grep -v '\\(^HEAD\$\\|^factory\$\\)'); do git -C $pkg push origin :\$i; done") == 0 or die "failed to cull branches"; system("for i in \$(git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ | grep -v '\\(^HEAD\$\\|^factory\$\)'); do git -C $pkg push origin :\$i; done") == 0 or die "failed to cull branches";
} }
} }
@@ -192,7 +167,6 @@ for my $package ( sort(@packages) ) {
or ( push( @tomove, $package ) and die "Can't fetch pool for $package" ); or ( push( @tomove, $package ) and die "Can't fetch pool for $package" );
my @commits = FindFactoryCommit($package); my @commits = FindFactoryCommit($package);
my $Md5Hashes = FactoryMd5($package);
my $c; my $c;
my $match = 0; my $match = 0;
for my $commit (@commits) { for my $commit (@commits) {
@@ -205,27 +179,16 @@ for my $package ( sort(@packages) ) {
system("git -C $package lfs fetch pool $commit") == 0 system("git -C $package lfs fetch pool $commit") == 0
and system("git -C $package checkout -B factory $commit") == 0 and system("git -C $package checkout -B factory $commit") == 0
and system("git -C $package lfs checkout") == 0 and system("git -C $package lfs checkout") == 0
and chdir($package)) { and system(
"cd $package; osc ls -v openSUSE:Factory $package | awk '{print \$1 FS \$7}' | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' | md5sum -c --quiet"
open(my $fh, "|-", "md5sum -c --quiet") or die $!; ) == 0
print $fh $Md5Hashes; and system("bash -c \"diff <(ls -1 $package | sort) <(osc ls openSUSE:Factory $package | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' | sort)\"") == 0
close $fh; )
if ($? >> 8 != 0) { {
chdir("..") || die;
next;
}
open($fh, "|-", "awk '{print \$2}' | sort | bash -c \"diff <(ls -1 | sort) -\"") or die $!;
print $fh $Md5Hashes;
close $fh;
my $ec = $? >> 8;
chdir("..") || die;
if ($ec == 0) {
$c = $commit;
$match = 1;
last;
}
$c = $commit;
$match = 1;
last;
} }
} }

View File

@@ -1,23 +1,16 @@
Java:packages
Kernel:firmware Kernel:firmware
Kernel:kdump Kernel:kdump
devel:gcc
devel:languages:clojure devel:languages:clojure
devel:languages:erlang devel:languages:erlang
devel:languages:erlang:Factory devel:languages:erlang:Factory
devel:languages:hare devel:languages:hare
devel:languages:javascript devel:languages:javascript
devel:languages:lua devel:languages:lua
devel:languages:nodejs
devel:languages:perl devel:languages:perl
devel:languages:python:Factory
devel:languages:python:pytest
devel:openSUSE:Factory devel:openSUSE:Factory
network:chromium
network:dhcp network:dhcp
network:im:whatsapp network:im:whatsapp
network:messaging:xmpp network:messaging:xmpp
science:HPC
server:dns server:dns
systemsmanagement:cockpit systemsmanagement:cockpit
X11:lxde X11:lxde

View File

@@ -14,11 +14,15 @@ import (
"src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common"
) )
type Status struct {
Context string `json:"context"`
State string `json:"state"`
TargetUrl string `json:"target_url"`
}
type StatusInput struct { type StatusInput struct {
Description string `json:"description"` State string `json:"state"`
Context string `json:"context"` TargetUrl string `json:"target_url"`
State string `json:"state"`
TargetUrl string `json:"target_url"`
} }
func main() { func main() {
@@ -55,26 +59,23 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
config, ok := r.Context().Value(configKey).(*Config) config, ok := r.Context().Value(configKey).(*Config)
if !ok { if !ok {
common.LogDebug("Config missing from context") common.LogError("Config missing from context")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
header := r.Header.Get("Authorization") header := r.Header.Get("Authorization")
if header == "" { if header == "" {
common.LogDebug("Authorization header not found")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
token_arr := strings.Split(header, " ") token_arr := strings.Split(header, " ")
if len(token_arr) != 2 { if len(token_arr) != 2 {
common.LogDebug("Authorization header malformed")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
if !strings.EqualFold(token_arr[0], "token") { if !strings.EqualFold(token_arr[0], "Bearer") {
common.LogDebug("Token not found in Authorization header")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
@@ -82,7 +83,6 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
token := token_arr[1] token := token_arr[1]
if !slices.Contains(config.Keys, token) { if !slices.Contains(config.Keys, token) {
common.LogDebug("Provided token is not known")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
@@ -104,8 +104,13 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
status := Status{
Context: "Build in obs",
State: statusinput.State,
TargetUrl: statusinput.TargetUrl,
}
status_payload, err := json.Marshal(statusinput) status_payload, err := json.Marshal(status)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
@@ -126,8 +131,8 @@ func StatusProxy(w http.ResponseWriter, r *http.Request) {
return return
} }
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "Content-Type")
req.Header.Add("Authorization", fmt.Sprintf("token %s", ForgeToken)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ForgeToken))
resp, err := client.Do(req) resp, err := client.Do(req)

View File

@@ -1,48 +0,0 @@
# gitea_status_proxy
Allows bots without code owner permission to set Gitea's commit status
## Basic usage
To beging, you need the json config and a Gitea token with permissions to the repository you want to write to.
Keys should be randomly generated, i.e by using openssl: `openssl rand -base64 48`
Generate a json config file, with the key generated from running the command above, save as example.json:
```
{
"forge_url": "https://src.opensuse.org/api/v1",
"keys": ["$YOUR_TOKEN_GOES_HERE"]
}
```
### start the proxy:
```
GITEA_TOKEN=YOURTOKEN ./gitea_status_proxy -config example.json
2025/10/30 12:53:18 [I] server up and listening on :3000
```
Now the proxy should be able to accept requests under: `localhost:3000/repos/{owner}/{repo}/statuses/{sha}`, the token to be used when authenticating to the proxy must be in the `keys` list of the configuration json file (example.json above)
### example:
On a separate terminal, you can use curl to post a status to the proxy, if the GITEA_TOKEN has permissions on the target
repository, it will result in a new status being set for the given commit
```
curl -X 'POST' \
'localhost:3000/repos/szarate/test-actions-gitea/statuses/cd5847c92fb65a628bdd6015f96ee7e569e1ad6e4fc487acc149b52e788262f9' \
-H 'accept: application/json' \
-H 'Authorization: token $YOUR_TOKEN_GOES_HERE' \
-H 'Content-Type: application/json' \
-d '{
"context": "Proxy test",
"description": "Status posted from the proxy",
"state": "success",
"target_url": "https://src.opensuse.org"
}'
```
After this you should be able to the results in the pull request, e.g from above: https://src.opensuse.org/szarate/test-actions-gitea/pulls/1

View File

@@ -1,65 +1,41 @@
Group Review Bot Group Review Bot
================ ================
This workaround is mainly needed because Gitea does not track which team member performed a review on behalf of a team. Areas of responsibility
-----------------------
Main Tasks 1. Is used to handle reviews associated with groups defined in the
---------- ProjectGit.
Awaits a comment in the format “@groupreviewbot-name: approve”, then approves the PR with the comment “<user> approved a review on behalf of <groupreviewbot-name>.” 2. Assumes: workflow-pr needs to associate and define the PR set from
which the groups.json is read (Base of the PrjGit PR)
Target Usage Target Usage
------------ ------------
Projects where policy reviews are required. Projects where policy reviews are required.
Configuration Configiuration
-------------- --------------
The bot is configured via the `ReviewGroups` field in the `workflow.config` file, located in the ProjectGit repository. Groups are defined in the workflow.config inside the project git. They take following options,
See `ReviewGroups` in the [workflow-pr configuration](../workflow-pr/README.md#config-file).
```json
{ {
... ...
"ReviewGroups": [ ReviewGroups: [
{ {
"Name": "name of the group user", "Name": "name of the group user",
"Reviewers": ["members", "of", "group"], "Reviewers": ["members", "of", "group"],
"Silent": "(true, false) -- if true, do not explicitly require review requests of group members" "Silent": (true, false) -- if true, do not explicitly require review requests of group members
} },
], ],
... ...
} }
```
Server configuration
--------------------------
**Configuration file:**
| Field | Type | Notes |
| ----- | ----- | ----- |
| root | Array of string | Format **org/repo\#branch** |
Requirements Requirements
------------ ------------
Gitea token with following permissions: * Gitea token to:
- R/W PullRequest + R/W PullRequest
- R/W Notification + R/W Notification
- R User + R User
Env Variables
-------------
The following variables can be used (and override) command line parameters.
* `AUTOGITS_CONFIG` - config file location
* `AUTOGITS_URL` - Gitea URL
* `AUTOGITS_RABBITURL` - RabbitMQ url
* `AUTOGITS_DEBUG` - when set, debug level logging enabled
Authentication env variables
* `GITEA_TOKEN` - Gitea user token
* `AMQP_USERNAME`, `AMQP_PASSWORD` - username and password for rabbitmq

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"log" "log"
"net/url" "net/url"
"os"
"regexp" "regexp"
"runtime/debug" "runtime/debug"
"slices" "slices"
@@ -18,23 +17,20 @@ import (
"src.opensuse.org/autogits/common/gitea-generated/models" "src.opensuse.org/autogits/common/gitea-generated/models"
) )
type ReviewBot struct { var configs common.AutogitConfigs
configs common.AutogitConfigs var acceptRx *regexp.Regexp
acceptRx *regexp.Regexp var rejectRx *regexp.Regexp
rejectRx *regexp.Regexp var groupName string
groupName string
gitea common.Gitea func InitRegex(newGroupName string) {
groupName = newGroupName
acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
rejectRx = regexp.MustCompile("^:\\s*")
} }
func (bot *ReviewBot) InitRegex(newGroupName string) { func ParseReviewLine(reviewText string) (bool, string) {
bot.groupName = newGroupName
bot.acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
bot.rejectRx = regexp.MustCompile("^:\\s*")
}
func (bot *ReviewBot) ParseReviewLine(reviewText string) (bool, string) {
line := strings.TrimSpace(reviewText) line := strings.TrimSpace(reviewText)
groupTextName := "@" + bot.groupName groupTextName := "@" + groupName
glen := len(groupTextName) glen := len(groupTextName)
if len(line) < glen || line[0:glen] != groupTextName { if len(line) < glen || line[0:glen] != groupTextName {
return false, line return false, line
@@ -54,20 +50,20 @@ func (bot *ReviewBot) ParseReviewLine(reviewText string) (bool, string) {
return false, line return false, line
} }
func (bot *ReviewBot) ReviewAccepted(reviewText string) bool { func ReviewAccepted(reviewText string) bool {
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") { for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
if matched, reviewLine := bot.ParseReviewLine(line); matched { if matched, reviewLine := ParseReviewLine(line); matched {
return bot.acceptRx.MatchString(reviewLine) return acceptRx.MatchString(reviewLine)
} }
} }
return false return false
} }
func (bot *ReviewBot) ReviewRejected(reviewText string) bool { func ReviewRejected(reviewText string) bool {
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") { for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
if matched, reviewLine := bot.ParseReviewLine(line); matched { if matched, reviewLine := ParseReviewLine(line); matched {
if bot.rejectRx.MatchString(reviewLine) { if rejectRx.MatchString(reviewLine) {
return !bot.acceptRx.MatchString(reviewLine) return !acceptRx.MatchString(reviewLine)
} }
} }
} }
@@ -117,10 +113,10 @@ var commentStrings = []string{
"change_time_estimate", "change_time_estimate",
}*/ }*/
func (bot *ReviewBot) FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment { func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
for _, t := range timeline { for _, t := range timeline {
if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated { if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated {
if bot.ReviewAccepted(t.Body) || bot.ReviewRejected(t.Body) { if ReviewAccepted(t.Body) || ReviewRejected(t.Body) {
return t return t
} }
} }
@@ -129,9 +125,9 @@ func (bot *ReviewBot) FindAcceptableReviewInTimeline(user string, timeline []*mo
return nil return nil
} }
func (bot *ReviewBot) FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment { func FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
for _, t := range timeline { for _, t := range timeline {
if t.Type == common.TimelineCommentType_Review && t.User.UserName == bot.groupName && t.Created == t.Updated { if t.Type == common.TimelineCommentType_Review && t.User.UserName == groupName && t.Created == t.Updated {
return t return t
} }
} }
@@ -139,13 +135,13 @@ func (bot *ReviewBot) FindOurLastReviewInTimeline(timeline []*models.TimelineCom
return nil return nil
} }
func (bot *ReviewBot) UnrequestReviews(org, repo string, id int64, users []string) { func UnrequestReviews(gitea common.Gitea, org, repo string, id int64, users []string) {
if err := bot.gitea.UnrequestReview(org, repo, id, users...); err != nil { if err := gitea.UnrequestReview(org, repo, id, users...); err != nil {
common.LogError("Can't remove reviewrs after a review:", err) common.LogError("Can't remove reviewrs after a review:", err)
} }
} }
func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThread) { func ProcessNotifications(notification *models.NotificationThread, gitea common.Gitea) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
common.LogInfo("panic cought --- recovered") common.LogInfo("panic cought --- recovered")
@@ -153,7 +149,7 @@ func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThre
} }
}() }()
rx := regexp.MustCompile(`^/?api/v\d+/repos/(?<org>[_\.a-zA-Z0-9-]+)/(?<project>[_\.a-zA-Z0-9-]+)/(?:issues|pulls)/(?<num>[0-9]+)$`) rx := regexp.MustCompile(`^/?api/v\d+/repos/(?<org>[_a-zA-Z0-9-]+)/(?<project>[_a-zA-Z0-9-]+)/(?:issues|pulls)/(?<num>[0-9]+)$`)
subject := notification.Subject subject := notification.Subject
u, err := url.Parse(notification.Subject.URL) u, err := url.Parse(notification.Subject.URL)
if err != nil { if err != nil {
@@ -172,14 +168,14 @@ func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThre
id, _ := strconv.ParseInt(match[3], 10, 64) id, _ := strconv.ParseInt(match[3], 10, 64)
common.LogInfo("processing:", fmt.Sprintf("%s/%s!%d", org, repo, id)) common.LogInfo("processing:", fmt.Sprintf("%s/%s!%d", org, repo, id))
pr, err := bot.gitea.GetPullRequest(org, repo, id) pr, err := gitea.GetPullRequest(org, repo, id)
if err != nil { if err != nil {
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err) common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
return return
} }
if err := bot.ProcessPR(pr); err == nil && !common.IsDryRun { if err := ProcessPR(pr); err == nil && !common.IsDryRun {
if err := bot.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)
} }
} else if err != nil && err != ReviewNotFinished { } else if err != nil && err != ReviewNotFinished {
@@ -189,24 +185,24 @@ func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThre
var ReviewNotFinished = fmt.Errorf("Review is not finished") var ReviewNotFinished = fmt.Errorf("Review is not finished")
func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error { func ProcessPR(pr *models.PullRequest) error {
org := pr.Base.Repo.Owner.UserName org := pr.Base.Repo.Owner.UserName
repo := pr.Base.Repo.Name repo := pr.Base.Repo.Name
id := pr.Index id := pr.Index
found := false found := false
for _, reviewer := range pr.RequestedReviewers { for _, reviewer := range pr.RequestedReviewers {
if reviewer != nil && reviewer.UserName == bot.groupName { if reviewer != nil && reviewer.UserName == groupName {
found = true found = true
break break
} }
} }
if !found { if !found {
common.LogInfo(" review is not requested for", bot.groupName) common.LogInfo(" review is not requested for", groupName)
return nil return nil
} }
config := bot.configs.GetPrjGitConfig(org, repo, pr.Base.Name) config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
if config == nil { if config == nil {
return fmt.Errorf("Cannot find config for: %s", pr.URL) return fmt.Errorf("Cannot find config for: %s", pr.URL)
} }
@@ -216,17 +212,17 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
return nil return nil
} }
reviews, err := bot.gitea.GetPullRequestReviews(org, repo, id) reviews, err := gitea.GetPullRequestReviews(org, repo, id)
if err != nil { if err != nil {
return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err) return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
} }
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(bot.gitea, bot.groupName, pr.Head.Sha, org, repo, id) timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
if err != nil { if err != nil {
return fmt.Errorf("Failed to fetch timeline to review. %w", err) return fmt.Errorf("Failed to fetch timeline to review. %w", err)
} }
groupConfig, err := config.GetReviewGroup(bot.groupName) groupConfig, err := config.GetReviewGroup(groupName)
if err != nil { if err != nil {
return fmt.Errorf("Failed to fetch review group. %w", err) return fmt.Errorf("Failed to fetch review group. %w", err)
} }
@@ -237,30 +233,30 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
// pr.Head.Sha // pr.Head.Sha
for _, reviewer := range requestReviewers { for _, reviewer := range requestReviewers {
if review := bot.FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil { if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
if bot.ReviewAccepted(review.Body) { if ReviewAccepted(review.Body) {
if !common.IsDryRun { if !common.IsDryRun {
text := reviewer + " approved a review on behalf of " + bot.groupName text := reviewer + " approved a review on behalf of " + groupName
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text { if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateApproved, text) _, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
if err != nil { if err != nil {
common.LogError(" -> failed to write approval comment", err) common.LogError(" -> failed to write approval comment", err)
} }
bot.UnrequestReviews(org, repo, id, requestReviewers) UnrequestReviews(gitea, org, repo, id, requestReviewers)
} }
} }
common.LogInfo(" -> approved by", reviewer) common.LogInfo(" -> approved by", reviewer)
common.LogInfo(" review at", review.Created) common.LogInfo(" review at", review.Created)
return nil return nil
} else if bot.ReviewRejected(review.Body) { } else if ReviewRejected(review.Body) {
if !common.IsDryRun { if !common.IsDryRun {
text := reviewer + " requested changes on behalf of " + bot.groupName + ". See " + review.HTMLURL text := reviewer + " requested changes on behalf of " + groupName + ". See " + review.HTMLURL
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text { if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, text) _, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
if err != nil { if err != nil {
common.LogError(" -> failed to write rejecting comment", err) common.LogError(" -> failed to write rejecting comment", err)
} }
bot.UnrequestReviews(org, repo, id, requestReviewers) UnrequestReviews(gitea, org, repo, id, requestReviewers)
} }
} }
common.LogInfo(" -> declined by", reviewer) common.LogInfo(" -> declined by", reviewer)
@@ -274,7 +270,7 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
if !groupConfig.Silent && len(requestReviewers) > 0 { if !groupConfig.Silent && len(requestReviewers) > 0 {
common.LogDebug(" Requesting reviews for:", requestReviewers) common.LogDebug(" Requesting reviews for:", requestReviewers)
if !common.IsDryRun { if !common.IsDryRun {
if _, err := bot.gitea.RequestReviews(pr, requestReviewers...); err != nil { if _, err := gitea.RequestReviews(pr, requestReviewers...); err != nil {
common.LogDebug(" -> err:", err) common.LogDebug(" -> err:", err)
} }
} else { } else {
@@ -287,40 +283,42 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
// add a helpful comment, if not yet added // add a helpful comment, if not yet added
found_help_comment := false found_help_comment := false
for _, t := range timeline { for _, t := range timeline {
if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == bot.groupName { if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == groupName {
found_help_comment = true found_help_comment = true
break break
} }
} }
if !found_help_comment && !common.IsDryRun { if !found_help_comment && !common.IsDryRun {
helpComment := fmt.Sprintln("Review by", bot.groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+ helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
"Do **not** use standard review interface to review on behalf of the group.\n"+ "Do **not** use standard review interface to review on behalf of the group.\n"+
"To accept the review on behalf of the group, create the following comment: `@"+bot.groupName+": approve`.\n"+ "To accept the review on behalf of the group, create the following comment: `@"+groupName+": approve`.\n"+
"To request changes on behalf of the group, create the following comment: `@"+bot.groupName+": decline` followed with lines justifying the decision.\n"+ "To request changes on behalf of the group, create the following comment: `@"+groupName+": decline` followed with lines justifying the decision.\n"+
"Future edits of the comments are ignored, a new comment is required to change the review state.") "Future edits of the comments are ignored, a new comment is required to change the review state.")
if slices.Contains(groupConfig.Reviewers, pr.User.UserName) { if slices.Contains(groupConfig.Reviewers, pr.User.UserName) {
helpComment = helpComment + "\n\n" + helpComment = helpComment + "\n\n" +
"Submitter is member of this review group, hence they are excluded from being one of the reviewers here" "Submitter is member of this review group, hence they are excluded from being one of the reviewers here"
} }
bot.gitea.AddComment(pr, helpComment) gitea.AddComment(pr, helpComment)
} }
return ReviewNotFinished return ReviewNotFinished
} }
func (bot *ReviewBot) PeriodReviewCheck() { func PeriodReviewCheck() {
notifications, err := bot.gitea.GetNotifications(common.GiteaNotificationType_Pull, 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
} }
for _, notification := range notifications { for _, notification := range notifications {
bot.ProcessNotifications(notification) ProcessNotifications(notification, gitea)
} }
} }
var gitea common.Gitea
func main() { func main() {
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews") giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews")
rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent") rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent")
@@ -330,24 +328,6 @@ func main() {
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging") flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
flag.Parse() flag.Parse()
if err := common.SetLoggingLevelFromString(*logging); err != nil {
common.LogError(err.Error())
return
}
if cf := os.Getenv("AUTOGITS_CONFIG"); len(cf) > 0 {
*configFile = cf
}
if url := os.Getenv("AUTOGITS_URL"); len(url) > 0 {
*giteaUrl = url
}
if url := os.Getenv("AUTOGITS_RABBITURL"); len(url) > 0 {
*rabbitMqHost = url
}
if debug := os.Getenv("AUTOGITS_DEBUG"); len(debug) > 0 {
common.SetLoggingLevel(common.LogLevelDebug)
}
args := flag.Args() args := flag.Args()
if len(args) != 1 { if len(args) != 1 {
log.Println(" syntax:") log.Println(" syntax:")
@@ -356,7 +336,7 @@ func main() {
flag.Usage() flag.Usage()
return return
} }
targetGroupName := args[0] groupName = args[0]
if *configFile == "" { if *configFile == "" {
common.LogError("Missing config file") common.LogError("Missing config file")
@@ -379,35 +359,36 @@ func main() {
return return
} }
giteaTransport := common.AllocateGiteaTransport(*giteaUrl) gitea = common.AllocateGiteaTransport(*giteaUrl)
configs, err := common.ResolveWorkflowConfigs(giteaTransport, configData) configs, err = common.ResolveWorkflowConfigs(gitea, configData)
if err != nil { if err != nil {
common.LogError("Cannot parse workflow configs:", err) common.LogError("Cannot parse workflow configs:", err)
return return
} }
reviewer, err := giteaTransport.GetCurrentUser() reviewer, err := gitea.GetCurrentUser()
if err != nil { if err != nil {
common.LogError("Cannot fetch review user:", err) common.LogError("Cannot fetch review user:", err)
return return
} }
if err := common.SetLoggingLevelFromString(*logging); err != nil {
common.LogError(err.Error())
return
}
if *interval < 1 { if *interval < 1 {
*interval = 1 *interval = 1
} }
bot := &ReviewBot{ InitRegex(groupName)
gitea: giteaTransport,
configs: configs,
}
bot.InitRegex(targetGroupName)
common.LogInfo(" ** processing group reviews for group:", bot.groupName) common.LogInfo(" ** processing group reviews for group:", groupName)
common.LogInfo(" ** username in Gitea:", reviewer.UserName) common.LogInfo(" ** username in Gitea:", reviewer.UserName)
common.LogInfo(" ** polling interval:", *interval, "min") common.LogInfo(" ** polling interval:", *interval, "min")
common.LogInfo(" ** connecting to RabbitMQ:", *rabbitMqHost) common.LogInfo(" ** connecting to RabbitMQ:", *rabbitMqHost)
if bot.groupName != reviewer.UserName { if groupName != reviewer.UserName {
common.LogError(" ***** Reviewer does not match group name. Aborting. *****") common.LogError(" ***** Reviewer does not match group name. Aborting. *****")
return return
} }
@@ -419,13 +400,10 @@ func main() {
} }
config_update := ConfigUpdatePush{ config_update := ConfigUpdatePush{
bot: bot,
config_modified: make(chan *common.AutogitConfig), config_modified: make(chan *common.AutogitConfig),
} }
process_issue_pr := IssueCommentProcessor{ process_issue_pr := IssueCommentProcessor{}
bot: bot,
}
configUpdates := &common.RabbitMQGiteaEventsProcessor{ configUpdates := &common.RabbitMQGiteaEventsProcessor{
Orgs: []string{}, Orgs: []string{},
@@ -435,7 +413,7 @@ func main() {
}, },
} }
configUpdates.Connection().RabbitURL = u configUpdates.Connection().RabbitURL = u
for _, c := range bot.configs { for _, c := range configs {
if org, _, _ := c.GetPrjGit(); !slices.Contains(configUpdates.Orgs, org) { if org, _, _ := c.GetPrjGit(); !slices.Contains(configUpdates.Orgs, org) {
configUpdates.Orgs = append(configUpdates.Orgs, org) configUpdates.Orgs = append(configUpdates.Orgs, org)
} }
@@ -448,17 +426,17 @@ func main() {
select { select {
case configTouched, ok := <-config_update.config_modified: case configTouched, ok := <-config_update.config_modified:
if ok { if ok {
for idx, c := range bot.configs { for idx, c := range configs {
if c == configTouched { if c == configTouched {
org, repo, branch := c.GetPrjGit() org, repo, branch := c.GetPrjGit()
prj := fmt.Sprintf("%s/%s#%s", org, repo, branch) prj := fmt.Sprintf("%s/%s#%s", org, repo, branch)
common.LogInfo("Detected config update for", prj) common.LogInfo("Detected config update for", prj)
new_config, err := common.ReadWorkflowConfig(bot.gitea, prj) new_config, err := common.ReadWorkflowConfig(gitea, prj)
if err != nil { if err != nil {
common.LogError("Failed parsing Project config for", prj, err) common.LogError("Failed parsing Project config for", prj, err)
} else { } else {
bot.configs[idx] = new_config configs[idx] = new_config
} }
} }
} }
@@ -468,7 +446,7 @@ func main() {
} }
} }
bot.PeriodReviewCheck() PeriodReviewCheck()
time.Sleep(time.Duration(*interval * int64(time.Minute))) time.Sleep(time.Duration(*interval * int64(time.Minute)))
} }
} }

View File

@@ -1,359 +1,6 @@
package main package main
import ( import "testing"
"fmt"
"testing"
"go.uber.org/mock/gomock"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
mock_common "src.opensuse.org/autogits/common/mock"
)
func TestProcessPR(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitea := mock_common.NewMockGitea(ctrl)
groupName := "testgroup"
bot := &ReviewBot{
gitea: mockGitea,
groupName: groupName,
}
bot.InitRegex(groupName)
org := "myorg"
repo := "myrepo"
prIndex := int64(1)
headSha := "abcdef123456"
pr := &models.PullRequest{
Index: prIndex,
URL: "http://gitea/pr/1",
State: "open",
Base: &models.PRBranchInfo{
Name: "main",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{
UserName: org,
},
},
},
Head: &models.PRBranchInfo{
Sha: headSha,
},
User: &models.User{
UserName: "submitter",
},
RequestedReviewers: []*models.User{
{UserName: groupName},
},
}
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#main",
ReviewGroups: []*common.ReviewGroup{
{
Name: groupName,
Reviewers: []string{"reviewer1", "reviewer2"},
},
},
}
bot.configs = common.AutogitConfigs{prjConfig}
t.Run("Review not requested for group", func(t *testing.T) {
prNoRequest := *pr
prNoRequest.RequestedReviewers = nil
err := bot.ProcessPR(&prNoRequest)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})
t.Run("PR is closed", func(t *testing.T) {
prClosed := *pr
prClosed.State = "closed"
err := bot.ProcessPR(&prClosed)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})
t.Run("Successful Approval", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
// reviewer1 approved in timeline
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": approve",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
expectedText := "reviewer1 approved a review on behalf of " + groupName
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Dry Run - No actions taken", func(t *testing.T) {
common.IsDryRun = true
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": approve",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
// No AddReviewComment or UnrequestReview should be called
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Approval already exists - No new comment", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
approvalText := "reviewer1 approved a review on behalf of " + groupName
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Review,
User: &models.User{UserName: groupName},
Body: approvalText,
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": approve",
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: groupName},
Body: "Help comment",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
// No AddReviewComment, UnrequestReview, or AddComment should be called
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Rejection already exists - No new comment", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
rejectionText := "reviewer1 requested changes on behalf of " + groupName + ". See http://gitea/comment/123"
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Review,
User: &models.User{UserName: groupName},
Body: rejectionText,
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": decline",
HTMLURL: "http://gitea/comment/123",
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: groupName},
Body: "Help comment",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Pending review - Help comment already exists", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: groupName},
Body: "Some help comment",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
// It will try to request reviews
mockGitea.EXPECT().RequestReviews(pr, "reviewer1", "reviewer2").Return(nil, nil)
// AddComment should NOT be called because bot already has a comment in timeline
err := bot.ProcessPR(pr)
if err != ReviewNotFinished {
t.Errorf("Expected ReviewNotFinished error, got %v", err)
}
})
t.Run("Submitter is group member - Excluded from review request", func(t *testing.T) {
common.IsDryRun = false
prSubmitterMember := *pr
prSubmitterMember.User = &models.User{UserName: "reviewer1"}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(nil, nil)
mockGitea.EXPECT().RequestReviews(&prSubmitterMember, "reviewer2").Return(nil, nil)
mockGitea.EXPECT().AddComment(&prSubmitterMember, gomock.Any()).Return(nil)
err := bot.ProcessPR(&prSubmitterMember)
if err != ReviewNotFinished {
t.Errorf("Expected ReviewNotFinished error, got %v", err)
}
})
t.Run("Successful Rejection", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer2"},
Body: "@" + groupName + ": decline",
HTMLURL: "http://gitea/comment/999",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
expectedText := "reviewer2 requested changes on behalf of " + groupName + ". See http://gitea/comment/999"
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateRequestChanges, expectedText).Return(nil, nil)
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Config not found", func(t *testing.T) {
bot.configs = common.AutogitConfigs{}
err := bot.ProcessPR(pr)
if err == nil {
t.Error("Expected error when config is missing, got nil")
}
})
t.Run("Gitea error in GetPullRequestReviews", func(t *testing.T) {
bot.configs = common.AutogitConfigs{prjConfig}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
err := bot.ProcessPR(pr)
if err == nil {
t.Error("Expected error from gitea, got nil")
}
})
}
func TestProcessNotifications(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitea := mock_common.NewMockGitea(ctrl)
groupName := "testgroup"
bot := &ReviewBot{
gitea: mockGitea,
groupName: groupName,
}
bot.InitRegex(groupName)
org := "myorg"
repo := "myrepo"
prIndex := int64(123)
notificationID := int64(456)
notification := &models.NotificationThread{
ID: notificationID,
Subject: &models.NotificationSubject{
URL: fmt.Sprintf("http://gitea/api/v1/repos/%s/%s/pulls/%d", org, repo, prIndex),
},
}
t.Run("Notification Success", func(t *testing.T) {
common.IsDryRun = false
pr := &models.PullRequest{
Index: prIndex,
Base: &models.PRBranchInfo{
Name: "main",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
Head: &models.PRBranchInfo{
Sha: "headsha",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
User: &models.User{UserName: "submitter"},
RequestedReviewers: []*models.User{{UserName: groupName}},
}
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(pr, nil)
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#main",
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
}
bot.configs = common.AutogitConfigs{prjConfig}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "r1"},
Body: "@" + groupName + ": approve",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
expectedText := "r1 approved a review on behalf of " + groupName
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
mockGitea.EXPECT().SetNotificationRead(notificationID).Return(nil)
bot.ProcessNotifications(notification)
})
t.Run("Invalid Notification URL", func(t *testing.T) {
badNotification := &models.NotificationThread{
Subject: &models.NotificationSubject{
URL: "http://gitea/invalid/url",
},
}
bot.ProcessNotifications(badNotification)
})
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
bot.ProcessNotifications(notification)
})
}
func TestReviewApprovalCheck(t *testing.T) { func TestReviewApprovalCheck(t *testing.T) {
tests := []struct { tests := []struct {
@@ -413,78 +60,16 @@ func TestReviewApprovalCheck(t *testing.T) {
InString: "@group2: disapprove", InString: "@group2: disapprove",
Rejected: true, Rejected: true,
}, },
{
Name: "Whitespace before colon",
GroupName: "group",
InString: "@group : LGTM",
Approved: true,
},
{
Name: "No whitespace after colon",
GroupName: "group",
InString: "@group:LGTM",
Approved: true,
},
{
Name: "Leading and trailing whitespace on line",
GroupName: "group",
InString: " @group: LGTM ",
Approved: true,
},
{
Name: "Multiline: Approved on second line",
GroupName: "group",
InString: "Random noise\n@group: approved",
Approved: true,
},
{
Name: "Multiline: Multiple group mentions, first wins",
GroupName: "group",
InString: "@group: decline\n@group: approve",
Rejected: true,
},
{
Name: "Multiline: Approved on second line",
GroupName: "group",
InString: "noise\n@group: approve\nmore noise",
Approved: true,
},
{
Name: "Not at start of line (even with whitespace)",
GroupName: "group",
InString: "Hello @group: approve",
Approved: false,
},
{
Name: "Rejecting with reason",
GroupName: "group",
InString: "@group: decline because of X, Y and Z",
Rejected: true,
},
{
Name: "No colon after group",
GroupName: "group",
InString: "@group LGTM",
Approved: false,
Rejected: false,
},
{
Name: "Invalid char after group",
GroupName: "group",
InString: "@group! LGTM",
Approved: false,
Rejected: false,
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
bot := &ReviewBot{} InitRegex(test.GroupName)
bot.InitRegex(test.GroupName)
if r := bot.ReviewAccepted(test.InString); r != test.Approved { if r := ReviewAccepted(test.InString); r != test.Approved {
t.Error("ReviewAccepted() returned", r, "expecting", test.Approved) t.Error("ReviewAccepted() returned", r, "expecting", test.Approved)
} }
if r := bot.ReviewRejected(test.InString); r != test.Rejected { if r := ReviewRejected(test.InString); r != test.Rejected {
t.Error("ReviewRejected() returned", r, "expecting", test.Rejected) t.Error("ReviewRejected() returned", r, "expecting", test.Rejected)
} }
}) })

View File

@@ -7,9 +7,7 @@ import (
"src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common"
) )
type IssueCommentProcessor struct { type IssueCommentProcessor struct{}
bot *ReviewBot
}
func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error { func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
if req.Type != common.RequestType_IssueComment { if req.Type != common.RequestType_IssueComment {
@@ -21,15 +19,14 @@ func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
repo := data.Repository.Name repo := data.Repository.Name
index := int64(data.Issue.Number) index := int64(data.Issue.Number)
pr, err := s.bot.gitea.GetPullRequest(org, repo, index) pr, err := gitea.GetPullRequest(org, repo, index)
if err != nil { if err != nil {
return fmt.Errorf("Failed to fetch PullRequest from event: %s/%s!%d Error: %w", org, repo, index, err) return fmt.Errorf("Failed to fetch PullRequest from event: %s/%s!%d Error: %w", org, repo, index, err)
} }
return s.bot.ProcessPR(pr) return ProcessPR(pr)
} }
type ConfigUpdatePush struct { type ConfigUpdatePush struct {
bot *ReviewBot
config_modified chan *common.AutogitConfig config_modified chan *common.AutogitConfig
} }
@@ -49,7 +46,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
} }
branch := data.Ref[len(branch_ref):] branch := data.Ref[len(branch_ref):]
c := s.bot.configs.GetPrjGitConfig(org, repo, branch) c := configs.GetPrjGitConfig(org, repo, branch)
if c == nil { if c == nil {
return nil return nil
} }
@@ -67,7 +64,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
} }
if modified_config { if modified_config {
for _, config := range s.bot.configs { for _, config := range configs {
if o, r, _ := config.GetPrjGit(); o == org && r == repo { if o, r, _ := config.GetPrjGit(); o == org && r == repo {
s.config_modified <- config s.config_modified <- config
} }

View File

@@ -1,203 +0,0 @@
package main
import (
"fmt"
"testing"
"go.uber.org/mock/gomock"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
mock_common "src.opensuse.org/autogits/common/mock"
)
func TestIssueCommentProcessor(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitea := mock_common.NewMockGitea(ctrl)
groupName := "testgroup"
bot := &ReviewBot{
gitea: mockGitea,
groupName: groupName,
}
bot.InitRegex(groupName)
processor := &IssueCommentProcessor{bot: bot}
org := "myorg"
repo := "myrepo"
index := 123
event := &common.IssueCommentWebhookEvent{
Repository: &common.Repository{
Name: repo,
Owner: &common.Organization{
Username: org,
},
},
Issue: &common.IssueDetail{
Number: index,
},
}
req := &common.Request{
Type: common.RequestType_IssueComment,
Data: event,
}
t.Run("Successful Processing", func(t *testing.T) {
pr := &models.PullRequest{
Index: int64(index),
Base: &models.PRBranchInfo{
Name: "main",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
Head: &models.PRBranchInfo{
Sha: "headsha",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
User: &models.User{UserName: "submitter"},
RequestedReviewers: []*models.User{{UserName: groupName}},
}
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(pr, nil)
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#main",
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
}
bot.configs = common.AutogitConfigs{prjConfig}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, int64(index)).Return(nil, nil)
mockGitea.EXPECT().GetTimeline(org, repo, int64(index)).Return(nil, nil)
mockGitea.EXPECT().RequestReviews(pr, "r1").Return(nil, nil)
mockGitea.EXPECT().AddComment(pr, gomock.Any()).Return(nil)
err := processor.ProcessFunc(req)
if err != ReviewNotFinished {
t.Errorf("Expected ReviewNotFinished, got %v", err)
}
})
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(nil, fmt.Errorf("gitea error"))
err := processor.ProcessFunc(req)
if err == nil {
t.Error("Expected error, got nil")
}
})
t.Run("Wrong Request Type", func(t *testing.T) {
wrongReq := &common.Request{Type: common.RequestType_Push}
err := processor.ProcessFunc(wrongReq)
if err == nil {
t.Error("Expected error for wrong request type, got nil")
}
})
}
func TestConfigUpdatePush(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
groupName := "testgroup"
bot := &ReviewBot{
groupName: groupName,
}
bot.InitRegex(groupName)
configChan := make(chan *common.AutogitConfig, 1)
processor := &ConfigUpdatePush{
bot: bot,
config_modified: configChan,
}
org := "myorg"
repo := "myrepo"
branch := "main"
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#" + branch,
Organization: org,
Branch: branch,
}
bot.configs = common.AutogitConfigs{prjConfig}
event := &common.PushWebhookEvent{
Ref: "refs/heads/" + branch,
Repository: &common.Repository{
Name: repo,
Owner: &common.Organization{
Username: org,
},
},
Commits: []common.Commit{
{
Modified: []string{common.ProjectConfigFile},
},
},
}
req := &common.Request{
Type: common.RequestType_Push,
Data: event,
}
t.Run("Config Modified", func(t *testing.T) {
err := processor.ProcessFunc(req)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
select {
case modified := <-configChan:
if modified != prjConfig {
t.Errorf("Expected modified config to be %v, got %v", prjConfig, modified)
}
default:
t.Error("Expected config modification signal, but none received")
}
})
t.Run("No Config Modified", func(t *testing.T) {
noConfigEvent := *event
noConfigEvent.Commits = []common.Commit{{Modified: []string{"README.md"}}}
noConfigReq := &common.Request{Type: common.RequestType_Push, Data: &noConfigEvent}
err := processor.ProcessFunc(noConfigReq)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
select {
case <-configChan:
t.Error("Did not expect config modification signal")
default:
// Success
}
})
t.Run("Wrong Branch Ref", func(t *testing.T) {
wrongBranchEvent := *event
wrongBranchEvent.Ref = "refs/tags/v1.0"
wrongBranchReq := &common.Request{Type: common.RequestType_Push, Data: &wrongBranchEvent}
err := processor.ProcessFunc(wrongBranchReq)
if err == nil {
t.Error("Expected error for wrong branch ref, got nil")
}
})
t.Run("Config Not Found", func(t *testing.T) {
bot.configs = common.AutogitConfigs{}
err := processor.ProcessFunc(req)
if err != nil {
t.Errorf("Expected nil error even if config not found, got %v", err)
}
})
}

View File

@@ -4,15 +4,11 @@ OBS Staging Bot
Build a PR against a ProjectGit, if review is requested. Build a PR against a ProjectGit, if review is requested.
Main Tasks Areas of Responsibility
---------- -----------------------
* A build in OBS is initiated when a review for this bot is requested. * Monitors Notification API in Gitea for review requests
* The overall build status is reported: * Reviews Package build results in OBS for all changed packages in ProjectGit PR
* Build successful
* Build failed
* It checks the build status only for the involved packages compared to the last state of the project for all architectures and all flavors.
* It adds an svg with detailed building status.
Target Usage Target Usage
@@ -25,48 +21,22 @@ Configuration File
------------------ ------------------
Bot reads `staging.config` from the project git or the PR to the project git. Bot reads `staging.config` from the project git or the PR to the project git.
It's a JSON file with following syntax: It's a JSON file with following syntax
```json ```
{ {
"ObsProject": "SUSE:SLFO:1.2", "ObsProject": "home:foo:project",
"StagingProject": "SUSE:SLFO:1.2:PullRequest", "StagingProject": "home:foo:project:staging",
"QA": [ "QA": [
{ {
"Name": "SLES", "Name": "ProjectBuild",
"Origin": "SUSE:SLFO:Products:SLES:16.0", "Origin": "home:foo:product:images"
"BuildDisableRepos": ["product"] }
} ]
]
} }
``` ```
| Field name | Details | Mandatory | Type | Allowed Values | Default | * ObsProject: (**required**) Project where the base project is built. Builds in this project will be used to compare to builds based on sources from the PR
| ----- | ----- | ----- | ----- | ----- | ----- | * StagingProject: template project that will be used as template for the staging project. Omitting this will use the ObsProject repositories to create the staging. Staging project will be created under the template, or in the bot's home directory if not specified.
| *ObsProject* | Product OBS project. Builds in this project will be used to compare to builds based on sources from the PR. | yes | string | `[a-zA-Z0-9-_:]+` | | * QA: set of projects to build ontop of the binaries built in staging.
| *StagingProject* | Used both as base project and prefix for all OBS staging projects. Upon being added as a reviewer to a PrjGit PR, this bot automatically generates an OBS project named *StagingProject:<PR_Number>*. It must be a sub-project of the *ObsProject*. | yes | string | `[a-zA-Z0-9-_:]+` | |
| *QA* | Crucial for generating a product build (such as an ISO or FTP tree) that incorporates the packages. | no | array of objects | | |
| *QA > Name* | Suffix for the QA OBS staging project. The project is named *StagingProject:<PR_Number>:Name*. | no | string | | |
| *QA > Origin* | OBS reference project | no | string | | |
| *QA > BuildDisableRepos* | The names of OBS repositories to build-disable, if any. | no | array of strings | | [] |
Details
-------
* **OBS staging projects are deleted** when the relative PrjGit PR is closed or merged.
* **PrjGit PR - staging project**
* The OBS staging project utilizes an **scmsync** tag, configured with the `onlybuild` flag, to exclusively build packages associated with this specific PrjGit PR.
* The **build config** is inherited from the PrjGit PR config file (even if unchanged).
* The **project meta** creates a standard repository following the StagingProject as a project path.
* The base *StagingProject* has the macro **FromScratch:** set in its config, which prevents inheriting the configuration from the included project paths.
* The bot copies the project maintainers from *StagingProject* to the specific staging project (*StagingProject:<PR_Number>*).
* The bot reports “Build successful” only if the build is successful for all repositories and all architectures.
* **PrjGit PR - QA staging project**
* The QA staging project is meant for building the product; the relative build config is inherited from the `QA > Origin` project.
* In this case, the **scmsync** tag is inherited from the `QA > Origin` project.
* It is desirable in some cases to avoid building some specific build service repositories when not needed. In this case, `QA > BuildDisableRepos` can be specified.
These repositories would be disabled in the project meta when generating the QA project.

View File

@@ -109,11 +109,6 @@ const (
BuildStatusSummaryUnknown = 4 BuildStatusSummaryUnknown = 4
) )
type DisableFlag struct {
XMLName string `xml:"disable"`
Name string `xml:"repository,attr"`
}
func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatusSummary { func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatusSummary {
if _, finished := refProject.BuildResultSummary(); !finished { if _, finished := refProject.BuildResultSummary(); !finished {
common.LogDebug("refProject not finished building??") common.LogDebug("refProject not finished building??")
@@ -382,7 +377,7 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
// stagingProject:$buildProject // stagingProject:$buildProject
// ^- stagingProject:$buildProject:$subProjectName (based on templateProject) // ^- stagingProject:$buildProject:$subProjectName (based on templateProject)
func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string, buildDisableRepos []string) error { func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string) error {
common.LogDebug("Setup QA sub projects") common.LogDebug("Setup QA sub projects")
templateMeta, err := ObsClient.GetProjectMeta(templateProject) templateMeta, err := ObsClient.GetProjectMeta(templateProject)
if err != nil { if err != nil {
@@ -391,42 +386,6 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
} }
// patch baseMeta to become the new project // patch baseMeta to become the new project
templateMeta.Name = stagingProject + ":" + subProjectName templateMeta.Name = stagingProject + ":" + subProjectName
// freeze tag for now
if len(templateMeta.ScmSync) > 0 {
repository, err := url.Parse(templateMeta.ScmSync)
if err != nil {
panic(err)
}
common.LogDebug("getting data for ", repository.EscapedPath())
split := strings.Split(repository.EscapedPath(), "/")
org, repo := split[1], split[2]
common.LogDebug("getting commit for ", org, " repo ", repo, " fragment ", repository.Fragment)
branch, err := gitea.GetCommit(org, repo, repository.Fragment)
if err != nil {
panic(err)
}
// set expanded commit url
repository.Fragment = branch.SHA
templateMeta.ScmSync = repository.String()
common.LogDebug("Setting scmsync url to ", templateMeta.ScmSync)
}
// Build-disable repositories if asked
if len(buildDisableRepos) > 0 {
toDisable := make([]DisableFlag, len(buildDisableRepos))
for idx, repositoryName := range buildDisableRepos {
toDisable[idx] = DisableFlag{Name: repositoryName}
}
output, err := xml.Marshal(toDisable)
if err != nil {
common.LogError("error while marshalling, skipping BuildDisableRepos: ", err)
} else {
templateMeta.BuildFlags.Contents += string(output)
}
}
// Cleanup ReleaseTarget and modify affected path entries // Cleanup ReleaseTarget and modify affected path entries
for idx, r := range templateMeta.Repositories { for idx, r := range templateMeta.Repositories {
templateMeta.Repositories[idx].ReleaseTargets = nil templateMeta.Repositories[idx].ReleaseTargets = nil
@@ -941,8 +900,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
CreateQASubProject(stagingConfig, git, gitea, pr, CreateQASubProject(stagingConfig, git, gitea, pr,
stagingProject, stagingProject,
setup.Origin, setup.Origin,
setup.Name, setup.Name)
setup.BuildDisableRepos)
msg = msg + ObsWebHost + "/project/show/" + msg = msg + ObsWebHost + "/project/show/" +
stagingProject + ":" + setup.Name + "\n" stagingProject + ":" + setup.Name + "\n"
} }
@@ -1086,7 +1044,6 @@ func main() {
ObsWebHost = ObsWebHostFromApiHost(*obsApiHost) ObsWebHost = ObsWebHostFromApiHost(*obsApiHost)
} }
common.LogDebug("OBS Gitea Host:", GiteaUrl)
common.LogDebug("OBS Web Host:", ObsWebHost) common.LogDebug("OBS Web Host:", ObsWebHost)
common.LogDebug("OBS API Host:", *obsApiHost) common.LogDebug("OBS API Host:", *obsApiHost)

View File

@@ -1,10 +1,7 @@
OBS Status Service OBS Status Service
================== ==================
Reports build status of OBS service as an easily to produce SVG. Repository Reports build status of OBS service as an easily to produce SVG
results (build results) are cached for 10 seconds and repository listing
for OBS instance are cached for 5 minutes -- new repositories take up to
5 minutes to be visible.
Requests for individual build results: Requests for individual build results:
@@ -20,31 +17,19 @@ Get requests for / will also return 404 statu normally. If the Backend redis
server is not available, it will return 500 server is not available, it will return 500
By default, SVG output is generated, suitable for inclusion. But JSON and XML
output is possible by setting `Accept:` request header
| Accept Request Header | Output format
|------------------------|---------------------
| | SVG image
| application/json | JSON data
| application/obs+xml | XML output
Areas of Responsibility Areas of Responsibility
----------------------- -----------------------
* Fetch and cache internal data from OBS and present it in usable format: * Monitors RabbitMQ interface for notification of OBS package and project status
+ Generate SVG output for specific OBS project or package * Produces SVG output based on GET request
+ Generate JSON/XML output for automated processing * Cache results (sqlite) and periodically update results from OBS (in case of messages are missing)
* Low-overhead
Target Usage Target Usage
------------ ------------
* inside README.md of package git or project git * README.md of package git or project git
* comment section of a Gitea PR * comment section of a Gitea PR
* automated build result processing
Running Running
------- -------
@@ -57,4 +42,3 @@ Default parameters can be changed by env variables
| `OBS_STATUS_SERVICE_LISTEN` | [::1]:8080 | Listening address and port | `OBS_STATUS_SERVICE_LISTEN` | [::1]:8080 | Listening address and port
| `OBS_STATUS_SERVICE_CERT` | /run/obs-status-service.pem | Location of certificate file for service | `OBS_STATUS_SERVICE_CERT` | /run/obs-status-service.pem | Location of certificate file for service
| `OBS_STATUS_SERVICE_KEY` | /run/obs-status-service.pem | Location of key file for service | `OBS_STATUS_SERVICE_KEY` | /run/obs-status-service.pem | Location of key file for service
| `REDIS` | | OBS's Redis instance URL

Binary file not shown.

View File

@@ -1,9 +1,6 @@
package main package main
import ( import (
"compress/bzip2"
"encoding/json"
"io"
"os" "os"
"testing" "testing"
@@ -85,36 +82,3 @@ func TestStatusSvg(t *testing.T) {
os.WriteFile("testpackage.svg", PackageStatusSummarySvg("pkg2", data), 0o777) os.WriteFile("testpackage.svg", PackageStatusSummarySvg("pkg2", data), 0o777)
os.WriteFile("testproject.svg", ProjectStatusSummarySvg(data), 0o777) os.WriteFile("testproject.svg", ProjectStatusSummarySvg(data), 0o777)
} }
func TestFactoryResults(t *testing.T) {
data, err := os.Open("factory.results.json.bz2")
if err != nil {
t.Fatal("Openning factory.results.json.bz2 failed:", err)
}
UncompressedData, err := io.ReadAll(bzip2.NewReader(data))
if err != nil {
t.Fatal("Reading factory.results.json.bz2 failed:", err)
}
var results []*common.BuildResult
if err := json.Unmarshal(UncompressedData, &results); err != nil {
t.Fatal("Failed parsing test data", err)
}
// add tests here
tests := []struct {
name string
}{
// add test data here
{
name: "First test",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// and test code here
})
}
}

View File

@@ -6,7 +6,6 @@ import (
"html" "html"
"net/url" "net/url"
"slices" "slices"
"strings"
) )
type SvgWriter struct { type SvgWriter struct {
@@ -134,7 +133,7 @@ func (svg *SvgWriter) WritePackageStatus(loglink, arch, status, detail string) {
} }
func (svg *SvgWriter) WriteProjectStatus(project, repo, arch, status string, count int) { func (svg *SvgWriter) WriteProjectStatus(project, repo, arch, status string, count int) {
u, err := url.Parse(*ObsUrl + "/project/monitor/" + url.PathEscape(project) + "?defaults=0&amp;" + url.QueryEscape(status) + "=1&amp;arch_" + url.QueryEscape(arch) + "=1&amp;repo_" + url.QueryEscape(strings.ReplaceAll(repo, ".", "_")) + "=1") u, err := url.Parse(*ObsUrl + "/project/monitor/" + url.PathEscape(project) + "?defaults=0&amp;" + url.QueryEscape(status) + "=1&amp;arch_" + url.QueryEscape(arch) + "=1&amp;repo_" + url.QueryEscape(repo) + "=1")
if err != nil { if err != nil {
return return
} }

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Group Review bot for %i
After=network-online.target
[Service]
Type=exec
ExecStart=/usr/bin/group-review %i
EnvironmentFile=-/etc/default/group-review/%i.env
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
[Install]
WantedBy=multi-user.target

View File

@@ -1,19 +0,0 @@
[Unit]
Description=WorkflowDirect git bot for %i
After=network-online.target
[Service]
Type=exec
ExecStart=/usr/bin/workflow-direct
EnvironmentFile=-/etc/default/%i/workflow-direct.env
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
RuntimeDirectory=%i
# SLES 15 doesn't have HOME set for dynamic users, so we improvise
BindReadOnlyPaths=/etc/default/%i/known_hosts:/etc/ssh/ssh_known_hosts /etc/default/%i/config.json:%t/%i/config.json /etc/default/%i/id_ed25519 /etc/default/%i/id_ed25519.pub
WorkingDirectory=%t/%i
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,9 @@
Purpose
-------
Automatically resolve git configlicts in .gitmodules if there's a conflict
due to a merge.
It uses HEAD and MERGE_HEAD to calculate merge base and pass it to the
conflict resolution

View File

@@ -0,0 +1,42 @@
package main
import (
"os"
"strings"
"src.opensuse.org/autogits/common"
)
func main() {
cwd, err := os.Getwd()
if err != nil {
common.LogError(err)
return
}
gh, err := common.AllocateGitWorkTree(cwd, "", "")
if err != nil {
common.LogError(err)
return
}
git, err := gh.ReadExistingPath("")
if err != nil {
common.LogError(err)
return
}
MergeBase := strings.TrimSpace(git.GitExecWithOutputOrPanic("", "merge-base", "HEAD", "MERGE_HEAD"))
status, err := git.GitStatus("")
if err != nil {
common.LogError(err)
return
}
for _, s := range status {
if s.Path == ".gitmodules" && s.Status == common.GitStatus_Unmerged {
if err := git.GitResolveSubmoduleFileConflict(s, "", MergeBase, "HEAD", "MERGE_HEAD"); err != nil {
common.LogError(err)
}
}
}
}

View File

@@ -1,51 +1,33 @@
Direct Workflow bot Direct Workflow bot
=================== ===================
The project submodule is automatically updated by the direct bot whenever a branch is updated in a package repository.
This bot can coexist with the Workflow PR bot, which is instead triggered by a new package PR.
Target Usage
------------
Devel project, where direct pushes to package git are possible.
Areas of responsibility Areas of responsibility
----------------------- -----------------------
1. Keep ProjectGit in sync with packages in the organization 1. Keep ProjectGit in sync with packages in the organization
* **On pushes to package**: updates the submodule commit ID to the default branch HEAD (as configured in Gitea). * on pushes to package, updates the submodule commit id
* **On repository adds**: creates a new submodule (if non-empty). to the default branch HEAD (as configured in Gitea)
* **On repository removal**: removes the submodule. * on repository adds, creates a new submodule (if non empty)
* on repository removal, removes the submodule
**Note:** If you want to revert a change in a package, you need to do that manually in the project git.
NOTE: reverts (push HEAD^) are not supported as they would step-on the
work of the workflow-pr bot. Manual update of the project git is
required in this case.
Configuration Configuration
------------- -------------
Uses `workflow.config` for configuration. Uses `workflow.config` for configuration. Parameters
| Field name | Details | Mandatory | Type | Allowed Values | Default | * _Workflows_: ["direct"] -- direct entry enables direct workflow. **Mandatory**
| ----- | ----- | ----- | ----- | ----- | ----- | * _Organization_: organization that holds all the packages. **Mandatory**
| *Workflows* | Type of workflow | yes | string | “direct” | | * _Branch_: branch updated in repo's, or blank for default package branch
| *Organization* | The organization that holds all the packages | yes | string | | | * _GitProjectName_: package in above org, or `org/package#branch` for PrjGit. By default assumes `_ObsPrj` with default branch and in the `Organization`
| *Branch* | The designated branch for packages | no | string | | blank (default package branch) |
| *GitProjectName* | Repository and branch where the ProjectGit lives. | no | string | **Format**: `org/project_repo#branch` | By default assumes `_ObsPrj` with default branch in the *Organization* |
NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project. NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
Target Usage
------------
Environment Variables Devel project, where direct pushes to package git are possible
-------
* `GITEA_TOKEN` (required)
* `AMQP_USERNAME` (required)
* `AMQP_PASSWORD` (required)
* `AUTOGITS_CONFIG` (required)
* `AUTOGITS_URL` - default: https://src.opensuse.org
* `AUTOGITS_RABBITURL` - default: amqps://rabbit.opensuse.org
* `AUTOGITS_DEBUG` - disabled by default, set to any value to enable
* `AUTOGITS_CHECK_ON_START` - disabled by default, set to any value to enable
* `AUTOGITS_REPO_PATH` - default is temporary directory
* `AUTOGITS_IDENTITY_FILE` - in case where we need explicit identify path for ssh specified

View File

@@ -22,6 +22,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"io/fs" "io/fs"
"log"
"math/rand" "math/rand"
"net/url" "net/url"
"os" "os"
@@ -39,7 +40,7 @@ import (
const ( const (
AppName = "direct_workflow" AppName = "direct_workflow"
GitAuthor = "AutoGits prjgit-updater" GitAuthor = "AutoGits prjgit-updater"
GitEmail = "autogits-direct@noreply@src.opensuse.org" GitEmail = "adam+autogits-direct@zombino.com"
) )
var configuredRepos map[string][]*common.AutogitConfig var configuredRepos map[string][]*common.AutogitConfig
@@ -52,6 +53,18 @@ func isConfiguredOrg(org *common.Organization) bool {
return found return found
} }
func concatenateErrors(err1, err2 error) error {
if err1 == nil {
return err2
}
if err2 == nil {
return err1
}
return fmt.Errorf("%w\n%w", err1, err2)
}
type RepositoryActionProcessor struct{} type RepositoryActionProcessor struct{}
func (*RepositoryActionProcessor) ProcessFunc(request *common.Request) error { func (*RepositoryActionProcessor) ProcessFunc(request *common.Request) error {
@@ -59,90 +72,69 @@ func (*RepositoryActionProcessor) ProcessFunc(request *common.Request) error {
configs, configFound := configuredRepos[action.Organization.Username] configs, configFound := configuredRepos[action.Organization.Username]
if !configFound { if !configFound {
common.LogInfo("Repository event for", action.Organization.Username, ". Not configured. Ignoring.", action.Organization.Username) log.Printf("Repository event for %s. Not configured. Ignoring.\n", action.Organization.Username)
return nil return nil
} }
for _, config := range configs { for _, config := range configs {
if org, repo, _ := config.GetPrjGit(); org == action.Repository.Owner.Username && repo == action.Repository.Name { if org, repo, _ := config.GetPrjGit(); org == action.Repository.Owner.Username && repo == action.Repository.Name {
common.LogError("+ ignoring repo event for PrjGit repository", config.GitProjectName) log.Println("+ ignoring repo event for PrjGit repository", config.GitProjectName)
return nil return nil
} }
} }
var err error
for _, config := range configs { for _, config := range configs {
processConfiguredRepositoryAction(action, config) err = concatenateErrors(err, processConfiguredRepositoryAction(action, config))
} }
return nil return err
} }
func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, config *common.AutogitConfig) { func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, config *common.AutogitConfig) error {
gitOrg, gitPrj, gitBranch := config.GetPrjGit() gitOrg, gitPrj, gitBranch := config.GetPrjGit()
git, err := gh.CreateGitHandler(config.Organization) git, err := gh.CreateGitHandler(config.Organization)
common.PanicOnError(err) common.PanicOnError(err)
defer git.Close() defer git.Close()
configBranch := config.Branch if len(config.Branch) == 0 {
if len(configBranch) == 0 { config.Branch = action.Repository.Default_Branch
configBranch = action.Repository.Default_Branch
if common.IsRemovedBranch(configBranch) {
common.LogDebug(" - default branch has deleted suffix. Skipping")
return
}
if len(configBranch) == 0 {
common.LogDebug("Empty default branch in message. Maybe race-condition?")
repo, err := gitea.GetRepository(action.Repository.Owner.Username, action.Repository.Name)
if err != nil {
common.LogError("Failed to fetch repository we have an event for?", action.Repository.Owner.Username, action.Repository.Name)
return
}
if len(repo.DefaultBranch) == 0 {
common.LogError("Default branch is somehow empty. We cannot do anything.")
return
}
configBranch = repo.DefaultBranch
}
} }
prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj) prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj)
if err != nil { if err != nil {
common.LogError("Error accessing/creating prjgit:", gitOrg, gitPrj, gitBranch, err) return fmt.Errorf("Error accessing/creating prjgit: %s/%s#%s err: %w", gitOrg, gitPrj, gitBranch, err)
return
} }
remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL) remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL)
common.PanicOnError(err) common.PanicOnError(err)
git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
switch action.Action { switch action.Action {
case "created": case "created":
if action.Repository.Object_Format_Name != "sha256" { if action.Repository.Object_Format_Name != "sha256" {
common.LogError(" - ", action.Repository.Name, "repo is not sha256. Ignoring.") return fmt.Errorf(" - '%s' repo is not sha256. Ignoring.", action.Repository.Name)
return
} }
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name)) common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name))
defer git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f") defer git.GitExecOrPanic(gitPrj, "submodule", "deinit", "--all")
branch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, action.Repository.Name), "branch", "--show-current")) branch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, action.Repository.Name), "branch", "--show-current"))
if branch != configBranch { if branch != config.Branch {
if err := git.GitExec(path.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "origin", configBranch+":"+configBranch); err != nil { if err := git.GitExec(path.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "origin", config.Branch+":"+config.Branch); err != nil {
common.LogError("error fetching branch", configBranch, ". ignoring as non-existent.", err) // no branch? so ignore repo here return fmt.Errorf("error fetching branch %s. ignoring as non-existent. err: %w", config.Branch, err) // no branch? so ignore repo here
return
} }
common.PanicOnError(git.GitExec(path.Join(gitPrj, action.Repository.Name), "checkout", configBranch)) common.PanicOnError(git.GitExec(path.Join(gitPrj, action.Repository.Name), "checkout", config.Branch))
} }
common.PanicOnError(git.GitExec(gitPrj, "commit", "-m", "Auto-inclusion "+action.Repository.Name)) common.PanicOnError(git.GitExec(gitPrj, "commit", "-m", "Automatic package inclusion via Direct Workflow"))
if !noop { if !noop {
common.PanicOnError(git.GitExec(gitPrj, "push")) common.PanicOnError(git.GitExec(gitPrj, "push"))
} }
case "deleted": case "deleted":
if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil || !stat.IsDir() { if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil || !stat.IsDir() {
common.LogDebug("delete event for", action.Repository.Name, "-- not in project. Ignoring") if DebugMode {
return log.Println("delete event for", action.Repository.Name, "-- not in project. Ignoring")
}
return nil
} }
common.PanicOnError(git.GitExec(gitPrj, "rm", action.Repository.Name)) common.PanicOnError(git.GitExec(gitPrj, "rm", action.Repository.Name))
common.PanicOnError(git.GitExec(gitPrj, "commit", "-m", "Automatic package removal via Direct Workflow")) common.PanicOnError(git.GitExec(gitPrj, "commit", "-m", "Automatic package removal via Direct Workflow"))
@@ -151,9 +143,10 @@ func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, co
} }
default: default:
common.LogError("Unknown action type:", action.Action) return fmt.Errorf("%s: %s", "Unknown action type", action.Action)
return
} }
return nil
} }
type PushActionProcessor struct{} type PushActionProcessor struct{}
@@ -163,83 +156,77 @@ func (*PushActionProcessor) ProcessFunc(request *common.Request) error {
configs, configFound := configuredRepos[action.Repository.Owner.Username] configs, configFound := configuredRepos[action.Repository.Owner.Username]
if !configFound { if !configFound {
common.LogDebug("Repository event for", action.Repository.Owner.Username, ". Not configured. Ignoring.") log.Printf("Repository event for %s. Not configured. Ignoring.\n", action.Repository.Owner.Username)
return nil return nil
} }
for _, config := range configs { for _, config := range configs {
if gitOrg, gitPrj, _ := config.GetPrjGit(); gitOrg == action.Repository.Owner.Username && gitPrj == action.Repository.Name { if gitOrg, gitPrj, _ := config.GetPrjGit(); gitOrg == action.Repository.Owner.Username && gitPrj == action.Repository.Name {
common.LogInfo("+ ignoring push to PrjGit repository", config.GitProjectName) log.Println("+ ignoring push to PrjGit repository", config.GitProjectName)
return nil return nil
} }
} }
var err error
for _, config := range configs { for _, config := range configs {
processConfiguredPushAction(action, config) err = concatenateErrors(err, processConfiguredPushAction(action, config))
} }
return nil
return err
} }
func processConfiguredPushAction(action *common.PushWebhookEvent, config *common.AutogitConfig) { func processConfiguredPushAction(action *common.PushWebhookEvent, config *common.AutogitConfig) error {
gitOrg, gitPrj, gitBranch := config.GetPrjGit() gitOrg, gitPrj, gitBranch := config.GetPrjGit()
git, err := gh.CreateGitHandler(config.Organization) git, err := gh.CreateGitHandler(config.Organization)
common.PanicOnError(err) common.PanicOnError(err)
defer git.Close() defer git.Close()
common.LogDebug("push to:", action.Repository.Owner.Username, action.Repository.Name, "for:", gitOrg, gitPrj, gitBranch) log.Printf("push to: %s/%s for %s/%s#%s", action.Repository.Owner.Username, action.Repository.Name, gitOrg, gitPrj, gitBranch)
branch := config.Branch if len(config.Branch) == 0 {
if len(branch) == 0 { config.Branch = action.Repository.Default_Branch
if common.IsRemovedBranch(branch) { log.Println(" + default branch", action.Repository.Default_Branch)
common.LogDebug(" + default branch has removed suffix:", branch, "Skipping.")
return
}
branch = action.Repository.Default_Branch
common.LogDebug(" + using default branch", branch)
} }
prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj) prjGitRepo, err := gitea.CreateRepositoryIfNotExist(git, gitOrg, gitPrj)
if err != nil { if err != nil {
common.LogError("Error accessing/creating prjgit:", gitOrg, gitPrj, err) return fmt.Errorf("Error accessing/creating prjgit: %s/%s err: %w", gitOrg, gitPrj, err)
return
} }
remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL) remoteName, err := git.GitClone(gitPrj, gitBranch, prjGitRepo.SSHURL)
common.PanicOnError(err) common.PanicOnError(err)
git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
headCommitId, err := git.GitRemoteHead(gitPrj, remoteName, gitBranch) headCommitId, err := git.GitRemoteHead(gitPrj, remoteName, gitBranch)
common.PanicOnError(err) common.PanicOnError(err)
commit, ok := git.GitSubmoduleCommitId(gitPrj, action.Repository.Name, headCommitId) commit, ok := git.GitSubmoduleCommitId(gitPrj, action.Repository.Name, headCommitId)
for ok && action.Head_Commit.Id == commit { for ok && action.Head_Commit.Id == commit {
common.LogDebug(" -- nothing to do, commit already in ProjectGit") log.Println(" -- nothing to do, commit already in ProjectGit")
return return nil
} }
if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil { if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil || !stat.IsDir() {
git.GitExecOrPanic(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name) if DebugMode {
common.LogDebug("Pushed to package that is not part of the project. Re-adding...", err) log.Println("Pushed to package that is not part of the project. Ignoring:", err)
} else if !stat.IsDir() { }
common.LogError("Pushed to a package that is not a submodule but exists in the project. Ignoring.") return nil
return
} }
git.GitExecOrPanic(gitPrj, "submodule", "update", "--init", "--force", "--depth", "1", "--checkout", action.Repository.Name) git.GitExecOrPanic(gitPrj, "submodule", "update", "--init", "--depth", "1", "--checkout", action.Repository.Name)
defer git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f") defer git.GitExecOrPanic(gitPrj, "submodule", "deinit", "--all")
if err := git.GitExec(filepath.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "--force", "origin", branch+":"+branch); err != nil { if err := git.GitExec(filepath.Join(gitPrj, action.Repository.Name), "fetch", "--depth", "1", "--force", remoteName, config.Branch+":"+config.Branch); err != nil {
common.LogError("Error fetching branch:", branch, "Ignoring as non-existent.", err) return fmt.Errorf("error fetching branch %s. ignoring as non-existent. err: %w", config.Branch, err) // no branch? so ignore repo here
return
} }
id, err := git.GitBranchHead(filepath.Join(gitPrj, action.Repository.Name), branch) id, err := git.GitRemoteHead(filepath.Join(gitPrj, action.Repository.Name), remoteName, config.Branch)
common.PanicOnError(err) common.PanicOnError(err)
if action.Head_Commit.Id == id { if action.Head_Commit.Id == id {
git.GitExecOrPanic(filepath.Join(gitPrj, action.Repository.Name), "checkout", id) git.GitExecOrPanic(filepath.Join(gitPrj, action.Repository.Name), "checkout", id)
git.GitExecOrPanic(gitPrj, "commit", "-a", "-m", fmt.Sprintf("'%s' update via Direct Workflow", action.Repository.Name)) git.GitExecOrPanic(gitPrj, "commit", "-a", "-m", "Automatic update via push via Direct Workflow")
if !noop { if !noop {
git.GitExecOrPanic(gitPrj, "push", remoteName) git.GitExecOrPanic(gitPrj, "push", remoteName)
} }
return return nil
} }
common.LogDebug("push of refs not on the configured branch", branch, ". ignoring.") log.Println("push of refs not on the configured branch", config.Branch, ". ignoring.")
return nil
} }
func verifyProjectState(git common.Git, org string, config *common.AutogitConfig, configs []*common.AutogitConfig) (err error) { func verifyProjectState(git common.Git, org string, config *common.AutogitConfig, configs []*common.AutogitConfig) (err error) {
@@ -261,65 +248,51 @@ func verifyProjectState(git common.Git, org string, config *common.AutogitConfig
remoteName, err := git.GitClone(gitPrj, gitBranch, repo.SSHURL) remoteName, err := git.GitClone(gitPrj, gitBranch, repo.SSHURL)
common.PanicOnError(err) common.PanicOnError(err)
git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f") defer git.GitExecOrPanic(gitPrj, "submodule", "deinit", "--all")
defer git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
common.LogDebug(" * Getting submodule list") log.Println(" * Getting submodule list")
sub, err := git.GitSubmoduleList(gitPrj, "HEAD") sub, err := git.GitSubmoduleList(gitPrj, "HEAD")
common.PanicOnError(err) common.PanicOnError(err)
common.LogDebug(" * Getting package links") log.Println(" * Getting package links")
var pkgLinks []*PackageRebaseLink var pkgLinks []*PackageRebaseLink
if f, err := fs.Stat(os.DirFS(path.Join(git.GetPath(), gitPrj)), common.PrjLinksFile); err == nil && (f.Mode()&fs.ModeType == 0) && f.Size() < 1000000 { if f, err := fs.Stat(os.DirFS(path.Join(git.GetPath(), gitPrj)), common.PrjLinksFile); err == nil && (f.Mode()&fs.ModeType == 0) && f.Size() < 1000000 {
if data, err := os.ReadFile(path.Join(git.GetPath(), gitPrj, common.PrjLinksFile)); err == nil { if data, err := os.ReadFile(path.Join(git.GetPath(), gitPrj, common.PrjLinksFile)); err == nil {
pkgLinks, err = parseProjectLinks(data) pkgLinks, err = parseProjectLinks(data)
if err != nil { if err != nil {
common.LogError("Cannot parse project links file:", err.Error()) log.Println("Cannot parse project links file:", err.Error())
pkgLinks = nil pkgLinks = nil
} else { } else {
ResolveLinks(org, pkgLinks, gitea) ResolveLinks(org, pkgLinks, gitea)
} }
} }
} else { } else {
common.LogInfo(" - No package links defined") log.Println(" - No package links defined")
} }
/* Check existing submodule that they are updated */ /* Check existing submodule that they are updated */
isGitUpdated := false isGitUpdated := false
next_package: next_package:
for filename, commitId := range sub { for filename, commitId := range sub {
// ignore project gits // ignore project gits
//for _, c := range configs { //for _, c := range configs {
if gitPrj == filename { if gitPrj == filename {
common.LogDebug(" prjgit as package? ignoring project git:", filename) log.Println(" prjgit as package? ignoring project git:", filename)
continue next_package continue next_package
} }
//} //}
branch := config.Branch log.Printf(" verifying package: %s -> %s(%s)", commitId, filename, config.Branch)
common.LogDebug(" verifying package:", commitId, "->", filename, "@", branch) commits, err := gitea.GetRecentCommits(org, filename, config.Branch, 10)
if repo, err := gitea.GetRepository(org, filename); repo == nil && err == nil { if len(commits) == 0 {
common.LogDebug(" repository removed...") if repo, err := gitea.GetRepository(org, filename); repo == nil && err == nil {
git.GitExecOrPanic(gitPrj, "rm", filename)
isGitUpdated = true
continue
} else if err != nil {
common.LogError("failed fetching repo data", org, filename, err)
continue
} else if len(branch) == 0 {
branch = repo.DefaultBranch
common.LogDebug(" -> using default branch", branch)
if common.IsRemovedBranch(branch) {
common.LogDebug(" Default branch for", filename, "is excluded")
git.GitExecOrPanic(gitPrj, "rm", filename) git.GitExecOrPanic(gitPrj, "rm", filename)
isGitUpdated = true isGitUpdated = true
continue
} }
} }
commits, err := gitea.GetRecentCommits(org, filename, branch, 10)
if err != nil { if err != nil {
common.LogDebug(" -> failed to fetch recent commits for package:", filename, " Err:", err) log.Println(" -> failed to fetch recent commits for package:", filename, " Err:", err)
continue continue
} }
@@ -336,7 +309,7 @@ next_package:
if l.Pkg == filename { if l.Pkg == filename {
link = l link = l
common.LogDebug(" -> linked package") log.Println(" -> linked package")
// so, we need to rebase here. Can't really optimize, so clone entire package tree and remote // so, we need to rebase here. Can't really optimize, so clone entire package tree and remote
pkgPath := path.Join(gitPrj, filename) pkgPath := path.Join(gitPrj, filename)
git.GitExecOrPanic(gitPrj, "submodule", "update", "--init", "--checkout", filename) git.GitExecOrPanic(gitPrj, "submodule", "update", "--init", "--checkout", filename)
@@ -350,7 +323,7 @@ next_package:
nCommits := len(common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkgPath, "rev-list", "^NOW", "HEAD"), "\n")) nCommits := len(common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkgPath, "rev-list", "^NOW", "HEAD"), "\n"))
if nCommits > 0 { if nCommits > 0 {
if !noop { if !noop {
git.GitExecOrPanic(pkgPath, "push", "-f", "origin", "HEAD:"+branch) git.GitExecOrPanic(pkgPath, "push", "-f", "origin", "HEAD:"+config.Branch)
} }
isGitUpdated = true isGitUpdated = true
} }
@@ -367,27 +340,42 @@ next_package:
common.PanicOnError(git.GitExec(gitPrj, "submodule", "update", "--init", "--depth", "1", "--checkout", filename)) common.PanicOnError(git.GitExec(gitPrj, "submodule", "update", "--init", "--depth", "1", "--checkout", filename))
common.PanicOnError(git.GitExec(filepath.Join(gitPrj, filename), "fetch", "--depth", "1", "origin", commits[0].SHA)) common.PanicOnError(git.GitExec(filepath.Join(gitPrj, filename), "fetch", "--depth", "1", "origin", commits[0].SHA))
common.PanicOnError(git.GitExec(filepath.Join(gitPrj, filename), "checkout", commits[0].SHA)) common.PanicOnError(git.GitExec(filepath.Join(gitPrj, filename), "checkout", commits[0].SHA))
common.LogDebug(" -> updated to", commits[0].SHA) log.Println(" -> updated to", commits[0].SHA)
isGitUpdated = true isGitUpdated = true
} else { } else {
// probably need `merge-base` or `rev-list` here instead, or the project updated already // probably need `merge-base` or `rev-list` here instead, or the project updated already
common.LogInfo(" *** Cannot find SHA of last matching update for package:", filename, " Ignoring") log.Println(" *** Cannot find SHA of last matching update for package:", filename, " Ignoring")
} }
} }
} }
// find all missing repositories, and add them // find all missing repositories, and add them
common.LogDebug("checking for missing repositories...") if DebugMode {
log.Println("checking for missing repositories...")
}
repos, err := gitea.GetOrganizationRepositories(org) repos, err := gitea.GetOrganizationRepositories(org)
if err != nil { if err != nil {
return err return err
} }
common.LogDebug(" nRepos:", len(repos)) if DebugMode {
log.Println(" nRepos:", len(repos))
}
/* Check repositories in org to make sure they are included in project git */ /* Check repositories in org to make sure they are included in project git */
next_repo: next_repo:
for _, r := range repos { for _, r := range repos {
if DebugMode {
log.Println(" -- checking", r.Name)
}
if r.ObjectFormatName != "sha256" {
if DebugMode {
log.Println(" + ", r.ObjectFormatName, ". Needs to be sha256. Ignoring")
}
continue next_repo
}
// for _, c := range configs { // for _, c := range configs {
if gitPrj == r.Name { if gitPrj == r.Name {
// ignore project gits // ignore project gits
@@ -402,45 +390,43 @@ next_repo:
} }
} }
common.LogDebug(" -- checking repository:", r.Name) if DebugMode {
log.Println(" -- checking repository:", r.Name)
branch := config.Branch
if len(branch) == 0 {
branch = r.DefaultBranch
if common.IsRemovedBranch(branch) {
continue
}
} }
if commits, err := gitea.GetRecentCommits(org, r.Name, branch, 1); err != nil || len(commits) == 0 {
if _, err := gitea.GetRecentCommits(org, r.Name, config.Branch, 1); err != nil {
// assumption that package does not exist, so not part of project // assumption that package does not exist, so not part of project
// https://github.com/go-gitea/gitea/issues/31976 // https://github.com/go-gitea/gitea/issues/31976
// or, we do not have commits here
continue continue
} }
// add repository to git project // add repository to git project
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", r.CloneURL, r.Name)) common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--depth", "1", r.CloneURL, r.Name))
curBranch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, r.Name), "branch", "--show-current")) if len(config.Branch) > 0 {
if branch != curBranch { branch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, r.Name), "branch", "--show-current"))
if err := git.GitExec(path.Join(gitPrj, r.Name), "fetch", "--depth", "1", "origin", branch+":"+branch); err != nil { if branch != config.Branch {
return fmt.Errorf("Fetching branch %s for %s/%s failed. Ignoring.", branch, repo.Owner.UserName, r.Name) if err := git.GitExec(path.Join(gitPrj, r.Name), "fetch", "--depth", "1", "origin", config.Branch+":"+config.Branch); err != nil {
return fmt.Errorf("Fetching branch %s for %s/%s failed. Ignoring.", config.Branch, repo.Owner.UserName, r.Name)
}
common.PanicOnError(git.GitExec(path.Join(gitPrj, r.Name), "checkout", config.Branch))
} }
common.PanicOnError(git.GitExec(path.Join(gitPrj, r.Name), "checkout", branch))
} }
isGitUpdated = true isGitUpdated = true
} }
if isGitUpdated { if isGitUpdated {
common.PanicOnError(git.GitExec(gitPrj, "commit", "-a", "-m", "Periodic SYNC in Direct Workflow")) common.PanicOnError(git.GitExec(gitPrj, "commit", "-a", "-m", "Automatic update via push via Direct Workflow -- SYNC"))
if !noop { if !noop {
git.GitExecOrPanic(gitPrj, "push", remoteName) git.GitExecOrPanic(gitPrj, "push", remoteName)
} }
} }
common.LogInfo("Verification finished for ", org, ", prjgit:", config.GitProjectName) if DebugMode {
log.Println("Verification finished for ", org, ", prjgit:", config.GitProjectName)
}
return nil return nil
} }
@@ -451,17 +437,17 @@ var checkInterval time.Duration
func checkOrg(org string, configs []*common.AutogitConfig) { func checkOrg(org string, configs []*common.AutogitConfig) {
git, err := gh.CreateGitHandler(org) git, err := gh.CreateGitHandler(org)
if err != nil { if err != nil {
common.LogError("Failed to allocate GitHandler:", err) log.Println("Faield to allocate GitHandler:", err)
return return
} }
defer git.Close() defer git.Close()
for _, config := range configs { for _, config := range configs {
common.LogInfo(" ++ starting verification, org:", org, "config:", config.GitProjectName) log.Printf(" ++ starting verification, org: `%s` config: `%s`\n", org, config.GitProjectName)
if err := verifyProjectState(git, org, config, configs); err != nil { if err := verifyProjectState(git, org, config, configs); err != nil {
common.LogError(" *** verification failed, org:", org, err) log.Printf(" *** verification failed, org: `%s`, err: %#v\n", org, err)
} else { } else {
common.LogError(" ++ verification complete, org:", org, config.GitProjectName) log.Printf(" ++ verification complete, org: `%s` config: `%s`\n", org, config.GitProjectName)
} }
} }
} }
@@ -470,7 +456,7 @@ func checkRepos() {
for org, configs := range configuredRepos { for org, configs := range configuredRepos {
if checkInterval > 0 { if checkInterval > 0 {
sleepInterval := checkInterval - checkInterval/2 + time.Duration(rand.Int63n(int64(checkInterval))) sleepInterval := checkInterval - checkInterval/2 + time.Duration(rand.Int63n(int64(checkInterval)))
common.LogInfo(" - sleep interval", sleepInterval, "until next check") log.Println(" - sleep interval", sleepInterval, "until next check")
time.Sleep(sleepInterval) time.Sleep(sleepInterval)
} }
@@ -482,9 +468,9 @@ func consistencyCheckProcess() {
if checkOnStart { if checkOnStart {
savedCheckInterval := checkInterval savedCheckInterval := checkInterval
checkInterval = 0 checkInterval = 0
common.LogInfo("== Startup consistency check begin...") log.Println("== Startup consistency check begin...")
checkRepos() checkRepos()
common.LogInfo("== Startup consistency check done...") log.Println("== Startup consistency check done...")
checkInterval = savedCheckInterval checkInterval = savedCheckInterval
} }
@@ -499,8 +485,7 @@ var gh common.GitHandlerGenerator
func updateConfiguration(configFilename string, orgs *[]string) { func updateConfiguration(configFilename string, orgs *[]string) {
configFile, err := common.ReadConfigFile(configFilename) configFile, err := common.ReadConfigFile(configFilename)
if err != nil { if err != nil {
common.LogError(err) log.Fatal(err)
os.Exit(4)
} }
configs, _ := common.ResolveWorkflowConfigs(gitea, configFile) configs, _ := common.ResolveWorkflowConfigs(gitea, configFile)
@@ -508,7 +493,9 @@ func updateConfiguration(configFilename string, orgs *[]string) {
*orgs = make([]string, 0, 1) *orgs = make([]string, 0, 1)
for _, c := range configs { for _, c := range configs {
if slices.Contains(c.Workflows, "direct") { if slices.Contains(c.Workflows, "direct") {
common.LogDebug(" + adding org:", c.Organization, ", branch:", c.Branch, ", prjgit:", c.GitProjectName) if DebugMode {
log.Printf(" + adding org: '%s', branch: '%s', prjgit: '%s'\n", c.Organization, c.Branch, c.GitProjectName)
}
configs := configuredRepos[c.Organization] configs := configuredRepos[c.Organization]
if configs == nil { if configs == nil {
configs = make([]*common.AutogitConfig, 0, 1) configs = make([]*common.AutogitConfig, 0, 1)
@@ -522,7 +509,7 @@ func updateConfiguration(configFilename string, orgs *[]string) {
} }
func main() { func main() {
configFilename := flag.String("config", "config.json", "List of PrjGit") configFilename := flag.String("config", "", "List of PrjGit")
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance") giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance")
rabbitUrl := flag.String("url", "amqps://rabbit.opensuse.org", "URL for RabbitMQ instance") rabbitUrl := flag.String("url", "amqps://rabbit.opensuse.org", "URL for RabbitMQ instance")
flag.BoolVar(&DebugMode, "debug", false, "Extra debugging information") flag.BoolVar(&DebugMode, "debug", false, "Extra debugging information")
@@ -533,35 +520,10 @@ func main() {
flag.Parse() flag.Parse()
if err := common.RequireGiteaSecretToken(); err != nil { if err := common.RequireGiteaSecretToken(); err != nil {
common.LogError(err) log.Fatal(err)
os.Exit(1)
} }
if err := common.RequireRabbitSecrets(); err != nil { if err := common.RequireRabbitSecrets(); err != nil {
common.LogError(err) log.Fatal(err)
os.Exit(1)
}
if cf := os.Getenv("AUTOGITS_CONFIG"); len(cf) > 0 {
*configFilename = cf
}
if url := os.Getenv("AUTOGITS_URL"); len(url) > 0 {
*giteaUrl = url
}
if url := os.Getenv("AUTOGITS_RABBITURL"); len(url) > 0 {
*rabbitUrl = url
}
if debug := os.Getenv("AUTOGITS_DEBUG"); len(debug) > 0 {
DebugMode = true
}
if check := os.Getenv("AUTOGITS_CHECK_ON_START"); len(check) > 0 {
checkOnStart = true
}
if p := os.Getenv("AUTOGITS_REPO_PATH"); len(p) > 0 {
*basePath = p
}
if DebugMode {
common.SetLoggingLevel(common.LogLevelDebug)
} }
defs := &common.RabbitMQGiteaEventsProcessor{} defs := &common.RabbitMQGiteaEventsProcessor{}
@@ -570,14 +532,12 @@ func main() {
if len(*basePath) == 0 { if len(*basePath) == 0 {
*basePath, err = os.MkdirTemp(os.TempDir(), AppName) *basePath, err = os.MkdirTemp(os.TempDir(), AppName)
if err != nil { if err != nil {
common.LogError(err) log.Fatal(err)
os.Exit(1)
} }
} }
gh, err = common.AllocateGitWorkTree(*basePath, GitAuthor, GitEmail) gh, err = common.AllocateGitWorkTree(*basePath, GitAuthor, GitEmail)
if err != nil { if err != nil {
common.LogError(err) log.Fatal(err)
os.Exit(1)
} }
// handle reconfiguration // handle reconfiguration
@@ -592,10 +552,10 @@ func main() {
} }
if sig != syscall.SIGHUP { if sig != syscall.SIGHUP {
common.LogError("Unexpected signal received:", sig) log.Println("Unexpected signal received:", sig)
continue continue
} }
common.LogError("*** Reconfiguring ***") log.Println("*** Reconfiguring ***")
updateConfiguration(*configFilename, &defs.Orgs) updateConfiguration(*configFilename, &defs.Orgs)
defs.Connection().UpdateTopics(defs) defs.Connection().UpdateTopics(defs)
} }
@@ -607,25 +567,23 @@ func main() {
gitea = common.AllocateGiteaTransport(*giteaUrl) gitea = common.AllocateGiteaTransport(*giteaUrl)
CurrentUser, err := gitea.GetCurrentUser() CurrentUser, err := gitea.GetCurrentUser()
if err != nil { if err != nil {
common.LogError("Cannot fetch current user:", err) log.Fatalln("Cannot fetch current user:", err)
os.Exit(2)
} }
common.LogInfo("Current User:", CurrentUser.UserName) log.Println("Current User:", CurrentUser.UserName)
updateConfiguration(*configFilename, &defs.Orgs) updateConfiguration(*configFilename, &defs.Orgs)
defs.Connection().RabbitURL, err = url.Parse(*rabbitUrl) defs.Connection().RabbitURL, err = url.Parse(*rabbitUrl)
if err != nil { if err != nil {
common.LogError("cannot parse server URL. Err:", err) log.Panicf("cannot parse server URL. Err: %#v\n", err)
os.Exit(3)
} }
go consistencyCheckProcess() go consistencyCheckProcess()
common.LogInfo("defs:", *defs) log.Println("defs:", *defs)
defs.Handlers = make(map[string]common.RequestProcessor) defs.Handlers = make(map[string]common.RequestProcessor)
defs.Handlers[common.RequestType_Push] = &PushActionProcessor{} defs.Handlers[common.RequestType_Push] = &PushActionProcessor{}
defs.Handlers[common.RequestType_Repository] = &RepositoryActionProcessor{} defs.Handlers[common.RequestType_Repository] = &RepositoryActionProcessor{}
common.LogError(common.ProcessRabbitMQEvents(defs)) log.Fatal(common.ProcessRabbitMQEvents(defs))
} }

View File

@@ -1,65 +1,54 @@
Workflow-PR bot Workflow-PR bot
=============== ===============
Keeps ProjectGit PRs in-sync with the relative PackageGit PRs. Keeps ProjectGit PR in-sync with a PackageGit PR
Areas of Responsibility
-----------------------
* Detects a PackageGit PR creation against a package and creates a coresponsing PR against the ProjectGit
* When a PackageGit PR is updated, the corresponding PR against the ProjectGit is updated
* Stores reference to the PackageGit PR in the headers of the ProjectGit PR comments, for later reference
* this allows ProjectGit PR to be merged to seperated later (via another tool, for example)
* Initiates all staging workflows via review requests
Target Usage Target Usage
------------ ------------
Any project (devel, codestream, product, etc.) that accepts PRs. Any project (devel, etc) that accepts PR
Main Tasks
----------
* **Synchronization**:
* When a **PackageGit PR** is created for a package on a specific project branch, a corresponding PR is automatically generated in **ProjectGit**.
* When a PackageGit PR is updated, the corresponding PR against the ProjectGit is updated.
* A link to the PackageGit PR is stored in the body of the ProjectGit PR comments in the following format:
* `PR: organization/package_name!pull_request_number`
* Example: `PR: pool/curl!4`
* It closes an empty ProjectGit PR (e.g., if a PR was initially created for a single package but later integrated into a larger ProjectGit PR).
* It forwards the Work In Progress (WIP) flag to the ProjectGit PR. If the ProjectGit PR references multiple Package PRs, triggering the WIP flag on the ProjectGit PR side only requires a single WIP package PR.
* **Reviewer Management**:
* It adds required reviewers in the ProjectGit PR.
* It adds required reviewers in the PackageGit PR.
* If new commits are added to a PackageGit PR, reviewers who have already approved it will be re-added.
* **Merge Management**:
* Manages PR merges based on configuration flags (`ManualMergeOnly`, `ManualMergeProject`).
* In general, merge only happens if all mandatory reviews are completed.
* **ManualMergeProject** is stricter than **ManualMergeOnly** and has higher priority.
| Flag | Value | Behavior |
| ----- | ----- | ----- |
| ManualMergeProject | true | Both ProjectGit and PackageGit PRs are merged upon an allowed project maintainer commenting "merge ok” in the ProjectGit PR. |
| ManualMergeOnly | true | Both PackageGit PR and ProjectGit PR are merged upon an allowed package maintainer or project maintainer commenting “merge ok” in the PackageGit PR. |
| ManualMergeOnly and ManualMergeProject | false | Both ProjectGit and PackageGit PRs are merged as soon as all reviews are completed in both PrjGit and PkgGit PRs. |
Config file Config file
----------- -----------
JSON
* _Workflows_: ["pr"] -- pr entry enables pr workflow. **Mandatory**
* _Organization_: organization that holds all the packages **Mandatory**
* _Branch_: branch updated in repo's **Mandatory**
* _GitProjectName_: package in above org, or `org/package#branch` for PrjGit. By default assumes `_ObsPrj` with default branch and in the `Organization`
* _Reviewers_: accounts associated with mandatory reviews for PrjGit. Can trigger additional
review requests for PrjGit or associated PkgGit repos. Only when all reviews are
satisfied, will the PrjGit PR be merged. See Reviewers below.
* _ManualMergeOnly_: (true, false) only merge if "merge ok" comment/review by package or project maintainers or reviewers
* _ManualMergeProject_: (true, false) only merge if "merge ok" by project maintainers or reviewers
* _ReviewRequired_: (true, false) ignores that submitter is a maintainer and require a review from other maintainer IFF available
* _NoProjectGitPR_: (true, false) do not create PrjGit PRs, but still process reviews, etc.
* _Permissions_: permissions and associated accounts/groups. See below.
* Filename: `workflow.config` NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
* Location: ProjectGit example:
* Format: non-standard JSON (comments allowed)
| Field name | Details | Mandatory | Type | Allowed Values | Default |
| ----- | ----- | ----- | ----- | ----- | ----- |
| *Workflows* | Type of workflow | yes | string | “pr” | |
| *Organization* | The organization where PackageGit PRs are expected to occur | yes | string | | |
| *Branch* | The designated branch for PackageGit PRs | yes | string | | |
| *GitProjectName* | Repository and branch where the ProjectGit lives. | no | string | **Format**: `org/project_repo#branch` | By default assumes `_ObsPrj` with default branch in the *Organization* |
| *ManualMergeOnly* | Merges are permitted only upon receiving a "merge ok" comment from designated maintainers in the PkgGit PR. | no | bool | true, false | false |
| *ManualMergeProject* | Merges are permitted only upon receiving a "merge ok" comment in the ProjectGit PR from project maintainers. | no | bool | true, false | false |
| *ReviewRequired* | (NOT IMPLEMENTED) If submitter is a maintainer, require review from another maintainer if available. | no | bool | true, false | false |
| *NoProjectGitPR* | Do not create PrjGit PR, but still perform other tasks. | no | bool | true, false | false |
| *Reviewers* | PrjGit reviewers. Additional review requests are triggered for associated PkgGit PRs. PrjGit PR is merged only when all reviews are complete. | no | array of strings | | `[]` |
| *ReviewGroups* | If a group is specified in Reviewers, its members are listed here. | no | array of objects | | `[]` |
| *ReviewGroups > Name* | Name of the group | no | string | | |
| *ReviewGroups > Reviewers* | Members of the group | no | array of strings | | |
| *ReviewGroups > Silent* | Add members for notifications. If true, members are not explicitly requested to review. If one member approves, others are removed. | no | bool | true, false | false |
[
{
"Workflows": ["pr", "direct"],
"Organization": "autogits",
"GitProjectName": "HiddenPrj",
"Branch": "hidden",
"Reviewers": []
},
...
]
Reviewers Reviewers
--------- ---------
@@ -68,72 +57,39 @@ Reviews is a list of accounts that need to review package and/or project. They h
[~][*|-|+]username [~][*|-|+]username
A tilde (`~`) before a prefix signifies an advisory reviewer. Their input is requested, but their review status will not otherwise affect the process. General prefix of ~ indicates advisory reviewer. They will be requested, but ignored otherwise.
Other prefixes indicate project or package association of the reviewer: Other prefixes indicate project or package association of the reviewer:
* `*` indicates project *and* package * `*` indicates project *and* package
* `-` indicates project-only reviewer * `-` indicates project-only reviewer
* `+` indicates package-only reviewer * `+` indicates package-only reviewer
`+` is implied. `+` is implied. For example
For example: `[foo, -bar, ~*moo]` results in: `[foo, -bar, ~*moo]`
* foo: package reviews
* bar: project reviews
* moo: package and project reviews, but ignored
Package Deletion Requests results in
------------------------- * foo -> package reviews
(NOT YET IMPLEMENTED) * bar -> project reviews
* moo -> package and project reviews, but ignored
* **Removing a Package:**
To remove a package from a project, submit a ProjectGit Pull Request (PR) that removes the corresponding submodule. The bot will then rename the project branch in the pool by appending "-removed" to its name.
* **Adding a Package Again:**
If you wish to re-add a package, create a new PrjGit PR which adds again the submodule on the branch that has the "-removed" suffix. The bot will automatically remove this suffix from the project branch in the pool.
Labels
------
The following labels are used, when defined in Repo/Org.
| Label Config Entry | Default label | Description
|--------------------|----------------|----------------------------------------
| StagingAuto | staging/Auto | Assigned to Project Git PRs when first staged
| ReviewPending | review/Pending | Assigned to Project Git PR when package reviews are still pending
| ReviewDone | review/Done | Assigned to Project Git PR when reviews are complete on all package PRs
Maintainership Maintainership
-------------- --------------
Filename: \_maintainership.json Maintainership information is defined per project. For reviews, package maintainers are coalesced
Location: ProjectGit with project maintainers. A review by any of the maintainers is acceptable.
Format: JSON
Fields:
| Key | Value | Notes | example:
| ----- | ----- | ----- |
| package name | array of strings representing the package maintainers | List of package maintainers |
| “” (empty string) | array of strings representing the project maintainers | List of project maintainers |
Maintainership information is defined per project. For PackageGit PR reviews, package maintainers are combined with project maintainers. A review by any of these maintainers is acceptable. {
"package1": [ "reviewer", "reviewer2"],
"package2": [],
If the submitter is a maintainer it will not get a review requested. // "project" maintainer
"": ["reviewer3", "reviewer4"]
}
Example:
```
{
"package1": [ "reviewer", "reviewer2"],
"package2": [],
// "project" maintainer
"": ["reviewer3", "reviewer4"]
}
```
Permissions Permissions
----------- -----------
@@ -153,11 +109,3 @@ the `workflow.config`.
NOTE: Project Maintainers have these permissions automatically. NOTE: Project Maintainers have these permissions automatically.
Server configuration
--------------------------
**Configuration file:**
| Field | Type | Notes |
| ----- | ----- | ----- |
| root | Array of string | Format **org/repo\#branch** |

View File

@@ -7,7 +7,7 @@ import "src.opensuse.org/autogits/common"
type StateChecker interface { type StateChecker interface {
VerifyProjectState(configs *common.AutogitConfig) ([]*PRToProcess, error) VerifyProjectState(configs *common.AutogitConfig) ([]*PRToProcess, error)
CheckRepos() CheckRepos() error
ConsistencyCheckProcess() error ConsistencyCheckProcess() error
} }

View File

@@ -170,7 +170,7 @@ func main() {
common.RequestType_PRSync: req, common.RequestType_PRSync: req,
common.RequestType_PRReviewAccepted: req, common.RequestType_PRReviewAccepted: req,
common.RequestType_PRReviewRejected: req, common.RequestType_PRReviewRejected: req,
common.RequestType_PRComment: req, common.RequestType_IssueComment: req,
}, },
} }
listenDefs.Connection().RabbitURL, _ = url.Parse(*rabbitUrl) listenDefs.Connection().RabbitURL, _ = url.Parse(*rabbitUrl)

View File

@@ -41,9 +41,6 @@ func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
refs = append(refs, ref) refs = append(refs, ref)
} }
slices.Sort(title_refs)
slices.Sort(refs)
title = "Forwarded PRs: " + strings.Join(title_refs, ", ") title = "Forwarded PRs: " + strings.Join(title_refs, ", ")
desc = fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor) + strings.Join(refs, "\n") + "\n" desc = fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor) + strings.Join(refs, "\n") + "\n"
@@ -230,18 +227,12 @@ func (pr *PRProcessor) CreatePRjGitPR(prjGitPRbranch string, prset *common.PRSet
} }
title, desc := PrjGitDescription(prset) title, desc := PrjGitDescription(prset)
pr, err, isNew := Gitea.CreatePullRequestIfNotExist(PrjGit, prjGitPRbranch, PrjGitBranch, title, desc) pr, err := Gitea.CreatePullRequestIfNotExist(PrjGit, prjGitPRbranch, PrjGitBranch, title, desc)
if err != nil { if err != nil {
common.LogError("Error creating PrjGit PR:", err) common.LogError("Error creating PrjGit PR:", err)
return err return err
} }
org := PrjGit.Owner.UserName Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, pr.Index, &models.EditPullRequestOption{
repo := PrjGit.Name
idx := pr.Index
if isNew {
Gitea.SetLabels(org, repo, idx, []string{prset.Config.Label(common.Label_StagingAuto)})
}
Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
RemoveDeadline: true, RemoveDeadline: true,
}) })
@@ -278,7 +269,6 @@ func (pr *PRProcessor) RebaseAndSkipSubmoduleCommits(prset *common.PRSet, branch
} }
var updatePrjGitError_requeue error = errors.New("Commits do not match. Requeing after 5 seconds.") var updatePrjGitError_requeue error = errors.New("Commits do not match. Requeing after 5 seconds.")
func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error { func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
_, _, PrjGitBranch := prset.Config.GetPrjGit() _, _, PrjGitBranch := prset.Config.GetPrjGit()
PrjGitPR, err := prset.GetPrjGitPR() PrjGitPR, err := prset.GetPrjGitPR()
@@ -355,20 +345,7 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
} }
// update PR // update PR
isPrTitleSame := func(CurrentTitle, NewTitle string) bool { if PrjGitPR.PR.Body != PrjGitBody || PrjGitPR.PR.Title != PrjGitTitle {
ctlen := len(CurrentTitle)
for _, suffix := range []string{"...", "…"} {
slen := len(suffix)
if ctlen > 250 && strings.HasSuffix(CurrentTitle, suffix) && len(NewTitle) > ctlen {
NewTitle = NewTitle[0:ctlen-slen] + suffix
if CurrentTitle == NewTitle {
return true
}
}
}
return CurrentTitle == NewTitle
}
if PrjGitPR.PR.User.UserName == CurrentUser.UserName && (PrjGitPR.PR.Body != PrjGitBody || !isPrTitleSame(PrjGitPR.PR.Title, PrjGitTitle)) {
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{ Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
RemoveDeadline: true, RemoveDeadline: true,
Title: PrjGitTitle, Title: PrjGitTitle,
@@ -404,10 +381,6 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo) prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
prjGitPR, err := prset.GetPrjGitPR() prjGitPR, err := prset.GetPrjGitPR()
if err == common.PRSet_PrjGitMissing { if err == common.PRSet_PrjGitMissing {
if req.State != "open" {
common.LogDebug("This PR is closed and no ProjectGit PR. Ignoring.")
return nil
}
common.LogDebug("Missing PrjGit. Need to create one under branch", prjGitPRbranch) common.LogDebug("Missing PrjGit. Need to create one under branch", prjGitPRbranch)
if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil { if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil {
@@ -554,14 +527,6 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
return err return err
} }
// update prset if we should build it or not
if prjGitPR != nil {
if file, err := git.GitCatFile(common.DefaultGitPrj, prjGitPR.PR.Head.Sha, "staging.config"); err == nil {
prset.HasAutoStaging = (file != nil)
common.LogDebug(" -> automatic staging enabled?:", prset.HasAutoStaging)
}
}
// handle case where PrjGit PR is only one left and there are no changes, then we can just close the PR // handle case where PrjGit PR is only one left and there are no changes, then we can just close the PR
if len(prset.PRs) == 1 && prjGitPR != nil && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser { if len(prset.PRs) == 1 && prjGitPR != nil && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
common.LogDebug(" --> checking if superflous PR") common.LogDebug(" --> checking if superflous PR")
@@ -607,7 +572,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
type RequestProcessor struct { type RequestProcessor struct {
configuredRepos map[string][]*common.AutogitConfig configuredRepos map[string][]*common.AutogitConfig
recursive int recursive int
} }
func ProcesPullRequest(pr *models.PullRequest, configs []*common.AutogitConfig) error { func ProcesPullRequest(pr *models.PullRequest, configs []*common.AutogitConfig) error {
@@ -648,7 +613,7 @@ func (w *RequestProcessor) ProcessFunc(request *common.Request) (err error) {
common.LogError("Cannot find PR for issue:", req.Pull_Request.Base.Repo.Owner.Username, req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number) common.LogError("Cannot find PR for issue:", req.Pull_Request.Base.Repo.Owner.Username, req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number)
return err return err
} }
} else if req, ok := request.Data.(*common.IssueCommentWebhookEvent); ok { } else if req, ok := request.Data.(*common.IssueWebhookEvent); ok {
pr, err = Gitea.GetPullRequest(req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number)) pr, err = Gitea.GetPullRequest(req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
if err != nil { if err != nil {
common.LogError("Cannot find PR for issue:", req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number)) common.LogError("Cannot find PR for issue:", req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))

View File

@@ -103,7 +103,7 @@ func TestOpenPR(t *testing.T) {
} }
// 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, true) 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)
gitea.EXPECT().RequestReviews(giteaPR, "reviewer1", "reviewer2").Return(nil, nil) gitea.EXPECT().RequestReviews(giteaPR, "reviewer1", "reviewer2").Return(nil, nil)
gitea.EXPECT().GetPullRequestReviews("test", "testRepo", int64(0)).Return([]*models.PullReview{}, nil) gitea.EXPECT().GetPullRequestReviews("test", "testRepo", int64(0)).Return([]*models.PullReview{}, nil)
@@ -153,7 +153,7 @@ func TestOpenPR(t *testing.T) {
} }
failedErr := errors.New("Returned error here") failedErr := errors.New("Returned error here")
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, false) gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, failedErr)
err := pr.Process(event) err := pr.Process(event)
if err != failedErr { if err != failedErr {
@@ -193,7 +193,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().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)
gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil, true) gitea.EXPECT().CreatePullRequestIfNotExist(prjgit, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(giteaPR, nil)
gitea.EXPECT().RequestReviews(giteaPR, "reviewer1", "reviewer2").Return(nil, failedErr) gitea.EXPECT().RequestReviews(giteaPR, "reviewer1", "reviewer2").Return(nil, failedErr)
gitea.EXPECT().FetchMaintainershipDirFile("test", "prjcopy", "branch", "_project").Return(nil, "", repository.NewRepoGetRawFileNotFound()) gitea.EXPECT().FetchMaintainershipDirFile("test", "prjcopy", "branch", "_project").Return(nil, "", repository.NewRepoGetRawFileNotFound())

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"math/rand" "math/rand"
"path" "path"
@@ -42,15 +43,6 @@ func pullRequestToEventState(state models.StateType) string {
} }
func (s *DefaultStateChecker) ProcessPR(pr *models.PullRequest, config *common.AutogitConfig) error { func (s *DefaultStateChecker) ProcessPR(pr *models.PullRequest, config *common.AutogitConfig) error {
defer func() {
if r := recover(); r != nil {
common.LogError("panic caught in ProcessPR", common.PRtoString(pr))
if err, ok := r.(error); !ok {
common.LogError(err)
}
common.LogError(string(debug.Stack()))
}
}()
return ProcesPullRequest(pr, common.AutogitConfigs{config}) return ProcesPullRequest(pr, common.AutogitConfigs{config})
} }
@@ -159,7 +151,7 @@ func (s *DefaultStateChecker) VerifyProjectState(config *common.AutogitConfig) (
return PrjGitSubmoduleCheck(config, git, prjGitRepo, submodules) return PrjGitSubmoduleCheck(config, git, prjGitRepo, submodules)
} }
func (s *DefaultStateChecker) CheckRepos() { func (s *DefaultStateChecker) CheckRepos() error {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
common.LogError("panic caught") common.LogError("panic caught")
@@ -169,6 +161,7 @@ func (s *DefaultStateChecker) CheckRepos() {
common.LogError(string(debug.Stack())) common.LogError(string(debug.Stack()))
} }
}() }()
errorList := make([]error, 0, 10)
for org, configs := range s.processor.configuredRepos { for org, configs := range s.processor.configuredRepos {
for _, config := range configs { for _, config := range configs {
@@ -182,12 +175,12 @@ func (s *DefaultStateChecker) CheckRepos() {
prs, err := s.i.VerifyProjectState(config) prs, err := s.i.VerifyProjectState(config)
if err != nil { if err != nil {
common.LogError(" *** verification failed, org:", org, err) common.LogError(" *** verification failed, org:", org, err)
errorList = append(errorList, err)
} }
for _, pr := range prs { for _, pr := range prs {
prs, err := Gitea.GetRecentPullRequests(pr.Org, pr.Repo, pr.Branch) prs, err := Gitea.GetRecentPullRequests(pr.Org, pr.Repo, pr.Branch)
if err != nil { if err != nil {
common.LogError("Error fetching pull requests for", fmt.Sprintf("%s/%s#%s", pr.Org, pr.Repo, pr.Branch), err) return fmt.Errorf("Error fetching pull requests for %s/%s#%s. Err: %w", pr.Org, pr.Repo, pr.Branch, err)
break
} }
if len(prs) > 0 { if len(prs) > 0 {
common.LogDebug(fmt.Sprintf("%s/%s#%s", pr.Org, pr.Repo, pr.Branch), " - # of PRs to check:", len(prs)) common.LogDebug(fmt.Sprintf("%s/%s#%s", pr.Org, pr.Repo, pr.Branch), " - # of PRs to check:", len(prs))
@@ -200,11 +193,9 @@ func (s *DefaultStateChecker) CheckRepos() {
common.LogInfo(" ++ verification complete, org:", org, "config:", config.GitProjectName) common.LogInfo(" ++ verification complete, org:", org, "config:", config.GitProjectName)
} }
if len(configs) == 0 {
common.LogError(" org:", org, "has 0 configs?")
}
} }
return errors.Join(errorList...)
} }
func (s *DefaultStateChecker) ConsistencyCheckProcess() error { func (s *DefaultStateChecker) ConsistencyCheckProcess() error {