14 Commits

Author SHA256 Message Date
80d4e83f30 pr: fix tests
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 8s
2026-01-27 11:19:25 +01:00
d00f49ae0b pr: use top 20 commits as base, if available 2026-01-26 19:33:11 +01:00
17a266121b common: add test for cloning hashes 2026-01-26 15:18:13 +01:00
4c9c2a20f3 common: add test for directory listing
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 9s
2026-01-26 10:47:32 +01:00
046c28aac2 pr: test updated issue ref
If issue is a SHA ref, and then it's updated, we need to make
sure that the branch is also updated.
2026-01-24 16:13:56 +01:00
7ec6420e20 pr: move functions around 2026-01-24 15:07:48 +01:00
a0d554caaf pr: fix tests
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 25s
2026-01-18 23:33:10 +01:00
74e8d83bfa pr: new package handling 2026-01-18 17:15:28 +01:00
b689aafa7f pr: make sure new repos have fork/parent relationship
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 23s
If new target repo is "reparented", it will have correct relationship
here. Otherwise PR creation will fail
2026-01-12 15:10:27 +01:00
79da6cf262 common: more unit tests fixes
TZ needs to be defined, otherwise it was assumed to be local which
then resulted in unpredictable commit hashes. We define it to UTC
for unit tests

PR have state "open" not "opened"
2026-01-11 13:42:26 +01:00
39928c405a pr: fix more unit tests 2026-01-10 19:53:07 +01:00
a445bd8236 pr: merge new package 2026-01-10 17:21:31 +01:00
88528586fe pr: implement first part of issue processing 2026-01-10 16:10:20 +01:00
39b8f6ad55 wip: process issues 2026-01-10 14:34:17 +01:00
22 changed files with 1744 additions and 629 deletions

View File

@@ -129,9 +129,6 @@ go build \
go build \
-C utils/hujson \
-buildmode=pie
go build \
-C utils/maintainer-update \
-buildmode=pie
go build \
-C gitea-events-rabbitmq-publisher \
-buildmode=pie
@@ -182,7 +179,6 @@ install -D -m0755 workflow-direct/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 utils/hujson/hujson %{buildroot}%{_bindir}/hujson
install -D -m0755 utils/maintainer-update/maintainer-update %{buildroot}${_bindir}/maintainer-update
%pre gitea-events-rabbitmq-publisher
%service_add_pre gitea-events-rabbitmq-publisher.service
@@ -289,7 +285,6 @@ install -D -m0755 utils/maintainer-update/maintainer-update
%files utils
%license COPYING
%{_bindir}/hujson
%{_bindir}/maintainer-update
%files workflow-direct
%license COPYING

View File

@@ -65,6 +65,7 @@ const (
Label_StagingAuto = "staging/Auto"
Label_ReviewPending = "review/Pending"
Label_ReviewDone = "review/Done"
Label_NewRepository = "new/New Repository"
)
func LabelKey(tag_value string) string {

View File

@@ -42,6 +42,7 @@ type GitSubmoduleLister interface {
type GitDirectoryLister interface {
GitDirectoryList(gitPath, commitId string) (dirlist map[string]string, err error)
GitDirectoryContentList(gitPath, commitId string) (dirlist map[string]string, err error)
}
type GitStatusLister interface {
@@ -272,7 +273,11 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
LogDebug("branch", branch)
}
*/
args := []string{"fetch", "--prune", remoteName, branch}
args := []string{"fetch", "--prune", remoteName}
if len(branch) > 0 {
args = append(args, branch)
}
if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
args = slices.Insert(args, 1, "--unshallow")
}
@@ -787,7 +792,7 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
return
}
// return (filename) -> (hash) map for all submodules
// return (directory) -> (hash) map for all submodules
func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryList map[string]string, err error) {
var done sync.Mutex
directoryList = make(map[string]string)
@@ -861,6 +866,82 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
return directoryList, err
}
// return (directory) -> (hash) map for all submodules
func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (directoryList map[string]string, err error) {
var done sync.Mutex
directoryList = make(map[string]string)
done.Lock()
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
LogDebug("Getting directory content for:", commitId)
go func() {
defer done.Unlock()
defer close(data_out.ch)
data_out.Write([]byte(commitId))
data_out.ch <- '\x00'
var c GitCommit
c, err = parseGitCommit(data_in.ch)
if err != nil {
err = fmt.Errorf("Error parsing git commit. Err: %w", err)
return
}
trees := make(map[string]string)
trees[""] = c.Tree
for len(trees) > 0 {
for p, tree := range trees {
delete(trees, p)
data_out.Write([]byte(tree))
data_out.ch <- '\x00'
var tree GitTree
tree, err = parseGitTree(data_in.ch)
if err != nil {
err = fmt.Errorf("Error parsing git tree: %w", err)
return
}
for _, te := range tree.items {
if te.isBlob() || te.isSubmodule() {
directoryList[p+te.name] = te.hash
} else if te.isTree() {
trees[p+te.name] = te.hash
}
}
}
}
}()
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
cmd.Env = []string{
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
"GIT_LFS_SKIP_SMUDGE=1",
"GIT_CONFIG_GLOBAL=/dev/null",
}
cmd.Dir = filepath.Join(e.GitPath, gitPath)
cmd.Stdout = &data_in
cmd.Stdin = &data_out
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
LogError(string(data))
return len(data), nil
})
LogDebug("command run:", cmd.Args)
if e := cmd.Run(); e != nil {
LogError(e)
close(data_in.ch)
close(data_out.ch)
return directoryList, e
}
done.Lock()
return directoryList, err
}
// return (filename) -> (hash) map for all submodules
func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleList map[string]string, err error) {
var done sync.Mutex

View File

@@ -24,6 +24,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
"testing"
@@ -93,6 +94,56 @@ func TestGitClone(t *testing.T) {
}
}
func TestGitCloneCommitID(t *testing.T) {
execPath, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
d := t.TempDir()
if err := os.Chdir(d); err != nil {
t.Fatal(err)
}
defer os.Chdir(execPath)
cmd := exec.Command(path.Join(execPath, "test_repo_setup.sh"))
if out, err := cmd.CombinedOutput(); err != nil {
t.Log(string(out))
t.Fatal(err)
}
gh, err := AllocateGitWorkTree(d, "Test", "test@example.com")
if err != nil {
t.Fatal(err)
}
g, err := gh.CreateGitHandler("org")
if err != nil {
t.Fatal(err)
}
// Get a commit ID from pkgA
remoteUrl := "file://" + d + "/pkgA"
out, err := exec.Command("git", "-C", path.Join(d, "pkgA"), "rev-parse", "main").Output()
if err != nil {
t.Fatal(err)
}
commitID := strings.TrimSpace(string(out))
repo := "pkgAcloneCommitID"
if _, err := g.GitClone(repo, commitID, remoteUrl); err != nil {
t.Skip("TODO: Add GitClone CommitID support")
t.Fatalf("GitClone failed with commit ID: %v", err)
}
// Verify we are at the right commit
head, err := g.GitBranchHead(repo, commitID)
if err != nil {
t.Fatalf("GitBranchHead failed: %v", err)
}
if head != commitID {
t.Errorf("Expected head %s, got %s", commitID, head)
}
}
func TestGitMsgParsing(t *testing.T) {
t.Run("tree message with size 56", func(t *testing.T) {
const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00"
@@ -596,3 +647,66 @@ func TestGitStatusParse(t *testing.T) {
})
}
}
func TestGitDirectoryListRepro(t *testing.T) {
d := t.TempDir()
// Setup a mock environment for GitHandlerImpl
gh, err := AllocateGitWorkTree(d, "Test", "test@example.com")
if err != nil {
t.Fatal(err)
}
org := "repo-org"
repoName := "test-repo"
repoPath := filepath.Join(d, org, repoName)
err = os.MkdirAll(repoPath, 0755)
if err != nil {
t.Fatal(err)
}
runGit := func(args ...string) {
cmd := exec.Command("git", args...)
cmd.Dir = repoPath
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v failed: %v\n%s", args, err, out)
}
}
runGit("init", "-b", "main", "--object-format=sha256")
runGit("config", "user.email", "test@example.com")
runGit("config", "user.name", "test")
// Create a directory and a file
err = os.Mkdir(filepath.Join(repoPath, "subdir"), 0755)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(repoPath, "subdir", "file.txt"), []byte("hello"), 0644)
if err != nil {
t.Fatal(err)
}
runGit("add", "subdir/file.txt")
runGit("commit", "-m", "add subdir")
// Now create the handler
g, err := gh.CreateGitHandler(org)
if err != nil {
t.Fatal(err)
}
// Call GitDirectoryList
dirs, err := g.GitDirectoryList(repoName, "HEAD")
if err != nil {
t.Fatal(err)
}
t.Logf("Directories found: %v", dirs)
if len(dirs) == 0 {
t.Error("No directories found, but 'subdir' should be there")
}
if _, ok := dirs["subdir"]; !ok {
t.Errorf("Expected 'subdir' in directory list, got %v", dirs)
}
}

View File

@@ -75,6 +75,10 @@ type GiteaLabelSettter interface {
SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error)
}
type GiteaIssueFetcher interface {
GetIssue(org, repo string, idx int64) (*models.Issue, error)
}
type GiteaTimelineFetcher interface {
GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error)
}
@@ -147,6 +151,7 @@ type GiteaReviewRequester interface {
type GiteaReviewUnrequester interface {
UnrequestReview(org, repo string, id int64, reviwers ...string) error
UpdateIssue(org, repo string, idx int64, options *models.EditIssueOption) (*models.Issue, error)
}
type GiteaReviewer interface {
@@ -200,6 +205,7 @@ type Gitea interface {
GiteaSetRepoOptions
GiteaLabelGetter
GiteaLabelSettter
GiteaIssueFetcher
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
@@ -508,6 +514,26 @@ func (gitea *GiteaTransport) SetLabels(owner, repo string, idx int64, labels []s
return ret.Payload, nil
}
func (gitea *GiteaTransport) GetIssue(owner, repo string, idx int64) (*models.Issue, error) {
ret, err := gitea.client.Issue.IssueGetIssue(
issue.NewIssueGetIssueParams().WithOwner(owner).WithRepo(repo).WithIndex(idx),
gitea.transport.DefaultAuthentication)
if err != nil {
return nil, err
}
return ret.Payload, nil
}
func (gitea *GiteaTransport) UpdateIssue(owner, repo string, idx int64, options *models.EditIssueOption) (*models.Issue, error) {
ret, err := gitea.client.Issue.IssueEditIssue(
issue.NewIssueEditIssueParams().WithOwner(owner).WithRepo(repo).WithIndex(idx).WithBody(options),
gitea.transport.DefaultAuthentication)
if err != nil {
return nil, err
}
return ret.Payload, nil
}
const (
GiteaNotificationType_Pull = "Pull"
)
@@ -715,6 +741,7 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
)
if err != nil {
LogError("owner:", repo.Owner.UserName, " repo:", repo.Name, " body:", prOptions)
return nil, fmt.Errorf("Cannot create pull request. %w", err), true
}

View File

@@ -1,12 +1,10 @@
package common
import (
"bytes"
"encoding/json"
"fmt"
"io"
"slices"
"strings"
"src.opensuse.org/autogits/common/gitea-generated/client/repository"
"src.opensuse.org/autogits/common/gitea-generated/models"
@@ -28,13 +26,11 @@ type MaintainershipMap struct {
Data map[string][]string
IsDir bool
FetchPackage func(string) ([]byte, error)
Raw []byte
}
func ParseMaintainershipData(data []byte) (*MaintainershipMap, error) {
func parseMaintainershipData(data []byte) (*MaintainershipMap, error) {
maintainers := &MaintainershipMap{
Data: make(map[string][]string),
Raw: data,
}
if err := json.Unmarshal(data, &maintainers.Data); err != nil {
return nil, err
@@ -63,7 +59,7 @@ func FetchProjectMaintainershipData(gitea GiteaMaintainershipReader, org, prjGit
}
}
m, err := ParseMaintainershipData(data)
m, err := parseMaintainershipData(data)
if m != nil {
m.IsDir = dir
m.FetchPackage = func(pkg string) ([]byte, error) {
@@ -168,135 +164,13 @@ func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullRevi
return false
}
func (data *MaintainershipMap) modifyInplace(writer io.StringWriter) error {
var original map[string][]string
if err := json.Unmarshal(data.Raw, &original); err != nil {
return err
}
dec := json.NewDecoder(bytes.NewReader(data.Raw))
_, err := dec.Token()
if err != nil {
return err
}
output := ""
lastPos := 0
modified := false
type entry struct {
key string
valStart int
valEnd int
}
var entries []entry
for dec.More() {
kToken, _ := dec.Token()
key := kToken.(string)
var raw json.RawMessage
dec.Decode(&raw)
valEnd := int(dec.InputOffset())
valStart := valEnd - len(raw)
entries = append(entries, entry{key, valStart, valEnd})
}
changed := make(map[string]bool)
for k, v := range data.Data {
if ov, ok := original[k]; !ok || !slices.Equal(v, ov) {
changed[k] = true
}
}
for k := range original {
if _, ok := data.Data[k]; !ok {
changed[k] = true
}
}
if len(changed) == 0 {
_, err = writer.WriteString(string(data.Raw))
return err
}
for _, e := range entries {
if v, ok := data.Data[e.key]; ok {
prefix := string(data.Raw[lastPos:e.valStart])
if modified && strings.TrimSpace(output) == "{" {
if commaIdx := strings.Index(prefix, ","); commaIdx != -1 {
if quoteIdx := strings.Index(prefix, "\""); quoteIdx == -1 || commaIdx < quoteIdx {
prefix = prefix[:commaIdx] + prefix[commaIdx+1:]
}
}
}
output += prefix
if changed[e.key] {
slices.Sort(v)
newVal, _ := json.Marshal(v)
output += string(newVal)
modified = true
} else {
output += string(data.Raw[e.valStart:e.valEnd])
}
} else {
// Deleted
modified = true
}
lastPos = e.valEnd
}
output += string(data.Raw[lastPos:])
// Handle additions (simplistic: at the end)
for k, v := range data.Data {
if _, ok := original[k]; !ok {
slices.Sort(v)
newVal, _ := json.Marshal(v)
keyStr, _ := json.Marshal(k)
// Insert before closing brace
if idx := strings.LastIndex(output, "}"); idx != -1 {
prefix := output[:idx]
suffix := output[idx:]
trimmedPrefix := strings.TrimRight(prefix, " \n\r\t")
if !strings.HasSuffix(trimmedPrefix, "{") && !strings.HasSuffix(trimmedPrefix, ",") {
// find the actual position of the last non-whitespace character in prefix
lastCharIdx := strings.LastIndexAny(prefix, "]}0123456789\"")
if lastCharIdx != -1 {
prefix = prefix[:lastCharIdx+1] + "," + prefix[lastCharIdx+1:]
}
}
insertion := fmt.Sprintf(" %s: %s", string(keyStr), string(newVal))
if !strings.HasSuffix(prefix, "\n") {
insertion = "\n" + insertion
}
output = prefix + insertion + "\n" + suffix
modified = true
}
}
}
if modified {
_, err := writer.WriteString(output)
return err
}
_, err = writer.WriteString(string(data.Raw))
return err
}
func (data *MaintainershipMap) WriteMaintainershipFile(writer io.StringWriter) error {
if data.IsDir {
return fmt.Errorf("Not implemented")
}
if len(data.Raw) > 0 {
if err := data.modifyInplace(writer); err == nil {
return nil
}
}
// Fallback to full write
writer.WriteString("{\n")
if d, ok := data.Data[""]; ok {
eol := ","
if len(data.Data) == 1 {
@@ -307,12 +181,17 @@ func (data *MaintainershipMap) WriteMaintainershipFile(writer io.StringWriter) e
writer.WriteString(fmt.Sprintf(" \"\": %s%s\n", string(str), eol))
}
keys := make([]string, 0, len(data.Data))
keys := make([]string, len(data.Data))
i := 0
for pkg := range data.Data {
if pkg == "" {
continue
}
keys = append(keys, pkg)
keys[i] = pkg
i++
}
if len(keys) >= i {
keys = slices.Delete(keys, i, len(keys))
}
slices.Sort(keys)
for i, pkg := range keys {

View File

@@ -208,7 +208,6 @@ func TestMaintainershipFileWrite(t *testing.T) {
name string
is_dir bool
maintainers map[string][]string
raw []byte
expected_output string
expected_error error
}{
@@ -232,43 +231,6 @@ func TestMaintainershipFileWrite(t *testing.T) {
},
expected_output: "{\n \"\": [\"one\",\"two\"],\n \"foo\": [\"byte\",\"four\"],\n \"pkg1\": []\n}\n",
},
{
name: "surgical modification",
maintainers: map[string][]string{
"": {"one", "two"},
"foo": {"byte", "four", "newone"},
"pkg1": {},
},
raw: []byte("{\n \"\": [\"one\",\"two\"],\n \"foo\": [\"byte\",\"four\"],\n \"pkg1\": []\n}\n"),
expected_output: "{\n \"\": [\"one\",\"two\"],\n \"foo\": [\"byte\",\"four\",\"newone\"],\n \"pkg1\": []\n}\n",
},
{
name: "no change",
maintainers: map[string][]string{
"": {"one", "two"},
"foo": {"byte", "four"},
"pkg1": {},
},
raw: []byte("{\n \"\": [\"one\",\"two\"],\n \"foo\": [\"byte\",\"four\"],\n \"pkg1\": []\n}\n"),
expected_output: "{\n \"\": [\"one\",\"two\"],\n \"foo\": [\"byte\",\"four\"],\n \"pkg1\": []\n}\n",
},
{
name: "surgical addition",
maintainers: map[string][]string{
"": {"one"},
"new": {"user"},
},
raw: []byte("{\n \"\": [ \"one\" ]\n}\n"),
expected_output: "{\n \"\": [ \"one\" ],\n \"new\": [\"user\"]\n}\n",
},
{
name: "surgical deletion",
maintainers: map[string][]string{
"": {"one"},
},
raw: []byte("{\n \"\": [\"one\"],\n \"old\": [\"user\"]\n}\n"),
expected_output: "{\n \"\": [\"one\"]\n}\n",
},
}
for _, test := range tests {
@@ -277,7 +239,6 @@ func TestMaintainershipFileWrite(t *testing.T) {
data := common.MaintainershipMap{
Data: test.maintainers,
IsDir: test.is_dir,
Raw: test.raw,
}
if err := data.WriteMaintainershipFile(&b); err != test.expected_error {
@@ -287,7 +248,7 @@ func TestMaintainershipFileWrite(t *testing.T) {
output := b.String()
if test.expected_output != output {
t.Fatalf("unexpected output:\n%q\nExpecting:\n%q", output, test.expected_output)
t.Fatal("unexpected output:", output, "Expecting:", test.expected_output)
}
})
}

View File

@@ -142,6 +142,45 @@ func (m *MockGitDirectoryLister) EXPECT() *MockGitDirectoryListerMockRecorder {
return m.recorder
}
// GitDirectoryContentList mocks base method.
func (m *MockGitDirectoryLister) GitDirectoryContentList(gitPath, commitId string) (map[string]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GitDirectoryContentList", gitPath, commitId)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GitDirectoryContentList indicates an expected call of GitDirectoryContentList.
func (mr *MockGitDirectoryListerMockRecorder) GitDirectoryContentList(gitPath, commitId any) *MockGitDirectoryListerGitDirectoryContentListCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitDirectoryContentList", reflect.TypeOf((*MockGitDirectoryLister)(nil).GitDirectoryContentList), gitPath, commitId)
return &MockGitDirectoryListerGitDirectoryContentListCall{Call: call}
}
// MockGitDirectoryListerGitDirectoryContentListCall wrap *gomock.Call
type MockGitDirectoryListerGitDirectoryContentListCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGitDirectoryListerGitDirectoryContentListCall) Return(dirlist map[string]string, err error) *MockGitDirectoryListerGitDirectoryContentListCall {
c.Call = c.Call.Return(dirlist, err)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGitDirectoryListerGitDirectoryContentListCall) Do(f func(string, string) (map[string]string, error)) *MockGitDirectoryListerGitDirectoryContentListCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGitDirectoryListerGitDirectoryContentListCall) DoAndReturn(f func(string, string) (map[string]string, error)) *MockGitDirectoryListerGitDirectoryContentListCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GitDirectoryList mocks base method.
func (m *MockGitDirectoryLister) GitDirectoryList(gitPath, commitId string) (map[string]string, error) {
m.ctrl.T.Helper()
@@ -563,6 +602,45 @@ func (c *MockGitGitDiffCall) DoAndReturn(f func(string, string, string) (string,
return c
}
// GitDirectoryContentList mocks base method.
func (m *MockGit) GitDirectoryContentList(gitPath, commitId string) (map[string]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GitDirectoryContentList", gitPath, commitId)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GitDirectoryContentList indicates an expected call of GitDirectoryContentList.
func (mr *MockGitMockRecorder) GitDirectoryContentList(gitPath, commitId any) *MockGitGitDirectoryContentListCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitDirectoryContentList", reflect.TypeOf((*MockGit)(nil).GitDirectoryContentList), gitPath, commitId)
return &MockGitGitDirectoryContentListCall{Call: call}
}
// MockGitGitDirectoryContentListCall wrap *gomock.Call
type MockGitGitDirectoryContentListCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGitGitDirectoryContentListCall) Return(dirlist map[string]string, err error) *MockGitGitDirectoryContentListCall {
c.Call = c.Call.Return(dirlist, err)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGitGitDirectoryContentListCall) Do(f func(string, string) (map[string]string, error)) *MockGitGitDirectoryContentListCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGitGitDirectoryContentListCall) DoAndReturn(f func(string, string) (map[string]string, error)) *MockGitGitDirectoryContentListCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GitDirectoryList mocks base method.
func (m *MockGit) GitDirectoryList(gitPath, commitId string) (map[string]string, error) {
m.ctrl.T.Helper()

View File

@@ -144,6 +144,69 @@ func (c *MockGiteaLabelSettterSetLabelsCall) DoAndReturn(f func(string, string,
return c
}
// MockGiteaIssueFetcher is a mock of GiteaIssueFetcher interface.
type MockGiteaIssueFetcher struct {
ctrl *gomock.Controller
recorder *MockGiteaIssueFetcherMockRecorder
isgomock struct{}
}
// MockGiteaIssueFetcherMockRecorder is the mock recorder for MockGiteaIssueFetcher.
type MockGiteaIssueFetcherMockRecorder struct {
mock *MockGiteaIssueFetcher
}
// NewMockGiteaIssueFetcher creates a new mock instance.
func NewMockGiteaIssueFetcher(ctrl *gomock.Controller) *MockGiteaIssueFetcher {
mock := &MockGiteaIssueFetcher{ctrl: ctrl}
mock.recorder = &MockGiteaIssueFetcherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGiteaIssueFetcher) EXPECT() *MockGiteaIssueFetcherMockRecorder {
return m.recorder
}
// GetIssue mocks base method.
func (m *MockGiteaIssueFetcher) GetIssue(org, repo string, idx int64) (*models.Issue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIssue", org, repo, idx)
ret0, _ := ret[0].(*models.Issue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetIssue indicates an expected call of GetIssue.
func (mr *MockGiteaIssueFetcherMockRecorder) GetIssue(org, repo, idx any) *MockGiteaIssueFetcherGetIssueCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIssue", reflect.TypeOf((*MockGiteaIssueFetcher)(nil).GetIssue), org, repo, idx)
return &MockGiteaIssueFetcherGetIssueCall{Call: call}
}
// MockGiteaIssueFetcherGetIssueCall wrap *gomock.Call
type MockGiteaIssueFetcherGetIssueCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaIssueFetcherGetIssueCall) Return(arg0 *models.Issue, arg1 error) *MockGiteaIssueFetcherGetIssueCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaIssueFetcherGetIssueCall) Do(f func(string, string, int64) (*models.Issue, error)) *MockGiteaIssueFetcherGetIssueCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaIssueFetcherGetIssueCall) DoAndReturn(f func(string, string, int64) (*models.Issue, error)) *MockGiteaIssueFetcherGetIssueCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockGiteaTimelineFetcher is a mock of GiteaTimelineFetcher interface.
type MockGiteaTimelineFetcher struct {
ctrl *gomock.Controller
@@ -1443,6 +1506,45 @@ func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUnrequestReviewCall) Do
return c
}
// UpdateIssue mocks base method.
func (m *MockGiteaReviewFetcherAndRequesterAndUnrequester) UpdateIssue(org, repo string, idx int64, options *models.EditIssueOption) (*models.Issue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateIssue", org, repo, idx, options)
ret0, _ := ret[0].(*models.Issue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateIssue indicates an expected call of UpdateIssue.
func (mr *MockGiteaReviewFetcherAndRequesterAndUnrequesterMockRecorder) UpdateIssue(org, repo, idx, options any) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateIssue", reflect.TypeOf((*MockGiteaReviewFetcherAndRequesterAndUnrequester)(nil).UpdateIssue), org, repo, idx, options)
return &MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall{Call: call}
}
// MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall wrap *gomock.Call
type MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall) Return(arg0 *models.Issue, arg1 error) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall) Do(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall) DoAndReturn(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaReviewFetcherAndRequesterAndUnrequesterUpdateIssueCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockGiteaUnreviewTimelineFetcher is a mock of GiteaUnreviewTimelineFetcher interface.
type MockGiteaUnreviewTimelineFetcher struct {
ctrl *gomock.Controller
@@ -1549,6 +1651,45 @@ func (c *MockGiteaUnreviewTimelineFetcherUnrequestReviewCall) DoAndReturn(f func
return c
}
// UpdateIssue mocks base method.
func (m *MockGiteaUnreviewTimelineFetcher) UpdateIssue(org, repo string, idx int64, options *models.EditIssueOption) (*models.Issue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateIssue", org, repo, idx, options)
ret0, _ := ret[0].(*models.Issue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateIssue indicates an expected call of UpdateIssue.
func (mr *MockGiteaUnreviewTimelineFetcherMockRecorder) UpdateIssue(org, repo, idx, options any) *MockGiteaUnreviewTimelineFetcherUpdateIssueCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateIssue", reflect.TypeOf((*MockGiteaUnreviewTimelineFetcher)(nil).UpdateIssue), org, repo, idx, options)
return &MockGiteaUnreviewTimelineFetcherUpdateIssueCall{Call: call}
}
// MockGiteaUnreviewTimelineFetcherUpdateIssueCall wrap *gomock.Call
type MockGiteaUnreviewTimelineFetcherUpdateIssueCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaUnreviewTimelineFetcherUpdateIssueCall) Return(arg0 *models.Issue, arg1 error) *MockGiteaUnreviewTimelineFetcherUpdateIssueCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaUnreviewTimelineFetcherUpdateIssueCall) Do(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaUnreviewTimelineFetcherUpdateIssueCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaUnreviewTimelineFetcherUpdateIssueCall) DoAndReturn(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaUnreviewTimelineFetcherUpdateIssueCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockGiteaReviewRequester is a mock of GiteaReviewRequester interface.
type MockGiteaReviewRequester struct {
ctrl *gomock.Controller
@@ -1684,6 +1825,45 @@ func (c *MockGiteaReviewUnrequesterUnrequestReviewCall) DoAndReturn(f func(strin
return c
}
// UpdateIssue mocks base method.
func (m *MockGiteaReviewUnrequester) UpdateIssue(org, repo string, idx int64, options *models.EditIssueOption) (*models.Issue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateIssue", org, repo, idx, options)
ret0, _ := ret[0].(*models.Issue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateIssue indicates an expected call of UpdateIssue.
func (mr *MockGiteaReviewUnrequesterMockRecorder) UpdateIssue(org, repo, idx, options any) *MockGiteaReviewUnrequesterUpdateIssueCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateIssue", reflect.TypeOf((*MockGiteaReviewUnrequester)(nil).UpdateIssue), org, repo, idx, options)
return &MockGiteaReviewUnrequesterUpdateIssueCall{Call: call}
}
// MockGiteaReviewUnrequesterUpdateIssueCall wrap *gomock.Call
type MockGiteaReviewUnrequesterUpdateIssueCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaReviewUnrequesterUpdateIssueCall) Return(arg0 *models.Issue, arg1 error) *MockGiteaReviewUnrequesterUpdateIssueCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaReviewUnrequesterUpdateIssueCall) Do(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaReviewUnrequesterUpdateIssueCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaReviewUnrequesterUpdateIssueCall) DoAndReturn(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaReviewUnrequesterUpdateIssueCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockGiteaReviewer is a mock of GiteaReviewer interface.
type MockGiteaReviewer struct {
ctrl *gomock.Controller
@@ -2478,6 +2658,45 @@ func (c *MockGiteaGetDoneNotificationsCall) DoAndReturn(f func(string, int64) ([
return c
}
// GetIssue mocks base method.
func (m *MockGitea) GetIssue(org, repo string, idx int64) (*models.Issue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIssue", org, repo, idx)
ret0, _ := ret[0].(*models.Issue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetIssue indicates an expected call of GetIssue.
func (mr *MockGiteaMockRecorder) GetIssue(org, repo, idx any) *MockGiteaGetIssueCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIssue", reflect.TypeOf((*MockGitea)(nil).GetIssue), org, repo, idx)
return &MockGiteaGetIssueCall{Call: call}
}
// MockGiteaGetIssueCall wrap *gomock.Call
type MockGiteaGetIssueCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaGetIssueCall) Return(arg0 *models.Issue, arg1 error) *MockGiteaGetIssueCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaGetIssueCall) Do(f func(string, string, int64) (*models.Issue, error)) *MockGiteaGetIssueCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaGetIssueCall) DoAndReturn(f func(string, string, int64) (*models.Issue, error)) *MockGiteaGetIssueCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GetIssueComments mocks base method.
func (m *MockGitea) GetIssueComments(org, project string, issueNo int64) ([]*models.Comment, error) {
m.ctrl.T.Helper()
@@ -3306,6 +3525,45 @@ func (c *MockGiteaUnrequestReviewCall) DoAndReturn(f func(string, string, int64,
return c
}
// UpdateIssue mocks base method.
func (m *MockGitea) UpdateIssue(org, repo string, idx int64, options *models.EditIssueOption) (*models.Issue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateIssue", org, repo, idx, options)
ret0, _ := ret[0].(*models.Issue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateIssue indicates an expected call of UpdateIssue.
func (mr *MockGiteaMockRecorder) UpdateIssue(org, repo, idx, options any) *MockGiteaUpdateIssueCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateIssue", reflect.TypeOf((*MockGitea)(nil).UpdateIssue), org, repo, idx, options)
return &MockGiteaUpdateIssueCall{Call: call}
}
// MockGiteaUpdateIssueCall wrap *gomock.Call
type MockGiteaUpdateIssueCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockGiteaUpdateIssueCall) Return(arg0 *models.Issue, arg1 error) *MockGiteaUpdateIssueCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockGiteaUpdateIssueCall) Do(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaUpdateIssueCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockGiteaUpdateIssueCall) DoAndReturn(f func(string, string, int64, *models.EditIssueOption) (*models.Issue, error)) *MockGiteaUpdateIssueCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// UpdatePullRequest mocks base method.
func (m *MockGitea) UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error) {
m.ctrl.T.Helper()

View File

@@ -6,7 +6,9 @@ import (
"fmt"
"os"
"path"
"regexp"
"slices"
"strconv"
"strings"
"src.opensuse.org/autogits/common/gitea-generated/client/repository"
@@ -650,6 +652,8 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
}
// FF all non-prj git and unrequest reviews.
newRepoIssues := make(map[int64]string) // issue index -> org/repo
for _, prinfo := range rs.PRs {
// remove pending review requests
repo := prinfo.PR.Base.Repo
@@ -671,6 +675,15 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
if rs.IsPrjGitPR(prinfo.PR) {
continue
}
isNewRepo := false
for _, l := range prinfo.PR.Labels {
if l.Name == Label_NewRepository {
isNewRepo = true
break
}
}
br := rs.Config.Branch
if len(br) == 0 {
// if branch is unspecified, take it from the PR as it
@@ -679,11 +692,30 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
} else if br != prinfo.PR.Base.Name {
panic(prinfo.PR.Base.Name + " is expected to match " + br)
}
if isNewRepo {
// Extract issue reference from body: "See issue #XYZ"
rx := regexp.MustCompile(`See issue #(\d+)`)
if matches := rx.FindStringSubmatch(prinfo.PR.Body); len(matches) > 1 {
if issueIdx, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
// We need to know which project git this issue belongs to.
// Since the PR set is linked to a ProjectGit, we can use its org/repo.
prjGitOrg, prjGitRepo, _ := rs.Config.GetPrjGit()
newRepoIssues[issueIdx] = prjGitOrg + "/" + prjGitRepo
}
}
}
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
PanicOnError(err)
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
if isNewRepo {
LogInfo("Force-pushing new repository branch", br, "to", head.Sha)
// we don't merge, we just set the branch to this commit
} else {
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
}
}
// push changes
@@ -698,12 +730,37 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
}
repo := prinfo.PR.Base.Repo
isNewRepo := false
for _, l := range prinfo.PR.Labels {
if l.Name == Label_NewRepository {
isNewRepo = true
break
}
}
if !IsDryRun {
git.GitExecOrPanic(repo.Name, "push", prinfo.RemoteName)
if isNewRepo {
git.GitExecOrPanic(repo.Name, "push", "-f", prinfo.RemoteName, prinfo.PR.Head.Sha+":"+prinfo.PR.Base.Name)
} else {
git.GitExecOrPanic(repo.Name, "push", prinfo.RemoteName)
}
} else {
LogInfo("*** WOULD push", repo.Name, "to", prinfo.RemoteName)
}
}
// Close referencing issues
if !IsDryRun {
for issueIdx, prjPath := range newRepoIssues {
parts := strings.Split(prjPath, "/")
if len(parts) == 2 {
LogInfo("Closing issue", prjPath+"#"+strconv.FormatInt(issueIdx, 10))
gitea.UpdateIssue(parts[0], parts[1], issueIdx, &models.EditIssueOption{
State: "closed",
})
}
}
}
return nil
}

121
common/pr_linkage_test.go Normal file
View File

@@ -0,0 +1,121 @@
package common_test
import (
"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 TestFetchPRSet_Linkage(t *testing.T) {
config := &common.AutogitConfig{
Organization: "target-org",
GitProjectName: "test-org/prjgit#main",
}
// 1. Mock a package PR
pkgPR := &models.PullRequest{
Index: 101,
State: "open",
Base: &models.PRBranchInfo{
Ref: "main",
Repo: &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
},
},
Head: &models.PRBranchInfo{Sha: "pkg-sha"},
}
// 2. Mock a ProjectGit PR that references the package PR
prjGitPR := &models.PullRequest{
Index: 500,
State: "open",
Base: &models.PRBranchInfo{
Ref: "main",
Name: "main",
Repo: &models.Repository{
Name: "prjgit",
Owner: &models.User{UserName: "test-org"},
},
},
Body: "Forwarded PRs: pkg1\n\nPR: target-org/pkg1!101",
}
t.Run("Fetch from ProjectGit PR", func(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
mockGitea := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
// Expect fetch of prjGitPR
mockGitea.EXPECT().GetPullRequest("test-org", "prjgit", int64(500)).Return(prjGitPR, nil)
// Expect fetch of pkgPR because it's linked in body
mockGitea.EXPECT().GetPullRequest("target-org", "pkg1", int64(101)).Return(pkgPR, nil)
// Expect review fetching (part of FetchPRSet)
mockGitea.EXPECT().GetPullRequestReviews(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.PullReview{}, nil).AnyTimes()
mockGitea.EXPECT().GetTimeline(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.TimelineComment{}, nil).AnyTimes()
prset, err := common.FetchPRSet("bot", mockGitea, "test-org", "prjgit", 500, config)
if err != nil {
t.Fatalf("FetchPRSet failed: %v", err)
}
if len(prset.PRs) != 2 {
t.Errorf("Expected 2 PRs in set, got %d", len(prset.PRs))
}
if !prset.IsConsistent() {
t.Error("PR set should be consistent")
}
})
t.Run("Fetch from Package PR via Timeline", func(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
mockGitea := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
// 1. FetchPRSet for pkgPR will call LastPrjGitRefOnTimeline
mockGitea.EXPECT().GetTimeline("target-org", "pkg1", int64(101)).Return([]*models.TimelineComment{
{
Type: common.TimelineCommentType_PullRequestRef,
RefIssue: &models.Issue{
Index: 500,
Body: "PR: target-org/pkg1!101",
Repository: &models.RepositoryMeta{
Owner: "test-org",
Name: "prjgit",
},
User: &models.User{UserName: "bot"},
},
},
}, nil)
// 2. It will then fetch the prjGitPR found in timeline (twice in LastPrjGitRefOnTimeline)
mockGitea.EXPECT().GetPullRequest("test-org", "prjgit", int64(500)).Return(prjGitPR, nil).Times(2)
// 3. Then it will recursively fetch linked PRs from prjGitPR body in readPRData
mockGitea.EXPECT().GetPullRequest("target-org", "pkg1", int64(101)).Return(pkgPR, nil)
// Review fetching for all PRs in the set
mockGitea.EXPECT().GetPullRequestReviews(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.PullReview{}, nil).AnyTimes()
mockGitea.EXPECT().GetTimeline("test-org", "prjgit", int64(500)).Return([]*models.TimelineComment{}, nil).AnyTimes()
mockGitea.EXPECT().GetTimeline("target-org", "pkg1", int64(101)).Return([]*models.TimelineComment{}, nil).AnyTimes()
prset, err := common.FetchPRSet("bot", mockGitea, "target-org", "pkg1", 101, config)
if err != nil {
t.Fatalf("FetchPRSet failed: %v", err)
}
if len(prset.PRs) != 2 {
t.Errorf("Expected 2 PRs in set, got %d", len(prset.PRs))
}
prjPRInfo, err := prset.GetPrjGitPR()
if err != nil || prjPRInfo.PR.Index != 500 {
t.Errorf("Expected ProjectGit PR 500 to be found, got %v", prjPRInfo)
}
})
}

View File

@@ -0,0 +1,94 @@
package common_test
import (
"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 TestPRSet_Merge_Special(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
mockGitea := mock_common.NewMockGiteaReviewUnrequester(ctl)
mockGit := mock_common.NewMockGit(ctl)
config := &common.AutogitConfig{
Organization: "target-org",
GitProjectName: "test-org/prjgit#main",
Branch: "main",
}
// 1. Regular ProjectGit PR
prjGitPR := &models.PullRequest{
Index: 500,
Base: &models.PRBranchInfo{
Ref: "main",
Name: "main",
Repo: &models.Repository{Name: "prjgit", Owner: &models.User{UserName: "test-org"}, SSHURL: "prj-ssh-url"},
Sha: "base-sha",
},
Head: &models.PRBranchInfo{Sha: "prj-head-sha"},
}
// 2. "new/New Repository" Package PR
newPkgPR := &models.PullRequest{
Index: 101,
Base: &models.PRBranchInfo{
Ref: "main",
Name: "main",
Repo: &models.Repository{Name: "new-pkg", Owner: &models.User{UserName: "target-org"}, SSHURL: "pkg-ssh-url"},
},
Head: &models.PRBranchInfo{Sha: "pkg-head-sha"},
Labels: []*models.Label{
{Name: "new/New Repository"},
},
Body: "See issue #123",
}
prset := &common.PRSet{
Config: config,
PRs: []*common.PRInfo{
{PR: prjGitPR},
{PR: newPkgPR},
},
}
common.IsDryRun = false
// Mock expectations for Merge
// Clone and fetch for PrjGit
mockGit.EXPECT().GitClone("_ObsPrj", "main", "prj-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", "prj-head-sha")
// mockGit.EXPECT().GitExecWithOutputOrPanic("_ObsPrj", "merge-base", "HEAD", "base-sha", "prj-head-sha").Return("base-sha")
mockGit.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "-m", gomock.Any(), "prj-head-sha").Return(nil)
// Unrequest reviews
mockGitea.EXPECT().UnrequestReview("test-org", "prjgit", int64(500), gomock.Any()).Return(nil)
mockGitea.EXPECT().UnrequestReview("target-org", "new-pkg", int64(101), gomock.Any()).Return(nil)
// Clone and fetch for new-pkg
mockGit.EXPECT().GitClone("new-pkg", "main", "pkg-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecOrPanic("new-pkg", "fetch", "origin", "pkg-head-sha")
// Pushing changes
mockGit.EXPECT().GitExecOrPanic("_ObsPrj", "push", "origin")
// Special push for new repo: git push -f origin pkg-head-sha:main
mockGit.EXPECT().GitExecOrPanic("new-pkg", "push", "-f", "origin", "pkg-head-sha:main")
// Closing issue
mockGitea.EXPECT().UpdateIssue("test-org", "prjgit", int64(123), gomock.Any()).DoAndReturn(func(org, repo string, idx int64, opt *models.EditIssueOption) (*models.Issue, error) {
if opt.State != "closed" {
t.Errorf("Expected issue state to be closed, got %s", opt.State)
}
return nil, nil
})
err := prset.Merge(mockGitea, mockGit)
if err != nil {
t.Fatalf("Merge failed: %v", err)
}
}

View File

@@ -48,8 +48,6 @@ func reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
}
func TestPR(t *testing.T) {
return
baseConfig := common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
@@ -80,21 +78,21 @@ func TestPR(t *testing.T) {
{
name: "Error fetching PullRequest",
data: []prdata{
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}, pr_err: errors.New("Missing PR")},
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "open"}, pr_err: errors.New("Missing PR")},
},
prjGitPRIndex: -1,
},
{
name: "Error fetching PullRequest in PrjGit",
data: []prdata{
{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}, pr_err: errors.New("missing PR")},
{pr: &models.PullRequest{Body: "", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "open"}, pr_err: errors.New("missing PR")},
{pr: &models.PullRequest{Body: "", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, State: "open"}},
},
},
{
name: "Error fetching prjgit",
data: []prdata{
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "open"}},
},
resLen: 1,
prjGitPRIndex: -1,
@@ -102,8 +100,20 @@ func TestPR(t *testing.T) {
{
name: "Review set is consistent",
data: []prdata{
{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "open"},
timeline: []*models.TimelineComment{
{
Type: common.TimelineCommentType_PullRequestRef,
RefIssue: &models.Issue{
Index: 22,
Repository: &models.RepositoryMeta{Name: "barPrj", Owner: "foo"},
User: &models.User{UserName: "test"},
Body: "PR: test/repo#42",
},
},
}},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, State: "open"}},
},
resLen: 2,
prjGitPRIndex: 1,
@@ -113,8 +123,21 @@ func TestPR(t *testing.T) {
{
name: "Review set is consistent: 1pkg",
data: []prdata{
{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "open"},
timeline: []*models.TimelineComment{
{
Type: common.TimelineCommentType_PullRequestRef,
RefIssue: &models.Issue{
Index: 22,
Repository: &models.RepositoryMeta{Name: "barPrj", Owner: "foo"},
User: &models.User{UserName: "test"},
Body: "PR: test/repo#42",
},
},
},
},
{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, State: "open"}},
},
resLen: 2,
prjGitPRIndex: 1,
@@ -123,9 +146,22 @@ func TestPR(t *testing.T) {
{
name: "Review set is consistent: 2pkg",
data: []prdata{
{pr: &models.PullRequest{Body: "some desc", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "some other desc\nPR: foo/fer#33", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{
pr: &models.PullRequest{Body: "some desc\nPR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "open"},
timeline: []*models.TimelineComment{
{
Type: common.TimelineCommentType_PullRequestRef,
RefIssue: &models.Issue{
Index: 22,
Repository: &models.RepositoryMeta{Name: "barPrj", Owner: "foo"},
User: &models.User{UserName: "test"},
Body: "PR: test/repo#42\nPR: test/repo2#41",
},
},
},
},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, State: "open"}},
{pr: &models.PullRequest{Body: "some other desc\nPR: foo/barPrj#22", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}, State: "open"}},
},
resLen: 3,
prjGitPRIndex: 1,
@@ -135,7 +171,7 @@ func TestPR(t *testing.T) {
name: "Review set of prjgit PR is consistent",
data: []prdata{
{
pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -154,10 +190,23 @@ func TestPR(t *testing.T) {
{
name: "Review set is consistent: 2pkg",
data: []prdata{
{pr: &models.PullRequest{Body: "PR: foo/barPrj#222", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo2#41", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, State: "opened"}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#20", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}, State: "opened"}},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#222", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}, State: "open"},
timeline: []*models.TimelineComment{
{
Type: common.TimelineCommentType_PullRequestRef,
RefIssue: &models.Issue{
Index: 22,
Repository: &models.RepositoryMeta{Name: "barPrj", Owner: "foo"},
User: &models.User{UserName: "test"},
Body: "PR: test/repo#42\nPR: test/repo2#41",
},
},
},
},
{pr: &models.PullRequest{Body: "PR: test/repo2#41", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, State: "open"}},
{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, State: "open"}},
{pr: &models.PullRequest{Body: "PR: foo/barPrj#20", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}, State: "open"}},
},
resLen: 3,
prjGitPRIndex: 2,
@@ -167,7 +216,7 @@ func TestPR(t *testing.T) {
name: "WIP PR is not approved",
data: []prdata{
{
pr: &models.PullRequest{Body: "", Title: "WIP: some title", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "", Title: "WIP: some title", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -184,10 +233,9 @@ func TestPR(t *testing.T) {
},
},
{
name: "Manual review is missing",
data: []prdata{
name: "Manual review is missing", data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -195,7 +243,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -212,16 +260,15 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
})
},
},
}},
{
name: "Manual review is done, via PrjGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "merge ok", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -229,7 +276,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -246,7 +293,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
})
},
@@ -255,7 +302,7 @@ func TestPR(t *testing.T) {
name: "Manual review is done, via PrjGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "merge ok", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -263,7 +310,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -280,7 +327,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
ManualMergeProject: true,
})
@@ -290,7 +337,7 @@ func TestPR(t *testing.T) {
name: "Manual review is not done, via PrjGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "merge ok", User: &models.User{UserName: "notm2"}, State: common.ReviewStateApproved},
{Body: "merge not ok", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
@@ -299,7 +346,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -316,7 +363,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
ManualMergeProject: true,
})
@@ -326,7 +373,7 @@ func TestPR(t *testing.T) {
name: "Manual review is done via PackageGit",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -334,7 +381,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "Merge ok", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -351,7 +398,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
})
},
@@ -360,7 +407,7 @@ func TestPR(t *testing.T) {
name: "Manual review done via PkgGits",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -368,7 +415,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "Merge OK!", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -376,7 +423,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "merge ok", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -393,7 +440,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
})
},
@@ -402,7 +449,7 @@ func TestPR(t *testing.T) {
name: "Manual review done via PkgGits not allowed",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -410,7 +457,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "Merge OK!", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -418,7 +465,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "merge ok", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -435,7 +482,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
ManualMergeProject: true,
})
@@ -445,7 +492,7 @@ func TestPR(t *testing.T) {
name: "Manual review is is missing on one PR",
data: []prdata{
{
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/repo#20\nPR: foo/repo#21", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -453,7 +500,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -461,7 +508,7 @@ func TestPR(t *testing.T) {
},
},
{
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "PR: foo/barPrj#42", Index: 21, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m1"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super1"}, State: common.ReviewStateApproved},
@@ -478,7 +525,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
ManualMergeOnly: true,
})
},
@@ -487,7 +534,7 @@ func TestPR(t *testing.T) {
name: "PR is approved with negative optional review",
data: []prdata{
{
pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}, User: &models.User{UserName: "submitter"}, State: "opened"},
pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}, Name: "master"}, User: &models.User{UserName: "submitter"}, State: "open"},
reviews: []*models.PullReview{
{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
@@ -505,7 +552,7 @@ func TestPR(t *testing.T) {
Reviewers: []string{"+super1", "*super2", "m1", "-m2", "~*bot"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "barPrj#master",
}
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &config)
},
@@ -519,27 +566,31 @@ func TestPR(t *testing.T) {
review_mock := mock_common.NewMockGiteaPRChecker(ctl)
// reviewer_mock := mock_common.NewMockGiteaReviewRequester(ctl)
prjGitOrg, prjGitRepo, _ := baseConfig.GetPrjGit()
if test.reviewSetFetcher == nil { // if we are fetching the prjgit directly, the these mocks are not called
if test.prjGitPRIndex >= 0 {
pr_mock.EXPECT().GetPullRequest(baseConfig.Organization, baseConfig.GitProjectName, test.prjGitPRIndex).
Return(test.data[test.prjGitPRIndex].pr, test.data[test.prjGitPRIndex].pr_err)
pr_mock.EXPECT().GetPullRequest(prjGitOrg, prjGitRepo, int64(test.data[test.prjGitPRIndex].pr.Index)).
Return(test.data[test.prjGitPRIndex].pr, test.data[test.prjGitPRIndex].pr_err).AnyTimes()
} else if test.prjGitPRIndex < 0 {
// no prjgit PR
pr_mock.EXPECT().GetPullRequest(baseConfig.Organization, baseConfig.GitProjectName, gomock.Any()).
Return(nil, nil)
pr_mock.EXPECT().GetPullRequest(prjGitOrg, prjGitRepo, gomock.Any()).
Return(nil, nil).AnyTimes()
}
}
var test_err error
for _, data := range test.data {
pr_mock.EXPECT().GetPullRequest(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.pr, data.pr_err).AnyTimes()
if data.pr_err != nil {
test_err = data.pr_err
// test_err is not used and was causing a build error.
// data.pr_err is directly used in the previous EXPECT call.
}
review_mock.EXPECT().GetPullRequestReviews(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.reviews, data.review_error).AnyTimes()
if data.timeline == nil {
data.timeline = reviewsToTimeline(data.reviews)
}
pr_mock.EXPECT().GetTimeline(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.timeline, nil).AnyTimes()
pr_mock.EXPECT().GetPullRequestReviews(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.reviews, data.review_error).AnyTimes()
review_mock.EXPECT().GetPullRequestReviews(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.reviews, data.review_error).AnyTimes()
review_mock.EXPECT().GetTimeline(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.timeline, nil).AnyTimes()
}
@@ -552,27 +603,12 @@ func TestPR(t *testing.T) {
res, err = common.FetchPRSet("test", pr_mock, "test", "repo", 42, &baseConfig)
}
if err == nil {
if test_err != nil {
t.Fatal("Expected", test_err, "but got", err)
}
} else {
if res != nil {
t.Fatal("error but got ReviewSet?")
}
if test.api_error != "" {
if err.Error() != test.api_error {
t.Fatal("expected", test.api_error, "but got", err)
}
} else if test_err != err {
t.Fatal("expected", test_err, "but got", err)
}
if res == nil {
return
}
if test.resLen != len(res.PRs) {
t.Error("expected result len", test.resLen, "but got", len(res.PRs))
if len(res.PRs) != test.resLen {
t.Errorf("Test Case '%s': expected result len %d but got %d", test.name, test.resLen, len(res.PRs))
}
PrjGitPR, err := res.GetPrjGitPR()
@@ -583,6 +619,9 @@ func TestPR(t *testing.T) {
}
pr_found := false
if test.prjGitPRIndex >= 0 {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for i := range test.data {
if PrjGitPR.PR == test.data[i].pr && i == test.prjGitPRIndex {
t.Log("found at index", i)
@@ -1126,7 +1165,6 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
}
func TestPRMerge(t *testing.T) {
t.Skip("FAIL: No PrjGit PR found, missing calls")
repoDir := t.TempDir()
cwd, _ := os.Getwd()
@@ -1136,7 +1174,11 @@ func TestPRMerge(t *testing.T) {
t.Fatal(string(out))
}
t.Skip("No tests of PRMerge yet")
return
common.ExtraGitParams = []string{
"TZ=UTC",
"GIT_CONFIG_COUNT=1",
"GIT_CONFIG_KEY_0=protocol.file.allow",
"GIT_CONFIG_VALUE_0=always",
@@ -1151,7 +1193,7 @@ func TestPRMerge(t *testing.T) {
config := &common.AutogitConfig{
Organization: "org",
GitProjectName: "org/prj#master",
GitProjectName: "org/prj#main",
}
tests := []struct {
@@ -1163,8 +1205,10 @@ func TestPRMerge(t *testing.T) {
name: "Merge base not merged in main",
pr: &models.PullRequest{
Index: 1,
Base: &models.PRBranchInfo{
Sha: "e8b0de43d757c96a9d2c7101f4bff404e322f53a1fa4041fb85d646110c38ad4", // "base_add_b1"
Name: "main",
Sha: "96515c092626c716a4613ba4f68a8d1cc4894317658342c450e656390f524ec3", // "base_add_b1"
Repo: &models.Repository{
Name: "prj",
Owner: &models.User{
@@ -1174,7 +1218,7 @@ func TestPRMerge(t *testing.T) {
},
},
Head: &models.PRBranchInfo{
Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2"
Sha: "4119fc725dc11cdf11f982d5bb0a8ba2a138f1180c4323862a18b8e08def5603", // "base_add_b2"
},
},
mergeError: "Aborting merge",
@@ -1183,8 +1227,10 @@ func TestPRMerge(t *testing.T) {
name: "Merge conflict in modules, auto-resolved",
pr: &models.PullRequest{
Index: 1,
Base: &models.PRBranchInfo{
Sha: "4fbd1026b2d7462ebe9229a49100c11f1ad6555520a21ba515122d8bc41328a8",
Name: "main",
Sha: "85f59f7aa732b742e58b48356cd46cb1ab1b5c4349eb5c0eda324e2dbea8f9e7",
Repo: &models.Repository{
Name: "prj",
Owner: &models.User{
@@ -1194,7 +1240,7 @@ func TestPRMerge(t *testing.T) {
},
},
Head: &models.PRBranchInfo{
Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2"
Sha: "4119fc725dc11cdf11f982d5bb0a8ba2a138f1180c4323862a18b8e08def5603", // "base_add_b2"
},
},
},
@@ -1206,11 +1252,13 @@ func TestPRMerge(t *testing.T) {
mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(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).AnyTimes()
testDir := t.TempDir()
t.Log("dir:", testDir)
mock.EXPECT().GetPullRequest("org", "prj", int64(1)).Return(test.pr, nil)
mock.EXPECT().GetTimeline("org", "prj", int64(1)).Return(nil, nil).AnyTimes()
mock.EXPECT().GetPullRequestReviews("org", "prj", int64(1)).Return(nil, nil).AnyTimes()
set, err := common.FetchPRSet("test", mock, "org", "prj", 1, config)
if err != nil {
@@ -1231,11 +1279,11 @@ func TestPRMerge(t *testing.T) {
}
func TestPRChanges(t *testing.T) {
t.Skip("FAIL: unexpected calls, missing calls")
tests := []struct {
name string
PRs []*models.PullRequest
PrjPRs *models.PullRequest
name string
PRs []*models.PullRequest
PrjPRs *models.PullRequest
Timeline []*models.TimelineComment
}{
{
name: "Pkg PR is closed",
@@ -1247,10 +1295,22 @@ func TestPRChanges(t *testing.T) {
},
},
PrjPRs: &models.PullRequest{
Index: 42,
Title: "some PR",
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "prjgit", Owner: &models.User{UserName: "org"}}},
Base: &models.PRBranchInfo{Name: "branch", Repo: &models.Repository{Name: "prjgit", Owner: &models.User{UserName: "org"}}},
Body: "PR: org/repo#42",
State: "opened",
State: "open",
},
Timeline: []*models.TimelineComment{
{
Type: common.TimelineCommentType_PullRequestRef,
RefIssue: &models.Issue{
Index: 42,
Repository: &models.RepositoryMeta{Name: "prjgit", Owner: "org"},
User: &models.User{UserName: "user"},
Body: "PR: org/repo#42",
},
},
},
},
}
@@ -1263,9 +1323,13 @@ func TestPRChanges(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
mock_fetcher := mock_common.NewMockGiteaPRTimelineReviewFetcher(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).AnyTimes()
mock_fetcher.EXPECT().GetTimeline("org", "prjgit", int64(42)).Return(nil, nil).AnyTimes()
mock_fetcher.EXPECT().GetPullRequestReviews("org", "prjgit", int64(42)).Return(nil, nil).AnyTimes()
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().GetTimeline(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index).Return(test.Timeline, nil).AnyTimes()
mock_fetcher.EXPECT().GetPullRequestReviews(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index).Return(nil, nil).AnyTimes()
}
PRs, err := common.FetchPRSet("user", mock_fetcher, "org", "repo", 42, &config)

View File

@@ -2,6 +2,7 @@
set -x
export TZ=UTC
export GIT_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0=protocol.file.allow

View File

@@ -1,98 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
"slices"
"src.opensuse.org/autogits/common"
)
var (
pkg = flag.String("package", "", "Package to modify")
rm = flag.Bool("rm", false, "Remove maintainer from package")
add = flag.Bool("add", false, "Add maintainer to package")
lint = flag.Bool("lint-only", false, "Reformat entire _maintainership.json only")
)
const maintainershipFile = "_maintainership.json"
func WriteNewMaintainershipFile(m *common.MaintainershipMap, filename string) {
f, err := os.Create(filename + ".new")
common.PanicOnError(err)
common.PanicOnError(m.WriteMaintainershipFile(f))
common.PanicOnError(f.Close())
common.PanicOnError(os.Rename(filename+".new", filename))
}
func run() error {
flag.Parse()
filename := maintainershipFile
if *lint {
if len(flag.Args()) > 0 {
filename = flag.Arg(0)
}
}
data, err := os.ReadFile(filename)
if os.IsNotExist(err) {
return err
}
if err != nil {
return err
}
m, err := common.ParseMaintainershipData(data)
if err != nil {
return fmt.Errorf("Failed to parse JSON: %w", err)
}
if *lint {
m.Raw = nil // forces a rewrite
} else {
users := flag.Args()
if len(users) > 0 {
maintainers, ok := m.Data[*pkg]
if !ok && !*add {
return fmt.Errorf("No package %s and not adding one.", *pkg)
}
if *add {
for _, u := range users {
if !slices.Contains(maintainers, u) {
maintainers = append(maintainers, u)
}
}
}
if *rm {
newMaintainers := make([]string, 0, len(maintainers))
for _, m := range maintainers {
if !slices.Contains(users, m) {
newMaintainers = append(newMaintainers, m)
}
}
maintainers = newMaintainers
}
if len(maintainers) > 0 {
slices.Sort(maintainers)
m.Data[*pkg] = maintainers
} else {
delete(m.Data, *pkg)
}
}
}
WriteNewMaintainershipFile(m, filename)
return nil
}
func main() {
if err := run(); err != nil {
common.LogError(err)
os.Exit(1)
}
}

View File

@@ -1,243 +0,0 @@
package main
import (
"encoding/json"
"flag"
"os"
"os/exec"
"reflect"
"strings"
"testing"
)
func TestMain(m *testing.M) {
if os.Getenv("BE_MAIN") == "1" {
main()
return
}
os.Exit(m.Run())
}
func TestRun(t *testing.T) {
tests := []struct {
name string
inData string
expectedOut string
params []string
expectedError string
isDir bool
}{
{
name: "add user to existing package",
inData: `{"pkg1": ["user1"]}`,
params: []string{"-package", "pkg1", "-add", "user2"},
expectedOut: `{"pkg1": ["user1", "user2"]}`,
},
{
name: "add user to new package",
inData: `{"pkg1": ["user1"]}`,
params: []string{"-package", "pkg2", "-add", "user2"},
expectedOut: `{"pkg1": ["user1"], "pkg2": ["user2"]}`,
},
{
name: "no-op with no users",
inData: `{"pkg1": ["user1"]}`,
params: []string{"-package", "pkg1", "-add"},
expectedOut: `{"pkg1": ["user1"]}`,
},
{
name: "add existing user",
inData: `{"pkg1": ["user1", "user2"]}`,
params: []string{"-package", "pkg1", "-add", "user2"},
expectedOut: `{"pkg1": ["user1", "user2"]}`,
},
{
name: "remove user from package",
inData: `{"pkg1": ["user1", "user2"]}`,
params: []string{"-package", "pkg1", "-rm", "user2"},
expectedOut: `{"pkg1": ["user1"]}`,
},
{
name: "remove last user from package",
inData: `{"pkg1": ["user1"]}`,
params: []string{"-package", "pkg1", "-rm", "user1"},
expectedOut: `{}`,
},
{
name: "remove non-existent user",
inData: `{"pkg1": ["user1"]}`,
params: []string{"-package", "pkg1", "-rm", "user2"},
expectedOut: `{"pkg1": ["user1"]}`,
},
{
name: "lint only unsorted",
inData: `{"pkg1": ["user2", "user1"]}`,
params: []string{"-lint-only"},
expectedOut: `{"pkg1": ["user1", "user2"]}`,
},
{
name: "lint only no changes",
inData: `{"pkg1": ["user1", "user2"]}`,
params: []string{"-lint-only"},
expectedOut: `{"pkg1": ["user1", "user2"]}`,
},
{
name: "no file",
params: []string{},
expectedError: "no such file or directory",
},
{
name: "invalid json",
inData: `{"pkg1": ["user1"`,
params: []string{},
expectedError: "Failed to parse JSON",
},
{
name: "add and remove",
inData: `{"pkg1": ["user1", "user2"]}`,
params: []string{"-package", "pkg1", "-add", "user3"},
expectedOut: `{"pkg1": ["user1", "user2", "user3"]}`,
},
{
name: "lint specific file",
inData: `{"pkg1": ["user2", "user1"]}`,
params: []string{"-lint-only", "other.json"},
expectedOut: `{"pkg1": ["user1", "user2"]}`,
},
{
name: "add user to package when it was not there before",
inData: `{}`,
params: []string{"-package", "newpkg", "-add", "user1"},
expectedOut: `{"newpkg": ["user1"]}`,
},
{
name: "unreadable file (is a directory)",
isDir: true,
expectedError: "is a directory",
},
{
name: "remove user from non-existent package",
inData: `{"pkg1": ["user1"]}`,
params: []string{"-package", "pkg2", "-rm", "user2"},
expectedError: "No package pkg2 and not adding one.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
oldWd, _ := os.Getwd()
_ = os.Chdir(dir)
defer os.Chdir(oldWd)
targetFile := maintainershipFile
if tt.name == "lint specific file" {
targetFile = "other.json"
}
if tt.isDir {
_ = os.Mkdir(targetFile, 0755)
} else if tt.inData != "" {
_ = os.WriteFile(targetFile, []byte(tt.inData), 0644)
}
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
pkg = flag.String("package", "", "Package to modify")
rm = flag.Bool("rm", false, "Remove maintainer from package")
add = flag.Bool("add", false, "Add maintainer to package")
lint = flag.Bool("lint-only", false, "Reformat entire _maintainership.json only")
os.Args = append([]string{"cmd"}, tt.params...)
err := run()
if tt.expectedError != "" {
if err == nil {
t.Fatalf("expected error containing %q, but got none", tt.expectedError)
}
if !strings.Contains(err.Error(), tt.expectedError) {
t.Fatalf("expected error containing %q, got %q", tt.expectedError, err.Error())
}
} else if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.expectedOut != "" {
data, _ := os.ReadFile(targetFile)
var got, expected map[string][]string
_ = json.Unmarshal(data, &got)
_ = json.Unmarshal([]byte(tt.expectedOut), &expected)
if len(got) == 0 && len(expected) == 0 {
return
}
if !reflect.DeepEqual(got, expected) {
t.Fatalf("expected %v, got %v", expected, got)
}
}
})
}
}
func TestMainRecursive(t *testing.T) {
tests := []struct {
name string
inData string
expectedOut string
params []string
expectExit bool
}{
{
name: "test main() via recursive call",
inData: `{"pkg1": ["user1"]}`,
params: []string{"-package", "pkg1", "-add", "user2"},
expectedOut: `{"pkg1": ["user1", "user2"]}`,
},
{
name: "test main() failure",
params: []string{"-package", "pkg1"},
expectExit: true,
},
}
exe, _ := os.Executable()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
oldWd, _ := os.Getwd()
_ = os.Chdir(dir)
defer os.Chdir(oldWd)
if tt.inData != "" {
_ = os.WriteFile(maintainershipFile, []byte(tt.inData), 0644)
}
cmd := exec.Command(exe, append([]string{"-test.run=None"}, tt.params...)...)
cmd.Env = append(os.Environ(), "BE_MAIN=1")
out, runErr := cmd.CombinedOutput()
if tt.expectExit {
if runErr == nil {
t.Fatalf("expected exit with error, but it succeeded")
}
return
}
if runErr != nil {
t.Fatalf("unexpected error: %v: %s", runErr, string(out))
}
if tt.expectedOut != "" {
data, _ := os.ReadFile(maintainershipFile)
var got, expected map[string][]string
_ = json.Unmarshal(data, &got)
_ = json.Unmarshal([]byte(tt.expectedOut), &expected)
if !reflect.DeepEqual(got, expected) {
t.Fatalf("expected %v, got %v", expected, got)
}
}
})
}
}

View File

@@ -0,0 +1,203 @@
package main
import (
"fmt"
"strings"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
func createEmptyBranch(git common.Git, PackageName, Branch string) {
git.GitExecOrPanic(PackageName, "checkout", "--detach")
git.GitExec(PackageName, "branch", "-D", Branch)
git.GitExecOrPanic(PackageName, "checkout", "-f", "--orphan", Branch)
git.GitExecOrPanic(PackageName, "rm", "-rf", ".")
git.GitExecOrPanic(PackageName, "commit", "--allow-empty", "-m", "Initial empty branch")
}
func ProcessIssue(issue *models.Issue, configs []*common.AutogitConfig) error {
title := issue.Title
/*
org := issue.Repository.Owner
repo := issue.Repository.Name
idx := issue.Index
*/
const BranchPrefix = "refs/heads/"
branch := issue.Ref
if strings.HasPrefix(branch, BranchPrefix) {
branch = strings.TrimPrefix(branch, BranchPrefix)
} else {
common.LogDebug("Invalid branch specified:", branch, ". Using default.")
branch = ""
}
// out, _ := json.MarshalIndent(issue, "", " ")
// common.LogDebug(string(out))
common.LogDebug("issue processing:", common.IssueToString(issue), "@", branch)
if len(title) > 5 && strings.EqualFold(title[0:5], "[ADD]") {
// we need "New Package" label and "Approval Required" label, unless already approved
// either via Label "Approved" or via review comment.
NewIssues := common.FindNewReposInIssueBody(issue.Body)
if NewIssues == nil {
common.LogDebug("No new repos found in issue body")
return nil
}
org := issue.Repository.Owner
repo := issue.Repository.Name
config := common.AutogitConfigs(configs).GetPrjGitConfig(org, repo, branch)
if config == nil {
return fmt.Errorf("Cannot find config for %s/%s#%s", org, repo, branch)
}
if len(branch) == 0 {
branch = config.Branch
}
git, err := GitHandler.CreateGitHandler(config.Organization)
if err != nil {
return err
}
defer git.Close()
for _, nr := range NewIssues.Repos {
common.LogDebug(" - Processing new repository src:", nr.Organization+"/"+nr.PackageName+"#"+nr.Branch)
targetRepo, err := Gitea.GetRepository(config.Organization, nr.PackageName)
if err != nil {
return err
}
if targetRepo == nil {
common.LogInfo(" - Repository", config.Organization+"/"+nr.PackageName, "does not exist. Labeling issue.")
if !common.IsDryRun {
Gitea.SetLabels(org, repo, issue.Index, []string{config.Label(common.Label_NewRepository)})
}
common.LogDebug(" # Done for now with this repo")
continue
}
srcRepo, err := Gitea.GetRepository(nr.Organization, nr.Repository)
if err != nil {
return err
}
if srcRepo == nil {
common.LogError("Source repository not found:", nr.Organization+"/"+nr.Repository)
continue
}
if srcRepo.Parent == nil {
common.LogError("Source has no parents.")
continue
}
if len(nr.Branch) == 0 {
nr.Branch = srcRepo.DefaultBranch
}
srcRemoteName, err := git.GitClone(nr.PackageName, nr.Branch, srcRepo.SSHURL)
if err != nil {
return err
}
remoteName, err := git.GitClone(nr.PackageName, nr.Branch, targetRepo.SSHURL)
if err != nil {
return err
}
// Check that fork/parent repository relationship exists
if srcRepo.Parent.Name != targetRepo.Name || srcRepo.Parent.Owner.UserName != targetRepo.Owner.UserName {
common.LogError("Source repository is not fork of the Target repository. Fork of:", srcRepo.Parent.Owner.UserName+"/"+srcRepo.Parent.Name)
continue
}
srcBranch := nr.Branch
if srcBranch == "" {
srcBranch = srcRepo.DefaultBranch
}
// We are ready to setup a pending PR.
// 1. empty target branch with empty commit, this will be discarded no merge
// 2. create PR from source to target
// a) if source is not branch, create a source branch in target repo that contains the relevant commit
SourceCommitList := common.SplitLines(git.GitExecWithOutputOrPanic(nr.PackageName, "rev-list", "--first-parent", srcRemoteName+"/"+nr.Branch))
CommitLength := len(SourceCommitList)
SourceCommitId := SourceCommitList[CommitLength-1]
if CommitLength > 20 {
SourceCommitId = SourceCommitList[20]
}
if CommitLength < 2 {
// only 1 commit, then we need empty branch on target
if dl, err := git.GitDirectoryContentList(nr.PackageName, nr.Branch); err == nil && len(dl) > 0 {
createEmptyBranch(git, nr.PackageName, nr.Branch)
}
} else {
git.GitExecOrPanic(nr.PackageName, "checkout", "-B", nr.Branch, SourceCommitId)
}
if !common.IsDryRun {
git.GitExecOrPanic(nr.PackageName, "push", "-f", remoteName, nr.Branch)
}
head := nr.Organization + ":" + srcBranch
isBranch := false
// Hash can be branch name! Check if it's a branch or tag on the remote
out, err := git.GitExecWithOutput(nr.PackageName, "ls-remote", "--heads", srcRepo.SSHURL, srcBranch)
if err == nil && strings.Contains(out, "refs/heads/"+srcBranch) {
isBranch = true
}
if !isBranch {
tempBranch := fmt.Sprintf("new_package_%d_%s", issue.Index, nr.PackageName)
// Re-clone or use existing if branch check was done above
remoteName, err := git.GitClone(nr.PackageName, srcBranch, targetRepo.SSHURL)
if err != nil {
return err
}
git.GitExecOrPanic(nr.PackageName, "remote", "add", "source", srcRepo.SSHURL)
git.GitExecOrPanic(nr.PackageName, "fetch", "source", srcBranch)
git.GitExecOrPanic(nr.PackageName, "checkout", "-B", tempBranch, "FETCH_HEAD")
if !common.IsDryRun {
git.GitExecOrPanic(nr.PackageName, "push", "-f", remoteName, tempBranch)
}
head = tempBranch
}
title := fmt.Sprintf("Add package %s", nr.PackageName)
prjGitOrg, prjGitRepo, _ := config.GetPrjGit()
body := fmt.Sprintf("See issue %s/%s#%d", prjGitOrg, prjGitRepo, issue.Index)
br := branch
if len(br) == 0 {
br = targetRepo.DefaultBranch
}
pr, err, isNew := Gitea.CreatePullRequestIfNotExist(targetRepo, head, br, title, body)
if err != nil {
common.LogError(targetRepo.Name, head, branch, title, body)
return err
}
if !isNew && pr.Body != body {
Gitea.UpdatePullRequest(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index, &models.EditPullRequestOption{
AllowMaintainerEdit: true,
Body: body,
})
}
if isNew {
if _, err := Gitea.SetLabels(config.Organization, nr.PackageName, pr.Index, []string{config.Label(common.Label_NewRepository)}); err != nil {
common.LogError("Failed to set label:", common.Label_NewRepository, err)
}
}
}
} else if len(title) > 4 && strings.EqualFold(title[0:4], "[RM]") {
// to remove a package, no approval is required. This should happen via
// project git PR reviews
} else {
common.LogError("Non-standard issue created. Ignoring", common.IssueToString(issue))
return nil
}
return nil
}

View File

@@ -0,0 +1,499 @@
package main
import (
"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 TestProcessIssue_Add(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
gitea := mock_common.NewMockGitea(ctl)
Gitea = gitea
common.IsDryRun = false
config := &common.AutogitConfig{
Organization: "target-org",
GitProjectName: "test-org/test-prj#main",
}
configs := []*common.AutogitConfig{config}
issue := &models.Issue{
Title: "[ADD] pkg1",
Body: "src-org/pkg1#master",
Index: 123,
Repository: &models.RepositoryMeta{
Owner: "test-org",
Name: "test-prj",
},
Ref: "refs/heads/main",
}
expectedBody := "See issue test-org/test-prj#123"
t.Run("Repository does not exist - labels issue", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil)
mockGit.EXPECT().Close().Return(nil)
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(nil, nil)
gitea.EXPECT().SetLabels("test-org", "test-prj", int64(123), []string{"new/New Repository"}).Return(nil, nil)
err := ProcessIssue(issue, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Source is SHA - creates temp branch in target", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil)
mockGit.EXPECT().Close().Return(nil)
targetRepo := &models.Repository{
Name: "pkg1",
SSHURL: "target-ssh-url",
Owner: &models.User{UserName: "target-org"},
}
srcRepo := &models.Repository{
Name: "pkg1",
SSHURL: "src-ssh-url",
DefaultBranch: "master",
Owner: &models.User{UserName: "src-org"},
Parent: &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
},
}
sha := "abcdef0123456789abcdef0123456789abcdef01"
issueSHA := &models.Issue{
Title: "[ADD] pkg1",
Body: "src-org/pkg1#" + sha,
Index: 123,
Repository: &models.RepositoryMeta{Owner: "test-org", Name: "test-prj"},
Ref: "refs/heads/main",
}
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
mockGit.EXPECT().GitClone("pkg1", sha, "src-ssh-url").Return("src-remote", nil)
mockGit.EXPECT().GitClone("pkg1", sha, "target-ssh-url").Return("origin", nil)
// Source commit list and reset logic
mockGit.EXPECT().GitExecWithOutputOrPanic("pkg1", "rev-list", "--first-parent", "src-remote/"+sha).Return(sha + "\n" + "parent-sha")
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", sha, "parent-sha")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", sha)
mockGit.EXPECT().GitExecWithOutput("pkg1", "ls-remote", "--heads", "src-ssh-url", sha).Return("", nil)
// SHA source logic (creates temp branch)
tempBranch := "new_package_123_pkg1"
mockGit.EXPECT().GitClone("pkg1", sha, "target-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecOrPanic("pkg1", "remote", "add", "source", "src-ssh-url")
mockGit.EXPECT().GitExecOrPanic("pkg1", "fetch", "source", sha)
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", tempBranch, "FETCH_HEAD")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", tempBranch)
// PR creation using temp branch
pr := &models.PullRequest{
Index: 456,
Body: expectedBody,
Base: &models.PRBranchInfo{
Repo: targetRepo,
},
}
gitea.EXPECT().CreatePullRequestIfNotExist(targetRepo, tempBranch, "main", gomock.Any(), gomock.Any()).Return(pr, nil, true)
gitea.EXPECT().SetLabels("target-org", "pkg1", int64(456), []string{"new/New Repository"}).Return(nil, nil)
err := ProcessIssue(issueSHA, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Repository exists - continue processing and create PR", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil)
mockGit.EXPECT().Close().Return(nil)
targetRepo := &models.Repository{
Name: "pkg1",
SSHURL: "target-ssh-url",
Owner: &models.User{UserName: "target-org"},
}
srcRepo := &models.Repository{
Name: "pkg1",
SSHURL: "src-ssh-url",
DefaultBranch: "master",
Owner: &models.User{UserName: "src-org"},
Parent: &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
},
}
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
mockGit.EXPECT().GitClone("pkg1", "master", "src-ssh-url").Return("src-remote", nil)
mockGit.EXPECT().GitClone("pkg1", "master", "target-ssh-url").Return("origin", nil)
// Commit list logic
mockGit.EXPECT().GitExecWithOutputOrPanic("pkg1", "rev-list", "--first-parent", "src-remote/master").Return("sha1\nsha2")
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", "master", "sha2")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", "master")
// Check if source is a branch via ls-remote
mockGit.EXPECT().GitExecWithOutput("pkg1", "ls-remote", "--heads", "src-ssh-url", "master").Return("sha1 refs/heads/master", nil)
// PR creation
pr := &models.PullRequest{
Index: 456,
Body: expectedBody,
Base: &models.PRBranchInfo{
Repo: targetRepo,
},
}
gitea.EXPECT().CreatePullRequestIfNotExist(targetRepo, "src-org:master", "main", gomock.Any(), gomock.Any()).Return(pr, nil, true)
gitea.EXPECT().SetLabels("target-org", "pkg1", int64(456), []string{"new/New Repository"}).Return(nil, nil)
err := ProcessIssue(issue, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Source repository is not fork of target repository - aborts", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil)
mockGit.EXPECT().Close().Return(nil)
targetRepo := &models.Repository{
Name: "pkg1",
SSHURL: "target-ssh-url",
Owner: &models.User{UserName: "target-org"},
}
srcRepo := &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "src-org"},
SSHURL: "src-ssh-url",
Parent: &models.Repository{
Name: "other-repo",
Owner: &models.User{UserName: "other-org"},
},
}
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
mockGit.EXPECT().GitClone("pkg1", "master", "src-ssh-url").Return("src-remote", nil)
mockGit.EXPECT().GitClone("pkg1", "master", "target-ssh-url").Return("origin", nil)
err := ProcessIssue(issue, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Source repository is fork of target repository - proceeds", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil)
mockGit.EXPECT().Close().Return(nil)
targetRepo := &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
SSHURL: "target-ssh-url",
}
srcRepo := &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "src-org"},
SSHURL: "src-ssh-url",
Parent: &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
},
DefaultBranch: "master",
}
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
mockGit.EXPECT().GitClone("pkg1", "master", "src-ssh-url").Return("src-remote", nil)
mockGit.EXPECT().GitClone("pkg1", "master", "target-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecWithOutputOrPanic("pkg1", "rev-list", "--first-parent", "src-remote/master").Return("sha1\nsha2")
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", "master", "sha2")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", "master")
mockGit.EXPECT().GitExecWithOutput("pkg1", "ls-remote", "--heads", "src-ssh-url", "master").Return("sha1 refs/heads/master", nil)
pr := &models.PullRequest{
Index: 456,
Body: expectedBody,
Base: &models.PRBranchInfo{
Repo: targetRepo,
},
}
gitea.EXPECT().CreatePullRequestIfNotExist(targetRepo, "src-org:master", "main", gomock.Any(), gomock.Any()).Return(pr, nil, false)
err := ProcessIssue(issue, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Source repository has no parent (not a fork) - aborts", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil)
mockGit.EXPECT().Close().Return(nil)
targetRepo := &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
SSHURL: "target-ssh-url",
}
srcRepo := &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "src-org"},
SSHURL: "src-ssh-url",
Parent: nil,
DefaultBranch: "master",
}
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
err := ProcessIssue(issue, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Target branch missing - creates orphan branch", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil)
mockGit.EXPECT().Close().Return(nil)
targetRepo := &models.Repository{
Name: "pkg1",
SSHURL: "target-ssh-url",
Owner: &models.User{UserName: "target-org"},
}
srcRepo := &models.Repository{
Name: "pkg1",
SSHURL: "src-ssh-url",
DefaultBranch: "master",
Owner: &models.User{UserName: "src-org"},
Parent: &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
},
}
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
mockGit.EXPECT().GitClone("pkg1", "master", "src-ssh-url").Return("src-remote", nil)
mockGit.EXPECT().GitClone("pkg1", "master", "target-ssh-url").Return("origin", nil)
// Branch check - rev-list works but says only 1 commit
mockGit.EXPECT().GitExecWithOutputOrPanic("pkg1", "rev-list", "--first-parent", "src-remote/master").Return("sha1")
// Orphan branch creation via createEmptyBranch
mockGit.EXPECT().GitDirectoryContentList("pkg1", "master").Return(map[string]string{"file": "sha"}, nil)
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "--detach")
mockGit.EXPECT().GitExec("pkg1", "branch", "-D", "master")
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-f", "--orphan", "master")
mockGit.EXPECT().GitExecOrPanic("pkg1", "rm", "-rf", ".")
mockGit.EXPECT().GitExecOrPanic("pkg1", "commit", "--allow-empty", "-m", "Initial empty branch")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", "master")
mockGit.EXPECT().GitExecWithOutput("pkg1", "ls-remote", "--heads", "src-ssh-url", "master").Return("sha1 refs/heads/master", nil)
// PR creation
pr := &models.PullRequest{
Index: 456,
Body: expectedBody,
Base: &models.PRBranchInfo{
Repo: targetRepo,
},
}
gitea.EXPECT().CreatePullRequestIfNotExist(targetRepo, "src-org:master", "main", gomock.Any(), gomock.Any()).Return(pr, nil, true)
gitea.EXPECT().SetLabels("target-org", "pkg1", int64(456), []string{"new/New Repository"}).Return(nil, nil)
err := ProcessIssue(issue, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Config not found", func(t *testing.T) {
issueNoConfig := &models.Issue{
Title: "[ADD] pkg1",
Body: "src-org/pkg1#master",
Index: 123,
Repository: &models.RepositoryMeta{
Owner: "other-org",
Name: "other-prj",
},
Ref: "refs/heads/main",
}
err := ProcessIssue(issueNoConfig, configs)
if err == nil || err.Error() != "Cannot find config for other-org/other-prj#main" {
t.Errorf("Expected config not found error, got %v", err)
}
})
t.Run("No repos in body", func(t *testing.T) {
err := ProcessIssue(&models.Issue{
Title: "[ADD] pkg1",
Body: "nothing here",
Ref: "refs/heads/main",
Repository: &models.RepositoryMeta{
Owner: "test-org",
Name: "test-prj",
},
}, configs)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
t.Run("Source SHA update - updates existing temp branch", func(t *testing.T) {
mockGitGen := mock_common.NewMockGitHandlerGenerator(ctl)
GitHandler = mockGitGen
mockGit := mock_common.NewMockGit(ctl)
mockGitGen.EXPECT().CreateGitHandler("target-org").Return(mockGit, nil).Times(2)
mockGit.EXPECT().Close().Return(nil).Times(2)
targetRepo := &models.Repository{
Name: "pkg1",
SSHURL: "target-ssh-url",
Owner: &models.User{UserName: "target-org"},
}
srcRepo := &models.Repository{
Name: "pkg1",
SSHURL: "src-ssh-url",
DefaultBranch: "master",
Owner: &models.User{UserName: "src-org"},
Parent: &models.Repository{
Name: "pkg1",
Owner: &models.User{UserName: "target-org"},
},
}
sha1 := "abcdef0123456789abcdef0123456789abcdef01"
issue1 := &models.Issue{
Title: "[ADD] pkg1",
Body: "src-org/pkg1#" + sha1,
Index: 123,
Repository: &models.RepositoryMeta{Owner: "test-org", Name: "test-prj"},
Ref: "refs/heads/main",
}
// First call expectations
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
mockGit.EXPECT().GitClone("pkg1", sha1, "src-ssh-url").Return("src-remote", nil)
mockGit.EXPECT().GitClone("pkg1", sha1, "target-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecWithOutputOrPanic("pkg1", "rev-list", "--first-parent", "src-remote/"+sha1).Return(sha1 + "\n" + "parent")
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", sha1, "parent")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", sha1)
mockGit.EXPECT().GitExecWithOutput("pkg1", "ls-remote", "--heads", "src-ssh-url", sha1).Return("", nil)
tempBranch := "new_package_123_pkg1"
mockGit.EXPECT().GitClone("pkg1", sha1, "target-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecOrPanic("pkg1", "remote", "add", "source", "src-ssh-url")
mockGit.EXPECT().GitExecOrPanic("pkg1", "fetch", "source", sha1)
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", tempBranch, "FETCH_HEAD")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", tempBranch)
pr := &models.PullRequest{
Index: 456,
Body: expectedBody,
Base: &models.PRBranchInfo{
Repo: targetRepo,
},
}
gitea.EXPECT().CreatePullRequestIfNotExist(targetRepo, tempBranch, "main", gomock.Any(), gomock.Any()).Return(pr, nil, true)
gitea.EXPECT().SetLabels("target-org", "pkg1", int64(456), []string{"new/New Repository"}).Return(nil, nil)
err := ProcessIssue(issue1, configs)
if err != nil {
t.Errorf("First call failed: %v", err)
}
// Second call with different SHA
sha2 := "0123456789abcdef0123456789abcdef01234567"
issue2 := &models.Issue{
Title: "[ADD] pkg1",
Body: "src-org/pkg1#" + sha2,
Index: 123,
Repository: &models.RepositoryMeta{Owner: "test-org", Name: "test-prj"},
Ref: "refs/heads/main",
}
gitea.EXPECT().GetRepository("target-org", "pkg1").Return(targetRepo, nil)
gitea.EXPECT().GetRepository("src-org", "pkg1").Return(srcRepo, nil)
mockGit.EXPECT().GitClone("pkg1", sha2, "src-ssh-url").Return("src-remote", nil)
mockGit.EXPECT().GitClone("pkg1", sha2, "target-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecWithOutputOrPanic("pkg1", "rev-list", "--first-parent", "src-remote/"+sha2).Return(sha2 + "\n" + "parent")
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", sha2, "parent")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", sha2)
mockGit.EXPECT().GitExecWithOutput("pkg1", "ls-remote", "--heads", "src-ssh-url", sha2).Return("", nil)
mockGit.EXPECT().GitClone("pkg1", sha2, "target-ssh-url").Return("origin", nil)
mockGit.EXPECT().GitExecOrPanic("pkg1", "remote", "add", "source", "src-ssh-url")
mockGit.EXPECT().GitExecOrPanic("pkg1", "fetch", "source", sha2)
mockGit.EXPECT().GitExecOrPanic("pkg1", "checkout", "-B", tempBranch, "FETCH_HEAD")
mockGit.EXPECT().GitExecOrPanic("pkg1", "push", "-f", "origin", tempBranch)
// CreatePullRequestIfNotExist should be called with same tempBranch, return existing PR
gitea.EXPECT().CreatePullRequestIfNotExist(targetRepo, tempBranch, "main", gomock.Any(), gomock.Any()).Return(pr, nil, false)
err = ProcessIssue(issue2, configs)
if err != nil {
t.Errorf("Second call failed: %v", err)
}
})
}

View File

@@ -146,15 +146,19 @@ func main() {
num, err := strconv.ParseInt(data[3], 10, 64)
common.LogInfo("Processing:", org, "/", repo, "#", num)
common.PanicOnError(err)
pr, err := Gitea.GetPullRequest(org, repo, num)
if err != nil {
common.LogError("Cannot fetch PR", err)
if pr, err := Gitea.GetPullRequest(org, repo, num); err == nil && pr != nil {
if err = ProcesPullRequest(pr, configs); err != nil {
common.LogError("PR processor returned error", err)
}
} else if issue, err := Gitea.GetIssue(org, repo, num); err == nil && issue != nil {
if err = ProcessIssue(issue, configs); err != nil {
common.LogError("issue processor returned error:", err)
}
} else {
common.LogError("Cannot fetch PR or Issue", err)
return
}
if err = ProcesPullRequest(pr, configs); err != nil {
common.LogError("processor returned error", err)
}
}
return

View File

@@ -190,7 +190,14 @@ func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
}
if !submodule_found {
common.LogError("Failed to find expected repo:", repo)
common.LogInfo("Adding new submodule", repo, "to PrjGit")
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
commitMsg := fmt.Sprintln("Add package", repo, "\n\nThis commit was autocreated by", GitAuthor, "\n\nreferencing PRs:\n", ref)
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "add", "-b", pr.PR.Base.Name, pr.PR.Base.Repo.SSHURL, repo)
updateSubmoduleInPR(repo, prHead, git)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
}
}
return nil
@@ -608,6 +615,14 @@ type RequestProcessor struct {
recursive int
}
func (w *RequestProcessor) Process(pr *models.PullRequest) error {
configs, ok := w.configuredRepos[pr.Base.Repo.Owner.UserName]
if !ok {
return fmt.Errorf("no config found for org %s", pr.Base.Repo.Owner.UserName)
}
return ProcesPullRequest(pr, configs)
}
func ProcesPullRequest(pr *models.PullRequest, configs []*common.AutogitConfig) error {
if len(configs) < 1 {
// ignoring pull request against unconfigured project (could be just regular sources?)
@@ -624,15 +639,6 @@ func ProcesPullRequest(pr *models.PullRequest, configs []*common.AutogitConfig)
return PRProcessor.Process(pr)
}
func (w *RequestProcessor) Process(pr *models.PullRequest) error {
configs, ok := w.configuredRepos[pr.Base.Repo.Owner.UserName]
if !ok {
common.LogError("*** Cannot find config for org:", pr.Base.Repo.Owner.UserName)
return fmt.Errorf("*** Cannot find config for org: %s", pr.Base.Repo.Owner.UserName)
}
return ProcesPullRequest(pr, configs)
}
func (w *RequestProcessor) ProcessFunc(request *common.Request) (err error) {
defer func() {
if r := recover(); r != nil {
@@ -661,6 +667,18 @@ func (w *RequestProcessor) ProcessFunc(request *common.Request) (err error) {
common.LogError("Cannot find PR for issue:", req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
return err
}
} else if req, ok := request.Data.(*common.IssueWebhookEvent); ok {
issue, err := Gitea.GetIssue(req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
if err != nil {
common.LogError("Cannot find issue for issue event:", req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
return err
}
configs, ok := w.configuredRepos[req.Repository.Owner.Username]
if !ok {
common.LogError("*** Cannot find config for org:", req.Repository.Owner.Username)
return nil
}
return ProcessIssue(issue, configs)
} else {
common.LogError("*** Invalid data format for PR processing.")
return fmt.Errorf("*** Invalid data format for PR processing.")

View File

@@ -11,6 +11,7 @@ import (
)
func TestSyncPR(t *testing.T) {
CurrentUser = &models.User{UserName: "testuser"}
config := &common.AutogitConfig{
Reviewers: []string{"reviewer1", "reviewer2"},
Branch: "testing",
@@ -151,4 +152,3 @@ func TestSyncPR(t *testing.T) {
}
})
}

View File

@@ -56,6 +56,7 @@ func commandsForPackages(dir, prefix string, startN, endN int) [][]string {
func setupGitForTests(t *testing.T, git *common.GitHandlerImpl) {
common.ExtraGitParams = []string{
"TZ=UTC",
"GIT_CONFIG_COUNT=1",
"GIT_CONFIG_KEY_0=protocol.file.allow",
"GIT_CONFIG_VALUE_0=always",