Compare commits
2 Commits
improve_po
...
t-refactor
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
430dfc7296 | ||
|
|
0464324ea7 |
@@ -34,9 +34,7 @@ jobs:
|
||||
run: make build
|
||||
working-directory: ./autogits
|
||||
- name: Prepare images
|
||||
run: |
|
||||
make build
|
||||
podman rmi $(podman images -f "dangling=true" -q)
|
||||
run: make build
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down
|
||||
run: make down
|
||||
@@ -45,16 +43,12 @@ jobs:
|
||||
run: |
|
||||
make up
|
||||
make wait_healthy
|
||||
podman ps
|
||||
sleep 5
|
||||
working-directory: ./autogits/integration
|
||||
- name: Run tests
|
||||
run: make pytest
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down
|
||||
if: always()
|
||||
run: |
|
||||
podman ps
|
||||
make down
|
||||
run: make down
|
||||
working-directory: ./autogits/integration
|
||||
|
||||
|
||||
@@ -396,17 +396,12 @@ func (e *GitHandlerImpl) GitExecQuietOrPanic(cwd string, params ...string) {
|
||||
}
|
||||
|
||||
type ChanIO struct {
|
||||
ch chan byte
|
||||
done chan struct{}
|
||||
ch chan byte
|
||||
}
|
||||
|
||||
func (c *ChanIO) Write(p []byte) (int, error) {
|
||||
for _, b := range p {
|
||||
select {
|
||||
case c.ch <- b:
|
||||
case <-c.done:
|
||||
return 0, io.EOF
|
||||
}
|
||||
c.ch <- b
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -415,32 +410,21 @@ func (c *ChanIO) Write(p []byte) (int, error) {
|
||||
func (c *ChanIO) Read(data []byte) (idx int, err error) {
|
||||
var ok bool
|
||||
|
||||
select {
|
||||
case data[idx], ok = <-c.ch:
|
||||
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
|
||||
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
|
||||
}
|
||||
idx++
|
||||
}
|
||||
|
||||
return
|
||||
@@ -487,14 +471,7 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
||||
var size int
|
||||
|
||||
pos := 0
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return GitMsg{}, io.EOF
|
||||
}
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
for c := <-data; c != ' '; c = <-data {
|
||||
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
||||
id[pos] = c
|
||||
pos++
|
||||
@@ -506,15 +483,7 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
||||
|
||||
pos = 0
|
||||
var c byte
|
||||
for {
|
||||
var ok bool
|
||||
c, ok = <-data
|
||||
if !ok {
|
||||
return GitMsg{}, io.EOF
|
||||
}
|
||||
if c == ' ' || c == '\x00' {
|
||||
break
|
||||
}
|
||||
for c = <-data; c != ' ' && c != '\x00'; c = <-data {
|
||||
if c >= 'a' && c <= 'z' {
|
||||
msgType[pos] = c
|
||||
pos++
|
||||
@@ -540,14 +509,7 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
||||
return GitMsg{}, fmt.Errorf("Invalid object type: '%s'", string(msgType))
|
||||
}
|
||||
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return GitMsg{}, io.EOF
|
||||
}
|
||||
if c == '\x00' {
|
||||
break
|
||||
}
|
||||
for c = <-data; c != '\000'; c = <-data {
|
||||
if c >= '0' && c <= '9' {
|
||||
size = size*10 + (int(c) - '0')
|
||||
} else {
|
||||
@@ -566,37 +528,18 @@ func parseGitCommitHdr(oldHdr [2]string, data <-chan byte) ([2]string, int, erro
|
||||
hdr := make([]byte, 0, 60)
|
||||
val := make([]byte, 0, 1000)
|
||||
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return [2]string{}, 0, io.EOF
|
||||
}
|
||||
c := <-data
|
||||
size := 1
|
||||
if c != '\n' { // end of header marker
|
||||
for {
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
for ; c != ' '; c = <-data {
|
||||
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 {
|
||||
var ok bool
|
||||
c, ok = <-data
|
||||
if !ok {
|
||||
return [2]string{}, size, io.EOF
|
||||
}
|
||||
if c == '\n' {
|
||||
break
|
||||
}
|
||||
for c := <-data; c != '\n'; c = <-data {
|
||||
val = append(val, c)
|
||||
size++
|
||||
}
|
||||
@@ -609,14 +552,7 @@ 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, ok := <-data
|
||||
if !ok {
|
||||
return string(msg), io.EOF
|
||||
}
|
||||
if c == '\x00' {
|
||||
break
|
||||
}
|
||||
for c := <-data; c != '\x00'; c = <-data {
|
||||
msg = append(msg, c)
|
||||
l--
|
||||
}
|
||||
@@ -642,7 +578,7 @@ func parseGitCommit(data <-chan byte) (GitCommit, error) {
|
||||
var hdr [2]string
|
||||
hdr, size, err := parseGitCommitHdr(hdr, data)
|
||||
if err != nil {
|
||||
return GitCommit{}, err
|
||||
return GitCommit{}, nil
|
||||
}
|
||||
l -= size
|
||||
|
||||
@@ -663,28 +599,14 @@ func parseGitCommit(data <-chan byte) (GitCommit, error) {
|
||||
func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
||||
var e GitTreeEntry
|
||||
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return e, io.EOF
|
||||
}
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
for c := <-data; c != ' '; c = <-data {
|
||||
e.mode = e.mode*8 + int(c-'0')
|
||||
e.size++
|
||||
}
|
||||
e.size++
|
||||
|
||||
name := make([]byte, 0, 128)
|
||||
for {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return e, io.EOF
|
||||
}
|
||||
if c == '\x00' {
|
||||
break
|
||||
}
|
||||
for c := <-data; c != '\x00'; c = <-data {
|
||||
name = append(name, c)
|
||||
e.size++
|
||||
}
|
||||
@@ -695,10 +617,7 @@ func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
||||
|
||||
hash := make([]byte, 0, hashLen*2)
|
||||
for range hashLen {
|
||||
c, ok := <-data
|
||||
if !ok {
|
||||
return e, io.EOF
|
||||
}
|
||||
c := <-data
|
||||
hash = append(hash, hexBinToAscii[((c&0xF0)>>4)], hexBinToAscii[c&0xF])
|
||||
}
|
||||
e.hash = string(hash)
|
||||
@@ -719,16 +638,13 @@ func parseGitTree(data <-chan byte) (GitTree, error) {
|
||||
for parsedLen < hdr.size {
|
||||
entry, err := parseTreeEntry(data, len(hdr.hash)/2)
|
||||
if err != nil {
|
||||
return GitTree{}, err
|
||||
return GitTree{}, nil
|
||||
}
|
||||
|
||||
t.items = append(t.items, entry)
|
||||
parsedLen += entry.size
|
||||
}
|
||||
c, ok := <-data // \0 read
|
||||
if !ok {
|
||||
return t, io.EOF
|
||||
}
|
||||
c := <-data // \0 read
|
||||
|
||||
if c != '\x00' {
|
||||
return t, fmt.Errorf("Unexpected character during git tree data read")
|
||||
@@ -749,16 +665,9 @@ func parseGitBlob(data <-chan byte) ([]byte, error) {
|
||||
|
||||
d := make([]byte, hdr.size)
|
||||
for l := 0; l < hdr.size; l++ {
|
||||
var ok bool
|
||||
d[l], ok = <-data
|
||||
if !ok {
|
||||
return d, io.EOF
|
||||
}
|
||||
}
|
||||
eob, ok := <-data
|
||||
if !ok {
|
||||
return d, io.EOF
|
||||
d[l] = <-data
|
||||
}
|
||||
eob := <-data
|
||||
if eob != '\x00' {
|
||||
return d, fmt.Errorf("invalid byte read in parseGitBlob")
|
||||
}
|
||||
@@ -770,25 +679,16 @@ func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsed
|
||||
var done sync.Mutex
|
||||
|
||||
done.Lock()
|
||||
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}
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
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.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
c, e := parseGitCommit(data_in.ch)
|
||||
if e != nil {
|
||||
err = fmt.Errorf("Error parsing git commit: %w", e)
|
||||
@@ -815,14 +715,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -831,21 +729,15 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
||||
var done sync.Mutex
|
||||
|
||||
done.Lock()
|
||||
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}
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
|
||||
go func() {
|
||||
defer done.Unlock()
|
||||
defer close_done()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -853,9 +745,11 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
||||
return
|
||||
}
|
||||
data_out.Write([]byte(c.Tree))
|
||||
data_out.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
if err != nil {
|
||||
LogError("Error parsing git tree:", err)
|
||||
return
|
||||
@@ -865,7 +759,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.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
data, err = parseGitBlob(data_in.ch)
|
||||
return
|
||||
}
|
||||
@@ -890,13 +784,11 @@ 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
|
||||
}
|
||||
|
||||
@@ -906,24 +798,16 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
||||
directoryList = make(map[string]string)
|
||||
|
||||
done.Lock()
|
||||
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}
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
|
||||
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.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -939,7 +823,7 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
||||
delete(trees, p)
|
||||
|
||||
data_out.Write([]byte(tree))
|
||||
data_out.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
@@ -973,14 +857,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -990,14 +872,7 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
||||
directoryList = make(map[string]string)
|
||||
|
||||
done.Lock()
|
||||
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}
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
|
||||
LogDebug("Getting directory content for:", commitId)
|
||||
|
||||
@@ -1006,7 +881,7 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -1022,7 +897,7 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
||||
delete(trees, p)
|
||||
|
||||
data_out.Write([]byte(tree))
|
||||
data_out.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
@@ -1058,14 +933,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -1075,24 +948,16 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
||||
submoduleList = make(map[string]string)
|
||||
|
||||
done.Lock()
|
||||
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}
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
|
||||
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.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
var c GitCommit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
@@ -1108,7 +973,7 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
||||
delete(trees, p)
|
||||
|
||||
data_out.Write([]byte(tree))
|
||||
data_out.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
var tree GitTree
|
||||
tree, err = parseGitTree(data_in.ch)
|
||||
|
||||
@@ -1145,26 +1010,17 @@ 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) {
|
||||
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}
|
||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
@@ -1180,18 +1036,17 @@ 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.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
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.Write([]byte{0})
|
||||
data_out.ch <- '\x00'
|
||||
tree, err := parseGitTree(data_in.ch)
|
||||
|
||||
if err != nil {
|
||||
@@ -1223,14 +1078,12 @@ 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,7 +28,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGitClone(t *testing.T) {
|
||||
@@ -718,44 +717,3 @@ 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.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ wait_healthy:
|
||||
@echo "All services are healthy!"
|
||||
|
||||
pytest:
|
||||
podman-compose exec tester pytest -v tests
|
||||
podman-compose exec tester pytest -v tests/*
|
||||
|
||||
build:
|
||||
podman pull docker.io/library/rabbitmq:3.13.7-management
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#!BuildTag: openbuildservice/gwf-test-basecontainer
|
||||
#!UseOBSRepositories
|
||||
FROM registry.suse.com/bci/bci-base:15.7
|
||||
|
||||
RUN zypper -n install binutils gawk git git-core git-lfs jq make openssh openssh-clients podman podman-compose python3-pytest python3-pytest-dependency python3-pytest-httpserver python3-requests sqlite3 vim which
|
||||
|
||||
RUN zypper -n install autogits-gitea-events-rabbitmq-publisher autogits-obs-staging-bot autogits-workflow-pr gitea
|
||||
@@ -29,11 +29,6 @@ services:
|
||||
image: rabbitmq:3.13.7-management
|
||||
container_name: rabbitmq-test
|
||||
init: true
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmq-diagnostics", "check_running", "-q"]
|
||||
interval: 30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
networks:
|
||||
- gitea-network
|
||||
ports:
|
||||
@@ -81,11 +76,6 @@ services:
|
||||
init: true
|
||||
networks:
|
||||
- gitea-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pgrep workflow-pr && [ $$(awk '{print $22}' /proc/$$(pgrep workflow-pr)/stat) -lt $$(($$(awk '{print $1}' /proc/uptime | cut -d. -f1)*100 - 1000)) ]"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
depends_on:
|
||||
gitea:
|
||||
condition: service_started
|
||||
@@ -100,7 +90,7 @@ services:
|
||||
- ./workflow-pr/workflow-pr.json:/etc/workflow-pr.json:ro,z
|
||||
- ./workflow-pr-repos:/var/lib/workflow-pr/repos:Z
|
||||
command: [
|
||||
"-check-on-start",
|
||||
"-check-on-start",
|
||||
"-debug",
|
||||
"-gitea-url", "http://gitea-test:3000",
|
||||
"-url", "amqps://rabbitmq-test:5671",
|
||||
@@ -134,7 +124,9 @@ services:
|
||||
networks:
|
||||
- gitea-network
|
||||
depends_on:
|
||||
workflow-pr:
|
||||
gitea:
|
||||
condition: service_started
|
||||
tester:
|
||||
condition: service_started
|
||||
environment:
|
||||
- OBS_USER=mock
|
||||
|
||||
@@ -9,17 +9,7 @@ import os
|
||||
import json
|
||||
import base64
|
||||
import re
|
||||
from tests.lib.common_test_utils import GiteaAPIClient, vprint
|
||||
import tests.lib.common_test_utils as common_utils
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def is_test_run():
|
||||
common_utils.IS_TEST_RUN = True
|
||||
yield
|
||||
common_utils.IS_TEST_RUN = False
|
||||
|
||||
if os.environ.get("AUTOGITS_PRINT_FIXTURES") is None:
|
||||
print("--- Fixture messages are suppressed. Set AUTOGITS_PRINT_FIXTURES=1 to enable them. ---")
|
||||
from tests.lib.common_test_utils import GiteaAPIClient
|
||||
|
||||
class ObsMockState:
|
||||
def __init__(self):
|
||||
@@ -162,10 +152,6 @@ BRANCH_CONFIG_CUSTOM = {
|
||||
"workflow.config": {
|
||||
"MergeMode": "devel"
|
||||
}
|
||||
},
|
||||
"zz-ready-to-start": {
|
||||
"workflow.config": {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +162,7 @@ _CREATED_USERS = set()
|
||||
_CREATED_LABELS = set()
|
||||
_ADDED_COLLABORATORS = set() # format: (org_repo, username)
|
||||
|
||||
def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict, stats: dict = None, handled: dict = None):
|
||||
def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict):
|
||||
"""
|
||||
Parses workflow.config and _maintainership.json, creates users, and adds them as collaborators.
|
||||
"""
|
||||
@@ -196,19 +182,13 @@ def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict, stats: d
|
||||
|
||||
# Create all users
|
||||
for username in all_users:
|
||||
new_user = client.create_user(username, "password123", f"{username}@example.com")
|
||||
_CREATED_USERS.add(username)
|
||||
if stats and handled and username not in handled["users"]:
|
||||
handled["users"].add(username)
|
||||
if new_user: stats["users"]["new"] += 1
|
||||
else: stats["users"]["reused"] += 1
|
||||
if username not in _CREATED_USERS:
|
||||
client.create_user(username, "password123", f"{username}@example.com")
|
||||
_CREATED_USERS.add(username)
|
||||
|
||||
new_coll = client.add_collaborator("myproducts", "mySLFO", username, "write")
|
||||
_ADDED_COLLABORATORS.add(("myproducts/mySLFO", username))
|
||||
if stats and handled and ("myproducts/mySLFO", username) not in handled["collaborators"]:
|
||||
handled["collaborators"].add(("myproducts/mySLFO", username))
|
||||
if new_coll: stats["collaborators"]["new"] += 1
|
||||
else: stats["collaborators"]["reused"] += 1
|
||||
if ("myproducts/mySLFO", username) not in _ADDED_COLLABORATORS:
|
||||
client.add_collaborator("myproducts", "mySLFO", username, "write")
|
||||
_ADDED_COLLABORATORS.add(("myproducts/mySLFO", username))
|
||||
|
||||
# Set specific repository permissions based on maintainership
|
||||
for pkg, users in mt.items():
|
||||
@@ -216,34 +196,20 @@ def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict, stats: d
|
||||
for username in users:
|
||||
if not repo_name:
|
||||
for r in ["pkgA", "pkgB"]:
|
||||
new_coll = client.add_collaborator("mypool", r, username, "write")
|
||||
_ADDED_COLLABORATORS.add((f"mypool/{r}", username))
|
||||
if stats and handled and (f"mypool/{r}", username) not in handled["collaborators"]:
|
||||
handled["collaborators"].add((f"mypool/{r}", username))
|
||||
if new_coll: stats["collaborators"]["new"] += 1
|
||||
else: stats["collaborators"]["reused"] += 1
|
||||
if (f"mypool/{r}", username) not in _ADDED_COLLABORATORS:
|
||||
client.add_collaborator("mypool", r, username, "write")
|
||||
_ADDED_COLLABORATORS.add((f"mypool/{r}", username))
|
||||
else:
|
||||
new_coll = client.add_collaborator("mypool", repo_name, username, "write")
|
||||
_ADDED_COLLABORATORS.add((f"mypool/{repo_name}", username))
|
||||
if stats and handled and (f"mypool/{repo_name}", username) not in handled["collaborators"]:
|
||||
handled["collaborators"].add((f"mypool/{repo_name}", username))
|
||||
if new_coll: stats["collaborators"]["new"] += 1
|
||||
else: stats["collaborators"]["reused"] += 1
|
||||
if (f"mypool/{repo_name}", username) not in _ADDED_COLLABORATORS:
|
||||
client.add_collaborator("mypool", repo_name, username, "write")
|
||||
_ADDED_COLLABORATORS.add((f"mypool/{repo_name}", username))
|
||||
|
||||
def ensure_config_file(client: GiteaAPIClient, owner: str, repo: str, branch: str, file_name: str, expected_content_dict: dict, existing_files: list = None):
|
||||
def ensure_config_file(client: GiteaAPIClient, owner: str, repo: str, branch: str, file_name: str, expected_content_dict: dict):
|
||||
"""
|
||||
Checks if a config file exists and has the correct content.
|
||||
Returns True if a change was made, False otherwise.
|
||||
"""
|
||||
file_info = None
|
||||
if existing_files is not None:
|
||||
if file_name not in [f["path"] for f in existing_files]:
|
||||
pass # File definitely doesn't exist
|
||||
else:
|
||||
file_info = client.get_file_info(owner, repo, file_name, branch=branch)
|
||||
else:
|
||||
file_info = client.get_file_info(owner, repo, file_name, branch=branch)
|
||||
|
||||
file_info = client.get_file_info(owner, repo, file_name, branch=branch)
|
||||
expected_content = json.dumps(expected_content_dict, indent=4)
|
||||
|
||||
if file_info:
|
||||
@@ -263,26 +229,6 @@ def gitea_env():
|
||||
"""
|
||||
Global fixture to set up the Gitea environment for all tests.
|
||||
"""
|
||||
setup_start_time = time.time()
|
||||
stats = {
|
||||
"orgs": {"new": 0, "reused": 0},
|
||||
"repos": {"new": 0, "reused": 0},
|
||||
"users": {"new": 0, "reused": 0},
|
||||
"labels": {"new": 0, "reused": 0},
|
||||
"collaborators": {"new": 0, "reused": 0},
|
||||
"branches": {"new": 0, "reused": 0},
|
||||
"webhooks": {"new": 0, "reused": 0},
|
||||
}
|
||||
handled_in_session = {
|
||||
"orgs": set(),
|
||||
"repos": set(),
|
||||
"users": set(),
|
||||
"labels": set(),
|
||||
"collaborators": set(),
|
||||
"branches": set(),
|
||||
"webhooks": set(),
|
||||
}
|
||||
|
||||
gitea_url = "http://gitea-test:3000"
|
||||
admin_token_path = os.path.join(os.path.dirname(__file__), "..", "gitea-data", "admin.token")
|
||||
|
||||
@@ -294,55 +240,35 @@ def gitea_env():
|
||||
raise Exception(f"Admin token file not found at {admin_token_path}.")
|
||||
|
||||
client = GiteaAPIClient(base_url=gitea_url, token=admin_token)
|
||||
client.use_cache = True
|
||||
|
||||
# Wait for Gitea
|
||||
for i in range(10):
|
||||
try:
|
||||
resp, dur = client._request("GET", "version")
|
||||
if resp.status_code == 200:
|
||||
vprint(f"DEBUG: Gitea connection successful (duration: {dur:.3f}s)")
|
||||
if client._request("GET", "version").status_code == 200:
|
||||
break
|
||||
except Exception as e:
|
||||
vprint(f"DEBUG: Gitea connection attempt {i+1} failed: {e}")
|
||||
except:
|
||||
pass
|
||||
time.sleep(1)
|
||||
else: raise Exception("Gitea not available.")
|
||||
else:
|
||||
raise Exception("Gitea not available.")
|
||||
|
||||
vprint("--- Starting Gitea Global Setup ---")
|
||||
print("--- Starting Gitea Global Setup ---")
|
||||
for org in ["myproducts", "mypool"]:
|
||||
new_org = client.create_org(org)
|
||||
_CREATED_ORGS.add(org)
|
||||
if org not in handled_in_session["orgs"]:
|
||||
handled_in_session["orgs"].add(org)
|
||||
if new_org: stats["orgs"]["new"] += 1
|
||||
else: stats["orgs"]["reused"] += 1
|
||||
if org not in _CREATED_ORGS:
|
||||
client.create_org(org)
|
||||
_CREATED_ORGS.add(org)
|
||||
|
||||
for org, repo in [("myproducts", "mySLFO"), ("mypool", "pkgA"), ("mypool", "pkgB")]:
|
||||
new_repo = client.create_repo(org, repo)
|
||||
client.update_repo_settings(org, repo)
|
||||
repo_full = f"{org}/{repo}"
|
||||
_CREATED_REPOS.add(repo_full)
|
||||
if repo_full not in handled_in_session["repos"]:
|
||||
handled_in_session["repos"].add(repo_full)
|
||||
if new_repo: stats["repos"]["new"] += 1
|
||||
else: stats["repos"]["reused"] += 1
|
||||
|
||||
# Create webhook for publisher
|
||||
new_hook = client.create_webhook(org, repo, "http://gitea-publisher:8002/rabbitmq-forwarder")
|
||||
if repo_full not in handled_in_session["webhooks"]:
|
||||
handled_in_session["webhooks"].add(repo_full)
|
||||
if new_hook: stats["webhooks"]["new"] += 1
|
||||
else: stats["webhooks"]["reused"] += 1
|
||||
if f"{org}/{repo}" not in _CREATED_REPOS:
|
||||
client.create_repo(org, repo)
|
||||
client.update_repo_settings(org, repo)
|
||||
_CREATED_REPOS.add(f"{org}/{repo}")
|
||||
|
||||
# Create labels
|
||||
for name, color in [("staging/Backlog", "#0000ff"), ("review/Pending", "#ffff00")]:
|
||||
new_label = client.create_label("myproducts", "mySLFO", name, color=color)
|
||||
_CREATED_LABELS.add(("myproducts/mySLFO", name))
|
||||
if ("myproducts/mySLFO", name) not in handled_in_session["labels"]:
|
||||
handled_in_session["labels"].add(("myproducts/mySLFO", name))
|
||||
if new_label: stats["labels"]["new"] += 1
|
||||
else: stats["labels"]["reused"] += 1
|
||||
if ("myproducts/mySLFO", name) not in _CREATED_LABELS:
|
||||
client.create_label("myproducts", "mySLFO", name, color=color)
|
||||
_CREATED_LABELS.add(("myproducts/mySLFO", name))
|
||||
|
||||
# Submodules in mySLFO
|
||||
client.add_submodules("myproducts", "mySLFO")
|
||||
@@ -351,51 +277,24 @@ def gitea_env():
|
||||
("myproducts/mySLFO", "workflow-pr"),
|
||||
("mypool/pkgA", "workflow-pr"),
|
||||
("mypool/pkgB", "workflow-pr")]:
|
||||
org_part, repo_part = repo_full.split("/")
|
||||
new_coll = client.add_collaborator(org_part, repo_part, bot, "write")
|
||||
_ADDED_COLLABORATORS.add((repo_full, bot))
|
||||
if (repo_full, bot) not in handled_in_session["collaborators"]:
|
||||
handled_in_session["collaborators"].add((repo_full, bot))
|
||||
if new_coll: stats["collaborators"]["new"] += 1
|
||||
else: stats["collaborators"]["reused"] += 1
|
||||
|
||||
# Collect all users from all configurations first to do setup once
|
||||
all_setup_users_wf = {}
|
||||
all_setup_users_mt = {}
|
||||
if (repo_full, bot) not in _ADDED_COLLABORATORS:
|
||||
org_part, repo_part = repo_full.split("/")
|
||||
client.add_collaborator(org_part, repo_part, bot, "write")
|
||||
_ADDED_COLLABORATORS.add((repo_full, bot))
|
||||
|
||||
restart_needed = False
|
||||
|
||||
# Setup all branches and configs
|
||||
repo_list = [("mypool", "pkgA"), ("mypool", "pkgB"), ("myproducts", "mySLFO")]
|
||||
repo_branches = {}
|
||||
for owner, repo in repo_list:
|
||||
resp, _ = client._request("GET", f"repos/{owner}/{repo}/branches")
|
||||
repo_branches[(owner, repo)] = {b["name"] for b in resp.json()}
|
||||
|
||||
for branch_name, custom_configs in BRANCH_CONFIG_CUSTOM.items():
|
||||
# Ensure branch exists in all 3 repos
|
||||
for owner, repo in repo_list:
|
||||
for owner, repo in [("myproducts", "mySLFO"), ("mypool", "pkgA"), ("mypool", "pkgB")]:
|
||||
if branch_name != "main":
|
||||
if branch_name not in repo_branches[(owner, repo)]:
|
||||
try:
|
||||
resp, _ = client._request("GET", f"repos/{owner}/{repo}/branches/main")
|
||||
main_sha = resp.json()["commit"]["id"]
|
||||
new_branch = client.create_branch(owner, repo, branch_name, main_sha)
|
||||
repo_branches[(owner, repo)].add(branch_name)
|
||||
if (f"{owner}/{repo}", branch_name) not in handled_in_session["branches"]:
|
||||
handled_in_session["branches"].add((f"{owner}/{repo}", branch_name))
|
||||
if new_branch: stats["branches"]["new"] += 1
|
||||
else: stats["branches"]["reused"] += 1
|
||||
except Exception as e:
|
||||
if "already exists" not in str(e).lower():
|
||||
raise
|
||||
else:
|
||||
if (f"{owner}/{repo}", branch_name) not in handled_in_session["branches"]:
|
||||
handled_in_session["branches"].add((f"{owner}/{repo}", branch_name))
|
||||
stats["branches"]["reused"] += 1
|
||||
else:
|
||||
# main branch always exists, but let's track it as reused if not handled
|
||||
if (f"{owner}/{repo}", "main") not in handled_in_session["branches"]:
|
||||
handled_in_session["branches"].add((f"{owner}/{repo}", "main"))
|
||||
stats["branches"]["reused"] += 1
|
||||
try:
|
||||
main_sha = client._request("GET", f"repos/{owner}/{repo}/branches/main").json()["commit"]["id"]
|
||||
client.create_branch(owner, repo, branch_name, main_sha)
|
||||
except Exception as e:
|
||||
if "already exists" not in str(e).lower():
|
||||
raise
|
||||
|
||||
# Merge configs
|
||||
merged_configs = {}
|
||||
@@ -414,40 +313,15 @@ def gitea_env():
|
||||
else:
|
||||
merged_configs[file_name] = custom_content
|
||||
|
||||
# Pre-fetch existing files in this branch to avoid 404s in ensure_config_file
|
||||
try:
|
||||
resp, _ = client._request("GET", f"repos/myproducts/mySLFO/contents?ref={branch_name}")
|
||||
existing_files = resp.json()
|
||||
except:
|
||||
existing_files = []
|
||||
|
||||
# Ensure config files in myproducts/mySLFO
|
||||
for file_name, content_dict in merged_configs.items():
|
||||
ensure_config_file(client, "myproducts", "mySLFO", branch_name, file_name, content_dict, existing_files=existing_files)
|
||||
if ensure_config_file(client, "myproducts", "mySLFO", branch_name, file_name, content_dict):
|
||||
restart_needed = True
|
||||
|
||||
# Collect configs for user setup
|
||||
wf_cfg = merged_configs.get("workflow.config", {})
|
||||
mt_cfg = merged_configs.get("_maintainership.json", {})
|
||||
# Simple merge for user collection
|
||||
if "Reviewers" in wf_cfg:
|
||||
all_setup_users_wf.setdefault("Reviewers", []).extend(wf_cfg["Reviewers"])
|
||||
for k, v in mt_cfg.items():
|
||||
all_setup_users_mt.setdefault(k, []).extend(v)
|
||||
# Setup users (using configs from this branch)
|
||||
setup_users_from_config(client, merged_configs.get("workflow.config", {}), merged_configs.get("_maintainership.json", {}))
|
||||
|
||||
# Dedup and setup users once
|
||||
if "Reviewers" in all_setup_users_wf:
|
||||
all_setup_users_wf["Reviewers"] = list(set(all_setup_users_wf["Reviewers"]))
|
||||
for k in all_setup_users_mt:
|
||||
all_setup_users_mt[k] = list(set(all_setup_users_mt[k]))
|
||||
|
||||
setup_users_from_config(client, all_setup_users_wf, all_setup_users_mt, stats=stats, handled=handled_in_session)
|
||||
|
||||
setup_duration = time.time() - setup_start_time
|
||||
print(f"--- Gitea Global Setup Complete (took {setup_duration:.2f}s) ---\n"
|
||||
f"Objects created: {stats['orgs']['new']} orgs, {stats['repos']['new']} repos, {stats['branches']['new']} branches, {stats['webhooks']['new']} webhooks, {stats['users']['new']} users, {stats['labels']['new']} labels, {stats['collaborators']['new']} collaborators\n"
|
||||
f"Objects reused: {stats['orgs']['reused']} orgs, {stats['repos']['reused']} repos, {stats['branches']['reused']} branches, {stats['webhooks']['reused']} webhooks, {stats['users']['reused']} users, {stats['labels']['reused']} labels, {stats['collaborators']['reused']} collaborators")
|
||||
|
||||
client.use_cache = False
|
||||
print("--- Gitea Global Setup Complete ---")
|
||||
yield client
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
||||
@@ -8,57 +8,30 @@ import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
import base64
|
||||
|
||||
IS_TEST_RUN = False
|
||||
|
||||
def vprint(*args, **kwargs):
|
||||
if IS_TEST_RUN or os.environ.get("AUTOGITS_PRINT_FIXTURES") == "1":
|
||||
print(*args, **kwargs)
|
||||
|
||||
class GiteaAPIClient:
|
||||
def __init__(self, base_url, token, sudo=None):
|
||||
self.base_url = base_url
|
||||
self.headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
||||
if sudo:
|
||||
self.headers["Sudo"] = sudo
|
||||
self._cache = {}
|
||||
self.use_cache = False
|
||||
|
||||
def _request(self, method, path, **kwargs):
|
||||
# Very basic cache for GET requests to speed up setup
|
||||
cache_key = (method, path, json.dumps(kwargs, sort_keys=True))
|
||||
if self.use_cache and method == "GET" and cache_key in self._cache:
|
||||
return self._cache[cache_key], 0.0
|
||||
|
||||
url = f"{self.base_url}/api/v1/{path}"
|
||||
start_time = time.time()
|
||||
response = requests.request(method, url, headers=self.headers, **kwargs)
|
||||
try:
|
||||
response = requests.request(method, url, headers=self.headers, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
response.raise_for_status()
|
||||
|
||||
if self.use_cache:
|
||||
if method == "GET":
|
||||
self._cache[cache_key] = response
|
||||
else:
|
||||
self._cache.clear()
|
||||
|
||||
return response, duration
|
||||
except requests.exceptions.HTTPError as e:
|
||||
duration = time.time() - start_time
|
||||
vprint(f"[{duration:.3f}s] HTTPError in _request: {e}")
|
||||
vprint(f"Response Content: {e.response.text}")
|
||||
raise
|
||||
except requests.exceptions.RequestException as e:
|
||||
duration = time.time() - start_time
|
||||
vprint(f"[{duration:.3f}s] Request failed: {e}")
|
||||
print(f"HTTPError in _request: {e}")
|
||||
print(f"Response Content: {e.response.text}")
|
||||
raise
|
||||
return response
|
||||
|
||||
def get_file_info(self, owner: str, repo: str, file_path: str, branch: str = "main"):
|
||||
url = f"repos/{owner}/{repo}/contents/{file_path}"
|
||||
if branch and branch != "main":
|
||||
url += f"?ref={branch}"
|
||||
try:
|
||||
response, duration = self._request("GET", url)
|
||||
response = self._request("GET", url)
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
@@ -66,7 +39,7 @@ class GiteaAPIClient:
|
||||
raise
|
||||
|
||||
def create_user(self, username, password, email):
|
||||
vprint(f"--- Creating user: {username} ---")
|
||||
print(f"--- Creating user: {username} ---")
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
@@ -75,20 +48,18 @@ class GiteaAPIClient:
|
||||
"send_notify": False
|
||||
}
|
||||
try:
|
||||
response, duration = self._request("POST", "admin/users", json=data)
|
||||
vprint(f"[{duration:.3f}s] User '{username}' created.")
|
||||
return True
|
||||
self._request("POST", "admin/users", json=data)
|
||||
print(f"User '{username}' created.")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 422: # Already exists
|
||||
vprint(f"User '{username}' already exists. Updating password...")
|
||||
print(f"User '{username}' already exists. Updating password...")
|
||||
# Update password to be sure it matches our expectation
|
||||
response, duration = self._request("PATCH", f"admin/users/{username}", json={"password": password, "login_name": username})
|
||||
return False
|
||||
self._request("PATCH", f"admin/users/{username}", json={"password": password, "login_name": username})
|
||||
else:
|
||||
raise
|
||||
|
||||
def get_user_token(self, username, password, token_name="test-token"):
|
||||
vprint(f"--- Getting token for user: {username} ---")
|
||||
print(f"--- Getting token for user: {username} ---")
|
||||
url = f"{self.base_url}/api/v1/users/{username}/tokens"
|
||||
|
||||
# Create new token using Basic Auth
|
||||
@@ -98,30 +69,27 @@ class GiteaAPIClient:
|
||||
response.raise_for_status()
|
||||
|
||||
def create_org(self, org_name):
|
||||
vprint(f"--- Checking organization: {org_name} ---")
|
||||
print(f"--- Checking organization: {org_name} ---")
|
||||
try:
|
||||
response, duration = self._request("GET", f"orgs/{org_name}")
|
||||
vprint(f"[{duration:.3f}s] Organization '{org_name}' already exists.")
|
||||
return False
|
||||
self._request("GET", f"orgs/{org_name}")
|
||||
print(f"Organization '{org_name}' already exists.")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
vprint(f"Creating organization '{org_name}'...")
|
||||
print(f"Creating organization '{org_name}'...")
|
||||
data = {"username": org_name, "full_name": org_name}
|
||||
response, duration = self._request("POST", "orgs", json=data)
|
||||
vprint(f"[{duration:.3f}s] Organization '{org_name}' created.")
|
||||
return True
|
||||
self._request("POST", "orgs", json=data)
|
||||
print(f"Organization '{org_name}' created.")
|
||||
else:
|
||||
raise
|
||||
|
||||
def create_repo(self, org_name, repo_name):
|
||||
vprint(f"--- Checking repository: {org_name}/{repo_name} ---")
|
||||
print(f"--- Checking repository: {org_name}/{repo_name} ---")
|
||||
try:
|
||||
response, duration = self._request("GET", f"repos/{org_name}/{repo_name}")
|
||||
vprint(f"[{duration:.3f}s] Repository '{org_name}/{repo_name}' already exists.")
|
||||
return False
|
||||
self._request("GET", f"repos/{org_name}/{repo_name}")
|
||||
print(f"Repository '{org_name}/{repo_name}' already exists.")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
vprint(f"Creating repository '{org_name}/{repo_name}'...")
|
||||
print(f"Creating repository '{org_name}/{repo_name}'...")
|
||||
data = {
|
||||
"name": repo_name,
|
||||
"auto_init": True,
|
||||
@@ -129,58 +97,43 @@ class GiteaAPIClient:
|
||||
"gitignores": "Go",
|
||||
"license": "MIT",
|
||||
"private": False,
|
||||
"readme": "Default",
|
||||
"object_format_name": "sha256"
|
||||
"readme": "Default"
|
||||
}
|
||||
response, duration = self._request("POST", f"orgs/{org_name}/repos", json=data)
|
||||
vprint(f"[{duration:.3f}s] Repository '{org_name}/{repo_name}' created with a README.")
|
||||
self._request("POST", f"orgs/{org_name}/repos", json=data)
|
||||
print(f"Repository '{org_name}/{repo_name}' created with a README.")
|
||||
time.sleep(0.1) # Added delay to allow Git operations to become available
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
|
||||
def add_collaborator(self, org_name, repo_name, collaborator_name, permission="write"):
|
||||
vprint(f"--- Adding {collaborator_name} as a collaborator to {org_name}/{repo_name} with '{permission}' permission ---")
|
||||
|
||||
# Check if already a collaborator to provide accurate stats
|
||||
try:
|
||||
self._request("GET", f"repos/{org_name}/{repo_name}/collaborators/{collaborator_name}")
|
||||
vprint(f"{collaborator_name} is already a collaborator of {org_name}/{repo_name}.")
|
||||
return False
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code != 404:
|
||||
raise
|
||||
|
||||
print(f"--- Adding {collaborator_name} as a collaborator to {org_name}/{repo_name} with '{permission}' permission ---")
|
||||
data = {"permission": permission}
|
||||
# Gitea API returns 204 No Content on success and doesn't fail if already present.
|
||||
response, duration = self._request("PUT", f"repos/{org_name}/{repo_name}/collaborators/{collaborator_name}", json=data)
|
||||
vprint(f"[{duration:.3f}s] Added {collaborator_name} to {org_name}/{repo_name}.")
|
||||
return True
|
||||
self._request("PUT", f"repos/{org_name}/{repo_name}/collaborators/{collaborator_name}", json=data)
|
||||
print(f"Attempted to add {collaborator_name} to {org_name}/{repo_name}.")
|
||||
|
||||
def add_submodules(self, org_name, repo_name):
|
||||
vprint(f"--- Adding submodules to {org_name}/{repo_name} using diffpatch ---")
|
||||
print(f"--- Adding submodules to {org_name}/{repo_name} using diffpatch ---")
|
||||
parent_repo_path = f"repos/{org_name}/{repo_name}"
|
||||
|
||||
try:
|
||||
response, duration = self._request("GET", f"{parent_repo_path}/contents/.gitmodules")
|
||||
vprint(f"[{duration:.3f}s] Submodules appear to be already added. Skipping.")
|
||||
self._request("GET", f"{parent_repo_path}/contents/.gitmodules")
|
||||
print("Submodules appear to be already added. Skipping.")
|
||||
return
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code != 404:
|
||||
raise
|
||||
|
||||
# Get latest commit SHAs for the submodules
|
||||
response_a, duration_a = self._request("GET", "repos/mypool/pkgA/branches/main")
|
||||
pkg_a_sha = response_a.json()["commit"]["id"]
|
||||
response_b, duration_b = self._request("GET", "repos/mypool/pkgB/branches/main")
|
||||
pkg_b_sha = response_b.json()["commit"]["id"]
|
||||
pkg_a_sha = self._request("GET", "repos/mypool/pkgA/branches/main").json()["commit"]["id"]
|
||||
pkg_b_sha = self._request("GET", "repos/mypool/pkgB/branches/main").json()["commit"]["id"]
|
||||
|
||||
if not pkg_a_sha or not pkg_b_sha:
|
||||
raise Exception("Error: Could not get submodule commit SHAs. Cannot apply patch.")
|
||||
|
||||
diff_content = f"""diff --git a/.gitmodules b/.gitmodules
|
||||
new file mode 100644
|
||||
index 00000000..f1838bd9
|
||||
index 0000000..f1838bd
|
||||
--- /dev/null
|
||||
+++ b/.gitmodules
|
||||
@@ -0,0 +1,6 @@
|
||||
@@ -192,14 +145,14 @@ index 00000000..f1838bd9
|
||||
+ url = ../../mypool/pkgB.git
|
||||
diff --git a/pkgA b/pkgA
|
||||
new file mode 160000
|
||||
index 00000000..{pkg_a_sha}
|
||||
index 0000000..{pkg_a_sha}
|
||||
--- /dev/null
|
||||
+++ b/pkgA
|
||||
@@ -0,0 +1 @@
|
||||
+Subproject commit {pkg_a_sha}
|
||||
diff --git a/pkgB b/pkgB
|
||||
new file mode 160000
|
||||
index 00000000..{pkg_b_sha}
|
||||
index 0000000..{pkg_b_sha}
|
||||
--- /dev/null
|
||||
+++ b/pkgB
|
||||
@@ -0,0 +1 @@
|
||||
@@ -211,81 +164,34 @@ index 00000000..{pkg_b_sha}
|
||||
"content": diff_content,
|
||||
"message": message
|
||||
}
|
||||
vprint(f"Applying submodule patch to {org_name}/{repo_name}...")
|
||||
response, duration = self._request("POST", f"{parent_repo_path}/diffpatch", json=data)
|
||||
vprint(f"[{duration:.3f}s] Submodule patch applied.")
|
||||
print(f"Applying submodule patch to {org_name}/{repo_name}...")
|
||||
self._request("POST", f"{parent_repo_path}/diffpatch", json=data)
|
||||
print("Submodule patch applied.")
|
||||
|
||||
def update_repo_settings(self, org_name, repo_name):
|
||||
vprint(f"--- Updating repository settings for: {org_name}/{repo_name} ---")
|
||||
response, duration = self._request("GET", f"repos/{org_name}/{repo_name}")
|
||||
repo_data = response.json()
|
||||
print(f"--- Updating repository settings for: {org_name}/{repo_name} ---")
|
||||
repo_data = self._request("GET", f"repos/{org_name}/{repo_name}").json()
|
||||
|
||||
# Ensure these are boolean values, not string
|
||||
repo_data["allow_manual_merge"] = True
|
||||
repo_data["autodetect_manual_merge"] = True
|
||||
|
||||
response, duration = self._request("PATCH", f"repos/{org_name}/{repo_name}", json=repo_data)
|
||||
vprint(f"[{duration:.3f}s] Repository settings for '{org_name}/{repo_name}' updated.")
|
||||
|
||||
def create_webhook(self, owner: str, repo: str, target_url: str):
|
||||
vprint(f"--- Checking webhook for {owner}/{repo} -> {target_url} ---")
|
||||
url = f"repos/{owner}/{repo}/hooks"
|
||||
|
||||
try:
|
||||
response, duration = self._request("GET", url)
|
||||
hooks = response.json()
|
||||
for hook in hooks:
|
||||
if hook["config"]["url"] == target_url:
|
||||
vprint(f"Webhook for {owner}/{repo} already exists with correct URL.")
|
||||
return False
|
||||
elif "gitea-publisher" in hook["config"]["url"] or "10.89.0." in hook["config"]["url"]:
|
||||
vprint(f"Found old webhook {hook['id']} with URL {hook['config']['url']}. Deleting...")
|
||||
self._request("DELETE", f"{url}/{hook['id']}")
|
||||
except requests.exceptions.HTTPError:
|
||||
pass
|
||||
|
||||
vprint(f"--- Creating webhook for {owner}/{repo} -> {target_url} ---")
|
||||
data = {
|
||||
"type": "gitea",
|
||||
"config": {
|
||||
"url": target_url,
|
||||
"content_type": "json"
|
||||
},
|
||||
"events": ["push", "pull_request", "pull_request_review", "issue_comment"],
|
||||
"active": True
|
||||
}
|
||||
response, duration = self._request("POST", url, json=data)
|
||||
vprint(f"[{duration:.3f}s] Webhook created for {owner}/{repo}.")
|
||||
return True
|
||||
self._request("PATCH", f"repos/{org_name}/{repo_name}", json=repo_data)
|
||||
print(f"Repository settings for '{org_name}/{repo_name}' updated.")
|
||||
|
||||
def create_label(self, owner: str, repo: str, name: str, color: str = "#abcdef"):
|
||||
vprint(f"--- Checking label '{name}' in {owner}/{repo} ---")
|
||||
print(f"--- Creating label '{name}' in {owner}/{repo} ---")
|
||||
url = f"repos/{owner}/{repo}/labels"
|
||||
|
||||
# Check if label exists first
|
||||
try:
|
||||
response, duration = self._request("GET", url)
|
||||
labels = response.json()
|
||||
for label in labels:
|
||||
if label["name"] == name:
|
||||
vprint(f"Label '{name}' already exists in {owner}/{repo}.")
|
||||
return False
|
||||
except requests.exceptions.HTTPError:
|
||||
pass
|
||||
|
||||
vprint(f"--- Creating label '{name}' in {owner}/{repo} ---")
|
||||
data = {
|
||||
"name": name,
|
||||
"color": color
|
||||
}
|
||||
try:
|
||||
response, duration = self._request("POST", url, json=data)
|
||||
vprint(f"[{duration:.3f}s] Label '{name}' created.")
|
||||
return True
|
||||
self._request("POST", url, json=data)
|
||||
print(f"Label '{name}' created.")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 422: # Already exists (race condition or other reason)
|
||||
vprint(f"Label '{name}' already exists.")
|
||||
return False
|
||||
if e.response.status_code == 422: # Already exists
|
||||
print(f"Label '{name}' already exists.")
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -299,7 +205,7 @@ index 00000000..{pkg_b_sha}
|
||||
}
|
||||
|
||||
if file_info:
|
||||
vprint(f"--- Updating file {file_path} in {owner}/{repo} ---")
|
||||
print(f"--- Updating file {file_path} in {owner}/{repo} ---")
|
||||
# Re-fetch file_info to get the latest SHA right before update
|
||||
latest_file_info = self.get_file_info(owner, repo, file_path, branch=branch)
|
||||
if not latest_file_info:
|
||||
@@ -308,12 +214,12 @@ index 00000000..{pkg_b_sha}
|
||||
data["message"] = f"Update {file_path}"
|
||||
method = "PUT"
|
||||
else:
|
||||
vprint(f"--- Creating file {file_path} in {owner}/{repo} ---")
|
||||
print(f"--- Creating file {file_path} in {owner}/{repo} ---")
|
||||
method = "POST"
|
||||
|
||||
url = f"repos/{owner}/{repo}/contents/{file_path}"
|
||||
response, duration = self._request(method, url, json=data)
|
||||
vprint(f"[{duration:.3f}s] File {file_path} {'updated' if file_info else 'created'} in {owner}/{repo}.")
|
||||
self._request(method, url, json=data)
|
||||
print(f"File {file_path} {'updated' if file_info else 'created'} in {owner}/{repo}.")
|
||||
|
||||
def create_gitea_pr(self, repo_full_name: str, diff_content: str, title: str, use_fork: bool, base_branch: str = "main", body: str = ""):
|
||||
owner, repo = repo_full_name.split("/")
|
||||
@@ -326,20 +232,20 @@ index 00000000..{pkg_b_sha}
|
||||
head_owner = sudo_user
|
||||
head_repo = repo
|
||||
|
||||
vprint(f"--- Forking {repo_full_name} ---")
|
||||
print(f"--- Forking {repo_full_name} ---")
|
||||
try:
|
||||
response, duration = self._request("POST", f"repos/{owner}/{repo}/forks", json={})
|
||||
vprint(f"[{duration:.3f}s] --- Forked to {head_owner}/{head_repo} ---")
|
||||
self._request("POST", f"repos/{owner}/{repo}/forks", json={})
|
||||
print(f"--- Forked to {head_owner}/{head_repo} ---")
|
||||
time.sleep(0.5) # Give more time for fork to be ready
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 409: # Already forked
|
||||
vprint(f"--- Already forked to {head_owner}/{head_repo} ---")
|
||||
print(f"--- Already forked to {head_owner}/{head_repo} ---")
|
||||
else:
|
||||
raise
|
||||
|
||||
# Apply the diff using diffpatch and create the new branch automatically
|
||||
vprint(f"--- Applying diff to {head_owner}/{head_repo} from {base_branch} to new branch {new_branch_name} ---")
|
||||
response, duration = self._request("POST", f"repos/{head_owner}/{head_repo}/diffpatch", json={
|
||||
print(f"--- Applying diff to {head_owner}/{head_repo} from {base_branch} to new branch {new_branch_name} ---")
|
||||
self._request("POST", f"repos/{head_owner}/{head_repo}/diffpatch", json={
|
||||
"branch": base_branch,
|
||||
"new_branch": new_branch_name,
|
||||
"content": diff_content,
|
||||
@@ -354,41 +260,40 @@ index 00000000..{pkg_b_sha}
|
||||
"body": body,
|
||||
"allow_maintainer_edit": True
|
||||
}
|
||||
vprint(f"--- Creating PR in {repo_full_name} from {data['head']} ---")
|
||||
response, duration = self._request("POST", f"repos/{owner}/{repo}/pulls", json=data)
|
||||
print(f"--- Creating PR in {repo_full_name} from {data['head']} ---")
|
||||
response = self._request("POST", f"repos/{owner}/{repo}/pulls", json=data)
|
||||
return response.json()
|
||||
|
||||
def create_branch(self, owner: str, repo: str, new_branch_name: str, old_ref: str):
|
||||
vprint(f"--- Checking branch '{new_branch_name}' in {owner}/{repo} ---")
|
||||
print(f"--- Checking branch '{new_branch_name}' in {owner}/{repo} ---")
|
||||
try:
|
||||
response, duration = self._request("GET", f"repos/{owner}/{repo}/branches/{new_branch_name}")
|
||||
vprint(f"[{duration:.3f}s] Branch '{new_branch_name}' already exists.")
|
||||
return False
|
||||
self._request("GET", f"repos/{owner}/{repo}/branches/{new_branch_name}")
|
||||
print(f"Branch '{new_branch_name}' already exists.")
|
||||
return
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code != 404:
|
||||
raise # Re-raise other HTTP errors
|
||||
|
||||
vprint(f"--- Creating branch '{new_branch_name}' in {owner}/{repo} from {old_ref} ---")
|
||||
print(f"--- Creating branch '{new_branch_name}' in {owner}/{repo} from {old_ref} ---")
|
||||
url = f"repos/{owner}/{repo}/branches"
|
||||
data = {
|
||||
"new_branch_name": new_branch_name,
|
||||
"old_ref": old_ref
|
||||
}
|
||||
response, duration = self._request("POST", url, json=data)
|
||||
vprint(f"[{duration:.3f}s] Branch '{new_branch_name}' created in {owner}/{repo}.")
|
||||
return True
|
||||
self._request("POST", url, json=data)
|
||||
print(f"Branch '{new_branch_name}' created in {owner}/{repo}.")
|
||||
|
||||
def ensure_branch_exists(self, owner: str, repo: str, branch: str = "main", timeout: int = 10):
|
||||
vprint(f"--- Ensuring branch '{branch}' exists in {owner}/{repo} ---")
|
||||
print(f"--- Ensuring branch '{branch}' exists in {owner}/{repo} ---")
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
response, duration = self._request("GET", f"repos/{owner}/{repo}/branches/{branch}")
|
||||
vprint(f"[{duration:.3f}s] Branch '{branch}' confirmed in {owner}/{repo}.")
|
||||
self._request("GET", f"repos/{owner}/{repo}/branches/{branch}")
|
||||
print(f"Branch '{branch}' confirmed in {owner}/{repo}.")
|
||||
return
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
vprint(f"Branch '{branch}' not found yet in {owner}/{repo}. Retrying...")
|
||||
print(f"Branch '{branch}' not found yet in {owner}/{repo}. Retrying...")
|
||||
time.sleep(1)
|
||||
continue
|
||||
raise
|
||||
@@ -398,15 +303,14 @@ index 00000000..{pkg_b_sha}
|
||||
owner, repo = repo_full_name.split("/")
|
||||
|
||||
# Get PR details to find the head branch AND head repo
|
||||
response, duration = self._request("GET", f"repos/{owner}/{repo}/pulls/{pr_number}")
|
||||
pr_details = response.json()
|
||||
pr_details = self._request("GET", f"repos/{owner}/{repo}/pulls/{pr_number}").json()
|
||||
head_branch = pr_details["head"]["ref"]
|
||||
head_repo_owner = pr_details["head"]["repo"]["owner"]["login"]
|
||||
head_repo_name = pr_details["head"]["repo"]["name"]
|
||||
|
||||
# Apply the diff using diffpatch
|
||||
vprint(f"--- Modifying PR #{pr_number} in {head_repo_owner}/{head_repo_name} branch {head_branch} ---")
|
||||
response, duration = self._request("POST", f"repos/{head_repo_owner}/{head_repo_name}/diffpatch", json={
|
||||
print(f"--- Modifying PR #{pr_number} in {head_repo_owner}/{head_repo_name} branch {head_branch} ---")
|
||||
self._request("POST", f"repos/{head_repo_owner}/{head_repo_name}/diffpatch", json={
|
||||
"branch": head_branch,
|
||||
"content": diff_content,
|
||||
"message": message
|
||||
@@ -415,15 +319,15 @@ index 00000000..{pkg_b_sha}
|
||||
def update_gitea_pr_properties(self, repo_full_name: str, pr_number: int, **kwargs):
|
||||
owner, repo = repo_full_name.split("/")
|
||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}"
|
||||
response, duration = self._request("PATCH", url, json=kwargs)
|
||||
response = self._request("PATCH", url, json=kwargs)
|
||||
return response.json()
|
||||
|
||||
def create_issue_comment(self, repo_full_name: str, issue_number: int, body: str):
|
||||
owner, repo = repo_full_name.split("/")
|
||||
url = f"repos/{owner}/{repo}/issues/{issue_number}/comments"
|
||||
data = {"body": body}
|
||||
vprint(f"--- Creating comment on {repo_full_name} issue #{issue_number} ---")
|
||||
response, duration = self._request("POST", url, json=data)
|
||||
print(f"--- Creating comment on {repo_full_name} issue #{issue_number} ---")
|
||||
response = self._request("POST", url, json=data)
|
||||
return response.json()
|
||||
|
||||
def get_timeline_events(self, repo_full_name: str, pr_number: int):
|
||||
@@ -433,15 +337,15 @@ index 00000000..{pkg_b_sha}
|
||||
# Retry logic for timeline events
|
||||
for i in range(10): # Try up to 10 times
|
||||
try:
|
||||
response, duration = self._request("GET", url)
|
||||
response = self._request("GET", url)
|
||||
timeline_events = response.json()
|
||||
if timeline_events: # Check if timeline_events list is not empty
|
||||
return timeline_events
|
||||
vprint(f"Attempt {i+1}: Timeline for PR {pr_number} is empty. Retrying in 1 seconds...")
|
||||
print(f"Attempt {i+1}: Timeline for PR {pr_number} is empty. Retrying in 1 seconds...")
|
||||
time.sleep(1)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
vprint(f"Attempt {i+1}: Timeline for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
||||
print(f"Attempt {i+1}: Timeline for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise # Re-raise other HTTP errors
|
||||
@@ -454,16 +358,16 @@ index 00000000..{pkg_b_sha}
|
||||
# Retry logic for comments
|
||||
for i in range(10): # Try up to 10 times
|
||||
try:
|
||||
response, duration = self._request("GET", url)
|
||||
response = self._request("GET", url)
|
||||
comments = response.json()
|
||||
vprint(f"[{duration:.3f}s] Attempt {i+1}: Comments for PR {pr_number} received: {comments}") # Added debug print
|
||||
print(f"Attempt {i+1}: Comments for PR {pr_number} received: {comments}") # Added debug print
|
||||
if comments: # Check if comments list is not empty
|
||||
return comments
|
||||
vprint(f"Attempt {i+1}: Comments for PR {pr_number} are empty. Retrying in 1 seconds...")
|
||||
print(f"Attempt {i+1}: Comments for PR {pr_number} are empty. Retrying in 1 seconds...")
|
||||
time.sleep(1)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
vprint(f"Attempt {i+1}: Comments for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
||||
print(f"Attempt {i+1}: Comments for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise # Re-raise other HTTP errors
|
||||
@@ -472,7 +376,7 @@ index 00000000..{pkg_b_sha}
|
||||
def get_pr_details(self, repo_full_name: str, pr_number: int):
|
||||
owner, repo = repo_full_name.split("/")
|
||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}"
|
||||
response, duration = self._request("GET", url)
|
||||
response = self._request("GET", url)
|
||||
return response.json()
|
||||
|
||||
def create_review(self, repo_full_name: str, pr_number: int, event: str = "APPROVED", body: str = "LGTM"):
|
||||
@@ -483,7 +387,7 @@ index 00000000..{pkg_b_sha}
|
||||
existing_reviews = self.list_reviews(repo_full_name, pr_number)
|
||||
for r in existing_reviews:
|
||||
if r["user"]["login"] == current_user and r["state"] == "APPROVED" and event == "APPROVED":
|
||||
vprint(f"User {current_user} already has an APPROVED review for {repo_full_name} PR #{pr_number}")
|
||||
print(f"User {current_user} already has an APPROVED review for {repo_full_name} PR #{pr_number}")
|
||||
return r
|
||||
|
||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||
@@ -491,13 +395,13 @@ index 00000000..{pkg_b_sha}
|
||||
"event": event,
|
||||
"body": body
|
||||
}
|
||||
vprint(f"--- Creating and submitting review ({event}) for {repo_full_name} PR #{pr_number} as {current_user} ---")
|
||||
print(f"--- Creating and submitting review ({event}) for {repo_full_name} PR #{pr_number} as {current_user} ---")
|
||||
try:
|
||||
response, duration = self._request("POST", url, json=data)
|
||||
response = self._request("POST", url, json=data)
|
||||
review = response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
# If it fails with 422, it might be because a review is already pending or something else
|
||||
vprint(f"Failed to create review: {e.response.text}")
|
||||
print(f"Failed to create review: {e.response.text}")
|
||||
# Try to find a pending review to submit
|
||||
existing_reviews = self.list_reviews(repo_full_name, pr_number)
|
||||
pending_review = next((r for r in existing_reviews if r["user"]["login"] == current_user and r["state"] == "PENDING"), None)
|
||||
@@ -515,11 +419,11 @@ index 00000000..{pkg_b_sha}
|
||||
"body": body
|
||||
}
|
||||
try:
|
||||
response, duration = self._request("POST", submit_url, json=submit_data)
|
||||
vprint(f"[{duration:.3f}s] --- Review {review_id} submitted ---")
|
||||
self._request("POST", submit_url, json=submit_data)
|
||||
print(f"--- Review {review_id} submitted ---")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if "already" in e.response.text.lower() or "stay pending" in e.response.text.lower():
|
||||
vprint(f"Review {review_id} could not be submitted further: {e.response.text}")
|
||||
print(f"Review {review_id} could not be submitted further: {e.response.text}")
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -528,29 +432,29 @@ index 00000000..{pkg_b_sha}
|
||||
def list_reviews(self, repo_full_name: str, pr_number: int):
|
||||
owner, repo = repo_full_name.split("/")
|
||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||
response, duration = self._request("GET", url)
|
||||
response = self._request("GET", url)
|
||||
return response.json()
|
||||
|
||||
def approve_requested_reviews(self, repo_full_name: str, pr_number: int):
|
||||
vprint(f"--- Checking for REQUEST_REVIEW state in {repo_full_name} PR #{pr_number} ---")
|
||||
print(f"--- Checking for REQUEST_REVIEW state in {repo_full_name} PR #{pr_number} ---")
|
||||
reviews = self.list_reviews(repo_full_name, pr_number)
|
||||
|
||||
requested_reviews = [r for r in reviews if r["state"] == "REQUEST_REVIEW"]
|
||||
if not requested_reviews:
|
||||
vprint(f"No reviews in REQUEST_REVIEW state found for {repo_full_name} PR #{pr_number}")
|
||||
print(f"No reviews in REQUEST_REVIEW state found for {repo_full_name} PR #{pr_number}")
|
||||
return
|
||||
|
||||
admin_token = self.headers["Authorization"].split(" ")[1]
|
||||
for r in requested_reviews:
|
||||
reviewer_username = r["user"]["login"]
|
||||
vprint(f"Reacting on REQUEST_REVIEW for user {reviewer_username} by approving...")
|
||||
print(f"Reacting on REQUEST_REVIEW for user {reviewer_username} by approving...")
|
||||
|
||||
reviewer_client = GiteaAPIClient(base_url=self.base_url, token=admin_token, sudo=reviewer_username)
|
||||
time.sleep(1) # give a chance to avoid possible concurrency issues with reviews request/approval
|
||||
reviewer_client.create_review(repo_full_name, pr_number, event="APPROVED", body="Approving requested review")
|
||||
|
||||
def wait_for_project_pr(self, package_pr_repo, package_pr_number, project_pr_repo="myproducts/mySLFO", timeout=60):
|
||||
vprint(f"Polling {package_pr_repo} PR #{package_pr_number} timeline for forwarded PR event in {project_pr_repo}...")
|
||||
print(f"Polling {package_pr_repo} PR #{package_pr_number} timeline for forwarded PR event in {project_pr_repo}...")
|
||||
for _ in range(timeout):
|
||||
time.sleep(1)
|
||||
timeline_events = self.get_timeline_events(package_pr_repo, package_pr_number)
|
||||
@@ -565,7 +469,7 @@ index 00000000..{pkg_b_sha}
|
||||
return None
|
||||
|
||||
def approve_and_wait_merge(self, package_pr_repo, package_pr_number, project_pr_number, project_pr_repo="myproducts/mySLFO", timeout=30):
|
||||
vprint(f"Approving reviews and verifying both PRs are merged ({package_pr_repo}#{package_pr_number} and {project_pr_repo}#{project_pr_number})...")
|
||||
print(f"Approving reviews and verifying both PRs are merged ({package_pr_repo}#{package_pr_number} and {project_pr_repo}#{project_pr_number})...")
|
||||
package_merged = False
|
||||
project_merged = False
|
||||
|
||||
@@ -577,13 +481,13 @@ index 00000000..{pkg_b_sha}
|
||||
pkg_details = self.get_pr_details(package_pr_repo, package_pr_number)
|
||||
if pkg_details.get("merged"):
|
||||
package_merged = True
|
||||
vprint(f"Package PR {package_pr_repo}#{package_pr_number} merged.")
|
||||
print(f"Package PR {package_pr_repo}#{package_pr_number} merged.")
|
||||
|
||||
if not project_merged:
|
||||
prj_details = self.get_pr_details(project_pr_repo, project_pr_number)
|
||||
if prj_details.get("merged"):
|
||||
project_merged = True
|
||||
vprint(f"Project PR {project_pr_repo}#{project_pr_number} merged.")
|
||||
print(f"Project PR {project_pr_repo}#{project_pr_number} merged.")
|
||||
|
||||
if package_merged and project_merged:
|
||||
return True, True
|
||||
|
||||
@@ -14,7 +14,7 @@ from tests.lib.common_test_utils import (
|
||||
def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
||||
"""End-to-end test for a successful PR workflow."""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
||||
diff = "diff --git a/test.txt b/test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||
diff = "diff --git a/test.txt b/test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
||||
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should succeed", False, base_branch=merge_branch_name)
|
||||
initial_pr_number = pr["number"]
|
||||
|
||||
@@ -59,7 +59,7 @@ def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
||||
def test_pr_workflow_failed(staging_main_env, mock_build_result):
|
||||
"""End-to-end test for a failed PR workflow."""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
||||
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should fail", False, base_branch=merge_branch_name)
|
||||
initial_pr_number = pr["number"]
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_001_project_pr_labels(label_env, staging_bot_client):
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/label_test_fixture.txt b/label_test_fixture.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} ---")
|
||||
package_pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test Labels Fixture", False, base_branch=branch_name)
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_001_automerge(automerge_env, test_user_client):
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/automerge_test.txt b/automerge_test.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Automerge Fixture", False, base_branch=merge_branch_name)
|
||||
@@ -36,7 +36,7 @@ index 00000000..473a0f4c
|
||||
print("Both PRs merged successfully.")
|
||||
|
||||
@pytest.mark.t002
|
||||
def test_002_manual_merge(manual_merge_env, test_user_client, usera_client, staging_bot_client, ownerA_client):
|
||||
def test_002_manual_merge(manual_merge_env, test_user_client, usera_client, staging_bot_client):
|
||||
"""
|
||||
Test scenario TC-MERGE-002:
|
||||
1. Create a PackageGit PR with ManualMergeOnly set to true.
|
||||
@@ -49,10 +49,10 @@ def test_002_manual_merge(manual_merge_env, test_user_client, usera_client, stag
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/manual_merge_test.txt b/manual_merge_test.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||
package_pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
||||
package_pr_number = package_pr["number"]
|
||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||
|
||||
@@ -62,14 +62,13 @@ index 00000000..473a0f4c
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
# 3. Approve reviews and verify NOT merged
|
||||
print("Waiting for required review requests and approving them...")
|
||||
print("Waiting for all expected review requests and approving them...")
|
||||
# Expected reviewers based on manual-merge branch config and pkgA maintainership
|
||||
mandatory_reviewers = {"usera", "userb"}
|
||||
maintainers = {"ownerA", "ownerX", "ownerY"}
|
||||
expected_reviewers = {"usera", "userb", "ownerA", "ownerX", "ownerY"}
|
||||
|
||||
# ManualMergeOnly still requires regular reviews to be satisfied.
|
||||
# We poll until required reviewers have approved.
|
||||
all_approved = False
|
||||
# We poll until all expected reviewers are requested, then approve them.
|
||||
all_requested = False
|
||||
for _ in range(30):
|
||||
# Trigger approvals for whatever is already requested
|
||||
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
||||
@@ -81,17 +80,20 @@ index 00000000..473a0f4c
|
||||
print("Staging bot has a pending/requested review. Approving...")
|
||||
staging_bot_client.create_review("myproducts/mySLFO", project_pr_number, event="APPROVED", body="Staging bot approves")
|
||||
|
||||
# Check if mandatory reviewers and at least one maintainer have approved
|
||||
# Check if all expected reviewers have at least one review record (any state)
|
||||
pkg_reviews = gitea_env.list_reviews("mypool/pkgA", package_pr_number)
|
||||
approved_reviewers = {r["user"]["login"] for r in pkg_reviews if r["state"] == "APPROVED"}
|
||||
current_reviewers = {r["user"]["login"] for r in pkg_reviews}
|
||||
|
||||
if mandatory_reviewers.issubset(approved_reviewers) and any(m in approved_reviewers for m in maintainers):
|
||||
# And check project PR for bot approval
|
||||
prj_approved = any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] == "APPROVED" for r in prj_reviews)
|
||||
if prj_approved:
|
||||
all_approved = True
|
||||
print(f"Required reviewers approved: mandatory={mandatory_reviewers}, maintainer={[m for m in maintainers if m in approved_reviewers]}, staging_bot=True")
|
||||
break
|
||||
if expected_reviewers.issubset(current_reviewers):
|
||||
# Also ensure they are all approved (not just requested)
|
||||
approved_reviewers = {r["user"]["login"] for r in pkg_reviews if r["state"] == "APPROVED"}
|
||||
if expected_reviewers.issubset(approved_reviewers):
|
||||
# And check project PR for bot approval
|
||||
prj_approved = any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] == "APPROVED" for r in prj_reviews)
|
||||
if prj_approved:
|
||||
all_requested = True
|
||||
print(f"All expected reviewers {expected_reviewers} and staging bot have approved.")
|
||||
break
|
||||
|
||||
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
||||
@@ -101,12 +103,12 @@ index 00000000..473a0f4c
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
assert all_approved, f"Timed out waiting for required approvals. Mandatory: {mandatory_reviewers}, Maintainers: {maintainers}. Current approved: {approved_reviewers}"
|
||||
assert all_requested, f"Timed out waiting for all expected reviewers {expected_reviewers} to approve. Current: {current_reviewers}"
|
||||
print("Both PRs have all required approvals but are not merged (as expected with ManualMergeOnly).")
|
||||
|
||||
# 4. Comment "merge ok" from a requested reviewer (ownerA)
|
||||
print("Commenting 'merge ok' on package PR from a maintainer...")
|
||||
ownerA_client.create_issue_comment("mypool/pkgA", package_pr_number, "merge ok")
|
||||
# 4. Comment "merge ok" from a requested reviewer (usera)
|
||||
print("Commenting 'merge ok' on package PR...")
|
||||
usera_client.create_issue_comment("mypool/pkgA", package_pr_number, "merge ok")
|
||||
|
||||
# 5. Verify both PRs are merged
|
||||
print("Polling for PR merge status...")
|
||||
@@ -149,7 +151,7 @@ def test_003_refuse_manual_merge(manual_merge_env, test_user_client, ownerB_clie
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/manual_merge_test.txt b/manual_merge_test.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
||||
@@ -162,14 +164,13 @@ index 00000000..473a0f4c
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
# 3. Approve reviews and verify NOT merged
|
||||
print("Waiting for required review requests and approving them...")
|
||||
print("Waiting for all expected review requests and approving them...")
|
||||
# Expected reviewers based on manual-merge branch config and pkgA maintainership
|
||||
mandatory_reviewers = {"usera", "userb"}
|
||||
maintainers = {"ownerA", "ownerX", "ownerY"}
|
||||
expected_reviewers = {"usera", "userb", "ownerA", "ownerX", "ownerY"}
|
||||
|
||||
# ManualMergeOnly still requires regular reviews to be satisfied.
|
||||
# We poll until required reviewers have approved.
|
||||
all_approved = False
|
||||
# We poll until all expected reviewers are requested, then approve them.
|
||||
all_requested = False
|
||||
for _ in range(30):
|
||||
# Trigger approvals for whatever is already requested
|
||||
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
||||
@@ -181,17 +182,20 @@ index 00000000..473a0f4c
|
||||
print("Staging bot has a pending/requested review. Approving...")
|
||||
staging_bot_client.create_review("myproducts/mySLFO", project_pr_number, event="APPROVED", body="Staging bot approves")
|
||||
|
||||
# Check if mandatory reviewers and at least one maintainer have approved
|
||||
# Check if all expected reviewers have at least one review record (any state)
|
||||
pkg_reviews = gitea_env.list_reviews("mypool/pkgA", package_pr_number)
|
||||
approved_reviewers = {r["user"]["login"] for r in pkg_reviews if r["state"] == "APPROVED"}
|
||||
current_reviewers = {r["user"]["login"] for r in pkg_reviews}
|
||||
|
||||
if mandatory_reviewers.issubset(approved_reviewers) and any(m in approved_reviewers for m in maintainers):
|
||||
# And check project PR for bot approval
|
||||
prj_approved = any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] == "APPROVED" for r in prj_reviews)
|
||||
if prj_approved:
|
||||
all_approved = True
|
||||
print(f"Required reviewers approved: mandatory={mandatory_reviewers}, maintainer={[m for m in maintainers if m in approved_reviewers]}, staging_bot=True")
|
||||
break
|
||||
if expected_reviewers.issubset(current_reviewers):
|
||||
# Also ensure they are all approved (not just requested)
|
||||
approved_reviewers = {r["user"]["login"] for r in pkg_reviews if r["state"] == "APPROVED"}
|
||||
if expected_reviewers.issubset(approved_reviewers):
|
||||
# And check project PR for bot approval
|
||||
prj_approved = any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] == "APPROVED" for r in prj_reviews)
|
||||
if prj_approved:
|
||||
all_requested = True
|
||||
print(f"All expected reviewers {expected_reviewers} and staging bot have approved.")
|
||||
break
|
||||
|
||||
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
||||
@@ -201,7 +205,7 @@ index 00000000..473a0f4c
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
assert all_approved, f"Timed out waiting for required approvals. Mandatory: {mandatory_reviewers}, Maintainers: {maintainers}. Current approved: {approved_reviewers}"
|
||||
assert all_requested, f"Timed out waiting for all expected reviewers {expected_reviewers} to approve. Current: {current_reviewers}"
|
||||
print("Both PRs have all required approvals but are not merged (as expected with ManualMergeOnly).")
|
||||
|
||||
# 4. Comment "merge ok" from a requested reviewer (ownerB)
|
||||
@@ -245,7 +249,7 @@ def test_008_merge_mode_ff_only_success(merge_ff_env, test_user_client):
|
||||
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||
diff = """diff --git a/ff_test.txt b/ff_test.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test FF Merge", False, base_branch=merge_branch_name)
|
||||
package_pr_number = package_pr["number"]
|
||||
@@ -269,7 +273,7 @@ def test_009_merge_mode_ff_only_failure(merge_ff_env, ownerA_client):
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -291,7 +295,7 @@ index 00000000..473a0f4c
|
||||
|
||||
print("Pushing another change to PR branch to trigger sync...")
|
||||
gitea_env.modify_gitea_pr("mypool/pkgA", package_pr_number,
|
||||
"diff --git a/sync_test.txt b/sync_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n",
|
||||
"diff --git a/sync_test.txt b/sync_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n",
|
||||
"Trigger Sync")
|
||||
|
||||
# The bot should detect it's not FF and NOT merge, and re-request reviews because of the new commit
|
||||
@@ -323,7 +327,7 @@ def test_010_merge_mode_devel_success(merge_devel_env, ownerA_client):
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -365,7 +369,7 @@ def test_011_merge_mode_replace_success(merge_replace_env, ownerA_client):
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -394,12 +398,10 @@ index 00000000..473a0f4c
|
||||
print("Replace merge successful.")
|
||||
|
||||
# Verify that the project branch HEAD is a merge commit
|
||||
resp, _ = gitea_env._request("GET", f"repos/myproducts/mySLFO/branches/{merge_branch_name}")
|
||||
branch_info = resp.json()
|
||||
branch_info = gitea_env._request("GET", f"repos/myproducts/mySLFO/branches/{merge_branch_name}").json()
|
||||
new_head_sha = branch_info["commit"]["id"]
|
||||
|
||||
resp, _ = gitea_env._request("GET", f"repos/myproducts/mySLFO/git/commits/{new_head_sha}")
|
||||
commit_details = resp.json()
|
||||
commit_details = gitea_env._request("GET", f"repos/myproducts/mySLFO/git/commits/{new_head_sha}").json()
|
||||
assert len(commit_details["parents"]) > 1, f"Project branch {merge_branch_name} HEAD should be a merge commit but has {len(commit_details['parents'])} parents"
|
||||
|
||||
# Verify that pkgA submodule points to the correct SHA
|
||||
@@ -419,7 +421,7 @@ def test_012_merge_mode_devel_ff_success(merge_devel_env, ownerA_client):
|
||||
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -439,13 +441,11 @@ index 00000000..473a0f4c
|
||||
print("Devel FF merge successful.")
|
||||
|
||||
# Verify that the package base branch HEAD is the same as the PR head (FF)
|
||||
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/branches/{merge_branch_name}")
|
||||
branch_info = resp.json()
|
||||
branch_info = gitea_env._request("GET", f"repos/mypool/pkgA/branches/{merge_branch_name}").json()
|
||||
new_head_sha = branch_info["commit"]["id"]
|
||||
assert new_head_sha == pkg_head_sha, f"Package branch {merge_branch_name} HEAD should be {pkg_head_sha} but is {new_head_sha}"
|
||||
|
||||
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/git/commits/{new_head_sha}")
|
||||
commit_details = resp.json()
|
||||
commit_details = gitea_env._request("GET", f"repos/mypool/pkgA/git/commits/{new_head_sha}").json()
|
||||
assert len(commit_details["parents"]) == 1, f"Package branch {merge_branch_name} HEAD should have 1 parent but has {len(commit_details['parents'])}"
|
||||
|
||||
@pytest.mark.t013
|
||||
@@ -461,7 +461,7 @@ def test_013_merge_mode_replace_ff_success(merge_replace_env, ownerA_client):
|
||||
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -481,12 +481,10 @@ index 00000000..473a0f4c
|
||||
print("Replace FF merge successful.")
|
||||
|
||||
# Verify that the package base branch HEAD is the same as the PR head (FF)
|
||||
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/branches/{merge_branch_name}")
|
||||
branch_info = resp.json()
|
||||
branch_info = gitea_env._request("GET", f"repos/mypool/pkgA/branches/{merge_branch_name}").json()
|
||||
new_head_sha = branch_info["commit"]["id"]
|
||||
assert new_head_sha == pkg_head_sha, f"Package branch {merge_branch_name} HEAD should be {pkg_head_sha} but is {new_head_sha}"
|
||||
|
||||
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/git/commits/{new_head_sha}")
|
||||
commit_details = resp.json()
|
||||
commit_details = gitea_env._request("GET", f"repos/mypool/pkgA/git/commits/{new_head_sha}").json()
|
||||
assert len(commit_details["parents"]) == 1, f"Package branch {merge_branch_name} HEAD should have 1 parent but has {len(commit_details['parents'])}"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_001_review_requests_matching_config(automerge_env, ownerA_client):
|
||||
filename = f"pkgB_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Requests Config", True, base_branch=branch_name)
|
||||
@@ -86,7 +86,7 @@ def test_004_maintainer(maintainer_env, ownerA_client):
|
||||
filename = f"maintainer_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Maintainer Merge", True, base_branch=branch_name)
|
||||
@@ -155,7 +155,7 @@ def test_005_any_maintainer_approval_sufficient(maintainer_env, ownerA_client, o
|
||||
filename = f"pkgB_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Single Maintainer Merge", True, base_branch=branch_name)
|
||||
@@ -217,7 +217,7 @@ def test_006_maintainer_rejection_removes_other_requests(maintainer_env, ownerA_
|
||||
filename = f"pkgB_rejection_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Maintainer Rejection", True, base_branch=branch_name)
|
||||
@@ -277,7 +277,7 @@ def test_007_review_required_needs_all_approvals(review_required_env, ownerA_cli
|
||||
filename = f"pkgB_review_required_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Required", True, base_branch=branch_name)
|
||||
|
||||
@@ -22,7 +22,7 @@ pytest.forwarded_pr_number = None
|
||||
@pytest.mark.dependency()
|
||||
def test_001_project_pr(gitea_env):
|
||||
"""Forwarded PR correct title"""
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
||||
pytest.pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR", False)
|
||||
pytest.initial_pr_number = pytest.pr["number"]
|
||||
time.sleep(5) # Give Gitea some time to process the PR and make the timeline available
|
||||
@@ -114,7 +114,7 @@ def test_005_NoProjectGitPR_edits_disabled(no_project_git_pr_env, test_user_clie
|
||||
# 1. Create a Package PR (without "Allow edits from maintainers" enabled)
|
||||
initial_diff = """diff --git a/first_file.txt b/first_file.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/first_file.txt
|
||||
@@ -0,0 +1 @@
|
||||
@@ -160,7 +160,7 @@ index {pkgA_main_sha[:7]}..{pkgA_pr_head_sha[:7]} 160000
|
||||
# 4. Trigger an update on the Package PR to prompt the bot to react to the manual Project PR
|
||||
new_diff_content = """diff --git a/trigger_bot.txt b/trigger_bot.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/trigger_bot.txt
|
||||
@@ -0,0 +1 @@
|
||||
@@ -200,7 +200,7 @@ def test_006_NoProjectGitPR_edits_enabled(no_project_git_pr_env, test_user_clien
|
||||
# 2. Create a Package PR with "Allow edits from maintainers" enabled
|
||||
diff = """diff --git a/new_feature.txt b/new_feature.txt
|
||||
new file mode 100644
|
||||
index 00000000..473a0f4c
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/new_feature.txt
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -19,19 +19,16 @@ export GITEA_TOKEN
|
||||
echo "GITEA_TOKEN exported (length: ${#GITEA_TOKEN})"
|
||||
|
||||
# Wait for the dummy data to be created by the gitea setup script
|
||||
echo "Waiting for workflow.config in myproducts/mySLFO (branch zz-ready-to-start)..."
|
||||
API_URL="http://gitea-test:3000/api/v1/repos/myproducts/mySLFO/contents/workflow.config?ref=zz-ready-to-start"
|
||||
echo "Waiting for workflow.config in myproducts/mySLFO..."
|
||||
API_URL="http://gitea-test:3000/api/v1/repos/myproducts/mySLFO/contents/workflow.config"
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
||||
|
||||
WAITED=false
|
||||
while [ "$HTTP_STATUS" != "200" ]; do
|
||||
echo "workflow.config on zz-ready-to-start not found yet (HTTP Status: $HTTP_STATUS). Retrying in 1s..."
|
||||
sleep 1
|
||||
echo "workflow.config not found yet (HTTP Status: $HTTP_STATUS). Retrying in 5s..."
|
||||
sleep 5
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
||||
done
|
||||
|
||||
echo "workflow.config found on zz-ready-to-start."
|
||||
|
||||
# Wait for the shared SSH key to be generated by the gitea setup script
|
||||
echo "Waiting for /var/lib/gitea/ssh-keys/id_ed25519..."
|
||||
while [ ! -f /var/lib/gitea/ssh-keys/id_ed25519 ]; do
|
||||
@@ -66,5 +63,4 @@ package=$(rpm -qa | grep autogits-workflow-pr) || :
|
||||
echo "!!!!!!!!!!!!!!!! using binary $exe; installed package: $package"
|
||||
which strings > /dev/null 2>&1 && strings "$exe" | grep -A 2 vcs.revision= | head -4 || :
|
||||
|
||||
set -x
|
||||
exec "$exe" "$@"
|
||||
|
||||
Reference in New Issue
Block a user