Skip to content

Commit ab3ab60

Browse files
committed
feat: pin git runtime for git pusher
1 parent 1ee1f1f commit ab3ab60

4 files changed

Lines changed: 226 additions & 18 deletions

File tree

MODULE.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# Listed first so its protoc toolchain wins resolution over the protobuf module's,
22
# which is pulled in transitively by rules_go/rules_proto.
33
bazel_dep(name = "toolchains_protoc", version = "0.6.1")
4+
bazel_dep(name = "git", version = "2.55.0")
45
bazel_dep(name = "rules_go", version = "0.57.0")
56
bazel_dep(name = "gazelle", version = "0.45.0")
67
bazel_dep(name = "rules_proto", version = "7.1.0")
78

9+
single_version_override(
10+
module_name = "git",
11+
version = "2.55.0",
12+
)
13+
814
# Direct dep so the well-known type proto_library targets (e.g.
915
# @protobuf//:descriptor_proto, needed by the message queue topics option that
1016
# extends google.protobuf.MessageOptions) are visible by apparent name.

submitqueue/extension/pusher/git/BUILD.bazel

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,21 @@ go_library(
2020
go_test(
2121
name = "go_default_test",
2222
srcs = ["git_pusher_test.go"],
23+
data = [
24+
"@git",
25+
"@git//:git-remote-http",
26+
"@git//:git_receive_pack",
27+
"@git//:git_remote_https",
28+
"@git//:git_upload_archive",
29+
"@git//:git_upload_pack",
30+
"@git//:templates",
31+
"@git//:templates/description",
32+
],
2333
embed = [":go_default_library"],
34+
env = {
35+
"SUBMITQUEUE_TEST_GIT": "$(location @git//:git)",
36+
"SUBMITQUEUE_TEST_GIT_TEMPLATE_DESCRIPTION": "$(location @git//:templates/description)",
37+
},
2438
deps = [
2539
"//platform/base/change:go_default_library",
2640
"//submitqueue/core/changeset/fake:go_default_library",

submitqueue/extension/pusher/git/git_pusher.go

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
7577
const 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.
7891
type 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.
433470
func (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").
451487
func (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

Comments
 (0)