680 lines
14 KiB
Go
680 lines
14 KiB
Go
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.ErrLogger.Printf("Error identifying gitdir in `%s`: %v\n", gitDir, err)
|
|
e.Error = err
|
|
}
|
|
|
|
refs, err := processRefs(path)
|
|
if err != nil {
|
|
e.ErrLogger.Printf("Error finding branches (%s): %v\n", 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.ErrLogger.Println(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 (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 *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.StdLogger.Println(data)
|
|
return len(data), nil
|
|
})
|
|
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
|
e.ErrLogger.Println(data)
|
|
return len(data), nil
|
|
})
|
|
cmd.Stdin = nil
|
|
|
|
e.StdLogger.Printf("git execute: %#v\n", 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
|
|
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 *RequestHandler) GitCatFile(cwd, commitId, filename string) []byte {
|
|
var done sync.Mutex
|
|
var data []byte
|
|
|
|
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 {
|
|
e.Error = err
|
|
e.ErrLogger.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 {
|
|
e.Error = err
|
|
e.ErrLogger.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)
|
|
if err != nil {
|
|
e.Error = err
|
|
e.ErrLogger.Printf("Error reading blob data: %v\n", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
e.Error = fmt.Errorf("file not found: '%s'", filename)
|
|
e.ErrLogger.Println(e.Error.Error())
|
|
}()
|
|
|
|
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.ErrLogger.Println(data)
|
|
return len(data), nil
|
|
})
|
|
e.StdLogger.Printf("command run: %v\n", cmd.Args)
|
|
e.Error = cmd.Run()
|
|
|
|
done.Lock()
|
|
return data
|
|
}
|
|
|
|
func (e *RequestHandler) GitSubmoduleList(cwd, commitId string) map[string]string {
|
|
var done sync.Mutex
|
|
submoduleList := make(map[string]string)
|
|
|
|
if e.HasError() {
|
|
return submoduleList
|
|
}
|
|
|
|
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 {
|
|
e.Error = err
|
|
e.ErrLogger.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 {
|
|
e.Error = err
|
|
e.ErrLogger.Printf("Error parsing git tree: %v\n", 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) {
|
|
e.ErrLogger.Println(data)
|
|
return len(data), nil
|
|
})
|
|
e.StdLogger.Printf("command run: %v\n", cmd.Args)
|
|
e.Error = cmd.Run()
|
|
|
|
done.Lock()
|
|
return submoduleList
|
|
}
|
|
|
|
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.StdLogger.Printf("getting commit id '%s' from git at '%s' with packageName: %s\n", 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.ErrLogger.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 {
|
|
e.Error = err
|
|
e.ErrLogger.Printf("Error parsing git tree: %v\n", 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.ErrLogger.Println(data)
|
|
return len(data), nil
|
|
})
|
|
e.StdLogger.Printf("command run: %v\n", cmd.Args)
|
|
e.Error = cmd.Run()
|
|
|
|
foundLock.Lock()
|
|
return subCommitId, len(subCommitId) == len(commitId)
|
|
}
|