diff --git a/bots-common/git_utils.go b/bots-common/git_utils.go index d7fa687..b4110f2 100644 --- a/bots-common/git_utils.go +++ b/bots-common/git_utils.go @@ -1,12 +1,15 @@ package common import ( + "errors" "fmt" + "io" "os" "os/exec" "path" "path/filepath" "strings" + "sync" ) //func (h *RequestHandler) ProcessBranchList() []string { @@ -203,23 +206,223 @@ func (e *RequestHandler) GitExec(cwd string, params ...string) ExecStream { return e } -type writerFunc func([]byte) (int, error) - -// Implement the Write method for the custom type -func (f writerFunc) Write(p []byte) (int, error) { - return f(p) +type ChanIO struct { + ch chan byte } -func (e *RequestHandler) GitSubmoduleCommitId(cwd, packageName string) (string, bool) { +func (c *ChanIO) Write(p []byte) (int, error) { + for i := range len(p) { + c.ch <- p[i] + } + return len(p), nil +} + +// read at least 1 byte, but don't block if nothing in channel +func (c *ChanIO) Read(p []byte) (int, error) { + data, ok := <-c.ch + for i := 0; i < cap(p); i++ { + data, ok = <-c.ch + + if !ok { + return len(p), io.EOF + } + + p[i] = data + if len(c.ch) < 1 { + break + } + } + + return len(p), nil +} + +type gitMsg struct { + hash string + itemType string + size int +} + +type commit struct { + Tree string + Msg string +} + +type tree_entry struct { + name string + mode int + hash string +} + +type tree struct { + items []tree_entry +} + +func parseGitMsg(data <-chan byte) (gitMsg, error) { + var id []byte = make([]byte, 64) + var msgType []byte = make([]byte, 16) + var size int + + pos := 0 + for c := <-data; c != ' '; c = <-data { + if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') { + id[pos] = c + pos++ + } else { + return gitMsg{}, errors.New("Invalid character during object hash parse") + } + } + id = id[:pos] + + pos = 0 + for c := <-data; c != ' '; c = <-data { + if c >= 'a' && c <= 'z' { + msgType[pos] = c + pos++ + } else { + return gitMsg{}, errors.New("Invalid character during object type parse") + } + } + msgType = msgType[:pos] + + switch string(msgType) { + case "commit", "tree": + break + default: + return gitMsg{}, fmt.Errorf("Invalid object type: '%s'", string(msgType)) + } + + for c := <-data; c != '\000'; c = <-data { + if c >= '0' && c <= '9' { + size = size*10 + (int(c) - '0') + } else { + return gitMsg{}, errors.New("Invalid character during commit size parse") + } + } + + return gitMsg{ + hash: string(id[:]), + itemType: string(msgType), + size: size, + }, nil +} + +func parseGitCommitHdr(data <-chan byte) ([2]string, error) { + hdr := make([]byte, 0, 60) + val := make([]byte, 0, 1000) + + c := <-data + if c != '\n' { // end of header marker + for ; c != ' '; c = <-data { + hdr = append(hdr, c) + } + for c := <-data; c != '\n'; c = <-data { + val = append(val, c) + } + } + + return [2]string{string(hdr), string(val)}, nil +} + +func parseGitCommitMsg(data <-chan byte, l int) (string, error) { + msg := make([]byte, 0, l) + + for c := <-data; c != '\x00'; c = <-data { + msg = append(msg, c) + } + + return string(msg), nil +} + +func parseGitCommit(data <-chan byte) (commit, error) { + hdr, err := parseGitMsg(data) + if err != nil { + return commit{}, err + } else if hdr.itemType != "commit" { + return commit{}, fmt.Errorf("expected commit but parsed %s", hdr.itemType) + } + + var c commit + l := hdr.size + for { + hdr, err := parseGitCommitHdr(data) + if err != nil { + return commit{}, nil + } + + if len(hdr[0])+len(hdr[1]) == 0 { // hdr end marker + break + } + + switch hdr[0] { + case "tree": + c.Tree = hdr[1] + } + + l -= len(hdr[0]) + len(hdr[1]) + 2 + } + l-- + + c.Msg, err = parseGitCommitMsg(data, l) + return c, err +} + +func parseTreeEntry(data <-chan byte) (tree_entry, error) { + var e tree_entry + + return e, nil +} + +func parseGitTree(data <-chan byte) (tree, error) { + + return tree{}, nil +} + +func (e *RequestHandler) GitSubmoduleCommitId(cwd, packageName, headId string) (string, bool) { if e.Error != nil { return "", false } - cmd := exec.Command("/usr/bin/git", "cat-file", "--batch") - cmd.Dir = filepath.Join(e.GitPath, cwd) - cmd.Stdout = writerFunc(func(p []byte) (int, error) { - return len(p), nil - }) + data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)} + var commitId string + var foundLock sync.Mutex - return "", false + foundLock.Lock() + + go func() { + defer foundLock.Unlock() + defer close(data_out.ch) + + data_out.Write([]byte("HEAD\n")) + c, err := parseGitCommit(data_in.ch) + if err != nil { + e.Error = err + e.LogError("Error parsing git commit: %v", err) + return + } + data_out.Write([]byte(c.Tree)) + data_out.ch <- '\n' + tree, err := parseGitTree(data_in.ch) + + if err != nil { + e.Error = err + e.LogError("Error parsing git tree: %v", err) + return + } + + for _, te := range tree.items { + if te.name == packageName { + commitId = te.hash + return + } + } + }() + + cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z") + cmd.Dir = filepath.Join(e.GitPath, cwd) + cmd.Stdout = &data_in + cmd.Stdin = &data_out + e.Log("command run: %v", cmd.Run()) + + foundLock.Lock() + return commitId, len(commitId) == len(headId) } diff --git a/bots-common/git_utils_test.go b/bots-common/git_utils_test.go new file mode 100644 index 0000000..c78f3b9 --- /dev/null +++ b/bots-common/git_utils_test.go @@ -0,0 +1,219 @@ +package common + +import ( + "testing" +) + +func TestGitMsgParsing(t *testing.T) { + t.Run("tree message with size 56", func(t *testing.T) { + const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00" + + data := make(chan byte, 500) + for _, b := range []byte(hdr) { + data <- b + } + gitHdr, err := parseGitMsg(data) + + if err != nil { + t.Error(err) + } + + if gitHdr.hash != "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98" { + t.Errorf("Invalid hash %s", gitHdr.hash) + } + + if gitHdr.size != 56 { + t.Errorf("Invalid msg size: %d", gitHdr.size) + } + + if gitHdr.itemType != "tree" { + t.Errorf("Invalid msg type: %s", gitHdr.itemType) + } + }) + t.Run("commit message with size 256", func(t *testing.T) { + const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99 commit 256\x00" + + data := make(chan byte, 500) + for _, b := range []byte(hdr) { + data <- b + } + gitHdr, err := parseGitMsg(data) + + if err != nil { + t.Error(err) + } + + if gitHdr.hash != "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99" { + t.Errorf("Invalid hash %s", gitHdr.hash) + } + + if gitHdr.size != 256 { + t.Errorf("Invalid msg size: %d", gitHdr.size) + } + + if gitHdr.itemType != "commit" { + t.Errorf("Invalid msg type: %s", gitHdr.itemType) + } + }) + t.Run("invalid tree message with size 56", func(t *testing.T) { + const hdr = "f408r8ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00" + + data := make(chan byte, 500) + for _, b := range []byte(hdr) { + data <- b + } + gitHdr, err := parseGitMsg(data) + + if err.Error() != "Invalid character during object hash parse" { + t.Error(err) + } + + if gitHdr.hash != "" { + t.Errorf("Invalid hash %s", gitHdr.hash) + } + + if gitHdr.size != 0 { + t.Errorf("Invalid msg size: %d", gitHdr.size) + } + + if gitHdr.itemType != "" { + t.Errorf("Invalid msg type: %s", gitHdr.itemType) + } + }) +} + +func TestGitCommitParsing(t *testing.T) { + t.Run("parse valid commit message", func(t *testing.T) { + const commitData = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99 commit 254\000"+ +`tree e20033df9f18780756ba4a96dbc7eb1a626253961039cb674156f266ba7a4e53 +parent 429cc2fe02170ca5668f0461928c7e7430c7a17cd64ac298286d7162572a7703 +author Adam Majer 1720709149 +0200 +committer Adam Majer 1720709149 +0200 + +.`+"\000" + ch := make(chan byte, 5000) + for _, b := range []byte(commitData) { + ch <- b + } + commit, err := parseGitCommit(ch) + + if err != nil { + t.Error(err) + } + + if commit.Tree != "e20033df9f18780756ba4a96dbc7eb1a626253961039cb674156f266ba7a4e53" { + t.Errorf("Invalid commit object: %#v", commit) + } + + if commit.Msg != "." { + t.Errorf("Invalid commit msg: '%s'", commit.Msg) + } + }) + + t.Run("parse multiline headers", func(t *testing.T) { + const commitData = "cae5831ab48470ff060a5aaa12eb6e5a7acaf91e commit 1492\x00" + +`tree 1f9c8fe8099615d6d3921528402ac53f09213b02 +parent e08a654fae0ecc91678819e0b62a2e014bad3339 +author Yagiz Nizipli 1720967314 -0400 +committer GitHub 1720967314 +0200 +gpgsig -----BEGIN PGP SIGNATURE----- + + wsFcBAABCAAQBQJmk+CSCRC1aQ7uu5UhlAAAQIYQAEQXCl3bUUuegiz5/oitIIF7 + 6xhndcjQIuqY4dONIeOARrGwbKh8OtMHpfJhMRUmvWvXrsTA6P1PWl0YcyyIMzHZ + a4sBsWyxA0uSztVywpvksvk6EdMoEXeXrHS3cBxePsH8bI+Pwnsv27PsevEwpyIT + reB4zZsoGySFVqf2lnXxG5hSRMYw++BDXSDMZk2BP9BvueRXasJ0lT1c7HlbHepF + TWzwyHZ91OhXjrdPY7qLQEEV/frwuM+UrxOPb2e83ZTg81vXFuugURfhHNx4Iu+F + LCMvOeaF2vO5yJtMe8+tY1l0Wb8S1aWcGCECN2XCXmmnWxt+yYh2gjqxq3y0DMcz + zvg6arQIepDFLkQPZMDlUCIjIJQn4FbAaQAvoyMF8Pi5YmxhRqgo3iOB5SBE8eES + 63ifZ311izuSdD+o3ObFpzLoTgq62kglwfegZN/X8CzTSIqrT1norYJEbSwkWID1 + WeRHUHfC7f6N3XK8zeb83zmhBU58ghW9sp5/LcefGMRJmVhBWhjBCpeMUaFHdKhl + /dfgPl5gJrrJ+wM3O6iaay0R1Iv4Upe/yXrbQnIgGj/qqgMLEPBY8lzNYimVTLxd + 2ObrcXnERo3wwxeUgWaAARbEGQjC51DK/2SXxVGUh+IokicsBNRKU7lVWwwFFely + ntjtge6Gs9pA5rSIilPH + =V1bK + -----END PGP SIGNATURE----- + + +meta: change email address of anonrig + +PR-URL: https://github.com/nodejs/node/pull/53829 +Reviewed-By: Antoine du Hamel +Reviewed-By: James M Snell +Reviewed-By: Luigi Pinca +Reviewed-By: Matteo Collina +Reviewed-By: Ulises Gascón +Reviewed-By: Richard Lau +Reviewed-By: Marco Ippolito ` + "\x00" + + ch := make(chan byte, 5000) + for _, b := range []byte(commitData) { + ch <- b + } + commit, err := parseGitCommit(ch) + + if err != nil { + t.Error(err) + } + + if commit.Tree != "1f9c8fe8099615d6d3921528402ac53f09213b02" { + t.Errorf("Invalid commit object: %#v", commit) + } + + if commit.Msg[len(commit.Msg)-55:] != "Reviewed-By: Marco Ippolito " { + t.Errorf("Invalid commit msg: '%s'", commit.Msg[len(commit.Msg)-55:]) + } + }) + + t.Run("parse tree object", func(t *testing.T) { + const treeData = "\x31\x31\x34\x31\x32\x39\x32\x30\x34\x38\x36\x38\x62\x30\x38\x65\x35\x36\x65\x39\x61\x66\x30\x64\x32\x39\x66\x37\x38\x36\x33\x34\x34\x37\x64\x32\x31\x36\x61\x62\x31\x35\x37\x32\x31\x30\x66\x65\x64\x37\x63\x36\x32\x37\x31\x64\x64\x35\x31\x30\x66\x35\x36\x62\x20\x74\x72\x65\x65\x20\x32\x30\x35\x0a\x34\x30\x30\x30\x30\x20\x62\x6f\x74\x73\x2d\x63\x6f\x6d\x6d\x6f\x6e\x00\x81\xac\x2e\xbd\x40\x77\xcb\x63\xbf\x40\x8c\xf8\xbc\xcd\x6f\xbe\x06\x0d\xfa\xd7\x45\x16\x85\x4f\xd0\xa0\x67\xb4\x21\x39\x11\x84\x34\x30\x30\x30\x30\x20\x6f\x62\x73\x2d\x73\x74\x61\x67\x69\x6e\x67\x2d\x62\x6f\x74\x00\x79\x77\x8b\x28\x7d\x37\x10\x59\xb9\x71\x28\x36\xed\x20\x31\x5f\xfb\xe1\xed\xb5\xba\x4f\x5e\xbb\x65\x65\x68\x23\x77\x32\x58\xfe\x34\x30\x30\x30\x30\x20\x70\x72\x2d\x72\x65\x76\x69\x65\x77\x00\x31\x6a\x4a\x1e\x67\xbb\xdd\x21\x89\xa4\x46\xf7\x62\x75\x60\x84\x73\x28\x6f\xb7\x3c\x51\x7f\x4a\x14\xa2\x28\xf9\x6e\x0b\xa7\x95\x34\x30\x30\x30\x30\x20\x70\x72\x6a\x67\x69\x74\x2d\x75\x70\x64\x61\x74\x65\x72\x00\xb4\x0b\x1c\xf5\xfb\xec\x9a\xb2\x9f\x48\x3e\x21\x18\x0d\x51\xb7\x98\x6e\x21\x99\x74\x84\x67\x71\x41\x24\x42\xfc\xc9\x04\x12\x99\x0a" + + ch := make(chan byte, 1000) + for _, b := range []byte(treeData) { + ch <- b + } + + tree, err := parseGitTree(ch) + + if err != nil { + t.Error(err) + } + + found := false + for _, item := range tree.items { + if item.name == "bots-common" && item.hash == "81ac2ebd4077cb63bf408cf8bccd6fbe060dfad74516854fd0a067b421391184" && item.mode == 0o40000 { + found = true + break + } + } + if !found { + t.Error("expected sub-tree not found") + } + }) + + t.Run("parse tree object with submodules", func(t *testing.T) { + const treeData = "\x34\x38\x34\x66\x31\x62\x65\x65\x63\x39\x35\x63\x61\x36\x62\x38\x36\x30\x64\x30\x64\x37\x63\x35\x65\x34\x38\x37\x31\x36\x36\x62\x30\x38\x65\x31\x61\x34\x35\x36\x37\x36\x62\x31\x37\x64\x39\x65\x65\x66\x32\x39\x39\x39\x65\x39\x64\x37\x34\x63\x62\x63\x64\x36\x20\x74\x72\x65\x65\x20\x33\x34\x39\x00\x31\x30\x30\x36\x34\x34\x20\x2e\x67\x69\x74\x6d\x6f\x64\x75\x6c\x65\x73\x00\xc8\x1e\x14\x29\xc5\xd4\x07\xfc\x32\xf1\xd7\xe2\x64\xee\x88\xfc\xce\xfc\x44\xf7\xae\x2e\xc4\x6e\x2c\x15\x27\x26\x65\xd4\xb8\x78\x31\x30\x30\x36\x34\x34\x20\x52\x45\x41\x44\x4d\x45\x2e\x6d\x64\x00\x90\x22\x84\x13\xbf\xd3\x5c\xfb\x1e\x27\x6b\xd6\x17\x3e\x89\xed\x0f\xc6\x31\x85\x24\x58\x6d\x9d\xf0\x6a\x1e\x17\x57\x19\x8d\xc2\x31\x36\x30\x30\x30\x30\x20\x6d\x69\x6e\x67\x77\x33\x32\x2d\x67\x63\x63\x00\xdc\x55\xb8\x28\x32\x8c\x8e\x49\x4f\x67\x87\x4a\x7d\x8c\x03\xdd\x1c\x6b\x4e\x02\xd1\x6b\x86\xe0\x8e\x47\xd7\x0e\xcd\x79\x96\x80\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x2d\x63\x6f\x6d\x6d\x6f\x6e\x00\xd6\xa7\x4c\x08\x40\x6c\xe4\x0c\xc8\xf7\xbf\xf2\xd5\xcf\x30\x90\x87\xa8\x72\x83\x61\xcc\x75\x35\x4b\x08\x62\xba\x50\x81\x93\xb8\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x32\x31\x00\x24\xee\x6b\xee\x74\x59\xa3\x86\xda\xda\xbf\x8a\x9f\x6a\xe4\xfa\x15\xc3\xf8\x10\xbf\xa0\x1c\xee\x52\x38\x13\x8a\xa2\x14\xd1\x80\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x32\x32\x00\x87\x3a\x32\x3b\x26\x2e\xbb\x3b\xd7\x7b\x25\x92\xb2\xe1\x1b\xdd\x08\xdb\xc7\x21\xcb\xf4\xac\x9f\x97\x63\x7e\x58\xe1\xff\xfc\xe7\x31\x36\x30\x30\x30\x30\x20\x70\x79\x74\x68\x6f\x6e\x33\x31\x31\x00\x35\xc7\x02\xe8\x50\x1e\xed\xeb\x5c\xe4\x3d\x6f\x34\x60\xd1\x1c\x79\x1c\xfe\xfa\xa7\x72\x48\xf0\x8c\xad\x55\xd0\x0c\x37\xe7\x3a\x00" + + ch := make(chan byte, 1000) + for _, b := range []byte(treeData) { + ch <- b + } + + tree, err := parseGitTree(ch) + + if err != nil { + t.Error(err) + } + + found := false + for _, item := range tree.items { + t.Log(item) + if item.name == "nodejs22" && item.hash == "873a323b262ebb3bd77b2592b2e11bdd08dbc721cbf4ac9f97637e58e1fffce7" { + found = true + break + } + } + if !found { + t.Error("expected submodule not found") + } + }) +}