package common import ( "fmt" "io" "os" "os/exec" "path" "path/filepath" "strings" "sync" ) //func (h *RequestHandler) ProcessBranchList() []string { // if h.HasError() { // return make([]string, 0) // } // // trackedBranches, err := os.ReadFile(path.Join(h.GitPath, DefaultGitPrj, TrackedBranchesFile)) // if err != nil { // if errors.Is(err, os.ErrNotExist) { // trackedBranches = []byte("factory") // } else { // h.LogError("file error reading '%s' file in repo", TrackedBranchesFile) // h.Error = err // return make([]string, 0) // } // } // // return strings.Split(string(trackedBranches), "\n") //} type GitReference struct { Branch string Id string } type GitReferences struct { refs []GitReference } func (refs *GitReferences) addReference(id, branch string) { for _, ref := range refs.refs { if ref.Id == id && ref.Branch == branch { return } } refs.refs = append(refs.refs, GitReference{Branch: branch, Id: id}) } func processRefs(gitDir string) ([]GitReference, error) { packedRefsPath := path.Join(gitDir, "packed-refs") stat, err := os.Stat(packedRefsPath) if err != nil { return nil, err } if stat.Size() > 10000 || stat.IsDir() { return nil, fmt.Errorf("Funny business with 'packed-refs' in '%s'", gitDir) } data, err := os.ReadFile(packedRefsPath) if err != nil { return nil, err } var references GitReferences for _, line := range strings.Split(string(data), "\n") { if len(line) < 1 || line[0] == '#' { continue } splitLine := strings.Split(line, " ") if len(splitLine) != 2 { return nil, fmt.Errorf("Unexpected packaged-refs entry '%#v' in '%s'", splitLine, packedRefsPath) } id, ref := splitLine[0], splitLine[1] const remoteRefPrefix = "refs/remotes/origin/" if ref[0:len(remoteRefPrefix)] != remoteRefPrefix { continue } references.addReference(id, ref[len(remoteRefPrefix):]) } return references.refs, nil } func findGitDir(p string) (string, error) { gitFile := path.Join(p, ".git") stat, err := os.Stat(gitFile) if err != nil { return "", err } if stat.IsDir() { return path.Join(p, ".git"), nil } data, err := os.ReadFile(gitFile) if err != nil { return "", err } for _, line := range strings.Split(string(data), "\n") { refs := strings.Split(line, ":") if len(refs) != 2 { return "", fmt.Errorf("Unknown format of .git file: '%s'\n", line) } if refs[0] != "gitdir" { return "", fmt.Errorf("Unknown header of .git file: '%s'\n", refs[0]) } return path.Join(p, strings.TrimSpace(refs[1])), nil } return "", fmt.Errorf("Can't find git subdirectory in '%s'", p) } func (e *RequestHandler) GitBranchHead(gitDir, branchName string) (string, error) { if e.HasError() { return "", e.Error } path, err := findGitDir(path.Join(e.GitPath, gitDir)) if err != nil { e.LogError("Error identifying gitdir in `%s`: %#v", gitDir, err) e.Error = err } refs, err := processRefs(path) if err != nil { e.LogError("Error finding branches (%s): %#v", branchName, err) e.Error = err return "", e.Error } for _, ref := range refs { if ref.Branch == branchName { return ref.Id, nil } } e.Error = fmt.Errorf("Can't find default remote branch: %s", branchName) e.LogError("%s", e.Error.Error()) return "", e.Error } type ExecStream interface { Close() HasError() bool GitExec(cwd string, param ...string) ExecStream } func (e *RequestHandler) Close() { if e.GitPath == "" { return } e.Error = os.RemoveAll(e.GitPath) e.GitPath = "" return } func (e *RequestHandler) HasError() bool { return e.Error != nil } type writeFunc func(data []byte) (int, error) func (f writeFunc) Write(data []byte) (int, error) { return f(data) } func (e *RequestHandler) GitExec(cwd string, params ...string) ExecStream { if e.Error != nil { return e } cmd := exec.Command("/usr/bin/git", params...) cmd.Env = []string{ "GIT_CEILING_DIRECTORIES=" + e.GitPath, "GIT_CONFIG_GLOBAL=/dev/null", "GIT_AUTHOR_NAME=" + e.GitCommiter, "EMAIL=not@exist@src.opensuse.org", "GIT_LFS_SKIP_SMUDGE=1", "GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes", } cmd.Dir = filepath.Join(e.GitPath, cwd) cmd.Stdout = writeFunc(func(data []byte) (int, error) { e.Logger.Log("%s", data) return len(data), nil }) cmd.Stderr = writeFunc(func(data []byte) (int, error) { e.Logger.LogError("%s", data) return len(data), nil }) cmd.Stdin = nil e.Log("git execute: %#v", cmd.Args) e.Error = cmd.Run() return e } type ChanIO struct { ch chan byte } func (c *ChanIO) Write(p []byte) (int, error) { for _, b := range p { c.ch <- b } return len(p), nil } // read at least 1 byte, but don't block if nothing more in channel func (c *ChanIO) Read(data []byte) (idx int, err error) { var ok bool data[idx], ok = <-c.ch if !ok { err = io.EOF return } idx++ for len(c.ch) > 0 && idx < len(data) { data[idx], ok = <- c.ch if !ok { err = io.EOF return } idx++ } return } 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 size int } type tree struct { items []tree_entry } func (t *tree_entry) isSubmodule() bool { return (t.mode & 0170000) == 0160000 } func (t *tree_entry) isTree() bool { return (t.mode & 0170000) == 0040000 } func (t *tree_entry) isBlob() bool { return !t.isTree() && !t.isSubmodule() } 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{}, fmt.Errorf("Invalid character during object hash parse '%c' at %d", c, pos) } } id = id[:pos] pos = 0 for c := <-data; c != ' '; c = <-data { if c >= 'a' && c <= 'z' { msgType[pos] = c pos++ } else { return gitMsg{}, fmt.Errorf("Invalid character during object type parse '%c' at %d", c, pos) } } 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{}, fmt.Errorf("Invalid character during object size parse: '%c'", c) } } 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, hashLen int) (tree_entry, error) { var e tree_entry 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 := <-data; c != '\x00'; c = <-data { name = append(name, c) e.size++ } e.size++ e.name = string(name) const hexBinToAscii = "0123456789abcdef" hash := make([]byte, 0, hashLen*2) for range hashLen { c := <-data hash = append(hash, hexBinToAscii[((c&0xF0)>>4)], hexBinToAscii[c&0xF]) } e.hash = string(hash) e.size += hashLen return e, nil } func parseGitTree(data <-chan byte) (tree, error) { hdr, err := parseGitMsg(data) if err != nil { return tree{}, err } // max capacity to length of hash t := tree{items: make([]tree_entry, 0, hdr.size/len(hdr.hash))} for parsedLen := 0; parsedLen+1 < hdr.size; { entry, err := parseTreeEntry(data, len(hdr.hash)/2) if err != nil { return tree{}, nil } t.items = append(t.items, entry) parsedLen += entry.size } return t, nil } func (e *RequestHandler) GitSubmoduleCommitId(cwd, packageName, commitId string) (string, bool) { if e.Error != nil { return "", false } data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)} var subCommitId string var foundLock sync.Mutex foundLock.Lock() e.Log("getting commit id '%s' from git at '%s' with packageName: %s", commitId, cwd, packageName) go func() { defer foundLock.Unlock() defer close(data_out.ch) data_out.Write([]byte(commitId)) data_out.ch <- '\x00' 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 <- '\x00' 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 && te.isSubmodule() { subCommitId = te.hash return } } }() cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z") cmd.Env = []string{ "GIT_CEILING_DIRECTORIES=" + e.GitPath, "GIT_CONFIG_GLOBAL=/dev/null", } cmd.Dir = filepath.Join(e.GitPath, cwd) cmd.Stdout = &data_in cmd.Stdin = &data_out cmd.Stderr = writeFunc(func(data []byte) (int, error) { e.Logger.LogError("%s", data) return len(data), nil }) e.Log("command run: %v", cmd.Args) e.Error = cmd.Run() foundLock.Lock() return subCommitId, len(subCommitId) == len(commitId) }