You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// diffMaxBytes caps the raw unified-diff stdout to keep MCP payloads / memory
|
||||
// bounded. Larger diffs are truncated and flagged via DiffResult.Truncated.
|
||||
const diffMaxBytes = 1 << 20 // 1 MiB
|
||||
|
||||
// DiffOptions controls `git diff` argument building.
|
||||
//
|
||||
// - Ref/Against both empty => working-tree vs HEAD (the default).
|
||||
// - Ref set, Against empty => `git diff <Ref>` (working-tree vs Ref).
|
||||
// - Ref and Against both set => `git diff <Ref>..<Against>` (two refs).
|
||||
// - Staged => add `--cached` (index vs HEAD); mutually composes with refs.
|
||||
// - Paths => restrict to the given pathspecs (passed after `--`).
|
||||
// - ContextLines => `-U<n>` when > 0 (git default 3 otherwise).
|
||||
type DiffOptions struct {
|
||||
Ref string
|
||||
Against string
|
||||
Staged bool
|
||||
Paths []string
|
||||
ContextLines int
|
||||
}
|
||||
|
||||
// DiffResult carries the raw unified-diff text. The viewer parses it; the git
|
||||
// layer never parses the hunks. Truncated reports whether stdout exceeded the
|
||||
// 1 MiB cap and was cut.
|
||||
type DiffResult struct {
|
||||
Diff string
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
// Diff returns the unified diff of dir per opts. The output is the raw `git
|
||||
// diff --no-color` text (capped at 1 MiB). On a clean tree the diff is empty.
|
||||
func (r *DefaultRunner) Diff(ctx context.Context, dir string, opts DiffOptions) (DiffResult, error) {
|
||||
// 安全护栏:ref/against 来自 MCP/HTTP 入参,可能未经分支校验。以 '-' 开头的值会被
|
||||
// git 当作选项解析(如 --output=、-O<file>),故一律拒绝,杜绝参数注入。
|
||||
if strings.HasPrefix(opts.Ref, "-") || strings.HasPrefix(opts.Against, "-") {
|
||||
return DiffResult{}, fmt.Errorf("git diff: ref/against must not begin with '-': %q %q", opts.Ref, opts.Against)
|
||||
}
|
||||
ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout)
|
||||
defer cancel()
|
||||
args := buildDiffArgs(opts)
|
||||
out, _, err := r.exec(ctx, dir, nil, args...)
|
||||
if err != nil {
|
||||
return DiffResult{}, err
|
||||
}
|
||||
res := DiffResult{}
|
||||
if len(out) > diffMaxBytes {
|
||||
out = out[:diffMaxBytes]
|
||||
res.Truncated = true
|
||||
}
|
||||
res.Diff = string(out)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// buildDiffArgs assembles the `git diff` argument slice from opts. Refs are
|
||||
// never user-supplied paths (workspace package validates branches), so they are
|
||||
// passed positionally; pathspecs go after the `--` separator to disambiguate.
|
||||
func buildDiffArgs(opts DiffOptions) []string {
|
||||
args := []string{"diff", "--no-color"}
|
||||
if opts.Staged {
|
||||
args = append(args, "--cached")
|
||||
}
|
||||
if opts.ContextLines > 0 {
|
||||
args = append(args, "-U"+itoa(opts.ContextLines))
|
||||
}
|
||||
switch {
|
||||
case opts.Ref != "" && opts.Against != "":
|
||||
args = append(args, opts.Ref+".."+opts.Against)
|
||||
case opts.Ref != "":
|
||||
args = append(args, opts.Ref)
|
||||
}
|
||||
if len(opts.Paths) > 0 {
|
||||
args = append(args, "--")
|
||||
args = append(args, opts.Paths...)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// itoa is a tiny strconv.Itoa avoiding an import just for one call site.
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
buf[i] = '-'
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
Reference in New Issue
Block a user