|
12 | 12 | // See the License for the specific language governing permissions and |
13 | 13 | // limitations under the License. |
14 | 14 |
|
15 | | -// Package gitexec locates a git binary and runs it with the ambient |
16 | | -// environment stripped out. |
| 15 | +// Package gitexec locates a git binary and composes the environment git runs |
| 16 | +// in. It is the single source of truth for that environment across every |
| 17 | +// SubmitQueue caller — demo tooling, the change provider's repository, and the |
| 18 | +// Runway merger. |
17 | 19 | // |
18 | | -// Demo and development tooling drives git on a developer's own machine, where |
19 | | -// hooks, a signing key, or a commit template configured globally would each |
20 | | -// break a run in a way that has nothing to do with SubmitQueue. Every command |
21 | | -// built here therefore carries the same scrubbed environment the git merger |
22 | | -// uses (see runway/extension/merger/git), so tooling behaves the same on every |
23 | | -// machine. |
| 20 | +// The environment has two halves. The scrub set denies git all ambient |
| 21 | +// configuration that could change what a command produces — a global hooks |
| 22 | +// path, a signing requirement, a commit template — which is what makes a |
| 23 | +// scripted run behave the same on every machine. The transport set carries |
| 24 | +// what a command needs to reach a remote — the SSH agent socket, git's ssh and |
| 25 | +// credential helpers on PATH, TLS roots, proxy settings — none of which can |
| 26 | +// change an answer. Every caller shares both halves; they differ only in the |
| 27 | +// literal entries they add (a pinned exec path, an isolated HOME), which is why |
| 28 | +// Env takes those as options rather than baking one caller's policy in. |
24 | 29 | // |
25 | | -// This resolves only the executable, because tooling runs porcelain |
26 | | -// (init, clone, commit, push) rather than constructing a merger's GitRuntime, |
27 | | -// which additionally pins the exec path and template directory. |
| 30 | +// HOME is deliberately not in the transport set: a caller that isolates HOME |
| 31 | +// and a caller that inherits it disagree, so each supplies it itself. |
28 | 32 | package gitexec |
29 | 33 |
|
30 | 34 | import ( |
@@ -67,23 +71,75 @@ func Resolve(path string) (string, error) { |
67 | 71 | return absolute, nil |
68 | 72 | } |
69 | 73 |
|
| 74 | +// scrubEnv denies git every ambient configuration input that could change what |
| 75 | +// a command produces. Always applied, first, so a later entry can override it. |
| 76 | +var scrubEnv = []string{ |
| 77 | + "GIT_CONFIG_NOSYSTEM=1", |
| 78 | + "GIT_CONFIG_GLOBAL=" + os.DevNull, |
| 79 | + "GIT_ATTR_NOSYSTEM=1", |
| 80 | + "GIT_TERMINAL_PROMPT=0", |
| 81 | + "GIT_PAGER=cat", |
| 82 | + "GIT_EDITOR=:", |
| 83 | +} |
| 84 | + |
| 85 | +// transportEnvNames are inherited from the parent process when set. None can |
| 86 | +// change what a command produces; each decides whether a remote is reachable. |
| 87 | +// HOME is intentionally absent — see the package doc. |
| 88 | +var transportEnvNames = []string{ |
| 89 | + "PATH", |
| 90 | + "SSH_AUTH_SOCK", "SSH_AGENT_PID", |
| 91 | + "GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT", |
| 92 | + "GIT_SSL_CAINFO", "GIT_SSL_CAPATH", |
| 93 | + "SSL_CERT_DIR", "SSL_CERT_FILE", |
| 94 | + "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", |
| 95 | + "http_proxy", "https_proxy", "no_proxy", |
| 96 | +} |
| 97 | + |
| 98 | +// EnvOptions selects what, on top of the always-applied scrub set, a git |
| 99 | +// command's environment carries. |
| 100 | +type EnvOptions struct { |
| 101 | + // Transport inherits the transport variables from the parent when set. |
| 102 | + Transport bool |
| 103 | + // Passthrough names further variables to inherit from the parent when set. |
| 104 | + Passthrough []string |
| 105 | + // Literal entries are appended last as "NAME=value", so they override any |
| 106 | + // inherited value of the same name. |
| 107 | + Literal []string |
| 108 | +} |
| 109 | + |
| 110 | +// Env composes a git command environment: the scrub set, then the requested |
| 111 | +// variables inherited from the parent (only those actually set, so an unset |
| 112 | +// SSH_AUTH_SOCK stays absent rather than becoming empty), then the literals. |
| 113 | +func Env(opts EnvOptions) []string { |
| 114 | + env := make([]string, 0, len(scrubEnv)+len(transportEnvNames)+len(opts.Passthrough)+len(opts.Literal)) |
| 115 | + env = append(env, scrubEnv...) |
| 116 | + |
| 117 | + names := make([]string, 0, len(transportEnvNames)+len(opts.Passthrough)) |
| 118 | + if opts.Transport { |
| 119 | + names = append(names, transportEnvNames...) |
| 120 | + } |
| 121 | + names = append(names, opts.Passthrough...) |
| 122 | + |
| 123 | + seen := make(map[string]bool, len(names)) |
| 124 | + for _, name := range names { |
| 125 | + if name == "" || seen[name] { |
| 126 | + continue |
| 127 | + } |
| 128 | + seen[name] = true |
| 129 | + if v, ok := os.LookupEnv(name); ok { |
| 130 | + env = append(env, name+"="+v) |
| 131 | + } |
| 132 | + } |
| 133 | + return append(env, opts.Literal...) |
| 134 | +} |
| 135 | + |
70 | 136 | // Command builds a git invocation in dir with the ambient environment removed. |
71 | | -// An empty dir runs in the current working directory. |
| 137 | +// An empty dir runs in the current working directory. PATH is passed so git can |
| 138 | +// find its helpers; nothing else the host sets reaches the command. |
72 | 139 | func Command(ctx context.Context, git, dir string, args ...string) *exec.Cmd { |
73 | 140 | cmd := exec.CommandContext(ctx, git, args...) |
74 | 141 | cmd.Dir = dir |
75 | | - // A developer's global config is the usual reason a scripted git run fails |
76 | | - // on one machine and not another: a hooks path, a signing requirement, or a |
77 | | - // commit template. None of it is relevant to seeding a sandbox. |
78 | | - cmd.Env = []string{ |
79 | | - "GIT_CONFIG_NOSYSTEM=1", |
80 | | - "GIT_CONFIG_GLOBAL=" + os.DevNull, |
81 | | - "GIT_ATTR_NOSYSTEM=1", |
82 | | - "GIT_TERMINAL_PROMPT=0", |
83 | | - "GIT_PAGER=cat", |
84 | | - "GIT_EDITOR=:", |
85 | | - "PATH=" + os.Getenv("PATH"), |
86 | | - } |
| 142 | + cmd.Env = Env(EnvOptions{Literal: []string{"PATH=" + os.Getenv("PATH")}}) |
87 | 143 | return cmd |
88 | 144 | } |
89 | 145 |
|
|
0 commit comments