diff --git a/bots-common/git_utils.go b/bots-common/git_utils.go index dbe4b3d..84bb8fd 100644 --- a/bots-common/git_utils.go +++ b/bots-common/git_utils.go @@ -19,6 +19,9 @@ package common */ import ( + "bufio" + "bytes" + "errors" "fmt" "io" "log" @@ -549,7 +552,7 @@ func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsed cmd.Stdin = &data_out cmd.Stderr = writeFunc(func(data []byte) (int, error) { if e.DebugLogger { - log.Printf(string(data)) + log.Println(string(data)) } return len(data), nil }) @@ -613,7 +616,7 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte cmd.Stdin = &data_out cmd.Stderr = writeFunc(func(data []byte) (int, error) { if e.DebugLogger { - log.Printf(string(data)) + log.Println(string(data)) } return len(data), nil }) @@ -753,3 +756,166 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string) wg.Wait() 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))) +} diff --git a/bots-common/git_utils_test.go b/bots-common/git_utils_test.go index 093c35e..ed3cb33 100644 --- a/bots-common/git_utils_test.go +++ b/bots-common/git_utils_test.go @@ -19,9 +19,12 @@ package common */ import ( + "bufio" + "bytes" "os" "os/exec" "path" + "slices" "strings" "testing" ) @@ -302,3 +305,94 @@ func TestCommitTreeParsingOfHead(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) + } + } + }) + } +} diff --git a/workflow-pr/pr.go b/workflow-pr/pr.go index 2cf7228..f616980 100644 --- a/workflow-pr/pr.go +++ b/workflow-pr/pr.go @@ -3,6 +3,7 @@ package main import ( "bufio" "errors" + "fmt" "slices" "strings" @@ -124,17 +125,25 @@ func (rs *PRSet) Merge() error { return err } 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 + 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") if rev != prjgit.Base.Sha { panic("FIXME") } - +*/ 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") return nil } diff --git a/workflow-pr/pr_test.go b/workflow-pr/pr_test.go index 1dbfa18..a92225d 100644 --- a/workflow-pr/pr_test.go +++ b/workflow-pr/pr_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path" + "strings" "testing" "go.uber.org/mock/gomock" @@ -227,20 +228,85 @@ func TestPRMerge(t *testing.T) { 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 { name string - prjgit_base, prjgit_head string + pr *models.PullRequest + mergeError string }{ { - name: "Merge conflicts in submodules", - prjgit_base: "base_add_b1", - prjgit_head: "base_add_b2", + name: "Merge base not merged in main", + + 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 { 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) + } }) } } diff --git a/workflow-pr/test_repo_setup.sh b/workflow-pr/test_repo_setup.sh index 580fdc1..a8014d3 100755 --- a/workflow-pr/test_repo_setup.sh +++ b/workflow-pr/test_repo_setup.sh @@ -20,15 +20,14 @@ create_prjgit_sample() { mkdir prjgit pushd prjgit - git init -q --object-format=sha256 + git init -q --object-format=sha256 -b main echo Project git is here > README.md git add README.md - git submodule init -b main + git submodule init git submodule -q add ../pkgA pkgA git submodule -q add ../pkgB pkgB git submodule -q add ../pkgC pkgC - git commit -q -m 'first commit' git checkout -b base_add_b1 main @@ -36,9 +35,15 @@ create_prjgit_sample() { git commit -q -m "pkgB1 added" git checkout -b base_add_b2 main + git clean -ffxd git submodule -q add ../pkgB2 pkgB2 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 }