forked from git-workflow/autogits
Compare commits
2 Commits
staging-co
...
obs-groups
| Author | SHA256 | Date | |
|---|---|---|---|
|
0db0536436
|
|||
| ba20810c99 |
2
Makefile
2
Makefile
@@ -1,4 +1,4 @@
|
||||
MODULES := devel-importer utils/hujson utils/maintainer-update gitea-events-rabbitmq-publisher gitea_status_proxy group-review obs-forward-bot obs-staging-bot obs-status-service workflow-direct workflow-pr
|
||||
MODULES := devel-importer utils/hujson utils/maintainer-update gitea-events-rabbitmq-publisher gitea_status_proxy group-review obs-forward-bot obs-groups-bot obs-staging-bot obs-status-service workflow-direct workflow-pr
|
||||
|
||||
build:
|
||||
for m in $(MODULES); do go build -C $$m -buildmode=pie || exit 1 ; done
|
||||
|
||||
@@ -396,12 +396,17 @@ func (e *GitHandlerImpl) GitExecQuietOrPanic(cwd string, params ...string) {
|
||||
}
|
||||
|
||||
type ChanIO struct {
|
||||
ch chan byte
|
||||
ch chan byte
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (c *ChanIO) Write(p []byte) (int, error) {
|
||||
for _, b := range p {
|
||||
c.ch <- b
|
||||
select {
|
||||
case c.ch <- b:
|
||||
case <-c.done:
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -410,21 +415,32 @@ func (c *ChanIO) Write(p []byte) (int, error) {
|
||||
func (c *ChanIO) Read(data []byte) (idx int, err error) {
|
||||
var ok bool
|
||||
|
||||
data[idx], ok = <-c.ch
|
||||
if !ok {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
idx++
|
||||
|
||||
for len(c.ch) > 0 && idx < len(data) {
|
||||
data[idx], ok = <-c.ch
|
||||
select {
|
||||
case data[idx], ok = <-c.ch:
|
||||
if !ok {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
idx++
|
||||
case <-c.done:
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
for len(c.ch) > 0 && idx < len(data) {
|
||||
select {
|
||||
case data[idx], ok = <-c.ch:
|
||||
if !ok {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
idx++
|
||||
case <-c.done:
|
||||
err = io.EOF
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
@@ -471,7 +487,14 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
||||
var size int
|
||||
|
||||
pos := 0
|
||||
for c := <-data; c != ' '; c = <-data {
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return GitMsg{}, io.EOF
|
||||
}
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
||||
id[pos] = c
|
||||
pos++
|
||||
@@ -483,7 +506,15 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
||||
|
||||
pos = 0
|
||||
var c byte
|
||||
for c = <-data; c != ' ' && c != '\x00'; c = <-data {
|
||||
for {
|
||||
var ok bool
|
||||
c, ok = <-data
|
||||
if !ok {
|
||||
return GitMsg{}, io.EOF
|
||||
}
|
||||
if c == ' ' || c == '\x00' {
|
||||
break
|
||||
}
|
||||
if c >= 'a' && c <= 'z' {
|
||||
msgType[pos] = c
|
||||
pos++
|
||||
@@ -509,7 +540,14 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
||||
return GitMsg{}, fmt.Errorf("Invalid object type: '%s'", string(msgType))
|
||||
}
|
||||
|
||||
for c = <-data; c != '\000'; c = <-data {
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return GitMsg{}, io.EOF
|
||||
}
|
||||
if c == '\x00' {
|
||||
break
|
||||
}
|
||||
if c >= '0' && c <= '9' {
|
||||
size = size*10 + (int(c) - '0')
|
||||
} else {
|
||||
@@ -528,18 +566,37 @@ func parseGitCommitHdr(oldHdr [2]string, data <-chan byte) ([2]string, int, erro
|
||||
hdr := make([]byte, 0, 60)
|
||||
val := make([]byte, 0, 1000)
|
||||
|
||||
c := <-data
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return [2]string{}, 0, io.EOF
|
||||
}
|
||||
size := 1
|
||||
if c != '\n' { // end of header marker
|
||||
for ; c != ' '; c = <-data {
|
||||
for {
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
hdr = append(hdr, c)
|
||||
size++
|
||||
var ok bool
|
||||
c, ok = <-data
|
||||
if !ok {
|
||||
return [2]string{}, size, io.EOF
|
||||
}
|
||||
}
|
||||
if size == 1 { // continuation header here
|
||||
hdr = []byte(oldHdr[0])
|
||||
val = append([]byte(oldHdr[1]), '\n')
|
||||
}
|
||||
for c := <-data; c != '\n'; c = <-data {
|
||||
for {
|
||||
var ok bool
|
||||
c, ok = <-data
|
||||
if !ok {
|
||||
return [2]string{}, size, io.EOF
|
||||
}
|
||||
if c == '\n' {
|
||||
break
|
||||
}
|
||||
val = append(val, c)
|
||||
size++
|
||||
}
|
||||
@@ -552,7 +609,14 @@ func parseGitCommitHdr(oldHdr [2]string, data <-chan byte) ([2]string, int, erro
|
||||
func parseGitCommitMsg(data <-chan byte, l int) (string, error) {
|
||||
msg := make([]byte, 0, l)
|
||||
|
||||
for c := <-data; c != '\x00'; c = <-data {
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return string(msg), io.EOF
|
||||
}
|
||||
if c == '\x00' {
|
||||
break
|
||||
}
|
||||
msg = append(msg, c)
|
||||
l--
|
||||
}
|
||||
@@ -578,7 +642,7 @@ func parseGitCommit(data <-chan byte) (GitCommit, error) {
|
||||
var hdr [2]string
|
||||
hdr, size, err := parseGitCommitHdr(hdr, data)
|
||||
if err != nil {
|
||||
return GitCommit{}, nil
|
||||
return GitCommit{}, err
|
||||
}
|
||||
l -= size
|
||||
|
||||
@@ -599,14 +663,28 @@ func parseGitCommit(data <-chan byte) (GitCommit, error) {
|
||||
func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
||||
var e GitTreeEntry
|
||||
|
||||
for c := <-data; c != ' '; c = <-data {
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return e, io.EOF
|
||||
}
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
e.mode = e.mode*8 + int(c-'0')
|
||||
e.size++
|
||||
}
|
||||
e.size++
|
||||
|
||||
name := make([]byte, 0, 128)
|
||||
for c := <-data; c != '\x00'; c = <-data {
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return e, io.EOF
|
||||
}
|
||||
if c == '\x00' {
|
||||
break
|
||||
}
|
||||
name = append(name, c)
|
||||
e.size++
|
||||
}
|
||||
@@ -617,7 +695,10 @@ func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
||||
|
||||
hash := make([]byte, 0, hashLen*2)
|
||||
for range hashLen {
|
||||
c := <-data
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return e, io.EOF
|
||||
}
|
||||
hash = append(hash, hexBinToAscii[((c&0xF0)>>4)], hexBinToAscii[c&0xF])
|
||||
}
|
||||
e.hash = string(hash)
|
||||
@@ -638,13 +719,16 @@ func parseGitTree(data <-chan byte) (GitTree, error) {
|
||||
for parsedLen < hdr.size {
|
||||
entry, err := parseTreeEntry(data, len(hdr.hash)/2)
|
||||
if err != nil {
|
||||
return GitTree{}, nil
|
||||
return GitTree{}, err
|
||||
}
|
||||
|
||||
t.items = append(t.items, entry)
|
||||
parsedLen += entry.size
|
||||
}
|
||||
c := <-data // \0 read
|
||||
c, ok := <-data // \0 read
|
||||
if !ok {
|
||||
return t, io.EOF
|
||||
}
|
||||
|
||||
if c != '\x00' {
|
||||
return t, fmt.Errorf("Unexpected character during git tree data read")
|
||||
@@ -665,9 +749,16 @@ func parseGitBlob(data <-chan byte) ([]byte, error) {
|
||||
|
||||
d := make([]byte, hdr.size)
|
||||
for l := 0; l < hdr.size; l++ {
|
||||
d[l] = <-data
|
||||
var ok bool
|
||||
d[l], ok = <-data
|
||||
if !ok {
|
||||
return d, io.EOF
|
||||
}
|
||||
}
|
||||
eob, ok := <-data
|
||||
if !ok {
|
||||
return d, io.EOF
|
||||
}
|
||||
eob := <-data
|
||||
if eob != '\x00' {
|
||||
return d, fmt.Errorf("invalid byte read in parseGitBlob")
|
||||
}
|
||||
@@ -679,16 +770,25 @@ func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsed
|
||||
var done sync.Mutex
|
||||
|
||||
done.Lock()
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
done_signal := make(chan struct{})
|
||||
var once sync.Once
|
||||
close_done := func() {
|
||||
once.Do(func() {
|
||||
close(done_signal)
|
||||
})
|
||||
}
|
||||
|
||||
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||
parsedCommits = make([]GitCommit, 0, len(commitIDs))
|
||||
|
||||
go func() {
|
||||
defer done.Unlock()
|
||||
defer close_done()
|
||||
defer close(data_out.ch)
|
||||
|
||||
for _, id := range commitIDs {
|
||||
data_out.Write([]byte(id))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
c, e := parseGitCommit(data_in.ch)
|
||||
if e != nil {
|
||||
err = fmt.Errorf("Error parsing git commit: %w", e)
|
||||
@@ -715,12 +815,14 @@ func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsed
|
||||
LogDebug("command run:", cmd.Args)
|
||||
if e := cmd.Run(); e != nil {
|
||||
LogError(e)
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
close(data_out.ch)
|
||||
return nil, e
|
||||
}
|
||||
|
||||
done.Lock()
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -729,15 +831,21 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
||||
var done sync.Mutex
|
||||
|
||||
done.Lock()
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
done_signal := make(chan struct{})
|
||||
var once sync.Once
|
||||
close_done := func() {
|
||||
once.Do(func() {
|
||||
close(done_signal)
|
||||
})
|
||||
}
|
||||
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||
|
||||
go func() {
|
||||
defer done.Unlock()
|
||||
defer close_done()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
|
||||
data_out.Write([]byte{0})
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -745,11 +853,9 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
||||
return
|
||||
}
|
||||
data_out.Write([]byte(c.Tree))
|
||||
data_out.ch <- '\x00'
|
||||
|
||||
data_out.Write([]byte{0})
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
if err != nil {
|
||||
LogError("Error parsing git tree:", err)
|
||||
return
|
||||
@@ -759,7 +865,7 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
||||
if te.isBlob() && te.name == filename {
|
||||
LogInfo("blob", te.hash)
|
||||
data_out.Write([]byte(te.hash))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
data, err = parseGitBlob(data_in.ch)
|
||||
return
|
||||
}
|
||||
@@ -784,11 +890,13 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
||||
LogDebug("command run:", cmd.Args)
|
||||
if e := cmd.Run(); e != nil {
|
||||
LogError(e)
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
close(data_out.ch)
|
||||
return nil, e
|
||||
}
|
||||
done.Lock()
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -798,16 +906,24 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
||||
directoryList = make(map[string]string)
|
||||
|
||||
done.Lock()
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
done_signal := make(chan struct{})
|
||||
var once sync.Once
|
||||
close_done := func() {
|
||||
once.Do(func() {
|
||||
close(done_signal)
|
||||
})
|
||||
}
|
||||
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||
|
||||
LogDebug("Getting directory for:", commitId)
|
||||
|
||||
go func() {
|
||||
defer done.Unlock()
|
||||
defer close_done()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -823,7 +939,7 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
||||
delete(trees, p)
|
||||
|
||||
data_out.Write([]byte(tree))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
@@ -857,12 +973,14 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
||||
LogDebug("command run:", cmd.Args)
|
||||
if e := cmd.Run(); e != nil {
|
||||
LogError(e)
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
close(data_out.ch)
|
||||
return directoryList, e
|
||||
}
|
||||
|
||||
done.Lock()
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
return directoryList, err
|
||||
}
|
||||
|
||||
@@ -872,7 +990,14 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
||||
directoryList = make(map[string]string)
|
||||
|
||||
done.Lock()
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
done_signal := make(chan struct{})
|
||||
var once sync.Once
|
||||
close_done := func() {
|
||||
once.Do(func() {
|
||||
close(done_signal)
|
||||
})
|
||||
}
|
||||
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||
|
||||
LogDebug("Getting directory content for:", commitId)
|
||||
|
||||
@@ -881,7 +1006,7 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -897,7 +1022,7 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
||||
delete(trees, p)
|
||||
|
||||
data_out.Write([]byte(tree))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
@@ -933,12 +1058,14 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
||||
LogDebug("command run:", cmd.Args)
|
||||
if e := cmd.Run(); e != nil {
|
||||
LogError(e)
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
close(data_out.ch)
|
||||
return directoryList, e
|
||||
}
|
||||
|
||||
done.Lock()
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
return directoryList, err
|
||||
}
|
||||
|
||||
@@ -948,16 +1075,24 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
||||
submoduleList = make(map[string]string)
|
||||
|
||||
done.Lock()
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
done_signal := make(chan struct{})
|
||||
var once sync.Once
|
||||
close_done := func() {
|
||||
once.Do(func() {
|
||||
close(done_signal)
|
||||
})
|
||||
}
|
||||
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||
|
||||
LogDebug("Getting submodules for:", commitId)
|
||||
|
||||
go func() {
|
||||
defer done.Unlock()
|
||||
defer close_done()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -973,7 +1108,7 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
||||
delete(trees, p)
|
||||
|
||||
data_out.Write([]byte(tree))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
@@ -1010,17 +1145,26 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
||||
LogDebug("command run:", cmd.Args)
|
||||
if e := cmd.Run(); e != nil {
|
||||
LogError(e)
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
close(data_out.ch)
|
||||
return submoduleList, e
|
||||
}
|
||||
|
||||
done.Lock()
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
return submoduleList, err
|
||||
}
|
||||
|
||||
func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool) {
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
done_signal := make(chan struct{})
|
||||
var once sync.Once
|
||||
close_done := func() {
|
||||
once.Do(func() {
|
||||
close(done_signal)
|
||||
})
|
||||
}
|
||||
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
@@ -1036,17 +1180,18 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
|
||||
}()
|
||||
|
||||
defer wg.Done()
|
||||
defer close_done()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
c, err := parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
LogError("Error parsing git commit:", err)
|
||||
panic(err)
|
||||
}
|
||||
data_out.Write([]byte(c.Tree))
|
||||
data_out.ch <- '\x00'
|
||||
data_out.Write([]byte{0})
|
||||
tree, err := parseGitTree(data_in.ch)
|
||||
|
||||
if err != nil {
|
||||
@@ -1078,12 +1223,14 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
|
||||
LogDebug("command run:", cmd.Args)
|
||||
if e := cmd.Run(); e != nil {
|
||||
LogError(e)
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
close(data_out.ch)
|
||||
return subCommitId, false
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close_done()
|
||||
close(data_in.ch)
|
||||
return subCommitId, len(subCommitId) > 0
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGitClone(t *testing.T) {
|
||||
@@ -717,3 +718,44 @@ func TestGitDirectoryListRepro(t *testing.T) {
|
||||
t.Errorf("Expected 'subdir' in directory list, got %v", dirs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitDeadlockFix(t *testing.T) {
|
||||
gitDir := t.TempDir()
|
||||
testDir, _ := os.Getwd()
|
||||
|
||||
cmd := exec.Command("/usr/bin/bash", path.Join(testDir, "tsetup.sh"))
|
||||
cmd.Dir = gitDir
|
||||
_, err := cmd.CombinedOutput()
|
||||
|
||||
gh, err := AllocateGitWorkTree(gitDir, "Test", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := gh.ReadExistingPath(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer h.Close()
|
||||
|
||||
// Use a blob ID to trigger error in GitParseCommits
|
||||
// This ensures that the function returns error immediately and doesn't deadlock
|
||||
blobId := "81aba862107f1e2f5312e165453955485f424612f313d6c2fb1b31fef9f82a14"
|
||||
|
||||
done := make(chan error)
|
||||
go func() {
|
||||
_, err := h.GitParseCommits("", []string{blobId})
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Error("Expected error from GitParseCommits with blob ID, got nil")
|
||||
} else {
|
||||
// This is expected
|
||||
t.Logf("Got expected error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("GitParseCommits deadlocked! Fix is NOT working.")
|
||||
}
|
||||
}
|
||||
|
||||
1
obs-groups-bot/.gitignore
vendored
Normal file
1
obs-groups-bot/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
obs-groups-bot
|
||||
399
obs-groups-bot/main.go
Normal file
399
obs-groups-bot/main.go
Normal file
@@ -0,0 +1,399 @@
|
||||
// Connect to the Open Build Service (OBS) API, retrieves a list of all groups,
|
||||
// and exports their metadata (specifically member lists) into individual JSON files.
|
||||
//
|
||||
// The tool supports both command-line flags and environment variables for configuration,
|
||||
// and includes a debug mode for verbose output. It handles different XML response formats
|
||||
// from the OBS API and ensures that the output JSON files are properly sanitized and formatted.
|
||||
//
|
||||
// The accepted command-line flags are:
|
||||
//
|
||||
// -debug: Enable debug output showing API URLs and responses.
|
||||
// -instance: Name of the OBS instance (used in metadata, default "openSUSE").
|
||||
// -host: Base URL of the OBS API (default "http://localhost:3000").
|
||||
// -user: OBS username (or set via OBS_USER environment variable).
|
||||
// -password: OBS password (or set via OBS_PASSWORD environment variable).
|
||||
// -output: Directory to save the JSON files (default "groups").
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// # Using flags for credentials
|
||||
// go run main.go -user "myuser" -password "mypass"
|
||||
//
|
||||
// # Using environment variables (OBS_USER, OBS_PASSWORD)
|
||||
// go run main.go
|
||||
//
|
||||
// # Targeting a specific OBS instance and output directory
|
||||
// go run main.go -host "https://api.opensuse.org" -output "./obs_groups"
|
||||
//
|
||||
// # Full command with debug mode
|
||||
// go run main.go -host http://localhost:8000 -user "myuser" -password "mypass" -output "./obs_groups" -instance "OBS" -debug
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
envOBSUser = "OBS_USER"
|
||||
envOBSPassword = "OBS_PASSWORD"
|
||||
)
|
||||
|
||||
type ObsClient struct {
|
||||
baseUrl *url.URL
|
||||
client *http.Client
|
||||
user string
|
||||
password string
|
||||
cookie string
|
||||
debug bool
|
||||
}
|
||||
|
||||
type groupsList struct {
|
||||
XMLName xml.Name `xml:"groups"`
|
||||
Groups []groupItem `xml:"group"`
|
||||
}
|
||||
|
||||
type groupsListAlt struct {
|
||||
XMLName xml.Name `xml:"directory"`
|
||||
Entries []groupEntry `xml:"entry"`
|
||||
}
|
||||
|
||||
type groupEntry struct {
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Inner string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (e *groupEntry) getName() string {
|
||||
if e.Name != "" {
|
||||
return e.Name
|
||||
}
|
||||
return e.Inner
|
||||
}
|
||||
|
||||
type groupItem struct {
|
||||
GroupID string `xml:"groupid,attr"`
|
||||
}
|
||||
|
||||
type personRepoMeta struct {
|
||||
XMLName xml.Name `xml:"person"`
|
||||
UserID string `xml:"userid,attr"`
|
||||
Role string `xml:"role,attr,omitempty"`
|
||||
}
|
||||
|
||||
type personGroup struct {
|
||||
XMLName xml.Name `xml:"person"`
|
||||
Persons []personRepoMeta `xml:"person"`
|
||||
}
|
||||
|
||||
type groupMeta struct {
|
||||
XMLName xml.Name `xml:"group"`
|
||||
Title string `xml:"title"`
|
||||
Persons personGroup `xml:"person"`
|
||||
}
|
||||
|
||||
func NewObsClient(host, user, password string, debug bool) (*ObsClient, error) {
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("host URL cannot be empty")
|
||||
}
|
||||
if user == "" || password == "" {
|
||||
return nil, fmt.Errorf("username and password are required")
|
||||
}
|
||||
|
||||
baseUrl, err := url.Parse(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse host URL: %w", err)
|
||||
}
|
||||
|
||||
if baseUrl.Scheme == "" || baseUrl.Host == "" {
|
||||
return nil, fmt.Errorf("host URL must contain scheme (http:// or https://) and hostname")
|
||||
}
|
||||
|
||||
return &ObsClient{
|
||||
baseUrl: baseUrl,
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
user: user,
|
||||
password: password,
|
||||
debug: debug,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ObsClient) ObsRequest(ctx context.Context, method string, url_path []string, body io.Reader) (*http.Response, error) {
|
||||
fullURL := c.baseUrl.JoinPath(url_path...).String()
|
||||
return c.ObsRequestRaw(ctx, method, fullURL, body)
|
||||
}
|
||||
|
||||
func (c *ObsClient) ObsRequestRaw(ctx context.Context, method string, url string, body io.Reader) (*http.Response, error) {
|
||||
if c.debug {
|
||||
log.Printf("[DEBUG] %s %s", method, url)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body != nil {
|
||||
req.Body = io.NopCloser(body)
|
||||
}
|
||||
if c.cookie != "" {
|
||||
req.Header.Add("Cookie", c.cookie)
|
||||
}
|
||||
if c.user != "" && c.password != "" {
|
||||
req.SetBasicAuth(c.user, c.password)
|
||||
}
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode == 200 {
|
||||
auth_cookie := res.Header.Get("set-cookie")
|
||||
if auth_cookie != "" {
|
||||
c.cookie = auth_cookie
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *ObsClient) GetAllGroups(ctx context.Context) ([]string, error) {
|
||||
res, err := c.ObsRequest(ctx, "GET", []string{"group"}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Response status: %d, body length: %d", res.StatusCode, len(data))
|
||||
if res.StatusCode != 200 {
|
||||
bodyStr := string(data)
|
||||
if len(bodyStr) > 500 {
|
||||
bodyStr = bodyStr[:500]
|
||||
}
|
||||
return nil, fmt.Errorf("Unexpected return code: %d, body: %s", res.StatusCode, bodyStr)
|
||||
}
|
||||
|
||||
// Try parsing as <groups> format
|
||||
var groupsList groupsList
|
||||
err = xml.Unmarshal(data, &groupsList)
|
||||
if err == nil && len(groupsList.Groups) > 0 {
|
||||
groupIDs := make([]string, len(groupsList.Groups))
|
||||
for i, g := range groupsList.Groups {
|
||||
groupIDs[i] = g.GroupID
|
||||
}
|
||||
return groupIDs, nil
|
||||
}
|
||||
|
||||
// Try parsing as <directory> format
|
||||
var groupsAlt groupsListAlt
|
||||
err = xml.Unmarshal(data, &groupsAlt)
|
||||
if err == nil && len(groupsAlt.Entries) > 0 {
|
||||
groupIDs := make([]string, len(groupsAlt.Entries))
|
||||
for i, e := range groupsAlt.Entries {
|
||||
groupIDs[i] = e.getName()
|
||||
}
|
||||
return groupIDs, nil
|
||||
}
|
||||
|
||||
// Log what we got
|
||||
bodyStr := string(data)
|
||||
if len(bodyStr) > 1000 {
|
||||
bodyStr = bodyStr[:1000]
|
||||
}
|
||||
log.Printf("Failed to parse XML, got: %s", bodyStr)
|
||||
return nil, fmt.Errorf("Could not parse groups response")
|
||||
}
|
||||
|
||||
func (c *ObsClient) GetGroupMeta(ctx context.Context, gid string) (*groupMeta, error) {
|
||||
log.Printf("[DEBUG] gid: %s", gid)
|
||||
groupPath := []string{"group", gid}
|
||||
fullURL := c.baseUrl.JoinPath(groupPath...).String()
|
||||
if c.debug {
|
||||
log.Printf("[DEBUG] Fetching group: %s", fullURL)
|
||||
}
|
||||
res, err := c.ObsRequestRaw(ctx, "GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
switch res.StatusCode {
|
||||
case 200:
|
||||
break
|
||||
case 404:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Unexpected return code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var meta groupMeta
|
||||
err = xml.Unmarshal(data, &meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
type GroupOutput struct {
|
||||
Meta ImportMeta `json:"_meta,omitempty"`
|
||||
Name string `json:"Name"`
|
||||
Reviewers []string `json:"Reviewers"`
|
||||
Silent bool `json:"Silent,omitempty"`
|
||||
}
|
||||
|
||||
type ImportMeta struct {
|
||||
ImportedFrom string `json:"imported_from"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
ImportTime time.Time `json:"import_time"`
|
||||
}
|
||||
|
||||
func sanitizeFilename(name string) string {
|
||||
name = strings.ReplaceAll(name, "/", "_")
|
||||
name = strings.ReplaceAll(name, ":", "_")
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
return name
|
||||
}
|
||||
|
||||
func processGroup(ctx context.Context, client *ObsClient, groupID, outputDir, instanceName string, importTime time.Time) error {
|
||||
meta, err := client.GetGroupMeta(ctx, groupID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching group meta: %w", err)
|
||||
}
|
||||
|
||||
if meta == nil {
|
||||
return fmt.Errorf("group not found")
|
||||
}
|
||||
|
||||
if client.debug {
|
||||
log.Printf("[DEBUG] Group meta for %s: Title: %s, Persons: %d", groupID, meta.Title, len(meta.Persons.Persons))
|
||||
}
|
||||
|
||||
reviewers := make([]string, 0, len(meta.Persons.Persons))
|
||||
for _, p := range meta.Persons.Persons {
|
||||
reviewers = append(reviewers, p.UserID)
|
||||
}
|
||||
|
||||
output := GroupOutput{
|
||||
Meta: ImportMeta{
|
||||
ImportedFrom: instanceName,
|
||||
ReadOnly: true,
|
||||
ImportTime: importTime,
|
||||
},
|
||||
Name: groupID,
|
||||
Reviewers: reviewers,
|
||||
}
|
||||
|
||||
filename := sanitizeFilename(groupID) + ".json"
|
||||
filePath := filepath.Join(outputDir, filename)
|
||||
|
||||
data, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling json: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
if client.debug {
|
||||
log.Printf("[DEBUG] Saved group %s to %s", groupID, filePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
debugModePtr := flag.Bool("debug", false, "Enable debug output showing API URLs")
|
||||
obsInstance := flag.String("instance", "openSUSE", "OBS instance name (used in metadata)")
|
||||
obsHost := flag.String("host", "http://localhost:3000", "OBS API host URL")
|
||||
obsUser := flag.String("user", "", "OBS username (or set OBS_USER env)")
|
||||
obsPassword := flag.String("password", "", "OBS password (or set OBS_PASSWORD env)")
|
||||
outputDir := flag.String("output", "groups", "Output directory for JSON files")
|
||||
flag.Parse()
|
||||
|
||||
obsUserVal := *obsUser
|
||||
obsPasswordVal := *obsPassword
|
||||
|
||||
if obsUserVal == "" {
|
||||
if *debugModePtr {
|
||||
log.Printf("[DEBUG] No OBS user provided. Trying environment variables `%s`...", envOBSUser)
|
||||
}
|
||||
obsUserVal = os.Getenv(envOBSUser)
|
||||
}
|
||||
if obsPasswordVal == "" {
|
||||
if *debugModePtr {
|
||||
log.Printf("[DEBUG] No OBS password provided. Trying environment variables `%s`...", envOBSPassword)
|
||||
}
|
||||
obsPasswordVal = os.Getenv(envOBSPassword)
|
||||
}
|
||||
|
||||
if obsUserVal == "" || obsPasswordVal == "" {
|
||||
log.Fatalf("OBS credentials required. Set -user/-password flags or %s/%s environment variables", envOBSUser, envOBSPassword)
|
||||
}
|
||||
|
||||
log.Printf("Connecting to OBS at %s (instance: %s)", *obsHost, *obsInstance)
|
||||
|
||||
client, err := NewObsClient(*obsHost, obsUserVal, obsPasswordVal, *debugModePtr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create OBS client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
log.Println("Fetching list of all groups...")
|
||||
groupIDs, err := client.GetAllGroups(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get groups list: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Found %d groups: %v", len(groupIDs), groupIDs)
|
||||
log.Printf("Found %s ", groupIDs)
|
||||
|
||||
err = os.MkdirAll(*outputDir, 0755)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create output directory: %v", err)
|
||||
}
|
||||
|
||||
importTime := time.Now()
|
||||
|
||||
successCount := 0
|
||||
errorCount := 0
|
||||
|
||||
for i, groupID := range groupIDs {
|
||||
log.Printf("[%d/%d] Fetching group: %s", i+1, len(groupIDs), groupID)
|
||||
|
||||
if err := processGroup(ctx, client, groupID, *outputDir, *obsInstance, importTime); err != nil {
|
||||
log.Printf("Error processing group %s: %v", groupID, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Printf("Done! Success: %d, Errors: %d", successCount, errorCount)
|
||||
log.Printf("JSON files saved to: %s", *outputDir)
|
||||
}
|
||||
247
obs-groups-bot/main_test.go
Normal file
247
obs-groups-bot/main_test.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGroupsListParsing(t *testing.T) {
|
||||
// Test <groups> format
|
||||
groupsXML := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<groups>
|
||||
<group groupid="group1"/>
|
||||
<group groupid="group2"/>
|
||||
<group groupid="group3"/>
|
||||
</groups>`
|
||||
|
||||
var groupsList groupsList
|
||||
err := xml.Unmarshal([]byte(groupsXML), &groupsList)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal groups XML: %v", err)
|
||||
}
|
||||
|
||||
if len(groupsList.Groups) != 3 {
|
||||
t.Errorf("Expected 3 groups, got %d", len(groupsList.Groups))
|
||||
}
|
||||
|
||||
expected := []string{"group1", "group2", "group3"}
|
||||
for i, g := range groupsList.Groups {
|
||||
if g.GroupID != expected[i] {
|
||||
t.Errorf("Expected group %s, got %s", expected[i], g.GroupID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessGroup(t *testing.T) {
|
||||
// 1. Mock the OBS API server for GetGroupMeta
|
||||
groupID := "test:group"
|
||||
mockGroupMetaResponse := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<group>
|
||||
<title>Test Group Title</title>
|
||||
<person>
|
||||
<person userid="user1" role="maintainer"/>
|
||||
<person userid="user2" role="reviewer"/>
|
||||
</person>
|
||||
</group>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
expectedPath := "/group/" + groupID
|
||||
if r.URL.Path != expectedPath {
|
||||
t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(mockGroupMetaResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// 2. Create a temporary directory for output
|
||||
outputDir := t.TempDir()
|
||||
|
||||
// 3. Initialize client pointing to mock server
|
||||
client, err := NewObsClient(server.URL, "testuser", "testpass", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
// 4. Call processGroup
|
||||
instanceName := "test-instance"
|
||||
importTime := time.Now().UTC().Truncate(time.Second) // Truncate for stable comparison
|
||||
err = processGroup(context.Background(), client, groupID, outputDir, instanceName, importTime)
|
||||
if err != nil {
|
||||
t.Fatalf("processGroup failed: %v", err)
|
||||
}
|
||||
|
||||
// 5. Verify the output file
|
||||
expectedFilename := sanitizeFilename(groupID) + ".json"
|
||||
filePath := filepath.Join(outputDir, expectedFilename)
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
t.Fatalf("Expected output file was not created: %s", filePath)
|
||||
}
|
||||
|
||||
// Read and verify file content
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read output file: %v", err)
|
||||
}
|
||||
|
||||
var result GroupOutput
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("Failed to unmarshal output JSON: %v", err)
|
||||
}
|
||||
|
||||
// Assertions
|
||||
expectedReviewers := []string{"user1", "user2"}
|
||||
expectedOutput := GroupOutput{
|
||||
Meta: ImportMeta{
|
||||
ImportedFrom: instanceName,
|
||||
ReadOnly: true,
|
||||
ImportTime: importTime,
|
||||
},
|
||||
Name: groupID,
|
||||
Reviewers: expectedReviewers,
|
||||
}
|
||||
|
||||
// Use reflect.DeepEqual for a robust comparison of the structs
|
||||
if !reflect.DeepEqual(result, expectedOutput) {
|
||||
t.Errorf("Output JSON does not match expected.\nGot: %+v\nWant: %+v", result, expectedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObsClient_GetAllGroups(t *testing.T) {
|
||||
// Mock the OBS API server
|
||||
mockResponse := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<groups>
|
||||
<group groupid="mock-group-1"/>
|
||||
<group groupid="mock-group-2"/>
|
||||
</groups>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify the request path
|
||||
if r.URL.Path != "/group" {
|
||||
t.Errorf("Expected path /group, got %s", r.URL.Path)
|
||||
}
|
||||
// Verify method
|
||||
if r.Method != "GET" {
|
||||
t.Errorf("Expected method GET, got %s", r.Method)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Initialize client pointing to mock server
|
||||
client, err := NewObsClient(server.URL, "testuser", "testpass", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
groups, err := client.GetAllGroups(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllGroups failed: %v", err)
|
||||
}
|
||||
|
||||
if len(groups) != 2 {
|
||||
t.Errorf("Expected 2 groups, got %d", len(groups))
|
||||
}
|
||||
if groups[0] != "mock-group-1" {
|
||||
t.Errorf("Expected first group to be mock-group-1, got %s", groups[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupsListDirectoryFormat(t *testing.T) {
|
||||
// Test <directory> format with name attribute
|
||||
dirXML := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<directory>
|
||||
<entry name="group-a"/>
|
||||
<entry name="group-b"/>
|
||||
<entry name="group-c"/>
|
||||
</directory>`
|
||||
|
||||
var groupsAlt groupsListAlt
|
||||
err := xml.Unmarshal([]byte(dirXML), &groupsAlt)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal directory XML: %v", err)
|
||||
}
|
||||
|
||||
if len(groupsAlt.Entries) != 3 {
|
||||
t.Errorf("Expected 3 entries, got %d", len(groupsAlt.Entries))
|
||||
}
|
||||
|
||||
expected := []string{"group-a", "group-b", "group-c"}
|
||||
for i, e := range groupsAlt.Entries {
|
||||
if e.getName() != expected[i] {
|
||||
t.Errorf("Expected entry %s, got %s", expected[i], e.getName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupMetaParsing(t *testing.T) {
|
||||
groupXML := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<group>
|
||||
<title>Test Group Title</title>
|
||||
<person>
|
||||
<person userid="user1" role="maintainer"/>
|
||||
<person userid="user2" role="reviewer"/>
|
||||
<person userid="user3"/>
|
||||
</person>
|
||||
</group>`
|
||||
|
||||
var meta groupMeta
|
||||
err := xml.Unmarshal([]byte(groupXML), &meta)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal group meta XML: %v", err)
|
||||
}
|
||||
|
||||
if meta.Title != "Test Group Title" {
|
||||
t.Errorf("Expected title 'Test Group Title', got '%s'", meta.Title)
|
||||
}
|
||||
|
||||
if len(meta.Persons.Persons) != 3 {
|
||||
t.Errorf("Expected 3 persons, got %d", len(meta.Persons.Persons))
|
||||
}
|
||||
|
||||
persons := meta.Persons.Persons
|
||||
if persons[0].UserID != "user1" || persons[0].Role != "maintainer" {
|
||||
t.Errorf("First person should be user1 with role maintainer")
|
||||
}
|
||||
if persons[1].UserID != "user2" || persons[1].Role != "reviewer" {
|
||||
t.Errorf("Second person should be user2 with role reviewer")
|
||||
}
|
||||
if persons[2].UserID != "user3" || persons[2].Role != "" {
|
||||
t.Errorf("Third person should be user3 with empty role")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"simple", "simple"},
|
||||
{"group/name", "group_name"},
|
||||
{"project:group", "project_group"},
|
||||
{"group with spaces", "group_with_spaces"},
|
||||
{"group/name:space", "group_name_space"},
|
||||
{"", ""},
|
||||
{"multiple///slashes", "multiple___slashes"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
result := sanitizeFilename(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("sanitizeFilename(%q) = %q, expected %q", tc.input, result, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user