From bb9e9a08e5e2a0538d11d716cec15a25762314f5033d390f12b800db35bc6f30 Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Sat, 24 Jan 2026 17:36:15 +0100 Subject: [PATCH 1/6] common: only change maintainership lines that changed --- common/maintainership.go | 133 +++++++++++++++++++++++++++++++--- common/maintainership_test.go | 33 ++++++++- 2 files changed, 155 insertions(+), 11 deletions(-) diff --git a/common/maintainership.go b/common/maintainership.go index 39d2528..88bbc40 100644 --- a/common/maintainership.go +++ b/common/maintainership.go @@ -1,10 +1,12 @@ 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" @@ -26,11 +28,13 @@ 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 @@ -59,7 +63,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) { @@ -164,13 +168,127 @@ 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 { + output += string(data.Raw[lastPos:e.valStart]) + if v, ok := data.Data[e.key]; ok { + 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 + output += string(data.Raw[e.valStart:e.valEnd]) + } + 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") } - writer.WriteString("{\n") + 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 { @@ -181,17 +299,12 @@ func (data *MaintainershipMap) WriteMaintainershipFile(writer io.StringWriter) e writer.WriteString(fmt.Sprintf(" \"\": %s%s\n", string(str), eol)) } - keys := make([]string, len(data.Data)) - i := 0 + keys := make([]string, 0, len(data.Data)) for pkg := range data.Data { if pkg == "" { continue } - keys[i] = pkg - i++ - } - if len(keys) >= i { - keys = slices.Delete(keys, i, len(keys)) + keys = append(keys, pkg) } slices.Sort(keys) for i, pkg := range keys { diff --git a/common/maintainership_test.go b/common/maintainership_test.go index e8c8573..3064248 100644 --- a/common/maintainership_test.go +++ b/common/maintainership_test.go @@ -208,6 +208,7 @@ func TestMaintainershipFileWrite(t *testing.T) { name string is_dir bool maintainers map[string][]string + raw []byte expected_output string expected_error error }{ @@ -231,6 +232,35 @@ 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", + }, } for _, test := range tests { @@ -239,6 +269,7 @@ 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 { @@ -248,7 +279,7 @@ func TestMaintainershipFileWrite(t *testing.T) { output := b.String() if test.expected_output != output { - t.Fatal("unexpected output:", output, "Expecting:", test.expected_output) + t.Fatalf("unexpected output:\n%q\nExpecting:\n%q", output, test.expected_output) } }) } -- 2.51.1 From 86a176a785076034e2f124016f0249919aa32013ca43800fe076db5b503dea14 Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Sat, 24 Jan 2026 17:55:41 +0100 Subject: [PATCH 2/6] common: precise key removal --- common/maintainership.go | 12 ++++++++++-- common/maintainership_test.go | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/common/maintainership.go b/common/maintainership.go index 88bbc40..ec65fe6 100644 --- a/common/maintainership.go +++ b/common/maintainership.go @@ -219,8 +219,16 @@ func (data *MaintainershipMap) modifyInplace(writer io.StringWriter) error { } for _, e := range entries { - output += string(data.Raw[lastPos:e.valStart]) 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) @@ -231,7 +239,7 @@ func (data *MaintainershipMap) modifyInplace(writer io.StringWriter) error { } } else { // Deleted - output += string(data.Raw[e.valStart:e.valEnd]) + modified = true } lastPos = e.valEnd } diff --git a/common/maintainership_test.go b/common/maintainership_test.go index 3064248..73d0f6e 100644 --- a/common/maintainership_test.go +++ b/common/maintainership_test.go @@ -258,8 +258,16 @@ func TestMaintainershipFileWrite(t *testing.T) { "": {"one"}, "new": {"user"}, }, - raw: []byte("{\n \"\": [\"one\"]\n}\n"), - expected_output: "{\n \"\": [\"one\"],\n \"new\": [\"user\"]\n}\n", + 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", }, } -- 2.51.1 From 1220799e5713362f9ee0a666be10c88d75d575bfffd1ceecf680dea8f13ee822 Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Sat, 24 Jan 2026 19:22:15 +0100 Subject: [PATCH 3/6] util: add maintainership linter --- utils/maintainer-update/main.go | 79 +++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 utils/maintainer-update/main.go diff --git a/utils/maintainer-update/main.go b/utils/maintainer-update/main.go new file mode 100644 index 0000000..a9df95f --- /dev/null +++ b/utils/maintainer-update/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "flag" + "os" + "slices" + + "src.opensuse.org/autogits/common" +) + +const maintainershipFile = "_maintainership.json" +const maintainershipFileNew = maintainershipFile + ".new" + +func WriteNewMaintainershipFile(m *common.MaintainershipMap) { + f, err := os.Create(maintainershipFileNew) + common.PanicOnError(err) + common.PanicOnError(m.WriteMaintainershipFile(f)) + common.PanicOnError(f.Close()) + common.PanicOnError(os.Rename(maintainershipFileNew, maintainershipFile)) +} + +func main() { + 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") + flag.Parse() + + data, err := os.ReadFile(maintainershipFile) + if os.IsNotExist(err) { + common.LogError(err) + os.Exit(1) + } + common.PanicOnError(err) + + m, err := common.ParseMaintainershipData(data) + common.PanicOnErrorWithMsg(err, "Failed to parse JSON: %v") + + if *lint { + m.Raw = nil // forces a rewrite + + WriteNewMaintainershipFile(m) + } + + users := flag.Args() + if len(users) > 0 { + maintainers, ok := m.Data[*pkg] + if !ok && *add { + maintainers = []string{} + } + + 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 || ok { + slices.Sort(maintainers) + m.Data[*pkg] = maintainers + } else { + delete(m.Data, *pkg) + } + } + + WriteNewMaintainershipFile(m) +} -- 2.51.1 From 6ee8fcc5975d27f5009b79b8bc20286ef3e4a294b7dbb01f20a65b11c1dc1571 Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Sat, 24 Jan 2026 21:47:45 +0100 Subject: [PATCH 4/6] utils: add unit tests --- utils/maintainer-update/main.go | 108 ++++++++------ utils/maintainer-update/main_test.go | 205 +++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 45 deletions(-) create mode 100644 utils/maintainer-update/main_test.go diff --git a/utils/maintainer-update/main.go b/utils/maintainer-update/main.go index a9df95f..84cced0 100644 --- a/utils/maintainer-update/main.go +++ b/utils/maintainer-update/main.go @@ -2,78 +2,96 @@ package main import ( "flag" + "fmt" "os" "slices" "src.opensuse.org/autogits/common" ) -const maintainershipFile = "_maintainership.json" -const maintainershipFileNew = maintainershipFile + ".new" +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") +) -func WriteNewMaintainershipFile(m *common.MaintainershipMap) { - f, err := os.Create(maintainershipFileNew) +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(maintainershipFileNew, maintainershipFile)) + common.PanicOnError(os.Rename(filename+".new", filename)) } -func main() { - 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") +func run() error { flag.Parse() - data, err := os.ReadFile(maintainershipFile) - if os.IsNotExist(err) { - common.LogError(err) - os.Exit(1) + 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 } - common.PanicOnError(err) m, err := common.ParseMaintainershipData(data) - common.PanicOnErrorWithMsg(err, "Failed to parse JSON: %v") + 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 { + maintainers = []string{} + } - WriteNewMaintainershipFile(m) - } - - users := flag.Args() - if len(users) > 0 { - maintainers, ok := m.Data[*pkg] - if !ok && *add { - maintainers = []string{} - } - - if *add { - for _, u := range users { - if !slices.Contains(maintainers, u) { - maintainers = append(maintainers, u) + 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) - } + if *rm { + newMaintainers := make([]string, 0, len(maintainers)) + for _, m := range maintainers { + if !slices.Contains(users, m) { + newMaintainers = append(newMaintainers, m) + maintainers = newMaintainers } - maintainers = newMaintainers - } - if len(maintainers) > 0 || ok { - slices.Sort(maintainers) - m.Data[*pkg] = maintainers - } else { - delete(m.Data, *pkg) + if len(maintainers) > 0 { + slices.Sort(maintainers) + m.Data[*pkg] = maintainers + } else { + delete(m.Data, *pkg) + } } } - WriteNewMaintainershipFile(m) + WriteNewMaintainershipFile(m, filename) + return nil } + +func main() { + if err := run(); err != nil { + common.LogError(err) + os.Exit(1) + } +} + diff --git a/utils/maintainer-update/main_test.go b/utils/maintainer-update/main_test.go new file mode 100644 index 0000000..de3509b --- /dev/null +++ b/utils/maintainer-update/main_test.go @@ -0,0 +1,205 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "os/exec" + "reflect" + "strings" + "testing" +) + +func TestMain(t *testing.T) { + if os.Getenv("BE_MAIN") == "1" { + main() + return + } + + tests := []struct { + name string + inData string + expectedOut string + params []string + expectedError string + useMain bool + expectExit bool + 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: "test main() via recursive call", + inData: `{"pkg1": ["user1"]}`, + params: []string{"-package", "pkg1", "-add", "user2"}, + useMain: true, + expectedOut: `{"pkg1": ["user1", "user2"]}`, + }, + { + name: "lint specific file", + inData: `{"pkg1": ["user2", "user1"]}`, + params: []string{"-lint-only", "other.json"}, + expectedOut: `{"pkg1": ["user1", "user2"]}`, + }, + { + name: "test main() failure", + params: []string{"-package", "pkg1"}, + useMain: true, + expectExit: true, + }, + { + 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", + }, + } + + 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) + + 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) + } + + var err error + if tt.useMain { + cmd := exec.Command(exe, append([]string{"-test.run=TestMain"}, tt.params...)...) + cmd.Env = append(os.Environ(), "BE_MAIN=1") + out, runErr := cmd.CombinedOutput() + if runErr != nil { + if tt.expectExit { + return + } + err = fmt.Errorf("%v: %s", runErr, string(out)) + } else if tt.expectExit { + t.Fatalf("expected exit with error, but it succeeded") + } + } else { + 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) + } + } + }) + } +} + -- 2.51.1 From 844ec8a87bc54bec7bb79c77c74f2da66ca467d6cb3cdf4cd2c4b5bcfb9de014 Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Sat, 24 Jan 2026 21:56:52 +0100 Subject: [PATCH 5/6] util: fix --- utils/maintainer-update/main.go | 7 +- utils/maintainer-update/main_test.go | 120 ++++++++++++++++++--------- 2 files changed, 83 insertions(+), 44 deletions(-) diff --git a/utils/maintainer-update/main.go b/utils/maintainer-update/main.go index 84cced0..1bc7789 100644 --- a/utils/maintainer-update/main.go +++ b/utils/maintainer-update/main.go @@ -55,8 +55,8 @@ func run() error { users := flag.Args() if len(users) > 0 { maintainers, ok := m.Data[*pkg] - if !ok && *add { - maintainers = []string{} + if !ok && !*add { + return fmt.Errorf("No package %s and not adding one.", *pkg) } if *add { @@ -72,6 +72,8 @@ func run() error { for _, m := range maintainers { if !slices.Contains(users, m) { newMaintainers = append(newMaintainers, m) + } + } maintainers = newMaintainers } @@ -94,4 +96,3 @@ func main() { os.Exit(1) } } - diff --git a/utils/maintainer-update/main_test.go b/utils/maintainer-update/main_test.go index de3509b..02de25c 100644 --- a/utils/maintainer-update/main_test.go +++ b/utils/maintainer-update/main_test.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "flag" - "fmt" "os" "os/exec" "reflect" @@ -11,20 +10,21 @@ import ( "testing" ) -func TestMain(t *testing.T) { +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 - useMain bool - expectExit bool isDir bool }{ { @@ -98,25 +98,12 @@ func TestMain(t *testing.T) { params: []string{"-package", "pkg1", "-add", "user3"}, expectedOut: `{"pkg1": ["user1", "user2", "user3"]}`, }, - { - name: "test main() via recursive call", - inData: `{"pkg1": ["user1"]}`, - params: []string{"-package", "pkg1", "-add", "user2"}, - useMain: true, - expectedOut: `{"pkg1": ["user1", "user2"]}`, - }, { name: "lint specific file", inData: `{"pkg1": ["user2", "user1"]}`, params: []string{"-lint-only", "other.json"}, expectedOut: `{"pkg1": ["user1", "user2"]}`, }, - { - name: "test main() failure", - params: []string{"-package", "pkg1"}, - useMain: true, - expectExit: true, - }, { name: "add user to package when it was not there before", inData: `{}`, @@ -128,10 +115,14 @@ func TestMain(t *testing.T) { 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.", + }, } - exe, _ := os.Executable() - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() @@ -150,29 +141,14 @@ func TestMain(t *testing.T) { _ = os.WriteFile(targetFile, []byte(tt.inData), 0644) } - var err error - if tt.useMain { - cmd := exec.Command(exe, append([]string{"-test.run=TestMain"}, tt.params...)...) - cmd.Env = append(os.Environ(), "BE_MAIN=1") - out, runErr := cmd.CombinedOutput() - if runErr != nil { - if tt.expectExit { - return - } - err = fmt.Errorf("%v: %s", runErr, string(out)) - } else if tt.expectExit { - t.Fatalf("expected exit with error, but it succeeded") - } - } else { - 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") + 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() - } + os.Args = append([]string{"cmd"}, tt.params...) + err := run() if tt.expectedError != "" { if err == nil { @@ -203,3 +179,65 @@ func TestMain(t *testing.T) { } } +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) + } + } + }) + } +} -- 2.51.1 From f0b053ca0779c5a41028a1d56a06dc4111f92323697e1d2b1fe3079a710c9de9 Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Mon, 26 Jan 2026 09:22:51 +0100 Subject: [PATCH 6/6] utils: add maintainer-update to utils --- autogits.spec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/autogits.spec b/autogits.spec index d763fe6..a90f423 100644 --- a/autogits.spec +++ b/autogits.spec @@ -129,6 +129,9 @@ 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 @@ -179,6 +182,7 @@ 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 @@ -285,6 +289,7 @@ install -D -m0755 utils/hujson/hujson %files utils %license COPYING %{_bindir}/hujson +%{_bindir}/maintainer-update %files workflow-direct %license COPYING -- 2.51.1