git.status

This commit is contained in:
Adam Majer 2025-01-29 17:29:09 +01:00
parent 5108019db0
commit d5dbb37e18
5 changed files with 353 additions and 13 deletions

View File

@ -19,6 +19,9 @@ package common
*/ */
import ( import (
"bufio"
"bytes"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -549,7 +552,7 @@ func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsed
cmd.Stdin = &data_out cmd.Stdin = &data_out
cmd.Stderr = writeFunc(func(data []byte) (int, error) { cmd.Stderr = writeFunc(func(data []byte) (int, error) {
if e.DebugLogger { if e.DebugLogger {
log.Printf(string(data)) log.Println(string(data))
} }
return len(data), nil return len(data), nil
}) })
@ -613,7 +616,7 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
cmd.Stdin = &data_out cmd.Stdin = &data_out
cmd.Stderr = writeFunc(func(data []byte) (int, error) { cmd.Stderr = writeFunc(func(data []byte) (int, error) {
if e.DebugLogger { if e.DebugLogger {
log.Printf(string(data)) log.Println(string(data))
} }
return len(data), nil return len(data), nil
}) })
@ -753,3 +756,166 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
wg.Wait() wg.Wait()
return subCommitId, len(subCommitId) == len(commitId) return subCommitId, len(subCommitId) == len(commitId)
} }
const (
GitStatus_Untracked = 0
GitStatus_Modified = 1
GitStatus_Ignored = 2
GitStatus_Unmerged = 3
)
type GitStatusData struct {
Path string
Status int
States [3]string
}
func parseGitStatusHexString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 32)
for {
c, err := data.ReadByte()
if err != nil {
return "", err
}
switch {
case c == 0 || c == ' ':
return string(str), nil
case c >= 'a' && c <= 'f':
case c >= 'A' && c <= 'F':
case c >= '0' && c <= '9':
default:
return "", errors.New("Invalid character in hex string:" + string(c))
}
str = append(str, c)
}
}
func parseGitStatusString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 100)
for {
c, err := data.ReadByte()
if err != nil {
return "", errors.New("Unexpected EOF. Expected NUL string term")
}
if c == 0 {
return string(str), nil
}
str = append(str, c)
}
}
func skipGitStatusEntry(data io.ByteReader, skipSpaceLen int) error {
for skipSpaceLen > 0 {
c, err := data.ReadByte()
if err != nil {
return err
}
if c == ' ' {
skipSpaceLen--
}
}
return nil
}
func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
ret := GitStatusData{}
statusType, err := data.ReadByte()
if err != nil {
return nil, nil
}
switch statusType {
case '1':
var err error
if err = skipGitStatusEntry(data, 8); err != nil {
return nil, err
}
ret.Status = GitStatus_Modified
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
case '?':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Untracked
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
case '!':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Ignored
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
case 'u':
var err error
if err = skipGitStatusEntry(data, 7); err != nil {
return nil, err
}
if ret.States[0], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
if ret.States[1], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
if ret.States[2], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
ret.Status = GitStatus_Unmerged
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
default:
return nil, errors.New("Invalid status type" + string(statusType))
}
return &ret, nil
}
func parseGitStatusData(data io.ByteReader) ([]GitStatusData, error) {
ret := make([]GitStatusData, 0, 10)
for {
data, err := parseSingleStatusEntry(data)
if err != nil {
return nil, err
} else if data == nil {
break
}
ret = append(ret, *data)
}
return ret, nil
}
func (e *GitHandlerImpl) Status(cwd string) (ret []GitStatusData, err error) {
if e.DebugLogger {
log.Println("getting git-status()")
}
cmd := exec.Command("/usr/bin/git", "status", "--porcelain=2", "-z")
cmd.Env = []string{
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
"GIT_CONFIG_GLOBAL=/dev/null",
}
cmd.Dir = filepath.Join(e.GitPath, cwd)
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
log.Println(string(data))
return len(data), nil
})
if e.DebugLogger {
log.Printf("command run: %v\n", cmd.Args)
}
out, err := cmd.Output()
if err != nil {
log.Printf("Error running command %v, err: %v", cmd.Args, err)
}
return parseGitStatusData(bufio.NewReader(bytes.NewReader(out)))
}

View File

@ -19,9 +19,12 @@ package common
*/ */
import ( import (
"bufio"
"bytes"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"slices"
"strings" "strings"
"testing" "testing"
) )
@ -302,3 +305,94 @@ func TestCommitTreeParsingOfHead(t *testing.T) {
t.Run("try to parse unknown item", func(t *testing.T) { t.Run("try to parse unknown item", func(t *testing.T) {
}) })
} }
func TestGitStatusParse(t *testing.T) {
testData := []struct {
name string
data []byte
res []GitStatusData
}{
{
name: "Single modified line",
data: []byte("1 .M N... 100644 100644 100644 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 bots-common/git_utils.go\x00"),
res: []GitStatusData{
{
Path: "bots-common/git_utils.go",
Status: GitStatus_Modified,
},
},
},
{
name: "Untracked entries",
data: []byte("1 .M N... 100644 100644 100644 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 bots-common/git_utils.go\x00? bots-common/c.out\x00? doc/Makefile\x00"),
res: []GitStatusData{
{
Path: "bots-common/git_utils.go",
Status: GitStatus_Modified,
},
{
Path: "bots-common/c.out",
Status: GitStatus_Untracked,
},
{
Path: "doc/Makefile",
Status: GitStatus_Untracked,
},
},
},
{
name: "Untracked entries",
data: []byte("1 .M N... 100644 100644 100644 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 bots-common/git_utils.go\x00? bots-common/c.out\x00! doc/Makefile\x00"),
res: []GitStatusData{
{
Path: "bots-common/git_utils.go",
Status: GitStatus_Modified,
},
{
Path: "bots-common/c.out",
Status: GitStatus_Untracked,
},
{
Path: "doc/Makefile",
Status: GitStatus_Ignored,
},
},
},
{
name: "Nothing",
},
{
name: "Unmerged .gitmodules during a merge",
data: []byte("1 A. S... 000000 160000 160000 0000000000000000000000000000000000000000000000000000000000000000 ed07665aea0522096c88a7555f1fa9009ed0e0bac92de4613c3479516dd3d147 pkgB2\x00u UU N... 100644 100644 100644 100644 587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76 d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c 087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33 .gitmodules\x00"),
res: []GitStatusData{
{
Path: "pkgB2",
Status: GitStatus_Modified,
},
{
Path: ".gitmodules",
Status: GitStatus_Unmerged,
States: [3]string{"587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76", "d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c", "087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33"},
},
},
},
}
for _, test := range testData {
t.Run(test.name, func(t *testing.T) {
r, err := parseGitStatusData(bufio.NewReader(bytes.NewReader(test.data)))
if err != nil {
t.Fatal(err)
}
if len(r) != len(test.res) {
t.Fatal("len(r):", len(r), "is not expected", len(test.res))
}
for _, expected := range test.res {
if !slices.Contains(r, expected) {
t.Fatal("result", r, "doesn't contains expected", expected)
}
}
})
}
}

View File

@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"errors" "errors"
"fmt"
"slices" "slices"
"strings" "strings"
@ -124,17 +125,25 @@ func (rs *PRSet) Merge() error {
return err return err
} }
git.GitExecOrPanic("", "clone", "--depth", "1", prjgit.Base.Repo.SSHURL, common.DefaultGitPrj) git.GitExecOrPanic("", "clone", "--depth", "1", prjgit.Base.Repo.SSHURL, common.DefaultGitPrj)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", common.DefaultGitPrj, "origin", prjgit.Base.Sha, prjgit.Head.Sha) git.GitExecOrPanic(common.DefaultGitPrj, "fetch", "origin", prjgit.Base.Sha, prjgit.Head.Sha)
// if other changes merged, check if we have conflicts // if other changes merged, check if we have conflicts
rev := strings.TrimSpace(git.GitExecWithOutputOrPanic(common.DefaultGitPrj, "merge-base", "HEAD", prjgit.Base.Sha, prjgit.Head.Sha))
if rev != prjgit.Base.Sha {
return fmt.Errorf("Base.Sha (%s) not yet merged into project-git. Aborting merge.", prjgit.Base.Sha)
}
/*
rev := git.GitExecWithOutputOrPanic(common.DefaultGitPrj, "rev-list", "-1", "HEAD") rev := git.GitExecWithOutputOrPanic(common.DefaultGitPrj, "rev-list", "-1", "HEAD")
if rev != prjgit.Base.Sha { if rev != prjgit.Base.Sha {
panic("FIXME") panic("FIXME")
} }
*/
msg := "haha" msg := "haha"
git.GitExecOrPanic(common.DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha) err = git.GitExec(common.DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha)
if err != nil {
return err
}
git.GitExecOrPanic(common.DefaultGitPrj, "push", "origin") git.GitExecOrPanic(common.DefaultGitPrj, "push", "origin")
return nil return nil
} }

View File

@ -5,6 +5,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"strings"
"testing" "testing"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
@ -227,20 +228,85 @@ func TestPRMerge(t *testing.T) {
t.Fatal(string(out)) t.Fatal(string(out))
} }
common.ExtraGitParams = []string{
"GIT_CONFIG_COUNT=1",
"GIT_CONFIG_KEY_0=protocol.file.allow",
"GIT_CONFIG_VALUE_0=always",
"GIT_AUTHOR_NAME=testname",
"GIT_AUTHOR_EMAIL=test@suse.com",
"GIT_AUTHOR_DATE='2005-04-07T22:13:13'",
"GIT_COMMITTER_NAME=testname",
"GIT_COMMITTER_EMAIL=test@suse.com",
"GIT_COMMITTER_DATE='2005-04-07T22:13:13'",
}
config := &common.AutogitConfig{
Organization: "org",
GitProjectName: "prj",
}
tests := []struct { tests := []struct {
name string name string
prjgit_base, prjgit_head string pr *models.PullRequest
mergeError string
}{ }{
{ {
name: "Merge conflicts in submodules", name: "Merge base not merged in main",
prjgit_base: "base_add_b1",
prjgit_head: "base_add_b2", pr: &models.PullRequest{
Base: &models.PRBranchInfo{
Sha: "e8b0de43d757c96a9d2c7101f4bff404e322f53a1fa4041fb85d646110c38ad4", // "base_add_b1"
Repo: &models.Repository{
Name: "prj",
Owner: &models.User{
UserName: "org",
},
SSHURL: path.Join(cmd.Dir, "prjgit"),
},
},
Head: &models.PRBranchInfo {
Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2"
},
},
mergeError: "Aborting merge",
},
{
name: "Merge conflict in modules",
pr: &models.PullRequest{
Base: &models.PRBranchInfo{
Sha: "4fbd1026b2d7462ebe9229a49100c11f1ad6555520a21ba515122d8bc41328a8",
Repo: &models.Repository{
Name: "prj",
Owner: &models.User{
UserName: "org",
},
SSHURL: path.Join(cmd.Dir, "prjgit"),
},
},
Head: &models.PRBranchInfo {
Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2"
},
},
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
ctl := gomock.NewController(t)
mock := mock_common.NewMockGiteaPRFetcher(ctl)
mock.EXPECT().GetPullRequest("org", "prj", int64(1)).Return(test.pr, nil)
set, err := FetchPRSet(mock, "org", "prj", 1, config)
if err != nil {
t.Fatal(err)
}
if err = set.Merge(); err != nil && (test.mergeError == "" || (len(test.mergeError) > 0 && !strings.Contains(err.Error(), test.mergeError))) {
t.Fatal(err)
}
}) })
} }
} }

View File

@ -20,15 +20,14 @@ create_prjgit_sample() {
mkdir prjgit mkdir prjgit
pushd prjgit pushd prjgit
git init -q --object-format=sha256 git init -q --object-format=sha256 -b main
echo Project git is here > README.md echo Project git is here > README.md
git add README.md git add README.md
git submodule init -b main git submodule init
git submodule -q add ../pkgA pkgA git submodule -q add ../pkgA pkgA
git submodule -q add ../pkgB pkgB git submodule -q add ../pkgB pkgB
git submodule -q add ../pkgC pkgC git submodule -q add ../pkgC pkgC
git commit -q -m 'first commit' git commit -q -m 'first commit'
git checkout -b base_add_b1 main git checkout -b base_add_b1 main
@ -36,9 +35,15 @@ create_prjgit_sample() {
git commit -q -m "pkgB1 added" git commit -q -m "pkgB1 added"
git checkout -b base_add_b2 main git checkout -b base_add_b2 main
git clean -ffxd
git submodule -q add ../pkgB2 pkgB2 git submodule -q add ../pkgB2 pkgB2
git commit -q -m "pkgB2 added" git commit -q -m "pkgB2 added"
git checkout main
git clean -ffxd
git submodule -q add -f ../pkgB1 pkgB1
git commit -q -m "main adding pkgB1"
popd popd
} }