150 lines
3.4 KiB
Go
150 lines
3.4 KiB
Go
package git
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"laodingbot/internal/logger"
|
|
)
|
|
|
|
// Tool provides common Git operations through a safe command whitelist.
|
|
type Tool struct {
|
|
repoDir string
|
|
allowedSubcmds map[string]struct{}
|
|
timeout time.Duration
|
|
maxOutputChars int
|
|
log *logger.Logger
|
|
}
|
|
|
|
// New creates a new git tool.
|
|
func New(repoDir string, timeout time.Duration, maxOutputChars int, log *logger.Logger) *Tool {
|
|
absRepo, err := filepath.Abs(strings.TrimSpace(repoDir))
|
|
if err != nil {
|
|
absRepo = strings.TrimSpace(repoDir)
|
|
}
|
|
if timeout <= 0 {
|
|
timeout = 20 * time.Second
|
|
}
|
|
if maxOutputChars <= 0 {
|
|
maxOutputChars = 4000
|
|
}
|
|
|
|
allowed := map[string]struct{}{
|
|
"status": {},
|
|
"log": {},
|
|
"show": {},
|
|
"diff": {},
|
|
"branch": {},
|
|
"checkout": {},
|
|
"switch": {},
|
|
"restore": {},
|
|
"add": {},
|
|
"commit": {},
|
|
"reset": {},
|
|
"revert": {},
|
|
"merge": {},
|
|
"rebase": {},
|
|
"cherry-pick": {},
|
|
"fetch": {},
|
|
"pull": {},
|
|
"push": {},
|
|
"remote": {},
|
|
"tag": {},
|
|
"stash": {},
|
|
"blame": {},
|
|
"rev-parse": {},
|
|
}
|
|
|
|
if log != nil {
|
|
log.Infof("git tool initialized repo_dir=%s timeout=%s max_output_chars=%d", absRepo, timeout, maxOutputChars)
|
|
}
|
|
|
|
return &Tool{
|
|
repoDir: absRepo,
|
|
allowedSubcmds: allowed,
|
|
timeout: timeout,
|
|
maxOutputChars: maxOutputChars,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
func (t *Tool) Name() string { return "git" }
|
|
|
|
func (t *Tool) Description() string {
|
|
return "Run common git commands inside repository. Input examples: status --short | log --oneline -n 10 | add . | commit -m fix | branch"
|
|
}
|
|
|
|
func (t *Tool) Call(ctx context.Context, input string) (string, error) {
|
|
trimmed := strings.TrimSpace(input)
|
|
if trimmed == "" {
|
|
if t.log != nil {
|
|
t.log.Warnf("git tool rejected empty command")
|
|
}
|
|
return "", fmt.Errorf("empty git command")
|
|
}
|
|
|
|
parts := strings.Fields(trimmed)
|
|
if len(parts) == 0 {
|
|
return "", fmt.Errorf("empty git command")
|
|
}
|
|
|
|
if strings.EqualFold(parts[0], "git") {
|
|
parts = parts[1:]
|
|
}
|
|
if len(parts) == 0 {
|
|
return "", fmt.Errorf("missing git subcommand")
|
|
}
|
|
|
|
subcmd := strings.ToLower(parts[0])
|
|
if _, ok := t.allowedSubcmds[subcmd]; !ok {
|
|
if t.log != nil {
|
|
t.log.Warnf("git tool rejected subcommand=%s", subcmd)
|
|
}
|
|
return "", fmt.Errorf("unsupported git subcommand: %s", subcmd)
|
|
}
|
|
|
|
runCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
|
defer cancel()
|
|
|
|
args := append([]string{subcmd}, parts[1:]...)
|
|
if t.log != nil {
|
|
t.log.Infof("git command start subcommand=%s args=%d full=%q", subcmd, len(parts)-1, strings.Join(args, " "))
|
|
}
|
|
|
|
cmd := exec.CommandContext(runCtx, "git", args...)
|
|
cmd.Dir = t.repoDir
|
|
cmd.Env = append(cmd.Environ(),
|
|
"GIT_TERMINAL_PROMPT=0",
|
|
"GIT_PAGER=cat",
|
|
"GIT_EDITOR=true",
|
|
)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
outText := strings.TrimSpace(string(out))
|
|
if len(outText) > t.maxOutputChars {
|
|
outText = outText[:t.maxOutputChars]
|
|
}
|
|
|
|
if err != nil {
|
|
if t.log != nil {
|
|
t.log.Errorf("git command failed subcommand=%s err=%v output=%q", subcmd, err, outText)
|
|
}
|
|
if outText == "" {
|
|
return "", err
|
|
}
|
|
return outText, err
|
|
}
|
|
|
|
if t.log != nil {
|
|
t.log.Infof("git command success subcommand=%s output_bytes=%d", subcmd, len(out))
|
|
}
|
|
if outText == "" {
|
|
return "ok", nil
|
|
}
|
|
return outText, nil
|
|
}
|