175 lines
3.7 KiB
Go
175 lines
3.7 KiB
Go
package vcs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
)
|
|
|
|
const gitTimeout = 10 * time.Second
|
|
|
|
type git struct {
|
|
bin string
|
|
top string
|
|
root string
|
|
prefix string
|
|
}
|
|
|
|
func openGit(projectRoot string) (*git, bool) {
|
|
bin, err := exec.LookPath("git")
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
abs, err := filepath.Abs(projectRoot)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
g := &git{bin: bin, root: abs}
|
|
out, err := g.run(abs, "rev-parse", "--show-toplevel")
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
g.top = filepath.Clean(strings.TrimSpace(string(out)))
|
|
top, err := filepath.EvalSymlinks(g.top)
|
|
if err != nil {
|
|
top = g.top
|
|
}
|
|
real, err := filepath.EvalSymlinks(abs)
|
|
if err != nil {
|
|
real = abs
|
|
}
|
|
rel, err := filepath.Rel(top, real)
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
return nil, false
|
|
}
|
|
if rel != "." {
|
|
g.prefix = filepath.ToSlash(rel) + "/"
|
|
}
|
|
return g, true
|
|
}
|
|
|
|
func (g *git) Name() string {
|
|
return "git"
|
|
}
|
|
|
|
func (g *git) Root() string {
|
|
return g.top
|
|
}
|
|
|
|
func (g *git) run(dir string, args ...string) ([]byte, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), gitTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, g.bin, append([]string{"-C", dir}, args...)...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
msg := strings.TrimSpace(stderr.String())
|
|
if msg == "" {
|
|
msg = err.Error()
|
|
}
|
|
return nil, errs.New(errs.EditorCommand, "git "+args[0]+" failed: "+msg, log.F("dir", dir))
|
|
}
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
func (g *git) repoPath(rel string) string {
|
|
return g.prefix + strings.TrimPrefix(filepath.ToSlash(rel), "/")
|
|
}
|
|
|
|
func (g *git) projectPath(repo string) (string, bool) {
|
|
if g.prefix == "" {
|
|
return repo, true
|
|
}
|
|
return strings.CutPrefix(repo, g.prefix)
|
|
}
|
|
|
|
func (g *git) Show(rev, rel string) ([]byte, error) {
|
|
if rev == "" {
|
|
rev = "HEAD"
|
|
}
|
|
return g.run(g.top, "show", rev+":"+g.repoPath(rel))
|
|
}
|
|
|
|
func statusOf(x, y byte) FileStatus {
|
|
switch {
|
|
case x == '?' && y == '?':
|
|
return Untracked
|
|
case x == 'U' || y == 'U' || (x == 'A' && y == 'A') || (x == 'D' && y == 'D'):
|
|
return Conflicted
|
|
case x == 'R' || y == 'R':
|
|
return Renamed
|
|
case x == 'A':
|
|
return Added
|
|
case x == 'D' || y == 'D':
|
|
return Deleted
|
|
}
|
|
return Modified
|
|
}
|
|
|
|
func (g *git) Status() (map[string]FileStatus, error) {
|
|
args := []string{"status", "--porcelain=v1", "-z", "--untracked-files=all"}
|
|
if g.prefix != "" {
|
|
args = append(args, "--", strings.TrimSuffix(g.prefix, "/"))
|
|
}
|
|
out, err := g.run(g.top, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseStatus(out, g.projectPath), nil
|
|
}
|
|
|
|
func parseStatus(out []byte, rel func(string) (string, bool)) map[string]FileStatus {
|
|
res := map[string]FileStatus{}
|
|
parts := bytes.Split(out, []byte{0})
|
|
for i := 0; i < len(parts); i++ {
|
|
e := parts[i]
|
|
if len(e) < 4 {
|
|
continue
|
|
}
|
|
st := statusOf(e[0], e[1])
|
|
name := string(e[3:])
|
|
if st == Renamed {
|
|
i++
|
|
}
|
|
if p, ok := rel(name); ok {
|
|
res[p] = st
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (g *git) Log(rel string, n int) ([]Commit, error) {
|
|
if n <= 0 {
|
|
n = 50
|
|
}
|
|
out, err := g.run(g.top, "log", "-n", strconv.Itoa(n), "--format=%H%x1f%h%x1f%an%x1f%at%x1f%s%x1e", "--", g.repoPath(rel))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseLog(out), nil
|
|
}
|
|
|
|
func parseLog(out []byte) []Commit {
|
|
var res []Commit
|
|
for _, rec := range strings.Split(string(out), "\x1e") {
|
|
rec = strings.TrimSpace(rec)
|
|
if rec == "" {
|
|
continue
|
|
}
|
|
f := strings.Split(rec, "\x1f")
|
|
if len(f) < 5 {
|
|
continue
|
|
}
|
|
sec, _ := strconv.ParseInt(f[3], 10, 64)
|
|
res = append(res, Commit{Hash: f[0], Short: f[1], Author: f[2], When: time.Unix(sec, 0), Subject: f[4]})
|
|
}
|
|
return res
|
|
}
|