Compare commits
3 Commits
t-sync-tar
...
mergemodes
| Author | SHA256 | Date | |
|---|---|---|---|
| 95632d3d7d | |||
| 6d8c0b9414 | |||
| 1d4cf96460 |
@@ -39,6 +39,10 @@ const (
|
||||
|
||||
Permission_ForceMerge = "force-merge"
|
||||
Permission_Group = "release-engineering"
|
||||
|
||||
MergeModeFF = "ff-only"
|
||||
MergeModeReplace = "replace"
|
||||
MergeModeDevel = "devel"
|
||||
)
|
||||
|
||||
type ConfigFile struct {
|
||||
@@ -52,9 +56,9 @@ type ReviewGroup struct {
|
||||
}
|
||||
|
||||
type QAConfig struct {
|
||||
Name string
|
||||
Origin string
|
||||
Label string // requires this gitea lable to be set or skipped
|
||||
Name string
|
||||
Origin string
|
||||
Label string // requires this gitea lable to be set or skipped
|
||||
BuildDisableRepos []string // which repos to build disable in the new project
|
||||
}
|
||||
|
||||
@@ -89,7 +93,8 @@ type AutogitConfig struct {
|
||||
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
|
||||
Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories
|
||||
|
||||
Labels map[string]string // list of tags, if not default, to apply
|
||||
Labels map[string]string // list of tags, if not default, to apply
|
||||
MergeMode string // project merge mode
|
||||
|
||||
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
|
||||
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers
|
||||
@@ -184,6 +189,17 @@ func ReadWorkflowConfig(gitea GiteaFileContentAndRepoFetcher, git_project string
|
||||
}
|
||||
}
|
||||
config.GitProjectName = config.GitProjectName + "#" + branch
|
||||
|
||||
// verify merge modes
|
||||
switch config.MergeMode {
|
||||
case MergeModeFF, MergeModeDevel, MergeModeReplace:
|
||||
break // good results
|
||||
case "":
|
||||
config.MergeMode = MergeModeFF
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported merge mode in %s: %s", git_project, config.MergeMode)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -341,3 +341,67 @@ func TestConfigPermissions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMergeModeParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
mergeMode string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
json: "{}",
|
||||
mergeMode: common.MergeModeFF,
|
||||
},
|
||||
{
|
||||
name: "ff-only",
|
||||
json: `{"MergeMode": "ff-only"}`,
|
||||
mergeMode: common.MergeModeFF,
|
||||
},
|
||||
{
|
||||
name: "replace",
|
||||
json: `{"MergeMode": "replace"}`,
|
||||
mergeMode: common.MergeModeReplace,
|
||||
},
|
||||
{
|
||||
name: "devel",
|
||||
json: `{"MergeMode": "devel"}`,
|
||||
mergeMode: common.MergeModeDevel,
|
||||
},
|
||||
{
|
||||
name: "unsupported",
|
||||
json: `{"MergeMode": "invalid"}`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
repo := models.Repository{
|
||||
DefaultBranch: "master",
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
gitea := mock_common.NewMockGiteaFileContentAndRepoFetcher(ctl)
|
||||
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.json), "abc", nil)
|
||||
gitea.EXPECT().GetRepository("foo", "bar").Return(&repo, nil)
|
||||
|
||||
config, err := common.ReadWorkflowConfig(gitea, "foo/bar")
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if config.MergeMode != test.mergeMode {
|
||||
t.Errorf("Expected MergeMode %s, got %s", test.mergeMode, config.MergeMode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
157
common/pr.go
157
common/pr.go
@@ -554,6 +554,144 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
||||
return is_manually_reviewed_ok
|
||||
}
|
||||
|
||||
func (rs *PRSet) AddMergeCommit(git Git, remote string, pr int) bool {
|
||||
prinfo := rs.PRs[pr]
|
||||
|
||||
LogDebug("Adding merge commit for %s", PRtoString(prinfo.PR))
|
||||
if !prinfo.PR.AllowMaintainerEdit {
|
||||
LogError(" PR is not editable by maintainer")
|
||||
return false
|
||||
}
|
||||
|
||||
repo := prinfo.PR.Base.Repo
|
||||
head := prinfo.PR.Head
|
||||
br := rs.Config.Branch
|
||||
if len(br) == 0 {
|
||||
br = prinfo.PR.Base.Name
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Merge branch '%s' into %s", br, head.Name)
|
||||
if err := git.GitExec(repo.Name, "merge", "--no-ff", "--no-commit", "-X", "theirs", head.Sha); err != nil {
|
||||
if err := git.GitExec(repo.Name, "merge", "--no-ff", "--no-commit", "--allow-unrelated-histories", "-X", "theirs", head.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
LogError("WARNING: Merging unrelated histories")
|
||||
}
|
||||
|
||||
// ensure only files that are in head.Sha are kept
|
||||
git.GitExecOrPanic(repo.Name, "read-tree", "--reset", "-u", head.Sha)
|
||||
git.GitExecOrPanic(repo.Name, "commit", "-m", msg)
|
||||
|
||||
if !IsDryRun {
|
||||
git.GitExecOrPanic(repo.Name, "push", remote, "HEAD:"+head.Name)
|
||||
prinfo.PR.Head.Sha = strings.TrimSpace(git.GitExecWithOutputOrPanic(repo.Name, "rev-list", "-1", "HEAD")) // need to update as it's pushed but pr not refetched
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (rs *PRSet) HasMerge(git Git, pr int) bool {
|
||||
prinfo := rs.PRs[pr]
|
||||
|
||||
repo := prinfo.PR.Base.Repo
|
||||
head := prinfo.PR.Head
|
||||
br := rs.Config.Branch
|
||||
if len(br) == 0 {
|
||||
br = prinfo.PR.Base.Name
|
||||
}
|
||||
|
||||
parents, err := git.GitExecWithOutput(repo.Name, "show", "-s", "--format=%P", head.Sha)
|
||||
if err == nil {
|
||||
p := strings.Fields(strings.TrimSpace(parents))
|
||||
if len(p) == 2 {
|
||||
targetHead, _ := git.GitExecWithOutput(repo.Name, "rev-parse", "HEAD")
|
||||
targetHead = strings.TrimSpace(targetHead)
|
||||
if p[0] == targetHead || p[1] == targetHead {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rs *PRSet) PrepareForMerge(git Git) bool {
|
||||
// verify that package can merge here. Checkout current target branch of each PRSet, make a temporary branch
|
||||
// PR_#_mergetest and perform the merge based
|
||||
|
||||
if rs.Config.MergeMode == MergeModeDevel {
|
||||
return true // always can merge as we set branch here, not merge anything
|
||||
} else {
|
||||
// make sure that all the package PRs are in mergeable state
|
||||
for idx, prinfo := range rs.PRs {
|
||||
if rs.IsPrjGitPR(prinfo.PR) {
|
||||
continue
|
||||
}
|
||||
|
||||
repo := prinfo.PR.Base.Repo
|
||||
head := prinfo.PR.Head
|
||||
br := rs.Config.Branch
|
||||
if len(br) == 0 {
|
||||
br = prinfo.PR.Base.Name
|
||||
}
|
||||
|
||||
remote, err := git.GitClone(repo.Name, br, repo.SSHURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
git.GitExecOrPanic(repo.Name, "fetch", remote, head.Sha)
|
||||
switch rs.Config.MergeMode {
|
||||
case MergeModeFF:
|
||||
if err := git.GitExec(repo.Name, "merge-base", "--is-ancestor", "HEAD", head.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
case MergeModeReplace:
|
||||
Verify:
|
||||
if err := git.GitExec(repo.Name, "merge-base", "--is-ancestor", "HEAD", head.Sha); err != nil {
|
||||
if !rs.HasMerge(git, idx) {
|
||||
forkRemote, err := git.GitClone(repo.Name, head.Name, head.Repo.SSHURL)
|
||||
if err != nil {
|
||||
LogError("Failed to clone head repo:", head.Name, head.Repo.SSHURL)
|
||||
return false
|
||||
}
|
||||
LogDebug("Merge commit is missing and this is not FF merge possibility")
|
||||
git.GitExecOrPanic(repo.Name, "checkout", remote+"/"+br)
|
||||
if !rs.AddMergeCommit(git, forkRemote, idx) {
|
||||
return false
|
||||
}
|
||||
if !IsDryRun {
|
||||
goto Verify
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now we check project git if mergeable
|
||||
prjgit_info, err := rs.GetPrjGitPR()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
prjgit := prjgit_info.PR
|
||||
|
||||
_, _, prjgitBranch := rs.Config.GetPrjGit()
|
||||
remote, err := git.GitClone(DefaultGitPrj, prjgitBranch, prjgit.Base.Repo.SSHURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
testBranch := fmt.Sprintf("PR_%d_mergetest", prjgit.Index)
|
||||
git.GitExecOrPanic(DefaultGitPrj, "fetch", remote, prjgit.Head.Sha)
|
||||
if err := git.GitExec(DefaultGitPrj, "checkout", "-B", testBranch, prjgit.Base.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := git.GitExec(DefaultGitPrj, "merge", "--no-ff", "--no-commit", prjgit.Head.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
||||
prjgit_info, err := rs.GetPrjGitPR()
|
||||
if err != nil {
|
||||
@@ -718,11 +856,10 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
||||
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
|
||||
PanicOnError(err)
|
||||
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, 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
|
||||
if rs.Config.MergeMode == MergeModeDevel || isNewRepo {
|
||||
git.GitExecOrPanic(repo.Name, "checkout", "-B", br, head.Sha)
|
||||
} else {
|
||||
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
||||
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
|
||||
}
|
||||
}
|
||||
@@ -748,11 +885,15 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
||||
}
|
||||
|
||||
if !IsDryRun {
|
||||
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)
|
||||
params := []string{"push"}
|
||||
if rs.Config.MergeMode == MergeModeDevel || isNewRepo {
|
||||
params = append(params, "-f")
|
||||
}
|
||||
params = append(params, prinfo.RemoteName)
|
||||
if isNewRepo {
|
||||
params = append(params, prinfo.PR.Head.Sha+":"+prinfo.PR.Base.Name)
|
||||
}
|
||||
git.GitExecOrPanic(repo.Name, params...)
|
||||
} else {
|
||||
LogInfo("*** WOULD push", repo.Name, "to", prinfo.RemoteName)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package common_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -1273,7 +1274,7 @@ func TestPRMerge(t *testing.T) {
|
||||
Owner: &models.User{
|
||||
UserName: "org",
|
||||
},
|
||||
SSHURL: "file://" + path.Join(repoDir, "prjgit"),
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/prj.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
@@ -1295,7 +1296,7 @@ func TestPRMerge(t *testing.T) {
|
||||
Owner: &models.User{
|
||||
UserName: "org",
|
||||
},
|
||||
SSHURL: "file://" + path.Join(cmd.Dir, "prjgit"),
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/prj.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
@@ -1405,3 +1406,344 @@ func TestPRChanges(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRPrepareForMerge(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*mock_common.MockGit, *models.PullRequest, *models.PullRequest)
|
||||
config *common.AutogitConfig
|
||||
expected bool
|
||||
editable bool
|
||||
}{
|
||||
{
|
||||
name: "Success Devel",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeDevel,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Success FF",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Success Replace MergeCommit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeReplace,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
// merge-base fails initially
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(fmt.Errorf("not ancestor"))
|
||||
// HasMerge returns true
|
||||
m.EXPECT().GitExecWithOutput("pkg", "show", "-s", "--format=%P", pkgPR.Head.Sha).Return("parent1 target_head", nil)
|
||||
m.EXPECT().GitExecWithOutput("pkg", "rev-parse", "HEAD").Return("target_head", nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Merge Conflict in PrjGit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(fmt.Errorf("conflict"))
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Not FF in PkgGit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(fmt.Errorf("not ancestor"))
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Success Replace with AddMergeCommit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeReplace,
|
||||
},
|
||||
editable: true,
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
// First merge-base fails
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(fmt.Errorf("not ancestor"))
|
||||
// HasMerge returns false
|
||||
m.EXPECT().GitExecWithOutput("pkg", "show", "-s", "--format=%P", pkgPR.Head.Sha).Return("parent1", nil)
|
||||
m.EXPECT().GitClone("pkg", pkgPR.Head.Name, pkgPR.Base.Repo.SSHURL).Return("origin_fork", nil)
|
||||
// AddMergeCommit is called
|
||||
m.EXPECT().GitExecOrPanic("pkg", "checkout", "origin/master")
|
||||
m.EXPECT().GitExec("pkg", "merge", "--no-ff", "--no-commit", "-X", "theirs", pkgPR.Head.Sha).Return(nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "read-tree", "--reset", "-u", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "commit", "-m", gomock.Any())
|
||||
m.EXPECT().GitExecOrPanic("pkg", "push", "origin_fork", "HEAD:"+pkgPR.Head.Name)
|
||||
m.EXPECT().GitExecWithOutputOrPanic("pkg", "rev-list", "-1", "HEAD").Return("new_pkg_head_sha")
|
||||
// Second merge-base succeeds (after goto Verify)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", "new_pkg_head_sha").Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
prjPR := &models.PullRequest{
|
||||
Index: 1,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "_ObsPrj",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/_ObsPrj.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "head_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "_ObsPrj",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/_ObsPrj.git",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pkgPR := &models.PullRequest{
|
||||
Index: 2,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "pkg_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Name: "branch_name",
|
||||
Sha: "pkg_head_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
AllowMaintainerEdit: test.editable,
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
git := mock_common.NewMockGit(ctl)
|
||||
test.setup(git, prjPR, pkgPR)
|
||||
|
||||
prset := &common.PRSet{
|
||||
Config: test.config,
|
||||
PRs: []*common.PRInfo{
|
||||
{PR: prjPR},
|
||||
{PR: pkgPR},
|
||||
},
|
||||
}
|
||||
|
||||
if res := prset.PrepareForMerge(git); res != test.expected {
|
||||
t.Errorf("Expected %v, got %v", test.expected, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRMergeMock(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*mock_common.MockGit, *models.PullRequest, *models.PullRequest)
|
||||
config *common.AutogitConfig
|
||||
}{
|
||||
{
|
||||
name: "Success FF",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "-m", gomock.Any(), prjPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin_pkg", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin_pkg", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "merge", "--ff", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "push", "origin_pkg")
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "push", "origin")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Success Devel",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeDevel,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "-m", gomock.Any(), prjPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin_pkg", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "checkout", "-B", "master", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "push", "-f", "origin_pkg")
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "push", "origin")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
prjPR := &models.PullRequest{
|
||||
Index: 1,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "prj_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "_ObsPrj",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/_ObsPrj.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "prj_head_sha",
|
||||
},
|
||||
}
|
||||
pkgPR := &models.PullRequest{
|
||||
Index: 2,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "pkg_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "pkg_head_sha",
|
||||
},
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
git := mock_common.NewMockGit(ctl)
|
||||
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
|
||||
reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
|
||||
test.setup(git, prjPR, pkgPR)
|
||||
|
||||
prset := &common.PRSet{
|
||||
Config: test.config,
|
||||
PRs: []*common.PRInfo{
|
||||
{PR: prjPR},
|
||||
{PR: pkgPR},
|
||||
},
|
||||
}
|
||||
|
||||
if err := prset.Merge(reviewUnrequestMock, git); err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRAddMergeCommit(t *testing.T) {
|
||||
pkgPR := &models.PullRequest{
|
||||
Index: 2,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "pkg_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Name: "branch_name",
|
||||
Sha: "pkg_head_sha",
|
||||
},
|
||||
AllowMaintainerEdit: true,
|
||||
}
|
||||
|
||||
config := &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeReplace,
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
git := mock_common.NewMockGit(ctl)
|
||||
|
||||
git.EXPECT().GitExec("pkg", "merge", "--no-ff", "--no-commit", "-X", "theirs", pkgPR.Head.Sha).Return(nil)
|
||||
git.EXPECT().GitExecOrPanic("pkg", "read-tree", "--reset", "-u", pkgPR.Head.Sha)
|
||||
git.EXPECT().GitExecOrPanic("pkg", "commit", "-m", gomock.Any())
|
||||
git.EXPECT().GitExecOrPanic("pkg", "push", "origin", "HEAD:branch_name")
|
||||
git.EXPECT().GitExecWithOutputOrPanic("pkg", "rev-list", "-1", "HEAD").Return("new_head_sha")
|
||||
|
||||
prset := &common.PRSet{
|
||||
Config: config,
|
||||
PRs: []*common.PRInfo{
|
||||
{PR: &models.PullRequest{}}, // prjgit at index 0
|
||||
{PR: pkgPR}, // pkg at index 1
|
||||
},
|
||||
}
|
||||
|
||||
if res := prset.AddMergeCommit(git, "origin", 1); !res {
|
||||
t.Errorf("Expected true, got %v", res)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,18 +59,17 @@ The testing will be conducted in a dedicated test environment that mimics the pr
|
||||
| **TC-SYNC-002** | P | **Update ProjectGit PR from PackageGit PR** | 1. Push a new commit to an existing PackageGit PR. | 1. The corresponding ProjectGit PR's head branch is updated with the new commit. | High |
|
||||
| **TC-SYNC-003** | P | **WIP Flag Synchronization** | 1. Mark a PackageGit PR as "Work In Progress".<br>2. Remove the WIP flag from the PackageGit PR. | 1. The corresponding ProjectGit PR is also marked as "Work In Progress".<br>2. The WIP flag on the ProjectGit PR is removed. | Medium |
|
||||
| **TC-SYNC-004** | - | **WIP Flag (multiple referenced package PRs)** | 1. Create a ProjectGit PR that references multiple PackageGit PRs.<br>2. Mark one of the PackageGit PRs as "Work In Progress".<br>3. Remove the "Work In Progress" flag from all PackageGit PRs. | 1. The ProjectGit PR is marked as "Work In Progress".<br>2. The "Work In Progress" flag is removed from the ProjectGit PR only after it has been removed from all associated PackageGit PRs. | Medium |
|
||||
| **TC-SYNC-005** | m | **NoProjectGitPR = true, edits disabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR without "Allow edits from maintainers" enabled. <br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The bot adds a warning comment to the PackageGit PR explaining that it cannot update the PR. | High |
|
||||
| **TC-SYNC-006** | m | **NoProjectGitPR = true, edits enabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR with "Allow edits from maintainers" enabled.<br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The submodule commit on the project PR is updated with the new commit from the PackageGit PR. | High |
|
||||
| **TC-SYNC-007** | m | **Change target branch** | 1. Create a Package PR targeting 'main'.<br>2. Change the target branch of the Package PR to 'merge'. | 1. The target branch of the corresponding Project PR is updated to 'merge' automatically. | High |
|
||||
| **TC-SYNC-005** | x | **NoProjectGitPR = true, edits disabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR without "Allow edits from maintainers" enabled. <br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The bot adds a warning comment to the PackageGit PR explaining that it cannot update the PR. | High |
|
||||
| **TC-SYNC-006** | x | **NoProjectGitPR = true, edits enabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR with "Allow edits from maintainers" enabled.<br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The submodule commit on the project PR is updated with the new commit from the PackageGit PR. | High |
|
||||
| **TC-COMMENT-001** | - | **Detect duplicate comments** | 1. Create a PackageGit PR.<br>2. Wait for the `workflow-pr` bot to act on the PR.<br>3. Edit the body of the PR to trigger the bot a second time. | 1. The bot should not post a duplicate comment. | High |
|
||||
| **TC-REVIEW-001** | P | **Add mandatory reviewers** | 1. Create a new PackageGit PR. | 1. All mandatory reviewers are added to both the PackageGit and ProjectGit PRs. | High |
|
||||
| **TC-REVIEW-002** | - | **Add advisory reviewers** | 1. Create a new PackageGit PR with advisory reviewers defined in the configuration. | 1. Advisory reviewers are added to the PR, but their approval is not required for merging. | Medium |
|
||||
| **TC-REVIEW-003** | - | **Re-add reviewers** | 1. Push a new commit to a PackageGit PR after it has been approved. | 1. The original reviewers are re-added to the PR. | Medium |
|
||||
| **TC-REVIEW-004** | X | **Package PR created by a maintainer** | 1. Create a PackageGit PR from the account of a package maintainer. | 1. No review is requested from other package maintainers. | High |
|
||||
| **TC-REVIEW-004** | x | **Package PR created by a maintainer** | 1. Create a PackageGit PR from the account of a package maintainer. | 1. No review is requested from other package maintainers. | High |
|
||||
| **TC-REVIEW-005** | P | **Package PR created by an external user (approve)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers approves the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer approves the PR, the other maintainers are removed as reviewers. | High |
|
||||
| **TC-REVIEW-006** | P | **Package PR created by an external user (reject)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers rejects the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer rejects the PR, the other maintainers are removed as reviewers. | High |
|
||||
| **TC-REVIEW-007** | P | **Package PR created by a maintainer with ReviewRequired=true** | 1. Set `ReviewRequired = true` in `workflow.config`.<br>2. Create a PackageGit PR from the account of a package maintainer. | 1. A review is requested from other package maintainers if available. | High |
|
||||
| **TC-MERGE-001** | X | **Automatic Merge** | 1. Create a PackageGit PR.<br>2. Ensure all mandatory reviews are completed on both project and package PRs. | 1. The PR is automatically merged. | High |
|
||||
| **TC-MERGE-001** | x | **Automatic Merge** | 1. Create a PackageGit PR.<br>2. Ensure all mandatory reviews are completed on both project and package PRs. | 1. The PR is automatically merged. | High |
|
||||
| **TC-MERGE-002** | - | **ManualMergeOnly with Package Maintainer** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a package maintainer for that package. | 1. The PR is merged. | High |
|
||||
| **TC-MERGE-003** | - | **ManualMergeOnly with unauthorized user** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a user who is not a maintainer for that package. | 1. The PR is not merged. | High |
|
||||
| **TC-MERGE-004** | - | **ManualMergeOnly with multiple packages** | 1. Create a ProjectGit PR that references multiple PackageGit PRs with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on each package PR from the account of a package maintainer. | 1. The PR is merged only after "merge ok" is commented on all associated PackageGit PRs. | High |
|
||||
@@ -87,5 +86,4 @@ The testing will be conducted in a dedicated test environment that mimics the pr
|
||||
* P = implemented and passing;
|
||||
* x = likely implemented, but investigation is needed;
|
||||
* X = implemented and likely to pass, but someteimes may fail, but troubleshooting is needed;
|
||||
* m = bot functionality is missing in the branch, the test is waiting for implementation in the code base
|
||||
* - = test is not implemented
|
||||
|
||||
0
integration/tests/test_pr_workflow.py
Normal file → Executable file
0
integration/tests/test_pr_workflow.py
Normal file → Executable file
51
integration/tests/workflow_pr_sync_test.py
Normal file → Executable file
51
integration/tests/workflow_pr_sync_test.py
Normal file → Executable file
@@ -324,54 +324,3 @@ index 0000000..f587a12
|
||||
print("Verification complete: Manually created Project PR was updated by the bot as expected.")
|
||||
|
||||
|
||||
@pytest.mark.t007
|
||||
@pytest.mark.xfail(reason="test is ready; bot functionality is missing: it creates a new project PR instead of updating existing")
|
||||
def test_007_change_target_branch(gitea_env):
|
||||
"""Target branch in project PR is updated when package PR target branch changes"""
|
||||
# 1. Create a Package PR targeting 'main'
|
||||
diff = "diff --git a/target_test.txt b/target_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
||||
package_pr = gitea_env.create_gitea_pr("pool/pkgA", diff, "Target Branch Test PR", False, base_branch="main")
|
||||
package_pr_number = package_pr["number"]
|
||||
|
||||
# 2. Poll for forwarded Project PR
|
||||
forwarded_pr_number = None
|
||||
print(f"Polling pool/pkgA PR #{package_pr_number} timeline for forwarded PR event...")
|
||||
for _ in range(20):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("pool/pkgA", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"products/SLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
forwarded_pr_number = match.group(1)
|
||||
break
|
||||
if forwarded_pr_number:
|
||||
break
|
||||
|
||||
assert forwarded_pr_number is not None, "Workflow bot did not create a forwarded PR."
|
||||
|
||||
# Verify initial target branch
|
||||
project_pr_details = gitea_env.get_pr_details("products/SLFO", forwarded_pr_number)
|
||||
assert project_pr_details["base"]["ref"] == "main"
|
||||
|
||||
# 3. User changes the target branch in the package PR to 'merge'
|
||||
print(f"Changing target branch of Package PR #{package_pr_number} to 'merge'")
|
||||
gitea_env.update_gitea_pr_properties("pool/pkgA", package_pr_number, base="merge")
|
||||
|
||||
# 4. Check that target branch in the project PR gets updated by the bots automatically
|
||||
updated_successfully = False
|
||||
print(f"Polling Project PR #{forwarded_pr_number} for target branch update...")
|
||||
for _ in range(20):
|
||||
time.sleep(1)
|
||||
project_pr_details = gitea_env.get_pr_details("products/SLFO", forwarded_pr_number)
|
||||
if project_pr_details["base"]["ref"] == "merge":
|
||||
updated_successfully = True
|
||||
print("Project PR target branch updated to 'merge'")
|
||||
break
|
||||
|
||||
assert updated_successfully, "Target branch in Project PR was not updated to 'merge'"
|
||||
|
||||
|
||||
|
||||
@@ -96,6 +96,19 @@ Package Deletion Requests
|
||||
If you wish to re-add a package, create a new PrjGit PR which adds again the submodule on the branch that has the "-removed" suffix. The bot will automatically remove this suffix from the project branch in the pool.
|
||||
|
||||
|
||||
Merge Modes
|
||||
-----------
|
||||
|
||||
| Merge Mode | Description
|
||||
|------------|--------------------------------------------------------------------------------
|
||||
| ff-only | Only allow --ff-only merges in the package branch. This is best suited for
|
||||
| | devel projects and openSUSE Tumbleweed development, where history should be linear
|
||||
| replace | Merge is done via `-X theirs` strategy and old files are removed in the merge.
|
||||
| | This works well for downstream codestreams, like Leap, that would update their branch
|
||||
| | using latest version.
|
||||
| devel | No merge, just set the project branch to PR HEAD. This is suitable for downstream
|
||||
| | projects like Leap during development cycle, where keeping maintenance history is not important
|
||||
|
||||
Labels
|
||||
------
|
||||
|
||||
|
||||
@@ -413,6 +413,12 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
||||
}
|
||||
common.LogInfo("fetched PRSet of size:", len(prset.PRs))
|
||||
|
||||
if !prset.PrepareForMerge(git) {
|
||||
common.LogError("PRs are NOT mergeable.")
|
||||
} else {
|
||||
common.LogInfo("PRs are in mergeable state.")
|
||||
}
|
||||
|
||||
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
|
||||
prjGitPR, err := prset.GetPrjGitPR()
|
||||
if err == common.PRSet_PrjGitMissing {
|
||||
|
||||
Reference in New Issue
Block a user