1package tools
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "strings"
16 "sync"
17 "time"
18
19 "github.com/bnema/zut/packages/core"
20 "github.com/bnema/zut/packages/provider"
21)
22
23const (
24 worktreeConfigKey = "zut.worktrees.path"
25 maxCreateWorktreeGitOutput = 64 * 1024
26 // worktreeRepoIDHashLength keeps the hash short enough to read but wide
27 // enough that distinct repositories do not collide in practice.
28 worktreeRepoIDHashLength = 12
29 worktreeRepoSlugMaxLen = 40
30)
31
32var createWorktreeOperationMu sync.Mutex
33
34// CreateWorktreeTool creates a persistent branch checkout. An unconfigured
35// repository checks out under a stable per-repository directory beneath
36// DefaultRoot. A local zut.worktrees.path Git configuration entry overrides
37// that root for the repository.
38type CreateWorktreeTool struct {
39 CWD string
40 Sandbox *Sandbox
41 DefaultRoot string
42
43 previewMu sync.Mutex
44 preview *createWorktreePreview
45}
46
47type createWorktreeArgs struct {
48 Branch string `json:"branch"`
49}
50
51const createWorktreeSchema = `{
52 "type":"object",
53 "properties":{
54 "branch":{
55 "type":"string",
56 "description":"Name of the new Git branch."
57 }
58 },
59 "required":["branch"]
60}`
61
62func (t *CreateWorktreeTool) Name() string { return "create_worktree" }
63func (t *CreateWorktreeTool) Description() string {
64 return "Create a persistent Git branch worktree under the repository's configured worktree root or zut's global worktrees root."
65}
66func (t *CreateWorktreeTool) Schema() json.RawMessage { return json.RawMessage(createWorktreeSchema) }
67
68// Preview reports the checkout that Execute will produce without modifying the
69// repository or filesystem.
70func (t *CreateWorktreeTool) Preview(ctx context.Context, raw json.RawMessage) (core.ToolResult, error) {
71 plan, err := t.plan(ctx, raw)
72 if err != nil {
73 return core.ToolResult{}, err
74 }
75 t.previewMu.Lock()
76 t.preview = &createWorktreePreview{raw: string(raw), plan: plan}
77 t.previewMu.Unlock()
78 return plan.preview, nil
79}
80
81func (t *CreateWorktreeTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
82 createWorktreeOperationMu.Lock()
83 defer createWorktreeOperationMu.Unlock()
84
85 plan, err := t.planForExecute(ctx, raw)
86 if err != nil {
87 return core.ToolResult{}, err
88 }
89 return executeCreateWorktreePlan(ctx, plan, progress)
90}
91
92func executeCreateWorktreePlan(ctx context.Context, plan createWorktreePlan, progress func(string)) (core.ToolResult, error) {
93 createdWorktreeParents, err := missingWorktreeParents(plan.worktreePath)
94 if err != nil {
95 return core.ToolResult{}, fmt.Errorf("create_worktree: inspect worktree directories: %w", err)
96 }
97 rollback := func(cause error) error {
98 cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
99 defer cancel()
100 if err := plan.cleanupFailedCheckout(cleanupCtx); err != nil {
101 cause = fmt.Errorf("%w; cleanup worktree: %v", cause, err)
102 }
103 if err := removeCreatedWorktreeParents(createdWorktreeParents); err != nil {
104 cause = fmt.Errorf("%w; remove created worktree directories: %v", cause, err)
105 }
106 return cause
107 }
108
109 if err := os.MkdirAll(filepath.Dir(plan.worktreePath), 0o755); err != nil {
110 return core.ToolResult{}, rollback(fmt.Errorf("create_worktree: make worktree directory: %w", err))
111 }
112 if progress != nil {
113 progress("Creating Git worktree...\n")
114 }
115 if _, err := createWorktreeGitOutput(ctx, plan.repoRoot, "worktree", "add", "-b", plan.branch, plan.worktreePath, plan.base); err != nil {
116 return core.ToolResult{}, rollback(fmt.Errorf("create_worktree: create worktree: %w", err))
117 }
118
119 result := plan.preview
120 previewDetails, _ := plan.preview.Details.(map[string]any)
121 details := make(map[string]any, len(previewDetails)+1)
122 for key, value := range previewDetails {
123 details[key] = value
124 }
125 details["state"] = "created"
126 result.Details = details
127 result.Content = []provider.Content{provider.TextBlock{Text: strings.Replace(plan.previewText, "create worktree", "created worktree", 1)}}
128 return result, nil
129}
130
131type createWorktreePreview struct {
132 raw string
133 plan createWorktreePlan
134}
135
136func (t *CreateWorktreeTool) planForExecute(ctx context.Context, raw json.RawMessage) (createWorktreePlan, error) {
137 t.previewMu.Lock()
138 cached := t.preview
139 t.preview = nil
140 t.previewMu.Unlock()
141
142 plan, err := t.plan(ctx, raw)
143 if err != nil {
144 return createWorktreePlan{}, err
145 }
146 if cached != nil && cached.raw == string(raw) && !sameCreateWorktreePlan(cached.plan, plan) {
147 return createWorktreePlan{}, errors.New("create_worktree: repository state changed since preview; inspect and retry")
148 }
149 return plan, nil
150}
151
152type createWorktreePlan struct {
153 repoRoot string
154 branch string
155 base string
156 worktreeRoot string
157 worktreePath string
158 rootSource string
159 repoID string
160 previewText string
161 preview core.ToolResult
162}
163
164func (t *CreateWorktreeTool) plan(ctx context.Context, raw json.RawMessage) (createWorktreePlan, error) {
165 var args createWorktreeArgs
166 if err := json.Unmarshal(raw, &args); err != nil {
167 return createWorktreePlan{}, fmt.Errorf("invalid args: %w", err)
168 }
169 branchInput := strings.TrimSpace(args.Branch)
170 if branchInput == "" {
171 return createWorktreePlan{}, errors.New("branch is required")
172 }
173 if strings.HasPrefix(branchInput, "-") || strings.Contains(branchInput, "@{") {
174 return createWorktreePlan{}, fmt.Errorf("create_worktree: invalid branch %q", branchInput)
175 }
176 if t.Sandbox == nil {
177 return createWorktreePlan{}, errors.New("create_worktree: sandbox is required")
178 }
179 if err := t.Sandbox.CheckBashPermission("git"); err != nil {
180 return createWorktreePlan{}, fmt.Errorf("create_worktree: %w", err)
181 }
182
183 cwd, err := t.accessibleCWD()
184 if err != nil {
185 return createWorktreePlan{}, err
186 }
187 candidateRoot, err := t.findGitRootCandidate(cwd)
188 if err != nil {
189 return createWorktreePlan{}, err
190 }
191 repoRoot, err := createWorktreeGitOutput(ctx, candidateRoot, "rev-parse", "--show-toplevel")
192 if err != nil {
193 return createWorktreePlan{}, fmt.Errorf("create_worktree: verify repository: %w", err)
194 }
195 repoRoot, err = canonical(repoRoot)
196 if err != nil {
197 return createWorktreePlan{}, fmt.Errorf("create_worktree: resolve repository root: %w", err)
198 }
199 if err := t.checkReadAccess(repoRoot); err != nil {
200 return createWorktreePlan{}, err
201 }
202 branch, err := createWorktreeGitOutput(ctx, repoRoot, "check-ref-format", "--branch", branchInput)
203 if err != nil {
204 return createWorktreePlan{}, fmt.Errorf("create_worktree: invalid branch %q: %w", branchInput, err)
205 }
206 if branch != branchInput {
207 return createWorktreePlan{}, fmt.Errorf("create_worktree: invalid branch %q", branchInput)
208 }
209 base, err := createWorktreeGitOutput(ctx, repoRoot, "rev-parse", "--verify", "HEAD^{commit}")
210 if err != nil {
211 return createWorktreePlan{}, fmt.Errorf("create_worktree: resolve current commit: %w", err)
212 }
213 commonDir, err := createWorktreeGitOutput(ctx, repoRoot, "rev-parse", "--git-common-dir")
214 if err != nil {
215 return createWorktreePlan{}, fmt.Errorf("create_worktree: resolve Git metadata: %w", err)
216 }
217 if !filepath.IsAbs(commonDir) {
218 commonDir = filepath.Join(repoRoot, commonDir)
219 }
220 commonDir, err = canonical(commonDir)
221 if err != nil {
222 return createWorktreePlan{}, fmt.Errorf("create_worktree: resolve Git metadata: %w", err)
223 }
224 if err := t.checkReadAccess(commonDir); err != nil {
225 return createWorktreePlan{}, err
226 }
227
228 repoID := worktreeRepoID(commonDir)
229 configuredRoot, configured, err := createWorktreeGitConfigValue(ctx, repoRoot, worktreeConfigKey)
230 if err != nil {
231 return createWorktreePlan{}, fmt.Errorf("create_worktree: read local worktree configuration: %w", err)
232 }
233 var worktreeRoot string
234 rootSource := "global default root"
235 if configured {
236 worktreeRoot, err = resolveConfiguredWorktreeRoot(configuredRoot)
237 if err != nil {
238 return createWorktreePlan{}, err
239 }
240 rootSource = "local Git config"
241 } else {
242 defaultRoot, err := t.defaultRoot()
243 if err != nil {
244 return createWorktreePlan{}, err
245 }
246 worktreeRoot = filepath.Join(defaultRoot, repoID)
247 }
248
249 if err := validateWorktreeRoot(worktreeRoot); err != nil {
250 return createWorktreePlan{}, err
251 }
252 worktreePath := filepath.Join(worktreeRoot, filepath.FromSlash(branch))
253 rootCanonical, err := canonicalOrParent(worktreeRoot)
254 if err != nil {
255 return createWorktreePlan{}, fmt.Errorf("create_worktree: resolve worktree root: %w", err)
256 }
257 if isUnder(repoRoot, rootCanonical) {
258 return createWorktreePlan{}, errors.New("create_worktree: worktree root must be outside the repository")
259 }
260 pathCanonical, err := canonicalOrParent(worktreePath)
261 if err != nil {
262 return createWorktreePlan{}, fmt.Errorf("create_worktree: resolve worktree path: %w", err)
263 }
264 if !isUnder(rootCanonical, pathCanonical) {
265 return createWorktreePlan{}, errors.New("create_worktree: branch path escapes the worktree root")
266 }
267
268 if err := t.checkReadWriteAccess(repoRoot, commonDir, worktreeRoot, worktreePath); err != nil {
269 return createWorktreePlan{}, err
270 }
271 if _, err := os.Lstat(worktreePath); err == nil {
272 return createWorktreePlan{}, fmt.Errorf("create_worktree: worktree path %q already exists", filepath.ToSlash(worktreePath))
273 } else if !os.IsNotExist(err) {
274 return createWorktreePlan{}, fmt.Errorf("create_worktree: inspect worktree path: %w", err)
275 }
276 exists, err := createWorktreeBranchExists(ctx, repoRoot, branch)
277 if err != nil {
278 return createWorktreePlan{}, fmt.Errorf("create_worktree: check branch: %w", err)
279 }
280 if exists {
281 return createWorktreePlan{}, fmt.Errorf("create_worktree: branch %q already exists", branch)
282 }
283
284 displayRoot := filepath.ToSlash(worktreeRoot)
285 displayPath := filepath.ToSlash(worktreePath)
286 previewText := fmt.Sprintf("create worktree\nbranch: %s\nbase: %s\nworktree root: %s\nroot source: %s\npath: %s\n", branch, base, displayRoot, rootSource, displayPath)
287 return createWorktreePlan{
288 repoRoot: repoRoot,
289 branch: branch,
290 base: base,
291 worktreeRoot: worktreeRoot,
292 worktreePath: worktreePath,
293 rootSource: rootSource,
294 repoID: repoID,
295 previewText: previewText,
296 preview: core.ToolResult{
297 Content: []provider.Content{provider.TextBlock{Text: previewText}},
298 Details: map[string]any{
299 "state": "ready",
300 "branch": branch,
301 "base": base,
302 "worktree_root": displayRoot,
303 "root_source": rootSource,
304 "repo_id": repoID,
305 "path": displayPath,
306 },
307 },
308 }, nil
309}
310
311func (t *CreateWorktreeTool) defaultRoot() (string, error) {
312 root := strings.TrimSpace(t.DefaultRoot)
313 if root == "" {
314 return "", errors.New("create_worktree: default worktree root is not configured")
315 }
316 return canonicalOrParent(root)
317}
318
319// worktreeRepoID derives a stable, collision-resistant directory name for the
320// repository that owns commonDir. The name pairs a readable slug with a hash of
321// the canonical common directory so repositories sharing a base name still get
322// distinct roots while one repository keeps the same directory across sessions.
323func worktreeRepoID(commonDir string) string {
324 sum := sha256.Sum256([]byte(filepath.ToSlash(commonDir)))
325 return worktreeRepoSlug(commonDir) + "-" + hex.EncodeToString(sum[:])[:worktreeRepoIDHashLength]
326}
327
328func worktreeRepoSlug(commonDir string) string {
329 base := filepath.Base(commonDir)
330 if trimmed := strings.TrimSuffix(base, ".git"); trimmed != "" {
331 base = trimmed
332 } else {
333 base = filepath.Base(filepath.Dir(commonDir))
334 }
335 var builder strings.Builder
336 dashed := false
337 for _, r := range base {
338 switch {
339 case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '.', r == '_', r == '-':
340 builder.WriteRune(r)
341 dashed = false
342 case r >= 'A' && r <= 'Z':
343 builder.WriteRune(r + ('a' - 'A'))
344 dashed = false
345 default:
346 if !dashed {
347 builder.WriteByte('-')
348 dashed = true
349 }
350 }
351 }
352 slug := strings.Trim(builder.String(), "-._")
353 if len(slug) > worktreeRepoSlugMaxLen {
354 slug = strings.Trim(slug[:worktreeRepoSlugMaxLen], "-._")
355 }
356 if slug == "" {
357 slug = "repo"
358 }
359 return slug
360}
361
362func (t *CreateWorktreeTool) accessibleCWD() (string, error) {
363 cwd := t.CWD
364 if cwd == "" {
365 var err error
366 cwd, err = os.Getwd()
367 if err != nil {
368 return "", fmt.Errorf("create_worktree: working directory: %w", err)
369 }
370 }
371 cwd, err := canonical(cwd)
372 if err != nil {
373 return "", fmt.Errorf("create_worktree: resolve working directory: %w", err)
374 }
375 if err := t.checkReadAccess(cwd); err != nil {
376 return "", err
377 }
378 return cwd, nil
379}
380
381func (t *CreateWorktreeTool) findGitRootCandidate(cwd string) (string, error) {
382 for dir := cwd; ; dir = filepath.Dir(dir) {
383 if err := t.checkReadAccess(dir); err != nil {
384 return "", err
385 }
386 if _, err := os.Lstat(filepath.Join(dir, ".git")); err == nil {
387 return dir, nil
388 } else if !os.IsNotExist(err) {
389 return "", fmt.Errorf("create_worktree: inspect Git metadata: %w", err)
390 }
391 parent := filepath.Dir(dir)
392 if parent == dir {
393 break
394 }
395 }
396 return "", errors.New("create_worktree: working directory is not a Git repository")
397}
398
399func (t *CreateWorktreeTool) checkReadAccess(paths ...string) error {
400 for _, path := range paths {
401 if err := t.Sandbox.CheckReadPath(path); err != nil {
402 return fmt.Errorf("create_worktree: %w", err)
403 }
404 }
405 return nil
406}
407
408func (t *CreateWorktreeTool) checkReadWriteAccess(paths ...string) error {
409 for _, path := range paths {
410 if err := t.Sandbox.CheckReadPath(path); err != nil {
411 return fmt.Errorf("create_worktree: %w", err)
412 }
413 if err := t.Sandbox.CheckWritePath(path); err != nil {
414 return fmt.Errorf("create_worktree: %w", err)
415 }
416 }
417 return nil
418}
419
420func resolveConfiguredWorktreeRoot(value string) (string, error) {
421 value = strings.TrimSpace(value)
422 if value == "" {
423 return "", errors.New("create_worktree: local worktree configuration is empty")
424 }
425 if !filepath.IsAbs(value) {
426 return "", errors.New("create_worktree: local worktree configuration must be an absolute path; remove a legacy relative value with `git config --local --unset zut.worktrees.path`")
427 }
428 return filepath.Clean(value), nil
429}
430
431func validateWorktreeRoot(path string) error {
432 info, err := os.Lstat(path)
433 if os.IsNotExist(err) {
434 return nil
435 }
436 if err != nil {
437 return fmt.Errorf("create_worktree: inspect worktree root: %w", err)
438 }
439 if info.Mode()&os.ModeSymlink != 0 {
440 return errors.New("create_worktree: worktree root must not be a symbolic link")
441 }
442 if !info.IsDir() {
443 return errors.New("create_worktree: worktree root exists but is not a directory")
444 }
445 return nil
446}
447
448func sameCreateWorktreePlan(a, b createWorktreePlan) bool {
449 return a.repoRoot == b.repoRoot &&
450 a.branch == b.branch &&
451 a.base == b.base &&
452 a.worktreeRoot == b.worktreeRoot &&
453 a.worktreePath == b.worktreePath &&
454 a.rootSource == b.rootSource &&
455 a.repoID == b.repoID
456}
457
458func missingWorktreeParents(worktreePath string) ([]string, error) {
459 var missing []string
460 for directory := filepath.Dir(worktreePath); ; directory = filepath.Dir(directory) {
461 info, err := os.Lstat(directory)
462 if err == nil {
463 if !info.IsDir() {
464 return nil, fmt.Errorf("%q is not a directory", directory)
465 }
466 break
467 }
468 if !os.IsNotExist(err) {
469 return nil, err
470 }
471 missing = append(missing, directory)
472 parent := filepath.Dir(directory)
473 if parent == directory {
474 return nil, errors.New("no existing parent directory")
475 }
476 }
477 for left, right := 0, len(missing)-1; left < right; left, right = left+1, right-1 {
478 missing[left], missing[right] = missing[right], missing[left]
479 }
480 return missing, nil
481}
482
483func removeCreatedWorktreeParents(parents []string) error {
484 for index := len(parents) - 1; index >= 0; index-- {
485 if err := os.Remove(parents[index]); err != nil && !os.IsNotExist(err) {
486 return err
487 }
488 }
489 return nil
490}
491
492func (p createWorktreePlan) cleanupFailedCheckout(ctx context.Context) error {
493 registered, err := createWorktreeRegistered(ctx, p.repoRoot, p.worktreePath)
494 if err != nil {
495 return err
496 }
497 var cleanupErr error
498 if registered {
499 if _, err := createWorktreeGitOutput(ctx, p.repoRoot, "worktree", "remove", "--force", p.worktreePath); err != nil {
500 cleanupErr = fmt.Errorf("remove registered worktree: %w", err)
501 }
502 }
503 branchHead, exists, err := createWorktreeGitRef(ctx, p.repoRoot, "refs/heads/"+p.branch)
504 if err == nil && exists && branchHead == p.base {
505 if _, err := createWorktreeGitOutput(ctx, p.repoRoot, "branch", "-D", p.branch); err != nil && cleanupErr == nil {
506 cleanupErr = fmt.Errorf("remove branch: %w", err)
507 }
508 }
509 if err != nil && cleanupErr == nil {
510 cleanupErr = err
511 }
512 return cleanupErr
513}
514
515func createWorktreeRegistered(ctx context.Context, repoRoot, worktreePath string) (bool, error) {
516 output, err := createWorktreeGitOutput(ctx, repoRoot, "worktree", "list", "--porcelain")
517 if err != nil {
518 return false, err
519 }
520 want, err := canonicalOrParent(worktreePath)
521 if err != nil {
522 return false, err
523 }
524 for _, line := range strings.Split(output, "\n") {
525 if !strings.HasPrefix(line, "worktree ") {
526 continue
527 }
528 path, err := canonicalOrParent(strings.TrimPrefix(line, "worktree "))
529 if err == nil && path == want {
530 return true, nil
531 }
532 }
533 return false, nil
534}
535
536func createWorktreeBranchExists(ctx context.Context, repoRoot, branch string) (bool, error) {
537 _, err := createWorktreeGitOutput(ctx, repoRoot, "show-ref", "--verify", "--quiet", "refs/heads/"+branch)
538 if err == nil {
539 return true, nil
540 }
541 if ctx.Err() != nil {
542 return false, ctx.Err()
543 }
544 if createWorktreeGitExitCode(err) == 1 {
545 return false, nil
546 }
547 return false, err
548}
549
550func createWorktreeGitRef(ctx context.Context, repoRoot, ref string) (string, bool, error) {
551 output, err := createWorktreeGitOutput(ctx, repoRoot, "rev-parse", "--verify", "--quiet", ref+"^{commit}")
552 if err == nil {
553 return output, true, nil
554 }
555 if createWorktreeGitExitCode(err) == 1 {
556 return "", false, nil
557 }
558 return "", false, err
559}
560
561func createWorktreeGitConfigValue(ctx context.Context, repoRoot, key string) (string, bool, error) {
562 output, err := createWorktreeGitOutput(ctx, repoRoot, "config", "--local", "--get", key)
563 if err == nil {
564 return output, true, nil
565 }
566 if createWorktreeGitExitCode(err) == 1 {
567 return "", false, nil
568 }
569 return "", false, err
570}
571
572type createWorktreeGitError struct {
573 args []string
574 output string
575 err error
576}
577
578func (e *createWorktreeGitError) Error() string {
579 message := strings.TrimSpace(e.output)
580 if message == "" {
581 message = e.err.Error()
582 }
583 return fmt.Sprintf("git %s: %s", strings.Join(e.args, " "), message)
584}
585
586func (e *createWorktreeGitError) Unwrap() error { return e.err }
587
588func createWorktreeGitExitCode(err error) int {
589 var exitErr *exec.ExitError
590 if errors.As(err, &exitErr) {
591 return exitErr.ExitCode()
592 }
593 return -1
594}
595
596func createWorktreeGitOutput(ctx context.Context, dir string, args ...string) (string, error) {
597 cmdArgs := append([]string{"-c", "core.hooksPath=" + os.DevNull, "-C", dir}, args...)
598 cmd := exec.CommandContext(ctx, "git", cmdArgs...)
599 cmd.Env = createWorktreeGitEnv()
600 setProcessGroup(cmd)
601
602 var stdout, output cappedGitOutput
603 cmd.Stdout = io.MultiWriter(&stdout, &output)
604 cmd.Stderr = &output
605 if err := cmd.Start(); err != nil {
606 return "", fmt.Errorf("start git %s: %w", strings.Join(args, " "), err)
607 }
608 watchDone := make(chan struct{})
609 watchStopped := make(chan struct{})
610 go func() {
611 defer close(watchStopped)
612 select {
613 case <-ctx.Done():
614 killProcessGroup(cmd)
615 case <-watchDone:
616 }
617 }()
618 waitErr := cmd.Wait()
619 close(watchDone)
620 <-watchStopped
621 if waitErr != nil {
622 if ctx.Err() != nil {
623 return "", ctx.Err()
624 }
625 return "", &createWorktreeGitError{args: args, output: output.String(), err: waitErr}
626 }
627 return strings.TrimSpace(stdout.String()), nil
628}
629
630type cappedGitOutput struct {
631 mu sync.Mutex
632 buffer bytes.Buffer
633 truncated bool
634}
635
636func (b *cappedGitOutput) Write(p []byte) (int, error) {
637 b.mu.Lock()
638 defer b.mu.Unlock()
639 if room := maxCreateWorktreeGitOutput - b.buffer.Len(); room > 0 {
640 if len(p) > room {
641 _, _ = b.buffer.Write(p[:room])
642 b.truncated = true
643 } else {
644 _, _ = b.buffer.Write(p)
645 }
646 } else {
647 b.truncated = true
648 }
649 return len(p), nil
650}
651
652func (b *cappedGitOutput) String() string {
653 b.mu.Lock()
654 defer b.mu.Unlock()
655 output := b.buffer.String()
656 if b.truncated {
657 output += "\n... [git output truncated]"
658 }
659 return output
660}
661
662func createWorktreeGitEnv() []string {
663 env := make([]string, 0, len(os.Environ())+5)
664 for _, entry := range os.Environ() {
665 key, _, _ := strings.Cut(entry, "=")
666 if strings.HasPrefix(key, "GIT_") || key == "SSH_ASKPASS" {
667 continue
668 }
669 env = append(env, entry)
670 }
671 return append(env,
672 "GIT_CONFIG_NOSYSTEM=1",
673 "GIT_CONFIG_GLOBAL="+os.DevNull,
674 "GIT_TERMINAL_PROMPT=0",
675 "GIT_ASKPASS=",
676 "SSH_ASKPASS=",
677 )
678}