@@ -54,7 +54,9 @@ import (
5454 "bytes"
5555 "context"
5656 "fmt"
57+ "os"
5758 "os/exec"
59+ "path/filepath"
5860 "strings"
5961 "sync"
6062
@@ -74,6 +76,17 @@ import (
7476// pathologically busy remote.
7577const defaultMaxPushAttempts = 10
7678
79+ // GitRuntime identifies the explicitly provided Git runtime used by the Pusher.
80+ type GitRuntime struct {
81+ // Executable is the absolute path to the Git executable.
82+ Executable string
83+ // ExecPath is the absolute directory containing Git's helper executables.
84+ ExecPath string
85+ // TemplateDir is the absolute directory containing Git's repository
86+ // templates.
87+ TemplateDir string
88+ }
89+
7790// Params holds the dependencies for the git Pusher.
7891type Params struct {
7992 // CheckoutPath is the absolute path to an existing git checkout that the
@@ -90,6 +103,8 @@ type Params struct {
90103 Logger * zap.SugaredLogger
91104 // MetricsScope is the metrics scope for instrumentation.
92105 MetricsScope tally.Scope
106+ // Runtime is the pinned Git runtime used for every invocation.
107+ Runtime GitRuntime
93108 // MaxPushAttempts caps how many times Push retries the full
94109 // fetch/reset/cherry-pick/push cycle when the remote tip moves under
95110 // it. Defaults to defaultMaxPushAttempts when zero or negative.
@@ -105,6 +120,7 @@ type gitPusher struct {
105120 resolver changeset.Resolver
106121 logger * zap.SugaredLogger
107122 metricsScope tally.Scope
123+ runtime GitRuntime
108124 maxPushAttempts int
109125
110126 // mu serializes concurrent Push calls — the underlying checkout cannot
@@ -117,7 +133,11 @@ var _ pusher.Pusher = (*gitPusher)(nil)
117133
118134// NewPusher constructs a new git-backed Pusher operating against the given
119135// checkout. The checkout must already exist and have the configured remote.
120- func NewPusher (params Params ) pusher.Pusher {
136+ // Runtime paths must be absolute.
137+ func NewPusher (params Params ) (pusher.Pusher , error ) {
138+ if err := params .Runtime .validate (); err != nil {
139+ return nil , err
140+ }
121141 maxAttempts := params .MaxPushAttempts
122142 if maxAttempts <= 0 {
123143 maxAttempts = defaultMaxPushAttempts
@@ -129,8 +149,25 @@ func NewPusher(params Params) pusher.Pusher {
129149 resolver : params .Resolver ,
130150 logger : params .Logger .Named ("git_pusher" ),
131151 metricsScope : params .MetricsScope .SubScope ("git_pusher" ),
152+ runtime : params .Runtime ,
132153 maxPushAttempts : maxAttempts ,
154+ }, nil
155+ }
156+
157+ func (r GitRuntime ) validate () error {
158+ for name , path := range map [string ]string {
159+ "executable" : r .Executable ,
160+ "exec path" : r .ExecPath ,
161+ "template dir" : r .TemplateDir ,
162+ } {
163+ if path == "" {
164+ return fmt .Errorf ("git runtime %s is required" , name )
165+ }
166+ if ! filepath .IsAbs (path ) {
167+ return fmt .Errorf ("git runtime %s must be absolute: %q" , name , path )
168+ }
133169 }
170+ return nil
134171}
135172
136173// Push fulfils the pusher.Pusher contract.
@@ -431,8 +468,7 @@ func (p *gitPusher) push(ctx context.Context) error {
431468// run executes a `git` command in the checkout. Returns captured stdout and
432469// an error that includes captured stderr for diagnostics.
433470func (p * gitPusher ) run (ctx context.Context , stdin []byte , args ... string ) ([]byte , error ) {
434- cmd := exec .CommandContext (ctx , "git" , args ... )
435- cmd .Dir = p .checkoutPath
471+ cmd := newGitCommand (ctx , p .runtime , p .checkoutPath , args ... )
436472 if stdin != nil {
437473 cmd .Stdin = bytes .NewReader (stdin )
438474 }
@@ -449,14 +485,43 @@ func (p *gitPusher) run(ctx context.Context, stdin []byte, args ...string) ([]by
449485// and failure. Used when the caller needs to inspect git's diagnostic
450486// output (e.g., to detect "previous cherry-pick is now empty").
451487func (p * gitPusher ) runCombined (ctx context.Context , stdin []byte , args ... string ) ([]byte , error ) {
452- cmd := exec .CommandContext (ctx , "git" , args ... )
453- cmd .Dir = p .checkoutPath
488+ cmd := newGitCommand (ctx , p .runtime , p .checkoutPath , args ... )
454489 if stdin != nil {
455490 cmd .Stdin = bytes .NewReader (stdin )
456491 }
457492 return cmd .CombinedOutput ()
458493}
459494
495+ // newGitCommand constructs a Git command without inheriting the caller's
496+ // environment. The executable and helper paths come from the pinned runtime;
497+ // repository-local configuration remains an intentional input.
498+ func newGitCommand (ctx context.Context , runtime GitRuntime , dir string , args ... string ) * exec.Cmd {
499+ gitArgs := make ([]string , 0 , len (args )+ 3 )
500+ gitArgs = append (gitArgs ,
501+ "--exec-path=" + runtime .ExecPath ,
502+ "-c" , "init.templateDir=" + runtime .TemplateDir ,
503+ )
504+ gitArgs = append (gitArgs , args ... )
505+
506+ cmd := exec .CommandContext (ctx , runtime .Executable , gitArgs ... )
507+ cmd .Dir = dir
508+ cmd .Env = []string {
509+ "HOME=" + filepath .Join (dir , ".submitqueue-git-home" ),
510+ "XDG_CONFIG_HOME=" + filepath .Join (dir , ".submitqueue-git-home" , "xdg" ),
511+ "GIT_CONFIG_NOSYSTEM=1" ,
512+ "GIT_CONFIG_GLOBAL=" + os .DevNull ,
513+ "GIT_ATTR_NOSYSTEM=1" ,
514+ "GIT_TERMINAL_PROMPT=0" ,
515+ "GIT_PAGER=cat" ,
516+ "GIT_EDITOR=:" ,
517+ "GIT_EXEC_PATH=" + runtime .ExecPath ,
518+ "GIT_TEMPLATE_DIR=" + runtime .TemplateDir ,
519+ "LC_ALL=C" ,
520+ "LANG=C" ,
521+ }
522+ return cmd
523+ }
524+
460525// isRedundantCherryPick reports whether git's cherry-pick output indicates
461526// the pick was rejected because the change is already present on target
462527// (i.e. applying it would produce no diff).
0 commit comments