add support for maintainership directories

This commit is contained in:
Adam Majer 2025-01-15 00:46:03 +01:00
parent e63a450c5d
commit fbaeddfcd8
3 changed files with 138 additions and 34 deletions

View File

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"slices" "slices"
"time" "time"
@ -38,7 +39,10 @@ import (
//go:generate mockgen -source=gitea_utils.go -destination=mock/gitea_utils.go -typed //go:generate mockgen -source=gitea_utils.go -destination=mock/gitea_utils.go -typed
// maintainer list file in ProjectGit // maintainer list file in ProjectGit
const MaintainershipFile = "_maitnainership.json" const (
MaintainershipFile = "_maitnainership.json"
MaintainershipDir = "maintaineirship"
)
const ( const (
// from Gitea // from Gitea
@ -58,6 +62,7 @@ const (
type GiteaMaintainershipInterface interface { type GiteaMaintainershipInterface interface {
FetchMaintainershipFile(org, prjGit, branch string) ([]byte, error) FetchMaintainershipFile(org, prjGit, branch string) ([]byte, error)
FetchMaintainershipDirFile(org, prjGit, branch, pkg string) ([]byte, error)
} }
type GiteaPRFetcher interface { type GiteaPRFetcher interface {
@ -115,6 +120,10 @@ func (gitea *GiteaTransport) FetchMaintainershipFile(org, repo, branch string) (
return gitea.GetRepositoryFileContent(org, repo, branch, MaintainershipFile) return gitea.GetRepositoryFileContent(org, repo, branch, MaintainershipFile)
} }
func (gitea *GiteaTransport) FetchMaintainershipDirFile(org, repo, branch, pkg string) ([]byte, error) {
return gitea.GetRepositoryFileContent(org, repo, branch, path.Join(MaintainershipDir, pkg))
}
func (gitea *GiteaTransport) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) { func (gitea *GiteaTransport) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) {
pr, err := gitea.client.Repository.RepoGetPullRequest( pr, err := gitea.client.Repository.RepoGetPullRequest(
repository.NewRepoGetPullRequestParams(). repository.NewRepoGetPullRequestParams().

View File

@ -14,25 +14,44 @@ type MaintainershipData interface {
} }
const ProjectKey = "" const ProjectKey = ""
const ProjectFileKey = "_project"
type MaintainershipMap map[string][]string type MaintainershipMap struct {
data map[string][]string
is_dir bool
fetchPackage func(string) ([]byte, error)
}
func parseMaintainershipData(data []byte) (*MaintainershipMap, error) { func parseMaintainershipData(data []byte) (*MaintainershipMap, error) {
maintainers := make(MaintainershipMap) maintainers := &MaintainershipMap{
if err := json.Unmarshal(data, &maintainers); err != nil { data: make(map[string][]string),
}
if err := json.Unmarshal(data, &maintainers.data); err != nil {
return nil, err return nil, err
} }
return &maintainers, nil return maintainers, nil
} }
func FetchProjectMaintainershipData(gitea common.GiteaMaintainershipInterface, org, prjGit, branch string) (*MaintainershipMap, error) { func FetchProjectMaintainershipData(gitea common.GiteaMaintainershipInterface, org, prjGit, branch string) (*MaintainershipMap, error) {
data, err := gitea.FetchMaintainershipFile(org, prjGit, branch) data, err := gitea.FetchMaintainershipDirFile(org, prjGit, branch, ProjectFileKey)
dir := true
if err != nil || data == nil {
dir = false
data, err = gitea.FetchMaintainershipFile(org, prjGit, branch)
if err != nil || data == nil { if err != nil || data == nil {
return nil, err return nil, err
} }
}
return parseMaintainershipData(data) m, err := parseMaintainershipData(data)
if m != nil {
m.is_dir = dir
m.fetchPackage = func(pkg string) ([]byte, error) {
return gitea.FetchMaintainershipDirFile(org, prjGit, branch, pkg)
}
}
return m, err
} }
func (data *MaintainershipMap) ListProjectMaintainers() []string { func (data *MaintainershipMap) ListProjectMaintainers() []string {
@ -40,7 +59,7 @@ func (data *MaintainershipMap) ListProjectMaintainers() []string {
return nil return nil
} }
m, found := (*data)[ProjectKey] m, found := data.data[ProjectKey]
if !found { if !found {
return nil return nil
} }
@ -48,12 +67,34 @@ func (data *MaintainershipMap) ListProjectMaintainers() []string {
return m return m
} }
func parsePkgDirData(pkg string, data []byte) []string {
m := make(map[string][]string)
if err := json.Unmarshal(data, &m); err != nil {
return nil
}
pkgMaintainers, found := m[pkg]
if !found {
return nil
}
return pkgMaintainers
}
func (data *MaintainershipMap) ListPackageMaintainers(pkg string) []string { func (data *MaintainershipMap) ListPackageMaintainers(pkg string) []string {
if data == nil { if data == nil {
return nil return nil
} }
pkgMaintainers := (*data)[pkg] pkgMaintainers, found := data.data[pkg]
if !found && data.is_dir {
pkgData, err := data.fetchPackage(pkg)
if err == nil {
pkgMaintainers = parsePkgDirData(pkg, pkgData)
if len(pkgMaintainers) > 0 {
data.data[pkg] = pkgMaintainers
}
}
}
prjMaintainers := data.ListProjectMaintainers() prjMaintainers := data.ListProjectMaintainers()
prjMaintainer: prjMaintainer:
@ -68,4 +109,3 @@ prjMaintainer:
return pkgMaintainers return pkgMaintainers
} }

View File

@ -19,11 +19,14 @@ func TestMaintainership(t *testing.T) {
packageTests := []struct { packageTests := []struct {
name string name string
maintainersFile []byte
maintainersFileErr error
maintainers []string maintainers []string
otherError bool otherError bool
packageName string packageName string
maintainersFile []byte
maintainersFileErr error
maintainersDir map[string][]byte
}{ }{
/* PACKAGE MAINTAINERS */ /* PACKAGE MAINTAINERS */
// package tests have packageName, projects do not // package tests have packageName, projects do not
@ -39,23 +42,33 @@ func TestMaintainership(t *testing.T) {
{ {
name: "Multiple package maintainers", name: "Multiple package maintainers",
maintainersFile: []byte(`{"pkg": ["user1", "user2"], "": ["user1", "user3"]}`), maintainersFile: []byte(`{"pkg": ["user1", "user2"], "": ["user1", "user3"]}`),
maintainersDir: map[string][]byte{
"_project": []byte(`{"": ["user1", "user3"]}`),
"pkg": []byte(`{"pkg": ["user1", "user2"]}`),
},
maintainers: []string{"user1", "user2", "user3"}, maintainers: []string{"user1", "user2", "user3"},
packageName: "pkg", packageName: "pkg",
}, },
{ {
name: "No package maintainers and only project maintainer", name: "No package maintainers and only project maintainer",
maintainersFile: []byte(`{"pkg2": ["user1", "user2"], "": ["user1", "user3"]}`), maintainersFile: []byte(`{"pkg2": ["user1", "user2"], "": ["user1", "user3"]}`),
maintainersDir: map[string][]byte{
"_project": []byte(`{"": ["user1", "user3"]}`),
},
maintainers: []string{"user1", "user3"}, maintainers: []string{"user1", "user3"},
packageName: "pkg", packageName: "pkg",
}, },
{ {
name: "Invalid list of package maintainers", name: "Invalid list of package maintainers",
maintainersFile: []byte(`{"pkg": 3,"": ["user", 4]}`), maintainersFile: []byte(`{"pkg": 3,"": ["user", 4]}`),
maintainersDir: map[string][]byte{
"_project": []byte(`{"": ["user1", 4]}`),
"pkg": []byte(`"pkg": 3`),
},
otherError: true, otherError: true,
packageName: "pkg", packageName: "pkg",
}, },
/* PROJECT MAINTAINERS */ /* PROJECT MAINTAINERS */
{ {
name: "No maintainer for empty project", name: "No maintainer for empty project",
@ -63,6 +76,9 @@ func TestMaintainership(t *testing.T) {
{ {
name: "No maintainer for empty project maintainer file", name: "No maintainer for empty project maintainer file",
maintainersFile: []byte("{}"), maintainersFile: []byte("{}"),
maintainersDir: map[string][]byte{
"_project": []byte(`{}`),
},
}, },
{ {
name: "Error in MaintainerListForProject when remote has an error", name: "Error in MaintainerListForProject when remote has an error",
@ -71,33 +87,40 @@ func TestMaintainership(t *testing.T) {
{ {
name: "Multiple project maintainers", name: "Multiple project maintainers",
maintainersFile: []byte(`{"": ["user1", "user2"]}`), maintainersFile: []byte(`{"": ["user1", "user2"]}`),
maintainersDir: map[string][]byte{
"_project": []byte(`{"": ["user1", "user2"]}`),
},
maintainers: []string{"user1", "user2"}, maintainers: []string{"user1", "user2"},
}, },
{ {
name: "Single project maintainer", name: "Single project maintainer",
maintainersFile: []byte(`{"": ["user"]}`), maintainersFile: []byte(`{"": ["user"]}`),
maintainersDir: map[string][]byte{
"_project": []byte(`{"": ["user"]}`),
},
maintainers: []string{"user"}, maintainers: []string{"user"},
}, },
{ {
name: "Invalid list of project maintainers", name: "Invalid list of project maintainers",
maintainersFile: []byte(`{"": ["user", 4]}`), maintainersFile: []byte(`{"": ["user", 4]}`),
maintainersDir: map[string][]byte{
"_project": []byte(`{"": ["user", 4]}`),
},
otherError: true, otherError: true,
}, },
{ {
name: "Invalid list of project maintainers", name: "Invalid list of project maintainers",
maintainersFile: []byte(`{"": 4}`), maintainersFile: []byte(`{"": 4}`),
maintainersDir: map[string][]byte{
"_project": []byte(`{"": 4}`),
},
otherError: true, otherError: true,
}, },
} }
notFoundError := errors.New("not found")
for _, test := range packageTests { for _, test := range packageTests {
t.Run(test.name, func(t *testing.T) { runTests := func(t *testing.T, mi common.GiteaMaintainershipInterface) {
ctl := gomock.NewController(t)
mi := mock_common.NewMockGiteaMaintainershipInterface(ctl)
mi.EXPECT().FetchMaintainershipFile("foo", common.DefaultGitPrj, "bar").
Return(test.maintainersFile, test.maintainersFileErr)
maintainers, err := FetchProjectMaintainershipData(mi, config.Organization, config.GitProjectName, config.Branch) maintainers, err := FetchProjectMaintainershipData(mi, config.Organization, config.GitProjectName, config.Branch)
if err != nil && !test.otherError { if err != nil && !test.otherError {
if test.maintainersFileErr == nil { if test.maintainersFileErr == nil {
@ -119,14 +142,46 @@ func TestMaintainership(t *testing.T) {
} }
if len(m) != len(test.maintainers) { if len(m) != len(test.maintainers) {
t.Error("Invalid number of maintainers for package", err) t.Error("Invalid number of maintainers for package", test.packageName, len(m), "vs", len(test.maintainers))
} }
for i := range m { for i := range m {
if !slices.Contains(test.maintainers, m[i]) { if !slices.Contains(test.maintainers, m[i]) {
t.Fatal("Can't find expected users. Found:", m) t.Fatal("Can't find expected users. Found:", m)
} }
} }
}
t.Run(test.name+"_File", func(t *testing.T) {
ctl := gomock.NewController(t)
mi := mock_common.NewMockGiteaMaintainershipInterface(ctl)
// tests with maintainership file
mi.EXPECT().FetchMaintainershipFile("foo", common.DefaultGitPrj, "bar").
Return(test.maintainersFile, test.maintainersFileErr)
mi.EXPECT().FetchMaintainershipDirFile("foo", common.DefaultGitPrj, "bar", ProjectFileKey).
Return(nil, notFoundError)
runTests(t, mi)
})
t.Run(test.name+"_Dir", func(t *testing.T) {
ctl := gomock.NewController(t)
mi := mock_common.NewMockGiteaMaintainershipInterface(ctl)
// run same tests with directory maintainership data
for filename, data := range test.maintainersDir {
mi.EXPECT().FetchMaintainershipDirFile("foo", common.DefaultGitPrj, "bar", filename).Return(data, test.maintainersFileErr).AnyTimes()
}
if _, found := test.maintainersDir[ProjectFileKey]; !found {
mi.EXPECT().FetchMaintainershipDirFile("foo", common.DefaultGitPrj, "bar", ProjectFileKey).Return(nil, test.maintainersFileErr).AnyTimes()
mi.EXPECT().FetchMaintainershipFile("foo", common.DefaultGitPrj, "bar").Return(nil, test.maintainersFileErr).AnyTimes()
}
mi.EXPECT().FetchMaintainershipDirFile("foo", common.DefaultGitPrj, "bar", gomock.Any()).Return(nil, notFoundError).AnyTimes()
runTests(t, mi)
}) })
} }
} }
func TestMaintainershipDir(t *testing.T) {
}