返回 DeepSeek-Reasonix
gitcmd.go
根目录 / internal / gitcmd / gitcmd.go
1 // Package gitcmd builds the git invocations Reasonix runs on its own behalf:
2 // the status readout, workspace change probes, worktree management, and plugin
3 // source checkouts.
4 //
5 // Every one of those may point at a repository Reasonix did not create, and a
6 // repository's own .git/config is data authored by whoever produced the
7 // repository — not configuration the user chose. Several config keys name a
8 // command that git then executes during ordinary read-only work: an index
9 // refresh runs core.fsmonitor, a diff runs diff.external or a textconv driver,
10 // auto-maintenance spawns a background daemon. Command-line -c overrides win
11 // over repository config, and the corresponding --no-* flags win over both, so
12 // every invocation carries the same baseline.
13 //
14 // Centralizing that baseline is the point of this package. The same overrides
15 // used to be spelled out at each call site, which is exactly how three of the
16 // five sites ended up carrying them and two did not.
17 //
18 // Known residual: content filters (filter.<driver>.clean/smudge) and textconv
19 // drivers are selected per driver name through .gitattributes, so they cannot
20 // be disabled by a fixed key list the way the settings above can. Diff-side
21 // drivers are covered by --no-ext-diff/--no-textconv; checkout/status-side
22 // clean filters are not, and would need a different mechanism.
23 package gitcmd
24
25 import (
26 "context"
27 "os/exec"
28 "runtime"
29 "slices"
30
31 "reasonix/internal/proc"
32 "reasonix/internal/secrets"
33 )
34
35 // baseConfig is the -c override set every invocation carries.
36 var baseConfig = []string{
37 // An index refresh (status, diff, rev-parse --show-toplevel in a dirty
38 // tree) executes this as a command when the repository sets it.
39 "core.fsmonitor=false",
40 // Keeps a probe from starting git's background maintenance daemon.
41 "maintenance.auto=false",
42 }
43
44 // Args returns the full argument list for a git invocation: the hardening
45 // overrides, an optional -C directory, then the caller's arguments. extraConfig
46 // entries are "key=value" pairs appended after the baseline, so a call site can
47 // add its own preferences but cannot drop the baseline.
48 func Args(dir string, extraConfig []string, args ...string) []string {
49 return argsFor(runtime.GOOS, dir, extraConfig, args...)
50 }
51
52 func argsFor(goos, dir string, extraConfig []string, args ...string) []string {
53 // No capacity hint: these lists are a handful of entries, and computing one
54 // from the input lengths buys nothing measurable.
55 var out []string
56 for _, cfg := range baseConfig {
57 out = append(out, "-c", cfg)
58 }
59 if goos == "windows" {
60 out = append(out, "-c", "core.longpaths=true")
61 }
62 for _, cfg := range extraConfig {
63 if cfg == "" {
64 continue
65 }
66 out = append(out, "-c", cfg)
67 }
68 if dir != "" {
69 out = append(out, "-C", dir)
70 }
71 return append(out, hardenSubcommand(args)...)
72 }
73
74 // hardenSubcommand adds the flags that disable repository-configured programs
75 // for the subcommands that can invoke them. The flags go after the subcommand
76 // name, where git accepts them, and are only added when absent so an explicit
77 // caller flag is never duplicated.
78 func hardenSubcommand(args []string) []string {
79 if len(args) == 0 || args[0] != "diff" {
80 return args
81 }
82 out := []string{args[0]}
83 for _, flag := range []string{"--no-ext-diff", "--no-textconv"} {
84 if !slices.Contains(args, flag) {
85 out = append(out, flag)
86 }
87 }
88 return append(out, args[1:]...)
89 }
90
91 // Command builds a hardened git command rooted at dir (empty runs in the
92 // process working directory). The environment drops credential variables so a
93 // git subprocess — and anything git itself starts — never inherits provider
94 // keys, and disables interactive prompts so a probe cannot block on one.
95 func Command(ctx context.Context, dir string, args ...string) *exec.Cmd {
96 return CommandWithConfig(ctx, dir, nil, args...)
97 }
98
99 // CommandWithConfig is Command with additional "key=value" config overrides
100 // layered on top of the baseline.
101 func CommandWithConfig(ctx context.Context, dir string, extraConfig []string, args ...string) *exec.Cmd {
102 if ctx == nil {
103 ctx = context.Background()
104 }
105 cmd := exec.CommandContext(ctx, "git", Args(dir, extraConfig, args...)...)
106 cmd.Env = Env()
107 proc.HideWindow(cmd)
108 return cmd
109 }
110
111 // Env is the environment a git subprocess runs with. GIT_EXTERNAL_DIFF and
112 // GIT_SSH_COMMAND are deliberately left alone: an empty value is a present
113 // value to git, so clearing them would break legitimate ssh remotes rather than
114 // harden anything, and --no-ext-diff already outranks both the config key and
115 // the environment variable.
116 func Env() []string {
117 return append(secrets.ProcessEnv(),
118 // Read-only probes must not take the index lock.
119 "GIT_OPTIONAL_LOCKS=0",
120 // Fail fast instead of blocking on a credential prompt for a terminal
121 // the TUI owns and the desktop app does not have.
122 "GIT_TERMINAL_PROMPT=0",
123 )
124 }
125
125 lines GO