package common import ( "fmt" "io" "log" "os" "os/exec" "path" "path/filepath" "strings" "sync" ) type GitHandler struct { DebugLogger bool GitPath string GitCommiter string GitEmail string } func CreateGitHandler(git_author, email, name string) (*GitHandler, error) { var err error git := new(GitHandler) git.GitCommiter = git_author git.GitPath, err = os.MkdirTemp("", name) if err != nil { return nil, fmt.Errorf("Cannot create temp dir: %w", err) } if err = os.Chmod(git.GitPath, 0700); err != nil { return nil, fmt.Errorf("Cannot fix permissions of temp dir: %w", err) } return git, nil } //func (h *GitHandler) 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 *GitHandler) GitBranchHead(gitDir, branchName string) (string, error) { path, err := findGitDir(path.Join(e.GitPath, gitDir)) if err != nil { return "", fmt.Errorf("Error identifying gitdir in `%s`: %w", gitDir, err) } refs, err := processRefs(path) if err != nil { return "", fmt.Errorf("Error finding branches (%s): %w\n", branchName, err) } for _, ref := range refs { if ref.Branch == branchName { return ref.Id, nil } } return "", fmt.Errorf("Can't find default remote branch: %s", branchName) } func (e *GitHandler) Close() error { if err := os.RemoveAll(e.GitPath); err != nil { return err } e.GitPath = "" return nil } type writeFunc func(data []byte) (int, error) func (f writeFunc) Write(data []byte) (int, error) { return f(data) } func (h writeFunc) UnmarshalText(text []byte) error { _, err := h.Write(text) return err } func (h writeFunc) Close() error { _, err := h.Write(nil) return err } func (e *GitHandler) GitExec(cwd string, params ...string) error { 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, "GIT_COMMITTER_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.Stdin = nil if e.DebugLogger { log.Printf("git execute: %#v\n", cmd.Args) } out, err := cmd.CombinedOutput() if e.DebugLogger { log.Println(string(out)) } if err != nil { if e.DebugLogger { log.Printf(" *** error: %v\n", err) } return fmt.Errorf("error executing: git %#v \n%s\n err: %w", cmd.Args, out, err) } return nil } 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 var c byte for c = <-data; c != ' ' && c != '\x00'; 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", "blob": break case "missing": if c != '\x00' { return gitMsg{}, fmt.Errorf("Missing format weird") } return gitMsg{ hash: string(id[:]), itemType: "missing", size: 0, }, fmt.Errorf("Object not found: '%s'", string(id)) 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) l-- } // l-- if l != 0 { return "", fmt.Errorf("Unexpected data in the git commit msg: l=%d", l) } 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))} parsedLen := 0 for parsedLen < 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 } c := <-data // \0 read if c != '\x00' { return t, fmt.Errorf("Unexpected character during git tree data read") } if parsedLen != hdr.size { return t, fmt.Errorf("Invalid size of git tree data") } return t, nil } func parseGitBlob(data <-chan byte) ([]byte, error) { hdr, err := parseGitMsg(data) if err != nil { return []byte{}, err } d := make([]byte, hdr.size) for l := 0; l < hdr.size; l++ { d[l] = <-data } eob := <-data if eob != '\x00' { return d, fmt.Errorf("invalid byte read in parseGitBlob") } return d, nil } // TODO: support sub-trees func (e *GitHandler) GitCatFile(cwd, commitId, filename string) (data []byte, err error) { var done sync.Mutex done.Lock() data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)} go func() { defer done.Unlock() defer close(data_out.ch) data_out.Write([]byte(commitId)) data_out.ch <- '\x00' c, err := parseGitCommit(data_in.ch) if err != nil { log.Printf("Error parsing git commit: %v\n", err) return } data_out.Write([]byte(c.Tree)) data_out.ch <- '\x00' tree, err := parseGitTree(data_in.ch) if err != nil { if e.DebugLogger { log.Printf("Error parsing git tree: %v\n", err) } return } for _, te := range tree.items { if te.isBlob() && te.name == filename { data_out.Write([]byte(te.hash)) data_out.ch <- '\x00' data, err = parseGitBlob(data_in.ch) return } } err = fmt.Errorf("file not found: '%s'", filename) }() 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) { if e.DebugLogger { log.Printf(string(data)) } return len(data), nil }) if e.DebugLogger { log.Printf("command run: %v\n", cmd.Args) } err = cmd.Run() done.Lock() return } // return (filename) -> (hash) map for all submodules // TODO: recursive? map different orgs, not just assume '.' for path func (e *GitHandler) GitSubmoduleList(cwd, commitId string) (submoduleList map[string]string, err error) { var done sync.Mutex submoduleList = make(map[string]string) done.Lock() data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)} go func() { defer done.Unlock() defer close(data_out.ch) data_out.Write([]byte(commitId)) data_out.ch <- '\x00' var c commit c, err = parseGitCommit(data_in.ch) if err != nil { err = fmt.Errorf("Error parsing git commit. Err: %w", err) return } data_out.Write([]byte(c.Tree)) data_out.ch <- '\x00' var tree tree tree, err = parseGitTree(data_in.ch) if err != nil { err = fmt.Errorf("Error parsing git tree: %w", err) return } for _, te := range tree.items { if te.isSubmodule() { submoduleList[te.name] = te.hash } } }() 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) { if e.DebugLogger { log.Println(string(data)) } return len(data), nil }) if e.DebugLogger { log.Printf("command run: %v\n", cmd.Args) } err = cmd.Run() done.Lock() return submoduleList, err } func (e *GitHandler) GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool) { defer func() { if recover() != nil { commitId = "" valid = false } }() data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)} var wg sync.WaitGroup wg.Add(1) if e.DebugLogger { log.Printf("getting commit id '%s' from git at '%s' with packageName: %s\n", commitId, cwd, packageName) } go func() { defer wg.Done() defer close(data_out.ch) data_out.Write([]byte(commitId)) data_out.ch <- '\x00' c, err := parseGitCommit(data_in.ch) if err != nil { log.Panicf("Error parsing git commit: %v\n", err) } data_out.Write([]byte(c.Tree)) data_out.ch <- '\x00' tree, err := parseGitTree(data_in.ch) if err != nil { log.Panicf("Error parsing git tree: %v\n", err) } 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) { log.Println(string(data)) return len(data), nil }) if e.DebugLogger { log.Printf("command run: %v\n", cmd.Args) } if err := cmd.Run(); err != nil { log.Printf("Error running command %v, err: %v", cmd.Args, err) } wg.Wait() return subCommitId, len(subCommitId) == len(commitId) }