When project is advanced, and we have other package changes to same project, the project git changes need to be rebased. The simplest way of doing this is to skip all the submodule conflicts and re-create them. This allows the submodules changes to be mergeable again.
1137 lines
27 KiB
Go
1137 lines
27 KiB
Go
package common
|
|
|
|
/*
|
|
* This file is part of Autogits.
|
|
*
|
|
* Copyright © 2024 SUSE LLC
|
|
*
|
|
* Autogits is free software: you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License as published by the Free Software
|
|
* Foundation, either version 2 of the License, or (at your option) any later
|
|
* version.
|
|
*
|
|
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
//go:generate mockgen -source=git_utils.go -destination=mock/git_utils.go -typed
|
|
|
|
type GitSubmoduleLister interface {
|
|
GitSubmoduleList(gitPath, commitId string) (submoduleList map[string]string, err error)
|
|
GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool)
|
|
}
|
|
|
|
type GitStatusLister interface {
|
|
GitStatus(cwd string) ([]GitStatusData, error)
|
|
}
|
|
|
|
type GitDiffLister interface {
|
|
GitDiff(cwd, base, head string) (string, error)
|
|
}
|
|
|
|
type Git interface {
|
|
// error if git, but wrong remote
|
|
GitClone(repo, branch, remoteUrl string) (string, error) // clone, or check if path is already checked out remote and force pulls, error otherwise. Return remotename, errror
|
|
|
|
GitParseCommits(cwd string, commitIDs []string) (parsedCommits []GitCommit, err error)
|
|
GitCatFile(cwd, commitId, filename string) (data []byte, err error)
|
|
GetPath() string
|
|
|
|
GitBranchHead(gitDir, branchName string) (string, error)
|
|
GitRemoteHead(gitDir, remoteName, branchName string) (string, error)
|
|
io.Closer
|
|
|
|
GitSubmoduleLister
|
|
GitStatusLister
|
|
|
|
GitExecWithOutputOrPanic(cwd string, params ...string) string
|
|
GitExecOrPanic(cwd string, params ...string)
|
|
GitExec(cwd string, params ...string) error
|
|
GitExecWithOutput(cwd string, params ...string) (string, error)
|
|
|
|
GitDiffLister
|
|
}
|
|
|
|
type GitHandlerImpl struct {
|
|
GitPath string
|
|
GitCommiter string
|
|
GitEmail string
|
|
|
|
lock *sync.Mutex
|
|
}
|
|
|
|
func (s *GitHandlerImpl) GetPath() string {
|
|
return s.GitPath
|
|
}
|
|
|
|
type GitHandlerGenerator interface {
|
|
CreateGitHandler(org string) (Git, error)
|
|
ReadExistingPath(org string) (Git, error)
|
|
|
|
ReleaseLock(path string)
|
|
}
|
|
|
|
type gitHandlerGeneratorImpl struct {
|
|
path string
|
|
git_author string
|
|
email string
|
|
|
|
lock_lock sync.Mutex
|
|
lock map[string]*sync.Mutex // per org
|
|
}
|
|
|
|
func AllocateGitWorkTree(basePath, gitAuthor, email string) (*gitHandlerGeneratorImpl, error) {
|
|
if fi, err := os.Stat(basePath); err != nil || !fi.IsDir() {
|
|
return nil, fmt.Errorf("Git basepath not a valid directory: %s %w", basePath, err)
|
|
}
|
|
|
|
if fi, err := os.Stat(basePath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
if err = os.MkdirAll(basePath, 0o700); err != nil {
|
|
return nil, fmt.Errorf("Cannot create git directory structure: %s: %w", basePath, err)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("Error checking git directory strcture: %s: %w", basePath, err)
|
|
}
|
|
} else if !fi.IsDir() {
|
|
return nil, fmt.Errorf("Invalid git directory structure: %s != directory", basePath)
|
|
}
|
|
|
|
return &gitHandlerGeneratorImpl{
|
|
path: basePath,
|
|
git_author: gitAuthor,
|
|
email: email,
|
|
|
|
lock: make(map[string]*sync.Mutex),
|
|
}, nil
|
|
}
|
|
|
|
func (s *gitHandlerGeneratorImpl) CreateGitHandler(org string) (Git, error) {
|
|
path := path.Join(s.path, org)
|
|
if fs, err := os.Stat(path); (err != nil && !os.IsNotExist(err)) || (err == nil && !fs.IsDir()) {
|
|
return nil, err
|
|
} else if err != nil && os.IsNotExist(err) {
|
|
if err := os.MkdirAll(path, 0o777); err != nil && !os.IsExist(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return s.ReadExistingPath(org)
|
|
}
|
|
|
|
func (s *gitHandlerGeneratorImpl) ReadExistingPath(org string) (Git, error) {
|
|
LogDebug("Locking git org:", org)
|
|
s.lock_lock.Lock()
|
|
defer s.lock_lock.Unlock()
|
|
|
|
if _, ok := s.lock[org]; !ok {
|
|
s.lock[org] = &sync.Mutex{}
|
|
}
|
|
s.lock[org].Lock()
|
|
|
|
git := &GitHandlerImpl{
|
|
GitCommiter: s.git_author,
|
|
GitEmail: s.email,
|
|
GitPath: path.Join(s.path, org),
|
|
lock: s.lock[org],
|
|
}
|
|
|
|
return git, nil
|
|
}
|
|
|
|
func (s *gitHandlerGeneratorImpl) ReleaseLock(org string) {
|
|
m, ok := s.lock[org]
|
|
if ok {
|
|
LogDebug("Unlocking git org:", org)
|
|
m.Unlock()
|
|
}
|
|
}
|
|
|
|
//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 (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error) {
|
|
LogDebug("Cloning", remoteUrl, " repo:", repo, " branch:", branch)
|
|
remoteUrlComp, err := ParseGitRemoteUrl(remoteUrl)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Cannot parse remote URL: %w", err)
|
|
}
|
|
remoteBranch := "HEAD"
|
|
if len(branch) == 0 && remoteUrlComp != nil {
|
|
branch = remoteUrlComp.Commit
|
|
remoteBranch = branch
|
|
} else if len(branch) > 0 {
|
|
remoteBranch = branch
|
|
}
|
|
remoteName := remoteUrlComp.RemoteName()
|
|
if remoteUrlComp != nil {
|
|
LogDebug("Clone", *remoteUrlComp, " -> ", remoteName)
|
|
} else {
|
|
LogDebug("Clone", "[default] -> ", remoteName)
|
|
}
|
|
|
|
remoteRef := remoteName + "/" + remoteBranch
|
|
if fi, err := os.Stat(path.Join(e.GitPath, repo)); os.IsNotExist(err) {
|
|
if err = e.GitExec("", "clone", "--origin", remoteName, remoteUrl, repo); err != nil {
|
|
return remoteName, err
|
|
}
|
|
} else if err != nil || !fi.IsDir() {
|
|
return remoteName, fmt.Errorf("Clone location not a directory or Stat error: %w", err)
|
|
} else {
|
|
if u, err := e.GitExecWithOutput(repo, "remote", "get-url", remoteName); err != nil {
|
|
e.GitExecOrPanic(repo, "remote", "add", remoteName, remoteUrl)
|
|
} else if clonedRemote := strings.TrimSpace(u); clonedRemote != remoteUrl {
|
|
e.GitExecOrPanic(repo, "remote", "set-url", remoteName, remoteUrl)
|
|
}
|
|
|
|
// check if we have submodule to deinit
|
|
if list, _ := e.GitSubmoduleList(repo, "HEAD"); len(list) > 0 {
|
|
e.GitExecOrPanic(repo, "submodule", "deinit", "--all", "--force")
|
|
}
|
|
|
|
e.GitExecOrPanic(repo, "fetch", "--prune", remoteName, remoteBranch)
|
|
}
|
|
|
|
refsBytes, err := os.ReadFile(path.Join(e.GitPath, repo, ".git/refs/remotes", remoteName, "HEAD"))
|
|
if err != nil {
|
|
LogError("Cannot read HEAD of remote", remoteName)
|
|
return remoteName, fmt.Errorf("Cannot read HEAD of remote %s", remoteName)
|
|
}
|
|
|
|
refs := string(refsBytes)
|
|
if refs[0:5] != "ref: " {
|
|
LogError("Unexpected format of remote HEAD ref:", refs)
|
|
return remoteName, fmt.Errorf("Unexpected format of remote HEAD ref: %s", refs)
|
|
}
|
|
|
|
if len(branch) == 0 || branch == "HEAD" {
|
|
remoteRef = strings.TrimSpace(refs[5:])
|
|
branch = remoteRef[strings.LastIndex(remoteRef, "/")+1:]
|
|
LogDebug("remoteRef", remoteRef)
|
|
LogDebug("branch", branch)
|
|
}
|
|
|
|
args := []string{"fetch", "--prune", remoteName, branch}
|
|
if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
|
|
args = slices.Insert(args, 1, "--unshallow")
|
|
}
|
|
e.GitExecOrPanic(repo, args...)
|
|
return remoteName, e.GitExec(repo, "checkout", "--track", "-B", branch, remoteRef)
|
|
}
|
|
|
|
func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error) {
|
|
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--hash", "--verify", "refs/heads/"+branchName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Can't find default branch: %s", branchName)
|
|
}
|
|
|
|
return strings.TrimSpace(id), nil
|
|
}
|
|
|
|
func (e *GitHandlerImpl) GitRemoteHead(gitDir, remote, branchName string) (string, error) {
|
|
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--hash", "--verify", "refs/remotes/"+remote+"/"+branchName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Can't find default branch: %s", branchName)
|
|
}
|
|
|
|
return strings.TrimSpace(id), nil
|
|
}
|
|
|
|
func (e *GitHandlerImpl) Close() error {
|
|
LogDebug("Unlocking git lock")
|
|
e.lock.Unlock()
|
|
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 *GitHandlerImpl) GitExecWithOutputOrPanic(cwd string, params ...string) string {
|
|
out, err := e.GitExecWithOutput(cwd, params...)
|
|
if err != nil {
|
|
LogError("git command failed:", params, "@", cwd, "err:", err)
|
|
panic(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (e *GitHandlerImpl) GitExecOrPanic(cwd string, params ...string) {
|
|
if err := e.GitExec(cwd, params...); err != nil {
|
|
LogError("git command failed:", params, "@", cwd, "err:", err)
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (e *GitHandlerImpl) GitExec(cwd string, params ...string) error {
|
|
_, err := e.GitExecWithOutput(cwd, params...)
|
|
return err
|
|
}
|
|
|
|
var ExtraGitParams []string
|
|
|
|
func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (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",
|
|
}
|
|
if len(ExtraGitParams) > 0 {
|
|
cmd.Env = append(cmd.Env, ExtraGitParams...)
|
|
}
|
|
cmd.Dir = filepath.Join(e.GitPath, cwd)
|
|
cmd.Stdin = nil
|
|
|
|
LogDebug("git execute @", cwd, ":", cmd.Args)
|
|
out, err := cmd.CombinedOutput()
|
|
LogDebug(string(out))
|
|
if err != nil {
|
|
LogError("git", cmd.Args, " error:", err)
|
|
return "", fmt.Errorf("error executing: git %#v \n%s\n err: %w", cmd.Args, out, err)
|
|
}
|
|
|
|
return string(out), 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 GitCommit struct {
|
|
Tree string
|
|
Msg string
|
|
}
|
|
|
|
type GitTreeEntry struct {
|
|
name string
|
|
mode int
|
|
hash string
|
|
|
|
size int
|
|
}
|
|
|
|
type GitTree struct {
|
|
items []GitTreeEntry
|
|
}
|
|
|
|
func (t *GitTreeEntry) isSubmodule() bool {
|
|
return (t.mode & 0170000) == 0160000
|
|
}
|
|
|
|
func (t *GitTreeEntry) isTree() bool {
|
|
return (t.mode & 0170000) == 0040000
|
|
}
|
|
|
|
func (t *GitTreeEntry) 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(oldHdr [2]string, data <-chan byte) ([2]string, int, error) {
|
|
hdr := make([]byte, 0, 60)
|
|
val := make([]byte, 0, 1000)
|
|
|
|
c := <-data
|
|
size := 1
|
|
if c != '\n' { // end of header marker
|
|
for ; c != ' '; c = <-data {
|
|
hdr = append(hdr, c)
|
|
size++
|
|
}
|
|
if size == 1 { // continuation header here
|
|
hdr = []byte(oldHdr[0])
|
|
val = append([]byte(oldHdr[1]), '\n')
|
|
}
|
|
for c := <-data; c != '\n'; c = <-data {
|
|
val = append(val, c)
|
|
size++
|
|
}
|
|
size++
|
|
}
|
|
|
|
return [2]string{string(hdr), string(val)}, size, 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--
|
|
}
|
|
|
|
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) (GitCommit, error) {
|
|
hdr, err := parseGitMsg(data)
|
|
if err != nil {
|
|
return GitCommit{}, err
|
|
} else if hdr.itemType != "commit" {
|
|
return GitCommit{}, fmt.Errorf("expected commit but parsed %s", hdr.itemType)
|
|
}
|
|
|
|
var c GitCommit
|
|
l := hdr.size
|
|
for {
|
|
var hdr [2]string
|
|
hdr, size, err := parseGitCommitHdr(hdr, data)
|
|
if err != nil {
|
|
return GitCommit{}, nil
|
|
}
|
|
l -= size
|
|
|
|
if size == 1 {
|
|
break
|
|
}
|
|
|
|
switch hdr[0] {
|
|
case "tree":
|
|
c.Tree = hdr[1]
|
|
}
|
|
}
|
|
|
|
c.Msg, err = parseGitCommitMsg(data, l)
|
|
return c, err
|
|
}
|
|
|
|
func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
|
var e GitTreeEntry
|
|
|
|
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) (GitTree, error) {
|
|
hdr, err := parseGitMsg(data)
|
|
if err != nil {
|
|
return GitTree{}, err
|
|
}
|
|
|
|
// max capacity to length of hash
|
|
t := GitTree{items: make([]GitTreeEntry, 0, hdr.size/len(hdr.hash))}
|
|
parsedLen := 0
|
|
for parsedLen < hdr.size {
|
|
entry, err := parseTreeEntry(data, len(hdr.hash)/2)
|
|
if err != nil {
|
|
return GitTree{}, 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
|
|
}
|
|
|
|
func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsedCommits []GitCommit, err error) {
|
|
var done sync.Mutex
|
|
|
|
done.Lock()
|
|
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
|
parsedCommits = make([]GitCommit, 0, len(commitIDs))
|
|
|
|
go func() {
|
|
defer done.Unlock()
|
|
defer close(data_out.ch)
|
|
|
|
for _, id := range commitIDs {
|
|
data_out.Write([]byte(id))
|
|
data_out.ch <- '\x00'
|
|
c, e := parseGitCommit(data_in.ch)
|
|
if e != nil {
|
|
err = fmt.Errorf("Error parsing git commit: %w", e)
|
|
return
|
|
}
|
|
|
|
parsedCommits = append(parsedCommits, c)
|
|
}
|
|
}()
|
|
|
|
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
|
|
cmd.Env = []string{
|
|
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
|
"GIT_LFS_SKIP_SMUDGE=1",
|
|
"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) {
|
|
LogError(string(data))
|
|
return len(data), nil
|
|
})
|
|
LogDebug("command run:", cmd.Args)
|
|
if e := cmd.Run(); e != nil {
|
|
LogError(e)
|
|
close(data_in.ch)
|
|
close(data_out.ch)
|
|
return nil, e
|
|
}
|
|
|
|
done.Lock()
|
|
return
|
|
}
|
|
|
|
// TODO: support sub-trees
|
|
func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte, err error) {
|
|
var done sync.Mutex
|
|
|
|
done.Lock()
|
|
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
|
|
|
go func() {
|
|
defer done.Unlock()
|
|
defer close(data_out.ch)
|
|
|
|
data_out.Write([]byte(commitId))
|
|
data_out.ch <- '\x00'
|
|
|
|
var c GitCommit
|
|
c, err = parseGitCommit(data_in.ch)
|
|
if err != nil {
|
|
LogError("Error parsing git commit:", err)
|
|
return
|
|
}
|
|
data_out.Write([]byte(c.Tree))
|
|
data_out.ch <- '\x00'
|
|
|
|
var tree GitTree
|
|
tree, err = parseGitTree(data_in.ch)
|
|
|
|
if err != nil {
|
|
LogError("Error parsing git tree:", err)
|
|
return
|
|
}
|
|
|
|
for _, te := range tree.items {
|
|
if te.isBlob() && te.name == filename {
|
|
LogInfo("blob", te.hash)
|
|
data_out.Write([]byte(te.hash))
|
|
data_out.ch <- '\x00'
|
|
data, err = parseGitBlob(data_in.ch)
|
|
return
|
|
}
|
|
}
|
|
|
|
LogError("file not found:", filename)
|
|
}()
|
|
|
|
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
|
|
cmd.Env = []string{
|
|
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
|
"GIT_LFS_SKIP_SMUDGE=1",
|
|
"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) {
|
|
LogError(string(data))
|
|
return len(data), nil
|
|
})
|
|
LogDebug("command run:", cmd.Args)
|
|
if e := cmd.Run(); e != nil {
|
|
LogError(e)
|
|
close(data_in.ch)
|
|
close(data_out.ch)
|
|
return nil, e
|
|
}
|
|
done.Lock()
|
|
return
|
|
}
|
|
|
|
// return (filename) -> (hash) map for all submodules
|
|
func (e *GitHandlerImpl) GitSubmoduleList(gitPath, 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)}, ChanIO{make(chan byte)}
|
|
|
|
LogDebug("Getting submodules for:", commitId)
|
|
|
|
go func() {
|
|
defer done.Unlock()
|
|
defer close(data_out.ch)
|
|
|
|
data_out.Write([]byte(commitId))
|
|
data_out.ch <- '\x00'
|
|
var c GitCommit
|
|
c, err = parseGitCommit(data_in.ch)
|
|
if err != nil {
|
|
err = fmt.Errorf("Error parsing git commit. Err: %w", err)
|
|
return
|
|
}
|
|
|
|
trees := make(map[string]string)
|
|
trees[""] = c.Tree
|
|
|
|
for len(trees) > 0 {
|
|
for p, tree := range trees {
|
|
delete(trees, p)
|
|
|
|
data_out.Write([]byte(tree))
|
|
data_out.ch <- '\x00'
|
|
var tree GitTree
|
|
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.isTree() {
|
|
trees[p+te.name+"/"] = te.hash
|
|
} else if te.isSubmodule() {
|
|
submoduleList[p+te.name] = te.hash
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
|
|
cmd.Env = []string{
|
|
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
|
"GIT_LFS_SKIP_SMUDGE=1",
|
|
"GIT_CONFIG_GLOBAL=/dev/null",
|
|
}
|
|
cmd.Dir = filepath.Join(e.GitPath, gitPath)
|
|
cmd.Stdout = &data_in
|
|
cmd.Stdin = &data_out
|
|
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
|
LogError(string(data))
|
|
return len(data), nil
|
|
})
|
|
LogDebug("command run:", cmd.Args)
|
|
if e := cmd.Run(); e != nil {
|
|
LogError(e)
|
|
close(data_in.ch)
|
|
close(data_out.ch)
|
|
return submoduleList, e
|
|
}
|
|
|
|
done.Lock()
|
|
return submoduleList, err
|
|
}
|
|
|
|
func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool) {
|
|
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(1)
|
|
LogDebug("getting commit id", commitId, "from git at", cwd, "with packageName:", packageName)
|
|
|
|
go func() {
|
|
defer func() {
|
|
if recover() != nil {
|
|
subCommitId = ""
|
|
commitId = "ok"
|
|
valid = false
|
|
}
|
|
}()
|
|
|
|
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 {
|
|
LogError("Error parsing git commit:", err)
|
|
panic(err)
|
|
}
|
|
data_out.Write([]byte(c.Tree))
|
|
data_out.ch <- '\x00'
|
|
tree, err := parseGitTree(data_in.ch)
|
|
|
|
if err != nil {
|
|
LogError("Error parsing git tree:", err)
|
|
panic(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_LFS_SKIP_SMUDGE=1",
|
|
"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) {
|
|
LogError(string(data))
|
|
return len(data), nil
|
|
})
|
|
LogDebug("command run:", cmd.Args)
|
|
if e := cmd.Run(); e != nil {
|
|
LogError(e)
|
|
close(data_in.ch)
|
|
close(data_out.ch)
|
|
return subCommitId, false
|
|
}
|
|
|
|
wg.Wait()
|
|
return subCommitId, len(subCommitId) > 0
|
|
}
|
|
|
|
const (
|
|
GitStatus_Untracked = 0
|
|
GitStatus_Modified = 1
|
|
GitStatus_Ignored = 2
|
|
GitStatus_Unmerged = 3 // States[0..3] -- Stage1, Stage2, Stage3 of merge objects
|
|
GitStatus_Renamed = 4 // orig name in States[0]
|
|
)
|
|
|
|
type GitStatusData struct {
|
|
Path string
|
|
Status int
|
|
States [3]string
|
|
|
|
/*
|
|
<sub> A 4 character field describing the submodule state.
|
|
"N..." when the entry is not a submodule.
|
|
"S<c><m><u>" when the entry is a submodule.
|
|
<c> is "C" if the commit changed; otherwise ".".
|
|
<m> is "M" if it has tracked changes; otherwise ".".
|
|
<u> is "U" if there are untracked changes; otherwise ".".
|
|
*/
|
|
SubmoduleChanges 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 || c == ' ' {
|
|
return string(str), nil
|
|
}
|
|
str = append(str, c)
|
|
}
|
|
}
|
|
|
|
func parseGitStatusStringWithSpace(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 = parseGitStatusStringWithSpace(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case '2':
|
|
var err error
|
|
if err = skipGitStatusEntry(data, 9); err != nil {
|
|
return nil, err
|
|
}
|
|
ret.Status = GitStatus_Renamed
|
|
ret.Path, err = parseGitStatusStringWithSpace(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.States[0], err = parseGitStatusStringWithSpace(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 = parseGitStatusStringWithSpace(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 = parseGitStatusStringWithSpace(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case 'u':
|
|
var err error
|
|
if err = skipGitStatusEntry(data, 2); err != nil {
|
|
return nil, err
|
|
}
|
|
if ret.SubmoduleChanges, err = parseGitStatusString(data); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = skipGitStatusEntry(data, 4); 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 = parseGitStatusStringWithSpace(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) GitStatus(cwd string) (ret []GitStatusData, err error) {
|
|
LogDebug("getting git-status()")
|
|
|
|
cmd := exec.Command("/usr/bin/git", "status", "--porcelain=2", "-z")
|
|
cmd.Env = []string{
|
|
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
|
"GIT_LFS_SKIP_SMUDGE=1",
|
|
"GIT_CONFIG_GLOBAL=/dev/null",
|
|
}
|
|
cmd.Dir = filepath.Join(e.GitPath, cwd)
|
|
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
|
LogError(string(data))
|
|
return len(data), nil
|
|
})
|
|
LogDebug("command run:", cmd.Args)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
LogError("Error running command", cmd.Args, err)
|
|
}
|
|
|
|
return parseGitStatusData(bufio.NewReader(bytes.NewReader(out)))
|
|
}
|
|
|
|
func (e *GitHandlerImpl) GitDiff(cwd, base, head string) (string, error) {
|
|
LogDebug("getting diff from", base, "..", head)
|
|
|
|
cmd := exec.Command("/usr/bin/git", "diff", base+".."+head)
|
|
cmd.Env = []string{
|
|
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
|
"GIT_LFS_SKIP_SMUDGE=1",
|
|
"GIT_CONFIG_GLOBAL=/dev/null",
|
|
}
|
|
cmd.Dir = filepath.Join(e.GitPath, cwd)
|
|
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
|
LogError(string(data))
|
|
return len(data), nil
|
|
})
|
|
LogDebug("command run:", cmd.Args)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
LogError("Error running command", cmd.Args, err)
|
|
}
|
|
|
|
return string(out), nil
|
|
}
|