Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Listed first so its protoc toolchain wins resolution over the protobuf module's,
# which is pulled in transitively by rules_go/rules_proto.
bazel_dep(name = "toolchains_protoc", version = "0.6.1")
bazel_dep(name = "git", version = "2.55.0")
bazel_dep(name = "rules_go", version = "0.57.0")
bazel_dep(name = "gazelle", version = "0.45.0")
bazel_dep(name = "rules_proto", version = "7.1.0")

single_version_override(
module_name = "git",
version = "2.55.0",
)

# Direct dep so the well-known type proto_library targets (e.g.
# @protobuf//:descriptor_proto, needed by the message queue topics option that
# extends google.protobuf.MessageOptions) are visible by apparent name.
Expand Down
14 changes: 14 additions & 0 deletions submitqueue/extension/pusher/git/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,21 @@ go_library(
go_test(
name = "go_default_test",
srcs = ["git_pusher_test.go"],
data = [
"@git",
"@git//:git-remote-http",
"@git//:git_receive_pack",
"@git//:git_remote_https",
"@git//:git_upload_archive",
"@git//:git_upload_pack",
"@git//:templates",
"@git//:templates/description",
],
embed = [":go_default_library"],
env = {
"SUBMITQUEUE_TEST_GIT": "$(location @git//:git)",
"SUBMITQUEUE_TEST_GIT_TEMPLATE_DESCRIPTION": "$(location @git//:templates/description)",
},
deps = [
"//platform/base/change:go_default_library",
"//submitqueue/core/changeset/fake:go_default_library",
Expand Down
75 changes: 70 additions & 5 deletions submitqueue/extension/pusher/git/git_pusher.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"

Expand All @@ -74,6 +76,17 @@ import (
// pathologically busy remote.
const defaultMaxPushAttempts = 10

// GitRuntime identifies the explicitly provided Git runtime used by the Pusher.
type GitRuntime struct {
// Executable is the absolute path to the Git executable.
Executable string
// ExecPath is the absolute directory containing Git's helper executables.
ExecPath string
// TemplateDir is the absolute directory containing Git's repository
// templates.
TemplateDir string
}

// Params holds the dependencies for the git Pusher.
type Params struct {
// CheckoutPath is the absolute path to an existing git checkout that the
Expand All @@ -90,6 +103,8 @@ type Params struct {
Logger *zap.SugaredLogger
// MetricsScope is the metrics scope for instrumentation.
MetricsScope tally.Scope
// Runtime is the pinned Git runtime used for every invocation.
Runtime GitRuntime
// MaxPushAttempts caps how many times Push retries the full
// fetch/reset/cherry-pick/push cycle when the remote tip moves under
// it. Defaults to defaultMaxPushAttempts when zero or negative.
Expand All @@ -105,6 +120,7 @@ type gitPusher struct {
resolver changeset.Resolver
logger *zap.SugaredLogger
metricsScope tally.Scope
runtime GitRuntime
maxPushAttempts int

// mu serializes concurrent Push calls — the underlying checkout cannot
Expand All @@ -117,7 +133,11 @@ var _ pusher.Pusher = (*gitPusher)(nil)

// NewPusher constructs a new git-backed Pusher operating against the given
// checkout. The checkout must already exist and have the configured remote.
func NewPusher(params Params) pusher.Pusher {
// Runtime paths must be absolute.
func NewPusher(params Params) (pusher.Pusher, error) {
if err := params.Runtime.validate(); err != nil {
return nil, err
}
maxAttempts := params.MaxPushAttempts
if maxAttempts <= 0 {
maxAttempts = defaultMaxPushAttempts
Expand All @@ -129,8 +149,25 @@ func NewPusher(params Params) pusher.Pusher {
resolver: params.Resolver,
logger: params.Logger.Named("git_pusher"),
metricsScope: params.MetricsScope.SubScope("git_pusher"),
runtime: params.Runtime,
maxPushAttempts: maxAttempts,
}, nil
}

func (r GitRuntime) validate() error {
for name, path := range map[string]string{
"executable": r.Executable,
"exec path": r.ExecPath,
"template dir": r.TemplateDir,
} {
if path == "" {
return fmt.Errorf("git runtime %s is required", name)
}
if !filepath.IsAbs(path) {
return fmt.Errorf("git runtime %s must be absolute: %q", name, path)
}
}
return nil
}

// Push fulfils the pusher.Pusher contract.
Expand Down Expand Up @@ -431,8 +468,7 @@ func (p *gitPusher) push(ctx context.Context) error {
// run executes a `git` command in the checkout. Returns captured stdout and
// an error that includes captured stderr for diagnostics.
func (p *gitPusher) run(ctx context.Context, stdin []byte, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = p.checkoutPath
cmd := newGitCommand(ctx, p.runtime, p.checkoutPath, args...)
if stdin != nil {
cmd.Stdin = bytes.NewReader(stdin)
}
Expand All @@ -449,14 +485,43 @@ func (p *gitPusher) run(ctx context.Context, stdin []byte, args ...string) ([]by
// and failure. Used when the caller needs to inspect git's diagnostic
// output (e.g., to detect "previous cherry-pick is now empty").
func (p *gitPusher) runCombined(ctx context.Context, stdin []byte, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = p.checkoutPath
cmd := newGitCommand(ctx, p.runtime, p.checkoutPath, args...)
if stdin != nil {
cmd.Stdin = bytes.NewReader(stdin)
}
return cmd.CombinedOutput()
}

// newGitCommand constructs a Git command without inheriting the caller's
// environment. The executable and helper paths come from the pinned runtime;
// repository-local configuration remains an intentional input.
func newGitCommand(ctx context.Context, runtime GitRuntime, dir string, args ...string) *exec.Cmd {
gitArgs := make([]string, 0, len(args)+3)
gitArgs = append(gitArgs,
"--exec-path="+runtime.ExecPath,
"-c", "init.templateDir="+runtime.TemplateDir,
)
gitArgs = append(gitArgs, args...)

cmd := exec.CommandContext(ctx, runtime.Executable, gitArgs...)
cmd.Dir = dir
cmd.Env = []string{
"HOME=" + filepath.Join(dir, ".submitqueue-git-home"),
"XDG_CONFIG_HOME=" + filepath.Join(dir, ".submitqueue-git-home", "xdg"),
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=" + os.DevNull,
"GIT_ATTR_NOSYSTEM=1",
"GIT_TERMINAL_PROMPT=0",
"GIT_PAGER=cat",
"GIT_EDITOR=:",
"GIT_EXEC_PATH=" + runtime.ExecPath,
"GIT_TEMPLATE_DIR=" + runtime.TemplateDir,
"LC_ALL=C",
"LANG=C",
}
return cmd
}

// isRedundantCherryPick reports whether git's cherry-pick output indicates
// the pick was rejected because the change is already present on target
// (i.e. applying it would produce no diff).
Expand Down
Loading
Loading